Saving panels replaces the whole list, so the spec's setup deleted every panel it had not created — and with it the credential of the screen on it. It now appends to the stored list and puts the original back in teardown, which also removes its own panel. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013Gf7WaExcJ9bs3kfJXB3nK
167 lines
5.4 KiB
TypeScript
167 lines
5.4 KiB
TypeScript
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 installation'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",
|
|
)
|
|
})
|