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

packages/forkhorse-dashboard/test/gatekeeper-fixture.ts

Snapshot at 34e3175

Containing directory

3,426 bytes · typescriptView raw
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