// Serve the fresh dist/ locally and run the persona-login dogfood against // it to prove the source-side fix works (independent of the broken live // proxy). Backend calls are stubbed at the route level so we measure UI // behavior, not backend availability. import { chromium } from "playwright"; import http from "node:http"; import { readFileSync, statSync } from "node:fs"; import { extname } from "node:path"; const ROOT = "/private/tmp/canvas-work/canvas-real/dist"; const MIME = { ".html": "text/html", ".js": "text/javascript", ".css": "text/css", ".svg": "image/svg+xml", ".png": "image/png", ".json": "application/json", }; const server = http.createServer((req, res) => { let path = req.url.split("?")[0]; if (path === "/") path = "/index.html"; try { const full = ROOT + path; statSync(full); res.setHeader("content-type", MIME[extname(full)] || "application/octet-stream"); res.end(readFileSync(full)); } catch { res.statusCode = 404; res.end("not found"); } }); await new Promise((r) => server.listen(0, r)); const port = server.address().port; const URL = `http://127.0.0.1:${port}/`; console.log("serving dist on", URL); const b = await chromium.launch({ headless: true }); const ctx = await b.newContext({ viewport: { width: 1440, height: 900 } }); const p = await ctx.newPage(); await ctx.route("**/api/v1/auth/microsoft/login", (route) => route.fulfill({ status: 503, body: "" })); await ctx.route("**/api/v1/auth/dev-login-config", (route) => route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ enabled: true }) }), ); await ctx.route("**/api/v1/auth/dev-login", (route) => route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ access_token: "T", refresh_token: "R" }) }), ); await ctx.route("**/api/v1/auth/me", (route) => route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ user_id: "u1", tenant_id: "t1", email: "ceo-head@flow-master.ai", display_name: "Mariana Cole" }), }), ); await ctx.route("**/api/ea2/work-items", (route) => route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ items: [] }) }), ); await ctx.route("**/api/ea2/flow/processes**", (route) => route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ items: [] }) }), ); await ctx.route("**/api/runtime/transactions**", (route) => route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ items: [] }) }), ); const results = []; function rec(n, ok, d = "") { results.push({ n, ok, d }); console.log(`${ok ? "PASS" : "FAIL"} ${n}${d ? " — " + d : ""}`); } await p.goto(URL, { waitUntil: "domcontentloaded" }); await p.waitForTimeout(1500); const onLogin = (await p.locator(".login-page").count()) === 1; rec("login_renders", onLogin); const personas = await p.locator(".persona-chip").allTextContents(); rec("login_shows_three_personas", personas.length === 3, personas.join(" | ")); const ssoCount = await p.locator(".sso-btn").count(); rec("login_has_sso_button", ssoCount === 1); console.log("→ clicking CEO persona chip…"); await p.locator(".persona-chip").filter({ hasText: /Mariana/ }).first().click(); await p.waitForTimeout(2500); const onLanding = (await p.locator(".login-page").count()) === 0; rec("persona_click_signs_in_and_leaves_login", onLanding); const errVisible = await p.locator(".login-error").count(); const errText = errVisible ? await p.locator(".login-error").innerText() : ""; rec("no_error_dump_on_login", !errText.includes("502") && !errText.includes(""), errText.slice(0, 80)); await p.screenshot({ path: "/tmp/canvas-evidence/04-persona-login-success.png" }); await b.close(); server.close(); const fails = results.filter((r) => !r.ok); console.log(`\n${results.length} checks · ${results.length - fails.length} pass · ${fails.length} fail`); process.exit(fails.length ? 1 : 0);