From 143395358a76a96da503aecca09041526d642622 Mon Sep 17 00:00:00 2001 From: stroblme Date: Sun, 16 Aug 2026 16:46:07 +0200 Subject: [PATCH] Delete the flows and users the Playwright specs create 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 Claude-Session: https://claude.ai/code/session_01H7LwYgJfpkbLCTeiAf8U4A --- frontend/tests/admin.spec.ts | 15 ++++++++++++ frontend/tests/drafts.spec.ts | 21 ++++------------ frontend/tests/endpoints.spec.ts | 33 ++++++------------------- frontend/tests/flows.spec.ts | 5 ++++ frontend/tests/runtime.spec.ts | 26 +++++--------------- frontend/tests/utils/api.ts | 41 ++++++++++++++++++++++++++++++++ 6 files changed, 79 insertions(+), 62 deletions(-) create mode 100644 frontend/tests/utils/api.ts diff --git a/frontend/tests/admin.spec.ts b/frontend/tests/admin.spec.ts index 490810e..192841e 100644 --- a/frontend/tests/admin.spec.ts +++ b/frontend/tests/admin.spec.ts @@ -1,9 +1,24 @@ import { expect, test } from "@playwright/test" import { firstSuperuser, firstSuperuserPassword } from "./config.ts" +import { api, apiPage } from "./utils/api" import { createUser } from "./utils/privateApi" import { randomEmail, randomPassword } from "./utils/random" import { logInUser } from "./utils/user" +test.afterAll(async ({ browser }) => { + // Every user below is created under a randomEmail(), so the superuser can + // sweep them all up by that shape — including what a run that failed halfway + // left in the shared development database. + const page = await apiPage(browser) + const { data } = await (await api(page, "/users/?limit=1000")).json() + for (const user of data as { id: string; email: string }[]) { + if (/^test_.*@example\.com$/.test(user.email)) { + await api(page, `/users/${user.id}`, { method: "DELETE" }) + } + } + await page.close() +}) + test("Admin page is accessible and shows correct title", async ({ page }) => { await page.goto("/admin") await expect(page.getByRole("heading", { name: "Users" })).toBeVisible() diff --git a/frontend/tests/drafts.spec.ts b/frontend/tests/drafts.spec.ts index 8c64565..a9e7bea 100644 --- a/frontend/tests/drafts.spec.ts +++ b/frontend/tests/drafts.spec.ts @@ -1,4 +1,5 @@ -import { expect, type Page, test } from "@playwright/test" +import { expect, test } from "@playwright/test" +import { api, deleteAll } from "./utils/api" /** * Editing writes a draft; only publishing hands it to the engine. The two @@ -12,19 +13,9 @@ test.use({ storageState: "playwright/.auth/user.json" }) test.describe.configure({ mode: "serial" }) -const apiUrl = process.env.VITE_API_URL || "http://api.localhost" - -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.afterAll(async ({ browser }) => { + await deleteAll(browser, [`/flows/${flowName}`]) +}) test("a draft stays off the engine until it is published", async ({ page }) => { await page.goto("/flows") @@ -93,6 +84,4 @@ test("discarding a draft goes back to the published flow", async ({ page }) => { const detail = await (await api(page, `/flows/${flowName}`)).json() expect(detail.has_draft).toBe(false) expect(detail.definition.title).not.toBe("Theirs") - - await api(page, `/flows/${flowName}`, { method: "DELETE" }) }) diff --git a/frontend/tests/endpoints.spec.ts b/frontend/tests/endpoints.spec.ts index db7250d..32b2de8 100644 --- a/frontend/tests/endpoints.spec.ts +++ b/frontend/tests/endpoints.spec.ts @@ -1,4 +1,5 @@ -import { expect, type Page, test } from "@playwright/test" +import { expect, test } from "@playwright/test" +import { api, apiPage, deleteAll } from "./utils/api" /** * Dashboards and other flows appear on the canvas but are not part of the flow. @@ -16,25 +17,8 @@ test.use({ storageState: "playwright/.auth/user.json" }) test.describe.configure({ mode: "serial" }) -const apiUrl = process.env.VITE_API_URL || "http://api.localhost" - -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("/") + const page = await apiPage(browser) // A flow reading a message, and a dashboard control that sets it. await api(page, `/flows/${flowName}`, { @@ -82,13 +66,10 @@ test.beforeAll(async ({ browser }) => { }) test.afterAll(async ({ browser }) => { - const page = await browser.newPage({ - storageState: "playwright/.auth/user.json", - }) - await page.goto("/") - await api(page, `/dashboards/${dashboardName}`, { method: "DELETE" }) - await api(page, `/flows/${flowName}`, { method: "DELETE" }) - await page.close() + await deleteAll(browser, [ + `/dashboards/${dashboardName}`, + `/flows/${flowName}`, + ]) }) test("a dashboard control is drawn on the flow it feeds", async ({ page }) => { diff --git a/frontend/tests/flows.spec.ts b/frontend/tests/flows.spec.ts index 333e080..e6a5a68 100644 --- a/frontend/tests/flows.spec.ts +++ b/frontend/tests/flows.spec.ts @@ -1,4 +1,5 @@ import { expect, type Page, test } from "@playwright/test" +import { deleteAll } from "./utils/api" /** * The flow editor's load-bearing behaviour: nodes are placed, connections come @@ -12,6 +13,10 @@ test.use({ storageState: "playwright/.auth/user.json" }) test.describe.configure({ mode: "serial" }) +test.afterAll(async ({ browser }) => { + await deleteAll(browser, [`/flows/${flowName}`]) +}) + const apiUrl = process.env.VITE_API_URL || "http://api.localhost" /** Write a node's source through the API; typing code is not what we test. */ diff --git a/frontend/tests/runtime.spec.ts b/frontend/tests/runtime.spec.ts index ae12b5c..25b5006 100644 --- a/frontend/tests/runtime.spec.ts +++ b/frontend/tests/runtime.spec.ts @@ -1,4 +1,5 @@ -import { expect, type Page, test } from "@playwright/test" +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 @@ -11,8 +12,6 @@ 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} @@ -21,23 +20,12 @@ 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.afterAll(async ({ browser }) => { + await deleteAll(browser, [`/flows/${flowName}`]) +}) test.beforeAll(async ({ browser }) => { - const page = await browser.newPage({ - storageState: "playwright/.auth/user.json", - }) - await page.goto("/") + const page = await apiPage(browser) await api(page, `/flows/${flowName}`, { method: "PUT", data: { @@ -123,6 +111,4 @@ test("a flow can be paused and let go again", async ({ page }) => { await page.getByTestId("resume-flow").click() await expect(page.getByTestId("pause-flow")).toBeVisible() - - await api(page, `/flows/${flowName}`, { method: "DELETE" }) }) diff --git a/frontend/tests/utils/api.ts b/frontend/tests/utils/api.ts new file mode 100644 index 0000000..deca937 --- /dev/null +++ b/frontend/tests/utils/api.ts @@ -0,0 +1,41 @@ +import type { Browser, Page } from "@playwright/test" + +/** + * Talking to the API directly, for the setup and teardown around a spec. + * + * The specs run against a development stack, so whatever they create has to go + * again — the same bargain the backend suite strikes with its throwaway + * database. What a spec leaves behind is in someone's flow list tomorrow. + */ + +const authFile = "playwright/.auth/user.json" +const apiUrl = process.env.VITE_API_URL || "http://api.localhost" + +/** Call the API as the logged-in user of *page*. */ +export 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}` }, + }) +} + +/** An authenticated page for a `beforeAll`/`afterAll`, which get no `page`. */ +export async function apiPage(browser: Browser) { + const page = await browser.newPage({ storageState: authFile }) + await page.goto("/") + return page +} + +/** Delete the given API paths, whether or not they are still there. */ +export async function deleteAll(browser: Browser, paths: string[]) { + const page = await apiPage(browser) + for (const path of paths) { + await api(page, path, { method: "DELETE" }) + } + await page.close() +}