152 lines
5.6 KiB
JavaScript
152 lines
5.6 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";
|
|
const fluidStorageKey = "epiphany:aquarium-fluid-params:v3";
|
|
|
|
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,
|
|
});
|
|
const desktop = await smokeViewport(browser, { width: 1366, height: 900 }, desktopScreenshotPath, true);
|
|
const mobile = await smokeViewport(browser, { width: 390, height: 844 }, mobileScreenshotPath, false);
|
|
await browser.close();
|
|
console.log(
|
|
JSON.stringify(
|
|
{
|
|
ok: true,
|
|
screenshots: [desktopScreenshotPath, mobileScreenshotPath],
|
|
probes: { desktop, mobile },
|
|
},
|
|
null,
|
|
2,
|
|
),
|
|
);
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
|
|
async function smokeViewport(browser, viewport, screenshotPath, exerciseFluidPanel) {
|
|
const page = await browser.newPage({ viewport });
|
|
await page.goto(url, { waitUntil: "networkidle" });
|
|
await page.locator("h1").filter({ hasText: "Operator Console" }).waitFor({ state: "attached" });
|
|
await page.locator(".immersiveShell").waitFor();
|
|
await page.locator(".agentSmokeCanvas").waitFor();
|
|
await page.locator(".agentCrispCanvas").waitFor();
|
|
await page.waitForTimeout(700);
|
|
|
|
const smokeProbe = await probeCanvas(page, ".agentSmokeCanvas");
|
|
const crispProbe = await probeCanvas(page, ".agentCrispCanvas");
|
|
if (!smokeProbe.nonBlank) {
|
|
throw new Error(`agent smoke canvas did not render: ${smokeProbe.reason}`);
|
|
}
|
|
if (!crispProbe.nonBlank) {
|
|
throw new Error(`agent crisp canvas did not render: ${crispProbe.reason}`);
|
|
}
|
|
|
|
const hiddenSurface = await page.evaluate(() => {
|
|
return [".immersiveTopbar", ".deckRail", ".diegeticPanel", ".hudToastStack"].every((selector) => {
|
|
const element = document.querySelector(selector);
|
|
if (!element) return false;
|
|
const style = window.getComputedStyle(element);
|
|
return style.opacity === "0" && style.pointerEvents === "none";
|
|
});
|
|
});
|
|
if (!hiddenSurface) {
|
|
throw new Error("static operator overlays are still visible or interactive");
|
|
}
|
|
|
|
let persistedParams = null;
|
|
if (exerciseFluidPanel) {
|
|
await page.evaluate((key) => window.localStorage.removeItem(key), fluidStorageKey);
|
|
await page.reload({ waitUntil: "networkidle" });
|
|
await page.locator(".agentSmokeCanvas").waitFor();
|
|
await page.waitForTimeout(700);
|
|
await page.mouse.click(viewport.width - 48, viewport.height - 48);
|
|
await page.waitForTimeout(120);
|
|
const railY = Math.round(viewport.height * 0.47);
|
|
await page.mouse.move(viewport.width - 226, railY);
|
|
await page.mouse.down();
|
|
await page.mouse.move(viewport.width - 46, railY, { steps: 8 });
|
|
await page.mouse.up();
|
|
persistedParams = await page.evaluate((key) => window.localStorage.getItem(key), fluidStorageKey);
|
|
if (!persistedParams || !persistedParams.includes("timeScale")) {
|
|
throw new Error("fluid parameter panel did not persist changed parameters");
|
|
}
|
|
}
|
|
|
|
await page.screenshot({ path: screenshotPath, fullPage: true });
|
|
const result = await page.evaluate(() => ({
|
|
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}`);
|
|
}
|
|
return { smokeProbe, crispProbe, persistedParams };
|
|
}
|
|
|
|
async function probeCanvas(page, selector) {
|
|
return page.locator(selector).evaluate((canvas) => {
|
|
if (!(canvas instanceof HTMLCanvasElement) || canvas.width === 0 || canvas.height === 0) {
|
|
return { nonBlank: false, reason: "canvas has no drawable dimensions" };
|
|
}
|
|
const gl = canvas.getContext("webgl2");
|
|
if (gl) {
|
|
const width = Math.min(12, canvas.width);
|
|
const height = Math.min(12, canvas.height);
|
|
const pixels = new Uint8Array(width * height * 4);
|
|
gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
|
|
return {
|
|
nonBlank: pixels.some((value) => value !== 0),
|
|
reason: "webgl2 sample",
|
|
};
|
|
}
|
|
const context = canvas.getContext("2d");
|
|
if (!context) return { nonBlank: false, reason: "no readable canvas context" };
|
|
const width = Math.min(64, canvas.width);
|
|
const height = Math.min(64, canvas.height);
|
|
const centerX = Math.max(0, Math.floor(canvas.width / 2 - width / 2));
|
|
const centerY = Math.max(0, Math.floor(canvas.height / 2 - height / 2));
|
|
const pixels = context.getImageData(centerX, centerY, width, height).data;
|
|
return {
|
|
nonBlank: Array.from(pixels).some((value) => value !== 0),
|
|
reason: "2d sample",
|
|
};
|
|
});
|
|
}
|
|
|
|
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.");
|
|
}
|