The previous $variable form forced lazy per-request DNS resolution. Per-request lookups against 10.43.0.10 → SERVFAIL → cold-cache 502 even when downstream was healthy. Switching to a literal hostname makes nginx resolve once at startup and reuse the cached upstream IP for the lifetime of the worker. Cloudflare anycast is stable so this is safe.
121 lines
6.2 KiB
JavaScript
121 lines
6.2 KiB
JavaScript
// 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}`);
|