This repository has been archived on 2026-10-29. You can view files and clone it, but you cannot make any changes to its state, such as pushing and creating new issues, pull requests or comments.
Epiphany/apps/epiphany-gui/scripts/visual-smoke.mjs
2026-04-29 17:22:46 +01:00

126 lines
5 KiB
JavaScript

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.getByText("Role Lanes").waitFor();
await page.getByText("Artifact Bundles").waitFor();
await page.getByRole("button", { name: "Prepare Checkpoint" }).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 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.");
}