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,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">← 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">← 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 }}
|