diff mrjunejune/test/theme_and_webp_test.js @ 241:9c2eec61a152

[assets] Generate site images as WebP with Bazel
author MrJuneJune <me@mrjunejune.com>
date Mon, 03 Aug 2026 11:26:30 -0700
parents
children 543df0fe7168
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mrjunejune/test/theme_and_webp_test.js	Mon Aug 03 11:26:30 2026 -0700
@@ -0,0 +1,209 @@
+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;
+}
+
+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;
+    }
+    return {
+      count,
+      luminance: count ? luminance / count : 0,
+      rootClass: document.documentElement.className,
+    };
+  });
+
+  if (errors.length) throw new Error(`${theme}\n${errors.join('\n')}`);
+  await context.close();
+  return sample;
+}
+
+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}`);
+
+    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();
+    for (const source of [home, dogGame, manifest]) {
+      assert.doesNotMatch(source, /\.png(?:["')]|$)/i);
+    }
+
+    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 light = await sampleTheme(browser, 'light');
+    const dark = await sampleTheme(browser, 'dark');
+    assert.ok(light.count > 0);
+    assert.ok(dark.count > 0);
+    assert.ok(light.luminance < 175, JSON.stringify(light));
+    assert.ok(dark.luminance > 200, JSON.stringify(dark));
+  } finally {
+    if (browser) await browser.close();
+    await stopProcess(server);
+  }
+})().catch(error => {
+  console.error(error.stack || error);
+  process.exitCode = 1;
+});