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,401 @@
1
+ /* Steppy run report.
2
+ *
3
+ * Deliberately NOT templates/static/guides.css. That file is 641 lines of
4
+ * guide-site chrome plus the @media print rules that ARE the PDF layout; a run
5
+ * report inherits none of it and would only be at risk from changes to it.
6
+ *
7
+ * This file is loaded through the Jinja loader (so a project can override it at
8
+ * templates/report/report.css and get the same three-rung precedence as every
9
+ * other template). Two consequences, and this comment learned the first one the
10
+ * hard way by containing an example of it:
11
+ *
12
+ * 1. It is parsed as a template, so a Jinja delimiter anywhere in it -- even
13
+ * inside a CSS comment -- is a syntax error. Do not write one, not even to
14
+ * warn about writing one.
15
+ * 2. The report's environment has autoescape ON, so a value interpolated here
16
+ * would come out HTML-escaped. Use CSS custom properties instead; there is
17
+ * nothing this file needs from Python.
18
+ */
19
+
20
+ /* Brand palette (Pin #15332A, Lichen #E4E9E1, Balise #D2561A, Granite #5B6B64).
21
+ * Balise as-is is 4.0:1 on the light ground, below AA for link text, so links
22
+ * use a deepened Balise in light mode and a lightened one in dark mode; the
23
+ * unaltered Balise appears only in the mark, where 3:1 is the bar. The status
24
+ * colours below are deliberately NOT brand colours: an orange that reads as
25
+ * brand must never be mistaken for fail or warn. */
26
+ :root {
27
+ --bg: #e4e9e1;
28
+ --surface: #fbfcfa;
29
+ --border: #c9d3cb;
30
+ --text: #15332a;
31
+ --muted: #5b6b64;
32
+ --accent: #a8410e;
33
+ --brand-check: #d2561a;
34
+ --pass: #1a7f45;
35
+ --pass-bg: #e6f4ec;
36
+ --fail: #c02b2b;
37
+ --fail-bg: #fdeaea;
38
+ --warn: #9a6200;
39
+ --warn-bg: #fdf3e0;
40
+ --neutral-bg: #dce3da;
41
+ --mono: "JetBrains Mono", ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
42
+ --radius: 8px;
43
+ }
44
+
45
+ /* The dark palette, defined once and applied from two places.
46
+ *
47
+ * A host has three theme states, not two: it may stamp data-theme="dark" or
48
+ * "light" on the root element, or stamp nothing at all and leave only the OS
49
+ * preference. So the media query is guarded on :not([data-theme="light"]) -- an
50
+ * explicit light choice has to beat a dark OS -- and the same tokens are
51
+ * repeated under [data-theme="dark"] so an explicit dark choice beats a light
52
+ * OS. Published to Pages only the media query ever fires; the stamped selectors
53
+ * are what make the page correct when it is embedded somewhere that themes it.
54
+ *
55
+ * Every colour in this file comes from these tokens. A colour whose only
56
+ * definition sits inside one of these blocks would go undefined in the
57
+ * unstamped state, which is how a page ends up painting one theme's text on the
58
+ * other theme's ground. */
59
+ @media (prefers-color-scheme: dark) {
60
+ :root:not([data-theme="light"]) {
61
+ --bg: #0e231c;
62
+ --surface: #15332a;
63
+ --border: #2b4a40;
64
+ --text: #e4e9e1;
65
+ --muted: #a9b5ac;
66
+ --accent: #f09a6a;
67
+ --pass: #58c98a;
68
+ --pass-bg: #16301f;
69
+ --fail: #ff8a8a;
70
+ --fail-bg: #3a1c1c;
71
+ --warn: #e0b055;
72
+ --warn-bg: #33280f;
73
+ --neutral-bg: #1f3d33;
74
+ }
75
+ }
76
+
77
+ :root[data-theme="dark"] {
78
+ --bg: #0e231c;
79
+ --surface: #15332a;
80
+ --border: #2b4a40;
81
+ --text: #e4e9e1;
82
+ --muted: #a9b5ac;
83
+ --accent: #f09a6a;
84
+ --pass: #58c98a;
85
+ --pass-bg: #16301f;
86
+ --fail: #ff8a8a;
87
+ --fail-bg: #3a1c1c;
88
+ --warn: #e0b055;
89
+ --warn-bg: #33280f;
90
+ --neutral-bg: #1f3d33;
91
+ }
92
+
93
+ * { box-sizing: border-box; }
94
+
95
+ body {
96
+ margin: 0;
97
+ background: var(--bg);
98
+ color: var(--text);
99
+ font: 15px/1.55 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
100
+ }
101
+
102
+ a { color: var(--accent); }
103
+
104
+ /* A report is a lot of links — every scenario name, every slug under a failure
105
+ * cause. Keyboard focus needs to be visible against either ground, so it is
106
+ * drawn from the token set like everything else. */
107
+ :where(a, button, summary):focus-visible {
108
+ outline: 2px solid var(--accent);
109
+ outline-offset: 2px;
110
+ border-radius: 3px;
111
+ }
112
+
113
+ .steppy-report-header {
114
+ background: var(--surface);
115
+ border-bottom: 1px solid var(--border);
116
+ padding: 18px 24px;
117
+ }
118
+
119
+ .steppy-report-header h1 {
120
+ display: flex;
121
+ align-items: center;
122
+ gap: 10px;
123
+ margin: 0 0 4px;
124
+ font-size: 20px;
125
+ }
126
+
127
+ /* The steppy mark: brackets in the text colour, the check in Balise. Drawn
128
+ * inline (not an <img>) so the brackets follow the theme without a second file. */
129
+ .steppy-mark {
130
+ flex: none;
131
+ width: 28px;
132
+ height: 28px;
133
+ }
134
+
135
+ .steppy-mark .steppy-mark-brackets { stroke: var(--text); }
136
+ .steppy-mark .steppy-mark-check { stroke: var(--brand-check); }
137
+
138
+ .steppy-report-main {
139
+ max-width: 1080px;
140
+ margin: 0 auto;
141
+ padding: 24px;
142
+ }
143
+
144
+ .steppy-report-section {
145
+ background: var(--surface);
146
+ border: 1px solid var(--border);
147
+ border-radius: var(--radius);
148
+ padding: 18px 20px;
149
+ margin-bottom: 20px;
150
+ }
151
+
152
+ .steppy-report-section > h2 {
153
+ margin: 0 0 12px;
154
+ font-size: 16px;
155
+ }
156
+
157
+ .steppy-report-meta {
158
+ display: flex;
159
+ flex-wrap: wrap;
160
+ gap: 6px 18px;
161
+ margin: 0;
162
+ color: var(--muted);
163
+ font-size: 13px;
164
+ }
165
+
166
+ .steppy-report-meta div { display: flex; gap: 6px; }
167
+ .steppy-report-meta dt { font-weight: 600; }
168
+ .steppy-report-meta dd { margin: 0; }
169
+
170
+ /* ── outcome chips ─────────────────────────────────────────────────────── */
171
+
172
+ .steppy-chip {
173
+ display: inline-block;
174
+ padding: 1px 8px;
175
+ border-radius: 999px;
176
+ font-size: 12px;
177
+ font-weight: 600;
178
+ background: var(--neutral-bg);
179
+ color: var(--muted);
180
+ white-space: nowrap;
181
+ }
182
+
183
+ .steppy-chip[data-outcome="passed"] { background: var(--pass-bg); color: var(--pass); }
184
+ .steppy-chip[data-outcome="skipped"] { background: var(--neutral-bg); color: var(--muted); }
185
+ .steppy-chip[data-outcome="failed"],
186
+ .steppy-chip[data-outcome="error"],
187
+ .steppy-chip[data-outcome="worker_died"] { background: var(--fail-bg); color: var(--fail); }
188
+ .steppy-chip[data-outcome="interrupted"],
189
+ .steppy-chip[data-outcome="unknown"] { background: var(--warn-bg); color: var(--warn); }
190
+
191
+ .steppy-verdict {
192
+ font-size: 22px;
193
+ font-weight: 700;
194
+ }
195
+ .steppy-verdict[data-outcome="passed"] { color: var(--pass); }
196
+ .steppy-verdict[data-outcome="failed"],
197
+ .steppy-verdict[data-outcome="error"],
198
+ .steppy-verdict[data-outcome="worker_died"] { color: var(--fail); }
199
+
200
+ .steppy-counts {
201
+ display: flex;
202
+ flex-wrap: wrap;
203
+ gap: 10px 20px;
204
+ align-items: baseline;
205
+ margin: 0 0 6px;
206
+ }
207
+
208
+ /* ── tables ────────────────────────────────────────────────────────────── */
209
+
210
+ .steppy-report-table {
211
+ width: 100%;
212
+ border-collapse: collapse;
213
+ font-size: 14px;
214
+ /* Counts and sizes sit in columns and get compared down the column, so the
215
+ * digits have to share a width. */
216
+ font-variant-numeric: tabular-nums;
217
+ }
218
+
219
+ .steppy-report-table th,
220
+ .steppy-report-table td {
221
+ text-align: left;
222
+ padding: 7px 10px;
223
+ border-bottom: 1px solid var(--border);
224
+ vertical-align: top;
225
+ }
226
+
227
+ .steppy-report-table th {
228
+ font-size: 12px;
229
+ text-transform: uppercase;
230
+ letter-spacing: .04em;
231
+ color: var(--muted);
232
+ }
233
+
234
+ .steppy-report-table tr:last-child td { border-bottom: 0; }
235
+
236
+ .steppy-scroll {
237
+ overflow-x: auto;
238
+ }
239
+
240
+ /* ── failure causes ────────────────────────────────────────────────────── */
241
+
242
+ .steppy-cause {
243
+ border: 1px solid var(--border);
244
+ border-radius: var(--radius);
245
+ padding: 12px 14px;
246
+ margin-bottom: 12px;
247
+ }
248
+
249
+ .steppy-cause-head {
250
+ display: flex;
251
+ flex-wrap: wrap;
252
+ gap: 10px;
253
+ align-items: baseline;
254
+ }
255
+
256
+ .steppy-cause-count {
257
+ font-weight: 700;
258
+ color: var(--fail);
259
+ }
260
+
261
+ .steppy-cause-headline {
262
+ font-family: var(--mono);
263
+ font-size: 13px;
264
+ word-break: break-word;
265
+ }
266
+
267
+ .steppy-fingerprint {
268
+ font-family: var(--mono);
269
+ font-size: 12px;
270
+ color: var(--muted);
271
+ }
272
+
273
+ pre.steppy-tail {
274
+ margin: 10px 0 0;
275
+ padding: 10px 12px;
276
+ background: var(--bg);
277
+ border: 1px solid var(--border);
278
+ border-radius: 6px;
279
+ overflow-x: auto;
280
+ font-family: var(--mono);
281
+ font-size: 12.5px;
282
+ line-height: 1.45;
283
+ white-space: pre;
284
+ }
285
+
286
+ /* ── steps ─────────────────────────────────────────────────────────────── */
287
+
288
+ .steppy-steps { list-style: none; margin: 0; padding: 0; }
289
+
290
+ .steppy-step {
291
+ display: flex;
292
+ gap: 12px;
293
+ padding: 8px 0;
294
+ border-bottom: 1px solid var(--border);
295
+ }
296
+
297
+ .steppy-step:last-child { border-bottom: 0; }
298
+
299
+ .steppy-step-num {
300
+ flex: 0 0 3.2em;
301
+ font-family: var(--mono);
302
+ font-size: 13px;
303
+ color: var(--muted);
304
+ }
305
+
306
+ .steppy-step[data-status="running"] .steppy-step-num { color: var(--fail); font-weight: 700; }
307
+
308
+ .steppy-step-body { flex: 1 1 auto; min-width: 0; }
309
+ .steppy-step-title { font-weight: 600; }
310
+
311
+ .steppy-step-facts {
312
+ color: var(--muted);
313
+ font-size: 12.5px;
314
+ display: flex;
315
+ flex-wrap: wrap;
316
+ gap: 4px 14px;
317
+ }
318
+
319
+ .steppy-step-action { font-family: var(--mono); }
320
+
321
+ .steppy-shots {
322
+ display: flex;
323
+ flex-wrap: wrap;
324
+ gap: 8px;
325
+ margin-top: 8px;
326
+ }
327
+
328
+ /* width:auto + max-width:100% renders at min(intrinsic, container), which is the
329
+ * same rule the guide stage uses: it respects a dense capture's real pixels and
330
+ * never upscales a low-res one. */
331
+ .steppy-shots img {
332
+ width: auto;
333
+ max-width: 100%;
334
+ max-height: 420px;
335
+ border: 1px solid var(--border);
336
+ border-radius: 6px;
337
+ background: var(--surface);
338
+ }
339
+
340
+ .steppy-variant {
341
+ border: 1px solid var(--border);
342
+ border-radius: var(--radius);
343
+ padding: 14px 16px;
344
+ margin-bottom: 16px;
345
+ }
346
+
347
+ .steppy-variant-head {
348
+ display: flex;
349
+ flex-wrap: wrap;
350
+ gap: 10px;
351
+ align-items: baseline;
352
+ margin-bottom: 8px;
353
+ }
354
+
355
+ .steppy-variant-head h3 { margin: 0; font-size: 15px; }
356
+
357
+ .steppy-nodeid {
358
+ font-family: var(--mono);
359
+ font-size: 12px;
360
+ color: var(--muted);
361
+ word-break: break-all;
362
+ }
363
+
364
+ /* ── notices ───────────────────────────────────────────────────────────── */
365
+
366
+ .steppy-notice {
367
+ border-left: 3px solid var(--warn);
368
+ background: var(--warn-bg);
369
+ color: var(--warn);
370
+ padding: 10px 14px;
371
+ border-radius: 4px;
372
+ margin-bottom: 14px;
373
+ font-size: 13.5px;
374
+ }
375
+
376
+ .steppy-notice ul { margin: 6px 0 0; padding-left: 18px; }
377
+
378
+ .steppy-muted { color: var(--muted); }
379
+ .steppy-small { font-size: 13px; }
380
+
381
+ .steppy-report-footer {
382
+ max-width: 1080px;
383
+ margin: 0 auto;
384
+ padding: 0 24px 32px;
385
+ color: var(--muted);
386
+ font-size: 12.5px;
387
+ }
388
+
389
+ .steppy-badge-line {
390
+ display: flex;
391
+ flex-wrap: wrap;
392
+ gap: 12px;
393
+ align-items: baseline;
394
+ }
395
+
396
+ .steppy-pct { font-size: 26px; font-weight: 700; }
397
+
398
+ @media (max-width: 640px) {
399
+ .steppy-report-main { padding: 14px; }
400
+ .steppy-report-section { padding: 14px; }
401
+ }
@@ -0,0 +1,137 @@
1
+ {# One scenario, one page: every (locale, layout) variant with its step timeline.
2
+ This is where the report earns its keep over a JUnit report — the failing step
3
+ is named and shown, not inferred from a stack trace. Autoescape is ON. #}
4
+ <!doctype html>
5
+ <html lang="en">
6
+ <head>
7
+ {% include "report/_head.html.j2" %}
8
+ </head>
9
+ <body>
10
+ <header class="steppy-report-header">
11
+ <h1>{% include "report/_mark.html.j2" %}{{ scenario.slug }}</h1>
12
+ <p class="steppy-report-meta steppy-small">
13
+ <span><a href="{{ root }}index.html">&larr; All scenarios</a></span>
14
+ <span><span class="steppy-chip" data-outcome="{{ scenario.outcome }}">{{ scenario.outcome }}</span></span>
15
+ <span>{{ scenario.variants | length }} variant{{ '' if scenario.variants | length == 1 else 's' }}</span>
16
+ <span>{{ scenario.duration_ms_sum | duration }} test time</span>
17
+ </p>
18
+ </header>
19
+
20
+ <main class="steppy-report-main">
21
+
22
+ {% if not scenario.variants %}
23
+ <div class="steppy-notice">
24
+ No test outcome was recorded for this scenario. Its captures are on disk, but the run
25
+ wrote down nothing about whether it passed — add <code>--bundle</code> to
26
+ <code>steppy run</code>.
27
+ </div>
28
+ {% endif %}
29
+
30
+ {% if scenario.failure_log %}
31
+ <div class="steppy-notice">
32
+ A worker log for this scenario is in the run at
33
+ <code>{{ scenario.failure_log }}</code>. It is not copied into this site.
34
+ </div>
35
+ {% endif %}
36
+
37
+ {% for variant in scenario.variants %}
38
+ <section class="steppy-variant">
39
+ <div class="steppy-variant-head">
40
+ <h3>{{ variant_label(variant) }}</h3>
41
+ <span class="steppy-chip" data-outcome="{{ variant.outcome }}">{{ variant.outcome }}</span>
42
+ {% if variant.duration_ms is not none %}
43
+ <span class="steppy-muted steppy-small">{{ variant.duration_ms | duration }}</span>
44
+ {% endif %}
45
+ {% if variant.attempts > 1 %}
46
+ {# Only ever visible for in-process pytest --reruns: the parallel
47
+ retry pass runs a fresh process, which truncates the JSONL and
48
+ takes attempt 1 with it. #}
49
+ <span class="steppy-chip" data-outcome="unknown">attempt {{ variant.attempts }} of {{ variant.attempts }}</span>
50
+ {% endif %}
51
+ </div>
52
+ <p class="steppy-nodeid">{{ variant.nodeid }}</p>
53
+
54
+ {% if variant.failed_step is not none %}
55
+ {# ``{%- ... -%}`` throughout: trim_blocks eats the newline after a block
56
+ tag but lstrip_blocks only strips before one, so a tag at end of line
57
+ leaves the next line's indentation in the middle of a sentence. #}
58
+ <p class="steppy-small">
59
+ <strong>Failed at step {{ variant.failed_step }}</strong>
60
+ {%- if variant.failed_step_title %} — {{ variant.failed_step_title }}{% endif %}.
61
+ {%- if variant.fingerprint %}
62
+ <a href="{{ root }}index.html#cause-{{ variant.fingerprint }}">cause {{ variant.fingerprint }}</a>
63
+ {%- endif %}
64
+ </p>
65
+ {% elif variant.outcome not in ('passed', 'skipped') %}
66
+ <p class="steppy-small steppy-muted">
67
+ No step was in flight when this test died — it failed in fixture setup or
68
+ teardown, or between steps. Blaming the last step reached would point at working
69
+ code.
70
+ </p>
71
+ {% endif %}
72
+
73
+ {% if variant.fail_tail %}
74
+ <pre class="steppy-tail">{{ variant.fail_tail }}</pre>
75
+ {% endif %}
76
+
77
+ {% if variant.failure_screenshot %}
78
+ <div class="steppy-shots">
79
+ <a href="{{ root }}{{ variant.failure_screenshot }}">
80
+ <img src="{{ root }}{{ variant.failure_screenshot }}" alt="Page at the moment of failure" loading="lazy">
81
+ </a>
82
+ </div>
83
+ {% endif %}
84
+
85
+ {% if variant.steps %}
86
+ <ol class="steppy-steps">
87
+ {% for step in variant.steps %}
88
+ {# Step numbers are the SCENARIO's and are sparse — a step skipped
89
+ by only-in:/layouts: emits nothing. Rendered as-is, never
90
+ renumbered, so "step 7" means step 7 of the markdown. #}
91
+ <li class="steppy-step" data-status="{{ step.status }}">
92
+ <span class="steppy-step-num">{{ '%02d' | format(step.step) }}</span>
93
+ <div class="steppy-step-body">
94
+ <div class="steppy-step-title">{{ step.title or '(untitled step)' }}</div>
95
+ <div class="steppy-step-facts">
96
+ <span class="steppy-step-action">{{ step.action }}</span>
97
+ <span>+{{ step.offset_ms | duration }}</span>
98
+ {% if step.duration_ms is not none %}
99
+ <span>took {{ step.duration_ms | duration }}</span>
100
+ {% else %}
101
+ <span>never finished</span>
102
+ {% endif %}
103
+ </div>
104
+ {% if step.screenshots %}
105
+ <div class="steppy-shots">
106
+ {% for url in step.screenshots %}
107
+ <a href="{{ root }}{{ url }}"><img src="{{ root }}{{ url }}" alt="Step {{ step.step }}" loading="lazy"></a>
108
+ {% endfor %}
109
+ </div>
110
+ {% endif %}
111
+ </div>
112
+ </li>
113
+ {% endfor %}
114
+ </ol>
115
+ {% endif %}
116
+
117
+ {% if variant.trace_path or variant.video_path %}
118
+ <p class="steppy-small steppy-muted">
119
+ {% if variant.trace_path %}
120
+ Trace in the run at <code>{{ variant.trace_path }}</code>
121
+ (open with <code>playwright show-trace</code>).
122
+ {% endif %}
123
+ {% if variant.video_path %}
124
+ Video at <code>{{ variant.video_path }}</code>.
125
+ {% endif %}
126
+ </p>
127
+ {% endif %}
128
+ </section>
129
+ {% endfor %}
130
+
131
+ </main>
132
+
133
+ <footer class="steppy-report-footer">
134
+ <p><a href="{{ root }}index.html">&larr; All scenarios</a></p>
135
+ </footer>
136
+ </body>
137
+ </html>
@@ -0,0 +1,30 @@
1
+ {# Response headers for the static site. Netlify and Cloudflare Pages read this
2
+ file; GitHub Pages, GitLab Pages and plain object storage ignore it entirely.
3
+ That is the same bargain `.nojekyll` makes -- meaningful where it is read,
4
+ harmless where it is not -- and none of it is access control. #}
5
+ {% if public %}
6
+ # A PDF is byte-for-byte the page it was printed from, and unlike that page it
7
+ # cannot carry a <meta name="robots">. Without this header the published PDF is
8
+ # the only indexable form of a handbook that is deliberately noindex, and it
9
+ # competes with the HTML of every guide it duplicates.
10
+ /*.pdf
11
+ X-Robots-Tag: noindex
12
+ {% else %}
13
+ # Private build. robots.txt already disallows the fetch and every *page* carries
14
+ # a noindex meta -- but a PDF has no meta to carry, and a disallowed URL is still
15
+ # listable from an external link. Say it for everything.
16
+ /*
17
+ X-Robots-Tag: noindex
18
+ {% endif %}
19
+ {% if tours %}
20
+
21
+ # In-app tour data (the tours `guide` target). Apps fetch these JSON files
22
+ # cross-origin with credentials omitted, so any origin may read them; nothing
23
+ # here is executable, and each file is pinned by the SRI hash the app bundles.
24
+ /{{ tours.dir }}/*
25
+ Access-Control-Allow-Origin: *
26
+ X-Content-Type-Options: nosniff
27
+ {% if public %}
28
+ X-Robots-Tag: noindex
29
+ {% endif %}
30
+ {% endif %}
@@ -0,0 +1,22 @@
1
+ {# The "other renderings of this guide" row, shared by all three of them.
2
+
3
+ Every mode presents the same steps in a different shell, and they canonical to
4
+ one another -- so without this row the non-canonical ones are emitted, told to
5
+ a crawler where the real page is, and reachable by a human only by typing the
6
+ URL. Built by BuildAssembler._mode_links, which also decides when there is
7
+ nothing to show (a guide with one rendering gets no row at all). #}
8
+ {% if mode_links %}
9
+ <nav class="steppy-mode-links" aria-label="Guide format">
10
+ <ul>
11
+ {% for link in mode_links %}
12
+ <li>
13
+ {% if link.active %}
14
+ <span aria-current="true">{{ link.label }}</span>
15
+ {% else %}
16
+ <a href="{{ link.href }}">{{ link.label }}</a>
17
+ {% endif %}
18
+ </li>
19
+ {% endfor %}
20
+ </ul>
21
+ </nav>
22
+ {% endif %}
@@ -0,0 +1,37 @@
1
+ {# Shared SEO <head> fragment. Rendered with a `seo` context dict built by
2
+ BuildAssembler._seo_for_page(). Private mode → only a noindex robots meta;
3
+ public mode → canonical + hreflang alternates + description + optional OG. #}
4
+ {% if seo %}
5
+ {% if seo.robots %}
6
+ <meta name="robots" content="{{ seo.robots }}">
7
+ {% endif %}
8
+ {% if seo.description %}
9
+ <meta name="description" content="{{ seo.description | e }}">
10
+ {% endif %}
11
+ {% if seo.canonical %}
12
+ <link rel="canonical" href="{{ seo.canonical | e }}">
13
+ {% endif %}
14
+ {% for alt in seo.alternates %}
15
+ <link rel="alternate" hreflang="{{ alt.hreflang | e }}" href="{{ alt.href | e }}">
16
+ {% endfor %}
17
+ {% if seo.og %}
18
+ <meta property="og:type" content="{{ seo.og.type }}">
19
+ <meta property="og:title" content="{{ seo.og.title | e }}">
20
+ {% if seo.og.description %}
21
+ <meta property="og:description" content="{{ seo.og.description | e }}">
22
+ {% endif %}
23
+ <meta property="og:url" content="{{ seo.og.url | e }}">
24
+ {% if seo.og.site_name %}
25
+ <meta property="og:site_name" content="{{ seo.og.site_name | e }}">
26
+ {% endif %}
27
+ {% if seo.og.locale %}
28
+ <meta property="og:locale" content="{{ seo.og.locale | e }}">
29
+ {% endif %}
30
+ {% if seo.og.image %}
31
+ <meta property="og:image" content="{{ seo.og.image | e }}">
32
+ <meta name="twitter:card" content="summary_large_image">
33
+ {% else %}
34
+ <meta name="twitter:card" content="summary">
35
+ {% endif %}
36
+ {% endif %}
37
+ {% endif %}
@@ -0,0 +1,10 @@
1
+ /* Steppy cursor animation driver — shared across all static guide pages.
2
+ Reads window.__STEPPY_STEPS__ (per-step frame data with bbox + viewport)
3
+ and window.__STEPPY_TIMINGS__ (cursor timing overrides), then animates
4
+ each step's cursor when its section scrolls into view.
5
+
6
+ This file is now a thin wrapper around the vendored IIFE bundle built
7
+ from packages/player/src/cursor/. To regenerate the bundle:
8
+ npm --prefix packages/player run build:iife
9
+ then copy dist/steppy-cursor-legacy.js into src/steppy/_player_bundle/. */
10
+ {{ cursor_iife | safe }}