saffron-ai 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -2
- package/dist-pkg/cli.js +106 -94
- package/package.json +2 -1
- package/skills/saffron/SKILL.md +152 -0
- package/skills/saffron/references/config.md +90 -0
- package/skills/saffron/references/syntax.md +167 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "saffron-ai",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Gherkin-native AI-fallback test runner: zero-token cached replay, runtime AI healing, honest reports",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "vitest run",
|
|
@@ -54,6 +54,7 @@
|
|
|
54
54
|
},
|
|
55
55
|
"files": [
|
|
56
56
|
"dist-pkg",
|
|
57
|
+
"skills",
|
|
57
58
|
"README.md",
|
|
58
59
|
"LICENSE"
|
|
59
60
|
]
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: saffron
|
|
3
|
+
description: Write, run, and maintain end-to-end UI tests with Saffron — a Gherkin-native runner where an AI agent records each scenario once and every later run replays at zero tokens. Use when creating or editing .feature / .saffron files, defining StepSets, reviewing .saffron/cache proposals, running `saffron run`, or when the user mentions Saffron, Gherkin scenarios, step sets, cached replay, or `saffron.config.json`.
|
|
4
|
+
license: Saffron Free Use License v1.0 (see the saffron-ai package LICENSE)
|
|
5
|
+
metadata:
|
|
6
|
+
author: saffron-ai
|
|
7
|
+
homepage: https://saffron-ai.lovable.app
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Saffron
|
|
11
|
+
|
|
12
|
+
Saffron runs plain-Gherkin scenarios in a real browser. There are **no
|
|
13
|
+
step definitions and no glue code**: on the first run an AI agent performs
|
|
14
|
+
each step and records what it did into a JSON cache; every later run
|
|
15
|
+
replays that cache with plain Playwright — zero AI calls, zero tokens,
|
|
16
|
+
~100 ms per scenario. When the UI drifts, the agent heals the failing
|
|
17
|
+
step mid-run and files a reviewable proposal.
|
|
18
|
+
|
|
19
|
+
Your job when writing tests is to make that economy work. Three rules
|
|
20
|
+
matter more than everything else:
|
|
21
|
+
|
|
22
|
+
## 1. Exact step text is the cache identity — reuse the vocabulary
|
|
23
|
+
|
|
24
|
+
A step is looked up by its exact wording. `When I visit the login page`
|
|
25
|
+
and `When I open the login page` are two different steps: the second one
|
|
26
|
+
costs a fresh AI recording (~$0.10–$2) even though the first already
|
|
27
|
+
replays for free.
|
|
28
|
+
|
|
29
|
+
**Before writing any step, look at what the project already knows:**
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npx saffron steps # every step: ● recorded, ● divergent, ○ unrecorded
|
|
33
|
+
npx saffron steps "login" # search
|
|
34
|
+
npx saffron steps --json # machine-readable
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Prefer a `●` recorded wording verbatim. Quoted values may differ freely
|
|
38
|
+
(`I enter "admin"` seeds from `I enter "bob"`), so parameterize with
|
|
39
|
+
quotes rather than inventing new phrasings. `saffron init` registers the
|
|
40
|
+
`saffron` MCP server with Claude Code, Cursor and VS Code — its
|
|
41
|
+
`search_steps` / `list_step_sets` tools give the same answer without a
|
|
42
|
+
shell.
|
|
43
|
+
|
|
44
|
+
## 2. `Then` steps are sacred — write them as the verdict
|
|
45
|
+
|
|
46
|
+
Saffron never heals an assertion: the agent may help *reach* a `Then`,
|
|
47
|
+
never make it pass. So `Then` lines must state exactly what must be true,
|
|
48
|
+
with stable text — never volatile values (prices, dates, counters). Put
|
|
49
|
+
the final assertions **last**; the trailing block of `Then` steps is
|
|
50
|
+
strict under every policy. Use `Given`/`When` for actions.
|
|
51
|
+
|
|
52
|
+
## 3. Never bake volatile or secret values into files
|
|
53
|
+
|
|
54
|
+
- Secrets: `{env:VAR}` — `When I enter "{env:ADMIN_PASSWORD}" in the password field`.
|
|
55
|
+
- Dates: write intent, not literals — "1 day from today" records as `{date+1}`.
|
|
56
|
+
- Dynamic display values: capture and compare — `I record the total as "first"` … `"first" should differ from the displayed total`.
|
|
57
|
+
|
|
58
|
+
## Writing a scenario — the shape that records cleanly
|
|
59
|
+
|
|
60
|
+
```gherkin
|
|
61
|
+
Feature: Checkout
|
|
62
|
+
|
|
63
|
+
Background:
|
|
64
|
+
Given I open the application
|
|
65
|
+
And I accept cookies if prompted # conditional steps are fine
|
|
66
|
+
|
|
67
|
+
@smoke
|
|
68
|
+
Scenario: Guest can complete checkout
|
|
69
|
+
When I add "Blue Widget" to the cart
|
|
70
|
+
And I place the order and wait for the order API to return 201
|
|
71
|
+
Then I should see "Order placed"
|
|
72
|
+
And the order request should have returned 201
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Guidelines the recorder rewards:
|
|
76
|
+
|
|
77
|
+
- One intent per step; 5–10 steps per scenario. Long scenarios heal badly.
|
|
78
|
+
- Name the page or control in the step (`on the checkout page`, `the "Save" button`).
|
|
79
|
+
- Data belongs in tables and doc strings, not in prose (see references).
|
|
80
|
+
- Conditional wording (`if prompted`) records as a no-op when absent.
|
|
81
|
+
- Real-world furniture just works — say it: dialogs ("and confirm the
|
|
82
|
+
dialog"), uploads ("upload the file "x.txt" as the attachment"),
|
|
83
|
+
drag-and-drop ("drag the card onto the done column"), iframes
|
|
84
|
+
(interact normally), network waits ("wait for the order API to return
|
|
85
|
+
201", "wait until the job status API reports "READY"").
|
|
86
|
+
|
|
87
|
+
## Repeated step groups → StepSets (`.saffron` files only)
|
|
88
|
+
|
|
89
|
+
`.saffron` is a superset of `.feature`: rename the file, nothing else
|
|
90
|
+
changes, and you gain the `StepSet:` keyword.
|
|
91
|
+
|
|
92
|
+
```gherkin
|
|
93
|
+
StepSet: Complete guest information # define — colon, like Scenario:
|
|
94
|
+
Given I am on the guest information page
|
|
95
|
+
When I enter guest name "Chathuranga"
|
|
96
|
+
And I click the continue button
|
|
97
|
+
Then I am not on the guest information page
|
|
98
|
+
|
|
99
|
+
Scenario: Checkout happy path
|
|
100
|
+
Given I am on the cart page
|
|
101
|
+
StepSet Complete guest information # invoke — no colon, like a step
|
|
102
|
+
Then I am on the checkout page
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
- Definition: `StepSet: <name>`. Invocation: `StepSet <name>` — the bare
|
|
106
|
+
name without the keyword is a silent parse error.
|
|
107
|
+
- Names are project-wide unique; sets resolve across files; no nesting.
|
|
108
|
+
- Application-wide flows (login, cookie banner) live in a sets-only
|
|
109
|
+
library file: `features/shared.steps.saffron` (still needs a
|
|
110
|
+
`Feature:` header; it yields no runnable scenarios).
|
|
111
|
+
- Start a set with a guard step, end it with an exit assertion.
|
|
112
|
+
|
|
113
|
+
## The workflow
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
npx saffron run # record misses, replay hits, heal failures
|
|
117
|
+
npx saffron run --filter @smoke # by tag
|
|
118
|
+
npx saffron run --no-agent # replay only (CI without AI access)
|
|
119
|
+
npx saffron accept # list pending proposals
|
|
120
|
+
npx saffron accept --all # promote verified proposals to caches
|
|
121
|
+
npx saffron report # open the HTML report
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Results: **green** = cached pass · **yellow** = passed with AI recording
|
|
125
|
+
or adaptation, a proposal awaits review · **red** = failed. Proposals
|
|
126
|
+
arrive stamped `verified ✓` (zero-AI proof replay passed) or
|
|
127
|
+
`UNVERIFIED ✗`; accept the former, investigate the latter. Commit
|
|
128
|
+
`.saffron/cache/` like snapshots; git-ignore `.saffron/reports/`.
|
|
129
|
+
|
|
130
|
+
**Never hand-edit files under `.saffron/cache/` or `.saffron/proposals/`**
|
|
131
|
+
unless deliberately authoring a manual cache (`"recordedBy": "manual"`).
|
|
132
|
+
To change behavior, change the `.feature`/`.saffron` text and let the run
|
|
133
|
+
re-record (mostly seeded from existing step recordings).
|
|
134
|
+
|
|
135
|
+
## Anti-patterns that cost money or hide bugs
|
|
136
|
+
|
|
137
|
+
| Don't | Do |
|
|
138
|
+
|---|---|
|
|
139
|
+
| Invent a synonym for an existing step | Reuse the `●` wording from `saffron steps` |
|
|
140
|
+
| Assert inside a `When` ("When I see the dashboard") | Act in `When`, assert in `Then` |
|
|
141
|
+
| `Then the price is "NOK 1,148"` | `Then the price should match "NOK [\d,]+"` intent, or capture + compare |
|
|
142
|
+
| Put a password literal in a step | `{env:VAR}` |
|
|
143
|
+
| Sleep/wait "for 3 seconds" | Wait for a UI signal or an API response |
|
|
144
|
+
| One 30-step scenario | Several 5–10 step scenarios + StepSets |
|
|
145
|
+
| Edit cache JSON to fix a test | Edit the scenario text; re-run |
|
|
146
|
+
|
|
147
|
+
## References
|
|
148
|
+
|
|
149
|
+
- [Syntax cheat sheet](references/syntax.md) — tables, doc strings,
|
|
150
|
+
outlines, secrets, dates, network waits, page furniture, StepSets.
|
|
151
|
+
- [Configuration & CI](references/config.md) — `saffron.config.json`,
|
|
152
|
+
flags, `--strict`, cross-browser, workers, heal model, secrets setup.
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# Configuration, CLI and CI
|
|
2
|
+
|
|
3
|
+
## Project layout
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
your-project/
|
|
7
|
+
features/ .feature and .saffron files
|
|
8
|
+
saffron.config.json
|
|
9
|
+
.saffron/
|
|
10
|
+
cache/ committed replay caches → commit
|
|
11
|
+
proposals/ pending AI proposals → review, then gone
|
|
12
|
+
history.jsonl one line per run (trends) → commit recommended
|
|
13
|
+
reports/ latest.html / latest.json → git-ignore
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## `saffron.config.json`
|
|
17
|
+
|
|
18
|
+
```json
|
|
19
|
+
{
|
|
20
|
+
"baseURL": "https://stage.your-app.com",
|
|
21
|
+
"features": "features",
|
|
22
|
+
"actionTimeoutMs": 5000,
|
|
23
|
+
"retries": 1,
|
|
24
|
+
"model": "claude-sonnet-5",
|
|
25
|
+
"healModel": "claude-haiku-4-5",
|
|
26
|
+
"maxTurns": 100,
|
|
27
|
+
"storageState": ".auth/state.json",
|
|
28
|
+
"strict": false,
|
|
29
|
+
"assertionPolicy": "strict",
|
|
30
|
+
"verifyProposals": true,
|
|
31
|
+
"reuseSteps": true,
|
|
32
|
+
"snapshotMode": "none",
|
|
33
|
+
"browser": "chromium",
|
|
34
|
+
"workers": 1
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
| Key | Meaning |
|
|
39
|
+
|---|---|
|
|
40
|
+
| `baseURL` | App under test; steps say "the login page", not full URLs |
|
|
41
|
+
| `storageState` | Playwright storage-state JSON so replays and the agent start authenticated (`npx playwright open --save-storage=.auth/state.json <url>`) |
|
|
42
|
+
| `strict` | Yellow (passed-with-adaptation) exits 1 until reviewed — cached-green-only CI |
|
|
43
|
+
| `assertionPolicy` | `strict` (default) or `adaptable-mid`; the final assertion block is always strict |
|
|
44
|
+
| `verifyProposals` | Proof-replay every recording zero-AI before filing (default true) |
|
|
45
|
+
| `reuseSteps` | Seed new recordings from existing step recordings (default true) |
|
|
46
|
+
| `snapshotMode` | `none` (default, ~60% fewer AI calls) or `full` for highly dynamic pages |
|
|
47
|
+
| `browser` | `chromium` / `firefox` / `webkit` — replay runs anywhere; recording and healing use Chromium |
|
|
48
|
+
| `workers` | Parallel replay workers; agent work stays sequential |
|
|
49
|
+
| `healModel` | Cheaper model for heal sessions only |
|
|
50
|
+
|
|
51
|
+
## CLI
|
|
52
|
+
|
|
53
|
+
| Command | Purpose |
|
|
54
|
+
|---|---|
|
|
55
|
+
| `saffron run [paths] [--filter @tag] [--no-agent] [--strict] [--browser b] [--workers n] [--heal-model m] [--headed]` | Run; cached replay, agent on misses/failures |
|
|
56
|
+
| `saffron accept [file \| --all] [--with-feature-edit] [--propagate]` | Promote proposals; `--with-feature-edit` rewrites adapted steps in the feature file; `--propagate` applies a heal's locator fix to every cache using that locator |
|
|
57
|
+
| `saffron reject [file \| --all]` | Discard proposals |
|
|
58
|
+
| `saffron steps [search] [--json] [--snippets]` | The step vocabulary with recorded/divergent/unrecorded badges |
|
|
59
|
+
| `saffron author <prose-file>` | Draft a `.saffron` file from plain-paragraph requirements using the project vocabulary |
|
|
60
|
+
| `saffron mcp` / `saffron lsp` | Vocabulary for AI assistants / language server for editors |
|
|
61
|
+
| `saffron init` | Install this skill into the project's agent directories and scaffold config |
|
|
62
|
+
| `saffron report` | Open the latest HTML report |
|
|
63
|
+
|
|
64
|
+
Exit codes: 0 green/yellow, 1 red (or yellow with `--strict`), 2 usage /
|
|
65
|
+
preflight (e.g. a missing `{env:VAR}`).
|
|
66
|
+
|
|
67
|
+
## AI access
|
|
68
|
+
|
|
69
|
+
Recording and healing need Claude credentials: `ANTHROPIC_API_KEY`, or a
|
|
70
|
+
Claude Code login on the machine. Replay-only runs (`--no-agent`) need
|
|
71
|
+
none — that is the normal CI mode once caches are committed.
|
|
72
|
+
|
|
73
|
+
## Recommended CI
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
npx playwright install chromium
|
|
77
|
+
npx saffron run --no-agent --strict # replay committed caches; no AI, no surprises
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Record and heal on developer machines (or a dedicated job with
|
|
81
|
+
credentials), review proposals in PRs like snapshot updates.
|
|
82
|
+
|
|
83
|
+
## Reading a report
|
|
84
|
+
|
|
85
|
+
Per scenario: status, duration, AI calls and tokens (including prompt
|
|
86
|
+
cache reads/writes — the real bill), adaptation narrative, cache diff and
|
|
87
|
+
suggested feature edit for yellows, drift chips when page fingerprints no
|
|
88
|
+
longer match. Trends vs. the previous run and 20-run sparklines come from
|
|
89
|
+
`.saffron/history.jsonl`; **chronic** scenarios (healing repeatedly) are
|
|
90
|
+
flagged — re-record those instead of paying for heals again.
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# Saffron syntax cheat sheet
|
|
2
|
+
|
|
3
|
+
Everything valid Gherkin is valid Saffron. `.saffron` files add `StepSet:`.
|
|
4
|
+
|
|
5
|
+
## Steps
|
|
6
|
+
|
|
7
|
+
```gherkin
|
|
8
|
+
Given I am on the login page # action (healable)
|
|
9
|
+
When I enter "standard_user" in the username field
|
|
10
|
+
And I click the "Log in" button
|
|
11
|
+
Then I should see the products page # assertion (never healed)
|
|
12
|
+
But I should not see an error banner
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
`And`/`But` inherit action-vs-assertion from the preceding step. The
|
|
16
|
+
trailing block of assertions is always strict; under the opt-in
|
|
17
|
+
`adaptable-mid` policy only mid-scenario checkpoints may be adapted.
|
|
18
|
+
|
|
19
|
+
## Scenario Outlines
|
|
20
|
+
|
|
21
|
+
One cache serves every row — `<param>` placeholders stay in the cache and
|
|
22
|
+
resolve per row at replay.
|
|
23
|
+
|
|
24
|
+
```gherkin
|
|
25
|
+
Scenario Outline: Failed login shows an error
|
|
26
|
+
When I enter "<username>" in the username field
|
|
27
|
+
And I enter "<password>" in the password field
|
|
28
|
+
And I click the "Log in" button
|
|
29
|
+
Then I should see the error message "<message>"
|
|
30
|
+
|
|
31
|
+
Examples:
|
|
32
|
+
| username | password | message |
|
|
33
|
+
| locked | secret | This user has been locked. |
|
|
34
|
+
| standard | wrong | Wrong username or password |
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Data tables
|
|
38
|
+
|
|
39
|
+
**2-column = key/value.** Values record as `<table:key>` references:
|
|
40
|
+
edit the values and replay stays free; change a key and the step
|
|
41
|
+
honestly re-records.
|
|
42
|
+
|
|
43
|
+
```gherkin
|
|
44
|
+
When I enter the following credentials
|
|
45
|
+
| username | premium_user |
|
|
46
|
+
| password | secret2 |
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
**Wider = records** (header row + one row per record). Cells record as
|
|
50
|
+
`<table:1:firstName>`, `<table:2:email>` (1-based rows). Editing any cell
|
|
51
|
+
replays free; adding/removing rows or renaming headers re-records.
|
|
52
|
+
|
|
53
|
+
```gherkin
|
|
54
|
+
When I add the following guests
|
|
55
|
+
| firstName | email | city |
|
|
56
|
+
| Alice | alice@test.com | Oslo |
|
|
57
|
+
| Bob | bob@test.com | Bergen |
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Doc strings
|
|
61
|
+
|
|
62
|
+
Content records as `<docstring>`; rewording replays free. A later
|
|
63
|
+
assertion on that content follows the edit too ("the saved note should
|
|
64
|
+
be shown").
|
|
65
|
+
|
|
66
|
+
```gherkin
|
|
67
|
+
When I leave a note
|
|
68
|
+
"""
|
|
69
|
+
Please deliver after 5pm.
|
|
70
|
+
"""
|
|
71
|
+
Then the saved note should be shown
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Secrets — `{env:VAR}`
|
|
75
|
+
|
|
76
|
+
```gherkin
|
|
77
|
+
When I enter "{env:ADMIN_USER}" in the username field
|
|
78
|
+
And I enter "{env:ADMIN_PASSWORD}" in the password field
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Resolved from the environment (or a git-ignored `.env`; real env wins) at
|
|
82
|
+
replay. Caches, proposals, reports and history contain only the token;
|
|
83
|
+
a missing variable fails fast by name. Honest note: during the *first*
|
|
84
|
+
recording the agent types the real value once — use rotatable staging
|
|
85
|
+
credentials.
|
|
86
|
+
|
|
87
|
+
## Dates and dynamic values
|
|
88
|
+
|
|
89
|
+
- Say the intent: "select a check-in date 1 day from today" → recorded
|
|
90
|
+
as `{date+1}`; stays valid every day. Formats: `{date}`, `{date-3}`,
|
|
91
|
+
`{date+1:DD.MM.YYYY}`.
|
|
92
|
+
- Capture and compare displayed values instead of literals:
|
|
93
|
+
|
|
94
|
+
```gherkin
|
|
95
|
+
When I record the displayed total as "before"
|
|
96
|
+
And I add another night
|
|
97
|
+
Then "before" should differ from the displayed total
|
|
98
|
+
And the total should match "NOK [\d,]+"
|
|
99
|
+
And the "Book" link should point to "/booking"
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Network-aware steps (no keywords — plain prose)
|
|
103
|
+
|
|
104
|
+
```gherkin
|
|
105
|
+
When I place the order and wait for the order API to return 201
|
|
106
|
+
And I wait until the job status API reports "READY"
|
|
107
|
+
Then the order request should have returned 201 # assertion, never healed
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Recorded as URL-pattern + method + status (+ body pattern) matchers;
|
|
111
|
+
waits are satisfied by responses from the triggering step onward, so
|
|
112
|
+
"click and wait" never races. Prefer these over any fixed sleep.
|
|
113
|
+
|
|
114
|
+
## Page furniture
|
|
115
|
+
|
|
116
|
+
```gherkin
|
|
117
|
+
When I click "Clear workspace" and confirm the dialog # alert/confirm/prompt
|
|
118
|
+
When I upload the file "examples/sample.txt" as the attachment
|
|
119
|
+
When I drag the task card onto the done column
|
|
120
|
+
When I enter "Great tool" in the feedback comment and send it # inside an iframe: just interact
|
|
121
|
+
Then the feedback widget should show "Thanks for: Great tool"
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Upload paths resolve from the working directory. Links that open new
|
|
125
|
+
tabs are followed automatically.
|
|
126
|
+
|
|
127
|
+
## StepSets (`.saffron` only)
|
|
128
|
+
|
|
129
|
+
```gherkin
|
|
130
|
+
Feature: Checkout
|
|
131
|
+
|
|
132
|
+
StepSet: Complete guest information
|
|
133
|
+
Given I am on the guest information page
|
|
134
|
+
When I enter guest name "Chathuranga"
|
|
135
|
+
And I click the continue button
|
|
136
|
+
Then I am not on the guest information page
|
|
137
|
+
|
|
138
|
+
Scenario: Checkout happy path
|
|
139
|
+
Given I am on the cart page
|
|
140
|
+
StepSet Complete guest information
|
|
141
|
+
Then I am on the checkout page
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
| Form | Syntax | Notes |
|
|
145
|
+
|---|---|---|
|
|
146
|
+
| Define | `StepSet: <name>` | Colon, like `Scenario:`; steps indented below |
|
|
147
|
+
| Invoke | `StepSet <name>` | No colon; sits anywhere among steps |
|
|
148
|
+
|
|
149
|
+
- Expanded at parse time: inlined steps cache and seed like ordinary
|
|
150
|
+
steps; editing a set makes every caller honestly stale (re-recorded
|
|
151
|
+
mostly seeded); heal edits route to the set definition.
|
|
152
|
+
- Names project-wide unique (also catches the colon typo at an
|
|
153
|
+
invocation). Sets may contain tables, doc strings and `<param>`
|
|
154
|
+
placeholders; no nesting; a set never runs standalone.
|
|
155
|
+
- Shared flows go in `features/shared.steps.saffron` — a sets-only
|
|
156
|
+
library file with a `Feature:` header and no scenarios.
|
|
157
|
+
- Assertions inside a set are checkpoints mid-scenario and part of the
|
|
158
|
+
strict final block when the set is invoked last.
|
|
159
|
+
|
|
160
|
+
## Tags
|
|
161
|
+
|
|
162
|
+
```gherkin
|
|
163
|
+
@smoke @checkout
|
|
164
|
+
Scenario: ...
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
`saffron run --filter @smoke`.
|