Visual regression: Playwright route-level visual sanity + axe lane
Visual regression: Playwright route-level visual sanity + axe lane
Status: Delivered
CAS: CAS-2742
Delivered: 2026-05-14
PRs: #729, #731
What’s new
A new tests/visual/ Playwright lane provides route-level visual sanity checks and WCAG AA accessibility validation on every PR. This is separate from the existing test/e2e/ smoke tests: it runs against a running dev server, covers five key routes × five viewport sizes, and enforces three hard-blocking layout invariants on every run.
Coverage:
| Route | Slug |
|---|---|
/transaction/list | transactions |
/settings/license | settings-license |
/invoices | invoices |
/settings/sync | settings-sync |
/ (no users) | onboarding |
Viewports: iPhone 14 Pro (375×812), iPhone SE (375×667), iPad (768×1024), MacBook 13 (1280×800), MacBook 16 (1536×960)
What it checks
Hard-blocking layout assertions (fail the PR)
- Horizontal overflow —
document.documentElement.scrollWidth > window.innerWidthcatches components that overflow the viewport and force horizontal scroll - Fixed-element collision — stacked fixed elements (FABs, nav bars, toolbars) must not overlap each other
- Safe-area bottom anchor — on notched viewports (iPhone 14 Pro), bottom-anchored elements must respect
env(safe-area-inset-bottom)
Advisory screenshot baseline (advisory only)
expect.soft + toHaveScreenshot captures per-route/per-viewport screenshots. Drift does not block the PR — Loki handles the authoritative visual gate. The screenshots are uploaded as CI artifacts for manual review.
axe-core WCAG AA per route (hard block)
axe violations fail the PR. The meta-viewport rule is suppressed (CAS-2387: conflicts with iOS viewport meta tag; this is a known exception). detailedReport: true means the HTML artifact shows the full tree of violations.
Sanity self-tests
Each hard-blocking assertion has a matching test that injects a hand-crafted violation (overflow element, stacked fixed divs, safe-area anchor missing) and verifies the assertion catches it. These run in the same job and guard against false-confidence in a passing green run.
How to use it
# Run the full visual lane locally (requires a running dev server on :1420)npm run dev &deno task test:visual
# Update screenshot baselines after intentional visual changesdeno task test:visual:update
# Run on a specific viewport onlydeno task test:visual -- --project=iphone14proCI job playwright-visual-sanity runs after the frontend job and uploads the HTML report as an artifact.
What changed under the hood
tests/visual/visual-sanity.spec.ts— main spec: 5 routes × 5 viewports, three layout assertions, axe per route, advisory screenshotstests/visual/playwright.config.ts— project matrix (5 viewports), base URL, artifact configurationtests/visual/tauri-mock-shared.ts— shared mock stubs (Tauri command mocks for the routes tested); includesfetch_settings: {}stub added in PR #731src/components/navbar/MobileNavbar.tsx— inactive item opacity 40→60% (contrast fix for WCAG AA)src/components/MobileTransactionList.tsx— date separator colorc="dimmed"→c="dark.1"(contrast fix)src/pages/InvoiceQueuePage.tsx— status badgevariant="light"added.github/workflows/ci.yml—playwright-visual-sanityjob addedpackage.json—axe-playwright ^2.2.2added to devDependencies
Why we built it
Every UI regression in the mobile polish arc (stacked FABs, dead bands, safe-area overlaps, contrast failures) was caught by the regent on a real TestFlight device, not during review. The three hard-blocking assertions in this lane directly target the failure shapes that appeared most often: overflow, collision, and safe-area anchor. A PR that re-introduces a FAB overlap or a viewport overflow will fail the playwright-visual-sanity check before it reaches Eivind or the regent.
Known limitations / follow-on work
- Screenshot baselines are empty
.gitkeepdirectories on first run — rundeno task test:visual:updatelocally after the first CI green pass, commit the PNGs, and they become the baseline. - Baselines are viewport-specific and will need regeneration when intentional layout changes ship.
- The axe suppression list (
meta-viewport, plus the CAS-2387 entries) should be reviewed if the iOS viewport meta tag situation changes.