diff --git a/app/react/components/PageHeader/HeaderTitle.test.tsx b/app/react/components/PageHeader/HeaderTitle.test.tsx
index 490e9608c..68385f0eb 100644
--- a/app/react/components/PageHeader/HeaderTitle.test.tsx
+++ b/app/react/components/PageHeader/HeaderTitle.test.tsx
@@ -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();
- }
-
- expect(renderComponent).toThrowErrorMatchingSnapshot();
-
- consoleErrorFn.mockRestore();
-});
-
test('should display a HeaderTitle', async () => {
const username = 'username';
const user = new UserViewModel({ Username: username });
diff --git a/app/react/components/PageHeader/__snapshots__/HeaderTitle.test.tsx.snap b/app/react/components/PageHeader/__snapshots__/HeaderTitle.test.tsx.snap
deleted file mode 100644
index 05a885748..000000000
--- a/app/react/components/PageHeader/__snapshots__/HeaderTitle.test.tsx.snap
+++ /dev/null
@@ -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]`;
diff --git a/app/react/docker/images/queries/useExportImageMutation.test.ts b/app/react/docker/images/queries/useExportImageMutation.test.ts
index 8598d1440..fd174b797 100644
--- a/app/react/docker/images/queries/useExportImageMutation.test.ts
+++ b/app/react/docker/images/queries/useExportImageMutation.test.ts
@@ -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 () => {
diff --git a/app/react/docker/stacks/ItemView/StackEditorTab/StackEditorTab.test.tsx b/app/react/docker/stacks/ItemView/StackEditorTab/StackEditorTab.test.tsx
index 4bc0ec548..7942670c1 100644
--- a/app/react/docker/stacks/ItemView/StackEditorTab/StackEditorTab.test.tsx
+++ b/app/react/docker/stacks/ItemView/StackEditorTab/StackEditorTab.test.tsx
@@ -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('
');
});
+
+ 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();
});
});
diff --git a/app/react/docker/stacks/ItemView/StackEditorTab/StackEditorTab.webhook.test.tsx b/app/react/docker/stacks/ItemView/StackEditorTab/StackEditorTab.webhook.test.tsx
index e677bd7c0..1447709c7 100644
--- a/app/react/docker/stacks/ItemView/StackEditorTab/StackEditorTab.webhook.test.tsx
+++ b/app/react/docker/stacks/ItemView/StackEditorTab/StackEditorTab.webhook.test.tsx
@@ -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();
});
});
});
diff --git a/app/react/docker/stacks/ItemView/StackEditorTab/useVersionedStackFile.test.tsx b/app/react/docker/stacks/ItemView/StackEditorTab/useVersionedStackFile.test.tsx
index d47410519..782b9390b 100644
--- a/app/react/docker/stacks/ItemView/StackEditorTab/useVersionedStackFile.test.tsx
+++ b/app/react/docker/stacks/ItemView/StackEditorTab/useVersionedStackFile.test.tsx
@@ -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();
});
});
diff --git a/app/react/docker/stacks/ItemView/StackInfoTab/StackRedeployGitForm/StackRedeployGitForm.test.tsx b/app/react/docker/stacks/ItemView/StackInfoTab/StackRedeployGitForm/StackRedeployGitForm.test.tsx
index e01803193..3d799e43a 100644
--- a/app/react/docker/stacks/ItemView/StackInfoTab/StackRedeployGitForm/StackRedeployGitForm.test.tsx
+++ b/app/react/docker/stacks/ItemView/StackInfoTab/StackRedeployGitForm/StackRedeployGitForm.test.tsx
@@ -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(
diff --git a/app/react/kubernetes/applications/queries/useAppStackFile.test.tsx b/app/react/kubernetes/applications/queries/useAppStackFile.test.tsx
index c976c95ae..03d44b5fa 100644
--- a/app/react/kubernetes/applications/queries/useAppStackFile.test.tsx
+++ b/app/react/kubernetes/applications/queries/useAppStackFile.test.tsx
@@ -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 () => {
diff --git a/app/react/kubernetes/cluster/NodeView/NodeDetails/NodeDetails.test.tsx b/app/react/kubernetes/cluster/NodeView/NodeDetails/NodeDetails.test.tsx
index 186b2b4e7..a62bf46fe 100644
--- a/app/react/kubernetes/cluster/NodeView/NodeDetails/NodeDetails.test.tsx
+++ b/app/react/kubernetes/cluster/NodeView/NodeDetails/NodeDetails.test.tsx
@@ -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(() => {
diff --git a/app/react/portainer/environments/environment-groups/ItemView/EditGroupView.test.tsx b/app/react/portainer/environments/environment-groups/ItemView/EditGroupView.test.tsx
index 47f2626c3..faab6c1a6 100644
--- a/app/react/portainer/environments/environment-groups/ItemView/EditGroupView.test.tsx
+++ b/app/react/portainer/environments/environment-groups/ItemView/EditGroupView.test.tsx
@@ -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();
});
});
diff --git a/app/react/portainer/registries/CreateView/TestConnection/RegistryTestConnection.test.tsx b/app/react/portainer/registries/CreateView/TestConnection/RegistryTestConnection.test.tsx
index fac3d4406..572cad21e 100644
--- a/app/react/portainer/registries/CreateView/TestConnection/RegistryTestConnection.test.tsx
+++ b/app/react/portainer/registries/CreateView/TestConnection/RegistryTestConnection.test.tsx
@@ -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 () => {
diff --git a/app/setup-tests/setup-handlers/docker.ts b/app/setup-tests/setup-handlers/docker.ts
index 6a82ac803..e7347481a 100644
--- a/app/setup-tests/setup-handlers/docker.ts
+++ b/app/setup-tests/setup-handlers/docker.ts
@@ -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([])
+ ),
];
diff --git a/app/setup-tests/suppress-console.ts b/app/setup-tests/suppress-console.ts
new file mode 100644
index 000000000..0acefb885
--- /dev/null
+++ b/app/setup-tests/suppress-console.ts
@@ -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 */