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
113 lines
3.2 KiB
TypeScript
113 lines
3.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(params):
|
|
print("sensor read 21.5 degrees")
|
|
return {"reading": 21.5}
|
|
`
|
|
const BROKEN_NODE = `def process(reading, params):
|
|
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.locator('[data-slot="popover-content"]')
|
|
await expect(panel).toContainText("sensor read 21.5 degrees")
|
|
await expect(panel).toContainText("RuntimeError")
|
|
})
|
|
|
|
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()
|
|
})
|