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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H7LwYgJfpkbLCTeiAf8U4A
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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<string, unknown> = {},
|
||||
) {
|
||||
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" })
|
||||
})
|
||||
|
||||
@@ -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<string, unknown> = {},
|
||||
) {
|
||||
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 }) => {
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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<string, unknown> = {},
|
||||
) {
|
||||
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" })
|
||||
})
|
||||
|
||||
@@ -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<string, unknown> = {},
|
||||
) {
|
||||
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()
|
||||
}
|
||||
Reference in New Issue
Block a user