Testing and Accessibility
Navaid uses several test layers because a passing component test does not prove that Row Level Security, an Edge Function, a browser session, and an external provider work together. Match the test to the boundary changed and keep the fastest useful checks close to the code.
Validation Matrix
| Change | Minimum validation |
|---|---|
| Pure TypeScript utility | targeted Vitest test, lint, frontend build |
| React component or store | Testing Library/Vitest, lint, frontend build |
| Copy or locale key | i18n audit, affected UI test, frontend build |
| Route or multi-step UI flow | relevant Playwright project |
| Accessibility-sensitive UI | keyboard check and axe scan |
| Migration, RLS policy, trigger or RPC | local database reset plus role/tenant tests |
| Edge Function | local function request tests, auth-negative cases, downstream failure cases |
| Voice/Realtime flow | token/room authorization tests and a controlled provider smoke test |
| Documentation | docs typecheck and production build |
| Application code release | relevant checks above and the local HawkScan loop |
For security-sensitive changes, a happy-path test is insufficient. Include unauthenticated, wrong-company, wrong-role, stale-ID, replay, and concurrent request cases as applicable.
Frontend Commands
Run these from frontend/:
npm run lint
npm test
npm run test:coverage
npm run build
npm run i18n:audit
npm run build is the frontend type check: it runs tsc -b before Vite. There
is no separate frontend typecheck script.
Useful interactive commands are:
npm run test:watch
npm run test:e2e:ui
npm run test:e2e:headed
npm run test:e2e:report
If a dependency changed, use the npm 10 lock workflow in Getting Started. A successful test run against an npm 11-generated lock does not prove Cloudflare's npm 10 install will succeed.
Unit and Component Tests
Vitest runs in JSDOM with Testing Library and shared setup in
frontend/src/test/setup.ts. The current source tree contains 16 test suites
plus the setup file. Coverage includes:
- authentication and conversation stores
- permissions and company-scope parity
- feature-flag creation
- token budget presentation
- maturity calculations and readiness types
- locale-specific US English behaviour
- image, CSV, URL, colour, and formatting utilities
- stale-chunk reload recovery
Add a test beside the existing domain grouping under frontend/src/test/.
Prefer observable behaviour over implementation details:
render(<Component />)
await user.click(screen.getByRole('button', { name: /save/i }))
expect(await screen.findByRole('status')).toHaveTextContent(/saved/i)
Mock network boundaries, time, and browser APIs explicitly. Reset Zustand stores and mocks between tests so order does not affect results. Do not copy a service-role credential into browser test code.
Browser End-to-End Tests
Playwright is configured by frontend/playwright.config.ts. The current suite
has 14 spec files:
auth,marketingdashboard,new-session,session-flow,session-history,session-summary,settings,responsive,a11y,v1-deprecationcompany-admin,invite-acceptance- opt-in
smoke-real-ai
Run the default suite with:
cd frontend
npm run test:e2e
Personas
Global setup creates isolated test users through the Supabase Admin API and
writes storage state under e2e/.auth/. Three Playwright projects divide the
suite:
| Project | Identity | Main scope |
|---|---|---|
anon | no session | marketing, sign-in and invitation entry |
user | ordinary member | product routes and session flows |
admin | company administrator | company management and invite acceptance |
Global teardown removes test data. The tests intentionally use one worker with
fullyParallel: false because they share a Supabase project. Do not turn on
parallelism without first giving every test isolated tenant data and proving
that teardown cannot cross-delete another worker's fixtures.
Safety Interlock
The Admin API helper refuses to run unless:
E2E_ALLOW_PROD=true
SUPABASE_SERVICE_ROLE_KEY=...
The awkwardly named E2E_ALLOW_PROD is an explicit acknowledgement that the
configured backend will have users and rows created and deleted. It is not a
recommendation to test against production. Prefer a dedicated test project,
check E2E_BASE_URL, VITE_SUPABASE_URL, and the project reference before
running, and keep the service-role key in Node-only .env.e2e.
Failure diagnostics retain the trace and video and take a screenshot. CI retries failures twice; local runs do not. Start with the HTML report, then inspect the trace before rerunning in headed mode.
AI Calls
Normal browser specs intercept /functions/v1/chat so they are deterministic,
fast, and inexpensive. smoke-real-ai.spec.ts is tagged @slow and excluded
unless E2E_RUN_SLOW=1 is present:
$env:E2E_RUN_SLOW = '1'
npx playwright test e2e/specs/smoke-real-ai.spec.ts
Use a controlled test tenant with a small budget. A real-provider smoke test checks connectivity and contract drift; it should not replace deterministic contract tests.
The checked-in frontend/e2e/README.md contains older test counts and mentions
a test:e2e:real-ai package script that does not currently exist. Treat
package.json and playwright.config.ts as authoritative until that README is
updated.
Database and Edge Function Tests
Start from a reproducible local schema:
npx supabase start
npx supabase db reset
The configured local database major is PostgreSQL 17. A migration test should prove both the intended operation and the denied operations:
- anonymous access where relevant
- authenticated user in the owning company
- authenticated user in another company
- member versus admin/consultant
- service-role behaviour only where required
- duplicate, stale, and concurrent requests
For Edge Functions, exercise OPTIONS, missing bearer token, invalid token,
wrong company, malformed body, rate limit, budget denial, provider timeout,
and success. Confirm error responses do not include provider payloads, SQL
details, stack traces, tokens, or secrets. See
Edge Functions for the common request pipeline.
Every function is deployed with --no-verify-jwt, so local tests must prove the
function's own auth.getUser(token) and authorization checks. A gateway-only
test creates false confidence.
Accessibility Contract
Accessibility is a product requirement, not a final audit step. New UI should provide:
- semantic landmarks, headings, labels, lists, buttons, links, and tables
- a visible keyboard focus indicator
- logical tab order without positive
tabindex - keyboard equivalents for pointer interactions
- names for icon-only controls
role="status"oraria-livefor non-blocking asynchronous updatesrole="alert"for actionable errors- correct tab, dialog, and disclosure relationships
- locale-aware text and document language
- reduced-motion behaviour for non-essential animation
- sufficient contrast in every runtime company theme
The app includes a skip-to-main-content link, focus-visible styling, reduced motion rules, accessible maturity-map controls, labelled dialogs, and live status regions. Preserve those patterns when extracting or replacing components.
Automated Axe Coverage
e2e/helpers/a11y.ts runs axe tags for WCAG 2.0/2.1 A and AA. The current
browser suite scans marketing, sign-in, sign-up, and dashboard pages and fails
on serious or critical findings. Each JSON axe report is attached to the test.
Colour contrast is currently disabled in the automated axe helper and is therefore a known test gap, not a statement of conformance. Check contrast manually for Navaid defaults and generated tenant palettes, and add a separate automated strategy before making it a release gate.
Manual Keyboard Pass
For a changed workflow:
- reload and use only keyboard input
- follow focus from skip link through navigation and the main action
- open, operate, and close every dialog or popover
- verify focus returns to a sensible trigger
- trigger validation and confirm the error is announced and reachable
- enable reduced motion and repeat animated transitions
- zoom to 200% and test the narrow responsive layout
Test voice and drag-like interactions with a non-pointer alternative. Do not hide focus merely to make a control look cleaner.
DAST
HawkScan is for a local or explicitly isolated test target. Never point the scanner at production. After application code changes:
- start the intended local/test stack
- confirm the scanner base URL and authentication
- run HawkScan
- remediate all reported application vulnerabilities
- rerun until the scan verifies the fixes
Documentation-only changes do not require a DAST run. See Security and Privacy for trust boundaries and Operations Runbooks for release sequencing.
Adding Coverage
When adding a feature:
- identify its browser, database, function, and provider boundaries
- add fast tests for deterministic domain behaviour
- add one end-to-end happy path in the correct persona
- add authorization and tenant-isolation negatives at the server boundary
- add an accessibility assertion for any new interaction pattern
- mock paid AI calls by default and document the opt-in smoke path
- update this guide if scripts, projects, or safety interlocks change
Flaky tests are defects. Diagnose shared state, unawaited updates, unstable selectors, provider dependence, or insufficient isolation rather than increasing timeouts indiscriminately.