// Runs a list of CTS queries against the suite's own standalone runner in a // single Chromium instance, reusing one page (navigating it fresh for each // query) rather than relaunching the browser per query. Mirrors run-shard.sh: // one JSON result (or error) file per query under OUT_DIR, a shard.log, and a // done.txt of finished queries so a rerun on the same OUT_DIR skips what // already finished. // // Usage: node browser-run-queries.cjs const fs = require('fs'); const path = require('path'); const { chromium } = require('playwright'); async function main() { const [, , baseUrl, outDir, queriesFile] = process.argv; if (!baseUrl || !outDir || !queriesFile) { console.error('usage: browser-run-queries.cjs '); process.exit(1); } fs.mkdirSync(outDir, { recursive: true }); const logPath = path.join(outDir, 'shard.log'); const donePath = path.join(outDir, 'done.txt'); const log = msg => fs.appendFileSync(logPath, msg + '\n'); const done = new Set( fs.existsSync(donePath) ? fs.readFileSync(donePath, 'utf8').split('\n').filter(Boolean) : [] ); const queries = fs .readFileSync(queriesFile, 'utf8') .split('\n') .map(l => l.trim()) .filter(l => l && !l.startsWith('#')); const launchArgs = { headless: true, args: [ '--enable-unsafe-webgpu', '--enable-features=Vulkan', '--use-angle=vulkan', '--use-gl=angle', '--use-vulkan=native', '--ignore-gpu-blocklist', ], }; let browser = await chromium.launch(launchArgs); try { let page = await browser.newPage(); for (const query of queries) { if (done.has(query)) continue; const fname = query.replace(/[^A-Za-z0-9_.-]/g, '_'); // A hung browser instance (navigation never returns, even though the // dev server itself answers instantly) leaves every later query on // this shard stuck the same way, so on a timeout the browser is // closed and relaunched before the query is retried once. Only a // timeout that survives the relaunch is recorded as done. for (let attempt = 1; attempt <= 2; attempt++) { log(`=== ${new Date().toISOString()} ${query} (attempt ${attempt}) ===`); try { const url = `${baseUrl}/standalone/browser-headless.html?q=${encodeURIComponent(query)}`; await page.goto(url, { timeout: 60000 }); // waitForFunction(pageFunction, arg, options) -- the timeout goes in // the third argument, not the second, or it silently falls back to // Playwright's 30s default and queries with many cases time out. await page.waitForFunction(() => window.__ctsDone === true, undefined, { timeout: 600000 }); const result = await page.evaluate(() => ({ json: window.__ctsResultJSON, error: window.__ctsError, })); if (result.json) { fs.writeFileSync(path.join(outDir, `${fname}.json`), result.json); log(`ok for ${query}`); } else { fs.writeFileSync(path.join(outDir, `${fname}.error.txt`), result.error || '(no error message)'); log(`error for ${query}: ${result.error}`); } fs.appendFileSync(donePath, query + '\n'); break; } catch (e) { log(`exception for ${query}: ${e}`); const isTimeout = /Timeout/i.test(String((e && e.message) || e)); if (isTimeout && attempt === 1) { log(`relaunching browser after timeout on ${query}`); await browser.close().catch(() => {}); browser = await chromium.launch(launchArgs); page = await browser.newPage(); continue; } fs.writeFileSync(path.join(outDir, `${fname}.error.txt`), String((e && e.stack) || e)); fs.appendFileSync(donePath, query + '\n'); break; } } } } finally { await browser.close(); } log(`=== shard done ${new Date().toISOString()} ===`); } main().catch(e => { console.error(e); process.exit(1); });