Test UI: data-* structural hooks + visual-refresh options (#5) #15

Merged
sufian merged 38 commits from wip/issue-05 into main 2026-10-14 15:19:51 +00:00
Member

Ships the test-UI refresh (Option A "Mono Terminal") together with the structural hooks it needed. Closes #5.

Changes

  • Structure is machine-readable. Stable data-* hooks on the runner/file/test nodes — data-testrunner-status, data-tests-* / data-files-*, data-test-status, data-test-name, data-test-error, data-file-status, data-file-src, data-test-ui, data-test-ui-controls. Classes are styling-only, and the headless runner keys off data-* exclusively. This removes the old class hooks (testrunner--pass/fail, test--running/pass/fail, testfile--run/pass/fail, suite--running/pass) — a breaking change for anyone selector-matching them.
  • Option A "Mono Terminal" implemented. Dense monospace UI: status gutter (glyph + word) with a state rail, keycap action bar, 3 px top signal rule, zebra rows. Light paper palette is the base, dark overrides under prefers-color-scheme: dark, pure CSS (color-scheme: light dark) — no JS.
  • The test list is the runner's only scroller. The shell is bound to the viewport, so the action bar and the sandbox pane stay put, and the running test row is revealed within the pane as the run proceeds.
  • Run elapsed is wall-clock — from the first test starting to the tests stopping, including an early stopOnFailure (it previously summed per-test durations).
  • README demo (docs/media/testrunner-demo.mp4): gooey's own suite — 18 files, 510 tests, performance tests excluded — running green in the new UI.

Settled in this work

  • AA is a policy, not a table of values: every signal clears ≥4.5:1 on every surface it renders on, and state is carried by glyph and word and tone. The light palette was corrected for this (worst case now 4.7:1).
  • The data-* hooks are the automation contract; classes are for styling and unstable.
  • The sandbox document stays unthemed (testsandbox.html), so a dark run shows a white sandbox pane — theming it would move the snapshot/screenshot baselines.
  • One formatter for durations (millis()); no magnitude-aware second one.
  • The version bump lands at release, not here.

Verification

  • Typecheck + build clean; a source guard in s/typecheck keeps the headless driver on the hooks (no class selectors).
  • Rendered evidence: both palettes across every state (ready/running/pass/fail, collapsed/expanded) captured and reviewed, with contrast audited numerically.
  • Scroll behaviour measured over a full run: the document never scrolls, the action bar and sandbox pane rects stay constant, and the running row never leaves the visible band.
  • Demo recorded against the shipped build: 510/510 passing, with the header value checked against the measured run phase.
  • The merge with main is resolved in src/headless/index.ts::readSummary — main's robustness kept, with the PR's data-* selector.

Notes

  • dist/ artifacts are intentionally not rebuilt here — they are updated in separate dist commits. As a consequence smoke cannot exercise this PR's src; that gap is filed as #24.
  • The demo's <video src> is a branchless raw URL, so it resolves once this lands on main.

Closes #5

Ships the test-UI refresh (Option A "Mono Terminal") together with the structural hooks it needed. Closes #5. ## Changes - **Structure is machine-readable.** Stable `data-*` hooks on the runner/file/test nodes — `data-testrunner-status`, `data-tests-*` / `data-files-*`, `data-test-status`, `data-test-name`, `data-test-error`, `data-file-status`, `data-file-src`, `data-test-ui`, `data-test-ui-controls`. Classes are styling-only, and the headless runner keys off `data-*` exclusively. This removes the old class hooks (`testrunner--pass/fail`, `test--running/pass/fail`, `testfile--run/pass/fail`, `suite--running/pass`) — a breaking change for anyone selector-matching them. - **Option A "Mono Terminal" implemented.** Dense monospace UI: status gutter (glyph + word) with a state rail, keycap action bar, 3 px top signal rule, zebra rows. Light paper palette is the base, dark overrides under `prefers-color-scheme: dark`, pure CSS (`color-scheme: light dark`) — no JS. - **The test list is the runner's only scroller.** The shell is bound to the viewport, so the action bar and the sandbox pane stay put, and the running test row is revealed within the pane as the run proceeds. - **Run elapsed is wall-clock** — from the first test starting to the tests stopping, including an early `stopOnFailure` (it previously summed per-test durations). - **README demo** (`docs/media/testrunner-demo.mp4`): gooey's own suite — 18 files, 510 tests, performance tests excluded — running green in the new UI. ## Settled in this work - **AA is a policy, not a table of values:** every signal clears ≥4.5:1 on every surface it renders on, and state is carried by glyph **and** word **and** tone. The light palette was corrected for this (worst case now 4.7:1). - **The `data-*` hooks are the automation contract**; classes are for styling and unstable. - **The sandbox document stays unthemed** (`testsandbox.html`), so a dark run shows a white sandbox pane — theming it would move the snapshot/screenshot baselines. - **One formatter** for durations (`millis()`); no magnitude-aware second one. - **The version bump lands at release**, not here. ## Verification - Typecheck + build clean; a source guard in `s/typecheck` keeps the headless driver on the hooks (no class selectors). - Rendered evidence: both palettes across every state (ready/running/pass/fail, collapsed/expanded) captured and reviewed, with contrast audited numerically. - Scroll behaviour measured over a full run: the document never scrolls, the action bar and sandbox pane rects stay constant, and the running row never leaves the visible band. - Demo recorded against the shipped build: 510/510 passing, with the header value checked against the measured run phase. - The merge with `main` is resolved in `src/headless/index.ts::readSummary` — main's robustness kept, with the PR's `data-*` selector. ## Notes - `dist/` artifacts are intentionally not rebuilt here — they are updated in separate dist commits. As a consequence `smoke` cannot exercise this PR's `src`; that gap is filed as #24. - The demo's `<video src>` is a branchless raw URL, so it resolves once this lands on `main`. Closes #5
Add stable structural attributes (classes remain purely for styling):
- .testrunner: data-testrunner-status = pass|fail|running|ready
- .test (per test): data-test-status, data-test-name, and data-test-error on failure
- .testfile (per file): data-file-status, data-file-src

Switch src/headless/index.ts to data-* selectors only:
- outcome wait: [data-testrunner-status="pass"|"fail"]
- failure details: [data-file-status="fail"] (+data-file-src),
  [data-test-status="fail"][data-test-name], [data-test-error]
- summary read: locator([data-testrunner-status]) instead of .testrunner

No visual change. Hooks are complete enough for a future single-updating-line
headless mode to identify the currently-running test (data-test-status="running"
+ data-test-name) and its file (nearest [data-file-src] ancestor).
Add docs/design/testrunner-refresh.md and self-contained static prototypes
under docs/design/prototypes/ for issue #5. Options:
- A Mono Terminal — dark, glyph-gutter, ultra-dense
- B Paper Ledger — warm, sans chrome + mono content, status stamps
- C Signal Grid — dark dashboard, aggregate band + proportion bar, chips

Each covers principles, type stack, color tokens (reusing the --color-* names),
status treatment, layout and action bar. Prototypes use the Part A data-*
hooks and open with no build step. The lead picks the direction.
- headless: inject layout CSS via data hooks —
  [data-testrunner-status]{max-height:100vh}[data-test-ui]{overflow-y:auto}
  (same elements as .testrunner / .test-ui; behaviour unchanged).
- testrunnerview: add structural data-test-ui hook on the .test-ui container.
- docs: reword the token claim — keep the semantic slots (pass/fail/running)
  and the data-* hook names; the token vocabulary may change.
- New docs/design/prototypes/option-a-light.html: identical layout/DOM/hooks to
  option-a-mono-terminal.html, only the palette differs — warm paper background,
  dark ink, Paper-Ledger-inspired signals darkened for AA on white.
- option-a-mono-terminal.html: add the data-test-ui hook so both variants share
  the same DOM/hook set as the landed app.
- index.html: add the A-light card.
- testrunner-refresh.md: new "Option A-light" section (palette tokens, contrast
  notes, relationship to A/B) and list the new prototype.
Rewrite testrunner.scss to the chosen "Mono Terminal" direction: dense
monospace UI, status gutter (glyph + word) with a status rail, keycap action
bar, 3px top signal rule, zebra rows. Light palette is the base; dark overrides
under prefers-color-scheme: dark; color-scheme: light dark so form controls and
scrollbars follow.

- testview.tsx: render the status gutter as spans (test__select / test__gutter
  / test__state / test__name / test__time); CSS supplies the glyph + word. All
  data-* hooks retained.
- testrunnerview.tsx: mark "Run all" primary; replace the STATUS/Time text with
  an aggregate badge + counts + elapsed; wrap files in .test-files.
- scss: make #root a full-bleed flex container and put the test list on the
  left (matching A); the two 50% panes now fill the viewport. Headless
  max-height/overflow override still works via [data-testrunner-status] /
  [data-test-ui].

Behavior unchanged: selection, run/rerun, stop-on-failure, perf copy, sandbox
iframes.
Keep main's robustness (zero fallback, count() guard, timeouts) with the
PR's data-* selector for the runner status hook.
- Drop vestigial state classes (test--*, testfile--*, testrunner--*): the
  SCSS now styles off data-* hooks, so the classes were dead.
- Fold the FAIL/PASS testInfo render branches into one.
- Merge anyFailing/allPassing into the runnerStatus calc (one pass).
- Dedupe the file/suite disclosure CSS into a shared rule; drop redundant
  .test-ui font/background declarations and the global pre reset.
- headless: read data-test-name + data-test-error off the single failing
  element instead of two waits.
.test__link inherited its background-color from the .test row while the
row itself also painted it, so the translucent status tints (running/fail)
rendered at roughly twice their specified alpha -- e.g. the running row
showed #e8ddc8 instead of the specified #f3ede3, and the fail row #f3dcda
instead of #f9edec. That diverged from the design prototype (which uses
transparent) and pushed the fail signal below AA on its row.

