diff --git a/__test__/vitest.jest-compat.ts b/__test__/vitest.jest-compat.ts deleted file mode 100644 index 4a9013a12f..0000000000 --- a/__test__/vitest.jest-compat.ts +++ /dev/null @@ -1,15 +0,0 @@ -// Vitest ↔ Jest compatibility shim for the repo-root `/src` and `/scripts` -// test suites (FR-2609). -// -// Mirrors the shims in `react/__test__/vitest.jest-compat.ts` and -// `packages/backend.ai-ui/__test__/vitest.jest-compat.ts`. New tests should -// use `vi.*` directly; this is a migration aid. -import { vi } from "vitest"; - -// `globals: true` in vitest.config.ts exposes `vi`/`describe`/`test`/etc as -// globals. This line ALSO exposes `vi` as `jest` so prior Jest-style calls -// (`jest.fn()`, `jest.spyOn()`, etc.) still resolve. -// NOTE: `jest.mock()` calls are NOT hoisted by Vitest — only literal -// `vi.mock()` is. Any test using `jest.mock()` must migrate to `vi.mock()`. -// eslint-disable-next-line @typescript-eslint/no-explicit-any -(globalThis as any).jest = vi; diff --git a/packages/backend.ai-ui/__test__/jest-globals-shim.ts b/packages/backend.ai-ui/__test__/jest-globals-shim.ts deleted file mode 100644 index 1e5ac5f9bf..0000000000 --- a/packages/backend.ai-ui/__test__/jest-globals-shim.ts +++ /dev/null @@ -1,18 +0,0 @@ -// `@jest/globals` re-export shim for Vitest (FR-2609). -// -// Some BUI tests import `{ describe, test, expect, jest }` from -// `'@jest/globals'`. Vitest's `globals: true` already makes these available -// as globals, and it also has direct named exports on `vitest` — but the -// two files that use this import style expect a module specifier of -// `@jest/globals`. The vitest.config alias maps that specifier to this file. -export { - describe, - test, - it, - expect, - beforeAll, - beforeEach, - afterAll, - afterEach, - vi as jest, -} from 'vitest'; diff --git a/packages/backend.ai-ui/__test__/vitest.jest-compat.ts b/packages/backend.ai-ui/__test__/vitest.jest-compat.ts deleted file mode 100644 index 7687ca936e..0000000000 --- a/packages/backend.ai-ui/__test__/vitest.jest-compat.ts +++ /dev/null @@ -1,14 +0,0 @@ -// Vitest ↔ Jest compatibility shim for the BUI package (FR-2609). -// -// BUI tests still use `jest.fn()`, `jest.mock()`, `jest.spyOn()` etc. Rather -// than rename every call site, we expose `vi` under the name `jest` so -// existing tests run unchanged. New tests should use `vi.*` directly. -// -// Mirrors `react/__test__/vitest.jest-compat.ts`. -import { vi } from 'vitest'; - -// `globals: true` in vitest.config.ts exposes `vi`/`describe`/`test`/etc as -// globals. This line ALSO exposes `vi` as `jest` so prior Jest-style calls -// still resolve. -// eslint-disable-next-line @typescript-eslint/no-explicit-any -(globalThis as any).jest = vi; diff --git a/packages/backend.ai-ui/setupTests.ts b/packages/backend.ai-ui/setupTests.ts index c484bbe089..98e32804d0 100644 --- a/packages/backend.ai-ui/setupTests.ts +++ b/packages/backend.ai-ui/setupTests.ts @@ -53,13 +53,24 @@ if (typeof MutationObserver !== 'undefined' && typeof document !== 'undefined') }); } -// jest-dom adds custom jest matchers for asserting on DOM nodes. +// jest-dom adds custom matchers for asserting on DOM nodes. // allows you to do things like: // expect(element).toHaveTextContent(/react/i) // learn more: https://github.com/testing-library/jest-dom import './src/__test__/matchMedia.mock.cjs'; import '@testing-library/jest-dom'; +// Expose `vi` under the global name `jest` so `@testing-library/dom`'s +// `waitFor` detects "Jest fake timers are active" and switches to its +// timer-aware polling path. Without this, tests that combine +// `vi.useFakeTimers()` with `await waitFor(...)` hang — waitFor's default +// polling uses `setTimeout`, which never fires under faked timers. +// (None of our test code references `jest.*` directly anymore; this is +// purely a `@testing-library/dom` integration hook.) +import { vi } from 'vitest'; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +(globalThis as any).jest = vi; + // Mock ResizeObserver for Ant Design v6 components global.ResizeObserver = class ResizeObserver { observe() {} diff --git a/packages/backend.ai-ui/src/__test__/matchMedia.mock.cjs b/packages/backend.ai-ui/src/__test__/matchMedia.mock.cjs index 9942ffeaa5..7a709f9752 100644 --- a/packages/backend.ai-ui/src/__test__/matchMedia.mock.cjs +++ b/packages/backend.ai-ui/src/__test__/matchMedia.mock.cjs @@ -2,7 +2,7 @@ * matchMedia mock for Jest testing environment * * This mock is loaded before tests run to provide a basic matchMedia implementation - * for components that use media queries. It avoids direct jest.fn() calls at module + * for components that use media queries. It avoids direct vi.fn() calls at module * load time to work properly with ES modules. */ diff --git a/packages/backend.ai-ui/src/__test__/matchMedia.mock.js b/packages/backend.ai-ui/src/__test__/matchMedia.mock.js index 099fa51778..37a7a9f79e 100644 --- a/packages/backend.ai-ui/src/__test__/matchMedia.mock.js +++ b/packages/backend.ai-ui/src/__test__/matchMedia.mock.js @@ -1,13 +1,14 @@ +/* global vi */ Object.defineProperty(window, 'matchMedia', { writable: true, - value: jest.fn().mockImplementation((query) => ({ + value: vi.fn().mockImplementation((query) => ({ matches: false, media: query, onchange: null, - addListener: jest.fn(), // deprecated - removeListener: jest.fn(), // deprecated - addEventListener: jest.fn(), - removeEventListener: jest.fn(), - dispatchEvent: jest.fn(), + addListener: vi.fn(), // deprecated + removeListener: vi.fn(), // deprecated + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + dispatchEvent: vi.fn(), })), }); diff --git a/packages/backend.ai-ui/src/components/BAIBulkEditFormItem.test.tsx b/packages/backend.ai-ui/src/components/BAIBulkEditFormItem.test.tsx index 02105023e7..ec1aced794 100644 --- a/packages/backend.ai-ui/src/components/BAIBulkEditFormItem.test.tsx +++ b/packages/backend.ai-ui/src/components/BAIBulkEditFormItem.test.tsx @@ -340,7 +340,7 @@ describe('BAIBulkEditFormItem', () => { }); it('should allow user selection in edit mode', async () => { - const onValuesChange = jest.fn(); + const onValuesChange = vi.fn(); const user = setupUser(); render( diff --git a/packages/backend.ai-ui/src/components/BAIButton.test.tsx b/packages/backend.ai-ui/src/components/BAIButton.test.tsx index 4b1612e2bb..582be81886 100644 --- a/packages/backend.ai-ui/src/components/BAIButton.test.tsx +++ b/packages/backend.ai-ui/src/components/BAIButton.test.tsx @@ -31,7 +31,7 @@ describe('BAIButton', () => { describe('onClick Handler', () => { it('should call onClick handler when clicked', async () => { - const onClick = jest.fn(); + const onClick = vi.fn(); const user = userEvent.setup(); render(Click); @@ -40,7 +40,7 @@ describe('BAIButton', () => { }); it('should not call onClick if disabled', async () => { - const onClick = jest.fn(); + const onClick = vi.fn(); const user = userEvent.setup(); render( @@ -55,7 +55,7 @@ describe('BAIButton', () => { describe('Async Action Handling', () => { it('should execute async action when provided', async () => { - const action = jest.fn().mockResolvedValue(undefined); + const action = vi.fn().mockResolvedValue(undefined); const user = userEvent.setup(); render(Execute Action); @@ -67,7 +67,7 @@ describe('BAIButton', () => { }); it('should show loading state during async action', async () => { - const action = jest + const action = vi .fn() .mockImplementation( () => new Promise((resolve) => setTimeout(resolve, 100)), @@ -87,7 +87,7 @@ describe('BAIButton', () => { }); it('should clear loading state after action completes', async () => { - const action = jest.fn().mockResolvedValue(undefined); + const action = vi.fn().mockResolvedValue(undefined); const user = userEvent.setup(); render(Complete Action); @@ -110,8 +110,8 @@ describe('BAIButton', () => { }); it('should call both action and onClick when both are provided', async () => { - const action = jest.fn().mockResolvedValue(undefined); - const onClick = jest.fn(); + const action = vi.fn().mockResolvedValue(undefined); + const onClick = vi.fn(); const user = userEvent.setup(); render( @@ -128,7 +128,7 @@ describe('BAIButton', () => { }); it('should handle async action with successful resolution', async () => { - const action = jest.fn().mockResolvedValue('success'); + const action = vi.fn().mockResolvedValue('success'); const user = userEvent.setup(); render(Async Success); @@ -160,7 +160,7 @@ describe('BAIButton', () => { }); it('should combine loading prop with action loading state', async () => { - const action = jest + const action = vi .fn() .mockImplementation( () => new Promise((resolve) => setTimeout(resolve, 50)), @@ -217,7 +217,7 @@ describe('BAIButton', () => { describe('Event Object Handling', () => { it('should pass click event to onClick handler', async () => { - const onClick = jest.fn(); + const onClick = vi.fn(); const user = userEvent.setup(); render(Click); @@ -231,8 +231,8 @@ describe('BAIButton', () => { }); it('should call onClick even when action is provided', async () => { - const action = jest.fn().mockResolvedValue(undefined); - const onClick = jest.fn(); + const action = vi.fn().mockResolvedValue(undefined); + const onClick = vi.fn(); const user = userEvent.setup(); render( @@ -254,7 +254,7 @@ describe('BAIButton', () => { describe('Edge Cases', () => { it('should handle undefined action gracefully', async () => { - const onClick = jest.fn(); + const onClick = vi.fn(); const user = userEvent.setup(); render( @@ -277,7 +277,7 @@ describe('BAIButton', () => { }); it('should handle rapid clicks during async action', async () => { - const action = jest + const action = vi .fn() .mockImplementation( () => new Promise((resolve) => setTimeout(resolve, 100)), @@ -301,7 +301,7 @@ describe('BAIButton', () => { describe('Accessibility', () => { it('should be keyboard accessible', async () => { - const onClick = jest.fn(); + const onClick = vi.fn(); const user = userEvent.setup(); render(Accessible); diff --git a/packages/backend.ai-ui/src/components/BAICard.test.tsx b/packages/backend.ai-ui/src/components/BAICard.test.tsx index 4d8b8d342e..5d59b95a08 100644 --- a/packages/backend.ai-ui/src/components/BAICard.test.tsx +++ b/packages/backend.ai-ui/src/components/BAICard.test.tsx @@ -82,7 +82,7 @@ describe('BAICard', () => { it('should call onClickExtraButton when extra button is clicked', async () => { const user = userEvent.setup(); - const handleClick = jest.fn(); + const handleClick = vi.fn(); render( { it('should call onTabChange when tab is clicked', async () => { const user = userEvent.setup(); - const handleTabChange = jest.fn(); + const handleTabChange = vi.fn(); render( { describe('Complex Scenarios', () => { it('should render card with multiple features combined', () => { - const handleClick = jest.fn(); + const handleClick = vi.fn(); render( { it('should render card with tabs and extra button', async () => { const user = userEvent.setup(); - const handleButtonClick = jest.fn(); - const handleTabChange = jest.fn(); + const handleButtonClick = vi.fn(); + const handleTabChange = vi.fn(); render( { describe('onClick Handler', () => { it('should call onClick handler when clicked on react-router Link', async () => { - const onClick = jest.fn((e) => e.preventDefault()); + const onClick = vi.fn((e) => e.preventDefault()); const user = userEvent.setup(); renderWithRouter( @@ -128,7 +128,7 @@ describe('BAILink', () => { }); it('should call onClick handler when clicked on Typography.Link', async () => { - const onClick = jest.fn(); + const onClick = vi.fn(); const user = userEvent.setup(); render(Clickable Typography Link); @@ -137,7 +137,7 @@ describe('BAILink', () => { }); it('should block click interaction when link is disabled', async () => { - const onClick = jest.fn(); + const onClick = vi.fn(); const user = userEvent.setup(); render( @@ -219,7 +219,7 @@ describe('BAILink', () => { describe('Accessibility', () => { it('should be keyboard accessible for react-router Link', async () => { - const onClick = jest.fn((e) => e.preventDefault()); + const onClick = vi.fn((e) => e.preventDefault()); const user = userEvent.setup(); renderWithRouter( @@ -235,7 +235,7 @@ describe('BAILink', () => { }); it('should call onClick for Typography.Link on click', async () => { - const onClick = jest.fn(); + const onClick = vi.fn(); const user = userEvent.setup(); render(Typography Click); diff --git a/packages/backend.ai-ui/src/components/BAIPropertyFilter.test.tsx b/packages/backend.ai-ui/src/components/BAIPropertyFilter.test.tsx index 8b31700e4f..7ce738368a 100644 --- a/packages/backend.ai-ui/src/components/BAIPropertyFilter.test.tsx +++ b/packages/backend.ai-ui/src/components/BAIPropertyFilter.test.tsx @@ -136,7 +136,7 @@ describe('BAIPropertyFilter Component', () => { it('should accept value prop and parse filters', async () => { const value = 'name ilike "%test%" & status == "active"'; - const mockOnChange = jest.fn(); + const mockOnChange = vi.fn(); render( { }); it('should call onChange when adding a new filter', async () => { - const mockOnChange = jest.fn(); + const mockOnChange = vi.fn(); render( { }); it('should remove filter tag when close button is clicked', async () => { - const mockOnChange = jest.fn(); + const mockOnChange = vi.fn(); const value = 'name ilike "%test%"'; const { container } = render( { }); it('should clear all filters when reset button is clicked', async () => { - const mockOnChange = jest.fn(); + const mockOnChange = vi.fn(); const value = 'name ilike "%test%" & status == "active"'; render( { }); it('should handle boolean type filters with strict selection', async () => { - const mockOnChange = jest.fn(); + const mockOnChange = vi.fn(); render( { }, ]; - const mockOnChange = jest.fn(); + const mockOnChange = vi.fn(); render( { }); it('should handle string filters with ilike operator adding wildcards', async () => { - const mockOnChange = jest.fn(); + const mockOnChange = vi.fn(); render( { }); it('should use defaultValue when value prop is not provided', async () => { - const mockOnChange = jest.fn(); + const mockOnChange = vi.fn(); render( { }); it('should not add filter when empty value is submitted', async () => { - const mockOnChange = jest.fn(); + const mockOnChange = vi.fn(); render( { }, ]; - const mockOnChange = jest.fn(); + const mockOnChange = vi.fn(); render( { describe('Close Functionality', () => { it('should call onClose when close button is clicked', () => { - const handleClose = jest.fn(); + const handleClose = vi.fn(); render( Closable @@ -155,7 +155,7 @@ describe('BAITag', () => { describe('Accessibility', () => { it('should be keyboard accessible when closable', () => { - const handleClose = jest.fn(); + const handleClose = vi.fn(); render( Accessible Tag @@ -187,8 +187,8 @@ describe('BAITag', () => { }); it('should handle multiple closable tags independently', () => { - const handleClose1 = jest.fn(); - const handleClose2 = jest.fn(); + const handleClose1 = vi.fn(); + const handleClose2 = vi.fn(); render( <> @@ -211,7 +211,7 @@ describe('BAITag', () => { describe('Event Handlers', () => { it('should call onClick when tag is clicked', () => { - const handleClick = jest.fn(); + const handleClick = vi.fn(); render(Clickable Tag); const tag = screen.getByText('Clickable Tag'); act(() => { @@ -221,7 +221,7 @@ describe('BAITag', () => { }); it('should call onClose with event parameter', () => { - const handleClose = jest.fn(); + const handleClose = vi.fn(); render( Closable diff --git a/packages/backend.ai-ui/src/components/BAIUnmountAfterClose.test.tsx b/packages/backend.ai-ui/src/components/BAIUnmountAfterClose.test.tsx index ecf046cdbb..cb8a4414a5 100644 --- a/packages/backend.ai-ui/src/components/BAIUnmountAfterClose.test.tsx +++ b/packages/backend.ai-ui/src/components/BAIUnmountAfterClose.test.tsx @@ -102,7 +102,7 @@ describe('BAIUnmountAfterClose', () => { describe('afterClose Callback (Modal)', () => { it('should call original afterClose callback when provided', async () => { - const originalAfterClose = jest.fn(); + const originalAfterClose = vi.fn(); render( @@ -149,7 +149,7 @@ describe('BAIUnmountAfterClose', () => { }); it('should handle modal with afterClose callback', () => { - const afterClose = jest.fn(); + const afterClose = vi.fn(); render( @@ -179,7 +179,7 @@ describe('BAIUnmountAfterClose', () => { }); it('should preserve original afterOpenChange callback', () => { - const originalAfterOpenChange = jest.fn(); + const originalAfterOpenChange = vi.fn(); render( @@ -200,7 +200,7 @@ describe('BAIUnmountAfterClose', () => { }); it('should handle drawer with custom props', () => { - const onClose = jest.fn(); + const onClose = vi.fn(); render( @@ -274,8 +274,8 @@ describe('BAIUnmountAfterClose', () => { }); it('should preserve modal props other than afterClose', () => { - const onCancel = jest.fn(); - const onOk = jest.fn(); + const onCancel = vi.fn(); + const onOk = vi.fn(); render( @@ -296,7 +296,7 @@ describe('BAIUnmountAfterClose', () => { }); it('should preserve drawer props other than afterOpenChange', () => { - const onClose = jest.fn(); + const onClose = vi.fn(); render( @@ -317,7 +317,7 @@ describe('BAIUnmountAfterClose', () => { describe('Performance and Re-rendering', () => { it('should not cause unnecessary re-renders when open prop stays true', () => { - const renderCounter = jest.fn(); + const renderCounter = vi.fn(); const TestModal = ({ open }: { open: boolean }) => { renderCounter(); diff --git a/packages/backend.ai-ui/src/helper/index.test.ts b/packages/backend.ai-ui/src/helper/index.test.ts index bcae98a812..814d5667e3 100644 --- a/packages/backend.ai-ui/src/helper/index.test.ts +++ b/packages/backend.ai-ui/src/helper/index.test.ts @@ -10,7 +10,6 @@ import { toFixedFloorWithoutTrailingZeros, transformSorterToOrderString, } from './index'; -import { describe, it, expect, test } from '@jest/globals'; describe('transformSorterToOrderString', () => { it('should correctly transform single sorter to order string', () => { diff --git a/packages/backend.ai-ui/src/helper/useDebouncedDeferredValue.test.ts b/packages/backend.ai-ui/src/helper/useDebouncedDeferredValue.test.ts index 670d0534fa..d6e351b7fc 100644 --- a/packages/backend.ai-ui/src/helper/useDebouncedDeferredValue.test.ts +++ b/packages/backend.ai-ui/src/helper/useDebouncedDeferredValue.test.ts @@ -4,12 +4,12 @@ import { act } from 'react'; describe('useDebouncedDeferredValue', () => { beforeEach(() => { - jest.useFakeTimers(); + vi.useFakeTimers(); }); afterEach(() => { - jest.runOnlyPendingTimers(); - jest.useRealTimers(); + vi.runOnlyPendingTimers(); + vi.useRealTimers(); }); describe('Basic Functionality', () => { @@ -70,9 +70,9 @@ describe('useDebouncedDeferredValue', () => { // Rapid updates within debounce window act(() => { rerender({ value: 'updated1' }); - jest.advanceTimersByTime(50); + vi.advanceTimersByTime(50); rerender({ value: 'updated2' }); - jest.advanceTimersByTime(50); + vi.advanceTimersByTime(50); rerender({ value: 'final' }); }); @@ -81,7 +81,7 @@ describe('useDebouncedDeferredValue', () => { // Advance past default debounce time (200ms) await act(async () => { - jest.advanceTimersByTime(200); + vi.advanceTimersByTime(200); }); await waitFor(() => { @@ -102,13 +102,13 @@ describe('useDebouncedDeferredValue', () => { // Should not update before custom wait time await act(async () => { - jest.advanceTimersByTime(400); + vi.advanceTimersByTime(400); }); expect(result.current).toBe('initial'); // Should update after custom wait time await act(async () => { - jest.advanceTimersByTime(100); + vi.advanceTimersByTime(100); }); await waitFor(() => { @@ -157,7 +157,7 @@ describe('useDebouncedDeferredValue', () => { // Should update after wait time await act(async () => { - jest.advanceTimersByTime(200); + vi.advanceTimersByTime(200); }); await waitFor(() => { @@ -178,11 +178,11 @@ describe('useDebouncedDeferredValue', () => { // Continuously update within debounce window act(() => { rerender({ value: 'update1' }); - jest.advanceTimersByTime(100); + vi.advanceTimersByTime(100); rerender({ value: 'update2' }); - jest.advanceTimersByTime(100); + vi.advanceTimersByTime(100); rerender({ value: 'update3' }); - jest.advanceTimersByTime(100); + vi.advanceTimersByTime(100); }); // After maxWait time, should force update even if keep updating @@ -209,7 +209,7 @@ describe('useDebouncedDeferredValue', () => { // After debounce time await act(async () => { - jest.advanceTimersByTime(200); + vi.advanceTimersByTime(200); }); // Should eventually update (deferred value may lag slightly) @@ -234,12 +234,12 @@ describe('useDebouncedDeferredValue', () => { // Advance through all updates await act(async () => { - jest.advanceTimersByTime(250); + vi.advanceTimersByTime(250); }); // Should debounce and show final value await act(async () => { - jest.advanceTimersByTime(200); + vi.advanceTimersByTime(200); }); await waitFor(() => { @@ -261,7 +261,7 @@ describe('useDebouncedDeferredValue', () => { // With zero wait, should update quickly await act(async () => { - jest.advanceTimersByTime(0); + vi.advanceTimersByTime(0); }); await waitFor(() => { @@ -280,7 +280,7 @@ describe('useDebouncedDeferredValue', () => { }); await act(async () => { - jest.advanceTimersByTime(200); + vi.advanceTimersByTime(200); }); expect(result.current).toBe('test'); @@ -302,7 +302,7 @@ describe('useDebouncedDeferredValue', () => { }); await act(async () => { - jest.advanceTimersByTime(200); + vi.advanceTimersByTime(200); }); await waitFor(() => { @@ -333,7 +333,7 @@ describe('useDebouncedDeferredValue', () => { // Should not throw or cause issues await act(async () => { - jest.advanceTimersByTime(200); + vi.advanceTimersByTime(200); }); }); }); diff --git a/packages/backend.ai-ui/src/hooks/useIntervalValue.test.tsx b/packages/backend.ai-ui/src/hooks/useIntervalValue.test.tsx index 70e923fa66..7517476f2d 100644 --- a/packages/backend.ai-ui/src/hooks/useIntervalValue.test.tsx +++ b/packages/backend.ai-ui/src/hooks/useIntervalValue.test.tsx @@ -38,7 +38,7 @@ const TestIntervalValueComponent: React.FC<{ describe('useInterval and useIntervalValue hooks', () => { beforeEach(() => { - jest.useFakeTimers(); + vi.useFakeTimers(); // Mock document.hidden as false initially (page is visible) Object.defineProperty(document, 'hidden', { writable: true, @@ -47,7 +47,7 @@ describe('useInterval and useIntervalValue hooks', () => { }); afterEach(() => { - jest.useRealTimers(); + vi.useRealTimers(); }); describe('useInterval', () => { @@ -57,13 +57,13 @@ describe('useInterval and useIntervalValue hooks', () => { expect(screen.getByTestId('interval-count')).toHaveTextContent('0'); act(() => { - jest.advanceTimersByTime(1000); + vi.advanceTimersByTime(1000); }); expect(screen.getByTestId('interval-count')).toHaveTextContent('1'); act(() => { - jest.advanceTimersByTime(2000); + vi.advanceTimersByTime(2000); }); expect(screen.getByTestId('interval-count')).toHaveTextContent('3'); @@ -82,7 +82,7 @@ describe('useInterval and useIntervalValue hooks', () => { // Timer should not increment act(() => { - jest.advanceTimersByTime(2000); + vi.advanceTimersByTime(2000); }); expect(screen.getByTestId('interval-count')).toHaveTextContent('0'); @@ -98,7 +98,7 @@ describe('useInterval and useIntervalValue hooks', () => { // Continue normal interval act(() => { - jest.advanceTimersByTime(1000); + vi.advanceTimersByTime(1000); }); expect(screen.getByTestId('interval-count')).toHaveTextContent('2'); @@ -117,7 +117,7 @@ describe('useInterval and useIntervalValue hooks', () => { // Timer should continue to increment even when hidden act(() => { - jest.advanceTimersByTime(2000); + vi.advanceTimersByTime(2000); }); expect(screen.getByTestId('interval-count')).toHaveTextContent('2'); @@ -129,7 +129,7 @@ describe('useInterval and useIntervalValue hooks', () => { expect(screen.getByTestId('interval-count')).toHaveTextContent('0'); act(() => { - jest.advanceTimersByTime(5000); + vi.advanceTimersByTime(5000); }); expect(screen.getByTestId('interval-count')).toHaveTextContent('0'); @@ -143,7 +143,7 @@ describe('useInterval and useIntervalValue hooks', () => { const initialValue = screen.getByTestId('interval-value').textContent; act(() => { - jest.advanceTimersByTime(1000); + vi.advanceTimersByTime(1000); }); const updatedValue = screen.getByTestId('interval-value').textContent; @@ -164,7 +164,7 @@ describe('useInterval and useIntervalValue hooks', () => { }); act(() => { - jest.advanceTimersByTime(2000); + vi.advanceTimersByTime(2000); }); // Value should not change when hidden @@ -197,7 +197,7 @@ describe('useInterval and useIntervalValue hooks', () => { }); act(() => { - jest.advanceTimersByTime(1000); + vi.advanceTimersByTime(1000); }); // Value should continue to update even when hidden diff --git a/packages/backend.ai-ui/svg.d.ts b/packages/backend.ai-ui/svg.d.ts index 5d874a6cc3..17c5bb0abd 100644 --- a/packages/backend.ai-ui/svg.d.ts +++ b/packages/backend.ai-ui/svg.d.ts @@ -1,3 +1,4 @@ +/// declare module '*.svg' { const content: React.FC>; export default content; diff --git a/packages/backend.ai-ui/vitest.config.ts b/packages/backend.ai-ui/vitest.config.ts index 27c8ba9541..476deb4adb 100644 --- a/packages/backend.ai-ui/vitest.config.ts +++ b/packages/backend.ai-ui/vitest.config.ts @@ -27,12 +27,6 @@ export default defineConfig({ // Matches the alias in BUI's vite.config.ts so relay imports // (`./__generated__/*.graphql`) resolve to `src/__generated__/*.graphql.ts`. { find: './__generated__', replacement: generatedDir }, - // Two BUI tests still `import { ... } from '@jest/globals'`. - // Redirect the specifier to a local shim that re-exports from vitest. - { - find: '@jest/globals', - replacement: resolve(__dirname, '__test__/jest-globals-shim.ts'), - }, ], }, @@ -53,9 +47,6 @@ export default defineConfig({ environment: 'jsdom', setupFiles: [ resolve(__dirname, 'setupTests.ts'), - // Map `jest.*` helpers to their `vi.*` equivalents so tests written - // against Jest can run under Vitest without per-file renames. - resolve(__dirname, '__test__/vitest.jest-compat.ts'), ], include: ['src/**/*.{test,spec}.{ts,tsx}'], exclude: ['**/node_modules/**', '**/dist/**', '**/__generated__/**'], diff --git a/react/__test__/matchMedia.mock.js b/react/__test__/matchMedia.mock.js index 099fa51778..37a7a9f79e 100644 --- a/react/__test__/matchMedia.mock.js +++ b/react/__test__/matchMedia.mock.js @@ -1,13 +1,14 @@ +/* global vi */ Object.defineProperty(window, 'matchMedia', { writable: true, - value: jest.fn().mockImplementation((query) => ({ + value: vi.fn().mockImplementation((query) => ({ matches: false, media: query, onchange: null, - addListener: jest.fn(), // deprecated - removeListener: jest.fn(), // deprecated - addEventListener: jest.fn(), - removeEventListener: jest.fn(), - dispatchEvent: jest.fn(), + addListener: vi.fn(), // deprecated + removeListener: vi.fn(), // deprecated + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + dispatchEvent: vi.fn(), })), }); diff --git a/react/__test__/vitest.jest-compat.ts b/react/__test__/vitest.jest-compat.ts deleted file mode 100644 index d6713e5899..0000000000 --- a/react/__test__/vitest.jest-compat.ts +++ /dev/null @@ -1,27 +0,0 @@ -// 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/src/ambient.d.ts b/react/src/ambient.d.ts new file mode 100644 index 0000000000..016419c490 --- /dev/null +++ b/react/src/ambient.d.ts @@ -0,0 +1,101 @@ +/** + @license + Copyright (c) 2015-2026 Lablup Inc. All rights reserved. + */ +/// +// Project-wide ambient TypeScript declarations: shared utility types, +// global variables seeded by the host shell, and `Window` augmentations. +// Previously lived in `react-app-env.d.ts` (CRA scaffolding name) — moved +// to `ambient.d.ts` after FR-2611 retired CRA, which also dropped the +// `/// ` directive that was the only +// CRA-specific line. +// +// The `` directive above pulls +// vitest's `vi`, `describe`, `it`, `expect`, etc. into the type system +// without forcing an explicit `"types"` list in `tsconfig.json` (which +// would exclude `@types/jest`, breaking `@testing-library/jest-dom`'s +// default matcher augmentation). + +declare module 'backend.ai-client-esm' { + const ai: { backend: { Client: any; ClientConfig: any } }; + export = ai; +} + +type ArrayElement = + ArrayType extends readonly (infer ElementType)[] ? ElementType : never; +type ArgumentTypes = F extends (...args: infer A) => any + ? A + : never; + +interface BackendAIOptions { + get(key: string, defaultValue?: T, namespace?: string): T; + set(key: string, value: any, namespace?: string): void; + exists(key: string, namespace?: string): boolean; +} + +type BackendAIClient = import('./hooks').BackendAIClient; + +declare module globalThis { + // eslint-disable-next-line no-var + var isDarkMode: boolean; + // eslint-disable-next-line no-var + var isElectron: boolean; + // eslint-disable-next-line no-var + var electronInitialHref: string; + // eslint-disable-next-line no-var + var packageEdition: string; + // eslint-disable-next-line no-var + var packageVersion: string; + // eslint-disable-next-line no-var + var packageValidUntil: string; + // eslint-disable-next-line no-var + var buildVersion: string; + // eslint-disable-next-line no-var + var appLauncher: { + showLauncher?: (sessionId: { + 'session-name'?: string; + 'session-uuid'?: string; + 'access-key'?: string; + mode?: SessionMode; + 'app-services'?: Array; + runtime?: string; + filename?: string; + }) => void; + forceUseV1Proxy?: { + checked: boolean; + }; + forceUseV2Proxy?: { + checked: boolean; + }; + }; + // eslint-disable-next-line no-var + var backendaiclient: BackendAIClient | null | undefined; + // eslint-disable-next-line no-var + var backendaioptions: BackendAIOptions | undefined; +} + +type DeepPartial = { + [P in keyof T]?: T[P] extends Array + ? Array> + : T[P] extends ReadonlyArray + ? ReadonlyArray> + : T[P] extends object + ? DeepPartial + : T[P]; +}; + +type SelectivePartial = Partial> & Omit; + +type OptionalFieldsOnly = { + [K in keyof T as {} extends Pick ? K : never]?: T[K]; +}; + +type NonNullableItem = NonNullable['items']>>[0]; + +type NonNullableNodeOnEdges = NonNullable< + NonNullable['edges'][0]>>['node'] +>; + +interface Window { + switchLanguage: (lang: string) => void; +} diff --git a/react/src/helper/big-number.test.ts b/react/src/helper/big-number.test.ts index fb7a7ef0be..c41335a4f1 100644 --- a/react/src/helper/big-number.test.ts +++ b/react/src/helper/big-number.test.ts @@ -5,9 +5,17 @@ import { BigNumber } from './big-number'; import Big from 'big.js'; -declare module '@jest/expect' { - interface Matchers { - toEqualBigNumber(expected: unknown, unit?: string): R; +// Custom matcher type augmentation. Targets the `jest.Matchers` namespace +// from `@types/jest` because `@testing-library/jest-dom` (the default +// entry, used in `setupTests.ts`) augments the same namespace, and `expect()` +// in this codebase resolves to jest's `expect` for matcher types via the +// auto-loaded `@types/jest`. +declare global { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace jest { + interface Matchers { + toEqualBigNumber(expected: unknown, unit?: string): R; + } } } diff --git a/react/src/helper/customThemeConfig.test.ts b/react/src/helper/customThemeConfig.test.ts index f536f58d49..50a82baa03 100644 --- a/react/src/helper/customThemeConfig.test.ts +++ b/react/src/helper/customThemeConfig.test.ts @@ -4,12 +4,12 @@ import { type CustomThemeConfig, type LogoConfig, } from './customThemeConfig'; -import type { Mock } from 'vitest'; +import type { Mock, MockInstance } from 'vitest'; describe('customThemeConfig', () => { let fetchMock: Mock; let originalFetch: typeof global.fetch; - let dispatchEventSpy: jest.SpyInstance; + let dispatchEventSpy: MockInstance; beforeEach(() => { // Save original values diff --git a/react/src/hooks/__tests__/useMultiStepNotification.test.tsx b/react/src/hooks/__tests__/useMultiStepNotification.test.tsx index 8ddc9e5231..b00c50bf9f 100644 --- a/react/src/hooks/__tests__/useMultiStepNotification.test.tsx +++ b/react/src/hooks/__tests__/useMultiStepNotification.test.tsx @@ -35,8 +35,7 @@ vi.mock('react-i18next', () => ({ }), })); -const mockedListenToBackgroundTask = - listenToBackgroundTask as jest.MockedFunction; +const mockedListenToBackgroundTask = vi.mocked(listenToBackgroundTask); const baseConfig = { key: 'test-notification', @@ -94,7 +93,7 @@ describe('useMultiStepNotification', () => { describe('step failure and retry', () => { it('sets failed state when a step rejects, then retry resumes from that step', async () => { const error = new Error('Step 2 failed'); - const step2Executor = jest + const step2Executor = vi .fn() .mockRejectedValueOnce(error) .mockResolvedValueOnce('result2'); diff --git a/react/src/hooks/useLoginOrchestration.test.ts b/react/src/hooks/useLoginOrchestration.test.ts index 7c7b337548..ae75f20ffc 100644 --- a/react/src/hooks/useLoginOrchestration.test.ts +++ b/react/src/hooks/useLoginOrchestration.test.ts @@ -63,10 +63,8 @@ vi.mock('../helper/loginSessionAuth', () => ({ loadConfigFromWebServer: vi.fn().mockResolvedValue(undefined), })); -const MockedTabCount = TabCount as jest.MockedClass; -const mockedLoadConfig = loadConfigFromWebServer as jest.MockedFunction< - typeof loadConfigFromWebServer ->; +const MockedTabCount = vi.mocked(TabCount, { partial: false }); +const mockedLoadConfig = vi.mocked(loadConfigFromWebServer); // --------------------------------------------------------------------------- // Helpers @@ -133,7 +131,7 @@ function makeConnectedClient() { function mockNavigationType( type: 'navigate' | 'reload' | 'back_forward', ): void { - (window.performance as any).getEntriesByType = jest + (window.performance as any).getEntriesByType = vi .fn() .mockImplementation((entryType: string) => { if (entryType === 'navigation') { diff --git a/react/src/setupTests.ts b/react/src/setupTests.ts index 3568eaf5ff..f36eb5fef5 100644 --- a/react/src/setupTests.ts +++ b/react/src/setupTests.ts @@ -2,12 +2,22 @@ @license Copyright (c) 2015-2026 Lablup Inc. All rights reserved. */ -// jest-dom adds custom jest matchers for asserting on DOM nodes. +// jest-dom adds custom matchers for asserting on DOM nodes. // allows you to do things like: // expect(element).toHaveTextContent(/react/i) // learn more: https://github.com/testing-library/jest-dom import '../__test__/matchMedia.mock.cjs'; import '@testing-library/jest-dom'; +// Expose `vi` under the global name `jest` so `@testing-library/dom`'s +// `waitFor` detects "Jest fake timers are active" and switches to its +// timer-aware polling path. Without this, tests that combine +// `vi.useFakeTimers()` with `await waitFor(...)` hang — waitFor's default +// polling uses `setTimeout`, which never fires under faked timers. +// (None of our test code references `jest.*` directly anymore; this is +// purely a `@testing-library/dom` integration hook.) +import { vi } from 'vitest'; + +(globalThis as any).jest = vi; // Polyfill fetch for jsdom environment if (typeof global.fetch === 'undefined') { diff --git a/react/vite.config.ts b/react/vite.config.ts index c9178acb39..2407eb8681 100644 --- a/react/vite.config.ts +++ b/react/vite.config.ts @@ -232,7 +232,23 @@ function projectRootStaticPlugin(): Plugin { } export default defineConfig(({ command, mode }) => { - Object.assign(process.env, loadEnv(mode, projectRoot, '')); + const env = loadEnv(mode, projectRoot, ''); + Object.assign(process.env, env); + + // Comma-separated list of additional hostnames to whitelist for the dev + // server's host check (Vite 6 default-blocks anything outside localhost + // since CVE-2025-30208). Example for SwitchHosts users: + // VITE_ALLOWED_HOSTS=local.backend.ai,*.lablup.local + // Reads from process.env first (shell override) so CI/scripts can set it + // ad-hoc, then falls back to the .env.development.local entry. + const allowedHostsRaw = + process.env.VITE_ALLOWED_HOSTS ?? env.VITE_ALLOWED_HOSTS; + const allowedHosts = allowedHostsRaw + ? allowedHostsRaw + .split(',') + .map((h) => h.trim()) + .filter(Boolean) + : undefined; // Electron target uses a custom `es6://` URL scheme; the main process // registers a protocol handler via `protocol.handle('es6', ...)` in @@ -365,9 +381,10 @@ export default defineConfig(({ command, mode }) => { server: { host: process.env.HOST || '0.0.0.0', - port: Number(process.env.PORT) || 9083, + port: Number(process.env.PORT) || 9081, strictPort: false, open: false, + allowedHosts: allowedHosts, fs: { // Allow Vite to read files from the whole monorepo so that the // alias to `../dist/lib/...` and `../packages/backend.ai-ui/src` diff --git a/react/vitest.config.ts b/react/vitest.config.ts index 78677b83ba..a192c2966c 100644 --- a/react/vitest.config.ts +++ b/react/vitest.config.ts @@ -90,10 +90,6 @@ export default defineConfig({ 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__/**'], diff --git a/scripts/i18n-merge-driver.test.ts b/scripts/i18n-merge-driver.test.ts index f1f212eaa4..866ecd0de1 100644 --- a/scripts/i18n-merge-driver.test.ts +++ b/scripts/i18n-merge-driver.test.ts @@ -199,7 +199,7 @@ describe("i18n-merge-driver utility functions", () => { describe("i18n-merge-driver integration scenarios", () => { beforeEach(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); }); it("should handle typical i18n merge scenario without conflicts", () => { diff --git a/vitest.config.ts b/vitest.config.ts index abd18343d2..cd43b5b70d 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -17,11 +17,6 @@ export default defineConfig({ test: { globals: true, environment: "node", - setupFiles: [ - // Map `jest.*` helpers to their `vi.*` equivalents so tests written - // against Jest can run under Vitest without per-file renames. - resolve(__dirname, "__test__/vitest.jest-compat.ts"), - ], include: ["{src,scripts}/**/*.{test,spec}.ts"], exclude: [ "**/node_modules/**",