Skip to content

Commit d9b3ffa

Browse files
committed
chore(workflow): migrate unit test runner from vitest to rstest
Replace vitest with @rstest/core across all packages and apps. Each project gains an rstest.config.ts; shared boilerplate (coverage, dotenv, jsdom storage polyfill, style stubs, photon externals, __VERSION__ define) is factored into scripts/rstest-*.ts. Two rstest behaviors required workarounds, each tracked upstream: - apps/studio launch-electron: rstest mocks are an rspack build-time transform, so import(variable) escapes them and would spawn a real Electron process. Use string-literal imports. (web-infra-dev/rstest#1454) - apps/studio updater-handlers: rstest externalizes node-env deps by default and still evaluates an externalized module even when mocked, so the real electron-updater crashed at import. Bundle all deps via output.bundleDependencies. (web-infra-dev/rstest#1456) core merge-browser-parse: replace the brittle absolute 15MB cap on the merged report with a guard relative to a single report, so it tracks the report-template size instead of breaking as the viewer bundle grows. Validation: pnpm run lint; full test:coverage (13 projects, 2328 tests) all green. Claude-Session: https://claude.ai/code/session_011FdpdFgXxUwWgRxpALzfoE
1 parent 34d7cd4 commit d9b3ffa

466 files changed

Lines changed: 5926 additions & 6761 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ MIDSCENE_MODEL_NAME="qwen3-vl-plus"
181181

182182
If you've fixed a bug or added code that should be tested, then add some tests.
183183

184-
You can add unit test cases in the `<PACKAGE_DIR>/tests` folder. The test runner is based on [Vitest](https://vitest.dev/).
184+
You can add unit test cases in the `<PACKAGE_DIR>/tests` folder. The test runner is based on [Rstest](https://rstest.rs/).
185185

186186
### Run Unit Tests
187187

apps/android-playground/package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@
1616
},
1717
"devDependencies": {
1818
"@rsbuild/core": "^1.6.15",
19-
"@rsbuild/plugin-less": "^1.5.0",
20-
"@rsbuild/plugin-node-polyfill": "1.4.2",
21-
"@rsbuild/plugin-react": "^1.4.1",
22-
"@rsbuild/plugin-svgr": "^1.2.2",
19+
"@rsbuild/plugin-less": "^1.6.4",
20+
"@rsbuild/plugin-node-polyfill": "1.4.6",
21+
"@rsbuild/plugin-react": "^1.4.6",
22+
"@rsbuild/plugin-svgr": "^1.3.1",
2323
"@rsbuild/plugin-type-check": "^1.3.2",
2424
"@types/react": "^18.3.1",
2525
"@types/react-dom": "^18.3.1",

apps/chrome-extension/package.json

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"dev:simple": "rsbuild dev --open",
1010
"preview": "rsbuild preview",
1111
"pack-extension": "node scripts/pack-extension.js",
12-
"test": "vitest run"
12+
"test": "rstest"
1313
},
1414
"dependencies": {
1515
"@ant-design/icons": "^5.3.1",
@@ -34,24 +34,25 @@
3434
},
3535
"devDependencies": {
3636
"@rsbuild/core": "^1.6.15",
37-
"rimraf": "~3.0.2",
38-
"@rsbuild/plugin-less": "^1.5.0",
39-
"@rsbuild/plugin-node-polyfill": "1.4.2",
40-
"@rsbuild/plugin-react": "^1.4.1",
41-
"@rsbuild/plugin-svgr": "^1.2.2",
37+
"@rsbuild/plugin-less": "^1.6.4",
38+
"@rsbuild/plugin-node-polyfill": "1.4.6",
39+
"@rsbuild/plugin-react": "^1.4.6",
40+
"@rsbuild/plugin-svgr": "^1.3.1",
4241
"@rsbuild/plugin-type-check": "^1.3.2",
42+
"@rstest/core": "0.10.6",
4343
"@tailwindcss/postcss": "4.1.11",
4444
"@types/chrome": "0.0.279",
4545
"@types/react": "^18.3.1",
4646
"@types/react-dom": "^18.3.1",
4747
"archiver": "^6.0.0",
4848
"concurrently": "^8.2.0",
49+
"jsdom": "29.0.2",
4950
"less": "^4.2.0",
5051
"openai": "6.3.0",
52+
"rimraf": "~3.0.2",
5153
"rsbuild-plugin-workspace-dev": "0.0.1",
5254
"tailwindcss": "4.1.11",
5355
"typescript": "^5.8.3",
54-
"vitest": "3.0.5",
5556
"web-ext": "9.0.0"
5657
}
5758
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import path from 'node:path';
2+
import { pluginReact } from '@rsbuild/plugin-react';
3+
import { defineConfig } from '@rstest/core';
4+
import { createCoverageConfig } from '../../scripts/rstest-coverage';
5+
import { stubStyleRules } from '../../scripts/rstest-style-stub';
6+
7+
export default defineConfig({
8+
plugins: [pluginReact()],
9+
coverage: createCoverageConfig(__dirname),
10+
// Default to node; jsdom is selected per file via the `@vitest-environment
11+
// jsdom` docblock. The storage polyfill is harmless under node, so it can be
12+
// applied globally.
13+
testEnvironment: 'node',
14+
setupFiles: [
15+
path.resolve(__dirname, '../../scripts/rstest-jsdom-storage.ts'),
16+
],
17+
include: ['tests/**/*.test.{ts,tsx}'],
18+
...stubStyleRules,
19+
});

apps/chrome-extension/tests/bridge-start-stop.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@
1010
* - Users cannot stop/start listening in bridge mode
1111
* - Users cannot change remote server URL because the input is always disabled
1212
*
13-
* Run: npx vitest run apps/chrome-extension/tests/bridge-start-stop.test.ts
13+
* Run: npx rstest run apps/chrome-extension/tests/bridge-start-stop.test.ts
1414
*/
15-
import { describe, expect, it, vi } from 'vitest';
15+
import { describe, expect, it, rs } from '@rstest/core';
1616

1717
// ─── BridgeConnector unit tests ──────────────────────────────────────────────
1818

apps/chrome-extension/tests/browser-extension-playground.test.tsx

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,26 @@
1+
import { afterEach, beforeEach, describe, expect, it, rs } from '@rstest/core';
12
/**
23
* @vitest-environment jsdom
34
*/
45
import { act } from 'react';
56
import type React from 'react';
67
import { createRoot } from 'react-dom/client';
7-
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
88

9-
const mockPlaygroundSDK = vi.fn();
9+
const mockPlaygroundSDK = rs.fn();
1010
const universalPlaygroundProps: Array<Record<string, unknown>> = [];
11-
const mockTabsQuery = vi.fn();
12-
const mockOnActivatedAddListener = vi.fn();
13-
const mockOnActivatedRemoveListener = vi.fn();
11+
const mockTabsQuery = rs.fn();
12+
const mockOnActivatedAddListener = rs.fn();
13+
const mockOnActivatedRemoveListener = rs.fn();
1414

15-
vi.mock('@midscene/playground', () => ({
15+
rs.mock('@midscene/playground', () => ({
1616
PlaygroundSDK: class MockPlaygroundSDK {
1717
constructor(config: unknown) {
1818
mockPlaygroundSDK(config);
1919
}
2020
},
2121
}));
2222

23-
vi.mock('@midscene/visualizer', () => ({
23+
rs.mock('@midscene/visualizer', () => ({
2424
UniversalPlayground: (props: Record<string, unknown>) => {
2525
universalPlaygroundProps.push(props);
2626
return <div>universal-playground</div>;
@@ -34,7 +34,7 @@ vi.mock('@midscene/visualizer', () => ({
3434
},
3535
}));
3636

37-
vi.mock('../../src/utils/chrome', () => ({
37+
rs.mock('../../src/utils/chrome', () => ({
3838
getExtensionVersion: () => '1.0.0',
3939
}));
4040

@@ -68,20 +68,20 @@ describe('BrowserExtensionPlayground', () => {
6868
},
6969
},
7070
});
71-
vi.stubGlobal('__SDK_VERSION__', 'test-sdk');
71+
rs.stubGlobal('__SDK_VERSION__', 'test-sdk');
7272
});
7373

7474
afterEach(() => {
7575
document.body.innerHTML = '';
76-
vi.unstubAllGlobals();
76+
rs.unstubAllGlobals();
7777
});
7878

7979
it('creates PlaygroundSDK even when env config is empty', async () => {
8080
const { BrowserExtensionPlayground } = await import(
8181
'../src/components/playground'
8282
);
83-
const onPlaygroundSDKChange = vi.fn();
84-
const getAgent = vi.fn(() => ({ page: { screenshot: vi.fn() } }));
83+
const onPlaygroundSDKChange = rs.fn();
84+
const getAgent = rs.fn(() => ({ page: { screenshot: rs.fn() } }));
8585
const container = document.createElement('div');
8686
document.body.appendChild(container);
8787
const root = createRoot(container);

apps/chrome-extension/tests/event-memory-stress.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@
99
* JSON.stringify is a close proxy for that cost. We measure actual heap usage
1010
* to prove the old approach exceeds Chrome Service Worker memory limits (~256-512 MB).
1111
*
12-
* Run: npx vitest run apps/chrome-extension/tests/event-memory-stress.test.ts
12+
* Run: npx rstest run apps/chrome-extension/tests/event-memory-stress.test.ts
1313
*/
14-
import { describe, expect, it } from 'vitest';
14+
import { describe, expect, it } from '@rstest/core';
1515

1616
// --- Helpers ---
1717

apps/chrome-extension/tests/playground-popup.test.tsx

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
1+
import { afterEach, beforeEach, describe, expect, it, rs } from '@rstest/core';
12
/**
23
* @vitest-environment jsdom
34
*/
45
import { act } from 'react';
56
import type React from 'react';
67
import { useEffect } from 'react';
78
import { createRoot } from 'react-dom/client';
8-
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
99

10-
const setPopupTab = vi.fn();
10+
const setPopupTab = rs.fn();
1111
const getAgentRefs: Array<unknown> = [];
1212
let sdkSyncEffectCount = 0;
1313

14-
vi.mock('@midscene/visualizer', () => ({
14+
rs.mock('@midscene/visualizer', () => ({
1515
NavActions: () => null,
1616
globalThemeConfig: () => ({}),
17-
safeOverrideAIConfig: vi.fn(),
17+
safeOverrideAIConfig: rs.fn(),
1818
useEnvConfig: (selector?: (state: Record<string, unknown>) => unknown) => {
1919
const state = {
2020
config: {
@@ -27,21 +27,21 @@ vi.mock('@midscene/visualizer', () => ({
2727
},
2828
}));
2929

30-
vi.mock('antd', () => ({
30+
rs.mock('antd', () => ({
3131
ConfigProvider: ({ children }: { children: React.ReactNode }) => children,
3232
Dropdown: ({ children }: { children: React.ReactNode }) => children,
3333
}));
3434

35-
vi.mock('@midscene/shared/env', () => ({
35+
rs.mock('@midscene/shared/env', () => ({
3636
MIDSCENE_MODEL_API_KEY: 'test-key',
3737
}));
3838

39-
vi.mock('@midscene/web/chrome-extension', () => ({
39+
rs.mock('@midscene/web/chrome-extension', () => ({
4040
ChromeExtensionProxyPage: class ChromeExtensionProxyPage {},
4141
ChromeExtensionProxyPageAgent: class ChromeExtensionProxyPageAgent {},
4242
}));
4343

44-
vi.mock('../src/components/playground', () => ({
44+
rs.mock('../src/components/playground', () => ({
4545
BrowserExtensionPlayground: ({
4646
getAgent,
4747
onPlaygroundSDKChange,
@@ -60,11 +60,11 @@ vi.mock('../src/components/playground', () => ({
6060
},
6161
}));
6262

63-
vi.mock('../src/extension/bridge', () => ({
63+
rs.mock('../src/extension/bridge', () => ({
6464
default: () => <div>bridge</div>,
6565
}));
6666

67-
vi.mock('../src/extension/recorder', () => ({
67+
rs.mock('../src/extension/recorder', () => ({
6868
default: () => <div>recorder</div>,
6969
}));
7070

apps/chrome-extension/tests/stop-recording-fixes.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@
77
* 3. Batched AI description generation: concurrency is bounded
88
* 4. Logger sanitization: events arrays are never passed to loggers
99
*
10-
* Run: npx vitest run apps/chrome-extension/tests/stop-recording-fixes.test.ts
10+
* Run: npx rstest run apps/chrome-extension/tests/stop-recording-fixes.test.ts
1111
*/
12-
import { describe, expect, it, vi } from 'vitest';
12+
import { describe, expect, it, rs } from '@rstest/core';
1313

1414
// --- Helpers ---
1515

apps/chrome-extension/tests/yaml-language-settings.test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { describe, expect, it, vi } from 'vitest';
1+
import { describe, expect, it, rs } from '@rstest/core';
22
import {
33
DEFAULT_YAML_LANGUAGE_PREFERENCE,
44
YAML_LANGUAGE_STORAGE_KEY,
@@ -10,7 +10,7 @@ import {
1010
describe('yaml language settings', () => {
1111
it('falls back to auto when storage is empty', () => {
1212
const storage = {
13-
getItem: vi.fn(() => null),
13+
getItem: rs.fn(() => null),
1414
};
1515

1616
expect(getStoredYamlLanguagePreference(storage)).toBe(
@@ -20,23 +20,23 @@ describe('yaml language settings', () => {
2020

2121
it('returns a custom language stored by the user', () => {
2222
const storage = {
23-
getItem: vi.fn(() => 'Japanese'),
23+
getItem: rs.fn(() => 'Japanese'),
2424
};
2525

2626
expect(getStoredYamlLanguagePreference(storage)).toBe('Japanese');
2727
});
2828

2929
it('returns an explicitly stored YAML language', () => {
3030
const storage = {
31-
getItem: vi.fn(() => 'Chinese'),
31+
getItem: rs.fn(() => 'Chinese'),
3232
};
3333

3434
expect(getStoredYamlLanguagePreference(storage)).toBe('Chinese');
3535
});
3636

3737
it('persists YAML language preference with the expected key', () => {
3838
const storage = {
39-
setItem: vi.fn(),
39+
setItem: rs.fn(),
4040
};
4141

4242
persistYamlLanguagePreference('English', storage);

0 commit comments

Comments
 (0)