Rendering the button transparent lets the row tint show through once.
The action-bar total summed per-test durations (milliseconds) but
rendered them with a literal `s` suffix, so a ~1.7s run read as
"1723.200s". Reuse the shared millis() formatter, matching the
per-test rows.
Ship the Option A (Mono Terminal) dark recording, cropped to the test-list
pane (sandbox pane dropped) and optimised, as the README demo: gooey's own
suite running to 510/510 passed, then the first file expanded to its suites.

Also fold the landed data-test-ui hook into the design doc's structural
contract and record the README demo there.
The GIF could not carry crisp text and was 942 KiB; the same recording is now
an h264 640x800 (rounded to the test-list pane at native resolution) MP4,
12.4s and 847 KiB, embedded as <video autoplay controls> instead.

Forgejo does not rewrite raw-HTML src attributes, so the player uses the
branchless absolute raw URL (redirects to the default branch and survives
renames). That resolves to main, so the video stays 404 in the PR preview
until merge; a relative markdown "Watch the demo" link sits beside it both as
the pre-merge escape hatch (Forgejo rewrites markdown-relative links to the
viewed ref) and as the fallback for renderers that drop <video>.

Remove the superseded testrunner-dark.gif and update the README demo note in
docs/design/testrunner-refresh.md to match.
suite--running / suite--pass were set by suiteview.tsx but no rule in
testrunner.scss read them (only suite--fail / suite--only are styled);
the state is already carried by data-test-status rows and the file
header. Remove the dead branches.
The light signal inks fell short of the design docs AA claim once the
transparent row/badge tints are composited: pass 3.52, running 3.72,
fail 4.35, muted 4.12 (badge tint / surface-2). Dark muted was also
3.59 on surface-2. Darken the light inks and lighten the dark muted ink
(lightness only; hues unchanged) so every signal clears >=4.6 on every
surface it renders on. Re-encode the README demo from the corrected
package so the asset stays truthful.
Keep only what still earns its place: the chosen Option A and why, the
data-* structural contract, and the settled decisions. Drop the throwaway
HTML prototypes and their links, the duplicated palette tables (the SCSS is
the single source of truth), and reduce B/C to a brief rejected-record.
State the AA contrast and glyph/word/tone policies instead of hard-coded
ratios and hexes. Aligns with the corrected palette in 91f821d.
The header timer summed every test's duration. Each duration is a real
in-iframe span (beforeEach -> afterEach), but their sum is not a run
length - it read ~1.7s for an ~5s run.

