import { expect, test } from "@playwright/test" import type { PanelDef } from "../src/client" import { api, apiPage, deleteAll } from "./utils/api" /** * A panel carrying more than one dashboard draws the way between them. * * The rail is part of what the screen shows rather than chrome beside it: it * is inside the canvas, scaled with it, and wearing the dashboard's own look. * Drawn outside, it was a strip of the app's design bolted to the edge of * somebody's wall panel — and on a scaled canvas it did not even line up. * * It is also as tall as what it carries. Stretched end to end, a panel with * two dashboards showed a pill nine tenths empty. */ const flowName = `test_panel_${Date.now().toString(36)}` const first = `${flowName}_a` const second = `${flowName}_b` /** The instance's own panels, put back by the teardown. */ let panels: PanelDef[] | null = null test.use({ storageState: "playwright/.auth/user.json" }) test.describe.configure({ mode: "serial" }) test.beforeAll(async ({ browser }) => { const page = await apiPage(browser) await api(page, `/flows/${flowName}`, { method: "PUT", data: { name: flowName, title: "Panel", version: 1, nodes: [ { id: "emit", type: "python", provides: [{ name: "level", dtype: "float" }], }, ], }, }) const flow = await (await api(page, `/flows/${flowName}?draft=true`)).json() await api(page, `/flows/${flowName}/publish`, { method: "POST", data: { version: flow.definition.version }, }) await api(page, `/messages/${flowName}.level`, { method: "POST", data: { value: 4 }, }) for (const name of [first, second]) { await api(page, `/dashboards/${name}`, { method: "POST" }) const doc = await (await api(page, `/dashboards/${name}?draft=true`)).json() doc.settings = { look: { value: "glass" } } doc.widgets = [ { id: "stat", type: "stat", title: "Level", layout: { lg: { x: 0, y: 0, w: 3, h: 2 } }, config: { message: `${flowName}.level`, dtype: "float" }, }, ] const put = await ( await api(page, `/dashboards/${name}`, { method: "PUT", data: doc }) ).json() await api(page, `/dashboards/${name}/publish`, { method: "POST", data: { version: put.version }, }) } // Saving is a whole-list replace, and a panel that disappears takes the // credential of the screen hanging on it. So append to what is there, and // keep the list for the teardown to put back. const config = await (await api(page, "/panels/")).json() const existing: PanelDef[] = (config.panels ?? []).filter( (p: PanelDef) => p.id !== flowName, ) panels = existing await api(page, "/panels/", { method: "PUT", data: { panels: [ ...existing, { id: flowName, title: "Hall", dashboards: [first, second] }, ], }, }) await page.close() }) test.afterAll(async ({ browser }) => { // Restoring the list is also what removes this spec's own panel. if (panels) { const page = await apiPage(browser) await api(page, "/panels/", { method: "PUT", data: { panels } }) await page.close() } await deleteAll(browser, [ `/dashboards/${first}`, `/dashboards/${second}`, `/flows/${flowName}`, ]) }) test("the rail is drawn on the panel, in the panel's own look", async ({ page, }) => { await page.goto(`/panel/${flowName}?d=${first}`) const rail = page.getByTestId("panel-rail") await rail.waitFor({ timeout: 20000 }) const canvas = (await page.getByTestId("canvas-surface").boundingBox())! const box = (await rail.boundingBox())! expect(box.x, "the rail starts left of the panel").toBeGreaterThanOrEqual( canvas.x - 1, ) expect(box.y, "the rail starts above the panel").toBeGreaterThanOrEqual( canvas.y - 1, ) expect( box.x + box.width, "the rail runs off the right of the panel", ).toBeLessThanOrEqual(canvas.x + canvas.width + 1) expect( box.y + box.height, "the rail runs off the bottom of the panel", ).toBeLessThanOrEqual(canvas.y + canvas.height + 1) // Inside the canvas is also what makes it wear the look: the attributes and // the palette are stated on the canvas, and inheritance does the rest. const inside = await rail.evaluate( (el) => el.closest("[data-testid=canvas-surface]") !== null, ) expect(inside, "the rail is drawn outside the panel it belongs to").toBe(true) // As tall as what it carries rather than as tall as the panel: a rail of // two stretched end to end is mostly empty pill. expect( box.height, "the rail is stretched to the height of the panel", ).toBeLessThan(canvas.height / 2) const centres = Math.abs( box.y + box.height / 2 - (canvas.y + canvas.height / 2), ) expect(centres, "the rail is not centred in its column").toBeLessThan(2) // And the arrangement keeps clear of it rather than sitting under it. const tile = (await page.getByTestId("widget-frame").first().boundingBox())! expect(tile.x, "a widget is drawn under the rail").toBeGreaterThanOrEqual( box.x + box.width, ) }) test("the rail switches the panel between its dashboards", async ({ page }) => { await page.goto(`/panel/${flowName}?d=${first}`) await page.getByTestId(`panel-rail-${second}`).click() await expect(page).toHaveURL(new RegExp(`d=${second}`)) await expect(page.getByTestId(`panel-rail-${second}`)).toHaveAttribute( "aria-current", "page", ) })