The specs run against a development stack, so every run left a test_flow_* in someone's flow list. Each spec now tears down what it made in an afterAll, which runs whether or not the tests passed, and the three copies of the API helper move into tests/utils/api.ts. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H7LwYgJfpkbLCTeiAf8U4A
108 lines
3.1 KiB
TypeScript
108 lines
3.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` },
|
|
},
|
|
]
|
|
await api(page, `/dashboards/${dashboardName}`, {
|
|
method: "PUT",
|
|
data: dashboard,
|
|
})
|
|
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("moving a node does not save the dashboard into the flow", async ({
|
|
page,
|
|
}) => {
|
|
await page.goto(`/flows/${flowName}`)
|
|
const node = page.locator(".react-flow__node-flow").first()
|
|
await node.waitFor()
|
|
|
|
const box = await node.boundingBox()
|
|
if (!box) throw new Error("the node has no position to drag from")
|
|
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()
|
|
// The autosave is debounced; give it room to land.
|
|
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",
|
|
])
|
|
})
|