Add a single modelled field, TestRunnerState.runDurationMs, cleared when a
run starts and set in runTests()'s finally (so stopOnFailure's early return
is included) from a local performance.now() captured at the RUNNING
transition. The header renders millis(runDurationMs), nothing before the
first run.

The suite-level total in suiteview.tsx sums the same per-test durations and
is left as-is (reported separately).
The README demo was cropped to the test-list pane (640x800), so the right
sandbox pane - where gooey's tests render into testRoot - was never on
screen. Capture the whole 1280x800 runner surface instead: dark theme, 1x,
H.264 yuv420p +faststart, silent.

The recorded header now reads the wall-clock run (5751.200ms), matching the
measured running->pass phase for that run (5737ms).
The full-frame demo was recorded dark. Switch the shipped cut to the light
theme (less jarring beside the README) and hold the sandbox pane's testRoot
render for 1.8s so the rendered DOM (Component, its children, the Diagnostics
click counter) is actually readable rather than flashing past in a handful of
frames.

The hold is inserted at encode time, so the runner header still reports the
true run (510/510 passed 5982.200ms, matching the measured running->pass phase
of 5969ms) and the clip still ends on the green settle with the first file card
expanded.

H.264 yuv420p +faststart, silent, 1280x800, 12.92s.
README: drop the stale 640 width and the dark/cropped description; embed the
1280-wide source at its native width, and describe the sandbox pane/testRoot
render the clip now holds on.

