Skip to content

Evidence: the browser rows (issue-239)

Testing-plan rows T6 (UI/visual), T10 (accessibility), T12 (the live pass) and T13 (an older service), executed in headless Chrome over CDP.

How, and why it needed no new dependency

The plan assumed the Chrome extension. It reports not connected on this machine and list_connected_browsers returns [], so that route was unavailable — but "no extension" is not "no browser". Chrome is installed, and bun speaks WebSocket, so the pass is driven by a ~200-line script that launches Chrome headless with --remote-debugging-port and talks the DevTools Protocol to it directly.

sh
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \
  --headless=new --remote-debugging-port=9333 --user-data-dir=<tmp> about:blank
bun run cdp.ts   # navigate, evaluate, screenshot
bun run live.ts  # the live-refresh measurement below

The script lives in the session scratchpad, not in the repository, and ui/package.json is untouched. Making a browser pass a routine part of verification is a dependency decision worth taking on its own merits — not one to acquire as a side effect of this work item.

Everything below ran against the real thing: a service built from this branch on :4139, a second with service.stream.enabled: false on :4140, and bun run dev serving the real bundle at :5173. Not the demo fixture.

T12 — a change reaches the screen with no poll

The claim the work item exists for, measured rather than asserted. Streaming mode, page idle, nothing clicked. window.fetch was wrapped to record every request the page makes.

text
chip before:            live · connected 0s ago
quiet for 3s, calls:    0                       <- no timer is running

Then another process appended one record to the event log — what the poller, the receiver or a harness does:

text
calls after the append: /api/v1/work-items
                        /api/v1/sessions
                        /api/v1/attention
                        /api/v1/daemons
                        /api/v1/events?type=session.awaiting_input&type=session.reply_sent&limit=200
                        /api/v1/graph/check
latency, append -> the page's first request:  283ms
chip after:             live · connected 6s ago

Three things at once: the page was genuinely idle (zero requests in three seconds, where polling would have made four calls plus a graph check), it refreshed 283ms after a change on the workstation (against a 15-second default — fifty times better, and well inside R1.2's two-second budget), and it refreshed the lists because the appended record was a session.* — which is the invalidation map's answer, not a full sweep for its own sake.

It also proves the one row the plan said no automated test could: R1.6, CORS parity. A real browser opened EventSource cross-origin from http://localhost:5173 to the service and the connection was accepted, because that origin is in service.cors.allowOrigins. httpx cannot demonstrate that — it does not enforce CORS at all.

the board, refreshed by a streamed change

T6 / R3 — the three refresh modes

Computed from the live DOM, not read off a screenshot:

text
radiogroup: {"label":"Refresh mode","count":3,"checked":["stream"],
             "values":["stream","poll","manual"],"intervalVisible":false}
interval shown for poll:    true
interval shown for manual:  false

The interval select belongs to exactly one mode and appears only with it.

streamingpolling
Streaming selected — no intervalPolling — the interval appears

manual

T6 / R6 — the trace panel and the chat bar

Real computed styles from a real engine — the half jsdom cannot reach:

text
panelOverflowY:      auto
panelMaxHeight:      495px          <- clamp(240px, 55vh, 720px) at a 900px viewport
panelScrolls:        true
panelRole:           log
panelName:           Session transcript
panelTabIndex:       0
chatPosition:        sticky
chatBottom:          0px
chatWithinViewport:  true

At a 600px-tall viewport (R6.5), both stay usable — the panel takes 330px and the chat bar is still on screen:

text
layout (600px): {"panelHeight":330,"chatWithinViewport":true,"viewportHeight":600}
tall viewport600px viewport
The panel scrolls in its own frame; the chat bar is in reachBoth still usable at 600px

Scroll anchoring, in a real engine: the panel is genuinely scrollable (scrollable: true), sits flush at the newest entry when scrolled there (atBottomGap: 0), and stays where it is put when scrolled back (scrolledBackTo: 0). The rule itself is unit-tested in WorkItemDetail.test.tsx.

scrolled back

T13 / R4.1 — a service that does not serve the stream, and the bug it found

This row found a real defect, which is what a browser pass is for.

EventSource retries a dropped connection, but a response it will not accept — a 404 from a service too old for the route — is terminal: the browser closes the source and never tries again. The hook was counting to five consecutive failures before falling back, so against a 404 it received exactly one, and sat on stream · reconnecting (1) with a frozen board forever. That is precisely the state R4.1 exists to prevent, reached through the mechanism meant to prevent it.

text
before the fix:  chip (stream 404): ["stream · reconnecting (1)"]
after the fix:   chip (stream 404): ["stream unavailable · polling instead"]

The transport now reports whether the browser has given up (readyState === CLOSED), and a terminal failure short-circuits the count. Covered by a regression test (useStream.test.ts — "falls back at once when the browser gave up rather than retrying").

the fallback

No stub would have found this: every stub retries politely.

T10 — accessibility

Asserted against the DOM the assistive-technology tree is built from, which is more reproducible than a description of what a screen reader said:

  • The mode control is a real radiogroup with aria-label="Refresh mode" and three native radios, so it is announced as a group and traversed with arrow keys.
  • The connection state is a role="status" region — aria-live="polite", announced on change without stealing focus — and its text carries the state (live · connected 2s ago, stream unavailable · polling instead); the dot is aria-hidden. Colour is never the only carrier.
  • The trace panel is role="log", named Session transcript, tabindex="0" — reachable and scrollable from the keyboard.

Not covered: an actual screen-reader listening pass. What is asserted is the tree a screen reader reads, not one particular reader's rendering of it.

Redaction

Every screenshot is of a real control plane driving a real work item — this one — so the capture script masks the trace panel's path caption and rewrites every text node containing a /Users/ path before the shutter, then refuses to capture at all if any remains. That check fired twice during this pass, which is why it exists.

Released under the MIT License.