Files
app/frontend/tests/editor.spec.ts
T

189 lines
5.9 KiB
TypeScript

import { expect, type Page, test } from "@playwright/test"
import { api, apiPage, deleteAll } from "./utils/api"
/**
* The dashboard editor's quiet failures.
*
* Two 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.
*
* The third is what a widget drawn without its title is dragged by, since the
* header it used to be dragged by is the thing that is gone.
*/
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.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.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()
})
test("a widget without its title can still be dragged and picked", async ({
page,
}) => {
await openEditor(page)
const frame = page.getByTestId("widget-frame").filter({ hasText: "Below" })
const settings = page.getByTestId("widget-settings")
await frame.click()
await expect(settings).toBeVisible()
await page.getByTestId("widget-show-title").click()
// The title is gone from the tile — and so is the header that used to be the
// handle, so the editor grows one of its own rather than losing the widget.
const untitled = page
.getByTestId("widget-frame")
.filter({ hasNotText: "Top" })
await expect(untitled).not.toContainText("Below")
await expect(untitled.locator(".widget-grip")).toBeVisible()
await untitled.locator(".widget-grip").click()
// A press on the grip is a move, not a pick — as it is with a header.
await expect(page.getByTestId("widget-settings")).toBeVisible()
})
test("a palette is pasted as a link and read as roles", async ({ page }) => {
await openEditor(page)
await page.getByTestId("edit-dashboard").click()
await page
.getByTestId("dashboard-palette-input")
.fill("https://coolors.co/palette/264653-2a9d8f-e9c46a-f4a261-e76f51")
const swatches = page.getByTestId("dashboard-palette").getByRole("img")
await expect(swatches).toHaveCount(5)
await expect(swatches.first()).toHaveAttribute("aria-label", /Background/)
// The roles are positional, so turning the list is how a palette that
// arrived in the wrong order is put right.
await page.getByTestId("dashboard-palette-rotate").click()
await expect(swatches.first()).toHaveAttribute(
"aria-label",
/Background #2a9d8f/,
)
// And the canvas beside the panel wears it.
await expect(page.getByTestId("canvas-surface")).toHaveAttribute(
"data-palette",
"",
)
})