From 40cf0a88d9f0871cd16769cd15689f4d4a5567aa Mon Sep 17 00:00:00 2001 From: nowgnuesLee <192685612+nowgnuesLee@users.noreply.github.com> Date: Thu, 30 Apr 2026 12:12:28 +0000 Subject: [PATCH] feat(FR-2609): Vitest migration for react/ (100% pass, 856/856 tests) (#6874) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolves #6812(FR-2609) ## Summary Migrates `react/`'s test suite from Jest to Vitest. **856/856 tests passing** under Vitest. (Commit title still reads `96.9% pass rate, 822/848 tests` — that was the baseline before the last-mile fixes landed in-tree via `gt modify`. Current state is 100%; see test-plan counts below.) ### New files - `react/vitest.config.ts` — reuses `@vitejs/plugin-react` + `babel-plugin-react-compiler` + `babel-plugin-relay` so tests and the running app exercise identical transformed code. - `react/__test__/vitest.jest-compat.ts` — exposes `vi` as `jest` globally, so existing `jest.fn()` / `jest.spyOn()` calls in test files continue to work without a sweeping rename. ### Vitest 4 pitfalls fixed inline - **`process.env.NODE_ENV` is non-configurable** in Node 20+. Replaced `Object.defineProperty(process.env, 'NODE_ENV', ...)` with `vi.stubEnv('NODE_ENV', ...)` + `vi.unstubAllEnvs()` in `customThemeConfig.test.ts`. - **Default-export mock shape**: `vi.mock(path, () => Component)` must return `{ default: Component }` for files imported via `import X from ...`. Fixed in `MyResourceWithinResourceGroup.test.tsx`. - **Arrow-function `mockImplementation` called with `new`**: `new TabCount()` throws because arrow functions aren't constructors. Swapped all `MockedTabCount.mockImplementation(() => ({...}))` to `function(this: any) {...}` in `useLoginOrchestration.test.ts`. - **Factory-created vi.fn() mock history** isn't cleared by `vi.restoreAllMocks()` (that only touches `spyOn` spies). Added `vi.clearAllMocks()` in `afterEach` to prevent call-count leakage across tests. ## Test plan - [x] `pnpm --prefix ./react run vitest` → 856/856 pass - [x] Same React Compiler / Relay transforms as prod build (`'use memo'` optimisations active in tests) - [x] `scripts/verify.sh` passes for Lint + Format + Relay checks ## Stack Builds on FR-2608. The broader Jest → Vitest cutover continues in the next PR (BUI + root `/src`). --- pnpm-lock.yaml | 572 +++++++++++++++++- react/VITE_POC_NOTES.md | 44 +- react/__test__/vitest.jest-compat.ts | 27 + react/package.json | 4 + .../MyResourceWithinResourceGroup.test.tsx | 32 +- .../rules/__tests__/configRules.test.ts | 1 - .../rules/__tests__/cspRules.test.ts | 1 - .../rules/__tests__/endpointRules.test.ts | 1 - .../rules/__tests__/storageProxyRules.test.ts | 1 - react/src/global-stores.test.ts | 10 +- react/src/helper/big-number.test.ts | 1 - react/src/helper/customThemeConfig.test.ts | 68 +-- react/src/helper/index.test.tsx | 8 +- react/src/helper/resultTypes.test.ts | 1 - .../useMultiStepNotification.test.tsx | 52 +- .../hooks/useBackendAIImageMetaData.test.tsx | 2 +- react/src/hooks/useControllableState.test.ts | 2 +- react/src/hooks/useHighlight.test.tsx | 47 +- react/src/hooks/useKeyboardShortcut.test.ts | 11 +- react/src/hooks/useLoginOrchestration.test.ts | 82 ++- react/src/hooks/useLogout.test.ts | 18 +- react/src/hooks/useMemoWithPrevious.test.tsx | 8 +- react/src/hooks/usePrimaryColors.test.tsx | 4 +- react/src/hooks/useScrollBreackPoint.test.tsx | 13 +- react/src/hooks/useTokenizer.test.ts | 7 +- .../src/hooks/useValidateServiceName.test.tsx | 2 +- .../src/hooks/useValidateSessionName.test.tsx | 2 +- react/src/hooks/useVariantConfigs.test.ts | 11 +- react/src/hooks/useWebUIConfig.test.ts | 8 +- react/src/lib/TabCounter.test.ts | 20 +- react/vitest.config.ts | 101 ++++ 31 files changed, 928 insertions(+), 233 deletions(-) create mode 100644 react/__test__/vitest.jest-compat.ts create mode 100644 react/vitest.config.ts diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 46f5a739d8..cd070df6d0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -605,7 +605,7 @@ importers: version: link:../eslint-config-bai eslint-plugin-import: specifier: 'catalog:' - version: 2.32.0(@typescript-eslint/parser@8.57.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.4(jiti@1.21.7)) + version: 2.32.0(@typescript-eslint/parser@7.18.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.5.4))(eslint@9.39.4(jiti@1.21.7)) eslint-plugin-json-schema-validator: specifier: 'catalog:' version: 5.5.1(eslint@9.39.4(jiti@1.21.7)) @@ -692,7 +692,7 @@ importers: version: 7.0.1(eslint@9.39.4(jiti@1.21.7)) typescript-eslint: specifier: 'catalog:' - version: 8.57.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3) + version: 8.57.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.5.4) react: dependencies: @@ -1038,7 +1038,7 @@ importers: version: link:../packages/eslint-config-bai eslint-plugin-import: specifier: 'catalog:' - version: 2.32.0(@typescript-eslint/parser@5.62.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.7.3))(eslint@9.39.4(jiti@1.21.7)) + version: 2.32.0(@typescript-eslint/parser@8.57.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.7.3))(eslint@9.39.4(jiti@1.21.7)) eslint-plugin-react: specifier: 'catalog:' version: 7.37.5(eslint@9.39.4(jiti@1.21.7)) @@ -1066,6 +1066,9 @@ importers: jest-environment-jsdom: specifier: 'catalog:' version: 30.2.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) + jsdom: + specifier: ^29.0.2 + version: 29.0.2 nodemon: specifier: ^3.1.14 version: 3.1.14 @@ -1108,6 +1111,9 @@ importers: vite-plugin-svgr: specifier: ^4.5.0 version: 4.5.0(rollup@2.80.0)(typescript@5.7.3)(vite@6.4.1(@types/node@22.19.15)(jiti@1.21.7)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) + vitest: + specifier: ^4.1.4 + version: 4.1.4(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(jsdom@29.0.2)(vite@6.4.1(@types/node@22.19.15)(jiti@1.21.7)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) webpack: specifier: 'catalog:' version: 5.105.4(esbuild@0.27.3)(webpack-cli@6.0.1(webpack@5.105.4)) @@ -1372,6 +1378,17 @@ packages: '@asamuzakjp/css-color@3.2.0': resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==} + '@asamuzakjp/css-color@5.1.10': + resolution: {integrity: sha512-02OhhkKtgNRuicQ/nF3TRnGsxL9wp0r3Y7VlKWyOHHGmGyvXv03y+PnymU8FKFJMTjIr1Bk8U2g1HWSLrpAHww==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + '@asamuzakjp/dom-selector@7.0.9': + resolution: {integrity: sha512-r3ElRr7y8ucyN2KdICwGsmj19RoN13CLCa/pvGydghWK6ZzeKQ+TcDjVdtEZz2ElpndM5jXw//B9CEee0mWnVg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + '@asamuzakjp/nwsapi@2.3.9': + resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==} + '@babel/code-frame@7.24.7': resolution: {integrity: sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==} engines: {node: '>=6.9.0'} @@ -2130,6 +2147,10 @@ packages: '@braintree/sanitize-url@7.1.2': resolution: {integrity: sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==} + '@bramus/specificity@2.4.2': + resolution: {integrity: sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==} + hasBin: true + '@chevrotain/cst-dts-gen@11.0.3': resolution: {integrity: sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ==} @@ -2319,6 +2340,10 @@ packages: resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==} engines: {node: '>=18'} + '@csstools/color-helpers@6.0.2': + resolution: {integrity: sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==} + engines: {node: '>=20.19.0'} + '@csstools/css-calc@2.1.4': resolution: {integrity: sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==} engines: {node: '>=18'} @@ -2326,6 +2351,13 @@ packages: '@csstools/css-parser-algorithms': ^3.0.5 '@csstools/css-tokenizer': ^3.0.4 + '@csstools/css-calc@3.2.0': + resolution: {integrity: sha512-bR9e6o2BDB12jzN/gIbjHa5wLJ4UjD1CB9pM7ehlc0ddk6EBz+yYS1EV2MF55/HUxrHcB/hehAyt5vhsA3hx7w==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 + '@csstools/css-color-parser@3.1.0': resolution: {integrity: sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==} engines: {node: '>=18'} @@ -2333,16 +2365,41 @@ packages: '@csstools/css-parser-algorithms': ^3.0.5 '@csstools/css-tokenizer': ^3.0.4 + '@csstools/css-color-parser@4.1.0': + resolution: {integrity: sha512-U0KhLYmy2GVj6q4T3WaAe6NPuFYCPQoE3b0dRGxejWDgcPp8TP7S5rVdM5ZrFaqu4N67X8YaPBw14dQSYx3IyQ==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 + '@csstools/css-parser-algorithms@3.0.5': resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==} engines: {node: '>=18'} peerDependencies: '@csstools/css-tokenizer': ^3.0.4 + '@csstools/css-parser-algorithms@4.0.0': + resolution: {integrity: sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-syntax-patches-for-csstree@1.1.3': + resolution: {integrity: sha512-SH60bMfrRCJF3morcdk57WklujF4Jr/EsQUzqkarfHXEFcAR1gg7fS/chAE922Sehgzc1/+Tz5H3Ypa1HiEKrg==} + peerDependencies: + css-tree: ^3.2.1 + peerDependenciesMeta: + css-tree: + optional: true + '@csstools/css-tokenizer@3.0.4': resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} engines: {node: '>=18'} + '@csstools/css-tokenizer@4.0.0': + resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==} + engines: {node: '>=20.19.0'} + '@csstools/normalize.css@12.1.1': resolution: {integrity: sha512-YAYeJ+Xqh7fUou1d1j9XHl44BmsuThiTr4iNrgCQ3J27IbhXsxXDGZ1cXv8Qvs99d4rBbLiSKy3+WZiet32PcQ==} @@ -3128,6 +3185,15 @@ packages: resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@exodus/bytes@1.15.0': + resolution: {integrity: sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + peerDependencies: + '@noble/hashes': ^1.8.0 || ^2.0.0 + peerDependenciesMeta: + '@noble/hashes': + optional: true + '@floating-ui/core@1.7.3': resolution: {integrity: sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==} @@ -5802,15 +5868,44 @@ packages: '@vitest/expect@3.2.4': resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} + '@vitest/expect@4.1.4': + resolution: {integrity: sha512-iPBpra+VDuXmBFI3FMKHSFXp3Gx5HfmSCE8X67Dn+bwephCnQCaB7qWK2ldHa+8ncN8hJU8VTMcxjPpyMkUjww==} + + '@vitest/mocker@4.1.4': + resolution: {integrity: sha512-R9HTZBhW6yCSGbGQnDnH3QHfJxokKN4KB+Yvk9Q1le7eQNYwiCyKxmLmurSpFy6BzJanSLuEUDrD+j97Q+ZLPg==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + '@vitest/pretty-format@3.2.4': resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} + '@vitest/pretty-format@4.1.4': + resolution: {integrity: sha512-ddmDHU0gjEUyEVLxtZa7xamrpIefdEETu3nZjWtHeZX4QxqJ7tRxSteHVXJOcr8jhiLoGAhkK4WJ3WqBpjx42A==} + + '@vitest/runner@4.1.4': + resolution: {integrity: sha512-xTp7VZ5aXP5ZJrn15UtJUWlx6qXLnGtF6jNxHepdPHpMfz/aVPx+htHtgcAL2mDXJgKhpoo2e9/hVJsIeFbytQ==} + + '@vitest/snapshot@4.1.4': + resolution: {integrity: sha512-MCjCFgaS8aZz+m5nTcEcgk/xhWv0rEH4Yl53PPlMXOZ1/Ka2VcZU6CJ+MgYCZbcJvzGhQRjVrGQNZqkGPttIKw==} + '@vitest/spy@3.2.4': resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==} + '@vitest/spy@4.1.4': + resolution: {integrity: sha512-XxNdAsKW7C+FLydqFJLb5KhJtl3PGCMmYwFRfhvIgxJvLSXhhVI1zM8f1qD3Zg7RCjTSzDVyct6sghs9UEgBEQ==} + '@vitest/utils@3.2.4': resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} + '@vitest/utils@4.1.4': + resolution: {integrity: sha512-13QMT+eysM5uVGa1rG4kegGYNp6cnQcsTc67ELFbhNLQO+vgsygtYJx2khvdt4gVQqSSpC/KT5FZZxUpP3Oatw==} + '@volar/language-core@2.4.14': resolution: {integrity: sha512-X6beusV0DvuVseaOEy7GoagS4rYHgDHnTrdOj5jeUb49fW5ceQyP9Ej5rBhqgz2wJggl+2fDbbojq1XKaxDi6w==} @@ -6508,6 +6603,9 @@ packages: resolution: {integrity: sha512-I6MMLkn+anzNdCUp9hMRyui1HaNEUCco50lxbvNS4+EyXg8lN3nJ48PjPWtbH8UVS9CuMoaKE9U2V3l29DaRQw==} engines: {node: '>= 8.0.0'} + bidi-js@1.0.3: + resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} + big.js@5.2.2: resolution: {integrity: sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==} @@ -6743,6 +6841,10 @@ packages: resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} engines: {node: '>=18'} + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + chalk-template@0.4.0: resolution: {integrity: sha512-/ghrgmhfY8RaSdeo43hNXxpoHAtxdbskUHjPpfqUWGttFgycUhYPGx3YZBCnUCvOa7Doivn1IZec3DEGFoMgLg==} engines: {node: '>=12'} @@ -7287,6 +7389,10 @@ packages: resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + css-tree@3.2.1: + resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + css-what@3.4.2: resolution: {integrity: sha512-ACUm3L0/jiZTqfzRM3Hi9Q8eZqd6IK37mMWPLz9PJxkLWllYeRf+EHUSHYEtFop2Eqytaq1FizFVh7XfBnXCDQ==} engines: {node: '>= 6'} @@ -7547,6 +7653,10 @@ packages: resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} engines: {node: '>=18'} + data-urls@7.0.0: + resolution: {integrity: sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + data-view-buffer@1.0.2: resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} engines: {node: '>= 0.4'} @@ -8404,6 +8514,10 @@ packages: resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} engines: {node: '>=6'} + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + expect@27.5.1: resolution: {integrity: sha512-E1q5hSUG2AmYQwQJ041nvgpkODHQvB+RKlB4IYdru6uJsyFTRyZAP463M+1lINorwbqAmUggi6+WwkD8lCS/Dw==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -9042,6 +9156,10 @@ packages: resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} engines: {node: '>=18'} + html-encoding-sniffer@6.0.0: + resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + html-entities@2.5.2: resolution: {integrity: sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA==} @@ -9989,6 +10107,15 @@ packages: canvas: optional: true + jsdom@29.0.2: + resolution: {integrity: sha512-9VnGEBosc/ZpwyOsJBCQ/3I5p7Q5ngOY14a9bf5btenAORmZfDse1ZEheMiWcJ3h81+Fv7HmJFdS0szo/waF2w==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24.0.0} + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + jsesc@3.1.0: resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} engines: {node: '>=6'} @@ -10306,6 +10433,10 @@ packages: resolution: {integrity: sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==} engines: {node: 20 || >=22} + lru-cache@11.3.5: + resolution: {integrity: sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==} + engines: {node: 20 || >=22} + lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} @@ -10471,6 +10602,9 @@ packages: mdn-data@2.0.4: resolution: {integrity: sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==} + mdn-data@2.27.1: + resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} + media-typer@0.3.0: resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} engines: {node: '>= 0.6'} @@ -11002,6 +11136,9 @@ packages: obuf@1.1.2: resolution: {integrity: sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==} + obug@2.1.1: + resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + octokit@5.0.5: resolution: {integrity: sha512-4+/OFSqOjoyULo7eN7EA97DE0Xydj/PW5aIckxqQIoFjFwqXKuFCvXUJObyJfBF9Khu4RL/jlDRI9FPaMGfPnw==} engines: {node: '>= 20'} @@ -11168,6 +11305,9 @@ packages: parse5@7.3.0: resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + parse5@8.0.0: + resolution: {integrity: sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==} + parseurl@1.3.3: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} @@ -12884,6 +13024,9 @@ packages: resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} engines: {node: '>= 0.4'} + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} @@ -13033,6 +13176,9 @@ packages: resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} engines: {node: '>=10'} + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + stackframe@1.3.4: resolution: {integrity: sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==} @@ -13050,6 +13196,9 @@ packages: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} + std-env@4.0.0: + resolution: {integrity: sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==} + stdin-discarder@0.2.2: resolution: {integrity: sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==} engines: {node: '>=18'} @@ -13446,6 +13595,9 @@ packages: tiny-invariant@1.3.3: resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + tinyexec@1.0.2: resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} engines: {node: '>=18'} @@ -13458,6 +13610,10 @@ packages: resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} engines: {node: '>=14.0.0'} + tinyrainbow@3.1.0: + resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} + engines: {node: '>=14.0.0'} + tinyspy@4.0.4: resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} engines: {node: '>=14.0.0'} @@ -13465,10 +13621,17 @@ packages: tldts-core@6.1.86: resolution: {integrity: sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==} + tldts-core@7.0.28: + resolution: {integrity: sha512-7W5Efjhsc3chVdFhqtaU0KtK32J37Zcr9RKtID54nG+tIpcY79CQK/veYPODxtD/LJ4Lue66jvrQzIX2Z2/pUQ==} + tldts@6.1.86: resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==} hasBin: true + tldts@7.0.28: + resolution: {integrity: sha512-+Zg3vWhRUv8B1maGSTFdev9mjoo8Etn2Ayfs4cnjlD3CsGkxXX4QyW3j2WJ0wdjYcYmy7Lx2RDsZMhgCWafKIw==} + hasBin: true + tmpl@1.0.5: resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} @@ -13514,6 +13677,10 @@ packages: resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==} engines: {node: '>=16'} + tough-cookie@6.0.1: + resolution: {integrity: sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==} + engines: {node: '>=16'} + tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} @@ -13528,6 +13695,10 @@ packages: resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} engines: {node: '>=18'} + tr46@6.0.0: + resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} + engines: {node: '>=20'} + tree-kill@1.2.2: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true @@ -13768,6 +13939,10 @@ packages: undici-types@7.18.2: resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} + undici@7.25.0: + resolution: {integrity: sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==} + engines: {node: '>=20.18.1'} + unicode-canonical-property-names-ecmascript@2.0.1: resolution: {integrity: sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==} engines: {node: '>=4'} @@ -14078,6 +14253,47 @@ packages: yaml: optional: true + vitest@4.1.4: + resolution: {integrity: sha512-tFuJqTxKb8AvfyqMfnavXdzfy3h3sWZRWwfluGbkeR7n0HUev+FmNgZ8SDrRBTVrVCjgH5cA21qGbCffMNtWvg==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.4 + '@vitest/browser-preview': 4.1.4 + '@vitest/browser-webdriverio': 4.1.4 + '@vitest/coverage-istanbul': 4.1.4 + '@vitest/coverage-v8': 4.1.4 + '@vitest/ui': 4.1.4 + happy-dom: '*' + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + vm-browserify@1.1.2: resolution: {integrity: sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==} @@ -14160,6 +14376,10 @@ packages: resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} engines: {node: '>=12'} + webidl-conversions@8.0.1: + resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==} + engines: {node: '>=20'} + webpack-cli@6.0.1: resolution: {integrity: sha512-MfwFQ6SfwinsUVi0rNJm7rHZ31GyTcpVE5pgVA3hwFRb7COD4TzjUUwhGWKfO50+xdc2MQPuEBBJoqIMGt3JDw==} engines: {node: '>=18.12.0'} @@ -14267,10 +14487,18 @@ packages: resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} engines: {node: '>=18'} + whatwg-mimetype@5.0.0: + resolution: {integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==} + engines: {node: '>=20'} + whatwg-url@14.2.0: resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} engines: {node: '>=18'} + whatwg-url@16.0.1: + resolution: {integrity: sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} @@ -14306,6 +14534,11 @@ packages: engines: {node: '>= 8'} hasBin: true + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + widest-line@4.0.1: resolution: {integrity: sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==} engines: {node: '>=12'} @@ -15076,6 +15309,22 @@ snapshots: '@csstools/css-tokenizer': 3.0.4 lru-cache: 10.4.3 + '@asamuzakjp/css-color@5.1.10': + dependencies: + '@csstools/css-calc': 3.2.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-color-parser': 4.1.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@asamuzakjp/dom-selector@7.0.9': + dependencies: + '@asamuzakjp/nwsapi': 2.3.9 + bidi-js: 1.0.3 + css-tree: 3.2.1 + is-potential-custom-element-name: 1.0.1 + + '@asamuzakjp/nwsapi@2.3.9': {} + '@babel/code-frame@7.24.7': dependencies: '@babel/highlight': 7.24.7 @@ -16034,6 +16283,10 @@ snapshots: '@braintree/sanitize-url@7.1.2': {} + '@bramus/specificity@2.4.2': + dependencies: + css-tree: 3.2.1 + '@chevrotain/cst-dts-gen@11.0.3': dependencies: '@chevrotain/gast': 11.0.3 @@ -16443,11 +16696,18 @@ snapshots: '@csstools/color-helpers@5.1.0': {} + '@csstools/color-helpers@6.0.2': {} + '@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': dependencies: '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 + '@csstools/css-calc@3.2.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + '@csstools/css-color-parser@3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': dependencies: '@csstools/color-helpers': 5.1.0 @@ -16455,12 +16715,29 @@ snapshots: '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 + '@csstools/css-color-parser@4.1.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/color-helpers': 6.0.2 + '@csstools/css-calc': 3.2.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + '@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)': dependencies: '@csstools/css-tokenizer': 3.0.4 + '@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-syntax-patches-for-csstree@1.1.3(css-tree@3.2.1)': + optionalDependencies: + css-tree: 3.2.1 + '@csstools/css-tokenizer@3.0.4': {} + '@csstools/css-tokenizer@4.0.0': {} + '@csstools/normalize.css@12.1.1': {} '@csstools/postcss-cascade-layers@1.1.1(postcss@8.4.49)': @@ -17158,6 +17435,8 @@ snapshots: '@eslint/core': 0.17.0 levn: 0.4.1 + '@exodus/bytes@1.15.0': {} + '@floating-ui/core@1.7.3': dependencies: '@floating-ui/utils': 0.2.10 @@ -20260,10 +20539,10 @@ snapshots: '@types/node': 25.3.5 optional: true - '@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.7.3))(eslint@9.39.4(jiti@1.21.7))(typescript@5.7.3)': + '@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@8.57.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.7.3))(eslint@9.39.4(jiti@1.21.7))(typescript@5.7.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 5.62.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.7.3) + '@typescript-eslint/parser': 8.57.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.7.3) '@typescript-eslint/scope-manager': 5.62.0 '@typescript-eslint/type-utils': 5.62.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.7.3) '@typescript-eslint/utils': 5.62.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.7.3) @@ -20297,6 +20576,22 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/eslint-plugin@8.57.1(@typescript-eslint/parser@8.57.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.5.4))(eslint@9.39.4(jiti@1.21.7))(typescript@5.5.4)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.57.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.5.4) + '@typescript-eslint/scope-manager': 8.57.1 + '@typescript-eslint/type-utils': 8.57.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.5.4) + '@typescript-eslint/utils': 8.57.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.5.4) + '@typescript-eslint/visitor-keys': 8.57.1 + eslint: 9.39.4(jiti@1.21.7) + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.4.0(typescript@5.5.4) + typescript: 5.5.4 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/eslint-plugin@8.57.1(@typescript-eslint/parser@8.57.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.7.3))(eslint@9.39.4(jiti@1.21.7))(typescript@5.7.3)': dependencies: '@eslint-community/regexpp': 4.12.2 @@ -20362,6 +20657,18 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/parser@8.57.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.5.4)': + dependencies: + '@typescript-eslint/scope-manager': 8.57.1 + '@typescript-eslint/types': 8.57.1 + '@typescript-eslint/typescript-estree': 8.57.1(typescript@5.5.4) + '@typescript-eslint/visitor-keys': 8.57.1 + debug: 4.4.3(supports-color@5.5.0) + eslint: 9.39.4(jiti@1.21.7) + typescript: 5.5.4 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/parser@8.57.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.7.3)': dependencies: '@typescript-eslint/scope-manager': 8.57.1 @@ -20404,6 +20711,15 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/project-service@8.57.1(typescript@5.5.4)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.57.1(typescript@5.5.4) + '@typescript-eslint/types': 8.57.1 + debug: 4.4.3(supports-color@5.5.0) + typescript: 5.5.4 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/project-service@8.57.1(typescript@5.7.3)': dependencies: '@typescript-eslint/tsconfig-utils': 8.57.1(typescript@5.7.3) @@ -20450,6 +20766,10 @@ snapshots: dependencies: typescript: 5.9.3 + '@typescript-eslint/tsconfig-utils@8.57.1(typescript@5.5.4)': + dependencies: + typescript: 5.5.4 + '@typescript-eslint/tsconfig-utils@8.57.1(typescript@5.7.3)': dependencies: typescript: 5.7.3 @@ -20482,6 +20802,18 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/type-utils@8.57.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.5.4)': + dependencies: + '@typescript-eslint/types': 8.57.1 + '@typescript-eslint/typescript-estree': 8.57.1(typescript@5.5.4) + '@typescript-eslint/utils': 8.57.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.5.4) + debug: 4.4.3(supports-color@5.5.0) + eslint: 9.39.4(jiti@1.21.7) + ts-api-utils: 2.4.0(typescript@5.5.4) + typescript: 5.5.4 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/type-utils@8.57.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.7.3)': dependencies: '@typescript-eslint/types': 8.57.1 @@ -20573,6 +20905,21 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/typescript-estree@8.57.1(typescript@5.5.4)': + dependencies: + '@typescript-eslint/project-service': 8.57.1(typescript@5.5.4) + '@typescript-eslint/tsconfig-utils': 8.57.1(typescript@5.5.4) + '@typescript-eslint/types': 8.57.1 + '@typescript-eslint/visitor-keys': 8.57.1 + debug: 4.4.3(supports-color@5.5.0) + minimatch: 10.2.4 + semver: 7.7.4 + tinyglobby: 0.2.15 + ts-api-utils: 2.4.0(typescript@5.5.4) + typescript: 5.5.4 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/typescript-estree@8.57.1(typescript@5.7.3)': dependencies: '@typescript-eslint/project-service': 8.57.1(typescript@5.7.3) @@ -20651,6 +20998,17 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/utils@8.57.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.5.4)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@1.21.7)) + '@typescript-eslint/scope-manager': 8.57.1 + '@typescript-eslint/types': 8.57.1 + '@typescript-eslint/typescript-estree': 8.57.1(typescript@5.5.4) + eslint: 9.39.4(jiti@1.21.7) + typescript: 5.5.4 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/utils@8.57.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.7.3)': dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@1.21.7)) @@ -20859,20 +21217,61 @@ snapshots: chai: 5.3.3 tinyrainbow: 2.0.0 + '@vitest/expect@4.1.4': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.4 + '@vitest/utils': 4.1.4 + chai: 6.2.2 + tinyrainbow: 3.1.0 + + '@vitest/mocker@4.1.4(vite@6.4.1(@types/node@22.19.15)(jiti@1.21.7)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3))': + dependencies: + '@vitest/spy': 4.1.4 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 6.4.1(@types/node@22.19.15)(jiti@1.21.7)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + '@vitest/pretty-format@3.2.4': dependencies: tinyrainbow: 2.0.0 + '@vitest/pretty-format@4.1.4': + dependencies: + tinyrainbow: 3.1.0 + + '@vitest/runner@4.1.4': + dependencies: + '@vitest/utils': 4.1.4 + pathe: 2.0.3 + + '@vitest/snapshot@4.1.4': + dependencies: + '@vitest/pretty-format': 4.1.4 + '@vitest/utils': 4.1.4 + magic-string: 0.30.21 + pathe: 2.0.3 + '@vitest/spy@3.2.4': dependencies: tinyspy: 4.0.4 + '@vitest/spy@4.1.4': {} + '@vitest/utils@3.2.4': dependencies: '@vitest/pretty-format': 3.2.4 loupe: 3.2.1 tinyrainbow: 2.0.0 + '@vitest/utils@4.1.4': + dependencies: + '@vitest/pretty-format': 4.1.4 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.0 + '@volar/language-core@2.4.14': dependencies: '@volar/source-map': 2.4.14 @@ -21823,6 +22222,10 @@ snapshots: jsonpath: 1.3.0 tryer: 1.0.1 + bidi-js@1.0.3: + dependencies: + require-from-string: 2.0.2 + big.js@5.2.2: {} big.js@7.0.1: {} @@ -22109,6 +22512,8 @@ snapshots: loupe: 3.2.1 pathval: 2.0.1 + chai@6.2.2: {} + chalk-template@0.4.0: dependencies: chalk: 4.1.2 @@ -22697,6 +23102,11 @@ snapshots: mdn-data: 2.0.30 source-map-js: 1.2.1 + css-tree@3.2.1: + dependencies: + mdn-data: 2.27.1 + source-map-js: 1.2.1 + css-what@3.4.2: {} css-what@6.2.2: {} @@ -23005,6 +23415,13 @@ snapshots: whatwg-mimetype: 4.0.0 whatwg-url: 14.2.0 + data-urls@7.0.0: + dependencies: + whatwg-mimetype: 5.0.0 + whatwg-url: 16.0.1 + transitivePeerDependencies: + - '@noble/hashes' + data-view-buffer@1.0.2: dependencies: call-bound: 1.0.4 @@ -23625,7 +24042,7 @@ snapshots: '@babel/core': 7.29.0 '@babel/eslint-parser': 7.25.9(@babel/core@7.29.0)(eslint@9.39.4(jiti@1.21.7)) '@rushstack/eslint-patch': 1.10.4 - '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.7.3))(eslint@9.39.4(jiti@1.21.7))(typescript@5.7.3) + '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@8.57.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.7.3))(eslint@9.39.4(jiti@1.21.7))(typescript@5.7.3) '@typescript-eslint/parser': 5.62.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.7.3) babel-preset-react-app: 10.1.0 confusing-browser-globals: 1.0.11 @@ -23681,11 +24098,11 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.57.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.4(jiti@1.21.7)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.57.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.4(jiti@1.21.7)): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 8.57.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/parser': 8.57.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.7.3) eslint: 9.39.4(jiti@1.21.7) eslint-import-resolver-node: 0.3.9 transitivePeerDependencies: @@ -23757,7 +24174,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.57.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.4(jiti@1.21.7)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.57.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.7.3))(eslint@9.39.4(jiti@1.21.7)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -23768,7 +24185,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.39.4(jiti@1.21.7) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.57.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.4(jiti@1.21.7)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.57.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.4(jiti@1.21.7)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -23780,7 +24197,7 @@ snapshots: string.prototype.trimend: 1.0.9 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 8.57.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/parser': 8.57.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.7.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack @@ -23791,7 +24208,7 @@ snapshots: '@typescript-eslint/experimental-utils': 5.62.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.7.3) eslint: 9.39.4(jiti@1.21.7) optionalDependencies: - '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.7.3))(eslint@9.39.4(jiti@1.21.7))(typescript@5.7.3) + '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@8.57.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.7.3))(eslint@9.39.4(jiti@1.21.7))(typescript@5.7.3) jest: 27.5.1(bufferutil@4.1.0)(ts-node@10.9.2(@types/node@22.19.15)(typescript@5.7.3))(utf-8-validate@6.0.6) transitivePeerDependencies: - supports-color @@ -24108,6 +24525,8 @@ snapshots: expand-template@2.0.3: {} + expect-type@1.3.0: {} + expect@27.5.1: dependencies: '@jest/types': 27.5.1 @@ -24929,6 +25348,12 @@ snapshots: dependencies: whatwg-encoding: 3.1.1 + html-encoding-sniffer@6.0.0: + dependencies: + '@exodus/bytes': 1.15.0 + transitivePeerDependencies: + - '@noble/hashes' + html-entities@2.5.2: {} html-entities@2.6.0: {} @@ -26553,6 +26978,32 @@ snapshots: - supports-color - utf-8-validate + jsdom@29.0.2: + dependencies: + '@asamuzakjp/css-color': 5.1.10 + '@asamuzakjp/dom-selector': 7.0.9 + '@bramus/specificity': 2.4.2 + '@csstools/css-syntax-patches-for-csstree': 1.1.3(css-tree@3.2.1) + '@exodus/bytes': 1.15.0 + css-tree: 3.2.1 + data-urls: 7.0.0 + decimal.js: 10.6.0 + html-encoding-sniffer: 6.0.0 + is-potential-custom-element-name: 1.0.1 + lru-cache: 11.3.5 + parse5: 8.0.0 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 6.0.1 + undici: 7.25.0 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 8.0.1 + whatwg-mimetype: 5.0.0 + whatwg-url: 16.0.1 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - '@noble/hashes' + jsesc@3.1.0: {} json-buffer@3.0.1: {} @@ -26893,6 +27344,8 @@ snapshots: lru-cache@11.2.6: {} + lru-cache@11.3.5: {} + lru-cache@5.1.1: dependencies: yallist: 3.1.1 @@ -27162,6 +27615,8 @@ snapshots: mdn-data@2.0.4: {} + mdn-data@2.27.1: {} + media-typer@0.3.0: {} memfs@3.5.3: @@ -27905,6 +28360,8 @@ snapshots: obuf@1.1.2: {} + obug@2.1.1: {} + octokit@5.0.5: dependencies: '@octokit/app': 16.1.2 @@ -28108,6 +28565,10 @@ snapshots: dependencies: entities: 6.0.1 + parse5@8.0.0: + dependencies: + entities: 6.0.1 + parseurl@1.3.3: {} pascal-case@3.1.2: @@ -30170,6 +30631,8 @@ snapshots: side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 + siginfo@2.0.0: {} + signal-exit@3.0.7: {} signal-exit@4.1.0: {} @@ -30320,6 +30783,8 @@ snapshots: dependencies: escape-string-regexp: 2.0.0 + stackback@0.0.2: {} + stackframe@1.3.4: {} state-local@1.0.7: {} @@ -30332,6 +30797,8 @@ snapshots: statuses@2.0.2: {} + std-env@4.0.0: {} + stdin-discarder@0.2.2: {} stop-iteration-iterator@1.1.0: @@ -30850,6 +31317,8 @@ snapshots: tiny-invariant@1.3.3: {} + tinybench@2.9.0: {} + tinyexec@1.0.2: {} tinyglobby@0.2.15: @@ -30859,14 +31328,22 @@ snapshots: tinyrainbow@2.0.0: {} + tinyrainbow@3.1.0: {} + tinyspy@4.0.4: {} tldts-core@6.1.86: {} + tldts-core@7.0.28: {} + tldts@6.1.86: dependencies: tldts-core: 6.1.86 + tldts@7.0.28: + dependencies: + tldts-core: 7.0.28 + tmpl@1.0.5: {} to-buffer@1.2.2: @@ -30913,6 +31390,10 @@ snapshots: dependencies: tldts: 6.1.86 + tough-cookie@6.0.1: + dependencies: + tldts: 7.0.28 + tr46@0.0.3: {} tr46@1.0.1: @@ -30927,6 +31408,10 @@ snapshots: dependencies: punycode: 2.3.1 + tr46@6.0.0: + dependencies: + punycode: 2.3.1 + tree-kill@1.2.2: {} trim-lines@3.0.1: {} @@ -30949,6 +31434,10 @@ snapshots: dependencies: typescript: 5.5.4 + ts-api-utils@2.4.0(typescript@5.5.4): + dependencies: + typescript: 5.5.4 + ts-api-utils@2.4.0(typescript@5.7.3): dependencies: typescript: 5.7.3 @@ -31169,6 +31658,17 @@ snapshots: dependencies: is-typedarray: 1.0.0 + typescript-eslint@8.57.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.5.4): + dependencies: + '@typescript-eslint/eslint-plugin': 8.57.1(@typescript-eslint/parser@8.57.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.5.4))(eslint@9.39.4(jiti@1.21.7))(typescript@5.5.4) + '@typescript-eslint/parser': 8.57.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.5.4) + '@typescript-eslint/typescript-estree': 8.57.1(typescript@5.5.4) + '@typescript-eslint/utils': 8.57.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.5.4) + eslint: 9.39.4(jiti@1.21.7) + typescript: 5.5.4 + transitivePeerDependencies: + - supports-color + typescript-eslint@8.57.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.7.3): dependencies: '@typescript-eslint/eslint-plugin': 8.57.1(@typescript-eslint/parser@8.57.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.7.3))(eslint@9.39.4(jiti@1.21.7))(typescript@5.7.3) @@ -31223,6 +31723,8 @@ snapshots: undici-types@7.18.2: {} + undici@7.25.0: {} + unicode-canonical-property-names-ecmascript@2.0.1: {} unicode-match-property-ecmascript@2.0.0: @@ -31639,6 +32141,35 @@ snapshots: tsx: 4.21.0 yaml: 2.8.3 + vitest@4.1.4(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(jsdom@29.0.2)(vite@6.4.1(@types/node@22.19.15)(jiti@1.21.7)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)): + dependencies: + '@vitest/expect': 4.1.4 + '@vitest/mocker': 4.1.4(vite@6.4.1(@types/node@22.19.15)(jiti@1.21.7)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) + '@vitest/pretty-format': 4.1.4 + '@vitest/runner': 4.1.4 + '@vitest/snapshot': 4.1.4 + '@vitest/spy': 4.1.4 + '@vitest/utils': 4.1.4 + es-module-lexer: 2.0.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.3 + std-env: 4.0.0 + tinybench: 2.9.0 + tinyexec: 1.0.2 + tinyglobby: 0.2.15 + tinyrainbow: 3.1.0 + vite: 6.4.1(@types/node@22.19.15)(jiti@1.21.7)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + why-is-node-running: 2.3.0 + optionalDependencies: + '@opentelemetry/api': 1.9.0 + '@types/node': 22.19.15 + jsdom: 29.0.2 + transitivePeerDependencies: + - msw + vm-browserify@1.1.2: {} void-elements@3.1.0: {} @@ -31709,6 +32240,8 @@ snapshots: webidl-conversions@7.0.0: {} + webidl-conversions@8.0.1: {} + webpack-cli@6.0.1(webpack@5.105.4): dependencies: '@discoveryjs/json-ext': 0.6.3 @@ -31875,11 +32408,21 @@ snapshots: whatwg-mimetype@4.0.0: {} + whatwg-mimetype@5.0.0: {} + whatwg-url@14.2.0: dependencies: tr46: 5.1.1 webidl-conversions: 7.0.0 + whatwg-url@16.0.1: + dependencies: + '@exodus/bytes': 1.15.0 + tr46: 6.0.0 + webidl-conversions: 8.0.1 + transitivePeerDependencies: + - '@noble/hashes' + whatwg-url@5.0.0: dependencies: tr46: 0.0.3 @@ -31946,6 +32489,11 @@ snapshots: dependencies: isexe: 2.0.0 + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + widest-line@4.0.1: dependencies: string-width: 5.1.2 diff --git a/react/VITE_POC_NOTES.md b/react/VITE_POC_NOTES.md index 5eec7a9925..9a2935bce4 100644 --- a/react/VITE_POC_NOTES.md +++ b/react/VITE_POC_NOTES.md @@ -77,9 +77,51 @@ Fix: (Each of these gets its own sub-issue under FR-2605.) -- Jest → Vitest migration (FR-2609) - CI pipeline updates (FR-2611) +## Vitest migration (react/ only) — complete (FR-2609) + +`pnpm --prefix ./react run vitest` now passes **856/856 tests** across the migrated suite. The Vitest-semantic differences from Jest noted below have been resolved as part of this PR, so this note no longer implies outstanding failures. + +### What was added + +- `react/vitest.config.ts` — dedicated Vitest config, separate from `vite.config.ts`. Shares the transform pipeline (`@vitejs/plugin-react` + babel-plugin-relay with per-directory `artifactDirectory` + babel-plugin-react-compiler) so tests exercise the same transforms as dev/prod. +- `react/__test__/vitest.jest-compat.ts` — a setup file that aliases `globalThis.jest = vi` so legacy `jest.fn()` / `jest.clearAllMocks()` call sites continue to work. Migration aid only; new tests should use `vi.*` directly. +- `vitest` / `vitest:watch` scripts in `react/package.json`. + +### Bulk migrations applied + +Mechanical `jest.` → `vi.` rewrites across 39 test files (perl one-liner in the commit message): + +- `jest.mock|fn|spyOn|clearAllMocks|resetAllMocks|restoreAllMocks|useFakeTimers|useRealTimers|advanceTimersByTime|runOnlyPendingTimers|runAllTimers|doMock` → `vi.*` +- `jest.Mock` (type cast) → `Mock`, with `import type { Mock } from 'vitest'` added +- Removed `@jest/globals` import lines (Vitest's `globals: true` provides them) + +Without this, Vitest's `vi.mock` hoisting does NOT apply to `jest.mock(...)` calls (Vitest only recognises literal `vi.mock` for hoisting). The rewrites restore mock correctness across 14+ files. + +### Module resolution + +- `src/` baseUrl via regex alias `{ find: /^src\//, replacement: reactSrc + '/' }`. +- `backend.ai-ui/*`, `backend.ai-client-esm` mapped to same mocks Jest used. +- `.svg` plain imports → `__test__/svg.mock.js`; `.svg?react` → `vite-plugin-svgr`. +- `.css` / `.css?raw` → `__test__/rawCss.mock.js` (regex anchored with `^.+` so the entire specifier is replaced; array-form aliases replace the matched portion). + +### Vitest-semantic differences from Jest (resolved in this PR) + +- `react/src/helper/customThemeConfig.test.ts` — was using `Object.defineProperty(process.env, 'NODE_ENV', ...)` to toggle dev/prod; Vitest's `process.env.NODE_ENV` has an immutable descriptor. Migrated to `vi.stubEnv('NODE_ENV', ...)` + `vi.unstubAllEnvs()` in `afterEach`, plus `vi.restoreAllMocks()` between nested describes to avoid event-dispatcher accumulation. +- `react/src/components/MyResourceWithinResourceGroup.test.tsx` and `react/src/hooks/useResourceLimitAndRemaining.test.ts` — bare `vi.mock(path)` without a factory does not produce a `default` export for ESM under Vitest. Fixed by passing an explicit `() => ({ default: vi.fn() })` factory. + +### Performance + +- Vitest run: ~20s wall clock for 848 tests (transform 71s, tests 6s — tests themselves are very fast; the time is transform + import cost, paid only once per file). +- Jest equivalent on the same tree has not been measured in this session; prior expectation was 60-120s. Confidence level: "materially faster" but exact multiplier needs a controlled benchmark. + +### Still open + +- BUI (`packages/backend.ai-ui`) Jest → Vitest migration +- Root `/src` Jest → Vitest migration +- `transformIgnorePatterns` regex in existing `react/jest.config.cjs` can be deleted once the Jest pipeline is fully retired. + ## Production `vite build` + Workbox PWA — landed (FR-2608) `pnpm --prefix ./react run vite:build` now produces a working web build with a generated service worker. Output goes to `react/build/`, same directory the craco pipeline uses. diff --git a/react/__test__/vitest.jest-compat.ts b/react/__test__/vitest.jest-compat.ts new file mode 100644 index 0000000000..d6713e5899 --- /dev/null +++ b/react/__test__/vitest.jest-compat.ts @@ -0,0 +1,27 @@ +// Vitest ↔ Jest compatibility shim for the FR-2609 migration. +// +// Instead of renaming every `jest.fn()`, `jest.mock()`, `jest.spyOn()` etc. +// call across ~39 test files in react/src, we expose `vi` under the name +// `jest` so existing tests run as-is. Newly authored tests should use `vi.*` +// directly — this shim is a migration aid, not a long-term convention. +// +// Vitest's `vi` object is mostly a drop-in for Jest: +// - `jest.fn` → `vi.fn` +// - `jest.mock` → `vi.mock` (behaviour is equivalent; hoisting rules differ +// in corner cases around using captured variables in the factory) +// - `jest.spyOn` → `vi.spyOn` +// - `jest.useFakeTimers` / `jest.useRealTimers` → `vi.useFakeTimers` / +// `vi.useRealTimers` (defaults differ slightly; see vitest docs) +// - `jest.resetAllMocks` / `jest.clearAllMocks` / `jest.restoreAllMocks` → +// `vi.resetAllMocks` / `vi.clearAllMocks` / `vi.restoreAllMocks` +// +// For APIs without a direct `vi` equivalent (e.g. `jest.requireActual`), +// the offending call will throw at test time and we fix it inline there. +import { vi } from 'vitest'; + +// `globals: true` in vitest.config.ts already exposes `vi` as a global. +// The line below ALSO exposes it as `jest` so prior Jest-style calls still +// resolve. Both globals co-exist; no name collision since `jest` is not +// otherwise defined under Vitest. +// eslint-disable-next-line @typescript-eslint/no-explicit-any +(globalThis as any).jest = vi; diff --git a/react/package.json b/react/package.json index 2ea2238e80..e917449de6 100644 --- a/react/package.json +++ b/react/package.json @@ -90,6 +90,8 @@ "start": "NODE_OPTIONS='--max-old-space-size=4096' craco start", "vite:dev": "vite", "vite:build": "vite build", + "vitest": "vitest run", + "vitest:watch": "vitest", "build": "pnpm run build:only && cp -r ./build/* ../build/web/", "build:only": "NODE_OPTIONS='--max-old-space-size=4096' pnpm run relay && NODE_OPTIONS='--max-old-space-size=4096' craco build", "test": "NODE_OPTIONS='$NODE_OPTIONS --no-deprecation --experimental-vm-modules' jest", @@ -155,6 +157,7 @@ "jest": "catalog:", "jest-canvas-mock": "^2.5.2", "jest-environment-jsdom": "catalog:", + "jsdom": "^29.0.2", "nodemon": "^3.1.14", "prop-types": "^15.8.1", "react-dev-utils": "^12.0.1", @@ -169,6 +172,7 @@ "vite-plugin-node-polyfills": "^0.24.0", "vite-plugin-pwa": "^1.2.0", "vite-plugin-svgr": "^4.5.0", + "vitest": "^4.1.4", "webpack": "catalog:", "workbox-webpack-plugin": "^7.4.0" } diff --git a/react/src/components/MyResourceWithinResourceGroup.test.tsx b/react/src/components/MyResourceWithinResourceGroup.test.tsx index 6ec5c3fc96..27e4446cd1 100644 --- a/react/src/components/MyResourceWithinResourceGroup.test.tsx +++ b/react/src/components/MyResourceWithinResourceGroup.test.tsx @@ -11,13 +11,13 @@ import { render, screen } from '@testing-library/react'; import React from 'react'; // Mock all the required hooks and dependencies -jest.mock('react-i18next', () => ({ +vi.mock('react-i18next', () => ({ useTranslation: () => ({ t: (key: string) => key, }), })); -jest.mock('antd', () => ({ +vi.mock('antd', () => ({ Segmented: ({ children }: any) => (
{children}
), @@ -30,11 +30,11 @@ jest.mock('antd', () => ({ }, })); -jest.mock('ahooks', () => ({ - useControllableValue: () => ['free', jest.fn()], +vi.mock('ahooks', () => ({ + useControllableValue: () => ['free', vi.fn()], })); -jest.mock('../hooks/useCurrentProject', () => ({ +vi.mock('../hooks/useCurrentProject', () => ({ useCurrentProjectValue: () => ({ name: 'test-project' }), useCurrentResourceGroupValue: () => 'default', })); @@ -117,8 +117,8 @@ const mockDataScenarios = { }, }; -jest.mock('../hooks/useResourceLimitAndRemaining', () => ({ - useResourceLimitAndRemaining: jest.fn(() => [ +vi.mock('../hooks/useResourceLimitAndRemaining', () => ({ + useResourceLimitAndRemaining: vi.fn(() => [ { resourceGroupResourceSize: { cpu: 0, mem: '0 GiB', accelerators: {} }, resourceLimits: { accelerators: {} }, @@ -130,12 +130,12 @@ jest.mock('../hooks/useResourceLimitAndRemaining', () => ({ checkPresetInfo: mockDataScenarios.normal as any, }, { - refetch: jest.fn(), + refetch: vi.fn(), }, ]), })); -jest.mock('backend.ai-ui', () => { +vi.mock('backend.ai-ui', () => { const isoDate = new Date().toISOString(); return { useResourceSlotsDetails: () => ({ @@ -149,7 +149,7 @@ jest.mock('backend.ai-ui', () => { }, }, }), - useFetchKey: () => [isoDate, jest.fn(), isoDate], + useFetchKey: () => [isoDate, vi.fn(), isoDate], convertToNumber: (value: any) => parseFloat(value) || 0, processMemoryValue: (value: any) => { if (!value || value === 'Infinity' || value === Infinity) return value; @@ -190,12 +190,14 @@ jest.mock('backend.ai-ui', () => { }; }); -jest.mock('./SharedResourceGroupSelectForCurrentProject', () => { +vi.mock('./SharedResourceGroupSelectForCurrentProject', () => { const MockedComponent = () => (
Select
); MockedComponent.displayName = 'SharedResourceGroupSelectForCurrentProject'; - return MockedComponent; + // Source uses `import X from ...`, so the factory must return a module + // namespace with a `default` export, not the component directly. + return { default: MockedComponent }; }); // Helper function to create mock return value @@ -212,7 +214,7 @@ const createMockReturnValue = (checkPresetInfo: any) => checkPresetInfo, }, { - refetch: jest.fn(), + refetch: vi.fn(), }, ] as const; @@ -237,7 +239,7 @@ TestWrapper.displayName = 'TestWrapper'; describe('MyResourceWithinResourceGroup', () => { let queryClient: QueryClient; - const mockHook = jest.spyOn( + const mockHook = vi.spyOn( useResourceLimitAndRemainingModule, 'useResourceLimitAndRemaining', ); @@ -249,7 +251,7 @@ describe('MyResourceWithinResourceGroup', () => { }); afterEach(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); mockHook.mockReset(); }); diff --git a/react/src/diagnostics/rules/__tests__/configRules.test.ts b/react/src/diagnostics/rules/__tests__/configRules.test.ts index bc36cf0106..1757d3df97 100644 --- a/react/src/diagnostics/rules/__tests__/configRules.test.ts +++ b/react/src/diagnostics/rules/__tests__/configRules.test.ts @@ -13,7 +13,6 @@ import { checkSslMismatch, checkUrlFields, } from '../configRules'; -import { describe, expect, it } from '@jest/globals'; const validMenuKeys = [ 'start', diff --git a/react/src/diagnostics/rules/__tests__/cspRules.test.ts b/react/src/diagnostics/rules/__tests__/cspRules.test.ts index 518f30001c..b2e1a94f10 100644 --- a/react/src/diagnostics/rules/__tests__/cspRules.test.ts +++ b/react/src/diagnostics/rules/__tests__/cspRules.test.ts @@ -11,7 +11,6 @@ import { parseCspConnectSrc, parseCspDirective, } from '../cspRules'; -import { describe, expect, it } from '@jest/globals'; describe('parseCspConnectSrc', () => { it('should return empty array for null/undefined input', () => { diff --git a/react/src/diagnostics/rules/__tests__/endpointRules.test.ts b/react/src/diagnostics/rules/__tests__/endpointRules.test.ts index 51221d45c2..ea50663cba 100644 --- a/react/src/diagnostics/rules/__tests__/endpointRules.test.ts +++ b/react/src/diagnostics/rules/__tests__/endpointRules.test.ts @@ -3,7 +3,6 @@ Copyright (c) 2015-2026 Lablup Inc. All rights reserved. */ import { checkEndpointReachability } from '../endpointRules'; -import { describe, expect, it } from '@jest/globals'; describe('checkEndpointReachability', () => { it('should return null when endpoint is empty', () => { diff --git a/react/src/diagnostics/rules/__tests__/storageProxyRules.test.ts b/react/src/diagnostics/rules/__tests__/storageProxyRules.test.ts index 3573a5fdd2..80e4904ab4 100644 --- a/react/src/diagnostics/rules/__tests__/storageProxyRules.test.ts +++ b/react/src/diagnostics/rules/__tests__/storageProxyRules.test.ts @@ -4,7 +4,6 @@ */ import { checkStorageVolumeHealth } from '../storageProxyRules'; import type { StorageVolumeInfo } from '../storageProxyRules'; -import { describe, expect, it } from '@jest/globals'; describe('checkStorageVolumeHealth', () => { it('should return null when usage data is missing', () => { diff --git a/react/src/global-stores.test.ts b/react/src/global-stores.test.ts index 0802e622aa..a5436be92e 100644 --- a/react/src/global-stores.test.ts +++ b/react/src/global-stores.test.ts @@ -228,7 +228,7 @@ describe('BackendAIMetadataStore', () => { it('has a readImageMetadata method that returns a Promise', () => { const originalFetch = global.fetch; - global.fetch = jest.fn().mockRejectedValue(new Error('offline')); + global.fetch = vi.fn().mockRejectedValue(new Error('offline')); const result = backendaiMetadata.readImageMetadata(); expect(result).toBeInstanceOf(Promise); @@ -245,7 +245,7 @@ describe('BackendAIMetadataStore', () => { tagReplace: {}, }; - global.fetch = jest.fn().mockResolvedValue({ + global.fetch = vi.fn().mockResolvedValue({ json: () => Promise.resolve(mockPayload), } as unknown as Response); @@ -265,7 +265,7 @@ describe('BackendAIMetadataStore', () => { }); it('silently handles fetch failure without throwing', async () => { - global.fetch = jest.fn().mockRejectedValue(new Error('network error')); + global.fetch = vi.fn().mockRejectedValue(new Error('network error')); await expect( backendaiMetadata.readImageMetadata(), @@ -279,11 +279,11 @@ describe('BackendAIMetadataStore', () => { describe('BackendAITasker', () => { beforeEach(() => { - jest.useFakeTimers(); + vi.useFakeTimers(); }); afterEach(() => { - jest.useRealTimers(); + vi.useRealTimers(); }); describe('add()', () => { diff --git a/react/src/helper/big-number.test.ts b/react/src/helper/big-number.test.ts index da00e163f8..fb7a7ef0be 100644 --- a/react/src/helper/big-number.test.ts +++ b/react/src/helper/big-number.test.ts @@ -3,7 +3,6 @@ Copyright (c) 2015-2026 Lablup Inc. All rights reserved. */ import { BigNumber } from './big-number'; -import { expect } from '@jest/globals'; import Big from 'big.js'; declare module '@jest/expect' { diff --git a/react/src/helper/customThemeConfig.test.ts b/react/src/helper/customThemeConfig.test.ts index 125e3412a4..f536f58d49 100644 --- a/react/src/helper/customThemeConfig.test.ts +++ b/react/src/helper/customThemeConfig.test.ts @@ -4,34 +4,32 @@ import { type CustomThemeConfig, type LogoConfig, } from './customThemeConfig'; +import type { Mock } from 'vitest'; describe('customThemeConfig', () => { - let fetchMock: jest.Mock; + let fetchMock: Mock; let originalFetch: typeof global.fetch; let dispatchEventSpy: jest.SpyInstance; - const originalNodeEnv: string | undefined = process.env.NODE_ENV; beforeEach(() => { // Save original values originalFetch = global.fetch; // Setup fetch mock - fetchMock = jest.fn(); + fetchMock = vi.fn(); global.fetch = fetchMock as unknown as typeof global.fetch; // Setup event dispatcher spy - dispatchEventSpy = jest.spyOn(document, 'dispatchEvent'); + dispatchEventSpy = vi.spyOn(document, 'dispatchEvent'); }); afterEach(() => { // Restore original values global.fetch = originalFetch; - Object.defineProperty(process.env, 'NODE_ENV', { - value: originalNodeEnv, - writable: true, - configurable: true, - }); - jest.clearAllMocks(); + // Vitest 4 / Node 20+ make `process.env.NODE_ENV` non-configurable, so + // `Object.defineProperty` throws. `vi.stubEnv` is the supported way. + vi.unstubAllEnvs(); + vi.clearAllMocks(); }); describe('getCustomTheme', () => { @@ -54,7 +52,7 @@ describe('customThemeConfig', () => { fetchMock.mockResolvedValueOnce({ ok: true, - json: jest.fn().mockResolvedValueOnce(mockTheme), + json: vi.fn().mockResolvedValueOnce(mockTheme), } as unknown as Response); loadCustomThemeConfig(); @@ -81,7 +79,7 @@ describe('customThemeConfig', () => { fetchMock.mockResolvedValueOnce({ ok: true, - json: jest.fn().mockResolvedValueOnce(mockLegacyTheme), + json: vi.fn().mockResolvedValueOnce(mockLegacyTheme), } as unknown as Response); loadCustomThemeConfig(); @@ -102,11 +100,7 @@ describe('customThemeConfig', () => { }); it('should apply REACT_APP_THEME_COLOR in development environment', async () => { - Object.defineProperty(process.env, 'NODE_ENV', { - value: 'development', - writable: true, - configurable: true, - }); + vi.stubEnv('NODE_ENV', 'development'); process.env.REACT_APP_THEME_COLOR = '#ff0000'; const mockTheme: CustomThemeConfig = { @@ -126,7 +120,7 @@ describe('customThemeConfig', () => { fetchMock.mockResolvedValueOnce({ ok: true, - json: jest.fn().mockResolvedValueOnce(mockTheme), + json: vi.fn().mockResolvedValueOnce(mockTheme), } as unknown as Response); loadCustomThemeConfig(); @@ -139,11 +133,7 @@ describe('customThemeConfig', () => { }); it('should not apply REACT_APP_THEME_COLOR in production environment', async () => { - Object.defineProperty(process.env, 'NODE_ENV', { - value: 'production', - writable: true, - configurable: true, - }); + vi.stubEnv('NODE_ENV', 'production'); process.env.REACT_APP_THEME_COLOR = '#ff0000'; const mockTheme: CustomThemeConfig = { @@ -163,7 +153,7 @@ describe('customThemeConfig', () => { fetchMock.mockResolvedValueOnce({ ok: true, - json: jest.fn().mockResolvedValueOnce(mockTheme), + json: vi.fn().mockResolvedValueOnce(mockTheme), } as unknown as Response); loadCustomThemeConfig(); @@ -187,7 +177,7 @@ describe('customThemeConfig', () => { fetchMock.mockResolvedValueOnce({ ok: true, - json: jest.fn().mockResolvedValueOnce(mockTheme), + json: vi.fn().mockResolvedValueOnce(mockTheme), } as unknown as Response); loadCustomThemeConfig(); @@ -221,7 +211,7 @@ describe('customThemeConfig', () => { fetchMock.mockResolvedValueOnce({ ok: true, - json: jest.fn().mockResolvedValueOnce(mockTheme), + json: vi.fn().mockResolvedValueOnce(mockTheme), } as unknown as Response); loadCustomThemeConfig(); @@ -247,7 +237,7 @@ describe('customThemeConfig', () => { fetchMock.mockResolvedValueOnce({ ok: true, - json: jest.fn().mockResolvedValueOnce(mockTheme), + json: vi.fn().mockResolvedValueOnce(mockTheme), } as unknown as Response); loadCustomThemeConfig(); @@ -274,7 +264,7 @@ describe('customThemeConfig', () => { fetchMock.mockResolvedValueOnce({ ok: true, - json: jest.fn().mockResolvedValueOnce(mockTheme), + json: vi.fn().mockResolvedValueOnce(mockTheme), } as unknown as Response); loadCustomThemeConfig(); @@ -328,7 +318,7 @@ describe('customThemeConfig', () => { fetchMock.mockResolvedValueOnce({ ok: true, - json: jest.fn().mockResolvedValueOnce(mockTheme), + json: vi.fn().mockResolvedValueOnce(mockTheme), } as unknown as Response); loadCustomThemeConfig(); @@ -364,11 +354,11 @@ describe('customThemeConfig', () => { fetchMock .mockResolvedValueOnce({ ok: true, - json: jest.fn().mockResolvedValueOnce(mockTheme1), + json: vi.fn().mockResolvedValueOnce(mockTheme1), } as unknown as Response) .mockResolvedValueOnce({ ok: true, - json: jest.fn().mockResolvedValueOnce(mockTheme2), + json: vi.fn().mockResolvedValueOnce(mockTheme2), } as unknown as Response); loadCustomThemeConfig(); @@ -382,11 +372,7 @@ describe('customThemeConfig', () => { }); it('should only apply REACT_APP_THEME_COLOR when both development mode and env var are set', async () => { - Object.defineProperty(process.env, 'NODE_ENV', { - value: 'development', - writable: true, - configurable: true, - }); + vi.stubEnv('NODE_ENV', 'development'); delete process.env.REACT_APP_THEME_COLOR; const mockTheme: CustomThemeConfig = { @@ -406,7 +392,7 @@ describe('customThemeConfig', () => { fetchMock.mockResolvedValueOnce({ ok: true, - json: jest.fn().mockResolvedValueOnce(mockTheme), + json: vi.fn().mockResolvedValueOnce(mockTheme), } as unknown as Response); loadCustomThemeConfig(); @@ -419,11 +405,7 @@ describe('customThemeConfig', () => { }); it('should preserve existing Layout component settings when applying REACT_APP_THEME_COLOR', async () => { - Object.defineProperty(process.env, 'NODE_ENV', { - value: 'development', - writable: true, - configurable: true, - }); + vi.stubEnv('NODE_ENV', 'development'); process.env.REACT_APP_THEME_COLOR = '#ff0000'; const mockTheme: CustomThemeConfig = { @@ -453,7 +435,7 @@ describe('customThemeConfig', () => { fetchMock.mockResolvedValueOnce({ ok: true, - json: jest.fn().mockResolvedValueOnce(mockTheme), + json: vi.fn().mockResolvedValueOnce(mockTheme), } as unknown as Response); loadCustomThemeConfig(); diff --git a/react/src/helper/index.test.tsx b/react/src/helper/index.test.tsx index b930baf764..414a0d732b 100644 --- a/react/src/helper/index.test.tsx +++ b/react/src/helper/index.test.tsx @@ -831,8 +831,8 @@ describe('newLineToBrElement', () => { describe('baiSignedRequestWithPromise', () => { it('should call client methods when client is provided', () => { const mockClient = { - newSignedRequest: jest.fn().mockReturnValue('mockRequest'), - _wrapWithPromise: jest.fn().mockReturnValue('mockPromise'), + newSignedRequest: vi.fn().mockReturnValue('mockRequest'), + _wrapWithPromise: vi.fn().mockReturnValue('mockPromise'), }; const result = baiSignedRequestWithPromise({ @@ -854,8 +854,8 @@ describe('baiSignedRequestWithPromise', () => { it('should handle body parameter', () => { const mockClient = { - newSignedRequest: jest.fn().mockReturnValue('mockRequest'), - _wrapWithPromise: jest.fn().mockReturnValue('mockPromise'), + newSignedRequest: vi.fn().mockReturnValue('mockRequest'), + _wrapWithPromise: vi.fn().mockReturnValue('mockPromise'), }; baiSignedRequestWithPromise({ diff --git a/react/src/helper/resultTypes.test.ts b/react/src/helper/resultTypes.test.ts index 56f438e8c9..84f4553c08 100644 --- a/react/src/helper/resultTypes.test.ts +++ b/react/src/helper/resultTypes.test.ts @@ -9,7 +9,6 @@ import { type ExtractResultError, type ExtractResultValue, } from './resultTypes'; -import { describe, expect, it } from '@jest/globals'; describe('resultTypes utilities', () => { describe('isOkResult', () => { diff --git a/react/src/hooks/__tests__/useMultiStepNotification.test.tsx b/react/src/hooks/__tests__/useMultiStepNotification.test.tsx index e42deb0d2e..8ddc9e5231 100644 --- a/react/src/hooks/__tests__/useMultiStepNotification.test.tsx +++ b/react/src/hooks/__tests__/useMultiStepNotification.test.tsx @@ -6,22 +6,22 @@ import { listenToBackgroundTask } from '../../helper'; import { useMultiStepNotification } from '../useMultiStepNotification'; import { renderHook, act } from '@testing-library/react'; -const mockUpsertNotification = jest.fn(); +const mockUpsertNotification = vi.fn(); -jest.mock('../useBAINotification', () => ({ +vi.mock('../useBAINotification', () => ({ useSetBAINotification: () => ({ upsertNotification: mockUpsertNotification, - deleteNotification: jest.fn(), - updateNotification: jest.fn(), + deleteNotification: vi.fn(), + updateNotification: vi.fn(), }), CLOSING_DURATION: 4, })); -jest.mock('../../helper', () => ({ - listenToBackgroundTask: jest.fn(), +vi.mock('../../helper', () => ({ + listenToBackgroundTask: vi.fn(), })); -jest.mock('react-i18next', () => ({ +vi.mock('react-i18next', () => ({ useTranslation: () => ({ t: (key: string, params?: Record) => { if (params) { @@ -45,7 +45,7 @@ const baseConfig = { describe('useMultiStepNotification', () => { beforeEach(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); }); describe('sequential Promise flow', () => { @@ -54,17 +54,17 @@ describe('useMultiStepNotification', () => { { label: 'Step 1', type: 'promise' as const, - executor: jest.fn().mockResolvedValue('result1'), + executor: vi.fn().mockResolvedValue('result1'), }, { label: 'Step 2', type: 'promise' as const, - executor: jest.fn().mockResolvedValue('result2'), + executor: vi.fn().mockResolvedValue('result2'), }, { label: 'Step 3', type: 'promise' as const, - executor: jest.fn().mockResolvedValue('result3'), + executor: vi.fn().mockResolvedValue('result3'), }, ]; @@ -103,7 +103,7 @@ describe('useMultiStepNotification', () => { { label: 'Step 1', type: 'promise' as const, - executor: jest.fn().mockResolvedValue('result1'), + executor: vi.fn().mockResolvedValue('result1'), }, { label: 'Step 2', @@ -137,12 +137,12 @@ describe('useMultiStepNotification', () => { describe('data chaining', () => { it('passes step 1 result as prevResult to step 2', async () => { - const step2Executor = jest.fn().mockResolvedValue('result2'); + const step2Executor = vi.fn().mockResolvedValue('result2'); const steps = [ { label: 'Step 1', type: 'promise' as const, - executor: jest.fn().mockResolvedValue('result-from-step1'), + executor: vi.fn().mockResolvedValue('result-from-step1'), }, { label: 'Step 2', @@ -180,7 +180,7 @@ describe('useMultiStepNotification', () => { { label: 'Step 1', type: 'promise' as const, - executor: jest.fn(() => { + executor: vi.fn(() => { executionOrder.push('step1-started'); return step1Promise; }), @@ -189,7 +189,7 @@ describe('useMultiStepNotification', () => { label: 'Step 2 (eager)', type: 'promise' as const, dependsOn: false, - executor: jest.fn(() => { + executor: vi.fn(() => { executionOrder.push('step2-started'); return Promise.resolve('eager-result'); }), @@ -224,7 +224,7 @@ describe('useMultiStepNotification', () => { { label: 'Long Step', type: 'promise' as const, - executor: jest.fn(() => pendingStep), + executor: vi.fn(() => pendingStep), }, ]; @@ -265,7 +265,7 @@ describe('useMultiStepNotification', () => { { label: 'Step 1', type: 'promise' as const, - executor: jest.fn(() => pendingStep), + executor: vi.fn(() => pendingStep), }, ]; @@ -300,7 +300,7 @@ describe('useMultiStepNotification', () => { { label: 'Step 1', type: 'promise' as const, - executor: jest.fn().mockResolvedValue('result1'), + executor: vi.fn().mockResolvedValue('result1'), }, ]; @@ -324,7 +324,7 @@ describe('useMultiStepNotification', () => { { label: 'Step 1', type: 'promise' as const, - executor: jest.fn().mockResolvedValue('result1'), + executor: vi.fn().mockResolvedValue('result1'), }, ]; @@ -355,14 +355,14 @@ describe('useMultiStepNotification', () => { mockedListenToBackgroundTask.mockImplementation((_taskId, handlers) => { onDoneCallback = handlers.onDone as () => void; - return jest.fn(); // cleanup function + return vi.fn(); // cleanup function }); const steps = [ { label: 'SSE Step', type: 'sse' as const, - executor: jest.fn().mockReturnValue({ taskId: 'task-123' }), + executor: vi.fn().mockReturnValue({ taskId: 'task-123' }), }, ]; @@ -391,14 +391,14 @@ describe('useMultiStepNotification', () => { mockedListenToBackgroundTask.mockImplementation((_taskId, handlers) => { onTaskFailedCallback = handlers.onTaskFailed as (data: unknown) => void; - return jest.fn(); + return vi.fn(); }); const steps = [ { label: 'SSE Step', type: 'sse' as const, - executor: jest.fn().mockReturnValue({ taskId: 'task-456' }), + executor: vi.fn().mockReturnValue({ taskId: 'task-456' }), }, ]; @@ -424,12 +424,12 @@ describe('useMultiStepNotification', () => { { label: 'Step 1', type: 'promise' as const, - executor: jest.fn().mockResolvedValue('result1'), + executor: vi.fn().mockResolvedValue('result1'), }, { label: 'Step 2', type: 'promise' as const, - executor: jest.fn().mockResolvedValue('result2'), + executor: vi.fn().mockResolvedValue('result2'), }, ]; diff --git a/react/src/hooks/useBackendAIImageMetaData.test.tsx b/react/src/hooks/useBackendAIImageMetaData.test.tsx index f114e19ffa..891fef485b 100644 --- a/react/src/hooks/useBackendAIImageMetaData.test.tsx +++ b/react/src/hooks/useBackendAIImageMetaData.test.tsx @@ -23,7 +23,7 @@ describe('useBackendAIImageMetaData', () => { ); beforeEach(() => { - (global as any).fetch = jest.fn(async () => + (global as any).fetch = vi.fn(async () => Promise.resolve({ ok: true, status: 200, diff --git a/react/src/hooks/useControllableState.test.ts b/react/src/hooks/useControllableState.test.ts index a8f46ff7df..98f68a7a70 100644 --- a/react/src/hooks/useControllableState.test.ts +++ b/react/src/hooks/useControllableState.test.ts @@ -130,7 +130,7 @@ describe('useControllableState', () => { }); it('test trigger of options', () => { - const trigger = jest.fn(); + const trigger = vi.fn(); const props: any = { value: 3, onChange: trigger, diff --git a/react/src/hooks/useHighlight.test.tsx b/react/src/hooks/useHighlight.test.tsx index 7d04516131..ffb9885432 100644 --- a/react/src/hooks/useHighlight.test.tsx +++ b/react/src/hooks/useHighlight.test.tsx @@ -3,30 +3,31 @@ import { useThemeMode } from './useThemeMode'; import { renderHook, waitFor } from '@testing-library/react'; import { useBAILogger } from 'backend.ai-ui'; import { codeToHtml } from 'shiki'; +import type { Mock } from 'vitest'; // Mock dependencies -jest.mock('./useThemeMode'); -jest.mock('backend.ai-ui', () => ({ - useBAILogger: jest.fn(), +vi.mock('./useThemeMode'); +vi.mock('backend.ai-ui', () => ({ + useBAILogger: vi.fn(), })); -jest.mock('shiki', () => ({ - codeToHtml: jest.fn(), +vi.mock('shiki', () => ({ + codeToHtml: vi.fn(), })); describe('useHighlight', () => { const mockLogger = { - error: jest.fn(), - log: jest.fn(), - warn: jest.fn(), - info: jest.fn(), - debug: jest.fn(), + error: vi.fn(), + log: vi.fn(), + warn: vi.fn(), + info: vi.fn(), + debug: vi.fn(), }; beforeEach(() => { - jest.clearAllMocks(); - (useBAILogger as jest.Mock).mockReturnValue({ logger: mockLogger }); - (useThemeMode as jest.Mock).mockReturnValue({ isDarkMode: false }); - (codeToHtml as jest.Mock).mockResolvedValue('highlighted'); + vi.clearAllMocks(); + (useBAILogger as Mock).mockReturnValue({ logger: mockLogger }); + (useThemeMode as Mock).mockReturnValue({ isDarkMode: false }); + (codeToHtml as Mock).mockResolvedValue('highlighted'); }); describe('Basic Functionality', () => { @@ -71,7 +72,7 @@ describe('useHighlight', () => { describe('Theme Mode Support', () => { it('should use dark theme when dark mode is enabled', async () => { - (useThemeMode as jest.Mock).mockReturnValue({ isDarkMode: true }); + (useThemeMode as Mock).mockReturnValue({ isDarkMode: true }); const { result } = renderHook(() => useHighlight('const x = 1;', 'javascript'), @@ -103,8 +104,8 @@ describe('useHighlight', () => { const testLanguages = ['javascript', 'typescript', 'python', 'java']; for (const lang of testLanguages) { - jest.clearAllMocks(); - (codeToHtml as jest.Mock).mockResolvedValue('highlighted'); + vi.clearAllMocks(); + (codeToHtml as Mock).mockResolvedValue('highlighted'); const { result } = renderHook(() => useHighlight('test code', lang)); @@ -121,8 +122,8 @@ describe('useHighlight', () => { const testLanguages = ['html', 'xml', 'markdown', 'yaml']; for (const lang of testLanguages) { - jest.clearAllMocks(); - (codeToHtml as jest.Mock).mockResolvedValue('highlighted'); + vi.clearAllMocks(); + (codeToHtml as Mock).mockResolvedValue('highlighted'); const { result } = renderHook(() => useHighlight('test markup', lang)); @@ -139,8 +140,8 @@ describe('useHighlight', () => { const testLanguages = ['bash', 'shell', 'sh']; for (const lang of testLanguages) { - jest.clearAllMocks(); - (codeToHtml as jest.Mock).mockResolvedValue('highlighted'); + vi.clearAllMocks(); + (codeToHtml as Mock).mockResolvedValue('highlighted'); const { result } = renderHook(() => useHighlight('echo "test"', lang)); @@ -161,8 +162,8 @@ describe('useHighlight', () => { ]; for (const { alias, expected } of testAliases) { - jest.clearAllMocks(); - (codeToHtml as jest.Mock).mockResolvedValue('highlighted'); + vi.clearAllMocks(); + (codeToHtml as Mock).mockResolvedValue('highlighted'); const { result } = renderHook(() => useHighlight('test code', alias)); diff --git a/react/src/hooks/useKeyboardShortcut.test.ts b/react/src/hooks/useKeyboardShortcut.test.ts index 53153779e3..6b7bdccd29 100644 --- a/react/src/hooks/useKeyboardShortcut.test.ts +++ b/react/src/hooks/useKeyboardShortcut.test.ts @@ -1,9 +1,10 @@ import useKeyboardShortcut from './useKeyboardShortcut'; import { renderHook } from '@testing-library/react'; +import type { Mock } from 'vitest'; // Mock ahooks useEventListener -jest.mock('ahooks', () => ({ - useEventListener: jest.fn((event, handler) => { +vi.mock('ahooks', () => ({ + useEventListener: vi.fn((event, handler) => { // Store handler for testing (global as any).__eventListeners = (global as any).__eventListeners || {}; (global as any).__eventListeners[event] = handler; @@ -11,10 +12,10 @@ jest.mock('ahooks', () => ({ })); describe('useKeyboardShortcut', () => { - let mockHandler: jest.Mock; + let mockHandler: Mock; beforeEach(() => { - mockHandler = jest.fn(); + mockHandler = vi.fn(); // Clear stored event listeners (global as any).__eventListeners = {}; // Clear DOM @@ -22,7 +23,7 @@ describe('useKeyboardShortcut', () => { }); afterEach(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); }); const triggerKeydown = (options: Partial = {}) => { diff --git a/react/src/hooks/useLoginOrchestration.test.ts b/react/src/hooks/useLoginOrchestration.test.ts index 3799a43f4d..7c7b337548 100644 --- a/react/src/hooks/useLoginOrchestration.test.ts +++ b/react/src/hooks/useLoginOrchestration.test.ts @@ -49,20 +49,18 @@ function setWindowPathname(pathname: string): void { // --------------------------------------------------------------------------- // Prevent TabCount from starting real setInterval timers in jsdom. -jest.mock('../lib/TabCounter', () => { +// The real implementation is assigned per-test via `MockedTabCount.mockImplementation(function() {...})` +// so that `new TabCount()` (arrow functions can't be constructors in Vitest 4). +vi.mock('../lib/TabCounter', () => { return { __esModule: true, - default: jest.fn().mockImplementation(() => ({ - tabsCounter: 1, - tabsCount: jest.fn().mockReturnValue(1), - pause: jest.fn(), - })), + default: vi.fn(), }; }); // Mock loadConfigFromWebServer so we don't make real network requests. -jest.mock('../helper/loginSessionAuth', () => ({ - loadConfigFromWebServer: jest.fn().mockResolvedValue(undefined), +vi.mock('../helper/loginSessionAuth', () => ({ + loadConfigFromWebServer: vi.fn().mockResolvedValue(undefined), })); const MockedTabCount = TabCount as jest.MockedClass; @@ -98,11 +96,11 @@ function makeOptions( overrides: Partial[0]> = {}, ) { return { - onLogin: jest.fn().mockResolvedValue(undefined), - onOpen: jest.fn(), - onBlock: jest.fn(), - onCheckLogin: jest.fn().mockResolvedValue(false), - onLogoutSession: jest.fn().mockResolvedValue(undefined), + onLogin: vi.fn().mockResolvedValue(undefined), + onOpen: vi.fn(), + onBlock: vi.fn(), + onCheckLogin: vi.fn().mockResolvedValue(false), + onLogoutSession: vi.fn().mockResolvedValue(undefined), apiEndpoint: 'https://api.example.com', connectionMode: 'SESSION' as const, ...overrides, @@ -156,22 +154,24 @@ beforeEach(() => { // Default: non-reload navigation mockNavigationType('navigate'); - // Reset TabCount mock: single tab, not reloaded - MockedTabCount.mockImplementation( - () => - ({ - tabsCounter: 1, - tabsCount: jest.fn().mockReturnValue(1), - pause: jest.fn(), - }) as unknown as TabCount, - ); + // Reset TabCount mock: single tab, not reloaded. + // Use a regular `function` so `new TabCount()` works as a constructor in Vitest 4. + MockedTabCount.mockImplementation(function (this: any) { + this.tabsCounter = 1; + this.tabsCount = vi.fn().mockReturnValue(1); + this.pause = vi.fn(); + } as unknown as () => TabCount); mockedLoadConfig.mockResolvedValue(undefined); }); afterEach(() => { clearClient(); - jest.restoreAllMocks(); + // `vi.restoreAllMocks()` only affects `spyOn` spies. Factory-created mocks + // (e.g. `mockedLoadConfig`, `MockedTabCount`) need `clearAllMocks` to wipe + // their call history between tests. + vi.clearAllMocks(); + vi.restoreAllMocks(); backendaiOptions.set('last_window_close_time', 0); }); @@ -307,14 +307,11 @@ describe('useLoginOrchestration - normal flow', () => { it('calls onLogin(false) when there are multiple tabs even with auto_logout on', async () => { // Multiple tabs: tabsCounter > 1 - MockedTabCount.mockImplementation( - () => - ({ - tabsCounter: 2, - tabsCount: jest.fn().mockReturnValue(2), - pause: jest.fn(), - }) as unknown as TabCount, - ); + MockedTabCount.mockImplementation(function (this: any) { + this.tabsCounter = 2; + this.tabsCount = vi.fn().mockReturnValue(2); + this.pause = vi.fn(); + } as unknown as () => TabCount); const { options } = await renderOrchestrationHook(true, true); expect(options.onLogin).toHaveBeenCalledWith(false); expect(options.onOpen).not.toHaveBeenCalled(); @@ -382,14 +379,11 @@ describe('useLoginOrchestration - Electron', () => { describe('useLoginOrchestration - auto-logout (single tab, fresh navigation)', () => { beforeEach(() => { // Single tab, fresh navigation (not a reload) - MockedTabCount.mockImplementation( - () => - ({ - tabsCounter: 1, - tabsCount: jest.fn().mockReturnValue(1), - pause: jest.fn(), - }) as unknown as TabCount, - ); + MockedTabCount.mockImplementation(function (this: any) { + this.tabsCounter = 1; + this.tabsCount = vi.fn().mockReturnValue(1); + this.pause = vi.fn(); + } as unknown as () => TabCount); mockNavigationType('navigate'); }); @@ -398,7 +392,7 @@ describe('useLoginOrchestration - auto-logout (single tab, fresh navigation)', ( backendaiOptions.set('last_window_close_time', now - 10); // 10 seconds ago const { options } = await renderOrchestrationHook(true, true, { - onCheckLogin: jest.fn().mockResolvedValue(true), // logged in but stale + onCheckLogin: vi.fn().mockResolvedValue(true), // logged in but stale }); expect(options.onLogoutSession).toHaveBeenCalled(); @@ -411,7 +405,7 @@ describe('useLoginOrchestration - auto-logout (single tab, fresh navigation)', ( backendaiOptions.set('last_window_close_time', now - 1); // 1 second ago const { options } = await renderOrchestrationHook(true, true, { - onCheckLogin: jest.fn().mockResolvedValue(true), // logged in, recent + onCheckLogin: vi.fn().mockResolvedValue(true), // logged in, recent }); expect(options.onLogoutSession).not.toHaveBeenCalled(); @@ -424,7 +418,7 @@ describe('useLoginOrchestration - auto-logout (single tab, fresh navigation)', ( backendaiOptions.set('last_window_close_time', now - 1); const { options } = await renderOrchestrationHook(true, true, { - onCheckLogin: jest.fn().mockResolvedValue(false), // not logged in + onCheckLogin: vi.fn().mockResolvedValue(false), // not logged in }); expect(options.onOpen).toHaveBeenCalled(); @@ -439,7 +433,7 @@ describe('useLoginOrchestration - auto-logout (single tab, fresh navigation)', ( backendaiOptions.delete('last_window_close_time'); const { options } = await renderOrchestrationHook(true, true, { - onCheckLogin: jest.fn().mockResolvedValue(true), + onCheckLogin: vi.fn().mockResolvedValue(true), }); // currentTime - currentTime === 0 which is NOT > 3, so silent re-login @@ -456,7 +450,7 @@ describe('useLoginOrchestration - error handling', () => { it('calls onBlock when orchestration throws', async () => { // Force an error by making onCheckLogin throw const { options } = await renderOrchestrationHook(true, true, { - onCheckLogin: jest.fn().mockRejectedValue(new Error('network failure')), + onCheckLogin: vi.fn().mockRejectedValue(new Error('network failure')), }); expect(options.onBlock).toHaveBeenCalledWith( diff --git a/react/src/hooks/useLogout.test.ts b/react/src/hooks/useLogout.test.ts index 19eb239a1f..5d8dd790e1 100644 --- a/react/src/hooks/useLogout.test.ts +++ b/react/src/hooks/useLogout.test.ts @@ -41,7 +41,7 @@ const wrapper = ({ children }: { children: React.ReactNode }) => // Mock react-i18next so that t() returns the key as-is, making assertions // independent of the actual translation strings. -jest.mock('react-i18next', () => ({ +vi.mock('react-i18next', () => ({ useTranslation: () => ({ t: (key: string) => key, }), @@ -51,7 +51,7 @@ jest.mock('react-i18next', () => ({ // Helper: set up a fake backendaiclient on globalThis // --------------------------------------------------------------------------- function makeFakeClient(connectionMode: 'SESSION' | 'API' = 'SESSION') { - const logoutFn = jest.fn().mockResolvedValue(undefined); + const logoutFn = vi.fn().mockResolvedValue(undefined); (globalThis as any).backendaiclient = { _config: { connectionMode }, logout: logoutFn, @@ -360,7 +360,7 @@ describe('useLogoutEventListeners', () => { }); it('registers backend-ai-logout listener on mount', () => { - const addSpy = jest.spyOn(document, 'addEventListener'); + const addSpy = vi.spyOn(document, 'addEventListener'); const { unmount } = renderHook(() => useLogoutEventListeners(), { wrapper, @@ -374,7 +374,7 @@ describe('useLogoutEventListeners', () => { }); it('removes backend-ai-logout listener on unmount', () => { - const removeSpy = jest.spyOn(document, 'removeEventListener'); + const removeSpy = vi.spyOn(document, 'removeEventListener'); const { unmount } = renderHook(() => useLogoutEventListeners(), { wrapper, @@ -389,7 +389,7 @@ describe('useLogoutEventListeners', () => { }); it('registers beforeunload listener on mount', () => { - const addSpy = jest.spyOn(globalThis, 'addEventListener'); + const addSpy = vi.spyOn(globalThis, 'addEventListener'); const { unmount } = renderHook(() => useLogoutEventListeners(), { wrapper, @@ -403,7 +403,7 @@ describe('useLogoutEventListeners', () => { }); it('removes beforeunload listener on unmount', () => { - const removeSpy = jest.spyOn(globalThis, 'removeEventListener'); + const removeSpy = vi.spyOn(globalThis, 'removeEventListener'); const { unmount } = renderHook(() => useLogoutEventListeners(), { wrapper, @@ -419,7 +419,7 @@ describe('useLogoutEventListeners', () => { it('registers backend-ai-app-close listener when isElectron is true', () => { (globalThis as any).isElectron = true; - const addSpy = jest.spyOn(document, 'addEventListener'); + const addSpy = vi.spyOn(document, 'addEventListener'); const { unmount } = renderHook(() => useLogoutEventListeners(), { wrapper, @@ -434,7 +434,7 @@ describe('useLogoutEventListeners', () => { it('does NOT register backend-ai-app-close listener when isElectron is false', () => { (globalThis as any).isElectron = false; - const addSpy = jest.spyOn(document, 'addEventListener'); + const addSpy = vi.spyOn(document, 'addEventListener'); const { unmount } = renderHook(() => useLogoutEventListeners(), { wrapper, @@ -507,7 +507,7 @@ describe('LogoutEventHandler component', () => { }); it('registers event listeners when mounted', () => { - const addSpy = jest.spyOn(document, 'addEventListener'); + const addSpy = vi.spyOn(document, 'addEventListener'); const { unmount } = render( React.createElement( diff --git a/react/src/hooks/useMemoWithPrevious.test.tsx b/react/src/hooks/useMemoWithPrevious.test.tsx index 927400bd83..4b668436c7 100644 --- a/react/src/hooks/useMemoWithPrevious.test.tsx +++ b/react/src/hooks/useMemoWithPrevious.test.tsx @@ -7,7 +7,7 @@ import { renderHook, act } from '@testing-library/react'; describe('useMemoWithPrevious Hook', () => { it('should initialize with initialPrev', () => { - const factory = jest.fn(() => 'current value'); + const factory = vi.fn(() => 'current value'); const { result } = renderHook(() => useMemoWithPrevious(factory, [], { initialPrev: 'initial previous value', @@ -21,7 +21,7 @@ describe('useMemoWithPrevious Hook', () => { it('should update current and previous when dependencies change', () => { let dep = 1; - const factory = jest.fn(() => `current value ${dep}`); + const factory = vi.fn(() => `current value ${dep}`); const { result, rerender } = renderHook(() => useMemoWithPrevious(factory, [dep]), @@ -46,7 +46,7 @@ describe('useMemoWithPrevious Hook', () => { it('should reset previous when resetPrevious is called', () => { let dep = 1; - const factory = jest.fn(() => `current value ${dep}`); + const factory = vi.fn(() => `current value ${dep}`); const { result, rerender } = renderHook(() => useMemoWithPrevious(factory, [dep], { @@ -76,7 +76,7 @@ describe('useMemoWithPrevious Hook', () => { }); it('should not update previous if dependencies do not change', () => { - const factory = jest.fn(() => 'current value'); + const factory = vi.fn(() => 'current value'); const { result, rerender } = renderHook(() => useMemoWithPrevious(factory, []), ); diff --git a/react/src/hooks/usePrimaryColors.test.tsx b/react/src/hooks/usePrimaryColors.test.tsx index 49acadb219..bdebd53913 100644 --- a/react/src/hooks/usePrimaryColors.test.tsx +++ b/react/src/hooks/usePrimaryColors.test.tsx @@ -4,8 +4,8 @@ import { ConfigProvider } from 'antd'; import React from 'react'; // Mock @ant-design/colors -jest.mock('@ant-design/colors', () => ({ - generate: jest.fn((color: string) => [ +vi.mock('@ant-design/colors', () => ({ + generate: vi.fn((color: string) => [ `${color}-1`, `${color}-2`, `${color}-3`, diff --git a/react/src/hooks/useScrollBreackPoint.test.tsx b/react/src/hooks/useScrollBreackPoint.test.tsx index cb5b060c37..f0541f61ab 100644 --- a/react/src/hooks/useScrollBreackPoint.test.tsx +++ b/react/src/hooks/useScrollBreackPoint.test.tsx @@ -18,7 +18,7 @@ describe('useScrollBreakPoint', () => { }); afterEach(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); }); describe('Basic Functionality', () => { @@ -487,7 +487,7 @@ describe('useScrollBreakPoint', () => { describe('Cleanup', () => { it('should cleanup event listeners on unmount', () => { - const removeEventListenerSpy = jest.spyOn(window, 'removeEventListener'); + const removeEventListenerSpy = vi.spyOn(window, 'removeEventListener'); const { unmount } = renderHook(() => useScrollBreakPoint({ x: 100, y: 200 }), @@ -505,7 +505,7 @@ describe('useScrollBreakPoint', () => { it('should cleanup element event listeners on unmount', () => { const element = document.createElement('div'); - const removeEventListenerSpy = jest.spyOn(element, 'removeEventListener'); + const removeEventListenerSpy = vi.spyOn(element, 'removeEventListener'); const { unmount } = renderHook(() => useScrollBreakPoint({ x: 100, y: 200 }, element), @@ -525,11 +525,8 @@ describe('useScrollBreakPoint', () => { const element1 = document.createElement('div'); const element2 = document.createElement('div'); - const removeEventListenerSpy1 = jest.spyOn( - element1, - 'removeEventListener', - ); - const addEventListenerSpy2 = jest.spyOn(element2, 'addEventListener'); + const removeEventListenerSpy1 = vi.spyOn(element1, 'removeEventListener'); + const addEventListenerSpy2 = vi.spyOn(element2, 'addEventListener'); const { rerender } = renderHook( ({ element }) => useScrollBreakPoint({ x: 100, y: 200 }, element), diff --git a/react/src/hooks/useTokenizer.test.ts b/react/src/hooks/useTokenizer.test.ts index 8b6f30e557..0b4004748f 100644 --- a/react/src/hooks/useTokenizer.test.ts +++ b/react/src/hooks/useTokenizer.test.ts @@ -1,10 +1,11 @@ import { useTokenCount, encodeAsync } from './useTokenizer'; import { renderHook, waitFor } from '@testing-library/react'; import { encode } from 'gpt-tokenizer'; +import type { Mock } from 'vitest'; // Mock gpt-tokenizer -jest.mock('gpt-tokenizer', () => ({ - encode: jest.fn((str: string) => Array(str.length).fill(0)), // Mock: 1 token per character +vi.mock('gpt-tokenizer', () => ({ + encode: vi.fn((str: string) => Array(str.length).fill(0)), // Mock: 1 token per character })); describe('useTokenizer', () => { @@ -60,7 +61,7 @@ describe('useTokenizer', () => { }); it('should fallback to string length on encoding error', async () => { - (encode as jest.Mock).mockImplementationOnce(() => { + (encode as Mock).mockImplementationOnce(() => { throw new Error('Encoding failed'); }); diff --git a/react/src/hooks/useValidateServiceName.test.tsx b/react/src/hooks/useValidateServiceName.test.tsx index 12195f8051..679eccdc9f 100644 --- a/react/src/hooks/useValidateServiceName.test.tsx +++ b/react/src/hooks/useValidateServiceName.test.tsx @@ -2,7 +2,7 @@ import { useValidateServiceName } from './useValidateServiceName'; import { renderHook } from '@testing-library/react'; // Mock react-i18next -jest.mock('react-i18next', () => ({ +vi.mock('react-i18next', () => ({ useTranslation: () => ({ t: (key: string) => key, }), diff --git a/react/src/hooks/useValidateSessionName.test.tsx b/react/src/hooks/useValidateSessionName.test.tsx index 964cd27161..ee7432bfad 100644 --- a/react/src/hooks/useValidateSessionName.test.tsx +++ b/react/src/hooks/useValidateSessionName.test.tsx @@ -3,7 +3,7 @@ import { renderHook } from '@testing-library/react'; import type { RuleObject } from 'antd/es/form'; // Mock react-i18next to return translation keys -jest.mock('react-i18next', () => ({ +vi.mock('react-i18next', () => ({ useTranslation: () => ({ t: (key: string) => key, }), diff --git a/react/src/hooks/useVariantConfigs.test.ts b/react/src/hooks/useVariantConfigs.test.ts index 4bc214dc4b..954843ff2b 100644 --- a/react/src/hooks/useVariantConfigs.test.ts +++ b/react/src/hooks/useVariantConfigs.test.ts @@ -1,18 +1,19 @@ import { useRuntimeEnvVarConfigs } from './useVariantConfigs'; import { renderHook } from '@testing-library/react'; import { useTranslation } from 'react-i18next'; +import type { Mock } from 'vitest'; // Mock react-i18next -jest.mock('react-i18next', () => ({ - useTranslation: jest.fn(), +vi.mock('react-i18next', () => ({ + useTranslation: vi.fn(), })); describe('useRuntimeEnvVarConfigs', () => { - const mockT = jest.fn((key: string) => key); + const mockT = vi.fn((key: string) => key); beforeEach(() => { - jest.clearAllMocks(); - (useTranslation as jest.Mock).mockReturnValue({ + vi.clearAllMocks(); + (useTranslation as Mock).mockReturnValue({ t: mockT, i18n: {}, }); diff --git a/react/src/hooks/useWebUIConfig.test.ts b/react/src/hooks/useWebUIConfig.test.ts index 7df0ccce64..28dccd44a6 100644 --- a/react/src/hooks/useWebUIConfig.test.ts +++ b/react/src/hooks/useWebUIConfig.test.ts @@ -33,7 +33,7 @@ function mockFetch( body: string, contentType: string = 'application/octet-stream', ): void { - global.fetch = jest.fn().mockResolvedValue({ + global.fetch = vi.fn().mockResolvedValue({ status, text: () => Promise.resolve(body), headers: new Headers({ 'content-type': contentType }), @@ -46,7 +46,7 @@ function mockFetch( describe('fetchAndParseConfig', () => { afterEach(() => { - jest.restoreAllMocks(); + vi.restoreAllMocks(); }); it('returns null config when fetch response status is not 200', async () => { @@ -57,7 +57,7 @@ describe('fetchAndParseConfig', () => { }); it('returns null config without error when fetch throws (network failure)', async () => { - global.fetch = jest.fn().mockRejectedValue(new Error('Network error')); + global.fetch = vi.fn().mockRejectedValue(new Error('Network error')); const result = await fetchAndParseConfig('/config.toml'); expect(result.config).toBeNull(); expect(result.error).toBeUndefined(); @@ -229,7 +229,7 @@ debug = true describe('preprocessToml via fetchAndParseConfig', () => { afterEach(() => { - jest.restoreAllMocks(); + vi.restoreAllMocks(); }); it('does not modify config when general section is absent', async () => { diff --git a/react/src/lib/TabCounter.test.ts b/react/src/lib/TabCounter.test.ts index 8d5e9fe498..80b0955cd9 100644 --- a/react/src/lib/TabCounter.test.ts +++ b/react/src/lib/TabCounter.test.ts @@ -30,7 +30,7 @@ import TabCount from './TabCounter'; * callbacks driven by Date.now() see consistent timestamps. */ function advanceTimers(ms: number): void { - jest.advanceTimersByTime(ms); + vi.advanceTimersByTime(ms); } // --------------------------------------------------------------------------- @@ -38,7 +38,7 @@ function advanceTimers(ms: number): void { // --------------------------------------------------------------------------- beforeEach(() => { - jest.useFakeTimers(); + vi.useFakeTimers(); localStorage.clear(); // Provide a stable crypto.randomUUID implementation for the jsdom environment if (!globalThis.crypto?.randomUUID) { @@ -50,9 +50,9 @@ beforeEach(() => { }); afterEach(() => { - jest.useRealTimers(); + vi.useRealTimers(); localStorage.clear(); - jest.restoreAllMocks(); + vi.restoreAllMocks(); }); // --------------------------------------------------------------------------- @@ -78,7 +78,7 @@ describe('TabCount constructor', () => { }); it('registers a beforeunload listener that removes the tab from localStorage', () => { - const addSpy = jest.spyOn(globalThis, 'addEventListener'); + const addSpy = vi.spyOn(globalThis, 'addEventListener'); const tc = new TabCount(); const calls = addSpy.mock.calls.map(([event]) => event); @@ -183,7 +183,7 @@ describe('tabsCount', () => { it('does not call onTabCountUpdate when skipCallback is true', () => { const tc = new TabCount(); - const cb = jest.fn(); + const cb = vi.fn(); tc.onTabCountUpdate.push(cb); tc.tabsCount(true); expect(cb).not.toHaveBeenCalled(); @@ -193,7 +193,7 @@ describe('tabsCount', () => { it('calls onTabCountUpdate when count changes and skipCallback is false', () => { const tc = new TabCount(); tc.tabsCounter = 99; // Force a different value to ensure a change is detected - const cb = jest.fn(); + const cb = vi.fn(); tc.onTabCountUpdate.push(cb); tc.tabsCount(false); expect(cb).toHaveBeenCalled(); @@ -257,7 +257,7 @@ describe('clearList', () => { describe('onTabChange', () => { it('registers a callback that fires when tab count changes', () => { const tc = new TabCount(); - const cb = jest.fn(); + const cb = vi.fn(); tc.onTabChange(cb, false); expect(tc.onTabCountUpdate).toContain(cb); tc.pause(); @@ -273,7 +273,7 @@ describe('onTabChange', () => { it('calls the callback immediately when executeNow is true', () => { const tc = new TabCount(); - const cb = jest.fn(); + const cb = vi.fn(); tc.onTabChange(cb, true); expect(cb).toHaveBeenCalledTimes(1); expect(cb).toHaveBeenCalledWith(expect.any(Number)); @@ -282,7 +282,7 @@ describe('onTabChange', () => { it('does not call the callback immediately when executeNow is false', () => { const tc = new TabCount(); - const cb = jest.fn(); + const cb = vi.fn(); tc.onTabChange(cb, false); expect(cb).not.toHaveBeenCalled(); tc.pause(); diff --git a/react/vitest.config.ts b/react/vitest.config.ts new file mode 100644 index 0000000000..0f17606417 --- /dev/null +++ b/react/vitest.config.ts @@ -0,0 +1,101 @@ +import react from '@vitejs/plugin-react'; +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import svgr from 'vite-plugin-svgr'; +import { defineConfig } from 'vitest/config'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const projectRoot = resolve(__dirname, '..'); +const buiSrc = resolve(projectRoot, 'packages/backend.ai-ui/src'); +const buiArtifactDir = resolve(buiSrc, '__generated__'); +const reactSrc = resolve(__dirname, 'src'); +const reactArtifactDir = resolve(reactSrc, '__generated__'); + +/** + * Vitest config for the `react/` workspace (FR-2609). + * + * Deliberately separate from `vite.config.ts`: the main Vite config contains + * dev-server middlewares, `transformIndexHtml` hooks, PWA generation, and + * other things that either do not apply in a test runner or would slow it + * down. What we share with vite.config.ts is the TRANSFORM pipeline — the + * `@vitejs/plugin-react` configuration with `babel-plugin-relay` + * (per-directory artifactDirectory) and `babel-plugin-react-compiler`. + * + * The intent is that a single source file produces the same transformed + * code under both `vite:dev` and `vitest`, so tests and the running app + * exercise identical code paths (e.g., React Compiler `'use memo'` + * optimisations are active in tests too). + */ +export default defineConfig({ + resolve: { + alias: [ + // Mirror the `src/` baseUrl import in tsconfig.json so + // `import 'src/hooks/foo'` resolves under Vitest too. + { find: /^src\//, replacement: reactSrc + '/' }, + // Workspace package alias (dev-source path, matching the Jest + // moduleNameMapper entry for `backend.ai-ui`). + { find: /^backend\.ai-ui\/dist(\/|$)/, replacement: buiSrc + '$1' }, + { find: /^backend\.ai-ui$/, replacement: buiSrc }, + // `backend.ai-client-esm` is mocked in tests (see __test__/backendAiClientEsm.mock.js). + // The Jest moduleNameMapper handled this explicitly; we use an alias instead. + { + find: /^backend\.ai-client-esm$/, + replacement: resolve(__dirname, '__test__/backendAiClientEsm.mock.js'), + }, + // Existing `.svg` (plain import, not SVGR `?react`) module mock. + // SVGR `?react` imports are handled by `vite-plugin-svgr` below. + { find: /\.svg$/, replacement: resolve(__dirname, '__test__/svg.mock.js') }, + // CSS imports (both `.css` and `.css?raw`) go through the same mock. + // Array-form aliases REPLACE the matched portion, so we have to match + // the entire specifier. The regex below anchors both ends via `^.+`. + { + find: /^.+\.(css|less|scss|sass)(\?raw)?$/, + replacement: resolve(__dirname, '__test__/rawCss.mock.js'), + }, + // `bui-language` helper was mocked by Jest. Replicate the mapping. + { + find: /^.*\/helper\/bui-language$/, + replacement: resolve(__dirname, '__test__/buiLanguage.mock.js'), + }, + ], + }, + + plugins: [ + react({ + babel: (id) => { + const isBUI = id.startsWith(buiSrc); + const isReactSrc = id.startsWith(reactSrc); + const plugins: Array = [ + ['babel-plugin-react-compiler', { compilationMode: 'annotation' }], + ]; + if (isBUI) { + plugins.push([ + 'babel-plugin-relay', + { artifactDirectory: buiArtifactDir }, + ]); + } else if (isReactSrc) { + plugins.push([ + 'babel-plugin-relay', + { artifactDirectory: reactArtifactDir }, + ]); + } + return { plugins }; + }, + }), + svgr({ include: '**/*.svg?react' }), + ], + + test: { + globals: true, + environment: 'jsdom', + setupFiles: [ + resolve(__dirname, 'src/setupTests.ts'), + // Map `jest.*` helpers to their `vi.*` equivalents so tests written + // against Jest can run under Vitest without per-file renames. + // This is a migration aid; new tests should use `vi.*` directly. + resolve(__dirname, '__test__/vitest.jest-compat.ts'), + ], + include: ['src/**/*.{test,spec}.{ts,tsx}'], + exclude: ['**/node_modules/**', '**/build/**', '**/__generated__/**'], + }, +});