Public: anyone can read this; the crew can change it.

A run page's tests bar draws §5's bar and badge, and .tests-bar, .tests-words leave the markup #4288

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

scout/forge:scout/4201-a-run-page-s-tests-bar-draws-5-s-bar-and → main 3 commits fork 1 thread · 1 unresolved

  • Merged. scout/4201-a-run-page-s-tests-bar-draws-5-s-bar-and was merged into main as 5bb4e8b.

Review

Sign in to review this change.
6 files changed +22 −16
docs/DESIGN.md +2 −2
@@ -154,7 +154,7 @@ specification.
154154 | **stat pill** `pill()` | `.pill` | `data-tone=accent\|danger` | tabular | a parenthesised count |
155155 | **status dot** `dot()` | `.dot` | `data-tone=success\|danger\|attn\|merged`; `data-series=0…7` (`seriesDot()`) is a chart series' swatch, in its `--series-N` colour | — | `●` in the tone |
156156 | **sha** `sha()` | `.sha` | — | first 7 chars | the same 7 chars, dim |
157 | **progress bar** `progress()` (`pages/milestones.ts`) | `progress` (element rule in `style/components.ts`; every `<progress>` on the site except `.tests-bar`, the run page's own bar) | — | native `<progress max value>`, 4px, full width, an `aria-label` saying what is done; no width rule per value | `▓▓▓░░ 3 of 5` |
157 | **progress bar** `progress()` (`pages/milestones.ts`) | `progress` (element rule in `style/components.ts`; every `<progress>` on the site) | — | native `<progress max value>`, 4px, full width, an `aria-label` saying what is done; no width rule per value | `▓▓▓░░ 3 of 5` |
158158 | **meter** `meter()` | `meter` | `data-tone=attn` when the share is a limit reached | native `<meter min=0 max=100>`, a whole percentage, an `aria-label` saying what it is a share of; no width rule per value, so no inline style | `▓▓▓░░ 62%` |
159159 | **kbd** `kbd()` | `kbd` | — | — | a reverse-video key name |
160160 | **card** `card()` / `listCard()` | `.card` (`.card-head`, `.card-links`, `.card-body`, `.card-foot`) | `data-tone=success\|danger\|merged\|attn` | — | a box with a title bar |
@@ -397,7 +397,7 @@ gone, so this list only shrinks.
397397 - **`style/content.ts`, code view** — the source view and the syntax-highlight token classes `highlight-html.ts` builds as `tok-${kind}`: `.blob-view` `.ln` `.tok-attribute` `.tok-comment` `.tok-constant` `.tok-decorator` `.tok-deleted` `.tok-emphasis` `.tok-function` `.tok-heading` `.tok-inserted` `.tok-keyword` `.tok-link` `.tok-meta` `.tok-number` `.tok-operator` `.tok-plain` `.tok-property` `.tok-punctuation` `.tok-regex` `.tok-string` `.tok-tag` `.tok-type` `.tok-variable`
398398 - **`style/content.ts`, prose (markdown)** — table cell alignment, built by `markdown.ts` as `ta-${align}`: `.ta-c` `.ta-r`
399399 - **`style/content.ts`, diff viewer** — the diff table: its gutters, line kinds, file head and inline review comments: `.add` `.del` `.dh` `.diff` `.diff-body` `.diff-head` `.diff-note` `.diff-path` `.diff-stat` `.diff-stub` `.diff-summary` `.diff-table` `.dl` `.dl-review` `.dn` `.dn-comment` `.nonl` `.rc-hidden` `.rc-resolve` `.rc-resolved` `.wd`
400 - **`style/content.ts`, apps, runs, and the crew** — run pages, their logs and checks (a pull's look is §5's **look**): `.ln-forge` `.ln-pulse` `.ln-say` `.ln-tool` `.ln-you` `.log-tail-row` `.pinned-failures` `.pinned-slot` `.run-jump` `.run-live` `.run-steer` `.tests-bar` `.tests-words` `.txt` `.who`
400 - **`style/content.ts`, apps, runs, and the crew** — run pages, their logs and checks (a pull's look is §5's **look**): `.ln-forge` `.ln-pulse` `.ln-say` `.ln-tool` `.ln-you` `.log-tail-row` `.pinned-failures` `.pinned-slot` `.run-jump` `.run-live` `.run-steer` `.txt` `.who`
401401 - **`style/content.ts`, search results** — search results, the inbox's ask cards: `.ask-acts` `.ask-card` `.ask-group` `.ask-kind` `.ask-meta` `.ask-more-acts` `.ask-pick` `.ask-pick-hit` `.ask-short` `.ask-waiting` `.live-toggle` `.needs-you`
402402 - **`pages/board.ts`** — the board itself is §5's **board**; the spend tables `spend.ts` draws wear §5's `.table` and `.stack`, no class of their own
403403 - **`pages/map.ts`** — the map of what waits for what: `.map-ask` `.map-breath` `.map-canvas` `.map-card` `.map-counts` `.map-dot` `.map-edge` `.map-edge-crit` `.map-edge-done` `.map-empty` `.map-list` `.map-main` `.map-meta` `.map-node` `.map-pillars` `.map-rule` `.map-svg` `.map-title` `.map-tool` `.map-tools` `.map-who` `.map-who-i` `.map-who-n` `.map-word` `.map-wrap`
docs/design/areas/runs.md added +3
@@ -0,0 +1,3 @@
1 # Runs
2
3 - `.check-words` — the run page's live tests line under the step list: reserves one line of height (1.4em) so the pin slot and log never move as the words arrive.
src/web/studio/run.ts +3 −3
@@ -309,11 +309,11 @@ export async function runView(
309309 ),
310310 )}
311311 ${known
312 ? html`<progress class="tests-bar" aria-label="Test files started" max="${row.of ?? 0}" value="${row.started ?? 0}"></progress>`
312 ? html`<progress aria-label="Test files started" max="${row.of ?? 0}" value="${row.started ?? 0}"></progress>`
313313 : open
314 ? html`<progress class="tests-bar" aria-label="Test files started"></progress>`
314 ? html`<progress aria-label="Test files started"></progress>`
315315 : ""}
316 <p class="tests-words small muted">${[words, runningWords].filter((w) => w !== "").join(" · ")}</p>
316 <p class="check-words small muted break">${[words, runningWords].filter((w) => w !== "").join(" · ")}</p>
outdated

