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") ` const FIXED_NODE = `def process(reading): print(f"logged {reading}") ` 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 home page lists flows and can stop one", async ({ page }) => { await page.goto("/") const row = page.getByTestId("home-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 node's failure outlives its next good run", async ({ page }) => { await page.goto(`/flows/${flowName}`) await page.waitForSelector(".react-flow__node") const logger = page.locator(".react-flow__node").filter({ hasText: "logger" }) await page.getByTestId("run-flow").click() await expect(logger.getByTestId("node-traceback")).toBeVisible() // Fix it the way the editor does, and put it on the engine: publishing // rebuilds every node, which is exactly what must not wipe the record. await api(page, `/flows/${flowName}/nodes/logger/source`, { method: "PUT", data: { code: FIXED_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.getByTestId("run-flow").click() // The dot says how the *last* run went, and the traceback button says the // node failed at some point since anyone looked. Both at once is the point. await expect(logger.getByLabel("Last run succeeded")).toBeVisible() await expect(logger.getByTestId("node-traceback")).toBeVisible() }) 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() })