import { chromium, webkit } from 'playwright'; import fs from 'node:fs/promises'; import assert from 'node:assert/strict'; const url = process.argv[2] ?? 'http://127.0.0.1:4173/'; const label = process.argv[3] ?? 'mascot-transparency'; const result = { url, checks: [], errors: [] }; const webkitRoot = process.env.WEBKIT_MINIBROWSER_ROOT; const points = [[.04,.04], [.5,.04], [.96,.5], [.05,.85], [.5,.97], [.75,.7]]; for (const engine of ['chromium', 'webkit']) { const browser = engine === 'chromium' ? await chromium.launch({ executablePath: '/usr/bin/chromium', headless: true }) : await webkit.launch({ headless: true, ...(webkitRoot ? { executablePath: webkitRoot + '/bin/MiniBrowser', env: { ...process.env, WEBKIT_EXEC_PATH: webkitRoot + '/bin', WEBKIT_INJECTED_BUNDLE_PATH: webkitRoot + '/lib', WEBKIT_INSPECTOR_RESOURCES_PATH: webkitRoot + '/share', LD_LIBRARY_PATH: webkitRoot + '/lib:' + webkitRoot + '/sys/lib:' + (process.env.WEBKIT_EXTRA_LIBS ?? ''), }, } : {}) }); try { for (const [width, height, scale] of [[1440,1000,1], [390,844,2]]) { const context = await browser.newContext({ viewport: { width, height }, deviceScaleFactor: scale }); const page = await context.newPage(); page.on('pageerror', e => result.errors.push(e.message)); const figure = page.locator('.mascot-cat'); const ready = async () => { await page.waitForFunction(() => window.browserPi?.terminal); await page.evaluate(() => window.browserPi.ready); await figure.waitFor({ state: 'visible' }); }; async function check(name) { const rect = await figure.boundingBox(); const actual = await page.screenshot(); // Compare the real backdrop at identical coordinates with the cat hidden. await figure.evaluate(el => { el.style.visibility = 'hidden'; }); const backdrop = await page.screenshot(); await figure.evaluate(el => { el.style.visibility = ''; }); const pixels = await page.evaluate(async ({ actual, backdrop, rect, scale, points }) => { async function read(data) { const img = new Image(); img.src = 'data:image/png;base64,' + data; await img.decode(); const canvas = document.createElement('canvas'); canvas.width = img.width; canvas.height = img.height; const ctx = canvas.getContext('2d'); ctx.drawImage(img,0,0); return points.map(([x,y]) => [...ctx.getImageData(Math.floor((rect.x+x*rect.width)*scale), Math.floor((rect.y+y*rect.height)*scale),1,1).data]); } const outside = { actual: await read(actual), backdrop: await read(backdrop) }; points = [[.45,.3], [.235,.18], [.32,.42]]; return { ...outside, cat: await read(actual) }; }, { actual: actual.toString('base64'), backdrop: backdrop.toString('base64'), rect, scale, points }); assert.deepEqual(pixels.actual,pixels.backdrop, `${engine} ${width} ${name}: opaque pixels outside silhouette`); const [fur, ear, eye] = pixels.cat; assert(fur[0] > 200 && fur[1] > 200 && fur[2] > 170, 'Cream fur must remain visible'); assert(ear[0] > 200 && ear[0] > ear[1] + 20, 'Pink inner ear must remain visible'); assert(eye.slice(0,3).every(v => v < 20), 'Black eye must be preserved, not color-keyed away'); await fs.writeFile(`../validation/${label}-${engine}-${width}-${name}.png`, actual); result.checks.push({ engine, width, height, scale, name, passed: true }); } await page.goto(url); await ready(); // Pause timers before the first automatic hop so screenshots cannot hide a first-paint defect. await page.clock.install(); await page.clock.pauseAt(new Date()); await check('cold-load'); assert.equal(await figure.evaluate(el => getComputedStyle(el).mixBlendMode), 'normal'); await figure.click(); const hopping = await figure.evaluate(el => { const a = el.getAnimations().find(a => a.id === 'mascot-bump'); if (!a) return false; a.pause(); a.currentTime = 170; return true; }); assert(hopping, 'Click must still animate the cat'); await check('mid-hop'); await figure.evaluate(el => el.getAnimations().forEach(a => a.finish())); await check('after-hop'); await page.emulateMedia({ reducedMotion: 'reduce' }); await page.clock.resume(); await page.reload(); await ready(); await check('reduced-motion-reload'); assert.equal(await figure.evaluate(el => el.getAnimations().length), 0); // A contrasting background proves actual clipping, not a dark-color cover-up. await page.addStyleTag({ content: '.mascot-cat { background: repeating-conic-gradient(#f57bc5 0 25%, #53d4ce 0 50%) 0 / 16px 16px !important; }' }); const rect = await figure.boundingBox(); const screenshot = await page.screenshot(); const colors = await page.evaluate(async ({ data, rect, scale, points }) => { const img=new Image();img.src='data:image/png;base64,'+data;await img.decode(); const canvas=document.createElement('canvas');canvas.width=img.width;canvas.height=img.height; const ctx=canvas.getContext('2d');ctx.drawImage(img,0,0); return points.map(([x,y])=>[...ctx.getImageData(Math.floor((rect.x+x*rect.width)*scale),Math.floor((rect.y+y*rect.height)*scale),1,1).data]); }, { data:screenshot.toString('base64'),rect,scale,points }); for (const pixel of colors) assert(pixel[0] > 50 && pixel[1] > 100 && pixel[2] > 150, JSON.stringify(pixel)); result.checks.push({engine,width,name:'patterned-backdrop',passed:true}); await fs.writeFile(`../validation/${label}-${engine}-${width}-patterned-backdrop.png`,screenshot); await context.close(); } } finally { await browser.close(); } } assert.deepEqual(result.errors, []); result.passed = true; await fs.writeFile(`../validation/${label}.json`,JSON.stringify(result,null,2)+'\n'); console.log(JSON.stringify(result));