// fiesta_node runs fiesta.js against a stub page in plain Node, one // fresh context per scenario, and reports what the scheme switch did: // what a submit in the [rst-scheme] menu applied, stored and // dispatched, with and without , // with a storage that throws, and what a stored choice does on load. // One scenario evaluates the script twice and counts listeners. // // check/scripts_test.go is the only caller. It reads back one JSON // object, a scenario per key. // // The page is the least DOM fiesta.js touches on the scheme path: a // root with attributes, a document with listeners, one scheme menu of // three forms, and a storage. Everything else the file reaches for on // load (getAnimations, matchMedia) is absent and guarded. import { readFileSync } from "node:fs"; import vm from "node:vm"; const src = readFileSync(new URL("../../js/fiesta.js", import.meta.url), "utf8"); // An element with attributes and nothing else. function element() { const attrs = new Map(); return { getAttribute: (k) => (attrs.has(k) ? attrs.get(k) : null), setAttribute: (k, v) => { attrs.set(k, String(v)); }, removeAttribute: (k) => { attrs.delete(k); }, hasAttribute: (k) => attrs.has(k), }; } // A storage over a Map that counts its calls, or one whose every // method throws, as a browser's does with storage switched off. function storage(broken) { const m = new Map(); const n = { calls: 0 }; const call = (fn) => (...args) => { n.calls++; if (broken) throw new Error("storage is off"); return fn(...args); }; return { n, m, getItem: call((k) => (m.has(k) ? m.get(k) : null)), setItem: call((k, v) => { m.set(k, String(v)); }), removeItem: call((k) => { m.delete(k); }), }; } // page builds the stub, evaluates fiesta.js in it, and hands back the // hooks a scenario drives. store sets the ownership attribute; stored // seeds the storage; broken makes it throw; twice evaluates the script // a second time in the same context. function page({ store = false, stored = null, broken = false, twice = false } = {}) { const root = element(); if (store) root.setAttribute("data-rst-scheme-store", "local"); const local = storage(broken); if (stored !== null) local.m.set("fiesta-scheme", stored); const on = {}; let listeners = 0; const events = []; const dispatchEvent = (e) => { events.push(e); return true; }; const forms = ["light", "dark", "system"].map((v) => { const form = { elements: { scheme: { value: v } }, dispatchEvent }; const button = Object.assign(element(), { form }); if (v === "system") button.setAttribute("aria-current", "true"); return { form, button }; }); const menu = { open: true, dispatchEvent, querySelectorAll: (sel) => (sel === "button" ? forms.map((f) => f.button) : []), }; for (const f of forms) f.form.closest = (sel) => (sel.includes("rst-scheme") ? menu : null); let cookies = 0; const document = { readyState: "loading", documentElement: root, set cookie(v) { cookies++; }, get cookie() { return ""; }, querySelectorAll: (sel) => (sel.includes("rst-scheme") ? [menu] : []), addEventListener: (type, fn) => { listeners++; (on[type] = on[type] || []).push(fn); }, dispatchEvent, }; const window = { localStorage: local, addEventListener: () => { listeners++; } }; const context = { window, document, localStorage: local, CustomEvent, setTimeout, clearTimeout, location: { protocol: "http:" }, }; vm.runInNewContext(src, context); const once = listeners; if (twice) vm.runInNewContext(src, context); return { once, twice: listeners, ready() { for (const fn of on.DOMContentLoaded || []) fn({}); }, submit(v) { const f = forms.find((x) => x.form.elements.scheme.value === v).form; const e = { type: "submit", target: f, prevented: false, preventDefault() { this.prevented = true; } }; let error = ""; try { for (const fn of on.submit || []) fn(e); } catch (err) { error = String(err); } return { prevented: e.prevented, error }; }, state(extra = {}) { const cur = forms.find((f) => f.button.getAttribute("aria-current") === "true"); const stored = local.m.get("fiesta-scheme"); return { theme: root.getAttribute("data-theme") || "", stored: stored === undefined ? "" : stored, keys: local.m.size, storageCalls: local.n.calls, cookies, events: events.filter((e) => e.type === "rst-scheme").map((e) => (e.detail && e.detail.scheme) || ""), current: cur ? cur.form.elements.scheme.value : "", open: menu.open, prevented: false, error: "", ran: true, ...extra, }; }, }; } // run opens a page, submits one scheme, and reports. function run(opts, scheme) { const p = page(opts); p.ready(); const { prevented, error } = p.submit(scheme); return p.state({ prevented, error }); } const out = {}; { const p = page({ twice: true }); out.idempotent = { once: p.once, twice: p.twice }; } out.standalone = run({ store: true }, "dark"); out.system = run({ store: true, stored: "dark" }, "system"); out.broken = run({ store: true, broken: true }, "dark"); out.server = run({ store: false }, "dark"); { const p = page({ store: true, stored: "dark" }); const before = p.state(); p.ready(); out.storedOnLoad = { ...p.state(), theme: before.theme }; } { const p = page({ store: false, stored: "dark" }); p.ready(); out.serverIgnoresStored = p.state(); } process.stdout.write(JSON.stringify(out));