forked from c-fu/arah2
feat(03-02): D-11 Mock-Telegram harness + the four requirement gates (17 cases green)
- 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
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
#!/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;
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user