#!/usr/bin/env node /* * theme.test.mjs — PX-02: the Telegram theme signal → data-tg-theme slot switch. * ----------------------------------------------------------------------------- * applyTheme/watchTheme touch `document.documentElement.dataset`, and the Node * runner has no DOM — so every case installs a minimal document stub and * restores globalThis.document in finally (a leaked stub would silently make a * later pass/fail for the wrong reason). The stub's documentElement.dataset is * a plain writable object — that IS the assertion target — and its style has a * no-op setProperty because safe-area.ts's applySafeArea may be imported * transitively. * * Pitfall 7's invariant is greppable: case 3 reads the shipped theme.ts source * and asserts no setStyle call exists (source-introspection, the * commands.test.mjs CS4 precedent). */ import { strict as assert } from "node:assert"; import { readFileSync } from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { test } from "./harness.mjs"; const themeMod = await import("../src/telegram/theme.ts"); const { resolveBranch, applyTheme, watchTheme } = themeMod; const realDocument = globalThis.document; function installDocumentStub() { const dataset = {}; globalThis.document = { documentElement: { dataset, style: { setProperty() {} } }, }; return dataset; } function fakeWebApp(opts = {}) { return { initData: "", colorScheme: opts.colorScheme, themeParams: opts.themeParams ?? {}, isVersionAtLeast() { return true; }, close() {}, }; } /* ————— PX02 — colorScheme paths ————— */ test("PX02 colorScheme 'dark' sets data-tg-theme=dark; 'light' sets light", () => { const ds = installDocumentStub(); try { applyTheme(fakeWebApp({ colorScheme: "dark" })); assert.strictEqual(ds.tgTheme, "dark"); applyTheme(fakeWebApp({ colorScheme: "light" })); assert.strictEqual(ds.tgTheme, "light"); } finally { globalThis.document = realDocument; } }); test("PX02 a missing colorScheme falls back to header_bg_color luminance >= 0.5 (light) / < 0.5 (dark)", () => { const ds = installDocumentStub(); try { // Luminance of the Rams dark header (#121212 ≈ 0.07) vs a light one (#f0ede8). const darkShell = fakeWebApp({ themeParams: { header_bg_color: "#121212" } }); assert.strictEqual(resolveBranch(darkShell), "dark"); const lightShell = fakeWebApp({ themeParams: { header_bg_color: "#f0ede8" } }); assert.strictEqual(lightShell && resolveBranch(lightShell), "light"); applyTheme(darkShell); assert.strictEqual(ds.tgTheme, "dark"); applyTheme(lightShell); assert.strictEqual(ds.tgTheme, "light"); // No usable signal at all → light default. assert.strictEqual(resolveBranch(fakeWebApp()), "light"); } finally { globalThis.document = realDocument; } }); test("PX02 applyTheme touches only the data-tg-theme attribute — the map style object is not re-set", () => { // Source-introspection form of the invariant (CS4 precedent): if the shipped // module ever acquires a setStyle call, this case fails the suite. const themeSrc = readFileSync( path.join(path.dirname(fileURLToPath(new URL(import.meta.url))), "..", "src", "telegram", "theme.ts"), "utf8", ); const code = themeSrc .replace(/\/\*[\s\S]*?\*\//g, " ") .replace(/(^|[^:\\])\/\/[^\n]*/g, "$1"); assert.ok(!code.includes("setStyle"), "theme.ts must never call setStyle (D-10 / Pitfall 7)"); assert.ok(!/import.*maplibre/i.test(code.split("\n").filter((l) => !l.trim().startsWith("*")).join("\n")), "theme.ts must not import MapLibre — the map is out of its reach by construction"); const ds = installDocumentStub(); try { applyTheme(fakeWebApp({ colorScheme: "dark" })); const touched = Object.keys(ds); assert.deepStrictEqual(touched, ["tgTheme"], "the attribute set is the ONLY observable mutation"); } finally { globalThis.document = realDocument; } }); test("PX02 watchTheme re-applies on the themeChanged event", () => { const ds = installDocumentStub(); try { const listeners = []; const wa = fakeWebApp({ colorScheme: "light" }); wa.onEvent = (event, cb) => { if (event === "themeChanged") listeners.push(cb); }; watchTheme(wa); assert.strictEqual(ds.tgTheme, "light"); assert.strictEqual(listeners.length, 1, "exactly one themeChanged listener registered"); // The flip: colorScheme goes dark; the recomputation must land on the attribute. wa.colorScheme = "dark"; listeners[0](); assert.strictEqual(ds.tgTheme, "dark", "themeChanged re-applied the slot switch"); } finally { globalThis.document = realDocument; } });