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 { expect, test } from "@playwright/test"
|
||||||
import { firstSuperuser, firstSuperuserPassword } from "./config.ts"
|
import { firstSuperuser, firstSuperuserPassword } from "./config.ts"
|
||||||
|
import { api, apiPage } from "./utils/api"
|
||||||
import { createUser } from "./utils/privateApi"
|
import { createUser } from "./utils/privateApi"
|
||||||
import { randomEmail, randomPassword } from "./utils/random"
|
import { randomEmail, randomPassword } from "./utils/random"
|
||||||
import { logInUser } from "./utils/user"
|
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 }) => {
|
test("Admin page is accessible and shows correct title", async ({ page }) => {
|
||||||
await page.goto("/admin")
|
await page.goto("/admin")
|
||||||
await expect(page.getByRole("heading", { name: "Users" })).toBeVisible()
|
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
|
* 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" })
|
test.describe.configure({ mode: "serial" })
|
||||||
|
|
||||||
const apiUrl = process.env.VITE_API_URL || "http://api.localhost"
|
test.afterAll(async ({ browser }) => {
|
||||||
|
await deleteAll(browser, [`/flows/${flowName}`])
|
||||||
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("a draft stays off the engine until it is published", async ({ page }) => {
|
test("a draft stays off the engine until it is published", async ({ page }) => {
|
||||||
await page.goto("/flows")
|
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()
|
const detail = await (await api(page, `/flows/${flowName}`)).json()
|
||||||
expect(detail.has_draft).toBe(false)
|
expect(detail.has_draft).toBe(false)
|
||||||
expect(detail.definition.title).not.toBe("Theirs")
|
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.
|
* 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" })
|
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 }) => {
|
test.beforeAll(async ({ browser }) => {
|
||||||
const page = await browser.newPage({
|
const page = await apiPage(browser)
|
||||||
storageState: "playwright/.auth/user.json",
|
|
||||||
})
|
|
||||||
await page.goto("/")
|
|
||||||
|
|
||||||
// A flow reading a message, and a dashboard control that sets it.
|
// A flow reading a message, and a dashboard control that sets it.
|
||||||
await api(page, `/flows/${flowName}`, {
|
await api(page, `/flows/${flowName}`, {
|
||||||
@@ -82,13 +66,10 @@ test.beforeAll(async ({ browser }) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test.afterAll(async ({ browser }) => {
|
test.afterAll(async ({ browser }) => {
|
||||||
const page = await browser.newPage({
|
await deleteAll(browser, [
|
||||||
storageState: "playwright/.auth/user.json",
|
`/dashboards/${dashboardName}`,
|
||||||
})
|
`/flows/${flowName}`,
|
||||||
await page.goto("/")
|
])
|
||||||
await api(page, `/dashboards/${dashboardName}`, { method: "DELETE" })
|
|
||||||
await api(page, `/flows/${flowName}`, { method: "DELETE" })
|
|
||||||
await page.close()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("a dashboard control is drawn on the flow it feeds", async ({ page }) => {
|
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 { expect, type Page, test } from "@playwright/test"
|
||||||
|
import { deleteAll } from "./utils/api"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The flow editor's load-bearing behaviour: nodes are placed, connections come
|
* 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.describe.configure({ mode: "serial" })
|
||||||
|
|
||||||
|
test.afterAll(async ({ browser }) => {
|
||||||
|
await deleteAll(browser, [`/flows/${flowName}`])
|
||||||
|
})
|
||||||
|
|
||||||
const apiUrl = process.env.VITE_API_URL || "http://api.localhost"
|
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. */
|
/** 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
|
* 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" })
|
test.describe.configure({ mode: "serial" })
|
||||||
|
|
||||||
const apiUrl = process.env.VITE_API_URL || "http://api.localhost"
|
|
||||||
|
|
||||||
const PRINTING_NODE = `def process(params):
|
const PRINTING_NODE = `def process(params):
|
||||||
print("sensor read 21.5 degrees")
|
print("sensor read 21.5 degrees")
|
||||||
return {"reading": 21.5}
|
return {"reading": 21.5}
|
||||||
@@ -21,23 +20,12 @@ const BROKEN_NODE = `def process(reading, params):
|
|||||||
raise RuntimeError("downstream blew up")
|
raise RuntimeError("downstream blew up")
|
||||||
`
|
`
|
||||||
|
|
||||||
async function api(
|
test.afterAll(async ({ browser }) => {
|
||||||
page: Page,
|
await deleteAll(browser, [`/flows/${flowName}`])
|
||||||
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 }) => {
|
test.beforeAll(async ({ browser }) => {
|
||||||
const page = await browser.newPage({
|
const page = await apiPage(browser)
|
||||||
storageState: "playwright/.auth/user.json",
|
|
||||||
})
|
|
||||||
await page.goto("/")
|
|
||||||
await api(page, `/flows/${flowName}`, {
|
await api(page, `/flows/${flowName}`, {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
data: {
|
data: {
|
||||||
@@ -123,6 +111,4 @@ test("a flow can be paused and let go again", async ({ page }) => {
|
|||||||
|
|
||||||
await page.getByTestId("resume-flow").click()
|
await page.getByTestId("resume-flow").click()
|
||||||
await expect(page.getByTestId("pause-flow")).toBeVisible()
|
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