Follows the portal: the noun is "instance" everywhere the app says it — UI strings, CLI output, error details, docs and comments. The wire keys (`instance_id`, `instance_token`) and the hub route this calls move with it. An existing cloud.json is adopted rather than refused: without the key alias the dataclass fails to parse, which the caller swallows and reads as "never enrolled" instead of "reconnect". `instance_key` on a node type becomes `target_key`. It means the outside thing a node points at, which is a different sense of the word, and keeping both would put two meanings of "instance" in one codebase. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015YrQnKV3bnQd4K342y8tKj
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 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",
|
|
)
|
|
})
|