Let the slider keep the drag the browser wanted to take
Docs / docs (push) Successful in 22s
Playwright Tests / test-playwright (1, 2) (push) Failing after 2m38s
Playwright Tests / test-playwright (2, 2) (push) Failing after 1m41s
pre-commit / pre-commit (push) Failing after 2m47s
Test Backend / test-backend (push) Successful in 2m19s
Compose Smoke Test / test-compose (push) Successful in 32s
Playwright Tests / merge-reports (push) Failing after 1m4s

Dragging the brightness slider on the wall panel set the handle and
published nothing; tapping a point on the track worked. The control is a
native range input laid transparent over the drawn track, and only the
release publishes — but the input never said the drag was its own. On a
touch panel a sideways swipe is a pan, or a back-navigation, so the
browser took the pointer over mid-drag and ended it in `pointercancel`.
The value had followed the finger and was never sent.

`touch-action: none`, as the colour disk beside it has always had. The
release also answers `pointercancel` and `lostpointercapture` now, which
covers a mouse let go outside the input and leaves no way for a draft to
sit there unpublished.

Two checks: a drag across the track reaches the engine, and the input
still owns its gesture. The second fails against a build without the
CSS, which is what makes it worth having.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012n6CehUsHYYUXJ48rxaD18
This commit is contained in:
2026-08-26 13:32:22 +02:00
co-authored by Claude Opus 5
parent 95aae7b95f
commit bda67d1c0d
3 changed files with 158 additions and 0 deletions
+144
View File
@@ -0,0 +1,144 @@
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")
})