igneum/tools/site-redesign/tests/hero-loop-test.cjs
igneum-labs 559494e8ac site-redesign: the full-site redesign on our content, 22 pages served
The package's design system, layouts and interactions on our tokens and self-hosted fonts (site/site.css section 10),
our facts, numbers, records and live services on every page. The home page: the GPU hero (site/hero-scene.js, continuous
while in view, labelled as drawn), the live DAG panel on scenes/feed.js and live-dag.js 2.0.2, three reasons, the
economics split, the download row with the OS marks, the ledger line. The miner page: the download layout, platform
tabs stamped from the live download index with sha256, the steps, the card table, the fee section (the one source of
/dev-fee), the FAQ. The wallet page in the package's wallet-hero layout, the window slot ready for the wallet redesign.
The secondary pages rendered by the build from one source each: /claims and /randomx from the litepaper's sections,
/dev-fee from the miner's fee section, /provenance from docs/provenance.md through the scrub, /journey from
journey.json; /benchmarks rewrites to /miners, /connect redirects to /metamask. The generated pages (bench, bench
table, ledger) restyled onto the page hero and the docs layout with a section filter. The footer's secondary links point
at the real pages. The copy pass on every served page (docs/plans/site-copy-pass.md). The package's data tests and the
hero loop test in the gate (tools/site-redesign). No sample data is served; the package's review chrome is not.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-07 10:30:18 +00:00

55 lines
5.6 KiB
JavaScript

/* The home hero's loop never idles while in view (the owner's call, 7 October 2026: "can we make the animations here not
* pause like they do now? looks like a bug"). Loads site/hero-scene.js in a stub window with a fake requestAnimationFrame
* and a 2D context that accepts every call, then checks: a frame is scheduled after every frame for as long as the hero
* is in view; a hidden tab stops the loop and a visible one restarts it; the scene clock does not jump across the pause;
* the stage advances on its own and on a button; reduced motion stops it. Known-finished and known-failed cases both run. */
const fs = require('node:fs'), vm = require('node:vm'), assert = require('node:assert/strict'), path = require('node:path');
const src = fs.readFileSync(path.join(__dirname, '..', '..', '..', 'site', 'hero-scene.js'), 'utf8');
function world(opts) {
opts = opts || {};
const listeners = {}, docListeners = {};
let rafQueue = [], rafId = 0, hidden = false, reduced = !!opts.reduced;
const ctx = new Proxy({}, { get: (t, k) => k === 'canvas' ? canvas : (() => (k === 'createRadialGradient' || k === 'createLinearGradient' ? { addColorStop() {} } : undefined)), set: () => true });
const canvas = { clientWidth: 600, clientHeight: 500, width: 0, height: 0, getContext: () => ctx, toDataURL: () => '' };
const el = () => ({ textContent: '', setAttribute() {}, getAttribute: () => '0', addEventListener() {} });
const document = {
get hidden() { return hidden; }, documentElement: { getAttribute: () => null, style: {} },
createElement: () => ({ width: 0, height: 0, getContext: () => ctx }), addEventListener(n, f) { (docListeners[n] = docListeners[n] || []).push(f); }, removeEventListener() {},
};
const window = {
document, devicePixelRatio: 1, requestAnimationFrame: cb => { rafQueue.push(cb); return ++rafId; }, cancelAnimationFrame: id => { rafQueue = rafQueue.filter(() => false); },
matchMedia: q => ({ matches: /reduced-motion/.test(q) ? reduced : false, addEventListener() {}, removeEventListener() {} }),
ResizeObserver: class { constructor(f) { this.f = f; } observe() { this.f(); } disconnect() {} },
IntersectionObserver: class { constructor(f) { window._io = f; } observe() {} disconnect() {} },
MutationObserver: class { observe() {} disconnect() {} },
addEventListener(n, f) { (listeners[n] = listeners[n] || []).push(f); },
};
const sandbox = { window, document, getComputedStyle: () => ({ getPropertyValue: () => '' }), performance: { now: () => 0 }, console };
for (const k of ['requestAnimationFrame', 'cancelAnimationFrame', 'matchMedia', 'ResizeObserver', 'IntersectionObserver', 'MutationObserver', 'devicePixelRatio']) sandbox[k] = window[k];
sandbox.requestAnimationFrame = window.requestAnimationFrame; sandbox.cancelAnimationFrame = window.cancelAnimationFrame;
vm.createContext(sandbox); vm.runInContext(src, sandbox);
const steps = [0, 1, 2].map(i => { const b = el(); b.getAttribute = () => String(i); return b; });
const status = el();
const scene = sandbox.window.IgneumHero.mount(canvas, { steps, status });
let t = 0;
function tick(ms) { t += ms; const q = rafQueue; rafQueue = []; q.forEach(cb => cb(t)); return q.length; }
function hide(v) { hidden = v; (docListeners.visibilitychange || []).forEach(f => f()); }
return { scene, tick, hide, status, pending: () => rafQueue.length, inView: v => window._io([{ isIntersecting: v }]), steps };
}
const results = [];
function check(name, fn) { try { fn(); results.push({ name, pass: true }); } catch (e) { results.push({ name, pass: false, error: e.message }); } }
check('a frame is scheduled after every frame for 600 frames in view (never idles)', () => { const w = world(); for (let i = 0; i < 600; i++) assert.equal(w.tick(16), 1, 'frame ' + i); assert.equal(w.pending(), 1); });
check('the stage advances on its own inside the first 10 s and keeps cycling', () => { const w = world(); const seen = new Set(); for (let i = 0; i < 1500; i++) { w.tick(16); seen.add(w.status.textContent); } seen.delete(''); assert.equal(seen.size, 3, [...seen].join('|')); });
check('a button advances the beat and the loop keeps running', () => { const w = world(); w.tick(16); w.scene.setStage(2); assert.equal(w.status.textContent, '03 / Locking'); assert.equal(w.tick(16), 1); assert.equal(w.pending(), 1); });
check('a hidden tab stops the loop, a visible one restarts it without a clock jump', () => {
const w = world(); for (let i = 0; i < 100; i++) w.tick(16);
w.hide(true); w.tick(16); assert.equal(w.pending(), 0, 'frames while hidden');
w.hide(false); assert.equal(w.pending(), 1, 'no restart on visible');
// the clock advances by frame delta: 50 ms capped, so a 10 s wall gap arrives as one short step and the stage does not skip
const before = w.status.textContent; w.tick(10000); w.tick(16); assert.equal(w.status.textContent, before, 'the stage jumped across the pause');
});
check('scrolled out of view stops the loop, back in view restarts it', () => { const w = world(); w.tick(16); w.inView(false); w.tick(16); assert.equal(w.pending(), 0); w.inView(true); assert.equal(w.pending(), 1); });
check('reduced motion never schedules a frame', () => { const w = world({ reduced: true }); assert.equal(w.pending(), 0); w.tick(16); assert.equal(w.pending(), 0); });
check('known failed case: a loop that idles is caught', () => { const w = world(); w.inView(false); assert.throws(() => { for (let i = 0; i < 5; i++) assert.equal(w.tick(16), 1); }); });
console.log(JSON.stringify({ passed: results.filter(r => r.pass).length, total: results.length, results }, null, 2));
if (results.some(r => !r.pass)) process.exitCode = 1;