Testing the Web Application

The web application’s automated tests: what each suite covers, how the machinery works (Vitest, Playwright, MSW, the auth fixtures), how to run them, and how to work test-first where it makes sense.

Why we test, in plain terms

Every change to the web app risks breaking something a user depends on — a login flow, an image that must load, a chat reply that must render. Tests are the safety net that turns “I think this still works” into “the machine checked.” They also serve a second purpose in KAP: registered test suites are evidence — each one cites the functional requirements it proves, feeding the traceability matrix that connects requirements → user stories → tests.

The suites follow the classic test pyramid — many fast, focused tests at the bottom; a few slow, realistic ones at the top:

  • Component tests (fast, no browser, no server) check one React component or hook in isolation.
  • API route tests (fast, no browser) check the backend-for-frontend: the route handlers under app/api/.
  • End-to-end (E2E) tests (slow, real browser, real server) check whole user journeys — log in, open a dataset, see the image.

The suites at a glance

Four suites cover the web app; the first three live entirely in web/tests/, the fourth pairs Playwright specs with a backend harness script. The Registry ID column is each suite’s entry in the test registry (docs/portfolio/_data/tests.yaml):

Suite Registry ID Runner Where Command (from web/)
Component tests TEST-WEB-01 Vitest (jsdom) tests/components/, tests/integration/ npm run test:components
API route tests TEST-WEB-02 Vitest (node) tests/api/ npm run test:api
End-to-end TEST-WEB-03 Playwright (Chromium) tests/e2e/ npm run test:e2e
kavai_server compatibility smokes TEST-E2E-01 Playwright + harness script tests/e2e/ask-*.spec.ts + scripts/e2e-kavai-server.sh pixi run test-e2e-kavai-server (repo root)

Two Vitest configs split the first two suites: vitest.config.ts (node environment, tests/api/) and vitest.components.config.ts (jsdom environment, tests/components/ + tests/integration/). Playwright is configured in playwright.config.ts.

Component tests (tests/components/)

Component tests render one React component or hook in a simulated DOM (jsdom — no browser involved) using Testing Library, and assert on what the user would see and do: text, roles, clicks, keyboard.

What the machinery provides (tests/components/setup.ts):

  • jest-dom matchers (@testing-library/jest-dom/vitest) — assertions like toBeInTheDocument().
  • MSW (Mock Service Worker) — tests/mocks/handlers.ts defines fake implementations of the app’s own API (/api/datasets, /api/ag-ui-chat, /api/systems, …) and tests/mocks/server.ts starts them, so a component that fetches data gets realistic responses without a server. Add a handler there when a component under test calls a new endpoint.
  • A lucide-react icon mock — icons resolve to lightweight stub SVGs via a Proxy, keeping memory down (the real icon map is huge).

Config notes (vitest.components.config.ts): tests run single-threaded (one worker) to avoid CI memory spikes, and a few heavy suites that are still unstable are explicitly excluded (dashboard, bounding-box-visualizer, image-viewer) — un-exclude them when you stabilize them, don’t add new tests to the exclusion list.

A minimal example, from tests/components/button.test.tsx:

import { render, screen } from '@testing-library/react'
import { Button } from '@/components/ui/button'

it('applies the correct variant class', () => {
  render(<Button variant="destructive">Delete</Button>)
  expect(screen.getByText('Delete').className).toContain('bg-destructive')
})

The suite’s heavier residents test the chat interface, gallery hooks (pagination, debounced search, deduped URL resolution), and the photo-map-3d asset-positioning logic — see TEST-WEB-01’s verifies list in the registry for what is promised.

API route tests (tests/api/)

These test the app’s own backend — the route handlers under app/api/ (Backend API chapter) — in a node environment, no browser. Two styles coexist; prefer the first for new tests:

