import { expect, test } from "@playwright/test" import { api, apiPage, deleteAll } from "./utils/api" /** * A slider publishes what a *drag* leaves it on, not only what a tap picks. * * The control is a native range input laid transparent over the drawn track, * and only the release publishes — so every way a drag can end has to reach * the release. On a touch panel the browser will happily read a sideways swipe * as a pan and take the pointer over mid-drag, which ends the drag in * `pointercancel`; the value then followed the finger and was never sent. * `touch-action: none` is what stops that, and this is what notices if it goes. */ const flowName = `test_slider_${Date.now().toString(36)}` const dashboard = `${flowName}_d` test.use({ hasTouch: true, storageState: "playwright/.auth/user.json" }) test.describe.configure({ mode: "serial" }) test.beforeAll(async ({ browser }) => { const page = await apiPage(browser) const madeFlow = await api(page, `/flows/${flowName}`, { method: "PUT", data: { name: flowName, title: "Slider drag", version: 1, nodes: [ { id: "level", type: "python", title: "Level", requires: [{ name: "level", dtype: "float" }], provides: [{ name: "echo", dtype: "float" }], }, ], inputs: [{ spec: { name: "level", dtype: "float" }, initial: 0 }], }, }) if (!madeFlow.ok()) throw new Error(`flow PUT ${madeFlow.status()}: ${await madeFlow.text()}`) await api(page, `/flows/${flowName}/nodes/level/source`, { method: "PUT", data: { code: "def process(level=0.0):\n return {'echo': level}\n" }, }) // A leftover from a run that failed before its teardown would answer the // create with a version conflict. await api(page, `/dashboards/${dashboard}`, { method: "DELETE" }) const madeBoard = await api(page, `/dashboards/${dashboard}`, { method: "PUT", data: { name: dashboard, title: "Slider drag", version: 0, widgets: [ { id: "level", type: "slider", title: "Level", layout: { lg: { x: 0, y: 0, w: 6, h: 3 } }, config: { target: `${flowName}.level`, dtype: "float", min: 0, max: 100, step: 1, }, }, ], }, }) if (!madeBoard.ok()) throw new Error( `dashboard PUT ${madeBoard.status()}: ${await madeBoard.text()}`, ) const board = await madeBoard.json() const shown = await api(page, `/dashboards/${dashboard}/publish`, { method: "POST", data: { version: board.version }, }) if (!shown.ok()) throw new Error(`publish ${shown.status()}: ${await shown.text()}`) 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, `/flows/${flowName}/start`, { method: "POST" }) await page.close() }) test.afterAll(async ({ browser }) => { await deleteAll(browser, [`/dashboards/${dashboard}`, `/flows/${flowName}`]) }) test("a dragged slider publishes where it was let go", async ({ page }) => { await page.goto(`/view/${dashboard}`) const slider = page.getByRole("slider", { name: "Level" }) await slider.waitFor({ timeout: 20000 }) const box = (await slider.boundingBox())! const y = box.y + box.height / 2 // A touch drag across the track, ending on the far side — the gesture a // browser is most willing to mistake for a pan. await page.touchscreen.tap(box.x + 4, y) await page.mouse.move(box.x + 4, y) await page.mouse.down() for (let step = 1; step <= 8; step++) { await page.mouse.move(box.x + (box.width * step) / 10, y, { steps: 2 }) } await page.mouse.up() // What the flow echoed back is what actually reached the engine. await expect .poll( async () => { const rows = await (await api(page, "/messages/")).json() const echo = (rows.data ?? rows).find( (m: { name: string }) => m.name === `${flowName}.echo`, ) return echo?.value ?? 0 }, { timeout: 15000, message: "the drag never reached the engine" }, ) .toBeGreaterThan(50) await page.mouse.up() }) test("the slider owns its own drag gesture", async ({ page }) => { await page.goto(`/view/${dashboard}`) const slider = page.getByRole("slider", { name: "Level" }) await slider.waitFor({ timeout: 20000 }) // The cure for the reported fault, asserted where it can be seen. Without // it a browser is free to read a sideways swipe on the track as a pan or a // back-navigation, take the pointer over mid-drag and end the drag in // `pointercancel` — the handle followed the finger and nothing was ever // published. A drag *is* this control; no gesture outranks it. await expect(slider).toHaveCSS("touch-action", "none") })