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,18 @@
1
+ # {{ title }}
2
+
3
+ > Guide pour l'application **{{ role }}**
4
+
5
+ {% for step in steps %}
6
+ ## Étape {{ step.index }} : {{ step.title }}
7
+
8
+ {% if step.subtitle %}
9
+ *{{ step.subtitle }}*
10
+
11
+ {% endif %}
12
+ {% if step.text %}
13
+ {{ step.text }}
14
+
15
+ {% endif %}
16
+ ![Étape {{ step.index }}]({{ step.screenshot }})
17
+
18
+ {% endfor %}
@@ -0,0 +1,124 @@
1
+ # yaml-language-server: $schema=./steppy-config.schema.json
2
+ #
3
+ # Steppy configuration — created by `steppy init`.
4
+ #
5
+ # A "role" is one front-end app / persona you test. This starter uses
6
+ # admin / editor / viewer; rename them (and their URLs) to fit your product,
7
+ # then add/remove roles freely.
8
+ #
9
+ # Credentials use ${ENV_VAR} references so no secrets live in this committed
10
+ # file. Set the vars in your environment/CI, or copy credentials.yaml.example to
11
+ # credentials.yaml (gitignored) and fill in real values. ${VAR:-default}
12
+ # supplies a fallback. See SECURITY.md in the Steppy repo for details.
13
+ #
14
+ # The `# yaml-language-server` line above wires up autocomplete + inline typo
15
+ # checking in editors with the YAML extension (VS Code, etc.).
16
+ apps:
17
+ admin:
18
+ name: "My App — Admin"
19
+ url: "http://localhost:3000"
20
+ auth:
21
+ # `form` = generic hosted-login form-fill (works with any IdP that has a
22
+ # fillable login form: Auth0, Keycloak, …). Other modes: storage-state,
23
+ # bypass, token. See STEPPY-REFERENCE.md.
24
+ mode: form
25
+ credentials:
26
+ username: "${STEPPY_ADMIN_USERNAME}"
27
+ password: "${STEPPY_ADMIN_PASSWORD}"
28
+ # Per-locale localStorage values, set before screenshots so the app renders
29
+ # in the right language during `steppy run --guide`.
30
+ i18n:
31
+ localstorage:
32
+ key: "app-language"
33
+ values:
34
+ en: "en"
35
+ fr: "fr"
36
+ editor:
37
+ name: "My App — Editor"
38
+ url: "http://localhost:3001"
39
+ auth:
40
+ mode: form
41
+ credentials:
42
+ username: "${STEPPY_EDITOR_USERNAME}"
43
+ password: "${STEPPY_EDITOR_PASSWORD}"
44
+ viewer:
45
+ name: "My App — Viewer"
46
+ url: "http://localhost:3002"
47
+ auth:
48
+ mode: form
49
+ credentials:
50
+ username: "${STEPPY_VIEWER_USERNAME}"
51
+ password: "${STEPPY_VIEWER_PASSWORD}"
52
+
53
+ # Auth settings. The `form:` block holds the three login-form selectors; name an
54
+ # IdP `preset:` (auth0 | keycloak) to fill them at once.
55
+ auth:
56
+ storage_state_dir: "./.auth"
57
+ storage_state_ttl: 3600 # seconds (1 hour)
58
+ form:
59
+ # preset: keycloak # -> #username / #password / #kc-login
60
+ username_selector: "input[name='username'], input[name='email'], #username"
61
+ password_selector: "input[name='password'], #password"
62
+ submit_selector: "button[type='submit'], button[name='action'][value='default']"
63
+ post_login_url_pattern: "**/callback**"
64
+ consent_selector: null
65
+
66
+ # Supported locales for guide generation. Add any locale code here.
67
+ locales:
68
+ - en
69
+ - fr
70
+
71
+ default_locale: en
72
+
73
+ screenshot:
74
+ viewport:
75
+ width: 1280
76
+ height: 720
77
+ format: png
78
+ full_page: false
79
+ highlight: true
80
+ highlight_color: "rgba(255, 0, 0, 0.3)"
81
+ highlight_border: "2px solid red"
82
+
83
+ timing:
84
+ navigation_wait: 2000
85
+ action_wait: 500
86
+ sqs_wait: 5000
87
+ step_timeout: 30000
88
+
89
+ # Where artifacts are written and read back, resolved against the project root.
90
+ # `screenshots_dir` is honoured by the capture side and by every reader
91
+ # (`steppy guide`, `steppy build`, `steppy bundle`); `guides_dir` is where
92
+ # `steppy guide` writes. Pointing either outside `output/` works, but only
93
+ # `output/` is bundled, uploaded by the CI template, gitignored, and mounted
94
+ # back to the host by `steppy run --workers N`.
95
+ output:
96
+ screenshots_dir: "./output/screenshots"
97
+ guides_dir: "./output/guides"
98
+
99
+ # Optional blocks not shown here (all off / absent by default):
100
+ # report: JUnit XML, HTML report, Playwright trace + video on failure, retries
101
+ # parallel: `steppy run --workers N` — Docker-backed parallel execution, plus the
102
+ # retry pass (flaky vs. real failures), circuit breaker and auth isolation
103
+ # coverage: `steppy run --coverage` + the `steppy coverage` gap report. Set
104
+ # `coverage.non_ui` there (health checks, webhooks, cron callbacks)
105
+ # or CI counts them against you — triage flags live in the
106
+ # gitignored .steppy/coverage.db and a fresh checkout has none.
107
+ # security: shell-capture gate; see also `screenshot.mask` for redaction
108
+ # pdf: paper settings for `--pdf` / `--handbook`
109
+ # video: `--video` narrated MP4 export, incl. the `video.tts` provider
110
+ # (none | piper | espeak | elevenlabs | openai | azure | google | http).
111
+ # Run `steppy video check` to verify the toolchain.
112
+ # tours: in-app tours from `tour:`-enabled scenarios — `steppy generate`
113
+ # writes tour JSON into your app's assets plus a CI replay per tour.
114
+ # Minimal: `tours: {enabled: true, targets: [{type: app-assets,
115
+ # path: ../your-app/public/tours}]}`; CI gate: `steppy tours
116
+ # --check`. The overlay's buttons ship in en/fr/nl/de (other tour
117
+ # languages need the host's `strings`). See docs/tours.md in the
118
+ # Steppy repository.
119
+ # cloud: `url:` of a Steppy control plane, for `steppy bundle upload`.
120
+ # The URL only — the token is read from STEPPY_CLOUD_TOKEN (or
121
+ # --token) and has no config key on purpose, because this file is
122
+ # committed. Everything else here works with no plane at all.
123
+ # Each is documented with commented examples in the Steppy repo's own config.yaml,
124
+ # and your editor will autocomplete them from the $schema line at the top.
@@ -0,0 +1,30 @@
1
+ # credentials.yaml — LOCAL, GIT-IGNORED credential overlay.
2
+ #
3
+ # Copy this file to `credentials.yaml` (same directory as config.yaml) and fill
4
+ # in real values. It is deep-merged ON TOP of config.yaml at load time, so it
5
+ # only needs the `apps.<role>.auth.credentials` blocks you want to override —
6
+ # everything else is inherited from config.yaml.
7
+ #
8
+ # `credentials.yaml` is listed in .gitignore and must never be committed. Prefer
9
+ # this file (or ${ENV_VAR} references in config.yaml) over inline plaintext
10
+ # secrets. See SECURITY.md for the full credential-handling guidance.
11
+ #
12
+ # You can ALSO keep credentials.yaml env-referenced if you like — the same
13
+ # ${VAR} / ${VAR:-default} interpolation applies to this overlay.
14
+
15
+ apps:
16
+ admin:
17
+ auth:
18
+ credentials:
19
+ username: "test-admin@example.com"
20
+ password: "change-me"
21
+ editor:
22
+ auth:
23
+ credentials:
24
+ username: "test-editor@example.com"
25
+ password: "change-me"
26
+ viewer:
27
+ auth:
28
+ credentials:
29
+ username: "test-viewer@example.com"
30
+ password: "change-me"
@@ -0,0 +1,53 @@
1
+ ---
2
+ title: Create a project
3
+ description: A starter scenario — an admin creates a new project
4
+ tags:
5
+ - e2e
6
+ - user-guide-admin
7
+ priority: high
8
+ guide:
9
+ enabled: true
10
+ ---
11
+
12
+ # This is a starter scenario written by `steppy init`. Each `###` heading is a
13
+ # step; each `- key: value` bullet is a field. Replace the selectors, URLs, and
14
+ # text with your app's. Run `steppy validate` to check it, then `steppy generate`
15
+ # to produce the Playwright test. See STEPPY-REFERENCE.md for the full grammar.
16
+
17
+ ### 1. Admin opens the projects list
18
+ - role: admin
19
+ - action: navigate
20
+ - target: /projects
21
+ - screenshot: admin
22
+ - guide-text:
23
+ en: "From the main menu, open 'Projects' to see all projects."
24
+ fr: "Depuis le menu principal, ouvrez 'Projets' pour voir tous les projets."
25
+
26
+ ### 2. Admin starts a new project
27
+ - role: admin
28
+ - action: click
29
+ - selector: [data-testid="new-project-btn"]
30
+ - screenshot: admin
31
+ - guide-text:
32
+ en: "Click 'New project' in the top-right corner."
33
+ fr: "Cliquez sur 'Nouveau projet' en haut à droite."
34
+
35
+ ### 3. Admin names the project
36
+ - role: admin
37
+ - action: fill
38
+ - selector: [data-testid="project-name"]
39
+ - value: "Website redesign"
40
+ - screenshot: admin
41
+ - guide-text:
42
+ en: "Give the project a clear, descriptive name."
43
+ fr: "Donnez au projet un nom clair et descriptif."
44
+
45
+ ### 4. Admin saves the project
46
+ - role: admin
47
+ - action: click
48
+ - selector: [data-testid="save-project-btn"]
49
+ - expect: contains([data-testid="toast"], "Project created")
50
+ - screenshot: admin
51
+ - guide-text:
52
+ en: "Save to create the project — it now appears in the list."
53
+ fr: "Enregistrez pour créer le projet — il apparaît maintenant dans la liste."
@@ -0,0 +1,14 @@
1
+ <meta charset="utf-8">
2
+ <meta name="viewport" content="width=device-width, initial-scale=1">
3
+ {# Always private. A run report has no SEO value, and ADR 0013 is emphatic
4
+ that a published Pages site means permanently indexed screenshots — so
5
+ there is deliberately no config knob to turn this off. The guides site
6
+ keeps its own SeoConfig for the case where publishing IS the point.
7
+
8
+ This meta is the real control: robots.txt only stops the fetch, and a
9
+ crawler that never fetches the page never learns it is noindex. #}
10
+ <meta name="robots" content="noindex,nofollow,noarchive">
11
+ <title>{{ page_title }}</title>
12
+ <link rel="stylesheet" href="{{ root }}assets/report.css">
13
+ {# The app icon from docs/brand/steppy-icon.svg, inlined so the report stays one self-contained tree. #}
14
+ <link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220.00 0.00 200.00 200.00%22 width=%22200%22 height=%22200%22 role=%22img%22 aria-label=%22steppy%22%3E%3Ctitle%3Esteppy%3C/title%3E%3Crect width=%22200%22 height=%22200%22 rx=%2250%22 fill=%22%2315332A%22/%3E%3Cg transform=%22translate%2815 15%29 scale%280.85%29%22%3E%3Cpath d=%22M68 40H42V160H68M132 40H158V160H132%22 fill=%22none%22 stroke=%22%23E4E9E1%22 stroke-width=%2218%22 stroke-linejoin=%22miter%22/%3E%3Cpath d=%22M74 102L95 123L130 80%22 fill=%22none%22 stroke=%22%23D2561A%22 stroke-width=%2220%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22/%3E%3C/g%3E%3C/svg%3E">
@@ -0,0 +1,3 @@
1
+ {# The steppy mark (logo track C), inline so the brackets follow the theme
2
+ via report.css; aria-hidden because the h1 beside it carries the text. #}
3
+ <svg class="steppy-mark" viewBox="0 0 200 200" aria-hidden="true" focusable="false"><path class="steppy-mark-brackets" d="M68 40H42V160H68M132 40H158V160H132" fill="none" stroke-width="18" stroke-linejoin="miter"/><path class="steppy-mark-check" d="M74 102L95 123L130 80" fill="none" stroke-width="20" stroke-linecap="round" stroke-linejoin="round"/></svg>
@@ -0,0 +1,124 @@
1
+ {# The gap buckets. Not line coverage: this answers "which of my declared routes
2
+ and API endpoints has any e2e scenario actually exercised". Autoescape is ON. #}
3
+ <!doctype html>
4
+ <html lang="en">
5
+ <head>
6
+ {% include "report/_head.html.j2" %}
7
+ </head>
8
+ <body>
9
+ <header class="steppy-report-header">
10
+ <h1>{% include "report/_mark.html.j2" %}E2E surface coverage</h1>
11
+ <p class="steppy-report-meta steppy-small">
12
+ <span><a href="{{ root }}index.html">&larr; Run report</a></span>
13
+ <span>source: {{ coverage.tier }}</span>
14
+ </p>
15
+ </header>
16
+
17
+ <main class="steppy-report-main">
18
+
19
+ <section class="steppy-report-section">
20
+ <div class="steppy-badge-line">
21
+ {% if coverage.badge.percent is not none %}
22
+ <span class="steppy-pct">{{ coverage.badge.percent }}%</span>
23
+ <span class="steppy-muted">
24
+ {{ coverage.badge.tested }} of {{ coverage.badge.declared }} declared
25
+ {{ coverage.badge.surface if coverage.badge.surface != 'all' else 'endpoints and routes' }}
26
+ a UI test could reach
27
+ </span>
28
+ {% else %}
29
+ <span class="steppy-pct steppy-muted">n/a</span>
30
+ <span class="steppy-muted">
31
+ nothing declared. Configure <code>coverage.apis</code> (and optionally
32
+ <code>coverage.sitemap</code>), then run <code>steppy coverage refresh</code>.
33
+ </span>
34
+ {% endif %}
35
+ </div>
36
+ <p class="steppy-small steppy-muted">
37
+ This is not line coverage. It answers which of the routes and API endpoints you
38
+ <em>declared</em> any scenario actually exercised — the denominator is the declared
39
+ surface minus what is marked non-UI-testable.
40
+ </p>
41
+ {% if coverage.non_ui.declared %}
42
+ <p class="steppy-small">
43
+ {{ coverage.non_ui.declared }} declared item{{ '' if coverage.non_ui.declared == 1 else 's' }}
44
+ discounted as <code>non_ui</code>
45
+ ({% for source, count in coverage.non_ui.by_source.items() %}{{ count }} from {{ source }}{% if not loop.last %}, {% endif %}{% endfor %}).
46
+ Committed rules live in <code>coverage.non_ui</code>; a <code>store</code> source is
47
+ a flag set locally with <code>steppy coverage triage set</code>.
48
+ </p>
49
+ {% endif %}
50
+ </section>
51
+
52
+ {% if not coverage.capabilities_strength %}
53
+ <div class="steppy-notice">
54
+ <strong>Assert strength is not shown.</strong> These numbers were rebuilt from the
55
+ run's raw records, and a bundle carries no <code>scenarios/*.md</code> to count assert
56
+ steps in — so every tested endpoint would read as weakly covered, which would be a
57
+ plausible-looking lie. Run <code>steppy coverage report</code> in the project for the
58
+ strong/weak split.
59
+ </div>
60
+ {% endif %}
61
+
62
+ {% for kind in ('endpoints', 'routes') %}
63
+ {% set counts = coverage.counts.get(kind) %}
64
+ {% if counts %}
65
+ <section class="steppy-report-section">
66
+ <h2>{{ kind | capitalize }}</h2>
67
+ <p class="steppy-report-meta">
68
+ <span><strong>{{ counts.declared }}</strong> declared</span>
69
+ <span><strong>{{ counts.tested }}</strong> tested{% if coverage.capabilities_strength %} ({{ counts.tested_strong }} strongly asserted){% endif %}</span>
70
+ <span><strong>{{ counts.declared_unreached }}</strong> never reached ({{ counts.declared_unreached_actionable }} actionable)</span>
71
+ <span><strong>{{ counts.discovered_uncovered }}</strong> seen but weakly covered</span>
72
+ {% if counts.discovered_tested %}<span><strong>{{ counts.discovered_tested }}</strong> tested but undeclared</span>{% endif %}
73
+ </p>
74
+
75
+ {% set unreached = bucket(kind, 'declared_unreached') | rejectattr('triage', 'equalto', 'non_ui') | list %}
76
+ {% if unreached %}
77
+ <h3 class="steppy-small">Declared but never reached &mdash; where a new scenario pays off</h3>
78
+ <div class="steppy-scroll">
79
+ <table class="steppy-report-table">
80
+ <thead><tr>{% if kind == 'endpoints' %}<th>API</th><th>Method</th>{% endif %}<th>Path</th></tr></thead>
81
+ <tbody>
82
+ {% for row in unreached[:200] %}
83
+ <tr>{% if kind == 'endpoints' %}<td>{{ row.api }}</td><td>{{ row.method }}</td>{% endif %}<td><code>{{ row.key }}</code></td></tr>
84
+ {% endfor %}
85
+ </tbody>
86
+ </table>
87
+ </div>
88
+ {% if unreached | length > 200 %}
89
+ <p class="steppy-small steppy-muted">&hellip; and {{ unreached | length - 200 }} more; see <a href="{{ root }}report.json">report.json</a>.</p>
90
+ {% endif %}
91
+ {% endif %}
92
+
93
+ {% set weak = bucket(kind, 'discovered_uncovered') %}
94
+ {% if weak %}
95
+ <h3 class="steppy-small">Exercised but not asserted</h3>
96
+ <div class="steppy-scroll">
97
+ <table class="steppy-report-table">
98
+ <thead><tr>{% if kind == 'endpoints' %}<th>API</th><th>Method</th>{% endif %}<th>Path</th><th>Scenarios</th></tr></thead>
99
+ <tbody>
100
+ {% for row in weak[:200] %}
101
+ <tr>{% if kind == 'endpoints' %}<td>{{ row.api }}</td><td>{{ row.method }}</td>{% endif %}<td><code>{{ row.key }}</code></td><td class="steppy-small">{{ row.scenarios | join(', ') }}</td></tr>
102
+ {% endfor %}
103
+ </tbody>
104
+ </table>
105
+ </div>
106
+ {% endif %}
107
+ </section>
108
+ {% endif %}
109
+ {% endfor %}
110
+
111
+ {% if coverage.warnings %}
112
+ <section class="steppy-report-section">
113
+ <h2>Warnings</h2>
114
+ <ul class="steppy-small">{% for warning in coverage.warnings %}<li>{{ warning }}</li>{% endfor %}</ul>
115
+ </section>
116
+ {% endif %}
117
+
118
+ </main>
119
+
120
+ <footer class="steppy-report-footer">
121
+ <p><a href="{{ root }}index.html">&larr; Run report</a></p>
122
+ </footer>
123
+ </body>
124
+ </html>
@@ -0,0 +1,203 @@
1
+ {# Run report landing page. Rendered with autoescape ON — every value below comes
2
+ from a bundle, and a failure tail is arbitrary page content. #}
3
+ <!doctype html>
4
+ <html lang="en">
5
+ <head>
6
+ {% include "report/_head.html.j2" %}
7
+ </head>
8
+ <body>
9
+ <header class="steppy-report-header">
10
+ <h1>{% include "report/_mark.html.j2" %}{{ title }}</h1>
11
+ <p class="steppy-report-meta steppy-small">
12
+ <span>{{ report.run.run_id }}</span>
13
+ <span>{{ report.run.created_at }}</span>
14
+ <span>steppy {{ report.run.steppy_version }}</span>
15
+ <span>{{ report.run.executor }}</span>
16
+ </p>
17
+ </header>
18
+
19
+ <main class="steppy-report-main">
20
+
21
+ <section class="steppy-report-section">
22
+ <div class="steppy-counts">
23
+ <span class="steppy-verdict" data-outcome="{{ report.totals.verdict }}">{{ report.totals.verdict | upper }}</span>
24
+ <span class="steppy-muted">
25
+ {{ report.totals.scenarios }} scenario{{ '' if report.totals.scenarios == 1 else 's' }},
26
+ {{ report.totals.tests }} test{{ '' if report.totals.tests == 1 else 's' }}
27
+ </span>
28
+ {% for outcome, count in report.totals.outcomes.items() %}
29
+ <span class="steppy-chip" data-outcome="{{ outcome }}">{{ count }} {{ outcome }}</span>
30
+ {% endfor %}
31
+ </div>
32
+ <dl class="steppy-report-meta">
33
+ {% if report.run.git.branch or report.run.git.sha %}
34
+ <div><dt>Commit</dt><dd>{{ report.run.git.branch or '(detached)' }}{% if report.run.git.sha %} @ {{ report.run.git.sha[:8] }}{% endif %}{% if report.run.git.pr %} (PR #{{ report.run.git.pr }}){% endif %}</dd></div>
35
+ {% endif %}
36
+ {% if report.dimensions.locales %}
37
+ <div><dt>Locales</dt><dd>{{ report.dimensions.locales | join(', ') }}</dd></div>
38
+ {% endif %}
39
+ {% if report.dimensions.layouts %}
40
+ <div><dt>Layouts</dt><dd>{{ report.dimensions.layouts | join(', ') }}</dd></div>
41
+ {% endif %}
42
+ {% if report.run.spec.workers and report.run.spec.workers > 1 %}
43
+ <div><dt>Workers</dt><dd>{{ report.run.spec.workers }}</dd></div>
44
+ {% endif %}
45
+ {% if report.run.spec.guide %}<div><dt>Mode</dt><dd>guide capture</dd></div>{% endif %}
46
+ {# Named for what it is: under --workers N this is not wall clock,
47
+ and a bundle carries no finished_at to compute that from. #}
48
+ <div><dt>Test time</dt><dd>{{ report.totals.duration_ms_sum | duration }}</dd></div>
49
+ </dl>
50
+ </section>
51
+
52
+ {% if report.truncated %}
53
+ <div class="steppy-notice">
54
+ <strong>Truncated.</strong> The size budget dropped artifacts that belonged in this
55
+ bundle, so something below may be missing. Screenshots and their sidecars are never
56
+ dropped — see the warnings for what was.
57
+ </div>
58
+ {% endif %}
59
+
60
+ {% if not report.capabilities.step_timeline and report.totals.tests %}
61
+ <div class="steppy-notice">
62
+ No per-step timeline was recorded for this run, so the pages below show outcomes
63
+ without step detail. Either the run did not pass <code>--bundle</code> (which sets
64
+ <code>STEPPY_STEP_EVENTS=1</code>), it set <code>STEPPY_STEP_EVENTS=0</code>, or the
65
+ tests were generated before per-step markers existed — re-run
66
+ <code>steppy generate</code> in that case.
67
+ </div>
68
+ {% endif %}
69
+
70
+ {% if not report.totals.tests %}
71
+ <div class="steppy-notice">
72
+ This run recorded <strong>no test outcomes</strong>. The scenarios below were
73
+ reconstructed from the captures on disk. Add <code>--bundle</code> to the run
74
+ (<code>steppy run --bundle</code>) so outcomes, failure tails and step boundaries are
75
+ written down.
76
+ </div>
77
+ {% endif %}
78
+
79
+ {% if report.failure_causes %}
80
+ <section class="steppy-report-section">
81
+ <h2>Failure causes ({{ report.failure_causes | length }} distinct)</h2>
82
+ <p class="steppy-small steppy-muted">
83
+ Grouped by normalised failure text, biggest first. One broken selector across
84
+ forty scenarios is one row here, not forty tracebacks. The tail shown is the
85
+ first occurrence's — the rest are the same failure.
86
+ </p>
87
+ {% for cause in report.failure_causes %}
88
+ <div class="steppy-cause">
89
+ <div class="steppy-cause-head">
90
+ <span class="steppy-cause-count">&times;{{ cause.count }}</span>
91
+ <span class="steppy-cause-headline">{{ cause.headline }}</span>
92
+ <span class="steppy-fingerprint">{{ cause.fingerprint }}</span>
93
+ </div>
94
+ <p class="steppy-small steppy-muted">
95
+ {% for slug in cause.scenarios[:8] %}<a href="{{ scenario_href(slug) }}">{{ slug }}</a>{% if not loop.last %}, {% endif %}{% endfor %}
96
+ {% if cause.scenarios | length > 8 %} and {{ cause.scenarios | length - 8 }} more{% endif %}
97
+ </p>
98
+ {% if cause.exemplar_tail %}
99
+ <pre class="steppy-tail">{{ cause.exemplar_tail }}</pre>
100
+ {% endif %}
101
+ </div>
102
+ {% endfor %}
103
+ </section>
104
+ {% endif %}
105
+
106
+ <section class="steppy-report-section">
107
+ <h2>Scenarios</h2>
108
+ <div class="steppy-scroll">
109
+ <table class="steppy-report-table">
110
+ <thead>
111
+ <tr>
112
+ <th>Scenario</th>
113
+ {% if matrix_columns %}{% for column in matrix_columns %}<th>{{ column.label }}</th>{% endfor %}
114
+ {% else %}<th>Outcome</th>{% endif %}
115
+ <th>Test time</th>
116
+ </tr>
117
+ </thead>
118
+ <tbody>
119
+ {% for scenario in report.scenarios %}
120
+ <tr>
121
+ <td><a href="{{ scenario.href }}">{{ scenario.slug }}</a></td>
122
+ {% if matrix_columns %}
123
+ {% for column in matrix_columns %}
124
+ {% set cell = matrix_cell(scenario, column) %}
125
+ <td>{% if cell %}<span class="steppy-chip" data-outcome="{{ cell }}">{{ cell }}</span>{% else %}<span class="steppy-muted">&mdash;</span>{% endif %}</td>
126
+ {% endfor %}
127
+ {% else %}
128
+ <td><span class="steppy-chip" data-outcome="{{ scenario.outcome }}">{{ scenario.outcome }}</span></td>
129
+ {% endif %}
130
+ <td class="steppy-muted">{{ scenario.duration_ms_sum | duration }}</td>
131
+ </tr>
132
+ {% endfor %}
133
+ </tbody>
134
+ </table>
135
+ </div>
136
+ </section>
137
+
138
+ {% if report.coverage.tier != 'none' %}
139
+ <section class="steppy-report-section">
140
+ <h2>E2E surface coverage</h2>
141
+ <div class="steppy-badge-line">
142
+ {% if report.coverage.badge.percent is not none %}
143
+ <span class="steppy-pct">{{ report.coverage.badge.percent }}%</span>
144
+ <span class="steppy-muted">
145
+ {{ report.coverage.badge.tested }} of {{ report.coverage.badge.declared }}
146
+ declared endpoints and routes a UI test could reach
147
+ </span>
148
+ {% else %}
149
+ <span class="steppy-pct steppy-muted">n/a</span>
150
+ <span class="steppy-muted">nothing declared — configure <code>coverage.apis</code> and run <code>steppy coverage refresh</code></span>
151
+ {% endif %}
152
+ </div>
153
+ <p class="steppy-small"><a href="coverage.html">Full gap report</a></p>
154
+ </section>
155
+ {% endif %}
156
+
157
+ {% if report.artifacts.by_kind %}
158
+ <section class="steppy-report-section">
159
+ <h2>Artifacts in this run</h2>
160
+ <p class="steppy-small steppy-muted">
161
+ What the run produced, by kind. Traces and videos are listed rather than copied
162
+ here — a Playwright trace needs <code>playwright show-trace</code> to open, so a
163
+ link would be a dead end.
164
+ </p>
165
+ <div class="steppy-scroll">
166
+ <table class="steppy-report-table">
167
+ <thead><tr><th>Kind</th><th>Files</th><th>Size</th></tr></thead>
168
+ <tbody>
169
+ {% for kind, group in report.artifacts.by_kind.items() %}
170
+ <tr><td>{{ kind }}</td><td>{{ group.count }}</td><td>{{ group.bytes | filesize }}</td></tr>
171
+ {% endfor %}
172
+ </tbody>
173
+ </table>
174
+ </div>
175
+ </section>
176
+ {% endif %}
177
+
178
+ {% if report.warnings or report.coverage.warnings %}
179
+ <section class="steppy-report-section">
180
+ <h2>Warnings</h2>
181
+ <ul class="steppy-small">
182
+ {% for warning in report.warnings %}<li>{{ warning }}</li>{% endfor %}
183
+ {% for warning in report.coverage.warnings %}<li>{{ warning }}</li>{% endfor %}
184
+ </ul>
185
+ </section>
186
+ {% endif %}
187
+
188
+ </main>
189
+
190
+ <footer class="steppy-report-footer">
191
+ <p>
192
+ Generated by Steppy {{ report.generated_by }} from {{ report.source.kind }}
193
+ <code>{{ report.source.label }}</code>.
194
+ Machine-readable: <a href="report.json">report.json</a>{% if report.coverage.tier != 'none' %},
195
+ <a href="badge.json">badge.json</a>{% endif %}.
196
+ </p>
197
+ <p>
198
+ This page is marked <code>noindex</code>. It is a run report, not documentation —
199
+ and a published Pages site means published screenshots.
200
+ </p>
201
+ </footer>
202
+ </body>
203
+ </html>