Tours — scripted browser walks for rendering + a11y review

A tour is a scripted walk through the running app that produces review artifacts: screenshots at desktop (1280×800) and mobile (375×800), accessibility-tree snapshots, axe-core violation reports, and any soft-assertion findings the tour author wrote. Tours live in test/tours/*.tour.ts; the framework is test/tours/tour-lib/.

A tour is the app's walk, written down. Writing the walk down is more solid than doing it by hand: the next person gets the walk instead of reinventing it. That value lasts only while the walk still runs, so a weekly session runs every tour and keeps it true (schedules/tour-check/). When a tour misses because the app changed deliberately — a landing on main, a plan, or a filed issue explains the new label or structure — the session edits the tour to describe the app as it now is. Anything else it finds (a miss nothing explains, an axe violation, a page error, visible breakage in a screenshot) it files as an issue and reports. Its report is the one thing that has to be read: a walk nobody runs rots, and a report nobody reads is the same failure one level up (three of five tours had been silently failing for months before this existed — 2026-08-26).

Tours are still not a test gate. Findings and axe violations never affect the exit code, artifacts are gitignored, and neither CI nor pre-commit runs them. A gate has one response to intended UI change — go red — so a soft-finding instrument makes a permanently red gate that gets ignored; the weekly session asks the other question, drift or new truth?, which a gate cannot. Behavior belongs in doctests (testing.md); "does the app boot at all" is the smoke tier (bin/smoke, a merge gate — same browser library, different failure semantics, deliberately not the same walk).

Running

bin/tour              # list available tours
bin/tour capture      # run one
bin/tour --all        # run every tour

Needs the shared dev router (pnpm dev at the monorepo root) reachable; the worktree is auto-detected from $PWD, box defaults to test1 (BROWSE_BOX overrides). The box must be one built for agent browsing — "agentBrowsing": "owner" in its _config/box.json, which test1 and its clones carry — or every owner-gated surface walks as a 401/403 page (docs/plans/agent-browsing-owner.md). A healthy tour takes tens of seconds — both viewport passes included.

Tours use named per-session Chrome profiles, so a tour can run alongside an interactive bin/browse session in the same worktree. Keep a tour's own session name stable when debugging its walk; different sessions have separate cookies and tabs.

Artifacts

Each run writes test/tours/.artifacts/<tour>/<runId>/ (gitignored):

A capture-time degradation (page-readiness timeout, axe crash) is recorded as a ⚠️ finding rather than silently producing a loading-state screenshot or a fake "0 violations".

When to use tours

When NOT to use tours

How an agent reviews with tours

  1. bin/tour <name>; note the console counts.
  2. Read summary.md — findings first.
  3. View the checkpoint PNGs directly (agents can read images): compare desktop vs mobile, look for clipped/overlapping/empty states, and report any visible breakage even when tangential to the task at hand.
  4. Where a checkpoint shows nonzero axe violations, open its .axe.json for the nodes and failure summaries.
  5. Treat ❌ findings and visible breakage as work items; ⚠️ capture degradations mean the artifact itself may be unreliable — re-run before drawing conclusions from it.

Writing and organizing tours

API sketch

tour({ name, description }, async (t) => {
  await t.go("/dashboard");          // path → dev-router URL
  await t.checkpoint("loaded");      // screenshot + AX + axe, both viewports
  await t.expect.heading("Dashboard", { level: 1 });  // soft assertion
  await t.expect.landmark("Primary");
  await t.expect.noPageErrors();
  await t.click({ role: "link", name: /^Browse/ });   // accessible-name locator; string = exact, RegExp = match
  await t.expect.custom("has rows", (ax) => ax.includes("row"));
});

Each tour runs as two full passes (desktop, then mobile). Expectations are soft — a miss records a ❌ finding and the tour continues; only a thrown error (unresolvable locator, browser failure) aborts a pass, and even then the other pass still runs and reports.