Playwright flows
End-to-end user journeys in e2e/** tagged @uidex:flow, and the registry's touches metadata.
End-to-end user journeys are Playwright tests in e2e/**. Any top-level
test.describe auto-promotes to a flow; the @uidex:flow tag adds richer
metadata. Opt out by adding export const uidex = { notFlow: true } as const satisfies Uidex.NotFlow at the top of the spec file.
import { expect, test } from "uidex/playwright"
import type { Uidex } from "@/uidex.gen"
export const uidex = {
flow: "add-and-complete-todo",
description: "User adds a todo and marks it complete.",
} as const satisfies Uidex.Flow
test.describe("Add and complete a todo", { tag: "@uidex:flow" }, () => {
test("adds a todo", async ({ uidex }) => {
await uidex("todo-field").fill("Buy milk")
await uidex("todo-add").click()
})
})Rules
- One tagged describe per
flow-*.spec.ts. - Component ids inside
uidex(...)MUST be string literals (static-analysable). Variables, concatenation, and template literals are invisible to the static scanner and will not contribute totouches. page-*.spec.tsandfeature-*.spec.tsscoped tests remain untagged.
Touches and meta.flows
Each uidex(...) call in a tagged describe contributes to the flow's touches
array. The scanner inverts that index per registered entity, so for any page,
feature, or widget you can ask "which flows cover me?" via
entity.meta.flows: FlowId[].
test.describe("Play a video", { tag: "@uidex:flow" }, () => {
test("plays on space", async ({ uidex, page }) => {
await uidex("video-player").focus()
await page.keyboard.press("Space")
await expect(uidex("video-player")).toHaveJSProperty("paused", false)
})
})For widgets specifically, meta.flows includes flows that touch any
descendant element of the widget — not just the widget root.
Scaffolding from acceptance
uidex scaffold widget <id> emits a Playwright spec stub from the widget's
declared acceptance criteria. See
uidex scan for the full reference.
npx uidex scaffold widget video-playerEmits a Playwright spec with exactly one test() per declared criterion. The
describe carries { tag: "@uidex:flow" }. Each test has a // TODO body for
you to fill in.
Behaviour:
- Deterministic file path: the scaffold writes to
<flows-glob-root>/flow-<id>.spec.ts. - Idempotent: re-running will refuse to overwrite an existing file unless
--forceis passed. - Without
--force, exits with a clear message naming the existing path.
Opting out
A spec file under the flows glob that does not model a user journey (a shared fixture, a utility) opts out with:
import type { Uidex } from "@/uidex.gen"
export const uidex = { notFlow: true } as const satisfies Uidex.NotFlowThe scanner leaves the file's describes alone; nothing in the file contributes
to touches.