// Serve dist locally, screenshot the new topbar + scenes with all backends mocked. import { chromium } from "playwright"; import http from "node:http"; import { readFileSync, statSync, writeFileSync } from "node:fs"; import { extname, join } 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"; const full = join(ROOT, path); try { statSync(full); res.setHeader("content-type", MIME[extname(full)] || "application/octet-stream"); res.end(readFileSync(full)); } catch { res.setHeader("content-type", "text/html"); res.end(readFileSync(join(ROOT, "index.html"))); } }); await new Promise((r) => server.listen(0, r)); const port = server.address().port; const URL = `http://127.0.0.1:${port}/`; console.log("serving on", URL); const OUT = "/tmp/canvas-evidence/wave1-local"; import { mkdirSync } from "node:fs"; mkdirSync(OUT, { recursive: true }); const b = await chromium.launch({ headless: true }); const ctx = await b.newContext({ viewport: { width: 1440, height: 900 } }); const p = await ctx.newPage(); // Stub backend await ctx.route("**/api/v1/auth/microsoft/login", (r) => r.fulfill({ status: 503, body: "" })); await ctx.route("**/internal/dev-login-config", (r) => r.fulfill({ status: 200, contentType: "application/json", body: '{"enabled":true}' })); await ctx.route("**/api/v1/auth/dev-login", (r) => r.fulfill({ status: 200, contentType: "application/json", body: '{"access_token":"T","refresh_token":"R"}' })); await ctx.route("**/api/v1/auth/me", (r) => r.fulfill({ status: 200, contentType: "application/json", body: '{"user_id":"u1","tenant_id":"t1","email":"ceo-head@flow-master.ai","display_name":"Mariana Cole"}' })); await ctx.route("**/api/ea2/work-items**", (r) => r.fulfill({ status: 200, contentType: "application/json", body: '{"items":[]}' })); await ctx.route("**/api/ea2/flow/processes**", (r) => r.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ items: [ { _key: "k1", display_name: "Purchase requisition to PO", status: "published", kind: "definition" }, { _key: "k2", display_name: "Laptop procurement (employee → manager → IT)", status: "published", kind: "definition" }, { _key: "k3", display_name: "Customer refund approval", status: "published", kind: "definition" }, ] }) })); await ctx.route("**/api/ea2/flow/**", (r) => r.fulfill({ status: 200, contentType: "application/json", body: '{"_key":"x","display_name":"X","status":"published","kind":"definition","config":{}}' })); await ctx.route("**/api/ea2/edges/**", (r) => r.fulfill({ status: 200, contentType: "application/json", body: '{"items":[]}' })); await ctx.route("**/api/v1/llm/generate", (r) => r.fulfill({ status: 200, contentType: "application/json", body: '{"text":"Sure — here are your published processes: purchase requisition to PO, laptop procurement, customer refund approval.","provider":"demo"}' })); await ctx.route("**/api/ea2/apply-batch", (r) => r.fulfill({ status: 200, contentType: "application/json", body: '{"applied":1,"result":{"ops":[{"key":"new1"},{"key":"new2"}]}}' })); await ctx.route("**/api/runtime/transactions**", (r) => r.fulfill({ status: 200, contentType: "application/json", body: '{"items":[]}' })); async function snap(name) { await p.screenshot({ path: `${OUT}/${name}.png`, fullPage: false }); } // Pre-seed localStorage to skip the onboarding tour await ctx.addInitScript(() => { localStorage.setItem("fm.canvas.tour.seen", String(Date.now())); }); await p.goto(URL, { waitUntil: "domcontentloaded" }); await p.waitForTimeout(1500); await snap("01-login"); await p.locator(".persona-chip").filter({ hasText: /Mariana/ }).first().click(); await p.waitForTimeout(3500); await snap("02-after-login-MISSION"); const shell = await p.evaluate(() => document.querySelector("[class*='shell-']")?.className); console.log("post-login shell:", shell); const tabCount = await p.locator(".tab").count(); console.log("topbar tab count:", tabCount); const tabTexts = await p.locator(".tab").allTextContents(); console.log("tabs:", tabTexts); // avatar/menu const avatar = p.locator(".user-avatar").first(); console.log("user-avatar present:", await avatar.count() > 0); if (await avatar.count()) { await avatar.click(); await p.waitForTimeout(800); await snap("03-user-menu-open"); await p.keyboard.press("Escape"); await p.waitForTimeout(400); } // Studio dogfood await p.locator(".tab").filter({ hasText: /Studio/ }).first().click(); await p.waitForTimeout(1500); await p.locator("textarea").first().fill("Laptop procurement for a regional store manager."); await snap("04-studio-intake"); await p.locator("button.btn-primary").filter({ hasText: /draft/i }).first().click(); await p.waitForTimeout(2500); await snap("05-studio-analyze"); await p.locator("button.btn-primary").filter({ hasText: /Confirm/i }).first().click(); await p.waitForTimeout(2500); await snap("06-studio-after-confirm"); const toastsAfterConfirm = await p.locator(".toast, .toast-msg").allTextContents(); console.log("studio toasts:", toastsAfterConfirm.slice(-3)); // Assistant await p.locator(".tab").filter({ hasText: /Assistant/ }).first().click(); await p.waitForTimeout(1500); await snap("07-assistant"); const input = p.locator(".agent-input, input[placeholder*='Ask']").first(); if (await input.count()) { await input.fill("list processes"); await input.press("Enter"); await p.waitForTimeout(3500); await snap("08-assistant-list-processes"); const replies = await p.locator(".agent-msg-text, .agent-msg-body").allTextContents(); console.log("assistant last reply:", replies.slice(-1)[0]?.slice(0, 200)); } // Chat await p.locator(".tab").filter({ hasText: /Chat/ }).first().click(); await p.waitForTimeout(2500); await snap("09-chat"); // Dark theme via user menu await p.locator(".user-avatar").first().click(); await p.waitForTimeout(400); await p.locator(".user-menu-item").filter({ hasText: /Switch to (dark|light)/i }).first().click(); await p.waitForTimeout(800); await snap("10-theme-toggled"); await b.close(); server.close(); console.log(`\nArtifacts in ${OUT}`);