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