Skip to main content

Testing

getbased uses two test layers:
  • Vitest wraps the node-side helper fixtures and fast logic checks.
  • Playwright runs the browser suite against the live app in headless Chrome.
Routstr/Cashu wallet work has an extra release gate because it touches bearer money. Before changing wallet funding, token export/import, seed restore, node deposit/top-up, refunds, or Cashu TS migration code, read Routstr/Cashu safety. Some browser fixtures are still self-executing IIFEs with local assert() helpers; Playwright executes those through tests/playwright/browser-script-runner.js so they run in the same browser suite as native Playwright specs. Generated marker catalogs have dedicated drift gates:
Use the matching :build command after editing authoring modules, then run the focused identity, placement, persistence, or terminology tests relevant to the diff. GitHub Actions owns the exhaustive Chromium and combined-coverage run.

The assert pattern

Browser-script fixture files define a local assert helper and collect results:
The detail argument appears in the failure output — use it to print the actual value that caused the failure.

Notable test files

All test files live in the tests/ directory. Native browser specs live under tests/playwright/; fixture scripts live as tests/test-*.js. The table calls out representative ownership areas rather than duplicating the complete file listing from the repository. A few tests run node-side (no browser) — pure-helper unit tests + node script guards. Marked node in the table; browser-driven coverage is owned by Playwright specs. The landing page test (test-landing.js) lives in the get-based-site repo.

Local AI and model-testing checks

Run the focused native suites while iterating:
tests/test-provider-local-ai-runtime.js is a legacy browser fixture executed by the Playwright browser-script runner as part of ./run-tests.sh. Always run the full script before shipping because Local AI changes also affect Settings, PDF import, sync, service-worker assets, and provider routing.

Run all tests headlessly

The script:
  1. Checks if a server is running on port 8000; starts node dev-server.js if not
  2. Runs the node-side tests first (fast fail on helper regressions, no browser needed)
  3. Runs the dev-server origin guard
  4. Runs the Playwright browser suite
  5. Exits with code 0 if all pass, 1 if any fail
Requires: Node.js and dependencies installed with npm ci.

Coverage status

COVERAGE=1 ./run-tests.sh runs Vitest with V8 coverage enabled, runs the normal Playwright suite with Chromium JS coverage enabled, then merges the Playwright suite shards from tests/.playwright-coverage/ with tests/.vitest-coverage/coverage-final.json. The reporter writes tests/.coverage.json and prints separate Playwright, Vitest/Node, and combined global function/byte coverage percentages for app-source JavaScript. Running node scripts/playwright-coverage.mjs directly still falls back to the legacy high-surface Chromium sampler when no Playwright suite shards are present. The full COVERAGE=1 ./run-tests.sh path requires suite shards so coverage regressions in the Playwright instrumentation fail clearly. Coverage is report-only by default. Set COVERAGE_MIN=90 or another percentage to fail the run when combined global function coverage falls below that floor.

Accessibility regression scan

tests/test-a11y-axe.js loads axe-core 4.10 from cdnjs at runtime and runs axe.run() against the live DOM at 14 stops (every lens + every modal). Severity policy:
  • critical / serious → test fails on regression vs baseline
  • moderate / minor → logged but doesn’t block (axe leans opinionated at those tiers)

Baseline-locked gate

The gate is baseline-relative, not zero-tolerance. Real-world a11y adoption gates on “no regression from current state” — gating on zero violations the first time a codebase adopts axe would block every PR forever. Baseline lives at tests/.a11y-baseline.json:
The test fails when any critical/serious rule’s count exceeds the baseline. New rules with non-zero counts ARE a regression (the suite never saw them before). Improvements (current < baseline) pass and emit a hint to refresh the baseline. _axeVersion pins the runtime axe-core load. Bumping the cdnjs URL without bumping this field would surface rule renames as false regressions; the test prints an info line on mismatch.

Refreshing the baseline

After a wave of fixes that legitimately drops violation counts:
tests/playwright/a11y-axe-browser.spec.js pipes the env var into the page context before loading the fixture. The test prints a ▶ {...} JSON line — copy it over the critical/serious/moderate/minor blocks in tests/.a11y-baseline.json. Don’t lower these numbers without an actual fix; the gate would lock in the new lower bound and silently accept regressions. If the baseline file is missing entirely, the test treats the first run as “establish baseline” and prints the JSON to stdout for paste-back.

Running a single test in the browser

Open the browser console while http://localhost:8000 is running, then:
Results appear in the console. This is useful during development before running the full suite.

Writing new tests

When you add a feature or fix a bug, add assertions to the relevant test file. If none fits, create test-yourfeature.js. What to cover:
  • Source inspection — verify the function or pattern exists in the source:
  • DOM state — check that elements render correctly:
  • Function behavior — call window-exported functions and check results:
  • CSS rules — verify styles are applied (use getComputedStyle or inspect stylesheets):
  • localStorage keys — verify storage conventions:

What the headless runner cannot test

  • Drag-and-drop interactions
  • File picker dialogs
  • Actual streaming AI responses (API key required)
  • IndexedDB state across page reloads (the runner resets between files)
For these, test the surrounding logic (e.g., that handleBatchPDFs function exists and has the right signature) rather than the interaction itself.