Site colours: real-browser check for image sampling + palette (Firefox + Chromium)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
39e954bd77
commit
fb9ac0e6c4
1 file changed
+123
@@ -0,0 +1,123 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Real-browser check (headless Firefox AND Chromium) for the colour picker's pixel reading:
|
||||
* src/utils/image-sample.ts + palette-extract.ts. jsdom has no canvas, so this bundles them with
|
||||
* esbuild, serves a generated PNG from one origin (the page's own) and from a second port
|
||||
* (cross-origin), and asserts exact pixel colours, the extracted palette, and the cross-origin
|
||||
* failure message.
|
||||
*
|
||||
* node scripts/e2e-image-sample.mjs (CHROMIUM_PATH / FIREFOX_PATH override binaries)
|
||||
*/
|
||||
import { chromium, firefox } from '@playwright/test';
|
||||
import { build as esbuild } from 'esbuild';
|
||||
import http from 'node:http';
|
||||
import zlib from 'node:zlib';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const W = 1024, H = 512;
|
||||
const RED = [200, 30, 30], BLUE = [20, 20, 200], GREEN = [0, 160, 0];
|
||||
|
||||
// ---- tiny PNG encoder (RGBA, filter 0)
|
||||
function crc32(buf) {
|
||||
let c, crc = ~0;
|
||||
for (let n = 0; n < buf.length; n++) { c = (crc ^ buf[n]) & 255; for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1; crc = (crc >>> 8) ^ c; }
|
||||
return ~crc >>> 0;
|
||||
}
|
||||
function chunk(type, data) {
|
||||
const len = Buffer.alloc(4); len.writeUInt32BE(data.length);
|
||||
const td = Buffer.concat([Buffer.from(type), data]);
|
||||
const crc = Buffer.alloc(4); crc.writeUInt32BE(crc32(td));
|
||||
return Buffer.concat([len, td, crc]);
|
||||
}
|
||||
function png() {
|
||||
const raw = Buffer.alloc((W * 4 + 1) * H);
|
||||
for (let y = 0; y < H; y++) {
|
||||
raw[y * (W * 4 + 1)] = 0;
|
||||
for (let x = 0; x < W; x++) {
|
||||
const c = x < W / 2 ? RED : y < H / 2 ? BLUE : GREEN;
|
||||
raw.set([...c, 255], y * (W * 4 + 1) + 1 + x * 4);
|
||||
}
|
||||
}
|
||||
const ihdr = Buffer.alloc(13); ihdr.writeUInt32BE(W, 0); ihdr.writeUInt32BE(H, 4); ihdr[8] = 8; ihdr[9] = 6;
|
||||
return Buffer.concat([Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]), chunk('IHDR', ihdr), chunk('IDAT', zlib.deflateSync(raw)), chunk('IEND', Buffer.alloc(0))]);
|
||||
}
|
||||
const PNG = png();
|
||||
|
||||
// ---- bundle the units under test
|
||||
const bundle = await esbuild({
|
||||
stdin: {
|
||||
contents: "export * from './src/utils/image-sample'; export * from './src/utils/palette-extract';",
|
||||
resolveDir: root, loader: 'ts',
|
||||
},
|
||||
bundle: true, write: false, format: 'iife', globalName: 'T', platform: 'browser', logLevel: 'error',
|
||||
});
|
||||
const BUNDLE = bundle.outputFiles[0].text;
|
||||
|
||||
// ---- servers: page + same-origin image on A; image only on B (cross-origin)
|
||||
const serve = (withPage) => http.createServer((req, res) => {
|
||||
if (withPage && req.url === '/page.html') { res.setHeader('content-type', 'text/html'); return res.end('<!doctype html><title>t</title><body></body>'); }
|
||||
if (withPage && req.url === '/bundle.js') { res.setHeader('content-type', 'text/javascript'); return res.end(BUNDLE); }
|
||||
if (req.url.startsWith('/img.png')) { res.setHeader('content-type', 'image/png'); return res.end(PNG); }
|
||||
res.statusCode = 404; res.end();
|
||||
});
|
||||
const A = serve(true), B = serve(false);
|
||||
await Promise.all([new Promise((r) => A.listen(0, '127.0.0.1', r)), new Promise((r) => B.listen(0, '127.0.0.1', r))]);
|
||||
const pa = A.address().port, pb = B.address().port;
|
||||
|
||||
let failures = 0;
|
||||
const check = (name, ok, extra = '') => { console.log(`${ok ? 'PASS' : 'FAIL'} ${name}${extra ? ' ' + extra : ''}`); if (!ok) failures++; };
|
||||
const hex = (c) => '#' + c.map((n) => n.toString(16).padStart(2, '0')).join('');
|
||||
|
||||
const targets = [
|
||||
['chromium', chromium, process.env.CHROMIUM_PATH],
|
||||
['firefox', firefox, process.env.FIREFOX_PATH],
|
||||
].filter(([n]) => !process.env.ONLY || process.env.ONLY === n);
|
||||
|
||||
for (const [name, type, exe] of targets) {
|
||||
console.log(`--- ${name}`);
|
||||
const browser = await type.launch({ headless: true, ...(exe ? { executablePath: exe } : {}) });
|
||||
const page = await browser.newPage();
|
||||
await page.goto(`http://127.0.0.1:${pa}/page.html`);
|
||||
await page.addScriptTag({ url: `/bundle.js` });
|
||||
const r = await page.evaluate(async ({ pa, pb }) => {
|
||||
const out = {};
|
||||
const same = await T.loadImageForSampling(`/img.png`);
|
||||
out.sameOk = same.ok;
|
||||
if (same.ok) {
|
||||
out.size = [same.width, same.height];
|
||||
const d = same.imageData;
|
||||
out.red = T.sampleAt(d, 10, 10);
|
||||
out.blue = T.sampleAt(d, 400, 10);
|
||||
out.green = T.sampleAt(d, 400, 200);
|
||||
out.palette = T.extractPalette(d.data);
|
||||
const c = document.createElement('canvas');
|
||||
same.draw(c);
|
||||
out.drawn = [c.width, c.height];
|
||||
} else out.sameMsg = same.message;
|
||||
const abs = await T.loadImageForSampling(`http://127.0.0.1:${pa}/img.png`);
|
||||
out.absSameOk = abs.ok;
|
||||
const cross = await T.loadImageForSampling(`http://127.0.0.1:${pb}/img.png`);
|
||||
out.cross = cross;
|
||||
const bad = await T.loadImageForSampling(`/missing.png`);
|
||||
out.bad = bad;
|
||||
return out;
|
||||
}, { pa, pb });
|
||||
check('same-origin image loads', r.sameOk, r.sameMsg || '');
|
||||
check('downscaled to 512x256', JSON.stringify(r.size) === '[512,256]', JSON.stringify(r.size));
|
||||
check('draw() sizes canvas', JSON.stringify(r.drawn) === '[512,256]');
|
||||
check('pixel red exact', r.red === hex(RED), r.red);
|
||||
check('pixel blue exact', r.blue === hex(BLUE), r.blue);
|
||||
check('pixel green exact', r.green === hex(GREEN), r.green);
|
||||
check('palette = red, blue, green in population order (red 50%, blue/green 25%)',
|
||||
r.palette[0] === hex(RED) && r.palette.length === 3 && r.palette.includes(hex(BLUE)) && r.palette.includes(hex(GREEN)), JSON.stringify(r.palette));
|
||||
check('absolute same-origin URL loads', r.absSameOk);
|
||||
check('cross-origin refused', r.cross.ok === false && r.cross.reason === 'cross-origin');
|
||||
check('cross-origin message exact', r.cross.message === 'That image comes from another website, so the browser will not let the builder read its colours. Upload it to this site first, then pick from the uploaded copy.');
|
||||
check('missing image -> error', r.bad.ok === false && r.bad.reason === 'error', r.bad.message);
|
||||
await browser.close();
|
||||
}
|
||||
A.close(); B.close();
|
||||
console.log(failures ? `\n${failures} FAILED` : '\nall passed');
|
||||
process.exit(failures ? 1 : 0);
|
||||
Reference in new issue
Block a user