import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { vi } from 'vitest'; import { HttpResponse } from 'msw'; import { withTestQueryProvider } from '@/react/test-utils/withTestQuery'; import { withTestRouter } from '@/react/test-utils/withRouter'; import { withUserProvider } from '@/react/test-utils/withUserProvider'; import { http, server } from '@/setup-tests/server'; import { createMockEnvironment } from '@/react-tools/test-mocks'; import { usePersistentVolumeClaims } from '@/react/kubernetes/volumes/queries/usePersistentVolumeClaims'; import { useNamespacesQuery } from '@/react/kubernetes/namespaces/queries/useNamespacesQuery'; import type { PortainerNamespace } from '@/react/kubernetes/namespaces/types'; import { PersistentVolumeClaimsDatatable } from './PersistentVolumeClaimsDatatable'; import type { PersistentVolumeClaim } from './types'; const mockUseEnvironmentId = vi.fn(); vi.mock('@/react/hooks/useEnvironmentId', () => ({ useEnvironmentId: () => mockUseEnvironmentId(), })); vi.mock('@/react/kubernetes/volumes/queries/usePersistentVolumeClaims', () => ({ usePersistentVolumeClaims: vi.fn(), })); vi.mock( '@/react/kubernetes/volumes/queries/useDeletePersistentVolumeClaims', () => ({ useDeletePersistentVolumeClaims: vi.fn(() => ({ mutate: vi.fn(), isLoading: false, })), }) ); vi.mock('@/react/kubernetes/namespaces/queries/useNamespacesQuery', () => ({ useNamespacesQuery: vi.fn(), })); vi.mock('@/react/kubernetes/volumes/ListView/ResizeClaimEditForm', () => ({ ResizeClaimEditForm: ({ claim }: { claim: PersistentVolumeClaim }) => (
Resize form for {claim.name}
), })); vi.mock('@@/Link', () => ({ Link: ({ children }: { children: React.ReactNode }) => ( {children} ), })); vi.mock('@/react/kubernetes/components/CreateFromManifestButton', () => ({ CreateFromManifestButton: ({ 'data-cy': dataCy }: { 'data-cy'?: string }) => ( ), })); const mockPVCs: PersistentVolumeClaim[] = [ { id: 'pvc-1', name: 'test-pvc-1', namespace: 'default', storage: 10737418240, storageRequest: '10Gi', creationDate: '2024-01-01T00:00:00Z', accessModes: ['ReadWriteOnce'], humanReadableAccessModes: ['ReadWriteOnce'], volumeName: 'pv-1', storageClass: 'standard', allowVolumeExpansion: true, phase: 'Bound', }, { id: 'pvc-2', name: 'test-pvc-2', namespace: 'kube-system', storage: 5368709120, storageRequest: '5Gi', creationDate: '2024-01-02T00:00:00Z', accessModes: ['ReadOnlyMany'], humanReadableAccessModes: ['ReadOnlyMany'], volumeName: 'pv-2', storageClass: 'standard', allowVolumeExpansion: false, phase: 'Pending', }, ]; const pvcWithWorkload: PersistentVolumeClaim = { id: 'pvc-3', name: 'used-pvc', namespace: 'default', storage: 1073741824, storageRequest: '1Gi', creationDate: '2024-01-03T00:00:00Z', accessModes: ['ReadWriteOnce'], humanReadableAccessModes: ['ReadWriteOnce'], volumeName: 'pv-3', storageClass: 'standard', allowVolumeExpansion: false, phase: 'Bound', owningApplications: [ { Name: 'my-deployment', ResourcePool: 'default', ApplicationType: 'Deployment', }, ], }; const mockNamespaces = [ { Name: 'default', IsSystem: false }, { Name: 'kube-system', IsSystem: true }, ] as PortainerNamespace[]; function renderComponent() { server.use( http.get('/api/endpoints/:endpointId', () => HttpResponse.json(createMockEnvironment()) ) ); const Wrapped = withTestQueryProvider( withUserProvider(withTestRouter(PersistentVolumeClaimsDatatable)) ); return render(); } describe('PersistentVolumeClaimsDatatable', () => { beforeEach(() => { localStorage.clear(); mockUseEnvironmentId.mockReturnValue(3); vi.mocked(usePersistentVolumeClaims).mockImplementation( (_envId, options) => ({ data: options?.select ? options.select(mockPVCs) : mockPVCs, isLoading: false, }) as ReturnType ); vi.mocked(useNamespacesQuery).mockReturnValue({ data: [], isLoading: false, } as ReturnType); }); it('renders the datatable with PVC names and title', async () => { renderComponent(); expect(await screen.findByText('Volume claims')).toBeVisible(); expect(await screen.findByText('test-pvc-1')).toBeVisible(); expect(screen.getByText('test-pvc-2')).toBeVisible(); }); it('shows an empty table when there are no PVCs', async () => { vi.mocked(usePersistentVolumeClaims).mockImplementation( (_envId, options) => ({ data: options?.select ? options.select([] as PersistentVolumeClaim[]) : [], isLoading: false, }) as ReturnType ); renderComponent(); expect(await screen.findByText('Volume claims')).toBeVisible(); expect(screen.queryByText('test-pvc-1')).not.toBeInTheDocument(); }); it('opens the resize modal when edit is clicked for an expandable PVC', async () => { const { container } = renderComponent(); await screen.findByText('test-pvc-1'); const editButton = container.querySelector( '[data-cy="kubernetes-pv-resize-edit-test-pvc-1"]' ) as HTMLButtonElement; expect(editButton).not.toBeNull(); expect(editButton).not.toBeDisabled(); await userEvent.click(editButton); expect(await screen.findByTestId('resize-form')).toBeVisible(); expect(screen.getByText('Resize form for test-pvc-1')).toBeVisible(); }); it('disables the resize button for PVCs where volume expansion is not allowed', async () => { const { container } = renderComponent(); await screen.findByText('test-pvc-2'); const editButton = container.querySelector( '[data-cy="kubernetes-pv-resize-edit-test-pvc-2"]' ) as HTMLButtonElement; expect(editButton).toBeDisabled(); }); it('does not show the resize modal initially', async () => { renderComponent(); await screen.findByText('test-pvc-1'); expect(screen.queryByTestId('resize-form')).not.toBeInTheDocument(); }); describe('Used by column', () => { it('shows the owning application name for PVCs with a workload', async () => { vi.mocked(usePersistentVolumeClaims).mockImplementation( (_envId, options) => { const data = [pvcWithWorkload]; return { data: options?.select ? options.select(data) : data, isLoading: false, } as ReturnType; } ); renderComponent(); expect(await screen.findByText('my-deployment')).toBeVisible(); }); it('shows a dash for PVCs with no owning applications', async () => { vi.mocked(usePersistentVolumeClaims).mockImplementation( (_envId, options) => { const data = [mockPVCs[0]]; return { data: options?.select ? options.select(data) : data, isLoading: false, } as ReturnType; } ); renderComponent(); await screen.findByText('test-pvc-1'); expect(screen.getByText('-')).toBeVisible(); }); }); describe('row selectability', () => { it('disables the row checkbox for PVCs with owning applications', async () => { vi.mocked(usePersistentVolumeClaims).mockImplementation( (_envId, options) => { const data = [mockPVCs[0], pvcWithWorkload]; return { data: options?.select ? options.select(data) : data, isLoading: false, } as ReturnType; } ); const { container } = renderComponent(); await screen.findByText('used-pvc'); // skip header checkbox (index 0); row checkboxes follow in render order const rowCheckboxes = Array.from( container.querySelectorAll('input[type="checkbox"]') ).slice(1); const [freeCheckbox, usedCheckbox] = rowCheckboxes; expect(freeCheckbox).not.toBeDisabled(); expect(usedCheckbox).toBeDisabled(); }); it('allows selection of a Bound PVC that has no owning applications', async () => { vi.mocked(usePersistentVolumeClaims).mockImplementation( (_envId, options) => { const data = [mockPVCs[0]]; // Bound, no owningApplications return { data: options?.select ? options.select(data) : data, isLoading: false, } as ReturnType; } ); const { container } = renderComponent(); await screen.findByText('test-pvc-1'); const rowCheckboxes = Array.from( container.querySelectorAll('input[type="checkbox"]') ).slice(1); expect(rowCheckboxes[0]).not.toBeDisabled(); }); }); it('hides system namespace PVCs by default', async () => { vi.mocked(useNamespacesQuery).mockReturnValue({ data: mockNamespaces, isLoading: false, } as ReturnType); renderComponent(); expect(await screen.findByText('test-pvc-1')).toBeVisible(); expect(screen.queryByText('test-pvc-2')).not.toBeInTheDocument(); }); it('shows system namespace PVCs when showSystemResources is enabled', async () => { localStorage.setItem( 'portainer.datatable_settings_kube-volumes-pvc', JSON.stringify({ state: { showSystemResources: true }, version: 1 }) ); vi.mocked(useNamespacesQuery).mockReturnValue({ data: mockNamespaces, isLoading: false, } as ReturnType); renderComponent(); expect(await screen.findByText('test-pvc-1')).toBeVisible(); expect(screen.getByText('test-pvc-2')).toBeVisible(); }); });