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 its declared // value 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 still reads its value", async ({ page, }) => { await openRuns(page) await page.getByTestId("run-link").first().click() const inputs = page.locator("section", { hasText: "Inputs" }).last() // `submit` folds every declared initial into the run's params, so the row is // self-describing and the panel reads the same either way: what was passed // and what was left alone both show the value the run actually started from. await expect(inputs).toContainText("epochs") await expect(inputs).toContainText("rate") await expect(inputs).toContainText("0.5") }) 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 }) }) test("a run is started from the form", async ({ page }) => { await page.goto(`/runs/new?flow=${flowName}`) await page.getByLabel("epochs", { exact: true }).fill("3") await page.getByTestId("submit-new-run").click() // The result card is the run just started, and it fills in as it goes. const result = page.getByTestId("run-result") await expect(result).toBeVisible() await expect(result.getByText("score")).toBeVisible({ timeout: 30_000 }) await expect(page.getByTestId("recent-run").first()).toBeVisible() }) test("a list of values is a sweep", async ({ page }) => { await page.goto(`/runs/new?flow=${flowName}`) await page.getByLabel("epochs", { exact: true }).fill("2,3") const run = page.getByTestId("submit-new-run") await expect(run).toHaveText("Run 2") await run.click() // Submitting a sweep lands on the history, filtered to that group. await page.waitForURL(/\/runs\?.*group=/) await expect(page.getByTestId("run-row")).toHaveCount(2) })