/* The chain scene paints on push whatever the document's visibility says (2.0.3, 7 October 2026). * Found by the coordinator at 14:3x UK: /live mounted, push(d) true, state live, Pause reading Pause, and a canvas with no frame * drawn after 3 s, because the only paint path was a requestAnimationFrame loop gated on document.hidden and the * IntersectionObserver. A page that loads with document.hidden true (the desktop pane; a background tab) and whose embedder * never fires visibilitychange stayed blank for good. Rule: a mounted scene with a live feed that has no block drawn after one * push FAILS, with the document hidden, with the observer never intersecting, and with requestAnimationFrame never firing. * * Loads scene/live-dag.js in a stub window with a 2D context that counts its calls (a block is an arc or a rounded path that is * filled), pushes the recorded observer fixture, and reads the count. Known-failed first: the same renderer with its * paint-on-push line removed draws nothing (the class this test guards), and an empty feed draws no block. */ const fs = require('node:fs'), vm = require('node:vm'), assert = require('node:assert/strict'), path = require('node:path'); const root = path.join(__dirname, '..', '..'); const src = fs.readFileSync(path.join(root, 'scene', 'live-dag.js'), 'utf8'); const fixture = JSON.parse(fs.readFileSync(path.join(root, 'scene', 'fixtures', 'live-2026-10-07.json'), 'utf8')); const T0 = new Date(fixture.now).getTime(); function world(opts) { opts = opts || {}; const calls = { fill: 0, arc: 0, arcTo: 0, fillText: 0, raf: 0 }; const ctx = new Proxy({}, { get: (t, k) => { if (k === 'canvas') return canvas; return (...a) => { if (k === 'fill') calls.fill++; if (k === 'arc') calls.arc++; if (k === 'arcTo') calls.arcTo++; if (k === 'fillText') calls.fillText++; if (k === 'createRadialGradient' || k === 'createLinearGradient') return { addColorStop() { } }; if (k === 'measureText') return { width: 10 }; if (k === 'getImageData') return { data: new Uint8ClampedArray(4) }; }; }, set: () => true, }); const canvas = { clientWidth: 900, clientHeight: 420, width: 0, height: 0, style: {}, attrs: {}, getContext: () => ctx, getAttribute(n) { return this.attrs[n] === undefined ? null : this.attrs[n]; }, setAttribute(n, v) { this.attrs[n] = String(v); }, removeAttribute(n) { delete this.attrs[n]; }, addEventListener() { }, removeEventListener() { }, classList: { add() { }, remove() { }, toggle() { } }, getBoundingClientRect: () => ({ left: 0, top: 0, width: 900, height: 420 }), focus() { } }; const doc = { get hidden() { return !!opts.hidden; }, documentElement: { getAttribute: () => null, style: {} }, baseURI: 'https://igneum.network/live', addEventListener() { }, removeEventListener() { }, createElement: () => ({ style: {}, getContext: () => ctx }) }; canvas.ownerDocument = doc; const window = { document: doc, devicePixelRatio: 1, innerWidth: 1440, requestAnimationFrame: () => { calls.raf++; return opts.rafFires ? 1 : ++calls.raf; }, cancelAnimationFrame() { }, performance: { now: () => 1000 }, Date, matchMedia: q => ({ matches: false, addEventListener() { }, removeEventListener() { } }), getComputedStyle: () => ({ getPropertyValue: () => '' }), ResizeObserver: class { constructor(f) { this.f = f; } observe() { } disconnect() { } }, IntersectionObserver: class { constructor(f) { this.f = f; } observe(el) { if (opts.io === 'never') this.f([{ isIntersecting: false, target: el }]); } disconnect() { } }, MutationObserver: class { observe() { } disconnect() { } }, AbortController: class { abort() { } }, URL, setTimeout: () => 0, clearTimeout() { }, setInterval: () => 0, clearInterval() { }, addEventListener() { }, removeEventListener() { }, console, }; window.window = window; return { window, canvas, calls }; } function run(source, opts, reply) { const w = world(opts); const origNow = Date.now; Date.now = () => T0 + 1500; try { vm.runInNewContext(source + '\n;window.IgneumDag', w.window, { filename: 'live-dag.js' }); const api = w.window.IgneumDag.mount(w.canvas, { poll: false, window: 60, fps: 60 }); const ok = api.push(reply); return { ok, calls: w.calls, stats: api.getStats(), state: api.getState().state }; } finally { Date.now = origNow; } } // a block is two rounded tiles (the outline and the core), four arcTo calls each const blocksDrawn = r => Math.floor(r.calls.arcTo / 8); const results = []; function check(name, fn) { try { fn(); results.push({ name, pass: true }); } catch (e) { results.push({ name, pass: false, error: e.message }); } } // known-failed first: the renderer without its paint-on-push line (the 2.0.2 behaviour) draws nothing with the document hidden check('known-failed: 2.0.2 behaviour (no paint on push) draws no block with the document hidden', () => { assert(src.includes('/*paint-on-push*/paint();'), 'the paint-on-push marker is gone from scene/live-dag.js'); const old = src.replace('/*paint-on-push*/paint();', ''); const r = run(old, { hidden: true }, fixture); assert.equal(r.ok, true); assert.equal(r.state, 'live'); assert(r.stats.blocks > 0, 'the fixture has blocks in the window'); assert.equal(blocksDrawn(r), 0, 'the old renderer must draw nothing here, or this test guards nothing'); }); check('known-failed: an empty feed draws no block', () => { const r = run(src, { hidden: true }, { ok: true, state: { stale: false, age_s: 0 }, blocks: [] }); assert.equal(blocksDrawn(r), 0); }); // the rule check('document hidden, requestAnimationFrame never fires: blocks are drawn on the first push', () => { const r = run(src, { hidden: true }, fixture); assert.equal(r.ok, true); assert.equal(r.state, 'live'); assert(r.stats.blocks > 50, 'blocks in the 60 s window: ' + r.stats.blocks); assert(blocksDrawn(r) >= r.stats.blocks, 'tiles drawn ' + blocksDrawn(r) + ' for ' + r.stats.blocks + ' blocks in the window'); assert(r.calls.fillText > 0, 'lane labels and the time axis are written'); }); check('the observer never reports the canvas intersecting: blocks are still drawn on push', () => { const r = run(src, { hidden: false, io: 'never' }, fixture); assert(blocksDrawn(r) >= r.stats.blocks); }); check('a visible page still schedules the motion loop after the synchronous paint', () => { const r = run(src, { hidden: false }, fixture); assert(r.calls.raf > 0, 'requestAnimationFrame was asked for'); }); check('a hidden page does not spin the motion loop', () => { const r = run(src, { hidden: true }, fixture); assert.equal(r.calls.raf, 0); }); for (const r of results) console.log((r.pass ? ' ok ' : ' FAIL ') + r.name + (r.error ? ': ' + r.error : '')); const failed = results.filter(r => !r.pass).length; console.log(failed ? 'paint-test: RED, ' + failed + ' failed' : 'paint-test: ok, ' + results.length + ' checks (two known-failed cases fail as they must)'); process.exit(failed ? 1 : 0);