Fork Horse repository, listed by its owner. Publisher snapshot.
packages/forkhorse-gatekeeper/test/fixture-main.ts
Snapshot at 34e3175
1import { newMessagePortRpcSession, RpcTarget, type RpcStub } from "capnweb";
2import type {
3 GatekeeperAppTheme,
4 GatekeeperAppThemeReceiver,
5} from "@gadgets/workshop-shared/theme";
6import { createRateLimitedCapability } from "../../../cloudflare-os/packages/workshop-frontend/src/rateLimitedCapability.ts";
7import { fixtureState } from "../../forkhorse-dashboard/test/fixtures.ts";
8import { stateView } from "../../forkhorse-dashboard/src/model.ts";
9import { SnapshotFeed } from "../src/feed.ts";
10import { FixtureUi } from "./fixture-ui.ts";
11
12const state = stateView(fixtureState());
13const feed = new SnapshotFeed(state);
14const recovery = new URL(location.href).searchParams.has("recovery");
15let oldPolls = 0;
16const fixture = recovery
17 ? new (class extends FixtureUi {
18 async subscribe(): Promise<never> {
19 throw new Error("Fixture stalled push channel");
20 }
21 async waitForChanges(): Promise<never> {
22 oldPolls++;
23 return new Promise(() => {});
24 }
25 })(feed)
26 : new FixtureUi(feed);
27const limited = createRateLimitedCapability(fixture, {
28 maxConcurrency: 8,
29 maxCallsPerMinute: 600,
30 maxPendingCalls: 128,
31 onRateLimit: "throttle",
32 label: "Fixture sidebar",
33});
34const output = document.getElementById("fixture-output")!;
35const freshState = stateView(fixtureState());
36freshState.packets[0]!.title = "Fresh recovered fixture data";
37const freshFeed = new SnapshotFeed(freshState);
38const fresh = new FixtureUi(freshFeed);
39const freshLimited = createRateLimitedCapability(fresh, {
40 maxConcurrency: 8,
41 maxCallsPerMinute: 600,
42 maxPendingCalls: 128,
43 onRateLimit: "throttle",
44 label: "Fixture recovered sidebar",
45});
46let acquired = 0,
47 themes = 0;
48let presenting = false;
49const presentationCalls: boolean[] = [];
50let theme: GatekeeperAppTheme = {
51 mode: matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light",
52 accentColor: null,
53};
54let themeReceiver: RpcStub<GatekeeperAppThemeReceiver> | undefined;
55function evidence() {
56 output.textContent = JSON.stringify({
57 acquired,
58 oldPolls,
59 freshCalls: fresh.calls,
60 themes,
61 presenting,
62 presentationCalls,
63 hostMode: theme.mode,
64 fixture: "offline replacement host capability",
65 });
66}
67class FixtureHost extends RpcTarget {
68 get ui() {
69 acquired++;
70 evidence();
71 return recovery && acquired > 1 ? freshLimited.capability : limited.capability;
72 }
73 subscribeTheme(receiver: RpcStub<GatekeeperAppThemeReceiver>) {
74 themeReceiver?.[Symbol.dispose]();
75 themeReceiver = receiver.dup();
76 themes++;
77 evidence();
78 return theme;
79 }
80 openPrompt(prompt: string) {
81 output.textContent = `Fixture openPrompt received: ${prompt}`;
82 }
83 setPresenting(active: boolean) {
84 const rect = iframe.getBoundingClientRect().toJSON();
85 presentationCalls.push(active);
86 presenting = active;
87 iframe.style.cssText = active
88 ? "position:fixed;inset:0;width:100vw;height:100vh;border:0;z-index:10"
89 : "width:100%;height:100vh;border:0";
90 evidence();
91 return { rect: active ? rect : null, willResize: true };
92 }
93}
94if (recovery)
95 for (const mode of ["light", "dark"] as const) {
96 const button = document.createElement("button");
97 button.textContent = `Fixture host ${mode} during stall`;
98 button.addEventListener("click", () => {
99 // Simulate a dropped theme callback; recovery must apply subscribeTheme's current reply.
100 theme = { mode, accentColor: null };
101 document.documentElement.dataset.mode = mode;
102 document.documentElement.style.colorScheme = mode;
103 evidence();
104 });
105 document.body.insertBefore(button, document.getElementById("sidebar"));
106 }
107
108// Exercise the actual built document in the same opaque-origin sandbox as pinned OS.
109const response = await fetch("/dist/sidebar.html");
110if (!response.ok) throw new Error("Build the sidebar before starting its fixture host.");
111const html = await response.text();
112const iframe = document.createElement("iframe");
113iframe.title = "Local fixture sidebar";
114iframe.setAttribute("sandbox", "allow-scripts");
115iframe.style.cssText = "width:100%;height:100vh;border:0";
116// Label only the fixture document; the production build has no fixture data or marker.
117iframe.srcdoc = html.replace('<div id="sidebar">', '<div id="sidebar" data-fixture="true" data-core-origin="https://core.fixture.idiot.tax">');
118let session: ReturnType<typeof newMessagePortRpcSession> | undefined;
119const handshake = (event: MessageEvent) => {
120 if (
121 event.source !== iframe.contentWindow ||
122 event.origin !== "null" ||
123 event.data?.type !== "handshake" ||
124 !event.ports[0]
125 )
126 return;
127 if (session) {
128 event.ports[0].close();
129 return;
130 }
131 session = newMessagePortRpcSession(event.ports[0], new FixtureHost());
132};
133window.addEventListener("message", handshake);
134document.getElementById("sidebar")!.replaceChildren(iframe);
135let batches = 0;
136const timer = setInterval(() => {
137 if (recovery) {
138 evidence();
139 return;
140 }
141 batches++;
142 state.packets[0]!.title = `Fixture update batch ${batches}`;
143 feed.publish(state);
144}, 250);
145window.addEventListener(
146 "pagehide",
147 () => {
148 clearInterval(timer);
149 window.removeEventListener("message", handshake);
150 feed.dispose();
151 freshFeed.dispose();
152 limited.dispose();
153 freshLimited.dispose();
154 themeReceiver?.[Symbol.dispose]();
155 session?.[Symbol.dispose]();
156 },
157 { once: true },
158);
159