189 lines
5.9 KiB
TypeScript
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",
|
|
"",
|
|
)
|
|
})
|