mirror of
https://github.com/portainer/portainer.git
synced 2026-06-23 07:30:13 +00:00
feat: clean frontend test logs (#1894)
This commit is contained in:
@@ -8,22 +8,6 @@ import { withTestQueryProvider } from '@/react/test-utils/withTestQuery';
|
||||
import { HeaderContainer } from './HeaderContainer';
|
||||
import { HeaderTitle } from './HeaderTitle';
|
||||
|
||||
test('should not render without a wrapping HeaderContainer', async () => {
|
||||
const consoleErrorFn = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => vi.fn());
|
||||
|
||||
const title = 'title';
|
||||
function renderComponent() {
|
||||
const Wrapped = withTestQueryProvider(HeaderTitle);
|
||||
return render(<Wrapped title={title} />);
|
||||
}
|
||||
|
||||
expect(renderComponent).toThrowErrorMatchingSnapshot();
|
||||
|
||||
consoleErrorFn.mockRestore();
|
||||
});
|
||||
|
||||
test('should display a HeaderTitle', async () => {
|
||||
const username = 'username';
|
||||
const user = new UserViewModel({ Username: username });
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`should not render without a wrapping HeaderContainer 1`] = `[Error: Should be nested inside a HeaderContainer component]`;
|
||||
@@ -6,6 +6,7 @@ import { createElement, Fragment } from 'react';
|
||||
|
||||
import { withTestQueryProvider } from '@/react/test-utils/withTestQuery';
|
||||
import { server } from '@/setup-tests/server';
|
||||
import { suppressConsoleLogs } from '@/setup-tests/suppress-console';
|
||||
|
||||
import {
|
||||
useExportMutation,
|
||||
@@ -227,6 +228,8 @@ describe('exportImage', () => {
|
||||
});
|
||||
|
||||
it('should throw error when export fails', async () => {
|
||||
const restoreConsole = suppressConsoleLogs();
|
||||
|
||||
server.use(
|
||||
http.get('/api/endpoints/:envId/docker/images/get', () =>
|
||||
HttpResponse.json({ message: 'Image not found' }, { status: 404 })
|
||||
@@ -240,6 +243,8 @@ describe('exportImage', () => {
|
||||
images: [{ id: 'sha256:abc123', tags: ['nginx:latest'] }],
|
||||
})
|
||||
).rejects.toThrow('Unable to export image');
|
||||
|
||||
restoreConsole();
|
||||
});
|
||||
|
||||
it('should handle filename without content-disposition header', async () => {
|
||||
@@ -297,6 +302,8 @@ describe('useExportMutation', () => {
|
||||
});
|
||||
|
||||
it('should handle export error', async () => {
|
||||
const restoreConsole = suppressConsoleLogs();
|
||||
|
||||
server.use(
|
||||
http.get('/api/endpoints/:envId/docker/images/get', () =>
|
||||
HttpResponse.json({ message: 'Internal server error' }, { status: 500 })
|
||||
@@ -315,6 +322,7 @@ describe('useExportMutation', () => {
|
||||
});
|
||||
|
||||
expect(result.current.error).toBeDefined();
|
||||
restoreConsole();
|
||||
});
|
||||
|
||||
it('should export multiple images with node name', async () => {
|
||||
|
||||
@@ -7,6 +7,7 @@ import { ComponentProps } from 'react';
|
||||
import { server } from '@/setup-tests/server';
|
||||
import { withTestQueryProvider } from '@/react/test-utils/withTestQuery';
|
||||
import { withUserProvider } from '@/react/test-utils/withUserProvider';
|
||||
import { suppressConsoleLogs } from '@/setup-tests/suppress-console';
|
||||
import { createMockUsers, createMockStack } from '@/react-tools/test-mocks';
|
||||
import { EnvironmentType } from '@/react/portainer/environments/types';
|
||||
import { Role } from '@/portainer/users/types';
|
||||
@@ -69,6 +70,7 @@ beforeEach(() => {
|
||||
|
||||
describe('initial loading', () => {
|
||||
it('should be empty when environment data is not loaded', async () => {
|
||||
const restoreConsole = suppressConsoleLogs();
|
||||
setupMswHandlers({ shouldReturnEnv: false });
|
||||
|
||||
const { container } = renderComponent();
|
||||
@@ -77,6 +79,8 @@ describe('initial loading', () => {
|
||||
await waitFor(() => {
|
||||
expect(container.innerHTML).toBe('<div></div>');
|
||||
});
|
||||
|
||||
restoreConsole();
|
||||
});
|
||||
|
||||
it('should be empty when schema data is not loaded', async () => {
|
||||
@@ -141,6 +145,8 @@ describe('initial loading', () => {
|
||||
|
||||
describe('form submission', () => {
|
||||
it('should show confirmation dialog before submitting', async () => {
|
||||
const restoreConsole = suppressConsoleLogs();
|
||||
|
||||
const mockConfirm = vi.mocked(confirmStackUpdate);
|
||||
renderComponent();
|
||||
const user = userEvent.setup();
|
||||
@@ -162,9 +168,13 @@ describe('form submission', () => {
|
||||
false // stackType is DockerCompose
|
||||
);
|
||||
});
|
||||
|
||||
restoreConsole();
|
||||
});
|
||||
|
||||
it('should call mutation API with correct payload', async () => {
|
||||
const restoreConsole = suppressConsoleLogs();
|
||||
|
||||
let capturedRequestBody: unknown;
|
||||
|
||||
server.use(
|
||||
@@ -203,6 +213,8 @@ describe('form submission', () => {
|
||||
},
|
||||
{ timeout: 3000 }
|
||||
);
|
||||
|
||||
restoreConsole();
|
||||
});
|
||||
|
||||
it('should not submit if confirmation is cancelled', async () => {
|
||||
@@ -244,6 +256,8 @@ describe('form submission', () => {
|
||||
});
|
||||
|
||||
it('should call onSubmitSuccess callback and show success notification after mutation completes', async () => {
|
||||
const restoreConsole = suppressConsoleLogs();
|
||||
|
||||
const onSubmitSuccess = vi.fn();
|
||||
renderComponent({ onSubmitSuccess });
|
||||
const user = userEvent.setup();
|
||||
@@ -266,9 +280,13 @@ describe('form submission', () => {
|
||||
);
|
||||
expect(onSubmitSuccess).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
restoreConsole();
|
||||
});
|
||||
|
||||
it('should handle API errors during submission', async () => {
|
||||
const restoreConsole = suppressConsoleLogs();
|
||||
|
||||
server.use(
|
||||
http.put('/api/stacks/:id', () =>
|
||||
HttpResponse.json({ message: 'Stack update failed' }, { status: 500 })
|
||||
@@ -295,6 +313,7 @@ describe('form submission', () => {
|
||||
});
|
||||
|
||||
expect(deployButton).toBeEnabled();
|
||||
restoreConsole();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import { withUserProvider } from '@/react/test-utils/withUserProvider';
|
||||
import { withTestRouter } from '@/react/test-utils/withRouter';
|
||||
import { createMockStack, createMockUsers } from '@/react-tools/test-mocks';
|
||||
import { Role } from '@/portainer/users/types';
|
||||
import { suppressConsoleLogs } from '@/setup-tests/suppress-console';
|
||||
|
||||
import { StackEditorTab } from './StackEditorTab';
|
||||
|
||||
@@ -103,6 +104,8 @@ describe('StackEditorTab - Webhook ID Handling', () => {
|
||||
|
||||
describe('Form submission', () => {
|
||||
it('should send webhook ID in API request when stack has webhook', async () => {
|
||||
const restoreConsole = suppressConsoleLogs();
|
||||
|
||||
const user = userEvent.setup();
|
||||
let capturedRequestBody: DefaultBodyType;
|
||||
|
||||
@@ -142,9 +145,12 @@ describe('StackEditorTab - Webhook ID Handling', () => {
|
||||
|
||||
assert(capturedRequestBody && typeof capturedRequestBody === 'object');
|
||||
expect(capturedRequestBody?.webhook).toBe('existing-webhook-123');
|
||||
|
||||
restoreConsole();
|
||||
});
|
||||
|
||||
it('should not send webhook ID in API request when stack has no webhook', async () => {
|
||||
const restoreConsole = suppressConsoleLogs();
|
||||
const user = userEvent.setup();
|
||||
let capturedRequestBody: DefaultBodyType;
|
||||
|
||||
@@ -183,6 +189,7 @@ describe('StackEditorTab - Webhook ID Handling', () => {
|
||||
);
|
||||
|
||||
expect(capturedRequestBody).not.toHaveProperty('webhook');
|
||||
restoreConsole();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@ import { vi } from 'vitest';
|
||||
|
||||
import { server } from '@/setup-tests/server';
|
||||
import { withTestQueryProvider } from '@/react/test-utils/withTestQuery';
|
||||
import { suppressConsoleLogs } from '@/setup-tests/suppress-console';
|
||||
|
||||
import { useVersionedStackFile } from './useVersionedStackFile';
|
||||
|
||||
@@ -305,6 +306,9 @@ describe('useVersionedStackFile', () => {
|
||||
});
|
||||
|
||||
describe('error handling', () => {
|
||||
const restoreConsole = suppressConsoleLogs();
|
||||
afterAll(restoreConsole);
|
||||
|
||||
it('should handle API errors gracefully', async () => {
|
||||
server.use(
|
||||
http.get('/api/stacks/:id/file', () =>
|
||||
@@ -420,6 +424,7 @@ describe('useVersionedStackFile', () => {
|
||||
});
|
||||
|
||||
it('should clear loading state after failed fetch', async () => {
|
||||
const restoreConsole = suppressConsoleLogs();
|
||||
server.use(
|
||||
http.get('/api/stacks/:id/file', () =>
|
||||
HttpResponse.json({ message: 'Error' }, { status: 500 })
|
||||
@@ -435,6 +440,8 @@ describe('useVersionedStackFile', () => {
|
||||
await waitFor(() => {
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
restoreConsole();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+5
@@ -18,6 +18,7 @@ import { withUserProvider } from '@/react/test-utils/withUserProvider';
|
||||
import { useApiVersion } from '@/react/docker/proxy/queries/useVersion';
|
||||
import { http, server } from '@/setup-tests/server';
|
||||
import { DeepPartial } from '@/types';
|
||||
import { suppressConsoleLogs } from '@/setup-tests/suppress-console';
|
||||
|
||||
import { StackRedeployGitForm } from './StackRedeployGitForm';
|
||||
|
||||
@@ -553,6 +554,10 @@ describe('StackRedeployGitForm', () => {
|
||||
});
|
||||
|
||||
describe('Error handling', () => {
|
||||
// Suppress console logs for error handling tests
|
||||
const restoreConsole = suppressConsoleLogs();
|
||||
afterAll(restoreConsole);
|
||||
|
||||
it('should handle updateGitStack mutation errors gracefully', async () => {
|
||||
const user = userEvent.setup();
|
||||
server.use(
|
||||
|
||||
@@ -5,6 +5,7 @@ import { HttpResponse } from 'msw';
|
||||
|
||||
import { server, http } from '@/setup-tests/server';
|
||||
import { withTestQueryProvider } from '@/react/test-utils/withTestQuery';
|
||||
import { suppressConsoleLogs } from '@/setup-tests/suppress-console';
|
||||
|
||||
import { useAppStackFile } from './useAppStackFile';
|
||||
|
||||
@@ -46,6 +47,7 @@ describe('useAppStackFile', () => {
|
||||
});
|
||||
|
||||
it('should handle fetch error for regular stack', async () => {
|
||||
const restoreConsole = suppressConsoleLogs();
|
||||
server.use(
|
||||
http.get('/api/stacks/999/file', () =>
|
||||
HttpResponse.json({ message: 'Stack not found' }, { status: 404 })
|
||||
@@ -57,6 +59,8 @@ describe('useAppStackFile', () => {
|
||||
await waitFor(() => {
|
||||
expect(result.current.isError).toBe(true);
|
||||
});
|
||||
|
||||
restoreConsole();
|
||||
});
|
||||
|
||||
it('should not fetch when query is disabled', async () => {
|
||||
|
||||
@@ -183,6 +183,7 @@ describe('NodeDetails', () => {
|
||||
});
|
||||
|
||||
it('shows drain warning when selecting Drain availability', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderComponent();
|
||||
|
||||
// Wait for component to load
|
||||
@@ -192,11 +193,11 @@ describe('NodeDetails', () => {
|
||||
|
||||
// Find the availability select and select Drain
|
||||
const availabilitySelect = screen.getByLabelText('Availability');
|
||||
await select(availabilitySelect, 'Drain');
|
||||
await select(availabilitySelect, 'Drain', { user });
|
||||
|
||||
// Try to submit the form to trigger validation
|
||||
const submitButton = screen.getByRole('button', { name: /update node/i });
|
||||
await userEvent.click(submitButton);
|
||||
await user.click(submitButton);
|
||||
|
||||
// Check that the confirmation modal is called with drain warning
|
||||
await waitFor(() => {
|
||||
@@ -210,6 +211,7 @@ describe('NodeDetails', () => {
|
||||
});
|
||||
|
||||
it('prevents submission when Portainer is running on node', async () => {
|
||||
const user = userEvent.setup();
|
||||
setupMocks({ applications: mockPortainerApplications });
|
||||
|
||||
renderComponent();
|
||||
@@ -219,7 +221,7 @@ describe('NodeDetails', () => {
|
||||
});
|
||||
|
||||
const availabilitySelect = screen.getByLabelText('Availability');
|
||||
await select(availabilitySelect, 'Drain');
|
||||
await select(availabilitySelect, 'Drain', { user });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
@@ -232,6 +234,7 @@ describe('NodeDetails', () => {
|
||||
});
|
||||
|
||||
it('prevents drain when only one node in cluster', async () => {
|
||||
const user = userEvent.setup();
|
||||
setupMocks({ nodes: [mockNode] });
|
||||
|
||||
renderComponent();
|
||||
@@ -241,7 +244,7 @@ describe('NodeDetails', () => {
|
||||
});
|
||||
|
||||
const availabilitySelect = screen.getByLabelText('Availability');
|
||||
await select(availabilitySelect, 'Drain');
|
||||
await select(availabilitySelect, 'Drain', { user });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
@@ -254,6 +257,7 @@ describe('NodeDetails', () => {
|
||||
});
|
||||
|
||||
it('prevents drain when another node is already draining', async () => {
|
||||
const user = userEvent.setup();
|
||||
const drainingNodes = [
|
||||
mockNode,
|
||||
{
|
||||
@@ -276,7 +280,7 @@ describe('NodeDetails', () => {
|
||||
});
|
||||
|
||||
const availabilitySelect = screen.getByLabelText('Availability');
|
||||
await select(availabilitySelect, 'Drain');
|
||||
await select(availabilitySelect, 'Drain', { user });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
@@ -286,6 +290,7 @@ describe('NodeDetails', () => {
|
||||
});
|
||||
|
||||
it('shows cordon warning when submitting with Pause availability', async () => {
|
||||
const user = userEvent.setup();
|
||||
vi.mocked(confirmUpdateNode).mockResolvedValue(true);
|
||||
|
||||
renderComponent();
|
||||
@@ -295,10 +300,10 @@ describe('NodeDetails', () => {
|
||||
});
|
||||
|
||||
const availabilitySelect = screen.getByLabelText('Availability');
|
||||
await select(availabilitySelect, 'Pause');
|
||||
await select(availabilitySelect, 'Pause', { user });
|
||||
|
||||
const submitButton = screen.getByRole('button', { name: /update node/i });
|
||||
await userEvent.click(submitButton);
|
||||
await user.click(submitButton);
|
||||
|
||||
// Verify confirmation modal was called with cordon warning
|
||||
await waitFor(() => {
|
||||
|
||||
@@ -7,6 +7,7 @@ import { withTestQueryProvider } from '@/react/test-utils/withTestQuery';
|
||||
import { withTestRouter } from '@/react/test-utils/withRouter';
|
||||
import { withUserProvider } from '@/react/test-utils/withUserProvider';
|
||||
import { server } from '@/setup-tests/server';
|
||||
import { suppressConsoleLogs } from '@/setup-tests/suppress-console';
|
||||
import {
|
||||
Environment,
|
||||
EnvironmentType,
|
||||
@@ -228,6 +229,10 @@ describe('EditGroupView', () => {
|
||||
});
|
||||
|
||||
describe('Error state', () => {
|
||||
// Suppress console logs for error state tests
|
||||
const restoreConsole = suppressConsoleLogs();
|
||||
afterAll(restoreConsole);
|
||||
|
||||
it('should show error Alert when group fetch fails', async () => {
|
||||
renderEditGroupView({ groupData: null });
|
||||
|
||||
@@ -392,9 +397,7 @@ describe('EditGroupView', () => {
|
||||
|
||||
describe('Error handling on update', () => {
|
||||
it('should handle API error gracefully on update', async () => {
|
||||
const consoleErrorSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
const restoreConsole = suppressConsoleLogs();
|
||||
|
||||
const mutationError = vi.fn();
|
||||
const errorMessage = 'Failed to update group';
|
||||
@@ -429,7 +432,7 @@ describe('EditGroupView', () => {
|
||||
expect(mutationError).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
consoleErrorSpy.mockRestore();
|
||||
restoreConsole();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+5
@@ -5,6 +5,7 @@ import { vi } from 'vitest';
|
||||
|
||||
import { withTestQueryProvider } from '@/react/test-utils/withTestQuery';
|
||||
import { server } from '@/setup-tests/server';
|
||||
import { suppressConsoleLogs } from '@/setup-tests/suppress-console';
|
||||
|
||||
import { RegistryTestConnection } from './RegistryTestConnection';
|
||||
|
||||
@@ -102,6 +103,8 @@ test('should show error message on failed test', async () => {
|
||||
});
|
||||
|
||||
test('should show error message on network error', async () => {
|
||||
const restoreConsole = suppressConsoleLogs();
|
||||
|
||||
const user = userEvent.setup();
|
||||
|
||||
// Mock network error
|
||||
@@ -117,6 +120,8 @@ test('should show error message on network error', async () => {
|
||||
screen.getByText(/Failed to test registry connection/)
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
restoreConsole();
|
||||
});
|
||||
|
||||
test('should send correct payload to API', async () => {
|
||||
|
||||
@@ -22,4 +22,7 @@ export const dockerHandlers = [
|
||||
http.get('/api/endpoints/:endpointId/docker/containers/json', () =>
|
||||
HttpResponse.json([])
|
||||
),
|
||||
http.get('/api/endpoints/:endpointId/docker/networks', () =>
|
||||
HttpResponse.json([])
|
||||
),
|
||||
];
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
/* eslint-disable no-console */
|
||||
import { vi } from 'vitest';
|
||||
|
||||
/**
|
||||
* Suppresses all console output during tests.
|
||||
* Useful for tests that trigger expected errors, warnings, or info messages
|
||||
* that clutter the test output without providing useful information.
|
||||
*
|
||||
* Can be used at file level or per-test level.
|
||||
*
|
||||
* @example File level usage
|
||||
* ```typescript
|
||||
* import { suppressConsoleLogs } from '@/setup-tests/suppress-console';
|
||||
*
|
||||
* const restoreConsole = suppressConsoleLogs();
|
||||
* afterAll(restoreConsole);
|
||||
* ```
|
||||
*
|
||||
* @example Per-test usage
|
||||
* ```typescript
|
||||
* import { suppressConsoleLogs } from '@/setup-tests/suppress-console';
|
||||
*
|
||||
* describe('some test suite', () => {
|
||||
* let restoreConsole: () => void;
|
||||
*
|
||||
* beforeEach(() => {
|
||||
* restoreConsole = suppressConsoleLogs();
|
||||
* });
|
||||
*
|
||||
* afterEach(() => {
|
||||
* restoreConsole();
|
||||
* });
|
||||
*
|
||||
* test('test that produces noisy logs', () => {
|
||||
* // console logs suppressed
|
||||
* });
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @returns A cleanup function to restore the original console methods
|
||||
*/
|
||||
export function suppressConsoleLogs() {
|
||||
const originalError = console.error;
|
||||
const originalWarn = console.warn;
|
||||
const originalInfo = console.info;
|
||||
const originalLog = console.log;
|
||||
|
||||
// Suppress all console output
|
||||
// Tests expect errors so no need to show them in the output
|
||||
console.error = vi.fn();
|
||||
console.warn = vi.fn();
|
||||
console.info = vi.fn();
|
||||
console.log = vi.fn();
|
||||
|
||||
// Return cleanup function to restore original console methods
|
||||
return () => {
|
||||
console.error = originalError;
|
||||
console.warn = originalWarn;
|
||||
console.info = originalInfo;
|
||||
console.log = originalLog;
|
||||
};
|
||||
}
|
||||
|
||||
/* eslint-enable no-console */
|
||||
Reference in New Issue
Block a user