// The observer's first-paint report (src/observer/paint.ts): the startup cover leaves when X shows // real content. A hidden document gets no animation frames (WebKit holds a hidden window's frames // back, and the app's window is hidden until the shell has laid out), so the check waits for the // document to be visible, is asked again the moment it is, and its give-up time counts only // visible time. Seen on macOS: the report never came and the panes stayed covered. import assert from 'node:assert/strict'; import test from 'node:test'; import { watchFirstPaint, type PaintClock, type PaintDocument } from '../src/observer/paint.ts'; function world(visible: boolean) { const frames: Array<() => void> = []; const timers = new Map void }>(); let now = 0; let next = 1; const listeners = new Set<() => void>(); const doc = { visibilityState: visible ? 'visible' : 'hidden', addEventListener: (_: 'visibilitychange', listener: () => void) => { listeners.add(listener); }, removeEventListener: (_: 'visibilitychange', listener: () => void) => { listeners.delete(listener); }, } satisfies PaintDocument & { visibilityState: string }; const clock: PaintClock = { // Like an engine: no frames for a hidden document. requestAnimationFrame: callback => { frames.push(callback); return frames.length; }, setTimeout: (callback, ms) => { const id = next++; timers.set(id, { at: now + ms, callback }); return id; }, clearTimeout: handle => { timers.delete(handle as number); }, }; let changed: () => void = () => {}; let observing = false; return { doc, clock, changed: (listener: () => void) => { changed = listener; observing = true; return () => { observing = false; }; }, get observing() { return observing; }, mutate: () => changed(), frame: () => { if (doc.visibilityState !== 'visible') return; const pending = frames.splice(0); for (const callback of pending) callback(); }, advance: (ms: number) => { now += ms; for (const [id, timer] of [...timers]) if (timer.at <= now) { timers.delete(id); timer.callback(); } }, show: () => { doc.visibilityState = 'visible'; for (const listener of [...listeners]) listener(); }, hide: () => { doc.visibilityState = 'hidden'; for (const listener of [...listeners]) listener(); }, listeners, }; } test('a visible page reports once, two frames after it shows content', () => { const w = world(true); let painted = false; let reports = 0; watchFirstPaint({ doc: w.doc, clock: w.clock, changed: w.changed, painted: () => painted, report: () => { reports += 1; } }); w.frame(); assert.equal(reports, 0, 'nothing yet'); painted = true; w.mutate(); w.frame(); assert.equal(reports, 0, 'the report waits two more frames'); w.frame(); w.frame(); assert.equal(reports, 1); w.mutate(); w.frame(); w.frame(); w.frame(); assert.equal(reports, 1, 'once'); assert.equal(w.observing, false, 'the DOM is no longer watched'); }); test('a page that painted while its window was hidden reports as soon as it is shown', () => { const w = world(false); let reports = 0; watchFirstPaint({ doc: w.doc, clock: w.clock, changed: w.changed, painted: () => true, report: () => { reports += 1; } }); // X renders while the window is hidden: changes come, frames do not. w.mutate(); w.frame(); w.advance(60_000); assert.equal(reports, 0); assert.equal(w.observing, true, 'hidden time does not count toward giving up'); w.show(); w.frame(); w.frame(); w.frame(); assert.equal(reports, 1, 'reported once visible, even with no further DOM change'); }); test('the give-up time counts only visible time', () => { const w = world(true); let reports = 0; watchFirstPaint({ doc: w.doc, clock: w.clock, changed: w.changed, painted: () => false, report: () => { reports += 1; }, limitMs: 10_000 }); w.advance(9_999); assert.equal(w.observing, true); w.advance(1); assert.equal(w.observing, false, 'gives up after the limit'); assert.equal(w.listeners.size, 0); assert.equal(reports, 0); }); test('stop ends everything at once', () => { const w = world(false); let reports = 0; const stop = watchFirstPaint({ doc: w.doc, clock: w.clock, changed: w.changed, painted: () => true, report: () => { reports += 1; } }); stop(); w.show(); w.frame(); w.frame(); w.frame(); assert.equal(reports, 0); assert.equal(w.observing, false); });