Fix IntersectionObserver mock for updated lib.dom.d.ts
This commit is contained in:
Generated
+555
-1903
File diff suppressed because it is too large
Load Diff
@@ -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,57 +0,0 @@
|
||||
import { expect, test } from 'vitest';
|
||||
import { getApiUrl } from '../api-url';
|
||||
|
||||
test('uses the default api path', () => {
|
||||
expect(getApiUrl('/websites', { apiUrl: '', basePath: '' })).toBe('/api/websites');
|
||||
});
|
||||
|
||||
test('uses basePath with the default api path', () => {
|
||||
expect(getApiUrl('/websites', { apiUrl: '', basePath: '/analytics' })).toBe(
|
||||
'/analytics/api/websites',
|
||||
);
|
||||
});
|
||||
|
||||
test('routes calls through a relative API_URL', () => {
|
||||
expect(getApiUrl('/websites', { apiUrl: '/backend/api', basePath: '' })).toBe(
|
||||
'/backend/api/websites',
|
||||
);
|
||||
});
|
||||
|
||||
test('routes calls through basePath with a relative API_URL', () => {
|
||||
expect(getApiUrl('/websites', { apiUrl: '/backend/api', basePath: '/analytics' })).toBe(
|
||||
'/analytics/backend/api/websites',
|
||||
);
|
||||
});
|
||||
|
||||
test('routes calls through an absolute API_URL', () => {
|
||||
expect(getApiUrl('/websites', { apiUrl: 'https://api.example.com/api', basePath: '' })).toBe(
|
||||
'https://api.example.com/api/websites',
|
||||
);
|
||||
});
|
||||
|
||||
test('leaves absolute request URLs unchanged', () => {
|
||||
expect(getApiUrl('https://example.com/custom', { apiUrl: '/backend/api', basePath: '' })).toBe(
|
||||
'https://example.com/custom',
|
||||
);
|
||||
});
|
||||
|
||||
test('keeps /auth/* on the default api path', () => {
|
||||
expect(
|
||||
getApiUrl('/auth/login', { apiUrl: 'https://api.example.com/api', basePath: '' }),
|
||||
).toBe('/api/auth/login');
|
||||
});
|
||||
|
||||
test('keeps /config on the default api path', () => {
|
||||
expect(getApiUrl('/config', { apiUrl: 'https://api.example.com/api', basePath: '' })).toBe(
|
||||
'/api/config',
|
||||
);
|
||||
});
|
||||
|
||||
test('keeps /auth/* on the default api path with basePath', () => {
|
||||
expect(
|
||||
getApiUrl('/auth/verify', {
|
||||
apiUrl: 'https://api.example.com/api',
|
||||
basePath: '/analytics',
|
||||
}),
|
||||
).toBe('/analytics/api/auth/verify');
|
||||
});
|
||||
@@ -1,246 +0,0 @@
|
||||
import { DATA_TYPE } from '../constants';
|
||||
import {
|
||||
flattenJSON,
|
||||
createKeyValue,
|
||||
isValidDateValue,
|
||||
getDataType,
|
||||
getStringValue,
|
||||
objectToArray,
|
||||
type KeyValueData,
|
||||
} from '../data';
|
||||
|
||||
describe('isValidDateValue', () => {
|
||||
test.each([
|
||||
['2024-01-15T10:30:00Z', true],
|
||||
['2024-01-15T10:30:00.123Z', true],
|
||||
['2024-01-15T10:30:00+02:00', true],
|
||||
['not-a-date', false],
|
||||
['2024/01/15', false],
|
||||
['', false],
|
||||
])('validates datetime strings correctly (%s → %s)', (input, expected) => {
|
||||
expect(isValidDateValue(input)).toBe(expected);
|
||||
});
|
||||
|
||||
test('returns false for non-string values', () => {
|
||||
expect(isValidDateValue(123 as any)).toBe(false);
|
||||
expect(isValidDateValue(null as any)).toBe(false);
|
||||
expect(isValidDateValue(undefined as any)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDataType', () => {
|
||||
test.each([
|
||||
['string', 'string'],
|
||||
[123, 'number'],
|
||||
[true, 'boolean'],
|
||||
[null, 'object'],
|
||||
[[], 'object'],
|
||||
[{}, 'object'],
|
||||
['2024-01-15T10:30:00Z', 'date'],
|
||||
])('detects type correctly (%s → %s)', (input, expected) => {
|
||||
expect(getDataType(input)).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createKeyValue', () => {
|
||||
test('handles string values', () => {
|
||||
const result = createKeyValue('name', 'test');
|
||||
expect(result).toEqual({
|
||||
key: 'name',
|
||||
value: 'test',
|
||||
dataType: DATA_TYPE.string,
|
||||
});
|
||||
});
|
||||
|
||||
test('handles number values', () => {
|
||||
const result = createKeyValue('count', 42);
|
||||
expect(result).toEqual({
|
||||
key: 'count',
|
||||
value: 42,
|
||||
dataType: DATA_TYPE.number,
|
||||
});
|
||||
});
|
||||
|
||||
test('handles boolean values and converts to string', () => {
|
||||
expect(createKeyValue('active', true)).toEqual({
|
||||
key: 'active',
|
||||
value: 'true',
|
||||
dataType: DATA_TYPE.boolean,
|
||||
});
|
||||
expect(createKeyValue('active', false)).toEqual({
|
||||
key: 'active',
|
||||
value: 'false',
|
||||
dataType: DATA_TYPE.boolean,
|
||||
});
|
||||
});
|
||||
|
||||
test('handles date strings', () => {
|
||||
const dateStr = '2024-01-15T10:30:00Z';
|
||||
const result = createKeyValue('timestamp', dateStr);
|
||||
expect(result).toEqual({
|
||||
key: 'timestamp',
|
||||
value: dateStr,
|
||||
dataType: DATA_TYPE.date,
|
||||
});
|
||||
});
|
||||
|
||||
test('handles arrays and converts to JSON string', () => {
|
||||
const arr = [1, 2, 3];
|
||||
const result = createKeyValue('items', arr);
|
||||
expect(result).toEqual({
|
||||
key: 'items',
|
||||
value: '[1,2,3]',
|
||||
dataType: DATA_TYPE.array,
|
||||
});
|
||||
});
|
||||
|
||||
test('handles null values', () => {
|
||||
const result = createKeyValue('nullable', null);
|
||||
expect(result.dataType).toBe(DATA_TYPE.array);
|
||||
expect(result.value).toBe('null');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getStringValue', () => {
|
||||
test('formats number values with 4 decimal places', () => {
|
||||
expect(getStringValue('42', DATA_TYPE.number)).toBe('42.0000');
|
||||
expect(getStringValue('3.14159', DATA_TYPE.number)).toBe('3.1416');
|
||||
});
|
||||
|
||||
test('converts date values to ISO string', () => {
|
||||
const result = getStringValue('2024-01-15T10:30:00', DATA_TYPE.date);
|
||||
expect(result).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/);
|
||||
});
|
||||
|
||||
test('returns string values as-is for other types', () => {
|
||||
expect(getStringValue('test', DATA_TYPE.string)).toBe('test');
|
||||
expect(getStringValue('true', DATA_TYPE.boolean)).toBe('true');
|
||||
});
|
||||
});
|
||||
|
||||
describe('objectToArray', () => {
|
||||
test('converts object values to array', () => {
|
||||
const obj = { a: 1, b: 2, c: 3 };
|
||||
const result = objectToArray(obj);
|
||||
expect(result).toEqual([1, 2, 3]);
|
||||
});
|
||||
|
||||
test('returns empty array for empty object', () => {
|
||||
expect(objectToArray({})).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('flattenJSON', () => {
|
||||
test('flattens simple object', () => {
|
||||
const input = {
|
||||
name: 'test',
|
||||
count: 42,
|
||||
};
|
||||
|
||||
const result = flattenJSON(input);
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result).toContainEqual(expect.objectContaining({ key: 'name', value: 'test' }));
|
||||
expect(result).toContainEqual(expect.objectContaining({ key: 'count', value: 42 }));
|
||||
});
|
||||
|
||||
test('flattens nested object with dot notation', () => {
|
||||
const input = {
|
||||
user: {
|
||||
name: 'John',
|
||||
age: 30,
|
||||
},
|
||||
};
|
||||
|
||||
const result = flattenJSON(input);
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result).toContainEqual(expect.objectContaining({ key: 'user.name', value: 'John' }));
|
||||
expect(result).toContainEqual(expect.objectContaining({ key: 'user.age', value: 30 }));
|
||||
});
|
||||
|
||||
test('flattens deeply nested object', () => {
|
||||
const input = {
|
||||
level1: {
|
||||
level2: {
|
||||
level3: {
|
||||
value: 'deep',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = flattenJSON(input);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].key).toBe('level1.level2.level3.value');
|
||||
expect(result[0].value).toBe('deep');
|
||||
});
|
||||
|
||||
test('treats arrays as leaf values (converts to JSON string)', () => {
|
||||
const input = {
|
||||
tags: ['a', 'b', 'c'],
|
||||
};
|
||||
|
||||
const result = flattenJSON(input);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].key).toBe('tags');
|
||||
expect(result[0].value).toBe('["a","b","c"]');
|
||||
expect(result[0].dataType).toBe(DATA_TYPE.array);
|
||||
});
|
||||
|
||||
test('treats date strings as leaf values', () => {
|
||||
const input = {
|
||||
createdAt: '2024-01-15T10:30:00Z',
|
||||
};
|
||||
|
||||
const result = flattenJSON(input);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].key).toBe('createdAt');
|
||||
expect(result[0].dataType).toBe(DATA_TYPE.date);
|
||||
});
|
||||
|
||||
test('handles mixed nested and flat structure', () => {
|
||||
const input = {
|
||||
id: '123',
|
||||
metadata: {
|
||||
timestamp: '2024-01-15T10:30:00Z',
|
||||
source: 'web',
|
||||
details: {
|
||||
referrer: 'google',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = flattenJSON(input);
|
||||
const keys = result.map(r => r.key);
|
||||
|
||||
expect(result).toHaveLength(4);
|
||||
expect(keys).toContain('id');
|
||||
expect(keys).toContain('metadata.timestamp');
|
||||
expect(keys).toContain('metadata.source');
|
||||
expect(keys).toContain('metadata.details.referrer');
|
||||
});
|
||||
|
||||
test('returns empty array for empty object', () => {
|
||||
expect(flattenJSON({})).toEqual([]);
|
||||
});
|
||||
|
||||
test('converts boolean values to strings', () => {
|
||||
const input = {
|
||||
isActive: true,
|
||||
isAdmin: false,
|
||||
};
|
||||
|
||||
const result = flattenJSON(input);
|
||||
|
||||
expect(result).toContainEqual(
|
||||
expect.objectContaining({ key: 'isActive', value: 'true', dataType: DATA_TYPE.boolean }),
|
||||
);
|
||||
expect(result).toContainEqual(
|
||||
expect.objectContaining({ key: 'isAdmin', value: 'false', dataType: DATA_TYPE.boolean }),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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 {
|
||||
+2
-1
@@ -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);
|
||||
@@ -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.
|
||||
@@ -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,
|
||||
}));
|
||||
@@ -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 };
|
||||
+53
-9
@@ -1,15 +1,59 @@
|
||||
import '@testing-library/jest-dom/vitest';
|
||||
import { afterAll, afterEach, beforeAll } from 'vitest';
|
||||
import { server } from './msw/server';
|
||||
import { cleanup, configure } from '@testing-library/react';
|
||||
import { afterEach, vi } from 'vitest';
|
||||
import { resetTestNavigation } from './navigation';
|
||||
|
||||
beforeAll(() => {
|
||||
server.listen({ onUnhandledRequest: 'error' });
|
||||
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 scrollMargin = '';
|
||||
readonly thresholds = [];
|
||||
|
||||
observe() {}
|
||||
unobserve() {}
|
||||
disconnect() {}
|
||||
takeRecords() {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
window.ResizeObserver = ResizeObserver;
|
||||
window.IntersectionObserver = IntersectionObserver;
|
||||
|
||||
afterEach(() => {
|
||||
server.resetHandlers();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
server.close();
|
||||
cleanup();
|
||||
resetTestNavigation();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
+5
-12
@@ -2,21 +2,14 @@ import { fileURLToPath } from 'node:url';
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
environment: 'jsdom',
|
||||
exclude: [
|
||||
'**/node_modules/**',
|
||||
'**/tests/e2e/**',
|
||||
'**/playwright-report/**',
|
||||
'**/test-results/**',
|
||||
],
|
||||
globals: true,
|
||||
include: ['src/**/*.{test,spec}.{ts,tsx,js,jsx}'],
|
||||
setupFiles: ['./src/test/setup.ts'],
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||
},
|
||||
},
|
||||
test: {
|
||||
environment: 'jsdom',
|
||||
include: ['src/**/*.test.{ts,tsx}'],
|
||||
setupFiles: ['./src/test/setup.ts'],
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user