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 liketoBeInTheDocument(). - MSW (Mock Service Worker) —
tests/mocks/handlers.tsdefines fake implementations of the app’s own API (/api/datasets,/api/ag-ui-chat,/api/systems, …) andtests/mocks/server.tsstarts 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 thechat_sessionsRLS tests) run against real auth. - Config (
vitest.config.ts): threads pool, JUnit output totest-results/junit.xml, coverage totest-results/coverage, CI sharding viaVITEST_SHARD(e.g."1/3"). - Load tests are opt-out by default —
npm run test:apisetsSKIP_LOAD_TESTS=1; runnpm run test:loaddeliberately.
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-testidattributes (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 isdocs/portfolio/_data/tests.yaml, which maps suites (TEST-WEB-01…) to FRs by file path. Don’t invent newTS-FE-*IDs; if your suite evidences a requirement, add it totests.yaml. - Placement: a new test goes where its subject lives — route handler →
tests/api/, component/hook →tests/components/, user journey →tests/e2e/(tag it@coreonly 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. Theequipment-positions.test.tspattern 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-01does) 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