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

packages/forkhorse-gatekeeper/test/expand.test.ts

Snapshot at 34e3175

Containing directory

6,067 bytes · typescriptView raw
1import { installDom } from "../../forkhorse-dashboard/test/dom.ts";
2import assert from "node:assert/strict";
3import { createRequire } from "node:module";
4import { setImmediate as turn } from "node:timers/promises";
5import { test } from "node:test";
6import { fixtureState } from "../../forkhorse-dashboard/test/fixtures.ts";
7import { stateView } from "../../forkhorse-dashboard/src/model.ts";
8import { getByRole, queryByRole } from "../../forkhorse-dashboard/test/roles.ts";
9import { framePresentation, mountSidebar } from "../src/mount.ts";
10
11// DOM fixtures only. The browser fixture and pinned OS review verify real frame geometry.
12const { JSDOM } = createRequire(import.meta.url)("jsdom");
13const ack = { rect: null, willResize: false };
14async function until(predicate: () => boolean) {
15  for (let i = 0; i < 300 && !predicate(); i++) await turn();
16  assert.ok(predicate(), "fixture condition settled");
17}
18function setup(setPresenting?: (active: boolean) => Promise<typeof ack>) {
19  const dom = new JSDOM('<div id="root"></div>', { url: "https://os.fixture.idiot.tax/" });
20  installDom(dom.window);
21  const root = dom.window.document.getElementById("root") as HTMLElement;
22  const view = mountSidebar(root, {
23    brand: { name: "Fixture product", tagline: "Fixture collaboration" },
24    project: "demo", fixture: true, setPresenting,
25    openPrompt: async () => {},
26    transport: {
27      getState: async () => stateView(fixtureState()),
28      getActivity: async () => ({ activity: [], nextAfter: 0 }),
29      approve: async () => { throw new Error("Agent cannot approve"); },
30      promote: async () => { throw new Error("Agent cannot promote"); },
31    },
32  });
33  return { dom, root, view, close() { view.dispose(); dom.window.close(); } };
34}
35
36test("Expand is optional; its label, icon and pressed state flip without losing focus", async (t) => {
37  const calls: boolean[] = [];
38  const f = setup(async (active) => { calls.push(active); return ack; });
39  t.after(() => f.close());
40  await until(() => !!f.root.querySelector(".fork-card"));
41  const expand = getByRole(f.root, "button", { name: "Expand" });
42  const icon = expand.querySelector("svg")!.innerHTML;
43  const activity = getByRole(f.root, "button", { name: "Hide activity" });
44  assert.ok(expand.compareDocumentPosition(activity) & 4, "Expand precedes Activity");
45  expand.focus();
46  expand.click();
47  await until(() => expand.getAttribute("aria-label") === "Exit full view");
48  assert.equal(expand.getAttribute("aria-pressed"), "true");
49  assert.notEqual(expand.querySelector("svg")!.innerHTML, icon);
50  assert.equal(f.dom.window.document.activeElement, expand);
51  for (const name of ["Work", "Agents", "Races", "Overview"]) {
52    getByRole(f.root, "tab", { name: new RegExp(`^${name}`) }).click();
53    await turn();
54  }
55  assert.equal(f.root.querySelectorAll(".fork-card").length, 1);
56  assert.equal(getByRole(f.root, "button", { name: "Exit full view" }), expand);
57  expand.click();
58  await until(() => expand.getAttribute("aria-label") === "Expand");
59  assert.equal(expand.getAttribute("aria-pressed"), "false");
60  assert.equal(expand.querySelector("svg")!.innerHTML, icon);
61  assert.deepEqual(calls, [true, false]);
62  for (const name of ["Work", "Overview"]) {
63    getByRole(f.root, "tab", { name: new RegExp(`^${name}`) }).click();
64    await turn();
65  }
66  assert.equal(f.root.querySelectorAll(".fork-card").length, 1);
67});
68
69test("no presentation option renders no Expand", async (t) => {
70  const f = setup();
71  t.after(() => f.close());
72  assert.equal(queryByRole(f.root, "button", { name: "Expand" }), null);
73});
74
75test("Escape closes the Kumo Activity dialog first, then exits full view", async (t) => {
76  const calls: boolean[] = [];
77  const f = setup(async (active) => { calls.push(active); return ack; });
78  t.after(() => f.close());
79  getByRole(f.root, "button", { name: "Expand" }).click();
80  await until(() => !!queryByRole(f.root, "button", { name: "Exit full view" }));
81  f.dom.window.innerWidth = 800;
82  f.dom.window.dispatchEvent(new f.dom.window.Event("resize"));
83  await until(() => !!f.dom.window.document.querySelector('[role="dialog"]'));
84  const escape = () => f.dom.window.document.activeElement.dispatchEvent(
85    new f.dom.window.KeyboardEvent("keydown", { key: "Escape", bubbles: true, cancelable: true }),
86  );
87  escape();
88  await until(() => !f.dom.window.document.querySelector('[role="dialog"]:not([data-closed])'));
89  assert.equal(calls.at(-1), true, "dialog dismissal does not exit full view");
90  escape();
91  await until(() => !!queryByRole(f.root, "button", { name: "Expand" }));
92  assert.equal(calls.at(-1), false);
93  const count = calls.length;
94  escape();
95  assert.equal(calls.length, count, "collapsed Escape does nothing");
96});
97
98test("reconnect reset and dispose restore the host, ignoring late opening acknowledgements", async (t) => {
99  const calls: boolean[] = [];
100  let release!: (ack: { rect: null; willResize: boolean }) => void;
101  const f = setup((active) => {
102    calls.push(active);
103    return active ? new Promise((resolve) => { release = resolve; }) : Promise.resolve(ack);
104  });
105  t.after(() => f.close());
106  getByRole(f.root, "button", { name: "Expand" }).click();
107  await until(() => !!queryByRole(f.root, "button", { name: "Exit full view" }));
108  f.view.resetPresentation();
109  release(ack);
110  await until(() => !!queryByRole(f.root, "button", { name: "Expand" }));
111  assert.equal(calls.at(-1), false);
112  getByRole(f.root, "button", { name: "Expand" }).click();
113  f.view.dispose();
114  release(ack);
115  await turn();
116  assert.equal(calls.at(-1), false);
117  assert.equal(f.root.childElementCount, 0);
118});
119
120test("rejected host presentation remains collapsed and restores with false", async () => {
121  const calls: boolean[] = [];
122  const presentation = framePresentation(async (active) => {
123    calls.push(active);
124    if (active) throw new Error("Fixture unsupported host");
125    return ack;
126  });
127  presentation.toggle();
128  await turn();
129  assert.equal(presentation.getSnapshot(), false);
130  assert.deepEqual(calls, [true, false]);
131  presentation.dispose();
132});
133