From de4def3548a6729b0dfd2120495a61beef8c8c2c Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Sun, 5 Jul 2026 20:21:50 -0300 Subject: pardes v2: the rewrite, complete and organized. src/ (core + three shells), test/ (snapshot parity harness + 18 frozen goldens). One sans-IO core, vaxis tty + SDL3 GPU native + wasm web shells, 18/18 parity with the purged prototype, 7.6k lines vs 12.1k. Fix: gui shell pre-sized the core at init so the greet-releasing resize never fired (blank panes until first interaction); live sessions now init at defaults and get the real grid as a resize event (the shell contract, documented on Options). --- tests/browser_replay_e2e.mjs | 356 ------------------------------------------- 1 file changed, 356 deletions(-) delete mode 100644 tests/browser_replay_e2e.mjs (limited to 'tests/browser_replay_e2e.mjs') diff --git a/tests/browser_replay_e2e.mjs b/tests/browser_replay_e2e.mjs deleted file mode 100644 index 2d8e202c..00000000 --- a/tests/browser_replay_e2e.mjs +++ /dev/null @@ -1,356 +0,0 @@ -import { createServer } from "node:http"; -import { mkdtemp, readFile, rm, stat } from "node:fs/promises"; -import { spawn } from "node:child_process"; -import { createRequire } from "node:module"; -import { tmpdir } from "node:os"; -import { resolve, join, extname } from "node:path"; -import { inflateSync } from "node:zlib"; - -const webDir = resolve(process.argv[2] ?? "zig-out/web"); -const screenshotPath = resolve(process.argv[3] ?? "zig-out/browser-replay.png"); -const backingScale = Number(process.argv[4] ?? "1.5"); -if (!Number.isFinite(backingScale) || backingScale <= 0) { - throw new Error(`invalid browser backing scale: ${process.argv[4]}`); -} - -const mime = new Map([ - [".html", "text/html; charset=utf-8"], - [".js", "text/javascript; charset=utf-8"], - [".wasm", "application/wasm"], - [".png", "image/png"], -]); - -function run(cmd, args) { - return new Promise((resolvePromise, reject) => { - const child = spawn(cmd, args, { stdio: "inherit" }); - child.on("error", reject); - child.on("exit", (code) => { - if (code === 0) resolvePromise(); - else reject(new Error(`${cmd} ${args.join(" ")} exited with ${code}`)); - }); - }); -} - -async function loadPlaywright() { - try { - return { api: await import("playwright"), cleanup: async () => {} }; - } catch (err) { - if (process.env.PARDES_BROWSER_SKIP_INSTALL === "1") throw err; - } - - const dir = await mkdtemp(join(tmpdir(), "pardes-playwright-")); - try { - await run("npm", ["--prefix", dir, "install", "--silent", "playwright"]); - await run("npm", ["--prefix", dir, "exec", "--", "playwright", "install", "chromium"]); - const require = createRequire(join(dir, "package.json")); - return { - api: require("playwright"), - cleanup: async () => { - await rm(dir, { recursive: true, force: true }); - }, - }; - } catch (err) { - await rm(dir, { recursive: true, force: true }); - throw err; - } -} - -async function serve(root) { - await stat(join(root, "replay.html")); - await stat(join(root, "pardes-replay.js")); - - const server = createServer(async (req, res) => { - try { - const url = new URL(req.url ?? "/", "http://127.0.0.1"); - const path = url.pathname === "/" ? "/replay.html" : url.pathname; - if (path.includes("..")) throw new Error("bad path"); - const file = join(root, path.slice(1)); - const body = await readFile(file); - res.writeHead(200, { - "content-type": mime.get(extname(file)) ?? "application/octet-stream", - "cache-control": "no-store", - }); - res.end(body); - } catch { - res.writeHead(404); - res.end("not found"); - } - }); - - await new Promise((resolvePromise) => server.listen(0, "127.0.0.1", resolvePromise)); - const address = server.address(); - if (!address || typeof address === "string") throw new Error("failed to bind test server"); - return { server, url: `http://127.0.0.1:${address.port}/replay.html` }; -} - -function parsePng(buf) { - const sig = "89504e470d0a1a0a"; - if (buf.subarray(0, 8).toString("hex") !== sig) throw new Error("not a png"); - - let offset = 8; - let width = 0; - let height = 0; - let bitDepth = 0; - let colorType = 0; - const idat = []; - while (offset < buf.length) { - const len = buf.readUInt32BE(offset); - const type = buf.subarray(offset + 4, offset + 8).toString("ascii"); - const data = buf.subarray(offset + 8, offset + 8 + len); - offset += 12 + len; - if (type === "IHDR") { - width = data.readUInt32BE(0); - height = data.readUInt32BE(4); - bitDepth = data[8]; - colorType = data[9]; - } else if (type === "IDAT") { - idat.push(data); - } else if (type === "IEND") { - break; - } - } - if (bitDepth !== 8) throw new Error(`unsupported png bit depth ${bitDepth}`); - const channels = colorType === 6 ? 4 : colorType === 2 ? 3 : colorType === 0 ? 1 : 0; - if (channels === 0) throw new Error(`unsupported png color type ${colorType}`); - - const raw = inflateSync(Buffer.concat(idat)); - const stride = width * channels; - const rows = Buffer.alloc(width * height * channels); - let rawOffset = 0; - for (let y = 0; y < height; y += 1) { - const filter = raw[rawOffset]; - rawOffset += 1; - const rowStart = y * stride; - for (let x = 0; x < stride; x += 1) { - const left = x >= channels ? rows[rowStart + x - channels] : 0; - const up = y > 0 ? rows[rowStart + x - stride] : 0; - const upLeft = y > 0 && x >= channels ? rows[rowStart + x - stride - channels] : 0; - const value = raw[rawOffset + x]; - let recon; - if (filter === 0) recon = value; - else if (filter === 1) recon = value + left; - else if (filter === 2) recon = value + up; - else if (filter === 3) recon = value + Math.floor((left + up) / 2); - else if (filter === 4) { - const p = left + up - upLeft; - const pa = Math.abs(p - left); - const pb = Math.abs(p - up); - const pc = Math.abs(p - upLeft); - recon = value + (pa <= pb && pa <= pc ? left : pb <= pc ? up : upLeft); - } else { - throw new Error(`unsupported png filter ${filter}`); - } - rows[rowStart + x] = recon & 0xff; - } - rawOffset += stride; - } - return { width, height, channels, rows }; -} - -function validateScreenshot(png) { - let bright = 0; - let colored = 0; - let nonDark = 0; - const pixels = png.width * png.height; - for (let i = 0; i < pixels; i += 1) { - const off = i * png.channels; - const r = png.rows[off]; - const g = png.channels === 1 ? r : png.rows[off + 1]; - const b = png.channels === 1 ? r : png.rows[off + 2]; - if (r > 90 || g > 90 || b > 90) bright += 1; - if (Math.max(r, g, b) - Math.min(r, g, b) > 20) colored += 1; - if (r > 35 || g > 35 || b > 35) nonDark += 1; - } - if (png.width < 640 || png.height < 360) { - throw new Error(`screenshot too small: ${png.width}x${png.height}`); - } - if (bright < 500 || nonDark < 2000) { - throw new Error(`browser replay appears blank: bright=${bright} nonDark=${nonDark} colored=${colored}`); - } - console.log(`browser replay screenshot ok: ${png.width}x${png.height}, bright=${bright}, nonDark=${nonDark}, colored=${colored}`); -} - -function pixelRgb(png, x, y) { - const off = (y * png.width + x) * png.channels; - const r = png.rows[off]; - return { - r, - g: png.channels === 1 ? r : png.rows[off + 1], - b: png.channels === 1 ? r : png.rows[off + 2], - }; -} - -function countChangedPixels(a, b, region = {}) { - if (a.width !== b.width || a.height !== b.height) { - throw new Error(`png sizes differ: ${a.width}x${a.height} vs ${b.width}x${b.height}`); - } - const x0 = Math.max(0, Math.floor(region.x0 ?? 0)); - const y0 = Math.max(0, Math.floor(region.y0 ?? 0)); - const x1 = Math.min(a.width, Math.ceil(region.x1 ?? a.width)); - const y1 = Math.min(a.height, Math.ceil(region.y1 ?? a.height)); - let changed = 0; - for (let y = y0; y < y1; y += 1) { - for (let x = x0; x < x1; x += 1) { - const ap = pixelRgb(a, x, y); - const bp = pixelRgb(b, x, y); - const dr = Math.abs(ap.r - bp.r); - const dg = Math.abs(ap.g - bp.g); - const db = Math.abs(ap.b - bp.b); - if (dr + dg + db > 45 && Math.max(dr, dg, db) > 12) changed += 1; - } - } - return changed; -} - -async function screenshotPng(page) { - return parsePng(await page.screenshot({ fullPage: true })); -} - -async function waitForNextDraw(page, action) { - const before = await page.evaluate(() => window.__pardesReplayE2E?.draws ?? 0); - await action(); - await page.waitForFunction((draws) => (window.__pardesReplayE2E?.draws ?? 0) > draws, before, { timeout: 5000 }); - await page.waitForTimeout(50); -} - -async function findHoverBoundary(page, baseline) { - const canvasSize = await page.evaluate(() => { - const canvas = document.querySelector("canvas"); - return { width: canvas.clientWidth, height: canvas.clientHeight }; - }); - const y = Math.floor(canvasSize.height * 0.55); - const center = canvasSize.width / 2; - const offsets = []; - for (let dx = -48; dx <= 48; dx += 3) offsets.push(dx); - - for (const dx of offsets) { - const x = Math.round(center + dx); - await waitForNextDraw(page, () => page.mouse.move(x, y)); - const hover = await screenshotPng(page); - const changed = countChangedPixels(baseline, hover, { - x0: x - 20, - x1: x + 20, - y0: 20, - y1: canvasSize.height - 20, - }); - if (changed >= 80) return { x, y, changed, canvasSize }; - } - - throw new Error("browser replay hover did not visibly draw the dashed resize handle"); -} - -const { server, url } = await serve(webDir); -let cleanupPlaywright = async () => {}; -let browser; -try { - const { api, cleanup } = await loadPlaywright(); - cleanupPlaywright = cleanup; - const { chromium } = api; - - browser = await chromium.launch({ - headless: true, - args: ["--use-angle=swiftshader", "--enable-unsafe-swiftshader"], - }); - const page = await browser.newPage({ viewport: { width: 1280, height: 720 } }); - const consoleMessages = []; - const pageErrors = []; - page.on("console", (msg) => consoleMessages.push(`${msg.type()}: ${msg.text()}`)); - page.on("pageerror", (err) => pageErrors.push(err.stack ?? err.message)); - - await page.addInitScript(() => { - window.__pardesReplayE2E = { draws: 0, contextLost: 0 }; - window.addEventListener("webglcontextlost", () => { - window.__pardesReplayE2E.contextLost += 1; - }, true); - - const wrapDraws = (target) => { - if (!target) return; - for (const name of ["drawArraysInstanced", "drawArrays", "drawElements", "drawElementsInstanced"]) { - if (typeof target[name] !== "function" || target[name].__pardesReplayE2E) continue; - const oldDraw = target[name]; - target[name] = function(...args) { - window.__pardesReplayE2E.draws += 1; - return oldDraw.apply(this, args); - }; - target[name].__pardesReplayE2E = true; - } - }; - - wrapDraws(globalThis.WebGLRenderingContext?.prototype); - wrapDraws(globalThis.WebGL2RenderingContext?.prototype); - - const oldGetContext = HTMLCanvasElement.prototype.getContext; - HTMLCanvasElement.prototype.getContext = function(type, attrs) { - const ctx = oldGetContext.call(this, type, attrs); - if (ctx && !ctx.__pardesReplayE2E && (type === "webgl2" || type === "webgl" || type === "experimental-webgl")) { - ctx.__pardesReplayE2E = true; - wrapDraws(ctx); - } - return ctx; - }; - }); - - await page.goto(url, { waitUntil: "load" }); - await page.waitForSelector("canvas", { timeout: 5000 }); - await page.waitForFunction(() => window.__pardesReplayE2E?.draws >= 2, null, { timeout: 5000 }); - await page.waitForTimeout(250); - const trace = await page.evaluate(() => window.__pardesReplayE2E); - if (trace.contextLost !== 0) { - throw new Error(`browser WebGL context was lost during replay: ${JSON.stringify(trace)}`); - } - if (pageErrors.length !== 0) { - throw new Error(`browser page error: ${pageErrors[0]}`); - } - const lostMessage = consoleMessages.find((msg) => msg.includes("CONTEXT_LOST_WEBGL")); - if (lostMessage) throw new Error(`browser WebGL context lost: ${lostMessage}`); - - const initialCanvasSize = await page.evaluate(() => { - const canvas = document.querySelector("canvas"); - return { - clientWidth: canvas.clientWidth, - clientHeight: canvas.clientHeight, - width: canvas.width, - height: canvas.height, - }; - }); - if ( - initialCanvasSize.clientWidth !== 1280 || - initialCanvasSize.clientHeight !== 720 || - initialCanvasSize.width !== Math.round(1280 * backingScale) || - initialCanvasSize.height !== Math.round(720 * backingScale) - ) { - throw new Error(`browser replay canvas did not fill initial viewport: ${JSON.stringify(initialCanvasSize)}`); - } - - await page.setViewportSize({ width: 913, height: 617 }); - await page.waitForFunction((scale) => { - const canvas = document.querySelector("canvas"); - return canvas?.clientWidth === 913 && - canvas?.clientHeight === 617 && - canvas?.width === Math.round(913 * scale) && - canvas?.height === Math.round(617 * scale); - }, backingScale, { timeout: 5000 }); - await page.waitForFunction(() => window.__pardesReplayE2E?.draws >= 3, null, { timeout: 5000 }); - - const baseline = await screenshotPng(page); - const hover = await findHoverBoundary(page, baseline); - console.log(`browser replay hover ok: changed=${hover.changed} x=${hover.x} y=${hover.y}`); - - const dragToX = Math.min(hover.canvasSize.width - 24, hover.x + 120); - await waitForNextDraw(page, () => page.mouse.down()); - await waitForNextDraw(page, () => page.mouse.move(dragToX, hover.y, { steps: 8 })); - await waitForNextDraw(page, () => page.mouse.up()); - const dragged = await screenshotPng(page); - const dragChanged = countChangedPixels(baseline, dragged); - if (dragChanged < 2500) { - throw new Error(`browser replay drag did not visibly resize panes: changed=${dragChanged}`); - } - console.log(`browser replay drag ok: changed=${dragChanged}`); - - await page.screenshot({ path: screenshotPath, fullPage: true }); - validateScreenshot(parsePng(await readFile(screenshotPath))); -} finally { - if (browser) await browser.close(); - await cleanupPlaywright(); - server.close(); -} -- cgit v1.3