feat: clean frontend test logs (#1894)

This commit is contained in:
Chaim Lev-Ari
2026-02-22 07:42:49 +00:00
committed by GitHub
parent caf6b2aa0c
commit 2bbcae39b6
13 changed files with 141 additions and 30 deletions
@@ -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();
});
});
@@ -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,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 () => {
+3
View File
@@ -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([])
),
];
+64
View File
@@ -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 */