// Development-only side-by-side evidence. This deliberately asserts no pixel parity. import fs from 'node:fs'; import path from 'node:path'; import { createRequire } from 'node:module'; import { createHash } from 'node:crypto'; const [modulePath, nativeDirectory, pwaDirectory, destination] = process.argv.slice(2); if (!destination) throw new Error('Usage: node compare-native-pwa.mjs PLAYWRIGHT_MODULE NATIVE_DIR PWA_DIR OUTPUT_DIR'); const { chromium } = createRequire(import.meta.url)(path.resolve(modulePath)); const viewport = JSON.parse(fs.readFileSync(path.join(nativeDirectory, 'viewport.json'))); if (viewport.fontScale !== 1) throw new Error('This CSS reference comparison requires fontScale 1'); const cssWidth = viewport.widthPx / viewport.density; const cssHeight = viewport.heightPx / viewport.density; const reference = JSON.parse(fs.readFileSync(path.join(pwaDirectory, 'measurements.json'))); if (!reference.screens.every(screen => screen.viewport.width === cssWidth && screen.viewport.height === cssHeight)) { throw new Error('PWA and native content viewports do not match'); } function image(directory, name) { const bytes = fs.readFileSync(path.join(directory, name)); if (bytes.subarray(0, 8).toString('hex') !== '89504e470d0a1a0a' || bytes.readUInt32BE(16) !== viewport.widthPx || bytes.readUInt32BE(20) !== viewport.heightPx) { throw new Error(`Invalid or mismatched PNG: ${name}`); } return { uri: `data:image/png;base64,${bytes.toString('base64')}`, sha256: createHash('sha256').update(bytes).digest('hex') }; } fs.mkdirSync(destination, { recursive: true }); const browser = await chromium.launch({ headless: true, args: process.getuid?.() === 0 ? ['--no-sandbox'] : [] }); const records = []; try { const page = await browser.newPage({ viewport: { width: viewport.widthPx * 2, height: viewport.heightPx + 48 }, deviceScaleFactor: 1 }); await page.route('**/*', route => route.abort()); for (const name of ['login', 'dashboard', 'tasks', 'notes', 'shopping']) { for (const theme of ['light', 'dark']) { const native = image(nativeDirectory, `${name}-${theme}.png`); const pwa = image(pwaDirectory, `${name}-${theme}-${cssWidth}.png`); await page.setContent(`
PWA 2.78.0 · ${name} · ${theme}
Android nativ · ${name} · ${theme} · keine 1:1-Abnahme
`); await page.locator('img').evaluateAll(images => Promise.all(images.map(image => image.decode()))); const file = `${name}-${theme}-comparison.png`; await page.screenshot({ path: path.join(destination, file) }); records.push({ file, nativeSha256: native.sha256, pwaSha256: pwa.sha256 }); } } } finally { await browser.close(); } fs.writeFileSync(path.join(destination, 'comparison.json'), JSON.stringify({ status: 'Side-by-side evidence only; no visual acceptance', viewport, upstreamRevision: reference.upstreamRevision, records, }, null, 2) + '\n'); console.log(`Wrote ${records.length} matched-viewport comparisons; no parity assertion.`);