Fork Horse repository, listed by its owner. Publisher snapshot.
packages/forkhorse-gatekeeper/test/expand.test.ts
Snapshot at 34e3175
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