// The browser sink in a real browser as `wai conform sink --suite browser` runs it: the page // web/binding/sink.html (wai-live-web's wasm, built into web/pkg/, and the page's own side table, // web/room/binding.js) answers each call of live-conformance/browser, and this script is the // conformance runner's HTTP adapter for it (WAI extensions/conformance-results.md; wai conform's // adapter.rs): each call `wai conform` POSTs is put to the page, and the page's answer, one JSON // text in RFC 8785 form and a line feed, is the response. // // node e2e/conform_sink.mjs [--web ] [--browser ] [--port ] [--plant] // serves the adapter on 127.0.0.1 and prints `adapter http://127.0.0.1:/`; runs until // interrupted; // node e2e/conform_sink.mjs --wai --wai-dir --out [--independent] [...] // the same, then runs `wai conform sink --suite browser --impl ` itself, its // record signed to and named for the page and the browser's version, and, when the // run passed, `wai verify --conformant` over the record; exits with the first that failed. // // `--browser` names an installed browser (BROWSER_BIN by default); without either, the automation // package's own. PLAYWRIGHT_HOST names the installed automation package's node_modules, as for // room.e2e.mjs. `--plant` is a falsification control: the adapter turns every frame the page // shows as refused into one shown as verified (browser-sink ยง10), which a run must refuse. // Nothing listens beyond loopback. import fs from "node:fs"; import http from "node:http"; import path from "node:path"; import { createRequire } from "node:module"; import { spawn } from "node:child_process"; import { serve, browserArgs, memguardedBrowser, killStrays, tmpdir, until } from "./lib.mjs"; const args = Object.fromEntries(process.argv.slice(2).reduce((a, x, i, all) => (x.startsWith("--") ? [...a, [x.slice(2), all[i + 1] && !all[i + 1].startsWith("--") ? all[i + 1] : "1"]] : a), [])); const HERE = path.dirname(new URL(import.meta.url).pathname); const WEB = path.resolve(args.web || path.join(HERE, "..", "web")); const BROWSER = args.browser || process.env.BROWSER_BIN || ""; const PLANT = args.plant === "1"; for (const f of ["binding/sink.html", "binding/sink.js", "room/binding.js", "pkg/wai_live_web.js", "pkg/wai_live_web_bg.wasm"]) { if (!fs.existsSync(path.join(WEB, f))) { console.error(`conform_sink: ${path.join(WEB, f)} is not there (e2e/run.sh builds web/pkg/)`); process.exit(2); } } const require = createRequire(path.join(process.env.PLAYWRIGHT_HOST || path.join(HERE, "node_modules"), "x")); const { chromium } = require("playwright"); const dir = tmpdir("conform-sink"); const srv = await serve(WEB); const opts = { headless: true, args: browserArgs() }; if (BROWSER) opts.executablePath = memguardedBrowser(path.resolve(BROWSER), dir).executable; const browser = await chromium.launch(opts); const done = async (code) => { await browser.close().catch(() => {}); await srv.close().catch(() => {}); killStrays(); process.exit(code); }; process.on("SIGINT", () => done(130)); process.on("SIGTERM", () => done(143)); const page = await (await browser.newContext()).newPage(); const logs = []; page.on("console", (m) => logs.push(`[${m.type()}] ${m.text()}`)); page.on("pageerror", (e) => logs.push(`[pageerror] ${e.stack || e}`)); await page.goto(`${srv.url}/binding/sink.html`, { waitUntil: "domcontentloaded", timeout: 60000 }); try { await until("the page's wasm is ready", 60000, () => page.evaluate(() => window.waiBrowserSink && window.waiBrowserSink.ready)); } catch (e) { console.error(`conform_sink: ${e.message}\n${logs.join("\n")}`); await done(1); } const ua = await page.evaluate(() => window.waiBrowserSink.version); console.error(`conform_sink: browser ${browser.version()} (${BROWSER || "the automation package's own"}); page ${srv.url}/binding/sink.html`); // The adapter: one call a POST, answered by the page. A call the page cannot answer is a 422 // with the reason, which the runner records as the case's answer. const adapter = http.createServer((req, res) => { if (req.method !== "POST") { res.writeHead(405, { allow: "POST" }); return res.end(); } const chunks = []; req.on("data", (c) => chunks.push(c)); req.on("end", async () => { let body; try { const call = JSON.parse(Buffer.concat(chunks).toString("utf8")); body = await page.evaluate((c) => window.waiBrowserSink.answer(c), call); if (PLANT) body = body.replaceAll('"state":"refused"', '"state":"verified"'); } catch (e) { res.writeHead(422, { "content-type": "text/plain; charset=utf-8" }); return res.end(`the page did not answer: ${String(e.message || e).split("\n")[0]}\n`); } res.writeHead(200, { "content-type": "application/json", "content-length": Buffer.byteLength(body) }); res.end(body); }); }); await new Promise((ok) => adapter.listen(Number(args.port || 0), "127.0.0.1", ok)); const url = `http://127.0.0.1:${adapter.address().port}/`; console.log(`adapter ${url}${PLANT ? " (planted: frames shown refused answer verified)" : ""}`); if (!args.wai) { await new Promise(() => {}); // serve until interrupted } // Run the conformance runner against the adapter, in a child whose exit is awaited: a synchronous // spawn would hold this process's event loop, and with it the adapter. const run = (argv) => new Promise((ok) => { const c = spawn(path.resolve(args.wai), argv, { stdio: "inherit" }); c.on("exit", (code, sig) => ok(code ?? (sig ? 128 : 1))); }); const out = path.resolve(args.out || path.join(dir, "browser.wai-conform.json")); fs.mkdirSync(path.dirname(out), { recursive: true }); const conform = ["conform", "sink", "--suite", "browser", "--impl", url, "--root", path.resolve(args["wai-dir"] || path.join(HERE, "..", "..", "..", "..", "..", "wai")), "--out", out, "--yes", "--text", "--name", `the browser sink page (joule-moq-web web/binding/sink.html)${PLANT ? ", planted" : ""}`, "--version", `${browser.version()}; ${ua}`]; if (args.independent) conform.push("--independent"); let code = await run(conform); if (code === 0) code = await run(["verify", "--conformant", "--root", conform[conform.indexOf("--root") + 1], out]); console.log(`conform_sink: ${code === 0 ? "the page conforms, its record admitted" : `exit ${code}`}: ${out}`); if (logs.some((l) => l.startsWith("[pageerror]"))) console.error(logs.join("\n")); await done(code);