polyglot-alpha / ui /scripts /w16a-verify.mjs
licaomeng
deploy: main@8970ffb → HF Spaces (2026-05-27T05:19Z)
88d2f2a
// W16-A: verify TriggerButton navigates IMMEDIATELY after POST returns
// event_id, instead of waiting for the lifecycle to terminate.
//
// Expected: click -> URL change measured in ms, NOT seconds.
//
// Usage from polyglot-alpha/ui:
// node scripts/w16a-verify.mjs
//
// Outputs:
// /tmp/w16a-verify.md
// screenshots/w16a/*.png
import { chromium } from "playwright";
import { promises as fs } from "fs";
import path from "path";
const UI = "http://localhost:3001";
const API = "http://127.0.0.1:8000";
const SHOT_DIR = path.resolve(
path.dirname(new URL(import.meta.url).pathname),
"..",
"screenshots",
"w16a",
);
const MANIFEST = "/tmp/w16a-verify.md";
// Targets per the W16-A spec.
const TARGET_CLICK_TO_NAV_MS = 500; // PASS if < 500ms
const now = () => Date.now();
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const report = {
startedAt: new Date().toISOString(),
runs: [],
};
async function runOnce(browser, mode) {
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
const page = await ctx.newPage();
const consoleErrors = [];
page.on("console", (msg) => {
if (msg.type() === "error") consoleErrors.push(msg.text());
});
page.on("pageerror", (err) =>
consoleErrors.push(`pageerror: ${err.message}`),
);
// Use ?mode=mock to force the demo mode for the mock run.
const url = mode === "mock" ? `${UI}/?mode=mock` : `${UI}/`;
console.log(`\n=== ${mode.toUpperCase()} RUN ===`);
console.log(`goto ${url}`);
await page.goto(url, { waitUntil: "domcontentloaded" });
await page.waitForLoadState("networkidle").catch(() => {});
// Give the mode context a moment to hydrate from the query string before
// we grab the button label (otherwise the SSR label shows "live" briefly).
await sleep(1000);
const namePattern = new RegExp(`Trigger ${mode} demo`, "i");
const triggerBtn = page.getByRole("button", { name: namePattern });
try {
await triggerBtn.waitFor({ state: "visible", timeout: 30_000 });
} catch {
// Fall back to aria-label.
}
const btn =
(await triggerBtn.count()) > 0
? triggerBtn.first()
: page.getByRole("button", { name: /Trigger a live demo event/i }).first();
await fs.mkdir(SHOT_DIR, { recursive: true });
await page.screenshot({ path: path.join(SHOT_DIR, `${mode}-00-before.png`) });
// Tight poll loop: click and then sample URL every 20ms so the
// click->URL-change delta is measured at sub-50ms resolution.
const clickT0 = now();
// Fire-and-forget click — don't await the playwright click() promise itself
// because it includes actionability checks that can add 30-100ms of slack.
// Instead we start a polling loop and a separate click promise; whichever
// sees the URL change wins.
let navT = null;
let navEventId = null;
const navPromise = (async () => {
const deadline = now() + 30_000;
while (now() < deadline) {
const u = page.url();
const m = u.match(/\/events\/(\d+)/);
if (m) {
navT = now() - clickT0;
navEventId = m[1];
return;
}
await sleep(20);
}
})();
await btn.click();
await navPromise;
// Detail page paint timing: time to first phase rail / DAG appears.
let firstPaintMs = null;
if (navEventId) {
const paintDeadline = now() + 60_000;
while (now() < paintDeadline) {
const hasPhase = await page
.locator("text=/phase|pending|running|Auction|Translation|Verdict/i")
.first()
.count()
.catch(() => 0);
if (hasPhase > 0) {
firstPaintMs = now() - clickT0;
break;
}
await sleep(50);
}
await page.screenshot({ path: path.join(SHOT_DIR, `${mode}-01-after-nav.png`) });
} else {
await page.screenshot({ path: path.join(SHOT_DIR, `${mode}-01-stuck.png`) });
}
// Quick API status sanity check — confirms backend really did pre-create
// the event row.
let backendStatus = null;
if (navEventId) {
try {
const r = await fetch(`${API}/events/${navEventId}`);
if (r.ok) {
const d = await r.json();
backendStatus = String(d.status ?? "");
} else {
backendStatus = `(HTTP ${r.status})`;
}
} catch (e) {
backendStatus = `(fetch err ${e?.message ?? e})`;
}
}
await ctx.close();
return {
mode,
clickToNavMs: navT,
firstPaintMs,
eventId: navEventId,
backendStatus,
consoleErrors: consoleErrors.slice(0, 10),
pass: navT !== null && navT < TARGET_CLICK_TO_NAV_MS,
};
}
async function writeManifest() {
const lines = [];
lines.push(`# W16-A Verify`);
lines.push(`Started: ${report.startedAt}`);
lines.push(``);
lines.push(`Target: click -> URL change **< ${TARGET_CLICK_TO_NAV_MS}ms**`);
lines.push(``);
for (const r of report.runs) {
lines.push(`## ${r.mode.toUpperCase()}`);
lines.push(`- event_id: \`${r.eventId ?? "n/a"}\``);
lines.push(
`- click -> URL change: **${r.clickToNavMs ?? "(no nav)"} ms** ${r.pass ? "PASS" : "FAIL"}`,
);
lines.push(
`- click -> first phase paint: ${r.firstPaintMs ?? "(not observed)"} ms`,
);
lines.push(`- backend status @ verify: \`${r.backendStatus ?? "n/a"}\``);
lines.push(`- console errors: ${r.consoleErrors.length}`);
r.consoleErrors.forEach((e) => lines.push(` - ${e.slice(0, 220)}`));
lines.push(``);
}
await fs.writeFile(MANIFEST, lines.join("\n"), "utf8");
console.log(`manifest: ${MANIFEST}`);
}
async function main() {
await fs.mkdir(SHOT_DIR, { recursive: true });
const browser = await chromium.launch({ headless: true });
try {
for (const mode of ["mock", "live"]) {
try {
const r = await runOnce(browser, mode);
report.runs.push(r);
console.log(
`${mode}: click->nav=${r.clickToNavMs}ms paint=${r.firstPaintMs}ms ` +
`id=${r.eventId} backend=${r.backendStatus} pass=${r.pass}`,
);
} catch (e) {
console.error(`${mode} run error:`, e);
report.runs.push({
mode,
clickToNavMs: null,
firstPaintMs: null,
eventId: null,
backendStatus: null,
consoleErrors: [`SCRIPT EXC: ${e?.message ?? e}`],
pass: false,
});
}
}
} finally {
await browser.close();
await writeManifest();
}
const allPass = report.runs.every((r) => r.pass);
process.exit(allPass ? 0 : 1);
}
await main();