Fork Horse repository, listed by its owner. Publisher snapshot.
packages/forkhorse-dashboard/test/gatekeeper-fixture.ts
Snapshot at 34e3175
1// Loads actual production bundles with synthetic host/data.
2// RPC comes from the gatekeeper's declared dependency, so this fixture adds no dashboard dependency.
3import {
4 newMessagePortRpcSession,
5 RpcTarget,
6 type RpcStub,
7} from "../../forkhorse-gatekeeper/node_modules/capnweb/dist/index.js";
8import type {
9 GatekeeperAppTheme,
10 GatekeeperAppThemeReceiver,
11} from "@gadgets/workshop-shared/theme";
12import { forkFixtureState } from "./fork-graph-fixtures.ts";
13import { stateView } from "../src/model.ts";
14
15const params = new URL(location.href).searchParams;
16const owner = params.get("bundle") === "owner";
17const state = stateView(
18 forkFixtureState(new URL(location.href).searchParams.get("scenario") ?? "fork-hero"),
19);
20// A static fixture capability uses the same RPC module as its host (no mixed prebundled targets).
21class FixtureUi extends RpcTarget {
22 async snapshot() {
23 return { version: 0, state: structuredClone(state) };
24 }
25 async subscribe() {
26 return new (class extends RpcTarget {
27 async close() {}
28 })();
29 }
30 async waitForChanges(): Promise<never> {
31 throw new Error("Offline fixture is static.");
32 }
33}
34const ui = new FixtureUi();
35let theme: GatekeeperAppTheme = { mode: "light", accentColor: null };
36let receiver: RpcStub<GatekeeperAppThemeReceiver> | undefined;
37class FixtureHost extends RpcTarget {
38 get ui() {
39 return ui;
40 }
41 subscribeTheme(next: RpcStub<GatekeeperAppThemeReceiver>) {
42 receiver?.[Symbol.dispose]();
43 receiver = next.dup();
44 return theme;
45 }
46 openPrompt() {
47 /* Offline fixture never sends a prompt. */
48 }
49}
50const response = await fetch(
51 owner
52 ? `/__fixtures/owner-bundle?scenario=${encodeURIComponent(state.project.id)}`
53 : "/__fixtures/gatekeeper-bundle",
54);
55if (!response.ok)
56 throw new Error("Build the gatekeeper sidebar assets before opening this fixture.");
57const iframe = document.createElement("iframe");
58iframe.title = owner ? "Offline built owner app" : "Offline built gatekeeper";
59iframe.setAttribute("sandbox", owner ? "allow-scripts allow-same-origin" : "allow-scripts");
60iframe.style.cssText = "width:100%;height:calc(100vh - 36px);border:0;display:block";
61if (owner) iframe.src = `/__fixtures/owner-bundle?scenario=${encodeURIComponent(state.project.id)}`;
62else
63 iframe.srcdoc = (await response.text()).replace(
64 '<div id="sidebar">',
65 '<div id="sidebar" data-fixture="true">',
66 );
67let session: ReturnType<typeof newMessagePortRpcSession> | undefined;
68const handshake = (event: MessageEvent) => {
69 if (
70 event.source !== iframe.contentWindow ||
71 event.origin !== "null" ||
72 event.data?.type !== "handshake" ||
73 !event.ports[0]
74 )
75 return;
76 if (session) {
77 event.ports[0].close();
78 return;
79 }
80 session = newMessagePortRpcSession(event.ports[0], new FixtureHost());
81};
82window.addEventListener("message", handshake);
83document.getElementById("frame")!.append(iframe);
84for (const mode of ["light", "dark"] as const)
85 document.getElementById(mode)!.addEventListener("click", () => {
86 theme = { mode, accentColor: null };
87 if (owner)
88 iframe.contentWindow?.postMessage({ type: "offline-fixture-theme", theme }, location.origin);
89 else void receiver?.setTheme(theme);
90 });
91window.addEventListener(
92 "pagehide",
93 () => {
94 window.removeEventListener("message", handshake);
95 receiver?.[Symbol.dispose]();
96 session?.[Symbol.dispose]();
97 },
98 { once: true },
99);
100