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,448 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: steppy-app-tours
|
|
3
|
+
description: |
|
|
4
|
+
How to wire Steppy's in-app tours into an application's own code — the work done in
|
|
5
|
+
the app's repository, not in the Steppy project. Use whenever a user asks to "add
|
|
6
|
+
in-app tours to the app", "mount the tour overlay", "add a Show me how button that
|
|
7
|
+
starts a tour", "set up @steppy/angular" or `provideSteppyTours`, mounts
|
|
8
|
+
`<steppy-tour-host>` or `<steppy-tours>`, imports `@steppy/player/tour`, adds
|
|
9
|
+
`data-testid`s so a tour can point at an element, or debugs a tour in the running app:
|
|
10
|
+
"the tour does not start", "Can't find this element", a `tour-error`,
|
|
11
|
+
`target-timeout`, CSP or Trusted Types errors at load, buttons in the wrong language.
|
|
12
|
+
Covers Angular (with the adapter or without), plain JavaScript, React and Vue, serving
|
|
13
|
+
the tour files, starting tours, the language, test-ids in components, security headers,
|
|
14
|
+
events and analytics, turning tours off, and checking that the wiring works.
|
|
15
|
+
|
|
16
|
+
Writing or changing the tours themselves (the `tour:` frontmatter, `tour-text`, compile
|
|
17
|
+
errors and warnings) is `steppy-scenario-author`. Restyling the overlay is
|
|
18
|
+
`steppy-template`. The CI replay and `steppy tours --check` are `steppy-ci-report`.
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
# Wiring Steppy's in-app tours into an app
|
|
22
|
+
|
|
23
|
+
An in-app tour is a sequence of popovers over the live app, pointing at the real
|
|
24
|
+
buttons and fields. Steppy compiles each tour from a Markdown scenario, the same file
|
|
25
|
+
that drives an end-to-end test, and the CI replays every tour against the app. The app
|
|
26
|
+
shows them with one web component, `<steppy-tour-host>`, from `@steppy/player/tour`.
|
|
27
|
+
Angular apps mount it through `@steppy/angular`.
|
|
28
|
+
|
|
29
|
+
The work is split between two places. Know which one you are in before changing anything.
|
|
30
|
+
|
|
31
|
+
| The Steppy project (`config.yaml`, `scenarios/`) | The app (this skill) |
|
|
32
|
+
|---|---|
|
|
33
|
+
| Decides which tours exist, their steps and their text | Serves the compiled tour files |
|
|
34
|
+
| `steppy generate` / `steppy tours` write `tours.manifest.json` and one `<tour-id>.<locale>.json` per tour and language into the app's folder | Mounts the overlay once, in the app shell |
|
|
35
|
+
| Replays every tour in CI | Starts tours from its own UI (a help button, a menu) |
|
|
36
|
+
| | Puts the `data-testid`s on the elements tours point at |
|
|
37
|
+
| | Keeps the overlay's language in step with the app's |
|
|
38
|
+
|
|
39
|
+
Five rules the overlay is built on, and that the app must not work around:
|
|
40
|
+
|
|
41
|
+
1. **Data, never code.** A tour file is JSON validated whole against a strict contract.
|
|
42
|
+
It holds no HTML, no script, no selector. The overlay's code comes only from the
|
|
43
|
+
app's own bundle.
|
|
44
|
+
2. **Test-ids only.** A step points at one `data-testid` value, never a CSS selector.
|
|
45
|
+
3. **The user acts.** The overlay highlights and explains. It never clicks, types or
|
|
46
|
+
submits.
|
|
47
|
+
4. **Non-blocking.** Clicks pass through to the app, and a tour can be closed at any time.
|
|
48
|
+
5. **Locked to a build.** A tour set carries the build id of the app it was compiled for.
|
|
49
|
+
|
|
50
|
+
## Before you start
|
|
51
|
+
|
|
52
|
+
Find out from the Steppy project (or ask):
|
|
53
|
+
|
|
54
|
+
- **Where the tour files land.** `tours.targets` in `config.yaml` has an `app-assets`
|
|
55
|
+
entry whose `path` is a folder in this app, e.g. `../invoice-app/public/tours`. That
|
|
56
|
+
folder holds `tours.manifest.json`.
|
|
57
|
+
- **The tour ids**, which are what the app passes to `start()`. They are in
|
|
58
|
+
`tours.manifest.json` (`tours[].id`). Without a `tour.id`, a tour's id is its
|
|
59
|
+
scenario's file name.
|
|
60
|
+
- **Each tour's entry route** (`tours[].entry.route`, e.g. `/invoices/:invoiceId`). A
|
|
61
|
+
tour started elsewhere navigates there first, and `:params` must come from the app.
|
|
62
|
+
- **The test-id attribute** (`testIdAttribute` in the manifest, default `data-testid`).
|
|
63
|
+
|
|
64
|
+
If the folder is empty, run `steppy tours` in the Steppy project. Never write or edit
|
|
65
|
+
tour JSON by hand: the manifest pins every file by its SHA-384 hash, `steppy tours
|
|
66
|
+
--check` fails CI on any difference, and the next compile overwrites it.
|
|
67
|
+
|
|
68
|
+
## 1. Serve the tour files
|
|
69
|
+
|
|
70
|
+
- The `app-assets` folder must be served by the app as static files. Angular 18+
|
|
71
|
+
serves `public/` (point the target at `public/tours`); older Angular serves
|
|
72
|
+
`src/assets/`; Vite and most bundlers serve `public/`.
|
|
73
|
+
- The URL that folder is served at is what the overlay's `fallback-manifest` gets, e.g.
|
|
74
|
+
`/tours/tours.manifest.json`. Steppy never derives it. Under a sub-path deployment
|
|
75
|
+
(`<base href="/app/">`), use a base-relative URL: `tours/tours.manifest.json`.
|
|
76
|
+
- Commit the folder with the app. CI checks it with `steppy tours --check`.
|
|
77
|
+
- Mark it `-text` in the app repository's `.gitattributes`
|
|
78
|
+
(`public/tours/*.json -text`). A line-ending conversion on checkout changes the bytes
|
|
79
|
+
the hashes cover, and the check then fails with "differ only in line endings".
|
|
80
|
+
- The app must be rebuilt and restarted to serve new tour files, after every
|
|
81
|
+
`steppy tours` or `steppy generate`.
|
|
82
|
+
|
|
83
|
+
## 2. Install the packages
|
|
84
|
+
|
|
85
|
+
`@steppy/player` (the overlay is its `./tour` entry) and `@steppy/angular` are ready to
|
|
86
|
+
publish but not on npm yet. Build them from a Steppy checkout:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
npm --prefix packages/player ci && npm --prefix packages/player run build
|
|
90
|
+
npm --prefix packages/angular ci && npm --prefix packages/angular run build # Angular apps only
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Then depend on them from the app, either as folders or as tarballs:
|
|
94
|
+
|
|
95
|
+
```jsonc
|
|
96
|
+
// package.json — folders
|
|
97
|
+
"@steppy/player": "file:../steppy/packages/player",
|
|
98
|
+
"@steppy/angular": "file:../steppy/packages/angular/dist" // dist/, never the folder itself
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
# tarballs
|
|
103
|
+
npm pack ./packages/player ./packages/angular/dist
|
|
104
|
+
npm install ./steppy-player-<version>.tgz ./steppy-angular-<version>.tgz
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
With `file:` folders in an Angular app, set `"preserveSymlinks": true` in the build
|
|
108
|
+
options of `angular.json`. Without it, Angular loads a second copy of `@angular/core`
|
|
109
|
+
from `packages/angular/node_modules` and fails with NG0203. `@steppy/angular` pins
|
|
110
|
+
`@steppy/player` to its exact version and needs Angular 20–22.
|
|
111
|
+
|
|
112
|
+
Bundle the overlay with the app. Never load it from a CDN or from the guide site.
|
|
113
|
+
|
|
114
|
+
## 3a. Angular, with the adapter (recommended)
|
|
115
|
+
|
|
116
|
+
`@steppy/angular` mounts the overlay the Angular way: zone.js or zoneless, safe under
|
|
117
|
+
server rendering, and no `CUSTOM_ELEMENTS_SCHEMA`.
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
// app.config.ts
|
|
121
|
+
import { inject } from '@angular/core';
|
|
122
|
+
import { toSignal } from '@angular/core/rxjs-interop';
|
|
123
|
+
import { TranslateService } from '@ngx-translate/core';
|
|
124
|
+
import { map } from 'rxjs';
|
|
125
|
+
import { provideSteppyTours } from '@steppy/angular';
|
|
126
|
+
|
|
127
|
+
export const appConfig: ApplicationConfig = {
|
|
128
|
+
providers: [
|
|
129
|
+
provideRouter(routes),
|
|
130
|
+
provideSteppyTours({
|
|
131
|
+
fallbackManifest: 'tours/tours.manifest.json', // the app-assets folder, in public/tours
|
|
132
|
+
// Optional. Without it, tours follow <html lang>.
|
|
133
|
+
locale: () => {
|
|
134
|
+
const translate = inject(TranslateService);
|
|
135
|
+
return toSignal(translate.onLangChange.pipe(map((e) => e.lang)), {
|
|
136
|
+
initialValue: translate.getCurrentLang() ?? '',
|
|
137
|
+
});
|
|
138
|
+
},
|
|
139
|
+
}),
|
|
140
|
+
],
|
|
141
|
+
};
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
// app.component.ts (the shell)
|
|
146
|
+
@Component({
|
|
147
|
+
selector: 'app-root',
|
|
148
|
+
imports: [RouterOutlet, SteppyToursComponent, SteppyTourStart],
|
|
149
|
+
template: `
|
|
150
|
+
<button type="button" steppyTourStart="create-invoice">Show me how</button>
|
|
151
|
+
<router-outlet />
|
|
152
|
+
<steppy-tours (tourComplete)="onDone($event)" />
|
|
153
|
+
`,
|
|
154
|
+
})
|
|
155
|
+
export class AppComponent {}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
// anywhere else
|
|
160
|
+
const tours = inject(SteppyTours);
|
|
161
|
+
await tours.start('send-invoice', { params: { invoiceId: 1001 } }); // resolves false, never rejects
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
| API | What |
|
|
165
|
+
|---|---|
|
|
166
|
+
| `provideSteppyTours(config)` | Root providers. Keys: `fallbackManifest`, `manifest`, `buildId`, `guideOrigin`, `targetTimeout`, `baseZIndex`, `disabled`, `strings`, `source` (as on the host, below); `locale` — a string, a signal, or a function run once in an injection context that returns either; `navigate` — `'router'` (default), `'document'` (full page loads) or your own function; `readyTimeout` — how long `start()` waits for the overlay (10 s). |
|
|
167
|
+
| `<steppy-tours />` (`SteppyToursComponent`) | The overlay. Place it **once**, in the shell, never in a routed page. Its inputs override the config; outputs `tourStart`, `tourStep`, `tourComplete`, `tourDismiss`, `tourOffpath`, `tourError` carry the event details. |
|
|
168
|
+
| `SteppyTours` (root service) | `start(id, {params, step})` waits for the overlay, lets the last call win, resolves `false` when a tour cannot start. `stop(reason)`, `next()`. Signals `active`, `current`, `lastError`; `events`, an Observable delivered inside the zone. |
|
|
169
|
+
| `steppyTourStart` (`SteppyTourStart`) | `<button steppyTourStart="create-invoice" [steppyTourParams]="{invoiceId: id}" steppyTourStep="…">` starts a tour on click. |
|
|
170
|
+
| `routerNavigate(router)` | The router bridge behind `navigate: 'router'`: it settles only once the router is where the tour wants to be, and rejects a refused or redirected navigation. For a `navigate` function of your own that still uses the router. |
|
|
171
|
+
|
|
172
|
+
What the adapter already does, so do not redo it: imports the overlay lazily and in the
|
|
173
|
+
browser only; navigates entry routes with `Router.navigateByUrl` **inside** the zone (a
|
|
174
|
+
guard that refuses or redirects fails the tour at once, `tour-error` `entry-params`);
|
|
175
|
+
runs the overlay's polls outside the zone; keeps the locale in sync. Destroying
|
|
176
|
+
`<steppy-tours>` suspends a running tour rather than ending it (call `stop()` first to
|
|
177
|
+
end it).
|
|
178
|
+
|
|
179
|
+
Not supported: hash routing (`withHashLocation()`), and targets inside a component that
|
|
180
|
+
uses `ViewEncapsulation.ShadowDom` (the overlay looks in the light DOM only).
|
|
181
|
+
|
|
182
|
+
### Angular without the adapter
|
|
183
|
+
|
|
184
|
+
Four things the adapter does are then yours: load the module in the browser only after
|
|
185
|
+
the first render; set `host.navigate` to call the router inside `NgZone.run` (outside
|
|
186
|
+
it, a zone.js app never renders the routed page and the tour times out on its target);
|
|
187
|
+
keep `host.locale` a string, never `undefined`; and call `start()` outside the zone
|
|
188
|
+
after `customElements.whenDefined('steppy-tour-host')`. The component also needs
|
|
189
|
+
`CUSTOM_ELEMENTS_SCHEMA`. Prefer the adapter.
|
|
190
|
+
|
|
191
|
+
## 3b. Plain JavaScript (any bundler)
|
|
192
|
+
|
|
193
|
+
```html
|
|
194
|
+
<steppy-tour-host fallback-manifest="/tours/tours.manifest.json"></steppy-tour-host>
|
|
195
|
+
<button id="show-me-how">Show me how</button>
|
|
196
|
+
<script type="module" src="/js/app.js"></script>
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
```js
|
|
200
|
+
// app.js — from the app's own bundle: no inline <script>, the overlay runs under a strict CSP
|
|
201
|
+
import '@steppy/player/tour'; // registers <steppy-tour-host>
|
|
202
|
+
const host = document.querySelector('steppy-tour-host');
|
|
203
|
+
host.navigate = (path, { appPath }) => router.navigate(appPath); // client-side routing only
|
|
204
|
+
document.querySelector('#show-me-how').addEventListener('click', () => host.start('create-invoice'));
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Without a bundler, serve `dist/steppy-tour.js` (it carries its own Lit) from the app's
|
|
208
|
+
origin and load it with `<script type="module" src="/vendor/steppy-tour.js">` before
|
|
209
|
+
the app's code. Mount **one** host per page, in the shell. Importing the module twice is
|
|
210
|
+
harmless.
|
|
211
|
+
|
|
212
|
+
## 3c. React and Vue
|
|
213
|
+
|
|
214
|
+
There are no tour wrappers yet (`@steppy/react` and `@steppy/vue` wrap the guide player
|
|
215
|
+
only). Use the element directly, and remember two things: `navigate`, `source` and
|
|
216
|
+
`strings` are **properties**, not attributes; and the module must load in the browser
|
|
217
|
+
only (in Next.js or Nuxt, import it from client-side code).
|
|
218
|
+
|
|
219
|
+
```tsx
|
|
220
|
+
// React
|
|
221
|
+
import { useEffect, useRef } from 'react';
|
|
222
|
+
import { useNavigate } from 'react-router-dom';
|
|
223
|
+
import '@steppy/player/tour';
|
|
224
|
+
import type { SteppyTourHost } from '@steppy/player/tour';
|
|
225
|
+
|
|
226
|
+
export function Tours({ locale }: { locale: string }) {
|
|
227
|
+
const ref = useRef<SteppyTourHost>(null);
|
|
228
|
+
const navigate = useNavigate();
|
|
229
|
+
useEffect(() => {
|
|
230
|
+
ref.current!.navigate = (_path, { appPath }) => navigate(appPath);
|
|
231
|
+
}, [navigate]);
|
|
232
|
+
useEffect(() => {
|
|
233
|
+
ref.current!.locale = locale;
|
|
234
|
+
}, [locale]);
|
|
235
|
+
return <steppy-tour-host ref={ref} fallback-manifest="/tours/tours.manifest.json" />;
|
|
236
|
+
}
|
|
237
|
+
// TypeScript: declare 'steppy-tour-host' in JSX.IntrinsicElements once.
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
```vue
|
|
241
|
+
<!-- Vue: vite.config.ts needs
|
|
242
|
+
vue({ template: { compilerOptions: { isCustomElement: (tag) => tag.startsWith('steppy-') } } }) -->
|
|
243
|
+
<script setup lang="ts">
|
|
244
|
+
import { onMounted, ref, watch } from 'vue';
|
|
245
|
+
import { useRouter } from 'vue-router';
|
|
246
|
+
import '@steppy/player/tour';
|
|
247
|
+
import type { SteppyTourHost } from '@steppy/player/tour';
|
|
248
|
+
|
|
249
|
+
const props = defineProps<{ locale: string }>();
|
|
250
|
+
const host = ref<SteppyTourHost>();
|
|
251
|
+
const router = useRouter();
|
|
252
|
+
onMounted(() => {
|
|
253
|
+
host.value!.navigate = (_path, { appPath }) => router.push(appPath);
|
|
254
|
+
host.value!.locale = props.locale;
|
|
255
|
+
});
|
|
256
|
+
watch(() => props.locale, (locale) => host.value && (host.value.locale = locale));
|
|
257
|
+
</script>
|
|
258
|
+
|
|
259
|
+
<template>
|
|
260
|
+
<steppy-tour-host ref="host" fallback-manifest="/tours/tours.manifest.json" />
|
|
261
|
+
</template>
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
Any other framework follows the same contract: lazy, browser-only import; `navigate`
|
|
265
|
+
mapped to the router with `appPath` and inside the framework's change detection
|
|
266
|
+
(rejecting when the router refuses or redirects); `locale` kept in sync as a string;
|
|
267
|
+
state built from the six events, never by reading the overlay's DOM; `start()`,
|
|
268
|
+
`stop()`, `next()` and property writes outside change detection in zone-based apps; no
|
|
269
|
+
injected scripts and no sanitizer bypass.
|
|
270
|
+
|
|
271
|
+
## The host element
|
|
272
|
+
|
|
273
|
+
| Property | Attribute | Default | Meaning |
|
|
274
|
+
|---|---|---|---|
|
|
275
|
+
| `fallbackManifest` | `fallback-manifest` | — | The bundled manifest (same origin). Almost always the one to set. |
|
|
276
|
+
| `manifest` | `manifest` | — | A remote manifest URL, may contain `{buildId}`. Remote mode, below. |
|
|
277
|
+
| `buildId` | `build-id` | the bundled manifest's | The app's build id, for `{buildId}`. |
|
|
278
|
+
| `locale` | `locale` | `<html lang>` | The preferred language; falls back to its base language, then the default. |
|
|
279
|
+
| `guideOrigin` | `guide-origin` | the bundled manifest's | The origin trusted for "Open the full guide" links. |
|
|
280
|
+
| `targetTimeout` | `target-timeout` | `8000` | Milliseconds to wait for a step's element (500–120000). |
|
|
281
|
+
| `baseZIndex` | `base-z-index` | `2147483000` | Stacking where the browser's top layer is unavailable. |
|
|
282
|
+
| `disabled` | `disabled` | `false` | Turns tours off and stops a running one. |
|
|
283
|
+
| `navigate` | — | `location.assign` | `(path, {appPath, tourId}) => …`. `path` includes `<base href>`, `appPath` does not. |
|
|
284
|
+
| `source` | — | — | `(tourId, locale, {buildId, manifestUrl}) => file`: supply tour files yourself (JSON text, a `Response` or the object, or a promise of one). Validated like a fetched file. |
|
|
285
|
+
| `strings` | — | — | Chrome string overrides per language (below). |
|
|
286
|
+
|
|
287
|
+
Methods: `start(tourId, {params, step})` resolves `true` once the tour started and
|
|
288
|
+
`false` when it could not (a `tour-error` says why) — it never rejects, and starting a
|
|
289
|
+
tour ends any other one. `stop(reason)` ends the tour. `next()` advances the current
|
|
290
|
+
step. State: `isActive`, and `current` (`{tourId, stepId, index, total}` or `null`).
|
|
291
|
+
|
|
292
|
+
## 4. Start tours from the app's UI
|
|
293
|
+
|
|
294
|
+
- Start from something the user chooses: a "Show me how" button, a help menu, an empty
|
|
295
|
+
state. The overlay never starts by itself, and an automatic start on every visit is
|
|
296
|
+
a product decision, not a default.
|
|
297
|
+
- **Entry params.** A tour whose entry route has `:params` needs them when it starts
|
|
298
|
+
away from that route: `start('send-invoice', {params: {invoiceId: 1001}})`. Started
|
|
299
|
+
on a matching URL, it reads them from the URL. Missing ones fail with `entry-params`.
|
|
300
|
+
- `step` starts at a step id instead of the first.
|
|
301
|
+
- Only offer a tour to the users whose role it was written for (`tours[].role` in the
|
|
302
|
+
manifest): its steps point at what that role sees.
|
|
303
|
+
|
|
304
|
+
## 5. Language
|
|
305
|
+
|
|
306
|
+
- Keep the overlay's language in step with the app's: set `locale` on every switch
|
|
307
|
+
(the adapter's `locale` option does this), or keep `<html lang>` current and leave
|
|
308
|
+
`locale` unset. A switch mid-tour reloads the tour at the same step.
|
|
309
|
+
- Each tour ships in the languages its scenario has text for; any other language gets
|
|
310
|
+
the base language's file, else the default one.
|
|
311
|
+
- Buttons and hints ("chrome strings") exist in English, French, Dutch and German. For
|
|
312
|
+
any other language, set them, or its users see English buttons around their text:
|
|
313
|
+
|
|
314
|
+
```js
|
|
315
|
+
host.strings = { es: { next: 'Siguiente', done: 'Listo', progress: 'Paso {current} de {total}', close: 'Cerrar el recorrido' /* … */ } };
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
The keys are `next`, `done`, `progress` (keep both placeholders), `close`,
|
|
319
|
+
`skipStep`, `endTour`, `resume`, `paused`, `dialogLabel`, `openGuide`,
|
|
320
|
+
`notFoundTitle`, `notFoundBody` and the five `instruction*` hints
|
|
321
|
+
(`DEFAULT_TOUR_STRINGS`, exported, holds the built-in tables). Missing keys fall back
|
|
322
|
+
to the built-in strings for that language, else English.
|
|
323
|
+
|
|
324
|
+
## 6. Test-ids: the part only the app can do
|
|
325
|
+
|
|
326
|
+
Every step a tour keeps points at one test-id. When the Steppy project reports
|
|
327
|
+
`target-not-test-id`, the fix is usually here: give the element its own test-id, then
|
|
328
|
+
point the scenario's selector at it.
|
|
329
|
+
|
|
330
|
+
- **Put the test-id on the element the user acts on**, or on the host element of the
|
|
331
|
+
component that wraps it. A click anywhere inside an element with the test-id counts,
|
|
332
|
+
a text step reads the first field inside it, and a choice step watches the value,
|
|
333
|
+
ARIA state and label inside it. So a component library's host works as is:
|
|
334
|
+
`<p-select data-testid="customer-select">`.
|
|
335
|
+
- **One visible element per test-id** at any moment. When several share a value, the
|
|
336
|
+
overlay takes the first *visible* one and Playwright the first in DOM order. The
|
|
337
|
+
replay catches a mismatch.
|
|
338
|
+
- **In the light DOM of the main document**: not inside another component's shadow
|
|
339
|
+
root, not in an iframe.
|
|
340
|
+
- **Stable.** A test-id is part of the tour's contract. Renaming one breaks the tour
|
|
341
|
+
and the CI replay. Rename it in the scenario too, then regenerate.
|
|
342
|
+
- Bind it like any attribute: `data-testid="save-invoice-btn"`, or
|
|
343
|
+
`[attr.data-testid]="…"` in Angular for a computed value. Keep values plain: no
|
|
344
|
+
`{{…}}` the scenario would have to guess.
|
|
345
|
+
|
|
346
|
+
Dialogs, drawers and phones:
|
|
347
|
+
|
|
348
|
+
- **Mark dialogs as dialogs** (`role="dialog"` or `role="alertdialog"`, or
|
|
349
|
+
`aria-modal="true"`, or a `<dialog>`). PrimeNG, Angular Material and CDK dialogs
|
|
350
|
+
already are. Escape inside one is then the app's, not the tour's, and a centred step
|
|
351
|
+
(a `confirm` kept with `tour: keep`) moves off an open one instead of hiding it.
|
|
352
|
+
- **A drawer that closes on Escape must consume it** (`preventDefault()`), or that
|
|
353
|
+
Escape also ends the tour. PrimeNG's `p-drawer` does; a hand-made one may not.
|
|
354
|
+
- **A link in a drawer is its own test-id** (`drawer-customers`), not the header
|
|
355
|
+
link's (`nav-customers`): the phone and desktop steps are twins with different
|
|
356
|
+
targets. The overlay follows a drawer that slides in.
|
|
357
|
+
- **`tours.layouts` must match the app's breakpoint.** If the app swaps its header for
|
|
358
|
+
a menu below 768px, the Steppy project's `mobile` query is `(max-width: 767.98px)`;
|
|
359
|
+
otherwise a phone gets the desktop step pointing at a hidden link.
|
|
360
|
+
|
|
361
|
+
## 7. Security headers
|
|
362
|
+
|
|
363
|
+
The overlay runs under a strict page: no inline script, no inline `style` attribute, no
|
|
364
|
+
`eval`.
|
|
365
|
+
|
|
366
|
+
- **CSP.** `script-src 'self'` is enough. Styles use constructable stylesheets; on
|
|
367
|
+
engines without them, Lit falls back to a `<style>` element, which needs `litNonce`
|
|
368
|
+
under a strict `style-src`. Angular and PrimeNG inject `<style>` elements anyway, so
|
|
369
|
+
such apps already allow `'unsafe-inline'` styles. Remote mode adds the guide origin to
|
|
370
|
+
`connect-src`.
|
|
371
|
+
- **Trusted Types.** Add `lit-html` to the policies the app lists, e.g.
|
|
372
|
+
`trusted-types angular angular#bundler lit-html`. When the app uses Lit too, or two
|
|
373
|
+
overlay bundles load, also add `'allow-duplicates'`, or the module fails to load.
|
|
374
|
+
- Never use `bypassSecurityTrust*`, `innerHTML` or an inline script for the overlay.
|
|
375
|
+
|
|
376
|
+
## 8. Events and analytics
|
|
377
|
+
|
|
378
|
+
The host dispatches six events, bubbling and composed, with plain JSON details:
|
|
379
|
+
`tour-start`, `tour-step`, `tour-complete`, `tour-dismiss` (with `via`: `close`,
|
|
380
|
+
`escape`, `end`, `api`, `replaced`, `disabled`), `tour-offpath` and `tour-error` (with
|
|
381
|
+
`reason`: `target-timeout`, `entry-params`, `load-failed`, `schema-unsupported`,
|
|
382
|
+
`invalid-data`). Listen on the host (or on the adapter's outputs or `events`) and
|
|
383
|
+
forward them to the app's analytics, if any. The overlay itself sends nothing anywhere
|
|
384
|
+
and stores one `sessionStorage` key, `steppy:tour`, which resumes a tour across a full
|
|
385
|
+
page load for 30 minutes.
|
|
386
|
+
|
|
387
|
+
## 9. Turning tours off
|
|
388
|
+
|
|
389
|
+
- `disabled` on the host (or the adapter's `disabled`), e.g. behind a feature flag.
|
|
390
|
+
- `globalThis.__STEPPY_TOURS_DISABLED__ = true` before the app starts a tour:
|
|
391
|
+
`start()` then does nothing. Steppy's own generated tests set it, so a tour never
|
|
392
|
+
covers what a test clicks. **Any other end-to-end suite** (Cypress, a hand-written
|
|
393
|
+
Playwright suite) must set it in its browser contexts too.
|
|
394
|
+
|
|
395
|
+
## Remote mode (optional)
|
|
396
|
+
|
|
397
|
+
By default the tour files ship with the app and roll back with it. To serve them from
|
|
398
|
+
the published guide site instead, set `manifest` to
|
|
399
|
+
`https://guide.example.com/tours/{buildId}/tours.manifest.json` and keep
|
|
400
|
+
`fallback-manifest`: the bundled manifest then pins every remote file to its hash, and
|
|
401
|
+
any remote failure falls back to the bundled copy. The guide host needs CORS for the
|
|
402
|
+
app's origin, and the app's CSP needs it in `connect-src`. A guide behind a login
|
|
403
|
+
cannot serve the files directly: use a same-origin proxy that relays the bytes
|
|
404
|
+
unchanged, or the `source` hook. Keep it bundled unless there is a reason not to.
|
|
405
|
+
|
|
406
|
+
## Check that it works
|
|
407
|
+
|
|
408
|
+
1. Rebuild and start the app, open it as the tour's role, and start the tour from its
|
|
409
|
+
button. The first popover appears on its element.
|
|
410
|
+
2. Open the console: a `tour-error` names the step and the test-id it could not find.
|
|
411
|
+
3. In the Steppy project, run the tour's scenario: `steppy run --scenario <slug>` runs
|
|
412
|
+
its replay, `test_<slug>__tour`, which follows the tour against the running app and
|
|
413
|
+
fails naming the tour, the step and the test-id. `STEPPY_TOUR_EXPECT_BUILD_ID=<id>`
|
|
414
|
+
also checks the app serves the tours for that build.
|
|
415
|
+
|
|
416
|
+
## Troubleshooting
|
|
417
|
+
|
|
418
|
+
| Symptom | Likely cause |
|
|
419
|
+
|---|---|
|
|
420
|
+
| `start()` resolves `false`, `load-failed` | `fallback-manifest` does not point at the served file (an HTML page came back, or a 404), or the tour id is not in the manifest. |
|
|
421
|
+
| A tour that starts on another route never shows its first step (Angular) | `navigate` runs outside the zone: use `@steppy/angular`, or wrap the router call in `NgZone.run`. |
|
|
422
|
+
| "Can't find this element" (`target-timeout`) | The test-id was renamed or removed, the element renders late (raise `targetTimeout`), it sits in a shadow root or iframe, or it is hidden. |
|
|
423
|
+
| `entry-params` | The entry route needs a param neither `start()` nor the URL gives, or a guard refused or redirected the navigation. |
|
|
424
|
+
| `invalid-data` / `schema-unsupported` | The file was edited by hand, or the overlay is older than the Steppy that compiled the tours: rebuild the packages. |
|
|
425
|
+
| The module fails to load: CSP or Trusted Types errors | Section 7. |
|
|
426
|
+
| English buttons around another language's text | That language has no built-in chrome strings: set `strings`. |
|
|
427
|
+
| The tour stays in the old language after a switch | Set `locale` on every switch, or keep `<html lang>` current with `locale` unset. |
|
|
428
|
+
| Two copies of Angular, NG0203 | `file:` dependencies without `"preserveSymlinks": true`. |
|
|
429
|
+
| `steppy tours --check` fails after a checkout | The JSON's line endings were converted: mark the folder `-text` in `.gitattributes`. |
|
|
430
|
+
| A native `<dialog>` opened with `showModal()` makes the popover's buttons inert | The browser makes everything outside a modal dialog inert until it closes. Steps advanced by the app's own events still work; non-native dialogs are unaffected. |
|
|
431
|
+
| Another E2E suite clicks into the overlay | Set `__STEPPY_TOURS_DISABLED__` in its browser contexts. |
|
|
432
|
+
|
|
433
|
+
## Limitations
|
|
434
|
+
|
|
435
|
+
Tours are linear (no branches, no Back button); navigating to another origin ends them;
|
|
436
|
+
hash routing is not supported; targets must be in the main document's light DOM;
|
|
437
|
+
changing a tour's text means regenerating the files and redeploying the app.
|
|
438
|
+
|
|
439
|
+
## Reference
|
|
440
|
+
|
|
441
|
+
In the Steppy repository: `docs/tours.md` (sections "Adding the player to your app",
|
|
442
|
+
"Angular", "React and Vue", "Localization", "Theming and accessibility", "Security",
|
|
443
|
+
"Troubleshooting"); `packages/angular/README.md`; and two working apps that the same
|
|
444
|
+
scenarios drive, `fixtures/tour-demo/app` (plain JavaScript) and
|
|
445
|
+
`fixtures/tour-demo-angular/app` (Angular, PrimeNG and ngx-translate).
|
|
446
|
+
|
|
447
|
+
This skill is installed by `steppy init --app-dir <this repository>`, run in the Steppy
|
|
448
|
+
project, and `steppy update` there keeps it current.
|