// Compares Electron capturePage output against render.html's own canvas pixels // for deterministic fixture scenes. This guards the capture step itself: // wrong scale-factor normalization, stale frames, and clipping show up here. const { app, BrowserWindow, nativeImage } = require('electron') const fs = require('node:fs') const path = require('node:path') const { pathToFileURL } = require('node:url') const { diffBgra } = require('../test/lib/pixelDiff.cjs') const fixtures = [ { file: path.join(process.cwd(), 'test', 'fixtures', 'scenes', 'capture-basic.json'), times: [0, 0.5, 1], }, ] function createRenderWindow(scene) { return new BrowserWindow({ width: scene.canvas.width, height: scene.canvas.height, show: false, frame: false, transparent: false, backgroundColor: '#ffffff', webPreferences: { contextIsolation: true, nodeIntegration: false, sandbox: true, backgroundThrottling: false, partition: 'ruwio-capture-fidelity', }, }) } async function main() { let pass = true const notes = [] const renderHtml = path.join(process.cwd(), 'server', 'render.html') for (const fixture of fixtures) { const scene = JSON.parse(fs.readFileSync(fixture.file, 'utf8')) const win = createRenderWindow(scene) win.webContents.session.webRequest.onBeforeRequest( { urls: ['*://*/*'] }, (_details, callback) => callback({ cancel: true }), ) win.webContents.setWindowOpenHandler(() => ({ action: 'deny' })) win.webContents.on('will-navigate', (event) => event.preventDefault()) try { await win.loadURL(pathToFileURL(renderHtml).toString()) await win.webContents.executeJavaScript(`window.__animatrInit(${JSON.stringify(scene)})`, true) for (const t of fixture.times) { await win.webContents.executeJavaScript(`window.__animatrSeek(${t})`, true) const capture = await win.webContents.capturePage({ x: 0, y: 0, width: scene.canvas.width, height: scene.canvas.height, }) const canvasDataUrl = await win.webContents.executeJavaScript( `document.querySelector('canvas').toDataURL('image/png')`, true, ) const expected = nativeImage.createFromDataURL(canvasDataUrl) const captureSize = capture.getSize() const expectedSize = expected.getSize() if (captureSize.width !== scene.canvas.width || captureSize.height !== scene.canvas.height) { pass = false notes.push(`${path.basename(fixture.file)} t=${t}: capture size ${captureSize.width}x${captureSize.height}`) continue } if (expectedSize.width !== scene.canvas.width || expectedSize.height !== scene.canvas.height) { pass = false notes.push(`${path.basename(fixture.file)} t=${t}: canvas size ${expectedSize.width}x${expectedSize.height}`) continue } const diff = diffBgra(capture.getBitmap(), expected.getBitmap(), { // capturePage composites the hidden window, which introduces // sub-perceptual AA/rounding noise (~maxDelta 6/255 on shape edges) // versus the raw canvas toDataURL. Tolerate that; genuine render // drift (wrong color, missing or stale shape, off-by-one frame) // produces much larger deltas over a contiguous region and still // trips the ratio gate. The explicit size checks above catch #10/#11. perChannelTolerance: 8, maxMismatchedRatio: 0.02, }) console.log( `${path.basename(fixture.file)} t=${t}: mismatch=${diff.mismatchedPixels}/${diff.pixels} ` + `(${(diff.mismatchedRatio * 100).toFixed(4)}%) maxDelta=${diff.maxDelta} meanDelta=${diff.meanDelta.toFixed(3)}`, ) if (!diff.ok) { pass = false notes.push( `${path.basename(fixture.file)} t=${t}: diff exceeds tolerance ` + `${diff.mismatchedPixels}/${diff.pixels}, maxDelta=${diff.maxDelta}`, ) } } } finally { if (!win.isDestroyed()) win.close() } } console.log(notes.length ? 'NOTES:\n ' + notes.join('\n ') : 'NOTES: none') console.log(pass ? 'RESULT: PASS' : 'RESULT: FAIL') app.exit(pass ? 0 : 1) } app.on('window-all-closed', () => {}) app.commandLine.appendSwitch('disable-gpu') app.whenReady().then(() => main().catch((err) => { console.error('HARNESS ERROR:', err) app.exit(2) }), )