Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
572 changes: 560 additions & 12 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

44 changes: 43 additions & 1 deletion react/VITE_POC_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
27 changes: 27 additions & 0 deletions react/__test__/vitest.jest-compat.ts
Original file line number Diff line number Diff line change
@@ -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;
4 changes: 4 additions & 0 deletions react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand All @@ -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"
}
Expand Down
32 changes: 17 additions & 15 deletions react/src/components/MyResourceWithinResourceGroup.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) => (
<div data-testid="segmented">{children}</div>
),
Expand All @@ -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',
}));
Expand Down Expand Up @@ -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: {} },
Expand All @@ -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: () => ({
Expand All @@ -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;
Expand Down Expand Up @@ -190,12 +190,14 @@ jest.mock('backend.ai-ui', () => {
};
});

jest.mock('./SharedResourceGroupSelectForCurrentProject', () => {
vi.mock('./SharedResourceGroupSelectForCurrentProject', () => {
const MockedComponent = () => (
<div data-testid="resource-group-select">Select</div>
);
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
Expand All @@ -212,7 +214,7 @@ const createMockReturnValue = (checkPresetInfo: any) =>
checkPresetInfo,
},
{
refetch: jest.fn(),
refetch: vi.fn(),
},
] as const;

Expand All @@ -237,7 +239,7 @@ TestWrapper.displayName = 'TestWrapper';
describe('MyResourceWithinResourceGroup', () => {
let queryClient: QueryClient;

const mockHook = jest.spyOn(
const mockHook = vi.spyOn(
useResourceLimitAndRemainingModule,
'useResourceLimitAndRemaining',
);
Expand All @@ -249,7 +251,7 @@ describe('MyResourceWithinResourceGroup', () => {
});

afterEach(() => {
jest.clearAllMocks();
vi.clearAllMocks();
mockHook.mockReset();
});

Expand Down
1 change: 0 additions & 1 deletion react/src/diagnostics/rules/__tests__/configRules.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import {
checkSslMismatch,
checkUrlFields,
} from '../configRules';
import { describe, expect, it } from '@jest/globals';

const validMenuKeys = [
'start',
Expand Down
1 change: 0 additions & 1 deletion react/src/diagnostics/rules/__tests__/cspRules.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
10 changes: 5 additions & 5 deletions react/src/global-stores.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);

Expand All @@ -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(),
Expand All @@ -279,11 +279,11 @@ describe('BackendAIMetadataStore', () => {

describe('BackendAITasker', () => {
beforeEach(() => {
jest.useFakeTimers();
vi.useFakeTimers();
});

afterEach(() => {
jest.useRealTimers();
vi.useRealTimers();
});

describe('add()', () => {
Expand Down
1 change: 0 additions & 1 deletion react/src/helper/big-number.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' {
Expand Down
Loading
Loading