import { expect, test } from "@playwright/test" import { api, apiPage, createDashboard, createFlow, deleteAll, } from "./utils/api" /** * A round dashboard's ring publishes once a turn settles, not per notch. * * A knob is turned in bursts, and a lamp or a thermostat fed a value per * detent stutters through every one of them. So the ring moves at once and * sends the value it came to rest on, once — which is what two wheel notches * in quick succession must come to. And a round dashboard is one face at a * time, paged through without touching the ring. */ const flowName = `test_ring_${Date.now().toString(36)}` const dashboard = `${flowName}_d` const target = `${flowName}.level` test.use({ storageState: "playwright/.auth/user.json" }) test.describe.configure({ mode: "serial" }) test.beforeAll(async ({ browser }) => { const page = await apiPage(browser) // One node that only declares: a publish lands without waking a run. await createFlow(page, { name: flowName, title: "Ring", version: 1, nodes: [ { id: "emit", type: "python", provides: [{ name: "level", dtype: "float" }], }, ], }) const flow = await (await api(page, `/flows/${flowName}?draft=true`)).json() await api(page, `/flows/${flowName}/publish`, { method: "POST", data: { version: flow.definition.version }, }) await api(page, `/dashboards/${dashboard}`, { method: "DELETE" }) const made = await createDashboard(page, { name: dashboard, title: "Ring", version: 0, canvas_width: 360, canvas_height: 360, shape: "round", widgets: [ { id: "level", type: "slider", title: "Level", config: { target, dtype: "float", min: 0, max: 10, step: 0.5 }, }, { id: "note", type: "markdown", title: "Note", config: { content: "The second face" }, }, ], }) if (!made.ok()) throw new Error(`dashboard PUT ${made.status()}: ${await made.text()}`) await api(page, `/dashboards/${dashboard}/publish`, { method: "POST", data: { version: (await made.json()).version }, }) await page.close() }) test.afterAll(async ({ browser }) => { await deleteAll(browser, [`/dashboards/${dashboard}`, `/flows/${flowName}`]) }) test("two wheel notches publish once, two steps on", async ({ page }) => { const sent: unknown[] = [] page.on("request", (request) => { if ( request.method() === "POST" && request.url().endsWith(`/messages/${target}`) ) sent.push(request.postDataJSON()?.value) }) await page.goto(`/view/${dashboard}`) const ring = page.getByRole("slider", { name: "Level" }) await ring.waitFor({ timeout: 20000 }) const box = (await ring.boundingBox())! await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2) // Away from you is clockwise, up the scale. await page.mouse.wheel(0, -100) await page.mouse.wheel(0, -100) // The value moves at once; the publish waits for the turn to settle. await expect(page.getByTestId("face-centre")).toHaveText("1.0") await expect.poll(() => sent, { timeout: 5000 }).toEqual([1]) // Settled: nothing follows the one publish. await page.waitForTimeout(1000) expect(sent).toEqual([1]) // Paging is not the ring's arrows: the next face, the ring untouched. await page.keyboard.press("PageDown") await expect(page.getByText("The second face")).toBeVisible() }) test("with reduced motion a notch moves the handle at once", async ({ page, }) => { await page.emulateMedia({ reducedMotion: "reduce" }) await page.goto(`/view/${dashboard}`) const ring = page.getByRole("slider", { name: "Level" }) await ring.waitFor({ timeout: 20000 }) const box = (await ring.boundingBox())! await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2) const before = await ring.getAttribute("aria-valuenow") await page.mouse.wheel(0, -100) await expect(ring).not.toHaveAttribute("aria-valuenow", before ?? "") // Two frames on, where a spring would still be on its way. const cx = () => page.evaluate( () => new Promise((done) => requestAnimationFrame(() => requestAnimationFrame(() => done( document .querySelector(".dui-ring-handle") ?.getAttribute("cx") ?? null, ), ), ), ), ) const first = await cx() await page.waitForTimeout(600) expect(await cx()).toBe(first) })