Draw the panel as a panel
Five things a wall panel showed that a wall panel should not. **A tile's body no longer clips.** It scrolled, and a box that scrolls also cuts whatever crosses its edge — which took the glow off a lit button at exactly the width where the button filled its tile, and off a gauge's arc at exactly the height where the dial filled its own. Only what is written or listed asks for a scroller now; everything else is a picture drawn to fit, and what overflows is left to the frame, which clips at the tile's edge where a shadow has already faded out. The slider's phantom scrollbar goes with it. **The selector is a selector.** Named for what it does rather than what it is, and the choice it is holding is held in the dashboard's own primary — a pill that slides between the options rather than a grey one that had to be looked for. The stored type is untouched, so no document changes meaning. **The arrangement is held off the panel's edges**, by the same distance it holds between two widgets. The ground is not held off with it: a background covers the whole panel, and only what is arranged on it has a margin. No stored panel loses a row to it. **The rail is drawn on the panel.** It was chrome bolted to the edge of the screen beside the canvas — in the app's own design rather than the dashboard's, and on a scaled canvas not even lined up with it. It now takes a column out of the canvas the way the margin does, scaled with it and wearing its look. Which cell each widget sits in is unchanged; only how big a cell is. Two of these were the same mistake twice: an unlayered rule stating `position` for everything wearing a class — `.gl-surface` on a rail placed by a utility, and a blanket lift over every child of a pressable on a pill placed by `layoutId`. Both now say it one element at a time.
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
import { expect, test } from "@playwright/test"
|
||||
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.
|
||||
*/
|
||||
|
||||
const flowName = `test_panel_${Date.now().toString(36)}`
|
||||
const first = `${flowName}_a`
|
||||
const second = `${flowName}_b`
|
||||
|
||||
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.pages[0].sections[0].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 },
|
||||
})
|
||||
}
|
||||
|
||||
await api(page, "/panels/", {
|
||||
method: "PUT",
|
||||
data: {
|
||||
panels: [{ id: flowName, title: "Hall", dashboards: [first, second] }],
|
||||
},
|
||||
})
|
||||
await page.close()
|
||||
})
|
||||
|
||||
test.afterAll(async ({ browser }) => {
|
||||
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)
|
||||
|
||||
// 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",
|
||||
)
|
||||
})
|
||||
@@ -374,6 +374,30 @@ test("a reading is written to its own precision while it is moving", async ({
|
||||
await publish(page, w("setpoint"), 21.5)
|
||||
})
|
||||
|
||||
test("the arrangement is held off the panel's edges", async ({ page }) => {
|
||||
await openPanel(page)
|
||||
|
||||
const canvas = (await page.getByTestId("canvas-surface").boundingBox())!
|
||||
const tiles = await page.getByTestId("widget-frame").all()
|
||||
const boxes = await Promise.all(tiles.map((tile) => tile.boundingBox()))
|
||||
|
||||
// A tile sits as far from the edge of the screen as it does from its
|
||||
// neighbour. The scale is whatever fits the viewport, so the margin is
|
||||
// asserted as "some room" rather than a pixel count.
|
||||
for (const box of boxes) {
|
||||
expect(box!.x, "a tile is flush against the left edge").toBeGreaterThan(
|
||||
canvas.x + 1,
|
||||
)
|
||||
expect(box!.y, "a tile is flush against the top edge").toBeGreaterThan(
|
||||
canvas.y + 1,
|
||||
)
|
||||
expect(
|
||||
box!.x + box!.width,
|
||||
"a tile is flush against the right edge",
|
||||
).toBeLessThan(canvas.x + canvas.width - 1)
|
||||
}
|
||||
})
|
||||
|
||||
test("a widget is drawn inside its tile rather than scrolled", async ({
|
||||
page,
|
||||
}) => {
|
||||
@@ -647,6 +671,13 @@ for (const scheme of ["light", "dark"] as const) {
|
||||
await expect(surface).toHaveAttribute("data-palette", "")
|
||||
await expect(page.getByTestId("canvas-ground")).toBeVisible()
|
||||
|
||||
// The margin holds the arrangement off the edges; it does not hold the
|
||||
// ground off with it, because a background covers the whole panel.
|
||||
const canvas = (await surface.boundingBox())!
|
||||
const ground = (await page.getByTestId("canvas-ground").boundingBox())!
|
||||
expect(Math.round(ground.width)).toBe(Math.round(canvas.width))
|
||||
expect(Math.round(ground.height)).toBe(Math.round(canvas.height))
|
||||
|
||||
const primary = await page
|
||||
.getByTestId("bar-fill")
|
||||
.first()
|
||||
|
||||
Reference in New Issue
Block a user