Skip to main content

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

ChangeMinimum validation
Pure TypeScript utilitytargeted Vitest test, lint, frontend build
React component or storeTesting Library/Vitest, lint, frontend build
Copy or locale keyi18n audit, affected UI test, frontend build
Route or multi-step UI flowrelevant Playwright project
Accessibility-sensitive UIkeyboard check and axe scan
Migration, RLS policy, trigger or RPClocal database reset plus role/tenant tests
Edge Functionlocal function request tests, auth-negative cases, downstream failure cases
Voice/Realtime flowtoken/room authorization tests and a controlled provider smoke test
Documentationdocs typecheck and production build
Application code releaserelevant 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, marketing
  • dashboard, new-session, session-flow, session-history, session-summary, settings, responsive, a11y, v1-deprecation
  • company-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:

ProjectIdentityMain scope
anonno sessionmarketing, sign-in and invitation entry
userordinary memberproduct routes and session flows
admincompany administratorcompany 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:

  1. anonymous access where relevant
  2. authenticated user in the owning company
  3. authenticated user in another company
  4. member versus admin/consultant
  5. service-role behaviour only where required
  6. 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" or aria-live for non-blocking asynchronous updates
  • role="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:

  1. reload and use only keyboard input
  2. follow focus from skip link through navigation and the main action
  3. open, operate, and close every dialog or popover
  4. verify focus returns to a sensible trigger
  5. trigger validation and confirm the error is announced and reachable
  6. enable reduced motion and repeat animated transitions
  7. 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:

  1. start the intended local/test stack
  2. confirm the scanner base URL and authentication
  3. run HawkScan
  4. remediate all reported application vulnerabilities
  5. 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:

  1. identify its browser, database, function, and provider boundaries
  2. add fast tests for deterministic domain behaviour
  3. add one end-to-end happy path in the correct persona
  4. add authorization and tenant-isolation negatives at the server boundary
  5. add an accessibility assertion for any new interaction pattern
  6. mock paid AI calls by default and document the opt-in smoke path
  7. 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.