PureYAML/docs/rules-swift/ui/flowspec.md
Mihaela M 5f3892f2c1 chore(rules): vendor the rules-swift canon into docs/rules-swift
Mirror the shared Swift rules canon (rules-swift, commit 4ebb892) into
docs/rules-swift/ as a read-only reference alongside the repo's tailored
docs/rules/. PROVENANCE.md records the source, commit, and what was
included or excluded; the upstream repo's own VCS, CI, and lint tooling
is omitted as machinery rather than rule content.

Exclude the vendored tree from the style gate (scripts/check-style.sh)
and the pre-commit hook: the canon's enforcement docs quote the
forbidden attribution phrases to define them, the same reason the
repo's own enforcement files are already excluded.
2026-06-26 05:36:12 +02:00

44 lines
4.5 KiB
Markdown

# FlowSpec (declarative UI scenarios)
**Status: RECOMMENDED, and the expected form when a UI ships in more than one renderer.** A UI flow is described once, as a declarative scenario, and replayed against every UI implementation. The scenario is the single source of truth for what a flow does; the implementations cannot drift in their behavior because they all run the same steps. The engine is the public package [FlowSpec](https://github.com/mihaelamj/FlowSpec).
## The model
- A **scenario** is a JSON file: `{ id, title, actor?, preconditions[], tags[], steps[] }`.
- A **step** is `{ verb, target, arg? }`. Its key is `"verb:target"`.
- A **verb** comes from a small closed vocabulary (`open`, `tap`, `type`, `swipe`, `wait`, `assert`, `request`). The vocabulary is deliberately tiny so a scenario reads like a specification, not a script; adding a verb is a coordinated change across the schema, the enum, and every registry.
- A **step registry** is the seam: one per UI, a `@MainActor` type with a single `execute(_ step: Step) throws` that turns a step into a real action against that UI. The registry throws on an unknown `verb:target`, so a scenario can never reference machinery that is not wired up. FlowSpec ships a default identifier-driven XCUITest registry in its `FlowSpecXCUI` module (see `pom.md` for the page-object pattern it drives); the headless registry that drives the model layer you write yourself. The core `FlowSpec` engine stays `Foundation`-only.
- The **runner** walks a scenario's steps in order, stops on the first failure, and reports the scenario id and step index, so a log points at the exact step.
## Why and when
- **One flow, many UIs.** When a feature ships in more than one renderer (SwiftUI, AppKit, UIKit; see `pre-ui-layer.md`), write the flow once and bind a registry per renderer. The renderers cannot disagree about what the flow does, because the steps are shared.
- **The same corpus, headless and on-device.** The engine is `Foundation`-only and transport-agnostic. The same scenario corpus runs on-device through an XCUITest registry that drives page objects (`pom.md`), AND headless through a registry that drives the model layer directly (`pre-ui-layer.md`). This is the strongest form of the pre-UI-layer's headless-parity invariant: do not keep a separate set of hand-written headless tests that can drift from the device scenarios; replay the one corpus both ways.
- **A closed vocabulary keeps scenarios honest.** Because the verb set is small and the registry throws on an unwired step, a scenario stays a readable specification and cannot silently no-op.
## DO
- Keep the flow in a scenario file; keep the per-UI mechanics in a step registry, one per renderer.
- Drive the on-device registry through page objects (`pom.md`), not raw queries.
- Replay the same scenario corpus headless against the model layer; do not fork a separate headless test set.
- Let the runner stop at the first failure and report the scenario id and step index.
- Throw `unknownStep` on an unwired `verb:target` so a scenario cannot reference missing machinery.
## DON'T
- Do not embed UI-framework calls in the scenario or the engine; the engine is `Foundation`-only and the framework binding lives in the registry.
- Do not grow the verb vocabulary casually; a wide verb set turns a scenario back into a script.
- Do not maintain device scenarios and headless tests as two separate corpora that drift; share one.
- Do not let a step silently pass when its handler is missing; the registry throws.
## Acceptance check
A conforming setup has: (1) flows expressed as scenario files, not procedural test code; (2) one `StepRegistry` per renderer, with the UI-framework binding confined to it and the engine `Foundation`-only; (3) the on-device registry driving page objects, not raw element queries; (4) the same scenario corpus replayed headless against the model layer when the UI has a pre-UI layer; (5) an unwired `verb:target` throwing `unknownStep`, verified by a test; (6) failures carrying scenario id and step index. A project that keeps its device scenarios and its headless coverage as two unrelated corpora fails the headless-parity intent of this rule.
## Companion rules
- `pre-ui-layer.md`: the headless registry drives the model layer; one corpus replayed headless and on-device is the strongest headless-parity form.
- `pom.md`: the on-device registry maps each step to a page-object action.
- `../core/testing-discipline.md`: real tests, on every change.
- `../core/no-shortcuts-first-principles.md`: an unwired step throws rather than silently passing.