openPrintFrame's autoprint prints on the frame's 'load', which waits for every image, but the frame was removed 60 s after it was CREATED, loaded or not. The Wall's poster loads 939 photos (132 MB), so on a connection below about 18 Mbps the frame was gone before 'load': no print dialog, and nothing to say so. Now the frame is reclaimed 60 s after its load, at afterprint (as before), or at a 10-minute ceiling for a load that never ends. printImageBlob revoked its object URL on the same creation clock, which blanks an image that has not loaded; it now follows the frame's clock too. New gate check:print-frame-reclaim (gates:pre), fake timers and a fake frame, so 61 s and 10 minutes take no time: a load at 61 s still finds the frame (and the object URL), 60 s after load it is reclaimed, a frame that never loads goes at the ceiling and not before, afterprint reclaims at once. Two-sided: the copy before this fix fails 6 of its 12 cases (the load-after-61-s and ceiling arms); the controls pass on both.
106 lines
5.9 KiB
JavaScript
106 lines
5.9 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* check-print-frame-reclaim — a print frame is kept until it has printed, and reclaimed after. Headless,
|
|
* with fake timers and a fake frame, so "61 s" and "10 minutes" take no time at all.
|
|
*
|
|
* THE DEFECT THIS GUARDS (2026-09-29). openPrintFrame's autoprint prints on the frame's 'load', which
|
|
* waits for every image. It also removed the frame 60 s after CREATION, loaded or not. The Wall's
|
|
* poster loads 939 photos (132 MB), so on a connection below about 18 Mbps the frame was gone before
|
|
* 'load': no print dialog, and nothing to say so. printImageBlob revoked its object URL on the same
|
|
* clock, which blanks an image that has not loaded yet.
|
|
*
|
|
* ASSERTS, for openPrintFrame's frame AND printImageBlob's object URL:
|
|
* 1. a frame whose 'load' arrives at 61 s is still there (so it prints), and is reclaimed 60 s later;
|
|
* 2. a frame that never loads is reclaimed at the ceiling (10 minutes), and not before;
|
|
* 3. afterprint reclaims at once.
|
|
* `--impl=<path>` runs the same assertions against another copy of printInPlace.js; the copy before
|
|
* this fix must FAIL arms 1 and 2 (the two-sided proof).
|
|
* EXIT 0 pass · 1 fail
|
|
*/
|
|
import { resolve, dirname } from 'node:path'
|
|
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
|
|
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
|
const implArg = process.argv.find((a) => a.startsWith('--impl='))
|
|
const IMPL = implArg ? resolve(implArg.slice(7)) : resolve(ROOT, 'library/composables/printInPlace.js')
|
|
|
|
// ── fake timers: a clock that only moves when the test says so ────────────────────────────────────
|
|
let now = 0, seq = 0
|
|
const timers = new Map()
|
|
globalThis.setTimeout = (fn, ms = 0) => { const id = ++seq; timers.set(id, { at: now + Math.max(0, ms), fn }); return id }
|
|
globalThis.clearTimeout = (id) => { timers.delete(id) }
|
|
function advanceTo(t) {
|
|
for (;;) {
|
|
let next = null
|
|
for (const [id, x] of timers) if (x.at <= t && (!next || x.at < next[1].at || (x.at === next[1].at && id < next[0]))) next = [id, x]
|
|
if (!next) break
|
|
timers.delete(next[0]); now = next[1].at; next[1].fn()
|
|
}
|
|
now = t
|
|
}
|
|
function reset() { now = 0; timers.clear() }
|
|
|
|
// ── a fake frame: records what was written, and whether it is still in the page ───────────────────
|
|
let frame = null
|
|
const body = { appendChild: (f) => { f.parentNode = body } }
|
|
globalThis.document = {
|
|
body,
|
|
createElement: () => {
|
|
const listeners = {}
|
|
frame = {
|
|
parentNode: null, style: {}, html: '',
|
|
setAttribute() {}, remove() { this.parentNode = null },
|
|
contentDocument: { open() {}, write(s) { frame.html += s }, close() {} },
|
|
contentWindow: { addEventListener: (type, fn) => { (listeners[type] = listeners[type] || []).push(fn) } },
|
|
fire(type) { for (const fn of listeners[type] || []) fn() },
|
|
}
|
|
return frame
|
|
},
|
|
}
|
|
const revoked = []
|
|
globalThis.URL = { createObjectURL: () => 'blob:fake', revokeObjectURL: (u) => revoked.push([u, now]) }
|
|
const attached = () => !!(frame && frame.parentNode)
|
|
|
|
const { openPrintFrame, printImageBlob } = await import(pathToFileURL(IMPL).href)
|
|
const fails = []
|
|
const check = (ok, what) => { console.log(` ${ok ? 'ok ' : 'FAIL'} ${what}`); if (!ok) fails.push(what) }
|
|
|
|
// ── openPrintFrame ─────────────────────────────────────────────────────────────────────────────────
|
|
reset(); openPrintFrame('<img src="x.jpg">', { autoprint: true })
|
|
check(frame.html.includes('window.print()'), 'autoprint writes its print-on-load bootstrap (control)')
|
|
advanceTo(61000)
|
|
check(attached(), "frame: a 'load' that arrives at 61 s finds the frame still there, so the poster prints")
|
|
frame.fire('load'); advanceTo(61000 + 59999)
|
|
check(attached(), "frame: kept for 60 s after its 'load' (the dialog is up)")
|
|
advanceTo(61000 + 60000)
|
|
check(!attached(), "frame: reclaimed 60 s after its 'load'")
|
|
|
|
reset(); openPrintFrame('<img src="x.jpg">', { autoprint: true })
|
|
advanceTo(599999)
|
|
check(attached(), 'frame: a load that never ends is not reclaimed before the ceiling')
|
|
advanceTo(600000)
|
|
check(!attached(), 'frame: a load that never ends is reclaimed at the 10-minute ceiling')
|
|
|
|
reset(); openPrintFrame('<img src="x.jpg">', { autoprint: true })
|
|
advanceTo(1000); frame.fire('load'); advanceTo(2000); frame.fire('afterprint'); advanceTo(2000)
|
|
check(!attached(), 'frame: afterprint reclaims it at once')
|
|
|
|
// ── printImageBlob's object URL ────────────────────────────────────────────────────────────────────
|
|
reset(); revoked.length = 0; printImageBlob({ size: 1 })
|
|
advanceTo(61000)
|
|
check(!revoked.length, "object URL: not revoked before a 'load' that arrives at 61 s (the preview would be blank)")
|
|
frame.fire('load'); advanceTo(61000 + 60000)
|
|
check(revoked.length === 1 && revoked[0][1] === 121000, "object URL: revoked 60 s after the frame's 'load'")
|
|
|
|
reset(); revoked.length = 0; printImageBlob({ size: 1 })
|
|
advanceTo(599999)
|
|
check(!revoked.length, 'object URL: a load that never ends keeps it until the ceiling')
|
|
advanceTo(600000)
|
|
check(revoked.length === 1, 'object URL: revoked at the 10-minute ceiling')
|
|
|
|
reset(); revoked.length = 0; printImageBlob({ size: 1 })
|
|
advanceTo(500); frame.fire('load'); frame.fire('afterprint'); advanceTo(500)
|
|
check(revoked.length >= 1 && revoked[0][1] === 500, 'object URL: afterprint revokes it at once')
|
|
|
|
if (fails.length) { console.log(`PRINT-FRAME-RECLAIM: FAIL — ${fails.length} case(s) [${IMPL.replace(ROOT + '/', '')}]`); process.exit(1) }
|
|
console.log('PRINT-FRAME-RECLAIM: PASS — a print frame waits for its load however long it takes, is reclaimed 60 s after it, at afterprint, or at the 10-minute ceiling; its object URL follows the same clock.')
|