Three things moved the viewport independently — the shape-fit effect, focusNode, and React Flow's own fitView prop — so a fourth for "centre the node I just selected" would have been a fourth party to the argument. There is one effect now, and which branch it takes is decided by what changed rather than by what is true: selecting a node brings that node into the lane the panel leaves, and every other change — new wiring, a new endpoint, a panel opening — re-fits the whole flow into the same lane. A selection centres once, so the port edits that follow re-fit around it, which is what makes a new edge's far end visible. The refit triggers on the edge count, not the bindings key: that key changes on every keystroke in a message-name field, and refitting per character is not what "an edge was created" means. renderedNodes overwrote xyflow's own `selected` flag, so a box-selection of several nodes was invisible even though delete and copy acted on all of them. The logs panel was a popover anchored on its own button, which is why it sat off centre, hugged the button and closed on any outside click. It is a plain surface above the dock now, and the button is stateful. Escape still closes it. Expanding a node's editor gives the panel the whole inset and puts the code on the left with the settings beside it, while the toolbar and the flow name translate off screen. Narrowing the window past `md` gives the room back — the sheet it becomes has no second column to hold. The zoom buttons are gone: there is a mouse, or there is a pinch. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Uq8mtNb97A7praJLyeEYgs
133 lines
4.2 KiB
TypeScript
133 lines
4.2 KiB
TypeScript
import { expect, test } from "@playwright/test"
|
|
import { api, apiPage, deleteAll } from "./utils/api"
|
|
|
|
/**
|
|
* Flows can be taken off the engine and put back, and what a node prints — or
|
|
* the traceback of one that fails — is readable without leaving the canvas.
|
|
*/
|
|
|
|
const flowName = `test_runtime_${Date.now().toString(36)}`
|
|
|
|
test.use({ storageState: "playwright/.auth/user.json" })
|
|
|
|
test.describe.configure({ mode: "serial" })
|
|
|
|
const PRINTING_NODE = `def process():
|
|
print("sensor read 21.5 degrees")
|
|
return {"reading": 21.5}
|
|
`
|
|
const BROKEN_NODE = `def process(reading):
|
|
raise RuntimeError("downstream blew up")
|
|
`
|
|
|
|
test.afterAll(async ({ browser }) => {
|
|
await deleteAll(browser, [`/flows/${flowName}`])
|
|
})
|
|
|
|
test.beforeAll(async ({ browser }) => {
|
|
const page = await apiPage(browser)
|
|
await api(page, `/flows/${flowName}`, {
|
|
method: "PUT",
|
|
data: {
|
|
name: flowName,
|
|
title: "Runtime",
|
|
nodes: [
|
|
{
|
|
id: "sensor",
|
|
type: "python",
|
|
provides: [{ name: "reading", dtype: "float" }],
|
|
},
|
|
{
|
|
id: "logger",
|
|
type: "python",
|
|
requires: [{ name: "reading", dtype: "float" }],
|
|
},
|
|
],
|
|
},
|
|
})
|
|
await api(page, `/flows/${flowName}/nodes/sensor/source`, {
|
|
method: "PUT",
|
|
data: { code: PRINTING_NODE },
|
|
})
|
|
await api(page, `/flows/${flowName}/nodes/logger/source`, {
|
|
method: "PUT",
|
|
data: { code: BROKEN_NODE },
|
|
})
|
|
const detail = await (await api(page, `/flows/${flowName}`)).json()
|
|
await api(page, `/flows/${flowName}/publish`, {
|
|
method: "POST",
|
|
data: { version: detail.definition.version },
|
|
})
|
|
await page.close()
|
|
})
|
|
|
|
test("the dashboard lists flows and can stop one", async ({ page }) => {
|
|
await page.goto("/")
|
|
const row = page
|
|
.getByTestId("dashboard-flow-row")
|
|
.filter({ hasText: "Runtime" })
|
|
await expect(row).toBeVisible()
|
|
await expect(row).toContainText("Running")
|
|
|
|
await row.getByTestId("flow-enabled-switch").click()
|
|
await expect(row).toContainText("Stopped")
|
|
|
|
// A stopped flow is not something the engine will run.
|
|
const refused = await api(page, `/flows/${flowName}/run`, {
|
|
method: "POST",
|
|
data: { inputs: {} },
|
|
})
|
|
expect(refused.status()).toBe(409)
|
|
|
|
await row.getByTestId("flow-enabled-switch").click()
|
|
await expect(row).toContainText("Running")
|
|
})
|
|
|
|
test("the logs panel shows what a node printed and why one failed", async ({
|
|
page,
|
|
}) => {
|
|
await page.goto(`/flows/${flowName}`)
|
|
await page.waitForSelector(".react-flow__node")
|
|
|
|
await page.getByTestId("run-flow").click()
|
|
await page.getByTestId("flow-logs").click()
|
|
|
|
const panel = page.getByTestId("logs-panel")
|
|
await expect(panel).toContainText("sensor read 21.5 degrees")
|
|
await expect(panel).toContainText("RuntimeError")
|
|
|
|
// Reading the logs is something you do *while* working on the flow, so a
|
|
// click on the canvas leaves them up — only the button or Escape closes it.
|
|
await page.locator(".react-flow__pane").click({ position: { x: 8, y: 8 } })
|
|
await expect(panel).toBeVisible()
|
|
|
|
// It sits above the dock rather than on it, and shares its centre. The dock
|
|
// has no box of its own to measure, so its span is its first and last
|
|
// buttons; its padding is symmetric, so their midpoint is its centre.
|
|
const logs = (await panel.boundingBox())!
|
|
const dock = (await page.getByTestId("run-flow").boundingBox())!
|
|
expect(logs.y + logs.height).toBeLessThan(dock.y)
|
|
|
|
const first = (await page.getByTestId("add-node").boundingBox())!
|
|
const last = (await page.getByTestId("publish-flow").boundingBox())!
|
|
const centre = (first.x + last.x + last.width) / 2
|
|
expect(Math.abs(logs.x + logs.width / 2 - centre)).toBeLessThan(2)
|
|
|
|
await page.getByTestId("flow-logs").click()
|
|
await expect(panel).toBeHidden()
|
|
})
|
|
|
|
test("a flow can be paused and let go again", async ({ page }) => {
|
|
await page.goto(`/flows/${flowName}`)
|
|
await page.waitForSelector(".react-flow__node")
|
|
|
|
await page.getByTestId("pause-flow").click()
|
|
await expect(page.getByTestId("resume-flow")).toBeVisible()
|
|
expect((await (await api(page, `/flows/${flowName}`)).json()).paused).toBe(
|
|
true,
|
|
)
|
|
|
|
await page.getByTestId("resume-flow").click()
|
|
await expect(page.getByTestId("pause-flow")).toBeVisible()
|
|
})
|