import { expect, type Page, test } from "@playwright/test" import { api, apiPage, deleteAll } from "./utils/api" /** * The dashboard editor's two quiet failures. * * Both are about a placement that nothing complains about: the grid library * corrects a stored layout against the columns alone, so shrinking the canvas * hides whatever now falls past its bottom edge; and the header that drags a * widget used to also select it, so every drop opened the settings panel. */ const dashboardName = `test_editor_${Date.now().toString(36)}` /** The layout the editor is opened on. `below` cannot compact above `top`. */ const WIDGETS = [ { id: "top", type: "clock", title: "Top", layout: { lg: { x: 0, y: 0, w: 8, h: 4 } }, config: {}, }, { id: "below", type: "clock", title: "Below", layout: { lg: { x: 0, y: 8, w: 4, h: 2 } }, config: {}, }, ] test.use({ storageState: "playwright/.auth/user.json" }) test.describe.configure({ mode: "serial" }) test.beforeAll(async ({ browser }) => { const page = await apiPage(browser) await api(page, `/dashboards/${dashboardName}`, { method: "POST" }) // The working copy, which is what a new dashboard may be: the editor reads // the draft, so the setup writes one rather than publishing. const dashboard = await ( await api(page, `/dashboards/${dashboardName}?draft=true`) ).json() dashboard.columns = 12 dashboard.canvas_width = 1920 dashboard.canvas_height = 1080 dashboard.pages[0].sections[0].widgets = WIDGETS await api(page, `/dashboards/${dashboardName}`, { method: "PUT", data: dashboard, }) await page.close() }) test.afterAll(async ({ browser }) => { await deleteAll(browser, [`/dashboards/${dashboardName}`]) }) /** Where each widget is stored right now, as the editor last saved it. */ async function placements(page: Page) { const doc = await ( await api(page, `/dashboards/${dashboardName}?draft=true`) ).json() const widgets = doc.pages[0].sections[0].widgets as { id: string layout: { lg: { x: number; y: number } } }[] return Object.fromEntries( widgets.map((widget) => [widget.id, widget.layout.lg]), ) } async function openEditor(page: Page) { await page.goto(`/dashboards/${dashboardName}?edit=true`) await page .getByTestId("widget-frame") .first() .waitFor({ state: "visible", timeout: 15000 }) } test("shrinking the canvas warns about what falls off it, and re-packs", async ({ page, }) => { await openEditor(page) const notice = page.getByTestId("canvas-clipped") await expect(notice).toBeHidden() // Where `below` sits before the reflow — anywhere but the top shelf. const before = (await placements(page)).below.y expect( before, "the setup layout was already packed across the columns", ).toBeGreaterThan(0) await page.getByTestId("edit-dashboard").click() // 400px is four grid rows, which the four-row `top` fills on its own. await page.getByTestId("canvas-height").fill("400") await expect(notice).toBeVisible() await expect(notice).toContainText("1 widget") await page.getByTestId("reflow-canvas").click() await expect(notice).toBeHidden() // A hidden notice on its own could just be a re-render. The stored draft is // what a panel will read, so that is what has to have moved. await expect .poll(async () => (await placements(page)).below.y, { message: "the reflow was never saved", timeout: 10000, }) .toBeLessThan(before) }) test("the drag handle moves a widget without selecting it", async ({ page, }) => { await openEditor(page) const frame = page.getByTestId("widget-frame").filter({ hasText: "Top" }) const settings = page.getByTestId("widget-settings") // A press on the grip is the start of a move; the click ending a drag must // not open the panel behind it. await frame.locator(".widget-grip").click() await expect(settings).toBeHidden() // The other half: selection still works, it just lives on the body now. await frame.click() await expect(settings).toBeVisible() })