forked from c-fu/arah2
- telegram-mock.ts: fake window.Telegram.WebApp injected into the SAME window
(never an iframe — Bot API 10.2 origin restriction, Pitfall 6); Web-Crypto
dev signer mirroring bot/src/security/initdata.ts's two-step HMAC (async
makeDevInitData — node:crypto stays out of the browser bundle path);
controllable LocationManager stream, themeChanged emitter, BackButton clicks
- location-chain.test.mjs (10 cases): GEO-01 version gate / null-WebApp guard
/ null-denial / hard timeout / stale-detector + one-read nudge; GEO-02 pin
pickup / bounded 60s poll / Desktop no-geolocation leg ' same denial keys
- no-storage.test.mjs (3 cases): GEO-03 comment-stripped grep gate over
web/src/**/*.{ts,html}, no-SW invariant, store.ts storage-assignment probe
- theme.test.mjs (4 cases): PX-02 colorScheme + luminance fallback + setStyle
source-introspection (Pitfall 7) + themeChanged re-apply; document stub
installed and restored per case
- Rule 1 fix in telegram.ts: looksStale/nudgeAndRetryTelegram accept an
omitted lastAccepted (undefined normalized to null) ' acquireFix(deps)
first-call contract
124 lines
4.7 KiB
JavaScript
124 lines
4.7 KiB
JavaScript
#!/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;
|
|
}
|
|
});
|