117 lines
4.6 KiB
JavaScript
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()
|