feat(tests): enforce no unexpected console output in unit tests [BE-13230] (#3097)

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Chaim Lev-Ari
2026-08-05 10:50:18 +03:00
committed by GitHub
parent c58edcb81f
commit df4c700667
38 changed files with 278 additions and 139 deletions
+9
View File
@@ -0,0 +1,9 @@
import { cloneElement, ReactElement } from 'react';
export * from 'recharts';
// jsdom reports zero layout dimensions, so recharts renders nothing without
// an explicit size. Pass one through instead of measuring the container.
export function ResponsiveContainer({ children }: { children: ReactElement }) {
return cloneElement(children, { width: 800, height: 300 });
}
@@ -5,10 +5,19 @@ import {
} from '@uirouter/angularjs'; } from '@uirouter/angularjs';
import { get, keyBuilder } from '@/react/hooks/useLocalStorage'; import { get, keyBuilder } from '@/react/hooks/useLocalStorage';
import { suppressConsoleLogs } from '@/setup-tests/suppress-console';
import { checkAuthorizations } from './authorization-guard'; import { checkAuthorizations } from './authorization-guard';
import { IAuthenticationService } from './services/types'; import { IAuthenticationService } from './services/types';
let restoreConsole: () => void;
beforeEach(() => {
restoreConsole = suppressConsoleLogs();
});
afterEach(() => {
restoreConsole();
});
describe('checkAuthorizations', () => { describe('checkAuthorizations', () => {
let authService = { let authService = {
init: vi.fn(), init: vi.fn(),
+7 -11
View File
@@ -1,10 +1,15 @@
import toastr from 'toastr'; import toastr from 'toastr';
import { suppressConsoleLogs } from '@/setup-tests/suppress-console';
import { notifyError, notifySuccess, notifyWarning } from './notifications'; import { notifyError, notifySuccess, notifyWarning } from './notifications';
vi.spyOn(console, 'error').mockImplementation(() => vi.fn()); let restoreConsole: () => void;
beforeEach(() => {
restoreConsole = suppressConsoleLogs();
});
afterEach(() => { afterEach(() => {
restoreConsole();
vi.resetAllMocks(); vi.resetAllMocks();
}); });
@@ -18,9 +23,6 @@ it('calling success should show success message', () => {
}); });
it('calling error with Error should show error message', () => { it('calling error with Error should show error message', () => {
const consoleErrorFn = vi
.spyOn(console, 'error')
.mockImplementation(() => vi.fn());
const title = 'title'; const title = 'title';
const errorMessage = 'message'; const errorMessage = 'message';
const fallback = 'fallback'; const fallback = 'fallback';
@@ -32,14 +34,9 @@ it('calling error with Error should show error message', () => {
title, title,
expect.anything() expect.anything()
); );
consoleErrorFn.mockRestore();
}); });
it('calling error without Error should show fallback message', () => { it('calling error without Error should show fallback message', () => {
const consoleErrorFn = vi
.spyOn(console, 'error')
.mockImplementation(() => vi.fn());
const title = 'title'; const title = 'title';
const fallback = 'fallback'; const fallback = 'fallback';
@@ -47,7 +44,6 @@ it('calling error without Error should show fallback message', () => {
notifyError(title, undefined, fallback); notifyError(title, undefined, fallback);
expect(toastr.error).toHaveBeenCalledWith(fallback, title, expect.anything()); expect(toastr.error).toHaveBeenCalledWith(fallback, title, expect.anything());
consoleErrorFn.mockRestore();
}); });
it('calling warning should show warning message', () => { it('calling warning should show warning message', () => {
@@ -11,6 +11,7 @@ import {
import { withUserProvider } from '@/react/test-utils/withUserProvider'; import { withUserProvider } from '@/react/test-utils/withUserProvider';
import { withTestRouter } from '@/react/test-utils/withRouter'; import { withTestRouter } from '@/react/test-utils/withRouter';
import { withTestQueryProvider } from '@/react/test-utils/withTestQuery'; import { withTestQueryProvider } from '@/react/test-utils/withTestQuery';
import { suppressConsoleLogs } from '@/setup-tests/suppress-console';
import { DashboardView } from './DashboardView'; import { DashboardView } from './DashboardView';
@@ -76,7 +77,7 @@ test('should correctly show total number of resource groups across multiple subs
}); });
test("when only subscriptions fail to load, don't show the dashboard", async () => { test("when only subscriptions fail to load, don't show the dashboard", async () => {
vi.spyOn(console, 'error').mockImplementation(() => {}); const restoreConsole = suppressConsoleLogs();
const { queryByLabelText } = await renderComponent( const { queryByLabelText } = await renderComponent(
1, 1,
@@ -86,10 +87,11 @@ test("when only subscriptions fail to load, don't show the dashboard", async ()
); );
expect(queryByLabelText('Subscription')).not.toBeInTheDocument(); expect(queryByLabelText('Subscription')).not.toBeInTheDocument();
expect(queryByLabelText('Resource group')).not.toBeInTheDocument(); expect(queryByLabelText('Resource group')).not.toBeInTheDocument();
restoreConsole();
}); });
test('when only resource groups fail to load, still show the subscriptions', async () => { test('when only resource groups fail to load, still show the subscriptions', async () => {
vi.spyOn(console, 'error').mockImplementation(() => {}); const restoreConsole = suppressConsoleLogs();
const { queryByLabelText, findByLabelText } = await renderComponent( const { queryByLabelText, findByLabelText } = await renderComponent(
1, 1,
@@ -99,6 +101,7 @@ test('when only resource groups fail to load, still show the subscriptions', asy
); );
await expect(findByLabelText('Subscription')).resolves.toBeInTheDocument(); await expect(findByLabelText('Subscription')).resolves.toBeInTheDocument();
expect(queryByLabelText('Resource group')).not.toBeInTheDocument(); expect(queryByLabelText('Resource group')).not.toBeInTheDocument();
restoreConsole();
}); });
async function renderComponent( async function renderComponent(
@@ -1,17 +1,9 @@
import React from 'react';
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import { StatsLineChart } from './StatsLineChart'; import { StatsLineChart } from './StatsLineChart';
import type { SeriesConfig } from './StatsLineChart'; import type { SeriesConfig } from './StatsLineChart';
vi.mock('recharts', async (importOriginal) => { vi.mock('recharts');
const original = await importOriginal<typeof import('recharts')>();
return {
...original,
ResponsiveContainer: ({ children }: { children: React.ReactElement }) =>
React.cloneElement(children, { width: 800, height: 300 }),
};
});
function yAxisFormatter(value: number): string { function yAxisFormatter(value: number): string {
return `${value}%`; return `${value}%`;
+8 -4
View File
@@ -84,19 +84,23 @@ export function ContainerStats({
running, running,
stopped, stopped,
}: ContainerStatsProps) { }: ContainerStatsProps) {
const actualTotal = total || running + stopped; const safeRunning = running || 0;
const safeStopped = stopped || 0;
const actualTotal = total || safeRunning + safeStopped;
return ( return (
<StatsItem title="CONTAINERS" icon={Hexagon}> <StatsItem title="CONTAINERS" icon={Hexagon}>
<div className="flex w-full flex-col"> <div className="flex w-full flex-col">
<div> <div>
<span className="text-base font-bold leading-none">{running}</span> <span className="text-base font-bold leading-none">
{safeRunning}
</span>
<span> / {actualTotal}</span> <span> / {actualTotal}</span>
</div> </div>
<progress <progress
className="h-[4px] w-auto rounded bg-gray-4 th-dark:bg-white/10" className="h-[4px] w-auto rounded bg-gray-4 th-dark:bg-white/10"
value={running} value={safeRunning}
max={Math.max(actualTotal, 1)} max={Math.max(actualTotal, 1)}
aria-label={`${running} of ${actualTotal} containers running`} aria-label={`${safeRunning} of ${actualTotal} containers running`}
/> />
</div> </div>
</StatsItem> </StatsItem>
@@ -3,6 +3,8 @@ import { Layers } from 'lucide-react';
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import { vi } from 'vitest'; import { vi } from 'vitest';
import { suppressConsoleLogs } from '@/setup-tests/suppress-console';
import { findSelectedTabIndex, Tab, WidgetTabs } from './WidgetTabs'; import { findSelectedTabIndex, Tab, WidgetTabs } from './WidgetTabs';
// Mock Link component to avoid ui-router relative state resolution in tests // Mock Link component to avoid ui-router relative state resolution in tests
@@ -122,6 +124,8 @@ describe('WidgetTabs', () => {
describe('error handling', () => { describe('error handling', () => {
it('throws an error when any tab has an invalid URL-encodable param value', () => { it('throws an error when any tab has an invalid URL-encodable param value', () => {
const restoreConsole = suppressConsoleLogs();
// Tabs with characters that change when URL-encoded // Tabs with characters that change when URL-encoded
const invalidTabs: Tab[] = [ const invalidTabs: Tab[] = [
{ {
@@ -135,6 +139,8 @@ describe('WidgetTabs', () => {
expect(() => expect(() =>
renderWidgetTabs({ tabs: invalidTabs, currentTabIndex: 1 }) renderWidgetTabs({ tabs: invalidTabs, currentTabIndex: 1 })
).toThrow('Invalid query param value for tab'); ).toThrow('Invalid query param value for tab');
restoreConsole();
}); });
}); });
@@ -252,7 +252,7 @@ export function MultiSelect<TValue = string>({
noOptionsMessage={noOptionsMessage} noOptionsMessage={noOptionsMessage}
loadingMessage={loadingMessage} loadingMessage={loadingMessage}
formatCreateLabel={formatCreateLabel} formatCreateLabel={formatCreateLabel}
onCreateOption={onCreateOption} onCreateOption={handleCreateOption}
inputValue={inputValue} inputValue={inputValue}
onInputChange={(textInput) => setInputValue(textInput)} onInputChange={(textInput) => setInputValue(textInput)}
onBlur={handleBlur} onBlur={handleBlur}
@@ -262,6 +262,15 @@ export function MultiSelect<TValue = string>({
/> />
); );
// Selecting the "create" menu option calls onCreateOption directly,
// bypassing the onChange handler above, so inputValue is cleared here
// instead — otherwise the stale text would trigger a second, duplicate
// creation from handleBlur below once the select loses focus.
function handleCreateOption(input: string) {
setInputValue('');
onCreateOption?.(input);
}
function handleBlur(e: React.FocusEvent<HTMLInputElement>) { function handleBlur(e: React.FocusEvent<HTMLInputElement>) {
onBlur?.(e); onBlur?.(e);
const trimmed = inputValue.trim(); const trimmed = inputValue.trim();
@@ -270,11 +279,10 @@ export function MultiSelect<TValue = string>({
return; return;
} }
if (onCreateOption && isCreatable) { if (onCreateOption && isCreatable) {
onCreateOption(trimmed); handleCreateOption(trimmed);
} else { } else {
onChange([...value, trimmed as TValue]); onChange([...value, trimmed as TValue]);
} }
setInputValue('');
} }
} }
@@ -6,7 +6,10 @@ import { withUserProvider } from '@/react/test-utils/withUserProvider';
import { withTestRouter } from '@/react/test-utils/withRouter'; import { withTestRouter } from '@/react/test-utils/withRouter';
import { server } from '@/setup-tests/server'; import { server } from '@/setup-tests/server';
import { Role } from '@/portainer/users/types'; import { Role } from '@/portainer/users/types';
import { createMockUsers } from '@/react-tools/test-mocks'; import {
createMockEnvironment,
createMockUsers,
} from '@/react-tools/test-mocks';
import { ListView } from './ListView'; import { ListView } from './ListView';
@@ -44,11 +47,9 @@ describe('ListView', () => {
beforeEach(() => { beforeEach(() => {
server.use( server.use(
http.get('/api/endpoints/1', () => http.get('/api/endpoints/1', () =>
HttpResponse.json({ HttpResponse.json(
Id: 1, createMockEnvironment({ Id: 1, Name: 'test-environment', Type: 1 })
Name: 'test-environment', )
Type: 1,
})
), ),
http.get('/api/endpoints/:environmentId/docker/configs', () => http.get('/api/endpoints/:environmentId/docker/configs', () =>
HttpResponse.json([]) HttpResponse.json([])
@@ -8,6 +8,7 @@ import { withTestRouter } from '@/react/test-utils/withRouter';
import { withTestQueryProvider } from '@/react/test-utils/withTestQuery'; import { withTestQueryProvider } from '@/react/test-utils/withTestQuery';
import { createMockUser } from '@/react-tools/test-mocks'; import { createMockUser } from '@/react-tools/test-mocks';
import { server } from '@/setup-tests/server'; import { server } from '@/setup-tests/server';
import { suppressConsoleLogs } from '@/setup-tests/suppress-console';
import { User } from '@/portainer/users/types'; import { User } from '@/portainer/users/types';
import { NameRow } from './NameRow'; import { NameRow } from './NameRow';
@@ -262,10 +263,8 @@ describe('NameRow', () => {
}); });
it('handles rename API error gracefully', async () => { it('handles rename API error gracefully', async () => {
// Mock console.error to suppress expected error logs // Suppress expected error logs
const consoleErrorSpy = vi const restoreConsole = suppressConsoleLogs();
.spyOn(console, 'error')
.mockImplementation(() => {});
server.use( server.use(
http.post( http.post(
@@ -304,7 +303,7 @@ describe('NameRow', () => {
expect(screen.getByTestId('containerNameInput')).toBeVisible(); expect(screen.getByTestId('containerNameInput')).toBeVisible();
expect(nameInput).toHaveValue('new-name'); expect(nameInput).toHaveValue('new-name');
consoleErrorSpy.mockRestore(); restoreConsole();
}); });
it('validates that container name is required', async () => { it('validates that container name is required', async () => {
@@ -8,6 +8,7 @@ import { withTestRouter } from '@/react/test-utils/withRouter';
import { withUserProvider } from '@/react/test-utils/withUserProvider'; import { withUserProvider } from '@/react/test-utils/withUserProvider';
import { server } from '@/setup-tests/server'; import { server } from '@/setup-tests/server';
import { createMockUser } from '@/react-tools/test-mocks'; import { createMockUser } from '@/react-tools/test-mocks';
import { suppressConsoleLogs } from '@/setup-tests/suppress-console';
import { CreateImageSection } from './CreateImageSection'; import { CreateImageSection } from './CreateImageSection';
@@ -174,9 +175,7 @@ describe('CreateImageSection', () => {
it('should handle API error', async () => { it('should handle API error', async () => {
const onMutationError = vi.fn(); const onMutationError = vi.fn();
const consoleErrorSpy = vi const restoreConsole = suppressConsoleLogs();
.spyOn(console, 'error')
.mockImplementation(() => {});
server.use( server.use(
http.post('/api/endpoints/:endpointId/docker/commit', () => http.post('/api/endpoints/:endpointId/docker/commit', () =>
@@ -209,7 +208,7 @@ describe('CreateImageSection', () => {
{ timeout: 3000 } { timeout: 3000 }
); );
consoleErrorSpy.mockRestore(); restoreConsole();
}); });
it('should show loading state during creation', async () => { it('should show loading state during creation', async () => {
@@ -16,6 +16,8 @@ vi.mock('@uirouter/react', async (importOriginal) => ({
})), })),
})); }));
vi.mock('recharts');
const minimalStats = { const minimalStats = {
read: '2024-01-01T00:00:01Z', read: '2024-01-01T00:00:01Z',
preread: '2024-01-01T00:00:00Z', preread: '2024-01-01T00:00:00Z',
@@ -59,6 +61,9 @@ function addBaseHandlers() {
), ),
http.get('/api/endpoints/1/docker/containers/container1/top', () => http.get('/api/endpoints/1/docker/containers/container1/top', () =>
HttpResponse.json({ Processes: [], Titles: [] }) HttpResponse.json({ Processes: [], Titles: [] })
),
http.get('/api/endpoints/1/docker/containers/container1/stats', () =>
HttpResponse.json(minimalStats)
) )
); );
} }
@@ -5,6 +5,7 @@ import { HttpResponse, http } from 'msw';
import { withTestQueryProvider } from '@/react/test-utils/withTestQuery'; import { withTestQueryProvider } from '@/react/test-utils/withTestQuery';
import { withTestRouter } from '@/react/test-utils/withRouter'; import { withTestRouter } from '@/react/test-utils/withRouter';
import { server } from '@/setup-tests/server'; import { server } from '@/setup-tests/server';
import { suppressConsoleLogs } from '@/setup-tests/suppress-console';
import { PruneButton } from './PruneButton'; import { PruneButton } from './PruneButton';
@@ -423,6 +424,14 @@ describe('PruneButton', () => {
}); });
describe('Error Handling', () => { describe('Error Handling', () => {
let restoreConsole: () => void;
beforeEach(() => {
restoreConsole = suppressConsoleLogs();
});
afterEach(() => {
restoreConsole();
});
it('should show error notification on API failure', async () => { it('should show error notification on API failure', async () => {
mockConfirmPruneImages.mockResolvedValue({ mockConfirmPruneImages.mockResolvedValue({
pruneAll: false, pruneAll: false,
@@ -6,6 +6,7 @@ import { withUserProvider } from '@/react/test-utils/withUserProvider';
import { withTestRouter } from '@/react/test-utils/withRouter'; import { withTestRouter } from '@/react/test-utils/withRouter';
import { withTestQueryProvider } from '@/react/test-utils/withTestQuery'; import { withTestQueryProvider } from '@/react/test-utils/withTestQuery';
import { server } from '@/setup-tests/server'; import { server } from '@/setup-tests/server';
import { createMockEnvironment } from '@/react-tools/test-mocks';
import { NetworkContainer } from '../types'; import { NetworkContainer } from '../types';
@@ -31,7 +32,11 @@ vi.mock('@uirouter/react', async (importOriginal: () => Promise<object>) => ({
})); }));
test('Network container values should be visible and the link should be valid', async () => { test('Network container values should be visible and the link should be valid', async () => {
server.use(http.get('/api/endpoints/1', () => HttpResponse.json({}))); server.use(
http.get('/api/endpoints/1', () =>
HttpResponse.json(createMockEnvironment())
)
);
const user = new UserViewModel({ Username: 'test', Role: 1 }); const user = new UserViewModel({ Username: 'test', Role: 1 });
@@ -5,6 +5,7 @@ import { UserViewModel } from '@/portainer/models/user';
import { server } from '@/setup-tests/server'; import { server } from '@/setup-tests/server';
import { withUserProvider } from '@/react/test-utils/withUserProvider'; import { withUserProvider } from '@/react/test-utils/withUserProvider';
import { withTestQueryProvider } from '@/react/test-utils/withTestQuery'; import { withTestQueryProvider } from '@/react/test-utils/withTestQuery';
import { createMockEnvironment } from '@/react-tools/test-mocks';
import { DockerNetwork } from '../types'; import { DockerNetwork } from '../types';
@@ -52,7 +53,11 @@ test('Non system networks should have a delete button', async () => {
}); });
async function renderComponent(isAdmin: boolean, network: DockerNetwork) { async function renderComponent(isAdmin: boolean, network: DockerNetwork) {
server.use(http.get('/api/endpoints/1', () => HttpResponse.json({}))); server.use(
http.get('/api/endpoints/1', () =>
HttpResponse.json(createMockEnvironment())
)
);
const user = new UserViewModel({ Username: 'test', Role: isAdmin ? 1 : 2 }); const user = new UserViewModel({ Username: 'test', Role: isAdmin ? 1 : 2 });
@@ -9,6 +9,7 @@ import { withTestQueryProvider } from '@/react/test-utils/withTestQuery';
import { withTestRouter } from '@/react/test-utils/withRouter'; import { withTestRouter } from '@/react/test-utils/withRouter';
import { withUserProvider } from '@/react/test-utils/withUserProvider'; import { withUserProvider } from '@/react/test-utils/withUserProvider';
import { server } from '@/setup-tests/server'; import { server } from '@/setup-tests/server';
import { suppressConsoleLogs } from '@/setup-tests/suppress-console';
import { CreateStackForm } from './CreateStackForm'; import { CreateStackForm } from './CreateStackForm';
@@ -334,10 +335,7 @@ describe('CreateStackForm', () => {
}); });
it('should handle API error gracefully', async () => { it('should handle API error gracefully', async () => {
const consoleErrorSpy = vi const restoreConsole = suppressConsoleLogs();
.spyOn(console, 'error')
.mockImplementation(() => {});
const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
const mutationError = vi.fn(); const mutationError = vi.fn();
const errorMessage = 'test - failed to create stack'; const errorMessage = 'test - failed to create stack';
@@ -380,7 +378,6 @@ describe('CreateStackForm', () => {
); );
}); });
consoleErrorSpy.mockRestore(); restoreConsole();
consoleLogSpy.mockRestore();
}); });
}); });
@@ -306,8 +306,13 @@ describe('useVersionedStackFile', () => {
}); });
describe('error handling', () => { describe('error handling', () => {
const restoreConsole = suppressConsoleLogs(); let restoreConsole: () => void;
afterAll(restoreConsole); beforeEach(() => {
restoreConsole = suppressConsoleLogs();
});
afterEach(() => {
restoreConsole();
});
it('should handle API errors gracefully', async () => { it('should handle API errors gracefully', async () => {
server.use( server.use(
@@ -6,6 +6,7 @@ import { server } from '@/setup-tests/server';
import { withTestQueryProvider } from '@/react/test-utils/withTestQuery'; import { withTestQueryProvider } from '@/react/test-utils/withTestQuery';
import { Stack } from '@/react/common/stacks/types'; import { Stack } from '@/react/common/stacks/types';
import { ResourceControlOwnership } from '@/react/portainer/access-control/types'; import { ResourceControlOwnership } from '@/react/portainer/access-control/types';
import { suppressConsoleLogs } from '@/setup-tests/suppress-console';
import { useAssociateStackToEnvironmentMutation } from './useAssociateStackToEnvironmentMutation'; import { useAssociateStackToEnvironmentMutation } from './useAssociateStackToEnvironmentMutation';
@@ -192,15 +193,14 @@ describe('useAssociateStackToEnvironmentMutation', () => {
}); });
describe('error handling', () => { describe('error handling', () => {
let consoleError: ReturnType<typeof vi.spyOn>; let restoreConsole: () => void;
beforeEach(() => { beforeEach(() => {
// Suppress console.error for error tests to reduce noise restoreConsole = suppressConsoleLogs();
consoleError = vi.spyOn(console, 'error').mockImplementation(() => {});
}); });
afterEach(() => { afterEach(() => {
consoleError.mockRestore(); restoreConsole();
}); });
it('should handle API error when association fails', async () => { it('should handle API error when association fails', async () => {
@@ -21,6 +21,8 @@ vi.mock('@uirouter/react', async (importOriginal) => ({
})), })),
})); }));
vi.mock('recharts');
const podMetricsSuccess = { const podMetricsSuccess = {
timestamp: '2024-01-01T00:00:00Z', timestamp: '2024-01-01T00:00:00Z',
containers: [ containers: [
@@ -39,6 +41,9 @@ function addBaseHandlers() {
), ),
http.get('/api/endpoints/1/kubernetes/api/v1/nodes/node1', () => http.get('/api/endpoints/1/kubernetes/api/v1/nodes/node1', () =>
HttpResponse.json({ status: { allocatable: { cpu: '4' } } }) HttpResponse.json({ status: { allocatable: { cpu: '4' } } })
),
http.get('/api/kubernetes/1/metrics/pods/namespace/default/my-pod', () =>
HttpResponse.json(podMetricsSuccess)
) )
); );
} }
@@ -60,7 +65,7 @@ function renderComponent() {
} }
describe('ApplicationStatsView', () => { describe('ApplicationStatsView', () => {
it('renders the page header "Application stats"', () => { it('renders the page header "Application stats"', async () => {
server.use( server.use(
http.get('/api/kubernetes/1/metrics/pods/namespace/default/my-pod', () => http.get('/api/kubernetes/1/metrics/pods/namespace/default/my-pod', () =>
HttpResponse.json(podMetricsSuccess) HttpResponse.json(podMetricsSuccess)
@@ -70,6 +75,12 @@ describe('ApplicationStatsView', () => {
renderComponent(); renderComponent();
expect(screen.getByText('Application stats')).toBeInTheDocument(); expect(screen.getByText('Application stats')).toBeInTheDocument();
await waitFor(() => {
expect(
screen.getByRole('combobox', { name: /refresh rate/i })
).toBeInTheDocument();
});
}); });
it('shows "Unable to retrieve container metrics" panel when pod metrics fetch returns 500', async () => { it('shows "Unable to retrieve container metrics" panel when pod metrics fetch returns 500', async () => {
@@ -7,6 +7,7 @@ import {
createMockEnvironment, createMockEnvironment,
createMockQueryResult, createMockQueryResult,
} from '@/react-tools/test-mocks'; } from '@/react-tools/test-mocks';
import { suppressConsoleLogs } from '@/setup-tests/suppress-console';
import { ClusterResourceReservation } from './ClusterResourceReservation'; import { ClusterResourceReservation } from './ClusterResourceReservation';
@@ -169,8 +170,7 @@ describe('ClusterResourceReservation', () => {
http.get('/api/kubernetes/3/metrics/nodes', () => HttpResponse.error()) http.get('/api/kubernetes/3/metrics/nodes', () => HttpResponse.error())
); );
// Mock console.error so test logs are not polluted const restoreConsole = suppressConsoleLogs();
vi.spyOn(console, 'error').mockImplementation(() => {});
renderComponent(); renderComponent();
@@ -205,7 +205,6 @@ describe('ClusterResourceReservation', () => {
) )
).toBeVisible(); ).toBeVisible();
// Restore console.error restoreConsole();
vi.spyOn(console, 'error').mockRestore();
}); });
}); });
@@ -44,7 +44,11 @@ function triggerStateChange(state: ShellState) {
beforeEach(() => { beforeEach(() => {
vi.clearAllMocks(); vi.clearAllMocks();
Object.defineProperty(window, 'location', { Object.defineProperty(window, 'location', {
value: { protocol: 'https:', host: 'localhost:3000' }, value: {
protocol: 'https:',
host: 'localhost:3000',
href: 'https://localhost:3000/',
},
writable: true, writable: true,
}); });
}); });
@@ -60,7 +64,11 @@ describe('KubectlShellView', () => {
it('builds ws:// URL when location is http', () => { it('builds ws:// URL when location is http', () => {
Object.defineProperty(window, 'location', { Object.defineProperty(window, 'location', {
value: { protocol: 'http:', host: 'localhost:3000' }, value: {
protocol: 'http:',
host: 'localhost:3000',
href: 'http://localhost:3000/',
},
writable: true, writable: true,
}); });
renderComponent(); renderComponent();
@@ -15,6 +15,8 @@ vi.mock('@uirouter/react', async (importOriginal) => ({
})), })),
})); }));
vi.mock('recharts');
const nodeMetricsSuccess = { const nodeMetricsSuccess = {
metadata: { creationTimestamp: '2024-01-01T00:00:00Z' }, metadata: { creationTimestamp: '2024-01-01T00:00:00Z' },
usage: { cpu: '250m', memory: '512Mi' }, usage: { cpu: '250m', memory: '512Mi' },
@@ -68,12 +68,16 @@ type RowProps = React.ComponentProps<typeof LinkedServiceAccountsRow>;
function renderRow(props: Partial<RowProps> = {}) { function renderRow(props: Partial<RowProps> = {}) {
return render( return render(
<table>
<tbody>
<LinkedServiceAccountsRowWithQuery <LinkedServiceAccountsRowWithQuery
secretName="my-secret" secretName="my-secret"
namespace="default" namespace="default"
isSystem={false} isSystem={false}
{...props} {...props}
/> />
</tbody>
</table>
); );
} }
@@ -8,6 +8,7 @@ import { UserViewModel } from '@/portainer/models/user';
import { withUserProvider } from '@/react/test-utils/withUserProvider'; import { withUserProvider } from '@/react/test-utils/withUserProvider';
import { mockCodeMirror } from '@/setup-tests/mock-codemirror'; import { mockCodeMirror } from '@/setup-tests/mock-codemirror';
import { mockLocalizeDate } from '@/setup-tests/mock-localizeDate'; import { mockLocalizeDate } from '@/setup-tests/mock-localizeDate';
import { suppressConsoleLogs } from '@/setup-tests/suppress-console';
import { HelmApplicationView } from './HelmApplicationView'; import { HelmApplicationView } from './HelmApplicationView';
@@ -266,8 +267,7 @@ describe('HelmApplicationView', () => {
) )
); );
// Mock console.error to prevent test output pollution const restoreConsole = suppressConsoleLogs();
vi.spyOn(console, 'error').mockImplementation(() => {});
renderComponent(); renderComponent();
@@ -280,8 +280,7 @@ describe('HelmApplicationView', () => {
) )
).toBeInTheDocument(); ).toBeInTheDocument();
// Restore console.error restoreConsole();
vi.spyOn(console, 'error').mockRestore();
}); });
it('should display additional details when available in helm release', async () => { it('should display additional details when available in helm release', async () => {
@@ -68,6 +68,8 @@ type RowProps = React.ComponentProps<typeof ImagePullSecretsRow>;
function renderRow(props: Partial<RowProps> = {}) { function renderRow(props: Partial<RowProps> = {}) {
return render( return render(
<table>
<tbody>
<ImagePullSecretsRowWithQuery <ImagePullSecretsRowWithQuery
namespace="default" namespace="default"
name="my-sa" name="my-sa"
@@ -75,6 +77,8 @@ function renderRow(props: Partial<RowProps> = {}) {
isSystem={false} isSystem={false}
{...props} {...props}
/> />
</tbody>
</table>
); );
} }
@@ -8,6 +8,7 @@ import { withTestRouter } from '@/react/test-utils/withRouter';
import { withUserProvider } from '@/react/test-utils/withUserProvider'; import { withUserProvider } from '@/react/test-utils/withUserProvider';
import { UserViewModel } from '@/portainer/models/user'; import { UserViewModel } from '@/portainer/models/user';
import { server } from '@/setup-tests/server'; import { server } from '@/setup-tests/server';
import { suppressConsoleLogs } from '@/setup-tests/suppress-console';
import { UpdateNamespaceForm } from './UpdateNamespaceForm'; import { UpdateNamespaceForm } from './UpdateNamespaceForm';
@@ -18,6 +19,11 @@ vi.mock('@uirouter/react', async (importOriginal: () => Promise<object>) => ({
useCurrentStateAndParams: vi.fn(() => ({ useCurrentStateAndParams: vi.fn(() => ({
params: { id: NAMESPACE_NAME }, params: { id: NAMESPACE_NAME },
})), })),
useRouter: vi.fn(() => ({
stateService: {
reload: vi.fn(),
},
})),
})); }));
vi.mock('@/react/hooks/useEnvironmentId', () => ({ vi.mock('@/react/hooks/useEnvironmentId', () => ({
@@ -197,6 +203,14 @@ describe('UpdateNamespaceForm', () => {
}); });
describe('error states', () => { describe('error states', () => {
let restoreConsole: () => void;
beforeEach(() => {
restoreConsole = suppressConsoleLogs();
});
afterEach(() => {
restoreConsole();
});
it('should show error alert when namespace query fails', async () => { it('should show error alert when namespace query fails', async () => {
setupDefaultHandlers({ namespaceError: true }); setupDefaultHandlers({ namespaceError: true });
renderComponent(); renderComponent();
@@ -96,7 +96,11 @@ export function PersistentVolumeClaimsDatatable() {
/> />
{editResizeClaim && ( {editResizeClaim && (
<Modal onDismiss={() => setEditResizeClaim(null)} size="md"> <Modal
onDismiss={() => setEditResizeClaim(null)}
size="md"
aria-label="Resize Persistent Volume Claim"
>
<ResizeClaimEditForm <ResizeClaimEditForm
claim={editResizeClaim} claim={editResizeClaim}
onDismiss={() => setEditResizeClaim(null)} onDismiss={() => setEditResizeClaim(null)}
@@ -8,6 +8,7 @@ import { withTestRouter } from '@/react/test-utils/withRouter';
import { withUserProvider } from '@/react/test-utils/withUserProvider'; import { withUserProvider } from '@/react/test-utils/withUserProvider';
import { server } from '@/setup-tests/server'; import { server } from '@/setup-tests/server';
import { createMockEnvironment } from '@/react-tools/test-mocks'; import { createMockEnvironment } from '@/react-tools/test-mocks';
import { suppressConsoleLogs } from '@/setup-tests/suppress-console';
import { CreateGroupView } from './CreateGroupView'; import { CreateGroupView } from './CreateGroupView';
@@ -225,9 +226,7 @@ describe('CreateGroupView', () => {
describe('Error handling', () => { describe('Error handling', () => {
it('should handle API error gracefully', async () => { it('should handle API error gracefully', async () => {
const consoleErrorSpy = vi const restoreConsole = suppressConsoleLogs();
.spyOn(console, 'error')
.mockImplementation(() => {});
const mutationError = vi.fn(); const mutationError = vi.fn();
const errorMessage = 'Failed to create group'; const errorMessage = 'Failed to create group';
@@ -255,7 +254,7 @@ describe('CreateGroupView', () => {
expect(mutationError).toHaveBeenCalled(); expect(mutationError).toHaveBeenCalled();
}); });
consoleErrorSpy.mockRestore(); restoreConsole();
}); });
}); });
@@ -365,7 +364,8 @@ describe('CreateGroupView', () => {
// Environment now appears in the associated list — select its row checkbox and remove it // Environment now appears in the associated list — select its row checkbox and remove it
await screen.findByText('removable-env'); await screen.findByText('removable-env');
const assocCheckboxes = screen.getAllByRole('checkbox'); const assocTable = screen.getByTestId('group-associatedEndpoints');
const assocCheckboxes = within(assocTable).getAllByRole('checkbox');
await user.click(assocCheckboxes[assocCheckboxes.length - 1]); await user.click(assocCheckboxes[assocCheckboxes.length - 1]);
const removeBtn = await screen.findByTestId('remove-environments-button'); const removeBtn = await screen.findByTestId('remove-environments-button');
await waitFor(() => expect(removeBtn).toBeEnabled()); await waitFor(() => expect(removeBtn).toBeEnabled());
@@ -45,11 +45,12 @@ export function CreateGroupView() {
</> </>
); );
async function handleSubmit( function handleSubmit(
values: GroupFormValues, values: GroupFormValues,
{ resetForm }: FormikHelpers<GroupFormValues> { resetForm }: FormikHelpers<GroupFormValues>
) { ): Promise<void> {
await createMutation.mutateAsync( return new Promise((resolve) => {
createMutation.mutate(
{ {
name: values.name, name: values.name,
description: values.description, description: values.description,
@@ -62,7 +63,9 @@ export function CreateGroupView() {
notifySuccess('Success', 'Group successfully created'); notifySuccess('Success', 'Group successfully created');
router.stateService.go('portainer.groups'); router.stateService.go('portainer.groups');
}, },
onSettled: () => resolve(),
} }
); );
});
} }
} }
@@ -262,9 +262,13 @@ describe('EditGroupView', () => {
}); });
describe('Error state', () => { describe('Error state', () => {
// Suppress console logs for error state tests let restoreConsole: () => void;
const restoreConsole = suppressConsoleLogs(); beforeEach(() => {
afterAll(restoreConsole); restoreConsole = suppressConsoleLogs();
});
afterEach(() => {
restoreConsole();
});
it('should show error Alert when group fetch fails', async () => { it('should show error Alert when group fetch fails', async () => {
renderEditGroupView({ groupData: null }); renderEditGroupView({ groupData: null });
@@ -557,6 +561,9 @@ describe('EditGroupView', () => {
}); });
it('should hide the delete button when group data is missing', async () => { it('should hide the delete button when group data is missing', async () => {
// when groupData is null - handler returns 404 and logs an error
const restoreLog = suppressConsoleLogs();
renderEditGroupView({ groupData: null }); renderEditGroupView({ groupData: null });
// Wait for the header error state to appear // Wait for the header error state to appear
@@ -567,6 +574,8 @@ describe('EditGroupView', () => {
expect( expect(
screen.queryByRole('button', { name: /Delete/i }) screen.queryByRole('button', { name: /Delete/i })
).not.toBeInTheDocument(); ).not.toBeInTheDocument();
restoreLog();
}); });
it('should have correct data-cy attribute', async () => { it('should have correct data-cy attribute', async () => {
@@ -14,7 +14,13 @@ import { Datatable } from '@@/datatables';
import { useTableStateWithoutStorage } from '@@/datatables/useTableState'; import { useTableStateWithoutStorage } from '@@/datatables/useTableState';
import { withControlledSelected } from '@@/datatables/extend-options/withControlledSelected'; import { withControlledSelected } from '@@/datatables/extend-options/withControlledSelected';
import { TableRow } from '@@/datatables/TableRow'; import { TableRow } from '@@/datatables/TableRow';
import { Sheet, SheetContent, SheetClose, SheetHeader } from '@@/Sheet'; import {
Sheet,
SheetContent,
SheetClose,
SheetHeader,
SheetDescription,
} from '@@/Sheet';
import { Button, LoadingButton } from '@@/buttons'; import { Button, LoadingButton } from '@@/buttons';
import { EnvironmentTableData } from './types'; import { EnvironmentTableData } from './types';
@@ -119,6 +125,9 @@ export function AddEnvironmentsDrawer({
<SheetContent className="flex flex-col !p-0"> <SheetContent className="flex flex-col !p-0">
<div className="flex-1 overflow-auto p-4"> <div className="flex-1 overflow-auto p-4">
<SheetHeader title="Add environments" /> <SheetHeader title="Add environments" />
<SheetDescription className="sr-only">
Select environments to add to this group.
</SheetDescription>
<Datatable<EnvironmentTableData> <Datatable<EnvironmentTableData>
title="Available environments" title="Available environments"
columns={columns} columns={columns}
@@ -1,4 +1,4 @@
import { render, screen, waitFor } from '@testing-library/react'; import { render, screen, waitFor, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
import { http, HttpResponse } from 'msw'; import { http, HttpResponse } from 'msw';
import { vi } from 'vitest'; import { vi } from 'vitest';
@@ -192,11 +192,11 @@ describe('AssociatedEnvironmentsSelector', () => {
await screen.findByText('Add environments'); await screen.findByText('Add environments');
await screen.findByText('new-env'); await screen.findByText('new-env');
// Select the available env — find the drawer's checkboxes // Select the available env — scope to the drawer table to avoid duplicates
// The drawer table has its own checkboxes after the main table ones const drawerTable = screen.getByTestId('add-environments-drawer-table');
const allCheckboxes = screen.getAllByRole('checkbox'); const drawerCheckboxes = within(drawerTable).getAllByRole('checkbox');
// Last checkbox belongs to the drawer table row // [0] = select-all header, [1] = first row
await user.click(allCheckboxes[allCheckboxes.length - 1]); await user.click(drawerCheckboxes[1]);
// Click the Add button in the drawer footer // Click the Add button in the drawer footer
const confirmAddBtn = screen.getByTestId( const confirmAddBtn = screen.getByTestId(
@@ -35,7 +35,6 @@ export interface GroupFormValues {
interface Props { interface Props {
initialValues: GroupFormValues; initialValues: GroupFormValues;
/** Should return a Promise that resolves when navigation happens (to keep isSubmitting true) */
onSubmit: ( onSubmit: (
values: GroupFormValues, values: GroupFormValues,
helpers: FormikHelpers<GroupFormValues> helpers: FormikHelpers<GroupFormValues>
+12
View File
@@ -0,0 +1,12 @@
import failOnConsole from 'vitest-fail-on-console';
failOnConsole({
shouldFailOnWarn: true,
shouldFailOnError: true,
shouldFailOnLog: true,
shouldFailOnInfo: true,
allowMessage: (message) =>
/Can't perform a React state update on an unmounted component/.test(
message
),
});
+10 -19
View File
@@ -1,4 +1,3 @@
/* eslint-disable no-console */
import { vi } from 'vitest'; import { vi } from 'vitest';
/** /**
@@ -40,25 +39,17 @@ import { vi } from 'vitest';
* @returns A cleanup function to restore the original console methods * @returns A cleanup function to restore the original console methods
*/ */
export function suppressConsoleLogs() { export function suppressConsoleLogs() {
const originalError = console.error; // Use vi.spyOn so the mocks integrate with vitest's mock system
const originalWarn = console.warn; // (compatible with vitest-fail-on-console and vi.restoreAllMocks)
const originalInfo = console.info; const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const originalLog = console.log; const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
const infoSpy = vi.spyOn(console, 'info').mockImplementation(() => {});
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
// 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 () => { return () => {
console.error = originalError; errorSpy.mockRestore();
console.warn = originalWarn; warnSpy.mockRestore();
console.info = originalInfo; infoSpy.mockRestore();
console.log = originalLog; logSpy.mockRestore();
}; };
} }
/* eslint-enable no-console */
+1
View File
@@ -237,6 +237,7 @@
"vite-plugin-svgr": "^4.5.0", "vite-plugin-svgr": "^4.5.0",
"vite-tsconfig-paths": "^4.3.1", "vite-tsconfig-paths": "^4.3.1",
"vitest": "^4.1.8", "vitest": "^4.1.8",
"vitest-fail-on-console": "^0.9.0",
"webpack": "^5.107.2", "webpack": "^5.107.2",
"webpack-build-notifier": "^3.1.0", "webpack-build-notifier": "^3.1.0",
"webpack-bundle-analyzer": "^5.2.0", "webpack-bundle-analyzer": "^5.2.0",
+21
View File
@@ -627,6 +627,9 @@ importers:
vitest: vitest:
specifier: ^4.1.8 specifier: ^4.1.8
version: 4.1.8(@types/node@25.0.3)(@vitest/coverage-v8@4.1.8)(jsdom@24.1.3)(msw@2.14.6(@types/node@25.0.3)(typescript@6.0.2))(vite@8.0.16(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.7.0)(less@4.4.2)(terser@5.44.1)(yaml@1.10.2)) version: 4.1.8(@types/node@25.0.3)(@vitest/coverage-v8@4.1.8)(jsdom@24.1.3)(msw@2.14.6(@types/node@25.0.3)(typescript@6.0.2))(vite@8.0.16(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.7.0)(less@4.4.2)(terser@5.44.1)(yaml@1.10.2))
vitest-fail-on-console:
specifier: ^0.9.0
version: 0.9.0(vite@8.0.16(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.7.0)(less@4.4.2)(terser@5.44.1)(yaml@1.10.2))(vitest@4.1.8)
webpack: webpack:
specifier: ^5.107.2 specifier: ^5.107.2
version: 5.107.2(@swc/core@1.15.11)(cssnano@8.0.1(postcss@8.5.15))(esbuild@0.27.3)(postcss@8.5.15)(webpack-cli@6.0.1) version: 5.107.2(@swc/core@1.15.11)(cssnano@8.0.1(postcss@8.5.15))(esbuild@0.27.3)(postcss@8.5.15)(webpack-cli@6.0.1)
@@ -4306,6 +4309,10 @@ packages:
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
engines: {node: '>=10'} engines: {node: '>=10'}
chalk@5.6.2:
resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==}
engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
character-entities-html4@2.1.0: character-entities-html4@2.1.0:
resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==}
@@ -8635,6 +8642,12 @@ packages:
yaml: yaml:
optional: true optional: true
vitest-fail-on-console@0.9.0:
resolution: {integrity: sha512-GgoS9gSy3ctwGVgFhY6NaAaRE5ve9EmgN9iDpqtEG1Z7Z9Y8x/4GzDrhky5+050Hy0FnndoM/mpmqsmGWywFZw==}
peerDependencies:
vite: '>=4.5.2'
vitest: '>=0.26.2'
vitest@4.1.8: vitest@4.1.8:
resolution: {integrity: sha512-flY6ScbCIt9HThs+C5HS7jvGOB560DJtk/Z15IQROTA6zEy49Nh8T/dofWTQL+n3vswqn87sbJNiuqw1SDp5Ig==} resolution: {integrity: sha512-flY6ScbCIt9HThs+C5HS7jvGOB560DJtk/Z15IQROTA6zEy49Nh8T/dofWTQL+n3vswqn87sbJNiuqw1SDp5Ig==}
engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0}
@@ -12930,6 +12943,8 @@ snapshots:
ansi-styles: 4.3.0 ansi-styles: 4.3.0
supports-color: 7.2.0 supports-color: 7.2.0
chalk@5.6.2: {}
character-entities-html4@2.1.0: {} character-entities-html4@2.1.0: {}
character-entities-legacy@3.0.0: {} character-entities-legacy@3.0.0: {}
@@ -17696,6 +17711,12 @@ snapshots:
terser: 5.44.1 terser: 5.44.1
yaml: 1.10.2 yaml: 1.10.2
vitest-fail-on-console@0.9.0(vite@8.0.16(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.7.0)(less@4.4.2)(terser@5.44.1)(yaml@1.10.2))(vitest@4.1.8):
dependencies:
chalk: 5.6.2
vite: 8.0.16(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.7.0)(less@4.4.2)(terser@5.44.1)(yaml@1.10.2)
vitest: 4.1.8(@types/node@25.0.3)(@vitest/coverage-v8@4.1.8)(jsdom@24.1.3)(msw@2.14.6(@types/node@25.0.3)(typescript@6.0.2))(vite@8.0.16(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.7.0)(less@4.4.2)(terser@5.44.1)(yaml@1.10.2))
vitest@4.1.8(@types/node@25.0.3)(@vitest/coverage-v8@4.1.8)(jsdom@24.1.3)(msw@2.14.6(@types/node@25.0.3)(typescript@6.0.2))(vite@8.0.16(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.7.0)(less@4.4.2)(terser@5.44.1)(yaml@1.10.2)): vitest@4.1.8(@types/node@25.0.3)(@vitest/coverage-v8@4.1.8)(jsdom@24.1.3)(msw@2.14.6(@types/node@25.0.3)(typescript@6.0.2))(vite@8.0.16(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.7.0)(less@4.4.2)(terser@5.44.1)(yaml@1.10.2)):
dependencies: dependencies:
'@vitest/expect': 4.1.8 '@vitest/expect': 4.1.8
+2 -5
View File
@@ -18,6 +18,7 @@ export default defineConfig({
'./app/setup-tests/stub-modules.ts', './app/setup-tests/stub-modules.ts',
'./app/setup-tests/setup.ts', './app/setup-tests/setup.ts',
'./app/setup-tests/setup-codemirror.ts', './app/setup-tests/setup-codemirror.ts',
'./app/setup-tests/setup-fail-on-console.ts',
], ],
coverage: { coverage: {
provider: 'v8', provider: 'v8',
@@ -38,9 +39,5 @@ export default defineConfig({
return !/Can't perform a React state update on an unmounted component/.test(log); return !/Can't perform a React state update on an unmounted component/.test(log);
}, },
}, },
plugins: [ plugins: [svgr({ include: /\?c$/ }), tsconfigPaths(), tsconfigPaths({ projects: ['./tsconfig.generated.json'] })],
svgr({ include: /\?c$/ }),
tsconfigPaths(),
tsconfigPaths({ projects: ['./tsconfig.generated.json'] }),
],
}); });