Browse Source

feat: migrate Jest to Vitest (#11841)

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
pull/11844/head
afc163 4 months ago
committed by GitHub
parent
commit
7461b1bb83
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 36
      jest.config.ts
  2. 18993
      package-lock.json
  3. 17
      package.json
  4. 1
      tests/__mocks__/mermaid.js
  5. 1
      tests/__mocks__/mermaid.ts
  6. 1
      tests/__mocks__/raw.js
  7. 1
      tests/__mocks__/raw.ts
  8. 118
      tests/setupTests.jsx
  9. 104
      tests/setupTests.ts
  10. 2
      tsconfig.json
  11. 39
      vitest.config.ts

36
jest.config.ts

@ -1,36 +0,0 @@
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import { configUmiAlias, createConfig } from '@umijs/max/test.js';
const readPkgVersion = (pkg: string) =>
JSON.parse(readFileSync(join('node_modules', pkg, 'package.json'), 'utf-8'))
.version;
export default async (): Promise<any> => {
const config = await configUmiAlias({
...createConfig({
target: 'browser',
}),
});
return {
...config,
testPathIgnorePatterns: ['/node_modules/', '/.worktrees/'],
moduleNameMapper: {
'\\.md$': '<rootDir>/tests/__mocks__/raw.js',
...(config.moduleNameMapper || {}),
'^mermaid$': '<rootDir>/tests/__mocks__/mermaid.js',
},
testEnvironmentOptions: {
...(config?.testEnvironmentOptions || {}),
url: 'http://localhost:8000',
},
setupFiles: [...(config.setupFiles || []), './tests/setupTests.jsx'],
globals: {
...config.globals,
localStorage: null,
__APP_VERSION__: 'test',
__UMI_VERSION__: readPkgVersion('@umijs/max'),
__UTOO_VERSION__: readPkgVersion('@utoo/pack'),
},
};
};

18993
package-lock.json

File diff suppressed because it is too large

17
package.json

@ -24,9 +24,10 @@
"start:no-mock": "cross-env MOCK=none max dev",
"start:pre": "cross-env UMI_ENV=pre MOCK=none max dev",
"start:test": "cross-env UMI_ENV=test MOCK=none max dev",
"test": "jest",
"test:coverage": "jest --coverage",
"test:update": "jest -u",
"test": "vitest run",
"test:coverage": "vitest run --coverage",
"test:watch": "vitest",
"test:ui": "vitest --ui",
"doctor": "react-doctor .",
"tsc": "tsc --noEmit"
},
@ -62,8 +63,9 @@
"@testing-library/react": "^16.3.2",
"@types/d3": "^7.4.3",
"@types/express": "^5.0.6",
"@types/jest": "^30.0.0",
"@types/node": "^25.9.1",
"@vitest/coverage-v8": "3.2.4",
"@vitest/ui": "3.2.4",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@types/react-helmet": "^6.1.11",
@ -77,16 +79,17 @@
"express": "^5.2.1",
"geojson": "^0.5.0",
"gh-pages": "^6.3.0",
"jsdom": "^26.0.0",
"happy-dom": "^20.0.0",
"husky": "^9.1.7",
"jest": "^30.4.1",
"jest-environment-jsdom": "^30.4.1",
"lint-staged": "^17.0.2",
"mockjs": "^1.1.0",
"react-doctor": "^0.2.1",
"swagger-ui-dist": "^5.32.5",
"tailwindcss": "^4.2.4",
"ts-node": "^10.9.2",
"typescript": "^6.0.3"
"typescript": "^6.0.3",
"vitest": "3.2.4"
},
"engines": {
"node": ">=20.0.0"

1
tests/__mocks__/mermaid.js

@ -1 +0,0 @@
module.exports = {};

1
tests/__mocks__/mermaid.ts

@ -0,0 +1 @@
export default {};

1
tests/__mocks__/raw.js

@ -1 +0,0 @@
module.exports = 'mock-md-content';

1
tests/__mocks__/raw.ts

@ -0,0 +1 @@
export default 'mock-md-content';

118
tests/setupTests.jsx

@ -1,118 +0,0 @@
import { defaultConfig } from 'antd/lib/theme/internal';
defaultConfig.hashed = false;
const store = {};
const localStorageMock = {
getItem: jest.fn((key) => store[key] ?? null),
setItem: jest.fn((key, value) => {
store[key] = String(value);
}),
removeItem: jest.fn((key) => {
delete store[key];
}),
clear: jest.fn(() => {
Object.keys(store).forEach((key) => {
delete store[key];
});
}),
};
globalThis.localStorage = localStorageMock;
Object.defineProperty(URL, 'createObjectURL', {
writable: true,
value: jest.fn(),
});
class MockWorker {
constructor(stringUrl) {
this.url = stringUrl;
this.onmessage = null;
}
postMessage(msg) {
if (typeof this.onmessage === 'function') {
this.onmessage({ data: msg });
}
}
terminate() {}
addEventListener() {}
removeEventListener() {}
}
globalThis.Worker = MockWorker;
if (typeof globalThis.MessageChannel === 'undefined') {
class PolyMessageChannel {
constructor() {
const channel = this;
this.port1 = {
onmessage: null,
postMessage(msg) {
setTimeout(() => {
if (typeof channel.port2.onmessage === 'function') {
channel.port2.onmessage({ data: msg });
}
}, 0);
},
};
this.port2 = {
onmessage: null,
postMessage(msg) {
setTimeout(() => {
if (typeof channel.port1.onmessage === 'function') {
channel.port1.onmessage({ data: msg });
}
}, 0);
},
};
}
}
globalThis.MessageChannel = PolyMessageChannel;
}
if (typeof window !== 'undefined' && !window.matchMedia) {
Object.defineProperty(window, 'matchMedia', {
writable: true,
configurable: true,
value: jest.fn((query) => ({
matches: false,
media: query,
onchange: null,
addListener: jest.fn(),
removeListener: jest.fn(),
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
})),
});
}
const originalError = console.error;
Object.defineProperty(window.console, 'error', {
writable: true,
configurable: true,
value: (...rest) => {
const logStr = rest.join('');
if (
logStr.includes(
'Warning: An update to %s inside a test was not wrapped in act(...)',
)
) {
return;
}
originalError(...rest);
},
});
globalThis.ResizeObserver = class {
observe() {}
unobserve() {}
disconnect() {}
};

104
tests/setupTests.ts

@ -0,0 +1,104 @@
import { vi } from 'vitest';
const store: Record<string, string> = {};
const localStorageMock = {
getItem: vi.fn((key: string) => store[key] ?? null),
setItem: vi.fn((key: string, value: string) => {
store[key] = String(value);
}),
removeItem: vi.fn((key: string) => {
delete store[key];
}),
clear: vi.fn(() => {
Object.keys(store).forEach((key) => {
delete store[key];
});
}),
};
globalThis.localStorage = localStorageMock as unknown as Storage;
Object.defineProperty(URL, 'createObjectURL', {
writable: true,
value: vi.fn(),
});
class MockWorker {
url: string;
onmessage: ((e: MessageEvent) => void) | null = null;
constructor(stringUrl: string) {
this.url = stringUrl;
}
postMessage(msg: unknown) {
if (typeof this.onmessage === 'function') {
this.onmessage({ data: msg } as MessageEvent);
}
}
terminate() {}
addEventListener() {}
removeEventListener() {}
}
globalThis.Worker = MockWorker as unknown as typeof Worker;
if (typeof globalThis.MessageChannel === 'undefined') {
class PolyMessageChannel {
port1: {
onmessage: ((e: MessageEvent) => void) | null;
postMessage: (msg: unknown) => void;
};
port2: {
onmessage: ((e: MessageEvent) => void) | null;
postMessage: (msg: unknown) => void;
};
constructor() {
const channel = this;
this.port1 = {
onmessage: null,
postMessage(msg: unknown) {
setTimeout(() => {
if (typeof channel.port2.onmessage === 'function') {
channel.port2.onmessage({ data: msg } as MessageEvent);
}
}, 0);
},
};
this.port2 = {
onmessage: null,
postMessage(msg: unknown) {
setTimeout(() => {
if (typeof channel.port1.onmessage === 'function') {
channel.port1.onmessage({ data: msg } as MessageEvent);
}
}, 0);
},
};
}
}
globalThis.MessageChannel =
PolyMessageChannel as unknown as typeof MessageChannel;
}
if (typeof window !== 'undefined' && !window.matchMedia) {
Object.defineProperty(window, 'matchMedia', {
writable: true,
configurable: true,
value: vi.fn((query: string) => ({
matches: false,
media: query,
onchange: null,
addListener: vi.fn(),
removeListener: vi.fn(),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
})),
});
}
globalThis.ResizeObserver = class {
observe() {}
unobserve() {}
disconnect() {}
};

2
tsconfig.json

@ -11,7 +11,7 @@
"declaration": true,
"skipLibCheck": true,
"resolveJsonModule": true,
"types": ["jest", "node"],
"types": ["vitest/globals", "node"],
"paths": {
"@/*": ["./src/*"],
"@@/*": ["./src/.umi/*"],

39
vitest.config.ts

@ -0,0 +1,39 @@
import { join } from 'node:path';
import { defineConfig } from 'vitest/config';
export default defineConfig({
resolve: {
alias: {
'@': join(__dirname, 'src'),
'@root': join(__dirname),
'@@': join(__dirname, 'src', '.umi'),
},
},
test: {
environment: 'happy-dom',
globals: true,
setupFiles: ['./tests/setupTests.ts'],
include: ['src/**/*.{test,spec}.{ts,tsx}'],
// Exclude Umi integration tests that depend on @umijs/max test infrastructure
// These require Umi's Jest runner and cannot be used with Vitest directly
exclude: [
'src/pages/user/login/login.test.tsx',
'node_modules',
'dist',
'.umi',
],
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
include: ['src/**/*.{ts,tsx}'],
exclude: [
'src/.umi/**',
'src/services/ant-design-pro/**',
'src/**/*.d.ts',
'src/**/index.style.ts',
],
},
passWithNoTests: true,
testTimeout: 15000,
},
});
Loading…
Cancel
Save