1. Direct handler import with mocked Supabase clients. Import the route’s GET/POST directly, mock @/lib/supabase/server and @/lib/supabase/admin, and call the handler as a function. Fast, deterministic, runs anywhere. tests/api/equipment-positions.test.ts is the model — including its “PostgREST-ish stub,” a chainable object that answers whichever query shape the handler uses:

const getUserMock = vi.hoisted(() => vi.fn());
vi.mock('@/lib/supabase/server', () => ({
  createServerClient: async () => ({ auth: { getUser: getUserMock } }),
}));

import { GET } from '@/app/api/equipment/positions/route';

it('returns 401 when unauthenticated', async () => {
  getUserMock.mockResolvedValue({ data: { user: null } });
  const res = await GET(req());
  expect(res.status).toBe(401);
});

2. HTTP against a running server. Older suites (e.g. tests/api/datasets.test.ts) fetch http://localhost:3000/api/... with a real JWT, and swap in mocks when CI=true. They double as smoke tests against a live dev server but need one running locally.

Machinery worth knowing:

  • Real JWTs without .env juggling — the global setup (tests/vitest-global-setup.ts + tests/jwt-utils.ts) mints a token via Supabase’s magic-link admin API and caches it while fresh, so authenticated suites (like the chat_sessions RLS tests) run against real auth.
  • Config (vitest.config.ts): threads pool, JUnit output to test-results/junit.xml, coverage to test-results/coverage, CI sharding via VITEST_SHARD (e.g. "1/3").
  • Load tests are opt-out by default — npm run test:api sets SKIP_LOAD_TESTS=1; run npm run test:load deliberately.

The suite’s promises (registry TEST-WEB-02) include auth status codes, the AG-UI chat endpoints, AG-UI event persistence, chat_sessions RLS isolation, and debug-message filtering.

End-to-end tests (tests/e2e/)

Playwright drives a real Chromium against the real app: the config’s webServer block boots npm run dev on :3000 automatically (or reuses a running one — PLAYWRIGHT_REUSE_EXISTING_SERVER=1, the default locally).

Auth and fixtures are prepared once, in tests/global-setup.ts: it signs in via the Supabase magic-link flow and saves browser storage states (playwright/.auth/storageState.json, plus a separate onboarding user), and provisions core fixtures — a test organization and dataset. Fixture hygiene is enforced by convention: any organization a spec creates must be named with the Playwright prefix, so the stale-fixture sweep can delete leftovers.

Specs are tagged by pack:

  • @core — the essential flows (auth, onboarding, org and dataset operations): npm run test:e2e:core.
  • @extra — additional high-value coverage (members, permissions, anomalies routes): npm run test:e2e:extras.
  • Milestone packs: m2-acceptance / m2-performance (the M2 journey and its performance gates — FPS ≥ 30, AI-query P95 < 3 s, FCP < 5 s): npm run test:e2e:m2.

CI behavior (all in playwright.config.ts): 2 retries, trace collection on first retry, JUnit + HTML + GitHub reporters, sharding via PLAYWRIGHT_SHARD. Only Chromium is enabled today; the other browser projects are scaffolded but commented out.

Debugging: npx playwright test --ui (interactive UI mode), or run a single spec: npx playwright test tests/e2e/chat-hardening.spec.ts.

The kavai_server compatibility smokes

ask-live-smoke.spec.ts and ask-gallery-smoke.spec.ts are excluded from the normal E2E pack and run under their own harness (scripts/e2e-kavai-server.sh, via pixi run test-e2e-kavai-server at the repo root): the script boots the extern/KavApps kavai_server on :8080, waits for health, runs the specs with AI_SERVER_URL pointed at it, and tears it down. They verify the real web ↔︎ AI contract — including the three acceptable gallery-response contracts (IMAGE_GALLERY event, report with an [[image-gallery:slug]] marker, or inline images) — and are registered as TEST-E2E-01, the one suite currently marked passing in the registry.

Command quick reference

All from web/ unless noted:

