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,1335 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: steppy-dev
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Generate Playwright pytest suites from Markdown scenarios, and turn their screenshots into multi-locale user guides
|
|
5
|
+
Project-URL: Homepage, https://github.com/Steppy-dev/steppy
|
|
6
|
+
Project-URL: Source, https://github.com/Steppy-dev/steppy
|
|
7
|
+
Project-URL: Changelog, https://github.com/Steppy-dev/steppy/blob/main/CHANGELOG.md
|
|
8
|
+
Project-URL: Issues, https://github.com/Steppy-dev/steppy/issues
|
|
9
|
+
License-Expression: Apache-2.0
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
License-File: NOTICE
|
|
12
|
+
Keywords: code-generation,e2e,markdown,playwright,pytest,screenshots,testing,user-guide
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Environment :: Console
|
|
15
|
+
Classifier: Framework :: Pytest
|
|
16
|
+
Classifier: Intended Audience :: Developers
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Topic :: Documentation
|
|
23
|
+
Classifier: Topic :: Software Development :: Code Generators
|
|
24
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
25
|
+
Classifier: Topic :: Software Development :: Testing
|
|
26
|
+
Requires-Python: >=3.12
|
|
27
|
+
Requires-Dist: httpx>=0.27.0
|
|
28
|
+
Requires-Dist: jinja2>=3.1.0
|
|
29
|
+
Requires-Dist: markdown-it-py>=3.0
|
|
30
|
+
Requires-Dist: playwright<1.61,>=1.60.0
|
|
31
|
+
Requires-Dist: pydantic>=2.0.0
|
|
32
|
+
Requires-Dist: pytest-playwright>=0.5.0
|
|
33
|
+
Requires-Dist: pytest>=8.0.0
|
|
34
|
+
Requires-Dist: python-frontmatter>=1.0.0
|
|
35
|
+
Requires-Dist: pyyaml>=6.0
|
|
36
|
+
Requires-Dist: rich>=13.0.0
|
|
37
|
+
Requires-Dist: typer>=0.9.0
|
|
38
|
+
Provides-Extra: bundle
|
|
39
|
+
Requires-Dist: zstandard>=0.22.0; extra == 'bundle'
|
|
40
|
+
Provides-Extra: report
|
|
41
|
+
Requires-Dist: pytest-html>=4.0.0; extra == 'report'
|
|
42
|
+
Provides-Extra: retry
|
|
43
|
+
Requires-Dist: pytest-rerunfailures>=14.0; extra == 'retry'
|
|
44
|
+
Description-Content-Type: text/markdown
|
|
45
|
+
|
|
46
|
+
<h1 align="center">
|
|
47
|
+
<picture>
|
|
48
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/Steppy-dev/steppy/main/docs/brand/steppy-mark-dark.svg">
|
|
49
|
+
<img alt="" src="https://raw.githubusercontent.com/Steppy-dev/steppy/main/docs/brand/steppy-mark.svg" width="96">
|
|
50
|
+
</picture>
|
|
51
|
+
<br>
|
|
52
|
+
Steppy
|
|
53
|
+
</h1>
|
|
54
|
+
|
|
55
|
+
<p align="center"><em>Every walkthrough, proven.</em></p>
|
|
56
|
+
|
|
57
|
+
Python CLI that generates Playwright pytest files from Markdown scenario files, then turns the captured screenshots into multi-locale user guides (standalone HTML, Angular components, or a static site).
|
|
58
|
+
|
|
59
|
+
- **Code generator, not a runtime engine** — generated tests are standalone pytest files you can run with or without Steppy.
|
|
60
|
+
- **Markdown-first** — scenarios are `.md` files with YAML frontmatter, no DSL to learn.
|
|
61
|
+
- **Multi-locale + multi-layout capture** — one run produces screenshots for every (locale × viewport) combination.
|
|
62
|
+
- **Three guide modes** — `stepbystep` (annotated screenshots with an animated cursor), `interactive` (a Lit web component that replays the flow), and `video` (a narrated MP4 synthesized from the same captures, per locale and layout).
|
|
63
|
+
- **In-app tours (opt-in)** — the same scenarios compile into guided popovers over the live app, and CI replays every tour.
|
|
64
|
+
- **Optional parallel execution** — opt-in Docker Compose orchestration for large suites.
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Installation
|
|
69
|
+
|
|
70
|
+
The package is named **`steppy-dev`**, because `steppy` on PyPI is an unrelated
|
|
71
|
+
project, which a pip command naming plain `steppy` installs. Use `steppy-dev` wherever pip
|
|
72
|
+
reads a name, extras included (`steppy-dev[report]`). The command you run and the
|
|
73
|
+
module you import are both `steppy`.
|
|
74
|
+
|
|
75
|
+
### Option 1 — From PyPI (simplest)
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
pip install steppy-dev
|
|
79
|
+
|
|
80
|
+
# With the optional extras (HTML report, --retries, zstd bundles)
|
|
81
|
+
pip install "steppy-dev[report,retry,bundle]"
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Option 2 — Directly from the git repo
|
|
85
|
+
|
|
86
|
+
Pin a release tag rather than a branch — see the
|
|
87
|
+
[releases page](https://github.com/Steppy-dev/steppy/releases) for the latest (the
|
|
88
|
+
examples below use `vX.Y.Z` / `X.Y.Z` as placeholders). The player JS bundle is
|
|
89
|
+
committed to git, so `pip` can build a wheel from the source tree with no Node
|
|
90
|
+
toolchain involved:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
# Pinned to a tag (recommended)
|
|
94
|
+
pip install "steppy-dev @ git+https://github.com/Steppy-dev/steppy.git@vX.Y.Z"
|
|
95
|
+
|
|
96
|
+
# With the optional extras (HTML report, --retries, zstd bundles)
|
|
97
|
+
pip install "steppy-dev[report,retry,bundle] @ git+https://github.com/Steppy-dev/steppy.git@vX.Y.Z"
|
|
98
|
+
|
|
99
|
+
# Latest main
|
|
100
|
+
pip install git+https://github.com/Steppy-dev/steppy.git
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Option 3 — From a GitHub Release wheel
|
|
104
|
+
|
|
105
|
+
Faster install (no clone, no build) and version-pinned:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
pip install "https://github.com/Steppy-dev/steppy/releases/download/vX.Y.Z/steppy_dev-X.Y.Z-py3-none-any.whl"
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Option 4 — In CI / `requirements.txt`
|
|
112
|
+
|
|
113
|
+
Every form above works unchanged in CI — no token is needed. In `requirements.txt`:
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
steppy-dev[report,retry]==X.Y.Z
|
|
117
|
+
# or, from git:
|
|
118
|
+
steppy-dev[report,retry] @ git+https://github.com/Steppy-dev/steppy.git@vX.Y.Z
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
For GitHub Actions, `steppy init --ci` scaffolds a complete workflow, and
|
|
122
|
+
[`.github/workflows/steppy-e2e.yml`](.github/workflows/steppy-e2e.yml) is the same
|
|
123
|
+
recipe as a reusable workflow you can call with `uses:`.
|
|
124
|
+
|
|
125
|
+
### Option 5 — From source, editable (for Steppy development)
|
|
126
|
+
|
|
127
|
+
Required if you're contributing to Steppy itself.
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
git clone https://github.com/Steppy-dev/steppy.git
|
|
131
|
+
cd steppy
|
|
132
|
+
|
|
133
|
+
python -m venv venv
|
|
134
|
+
.\venv\Scripts\activate # Windows
|
|
135
|
+
# source venv/bin/activate # Linux/macOS
|
|
136
|
+
pip install -e .
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
If you modify the player TypeScript (`packages/player/src/`), rebuild and re-vendor the JS into the Python package, then commit the result:
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
npm --prefix packages/player install # once
|
|
143
|
+
npm --prefix packages/player run build
|
|
144
|
+
for f in steppy-player steppy-cursor-legacy steppy-video; do
|
|
145
|
+
cp packages/player/dist/$f.js packages/player/dist/$f.js.map src/steppy/_player_bundle/
|
|
146
|
+
done
|
|
147
|
+
git add src/steppy/_player_bundle/*.js src/steppy/_player_bundle/*.js.map
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Only those three are vendored: `dist/` also holds the in-app tour overlay (`steppy-tour.js`), which has no Python consumer and is meant for apps through npm (`@steppy/player/tour`; publish-ready, not on npm yet: until then, depend on a built `packages/player` or its `npm pack` tarball).
|
|
151
|
+
|
|
152
|
+
The release pipeline rebuilds these from scratch for each tagged release, but the committed copies are what makes `pip install git+...` work for everyone else without needing Node.
|
|
153
|
+
|
|
154
|
+
### After install
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
# Install Playwright browsers (one-time)
|
|
158
|
+
playwright install chromium
|
|
159
|
+
|
|
160
|
+
# Verify
|
|
161
|
+
steppy --help
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## Releasing a new version (maintainers)
|
|
167
|
+
|
|
168
|
+
The release pipeline lives in [.github/workflows/release.yml](.github/workflows/release.yml) and runs on tag push.
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
# 1. Bump version in pyproject.toml
|
|
172
|
+
# version = "X.Y.Z"
|
|
173
|
+
|
|
174
|
+
git add pyproject.toml
|
|
175
|
+
git commit -m "Release vX.Y.Z"
|
|
176
|
+
|
|
177
|
+
# 2. Tag and push
|
|
178
|
+
git tag vX.Y.Z
|
|
179
|
+
git push origin main --tags
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
`packages/*/package.json` carry the same version (`tests/test_versions_aligned.py`
|
|
183
|
+
enforces it), so bump them in the same commit.
|
|
184
|
+
|
|
185
|
+
CI then:
|
|
186
|
+
1. Validates the tag matches `pyproject.toml`'s `version` and the npm packages' versions.
|
|
187
|
+
2. Builds the player bundles (`npm run build`).
|
|
188
|
+
3. Vendors all three into `src/steppy/_player_bundle/`.
|
|
189
|
+
4. Builds the Python wheel and sdist.
|
|
190
|
+
5. Sanity-checks that the three bundles, their third-party license file, and `LICENSE`/`NOTICE` are in the wheel (the build fails loudly if any is missing).
|
|
191
|
+
6. Creates a GitHub Release with the wheel and sdist attached.
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## Quickstart
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
# 1. Scaffold a project (config.yaml, an example scenario, .gitignore, schema)
|
|
199
|
+
steppy init # add --ci to also drop in a GitHub Actions workflow
|
|
200
|
+
|
|
201
|
+
# 2. Edit config.yaml (app URLs, roles, credentials) and write your scenarios
|
|
202
|
+
# in scenarios/*.md (see STEPPY-REFERENCE.md)
|
|
203
|
+
|
|
204
|
+
# 2b. (optional) Bootstrap a scenario draft from a live click-through
|
|
205
|
+
steppy record https://app.example.com --role admin --slug my-flow
|
|
206
|
+
|
|
207
|
+
# 3. Validate them
|
|
208
|
+
steppy validate
|
|
209
|
+
|
|
210
|
+
# 4. Generate the pytest files
|
|
211
|
+
steppy generate
|
|
212
|
+
|
|
213
|
+
# 5. Run them
|
|
214
|
+
steppy run --scenario my-flow
|
|
215
|
+
|
|
216
|
+
# 6. (optional) Capture screenshots and assemble a guide
|
|
217
|
+
steppy run --guide
|
|
218
|
+
steppy build
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## CLI reference
|
|
224
|
+
|
|
225
|
+
### Scaffold
|
|
226
|
+
|
|
227
|
+
```bash
|
|
228
|
+
steppy init # scaffold in the current directory
|
|
229
|
+
steppy init my-project # scaffold into a subdirectory
|
|
230
|
+
steppy init --ci # also write a GitHub Actions workflow
|
|
231
|
+
steppy init --no-skills # skip the packaged Claude skills
|
|
232
|
+
steppy init --app-dir ../invoice-app # also put the app-side skill in the app's repository
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
`init` writes `config.yaml` (with a `$schema` header for editor autocomplete),
|
|
236
|
+
`scenarios/example--first-flow.md`, `credentials.yaml.example`,
|
|
237
|
+
`steppy-config.schema.json`, and a `.steppy/scaffold.json` manifest, and appends
|
|
238
|
+
the right `.gitignore` entries. It never overwrites existing files, so it is safe
|
|
239
|
+
to re-run in an existing project: it adds what is missing, merges the manifest, and
|
|
240
|
+
leaves the starter scenario out when `scenarios/` already has some.
|
|
241
|
+
|
|
242
|
+
By default it also installs four packaged Claude skills into `.claude/skills/`,
|
|
243
|
+
split by job so an AI assistant in the project is expert on the surface it is
|
|
244
|
+
actually being asked about:
|
|
245
|
+
|
|
246
|
+
| Skill | Covers |
|
|
247
|
+
|---|---|
|
|
248
|
+
| `steppy-scenario-author` | the scenario grammar, guides, `steppy build`, PDF and video |
|
|
249
|
+
| `steppy-template` | styling and theming — guides, the player, print, the run report |
|
|
250
|
+
| `steppy-ci-report` | CI wiring, `steppy report`, publishing to Pages, the coverage map and its badge |
|
|
251
|
+
| `steppy-app-tours` | wiring in-app tours into the app's own code: the overlay, the Angular adapter, test-ids, CSP |
|
|
252
|
+
|
|
253
|
+
Pass `--no-skills` to skip them. `--ci` / `--ci-gitlab` add the matching CI
|
|
254
|
+
pipeline. The app-side skill is only useful where the app's code is, which is often
|
|
255
|
+
another repository: `--app-dir <app>` installs it there too, and records it so
|
|
256
|
+
`steppy update` keeps it current.
|
|
257
|
+
|
|
258
|
+
```bash
|
|
259
|
+
steppy update # reconcile a project after upgrading steppy
|
|
260
|
+
steppy update --check # report-only: scan for deprecated usage (CI-able)
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
`update` runs **after** you upgrade the `steppy-dev` package (pip/uv/pipx own the
|
|
264
|
+
upgrade itself). It refreshes the Steppy-owned scaffold assets — the config
|
|
265
|
+
schema, the Claude skills, and the CI workflow — from the newly-installed
|
|
266
|
+
version: files you haven't touched are replaced silently, and any file you've
|
|
267
|
+
edited is left intact with the new version written alongside as `<file>.new` (so
|
|
268
|
+
you never lose edits, and never silently miss an update). Skills shipped since the
|
|
269
|
+
project was scaffolded are installed, and skill folders copied in by hand before
|
|
270
|
+
Steppy packaged them (`steppy`, `steppy-angular-export`) are reported for you to
|
|
271
|
+
delete. Starter files you own (`config.yaml`, your scenarios) are never touched. It then reports any deprecated
|
|
272
|
+
scenario/config usage and the changelog highlights since your last version.
|
|
273
|
+
`steppy update --check` is the report-only compat scan on its own — it writes
|
|
274
|
+
nothing and exits non-zero on breaking usage, so CI can gate on it.
|
|
275
|
+
|
|
276
|
+
### Record
|
|
277
|
+
|
|
278
|
+
```bash
|
|
279
|
+
steppy record https://app.example.com --role admin --slug my-flow # record a draft scenario
|
|
280
|
+
steppy record --role admin --slug my-flow # start URL defaults to the role's app URL
|
|
281
|
+
steppy record --slug my-flow --no-auth # fresh session (e.g. to record the login flow)
|
|
282
|
+
steppy record --slug my-flow --placeholderize-all # turn every recorded value into a ${VAR} ref
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
`record` wraps `playwright codegen`: a headed browser opens, you perform the flow, and
|
|
286
|
+
closing the window writes a **draft** `scenarios/<slug>.md` — a reviewable starting point,
|
|
287
|
+
not a finished scenario. Refine it with the `steppy-scenario-author` skill, then `steppy
|
|
288
|
+
validate` / `steppy generate`.
|
|
289
|
+
|
|
290
|
+
It records as an **authenticated** session by default (loading `.auth/<role>.json` if
|
|
291
|
+
present — run `steppy auth login <role>` first) and **never bakes secrets**: a
|
|
292
|
+
username/password/submit sequence collapses to a single `- action: login` (credentials
|
|
293
|
+
come from `config.yaml`), password/token fields become `${VAR}` env refs, and the command
|
|
294
|
+
prints a ready-to-paste `screenshot.mask` block to redact passwords in screenshots.
|
|
295
|
+
Selectors prefer `data-testid`; the selector lint runs on the output.
|
|
296
|
+
|
|
297
|
+
> **V1 (CLI plumbing).** A foundational recorder — the client-facing browser-extension
|
|
298
|
+
> version with a visual blur picker and instant preview is a later phase. Recorded
|
|
299
|
+
> selectors and mappings are best-effort against Playwright codegen output; always review
|
|
300
|
+
> the draft before committing.
|
|
301
|
+
|
|
302
|
+
### Scenarios
|
|
303
|
+
|
|
304
|
+
```bash
|
|
305
|
+
steppy validate # validate all scenarios
|
|
306
|
+
steppy validate scenarios/my-flow.md # validate a single file
|
|
307
|
+
steppy list # tabular view of all scenarios
|
|
308
|
+
steppy list --tag e2e # filtered
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
### Generate
|
|
312
|
+
|
|
313
|
+
```bash
|
|
314
|
+
steppy generate # regenerate all test files
|
|
315
|
+
steppy generate scenarios/my-flow.md # specific scenario
|
|
316
|
+
steppy generate --tag e2e # filter by tag
|
|
317
|
+
steppy generate --dry-run # preview changes
|
|
318
|
+
steppy generate --force # overwrite without prompting
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
### Run
|
|
322
|
+
|
|
323
|
+
```bash
|
|
324
|
+
steppy run # run all generated tests
|
|
325
|
+
steppy run --scenario my-flow # specific scenario
|
|
326
|
+
steppy run --tag e2e # filter by tag
|
|
327
|
+
steppy run --headed # visible browser
|
|
328
|
+
steppy run --screenshots # enable screenshot capture
|
|
329
|
+
steppy run --no-asserts # skip `expect()` calls
|
|
330
|
+
steppy run --locales fr,nl # parametrize over locales
|
|
331
|
+
steppy run --layouts desktop,mobile # parametrize over viewports
|
|
332
|
+
steppy run --guide # guide-capture mode (see below)
|
|
333
|
+
steppy run --trace # save a Playwright trace per FAILED test
|
|
334
|
+
steppy run -- -v -x --tb=short # passthrough args to pytest
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
See [Continuous integration](#continuous-integration-opt-in) for JUnit/HTML
|
|
338
|
+
reports and the shipped GitHub Actions template.
|
|
339
|
+
|
|
340
|
+
### Guide capture and assembly
|
|
341
|
+
|
|
342
|
+
```bash
|
|
343
|
+
steppy run --guide # runs every `guide.enabled` scenario
|
|
344
|
+
# across all locales × layouts,
|
|
345
|
+
# auto-enables screenshots + --no-asserts
|
|
346
|
+
|
|
347
|
+
steppy guide --tag user-guide-admin --locale fr
|
|
348
|
+
steppy guide --guide # auto-discovers all user-guide-* tags
|
|
349
|
+
# from `guide.enabled` scenarios
|
|
350
|
+
steppy guide --guide --pdf # + one PDF next to each guide
|
|
351
|
+
steppy guide --guide --handbook # + one handbook (HTML + PDF) per
|
|
352
|
+
# role/locale/layout
|
|
353
|
+
steppy guide --guide --video # + a narrated MP4 (+ .vtt + poster)
|
|
354
|
+
# next to each guide (needs ffmpeg)
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
Captures land in `output/screenshots/` and guides in `output/guides/`. Both are
|
|
358
|
+
configurable in one place — `output.screenshots_dir` / `output.guides_dir` in
|
|
359
|
+
`config.yaml` — and the capture side and every reader (`guide`, `build`,
|
|
360
|
+
`bundle`, the parallel failure harvest) resolve the same value, so they cannot
|
|
361
|
+
drift apart. `--screenshots-dir` / `--output-dir` still override for one
|
|
362
|
+
invocation. See [Artifact output paths](STEPPY-REFERENCE.md#8-artifact-output-paths)
|
|
363
|
+
for what happens if you point them outside `output/`.
|
|
364
|
+
|
|
365
|
+
See [PDF export](#pdf-export-opt-in) and [Video guide mode](#video-guide-mode-opt-in) below.
|
|
366
|
+
|
|
367
|
+
### Build
|
|
368
|
+
|
|
369
|
+
```bash
|
|
370
|
+
steppy build # target chosen by steppy.guide.config.ts
|
|
371
|
+
steppy build --config path/to/config.ts
|
|
372
|
+
steppy build --dry-run
|
|
373
|
+
steppy build --pdf --handbook # + per-page PDFs and a site handbook
|
|
374
|
+
# (static target)
|
|
375
|
+
steppy build --video # + a narrated MP4 per locale/layout for
|
|
376
|
+
# each guide declaring modes: ['video']
|
|
377
|
+
|
|
378
|
+
steppy video check # is ffmpeg/Chromium/the voice ready?
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
See [the `steppy build` section](#steppy-build) and [Video guide mode](#video-guide-mode-opt-in) below.
|
|
382
|
+
|
|
383
|
+
### In-app tours (opt-in)
|
|
384
|
+
|
|
385
|
+
With `tours: {enabled: true}` in `config.yaml`, `validate` also compiles the
|
|
386
|
+
tours of every `tour:`-enabled scenario, and `generate` writes their files and
|
|
387
|
+
appends a CI replay to each scenario's test.
|
|
388
|
+
|
|
389
|
+
```bash
|
|
390
|
+
steppy tours # write the tour files only (tests untouched)
|
|
391
|
+
steppy tours --check # CI: exit 1 when the app's committed tour files are stale
|
|
392
|
+
steppy tours --build-id 2026.10 # stamp another build id (this command only)
|
|
393
|
+
steppy tours --explain # also say why each step was left out
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
See [In-app tours](#in-app-tours-opt-in-1) below.
|
|
397
|
+
|
|
398
|
+
### Authentication
|
|
399
|
+
|
|
400
|
+
```bash
|
|
401
|
+
steppy auth login editor # interactive (headed) login
|
|
402
|
+
steppy auth status # cached session state per role
|
|
403
|
+
steppy auth clear # clear everything
|
|
404
|
+
steppy auth clear editor # clear a single role
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
### Seed scripts
|
|
408
|
+
|
|
409
|
+
```bash
|
|
410
|
+
steppy seed base # run seed/base.py via subprocess
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
### Parallel execution (opt-in)
|
|
414
|
+
|
|
415
|
+
```bash
|
|
416
|
+
steppy run --workers 3 # 3 isolated Docker stacks
|
|
417
|
+
steppy run --workers 3 --no-build # skip auto-build of worker image
|
|
418
|
+
steppy stacks cleanup # rescue orphaned steppy-run-* projects
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
Requires a `parallel:` block in `config.yaml` + per-app `internal_url`. See [Parallel execution](#parallel-execution-opt-in) below.
|
|
422
|
+
|
|
423
|
+
### Coverage map (opt-in)
|
|
424
|
+
|
|
425
|
+
```bash
|
|
426
|
+
steppy run --coverage # record route/API/JS signals + merge into the map
|
|
427
|
+
steppy run --coverage --scenario my-flow # works for a single scenario in isolation
|
|
428
|
+
steppy coverage refresh # pull declared surface from openapi.json (+ sitemap)
|
|
429
|
+
steppy coverage report --json gaps.json # tested / declared-unreached / discovered-uncovered
|
|
430
|
+
steppy coverage triage set /webhooks/stripe --method POST --api my-api --flag non_ui
|
|
431
|
+
steppy coverage merge --run-id <id> # rescue: re-merge a recorded run
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
Requires a `coverage:` block in `config.yaml` for manifest/API attribution. See [Coverage map](#coverage-map-opt-in-1) below.
|
|
435
|
+
|
|
436
|
+
### Results bundle (opt-in)
|
|
437
|
+
|
|
438
|
+
```bash
|
|
439
|
+
steppy run --bundle # record per-test events + JUnit XML alongside the run
|
|
440
|
+
steppy bundle create # package output/ into output/bundles/steppy-bundle-<id>.tar.zst
|
|
441
|
+
steppy bundle create --dry-run # show what would be archived, write nothing
|
|
442
|
+
steppy bundle inspect <bundle> # print the manifest without extracting
|
|
443
|
+
steppy bundle verify <bundle> # re-hash every member against the manifest
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
A portable, self-describing archive of one run. See [Results bundles](#results-bundles-opt-in-1) below.
|
|
447
|
+
|
|
448
|
+
---
|
|
449
|
+
|
|
450
|
+
## `steppy build`
|
|
451
|
+
|
|
452
|
+
Generates guide artifacts from a TypeScript config. The config's `target` field picks the output:
|
|
453
|
+
|
|
454
|
+
- `static` (default) — self-contained, framework-agnostic static site (HTML pages,
|
|
455
|
+
manifest, no runtime deps); embed anywhere, including via the `@steppy/react` /
|
|
456
|
+
`@steppy/vue` wrapper packages
|
|
457
|
+
- `angular` — standalone Angular 21 components + routes + ngx-translate i18n bundles
|
|
458
|
+
(set `target: 'angular'` explicitly)
|
|
459
|
+
|
|
460
|
+
### Config (`steppy.guide.config.ts`)
|
|
461
|
+
|
|
462
|
+
```ts
|
|
463
|
+
import { defineGuideConfig } from 'steppy';
|
|
464
|
+
|
|
465
|
+
export default defineGuideConfig({
|
|
466
|
+
scenariosRoot: './e2e/scenarios',
|
|
467
|
+
screenshotsDir: '../steppy/output/screenshots',
|
|
468
|
+
outputDir: './src/app/features/guides/generated',
|
|
469
|
+
assetsDir: './public/assets/editor',
|
|
470
|
+
role: 'editor',
|
|
471
|
+
locales: ['fr', 'nl'],
|
|
472
|
+
defaultLocale: 'fr',
|
|
473
|
+
layouts: ['desktop', 'mobile'],
|
|
474
|
+
defaultLayout: 'desktop',
|
|
475
|
+
site: { title: 'Demo Guides' },
|
|
476
|
+
guides: [
|
|
477
|
+
{ slug: 'onboarding', title: 'Onboarding', scenario: 'editor/onboarding.md', modes: ['stepbystep'] },
|
|
478
|
+
{ slug: 'invoices', title: 'Invoices', scenario: 'editor/invoices.md', modes: ['stepbystep', 'interactive', 'video'] },
|
|
479
|
+
],
|
|
480
|
+
navbar: { order: ['onboarding', 'invoices'] },
|
|
481
|
+
});
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
Install the types package in your Angular app: `"steppy": "file:../path-to-steppy/packages/steppy"` in `devDependencies`. The host app also needs `@ngx-translate/core` — generated components inject `TranslateService` and resolve titles via the translate pipe.
|
|
485
|
+
|
|
486
|
+
> **Why two config languages?** Tests use `config.yaml` (no toolchain beyond
|
|
487
|
+
> Python); builds use `steppy.guide.config.ts` because it buys type-checked DX
|
|
488
|
+
> via `defineGuideConfig`, and `steppy build` already requires Node for `tsx`
|
|
489
|
+
> regardless. If you build guides without Node, a JSON config variant is a
|
|
490
|
+
> possible future addition.
|
|
491
|
+
|
|
492
|
+
### Angular output
|
|
493
|
+
|
|
494
|
+
```
|
|
495
|
+
outputDir/
|
|
496
|
+
<role>.routes.ts # exports <ROLE>_ROUTES (lazy-loaded)
|
|
497
|
+
<role>.nav.json # navigation tree (translation-key references)
|
|
498
|
+
i18n/<role>/<locale>.json # per-locale bundle, keyed under uppercase <ROLE>
|
|
499
|
+
{slug}/
|
|
500
|
+
{slug}.component.ts # standalone component, ngx-translate driven
|
|
501
|
+
{slug}.content.ts # CONTENT[locale][layout] HTML fragments
|
|
502
|
+
{slug}-interactive.component.ts # if modes includes 'interactive'
|
|
503
|
+
{slug}-video.component.ts # if modes includes 'video' and --video was passed
|
|
504
|
+
{slug}-stepbystep-{locale}.html # human-readable debug fragment
|
|
505
|
+
_groups/<group>.group.component.ts # if groups configured
|
|
506
|
+
_onboarding/onboarding.component.ts # if onboarding enabled
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
Screenshots are copied to `assetsDir`. Image URLs are absolute `/assets/<role>/<slug>/[<locale>/][<layout>/]step-NN.png` — point `assetsDir` at your host's `public/assets/<role>/` to wire them up directly.
|
|
510
|
+
|
|
511
|
+
Integration:
|
|
512
|
+
|
|
513
|
+
```ts
|
|
514
|
+
// app.routes.ts
|
|
515
|
+
import { EDITOR_ROUTES } from './features/guides/generated/editor.routes';
|
|
516
|
+
export const routes: Routes = [
|
|
517
|
+
{ path: 'guides', children: EDITOR_ROUTES },
|
|
518
|
+
];
|
|
519
|
+
|
|
520
|
+
// app.config.ts — merge generated i18n bundles into TranslateService
|
|
521
|
+
import frTranslations from './features/guides/generated/i18n/editor/fr.json';
|
|
522
|
+
import nlTranslations from './features/guides/generated/i18n/editor/nl.json';
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
Steppy never touches `app.routes.ts` or your navbar — integration is purely via imports.
|
|
526
|
+
|
|
527
|
+
### Static output
|
|
528
|
+
|
|
529
|
+
Set `target: 'static'` in the config. Emits a self-contained site with HTML pages at `<slug>/<locale>.html`, an `index.html` per locale, a `manifest.json`, a shared `assets/guides.css`, `.nojekyll` and `_headers`. Screenshots co-located under `outputDir/assets/<slug>/`. Deploy anywhere static.
|
|
530
|
+
|
|
531
|
+
A guide gets one sibling page per extra mode — `<slug>/interactive.html` for the tour, `<slug>/video.html` for the narrated MP4 — and each guide page links whichever of them it has.
|
|
532
|
+
|
|
533
|
+
### SEO / indexing (`seo` block)
|
|
534
|
+
|
|
535
|
+
Static builds are **private by default** — every page gets `<meta name="robots" content="noindex,nofollow,noarchive">` and a `robots.txt` with `Disallow: /`, so an accidentally-exposed client guide site is not indexable. For a public showcase site, opt in:
|
|
536
|
+
|
|
537
|
+
```ts
|
|
538
|
+
export default defineGuideConfig({
|
|
539
|
+
// …
|
|
540
|
+
target: 'static',
|
|
541
|
+
seo: {
|
|
542
|
+
mode: 'public', // default: 'private'
|
|
543
|
+
baseUrl: 'https://guides.example.com', // required when public
|
|
544
|
+
description: { fr: 'Guides produit', nl: 'Producthandleidingen' }, // optional site-wide default
|
|
545
|
+
openGraph: true, // optional OG / Twitter cards
|
|
546
|
+
lastmod: '2026-09-14', // optional <lastmod> on every sitemap entry
|
|
547
|
+
},
|
|
548
|
+
guides: [
|
|
549
|
+
{ slug: 'onboarding', title: 'Onboarding', scenario: 'onboarding.md',
|
|
550
|
+
description: 'Get started in 5 minutes' }, // optional per-guide meta description
|
|
551
|
+
],
|
|
552
|
+
});
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
Public mode emits `sitemap.xml` (absolute URLs + per-locale `hreflang` alternates), a `robots.txt` pointing at it, `<link rel="canonical">` + `hreflang` alternates (with `x-default`) on every indexable page, `<meta name="description">` (guide `description` → its title; index uses `seo.description` → site title), and — when `openGraph: true` — Open Graph / Twitter tags (`og:image` is a guide's first screenshot). Indexing controls are **defense-in-depth, not access control** — keep private sites behind auth. Static target only (the Angular host owns its own `<head>`).
|
|
556
|
+
|
|
557
|
+
**One canonical URL per guide.** A guide is rendered up to four ways — `<slug>/<locale>.html` per locale, plus `interactive.html` and `video.html` — from the same steps. The per-locale pages are canonical; `interactive.html` and `video.html` point `rel=canonical` at the default-locale one and stay out of `sitemap.xml`, so a guide occupies one indexable URL per language rather than three or four near-identical ones. The site root is canonical as `https://…/` rather than `https://…/index.html` for the same reason. Bare directory URLs (`/<slug>/`, `/<group>/`, `/getting-started/`) serve a meta-refresh stub to the default locale; stubs are never listed in the sitemap. The handbook (`--handbook`) carries `noindex` and is excluded too — it duplicates every guide on the site.
|
|
558
|
+
|
|
559
|
+
**`sitemap.xml` is generated, not templated.** Steppy ships no `sitemap.xml.j2`; the document is built by `src/steppy/sitemap.py`. Dropping a `templates/static/sitemap.xml.j2` into your project still takes over — that is the way to reach the parts of the protocol Steppy does not emit, such as `<image:image>` over a guide's screenshots, `<video:video>`, or a `<sitemapindex>` split. The context is `urls` (each with `.loc`, `.alternates[].hreflang` / `.href`, `.lastmod`) plus `lastmod`.
|
|
560
|
+
|
|
561
|
+
**`lastmod` is opt-in.** With `seo.lastmod` unset no `<lastmod>` is emitted at all, which is what keeps a build byte-reproducible. There is deliberately no `'auto'` mode: `actions/checkout` stamps every file at clone time, so an mtime-derived value would read as "now" on every build — which crawlers learn to ignore — and screenshots are not in git at all. Feed it the commit date instead (`git log -1 --format=%cI`). Note that a single site-wide value over-reports, since a commit touching one guide did not change the others; it buys standards-compliance and control, not ranking. `<changefreq>` and `<priority>` are not emitted — Google and Bing both ignore them.
|
|
562
|
+
|
|
563
|
+
**Locales must be BCP-47 — on both sides.** `config.yaml`'s `locales:`, `steppy run --locales`, `steppy guide --locale` and the build config's `locales` are all validated at load, because every one of them becomes a screenshot directory segment that the other side reads back; an unchecked entry only defers the failure to a place that cannot explain it. Each entry in `locales` is validated at config load (`fr`, `fr-BE`, `pt-BR`, `zh-Hant`, `es-419`, …), because a single malformed `hreflang` makes a search engine discard the *whole* alternates cluster. The tag you write is kept **verbatim** in filenames and screenshot directories — it is also your app's i18n key and your `guide-text:` map key — while `hreflang`, `<html lang>` and `og:locale` get the canonical spelling, so `fr_BE` is accepted and becomes `<html lang="fr-BE">` at `…/fr_BE.html`.
|
|
564
|
+
|
|
565
|
+
A regional locale resolves *content* through its bare language, so adding `fr-BE` does not mean rewriting every scenario: `guide-text: { fr: … }`, a `{fr: …}` `LocalizedText` map and an `i18n.localstorage.values` entry keyed `fr` all serve `fr-BE`, and an exact `fr-BE:` key still wins where you write one. When the app cannot be switched into a locale at all — no matching `i18n.localstorage.values` entry, not even by language — the run **warns** rather than skipping quietly, because the screenshots get captured and filed under that locale either way.
|
|
566
|
+
|
|
567
|
+
---
|
|
568
|
+
|
|
569
|
+
## PDF export (opt-in)
|
|
570
|
+
|
|
571
|
+
Two flags, on both `steppy guide` and `steppy build`. Steppy does not lay out a PDF itself — it prints the guide pages it already renders through headless Chromium, so the `@media print` rules in the templates are exactly what the PDF shows (no animated cursor, a step never split across a page break, tall captures scaled to fit instead of clipped).
|
|
572
|
+
|
|
573
|
+
| Flag | `steppy guide` | `steppy build` (static target) |
|
|
574
|
+
|---|---|---|
|
|
575
|
+
| `--pdf` | `<slug>.pdf` next to every generated guide page | `<slug>/<locale>.pdf` per guide page, plus a "PDF" link in the page header |
|
|
576
|
+
| `--handbook` | `<role>-handbook.html` + `.pdf` per (role, locale, layout) | `handbook/<locale>.html` + `.pdf` for the whole site, linked from the index |
|
|
577
|
+
|
|
578
|
+
```bash
|
|
579
|
+
steppy run --guide # capture first
|
|
580
|
+
steppy guide --guide --pdf --handbook
|
|
581
|
+
steppy build --pdf --handbook
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
The **handbook** is every guide in one document — cover, table of contents, then each guide starting on a fresh page (grouped under their group titles in the build target, ordered by slug in `steppy guide`). It is written as real HTML next to the PDF, so it doubles as a single-page read.
|
|
585
|
+
|
|
586
|
+
Paper settings live in `config.yaml` and are shared by both commands:
|
|
587
|
+
|
|
588
|
+
```yaml
|
|
589
|
+
pdf:
|
|
590
|
+
format: A4 # A0–A6 | Letter | Legal | Tabloid | Ledger
|
|
591
|
+
landscape: false
|
|
592
|
+
margin: { top: 12mm, right: 12mm, bottom: 14mm, left: 12mm }
|
|
593
|
+
print_background: true # keep the step badges / role chip coloured
|
|
594
|
+
scale: 1.0 # 0.1–2.0
|
|
595
|
+
page_numbers: true # footer: guide title + "N / M"
|
|
596
|
+
timeout: 30000 # ms per rendered page
|
|
597
|
+
```
|
|
598
|
+
|
|
599
|
+
Notes:
|
|
600
|
+
|
|
601
|
+
- **Chromium is required** (`python -m playwright install chromium`); the error says so if it is missing.
|
|
602
|
+
- One browser renders the whole batch, so a large (scenarios × locales × layouts) project pays a single launch.
|
|
603
|
+
- A page whose screenshots don't finish loading is still printed, with a warning — a PDF missing one image beats no PDF.
|
|
604
|
+
- Multi-layout build pages print the guide's **default layout** (one PDF per guide and locale). The layout toggle is a viewing preference; the file name stays predictable.
|
|
605
|
+
- Chromium's print-to-PDF cannot emit PDF bookmarks or page numbers inside a table of contents, so the handbook's TOC links to in-document anchors only.
|
|
606
|
+
- The handbook page carries `noindex` (in both SEO modes) and stays out of `sitemap.xml` — it duplicates every guide's content, which search engines penalise. It is the one static page that does not include the shared SEO `<head>` partial, so it carries no canonical either. Override `templates/guide.book.html.j2` if you want it indexed.
|
|
607
|
+
- **A PDF cannot carry a `robots` meta**, so the published `handbook/<locale>.pdf` used to be the only indexable form of a page that is deliberately `noindex` — and `<slug>/<locale>.pdf` competes the same way with its own HTML. Every static build now emits a `_headers` sidecar setting `X-Robots-Tag: noindex` on PDFs (on everything, in private mode). **Read the limitation**: Netlify and Cloudflare Pages honour `_headers`; GitHub Pages and GitLab Pages — the two the shipped CI scaffolds target — ignore it, and neither can serve a custom header at all. It is emitted for the same reason `.nojekyll` is: meaningful where it is read, harmless where it is not, and not access control anywhere.
|
|
608
|
+
- `robots.txt` and `_headers` are only honoured at the **origin root**. A GitHub Pages *project* site is served under `/<repo>/`, so a correct `seo.baseUrl` there carries a path and both files land somewhere nothing reads them; the build warns when it detects this. Canonical links, `hreflang` and `sitemap.xml` are absolute and unaffected, and every page still carries its own robots meta.
|
|
609
|
+
- Angular builds reject `--pdf`/`--handbook`: that target emits components for a host app to render, so there is no page for Steppy to print.
|
|
610
|
+
- To retheme, override `--steppy-print-max-image-height` (default `180mm`) in your `templates/` copy — it is the cap that keeps a full-page capture on one page.
|
|
611
|
+
|
|
612
|
+
---
|
|
613
|
+
|
|
614
|
+
## Video guide mode (opt-in)
|
|
615
|
+
|
|
616
|
+
Narrated walkthrough videos, synthesized from the screenshots and cursor
|
|
617
|
+
choreography a run already captured — one MP4 per (guide × locale × layout).
|
|
618
|
+
Nothing is screen-recorded: Steppy replays the same timeline the interactive
|
|
619
|
+
player uses, frame by frame, in headless Chromium, and pipes the stills to
|
|
620
|
+
ffmpeg. So the video is a function of the scenario, not of how fast the render
|
|
621
|
+
machine happens to be, and it regenerates on every release in every language
|
|
622
|
+
alongside the tests that prove the flow still works.
|
|
623
|
+
|
|
624
|
+
> Not to be confused with `steppy run --video`, which records Playwright
|
|
625
|
+
> debugging footage of *failed tests*. Different command, different artifact.
|
|
626
|
+
|
|
627
|
+
**Two gates, both required.** The guide declares the mode (a content decision,
|
|
628
|
+
versioned in the build config); the flag arms the render (a cost decision — a
|
|
629
|
+
render is minutes per guide, and a paid TTS provider bills per character).
|
|
630
|
+
|
|
631
|
+
```ts
|
|
632
|
+
// steppy.guide.config.ts
|
|
633
|
+
guides: [
|
|
634
|
+
{ slug: 'invoices', title: 'Invoices', scenario: 'editor/invoices.md',
|
|
635
|
+
modes: ['stepbystep', 'interactive', 'video'] },
|
|
636
|
+
]
|
|
637
|
+
```
|
|
638
|
+
|
|
639
|
+
```bash
|
|
640
|
+
steppy video check # verify ffmpeg / Chromium / voice, in 5s
|
|
641
|
+
steppy run --guide --locales fr,nl --layouts desktop,mobile
|
|
642
|
+
steppy build --video # MP4 + WebVTT + poster per guide/locale/layout
|
|
643
|
+
steppy guide --video # same, beside each generated guide page
|
|
644
|
+
steppy video cache # what the caches hold; --prune-older-than / --max-size / --clear
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
| Flag | `steppy guide` | `steppy build` |
|
|
648
|
+
|---|---|---|
|
|
649
|
+
| `--video` | `<slug>.mp4` + `.vtt` + `.jpg` next to every generated guide page | `<locale>[-<layout>].mp4` per guide, a `<slug>/video.html` player page (static) or a `<slug>-video.component.ts` host (Angular) |
|
|
650
|
+
|
|
651
|
+
Both build targets are supported. The static target emits a sibling
|
|
652
|
+
`video.html` (like `interactive.html`) with a locale/layout switcher; Angular
|
|
653
|
+
emits a component wired into the existing mode toggle, with the media served
|
|
654
|
+
from `assetsDir` like every other asset.
|
|
655
|
+
|
|
656
|
+
### Narration
|
|
657
|
+
|
|
658
|
+
Voiceover text is the per-locale `guide-text` you already wrote — there is no
|
|
659
|
+
new authoring surface. The provider is chosen entirely in config:
|
|
660
|
+
|
|
661
|
+
```yaml
|
|
662
|
+
video:
|
|
663
|
+
fps: 24 # cost knob: a render is frames × screenshot latency
|
|
664
|
+
width: 1280 # height follows the capture aspect, so a mobile
|
|
665
|
+
# layout yields a portrait video. NOTE: cursor travel
|
|
666
|
+
# is timed over display distance, so changing width
|
|
667
|
+
# changes the video's duration.
|
|
668
|
+
captions: band # band | off
|
|
669
|
+
crf: 23
|
|
670
|
+
preset: medium
|
|
671
|
+
intro_ms: 1200
|
|
672
|
+
outro_ms: 1500
|
|
673
|
+
min_step_ms: 1500 # floor on a step's on-screen time
|
|
674
|
+
narration_pad_ms: 400 # breathing room after the voiceover
|
|
675
|
+
branding: { logo: ./assets/logo.png, accent: '#d24f2c' }
|
|
676
|
+
stamp:
|
|
677
|
+
enabled: true # burn a provenance line into every frame
|
|
678
|
+
text: "{version} · {date}"
|
|
679
|
+
version: ${STEPPY_RELEASE:-dev}
|
|
680
|
+
tts:
|
|
681
|
+
provider: none # captions only — the default: no install, no bill
|
|
682
|
+
voice: { fr: fr_FR-siwis-medium, nl: nl_NL-mls-medium }
|
|
683
|
+
api_key: ${ELEVENLABS_API_KEY}
|
|
684
|
+
```
|
|
685
|
+
|
|
686
|
+
| `provider` | What it is |
|
|
687
|
+
|---|---|
|
|
688
|
+
| `none` *(default)* | Burned-in captions, silent track. No install, no network, no cost. |
|
|
689
|
+
| `piper` | Free, offline, neural, deterministic. `pip install piper-tts` ([OHF-Voice/piper1-gpl](https://github.com/OHF-Voice/piper1-gpl)); voices download on first use into `.steppy/voices/`. Run as a subprocess, never imported — it is GPL-3.0 and deliberately not a Steppy dependency. |
|
|
690
|
+
| `espeak` | Free, offline, no download. Audibly robotic; a fallback, not a finish. |
|
|
691
|
+
| `elevenlabs` `openai` `azure` `google` | Cloud voices. Set `api_key` (and `region` for Azure). |
|
|
692
|
+
| `http` | Any REST API, described directly under `video.tts.http` — no preset, no code, no Steppy release needed. |
|
|
693
|
+
| *your own* | Register a `steppy.tts_providers` entry point. |
|
|
694
|
+
|
|
695
|
+
Swapping providers is a config edit and nothing else: the vendor presets are
|
|
696
|
+
data rows over one generic HTTP engine, so no code path branches on vendor.
|
|
697
|
+
|
|
698
|
+
Notes:
|
|
699
|
+
|
|
700
|
+
- **ffmpeg is required**, including with `tts.provider: none` — a silent captioned video still has to be encoded. `steppy video check` tells you before a long render does.
|
|
701
|
+
- **Chromium is required** (`python -m playwright install --with-deps chromium`). Use `--with-deps`: a bare install has no CJK/Arabic fonts, and captions are burned into the frame permanently.
|
|
702
|
+
- Put the API key in the environment (`${ELEVENLABS_API_KEY}`), not in `config.yaml` — a plaintext key earns a hygiene warning, and an unresolved `${VAR}` is rejected with the variable name rather than becoming a confusing vendor 401.
|
|
703
|
+
- Frames are deduplicated: a hold or a voiceover pause reuses the previous still instead of re-screenshotting. Holds dominate a narrated video, so this is most of the render budget.
|
|
704
|
+
- Each step is stretched to fit its voiceover, so narration is never cut off mid-sentence.
|
|
705
|
+
- A WebVTT caption track and a poster frame are emitted beside every MP4.
|
|
706
|
+
|
|
707
|
+
### What an edit actually costs
|
|
708
|
+
|
|
709
|
+
Narration is synthesized **one clip per step**, keyed on the *text* rather than
|
|
710
|
+
the step's position, and assembled onto the timeline afterwards. So editing one
|
|
711
|
+
sentence re-synthesizes one sentence — reordering steps, inserting a step, or
|
|
712
|
+
adding a guide that reuses a line costs nothing at all, and a paid provider is
|
|
713
|
+
never re-billed for text that has not changed.
|
|
714
|
+
|
|
715
|
+
Videos are cached whole, because they have to be: a step's duration changes with
|
|
716
|
+
its narration, and every later step shifts with it, so no rendered frame
|
|
717
|
+
survives an edit. The two caches therefore invalidate on different things.
|
|
718
|
+
|
|
719
|
+
| Cache | Key includes | So it re-runs when |
|
|
720
|
+
|---|---|---|
|
|
721
|
+
| `.steppy/tts-cache/<sha>.wav` | provider, engine version (or the `http` request shape), text, locale, voice, model, speed, format | that step's wording changes — nothing else |
|
|
722
|
+
| `.steppy/video-cache/<sha>.mp4` | the manifest, **screenshot bytes**, clip bytes and durations, branding, the player bundle, and the pixel-affecting `video:` settings | any of those change. Re-capturing screenshots re-renders even if the manifest is identical; setting an API key or a timeout does **not** |
|
|
723
|
+
|
|
724
|
+
Whitespace is normalized before hashing, so re-wrapping a `guide-text` line is
|
|
725
|
+
not a re-bill. Both caches are written atomically — an interrupted render leaves
|
|
726
|
+
no entry rather than a truncated one that would be served forever.
|
|
727
|
+
|
|
728
|
+
Both grow monotonically. `steppy video cache` reports what they hold;
|
|
729
|
+
`--prune-older-than <days>` and `--max-size <MB>` reclaim least-recently-*used*
|
|
730
|
+
entries first (a lookup touches what it reads, so a clip rebuilt into every
|
|
731
|
+
release stays young), `--clear` empties them, and `--tts` / `--video` scope any
|
|
732
|
+
of those to one cache.
|
|
733
|
+
|
|
734
|
+
---
|
|
735
|
+
|
|
736
|
+
## In-app tours (opt-in)
|
|
737
|
+
|
|
738
|
+
The scenarios that test a flow and document it can also teach it inside the
|
|
739
|
+
app: a tour is a sequence of popovers over the live application, each pointing
|
|
740
|
+
at the element a step acts on. Steppy compiles tours from your scenarios —
|
|
741
|
+
never by hand — and **CI replays every tour**, so renaming a test-id a tour
|
|
742
|
+
depends on fails the build with the tour, the step and the test-id named,
|
|
743
|
+
instead of breaking the tour in production.
|
|
744
|
+
|
|
745
|
+
1. Enable tours and say where your app serves them from:
|
|
746
|
+
|
|
747
|
+
```yaml
|
|
748
|
+
# config.yaml
|
|
749
|
+
tours:
|
|
750
|
+
enabled: true
|
|
751
|
+
targets:
|
|
752
|
+
- type: app-assets
|
|
753
|
+
path: ../invoice-app/public/tours
|
|
754
|
+
```
|
|
755
|
+
|
|
756
|
+
2. Opt a scenario in with `tour: true` in its frontmatter (or a `tour:` block
|
|
757
|
+
with an id, a title, an entry route). Steps can be fine-tuned with
|
|
758
|
+
`tour: skip|keep`, `tour-text`, `tour-advance` and `tour-placement`.
|
|
759
|
+
|
|
760
|
+
3. Mount the overlay once in your app and start a tour from a button. Import
|
|
761
|
+
`@steppy/player/tour` from your app's own code (it registers the element),
|
|
762
|
+
then:
|
|
763
|
+
|
|
764
|
+
```html
|
|
765
|
+
<steppy-tour-host fallback-manifest="/tours/tours.manifest.json"></steppy-tour-host>
|
|
766
|
+
```
|
|
767
|
+
|
|
768
|
+
Angular apps use `@steppy/angular` instead: `provideSteppyTours()`,
|
|
769
|
+
`<steppy-tours />` once in the shell, and the `SteppyTours` service.
|
|
770
|
+
|
|
771
|
+
| What | Where |
|
|
772
|
+
|---|---|
|
|
773
|
+
| Canonical tour files (manifest + one file per tour and locale, SRI-pinned) | `output/tours/<build-id>/` |
|
|
774
|
+
| The copy your app bundles | each `app-assets` target (flat) |
|
|
775
|
+
| The copy a static guide site serves, for remote mode | `<site>/tours/<build-id>/` (`steppy build`, `guide` target) |
|
|
776
|
+
| The CI replay | `test_<slug>__tour` in the scenario's own test file |
|
|
777
|
+
|
|
778
|
+
A step points at one test-id value, never a CSS selector; tour files are data,
|
|
779
|
+
never code; the overlay never clicks for the user. The full reference —
|
|
780
|
+
configuration, annotations, how steps are inferred, the host element's API and
|
|
781
|
+
events, [localization](docs/tours.md#localization) (buttons ship in English,
|
|
782
|
+
French, Dutch and German; the tour follows the app's language mid-tour), React,
|
|
783
|
+
Vue and Angular wiring, theming, CSP and CORS, the replay — is
|
|
784
|
+
[docs/tours.md](docs/tours.md). `fixtures/tour-demo/` (plain JavaScript) and
|
|
785
|
+
`fixtures/tour-demo-angular/` (Angular, PrimeNG, ngx-translate) are working
|
|
786
|
+
examples driven by the same scenarios.
|
|
787
|
+
|
|
788
|
+
## Parallel execution (opt-in)
|
|
789
|
+
|
|
790
|
+
`steppy run --workers N` spins up N isolated Docker Compose stacks pulling scenarios from a shared queue. **Strictly opt-in** — `steppy run` (no flags) and `steppy run --workers 1` never touch Docker.
|
|
791
|
+
|
|
792
|
+
### When to use it
|
|
793
|
+
|
|
794
|
+
A 1-hour sequential suite typically completes in ~20–25 min with `--workers 3`. Each stack runs its own copy of your app + chromium, so you need RAM + CPU headroom.
|
|
795
|
+
|
|
796
|
+
### Setup
|
|
797
|
+
|
|
798
|
+
1. Add a `parallel:` block to `config.yaml` and an `internal_url` per role:
|
|
799
|
+
|
|
800
|
+
```yaml
|
|
801
|
+
apps:
|
|
802
|
+
editor:
|
|
803
|
+
url: "http://localhost:4200" # host-facing (single-worker mode)
|
|
804
|
+
internal_url: "http://editor-app:3000" # inside the docker network
|
|
805
|
+
# ...
|
|
806
|
+
parallel:
|
|
807
|
+
workers: 3 # default if --workers omitted
|
|
808
|
+
compose_file: "docker-compose.steppy.yml"
|
|
809
|
+
# Optional — capture auth at the in-network origin before dispatching
|
|
810
|
+
# workers. Needed for `mode: storage-state` (see "Pre-authentication in
|
|
811
|
+
# worker mode" below); omit it entirely if you don't use storage-state auth.
|
|
812
|
+
# pre_auth:
|
|
813
|
+
# command: "python auth_login.py --internal"
|
|
814
|
+
```
|
|
815
|
+
|
|
816
|
+
2. Make sure your `compose_file` declares healthchecks for every service referenced by `depends_on: condition: service_healthy`. The coordinator pre-flights this and refuses to start otherwise.
|
|
817
|
+
|
|
818
|
+
3. Run:
|
|
819
|
+
|
|
820
|
+
```bash
|
|
821
|
+
steppy run --workers 3
|
|
822
|
+
```
|
|
823
|
+
|
|
824
|
+
First parallel run auto-builds the `steppy-worker:<version>` image (~3 min). Subsequent runs reuse it.
|
|
825
|
+
|
|
826
|
+
### Prerequisites
|
|
827
|
+
|
|
828
|
+
- **Docker Compose 2.20+** (uses the `!reset` YAML tag to clear inherited port mappings).
|
|
829
|
+
- **Per-role `internal_url`** for every role used by selected scenarios.
|
|
830
|
+
- **Healthchecks** on every service referenced via `depends_on: condition: service_healthy`.
|
|
831
|
+
|
|
832
|
+
### Rescue command
|
|
833
|
+
|
|
834
|
+
If a parallel run is force-killed before tearing down its stacks:
|
|
835
|
+
|
|
836
|
+
```bash
|
|
837
|
+
steppy stacks cleanup
|
|
838
|
+
```
|
|
839
|
+
|
|
840
|
+
Lists every `steppy-run-*` Compose project and tears each one down.
|
|
841
|
+
|
|
842
|
+
### Parallel + guide mode
|
|
843
|
+
|
|
844
|
+
`steppy run --workers N --guide` composes cleanly. Each scenario is claimed by exactly one worker; that worker's pytest invocation handles every (locale × layout) cell sequentially in one process — browser launched once, auth amortized. Screenshots land in the shared `output/` bind mount at the same paths a single-worker `--guide` run produces, so `steppy guide --guide` and `steppy build` (pure disk readers) work unchanged.
|
|
845
|
+
|
|
846
|
+
### Scenarios that must not run concurrently (`exclusive:`)
|
|
847
|
+
|
|
848
|
+
Work stealing assumes every scenario is independent of every other. Some are not:
|
|
849
|
+
two flows that mutate the same record, drain the same mailbox, or consume the same
|
|
850
|
+
third-party sandbox cannot be in flight at once. Declare an **exclusion group** in
|
|
851
|
+
frontmatter and Steppy will never run two of its members at the same time:
|
|
852
|
+
|
|
853
|
+
```yaml
|
|
854
|
+
---
|
|
855
|
+
title: Checkout with a stored card
|
|
856
|
+
exclusive: billing
|
|
857
|
+
---
|
|
858
|
+
```
|
|
859
|
+
|
|
860
|
+
Any scenario naming `billing` is serialised against every other one that does.
|
|
861
|
+
Different groups still run concurrently, and ungrouped scenarios are untouched —
|
|
862
|
+
so `{s1, s3}` in `billing` and `{s2, s5}` in `mail` gives you `s1 ∥ s2` and
|
|
863
|
+
`s3 ∥ s5`, but never `s1 ∥ s3`.
|
|
864
|
+
|
|
865
|
+
Mechanically, the coordinator collapses each group into a **single queue task**
|
|
866
|
+
before any container starts; one worker claims it and runs the members in
|
|
867
|
+
sequence, one pytest process each. There is no lock and no waiting state, so a
|
|
868
|
+
worker dying mid-group is reported and retried like any other failure. The run
|
|
869
|
+
prints its census next to the queue line:
|
|
870
|
+
|
|
871
|
+
```
|
|
872
|
+
queued 11 scenario(s) in 6 task(s) across 4 worker(s)
|
|
873
|
+
exclusion groups: billing[3], mail[2] — 2 group(s) serialising 5 scenario(s) into 2 task(s)
|
|
874
|
+
```
|
|
875
|
+
|
|
876
|
+
Four things worth knowing before you declare one:
|
|
877
|
+
|
|
878
|
+
- **It costs wall-clock.** The run can never finish faster than the group's total
|
|
879
|
+
duration. A group is free while that total stays under *(suite total ÷ workers)*;
|
|
880
|
+
past that, the makespan is the heaviest group's total.
|
|
881
|
+
- **Non-overlap only** — not ordering, and not indivisibility. Members may run in
|
|
882
|
+
any order, and a retry pass legitimately re-runs only the ones that failed.
|
|
883
|
+
- **The name is your assertion, and nothing verifies it.** Two differently-named
|
|
884
|
+
groups that actually contend on the same system still run concurrently. If a
|
|
885
|
+
flow touches two shared resources, every scenario touching *either* must use the
|
|
886
|
+
same name — naming the combination (`stripe-and-mailbox`) does **not** exclude
|
|
887
|
+
it against a scenario named `stripe`. `steppy validate` warns about a group with
|
|
888
|
+
a single member, which is what a typo looks like.
|
|
889
|
+
- **Scope is one run.** Two concurrent `steppy run` invocations — CI plus a dev
|
|
890
|
+
box against one shared sandbox — do not see each other's groups. And it is inert
|
|
891
|
+
without `--workers N ≥ 2`, correctly: one pytest process is already sequential.
|
|
892
|
+
|
|
893
|
+
### Pre-authentication in worker mode (`pre_auth`)
|
|
894
|
+
|
|
895
|
+
`mode: storage-state` auth needs care in parallel mode. Playwright storage state is **origin-bound** — a session captured on the host (`http://localhost:4200`) does *not* authenticate a worker, which reaches the app at its `internal_url` (`http://web:80`). Left alone, every scenario in a worker run redirects to login.
|
|
896
|
+
|
|
897
|
+
`parallel.pre_auth` closes the gap: before dispatching workers, the coordinator runs your `command` in a one-off `steppy-preauth` container **inside the stack network**, gated (Compose `service_completed_successfully`) to finish before any worker claims a scenario. Point it at whatever logs in at the internal origin and writes `.auth/<role>.json`:
|
|
898
|
+
|
|
899
|
+
```yaml
|
|
900
|
+
parallel:
|
|
901
|
+
pre_auth:
|
|
902
|
+
command: "python auth_login.py --internal" # runs in-network; captures the internal origin
|
|
903
|
+
timeout: 600 # optional (default 600s)
|
|
904
|
+
# image: my/custom:tag # optional (defaults to the worker image)
|
|
905
|
+
```
|
|
906
|
+
|
|
907
|
+
- The command runs with `STEPPY_BASE_URL_OVERRIDE` (`{role: internal_url}`) in its env, the project mounted read-only, and `.auth/` mounted read-write — so it can resolve each role's `internal_url` and write the cache all workers share.
|
|
908
|
+
- It's **deduped once per run** across the N stacks: an O_EXCL lock elects a single winner that logs in; the other stacks wait for its sentinel. Make the command idempotent (skip roles whose session is still fresh) to keep repeat runs cheap.
|
|
909
|
+
- The preauth container waits for every app service that declares a **healthcheck** before running — add a healthcheck to your web/frontend service so the login doesn't race startup.
|
|
910
|
+
- Leaving `pre_auth` unset renders no preauth service and keeps dispatch byte-identical to before. (`bypass` / `token` auth, or a host-and-internal same-origin setup, don't need it.)
|
|
911
|
+
|
|
912
|
+
### Known caveats
|
|
913
|
+
|
|
914
|
+
- **Shared `.auth/<role>.json`.** With `auth.storage_state_ttl > 0` (default 3600s), one worker's cached session may be reused by another worker whose stack has different signing keys. If each stack runs an isolated app + DB, set `auth.storage_state_ttl: 0` to force per-worker re-auth. For `mode: storage-state`, remember sessions are also **origin-bound** — use `pre_auth` (above) to capture the in-network origin, or workers will redirect to login.
|
|
915
|
+
- **Seed scripts run inside the worker container.** Hard-coded `http://localhost:5432` URLs are unreachable. Have seeds read the `STEPPY_BASE_URL_OVERRIDE` JSON env var (`{role: internal_url}`) and fall back to the host URL when unset (single-worker mode).
|
|
916
|
+
- **Only `output/` comes back from the container.** The project itself is mounted read-only and `output/` is the one writable bind mount, so an `output.screenshots_dir` pointing outside it cannot produce host-visible captures. The coordinator warns at startup if you have configured one.
|
|
917
|
+
- **v0.1 maturity.** Parallel mode is single-machine and Linux-container only (Docker + Compose 2.20+). A worker crash mid-scenario is fatal — the scenario is not requeued (`Queue.orphaned_running_tasks()` reports it). Use `steppy stacks cleanup` to tear down orphaned stacks after a force-kill.
|
|
918
|
+
|
|
919
|
+
---
|
|
920
|
+
|
|
921
|
+
## Continuous integration (opt-in)
|
|
922
|
+
|
|
923
|
+
Steppy runs anywhere pytest runs. Two pieces make it CI-native:
|
|
924
|
+
|
|
925
|
+
- **Reports** — set a `report:` block in `config.yaml` and `steppy run` emits the
|
|
926
|
+
matching artifacts:
|
|
927
|
+
|
|
928
|
+
```yaml
|
|
929
|
+
report:
|
|
930
|
+
junit_xml: "./output/reports/junit.xml" # native JUnit (no extra deps)
|
|
931
|
+
html: "./output/reports/report.html" # needs: pip install 'steppy-dev[report]'
|
|
932
|
+
trace: on-failure # off | on | on-failure
|
|
933
|
+
```
|
|
934
|
+
|
|
935
|
+
JUnit XML is ingested natively by most CI systems. `trace: on-failure` (or the
|
|
936
|
+
`steppy run --trace` flag) saves a Playwright trace zip under `output/traces/`
|
|
937
|
+
for each failed test — open it with `playwright show-trace <zip>` for a
|
|
938
|
+
step-by-step, DOM-snapshotted replay. The per-failure full-page screenshot +
|
|
939
|
+
console/network dump under `output/screenshots/_failures/` is always produced.
|
|
940
|
+
|
|
941
|
+
- **Workflow templates** — ready-to-copy pipelines ship in the package for both
|
|
942
|
+
vendors: `templates/ci/github-actions.yml` and `templates/ci/gitlab-ci.yml`
|
|
943
|
+
(checkout → install → `playwright install chromium` → `steppy generate` →
|
|
944
|
+
`steppy run --bundle` → `steppy report` → upload). `steppy init --ci` writes the
|
|
945
|
+
GitHub one, `--ci-gitlab` the GitLab one; map your credential secrets onto the
|
|
946
|
+
`${VAR}` refs in `config.yaml`. Each carries a commented, opt-in Pages job — see
|
|
947
|
+
[Publishing to Pages](#publishing-to-pages).
|
|
948
|
+
|
|
949
|
+
---
|
|
950
|
+
|
|
951
|
+
## Run report (opt-in)
|
|
952
|
+
|
|
953
|
+
`steppy report` turns one run into a self-contained static site: what passed, what
|
|
954
|
+
failed, **which step it failed on**, and the screenshot at that moment.
|
|
955
|
+
|
|
956
|
+
```bash
|
|
957
|
+
steppy run --bundle # records per-test and per-step events
|
|
958
|
+
steppy report # → output/report/index.html
|
|
959
|
+
```
|
|
960
|
+
|
|
961
|
+
It reads a live `output/` tree, a `.tar.zst`/`.tar.gz` results bundle, or a
|
|
962
|
+
directory somebody already unpacked one into — all three produce the same report:
|
|
963
|
+
|
|
964
|
+
```bash
|
|
965
|
+
steppy report # this project's output/
|
|
966
|
+
steppy report steppy-bundle-*.tar.zst # a bundle downloaded from CI
|
|
967
|
+
steppy report ./unpacked-bundle -o public # after tar -xf
|
|
968
|
+
```
|
|
969
|
+
|
|
970
|
+
### What it shows
|
|
971
|
+
|
|
972
|
+
- **Failure causes, grouped.** One broken selector across forty scenarios reads as
|
|
973
|
+
one row with a count, not forty tracebacks — the same normalisation the parallel
|
|
974
|
+
console summary uses. The full tail is shown once per cause.
|
|
975
|
+
- **The failing step, named.** `Failed at step 7 — Submit the order`, with the
|
|
976
|
+
step timeline around it (title, action, elapsed) and the captures for each step.
|
|
977
|
+
Step numbers are the scenario's own and stay sparse: a step skipped by
|
|
978
|
+
`only-in:` or `layouts:` is a gap, never renumbered.
|
|
979
|
+
- **A scenario × (locale, layout) matrix**, built from what actually ran — a flow
|
|
980
|
+
deselected by a disjoint frontmatter `layouts:` does not grow an empty column.
|
|
981
|
+
- **Coverage buckets and the e2e-surface percentage**, when the run recorded them.
|
|
982
|
+
- **`report.json`** — the same model, versioned (`steppy_report: 1`) and
|
|
983
|
+
deterministic, so CI can diff two of them. Shape:
|
|
984
|
+
[`contracts/report.schema.json`](contracts/report.schema.json).
|
|
985
|
+
|
|
986
|
+
### Screenshots
|
|
987
|
+
|
|
988
|
+
`--screenshots failed` (the default) copies the captures of variants that did not
|
|
989
|
+
pass, plus every failure screenshot. `all` copies everything — worth knowing that
|
|
990
|
+
on a 4-locale × 2-layout corpus that is hundreds of megabytes. `none` renders text
|
|
991
|
+
only. Captures identical across layouts are copied once.
|
|
992
|
+
|
|
993
|
+
Traces and videos are **named, not copied**: a Playwright trace needs
|
|
994
|
+
`playwright show-trace` to open, so a link would be a dead end.
|
|
995
|
+
|
|
996
|
+
### Honest about what it cannot know
|
|
997
|
+
|
|
998
|
+
A report of a run made without `--bundle` says so and lists the scenarios it found
|
|
999
|
+
captures for, rather than rendering an empty page. A run with no per-step events
|
|
1000
|
+
says *that* instead of drawing an empty timeline. Coverage rebuilt from a detached
|
|
1001
|
+
bundle's raw records refuses to report assert *strength*, because the scenario
|
|
1002
|
+
files it would need to count assert steps in never travel — every endpoint would
|
|
1003
|
+
otherwise read as weakly covered.
|
|
1004
|
+
|
|
1005
|
+
**The exit code describes rendering, not the run.** A report of a failing run exits
|
|
1006
|
+
0. Gate your build on `steppy run`, or read `totals.exit_code` from `report.json`.
|
|
1007
|
+
|
|
1008
|
+
### Publishing to Pages
|
|
1009
|
+
|
|
1010
|
+
The site is relative and self-contained, so publishing it is a copy. Both shipped
|
|
1011
|
+
CI templates carry the job, commented out, plus the warnings below.
|
|
1012
|
+
|
|
1013
|
+
> **Pages is public.** Your screenshots show your app with real-looking data in it.
|
|
1014
|
+
> `screenshot.mask` redacts fields at capture time and the report marks every page
|
|
1015
|
+
> `noindex` with a `Disallow: /` robots.txt — but a leak is permanent and cannot be
|
|
1016
|
+
> taken back. This is for showcase and open-source repos; for client work, read the
|
|
1017
|
+
> report from the CI artifact instead. GitLab Pages can be restricted to project
|
|
1018
|
+
> members, which is the one place it beats GitHub's for this.
|
|
1019
|
+
|
|
1020
|
+
Deploy as an **artifact, never a `gh-pages` branch** — screenshots committed on
|
|
1021
|
+
every run grow the repository's history forever.
|
|
1022
|
+
|
|
1023
|
+
### Coverage badge
|
|
1024
|
+
|
|
1025
|
+
With `--coverage` recorded, the site includes a shields.io endpoint at
|
|
1026
|
+
`badge.json`:
|
|
1027
|
+
|
|
1028
|
+
```markdown
|
|
1029
|
+

|
|
1030
|
+
```
|
|
1031
|
+
|
|
1032
|
+
The number is **`tested / (declared − non_ui)`** over your declared routes and API
|
|
1033
|
+
endpoints — not line coverage. Declare `coverage.non_ui` in `config.yaml` first
|
|
1034
|
+
(health checks, webhook receivers, OAuth callbacks, internal cron): triage flags
|
|
1035
|
+
set with `steppy coverage triage` live in the gitignored `.steppy/coverage.db`, so
|
|
1036
|
+
a fresh CI checkout has none and those endpoints would count against you.
|
|
1037
|
+
|
|
1038
|
+
---
|
|
1039
|
+
|
|
1040
|
+
## Results bundles (opt-in)
|
|
1041
|
+
|
|
1042
|
+
A **results bundle** is one run's artifacts in a single portable file: a `.tar.zst` (or `.tar.gz`) holding your `output/` tree **exactly as Steppy wrote it** — no path rewriting — plus three generated members.
|
|
1043
|
+
|
|
1044
|
+
```
|
|
1045
|
+
manifest.json what this run was, what it produced, sha256 of every file
|
|
1046
|
+
junit.xml every worker's JUnit XML merged into one document
|
|
1047
|
+
events/ per-test records: <scenario>__<locale>-<layout>.jsonl
|
|
1048
|
+
output/ screenshots, sidecars, coverage records, guides, failures, traces, videos
|
|
1049
|
+
```
|
|
1050
|
+
|
|
1051
|
+
Because the tree is unrewritten, `tar -xf` then `steppy guide` or `steppy build` just works — the bundle is an archive of a run, not a new format to convert back out of.
|
|
1052
|
+
|
|
1053
|
+
### Producing one
|
|
1054
|
+
|
|
1055
|
+
```bash
|
|
1056
|
+
steppy run --guide --bundle # capture events + JUnit alongside the normal run
|
|
1057
|
+
steppy bundle create # → output/bundles/steppy-bundle-<run-id>.tar.zst
|
|
1058
|
+
```
|
|
1059
|
+
|
|
1060
|
+
`--bundle` is opt-in because it changes the pytest invocation. Without it a bundle is still valid, but `manifest.tests[]` will be empty — there is nothing on disk recording what passed.
|
|
1061
|
+
|
|
1062
|
+
It also records step boundaries, so a failure names the step rather than only the test:
|
|
1063
|
+
|
|
1064
|
+
```
|
|
1065
|
+
outcome=failed scenario=order-lifecycle locale=fr layout=mobile failed_step=7
|
|
1066
|
+
```
|
|
1067
|
+
|
|
1068
|
+
`failed_step` is the step that started and never finished, which is why a test that dies in fixture teardown reports `null` instead of blaming its last step. Export `STEPPY_STEP_EVENTS=0` to keep everything else and drop the per-step records.
|
|
1069
|
+
|
|
1070
|
+
It composes with everything else, including `--workers N`: each worker writes into the shared `output/` mount under per-scenario filenames, and `steppy bundle create` merges them.
|
|
1071
|
+
|
|
1072
|
+
### Reading one
|
|
1073
|
+
|
|
1074
|
+
```bash
|
|
1075
|
+
steppy bundle inspect bundle.tar.zst # manifest summary; --json for the raw manifest
|
|
1076
|
+
steppy bundle inspect bundle.tar.zst --json # feed it to jq, or to a CI annotation
|
|
1077
|
+
steppy bundle verify bundle.tar.zst # re-hash every member; non-zero on any mismatch
|
|
1078
|
+
```
|
|
1079
|
+
|
|
1080
|
+
### Compression
|
|
1081
|
+
|
|
1082
|
+
`.tar.zst` when the optional extra is installed, `.tar.gz` otherwise — automatically, with a notice:
|
|
1083
|
+
|
|
1084
|
+
```bash
|
|
1085
|
+
pip install 'steppy-dev[bundle]' # adds zstandard
|
|
1086
|
+
steppy bundle create --format gz # or force gzip explicitly
|
|
1087
|
+
```
|
|
1088
|
+
|
|
1089
|
+
An explicitly named output file decides the codec, so `-o run.tar.zst` never silently produces gzip content.
|
|
1090
|
+
|
|
1091
|
+
### The manifest is a published contract
|
|
1092
|
+
|
|
1093
|
+
`manifest.json` is versioned (`steppy_bundle: 1`) and its shape is committed at
|
|
1094
|
+
[`contracts/bundle-manifest.schema.json`](contracts/bundle-manifest.schema.json),
|
|
1095
|
+
generated from the models in `src/steppy/contracts/` and drift-guarded by a test.
|
|
1096
|
+
Write a consumer against it in any language.
|
|
1097
|
+
|
|
1098
|
+
Two rules make it safe to build on: fields are only ever **added**, never
|
|
1099
|
+
repurposed, and readers default what is absent. So a newer Steppy's bundle stays
|
|
1100
|
+
readable by an older consumer, and an artifact `kind` you have never seen is
|
|
1101
|
+
something to store rather than something to fail on.
|
|
1102
|
+
|
|
1103
|
+
### Useful properties
|
|
1104
|
+
|
|
1105
|
+
- **Reproducible.** The same `output/` tree bundles to the same bytes: members are sorted, timestamps and ownership are normalised, and the run's own creation time comes from the run rather than the clock.
|
|
1106
|
+
- **Content-addressed.** Every archived file carries its sha256 in the manifest, so a consumer can fetch only what it does not already have.
|
|
1107
|
+
- **Honest about what is missing.** `--max-size` drops the cheapest-to-lose artifacts first (videos, then traces, then guides — all regenerable), sets `truncated: true`, and lists every drop. Screenshots and their sidecars are never dropped.
|
|
1108
|
+
|
|
1109
|
+
### In CI
|
|
1110
|
+
|
|
1111
|
+
```yaml
|
|
1112
|
+
- run: steppy run --guide --bundle
|
|
1113
|
+
- run: steppy bundle create
|
|
1114
|
+
- uses: actions/upload-artifact@v4
|
|
1115
|
+
if: always()
|
|
1116
|
+
with:
|
|
1117
|
+
name: steppy-bundle
|
|
1118
|
+
path: output/bundles/
|
|
1119
|
+
```
|
|
1120
|
+
|
|
1121
|
+
---
|
|
1122
|
+
|
|
1123
|
+
## Coverage map (opt-in)
|
|
1124
|
+
|
|
1125
|
+
`steppy run --coverage` records what each scenario actually exercises and merges it into a persistent map, so you can ask the inverse question: **what is NOT yet tested?** Everything is deterministic — no LLM calls in recording, merging, or gap computation — and framework-agnostic (URL, History API, V8 coverage, network requests; the app under test can be Angular, React, Vue, …).
|
|
1126
|
+
|
|
1127
|
+
Three signals per scenario run, captured by the generated conftest (env-gated on `STEPPY_COVERAGE=1`, zero overhead when off):
|
|
1128
|
+
|
|
1129
|
+
- **Route trail** — every URL the run visited, SPA-aware: Playwright's `framenavigated` fires for History-API changes, and an injected hook tags push/replace/pop/hash transitions. Dynamic segments cluster (`/invoices/123` + `/invoices/456` → `/invoices/:id`).
|
|
1130
|
+
- **API endpoints** — every request's `{method, url}`, attributed to a configured API by base-URL prefix and matched against its OpenAPI path templates.
|
|
1131
|
+
- **Source modules** — V8 JS coverage via CDP (Chromium), mapped back to source files through source maps (read from `coverage.sourcemap_dir` on disk; inline `data:` maps captured at record time). Degrades gracefully: no map → bundle-level granularity; non-Chromium → routes/endpoints still work.
|
|
1132
|
+
|
|
1133
|
+
### Flow
|
|
1134
|
+
|
|
1135
|
+
```bash
|
|
1136
|
+
steppy run --coverage --scenario my-flow # 1. record (works per-scenario) + auto-merge
|
|
1137
|
+
steppy coverage refresh # 2. declared surface from openapi.json/sitemap.xml
|
|
1138
|
+
steppy coverage report --json gaps.json # 3. gap buckets
|
|
1139
|
+
```
|
|
1140
|
+
|
|
1141
|
+
The report buckets every endpoint/route as **tested** (declared ∩ exercised; *strong* when an asserting scenario covered it, *weak* when only assert-free or `--no-asserts`/`--guide` runs did), **declared-unreached** (in the manifest, never hit — the key bucket for directing new scenarios), or **discovered-uncovered** (seen in a trail, weakly covered). Endpoints that aren't UI-reachable (payment webhooks, cron callbacks) get a persisted triage flag so they stop polluting the actionable list:
|
|
1142
|
+
|
|
1143
|
+
```bash
|
|
1144
|
+
steppy coverage triage set /webhooks/stripe --method POST --api my-api --flag non_ui --note "payment callback"
|
|
1145
|
+
```
|
|
1146
|
+
|
|
1147
|
+
**In CI, declare them in `config.yaml` instead.** The flag above is stored in
|
|
1148
|
+
`.steppy/coverage.db`, which is gitignored — so a fresh checkout has an empty store,
|
|
1149
|
+
every endpoint reads `unknown`, and any percentage computed there is wrong for
|
|
1150
|
+
reasons nobody can fix from CI:
|
|
1151
|
+
|
|
1152
|
+
```yaml
|
|
1153
|
+
coverage:
|
|
1154
|
+
non_ui:
|
|
1155
|
+
- "GET /health"
|
|
1156
|
+
- "/webhooks/*"
|
|
1157
|
+
- "POST /internal/cron/*"
|
|
1158
|
+
```
|
|
1159
|
+
|
|
1160
|
+
Grammar is `[<METHOD> ]<glob>`. `*` crosses `/`, a pattern applies across every
|
|
1161
|
+
configured API, and a method-qualified pattern never matches a route.
|
|
1162
|
+
`steppy coverage report` warns about a pattern that matches nothing — a renamed
|
|
1163
|
+
endpoint leaves a stale rule that silently inflates the number. An explicit
|
|
1164
|
+
`triage set --flag ui_testable` still wins over a pattern; nothing is ever written
|
|
1165
|
+
back to the store.
|
|
1166
|
+
|
|
1167
|
+
The percentage this settles is **`tested / (declared − non_ui)`**, printed by
|
|
1168
|
+
`steppy coverage report` and available as a shields.io badge:
|
|
1169
|
+
|
|
1170
|
+
```bash
|
|
1171
|
+
steppy coverage report --badge output/badge.json # also --badge-surface endpoints|routes|all
|
|
1172
|
+
```
|
|
1173
|
+
|
|
1174
|
+
### Storage
|
|
1175
|
+
|
|
1176
|
+
- **Raw records** (lossless observations): `output/coverage/raw/<run_id>/<scenario>/*.json` — one per page fixture. All interpretation happens at merge time, so normalization rules can evolve without recapturing.
|
|
1177
|
+
- **The map**: sqlite at `.steppy/coverage.db` (configurable `coverage.store_path`). Lives outside `output/` so wiping run artifacts never deletes triage flags. Gitignore `.steppy/` (or commit it if you want team-shared triage).
|
|
1178
|
+
- **Merge semantics**: keyed `(node, scenario_id)` and idempotent — re-running a scenario *replaces* its prior contribution; nothing accumulates stale rows. Merge↔refresh order doesn't matter (observed endpoints re-link onto newly declared templates).
|
|
1179
|
+
|
|
1180
|
+
### Config
|
|
1181
|
+
|
|
1182
|
+
```yaml
|
|
1183
|
+
coverage:
|
|
1184
|
+
apis:
|
|
1185
|
+
- name: editor-api
|
|
1186
|
+
openapi: "http://localhost:8080/openapi.json" # URL or local file path
|
|
1187
|
+
base_urls: ["http://localhost:8080", "http://editor-backend:8080"]
|
|
1188
|
+
sitemap: null # optional: declares the route surface
|
|
1189
|
+
sourcemap_dir: "../app/dist" # local build output holding *.js.map
|
|
1190
|
+
store_path: "./.steppy/coverage.db"
|
|
1191
|
+
records_dir: "./output/coverage"
|
|
1192
|
+
```
|
|
1193
|
+
|
|
1194
|
+
`base_urls` must mirror each OpenAPI `servers[].url` (including path prefixes like `/api`) and should include Docker-internal `internal_url` origins so records captured by parallel workers classify correctly. `--coverage` composes with `--scenario`, `--tag`, `--guide` (guide runs record as weak evidence), and `--workers N` (workers record into the shared `output/` mount; the coordinator merges after the run).
|
|
1195
|
+
|
|
1196
|
+
**v1 limits**: a mid-scenario full document navigation may drop pre-navigation JS coverage (CDP limitation; routes/endpoints unaffected); modules have no declared manifest, so they're reported as a covered inventory only; `sourcemap_dir` must correspond to the build actually being served (mismatch falls back to bundle granularity, never an error).
|
|
1197
|
+
|
|
1198
|
+
---
|
|
1199
|
+
|
|
1200
|
+
## Project layout
|
|
1201
|
+
|
|
1202
|
+
```
|
|
1203
|
+
steppy/
|
|
1204
|
+
├── src/steppy/
|
|
1205
|
+
│ ├── cli.py # CLI entry point (Typer app)
|
|
1206
|
+
│ ├── init_cli.py # `steppy init` — scaffold a project
|
|
1207
|
+
│ ├── update_cli.py # `steppy update` — reconcile with the installed version
|
|
1208
|
+
│ ├── record_cli.py # `steppy record` — click-to-Markdown recorder (draft scenarios)
|
|
1209
|
+
│ ├── coverage_cli.py # `steppy coverage` commands (lazy-imported)
|
|
1210
|
+
│ ├── tours_cli.py # `steppy tours` + the validate/generate tour hooks
|
|
1211
|
+
│ ├── video_cli.py # `steppy video check` — video toolchain readiness
|
|
1212
|
+
│ ├── _cli_common.py # shared CLI helpers (error handling, config load)
|
|
1213
|
+
│ ├── config.py # config loader (config.yaml)
|
|
1214
|
+
│ ├── config_schema.py # config JSON Schema generation + drift guard
|
|
1215
|
+
│ ├── compat.py # kept-forever deprecated aliases
|
|
1216
|
+
│ ├── presets.py # UI-kit selector presets (primeng/material/…)
|
|
1217
|
+
│ ├── models.py # pydantic models (scenarios + config)
|
|
1218
|
+
│ ├── build_models.py # pydantic models for the `steppy build` config
|
|
1219
|
+
│ ├── parser.py # scenario parser + selector lint
|
|
1220
|
+
│ ├── generator.py # codegen → tests/e2e/
|
|
1221
|
+
│ ├── selection.py # scenario/locale/layout selection (shared by run + parallel)
|
|
1222
|
+
│ ├── auth.py # auth manager
|
|
1223
|
+
│ ├── totp.py # RFC 6238 TOTP helper (MFA login)
|
|
1224
|
+
│ ├── guide.py # guide assembler
|
|
1225
|
+
│ ├── build.py # `steppy build` pipeline
|
|
1226
|
+
│ ├── pdf.py # print-to-PDF over generated guide HTML (lazy-imported)
|
|
1227
|
+
│ ├── video.py # narrated MP4 renderer (lazy-imported, leaf module)
|
|
1228
|
+
│ ├── tts/ # voiceover providers: registry, presets, cache, track (lazy-imported)
|
|
1229
|
+
│ ├── coordinator.py # parallel-mode coordinator (lazy-imported)
|
|
1230
|
+
│ ├── worker.py # parallel-mode worker (lazy-imported)
|
|
1231
|
+
│ ├── coverage/ # coverage map: store, normalize, sourcemap, merge, manifest, gaps (lazy-imported)
|
|
1232
|
+
│ ├── tours/ # in-app tour compiler, emitter, replay planner, build publish (lazy-imported)
|
|
1233
|
+
│ ├── contracts/ # versioned wire contracts: results bundle, run report, in-app tours
|
|
1234
|
+
│ ├── naming.py # product-derived names (tour element, kinds, kill switch); TS twin in player/src/ui
|
|
1235
|
+
│ ├── tour_spec.py # in-app tour enums, patterns and size caps (stdlib leaf)
|
|
1236
|
+
│ ├── testids.py # selector → single test-id extraction (stdlib leaf)
|
|
1237
|
+
│ ├── buildid.py # tour build-id grammar + git resolution (stdlib leaf)
|
|
1238
|
+
│ ├── _parallel/ # queue, compose, image, lifecycle, progress
|
|
1239
|
+
│ ├── _player_bundle/ # vendored player + cursor + video JS (committed to git)
|
|
1240
|
+
│ ├── templates/ # bundled fallback templates — GENERATED from root templates/ (gitignored)
|
|
1241
|
+
│ └── skills/ # bundled Claude skills — GENERATED from root skills/ (gitignored)
|
|
1242
|
+
├── packages/
|
|
1243
|
+
│ ├── player/ # TypeScript: Lit <steppy-guide> + cursor animator + video timeline,
|
|
1244
|
+
│ │ # and the in-app tour overlay <steppy-tour-host> (./tour, not vendored)
|
|
1245
|
+
│ ├── steppy/ # npm types package (defineGuideConfig)
|
|
1246
|
+
│ ├── react/ # @steppy/react — thin <steppy-guide> wrapper
|
|
1247
|
+
│ ├── vue/ # @steppy/vue — thin <steppy-guide> wrapper
|
|
1248
|
+
│ └── angular/ # @steppy/angular — the in-app tour adapter (provideSteppyTours, <steppy-tours>)
|
|
1249
|
+
├── templates/ # CANONICAL Jinja2 templates — the single source of truth;
|
|
1250
|
+
│ # edit here (the bundled copy above regenerates on build)
|
|
1251
|
+
├── skills/ # CANONICAL Claude skills (scenario-author, template, ci-report, app-tours);
|
|
1252
|
+
│ # installed by `steppy init`; bundled copy regenerates on build
|
|
1253
|
+
├── contracts/ # generated JSON Schemas + shared golden fixtures (contracts/fixtures/)
|
|
1254
|
+
├── docs/ # docs/tours.md (in-app tours), docs/design/, brand assets
|
|
1255
|
+
├── fixtures/ # demo-guide (build fixture), tour-demo (in-app tour demo app + project),
|
|
1256
|
+
│ # tour-demo-angular (the same demo in Angular + PrimeNG)
|
|
1257
|
+
├── scenarios/ # example .md scenario files
|
|
1258
|
+
├── seed/ # seed scripts (run via subprocess)
|
|
1259
|
+
├── tests/ # unit tests (tests/e2e/ is generated, gitignored)
|
|
1260
|
+
├── output/ # gitignored: screenshots, assembled guides
|
|
1261
|
+
├── config.yaml # global configuration
|
|
1262
|
+
├── pyproject.toml
|
|
1263
|
+
└── .github/workflows/release.yml # tag push → build wheel → GitHub Release
|
|
1264
|
+
```
|
|
1265
|
+
|
|
1266
|
+
---
|
|
1267
|
+
|
|
1268
|
+
## Real-world scenarios
|
|
1269
|
+
|
|
1270
|
+
The `scenarios/` directory ships a small, runnable set that between them exercise
|
|
1271
|
+
every action *family* — a good starting point to copy from:
|
|
1272
|
+
|
|
1273
|
+
- [`admin--create-project.md`](scenarios/admin--create-project.md) — navigate, click, fill, `select-option`, `contains(...)`.
|
|
1274
|
+
- [`admin--edit-settings.md`](scenarios/admin--edit-settings.md) — the component drivers (`expand`, `toggle`, `fill-date`, `pick-time`, `confirm`) + the `count(sel, >=N)` comparator.
|
|
1275
|
+
- [`editor--manage-items.md`](scenarios/editor--manage-items.md) — `within`/`match` disambiguation, `only-in`/`layouts` conditional steps, `pick-date`, plain `select`.
|
|
1276
|
+
- [`cross--task-approval.md`](scenarios/cross--task-approval.md) — a multi-role (editor → admin) flow.
|
|
1277
|
+
|
|
1278
|
+
---
|
|
1279
|
+
|
|
1280
|
+
## Troubleshooting
|
|
1281
|
+
|
|
1282
|
+
- **`playwright install`** — the generated tests need browser binaries. Run
|
|
1283
|
+
`playwright install chromium --with-deps` once (per machine / CI runner).
|
|
1284
|
+
- **`steppy build` needs Node** — the guide build reads a TypeScript config via
|
|
1285
|
+
`npx tsx`, so Node ≥ 18 must be on `PATH`. `generate`/`run` do **not** need Node.
|
|
1286
|
+
- **See the full error** — commands print a friendly one-liner by default. Add
|
|
1287
|
+
`--verbose` (or set `STEPPY_DEBUG=1`) to get the full traceback.
|
|
1288
|
+
- **`validate` warnings vs errors** — brittle-selector notices print as yellow
|
|
1289
|
+
`WARNING`s and do **not** fail validation; unknown roles, malformed steps, and a
|
|
1290
|
+
too-new `steppy:` version are red `ERROR`s that do.
|
|
1291
|
+
- **Config typos** — with the `$schema` header that `steppy init` writes, a YAML-aware
|
|
1292
|
+
editor flags unknown keys inline; otherwise `extra = "forbid"` reports them at load.
|
|
1293
|
+
- **"In-app tours for build … not found"** (`steppy build`) — the guide target copies the tree
|
|
1294
|
+
`steppy generate` / `steppy tours` wrote; run one of them first, with the same `tours.build_id`.
|
|
1295
|
+
- **An app's tour covers what a non-Steppy E2E suite clicks** — set
|
|
1296
|
+
`globalThis.__STEPPY_TOURS_DISABLED__ = true` in that suite's browser contexts (Steppy's own
|
|
1297
|
+
generated tests already do). See [docs/tours.md](docs/tours.md#kill-switch).
|
|
1298
|
+
|
|
1299
|
+
---
|
|
1300
|
+
|
|
1301
|
+
## Development
|
|
1302
|
+
|
|
1303
|
+
```bash
|
|
1304
|
+
# Install in editable mode
|
|
1305
|
+
pip install -e .
|
|
1306
|
+
|
|
1307
|
+
# Run unit tests
|
|
1308
|
+
python -m pytest tests/ --ignore=tests/e2e -q
|
|
1309
|
+
|
|
1310
|
+
# Build the player workspace
|
|
1311
|
+
npm --prefix packages/player install
|
|
1312
|
+
npm --prefix packages/player run build
|
|
1313
|
+
npm --prefix packages/player test # Vitest suite
|
|
1314
|
+
npm --prefix packages/player run size:tour # in-app tour overlay size budget
|
|
1315
|
+
|
|
1316
|
+
# In-app tours in Chromium (needs the player build above and `playwright install chromium`)
|
|
1317
|
+
STEPPY_TOUR_E2E=1 python -m pytest tests/integration/test_tour_replay_it.py tests/integration/test_tour_player_it.py -q
|
|
1318
|
+
|
|
1319
|
+
# After editing player TS, re-vendor the three wheel bundles (not the tour overlay)
|
|
1320
|
+
for f in steppy-player steppy-cursor-legacy steppy-video; do
|
|
1321
|
+
cp packages/player/dist/$f.js packages/player/dist/$f.js.map src/steppy/_player_bundle/
|
|
1322
|
+
done
|
|
1323
|
+
```
|
|
1324
|
+
|
|
1325
|
+
See [STEPPY-REFERENCE.md](STEPPY-REFERENCE.md) for the complete scenario file format reference and [CLAUDE.md](CLAUDE.md) for architectural notes.
|
|
1326
|
+
|
|
1327
|
+
---
|
|
1328
|
+
|
|
1329
|
+
## License
|
|
1330
|
+
|
|
1331
|
+
Steppy is licensed under the [Apache License, Version 2.0](LICENSE). See
|
|
1332
|
+
[NOTICE](NOTICE) for attribution, including the third-party code bundled into the
|
|
1333
|
+
player JavaScript.
|
|
1334
|
+
|
|
1335
|
+
Contributions are accepted under the same license — see [CONTRIBUTING.md](CONTRIBUTING.md).
|