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

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

Snapshot at 34e3175

Containing directory

9,978 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 { createElement } from "react";
6import { renderToStaticMarkup } from "react-dom/server";
7import { TooltipProvider } from "@cloudflare/kumo";
8import { stateView } from "../src/model.ts";
9import { mountDashboard } from "../src/dashboard.ts";
10import { createHttpTransport } from "../src/transport.ts";
11import { Races, openRaceCount, orderedRaces } from "../src/races.tsx";
12import { racesFixtureState } from "./races-fixtures.ts";
13import { getByRole, queryByRole } from "./roles.ts";
14
15const { JSDOM } = createRequire(import.meta.url)("jsdom");
16const brand = { name: "Fixture product", tagline: "Fixture races" };
17const fixture = () => stateView(racesFixtureState());
18const markup = (owner = true, state = fixture()) => new JSDOM(renderToStaticMarkup(
19  createElement(TooltipProvider, null, createElement(Races, { state, owner, openRace() {} })),
20));
21
22test("FH-081 orders recorded races by open/start then finished/finish, with empty opens last", () => {
23  const races = fixture().races!;
24  const original = races.map((race) => race.packet);
25  assert.deepEqual(orderedRaces(races).map((race) => race.packet), [
26    "race-advice", "race-judged", "race-one", "race-three", "race-zero", "race-photo", "race-winner",
27  ]);
28  assert.deepEqual(races.map((race) => race.packet), original, "sorting never mutates state");
29  assert.equal(openRaceCount(races), 4);
30  assert.equal(openRaceCount(races.filter((race) => race.status === "finished" || !race.entrants.length)), 0);
31});
32
33test("FH-081 table names, lane identities, archived and photo-finish history use existing contracts", () => {
34  const dom = markup();
35  try {
36    const root = dom.window.document.body as HTMLElement;
37    const table = getByRole(root, "table", { name: "Races" });
38    assert.deepEqual([...table.querySelectorAll("thead th")].map((cell) => cell.textContent),
39      ["Race", "Status", "Entrants", "Winner", "Judge's pick", "Started"]);
40    const three = table.querySelector('[data-race-packet="race-three"]')!;
41    assert.deepEqual([...three.querySelectorAll("td:nth-child(3) .race-list-name")].map((name) => name.textContent),
42      ["birch", "cedar", "amber"]);
43    assert.deepEqual([...three.querySelectorAll("td:nth-child(3) .race-saddlecloth .sr-only")].map((name) => name.textContent),
44      ["Agent number 2", "Agent number 3", "Agent number 1"]);
45    const photo = table.querySelector('[data-race-packet="race-photo"]')!;
46    assert.match(photo.textContent!, /Archived.*Finished.*photo finish/);
47    assert.match(photo.querySelector("td:nth-child(4)")!.textContent!, /cedarwon in 12:00\.0/);
48    assert.match(photo.querySelector("td:nth-child(6)")!.textContent!, /Finished 12 min later/);
49    assert.ok(photo.querySelector("time[datetime]"));
50    const zero = table.querySelector('[data-race-packet="race-zero"]')!;
51    assert.match(zero.querySelector("td:nth-child(2)")!.textContent!, /No entrants yet/);
52    assert.equal(zero.querySelector("td:nth-child(6)")!.textContent, "—");
53    assert.equal(root.querySelectorAll('[aria-live], [role="status"]').length, 0);
54  } finally { dom.window.close(); }
55});
56
57test("FH-081 advice never becomes a winner and missing winner history stays explicit", () => {
58  const state = fixture();
59  state.races!.find((race) => race.packet === "race-winner")!.winner = null;
60  const dom = markup(true, state);
61  try {
62    const root = dom.window.document.body as HTMLElement;
63    const judged = root.querySelector('[data-race-packet="race-judged"]')!;
64    assert.equal(judged.querySelector("td:nth-child(4)")!.textContent, "—");
65    assert.match(judged.querySelector("td:nth-child(5)")!.textContent!, /birchJudge fixture-judge/);
66    assert.equal(judged.querySelectorAll("td:nth-child(5) .race-saddlecloth").length, 1);
67    assert.match(root.querySelector('[data-race-packet="race-advice"] td:nth-child(5)')!.textContent!, /fixture-judge · no advice yet/);
68    assert.match(root.querySelector('[data-race-packet="race-winner"] td:nth-child(4)')!.textContent!, /No winner recorded/);
69    assert.doesNotMatch(root.textContent!, /verdict/i);
70  } finally { dom.window.close(); }
71});
72
73test("FH-081 empty owner guidance is copy-only and agent guidance has no command", () => {
74  for (const owner of [true, false]) {
75    const dom = markup(owner, stateView(racesFixtureState("races-empty")));
76    try {
77      const root = dom.window.document.body as HTMLElement;
78      assert.match(root.textContent!, /Races are started by the owner \(CLI or owner API\)\./);
79      assert.match(root.textContent!, /No races yet/);
80      assert.match(root.textContent!, /The first approved promote wins\./);
81      assert.equal(root.querySelector("table"), null);
82      assert.equal(!!root.querySelector(".first-run-copy"), owner);
83      if (owner) {
84        assert.match(root.textContent!, /Add mode: race to the packet file\./);
85        assert.match(root.textContent!, /packet create packet.yaml --config "\$CLI_CONFIG"/);
86      } else {
87        assert.match(root.textContent!, /The owner starts races\./);
88        assert.doesNotMatch(root.textContent!, /CLI_CONFIG/);
89      }
90      assert.equal(queryByRole(root, "button", { name: /Start a race/i }), null);
91    } finally { dom.window.close(); }
92  }
93});
94
95async function until(check: () => boolean) {
96  for (let attempt = 0; attempt < 200; attempt++) {
97    if (check()) return;
98    await new Promise((resolve) => setTimeout(resolve, 5));
99  }
100  assert.fail("Races fixture DOM did not settle");
101}
102
103test("FH-081 last tab is named and read-only for both roles; race navigation returns to its source", async () => {
104  for (const owner of [true, false]) {
105    const dom = new JSDOM('<div id="dashboard"></div>', { url: "https://fixture.idiot.tax/#races" });
106    installDom(dom.window);
107    const root = dom.window.document.getElementById("dashboard")! as HTMLElement;
108    let wire = racesFixtureState();
109    const calls: { path: string; method: string }[] = [];
110    const transport = createHttpTransport({ origin: "https://fixture.idiot.tax", fetch: async (input, init) => {
111      const path = new URL(String(input)).pathname;
112      calls.push({ path, method: init?.method ?? "GET" });
113      return Response.json(path.endsWith("/state") ? wire : { activity: [], nextAfter: 0 });
114    } });
115    const app = mountDashboard(root, { brand, transport, initialProject: wire.project.id, owner });
116    try {
117      await until(() => !!root.querySelector(".table-races"));
118      assert.ok(getByRole(root, "tabpanel", { name: "Races" }));
119      const tabs = [...root.querySelectorAll('[role="tab"]')];
120      assert.equal(tabs.length, 5);
121      assert.equal(tabs[3].textContent, "Agents");
122      assert.equal(tabs[4], getByRole(root, "tab", { name: /^Races\s*,\s+4$/ }));
123      const link = getByRole(root, "link", { name: "Open race Make delivery failures readable" });
124      assert.equal(link.getAttribute("href"), "#race/race-one");
125      if (!owner) dom.window.history.pushState = () => { throw new Error("Opaque fixture history"); };
126      link.click();
127      await until(() => !!root.querySelector(".race-page"));
128      // Let hashchange run too, including the opaque-frame History API fallback.
129      await new Promise((resolve) => setTimeout(resolve, 20));
130      assert.equal(dom.window.location.hash, "#race/race-one");
131      getByRole(root, "button", { name: "← Races" }).click();
132      await until(() => !root.querySelector(".race-page"));
133      assert.equal(dom.window.location.hash, "#races");
134      getByRole(root, "tab", { name: /^Work/ }).click();
135      root.querySelector<HTMLButtonElement>('[data-packet-id="race-one"] .packet-link')!.click();
136      await until(() => !!root.querySelector(".race-page"));
137      getByRole(root, "button", { name: "← Work" }).click();
138      assert.equal(dom.window.location.hash, "#work");
139      getByRole(root, "tab", { name: /^Races/ }).click();
140      wire = { ...wire, races: [] };
141      await app.refresh();
142      assert.ok(getByRole(root, "tab", { name: "Races" }));
143      assert.match(root.querySelector("#view-races")!.textContent!, /No races yet/);
144      assert.ok(calls.every((call) => call.method === "GET" && /\/(state|activity)$/.test(call.path)));
145    } finally {
146      app.dispose();
147      dom.window.close();
148    }
149  }
150});
151
152test("tab round trips keep exactly one copy of each Overview card (unique sibling keys)", async () => {
153  for (const owner of [true, false]) {
154    const dom = new JSDOM('<div id="dashboard"></div>', { url: "https://fixture.idiot.tax/#overview" });
155    installDom(dom.window);
156    const root = dom.window.document.getElementById("dashboard")! as HTMLElement;
157    const wire = racesFixtureState();
158    const transport = createHttpTransport({ origin: "https://fixture.idiot.tax", fetch: async (input) =>
159      Response.json(String(input).endsWith("/state") ? wire : { activity: [], nextAfter: 0 }) });
160    const app = mountDashboard(root, { brand, transport, initialProject: wire.project.id, owner, agent: owner ? undefined : "fixture-agent" });
161    const cards = () => [".fork-picture", '[aria-labelledby="needs-you-title"]', ".resume-card", ".project-card"]
162      .map((selector) => root.querySelectorAll(`#view-overview ${selector}`).length);
163    try {
164      await until(() => cards()[0] > 0);
165      const first = cards();
166      for (const tab of [/^Races/, /^Work/, /^Review/, /^Agents/]) {
167        getByRole(root, "tab", { name: tab }).click();
168        getByRole(root, "tab", { name: "Overview" }).click();
169        await new Promise((resolve) => setTimeout(resolve, 10));
170        assert.deepEqual(cards(), first, `${owner ? "owner" : "agent"} Overview after ${tab}`);
171      }
172      assert.equal(first[0], 1);
173      assert.equal(first[3], 1);
174      assert.ok(first.every((count) => count <= 1), `no Overview card repeats: ${first}`);
175    } finally {
176      app.dispose();
177      dom.window.close();
178    }
179  }
180});
181