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();
  }
}