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
115 lines
3.3 KiB
TypeScript
115 lines
3.3 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",
|
|
position: { x: 0, y: 0 },
|
|
provides: [{ name: "reading", dtype: "float" }],
|
|
},
|
|
{
|
|
id: "logger",
|
|
type: "python",
|
|
position: { x: 260, y: 0 },
|
|
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()
|
|
})
|