MiniCPM5-2B-WebGPU-Pi-HTTP / app /tests /mascot-transparency.mjs
Mike0021's picture
Fix Safari idle cat transparency using an explicit silhouette clip
473337c verified
Raw History Blame Contribute Delete
6.05 kB
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));