Files
app/frontend/tests/drafts.spec.ts
T
stroblmeandClaude Opus 5 bb90a24b90 Computed flow layout, and mobile written into the design
The canvas lays itself out: a layered graph, left to right on a desktop and
top to bottom on a phone, with room reserved for the value each edge carries.
Nodes cannot be dragged and `NodeDef.position` is gone from the document —
a graph nobody can arrange is one worth keeping small, which is what keeps
flows atomic. Endpoints join the same layout, so their lanes and the
localStorage that remembered where they were dragged go too.

Mobile, per the new Responsive section of DESIGN-GUIDELINES.md: the dock caps
its width and wraps instead of running off the screen, the dashboard stacks
into one column rather than shrinking a wall panel to a fifth of its size, and
Home stops widening its grid track past the viewport. A Playwright project at
a phone's width fails the build when a screen no longer fits.

Along the way: publish is the checkmark that was already there rather than a
button that appears and disappears, with discard beside it on both the flow
and the dashboard; the brain reveals a neuron's name on the first tap; and the
port sparklines get room to breathe.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VDSXaRhvqHYNevgDGmNAto
2026-08-17 17:35:14 +02:00

94 lines
3.3 KiB
TypeScript

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
* things worth proving here are that the engine ignores a draft until it is
* published, and that a second client cannot quietly overwrite the first.
*/
const flowName = `test_draft_${Date.now().toString(36)}`
test.use({ storageState: "playwright/.auth/user.json" })
test.describe.configure({ mode: "serial" })
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")
await page.getByTestId("new-flow").click()
await page.getByTestId("new-flow-name").fill(flowName)
await page.getByTestId("create-flow").click()
await page.waitForURL(`/flows/${flowName}`)
await page.getByTestId("add-node").click()
await page
.getByRole("option", { name: /function/i })
.first()
.click()
await expect(page.locator(".react-flow__node")).toHaveCount(1)
// Adding a node opens its panel, and the floating chrome steps aside for it.
await page.keyboard.press("Escape")
// The publish glyph never moves; it is enabled only while there is
// something to put live.
await expect(page.getByTestId("publish-flow")).toBeEnabled()
// Nothing of this flow is loaded while it is only a draft.
await page.waitForTimeout(1500)
const before = await (await api(page, `/flows/${flowName}/state`)).json()
expect(before.nodes).toHaveLength(0)
await page.getByTestId("publish-flow").click()
// Discard is what marks a draft: it goes when the publish has landed. The
// publish glyph disables while the request is in flight too, so waiting on
// that would not wait for anything.
await expect(page.getByTestId("discard-draft")).toBeHidden()
await expect(page.getByTestId("publish-flow")).toBeDisabled()
const after = await (await api(page, `/flows/${flowName}/state`)).json()
expect(after.nodes.length).toBeGreaterThan(0)
})
test("a save against a version someone else moved on from is refused", async ({
page,
}) => {
await page.goto(`/flows/${flowName}`)
await page.waitForSelector(".react-flow__node")
const detail = await (await api(page, `/flows/${flowName}`)).json()
// Stand in for a second client that saved first.
const first = await api(page, `/flows/${flowName}`, {
method: "PUT",
data: { ...detail.definition, title: "Theirs" },
})
expect(first.ok()).toBeTruthy()
const stale = await api(page, `/flows/${flowName}`, {
method: "PUT",
data: { ...detail.definition, title: "Mine" },
})
expect(stale.status()).toBe(409)
expect((await stale.json()).detail.current_version).toBe(
detail.definition.version + 1,
)
})
test("discarding a draft goes back to the published flow", async ({ page }) => {
await page.goto(`/flows/${flowName}`)
await page.waitForSelector(".react-flow__node")
await page.getByTestId("discard-draft").click()
await page.getByTestId("confirm-discard-draft").click()
await expect(page.getByTestId("discard-draft")).toBeHidden()
await expect(page.getByTestId("publish-flow")).toBeDisabled()
const detail = await (await api(page, `/flows/${flowName}`)).json()
expect(detail.has_draft).toBe(false)
expect(detail.definition.title).not.toBe("Theirs")
})