design doc: replace the stale "dark, cropped 640x800" demo note with the
reasoning (light, full frame, the testRoot hold) plus Forgejo embed mechanics
and legibility at README width, and let the README own the copy.
Render the status word/glyph as real DOM text instead of CSS `content`
(which only reaches AT via generated content) and drop the now-dead
`::before`/`::after` rules. Make file/suite summary status total and
order-independent (fail > running > ready > pass), so a later failure
cannot hide behind an earlier READY test (reachable with "only" /
"Rerun selected"). Unify the file-status vocabulary on `running`.
The value read from `data-file-src` is a file path, not a status:
rename the field/local accordingly. Drop the redundant READY assignment
before the stopOnFailure return (the `finally` already resets state).
Narrow the `data-test-error` doc row to the first string test-info item.
Assert the FAIL summary decodes the failing test name and error text
from the data-* hooks, and that the packed headless bundle keys off
`data-testrunner-status` with no old class selectors.
The runner's data-* hooks are a public contract (the headless runner and
external automation key off them) but were documented only in
docs/design/testrunner-refresh.md, which the package does not ship. Add a
"DOM hooks" section to the README, state that classes are styling-only and
unstable, and note the removed class selectors for migration.
The document used to be the scroller (body/#root/.testrunner were
min-height:100vh), so the action bar only looked pinned thanks to sticky,
and the sandbox pane scrolled away. Bound the shell to the viewport with
height + overflow:hidden and make [data-test-ui] the single scroll
container; .test-ui-controls stays sticky inside it.

Reveal the running row by adjusting that scroller's scrollTop to
"nearest" (24px margin, below the sticky action bar). Replaces
scrollIntoView, which walked every scrollable ancestor -- including the
document and sandbox iframes -- shifting the rest of the app. The reveal
defers one microtask so the row has a layout box after its file/suite
<details> opens in the same flush, and is dropped if the view unmounts
first.
The base stylesheet now bounds the shell to the viewport and makes
[data-test-ui] the scroll container, so the injected
`[data-testrunner-status]{max-height:100vh}[data-test-ui]{overflow-y:auto}`
no longer does anything. Injecting it on a settled run changes neither the
laid-out geometry (shell/panes/iframe rects identical) nor the rendered
pixels (full-page screenshots byte-identical before/after injection), and
the headless CLI still reports pass/fail correctly for the scratch
consumer.
Re-recorded with the list pane as the only scroller: the running row is
revealed within the pane while the action bar and sandbox pane stay put.
Full frame 1280x800, 1x, light theme, 1.8s hold at the automated pick.
Record the lead settled decisions on the refresh: the bounded-shell
pane-scroller layout (only [data-test-ui] scrolls, nearest reveal with a
24px margin, headless override removed), the deliberately unthemed sandbox,
the single millis() run-total formatter, and the release-deferred version
bump. Correct the stale data-test-ui hook row (the headless override no
longer exists) and update the README aria-label, which still described the
held frame as a click-counter tree rather than the SVG circle the
re-recorded clip now holds on.
- Extract one summaryStatus(tests) (+ iterFileTests) in logic.ts: the file
  and suite views now share the fail > running > ready > pass precedence
  instead of each re-implementing it.
