• English
  • Testing

    The test suite uses Vitest with Testing Library in a JSDOM environment. All tests live in src/test/ and mirror the src/ directory structure. There are no end-to-end tests in regular use.


    Running tests

    npm test              # Vitest run (single pass, all tests)
    npm run test:watch    # Vitest watch mode
    npm run test:ui       # Vitest UI (browser-based test explorer)

    The npm test:browser script exists (from the template) but runs Playwright browser tests that are dormant — do not rely on it.


    Test directory layout

    src/test/
    ├── setup.ts                        # Global test setup (jest-dom, ResizeObserver polyfill)
    ├── smoke.test.ts                   # Sanity check for apiUrl utility
    ├── auth/                           # Auth system (8 files)
    │   ├── AuthContext.test.tsx        # Provider lifecycle: restore, bad token, /me
    │   ├── Login.test.tsx
    │   ├── RequirePermission.test.tsx
    │   ├── authApi.test.ts
    │   ├── can.test.tsx                # useAuth().can() permission logic
    │   ├── drift.test.tsx              # Token expiry/drift scenarios
    │   ├── guard.test.tsx
    │   └── meApi.test.ts
    ├── components/                     # Shared components (6 files)
    │   ├── HelpHint.test.tsx
    │   ├── async-boundary.test.tsx
    │   ├── filter-nav.test.ts          # filterNavByPermission() utility
    │   ├── firstPermittedPath.test.ts
    │   ├── pager.test.tsx
    │   └── responsive-list.test.tsx
    ├── features/                       # Feature modules
    │   ├── stores/                     # 17 test files — API hooks, list/detail UI
    │   ├── diagnostics/                # 18 test files — most complex feature
    │   ├── tasks/                      # 10 test files
    │   ├── operators/                  # 3 test files
    │   ├── roles/                      # 4 test files
    │   ├── audit/                      # 4 test files
    │   ├── health/
    │   └── overview/
    └── lib/                            # Infrastructure (9 files)
        ├── config.test.ts
        ├── errorMap.test.ts
        ├── handle-server-error.test.ts
        ├── httpClient.test.ts          # Bearer token, ApiError parsing
        ├── httpClient.notify.test.ts   # Event emission (onSessionDead, onForbidden)
        ├── httpClient.refresh.test.ts  # 401 → silent refresh → retry flow
        ├── queryClient.test.ts
        ├── relativeTime.test.ts
        └── tokenStore.test.ts          # In-memory access, localStorage refresh

    In src/components/, individual component files also have co-located tests (*.test.tsx) for config-drawer, confirm-dialog, password-input, and sign-out-dialog.


    Test configuration

    vitest.config.ts — the primary test runner config:

    SettingValue
    Environmentjsdom
    Setup filesrc/test/setup.ts
    Include patternsrc/test/**/*.test.{ts,tsx}
    Globalstrue (describe, it, expect available without imports)
    unstubEnvstrue (auto-cleans stubbed env vars after each test)

    src/test/setup.ts imports @testing-library/jest-dom/vitest (adds toBeInTheDocument etc.) and polyfills ResizeObserver for JSDOM, which Radix UI components require.

    The separate vite.config.ts also contains Vitest browser configuration (Playwright/Chromium), but browser-mode tests are not part of npm test.


    Mocking approach

    No MSW, no network calls. All HTTP is mocked at the fetch global level.

    fetch mocking

    // Standard pattern in every test file that touches the network:
    vi.stubGlobal('fetch', vi.fn())
    // ...
    ;(fetch as ReturnType<typeof vi.fn>).mockResolvedValueOnce(
      new Response(JSON.stringify({ ok: true, data: [...] }), { status: 200 })
    )

    Tests construct real Response objects, giving precise control over status codes and body shapes. vi.unstubAllGlobals() in afterEach restores the real fetch.

    Environment variable mocking

    beforeEach(() => {
      vi.stubEnv('VITE_API_BASE_URL', 'https://edge.test')
    })
    afterEach(() => {
      vi.unstubAllEnvs()
    })

    vitest.config.ts sets unstubEnvs: true so env stubs are auto-cleaned, but explicit cleanup is also present in most files for clarity.

    Router mocking

    vi.mock('@tanstack/react-router', () => ({
      useNavigate: () => vi.fn(),
      useParams: () => ({ storeId: 'store-1' }),
      useSearch: () => ({}),
      // ...
    }))

    TanStack Router hooks are mocked per-test-file. Tests that need navigation assertions capture the mock and use expect(mockNavigate).toHaveBeenCalledWith(...).


    Test utilities

    src/test/utils/renderWithClient.tsx

    renderWithClient(ui: ReactElement)

    Wraps a component in a fresh QueryClient (with retry: false) + QueryClientProvider. Returns all Testing Library render results plus the queryClient instance. Use this instead of the bare render for any component that calls TanStack Query hooks.

    src/test-utils/tanstack-table.ts

    createTableMock(rowCount: number)

    Minimal TanStack Table mock with getFilteredSelectedRowModel() and a resetRowSelection spy. Used for bulk-action dialog tests that need a table context without a full useReactTable setup.

    src/test-utils/cookies.ts

    clearCookies(filter?: string | RegExp)

    Removes cookies from document.cookie by prefix/regex. Used in auth tests that rely on cookie-persisted state.


    Test types

    TypeWhat it coversExample files
    UnitPure functions — URL builders, error maps, relative time, filter utilitiesconfig.test.ts, errorMap.test.ts, filter-nav.test.ts, relativeTime.test.ts
    Integration (component)Full component render + user interactions + mocked API responsesAuthContext.test.tsx, StoresList.test.tsx, TasksWorklist.test.tsx
    HookrenderHook for data-fetching and state hooksuseStores.test.ts, useTasks.test.ts, useTableUrlState.test.ts
    API moduleLow-level fetch wrappers (token injection, 401 retry, event emission)httpClient.refresh.test.ts, tokenStore.test.ts

    Coverage areas

    The most tested areas:

    • src/lib/httpClient.ts — Three dedicated test files covering bearer headers, 401 → refresh → retry, concurrent-request deduplication, onSessionDead / onForbidden events, and ApiError parsing.
    • src/auth/ — AuthContext lifecycle, can() logic, RequirePermission, OTP flow, token drift.
    • src/features/diagnostics/ — 18 test files covering all four sections (events, devices, bundles, commands), download flow, command cancellation, and section-level lazy loading.
    • src/features/stores/ — 17 test files covering list/detail/devices/entitlements/booking.

    Areas with lighter coverage:

    • src/features/settings/ — mostly scaffolded UI, not meaningfully tested
    • src/features/dashboard/ — static demo content, not tested
    • Route files in src/routes/ are excluded from coverage

    What to check when changing things

    Change areaTests to run / watch
    src/lib/httpClient.tssrc/test/lib/httpClient*.test.ts
    src/auth/src/test/auth/**
    src/features/stores/src/test/features/stores/**
    src/features/diagnostics/src/test/features/diagnostics/**
    src/features/tasks/src/test/features/tasks/**
    src/components/layout/filter-nav.tssrc/test/components/filter-nav.test.ts
    src/lib/tokenStore.tssrc/test/lib/tokenStore.test.ts
    Any new featureAdd src/test/features/<name>/ mirroring the source structure

    Always run npm test and npm run lint before deploying. There is no CI pipeline.