Files
app/frontend/tests/persistence.spec.ts
T
stroblmeandClaude Opus 5 7600aaf7ea Guard what a dashboard save does not change
`PUT /dashboards/{name}` had no backend test: three cover what it promises —
a first draft for a name nobody has used (200, not the 404 that came off in
4a2337f), an update that keeps everything the edit did not name, and a save
based on a version someone moved past.

The Playwright half is the same property through the editor. Pages and
sections are gone since 7ff29ca, so what the editor never draws — a widget's
md/sm placements and the dashboard-wide settings — is what a save has to
carry, and dragging one widget is what the spec makes it carry it through.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K1moruzue2kTJd3uVisgNk
2026-08-28 12:59:08 +02:00

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" },
touch: { 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")
})