Task Command
Component tests npm run test:components (watch: test:components:watch)
API route tests npm run test:api (watch: test:api:watch; coverage: test:api:coverage)
All E2E npm run test:e2e
Core / extras packs npm run test:e2e:core / npm run test:e2e:extras
M2 acceptance + perf npm run test:e2e:m2
Load tests npm run test:load
kavai_server smokes pixi run test-e2e-kavai-server (repo root)
One Playwright spec, UI mode npx playwright test --ui

Conventions

  • Selectors: target data-testid attributes (or accessible roles and labels), never CSS classes — Tailwind class lists are an implementation detail that changes constantly.
  • Registry headers: frontend specs carry an annotation comment (@registry TS-FE-API-001, @tier, @req_ids, …). Note honestly: these in-code IDs predate the live registry — the registry of record is docs/portfolio/_data/tests.yaml, which maps suites (TEST-WEB-01…) to FRs by file path. Don’t invent new TS-FE-* IDs; if your suite evidences a requirement, add it to tests.yaml.
  • Placement: a new test goes where its subject lives — route handler → tests/api/, component/hook → tests/components/, user journey → tests/e2e/ (tag it @core only if the product is broken without it).
  • Mock at the boundary: mock Supabase clients and external services, not your own functions. If you’re mocking the thing you’re testing, the test proves nothing.

Working test-first (TDD)

We follow test-driven development where it makes sense — which in this codebase has a fairly crisp boundary.

TDD pays off where a test is cheap to write and the behavior is easy to state before the code exists:

  • Route handlers. The mocked-handler style makes the red-green loop fast: write the failing test for the status code and payload you want (401 unauthenticated, 200 shape, 403 cross-org), then implement until green, in npm run test:api:watch. The equipment-positions.test.ts pattern is the template.
  • Pure logic. Parsers, filters, formatters (tests/api/parsers.test.ts, debug-filtering.test.ts), hook logic like pagination or debouncing — state the contract as assertions first.
  • Bug fixes, always. Reproduce the bug as a failing test before fixing it — that’s TDD’s red step, and it’s what keeps the bug from coming back. The image-viewer’s “naturalWidth > 0 before bounding boxes” E2E check exists precisely because boxes once rendered over images that never loaded.

TDD is the wrong tool where the test can only be written by watching real behavior:

  • Cesium / WebGL rendering — you can’t assert a globe pixel-first; test the logic around the scene instead (asset positioning, marker filtering — as TEST-WEB-01 does) and verify rendering in an E2E smoke.
  • E2E journeys — these encode flows that emerge from the UI as built; write them once the flow exists, then let them guard it.
  • Exploratory UI work — when the design is still moving, test-after is honest; test-first would just mean rewriting assertions.

The working rhythm for a typical feature: state the API contract as failing route-handler tests → implement the handler → add component tests for the new UI states (loading, error, success — MSW handlers make these cheap) → finish with one E2E assertion in the relevant journey if the feature is user-visible. And remember the house rule: a new or changed route handler also means updating swagger.yaml and the API inventory in the same change.

Where things live — the map

Concern Path
Component/integration specs + setup web/tests/components/, web/tests/integration/, tests/components/setup.ts
API route specs + setup web/tests/api/, tests/api/setup.ts, tests/api/mocks.ts
E2E specs + helpers web/tests/e2e/, auth-helper.ts, core-test-utils.ts
MSW handlers web/tests/mocks/handlers.ts, server.ts
Auth/JWT machinery web/tests/jwt-utils.ts, vitest-global-setup.ts, global-setup.ts
Configs web/vitest.config.ts, web/vitest.components.config.ts, web/playwright.config.ts
Registry & traceability docs/portfolio/_data/tests.yaml → Test Traceability

The platform-wide testing story — the AI backend’s three-tier pytest suites, the KavApps backend suites, and the requirements-traceability system — is the Tests Handbook’s territory; its Frontend Web Suite section summarizes what this chapter covers in depth.


Last Updated: 2026-07-30