// Blueprint canvas smoke covering S1, S2, S3 from the canvas-reimagine // scenario contract. Run after `vite dev` is up at 127.0.0.1:5173. import { chromium } from "playwright"; import { mkdirSync } from "node:fs"; const URL = process.env.URL || "http://127.0.0.1:5173"; const OUT = "qa/screenshots"; mkdirSync(OUT, { recursive: true }); function ok(label, cond, detail = "") { console.log(`${cond ? "✓" : "✗"} ${label}${detail ? " · " + detail : ""}`); if (!cond) process.exitCode = 1; } const b = await chromium.launch({ headless: true }); const ctx = await b.newContext({ viewport: { width: 1440, height: 900 } }); const p = await ctx.newPage(); const errors = []; p.on("pageerror", (e) => errors.push(`pageerror ${e.message}`)); p.on("console", (m) => { if (m.type() === "error") errors.push(`console.error ${m.text().slice(0, 200)}`); }); await p.goto(URL, { waitUntil: "networkidle" }); await p.locator(".sc-card").first().click(); await p.waitForSelector(".mc"); await p.waitForTimeout(800); await p.screenshot({ path: `${OUT}/blueprint-canvas.png`, fullPage: false }); // S1: paper background + navy hairline grid + square nodes const frame = p.locator(".bp-frame").first(); ok("S1.a blueprint frame mounted", await frame.isVisible()); const nodeStyle = await p.locator(".bp-node").first().evaluate((el) => { const cs = getComputedStyle(el); return { radius: cs.borderTopLeftRadius, borderColor: cs.borderTopColor, fontFamily: cs.fontFamily, }; }); ok("S1.b node has zero border radius", nodeStyle.radius === "0px", `was ${nodeStyle.radius}`); ok("S1.c node uses monospace font", /mono|Fira Code/i.test(nodeStyle.fontFamily), `font=${nodeStyle.fontFamily}`); const canvasBg = await p.locator(".bp-canvas-wrap").first().evaluate((el) => getComputedStyle(el).backgroundColor); ok("S1.d canvas background is paper #f5f7fb", canvasBg === "rgb(245, 247, 251)", `was ${canvasBg}`); const gridLines = await p.locator(".react-flow__background").count(); ok("S1.e two grid backgrounds rendered (8px minor + 64px major)", gridLines >= 2, `count=${gridLines}`); // S2: edges use step (orthogonal) — path commands are only M/L/H/V const pathD = await p.locator(".react-flow__edge-path").first().evaluate((el) => el.getAttribute("d") || ""); const hasCurve = /[CQcq]/.test(pathD); ok("S2 edges are orthogonal (no curve commands)", !hasCurve, `d=${pathD.slice(0, 60)}…`); // S3: ruler chrome + corner readout ok("S3.a top ruler present", await p.locator(".bp-ruler-top").isVisible()); ok("S3.b left ruler present", await p.locator(".bp-ruler-left").isVisible()); const tickCount = await p.locator(".bp-tick.is-major").count(); ok("S3.c major ticks rendered (>= 4)", tickCount >= 4, `major ticks=${tickCount}`); ok("S3.d corner glyph present", await p.locator(".bp-corner-tl").isVisible()); const readouts = await p.locator(".bp-readout-top .bp-readout-cell").count(); ok("S3.e top readout has >= 6 cells", readouts >= 6, `cells=${readouts}`); ok("S3.f legend swatches present", await p.locator(".bp-swatch").count() >= 5); // S5: Studio + Settings + Console still render await p.locator(".tab", { hasText: "Studio" }).click(); await p.waitForSelector(".studio"); ok("S5.a studio renders", await p.locator(".studio-panel").count() >= 4); await p.locator(".tab", { hasText: "Settings" }).click(); await p.waitForSelector(".settings"); ok("S5.b settings renders", await p.locator(".quick-users .link-btn").count() >= 1); await p.locator(".tab", { hasText: "Mission" }).click(); await p.waitForSelector(".bp-frame"); await p.locator(".link-btn", { hasText: "Console" }).first().click(); await p.waitForSelector(".console"); ok("S5.c console renders", await p.locator(".console").isVisible()); // S6 cleanup: take a final picture of the canvas await p.locator(".link-btn", { hasText: "Console" }).first().click(); await p.waitForTimeout(300); await p.screenshot({ path: `${OUT}/blueprint-canvas-clean.png`, fullPage: false }); console.log(`\nconsole errors: ${errors.length}`); errors.slice(0, 5).forEach((e) => console.log(" -", e)); if (errors.length > 0) process.exitCode = 1; await b.close();