Check the colour disc's gesture: one publish, on release
The wheel's arithmetic is checked in color.check.ts; nothing checked what a drag across it sends. A new Playwright spec drags from the centre out to three o'clock, counts the panel's publishes off the wire, and holds the disc to one message at the release rather than one per pixel. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K1moruzue2kTJd3uVisgNk
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
import { expect, test } from "@playwright/test"
|
||||
|
||||
import { api, apiPage, deleteAll } from "./utils/api"
|
||||
|
||||
/**
|
||||
* A colour disc sends once, at the end of the gesture.
|
||||
*
|
||||
* Hue is the angle and saturation the radius, so setting a colour is one drag
|
||||
* across one picture — and the disc follows the pointer from the moment it
|
||||
* goes down. Only the release publishes: a value per pixel would flood
|
||||
* whatever is listening, and on a lamp it would strobe it.
|
||||
*
|
||||
* The arithmetic behind the wheel is checked in `color.check.ts`, which needs
|
||||
* no browser. What needs one is the gesture: how many messages a drag sends,
|
||||
* and whether the one it does send is where the pointer was let go.
|
||||
*/
|
||||
const flowName = `test_color_${Date.now().toString(36)}`
|
||||
const dashboard = `${flowName}_d`
|
||||
const target = `${flowName}.tint`
|
||||
|
||||
test.use({ storageState: "playwright/.auth/user.json" })
|
||||
|
||||
test.beforeAll(async ({ browser }) => {
|
||||
const page = await apiPage(browser)
|
||||
// A node that only declares: nothing consumes the message, so a publish
|
||||
// reaches the graph without waking an engine run. The widget is what is
|
||||
// under test, not a flow.
|
||||
const madeFlow = await api(page, `/flows/${flowName}`, {
|
||||
method: "PUT",
|
||||
data: {
|
||||
name: flowName,
|
||||
title: "Colour drag",
|
||||
version: 1,
|
||||
nodes: [
|
||||
{
|
||||
id: "lamp",
|
||||
type: "python",
|
||||
provides: [{ name: "tint", dtype: "list", item: "float" }],
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
if (!madeFlow.ok())
|
||||
throw new Error(`flow PUT ${madeFlow.status()}: ${await madeFlow.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 },
|
||||
})
|
||||
|
||||
// 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: "Colour drag",
|
||||
version: 0,
|
||||
widgets: [
|
||||
{
|
||||
id: "tint",
|
||||
type: "color",
|
||||
title: "Tint",
|
||||
layout: { lg: { x: 0, y: 0, w: 6, h: 4 } },
|
||||
config: { target, dtype: "list", format: "hsv" },
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
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()}`)
|
||||
await page.close()
|
||||
})
|
||||
|
||||
test.afterAll(async ({ browser }) => {
|
||||
await deleteAll(browser, [`/dashboards/${dashboard}`, `/flows/${flowName}`])
|
||||
})
|
||||
|
||||
test("a dragged disc publishes once, where it was let go", async ({ page }) => {
|
||||
await page.goto(`/view/${dashboard}`)
|
||||
const disc = page.getByTestId("color-wheel")
|
||||
await disc.waitFor({ timeout: 20000 })
|
||||
// A tile fades and scales in, so where the disc is drawn is only settled
|
||||
// once that has finished — and the gesture below is aimed in pixels.
|
||||
await page.waitForFunction(
|
||||
() =>
|
||||
[...document.querySelectorAll(".widget-cell")].every(
|
||||
(cell) => Number(getComputedStyle(cell).opacity) > 0.99,
|
||||
),
|
||||
undefined,
|
||||
{ timeout: 15000 },
|
||||
)
|
||||
|
||||
// Every publish this panel makes, as the browser sends it. The setup above
|
||||
// talks to the API out of band, so nothing but the widget is counted here.
|
||||
const sent: [number, number, number][] = []
|
||||
page.on("request", (request) => {
|
||||
if (
|
||||
request.method() === "POST" &&
|
||||
request.url().includes(`/messages/${target}`)
|
||||
)
|
||||
sent.push(request.postDataJSON()?.value)
|
||||
})
|
||||
|
||||
const box = (await disc.boundingBox())!
|
||||
const middle = { x: box.x + box.width / 2, y: box.y + box.height / 2 }
|
||||
|
||||
// Down in the white centre and out towards three o'clock, which the wheel's
|
||||
// own frame — zero degrees at twelve, running clockwise — reads as hue 90.
|
||||
await page.mouse.move(middle.x, middle.y)
|
||||
await page.mouse.down()
|
||||
for (let step = 1; step <= 8; step++) {
|
||||
await page.mouse.move(middle.x + (box.width * 0.4 * step) / 8, middle.y, {
|
||||
steps: 2,
|
||||
})
|
||||
}
|
||||
// Long enough that a publish made mid-drag would have been seen by now.
|
||||
await page.waitForTimeout(500)
|
||||
expect(sent, "the disc published while it was still being dragged").toEqual(
|
||||
[],
|
||||
)
|
||||
|
||||
await page.mouse.up()
|
||||
await expect
|
||||
.poll(() => sent.length, {
|
||||
timeout: 10000,
|
||||
message: "letting go of the disc published nothing",
|
||||
})
|
||||
.toBe(1)
|
||||
|
||||
// And it carries the colour the gesture ended on rather than the one it
|
||||
// started from — otherwise a disc that ignored the drag would pass.
|
||||
// Loosely: a few degrees of pixel rounding is not the point, and the
|
||||
// colour it started on — white, hue 0 — is nowhere near either bound.
|
||||
const [hue, saturation] = sent[0]
|
||||
expect(
|
||||
Math.abs(hue - 90),
|
||||
`let go at three o'clock, published hue ${hue}`,
|
||||
).toBeLessThan(10)
|
||||
expect(
|
||||
saturation,
|
||||
`let go four fifths out, published saturation ${saturation}`,
|
||||
).toBeGreaterThan(60)
|
||||
|
||||
// Nothing follows the release either: one gesture, one message.
|
||||
await page.waitForTimeout(1000)
|
||||
expect(sent, "the release published more than once").toHaveLength(1)
|
||||
|
||||
// And that one message reached the graph rather than only the wire.
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
const rows = await (await api(page, "/messages/")).json()
|
||||
return (rows.data ?? rows).find(
|
||||
(message: { name: string }) => message.name === target,
|
||||
)?.value
|
||||
},
|
||||
{ timeout: 10000, message: "the release never reached the engine" },
|
||||
)
|
||||
.toEqual(sent[0])
|
||||
})
|
||||
Reference in New Issue
Block a user