/** * The house panels, at the size of the screen they hang on. * * `/view/{name}` is the wall-panel route: no sidebar, no editor. Shooting it * at exactly the panel's pixels is the only way to see whether a tile fell off * the bottom, because a panel does not scroll. * * Env: APP_URL, FIRST_SUPERUSER, FIRST_SUPERUSER_PASSWORD, * PANELS (comma-separated), PANEL_SIZE ("1280x800"), SCREENSHOT_DIR */ import { mkdir } from "node:fs/promises" import { chromium } from "@playwright/test" const APP_URL = process.env.APP_URL || "http://app.localhost" const EMAIL = process.env.FIRST_SUPERUSER const PASSWORD = process.env.FIRST_SUPERUSER_PASSWORD const OUT = process.env.SCREENSHOT_DIR || "screenshots/panels" const NAMES = (process.env.PANELS || "home,comfort,energy").split(",") const [width, height] = (process.env.PANEL_SIZE || "1280x800") .split("x") .map(Number) if (!EMAIL || !PASSWORD) { console.error("FIRST_SUPERUSER / FIRST_SUPERUSER_PASSWORD are unset.") process.exit(1) } /** What the bundle asks for, and where it is really answered. */ const API_PUBLIC = process.env.API_PUBLIC || "https://api.localhost" const API_ORIGIN = process.env.API_ORIGIN || "" /** Where the bundle is really served from, for fetches outside the browser. */ const APP_ORIGIN = process.env.APP_ORIGIN || "" const RESOLVER = process.env.HOST_RESOLVER_RULES const browser = await chromium.launch( RESOLVER ? { args: [`--host-resolver-rules=${RESOLVER}`] } : {}, ) for (const theme of ["light", "dark"]) { const dir = `${OUT}/${theme}` await mkdir(dir, { recursive: true }) const context = await browser.newContext({ viewport: { width, height }, colorScheme: theme, // A panel is a touch screen, and several controls draw a taller target // for one. Shooting it as a mouse would misreport the layout. hasTouch: true, }) await context.addInitScript((t) => { localStorage.setItem("fluksio-ui-theme", t) }, theme) // The bundle addresses the API at whatever origin it was built for, and // this stack has no TLS in front of it — so the calls are re-issued at the // address the container actually answers on. API_ORIGIN is that address; // without it nothing is intercepted and the page is left alone. if (API_ORIGIN) { // The origin is baked into the bundle at build time, and this stack has no // TLS in front of it. Rewriting it in the JavaScript as it is served fixes // the websocket too — routing only the HTTP calls would leave every widget // showing an em dash, since live values arrive over the socket. const wsPublic = API_PUBLIC.replace(/^http/, "ws") const wsOrigin = API_ORIGIN.replace(/^http/, "ws") await context.route("**/*.js", async (route) => { // route.fetch runs in Playwright's own process, which does not have the // browser's resolver rules — so it is told the address directly. const response = await route.fetch( APP_ORIGIN ? { url: route.request().url().replace(APP_URL, APP_ORIGIN) } : {}, ) const body = (await response.text()) .split(API_PUBLIC) .join(API_ORIGIN) .split(wsPublic) .join(wsOrigin) await route.fulfill({ response, body }) }) } const page = await context.newPage() page.on("console", (m) => { if (m.type() === "error") console.log(` console: ${m.text()}`) }) page.on("requestfailed", (r) => console.log(` failed: ${r.url()} ${r.failure()?.errorText}`), ) await page.goto(`${APP_URL}/login`, { waitUntil: "networkidle" }) await page.getByTestId("email-input").fill(EMAIL) await page.getByTestId("password-input").fill(PASSWORD) await page.getByRole("button", { name: /log in/i }).click() await page.waitForURL(`${APP_URL}/`, { timeout: 20000 }) for (const name of NAMES) { await page.goto(`${APP_URL}/view/${name}`, { waitUntil: "networkidle" }) // Widgets fetch their own values; the chart draws after its answer lands. await page.waitForTimeout(3000) await page.screenshot({ path: `${dir}/${name}.png` }) const overflow = await page.evaluate(() => ({ scrollH: document.documentElement.scrollHeight, clientH: document.documentElement.clientHeight, scrollW: document.documentElement.scrollWidth, clientW: document.documentElement.clientWidth, })) const spills = overflow.scrollH > overflow.clientH + 1 || overflow.scrollW > overflow.clientW + 1 console.log( ` ${theme}/${name}: ${spills ? "SPILLS " : "fits "} ` + `${overflow.scrollW}x${overflow.scrollH} in ${overflow.clientW}x${overflow.clientH}`, ) } await context.close() } await browser.close()