Mercurial
view 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 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; } 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; });