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.
Files changed (177) hide show
  1. steppy/CHANGELOG.md +862 -0
  2. steppy/__init__.py +9 -0
  3. steppy/__main__.py +13 -0
  4. steppy/_cli_common.py +183 -0
  5. steppy/_docker/Dockerfile.installed +53 -0
  6. steppy/_parallel/__init__.py +7 -0
  7. steppy/_parallel/compose.py +349 -0
  8. steppy/_parallel/event_bus.py +418 -0
  9. steppy/_parallel/failures.py +267 -0
  10. steppy/_parallel/fingerprint.py +141 -0
  11. steppy/_parallel/image.py +380 -0
  12. steppy/_parallel/lastfailed.py +125 -0
  13. steppy/_parallel/lifecycle.py +90 -0
  14. steppy/_parallel/preauth.py +109 -0
  15. steppy/_parallel/progress.py +101 -0
  16. steppy/_parallel/queue.py +385 -0
  17. steppy/_parallel/renderer.py +489 -0
  18. steppy/_parallel/retry.py +316 -0
  19. steppy/_player_bundle/THIRD_PARTY_LICENSES.txt +158 -0
  20. steppy/_player_bundle/__init__.py +100 -0
  21. steppy/_player_bundle/steppy-cursor-legacy.js +2 -0
  22. steppy/_player_bundle/steppy-cursor-legacy.js.map +1 -0
  23. steppy/_player_bundle/steppy-player.js +5419 -0
  24. steppy/_player_bundle/steppy-player.js.map +1 -0
  25. steppy/_player_bundle/steppy-video.js +472 -0
  26. steppy/_player_bundle/steppy-video.js.map +1 -0
  27. steppy/_templating.py +117 -0
  28. steppy/assertions.py +251 -0
  29. steppy/auth.py +493 -0
  30. steppy/build.py +2798 -0
  31. steppy/build_models.py +669 -0
  32. steppy/buildid.py +134 -0
  33. steppy/bundle/__init__.py +47 -0
  34. steppy/bundle/archive.py +239 -0
  35. steppy/bundle/classify.py +329 -0
  36. steppy/bundle/create.py +490 -0
  37. steppy/bundle/hashing.py +28 -0
  38. steppy/bundle/manifest.py +334 -0
  39. steppy/bundle/results.py +546 -0
  40. steppy/bundle_cli.py +455 -0
  41. steppy/captures.py +207 -0
  42. steppy/cli.py +2489 -0
  43. steppy/cloud/__init__.py +58 -0
  44. steppy/cloud/client.py +267 -0
  45. steppy/cloud/upload.py +497 -0
  46. steppy/compat.py +109 -0
  47. steppy/config.py +231 -0
  48. steppy/config_schema.py +87 -0
  49. steppy/contracts/__init__.py +54 -0
  50. steppy/contracts/bundle.py +215 -0
  51. steppy/contracts/report.py +346 -0
  52. steppy/contracts/tours.py +505 -0
  53. steppy/coordinator.py +1352 -0
  54. steppy/coverage/__init__.py +9 -0
  55. steppy/coverage/gaps.py +190 -0
  56. steppy/coverage/manifest.py +193 -0
  57. steppy/coverage/merge.py +363 -0
  58. steppy/coverage/normalize.py +174 -0
  59. steppy/coverage/report.py +178 -0
  60. steppy/coverage/sourcemap.py +219 -0
  61. steppy/coverage/store.py +473 -0
  62. steppy/coverage/triage.py +151 -0
  63. steppy/coverage_cli.py +265 -0
  64. steppy/durations.py +48 -0
  65. steppy/exclusive.py +178 -0
  66. steppy/flaky_cli.py +169 -0
  67. steppy/generator.py +633 -0
  68. steppy/guide.py +1467 -0
  69. steppy/history.py +352 -0
  70. steppy/init_cli.py +330 -0
  71. steppy/locales.py +282 -0
  72. steppy/migrate/__init__.py +35 -0
  73. steppy/migrate/apply.py +194 -0
  74. steppy/migrate/plan.py +199 -0
  75. steppy/migrate/rules.py +465 -0
  76. steppy/migrate/verify.py +163 -0
  77. steppy/migrate_cli.py +152 -0
  78. steppy/models.py +2539 -0
  79. steppy/naming.py +37 -0
  80. steppy/parser.py +475 -0
  81. steppy/pdf.py +193 -0
  82. steppy/presets.py +284 -0
  83. steppy/record_cli.py +796 -0
  84. steppy/report/__init__.py +37 -0
  85. steppy/report/assets.py +197 -0
  86. steppy/report/build.py +261 -0
  87. steppy/report/coverage_source.py +277 -0
  88. steppy/report/model.py +423 -0
  89. steppy/report/render.py +303 -0
  90. steppy/report/source.py +238 -0
  91. steppy/report_cli.py +173 -0
  92. steppy/runmeta.py +114 -0
  93. steppy/selection.py +183 -0
  94. steppy/sitemap.py +205 -0
  95. steppy/skills/steppy-app-tours/SKILL.md +448 -0
  96. steppy/skills/steppy-ci-report/SKILL.md +567 -0
  97. steppy/skills/steppy-scenario-author/SKILL.md +2176 -0
  98. steppy/skills/steppy-template/SKILL.md +676 -0
  99. steppy/templates/_steppy_events.py +291 -0
  100. steppy/templates/_steppy_tours.py +404 -0
  101. steppy/templates/angular/component-interactive.ts.j2 +109 -0
  102. steppy/templates/angular/component-video.ts.j2 +112 -0
  103. steppy/templates/angular/component.ts.j2 +229 -0
  104. steppy/templates/angular/content.ts.j2 +31 -0
  105. steppy/templates/angular/gitignore.j2 +3 -0
  106. steppy/templates/angular/group.component.ts.j2 +39 -0
  107. steppy/templates/angular/onboarding.component.ts.j2 +43 -0
  108. steppy/templates/angular/routes.ts.j2 +34 -0
  109. steppy/templates/ci/github-actions.yml +284 -0
  110. steppy/templates/ci/gitlab-ci.yml +178 -0
  111. steppy/templates/conftest.py.j2 +2405 -0
  112. steppy/templates/guide.book.html.j2 +145 -0
  113. steppy/templates/guide.fragment.html.j2 +257 -0
  114. steppy/templates/guide.html.j2 +301 -0
  115. steppy/templates/guide.md.j2 +18 -0
  116. steppy/templates/init/config.yaml +124 -0
  117. steppy/templates/init/credentials.yaml.example +30 -0
  118. steppy/templates/init/scenarios/example--first-flow.md +53 -0
  119. steppy/templates/report/_head.html.j2 +14 -0
  120. steppy/templates/report/_mark.html.j2 +3 -0
  121. steppy/templates/report/coverage.html.j2 +124 -0
  122. steppy/templates/report/index.html.j2 +203 -0
  123. steppy/templates/report/report.css +401 -0
  124. steppy/templates/report/scenario.html.j2 +137 -0
  125. steppy/templates/static/_headers.j2 +30 -0
  126. steppy/templates/static/_mode_links.html.j2 +22 -0
  127. steppy/templates/static/_seo_head.html.j2 +37 -0
  128. steppy/templates/static/cursor.js.j2 +10 -0
  129. steppy/templates/static/group.html.j2 +78 -0
  130. steppy/templates/static/guides.css +678 -0
  131. steppy/templates/static/index.html.j2 +79 -0
  132. steppy/templates/static/interactive.html.j2 +100 -0
  133. steppy/templates/static/onboarding.html.j2 +64 -0
  134. steppy/templates/static/page.html.j2 +145 -0
  135. steppy/templates/static/robots.txt.j2 +7 -0
  136. steppy/templates/static/video.html.j2 +135 -0
  137. steppy/templates/test_scenario.py.j2 +767 -0
  138. steppy/testids.py +166 -0
  139. steppy/totp.py +26 -0
  140. steppy/tour_spec.py +163 -0
  141. steppy/tours/__init__.py +22 -0
  142. steppy/tours/actions.py +402 -0
  143. steppy/tours/compiler.py +144 -0
  144. steppy/tours/diagnostics.py +137 -0
  145. steppy/tours/emit.py +239 -0
  146. steppy/tours/ids.py +40 -0
  147. steppy/tours/inference.py +399 -0
  148. steppy/tours/model.py +120 -0
  149. steppy/tours/paths.py +126 -0
  150. steppy/tours/pipeline.py +277 -0
  151. steppy/tours/publish.py +124 -0
  152. steppy/tours/replay.py +234 -0
  153. steppy/tours/routes.py +175 -0
  154. steppy/tours/settings.py +164 -0
  155. steppy/tours/text.py +237 -0
  156. steppy/tours_cli.py +219 -0
  157. steppy/tts/__init__.py +280 -0
  158. steppy/tts/cache.py +185 -0
  159. steppy/tts/presets.py +125 -0
  160. steppy/tts/providers/__init__.py +6 -0
  161. steppy/tts/providers/http.py +184 -0
  162. steppy/tts/providers/local.py +207 -0
  163. steppy/tts/providers/null.py +28 -0
  164. steppy/tts/registry.py +201 -0
  165. steppy/tts/track.py +158 -0
  166. steppy/tts/types.py +94 -0
  167. steppy/update_cli.py +460 -0
  168. steppy/video.py +730 -0
  169. steppy/video_cache.py +239 -0
  170. steppy/video_cli.py +271 -0
  171. steppy/worker.py +301 -0
  172. steppy_dev-0.3.0.dist-info/METADATA +1335 -0
  173. steppy_dev-0.3.0.dist-info/RECORD +177 -0
  174. steppy_dev-0.3.0.dist-info/WHEEL +4 -0
  175. steppy_dev-0.3.0.dist-info/entry_points.txt +2 -0
  176. steppy_dev-0.3.0.dist-info/licenses/LICENSE +202 -0
  177. 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.