import { expect, type Page, test } from "@playwright/test" import { api, apiPage, deleteAll } from "./utils/api" /** * Objects made the ordinary way, with an id the server issues. * * Every other spec pins its id to its key, and with the two equal a key * passed where an id belongs still works. Here they differ, so each place the * app has to tell them apart is exercised: routes and API calls by id, live * values, hooks and message names by key. */ const stamp = Date.now().toString(36) const key = `test_ids_${stamp}` const boardKey = `${key}_board` const batchKey = `${key}_batch` const FLOW_ID = /^f_[a-z2-7]{8}$/ const BOARD_ID = /^d_[a-z2-7]{8}$/ let flowId = "" let boardId = "" let batchId = "" let runId = "" let resultsId = "" test.use({ storageState: "playwright/.auth/user.json", permissions: ["clipboard-read", "clipboard-write"], }) test.describe.configure({ mode: "serial" }) /** Publish a flow's working copy, whatever version it has reached. */ async function publishFlow(page: Page, id: string) { const detail = await (await api(page, `/flows/${id}`)).json() await api(page, `/flows/${id}/publish`, { method: "POST", data: { version: detail.definition.version }, }) } test.beforeAll(async ({ browser }) => { const page = await apiPage(browser) // A webhook feeding `level`, created without an id. const flow = await api(page, "/flows/", { method: "POST", data: { name: key, nodes: [ { id: "hook", type: "http", params: { url: "/ping", method: "POST" }, requires: [], provides: [{ name: "level", dtype: "float" }], }, ], }, }) expect(flow.status()).toBe(201) flowId = (await flow.json()).definition.id expect(flowId).toMatch(FLOW_ID) // A dashboard reading it, bound by the flow's key. const board = await api(page, "/dashboards/", { method: "POST", data: { name: boardKey, title: "Issued board" }, }) expect(board.status()).toBe(201) const made = await board.json() boardId = made.id expect(boardId).toMatch(BOARD_ID) const saved = await ( await api(page, `/dashboards/${boardId}`, { method: "PUT", data: { ...made, widgets: [ { id: "level", type: "stat", title: "Issued level", layout: { lg: { x: 0, y: 0, w: 3, h: 2 } }, config: { message: `${key}.level`, dtype: "float" }, }, ], }, }) ).json() await api(page, `/dashboards/${boardId}/publish`, { method: "POST", data: { version: saved.version }, }) // A batch flow with one finished run, for its results dashboard. const batch = await api(page, "/flows/", { method: "POST", data: { name: batchKey, mode: "batch", outputs: ["score"], nodes: [ { id: "score", type: "python", provides: [{ name: "score", dtype: "float" }], }, ], }, }) batchId = (await batch.json()).definition.id expect(batchId).toMatch(FLOW_ID) await api(page, `/flows/${batchId}/nodes/score/source`, { method: "PUT", data: { code: 'def process():\n return {"score": 1.5}\n' }, }) await publishFlow(page, batchId) const run = await api(page, `/runs/flows/${batchId}`, { method: "POST", data: { params: {} }, }) expect(run.ok()).toBeTruthy() runId = (await run.json()).id await page.close() }) test.afterAll(async ({ browser }) => { await deleteAll( browser, [ resultsId && `/dashboards/${resultsId}`, boardId && `/dashboards/${boardId}`, flowId && `/flows/${flowId}`, batchId && `/flows/${batchId}`, // After the flow, as the editor does it, and only by its id. batchId && `/runs?flow=${batchId}&history=true`, ].filter(Boolean), ) }) test("a flow opens, saves and publishes by its issued id", async ({ page }) => { await page.goto(`/flows/${flowId}`) await page.waitForSelector(".react-flow__node") // The tab is named from the document, never from the opaque address. await expect(page).toHaveTitle(`${key} - Fluksio`) await page.getByTestId("edit-flow").click() const panel = page.getByTestId("flow-panel") await expect(panel.getByTestId("identity-key")).toHaveText(key) await expect(panel.getByTestId("identity-id")).toHaveText(flowId) await panel.getByLabel("Flow title").fill("Issued id") await panel.getByTestId("confirm-rename").click() await expect .poll( async () => (await (await api(page, `/flows/${flowId}`)).json()).definition.title, ) .toBe("Issued id") await expect(page).toHaveTitle("Issued id - Fluksio") await page.getByTestId("publish-flow").click() await expect(page.getByTestId("discard-draft")).toBeHidden() const detail = await (await api(page, `/flows/${flowId}`)).json() expect(detail.has_draft).toBe(false) expect(detail.definition.name).toBe(key) }) test("its hook URL carries the id, and a call through it shows up live", async ({ page, }) => { await page.goto(`/flows/${flowId}?node=hook`) const url = page.getByTestId("hook-url") await expect(url).toHaveText(new RegExp(`/hooks/${flowId}/ping$`)) const hook = (await url.textContent()) ?? "" await page.getByRole("button", { name: "Copy the webhook URL" }).click() expect(await page.evaluate(() => navigator.clipboard.readText())).toBe(hook) // The route is mounted once the published flow is rebuilt. await expect .poll(async () => (await page.request.post(hook, { data: { level: 7 } })).status(), ) .toBe(200) // The value is named by the key, which is what the canvas matches on. await expect(page.locator(".react-flow__edgelabel-renderer")).toContainText( "7", { timeout: 15000 }, ) // The dashboard drawn on the canvas opens by its own id. await page.getByText("Issued level").first().click() await page.waitForURL(`**/dashboards/${boardId}`) await expect(page).toHaveTitle("Issued board - Fluksio") await expect(page.getByTestId("canvas-surface")).toContainText("7") }) test("a dashboard shows its key and id, and its view opens by the id", async ({ page, }) => { await page.goto(`/dashboards/${boardId}?edit=true`) await page.getByTestId("edit-dashboard").click() const panel = page.getByTestId("dashboard-panel") await expect(panel.getByTestId("identity-key")).toHaveText(boardKey) await expect(panel.getByTestId("identity-id")).toHaveText(boardId) await page.goto(`/view/${boardId}`) await expect(page).toHaveTitle("Issued board - Fluksio") await expect(page.getByText("Issued level")).toBeVisible() }) test("a run's results dashboard is built and opened by its issued id", async ({ page, }) => { await page.goto("/") await expect .poll( async () => (await (await api(page, `/runs/${runId}`)).json()).status, { timeout: 30_000 }, ) .toBe("ok") await page.goto(`/runs/${runId}`) // Labelled by the flow's key, linked by its id. await expect(page.getByRole("link", { name: batchKey })).toBeVisible() await page.getByRole("button", { name: "Open in dashboard" }).click() await page.getByText(`Build a results dashboard for ${batchKey}`).click() await page.waitForURL(/\/view\/d_[a-z2-7]{8}\?runs=/) resultsId = new URL(page.url()).pathname.split("/").pop() ?? "" const results = await (await api(page, `/dashboards/${resultsId}`)).json() expect(results.name).toBe(`${batchKey}_results`) })