- Derive the whole-run runnerStatus from the existing summary counts rather
  than re-walking every test.
- Pass the status calculations straight to data-* attributes (drop the
  redundant calc(() => x.get()) wrappers).
- revealRow: isConnected alone proves the row is still mounted.
- SCSS: drop the duplicate height / default align-items and the defensive
  min-height:0 / overflow:hidden chain the bounded shell never needed.

Verified: tsc x2 + vite build clean, bash -n smoke.sh clean, before/after
screenshots byte-identical (pass/fail, light/dark), scroll invariants and
contrast audit unchanged.
The bounded shell is held by html/body height + body overflow alone; the
flex chain fills it. Correct the parenthetical that named declarations the
shell no longer carries.
The packed-artifact shape assertions in smoke cannot be green until the
dist rebuild (npm pack ships the committed dist; issue #24). Keep the
failure-decode assertions, drop the two artifact-shape ones with a note,
and assert the same contract at the source level in s/typecheck: the
headless driver references data-testrunner-status and uses none of the
CSS-class structural selectors.
The reveal found the sticky action bar by its .test-ui-controls class,
which the README calls styling-only. Add a data-test-ui-controls hook to
the bar (documented in the design doc hook table) and query it, so a
class rename cannot silently drop the band back to the scroller top.

Also bail before the band maths when the running row has no layout box
(inside a closed <details>): its zero rect otherwise reads as "above the
band" and marches the list to the top on each hidden test start.
READY renders as the dash art "----" (and "· ----" on the file row),
which assistive tech reads literally. Mark the visible art aria-hidden and
add a visually-hidden "READY" beside it. Pixel-neutral: the sr-only box is
absolutely positioned and pinned to its containing block origin so it
cannot stretch the document scrollHeight.
README DOM-hook table gains data-test-ui-controls and narrows
data-test-error to the first string test-info item, matching the design
doc. Design doc names the whole-run subject for data-testrunner-status
precedence against logic.ts summaryStatus (file/suite), and records that
READY announces as a visually-hidden word.
The stylesheet used no SCSS features beyond `&` nesting (9 sites), and
theming already used CSS custom properties; `sass` was a devDependency
with no other consumer. Rename testrunner.scss -> testrunner.css, flatten
the 9 nesting sites, update the <link> and the stale comment, and drop
`sass` plus its now-orphaned transitive deps from package-lock.json.

Vite no longer loads sass and the Dart Sass deprecation warning is gone.
The compiled dist/testrunner tree is byte-identical (an `@charset "UTF-8"`
line in source preserves the byte-for-byte output). Pixel diffs
(light/dark x pass/fail), the document-never-scrolls /
[data-test-ui]-is-the-only-scroller invariants, the DOM hook contract, and
the 0-low-contrast audit are unchanged.
The committed dist had drifted from src — it still carried the old
class-based headless selectors and the pre-refresh palette, and
dist/vite/ (the ./vite export) was missing entirely. Rebuild with the
s/dist steps (skipping the interactive s/clean) so the shipped artifacts
match source.

Also drop the yarn@3.2.0 packageManager pin: the repo ships npm's
package-lock.json and no yarn.lock.
sufian merged commit 257acebe50 into main 2026-10-14 15:19:51 +00:00
sufian referenced this pull request from a commit 2026-10-14 15:19:53 +00:00
sufian deleted branch wip/issue-05 2026-10-14 15:19:57 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
legend/gooey-test!15
No description provided.