Files
app/frontend/scripts/capture-panels.mjs
T

117 lines
4.6 KiB
JavaScript

/**
* Capture dashboards at their target screen size.
*
* `/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()