import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { vi } from 'vitest'; import _ from 'lodash'; import { http, HttpResponse } from 'msw'; import { withUserProvider } from '@/react/test-utils/withUserProvider'; import { withTestRouter } from '@/react/test-utils/withRouter'; import { withTestQueryProvider } from '@/react/test-utils/withTestQuery'; import { ContainerDetailsViewModel } from '@/docker/models/containerDetails'; import { ContainerEngine } from '@/react/portainer/environments/types'; import { server } from '@/setup-tests/server'; import { createMockEnvironment, createMockUser, } from '@/react-tools/test-mocks'; import { ContainerActionsSection } from './ContainerActionsSection'; const mockStartMutate = vi.fn(); const mockStopMutate = vi.fn(); const mockKillMutate = vi.fn(); const mockRestartMutate = vi.fn(); const mockPauseMutate = vi.fn(); const mockUnpauseMutate = vi.fn(); const mockRemoveMutate = vi.fn(); vi.mock('@uirouter/react', async (importOriginal: () => Promise) => ({ ...(await importOriginal()), useCurrentStateAndParams: vi.fn(() => ({ params: { endpointId: 1 }, // Must match environmentId in test props and user authorizations })), })); vi.mock('./queries/useStartContainer', () => ({ useStartContainer: () => ({ mutate: mockStartMutate, isLoading: false }), })); vi.mock('./queries/useStopContainer', () => ({ useStopContainer: () => ({ mutate: mockStopMutate, isLoading: false }), })); vi.mock('./queries/useKillContainer', () => ({ useKillContainer: () => ({ mutate: mockKillMutate, isLoading: false }), })); vi.mock('./queries/useRestartContainer', () => ({ useRestartContainer: () => ({ mutate: mockRestartMutate, isLoading: false }), })); vi.mock('./queries/usePauseContainer', () => ({ usePauseContainer: () => ({ mutate: mockPauseMutate, isLoading: false }), })); vi.mock('./queries/useResumeContainer', () => ({ useResumeContainer: () => ({ mutate: mockUnpauseMutate, isLoading: false }), })); vi.mock('./queries/useRemoveContainer', () => ({ useRemoveContainer: () => ({ mutate: mockRemoveMutate, isLoading: false }), })); vi.mock('@/react/docker/proxy/queries/useInfo', () => ({ useIsSwarm: () => false, })); const mockConfirmContainerDeletion = vi.fn(); vi.mock( '@/react/docker/containers/common/confirm-container-delete-modal', () => ({ confirmContainerDeletion: (title: string) => mockConfirmContainerDeletion(title), }) ); describe('ContainerActionsSection', () => { beforeEach(() => { vi.clearAllMocks(); // Setup MSW handler for environment endpoint server.use( http.get('/api/endpoints/:id', () => HttpResponse.json( createMockEnvironment({ Id: 1, ContainerEngine: ContainerEngine.Docker, SecuritySettings: { allowContainerCapabilitiesForRegularUsers: true, allowBindMountsForRegularUsers: true, allowDeviceMappingForRegularUsers: true, allowSysctlSettingForRegularUsers: true, allowHostNamespaceForRegularUsers: true, allowPrivilegedModeForRegularUsers: true, allowVolumeBrowserForRegularUsers: true, allowStackManagementForRegularUsers: true, enableHostManagementFeatures: false, }, }) ) ) ); }); it('should render all action buttons when authorized', async () => { renderComponent(); // Wait for environment data to load await waitFor(() => { expect(screen.getByRole('button', { name: 'Start' })).toBeVisible(); }); expect(screen.getByRole('button', { name: 'Stop' })).toBeVisible(); expect(screen.getByRole('button', { name: 'Kill' })).toBeVisible(); expect(screen.getByRole('button', { name: 'Restart' })).toBeVisible(); expect(screen.getByRole('button', { name: 'Pause' })).toBeVisible(); expect(screen.getByRole('button', { name: 'Resume' })).toBeVisible(); expect(screen.getByRole('button', { name: 'Remove' })).toBeVisible(); }); it('should disable start button when container is running', async () => { renderComponent({ container: mockContainer({ Id: 'test-container-id', State: { Running: true, Paused: false }, Config: { Image: 'nginx:latest' }, }), }); await waitFor(() => { expect(screen.getByRole('button', { name: 'Start' })).toBeDisabled(); }); }); it('should disable stop, kill, restart buttons when container is not running', async () => { renderComponent({ container: mockContainer({ Id: 'test-container-id', State: { Running: false, Paused: false }, Config: { Image: 'nginx:latest' }, }), }); await waitFor(() => { expect(screen.getByRole('button', { name: 'Stop' })).toBeDisabled(); }); expect(screen.getByRole('button', { name: 'Kill' })).toBeDisabled(); expect(screen.getByRole('button', { name: 'Restart' })).toBeDisabled(); }); it('should disable pause button when container is paused', async () => { renderComponent({ container: mockContainer({ Id: 'test-container-id', State: { Running: true, Paused: true }, Config: { Image: 'nginx:latest' }, }), }); await waitFor(() => { expect(screen.getByRole('button', { name: 'Pause' })).toBeDisabled(); }); }); it('should disable unpause button when container is not paused', async () => { renderComponent({ container: mockContainer({ Id: 'test-container-id', State: { Running: true, Paused: false }, Config: { Image: 'nginx:latest' }, }), }); await waitFor(() => { expect(screen.getByRole('button', { name: 'Resume' })).toBeDisabled(); }); }); it('should disable all buttons when container is Portainer', async () => { renderComponent({ container: mockContainer({ Id: 'test-container-id', State: { Running: true, Paused: false }, Config: { Image: 'portainer:latest' }, IsPortainer: true, }), }); await waitFor(() => { expect(screen.getByRole('button', { name: 'Start' })).toBeDisabled(); }); expect(screen.getByRole('button', { name: 'Stop' })).toBeDisabled(); expect(screen.getByRole('button', { name: 'Kill' })).toBeDisabled(); expect(screen.getByRole('button', { name: 'Restart' })).toBeDisabled(); expect(screen.getByRole('button', { name: 'Pause' })).toBeDisabled(); expect(screen.getByRole('button', { name: 'Resume' })).toBeDisabled(); expect(screen.getByRole('button', { name: 'Remove' })).toBeDisabled(); }); it('should call start mutation when start button is clicked', async () => { const user = userEvent.setup(); renderComponent(); const startButton = await screen.findByRole('button', { name: 'Start' }); await user.click(startButton); expect(mockStartMutate).toHaveBeenCalledWith( { environmentId: 1, containerId: 'test-container-id', nodeName: 'node1', }, expect.objectContaining({ onSuccess: expect.any(Function), }) ); }); it('should call stop mutation when stop button is clicked', async () => { const user = userEvent.setup(); renderComponent({ container: mockContainer({ Id: 'test-container-id', State: { Running: true, Paused: false }, Config: { Image: 'nginx:latest' }, }), }); const stopButton = await screen.findByRole('button', { name: 'Stop' }); await user.click(stopButton); expect(mockStopMutate).toHaveBeenCalledWith( { environmentId: 1, containerId: 'test-container-id', nodeName: 'node1', }, expect.objectContaining({ onSuccess: expect.any(Function), }) ); }); it('should show confirmation modal and call remove mutation when confirmed', async () => { const user = userEvent.setup(); mockConfirmContainerDeletion.mockResolvedValue({ removeVolumes: true }); renderComponent(); const removeButton = await screen.findByRole('button', { name: 'Remove' }); await user.click(removeButton); await waitFor(() => { expect(mockConfirmContainerDeletion).toHaveBeenCalledWith( 'You are about to remove a container.' ); }); await waitFor(() => { expect(mockRemoveMutate).toHaveBeenCalledWith( { environmentId: 1, containerId: 'test-container-id', nodeName: 'node1', removeVolumes: true, }, expect.objectContaining({ onSuccess: expect.any(Function), }) ); }); }); it('should not call remove mutation when confirmation is cancelled', async () => { const user = userEvent.setup(); mockConfirmContainerDeletion.mockResolvedValue(undefined); renderComponent(); const removeButton = await screen.findByRole('button', { name: 'Remove' }); await user.click(removeButton); await waitFor(() => { expect(mockConfirmContainerDeletion).toHaveBeenCalled(); }); expect(mockRemoveMutate).not.toHaveBeenCalled(); }); }); function renderComponent( props: Partial> & { userAuthorizations?: Record; } = {} ) { const { userAuthorizations, ...componentProps } = props; const defaultProps: React.ComponentProps = { environmentId: 1, nodeName: 'node1', container: mockContainer(), ...componentProps, }; const defaultAuthorizations = { DockerContainerStart: true, DockerContainerStop: true, DockerContainerKill: true, DockerContainerRestart: true, DockerContainerPause: true, DockerContainerUnpause: true, DockerContainerDelete: true, DockerContainerCreate: true, }; const mockUser = createMockUser({ EndpointAuthorizations: { 1: userAuthorizations || defaultAuthorizations, }, Id: 1, Role: 1, }); const Wrapper = withTestQueryProvider( withUserProvider(withTestRouter(ContainerActionsSection), mockUser) ); return render(); } function mockContainer( overrides: Partial = {} ): ContainerDetailsViewModel { return _.merge( { Id: 'test-container-id', State: { Running: false, Paused: false, }, Config: { Image: 'nginx:latest', }, IsPortainer: false, } as ContainerDetailsViewModel, overrides ); }