import { chromium } from "playwright"; import { mkdir } from "node:fs/promises"; import { resolve } from "node:path"; import { createServer } from "vite"; const root = resolve(import.meta.dirname, "..", "..", ".."); const artifactDir = resolve(root, ".epiphany-gui"); const desktopScreenshotPath = resolve(artifactDir, "operator-console-smoke-desktop.png"); const mobileScreenshotPath = resolve(artifactDir, "operator-console-smoke-mobile.png"); const url = "http://127.0.0.1:1420"; await mkdir(artifactDir, { recursive: true }); const server = await createServer({ root: resolve(root, "apps", "epiphany-gui"), logLevel: "silent", server: { host: "127.0.0.1", port: 1420, strictPort: true, }, }); try { await server.listen(); await waitForServer(url); const browser = await chromium.launch({ channel: "msedge", headless: true, }); await smokeViewport(browser, { width: 1366, height: 900 }, desktopScreenshotPath); await smokeViewport(browser, { width: 390, height: 844 }, mobileScreenshotPath); await browser.close(); console.log( JSON.stringify( { ok: true, screenshots: [desktopScreenshotPath, mobileScreenshotPath], }, null, 2, ), ); } finally { await server.close(); } async function smokeViewport(browser, viewport, screenshotPath) { const page = await browser.newPage({ viewport }); await page.goto(url, { waitUntil: "networkidle" }); await page.getByRole("heading", { name: "Operator Console" }).waitFor(); await page.getByText("prepareCheckpoint").waitFor(); await page.getByRole("heading", { name: "Environment" }).waitFor(); await page.getByText("Unity Editor").waitFor(); await page.getByText("Rider").waitFor(); await page.getByRole("heading", { name: "Planning", exact: true }).waitFor(); await page.getByRole("heading", { name: "Build the planning dashboard slice", exact: true }).waitFor(); await page.getByText("Role Lanes").waitFor(); await page.getByText("Artifact Bundles").waitFor(); await page.getByRole("button", { name: "Prepare Checkpoint" }).waitFor(); await page.getByRole("button", { name: "Adopt Draft" }).waitFor(); await page.getByRole("button", { name: "Launch Imagination" }).waitFor(); await page.getByRole("button", { name: "Read Imagination" }).waitFor(); await page.getByRole("button", { name: "Launch Modeling" }).waitFor(); await page.getByRole("button", { name: "Read Modeling" }).waitFor(); await page.getByRole("button", { name: "Launch Verification" }).waitFor(); await page.getByRole("button", { name: "Read Verification" }).waitFor(); await page.getByRole("button", { name: "Launch Reorient" }).waitFor(); await page.getByRole("button", { name: "Read Reorient" }).waitFor(); await page.getByRole("button", { name: "Accept Reorient" }).waitFor(); if (viewport.width >= 900) { await page.getByRole("button", { name: "Status Snapshot" }).click(); await page.getByText("statusSnapshot sample completed.").waitFor(); await page.getByRole("button", { name: "Coordinator Plan" }).click(); await page.getByText("coordinatorPlan sample completed.").waitFor(); await page.getByRole("button", { name: "Prepare Checkpoint" }).click(); await page.getByText("prepareCheckpoint sample completed.").waitFor(); await page.getByRole("button", { name: "Read Imagination" }).click(); await page.getByText("readImaginationResult sample completed.").waitFor(); await page.getByRole("button", { name: "Read Modeling" }).click(); await page.getByText("readModelingResult sample completed.").waitFor(); await page.getByRole("button", { name: "Read Reorient" }).click(); await page.getByText("readReorientResult sample completed.").waitFor(); } await page.screenshot({ path: screenshotPath, fullPage: true }); const result = await page.evaluate(() => { const elements = Array.from(document.querySelectorAll("h1, h2, h3, p, dd, code, button, input, .pill")); const overlaps = []; for (let index = 0; index < elements.length; index += 1) { const left = elements[index]; const a = elements[index].getBoundingClientRect(); if (a.width === 0 || a.height === 0) continue; for (let other = index + 1; other < elements.length; other += 1) { const right = elements[other]; if (left.contains(right) || right.contains(left)) continue; const b = elements[other].getBoundingClientRect(); if (b.width === 0 || b.height === 0) continue; const x = Math.min(a.right, b.right) - Math.max(a.left, b.left); const y = Math.min(a.bottom, b.bottom) - Math.max(a.top, b.top); if (x > 2 && y > 2) { overlaps.push({ left: left.textContent?.trim() || left.getAttribute("placeholder") || left.tagName, right: right.textContent?.trim() || right.getAttribute("placeholder") || right.tagName, }); } } } return { overlaps, horizontalOverflow: document.documentElement.scrollWidth > document.documentElement.clientWidth + 1, }; }); await page.close(); if (result.horizontalOverflow) { throw new Error(`visual smoke found horizontal overflow at ${viewport.width}x${viewport.height}`); } if (result.overlaps.length > 0) { throw new Error(`visual smoke found overlapping text/control boxes: ${JSON.stringify(result.overlaps)}`); } } async function waitForServer(target) { const deadline = Date.now() + 30000; while (Date.now() < deadline) { try { const response = await fetch(target); if (response.ok) return; } catch { // Server is still waking up. } await new Promise((resolveWait) => setTimeout(resolveWait, 500)); } throw new Error("Vite server did not start."); }