This line used to reserve 1.4em of height. Without it an empty <p> collapses and the page shifts when the words arrive. Put the reservation back with a token-based rule.

317317 </div>
318318 <div class="pinned-slot" id="check-pins"${row.failures.length > 0 ? raw(' role="region" tabindex="0" aria-label="Failed tests"') : ""}>
319319 ${when(
src/web/style/components.ts +4 −4
@@ -238,10 +238,10 @@ a.label:hover { color: color-mix(in srgb, var(--label) 90%, var(--fg)); }
238238 .pill[data-tone="accent"] { background: color-mix(in srgb, var(--accent) 14%, transparent); color: var(--accent); }
239239 .pill[data-tone="danger"] { background: color-mix(in srgb, var(--danger) 14%, transparent); color: var(--danger); }
240240
241 progress:not(.tests-bar) { display: block; width: 100%; height: 4px; margin: 6px 0; appearance: none; border: 0; border-radius: var(--radius-sm); background: var(--border); overflow: hidden; }
242 progress:not(.tests-bar)::-webkit-progress-bar { background: var(--border); }
243 progress:not(.tests-bar)::-webkit-progress-value { background: var(--success); }
244 progress:not(.tests-bar)::-moz-progress-bar { background: var(--success); }
241 progress { display: block; width: 100%; height: 4px; margin: 6px 0; appearance: none; border: 0; border-radius: var(--radius-sm); background: var(--border); overflow: hidden; }
242 progress::-webkit-progress-bar { background: var(--border); }
243 progress::-webkit-progress-value { background: var(--success); }
244 progress::-moz-progress-bar { background: var(--success); }
245245
246246 .dot { display: inline-block; width: 8px; height: 8px; border-radius: 999px; background: var(--faint); flex: none; }
247247 .dot[data-tone="success"] { background: var(--success); }
src/web/style/content.ts +1 −2
@@ -766,8 +766,7 @@ form.inline { display: inline; margin-left: auto; }
766766 .checks-line .ok { display: inline-flex; align-items: center; gap: 4px; color: var(--success); }
767767 .checks-line .bad { display: inline-flex; align-items: center; gap: 4px; color: var(--danger); }
768768 .checks-line .now { display: inline-flex; align-items: center; gap: 4px; color: var(--attn); }
769 .tests-bar { display: block; width: 100%; height: 8px; margin-top: 8px; }
770 .tests-words { margin: 4px 0 0; min-height: 1.4em; }
769 .check-words { margin: 4px 0 0; min-height: 1.4em; }
771770 /* Reserved only while the run is open: pins arriving never move the log. */
772771 .pinned-slot { max-height: 9em; overflow-y: auto; }
773772 [data-check-progress][data-state="queued"] .pinned-slot, [data-check-progress][data-state="running"] .pinned-slot { height: 9em; }
test/studio-runs.test.ts +9 −5
@@ -840,14 +840,14 @@ describe("working: a run's own page in the studio", () => {
840840 );
841841 expect(stepsLine(app.config.dataDir, { state: "running", run }, false)).toBe("");
842842 const page = await (await get(`/alice/engine/runs/${run.id}`, writerCookie)).text();
843 expect(page).toContain('<progress class="tests-bar" aria-label="Test files started" max="9" value="2"');
843 expect(page).toContain('<progress aria-label="Test files started" max="9" value="2"');
844844 expect(page).toContain("2 of 9 files started · 4 passed, 1 failed · running test/b.test.ts");
845845 expect(page).toContain("<strong>a breaks</strong>");
846846 expect(page).toContain("error: &lt;b&gt;boom&lt;/b&gt;");
847847 // The pins sit after the live region closes.
848848 const live = page.indexOf('role="status"');
849849 expect(live).toBeGreaterThan(-1);
850 expect(page.indexOf("pinned-failures")).toBeGreaterThan(page.indexOf("tests-words"));
850 expect(page.indexOf("pinned-failures")).toBeGreaterThan(page.indexOf("Test files"));
851851 expect(page.slice(live, page.indexOf("pinned-slot"))).not.toContain("a breaks");
852852 // The replaced element (#check-steps) closes before the pin slot, which is
853853 // patched in place so focus and scroll survive a refetch.
@@ -859,15 +859,19 @@ describe("working: a run's own page in the studio", () => {
859859 // Nothing logged yet: the bar and the line are still there, empty.
860860 const fresh = mk([]);
861861 const quiet = await (await get(`/alice/engine/runs/${fresh.id}`, writerCookie)).text();
862 expect(quiet).toContain('<progress class="tests-bar" aria-label="Test files started"></progress>');
862 expect(quiet).toContain('<progress aria-label="Test files started"></progress>');
863863 const block = quiet.slice(quiet.indexOf('id="check-steps"'), quiet.indexOf('id="run-live"'));
864864 expect(block).not.toContain("value=");
865865 expect(block).not.toContain("tabindex");
866866 expect(block).not.toContain('role="region"');
867867 // With pins, the slot is focusable and labelled; the live region holds no pin.
868868 expect(page).toContain('class="pinned-slot" id="check-pins" role="region" tabindex="0" aria-label="Failed tests"');
869 expect(page.slice(live, page.indexOf(`id="check-pins"`, live))).toContain("tests-words");
870 expect(quiet).toContain('class="tests-words');
869 expect(page.slice(live, page.indexOf(`id="check-pins"`, live))).toContain("2 of 9 files started");
870 expect(quiet).toContain('<p class="check-words small muted break"></p>'); // empty, but still rendered
871 for (const old of ["tests-bar", "tests-words"]) {
872 expect(page).not.toContain(old);
873 expect(quiet).not.toContain(old);
874 }
871875 // A finished run with no known count shows no bar at all.
872876 const done = mk(["$ bun install"]);
873877 finishRun(app.db, done.id, { state: "failed", summary: "x" });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs