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 }[] } } 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") })