Docs / docs (push) Successful in 22s
Playwright Tests / test-playwright (1, 2) (push) Failing after 1m9s
Playwright Tests / test-playwright (2, 2) (push) Failing after 11s
pre-commit / pre-commit (push) Failing after 1m59s
Test Backend / test-backend (push) Failing after 2m28s
Compose Smoke Test / test-compose (push) Failing after 11s
Playwright Tests / merge-reports (push) Failing after 2m19s
It described the wrong object. A dashboard is a document that may hang on a
hallway tablet and in a desk browser at the same time, and only one of those
has fingers on it — so the flag moves off `DashboardDef.settings` and onto
`PanelDef` as a plain bool, ticked in the Panels dialog. `useCanvasRoot` takes
it as an argument rather than reading the document, and `/panel/{id}` is the
only surface with a panel to ask.
Dropping the message binding with it is deliberate: nothing drove it, and a
flow deciding whether a screen has fingers on it was never the point. A stored
`settings.touch` is inert rather than migrated, which `_check_settings`
skipping unknown names already guaranteed.
The rail was the other half. It had no touch behaviour at all and its 40px
buttons met neither branch of the 44/32 rule. `[data-touch] .dui-rail{-item}`
in `ui/core/core.css` spends the padding and the gap on the buttons instead,
so they reach the 44px target and the rail comes out taller at exactly the
same width — `RAIL_INSET` never moves, and the arrangement under it does not
either.
Also closes the panels-dialog icon gap: `DashboardSummary` carries the `icon`
now, so the dialog draws each assigned dashboard's rail glyph beside its
checkbox. `initials()` went from three identical copies in the looks to one in
`Dashboard/icons.ts`, so the dialog and the rail fall back the same way.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Va7ExQDtuwKN7kNpHhWWNQ
119 lines
3.7 KiB
TypeScript
119 lines
3.7 KiB
TypeScript
import { expect, type Page, test } from "@playwright/test"
|
|
import { api, apiPage, deleteAll } from "./utils/api"
|
|
|
|
/**
|
|
* An editor save is a rewrite of the whole document, not a patch of what moved.
|
|
*
|
|
* So everything the editor never draws rides on that one request: a widget's
|
|
* placements at the narrower breakpoints, which only a panel of that width
|
|
* reads, and the dashboard-wide settings, which live in the settings panel
|
|
* rather than on the canvas. `applyLayout` keeps them by spreading the stored
|
|
* layout under the one column set it knows; a save that forgot to would lose a
|
|
* phone's arrangement with nothing on screen to show for it.
|
|
*/
|
|
|
|
const dashboardName = `test_persist_${Date.now().toString(36)}`
|
|
|
|
/** The tile the editor is told to move. */
|
|
const MOVED = {
|
|
id: "moved",
|
|
type: "clock",
|
|
title: "Moved",
|
|
layout: { lg: { x: 0, y: 0, w: 3, h: 3 } },
|
|
config: {},
|
|
}
|
|
|
|
/** The tile nothing touches. Its stored form is the assertion. */
|
|
const KEPT = {
|
|
id: "kept",
|
|
type: "stat",
|
|
title: "Kept",
|
|
layout: {
|
|
lg: { x: 8, y: 0, w: 4, h: 3 },
|
|
md: { x: 0, y: 6, w: 5, h: 3 },
|
|
sm: { x: 0, y: 9, w: 2, h: 2 },
|
|
},
|
|
config: { message: "house.kept", dtype: "float", unit: "°C" },
|
|
}
|
|
|
|
/** Dashboard-wide, and nowhere on the canvas the drag happens on. */
|
|
const SETTINGS = {
|
|
theme: { value: "dark", message: "", dtype: "str" },
|
|
locked: { value: true, message: "", dtype: "bool" },
|
|
}
|
|
|
|
test.use({ storageState: "playwright/.auth/user.json" })
|
|
|
|
test.describe.configure({ mode: "serial" })
|
|
|
|
test.beforeAll(async ({ browser }) => {
|
|
const page = await apiPage(browser)
|
|
// Version 0 creates: a first draft is what a save of a name nobody has used
|
|
// yet means, and the editor reads the draft.
|
|
const made = await api(page, `/dashboards/${dashboardName}`, {
|
|
method: "PUT",
|
|
data: {
|
|
name: dashboardName,
|
|
title: "Persistence",
|
|
icon: "layout-dashboard",
|
|
columns: 12,
|
|
canvas_width: 1920,
|
|
canvas_height: 1080,
|
|
version: 0,
|
|
widgets: [MOVED, KEPT],
|
|
settings: SETTINGS,
|
|
},
|
|
})
|
|
if (!made.ok())
|
|
throw new Error(`dashboard PUT ${made.status()}: ${await made.text()}`)
|
|
await page.close()
|
|
})
|
|
|
|
test.afterAll(async ({ browser }) => {
|
|
await deleteAll(browser, [`/dashboards/${dashboardName}`])
|
|
})
|
|
|
|
/** The draft as stored — what a panel would be shown once it is published. */
|
|
async function stored(page: Page) {
|
|
const response = await api(page, `/dashboards/${dashboardName}?draft=true`)
|
|
return (await response.json()) as {
|
|
icon: string
|
|
settings: unknown
|
|
widgets: { id: string; layout: Record<string, unknown> }[]
|
|
}
|
|
}
|
|
|
|
test("moving one widget leaves the rest of the document alone", async ({
|
|
page,
|
|
}) => {
|
|
await page.goto(`/dashboards/${dashboardName}?edit=true`)
|
|
const moved = page.getByTestId("widget-frame").filter({ hasText: "Moved" })
|
|
await moved.waitFor({ state: "visible", timeout: 15000 })
|
|
|
|
// Drag it a tile's width to the right, which is clear of `kept` at column 8.
|
|
const box = (await moved.boundingBox())!
|
|
await moved.locator(".widget-grip").hover()
|
|
await page.mouse.down()
|
|
await page.mouse.move(box.x + box.width, box.y + box.height / 4, {
|
|
steps: 10,
|
|
})
|
|
await page.mouse.up()
|
|
|
|
// The save is debounced, so wait for the store rather than for the canvas.
|
|
await expect
|
|
.poll(
|
|
async () =>
|
|
(
|
|
(await stored(page)).widgets.find((w) => w.id === "moved")?.layout
|
|
.lg as { x: number }
|
|
)?.x,
|
|
{ message: "the drag was never saved", timeout: 10000 },
|
|
)
|
|
.toBeGreaterThan(0)
|
|
|
|
const after = await stored(page)
|
|
expect(after.widgets.find((w) => w.id === "kept")).toEqual(KEPT)
|
|
expect(after.settings).toEqual(SETTINGS)
|
|
expect(after.icon).toBe("layout-dashboard")
|
|
})
|