view mrjunejune/test/theme_and_webp_test.js @ 258:60a876c4587a

[ui] Add semantic primitive ownership Build a layered Zenbu token and sizing system, make authored controls use native-underneath primitives, migrate mrjunejune without imposing visual surfaces, and document/enforce HTML ownership in the catalog and wiki. Co-authored-by: Copilot <[email protected]>
author MrJuneJune <me@mrjunejune.com>
date Wed, 05 Aug 2026 05:25:40 -0700
parents 30c2196d03d4
children 667156fcd3e3
line wrap: on
line source

const assert = require('node:assert/strict');
const fs = require('node:fs');
const net = require('node:net');
const path = require('node:path');
const { spawn } = require('node:child_process');

const RUNFILES = process.env.JS_BINARY__RUNFILES;
const WORKSPACE = process.env.JS_BINARY__WORKSPACE;
let port;
let baseUrl;
const playwrightPath = path.join(
  RUNFILES,
  WORKSPACE,
  'hg-web/e2e/node_modules/playwright-core',
);
const { chromium } = require(playwrightPath);

function stopProcess(child) {
  if (!child || child.exitCode !== null) return Promise.resolve();
  child.kill('SIGTERM');
  return new Promise(resolve => {
    const timer = setTimeout(() => {
      if (child.exitCode === null) child.kill('SIGKILL');
    }, 3000);
    child.once('exit', () => {
      clearTimeout(timer);
      resolve();
    });
  });
}

async function waitForServer(child, logs) {
  const deadline = Date.now() + 15000;
  while (Date.now() < deadline) {
    if (child.exitCode !== null) {
      throw new Error(`Server exited with ${child.exitCode}\n${logs.join('')}`);
    }
    try {
      const response = await fetch(baseUrl);
      if (response.ok) return;
    } catch {
      // Keep waiting for startup.
    }
    await new Promise(resolve => setTimeout(resolve, 100));
  }
  throw new Error(`Server startup timed out\n${logs.join('')}`);
}

function listFiles(root) {
  const files = [];
  for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
    const absolute = path.join(root, entry.name);
    if (entry.isDirectory()) files.push(...listFiles(absolute));
    else files.push(absolute);
  }
  return files;
}

