import { expect, type Page, test } from "@playwright/test" /** * 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 apiUrl = process.env.VITE_API_URL || "http://api.localhost" 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") ` async function api( page: Page, path: string, init: Record = {}, ) { const token = await page.evaluate(() => localStorage.getItem("access_token")) return page.request.fetch(`${apiUrl}/api/v1${path}`, { ...init, headers: { Authorization: `Bearer ${token}` }, }) } test.beforeAll(async ({ browser }) => { const page = await browser.newPage({ storageState: "playwright/.auth/user.json", }) await page.goto("/") 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() await api(page, `/flows/${flowName}`, { method: "DELETE" }) })