diff --git a/qa/evidence/04-persona-login-success.png b/qa/evidence/04-persona-login-success.png new file mode 100644 index 0000000..1e4d303 Binary files /dev/null and b/qa/evidence/04-persona-login-success.png differ diff --git a/qa/evidence/_dogfood_dist.mjs b/qa/evidence/_dogfood_dist.mjs new file mode 100644 index 0000000..4d268eb --- /dev/null +++ b/qa/evidence/_dogfood_dist.mjs @@ -0,0 +1,104 @@ +// 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);