function primitiveOwnershipViolations(source) {
  const violations = [];
  const stack = [];
  const voidElements = new Set([
    'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link',
    'meta', 'param', 'source', 'track', 'wbr',
  ]);
  const ownerMap = {
    button: new Set(['zen-button']),
    select: new Set(['zen-field', 'zen-native-select', 'zen-select']),
    textarea: new Set(['zen-field', 'zen-textarea']),
  };

  for (const token of source.matchAll(
    /<!--[\s\S]*?-->|<\/?([a-zA-Z][\w-]*)\b([^>]*)>/g,
  )) {
    if (!token[1]) continue;
    const name = token[1].toLowerCase();
    if (token[0].startsWith('</')) {
      while (stack.length) {
        if (stack.pop() === name) break;
      }
      continue;
    }

    let owners = ownerMap[name];
    if (name === 'input') {
      const type = /\btype\s*=\s*["']?([^"'\s>]+)/i.exec(token[2])?.[1]
        ?.toLowerCase() || 'text';
      if (type === 'checkbox') {
        owners = new Set(['zen-checkbox', 'zen-field', 'zen-switch']);
      } else if (type === 'radio') {
        owners = new Set(['zen-field', 'zen-radio-group']);
      } else if (type === 'range') {
        owners = new Set(['zen-field', 'zen-slider']);
      } else if (type === 'date') {
        owners = new Set(['zen-calendar', 'zen-date-picker', 'zen-field']);
      } else {
        owners = new Set([
          'zen-combobox',
          'zen-command',
          'zen-field',
          'zen-input',
          'zen-input-group',
          'zen-input-otp',
        ]);
      }
    }
    if (owners) {
      const matchingOwners = stack.filter(ancestor => owners.has(ancestor));
      if (matchingOwners.length !== 1) {
        violations.push({
          control: name,
          owners: matchingOwners,
          source: token[0],
        });
      }
    }

    if (!voidElements.has(name) && !token[0].endsWith('/>')) {
      stack.push(name);
    }
  }
  return violations;
}

async function sampleTheme(browser, theme) {
  const context = await browser.newContext({ colorScheme: 'dark' });
  await context.addInitScript(value => {
    localStorage.setItem('theme-preference', value);
  }, theme);
  const page = await context.newPage();
  const errors = [];
  page.on('pageerror', error => errors.push(`pageerror: ${error.message}`));
  page.on('console', message => {
    if (message.type() === 'error') errors.push(`console: ${message.text()}`);
  });
  page.on('requestfailed', request => {
    errors.push(`requestfailed: ${request.url()} ${request.failure()?.errorText || ''}`);
  });
  page.on('response', response => {
    if (response.status() >= 400) {
      errors.push(`response: ${response.status()} ${response.url()}`);
    }
  });

  await page.goto(baseUrl, { waitUntil: 'networkidle' });
  await page.waitForFunction(() => {
    const canvas = document.querySelector('#background');
    if (!canvas) return false;
    const data = canvas.getContext('2d').getImageData(
      0,
      0,
      canvas.width,
      canvas.height,
    ).data;
    for (let index = 3; index < data.length; index += 4) {
      if (data[index] >= 20) return true;
    }
    return false;
  });

  const sample = await page.evaluate(() => {
    const canvas = document.querySelector('#background');
    const data = canvas.getContext('2d').getImageData(
      0,
      0,
      canvas.width,
      canvas.height,
    ).data;
    let count = 0;
    let luminance = 0;
    for (let index = 0; index < data.length; index += 4) {
      if (data[index + 3] < 20) continue;
      count++;
      luminance +=
        data[index] * 0.2126 +
        data[index + 1] * 0.7152 +
        data[index + 2] * 0.0722;
    }
    const channels = value => value.match(/\d+(?:\.\d+)?/g)
      .slice(0, 3)
      .map(Number);
    const colorLuminance = value => {
      const converted = channels(value).map(channel => {
        const normalized = channel / 255;
        return normalized <= 0.04045
          ? normalized / 12.92
          : ((normalized + 0.055) / 1.055) ** 2.4;
      });
      return converted[0] * 0.2126 +
        converted[1] * 0.7152 +
        converted[2] * 0.0722;
    };
    const bodyStyle = getComputedStyle(document.body);
    const themeButtonStyle = getComputedStyle(
      document.querySelector('#themeToggle'),
    );
    const foreground = colorLuminance(bodyStyle.color);
    const contrastProbe = document.createElement('span');
    contrastProbe.style.background = 'var(--zen-color-surface)';
    document.body.append(contrastProbe);
    const background = colorLuminance(
      getComputedStyle(contrastProbe).backgroundColor,
    );
    contrastProbe.remove();
    return {
      backgroundRepeat: bodyStyle.backgroundRepeat,
      backgroundSize: bodyStyle.backgroundSize,
      bodyCoversViewport: document.body.getBoundingClientRect().height >=
        innerHeight,
      componentReady: Boolean(customElements.get('zen-button')),
      count,
      fontFamily: getComputedStyle(document.body).fontFamily,
      headerPaws: document.querySelectorAll(
        'header [data-zen-link-decoration]',
      ).length,
      luminance: count ? luminance / count : 0,
      textContrast: (Math.max(foreground, background) + 0.05) /
        (Math.min(foreground, background) + 0.05),
      rootTheme: document.documentElement.dataset.zenTheme || 'auto',
      enhancedLinks: document.querySelectorAll(
        'zen-link[effect="paw"] > a > zen-icon[name="paw"]',
      ).length,
      links: document.querySelectorAll('a').length,
      pawLinks: [...document.querySelectorAll('a')].filter(link =>
        !link.closest('zen-button') &&
        link.dataset.zenLinkEffect !== 'none'
      ).length,
      mainBackground: getComputedStyle(
        document.querySelector('main'),
      ).backgroundColor,
      themeButtonBackground: themeButtonStyle.backgroundColor,
      themeButtonBorder: themeButtonStyle.borderTopWidth,
      themeButtonShadow: themeButtonStyle.boxShadow,
      themeLabel: document.querySelector('#themeName')?.textContent,
    };
  });

  if (errors.length) throw new Error(`${theme}\n${errors.join('\n')}`);
  await context.close();
  return sample;
}

async function testThemeCycle(browser) {
  const context = await browser.newContext({ colorScheme: 'light' });
  const page = await context.newPage();
  await page.goto(baseUrl, { waitUntil: 'networkidle' });
  const expected = ['paper', 'ink', 'playful', 'auto'];
  for (const theme of expected) {
    await page.locator('#themeToggle').click();
    assert.equal(
      await page.evaluate(() =>
        document.documentElement.dataset.zenTheme || 'auto'
      ),
      theme,
    );
    assert.equal(
      await page.evaluate(() => localStorage.getItem('theme-preference')),
      theme,
    );
  }
  await context.close();
}

async function testPlainField(browser) {
  const page = await browser.newPage();
  await page.goto(`${baseUrl}/tools/markdown_to_html`, {
    waitUntil: 'networkidle',
  });
  await page.waitForFunction(() => customElements.get('zen-field'));
  const state = await page.locator('zen-field[appearance="plain"]').evaluate(
    field => {
      const label = field.querySelector('label');
      const textarea = field.querySelector('textarea');
      const probe = document.createElement('span');
      probe.style.border = '1px solid var(--accent)';
      document.body.append(probe);
      const accent = getComputedStyle(probe).borderColor;
      probe.remove();
      textarea.focus();
      return {
        borderColor: getComputedStyle(textarea).borderColor,
        display: getComputedStyle(field).display,
        labelFor: label.htmlFor,
        accent,
        textareaId: textarea.id,
      };
    },
  );
  assert.deepEqual(state, {
    accent: state.borderColor,
    borderColor: state.borderColor,
    display: 'contents',
    labelFor: 'input',
    textareaId: 'input',
  });
  await page.close();
}

async function testButtonScale(browser) {
  const page = await browser.newPage();
  for (const route of [
    '/talk',
    '/notes/login',
    '/tools/hls_player',
    '/tools/latex_editor',
    '/tools/markdown_to_html',
    '/tools/file_converter',
    '/offline.html',
  ]) {
    await page.goto(`${baseUrl}${route}`, { waitUntil: 'networkidle' });
    await page.waitForFunction(() => customElements.get('zen-button'));
    const controls = await page.locator(
      'zen-button[size] > :is(button, a):not(#themeToggle)',
    ).evaluateAll(elements => elements.map(control => {
      const host = control.parentElement;
      const style = getComputedStyle(control);
      const hostStyle = getComputedStyle(host);
      return {
        height: control.getBoundingClientRect().height,
        minimum: Number.parseFloat(
          hostStyle.getPropertyValue('--zenbu-control-height'),
        ) * Number.parseFloat(getComputedStyle(document.documentElement).fontSize),
        paddingBlock: style.paddingBlockStart,
        paddingVariable: Number.parseFloat(
          hostStyle.getPropertyValue('--zenbu-control-padding-block'),
        ) * Number.parseFloat(getComputedStyle(document.documentElement).fontSize),
        size: host.getAttribute('size'),
      };
    }));
    assert.ok(controls.length > 0, route);
    for (const control of controls) {
      assert.match(control.size, /^(?:xs|sm|md|lg|xl)$/);
      assert.ok(control.height >= control.minimum - 1, JSON.stringify(control));
      assert.ok(
        Math.abs(Number.parseFloat(control.paddingBlock) -
          control.paddingVariable) < 1,
        JSON.stringify(control),
      );
    }
  }
  await page.close();
}

async function testDynamicButtonOwnership(browser) {
  const page = await browser.newPage();
  await page.goto(baseUrl, { waitUntil: 'networkidle' });
  await page.evaluate(() => {
    window.dispatchEvent(new Event('beforeinstallprompt', {
      cancelable: true,
    }));
  });
  const state = await page.locator('#pwa-install-btn').evaluate(button => ({
    ownerCount: button.closest('zen-button') ? 1 : 0,
    appearance: button.closest('zen-button')?.getAttribute('appearance'),
    size: button.closest('zen-button')?.getAttribute('size'),
  }));
  assert.deepEqual(state, {
    appearance: 'plain',
    ownerCount: 1,
    size: 'md',
  });
  await page.close();
}

async function testResumePrint(browser) {
  const page = await browser.newPage();
  await page.emulateMedia({ media: 'print' });
  await page.goto(`${baseUrl}/resume`, { waitUntil: 'networkidle' });
  assert.equal(
    await page.locator('.info > a[href="/public/resume.pdf"]').isVisible(),
    false,
  );
  await page.close();
}

async function testHlsPlayer(browser, siteRoot) {
  const page = await browser.newPage();
  const errors = [];
  const mediaRequests = [];
  let testingExpectedFailure = false;
  let testingExpectedReload = false;
  page.on('pageerror', error => errors.push(`pageerror: ${error.message}`));
  page.on('console', message => {
    if (message.type() !== 'error') return;
    if (testingExpectedFailure &&
        message.text().startsWith('Failed to load resource:')) return;
    errors.push(`console: ${message.text()}`);
  });
  page.on('requestfailed', request => {
    if (testingExpectedReload &&
        request.failure()?.errorText === 'net::ERR_ABORTED') return;
    errors.push(`requestfailed: ${request.url()} ${request.failure()?.errorText || ''}`);
  });
  page.on('request', request => {
    if (/\.(?:m3u8|m4s|mp4)(?:\?|$)/.test(request.url())) {
      mediaRequests.push(request.url());
    }
  });

  const playlistResponse = await fetch(
    `${baseUrl}/public/hls-sample/master.m3u8`,
  );
  assert.equal(playlistResponse.status, 200);
  assert.match(
    playlistResponse.headers.get('content-type') || '',
    /^application\/vnd\.apple\.mpegurl/,
  );
  assert.match(
    await playlistResponse.text(),
    /#EXT-X-STREAM-INF:.*CODECS="vp09\.00\.10\.08,opus"/,
  );

  const variantResponse = await fetch(
    `${baseUrl}/public/hls-sample/vp9-stream.m3u8`,
  );
  assert.equal(variantResponse.status, 200);
  assert.match(
    await variantResponse.text(),
    /#EXT-X-MAP:URI="vp9-init\.mp4"/,
  );

  const segmentResponse = await fetch(
    `${baseUrl}/public/hls-sample/vp9-segment000.m4s`,
  );
  assert.equal(segmentResponse.status, 200);
  assert.match(
    segmentResponse.headers.get('content-type') || '',
    /^video\/iso\.segment/,
  );
  const transportStreamResponse = await fetch(
    `${baseUrl}/public/hls-sample/h264-ts-segment000.ts`,
  );
  assert.equal(transportStreamResponse.status, 200);
  assert.match(
    transportStreamResponse.headers.get('content-type') || '',
    /^video\/mp2t/,
  );
  assert.ok((await transportStreamResponse.arrayBuffer()).byteLength > 0);

  await page.goto(
    `${baseUrl}/tools/hls_player?url=${encodeURIComponent('/public/hls-sample/master.m3u8')}`,
    {
      waitUntil: 'networkidle',
    },
  );
  await page.getByRole('button', { name: 'Sample', exact: true }).waitFor();
  assert.equal(
    await page.locator('#hlsUrl').evaluate(input => input.defaultValue),
    '/public/hls-sample/h264-ts-stream.m3u8',
  );
  await page.waitForFunction(() => {
    const status = document.querySelector('#hlsStatus');
    return status?.dataset.state === 'ready' ||
      status?.dataset.state === 'error';
  }, null, { timeout: 15000 });
  const terminalState = await page.locator('#hlsStatus').getAttribute('data-state');
  if (terminalState !== 'ready') {
    throw new Error(
      `HLS player failed: ${await page.locator('#hlsStatus').textContent()}\n${errors.join('\n')}`,
    );
  }
  assert.match(await page.locator('#hlsStatus').textContent(), /Stream ready/);
  assert.equal(
    await page.locator('[data-detail="mode"]').textContent(),
    'hls.js',
  );
  assert.match(
    await page.locator('[data-detail="segments"]').textContent(),
    /Managed by hls\.js|adaptive levels/,
  );
  await page.waitForFunction(() => {
    const video = document.querySelector('#hlsVideo');
    return video.readyState >= HTMLMediaElement.HAVE_METADATA &&
      Number.isFinite(video.duration) &&
      video.duration >= 14;
  });
  const firstFrame = await page.locator('#hlsVideo').evaluate(video => {
    const canvas = document.createElement('canvas');
    canvas.width = video.videoWidth;
    canvas.height = video.videoHeight;
    const context = canvas.getContext('2d');
    context.drawImage(video, 0, 0);
    const pixels = context.getImageData(0, 0, canvas.width, canvas.height).data;
    let total = 0;
    for (let index = 0; index < pixels.length; index += 4) {
      total += pixels[index] + pixels[index + 1] + pixels[index + 2];
    }
    return total / (canvas.width * canvas.height * 3);
  });
  assert.ok(firstFrame > 10, `Initial HLS frame is black: ${firstFrame}`);
  await page.locator('#hlsVideo').evaluate(video => video.play());
  await page.waitForFunction(() => document.querySelector('#hlsVideo').currentTime > 0.2);
  await page.locator('#hlsVideo').evaluate(video => video.pause());
  assert.ok(mediaRequests.some(url => url.endsWith('/hls-sample/master.m3u8')));
  assert.ok(mediaRequests.some(url => url.endsWith('-stream.m3u8')));
  assert.ok(mediaRequests.some(url => url.endsWith('-init.mp4')));
  assert.ok(mediaRequests.some(url => /-segment\d+\.m4s$/.test(url)));

  await page.evaluate(() => {
    window.hlsPlayer.hls.trigger(window.Hls.Events.ERROR, {
      fatal: true,
      type: window.Hls.ErrorTypes.MEDIA_ERROR,
      details: 'testRuntimeFailure',
      error: new Error('runtime segment failed'),
    });
  });
  await page.locator('#hlsStatus[data-state="error"]').waitFor();
  assert.match(
    await page.locator('#hlsStatus').textContent(),
    /runtime segment failed/,
  );
  await page.locator('#hlsUrl').fill('/public/hls-sample/master.m3u8');
  await page.getByRole('button', { name: 'Load', exact: true }).click();
  await page.locator('#hlsStatus[data-state="ready"]').waitFor();

  let delayedSegment = true;
  await page.route('**/hls-sample/vp9-segment000.m4s', async route => {
    if (delayedSegment) {
      delayedSegment = false;
      await new Promise(resolve => setTimeout(resolve, 250));
    }
    await route.continue();
  });
  testingExpectedReload = true;
  await page.locator('#hlsUrl').fill('/public/hls-sample/master.m3u8');
  await page.getByRole('button', { name: 'Load', exact: true }).click();
  await page.waitForTimeout(25);
  await page.getByRole('button', { name: 'Load', exact: true }).click();
  await page.locator('#hlsStatus[data-state="ready"]').waitFor();
  testingExpectedReload = false;
  assert.match(await page.locator('#hlsStatus').textContent(), /Stream ready/);

  await page.evaluate(() => {
    window.__hlsObjectUrls = { created: [], revoked: [] };
    const createObjectURL = URL.createObjectURL.bind(URL);
    const revokeObjectURL = URL.revokeObjectURL.bind(URL);
    URL.createObjectURL = value => {
      const url = createObjectURL(value);
      window.__hlsObjectUrls.created.push(url);
      return url;
    };
    URL.revokeObjectURL = url => {
      window.__hlsObjectUrls.revoked.push(url);
      revokeObjectURL(url);
    };
  });
  const localFiles = listFiles(path.join(siteRoot, 'public/hls-sample'));
  await page.locator('#hlsFiles').setInputFiles(localFiles);
  await page.waitForFunction(() =>
    document.querySelector('#hlsUrl')?.value.startsWith('Local: ')
  );
  await page.locator('#hlsStatus[data-state="ready"]').waitFor();
  assert.match(await page.locator('#hlsUrl').inputValue(), /^Local: /);
  assert.equal(
    await page.locator('[data-detail="mode"]').textContent(),
    'hls.js',
  );
  await page.locator('#hlsVideo').evaluate(video => video.play());
  await page.waitForFunction(() => document.querySelector('#hlsVideo').currentTime > 0.3);
  await page.locator('#hlsVideo').evaluate(video => video.pause());
  const incompleteLocalFiles = localFiles.filter(file =>
    file.endsWith('master.m3u8') ||
    file.endsWith('vp9-stream.m3u8')
  );
  await page.locator('#hlsFiles').setInputFiles(incompleteLocalFiles);
  await page.locator('#hlsStatus[data-state="error"]').waitFor();
  assert.match(
    await page.locator('#hlsStatus').textContent(),
    /Local HLS file is missing/,
  );
  const objectUrlCounts = await page.evaluate(() => ({
    created: window.__hlsObjectUrls.created.length,
    revoked: window.__hlsObjectUrls.revoked.length,
  }));
  assert.ok(objectUrlCounts.created > 10, JSON.stringify(objectUrlCounts));
  assert.ok(
    objectUrlCounts.revoked >= objectUrlCounts.created,
    JSON.stringify(objectUrlCounts),
  );

  await page.route('**/invalid.m3u8', route => route.fulfill({
    status: 404,
    body: 'not found',
  }));
  testingExpectedFailure = true;
  await page.locator('#hlsUrl').fill(`${baseUrl}/invalid.m3u8`);
  await page.getByRole('button', { name: 'Load', exact: true }).click();
  await page.locator('#hlsStatus[data-state="error"]').waitFor();
  assert.match(
    await page.locator('#hlsStatus').textContent(),
    /404|manifestLoadError|Playlist request failed/,
  );

  if (errors.length) throw new Error(errors.join('\n'));
  await page.close();
}

function findFreePort() {
  return new Promise((resolve, reject) => {
    const socket = net.createServer();
    socket.once('error', reject);
    socket.listen(0, '127.0.0.1', () => {
      const address = socket.address();
      socket.close(error => {
        if (error) reject(error);
        else resolve(String(address.port));
      });
    });
  });
}

(async () => {
  assert.ok(RUNFILES);
  assert.ok(WORKSPACE);
  const runfilesWorkspace = path.join(RUNFILES, WORKSPACE);
  const serverBinary = path.join(
    runfilesWorkspace,
    'mrjunejune/mrjunejune_server',
  );
  const siteRoot = path.join(runfilesWorkspace, 'mrjunejune/src');
  const chromiumPath = path.resolve(process.env.CHROMIUM_PATH);
  const serverLogs = [];
  let server;
  let browser;

  try {
    port = await findFreePort();
    baseUrl = `http://127.0.0.1:${port}`;
    const pngFiles = listFiles(siteRoot).filter(file => file.endsWith('.png'));
    assert.deepEqual(pngFiles, [], `PNG files leaked into runfiles: ${pngFiles}`);
    for (const file of listFiles(siteRoot).filter(file =>
      file.endsWith('.html')
    )) {
      const source = fs.readFileSync(file, 'utf8');
      for (const primitive of source.matchAll(/<zen-button\b([^>]*)>/g)) {
        assert.match(
          primitive[1],
          /\bsize=["'](?:xs|sm|md|lg|xl)["']/,
          `${path.relative(siteRoot, file)} has an unsized zen-button`,
        );
      }
      assert.deepEqual(
        primitiveOwnershipViolations(source),
        [],
        `${path.relative(siteRoot, file)} violates primitive ownership`,
      );
    }
    for (const file of listFiles(siteRoot).filter(file =>
      /\.(?:css|html|js)$/.test(file) &&
      !file.includes(`${path.sep}public${path.sep}design-system${path.sep}`)
    )) {
      const source = fs.readFileSync(file, 'utf8');
      assert.doesNotMatch(
        source,
        /var\(\s*--zen-/,
        `${path.relative(siteRoot, file)} consumes a deprecated token`,
      );
    }

    server = spawn(serverBinary, [], {
      cwd: runfilesWorkspace,
      env: { ...process.env, MRJUNEJUNE_PORT: port },
      stdio: ['ignore', 'pipe', 'pipe'],
    });
    server.stdout.on('data', chunk => serverLogs.push(chunk.toString()));
    server.stderr.on('data', chunk => serverLogs.push(chunk.toString()));
    await waitForServer(server, serverLogs);

    const home = await (await fetch(baseUrl)).text();
    const dogGame = await (
      await fetch(`${baseUrl}/public/dog-game.js`)
    ).text();
    const manifest = await (
      await fetch(`${baseUrl}/public/manifest.json`)
    ).text();
    const serviceWorker = await (
      await fetch(`${baseUrl}/sw.js`)
    ).text();
    for (const source of [home, dogGame, manifest]) {
      assert.doesNotMatch(source, /\.png(?:["')]|$)/i);
    }
    assert.match(serviceWorker, /v6-zenbu-headless/);
    assert.match(
      await (
        await fetch(`${baseUrl}/public/pwa-register.js`)
      ).text(),
      /register\('\/sw\.js', \{ scope: '\/' \}\)/,
    );
    assert.ok(serviceWorker.includes("startsWith('/tools/hls_player')"));

    for (const asset of [
      'sprite_shiba0.webp',
      'dog-treat.webp',
      'start-large.webp',
      'icon-192.webp',
      'vscode.webp',
    ]) {
      const response = await fetch(`${baseUrl}/public/${asset}`);
      assert.equal(response.status, 200, asset);
      assert.ok((await response.arrayBuffer()).byteLength > 0, asset);
    }

    browser = await chromium.launch({
      executablePath: chromiumPath,
      headless: true,
      args: ['--no-sandbox'],
    });
    const paper = await sampleTheme(browser, 'paper');
    const ink = await sampleTheme(browser, 'ink');
    const playful = await sampleTheme(browser, 'playful');
    const automatic = await sampleTheme(browser, 'auto');
    assert.equal(paper.rootTheme, 'paper');
    assert.equal(ink.rootTheme, 'ink');
    assert.equal(playful.rootTheme, 'playful');
    assert.equal(automatic.rootTheme, 'auto');
    assert.equal(paper.themeLabel, 'Paper');
    assert.equal(ink.themeLabel, 'Ink');
    assert.equal(playful.themeLabel, 'Playful');
    assert.equal(automatic.themeLabel, 'Auto');
    for (const sample of [paper, ink, playful, automatic]) {
      assert.ok(sample.count > 0);
      assert.equal(sample.backgroundRepeat, 'no-repeat');
      assert.equal(sample.backgroundSize, 'cover');
      assert.equal(sample.bodyCoversViewport, true);
      assert.equal(sample.componentReady, true);
      assert.ok(sample.links > 0);
      assert.equal(sample.enhancedLinks, sample.pawLinks);
      assert.match(sample.fontFamily, /More/);
      assert.equal(sample.headerPaws, 0);
      assert.ok(sample.textContrast >= 4.5, JSON.stringify(sample));
      assert.equal(sample.mainBackground, 'rgba(0, 0, 0, 0)');
      assert.equal(sample.themeButtonBackground, 'rgba(0, 0, 0, 0)');
      assert.equal(sample.themeButtonBorder, '0px');
      assert.equal(sample.themeButtonShadow, 'none');
    }
    assert.ok(paper.luminance < 175, JSON.stringify(paper));
    assert.ok(playful.luminance < 175, JSON.stringify(playful));
    assert.ok(ink.luminance > 200, JSON.stringify(ink));
    await testThemeCycle(browser);
    await testPlainField(browser);
    await testButtonScale(browser);
    await testDynamicButtonOwnership(browser);
    await testResumePrint(browser);
    await testHlsPlayer(browser, siteRoot);
  } finally {
    if (browser) await browser.close();
    await stopProcess(server);
  }
})().catch(error => {
  console.error(error.stack || error);
  process.exitCode = 1;
});