File size: 3,611 Bytes
39309b1 08132d7 39309b1 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 | /* browser-harness.mjs: the one place the browser tests get a page from.
*
* Real behaviour (canvas pixels, SVG-filter textures, ::-webkit-slider-thumb,
* rAF timing) cannot be checked in Node, so these tests drive a real Chrome.
* It uses playwright-core, which ships no browser, and finds one that is
* already installed. Point DSSL_CHROME at a binary if yours is somewhere odd.
*
* import { withBrowser } from './lib/browser-harness.mjs';
* await withBrowser(async ({ open }) => { const page = await open('/x.html'); ... });
*/
import { createServer } from 'node:http';
import { readFileSync, existsSync } from 'node:fs';
import { extname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { chromium } from 'playwright-core';
export const ROOT = resolve(fileURLToPath(new URL('../..', import.meta.url)));
const MIME = { '.js': 'text/javascript', '.mjs': 'text/javascript', '.html': 'text/html', '.css': 'text/css', '.json': 'application/json', '.dssl': 'text/plain', '.svg': 'image/svg+xml' };
const CANDIDATES = [
process.env.DSSL_CHROME,
'/usr/bin/google-chrome', '/usr/bin/google-chrome-stable', '/usr/bin/chromium', '/usr/bin/chromium-browser',
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
].filter(Boolean);
export function findChrome() {
return CANDIDATES.find((p) => existsSync(p)) || null;
}
/** static server: /dssl/* is public/dssl/*, /styles/* is public/styles/*, /t/* is a scratch dir of test pages */
export function startServer(pages = {}) {
const server = createServer((req, res) => {
const url = req.url.split('?')[0];
if (url === '/favicon.ico') { res.writeHead(204); return res.end(); }
if (pages[url]) { res.writeHead(200, { 'content-type': 'text/html' }); return res.end(pages[url]); }
let file;
if (url.startsWith('/dssl/')) file = join(ROOT, 'public', url);
else if (url.startsWith('/styles/') || url.startsWith('/showcase/')) file = join(ROOT, 'public', url);
else file = join(ROOT, url);
try {
const body = readFileSync(file);
res.writeHead(200, { 'content-type': MIME[extname(file)] || 'application/octet-stream', 'access-control-allow-origin': '*' });
res.end(body);
} catch { res.writeHead(404); res.end('not found'); }
});
return new Promise((ok) => server.listen(0, '127.0.0.1', () => ok({ server, port: server.address().port })));
}
export async function withBrowser(fn, { pages = {}, viewport = { width: 1000, height: 700 }, reducedMotion = 'no-preference', colorScheme = 'light', deviceScaleFactor = 1 } = {}) {
const exe = findChrome();
if (!exe) throw new Error('no Chrome found; set DSSL_CHROME to a Chrome or Chromium binary');
const { server, port } = await startServer(pages);
const browser = await chromium.launch({ executablePath: exe, headless: true, args: ['--no-sandbox', '--disable-gpu', '--hide-scrollbars'] });
const context = await browser.newContext({ viewport, reducedMotion, colorScheme, deviceScaleFactor });
const base = `http://127.0.0.1:${port}`;
const errors = [];
try {
const open = async (path, { wait = 'load' } = {}) => {
const page = await context.newPage();
page.on('pageerror', (e) => errors.push(String(e)));
page.on('console', (m) => { if (m.type() === 'error') errors.push(m.text()); });
await page.goto(base + path, { waitUntil: wait });
return page;
};
return await fn({ open, base, errors, context, browser });
} finally {
await browser.close();
server.close();
}
}
|