steppy-dev 0.3.0__py3-none-any.whl
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.
- steppy/CHANGELOG.md +862 -0
- steppy/__init__.py +9 -0
- steppy/__main__.py +13 -0
- steppy/_cli_common.py +183 -0
- steppy/_docker/Dockerfile.installed +53 -0
- steppy/_parallel/__init__.py +7 -0
- steppy/_parallel/compose.py +349 -0
- steppy/_parallel/event_bus.py +418 -0
- steppy/_parallel/failures.py +267 -0
- steppy/_parallel/fingerprint.py +141 -0
- steppy/_parallel/image.py +380 -0
- steppy/_parallel/lastfailed.py +125 -0
- steppy/_parallel/lifecycle.py +90 -0
- steppy/_parallel/preauth.py +109 -0
- steppy/_parallel/progress.py +101 -0
- steppy/_parallel/queue.py +385 -0
- steppy/_parallel/renderer.py +489 -0
- steppy/_parallel/retry.py +316 -0
- steppy/_player_bundle/THIRD_PARTY_LICENSES.txt +158 -0
- steppy/_player_bundle/__init__.py +100 -0
- steppy/_player_bundle/steppy-cursor-legacy.js +2 -0
- steppy/_player_bundle/steppy-cursor-legacy.js.map +1 -0
- steppy/_player_bundle/steppy-player.js +5419 -0
- steppy/_player_bundle/steppy-player.js.map +1 -0
- steppy/_player_bundle/steppy-video.js +472 -0
- steppy/_player_bundle/steppy-video.js.map +1 -0
- steppy/_templating.py +117 -0
- steppy/assertions.py +251 -0
- steppy/auth.py +493 -0
- steppy/build.py +2798 -0
- steppy/build_models.py +669 -0
- steppy/buildid.py +134 -0
- steppy/bundle/__init__.py +47 -0
- steppy/bundle/archive.py +239 -0
- steppy/bundle/classify.py +329 -0
- steppy/bundle/create.py +490 -0
- steppy/bundle/hashing.py +28 -0
- steppy/bundle/manifest.py +334 -0
- steppy/bundle/results.py +546 -0
- steppy/bundle_cli.py +455 -0
- steppy/captures.py +207 -0
- steppy/cli.py +2489 -0
- steppy/cloud/__init__.py +58 -0
- steppy/cloud/client.py +267 -0
- steppy/cloud/upload.py +497 -0
- steppy/compat.py +109 -0
- steppy/config.py +231 -0
- steppy/config_schema.py +87 -0
- steppy/contracts/__init__.py +54 -0
- steppy/contracts/bundle.py +215 -0
- steppy/contracts/report.py +346 -0
- steppy/contracts/tours.py +505 -0
- steppy/coordinator.py +1352 -0
- steppy/coverage/__init__.py +9 -0
- steppy/coverage/gaps.py +190 -0
- steppy/coverage/manifest.py +193 -0
- steppy/coverage/merge.py +363 -0
- steppy/coverage/normalize.py +174 -0
- steppy/coverage/report.py +178 -0
- steppy/coverage/sourcemap.py +219 -0
- steppy/coverage/store.py +473 -0
- steppy/coverage/triage.py +151 -0
- steppy/coverage_cli.py +265 -0
- steppy/durations.py +48 -0
- steppy/exclusive.py +178 -0
- steppy/flaky_cli.py +169 -0
- steppy/generator.py +633 -0
- steppy/guide.py +1467 -0
- steppy/history.py +352 -0
- steppy/init_cli.py +330 -0
- steppy/locales.py +282 -0
- steppy/migrate/__init__.py +35 -0
- steppy/migrate/apply.py +194 -0
- steppy/migrate/plan.py +199 -0
- steppy/migrate/rules.py +465 -0
- steppy/migrate/verify.py +163 -0
- steppy/migrate_cli.py +152 -0
- steppy/models.py +2539 -0
- steppy/naming.py +37 -0
- steppy/parser.py +475 -0
- steppy/pdf.py +193 -0
- steppy/presets.py +284 -0
- steppy/record_cli.py +796 -0
- steppy/report/__init__.py +37 -0
- steppy/report/assets.py +197 -0
- steppy/report/build.py +261 -0
- steppy/report/coverage_source.py +277 -0
- steppy/report/model.py +423 -0
- steppy/report/render.py +303 -0
- steppy/report/source.py +238 -0
- steppy/report_cli.py +173 -0
- steppy/runmeta.py +114 -0
- steppy/selection.py +183 -0
- steppy/sitemap.py +205 -0
- steppy/skills/steppy-app-tours/SKILL.md +448 -0
- steppy/skills/steppy-ci-report/SKILL.md +567 -0
- steppy/skills/steppy-scenario-author/SKILL.md +2176 -0
- steppy/skills/steppy-template/SKILL.md +676 -0
- steppy/templates/_steppy_events.py +291 -0
- steppy/templates/_steppy_tours.py +404 -0
- steppy/templates/angular/component-interactive.ts.j2 +109 -0
- steppy/templates/angular/component-video.ts.j2 +112 -0
- steppy/templates/angular/component.ts.j2 +229 -0
- steppy/templates/angular/content.ts.j2 +31 -0
- steppy/templates/angular/gitignore.j2 +3 -0
- steppy/templates/angular/group.component.ts.j2 +39 -0
- steppy/templates/angular/onboarding.component.ts.j2 +43 -0
- steppy/templates/angular/routes.ts.j2 +34 -0
- steppy/templates/ci/github-actions.yml +284 -0
- steppy/templates/ci/gitlab-ci.yml +178 -0
- steppy/templates/conftest.py.j2 +2405 -0
- steppy/templates/guide.book.html.j2 +145 -0
- steppy/templates/guide.fragment.html.j2 +257 -0
- steppy/templates/guide.html.j2 +301 -0
- steppy/templates/guide.md.j2 +18 -0
- steppy/templates/init/config.yaml +124 -0
- steppy/templates/init/credentials.yaml.example +30 -0
- steppy/templates/init/scenarios/example--first-flow.md +53 -0
- steppy/templates/report/_head.html.j2 +14 -0
- steppy/templates/report/_mark.html.j2 +3 -0
- steppy/templates/report/coverage.html.j2 +124 -0
- steppy/templates/report/index.html.j2 +203 -0
- steppy/templates/report/report.css +401 -0
- steppy/templates/report/scenario.html.j2 +137 -0
- steppy/templates/static/_headers.j2 +30 -0
- steppy/templates/static/_mode_links.html.j2 +22 -0
- steppy/templates/static/_seo_head.html.j2 +37 -0
- steppy/templates/static/cursor.js.j2 +10 -0
- steppy/templates/static/group.html.j2 +78 -0
- steppy/templates/static/guides.css +678 -0
- steppy/templates/static/index.html.j2 +79 -0
- steppy/templates/static/interactive.html.j2 +100 -0
- steppy/templates/static/onboarding.html.j2 +64 -0
- steppy/templates/static/page.html.j2 +145 -0
- steppy/templates/static/robots.txt.j2 +7 -0
- steppy/templates/static/video.html.j2 +135 -0
- steppy/templates/test_scenario.py.j2 +767 -0
- steppy/testids.py +166 -0
- steppy/totp.py +26 -0
- steppy/tour_spec.py +163 -0
- steppy/tours/__init__.py +22 -0
- steppy/tours/actions.py +402 -0
- steppy/tours/compiler.py +144 -0
- steppy/tours/diagnostics.py +137 -0
- steppy/tours/emit.py +239 -0
- steppy/tours/ids.py +40 -0
- steppy/tours/inference.py +399 -0
- steppy/tours/model.py +120 -0
- steppy/tours/paths.py +126 -0
- steppy/tours/pipeline.py +277 -0
- steppy/tours/publish.py +124 -0
- steppy/tours/replay.py +234 -0
- steppy/tours/routes.py +175 -0
- steppy/tours/settings.py +164 -0
- steppy/tours/text.py +237 -0
- steppy/tours_cli.py +219 -0
- steppy/tts/__init__.py +280 -0
- steppy/tts/cache.py +185 -0
- steppy/tts/presets.py +125 -0
- steppy/tts/providers/__init__.py +6 -0
- steppy/tts/providers/http.py +184 -0
- steppy/tts/providers/local.py +207 -0
- steppy/tts/providers/null.py +28 -0
- steppy/tts/registry.py +201 -0
- steppy/tts/track.py +158 -0
- steppy/tts/types.py +94 -0
- steppy/update_cli.py +460 -0
- steppy/video.py +730 -0
- steppy/video_cache.py +239 -0
- steppy/video_cli.py +271 -0
- steppy/worker.py +301 -0
- steppy_dev-0.3.0.dist-info/METADATA +1335 -0
- steppy_dev-0.3.0.dist-info/RECORD +177 -0
- steppy_dev-0.3.0.dist-info/WHEEL +4 -0
- steppy_dev-0.3.0.dist-info/entry_points.txt +2 -0
- steppy_dev-0.3.0.dist-info/licenses/LICENSE +202 -0
- steppy_dev-0.3.0.dist-info/licenses/NOTICE +13 -0
|
@@ -0,0 +1,567 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: steppy-ci-report
|
|
3
|
+
description: |
|
|
4
|
+
How to run Steppy in CI and publish what came out of it. Use whenever a user asks to
|
|
5
|
+
"set up Steppy in CI / GitHub Actions / GitLab CI", "add a CI workflow", "publish the
|
|
6
|
+
test results", "publish to GitHub Pages / GitLab Pages", "build a run report", "show
|
|
7
|
+
which step failed", "why did my report render empty", "add a coverage badge", "what
|
|
8
|
+
isn't my suite testing", "why is my coverage percentage so low", "set up
|
|
9
|
+
coverage.non_ui", "package a run", "attach the results to a build", "tour replay
|
|
10
|
+
failed", "set up in-app tour CI", or edits
|
|
11
|
+
`.github/workflows/steppy.yml`, `.gitlab-ci.yml`, or the `coverage:` / `report:`
|
|
12
|
+
blocks in `config.yaml`.
|
|
13
|
+
|
|
14
|
+
Covers: `steppy report` (the static run-report site), `steppy run --bundle` +
|
|
15
|
+
`steppy bundle`, the coverage map end to end (record → merge → refresh → gaps), the
|
|
16
|
+
e2e-surface badge and the `coverage.non_ui` declaration it depends on, the in-app tour
|
|
17
|
+
replays (`test_<slug>__tour`) and their pipeline order, and the two shipped CI templates.
|
|
18
|
+
|
|
19
|
+
For writing scenarios use `steppy-scenario-author`. For styling guides use
|
|
20
|
+
`steppy-template`. For wiring tours into the app's code use `steppy-app-tours`. This
|
|
21
|
+
skill is about shipping results, not authoring or theming.
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
# Running Steppy in CI, and publishing the results
|
|
25
|
+
|
|
26
|
+
Three artifacts come out of a run, and they answer different questions:
|
|
27
|
+
|
|
28
|
+
| Artifact | Question it answers | Command |
|
|
29
|
+
|---|---|---|
|
|
30
|
+
| **Run report** | Did it pass, and *which step* failed? | `steppy report` |
|
|
31
|
+
| **Results bundle** | Can I hand this whole run to someone else? | `steppy bundle create` |
|
|
32
|
+
| **Guides** | What does the app look like to a user? | `steppy guide` / `steppy build` |
|
|
33
|
+
|
|
34
|
+
Guides are a different job (see `steppy-scenario-author`). This skill is the first two,
|
|
35
|
+
plus the coverage map that feeds the report's badge.
|
|
36
|
+
|
|
37
|
+
> **One thing to be clear about up front.** `report:` in `config.yaml` is an *unrelated*
|
|
38
|
+
> block — it configures pytest artifacts (JUnit XML, an HTML report, Playwright traces
|
|
39
|
+
> and videos, reruns). The `steppy-dev[report]` extra is `pytest-html`. Neither has anything
|
|
40
|
+
> to do with the `steppy report` command. If a user says "the report", ask which they
|
|
41
|
+
> mean when it is ambiguous.
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## The CI loop
|
|
46
|
+
|
|
47
|
+
Two shipped templates, both installed by `steppy init`:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
steppy init --ci # → .github/workflows/steppy.yml
|
|
51
|
+
steppy init --ci-gitlab # → .gitlab-ci.yml
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Separate booleans rather than `--ci <vendor>`, because `--ci` is a flag people already
|
|
55
|
+
type. Run `steppy update` after upgrading Steppy and both files are reconciled with the
|
|
56
|
+
installed version.
|
|
57
|
+
|
|
58
|
+
### Two ways to run it on GitHub
|
|
59
|
+
|
|
60
|
+
The scaffolded workflow is the **owned** copy: every trap documented as a comment beside
|
|
61
|
+
the step it applies to, yours to edit. The alternative is the **reusable** workflow Steppy
|
|
62
|
+
ships, called by ref:
|
|
63
|
+
|
|
64
|
+
```yaml
|
|
65
|
+
jobs:
|
|
66
|
+
e2e:
|
|
67
|
+
uses: Steppy-dev/steppy/.github/workflows/steppy-e2e.yml@v0.3.0
|
|
68
|
+
with:
|
|
69
|
+
compose-file: docker-compose.yml
|
|
70
|
+
secrets:
|
|
71
|
+
steppy-env: ${{ secrets.STEPPY_ENV }}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Pin a tag, never a branch — that ref selects both the recipe and, by default, the Steppy
|
|
75
|
+
version it installs, so a suite's tooling changes only when someone edits that line.
|
|
76
|
+
|
|
77
|
+
It exposes `verdict`, `exit-code`, `scenarios-not-passed` and `badge-percent` as job
|
|
78
|
+
outputs, read from `report.json`, so a caller can gate or comment on the result without
|
|
79
|
+
re-parsing anything. Credentials arrive as **one optional `steppy-env` secret** holding
|
|
80
|
+
dotenv `KEY=VALUE` lines — the `${VAR}` names in a project's config.yaml belong to that
|
|
81
|
+
project and cannot be declared as individual secrets upstream.
|
|
82
|
+
|
|
83
|
+
The trade against a composite action: a reusable workflow **is** the job, so a caller
|
|
84
|
+
cannot interleave its own steps into it. For an e2e repo whose compose pulls published
|
|
85
|
+
images that costs nothing. Someone who needs to build an image first, in the same job, has
|
|
86
|
+
found the reason to promote it to a composite action.
|
|
87
|
+
|
|
88
|
+
The order that matters:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
docker compose up -d --wait # 0. the app the scenarios talk to
|
|
92
|
+
steppy generate # 1. rebuild the tests from scenarios
|
|
93
|
+
steppy run --trace --bundle # 2. run them, recording what a report needs
|
|
94
|
+
steppy report # 3. render output/report/
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
With in-app tours enabled, two steps come *before* step 0, because the app must be built
|
|
98
|
+
with the current tour files: `steppy tours --check` (or `steppy tours` when CI
|
|
99
|
+
builds the app itself), then `docker compose up --build`. See "In-app tour replays" below;
|
|
100
|
+
the shipped CI templates carry both as commented steps to uncomment.
|
|
101
|
+
|
|
102
|
+
### Step 0 — the stack under test
|
|
103
|
+
|
|
104
|
+
Steppy runs scenarios; it does not run your application. On a single-worker run nothing
|
|
105
|
+
starts your app for you, so CI has to — either `docker compose up` in the job, or an
|
|
106
|
+
`apps.<role>.url` pointing at an already-deployed environment.
|
|
107
|
+
|
|
108
|
+
A hosted runner is just another Linux box with Docker on it: if `docker compose up` then
|
|
109
|
+
`steppy run` works on a developer's laptop against that repo, CI is the same two commands
|
|
110
|
+
plus artifact plumbing. Two properties of **your** compose file decide whether it actually
|
|
111
|
+
works, and both fail the same way — every scenario red, no error that names the cause:
|
|
112
|
+
|
|
113
|
+
- **`--wait` only waits on services that declare a `healthcheck:`.** With none, it returns
|
|
114
|
+
as soon as the containers are created and the run starts against a port that is not
|
|
115
|
+
serving yet. On a suite of any size the circuit breaker then aborts after ~10 identical
|
|
116
|
+
failures, which is the good outcome — it fails in a minute instead of thirty.
|
|
117
|
+
- **The scenarios and the seed scripts run on the runner, not in a container.** So the app
|
|
118
|
+
— and the database, if `seed/*.py` touches it — must publish its ports, and
|
|
119
|
+
`apps.<role>.url` points at `localhost`. (Under `--workers N` this inverts: seeds run
|
|
120
|
+
*inside* the worker container, where host URLs do not resolve.)
|
|
121
|
+
|
|
122
|
+
Tear down with `docker compose down -v` in an always-run step. Without `-v` the next run
|
|
123
|
+
inherits the last one's seeded database, which produces failures that reproduce in CI and
|
|
124
|
+
never locally. Dump `docker compose logs` on failure too — when a suite goes red the first
|
|
125
|
+
question is always "the tests or the stack?", and that is the cheapest way to know.
|
|
126
|
+
|
|
127
|
+
`--workers N ≥ 2` changes this picture entirely: the coordinator brings up **one full
|
|
128
|
+
stack per worker** from `parallel.compose_file` and tears them down itself, so the
|
|
129
|
+
explicit compose steps come out. N copies of an app on one hosted runner is a lot — that
|
|
130
|
+
mode earns its keep on a large suite or a self-hosted runner, not on a thirty-scenario one.
|
|
131
|
+
|
|
132
|
+
**`--bundle` is the load-bearing flag.** It is opt-in because it changes the pytest
|
|
133
|
+
invocation, and without it the report can still tell you a test failed but *not which
|
|
134
|
+
step* — there are no per-step event records to read. It sets three things:
|
|
135
|
+
`STEPPY_EVENTS_DIR` (per-test and per-step JSONL), a fixed-path `--junitxml`, and
|
|
136
|
+
`STEPPY_RUN_ID`. Export `STEPPY_STEP_EVENTS=0` to keep the rest and drop step
|
|
137
|
+
granularity.
|
|
138
|
+
|
|
139
|
+
**`steppy report` exits 0 even when the run failed.** It describes rendering, not the
|
|
140
|
+
run. Gate the build on `steppy run` itself, or read `totals.exit_code` out of
|
|
141
|
+
`report.json`. A report step that goes red for a red run is a step people delete — so
|
|
142
|
+
run it with `if: always()` (GitHub) or in `after_script` (GitLab), because a failing run
|
|
143
|
+
is exactly when it matters.
|
|
144
|
+
|
|
145
|
+
### In-app tour replays
|
|
146
|
+
|
|
147
|
+
When `config.yaml` has `tours.enabled: true`, every scenario that opts into an in-app tour
|
|
148
|
+
(`tour:` in its frontmatter) gets a second test, **`test_<slug>__tour`**, appended to its own
|
|
149
|
+
test file by `steppy generate`. It is the feature's CI promise: a developer who renames a
|
|
150
|
+
test-id a tour points at gets a red build naming the tour, the step and the test-id, instead
|
|
151
|
+
of a tour that quietly breaks in production. The replay:
|
|
152
|
+
|
|
153
|
+
1. runs the steps before the tour's start point, as the scenario test does;
|
|
154
|
+
2. starts the tour through the host element's API;
|
|
155
|
+
3. for each tour step, checks the popover shows that step anchored on the element it acts
|
|
156
|
+
on (the spotlight around its centre, the popover not covering it), performs **the
|
|
157
|
+
scenario's own action code** for it, and checks the tour moved on;
|
|
158
|
+
4. checks `tour-complete` fired and no `tour-error` did.
|
|
159
|
+
|
|
160
|
+
Because it lives in the scenario's file, `--scenario` / `--tag` selection, `--workers N`,
|
|
161
|
+
`lastfailed` and JUnit treat it like its scenario — running the replay needs no extra
|
|
162
|
+
step. What does need one is getting the *current* tour files into the app before it
|
|
163
|
+
starts:
|
|
164
|
+
|
|
165
|
+
**The order is the whole trick.** The replay tests the tour files *the running app serves*,
|
|
166
|
+
so they have to be in the app image before the app starts:
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
steppy tours # 1. write the tour files into the app's app-assets folder
|
|
170
|
+
docker compose up -d --build --wait # 2. build the app image WITH them, then start it
|
|
171
|
+
steppy generate # 3. tests + replays
|
|
172
|
+
steppy run --trace --bundle # 4. the replays run with their scenarios
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
(In a one-repository setup the app's own build usually runs `steppy tours`, and the
|
|
176
|
+
committed tour files are what step 2 bakes in.) Start the app first and the replays exercise
|
|
177
|
+
whatever tour files the image already had.
|
|
178
|
+
|
|
179
|
+
**Reading a failure.** One message carries the whole diagnosis:
|
|
180
|
+
|
|
181
|
+
```text
|
|
182
|
+
TourReplayError: [tour create-invoice] step 'invoice-title' (scenario step 4 "Name the invoice", fill,
|
|
183
|
+
test-id "invoice-title"): target-timeout: the overlay never found [data-testid="invoice-title"]
|
|
184
|
+
hint: the test-id was renamed or removed in the app, or the element appears too late
|
|
185
|
+
page: /invoices tour: active, current 'invoice-title' bubble: fallback navigations since start: 0
|
|
186
|
+
last events: tour-start(bundled) | tour-step(new-invoice-btn) | tour-error(target-timeout, invoice-title)
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
The first line is *which* — tour, tour step, scenario step, action, test-id — and the
|
|
190
|
+
overlay's error reason; `hint:` is the likely cause; `page:` / `tour:` / `bubble:` are the
|
|
191
|
+
state at the moment it failed; `last events:` is the overlay's own event tail
|
|
192
|
+
(`tour-start(bundled)` says the app's bundled tour files were used).
|
|
193
|
+
|
|
194
|
+
Things worth knowing before wiring it in:
|
|
195
|
+
|
|
196
|
+
- **The kill switch.** With tours enabled, the generated tests set
|
|
197
|
+
`globalThis.__STEPPY_TOURS_DISABLED__ = true` in every browser context, so a tour the
|
|
198
|
+
app starts on its own never covers what an ordinary test clicks; only the replays lift it.
|
|
199
|
+
Another E2E suite testing the same app should set it too.
|
|
200
|
+
- `steppy run -- -k "not __tour"` skips the replays for one run; `tours.replay: false`
|
|
201
|
+
stops generating them.
|
|
202
|
+
- `steppy run --guide` deselects them — guide captures are not tours.
|
|
203
|
+
- Replays record **no coverage**; `--coverage` numbers come from the scenario tests alone.
|
|
204
|
+
- `STEPPY_TOUR_EXPECT_BUILD_ID=<id>` makes a replay also check that the app serves tours
|
|
205
|
+
for that build — set it to the id the app build stamped to catch an image baked from the
|
|
206
|
+
wrong tour files. It reaches every worker under `--workers N` too.
|
|
207
|
+
- A replay re-runs its scenario's seed and steps, so the flow must be repeatable.
|
|
208
|
+
- **`steppy tours --check` is a freshness gate**: it writes nothing and exits 1 when a
|
|
209
|
+
committed `app-assets` copy is missing or its *content* no longer matches what the
|
|
210
|
+
scenarios compile to. No browser, no app — a cheap step that catches a scenario edited
|
|
211
|
+
without regenerating the committed tour files. When it reports files that "differ only
|
|
212
|
+
in line endings", git converted them on checkout (`core.autocrlf` on a Windows runner):
|
|
213
|
+
regenerating cannot fix that — mark the folder `-text` in the app repository's
|
|
214
|
+
`.gitattributes`. It compares each copy for the build id
|
|
215
|
+
that copy records and only prints a note when that differs from the one resolved now
|
|
216
|
+
(under `git-sha` a committed copy always carries an earlier commit), so it passes on a
|
|
217
|
+
fresh checkout; the gitignored `output/tours/` tree is not checked. It needs the app's
|
|
218
|
+
checkout (a missing `app-assets` folder is an error; `generate` only warns
|
|
219
|
+
`app-assets-skipped`), and it re-renders under CI's environment, so any `${VAR}` in
|
|
220
|
+
`tours.guide_base_url` or an app URL must resolve as it did where the files were
|
|
221
|
+
written. To gate the *build id* the running app serves, use
|
|
222
|
+
`STEPPY_TOUR_EXPECT_BUILD_ID` above.
|
|
223
|
+
- **Wiring it into a scaffolded pipeline.** `steppy init --ci` / `--ci-gitlab` write the
|
|
224
|
+
tour steps commented out (tours are opt-in). When the project enables tours, uncomment
|
|
225
|
+
them; leave them out of the reusable `steppy-e2e.yml` workflow, whose caller cannot
|
|
226
|
+
interleave steps — a project that builds its app in CI promotes it to a composite
|
|
227
|
+
action (see above).
|
|
228
|
+
- **In the run report, a replay is a row of its own under its scenario.** When both fail in
|
|
229
|
+
the same variant they share the failure screenshot, so read them as one cause first. A
|
|
230
|
+
red replay under a *green* scenario is the tour, not the app.
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
## `steppy report` — the run report site
|
|
235
|
+
|
|
236
|
+
```bash
|
|
237
|
+
steppy report # this project's live output/
|
|
238
|
+
steppy report steppy-bundle-<run-id>.tar.zst # a bundle downloaded from CI
|
|
239
|
+
steppy report ./unpacked-bundle -o public # after tar -xf
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
All three inputs produce the same report. That is the point: an assistant debugging a
|
|
243
|
+
failure from a downloaded CI artifact sees exactly what CI saw.
|
|
244
|
+
|
|
245
|
+
### What the site contains
|
|
246
|
+
|
|
247
|
+
- **`index.html`** — run identity (run id, commit, branch, PR, executor), the spec
|
|
248
|
+
dimensions, outcome counts, **failure causes grouped by cause**, a scenario ×
|
|
249
|
+
(locale, layout) outcome matrix, warnings, and a coverage card.
|
|
250
|
+
- **`scenarios/<slug>.html`** — per (locale, layout) variant: outcome, duration, the
|
|
251
|
+
failing step **named**, the decoded failure tail, the step timeline (number, title,
|
|
252
|
+
action, elapsed) and the captures for each step.
|
|
253
|
+
- **`coverage.html`** + **`badge.json`** — when the run recorded coverage.
|
|
254
|
+
- **`report.json`** — the same model as a versioned wire contract
|
|
255
|
+
(`steppy_report: 1`, shape at `contracts/report.schema.json`). Deterministic: no
|
|
256
|
+
render-time clock, no absolute paths, so two renders of one run are byte-identical and
|
|
257
|
+
CI can diff them.
|
|
258
|
+
|
|
259
|
+
### Failure causes are the feature
|
|
260
|
+
|
|
261
|
+
The report groups failures by *normalised* failure text, biggest first — the same
|
|
262
|
+
grouping the parallel console summary uses. One broken selector across forty scenarios
|
|
263
|
+
is **one row with a count**, not forty tracebacks, and the full tail is shown once per
|
|
264
|
+
cause. When a user says "the CI output is unreadable", this is the answer.
|
|
265
|
+
|
|
266
|
+
### Flags worth knowing
|
|
267
|
+
|
|
268
|
+
| Flag | Why |
|
|
269
|
+
|---|---|
|
|
270
|
+
| `--screenshots failed \| all \| none` | Default `failed`: the captures of variants that did not pass, plus every failure screenshot. `all` on a 4-locale × 2-layout corpus is hundreds of MB pushed on every run. Captures identical across layouts are copied once. |
|
|
271
|
+
| `-o/--output` | Default `output/report`. See the warning below before pointing it elsewhere. |
|
|
272
|
+
| `--badge-surface all \| endpoints \| routes` | What the badge measures. `all` = endpoints + routes. |
|
|
273
|
+
| `--dry-run` | Report what would be written, write nothing. |
|
|
274
|
+
| `--json` | Print `report.json` to stdout instead of a summary — for a CI annotation. |
|
|
275
|
+
| `--force` | Replace a non-empty output directory. |
|
|
276
|
+
| `--open` | Open it in a browser locally. |
|
|
277
|
+
|
|
278
|
+
**`output/report/` is the one path `steppy bundle create` skips by name.** The rendered
|
|
279
|
+
site contains *copies* of the screenshots a bundle already carries, so archiving it
|
|
280
|
+
would roughly double the payload. Point `-o` somewhere else under `output/` and the
|
|
281
|
+
command warns you about exactly that. There is deliberately no `output.report_dir`
|
|
282
|
+
config key — one writer, no automated reader, and a live `report_dir` sitting one letter
|
|
283
|
+
from the deprecated `output.reports_dir` would be a permanent trap.
|
|
284
|
+
|
|
285
|
+
### It is honest about what it could not know
|
|
286
|
+
|
|
287
|
+
This matters when reading a report that looks thin — the report says which case it is
|
|
288
|
+
rather than rendering an empty box, and `report.json`'s `capabilities` block carries the
|
|
289
|
+
same information for a machine:
|
|
290
|
+
|
|
291
|
+
| What you see | What it means |
|
|
292
|
+
|---|---|
|
|
293
|
+
| "No per-step timeline was recorded" | The run did not pass `--bundle`, or set `STEPPY_STEP_EVENTS=0`, or the tests were generated before per-step markers existed (`steppy generate` again). |
|
|
294
|
+
| "This run recorded no test outcomes" + verdict `unknown` | No `--bundle`, so there are no event records. The scenarios listed were reconstructed from captures on disk. **Not** `passed` — that would be a claim with no evidence. |
|
|
295
|
+
| "Assert strength is not shown" | Coverage was rebuilt from a detached bundle's raw records, which carry no scenario markdown to count assert steps in. Every endpoint would otherwise read as weakly covered. |
|
|
296
|
+
| "Truncated" | A `--max-size` budget dropped artifacts that belonged in the bundle. Screenshots and sidecars are never dropped. |
|
|
297
|
+
|
|
298
|
+
Traces and videos are **named, not copied** — a Playwright trace needs
|
|
299
|
+
`playwright show-trace` to open, so a link would be a dead end.
|
|
300
|
+
|
|
301
|
+
---
|
|
302
|
+
|
|
303
|
+
## Publishing to Pages
|
|
304
|
+
|
|
305
|
+
The site is relative and self-contained, so publishing it is a copy. Both shipped
|
|
306
|
+
templates carry the job **commented out**, with the warning below. Do not arm it for a
|
|
307
|
+
user without saying this out loud:
|
|
308
|
+
|
|
309
|
+
> **Pages is public.** Screenshots show the app with real-looking data in it.
|
|
310
|
+
> `screenshot.mask` redacts fields at capture time and the report marks every page
|
|
311
|
+
> `noindex` with a `Disallow: /` robots.txt — but a leak is permanent and cannot be
|
|
312
|
+
> taken back. This is for showcase and open-source repos. For client work, read the
|
|
313
|
+
> report from the CI artifact instead.
|
|
314
|
+
|
|
315
|
+
Two vendor facts worth carrying:
|
|
316
|
+
|
|
317
|
+
- **GitHub Pages**: publish as an **artifact** (`actions/configure-pages` +
|
|
318
|
+
`upload-pages-artifact` + `deploy-pages`), never by committing to a `gh-pages`
|
|
319
|
+
branch — screenshots pushed on every run grow the repository's history forever and
|
|
320
|
+
cannot be pruned without a rewrite. Needs `permissions: {pages: write, id-token:
|
|
321
|
+
write}` and Settings → Pages → Source: "GitHub Actions".
|
|
322
|
+
- **GitLab Pages** can be restricted to **project members** (Settings → Pages → Access
|
|
323
|
+
Control). That is the one place it beats GitHub's for this, and it is what makes
|
|
324
|
+
publishing a run report viable for client work. It is a project setting, not something
|
|
325
|
+
the pipeline can guarantee — check it before the first deploy.
|
|
326
|
+
|
|
327
|
+
Gate the deploy on the default branch either way: a report from every PR publishes work
|
|
328
|
+
in progress, and fork PRs cannot get an id-token anyway.
|
|
329
|
+
|
|
330
|
+
**A repository has exactly one Pages site.** A project that publishes both the run report
|
|
331
|
+
and generated guides (`steppy build`) has to place them — `site/report/` and
|
|
332
|
+
`site/guides/` — and that decision is worth making before anything links to either,
|
|
333
|
+
because moving the report also moves `badge.json`, and that URL is a contract with every
|
|
334
|
+
README and dashboard pointing at it. Deciding it late means either a dead badge or a
|
|
335
|
+
permanent redirect nobody owns.
|
|
336
|
+
|
|
337
|
+
The badge is then served at `<pages-url>/badge.json`, **when the run recorded coverage** —
|
|
338
|
+
`render.py` writes the file only when there is a coverage section, so a badge URL added
|
|
339
|
+
before `--coverage` is a 404 that reads to a visitor as an abandoned project:
|
|
340
|
+
|
|
341
|
+
```markdown
|
|
342
|
+

|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
A shields **endpoint JSON**, not a generated SVG — it inherits whatever style the rest of
|
|
346
|
+
the README's badge row uses, and no image generation lands in the repo.
|
|
347
|
+
|
|
348
|
+
---
|
|
349
|
+
|
|
350
|
+
## The coverage map
|
|
351
|
+
|
|
352
|
+
**This is not line coverage.** Codecov answers "what percentage of source lines ran".
|
|
353
|
+
Steppy's map answers *which of the routes and API endpoints I declared has any e2e
|
|
354
|
+
scenario actually exercised* — which nothing else measures. Everything below is
|
|
355
|
+
deterministic: no LLM in recording, merging, or gap computation.
|
|
356
|
+
|
|
357
|
+
### How it is built — four stages
|
|
358
|
+
|
|
359
|
+
**1. Record** (`steppy run --coverage`, sets `STEPPY_COVERAGE=1`). The *generated
|
|
360
|
+
conftest* does it, so there is zero overhead when the flag is off. Three signals per
|
|
361
|
+
scenario run:
|
|
362
|
+
|
|
363
|
+
- **Route trail** — Playwright's `framenavigated`, which is authoritative because it
|
|
364
|
+
fires for History-API changes too, plus an injected hook tagging push/replace/pop/hash.
|
|
365
|
+
- **API endpoints** — every request's `{method, url}`, filtered to
|
|
366
|
+
`coverage.api_resource_types` (default `xhr`, `fetch`).
|
|
367
|
+
- **Source modules** — V8 coverage over raw CDP `Profiler.startPreciseCoverage`
|
|
368
|
+
(playwright-python has no `page.coverage` API), with inline `data:` source maps
|
|
369
|
+
captured and gzipped.
|
|
370
|
+
|
|
371
|
+
Raw records land at
|
|
372
|
+
`output/coverage/raw/<run_id>/<scenario>/<fixture>__<locale>-<layout>.json`. The recorder
|
|
373
|
+
is deliberately **dumb** — it writes observations, nothing interpreted.
|
|
374
|
+
|
|
375
|
+
**2. Merge** (automatic after the run, or `steppy coverage merge`). *All* interpretation
|
|
376
|
+
happens here: dynamic route segments cluster (`/invoices/123` → `/invoices/:id`, extra
|
|
377
|
+
regexes via `coverage.route_param_patterns`), API attribution by longest `base_urls`
|
|
378
|
+
prefix, source-map decode with a bundle-granularity fallback. Upserts replace a
|
|
379
|
+
scenario's whole contribution, so re-running one scenario cannot leave stale rows.
|
|
380
|
+
|
|
381
|
+
**3. Refresh** (`steppy coverage refresh`) writes the **declared surface** — the
|
|
382
|
+
denominator. This is the step people forget, and without it every percentage is
|
|
383
|
+
meaningless.
|
|
384
|
+
|
|
385
|
+
**4. Gaps** (`steppy coverage report`) is pure bucket math:
|
|
386
|
+
|
|
387
|
+
| Bucket | Meaning |
|
|
388
|
+
|---|---|
|
|
389
|
+
| **tested** | declared ∩ exercised. *Strong* when an asserting scenario covered it, *weak* when only assert-free or `--guide`/`--no-asserts` runs did. |
|
|
390
|
+
| **declared-unreached** | In the manifest, never hit. **The bucket that matters** — this is the answer to "what should I write a scenario for next?" |
|
|
391
|
+
| **discovered-uncovered** | Seen in a trail, only weakly covered. |
|
|
392
|
+
| **discovered-tested** | Exercised but *absent* from the manifest — a stale spec, or a `base_urls` prefix mismatch. |
|
|
393
|
+
| **unclassified** | Requests matching no configured `base_urls` at all. Check `base_urls`. |
|
|
394
|
+
|
|
395
|
+
### It needs a declared surface — usually an OpenAPI document
|
|
396
|
+
|
|
397
|
+
```yaml
|
|
398
|
+
coverage:
|
|
399
|
+
apis:
|
|
400
|
+
- name: core
|
|
401
|
+
openapi: "./api/openapi.yaml" # URL or local path; JSON or YAML
|
|
402
|
+
base_urls: ["http://localhost:8080", "http://app-backend:8080"]
|
|
403
|
+
sitemap: "./public/sitemap.xml" # optional — declares the ROUTE surface
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
Two things to get right:
|
|
407
|
+
|
|
408
|
+
- **Prefer a committed file over a URL in CI.** A URL makes the refresh depend on the
|
|
409
|
+
backend being up at that moment, which is an ordering constraint nobody needs.
|
|
410
|
+
- **`base_urls` must mirror the OpenAPI `servers[].url`, including any path prefix**, and
|
|
411
|
+
should include the Docker-internal origins for parallel runs. Get this wrong and
|
|
412
|
+
observed requests land in `unclassified` while the declared surface reads untested.
|
|
413
|
+
|
|
414
|
+
With **no** declared surface you still get recording and the *discovered* buckets — "here
|
|
415
|
+
is what the suite touched" — and the badge honestly reads **`n/a` in grey**, never `0%`.
|
|
416
|
+
What you lose is `declared-unreached`, which is the actionable half. A frontend-only
|
|
417
|
+
project can get a real route denominator from `coverage.sitemap` alone, with no backend
|
|
418
|
+
spec at all.
|
|
419
|
+
|
|
420
|
+
### `coverage.non_ui` — required for a correct badge in CI
|
|
421
|
+
|
|
422
|
+
Some endpoints can *never* have a UI test: health checks, webhook receivers, OAuth
|
|
423
|
+
callbacks, internal cron. Marking them with `steppy coverage triage set` writes to
|
|
424
|
+
`.steppy/coverage.db`, which is **gitignored** — so a fresh CI checkout has an empty
|
|
425
|
+
store, every endpoint reads `unknown`, and all of them count against the denominator. The
|
|
426
|
+
badge then reads artificially low for reasons the maintainer cannot fix from CI, and a
|
|
427
|
+
badge like that gets deleted.
|
|
428
|
+
|
|
429
|
+
Declare them in committed config instead:
|
|
430
|
+
|
|
431
|
+
```yaml
|
|
432
|
+
coverage:
|
|
433
|
+
non_ui:
|
|
434
|
+
- "GET /health"
|
|
435
|
+
- "/webhooks/*"
|
|
436
|
+
- "POST /internal/cron/*"
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
Grammar is `[<METHOD> ]<glob>`:
|
|
440
|
+
|
|
441
|
+
- A bare glob matches **any method**; a method-qualified pattern matches only that one.
|
|
442
|
+
- `*` **crosses `/`** — `/internal/*` covers the whole subtree. This is intentional.
|
|
443
|
+
- A pattern applies across **every** configured API, because it matches on the path
|
|
444
|
+
alone. Use `steppy coverage triage set` for the surgical single-node case;
|
|
445
|
+
`steppy coverage triage list` shows every flag set that way.
|
|
446
|
+
- A method-qualified pattern never matches a *route* (routes have no method).
|
|
447
|
+
|
|
448
|
+
Precedence is a total order: **db `ui_testable` > config pattern > db `non_ui` >
|
|
449
|
+
`unknown`/absent**. An explicit human "yes this is reachable" outranks a glob; nothing is
|
|
450
|
+
ever written back to the store, so merge and refresh still never touch triage.
|
|
451
|
+
`steppy coverage report` **warns about a pattern that matches nothing** — after an API
|
|
452
|
+
rename a stale rule silently inflates the number.
|
|
453
|
+
|
|
454
|
+
### The badge number
|
|
455
|
+
|
|
456
|
+
**`tested / (declared − non_ui)`, with non_ui subtracted from *both* sides.** Subtracting
|
|
457
|
+
it from the denominator only would let a tested-but-non_ui endpoint push the result past
|
|
458
|
+
100%.
|
|
459
|
+
|
|
460
|
+
```bash
|
|
461
|
+
steppy coverage report --badge output/badge.json --badge-surface all
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
Worked example — 6 declared endpoints, 3 matched by the patterns above, 2 tested:
|
|
465
|
+
denominator 3, numerator 2, **67%**. Without `non_ui` the same suite reads 33%.
|
|
466
|
+
|
|
467
|
+
**Cost note:** `--coverage` drives CDP precise coverage, so it costs runtime. On free
|
|
468
|
+
runners, recording it on default-branch pushes (to refresh the badge) rather than on
|
|
469
|
+
every PR is usually the right trade.
|
|
470
|
+
|
|
471
|
+
**Why the coverage steps ship unarmed in both CI templates:** `steppy coverage refresh`
|
|
472
|
+
**exits 1** when neither `coverage.apis` nor `coverage.sitemap` is configured. Arming it
|
|
473
|
+
in the scaffold would break the pipeline of every project that has not declared a surface
|
|
474
|
+
yet — which is every project on the day it runs `steppy init`. Add the two steps once the
|
|
475
|
+
`coverage:` block exists. Until then the honest outcome is what the report already
|
|
476
|
+
produces: coverage recorded, nothing to score it against, badge reads **`n/a` in grey**
|
|
477
|
+
rather than a `0%` that looks like a finding.
|
|
478
|
+
|
|
479
|
+
### How the report gets coverage — four tiers
|
|
480
|
+
|
|
481
|
+
The store lives **outside `output/`** on purpose, so wiping run artifacts never destroys
|
|
482
|
+
hand-made triage — which also means it is never inside a bundle. So:
|
|
483
|
+
|
|
484
|
+
| Tier | Source | When |
|
|
485
|
+
|---|---|---|
|
|
486
|
+
| `bundled` | `output/coverage/gaps.json` carried in the run | any input that has one |
|
|
487
|
+
| `store` | the project's live `.steppy/coverage.db` | **live `output/` tree only** |
|
|
488
|
+
| `records` | a throwaway map rebuilt from the raw records | records, no gaps.json, no store |
|
|
489
|
+
| `none` | no coverage section at all | nothing recorded |
|
|
490
|
+
|
|
491
|
+
`gaps.json` is written automatically after the post-run merge, so
|
|
492
|
+
`steppy run --coverage --bundle` is self-sufficient — tier 1 with no extra CI step. Two
|
|
493
|
+
guardrails are deliberate and worth knowing, because the wrong answer would look
|
|
494
|
+
entirely plausible:
|
|
495
|
+
|
|
496
|
+
- **Tier 2 is live-tree only.** Reading this machine's map to describe someone else's
|
|
497
|
+
downloaded bundle would attribute your coverage to their run.
|
|
498
|
+
- **Tier 3 cannot report assert strength**, because the scenario markdown it would need
|
|
499
|
+
to count assert steps in never travels in a bundle. It says so instead of guessing.
|
|
500
|
+
|
|
501
|
+
---
|
|
502
|
+
|
|
503
|
+
## Results bundles
|
|
504
|
+
|
|
505
|
+
```bash
|
|
506
|
+
steppy run --bundle # capture per-test AND per-step events + JUnit XML
|
|
507
|
+
steppy bundle create # → output/bundles/steppy-bundle-<run-id>.tar.zst
|
|
508
|
+
steppy bundle inspect <file> # the manifest, without extracting
|
|
509
|
+
steppy bundle verify <file> # re-hash every member against it
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
`--bundle` does not create the bundle; it captures what a bundle needs. `create` then
|
|
513
|
+
packages `output/` **exactly as Steppy wrote it** — no path rewriting — plus a generated
|
|
514
|
+
`manifest.json`, a merged `junit.xml`, the event JSONL, and `coverage/gaps.json` when the
|
|
515
|
+
run has one. Unpacking yields a tree that `steppy report`, `steppy guide` and
|
|
516
|
+
`steppy build` all consume unchanged.
|
|
517
|
+
|
|
518
|
+
Useful with no server involved: a self-describing archive to attach to a CI job, so a
|
|
519
|
+
failed run is one download rather than a scavenger hunt. `--no-videos` / `--no-traces` /
|
|
520
|
+
`--no-guides` trim it; `--max-size 2GiB` drops the cheapest-to-lose artifacts first and
|
|
521
|
+
marks the manifest `truncated`.
|
|
522
|
+
|
|
523
|
+
`steppy bundle upload` pushes to a Steppy control plane, which is optional and out of
|
|
524
|
+
scope here — everything above works with no plane at all.
|
|
525
|
+
|
|
526
|
+
---
|
|
527
|
+
|
|
528
|
+
## Troubleshooting
|
|
529
|
+
|
|
530
|
+
| Symptom | Cause | Fix |
|
|
531
|
+
|---|---|---|
|
|
532
|
+
| Report says "no test outcomes", verdict `unknown` | The run had no `--bundle`, so no event records exist | `steppy run --bundle` |
|
|
533
|
+
| Report shows outcomes but no step detail | `STEPPY_STEP_EVENTS=0`, or tests generated before per-step markers | Unset the env var; re-run `steppy generate` |
|
|
534
|
+
| Badge says `n/a` | Nothing declared — no `coverage.apis`, or `refresh` never ran | Configure `coverage.apis[].openapi`, then `steppy coverage refresh` |
|
|
535
|
+
| Coverage percentage far lower in CI than locally | Triage flags live in the gitignored `.steppy/coverage.db`; CI starts empty | Declare `coverage.non_ui` in `config.yaml` |
|
|
536
|
+
| Everything in `unclassified`, declared surface reads untested | `base_urls` does not match the OpenAPI `servers[].url` prefix | Fix `base_urls`; include internal origins for parallel runs |
|
|
537
|
+
| `discovered-tested` endpoints appear | Stale spec, or a base-URL mismatch | `steppy coverage refresh`, then check `base_urls` |
|
|
538
|
+
| Every tested endpoint reads "weak" | Assert-free run (`--guide` implies `--no-asserts`), or coverage rebuilt from a bundle (tier 3) | Record coverage on a normal test run, not a guide capture |
|
|
539
|
+
| No coverage section at all | Nothing recorded | `steppy run --coverage` |
|
|
540
|
+
| Report refuses to write | Target looks like a project (`config.yaml`, `.git`, `scenarios/`, `pyproject.toml`) — refused even with `--force` | Point `-o` at a dedicated directory |
|
|
541
|
+
| Report refuses a non-empty directory | Not a previous report (no `report.json` with a `steppy_report` key) | `--force`, or a different `-o` |
|
|
542
|
+
| Bundle roughly doubled in size | The report was written somewhere under `output/` other than `output/report/`, so the bundler archived the site | Use the default `-o`, or exclude it |
|
|
543
|
+
| CI step red but the run passed | Something else — `steppy report` always exits 0 | Read `report.json`'s `totals.exit_code` |
|
|
544
|
+
| A tour replay fails with `target-timeout`, or on a step id it does not expect, while its scenario passes | Stale tour files in the app image: the app serves tours compiled from older scenarios | Run `steppy tours`, rebuild the app image, then run; add `steppy tours --check` as a gate (it compares the committed `app-assets` copy's content, never the build id) |
|
|
545
|
+
|
|
546
|
+
---
|
|
547
|
+
|
|
548
|
+
## What this deliberately does not do
|
|
549
|
+
|
|
550
|
+
The report **transforms one run**. With a single run's artifacts there is nothing to
|
|
551
|
+
compare against, so it structurally cannot:
|
|
552
|
+
|
|
553
|
+
- diff guides or screenshots against an accepted baseline,
|
|
554
|
+
- track flake rate or duration trends across runs,
|
|
555
|
+
- show a coverage delta for a PR,
|
|
556
|
+
- gate a pull request, or let a human accept a baseline.
|
|
557
|
+
|
|
558
|
+
Those need memory across runs, which is a control plane's job. Do not try to build them
|
|
559
|
+
on top of `steppy report`. Two things that *do* exist for the adjacent needs:
|
|
560
|
+
`steppy flaky` ranks scenarios by flake rate from `.steppy/history.jsonl` (parallel runs
|
|
561
|
+
only — cache that file between CI runs or the history evaporates), and
|
|
562
|
+
`steppy bundle upload` hands a run to a plane if the project has one.
|
|
563
|
+
|
|
564
|
+
Nor is there an **analytics backend for in-app tours**. The overlay sends nothing anywhere:
|
|
565
|
+
its events (`tour-start`, `tour-step`, `tour-complete`, `tour-dismiss`, `tour-offpath`,
|
|
566
|
+
`tour-error`) stay in the page, for the app's own analytics if it has any. CI replays prove
|
|
567
|
+
a tour *works*; whether real users finish it is the app's question, not this pipeline's.
|