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

147 lines
4.6 KiB
TypeScript

import { expect, test } from "@playwright/test"
import { api, apiPage, 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 api(page, `/flows/${flowName}`, {
method: "PUT",
data: {
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 api(page, `/dashboards/${dashboard}`, {
method: "PUT",
data: {
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<string | null>((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)
})