The canvas lays itself out: a layered graph, left to right on a desktop and top to bottom on a phone, with room reserved for the value each edge carries. Nodes cannot be dragged and `NodeDef.position` is gone from the document — a graph nobody can arrange is one worth keeping small, which is what keeps flows atomic. Endpoints join the same layout, so their lanes and the localStorage that remembered where they were dragged go too. Mobile, per the new Responsive section of DESIGN-GUIDELINES.md: the dock caps its width and wraps instead of running off the screen, the dashboard stacks into one column rather than shrinking a wall panel to a fifth of its size, and Home stops widening its grid track past the viewport. A Playwright project at a phone's width fails the build when a screen no longer fits. Along the way: publish is the checkmark that was already there rather than a button that appears and disappears, with discard beside it on both the flow and the dashboard; the brain reveals a neuron's name on the first tap; and the port sparklines get room to breathe. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VDSXaRhvqHYNevgDGmNAto
131 lines
4.1 KiB
TypeScript
131 lines
4.1 KiB
TypeScript
import { expect, test } from "@playwright/test"
|
|
import { api, apiPage, deleteAll } from "./utils/api"
|
|
|
|
/**
|
|
* Dashboards and other flows appear on the canvas but are not part of the flow.
|
|
*
|
|
* The risk this covers: they are React Flow nodes like any other, so anything
|
|
* that writes the document — autosave, undo, delete, drag — could pick one up
|
|
* and store it. A dashboard widget written into flow.json would become a node
|
|
* the engine then tries to build.
|
|
*/
|
|
|
|
const flowName = `test_endpoint_${Date.now().toString(36)}`
|
|
const dashboardName = `${flowName}_panel`
|
|
|
|
test.use({ storageState: "playwright/.auth/user.json" })
|
|
|
|
test.describe.configure({ mode: "serial" })
|
|
|
|
test.beforeAll(async ({ browser }) => {
|
|
const page = await apiPage(browser)
|
|
|
|
// A flow reading a message, and a dashboard control that sets it.
|
|
await api(page, `/flows/${flowName}`, {
|
|
method: "PUT",
|
|
data: {
|
|
name: flowName,
|
|
nodes: [
|
|
{
|
|
id: "sink",
|
|
type: "python",
|
|
requires: [{ name: "level", port: "level", dtype: "float" }],
|
|
provides: [],
|
|
},
|
|
],
|
|
inputs: [
|
|
{ spec: { name: "level", port: "level", dtype: "float" }, initial: 0 },
|
|
],
|
|
version: 1,
|
|
},
|
|
})
|
|
const saved = await (await api(page, `/flows/${flowName}`)).json()
|
|
await api(page, `/flows/${flowName}/publish`, {
|
|
method: "POST",
|
|
data: { version: saved.definition.version },
|
|
})
|
|
|
|
await api(page, `/dashboards/${dashboardName}`, { method: "POST" })
|
|
const dashboard = await (
|
|
await api(page, `/dashboards/${dashboardName}`)
|
|
).json()
|
|
dashboard.pages[0].sections[0].widgets = [
|
|
{
|
|
id: "lever",
|
|
type: "slider",
|
|
title: "Lever",
|
|
layout: {},
|
|
config: { target: `${flowName}.level` },
|
|
},
|
|
]
|
|
const draft = await (
|
|
await api(page, `/dashboards/${dashboardName}`, {
|
|
method: "PUT",
|
|
data: dashboard,
|
|
})
|
|
).json()
|
|
// A dashboard edit is a draft, and the flow canvas draws the controls the
|
|
// panels actually carry — so the endpoint only exists once this is published,
|
|
// the same way the flow above had to be.
|
|
await api(page, `/dashboards/${dashboardName}/publish`, {
|
|
method: "POST",
|
|
data: { version: draft.version },
|
|
})
|
|
await page.close()
|
|
})
|
|
|
|
test.afterAll(async ({ browser }) => {
|
|
await deleteAll(browser, [
|
|
`/dashboards/${dashboardName}`,
|
|
`/flows/${flowName}`,
|
|
])
|
|
})
|
|
|
|
test("a dashboard control is drawn on the flow it feeds", async ({ page }) => {
|
|
await page.goto(`/flows/${flowName}`)
|
|
await page.waitForSelector(".react-flow__node")
|
|
|
|
await expect(page.getByText("Lever")).toBeVisible()
|
|
})
|
|
|
|
test("the dashboard is never stored as a node of the flow", async ({
|
|
page,
|
|
}) => {
|
|
await page.goto(`/flows/${flowName}`)
|
|
await page.waitForSelector(".react-flow__node")
|
|
// Long enough for an autosave to have landed if the canvas had queued one.
|
|
await page.waitForTimeout(2000)
|
|
|
|
const detail = await (await api(page, `/flows/${flowName}`)).json()
|
|
|
|
expect(detail.definition.nodes.map((n: { id: string }) => n.id)).toEqual([
|
|
"sink",
|
|
])
|
|
// Still reported as an endpoint, just never as a node.
|
|
expect(detail.endpoints.map((e: { label: string }) => e.label)).toEqual([
|
|
"Lever",
|
|
])
|
|
})
|
|
|
|
test("a node cannot be dragged: the graph places itself", async ({ page }) => {
|
|
await page.goto(`/flows/${flowName}`)
|
|
const node = page.locator(".react-flow__node-flow").first()
|
|
await node.waitFor()
|
|
|
|
// React Flow writes a node's place in the graph as the transform on its
|
|
// wrapper. The screen position is no good here: dragging the canvas pans
|
|
// the viewport, which moves every node on screen without moving any of them
|
|
// in the graph — which is the whole point.
|
|
const at = () => node.evaluate((el) => (el as HTMLElement).style.transform)
|
|
const before = await at()
|
|
|
|
const box = await node.boundingBox()
|
|
if (!box) throw new Error("the node was not rendered")
|
|
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2)
|
|
await page.mouse.down()
|
|
await page.mouse.move(box.x + box.width / 2 + 80, box.y + box.height / 2 + 40)
|
|
await page.mouse.up()
|
|
|
|
expect(await at()).toBe(before)
|
|
})
|