Fork Horse repository, listed by its owner. Publisher snapshot.
packages/forkhorse-dashboard/test/a11y.test.ts
Snapshot at 34e3175
1import { installDom } from "./dom.ts";
2import assert from "node:assert/strict";
3import test from "node:test";
4import { createRequire } from "node:module";
5import { act, createElement } from "react";
6import { createRoot } from "react-dom/client";
7import { renderToStaticMarkup } from "react-dom/server";
8import { TooltipProvider } from "@cloudflare/kumo";
9import { DashboardView, type Section, type ViewActions } from "../src/view.tsx";
10import { ForkGraph } from "../src/fork-graph.tsx";
11import { RaceView } from "../src/race.tsx";
12import { ReceiptCell, receiptWord } from "../src/receipts.tsx";
13import { Saddlecloth } from "../src/lane.tsx";
14import { CollisionBanner } from "../src/collisions.tsx";
15import { blockerPacket, stateView, type ViewModel } from "../src/model.ts";
16import { raceSplit, raceStart } from "../src/race-model.ts";
17import { fixtureReceipt, fixtureState } from "./fixtures.ts";
18import { forkFixtureState } from "./fork-graph-fixtures.ts";
19import { raceFixtureState } from "./race-fixtures.ts";
20import { computeAccessibleDescription, getAllByRole, getByRole, queryByRole } from "./roles.ts";
21
22const { JSDOM } = createRequire(import.meta.url)("jsdom");
23const brand = { name: "Fixture app", tagline: "Offline fixture" };
24const actions: ViewActions = {
25 section: "overview", owner: true, openReview() {}, setSection() {},
26 async selectProject() {}, async action() {},
27};
28const model = (state = stateView(fixtureState("history-required"))): ViewModel => ({
29 brand, project: state.project.id, projects: [], fixture: true, state,
30 activity: state.recentActivity ?? [], busy: false,
31});
32function markup(element: Parameters<typeof renderToStaticMarkup>[0]) {
33 return new JSDOM(renderToStaticMarkup(element));
34}
35
36test("fork controls stay accessible within a named group; decorative legend and hint retain spoken words", () => {
37 const now = Date.parse("2026-10-13T16:00:00Z");
38 const state = stateView(forkFixtureState("fork-hero", now));
39 const dom = markup(createElement(TooltipProvider, null, createElement(ForkGraph, {
40 state, brand, owner: true, openReview() {}, openPacket() {}, openReceipt() {}, now,
41 })));
42 try {
43 const root = dom.window.document.body;
44 const group = getByRole(root, "group", { name: /^Fork graph: / });
45 assert.equal(queryByRole(root, "img", { name: /^Fork graph: / }), null);
46 assert.ok(getAllByRole(group, "button", { name: /.+/ }).length > 0);
47 assert.ok(getByRole(group, "link", { name: /^Race · / }));
48 const buttons = getAllByRole(group, "button", { name: /^Agent number / });
49 assert.ok(buttons.length);
50 assert.ok(buttons.every((button) => button.title.startsWith("Tokens: ")));
51 assert.ok(buttons.some((button) => button.title === "Tokens: not reported"));
52 const legend = root.querySelector(".fork-legend")!;
53 assert.equal(legend.querySelectorAll('[aria-hidden="true"]').length, 6);
54 assert.ok([...legend.querySelectorAll('[aria-hidden="true"]')].some((glyph) => (glyph as Element).textContent === "!"));
55 assert.match(legend.textContent!, /! checks didn't finish/);
56 assert.match(legend.textContent!, /push · .* review requested · .* checks passed\/failed/);
57 const hint = root.querySelector(".fork-window")!;
58 assert.match(hint.textContent!, /now\. hover a mark for its time/);
59 assert.equal(hint.querySelector(".sr-only")!.textContent, ". ");
60 } finally { dom.window.close(); }
61});
62
63test("each receipt names its packet, exact short head and status, and exposes its caption as description", () => {
64 for (const status of ["running", "passed", "failed", "timed-out", "infrastructure-error"] as const) {
65 const receipt = fixtureReceipt(status);
66 const dom = markup(createElement("div", null,
67 createElement(ReceiptCell, { packet: "fixture-01", sha: "a".repeat(40), receipt, required: true, open() {} }),
68 createElement(ReceiptCell, { packet: "fixture-02", sha: "b".repeat(40), receipt, required: true, open() {} }),
69 ));
70 try {
71 const root = dom.window.document.body;
72 const first = getByRole(root, "button", { name: `View receipt for fixture-01 at aaaaaaa: ${receiptWord(status).label}` });
73 const second = getByRole(root, "button", { name: `View receipt for fixture-02 at bbbbbbb: ${receiptWord(status).label}` });
74 assert.ok(computeAccessibleDescription(first).length);
75 assert.equal(computeAccessibleDescription(first), first.querySelector(".caption")!.textContent!.trim());
76 assert.notEqual(first.getAttribute("aria-describedby"), second.getAttribute("aria-describedby"));
77 assert.equal(queryByRole(root, "button", { name: `View receipt: ${receiptWord(status).label}` }), null);
78 assert.equal(queryByRole(root, "button", { name: /at ccccccc/ }), null);
79 } finally { dom.window.close(); }
80 }
81});
82
83test("collision packet buttons identify the packet they open", () => {
84 const blockers = stateView(fixtureState()).blockers;
85 const first = blockers[0];
86 assert.ok(first);
87 const second = { ...first, id: "fixture-collision-2", pushingPacket: "fixture-other" };
88 const dom = markup(createElement(TooltipProvider, null, createElement("div", null,
89 ...[first, second].map((blocker) => createElement(CollisionBanner, { key: blocker.id, blocker, owner: true, showPacket() {} })),
90 )));
91 try {
92 for (const blocker of [first, second]) {
93 assert.ok(getByRole(dom.window.document.body, "button", { name: `View packet ${blockerPacket(blocker)}` }));
94 }
95 assert.equal(queryByRole(dom.window.document.body, "button", { name: "View packet" }), null);
96 } finally { dom.window.close(); }
97});
98
99test("summary values leave the heading outline, panels use title case, tabs separate counts and all four tables are named", () => {
100 // Keep live claims and blockers for the count tabs, plus promoted receipts for
101 // the history table. The receipt-history fixture alone has no Work count.
102 const state = stateView(fixtureState());
103 const history = stateView(fixtureState("history-required"));
104 state.reviews.push(...history.reviews.filter((review) => review.status === "promoted"));
105 state.ledger = history.ledger;
106 const view = model(state);
107 for (const section of ["overview", "work", "review", "agents"] as const satisfies readonly Section[]) {
108 const dom = markup(createElement(DashboardView, { view, actions: { ...actions, section } }));
109 try {
110 const root = dom.window.document.body;
111 const name = { overview: "Overview", work: "Work", review: "Review", agents: "Agents" }[section];
112 assert.ok(getByRole(root, "tabpanel", { name }));
113 assert.equal(queryByRole(root, "tabpanel", { name: section }), null);
114 assert.ok(getByRole(root, "tab", { name: /^Work\s*,\s+\d+$/ }));
115 assert.ok(getByRole(root, "tab", { name: /^Review\s*,\s+\d+$/ }));
116 assert.equal(queryByRole(root, "tab", { name: /^Work\d+$/ }), null);
117 if (section === "overview") {
118 const summary = getByRole(root, "region", { name: "Project summary" });
119 assert.equal(summary.querySelectorAll("h3").length, 0);
120 assert.equal(summary.querySelectorAll("p.metric-value").length, 4);
121 assert.equal(queryByRole(summary, "heading", { name: /^\d+$/ }), null);
122 assert.ok(summary.querySelector('.warning-dot[aria-hidden="true"]'));
123 assert.match(summary.textContent!, /\d+ open blockers/);
124 }
125 const tableNames = { overview: [], work: ["Packets and claims"], review: ["Review queue", "Recent receipts"], agents: ["Agents"] }[section];
126 for (const tableName of tableNames) {
127 assert.ok(getByRole(root, "table", { name: tableName }));
128 }
129 } finally { dom.window.close(); }
130 }
131 const uncounted = markup(createElement(DashboardView, { view: model(), actions }));
132 try {
133 assert.ok(getByRole(uncounted.window.document.body, "tab", { name: "Work" }));
134 assert.equal(queryByRole(uncounted.window.document.body, "tab", { name: /^Work\s*,/ }), null);
135 } finally { uncounted.window.close(); }
136});
137
138test("saddlecloth exposes spoken identity once, including unknown agents; the race flag is decorative", () => {
139 for (const number of [1, 12]) {
140 const dom = markup(createElement("button", null,
141 createElement(Saddlecloth, { identity: { number, silk: 1 } }), " cedar",
142 ));
143 try {
144 const button = getByRole(dom.window.document.body, "button", { name: `Agent number ${number} cedar` });
145 assert.equal(queryByRole(dom.window.document.body, "button", { name: `${number} cedar` }), null);
146 assert.equal(button.querySelector('[aria-hidden="true"]')!.textContent, String(number));
147 } finally { dom.window.close(); }
148 }
149 const unknown = markup(createElement("button", null, createElement(Saddlecloth), " guest"));
150 try { assert.ok(getByRole(unknown.window.document.body, "button", { name: "Agent number unavailable guest" })); }
151 finally { unknown.window.close(); }
152 const dom = markup(createElement(TooltipProvider, null, createElement(RaceView, {
153 packet: "race-01", view: model(stateView(raceFixtureState())), actions,
154 })));
155 try {
156 assert.ok(getByRole(dom.window.document.body, "table", { name: "Race progress for race-01" }));
157 assert.equal(dom.window.document.querySelector('.race-flag [aria-hidden="true"]')!.textContent, "⚑");
158 assert.match(dom.window.document.querySelector(".race-flag")!.textContent!, /Racing/);
159 } finally { dom.window.close(); }
160});
161
162test("race status is initially silent, batches new milestones and failure, suppresses repeats, and resets on switches", async () => {
163 const dom = new JSDOM('<div id="fixture"></div>', { url: "https://fixture.idiot.tax" });
164 installDom(dom.window);
165 const container = dom.window.document.getElementById("fixture")!;
166 const root = createRoot(container);
167 let state = stateView(raceFixtureState());
168 // The open fixture's last entrant has not requested review yet. Establish an
169 // in-flight receipt before mounting, so the next snapshot tests its failure.
170 const initialAmber = state.races![0].entrants.find((entrant) => entrant.agent === "amber")!;
171 initialAmber.firstPushAt = "2026-10-13T14:09:00Z";
172 initialAmber.reviewRequestedAt = "2026-10-13T14:10:00Z";
173 state.reviews.push({ ...structuredClone(state.reviews[0]),
174 id: "fixture-amber-review", agent: "amber", repoName: initialAmber.repoName,
175 requestedAt: initialAmber.reviewRequestedAt, receipt: fixtureReceipt("running"),
176 });
177 const render = async (packet = "race-01", project = "fixture-race") => {
178 await act(async () => root.render(createElement(TooltipProvider, null,
179 createElement(RaceView, { packet, view: { ...model(state), project }, actions }),
180 )));
181 };
182 const status = () => getByRole(container, "status", { name: "" });
183 try {
184 await render();
185 assert.equal(status().getAttribute("aria-live"), "polite");
186 assert.equal(status().textContent, "");
187 state = structuredClone(state);
188 const race = state.races![0];
189 const birch = race.entrants.find((entrant) => entrant.agent === "birch")!;
190 birch.approvedAt = "2026-10-13T14:13:00Z";
191 const amber = race.entrants.find((entrant) => entrant.agent === "amber")!;
192 const failedReview = state.reviews.find((review) => review.repoName === amber.repoName)!;
193 failedReview.receipt = { ...failedReview.receipt!, status: "failed", finishedAt: "2026-10-13T14:13:05Z" };
194 await render();
195 const message = status().textContent!;
196 assert.equal(message, `birch: Approved, +${raceSplit(birch.approvedAt, raceStart(race))}. amber: Checks failed, +${raceSplit(failedReview.receipt.finishedAt, raceStart(race))}`);
197 const failedCell = container.querySelector('[data-race-agent="amber"] [data-milestone="receiptPassedAt"] .sr-only')!;
198 assert.match(failedCell.textContent!, /^Checks failed, plus/);
199 state = structuredClone(state);
200 await render();
201 assert.equal(status().textContent, message);
202 await render("race-01", "other-project");
203 assert.equal(status().textContent, "");
204 state = structuredClone(state);
205 state.races![0].packet = "race-02";
206 await render("race-02", "other-project");
207 assert.equal(status().textContent, "");
208 } finally {
209 await act(async () => root.unmount());
210 dom.window.close();
211 }
212});
213
214test("race finish announces an observed winner or a winnerless finish, never a historical finish", async () => {
215 for (const winner of ["cedar", null]) {
216 const dom = new JSDOM('<div id="fixture"></div>', { url: "https://fixture.idiot.tax" });
217 installDom(dom.window);
218 const container = dom.window.document.getElementById("fixture")!;
219 const root = createRoot(container);
220 let state = stateView(raceFixtureState());
221 const render = async () => { await act(async () => root.render(createElement(TooltipProvider, null,
222 createElement(RaceView, { packet: "race-01", view: model(state), actions }),
223 ))); };
224 try {
225 await render();
226 state = structuredClone(state);
227 const race = state.races![0];
228 race.status = "finished";
229 race.winner = winner;
230 race.finishedAt = "2026-10-13T14:14:00Z";
231 await render();
232 const expected = winner ? `Race finished. cedar won in ${raceSplit(race.finishedAt, raceStart(race))}.` : "Race finished.";
233 assert.equal(getByRole(container, "status", { name: "" }).textContent, expected);
234 await act(async () => root.unmount());
235 const freshRoot = createRoot(container);
236 await act(async () => freshRoot.render(createElement(TooltipProvider, null,
237 createElement(RaceView, { packet: "race-01", view: model(state), actions }),
238 )));
239 assert.equal(getByRole(container, "status", { name: "" }).textContent, "");
240 await act(async () => freshRoot.unmount());
241 } finally { dom.window.close(); }
242 }
243});
244