Migrate tests to Vitest

This commit is contained in:
Mike Cao
2026-05-14 22:05:34 -07:00
parent cbe4d79b3c
commit c0ea3aefbe
15 changed files with 1393 additions and 1839 deletions
-10
View File
@@ -1,10 +0,0 @@
export default {
roots: ['./src'],
testMatch: ['**/__tests__/**/*.+(ts|tsx|js)', '**/?(*.)+(spec|test).+(ts|tsx|js)'],
transform: {
'^.+\\.(ts|tsx)$': 'ts-jest',
},
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
},
};
+8 -5
View File
@@ -39,7 +39,8 @@
"download-language-names": "node scripts/download-language-names.js",
"change-password": "node scripts/change-password.js",
"postbuild": "node scripts/postbuild.js",
"test": "jest",
"test": "vitest run",
"test:watch": "vitest",
"cypress-open": "cypress open cypress run",
"cypress-run": "cypress run cypress run",
"seed-data": "tsx scripts/seed-data.ts",
@@ -126,7 +127,9 @@
"@rollup/plugin-replace": "^5.0.2",
"@rollup/plugin-terser": "^0.4.4",
"@rollup/plugin-typescript": "^12.3.0",
"@types/jest": "^30.0.0",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.1",
"@types/node": "^24.12.0",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.2",
@@ -134,7 +137,7 @@
"cross-env": "^10.1.0",
"cypress": "^13.6.6",
"dotenv-cli": "^11.0.0",
"jest": "^29.7.0",
"jsdom": "^29.1.1",
"postcss": "^8.5.10",
"postcss-flexbugs-fixes": "^5.0.2",
"postcss-import": "^15.1.0",
@@ -148,11 +151,11 @@
"rollup-plugin-peer-deps-external": "^2.2.4",
"rollup-plugin-postcss": "^4.0.2",
"tar": "^7.5.13",
"ts-jest": "^29.4.6",
"ts-morph": "^27.0.2",
"ts-node": "^10.9.1",
"tsup": "^8.5.0",
"tsx": "^4.19.0",
"typescript": "^5.9.3"
"typescript": "^5.9.3",
"vitest": "^4.1.6"
}
}
+1146 -1812
View File
File diff suppressed because it is too large Load Diff
+15
View File
@@ -0,0 +1,15 @@
import { expect, test } from 'vitest';
import { render, screen } from '@/test/render';
import { Empty } from './Empty';
test('renders the default empty state message', () => {
render(<Empty />);
expect(screen.getByText('No data available.')).toBeInTheDocument();
});
test('renders a custom empty state message', () => {
render(<Empty message="Nothing matched the current filters." />);
expect(screen.getByText('Nothing matched the current filters.')).toBeInTheDocument();
});
@@ -1,11 +1,9 @@
import {
BOARD_ENTITY_TYPES,
isBoardComponentSupported,
} from '../boards';
import { expect, test } from 'vitest';
import {
BOARD_COMPONENT_COMPATIBILITY_MATRIX,
getSupportedBoardComponentEntityTypes,
} from '../boardComponentCompatibility';
} from './boardComponentCompatibility';
import { BOARD_ENTITY_TYPES, isBoardComponentSupported } from './boards';
test('isBoardComponentSupported allows events chart on website boards', () => {
expect(isBoardComponentSupported('EventsChart', BOARD_ENTITY_TYPES.website)).toBe(true);
@@ -1,4 +1,5 @@
import { renderNumberLabels } from '../charts';
import { describe, expect, test } from 'vitest';
import { renderNumberLabels } from './charts';
// test for renderNumberLabels
@@ -1,7 +1,7 @@
import { getIpAddress } from '../ip';
import { expect, test } from 'vitest';
import { getIpAddress } from './ip';
const IP = '127.0.0.1';
const BAD_IP = '127.127.127.127';
test('getIpAddress: Custom header', () => {
process.env.CLIENT_IP_HEADER = 'x-custom-ip-header';
@@ -1,4 +1,5 @@
import * as format from '../format';
import { expect, test } from 'vitest';
import * as format from './format';
test('parseTime', () => {
expect(format.parseTime(86400 + 3600 + 60 + 1)).toEqual({
@@ -1,5 +1,6 @@
import { HOMEPAGE_URL } from '../constants';
import { getBaseUrl } from '../get-base-url';
import { expect, test } from 'vitest';
import { HOMEPAGE_URL } from './constants';
import { getBaseUrl } from './get-base-url';
function createHeaders(entries: Record<string, string>) {
return {
@@ -1,4 +1,5 @@
import { matchesConfiguredPath } from '../match-configured-path';
import { expect, test } from 'vitest';
import { matchesConfiguredPath } from './match-configured-path';
test('matches the exact configured path', () => {
expect(matchesConfiguredPath('/d.js', 'd.js')).toBe(true);
+11
View File
@@ -0,0 +1,11 @@
# Test Convention
Use Vitest for unit and component tests. Cypress remains the end-to-end test runner.
- Place tests next to the code they cover as `*.test.ts` or `*.test.tsx`.
- Import Vitest APIs explicitly: `import { describe, expect, test, vi } from 'vitest';`.
- Use `test`, not `it`.
- React component tests should import from `@/test/render`.
- Prefer accessible Testing Library queries such as `getByRole`, `getByLabelText`, and `getByText`.
- Use `getByTestId` only when there is no useful accessible query. The test id attribute is `data-test`.
- Keep test doubles in the test file unless they are shared framework concerns, such as Next navigation.
+43
View File
@@ -0,0 +1,43 @@
import { vi } from 'vitest';
const testNavigation = vi.hoisted(() => ({
pathname: '/',
searchParams: new URLSearchParams(),
router: {
back: vi.fn(),
forward: vi.fn(),
prefetch: vi.fn(),
push: vi.fn(),
refresh: vi.fn(),
replace: vi.fn(),
},
}));
export function setTestUrl(url: string) {
const nextUrl = new URL(url, 'http://localhost');
testNavigation.pathname = nextUrl.pathname;
testNavigation.searchParams = nextUrl.searchParams;
window.history.pushState({}, '', `${nextUrl.pathname}${nextUrl.search}${nextUrl.hash}`);
}
export function getTestRouter() {
return testNavigation.router;
}
export function resetTestNavigation() {
setTestUrl('/');
Object.values(testNavigation.router).forEach(mock => {
mock.mockReset();
});
}
vi.mock('next/navigation', () => ({
notFound: vi.fn(),
redirect: vi.fn(),
usePathname: () => testNavigation.pathname,
useRouter: () => testNavigation.router,
useSearchParams: () => testNavigation.searchParams,
}));
+83
View File
@@ -0,0 +1,83 @@
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import {
type RenderOptions,
screen,
render as testingLibraryRender,
waitFor,
within,
} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { RouterProvider, ZenProvider } from '@umami/react-zen';
import { NextIntlClientProvider } from 'next-intl';
import type { ReactElement, ReactNode } from 'react';
import enUS from '../../public/intl/messages/en-US.json';
import { setTestUrl } from './navigation';
type TestRenderOptions = Omit<RenderOptions, 'wrapper'> & {
locale?: string;
messages?: Record<string, unknown>;
queryClient?: QueryClient;
route?: string;
};
export function createTestQueryClient() {
return new QueryClient({
defaultOptions: {
queries: {
retry: false,
refetchOnWindowFocus: false,
staleTime: 1000 * 60,
},
},
});
}
function TestProviders({
children,
locale = 'en-US',
messages = enUS,
queryClient = createTestQueryClient(),
}: {
children: ReactNode;
locale?: string;
messages?: Record<string, unknown>;
queryClient?: QueryClient;
}) {
return (
<ZenProvider>
<RouterProvider navigate={url => window.history.pushState({}, '', url)}>
<NextIntlClientProvider locale={locale} messages={messages} onError={() => null}>
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
</NextIntlClientProvider>
</RouterProvider>
</ZenProvider>
);
}
export function render(
ui: ReactElement,
{
locale = 'en-US',
messages = enUS,
queryClient = createTestQueryClient(),
route = '/',
...options
}: TestRenderOptions = {},
) {
setTestUrl(route);
return {
queryClient,
user: userEvent.setup(),
...testingLibraryRender(ui, {
wrapper: ({ children }) => (
<TestProviders locale={locale} messages={messages} queryClient={queryClient}>
{children}
</TestProviders>
),
...options,
}),
};
}
export { screen, userEvent, waitFor, within };
+58
View File
@@ -0,0 +1,58 @@
import '@testing-library/jest-dom/vitest';
import { cleanup, configure } from '@testing-library/react';
import { afterEach, vi } from 'vitest';
import { resetTestNavigation } from './navigation';
configure({ testIdAttribute: 'data-test' });
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: vi.fn().mockImplementation((query: string) => ({
matches: false,
media: query,
onchange: null,
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
addListener: vi.fn(),
removeListener: vi.fn(),
dispatchEvent: vi.fn(),
})),
});
Object.defineProperty(navigator, 'clipboard', {
configurable: true,
value: {
readText: vi.fn(),
writeText: vi.fn(),
},
});
window.scrollTo = vi.fn();
class ResizeObserver {
observe() {}
unobserve() {}
disconnect() {}
}
class IntersectionObserver {
readonly root = null;
readonly rootMargin = '';
readonly thresholds = [];
observe() {}
unobserve() {}
disconnect() {}
takeRecords() {
return [];
}
}
window.ResizeObserver = ResizeObserver;
window.IntersectionObserver = IntersectionObserver;
afterEach(() => {
cleanup();
resetTestNavigation();
vi.restoreAllMocks();
});
+15
View File
@@ -0,0 +1,15 @@
import { fileURLToPath } from 'node:url';
import { defineConfig } from 'vitest/config';
export default defineConfig({
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
test: {
environment: 'jsdom',
include: ['src/**/*.test.{ts,tsx}'],
setupFiles: ['./src/test/setup.ts'],
},
});