Fork Horse repository, listed by its owner. Publisher snapshot.

packages/forkhorse-dashboard/test/a11y.test.ts

Snapshot at 34e3175

Containing directory

13,738 bytes · typescriptView raw
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