eleven / messenger Public

Clone
git clone https://amadan.net/eleven/messenger

Plain git — no account needed to clone.

Download

Download this file

1// Regression coverage for issue #402 ("sandbox apps don't update theme until
2// you switch away and then back"): a live "theme" postMessage from the host
3// (sandbox.js's pushTheme, fired whenever the member changes theme/appearance)
4// must re-tint an already-mounted sandbox frame in place — not just seed the
5// frame's INITIAL theme at mount time. Same minimal-DOM-over-a-real-
6// MessageChannel harness as sandbox-bridge.test.mjs, with an instrumented
7// documentElement.style so applyTheme's effect is observable.
8// node test/sandbox-theme.test.mjs
9import test from "node:test";
10import assert from "node:assert/strict";
11import { MessageChannel } from "node:worker_threads";
12
13const PARENT = { name: "parent-window" };
14function fakeNode() {
15 const node = {
16 style: { setProperty() {}, cssText: "", font: "" },
17 dataset: {},
18 children: [],
19 textContent: "",
20 appendChild(c) {
21 this.children.push(c);
22 return c;
23 },
24 prepend() {},
25 remove() {},
26 querySelector: () => null,
27 setAttribute() {},
28 addEventListener() {},
29 classList: { add() {}, toggle() {} },
30 };
31 return node;
32}
33
34// A style object that records every setProperty call and the colorScheme
35// assignment, so a test can assert exactly what a "theme" message applied.
36function spyStyle() {
37 const calls = [];
38 const style = {
39 setProperty(k, v) {
40 calls.push([k, v]);
41 },
42 get colorScheme() {
43 return style._colorScheme || "";
44 },
45 set colorScheme(v) {
46 style._colorScheme = v;
47 },
48 };
49 return { style, calls };
50}
51
52let bust = 0;
53async function connect({ source, manifest, seed, theme }) {
54 const rootStyle = spyStyle();
55 const body = fakeNode();
56 globalThis.document = {
57 createElement: () => fakeNode(),
58 body,
59 head: fakeNode(),
60 documentElement: { style: rootStyle.style },
61 getElementById: () => null,
62 addEventListener() {},
63 };
64 const listeners = {};
65 globalThis.window = {
66 parent: PARENT,
67 addEventListener: (t, fn) => ((listeners[t] ||= []).push(fn)),
68 removeEventListener: (t, fn) => {
69 listeners[t] = (listeners[t] || []).filter((f) => f !== fn);
70 },
71 };
72 await import(`../web/static/sandbox-frame.js?b=${++bust}`);
73
74 const chan = new MessageChannel();
75 const inbox = [];
76 const waiters = [];
77 chan.port1.onmessage = (ev) => {
78 inbox.push(ev.data);
79 for (const w of waiters.splice(0)) w();
80 };
81 for (const fn of listeners.message || []) {
82 fn({
83 source: PARENT,
84 origin: "http://instance.test",
85 data: { type: "lchat-sandbox-init", source, manifest, seed, me: { uid: "u1", name: "Ana" }, theme: theme || {} },
86 ports: [chan.port2],
87 });
88 }
89 const next = (pred) =>
90 new Promise((resolve) => {
91 const check = () => {
92 const i = inbox.findIndex(pred);
93 if (i >= 0) return resolve(inbox.splice(i, 1)[0]);
94 waiters.push(check);
95 };
96 check();
97 });
98 return {
99 post: (m) => chan.port1.postMessage(m),
100 next,
101 rootCalls: rootStyle.calls,
102 rootStyle: rootStyle.style,
103 body,
104 close: () => {
105 chan.port1.close();
106 chan.port2.close();
107 },
108 };
109}
110
111const HELLO_APP = `
112import { defineApp } from "../apps.js";
113export const app = defineApp({
114 name: "hello", title: "Hello", glyph: "H",
115 initialState: () => ({ count: 0 }),
116 events: { "hello.tap": (s) => ({ count: s.count + 1 }) },
117 render(state, el) { el.textContent = "count " + state.count; },
118});
119`;
120const MANIFEST = { v: 1, name: "hello", title: "Hello", glyph: "H", types: ["hello.tap"], chat: false, commands: [] };
121
122test("init seeds the mount-time theme onto the frame's root", async () => {
123 const d = await connect({
124 source: HELLO_APP,
125 manifest: MANIFEST,
126 seed: { v: null, vUpTo: 0, pend: [], chat: [] },
127 theme: { vars: { "--panel": "#111" }, font: "13px sans", dark: true },
128 });
129 try {
130 await d.next((m) => m.type === "state");
131 assert.deepEqual(d.rootCalls, [["--panel", "#111"]]);
132 assert.equal(d.rootStyle.colorScheme, "dark");
133 assert.equal(d.body.style.font, "13px sans");
134 } finally {
135 d.close();
136 }
137});
138
139test("a live 'theme' message re-tints an already-mounted frame, not just init", async () => {
140 const d = await connect({
141 source: HELLO_APP,
142 manifest: MANIFEST,
143 seed: { v: null, vUpTo: 0, pend: [], chat: [] },
144 theme: { vars: { "--panel": "#111" }, font: "13px sans", dark: true },
145 });
146 try {
147 await d.next((m) => m.type === "state");
148 d.rootCalls.length = 0; // only care about what the LIVE push applies, not init
149
150 // The member switched to a light theme with different vars — this is what
151 // sandbox.js's pushTheme sends to every mounted frame's port (issue #402).
152 d.post({ type: "theme", vars: { "--panel": "#fff", "--ink": "#111" }, font: "15px sans", dark: false });
153 // Messages on one port are delivered in order — a no-op events batch posted
154 // right after fences on the theme message actually having been handled
155 // first, without a fixed/racy timeout.
156 d.post({ type: "events", batch: [] });
157 await d.next((m) => m.type === "state");
158
159 assert.deepEqual(
160 d.rootCalls.sort(),
161 [
162 ["--ink", "#111"],
163 ["--panel", "#fff"],
164 ].sort(),
165 );
166 assert.equal(d.rootStyle.colorScheme, "light");
167 assert.equal(d.body.style.font, "15px sans");
168 } finally {
169 d.close();
170 }
171});
172
173test("a live 'theme' message with no vars still flips colorScheme and font", async () => {
174 const d = await connect({
175 source: HELLO_APP,
176 manifest: MANIFEST,
177 seed: { v: null, vUpTo: 0, pend: [], chat: [] },
178 theme: { vars: { "--panel": "#111" }, font: "13px sans", dark: true },
179 });
180 try {
181 await d.next((m) => m.type === "state");
182 d.post({ type: "theme", dark: false });
183 d.post({ type: "events", batch: [] });
184 await d.next((m) => m.type === "state");
185 assert.equal(d.rootStyle.colorScheme, "light");
186 } finally {
187 d.close();
188 }
189});
190