Docs / docs (push) Successful in 23s
Playwright Tests / test-playwright (1, 2) (push) Successful in 3m18s
Playwright Tests / test-playwright (2, 2) (push) Successful in 1m49s
pre-commit / pre-commit (push) Failing after 2m5s
Test Backend / test-backend (push) Successful in 2m39s
Compose Smoke Test / test-compose (push) Successful in 33s
Playwright Tests / merge-reports (push) Successful in 1m11s
Runs: a run can now be deleted (DELETE /runs/{id}, cancelling a live one
first), exported as csv from the screen's own filters, and its flow label
opens the flow. Its "Parameters" panel became "Inputs" and lists every
input the flow declares, marking the ones that took the flow's own value
rather than the run's — the comparison table resolves the same defaults
instead of printing "unset".
Home reads brain, dashboards, health, flows: the mosaic is one full-width
scrolling strip, and the flows list and the flow-activity rollups merged
into a single left-joined table so a flow's state and its numbers sit on
one row.
Charts take a drag to narrow the x window and a double click or tap to
come back out. UplotChart holds the scale and passes resetScales:false
while a window is held, which is what the old comment said made this
impossible.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019LrWVRguqbk33YzfEeUx5W
152 lines
4.7 KiB
TypeScript
152 lines
4.7 KiB
TypeScript
import { expect, test } from "@playwright/test"
|
|
import { api, apiPage, deleteAll } from "./utils/api"
|
|
|
|
/**
|
|
* What the runs screen can do to a run, rather than what it can show about one.
|
|
*
|
|
* The reading half is covered by the run itself being there; these are the two
|
|
* things that change something — a delete, and a download — plus the two links
|
|
* a run makes to what produced it.
|
|
*/
|
|
|
|
const flowName = `test_runs_${Date.now().toString(36)}`
|
|
|
|
test.use({ storageState: "playwright/.auth/user.json" })
|
|
test.describe.configure({ mode: "serial" })
|
|
|
|
// Yields a curve so the run has something to draw, which is what the zoom
|
|
// below needs; the streaming port is what makes those yields a series.
|
|
const TRAIN = `def process(epochs):
|
|
for step in range(epochs):
|
|
yield {"loss": 1.0 / (step + 1)}
|
|
return {"score": epochs * 0.5}
|
|
`
|
|
|
|
test.afterAll(async ({ browser }) => {
|
|
await deleteAll(browser, [`/flows/${flowName}`])
|
|
})
|
|
|
|
test.beforeAll(async ({ browser }) => {
|
|
const page = await apiPage(browser)
|
|
await api(page, `/flows/${flowName}`, {
|
|
method: "PUT",
|
|
data: {
|
|
name: flowName,
|
|
title: "Runs under test",
|
|
// `rate` is declared and never passed, which is what puts a default in
|
|
// the Inputs panel below.
|
|
inputs: [
|
|
{ spec: { name: "epochs", dtype: "int" }, initial: 2 },
|
|
{ spec: { name: "rate", dtype: "float" }, initial: 0.5 },
|
|
],
|
|
nodes: [
|
|
{
|
|
id: "train",
|
|
type: "python",
|
|
requires: [{ name: "epochs", dtype: "int" }],
|
|
provides: [
|
|
{ name: "loss", dtype: "float", stream: true },
|
|
{ name: "score", dtype: "float" },
|
|
],
|
|
},
|
|
],
|
|
},
|
|
})
|
|
await api(page, `/flows/${flowName}/nodes/train/source`, {
|
|
method: "PUT",
|
|
data: { code: TRAIN },
|
|
})
|
|
const detail = await (await api(page, `/flows/${flowName}`)).json()
|
|
await api(page, `/flows/${flowName}/publish`, {
|
|
method: "POST",
|
|
data: { version: detail.definition.version },
|
|
})
|
|
|
|
for (const epochs of [4, 6]) {
|
|
const answer = await api(page, `/runs/flows/${flowName}`, {
|
|
method: "POST",
|
|
data: { params: { epochs } },
|
|
})
|
|
expect(answer.ok()).toBeTruthy()
|
|
}
|
|
await page.close()
|
|
})
|
|
|
|
/** The screen filtered to this flow, once both runs have stopped moving. */
|
|
async function openRuns(page: import("@playwright/test").Page) {
|
|
await page.goto(`/runs?flow=${flowName}`)
|
|
await expect(page.getByTestId("run-row")).toHaveCount(2)
|
|
await expect(page.getByText(/queued|running/)).toHaveCount(0, {
|
|
timeout: 30_000,
|
|
})
|
|
}
|
|
|
|
test("a run names the flow it came from, and links to it", async ({ page }) => {
|
|
await page.goto("/runs")
|
|
const link = page
|
|
.getByTestId("run-row")
|
|
.filter({ hasText: flowName })
|
|
.first()
|
|
.getByRole("link", { name: flowName })
|
|
await link.click()
|
|
await page.waitForURL(`**/flows/${flowName}`)
|
|
})
|
|
|
|
test("an input the run never passed reads as the flow's own", async ({
|
|
page,
|
|
}) => {
|
|
await openRuns(page)
|
|
await page.getByTestId("run-link").first().click()
|
|
|
|
const inputs = page.locator("section", { hasText: "Inputs" }).last()
|
|
// Passed, so no marker.
|
|
await expect(inputs).toContainText("epochs")
|
|
// Declared and left alone: the value shows, and says where it came from.
|
|
await expect(inputs).toContainText("rate")
|
|
await expect(inputs).toContainText("0.5")
|
|
await expect(inputs.getByText("default").first()).toBeVisible()
|
|
})
|
|
|
|
test("a chart can be dragged into and double-clicked back out of", async ({
|
|
page,
|
|
}) => {
|
|
await openRuns(page)
|
|
await page.getByTestId("run-link").first().click()
|
|
|
|
const plot = page.locator(".u-over").first()
|
|
await expect(plot).toBeVisible()
|
|
const box = await plot.boundingBox()
|
|
if (!box) throw new Error("the chart has no box to drag across")
|
|
|
|
const y = box.y + box.height / 2
|
|
await page.mouse.move(box.x + box.width * 0.3, y)
|
|
await page.mouse.down()
|
|
await page.mouse.move(box.x + box.width * 0.7, y, { steps: 8 })
|
|
await page.mouse.up()
|
|
|
|
const reset = page.getByTestId("chart-reset-zoom")
|
|
await expect(reset).toBeVisible()
|
|
await plot.dblclick()
|
|
await expect(reset).toBeHidden()
|
|
})
|
|
|
|
test("the export button downloads the selection", async ({ page }) => {
|
|
await openRuns(page)
|
|
await page.getByTestId("export-runs").click()
|
|
|
|
const download = page.waitForEvent("download")
|
|
await page.getByRole("menuitem", { name: /runs table/i }).click()
|
|
expect((await download).suggestedFilename()).toBe("runs.csv")
|
|
})
|
|
|
|
test("picked runs can be deleted", async ({ page }) => {
|
|
await openRuns(page)
|
|
for (const box of await page.getByTestId("run-select").all())
|
|
await box.click()
|
|
|
|
await page.getByTestId("delete-selected").click()
|
|
await page.getByTestId("confirm-delete").click()
|
|
|
|
await expect(page.getByTestId("run-row")).toHaveCount(0, { timeout: 30_000 })
|
|
})
|