@sun-asterisk/sungen 3.2.18 → 3.2.20

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 (264) hide show
  1. package/dist/capabilities/discover.d.ts.map +1 -1
  2. package/dist/capabilities/discover.js +4 -1
  3. package/dist/capabilities/discover.js.map +1 -1
  4. package/dist/cli/commands/add.d.ts.map +1 -1
  5. package/dist/cli/commands/add.js +4 -2
  6. package/dist/cli/commands/add.js.map +1 -1
  7. package/dist/cli/commands/dashboard.d.ts.map +1 -1
  8. package/dist/cli/commands/dashboard.js +10 -1
  9. package/dist/cli/commands/dashboard.js.map +1 -1
  10. package/dist/cli/commands/delivery.d.ts.map +1 -1
  11. package/dist/cli/commands/delivery.js +19 -1
  12. package/dist/cli/commands/delivery.js.map +1 -1
  13. package/dist/cli/commands/generate.d.ts.map +1 -1
  14. package/dist/cli/commands/generate.js +10 -1
  15. package/dist/cli/commands/generate.js.map +1 -1
  16. package/dist/dashboard/html-renderer.d.ts.map +1 -1
  17. package/dist/dashboard/html-renderer.js +7 -2
  18. package/dist/dashboard/html-renderer.js.map +1 -1
  19. package/dist/dashboard/snapshot-builder.d.ts.map +1 -1
  20. package/dist/dashboard/snapshot-builder.js +3 -0
  21. package/dist/dashboard/snapshot-builder.js.map +1 -1
  22. package/dist/dashboard/types.d.ts +4 -0
  23. package/dist/dashboard/types.d.ts.map +1 -1
  24. package/dist/exporters/api-environment.d.ts.map +1 -1
  25. package/dist/exporters/api-environment.js +4 -24
  26. package/dist/exporters/api-environment.js.map +1 -1
  27. package/dist/exporters/api-testcase-formatter.d.ts +6 -0
  28. package/dist/exporters/api-testcase-formatter.d.ts.map +1 -1
  29. package/dist/exporters/api-testcase-formatter.js +11 -4
  30. package/dist/exporters/api-testcase-formatter.js.map +1 -1
  31. package/dist/exporters/csv-exporter.d.ts.map +1 -1
  32. package/dist/exporters/csv-exporter.js +40 -9
  33. package/dist/exporters/csv-exporter.js.map +1 -1
  34. package/dist/exporters/env-qa.d.ts +13 -0
  35. package/dist/exporters/env-qa.d.ts.map +1 -0
  36. package/dist/exporters/env-qa.js +82 -0
  37. package/dist/exporters/env-qa.js.map +1 -0
  38. package/dist/exporters/feature-parser.d.ts.map +1 -1
  39. package/dist/exporters/feature-parser.js +16 -33
  40. package/dist/exporters/feature-parser.js.map +1 -1
  41. package/dist/exporters/json-exporter.d.ts.map +1 -1
  42. package/dist/exporters/json-exporter.js +57 -20
  43. package/dist/exporters/json-exporter.js.map +1 -1
  44. package/dist/exporters/matrix/build.d.ts.map +1 -1
  45. package/dist/exporters/matrix/build.js +15 -6
  46. package/dist/exporters/matrix/build.js.map +1 -1
  47. package/dist/exporters/matrix/gates.d.ts.map +1 -1
  48. package/dist/exporters/matrix/gates.js +3 -2
  49. package/dist/exporters/matrix/gates.js.map +1 -1
  50. package/dist/exporters/matrix/step-buckets.d.ts +13 -0
  51. package/dist/exporters/matrix/step-buckets.d.ts.map +1 -0
  52. package/dist/exporters/matrix/step-buckets.js +7 -0
  53. package/dist/exporters/matrix/step-buckets.js.map +1 -0
  54. package/dist/exporters/qa-env-secret-denylist.d.ts +43 -0
  55. package/dist/exporters/qa-env-secret-denylist.d.ts.map +1 -0
  56. package/dist/exporters/qa-env-secret-denylist.js +143 -0
  57. package/dist/exporters/qa-env-secret-denylist.js.map +1 -0
  58. package/dist/exporters/scenario-merger.d.ts +20 -13
  59. package/dist/exporters/scenario-merger.d.ts.map +1 -1
  60. package/dist/exporters/scenario-merger.js +55 -30
  61. package/dist/exporters/scenario-merger.js.map +1 -1
  62. package/dist/exporters/step-formatter.d.ts +42 -33
  63. package/dist/exporters/step-formatter.d.ts.map +1 -1
  64. package/dist/exporters/step-formatter.js +68 -68
  65. package/dist/exporters/step-formatter.js.map +1 -1
  66. package/dist/exporters/test-data-resolver.d.ts +15 -0
  67. package/dist/exporters/test-data-resolver.d.ts.map +1 -1
  68. package/dist/exporters/test-data-resolver.js +34 -1
  69. package/dist/exporters/test-data-resolver.js.map +1 -1
  70. package/dist/exporters/types.d.ts +9 -8
  71. package/dist/exporters/types.d.ts.map +1 -1
  72. package/dist/generators/test-generator/adapters/adapter-interface.d.ts +2 -0
  73. package/dist/generators/test-generator/adapters/adapter-interface.d.ts.map +1 -1
  74. package/dist/generators/test-generator/adapters/appium/appium-adapter.d.ts +2 -0
  75. package/dist/generators/test-generator/adapters/appium/appium-adapter.d.ts.map +1 -1
  76. package/dist/generators/test-generator/adapters/appium/appium-adapter.js.map +1 -1
  77. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/keyboard-global-action.hbs +3 -1
  78. package/dist/generators/test-generator/adapters/playwright/playwright-adapter.d.ts +2 -0
  79. package/dist/generators/test-generator/adapters/playwright/playwright-adapter.d.ts.map +1 -1
  80. package/dist/generators/test-generator/adapters/playwright/playwright-adapter.js.map +1 -1
  81. package/dist/generators/test-generator/adapters/playwright/templates/imports.hbs +6 -0
  82. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/keyboard-global-action.hbs +3 -1
  83. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/page-assertion.hbs +1 -1
  84. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/storage-key-assertion.hbs +4 -0
  85. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/tab-order-assertion.hbs +30 -0
  86. package/dist/generators/test-generator/adapters/playwright/templates/steps/navigation/navigation.hbs +4 -0
  87. package/dist/generators/test-generator/adapters/playwright/templates/steps/navigation/route-assertion.hbs +1 -4
  88. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/dialog-root.hbs +1 -1
  89. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/url-assertion.hbs +15 -0
  90. package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
  91. package/dist/generators/test-generator/code-generator.js +78 -7
  92. package/dist/generators/test-generator/code-generator.js.map +1 -1
  93. package/dist/generators/test-generator/diagnostics.d.ts +16 -1
  94. package/dist/generators/test-generator/diagnostics.d.ts.map +1 -1
  95. package/dist/generators/test-generator/diagnostics.js +15 -0
  96. package/dist/generators/test-generator/diagnostics.js.map +1 -1
  97. package/dist/generators/test-generator/recipe-expander.d.ts +1 -1
  98. package/dist/generators/test-generator/recipe-expander.d.ts.map +1 -1
  99. package/dist/generators/test-generator/recipe-expander.js +1 -1
  100. package/dist/generators/test-generator/recipe-expander.js.map +1 -1
  101. package/dist/generators/test-generator/step-mapper.d.ts +41 -0
  102. package/dist/generators/test-generator/step-mapper.d.ts.map +1 -1
  103. package/dist/generators/test-generator/step-mapper.js +161 -17
  104. package/dist/generators/test-generator/step-mapper.js.map +1 -1
  105. package/dist/generators/test-generator/template-engine.d.ts +2 -0
  106. package/dist/generators/test-generator/template-engine.d.ts.map +1 -1
  107. package/dist/generators/test-generator/template-engine.js +1 -1
  108. package/dist/generators/test-generator/template-engine.js.map +1 -1
  109. package/dist/generators/test-generator/utils/data-resolver.d.ts +18 -1
  110. package/dist/generators/test-generator/utils/data-resolver.d.ts.map +1 -1
  111. package/dist/generators/test-generator/utils/data-resolver.js +28 -4
  112. package/dist/generators/test-generator/utils/data-resolver.js.map +1 -1
  113. package/dist/generators/test-generator/utils/runtime-data-transformer.d.ts +2 -2
  114. package/dist/generators/test-generator/utils/runtime-data-transformer.d.ts.map +1 -1
  115. package/dist/generators/test-generator/utils/runtime-data-transformer.js +56 -11
  116. package/dist/generators/test-generator/utils/runtime-data-transformer.js.map +1 -1
  117. package/dist/generators/test-generator/utils/selector-resolver.d.ts +18 -2
  118. package/dist/generators/test-generator/utils/selector-resolver.d.ts.map +1 -1
  119. package/dist/generators/test-generator/utils/selector-resolver.js +29 -6
  120. package/dist/generators/test-generator/utils/selector-resolver.js.map +1 -1
  121. package/dist/harness/audit.d.ts.map +1 -1
  122. package/dist/harness/audit.js +8 -7
  123. package/dist/harness/audit.js.map +1 -1
  124. package/dist/harness/capability-plan.d.ts.map +1 -1
  125. package/dist/harness/capability-plan.js +2 -3
  126. package/dist/harness/capability-plan.js.map +1 -1
  127. package/dist/harness/catalog/drivers.yaml +1 -1
  128. package/dist/harness/challenge.d.ts.map +1 -1
  129. package/dist/harness/challenge.js +1 -55
  130. package/dist/harness/challenge.js.map +1 -1
  131. package/dist/harness/data-driven-lint.d.ts.map +1 -1
  132. package/dist/harness/data-driven-lint.js +14 -9
  133. package/dist/harness/data-driven-lint.js.map +1 -1
  134. package/dist/harness/depth-lint.d.ts.map +1 -1
  135. package/dist/harness/depth-lint.js +1 -3
  136. package/dist/harness/depth-lint.js.map +1 -1
  137. package/dist/harness/flow-check.d.ts.map +1 -1
  138. package/dist/harness/flow-check.js +9 -7
  139. package/dist/harness/flow-check.js.map +1 -1
  140. package/dist/harness/flow-plan.d.ts.map +1 -1
  141. package/dist/harness/flow-plan.js +4 -3
  142. package/dist/harness/flow-plan.js.map +1 -1
  143. package/dist/harness/manifest.js +2 -2
  144. package/dist/harness/manifest.js.map +1 -1
  145. package/dist/harness/parse.d.ts +12 -0
  146. package/dist/harness/parse.d.ts.map +1 -1
  147. package/dist/harness/parse.js +28 -1
  148. package/dist/harness/parse.js.map +1 -1
  149. package/dist/harness/script-check.d.ts.map +1 -1
  150. package/dist/harness/script-check.js +61 -31
  151. package/dist/harness/script-check.js.map +1 -1
  152. package/dist/harness/secret-scan.d.ts +3 -1
  153. package/dist/harness/secret-scan.d.ts.map +1 -1
  154. package/dist/harness/secret-scan.js +27 -11
  155. package/dist/harness/secret-scan.js.map +1 -1
  156. package/dist/harness/sensors.d.ts.map +1 -1
  157. package/dist/harness/sensors.js +15 -4
  158. package/dist/harness/sensors.js.map +1 -1
  159. package/dist/harness/trace.js +1 -1
  160. package/dist/harness/trace.js.map +1 -1
  161. package/dist/harness/unit-paths.d.ts +16 -0
  162. package/dist/harness/unit-paths.d.ts.map +1 -1
  163. package/dist/harness/unit-paths.js +39 -2
  164. package/dist/harness/unit-paths.js.map +1 -1
  165. package/dist/index.d.ts +1 -0
  166. package/dist/index.d.ts.map +1 -1
  167. package/dist/index.js +3 -1
  168. package/dist/index.js.map +1 -1
  169. package/dist/orchestrator/screen-manager.d.ts.map +1 -1
  170. package/dist/orchestrator/screen-manager.js +8 -2
  171. package/dist/orchestrator/screen-manager.js.map +1 -1
  172. package/dist/orchestrator/templates/ai-src/commands/dashboard.md +2 -1
  173. package/dist/orchestrator/templates/ai-src/commands/run-test.md +1 -1
  174. package/dist/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +8 -0
  175. package/dist/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +4 -0
  176. package/dist/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +79 -2
  177. package/dist/orchestrator/templates/ai-src/skills/sungen-selector-fix/SKILL.md +7 -1
  178. package/dist/orchestrator/templates/ai-src/skills/sungen-selector-keys/SKILL.md +12 -3
  179. package/dist/orchestrator/templates/playwright.config.ts +33 -2
  180. package/dist/orchestrator/templates/specs-mock.d.ts +66 -0
  181. package/dist/orchestrator/templates/specs-mock.d.ts.map +1 -0
  182. package/dist/orchestrator/templates/specs-mock.js +137 -0
  183. package/dist/orchestrator/templates/specs-mock.js.map +1 -0
  184. package/dist/orchestrator/templates/specs-mock.ts +179 -0
  185. package/dist/orchestrator/templates/specs-url-assert.d.ts +67 -0
  186. package/dist/orchestrator/templates/specs-url-assert.d.ts.map +1 -0
  187. package/dist/orchestrator/templates/specs-url-assert.js +121 -0
  188. package/dist/orchestrator/templates/specs-url-assert.js.map +1 -0
  189. package/dist/orchestrator/templates/specs-url-assert.ts +134 -0
  190. package/dist/orchestrator/test-data-guide.d.ts.map +1 -1
  191. package/dist/orchestrator/test-data-guide.js +4 -0
  192. package/dist/orchestrator/test-data-guide.js.map +1 -1
  193. package/package.json +3 -3
  194. package/src/capabilities/discover.ts +4 -1
  195. package/src/cli/commands/add.ts +4 -2
  196. package/src/cli/commands/dashboard.ts +10 -1
  197. package/src/cli/commands/delivery.ts +19 -1
  198. package/src/cli/commands/generate.ts +9 -1
  199. package/src/dashboard/html-renderer.ts +8 -2
  200. package/src/dashboard/snapshot-builder.ts +3 -0
  201. package/src/dashboard/types.ts +4 -0
  202. package/src/exporters/api-environment.ts +3 -20
  203. package/src/exporters/api-testcase-formatter.ts +8 -2
  204. package/src/exporters/csv-exporter.ts +47 -8
  205. package/src/exporters/env-qa.ts +41 -0
  206. package/src/exporters/feature-parser.ts +17 -38
  207. package/src/exporters/json-exporter.ts +71 -24
  208. package/src/exporters/matrix/build.ts +15 -6
  209. package/src/exporters/matrix/gates.ts +3 -2
  210. package/src/exporters/matrix/step-buckets.ts +15 -0
  211. package/src/exporters/qa-env-secret-denylist.ts +118 -0
  212. package/src/exporters/scenario-merger.ts +80 -42
  213. package/src/exporters/step-formatter.ts +67 -66
  214. package/src/exporters/test-data-resolver.ts +34 -1
  215. package/src/exporters/types.ts +9 -8
  216. package/src/generators/test-generator/adapters/adapter-interface.ts +1 -1
  217. package/src/generators/test-generator/adapters/appium/appium-adapter.ts +1 -1
  218. package/src/generators/test-generator/adapters/appium/templates/steps/actions/keyboard-global-action.hbs +3 -1
  219. package/src/generators/test-generator/adapters/playwright/playwright-adapter.ts +1 -1
  220. package/src/generators/test-generator/adapters/playwright/templates/imports.hbs +6 -0
  221. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/keyboard-global-action.hbs +3 -1
  222. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/page-assertion.hbs +1 -1
  223. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/storage-key-assertion.hbs +4 -0
  224. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/tab-order-assertion.hbs +30 -0
  225. package/src/generators/test-generator/adapters/playwright/templates/steps/navigation/navigation.hbs +4 -0
  226. package/src/generators/test-generator/adapters/playwright/templates/steps/navigation/route-assertion.hbs +1 -4
  227. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/dialog-root.hbs +1 -1
  228. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/url-assertion.hbs +15 -0
  229. package/src/generators/test-generator/code-generator.ts +91 -8
  230. package/src/generators/test-generator/diagnostics.ts +16 -1
  231. package/src/generators/test-generator/recipe-expander.ts +2 -2
  232. package/src/generators/test-generator/step-mapper.ts +160 -15
  233. package/src/generators/test-generator/template-engine.ts +2 -2
  234. package/src/generators/test-generator/utils/data-resolver.ts +27 -5
  235. package/src/generators/test-generator/utils/runtime-data-transformer.ts +57 -11
  236. package/src/generators/test-generator/utils/selector-resolver.ts +29 -5
  237. package/src/harness/audit.ts +10 -9
  238. package/src/harness/capability-plan.ts +2 -2
  239. package/src/harness/catalog/drivers.yaml +1 -1
  240. package/src/harness/challenge.ts +2 -3
  241. package/src/harness/data-driven-lint.ts +13 -9
  242. package/src/harness/depth-lint.ts +2 -4
  243. package/src/harness/flow-check.ts +9 -7
  244. package/src/harness/flow-plan.ts +4 -3
  245. package/src/harness/manifest.ts +3 -3
  246. package/src/harness/parse.ts +28 -1
  247. package/src/harness/script-check.ts +61 -33
  248. package/src/harness/secret-scan.ts +23 -8
  249. package/src/harness/sensors.ts +15 -4
  250. package/src/harness/trace.ts +2 -2
  251. package/src/harness/unit-paths.ts +39 -2
  252. package/src/index.ts +1 -0
  253. package/src/orchestrator/screen-manager.ts +7 -2
  254. package/src/orchestrator/templates/ai-src/commands/dashboard.md +2 -1
  255. package/src/orchestrator/templates/ai-src/commands/run-test.md +1 -1
  256. package/src/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +8 -0
  257. package/src/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +4 -0
  258. package/src/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +79 -2
  259. package/src/orchestrator/templates/ai-src/skills/sungen-selector-fix/SKILL.md +7 -1
  260. package/src/orchestrator/templates/ai-src/skills/sungen-selector-keys/SKILL.md +12 -3
  261. package/src/orchestrator/templates/playwright.config.ts +33 -2
  262. package/src/orchestrator/templates/specs-mock.ts +179 -0
  263. package/src/orchestrator/templates/specs-url-assert.ts +134 -0
  264. package/src/orchestrator/test-data-guide.ts +4 -0
@@ -56,27 +56,34 @@ export interface MergedScenario {
56
56
  spec: SpecTest | null;
57
57
 
58
58
  /**
59
- * When/Then/Given steps with any `@extend:<name>` chain pre-resolved
60
- * against the matching `@steps:<name>` base scenario (its steps are
61
- * prepended). Use these for surfaces that show the test case text
62
- * (dashboard Steps section) so we get the canonical feature wording
63
- * without re-parsing compiled spec.ts comments.
59
+ * Feature Background steps, kept separate from `resolvedSteps` so the
60
+ * exporters render them in the Pre-condition column. Inlining them into
61
+ * Steps shifted the step numbering against the .feature source, which made
62
+ * QA's Gherkin-vs-export review a manual re-mapping exercise.
64
63
  */
65
- resolvedWhenSteps: string[];
66
- resolvedThenSteps: string[];
67
- resolvedGivenSteps: string[];
64
+ resolvedBackgroundSteps: OrderedStep[];
68
65
  /**
69
66
  * Steps section for display, in chronological order:
70
- * Background (all) → @extend base (all incl. its Then) → own up to the
71
- * first `Then`. Base's `Then` stays here because it's mid-flow setup,
72
- * not the scenario's final assertion.
67
+ * @extend base (all incl. its Then) → own given/when-bucket steps.
68
+ * Base's `Then` stays here because it's mid-flow setup, not the
69
+ * scenario's final assertion. Background is NOT included — it renders
70
+ * as Pre-condition (see `resolvedBackgroundSteps`).
73
71
  */
74
72
  resolvedSteps: OrderedStep[];
75
73
  /**
76
- * Expected results — own scenario's first `Then` onwards. Empty when the
77
- * scenario has no `Then`.
74
+ * Expected results — own scenario's then-bucket steps (a `When` after a
75
+ * `Then` starts a new action and goes back to Steps; an `And` follows its
76
+ * preceding explicit keyword). Empty when the scenario has no `Then`.
78
77
  */
79
78
  resolvedExpected: OrderedStep[];
79
+ /**
80
+ * Count of anchor-eligible action lines (@extend base + own given/when
81
+ * steps). This — NOT the rendered Steps length — is the reference for the
82
+ * "is any assertion mid-flow?" check in groupExpectedByAnchor: manual-comment
83
+ * steps are appended to resolvedSteps but never participate in anchoring,
84
+ * so counting them would falsely mark @manual scenarios as interleaved.
85
+ */
86
+ resolvedAnchorActionCount: number;
80
87
  }
81
88
 
82
89
  /**
@@ -98,20 +105,40 @@ export function mergeFeatureAndSpec(
98
105
  if (isStepsBaseScenario(scenario)) continue; // skip @steps base scenarios
99
106
  if (isSampleScaffoldScenario(scenario)) continue; // skip default sample
100
107
 
101
- const specTest = findMatchingSpecTest(scenario, spec.tests);
108
+ const specTest = findMatchingSpecTest(scenario, spec.tests, feature.scenarios);
102
109
  const base = scenario.extendsName ? stepsBaseByName.get(scenario.extendsName) : undefined;
103
110
 
104
111
  // Order of execution for a runnable scenario:
105
- // 1. Feature Background (once) — all buckets are setup, stay in Steps.
112
+ // 1. Feature Background (once) — setup state, rendered as Pre-condition
113
+ // so Steps numbering stays aligned with the .feature scenario.
106
114
  // 2. @extend base scenario's own steps (if any) — *including* its Then,
107
115
  // because the base's Then is a mid-flow assertion, not the scenario's
108
116
  // final verification.
109
- // 3. This scenario's own steps in original sequence, split at the
110
- // first `Then`: pre-Then → Steps, Then-onwards → Expected results.
117
+ // 3. This scenario's own steps, partitioned by BUCKET (not by position):
118
+ // given/when → Steps, then → Expected results. A positional split at
119
+ // the first `Then` broke interleaved scenarios (When → Then → When →
120
+ // Then …): every later `When` action was exported as an "expected
121
+ // result". classifySteps already resolves `And` to its preceding
122
+ // explicit keyword, so an `And` after `Then` stays an assertion and
123
+ // an `And` after `When` stays an action. Relative order is preserved
124
+ // within each column.
125
+ // Each assertion also records its ANCHOR — the 1-based index of the
126
+ // last action before it in the rendered Steps column (@extend base
127
+ // steps count toward the numbering; Background does not, it renders
128
+ // as Pre-condition). The exporters use anchors to append
129
+ // "— after step N" when a scenario interleaves actions and
130
+ // assertions, restoring the correspondence the 2-column layout loses.
111
131
  const ownOrdered = scenario.orderedSteps;
112
- const firstThenIdx = ownOrdered.findIndex((s) => s.bucket === 'then');
113
- const ownPreThen = firstThenIdx >= 0 ? ownOrdered.slice(0, firstThenIdx) : ownOrdered;
114
- const ownPostThen = firstThenIdx >= 0 ? ownOrdered.slice(firstThenIdx) : [];
132
+ const baseLen = base?.orderedSteps.length ?? 0;
133
+ const ownActions: OrderedStep[] = [];
134
+ const ownAssertions: OrderedStep[] = [];
135
+ for (const s of ownOrdered) {
136
+ if (s.bucket === 'then') {
137
+ ownAssertions.push({ ...s, anchorStep: baseLen + ownActions.length });
138
+ } else {
139
+ ownActions.push(s);
140
+ }
141
+ }
115
142
 
116
143
  // A @manual scenario carries no Gherkin steps — its tester procedure lives in the
117
144
  // `# Tester verifies:` comment block. Parse it into Steps (Setup/Action) + Expected
@@ -125,49 +152,60 @@ export function mergeFeatureAndSpec(
125
152
  result.push({
126
153
  feature: scenario,
127
154
  spec: specTest,
128
- resolvedWhenSteps: [
129
- ...feature.backgroundWhenSteps,
130
- ...(base?.rawWhenSteps ?? []),
131
- ...scenario.rawWhenSteps,
132
- ],
133
- resolvedThenSteps: [
134
- ...feature.backgroundThenSteps,
135
- ...(base?.rawThenSteps ?? []),
136
- ...scenario.rawThenSteps,
137
- ],
138
- resolvedGivenSteps: [
139
- ...feature.backgroundGivenSteps,
140
- ...(base?.rawGivenSteps ?? []),
141
- ...scenario.rawGivenSteps,
142
- ],
155
+ resolvedBackgroundSteps: feature.backgroundOrderedSteps,
143
156
  resolvedSteps: [
144
- ...feature.backgroundOrderedSteps,
145
157
  ...(base?.orderedSteps ?? []),
146
- ...ownPreThen,
158
+ ...ownActions,
147
159
  ...manualParsed.steps,
148
160
  ],
149
- resolvedExpected: [...ownPostThen, ...manualParsed.expected],
161
+ resolvedExpected: [...ownAssertions, ...manualParsed.expected],
162
+ resolvedAnchorActionCount: baseLen + ownActions.length,
150
163
  });
151
164
  }
152
165
 
153
166
  return result;
154
167
  }
155
168
 
169
+ /** `@manual` / `@manual:<reason>` — a manual scenario has no automated test by definition. */
170
+ function isManualScenario(scenario: ScenarioMetadata): boolean {
171
+ return scenario.tags.some((t) => t === '@manual' || t.startsWith('@manual:'));
172
+ }
173
+
156
174
  /**
157
175
  * Find the spec test that corresponds to a given feature scenario.
158
176
  * Match by scenario title (exact match on the scenario name).
177
+ *
178
+ * The VP-ID fallback exists only to survive small TITLE drift between compile
179
+ * and export — it must never RE-ASSIGN a result to a different scenario. Field
180
+ * report: a feature carried the same VP id on many scenarios (VP-NAV-001 ×14,
181
+ * mixing @manual and automated); the old first-match fallback attached an
182
+ * automated sibling's executed result to every @manual sibling, so 7 testcases
183
+ * nobody ever ran shipped as "Passed" in the client deliverable. Two guards:
184
+ * 1. a @manual scenario never matches ANY test;
185
+ * 2. the fallback applies only when the vpId is UNIQUE on both sides —
186
+ * exactly one test AND exactly one automatable scenario carry it.
187
+ * Ambiguity → null → the row reports an honest "Pending".
159
188
  */
160
- function findMatchingSpecTest(scenario: ScenarioMetadata, tests: SpecTest[]): SpecTest | null {
189
+ function findMatchingSpecTest(
190
+ scenario: ScenarioMetadata,
191
+ tests: SpecTest[],
192
+ allScenarios: ScenarioMetadata[],
193
+ ): SpecTest | null {
194
+ if (isManualScenario(scenario)) return null;
195
+
161
196
  // Exact title match
162
197
  const exact = tests.find((t) => t.scenarioName === scenario.name);
163
198
  if (exact) return exact;
164
199
 
165
- // Fallback: match by VP-ID prefix (ignore trailing text differences)
200
+ // Fallback: match by VP-ID prefix only when unique both ways (see above).
166
201
  const vpMatch = scenario.name.match(SCENARIO_ID);
167
202
  if (vpMatch) {
168
203
  const vpId = vpMatch[1];
169
- const byVp = tests.find((t) => t.vpId === vpId);
170
- if (byVp) return byVp;
204
+ const testsWithVp = tests.filter((t) => t.vpId === vpId);
205
+ const scenariosWithVp = allScenarios.filter(
206
+ (s) => !isManualScenario(s) && s.name.match(SCENARIO_ID)?.[1] === vpId,
207
+ );
208
+ if (testsWithVp.length === 1 && scenariosWithVp.length === 1) return testsWithVp[0];
171
209
  }
172
210
 
173
211
  return null;
@@ -1,25 +1,10 @@
1
1
  /**
2
- * Convert raw Gherkin step text OR generated .spec.ts comment lines into
3
- * concise, numbered natural language for the CSV Steps/Expected columns.
2
+ * Convert raw Gherkin step text into concise natural language for the
3
+ * CSV Steps/Expected columns.
4
4
  *
5
5
  * Deterministic — no AI calls.
6
6
  */
7
7
 
8
- /**
9
- * Format a list of raw step lines for the Steps / Expected Results columns.
10
- *
11
- * - 0 lines → empty string
12
- * - 1 line → the line verbatim (no "1. " prefix — avoids Google Sheets clutter)
13
- * - ≥ 2 → each line on its own row, "1. ", "2. ", … prefixes, newline-joined
14
- * so spreadsheet cells auto-wrap one step per visual line.
15
- */
16
- export function formatNumberedSteps(lines: string[]): string {
17
- const cleaned = lines.map(cleanStepLine).filter((l) => l.length > 0);
18
- if (cleaned.length === 0) return '';
19
- if (cleaned.length === 1) return cleaned[0];
20
- return cleaned.map((line, idx) => `${idx + 1}. ${line}`).join('\n');
21
- }
22
-
23
8
  /**
24
9
  * Strip the leading actor (`User `) from a raw Gherkin step and capitalise
25
10
  * the first letter. Brackets `[ref]`, `{{var}}` placeholders and element
@@ -42,64 +27,80 @@ export function keepActor(raw: string): string {
42
27
  }
43
28
 
44
29
  /**
45
- * Number a list of raw feature steps. Mirrors `formatNumberedSteps` but
46
- * preserves the original Gherkin punctuation (no bracket-to-quote conversion).
47
- * The mapper picks between `stripActor` (When/Then) and `keepActor`
48
- * (Given) so the actor word matches the step's grammatical role.
49
- * Use this for feature-direct rendering (dashboard + delivery).
50
- */
51
- export function formatFeatureSteps(lines: string[], mapper: (s: string) => string = stripActor): string {
52
- const cleaned = lines.map(mapper).filter((l) => l.length > 0);
53
- if (cleaned.length === 0) return '';
54
- if (cleaned.length === 1) return cleaned[0];
55
- return cleaned.map((line, idx) => `${idx + 1}. ${line}`).join('\n');
56
- }
57
-
58
- /**
59
- * Clean a raw step line (either Gherkin text or .spec.ts comment).
30
+ * Group formatted Expected-results lines by their anchor step.
60
31
  *
61
- * Handles patterns like:
62
- * "User click [login] button" → "Click 'login' button"
63
- * "User fill [Email] field with {{email}}" → "Fill 'Email' field with {{email}}"
64
- * "Click Signature 2025 Creator" → "Click 'Signature 2025 Creator'"
65
- * "Assert title error has text err_xxx" → "Title error has text err_xxx"
66
- * "Open awards page" → "Open awards page"
32
+ * Single source of truth for the anchor rendering (used by csv-exporter AND
33
+ * json-exporter so the delivery sheet and the dashboard cannot drift). Final
34
+ * format per user review 2026-08-06:
35
+ *
36
+ * - When assertions verify DIFFERENT moments (more than one distinct
37
+ * anchor, or the single anchor is not the final action), the cell is
38
+ * rendered as groups — a "— after step N" header line (anchor 0 →
39
+ * "— before step 1") followed by that group's assertions. Within a
40
+ * group, lines are numbered 1..k only when the group has more than one
41
+ * assertion; a lone assertion stays bare.
42
+ * - When ALL assertions verify the final action (the common shape — every
43
+ * Then after the last When), no headers appear and the caller renders
44
+ * the plain 1..M checklist exactly as before the anchor feature.
45
+ * - Pure-assertion scenarios (actionCount 0) and manual-comment entries
46
+ * (no anchor) never produce headers; unanchored entries trail as their
47
+ * own headerless group.
48
+ *
49
+ * `grouped` tells the caller headers were emitted, so it can force-number
50
+ * the Steps cell even when it has a single line (headers reference step
51
+ * numbers QA must be able to see) and must join the lines verbatim instead
52
+ * of applying checklist numbering.
53
+ *
54
+ * Anchors are computed against the merger's action list; formatted gherkin
55
+ * lines are never empty (keepActor/stripActor on non-empty text), so the
56
+ * rendered numbering cannot drift from the anchor indices.
67
57
  */
68
- export function cleanStepLine(raw: string): string {
69
- let s = raw.trim();
70
-
71
- // Strip leading "User " actor
72
- s = s.replace(/^User\s+/i, '');
73
-
74
- // Strip Gherkin-style element decoration "[Ref] type" → "'Ref' type"
75
- // e.g., "click [Login] button" → "click 'Login' button"
76
- s = s.replace(/\[([^\]]+)\]/g, "'$1'");
77
-
78
- // Collapse multiple spaces
79
- s = s.replace(/\s+/g, ' ');
80
-
81
- // For .spec.ts comments that start with "Assert ..." keep as-is (they read naturally)
82
- // For action comments like "Click X", "Fill X with Y" keep as-is.
83
-
84
- // Capitalize first letter
85
- if (s.length > 0) s = s.charAt(0).toUpperCase() + s.slice(1);
58
+ export function groupExpectedByAnchor(
59
+ entries: Array<{ text: string; anchorStep?: number }>,
60
+ actionCount: number
61
+ ): { lines: string[]; grouped: boolean } {
62
+ const anchors = entries
63
+ .filter((e) => e.anchorStep !== undefined)
64
+ .map((e) => e.anchorStep as number);
65
+ const distinct = new Set(anchors);
66
+ const grouped =
67
+ distinct.size > 1 || (distinct.size === 1 && anchors[0] !== actionCount);
68
+ if (!grouped) return { lines: entries.map((e) => e.text), grouped };
86
69
 
87
- return s;
70
+ const lines: string[] = [];
71
+ let i = 0;
72
+ while (i < entries.length) {
73
+ const anchor = entries[i].anchorStep;
74
+ const group: string[] = [];
75
+ while (i < entries.length && entries[i].anchorStep === anchor) {
76
+ group.push(entries[i].text);
77
+ i++;
78
+ }
79
+ if (anchor !== undefined) {
80
+ lines.push(anchor === 0 ? '— before step 1' : `— after step ${anchor}`);
81
+ }
82
+ if (group.length === 1) lines.push(group[0]);
83
+ else group.forEach((t, k) => lines.push(`${k + 1}. ${t}`));
84
+ }
85
+ return { lines, grouped };
88
86
  }
89
87
 
90
88
  /**
91
- * Pre-condition — login/auth context only.
89
+ * Pre-condition — login/auth context + feature Background steps.
92
90
  *
93
91
  * `@auth:user` → `Logged in as user`
94
92
  * `@no-auth` → `Not authenticated`
95
- * (no auth tag) → empty string
93
+ * (no auth tag) → no auth line
96
94
  *
97
- * Background and Given steps are no longer echoed here — they're rendered as
98
- * the first entries in the Steps list so the chain of actions reads as one
99
- * uninterrupted sequence.
95
+ * `backgroundLines` (formatted feature Background steps) follow the auth
96
+ * line, one per line. Background belongs here rather than in Steps: inlining
97
+ * it shifted the Steps numbering against the .feature scenario, forcing QA
98
+ * to re-map step numbers between Gherkin and the exported sheet.
100
99
  */
101
- export function formatPrecondition(authRole: string | null, _givenLines: string[] = []): string {
102
- if (authRole === 'no-auth') return 'Not authenticated';
103
- if (authRole) return `Logged in as ${authRole}`;
104
- return '';
100
+ export function formatPrecondition(authRole: string | null, backgroundLines: string[] = []): string {
101
+ const lines: string[] = [];
102
+ if (authRole === 'no-auth') lines.push('Not authenticated');
103
+ else if (authRole) lines.push(`Logged in as ${authRole}`);
104
+ lines.push(...backgroundLines.filter((l) => l.length > 0));
105
+ return lines.join('\n');
105
106
  }
@@ -141,10 +141,43 @@ function truncate(s: string, max: number): string {
141
141
  * test-data map (or resolving to empty) is left as the literal `{{var}}` — never blanked.
142
142
  */
143
143
  export function substituteTestDataVars(text: string, testData: Record<string, string>): string {
144
- return text.replace(/\{\{\s*([\w.]+)\s*\}\}/g, (m, key: string) => {
144
+ const out = text.replace(/\{\{\s*([\w.]+)\s*\}\}/g, (m, key: string) => {
145
145
  const v = testData[key];
146
146
  return v === undefined || v === null || v === '' ? m : String(v);
147
147
  });
148
+ // A substituted value may itself carry a dynamic token ("Award-{{$timestamp}}").
149
+ // String.replace never re-scans its own replacement, so name them afterwards.
150
+ return renderDynamicTokens(out);
151
+ }
152
+
153
+ /**
154
+ * Give the documented dynamic vars (`{{$timestamp}}`, `{{$uuid}}`,
155
+ * `{{$random:min:max}}`, `{{$date}}`, `{{$datetime}}`) a form a tester can act on.
156
+ *
157
+ * These are resolved by TestDataLoader at execution time, never from
158
+ * test-data.yaml — so at export time they have no static value, exactly like a
159
+ * value the run captures (`remember … as {{v}}` → `the captured v`). Left raw,
160
+ * the token ships into the deliverable and the matrix's Gate D rejects the
161
+ * whole export. Named, the tester reads what to type: any value of that shape,
162
+ * uniqueness being the whole point.
163
+ *
164
+ * An UNKNOWN `{{$foo}}` stays literal on purpose — the runtime resolver also
165
+ * leaves it raw, so it is an authoring typo and must keep failing Gate D.
166
+ */
167
+ export function renderDynamicTokens(text: string): string {
168
+ return text.replace(/\{\{\s*\$(\w+)(?::([^}]*))?\s*\}\}/g, (m, name: string, args?: string) => {
169
+ switch (name) {
170
+ case 'timestamp': return '<unique timestamp>';
171
+ case 'uuid': return '<unique id>';
172
+ case 'random': {
173
+ const [min, max] = (args || '1:9999').split(':');
174
+ return `<random ${min}-${max}>`;
175
+ }
176
+ case 'date': return "<today's date>";
177
+ case 'datetime': return '<current date-time>';
178
+ default: return m;
179
+ }
180
+ });
148
181
  }
149
182
 
150
183
  /**
@@ -59,11 +59,8 @@ export interface FeatureMetadata {
59
59
  featureName: string; // e.g., "Create Kudo Modal"
60
60
  featurePath?: string; // e.g., "/kudos"
61
61
  featureTags: string[]; // Feature-level tags
62
- /** Background block classified by bucket. Shared across all runnable
63
- * scenarios — the merger is responsible for prepending these. */
64
- backgroundGivenSteps: string[];
65
- backgroundWhenSteps: string[];
66
- backgroundThenSteps: string[];
62
+ /** Background block in chronological order. Shared across all runnable
63
+ * scenarios — the merger surfaces it as `resolvedBackgroundSteps`. */
67
64
  backgroundOrderedSteps: OrderedStep[];
68
65
  scenarios: ScenarioMetadata[];
69
66
  }
@@ -71,6 +68,13 @@ export interface FeatureMetadata {
71
68
  export interface OrderedStep {
72
69
  text: string;
73
70
  bucket: 'given' | 'when' | 'then';
71
+ /**
72
+ * For then-bucket entries in `MergedScenario.resolvedExpected`: 1-based index
73
+ * of the last action line (in the rendered Steps column) this assertion
74
+ * verifies; 0 when the assertion precedes any action. Set by
75
+ * mergeFeatureAndSpec — absent on action steps and manual-comment entries.
76
+ */
77
+ anchorStep?: number;
74
78
  }
75
79
 
76
80
  export interface ScenarioMetadata {
@@ -79,9 +83,6 @@ export interface ScenarioMetadata {
79
83
  stepsName?: string; // @steps:<name>
80
84
  extendsName?: string; // @extend:<name>
81
85
  referencedVars: string[]; // Variables {{var}} used in this scenario
82
- rawGivenSteps: string[]; // Raw Given/And-after-Given step text
83
- rawWhenSteps: string[]; // Raw When/And-after-When step text
84
- rawThenSteps: string[]; // Raw Then/And-after-Then step text
85
86
  /**
86
87
  * Steps in original chronological order with each step labelled by its
87
88
  * effective bucket (And/But inherits from the prior explicit keyword).
@@ -67,7 +67,7 @@ export interface TestGeneratorAdapter {
67
67
  // Template rendering methods
68
68
  renderTestFile(data: TestFileData): string;
69
69
  renderScenario(data: ScenarioData): string;
70
- renderImports(options?: { runtimeData?: boolean; basePath?: string; isParallel?: boolean; needsCleanupImport?: boolean ; needsDb?: boolean; needsApi?: boolean }): string;
70
+ renderImports(options?: { runtimeData?: boolean; basePath?: string; isParallel?: boolean; needsCleanupImport?: boolean ; needsDb?: boolean; needsApi?: boolean; needsMock?: boolean; urlAssertImports?: string }): string;
71
71
  renderBeforeEach(data: { steps: Array<{ comment?: string; code: string }> }): string;
72
72
  renderBeforeAll(data: { steps: Array<{ comment?: string; code: string }> }): string;
73
73
  renderAfterEach(data: { steps: Array<{ comment?: string; code: string }> }): string;
@@ -31,7 +31,7 @@ export class AppiumAdapter implements TestGeneratorAdapter {
31
31
  return this.templateEngine.renderScenario(data);
32
32
  }
33
33
 
34
- renderImports(options?: { runtimeData?: boolean; basePath?: string; isParallel?: boolean; needsCleanupImport?: boolean; needsDb?: boolean; needsApi?: boolean }): string {
34
+ renderImports(options?: { runtimeData?: boolean; basePath?: string; isParallel?: boolean; needsCleanupImport?: boolean; needsDb?: boolean; needsApi?: boolean; needsMock?: boolean; urlAssertImports?: string }): string {
35
35
  return this.templateEngine.renderImports(options);
36
36
  }
37
37
 
@@ -1,2 +1,4 @@
1
1
  {{!-- Global key event (no target). On Android, Escape maps to the system Back button. --}}
2
- {{#if (eq key 'Escape')}}await driver.back();{{else}}await driver.keys('{{key}}');{{/if}}
2
+ {{#if repeat}}for (let i = 0; i < {{repeat}}; i++) {
3
+ {{#if (eq key 'Escape')}}await driver.back();{{else}}await driver.keys('{{key}}');{{/if}}
4
+ }{{else}}{{#if (eq key 'Escape')}}await driver.back();{{else}}await driver.keys('{{key}}');{{/if}}{{/if}}
@@ -26,7 +26,7 @@ export class PlaywrightAdapter implements TestGeneratorAdapter {
26
26
  return this.templateEngine.renderScenario(data);
27
27
  }
28
28
 
29
- renderImports(options?: { runtimeData?: boolean; basePath?: string; isParallel?: boolean; needsCleanupImport?: boolean ; needsDb?: boolean; needsApi?: boolean }): string {
29
+ renderImports(options?: { runtimeData?: boolean; basePath?: string; isParallel?: boolean; needsCleanupImport?: boolean ; needsDb?: boolean; needsApi?: boolean; needsMock?: boolean; urlAssertImports?: string }): string {
30
30
  return this.templateEngine.renderImports(options);
31
31
  }
32
32
 
@@ -9,6 +9,12 @@ import { db } from '{{basePath}}/db';
9
9
  {{#if needsApi}}
10
10
  import { api } from '{{basePath}}/api';
11
11
  {{/if}}
12
+ {{#if needsMock}}
13
+ import { mock } from '{{basePath}}/mock';
14
+ {{/if}}
15
+ {{#if urlAssertImports}}
16
+ import { {{urlAssertImports}} } from '{{basePath}}/url-assert';
17
+ {{/if}}
12
18
 
13
19
  // This file is auto-generated from Gherkin feature files
14
20
  // DO NOT EDIT MANUALLY - changes will be overwritten
@@ -1 +1,3 @@
1
- await page.keyboard.press('{{key}}');
1
+ {{#if repeat}}for (let i = 0; i < {{repeat}}; i++) {
2
+ await page.keyboard.press('{{key}}');
3
+ }{{else}}await page.keyboard.press('{{key}}');{{/if}}
@@ -1 +1 @@
1
- await expect(page).toHaveURL(/{{pathRegex}}/);
1
+ {{> url-assertion}}
@@ -0,0 +1,4 @@
1
+ {
2
+ const ok = await page.evaluate(({ k, arg }) => {{{checkExpr}}}, { k: {{{keyExpr}}}, arg: {{{argExpr}}} });
3
+ expect(ok, {{{messageExpr}}}).toBe(true);
4
+ }
@@ -0,0 +1,30 @@
1
+ {
2
+ const __stops_{{stepCounter}} = [
3
+ {{#each cells}}
4
+ { label: '{{escapeQuotes label}}', loc: {{> locator}} },
5
+ {{/each}}
6
+ ];
7
+ const __focusReport_{{stepCounter}} = () => page.evaluate(() => {
8
+ let el = document.activeElement;
9
+ while (el && el.shadowRoot && el.shadowRoot.activeElement) el = el.shadowRoot.activeElement;
10
+ if (!el || el === document.body) return 'document.body (focus left the page content)';
11
+ const role = el.getAttribute('role') ? `[role=${el.getAttribute('role')}]` : '';
12
+ const name = el.getAttribute('aria-label') || el.getAttribute('name') || el.id || '';
13
+ return `${el.tagName.toLowerCase()}${role}${name ? ` (${name})` : ''}`;
14
+ });
15
+ const __firstLine_{{stepCounter}} = (e: unknown) => (e instanceof Error ? ` (${e.message.split('\n')[0]})` : '');
16
+ await __stops_{{stepCounter}}[0].loc.focus();
17
+ try {
18
+ await expect(__stops_{{stepCounter}}[0].loc, `tab order origin: expected [${__stops_{{stepCounter}}[0].label}] to hold focus`).toBeFocused({ timeout: 2000 });
19
+ } catch (err) {
20
+ throw new Error(`Tab order origin not focused: [${__stops_{{stepCounter}}[0].label}] did not take focus (is it focusable?), actual focus: ${await __focusReport_{{stepCounter}}()}${__firstLine_{{stepCounter}}(err)}`);
21
+ }
22
+ for (let i = 1; i < __stops_{{stepCounter}}.length; i++) {
23
+ await page.keyboard.press('Tab');
24
+ try {
25
+ await expect(__stops_{{stepCounter}}[i].loc, `tab order position ${i + 1}: expected [${__stops_{{stepCounter}}[i].label}]`).toBeFocused({ timeout: 2000 });
26
+ } catch (err) {
27
+ throw new Error(`Tab order mismatch at position ${i + 1}: expected [${__stops_{{stepCounter}}[i].label}], actual focus: ${await __focusReport_{{stepCounter}}()}${__firstLine_{{stepCounter}}(err)}`);
28
+ }
29
+ }
30
+ }
@@ -1,2 +1,6 @@
1
+ {{#if query}}
2
+ await page.goto(joinPath('{{#if baseURL}}{{baseURL}}{{/if}}', '{{path}}', '{{escapeQuotes query}}'), { waitUntil: 'load' });
3
+ {{else}}
1
4
  await page.goto('{{#if baseURL}}{{baseURL}}{{/if}}{{path}}', { waitUntil: 'load' });
5
+ {{/if}}
2
6
  await page.waitForLoadState('domcontentloaded');
@@ -1,4 +1 @@
1
- await expect(page).toHaveURL(/{{escapeRegex path}}/);
2
- {{#if query}}
3
- expect(page.url()).toContain('{{escapeQuotes query}}');
4
- {{/if}}
1
+ {{> url-assertion}}
@@ -1 +1 @@
1
- {{#if dialogRoleName}}page.getByRole('dialog', { name: '{{escapeQuotes dialogRoleName}}' }){{else if dialogFilterText}}page.getByRole('dialog').filter({ hasText: '{{escapeQuotes dialogFilterText}}' }){{else}}page.getByRole('dialog'){{/if}}
1
+ {{#if dialogRootExpr}}{{{dialogRootExpr}}}{{#if dialogFilterText}}.filter({ hasText: '{{escapeQuotes dialogFilterText}}' }){{/if}}{{else if dialogRoleName}}page.getByRole('dialog', { name: '{{escapeQuotes dialogRoleName}}' }){{else if dialogFilterText}}page.getByRole('dialog').filter({ hasText: '{{escapeQuotes dialogFilterText}}' }){{else}}page.getByRole('dialog'){{/if}}
@@ -0,0 +1,15 @@
1
+ {{!-- The ONE shape for every Then-side URL assertion (page-assertion + route-assertion).
2
+ Both used to carry their own copy and drifted apart: `see [X] page` wildcarded a dynamic
3
+ `:id` segment but silently dropped `with {{v}}`, while `is on [X] page` asserted the query
4
+ but escaped `:id` literally into a regex that can never match. One partial, one behaviour.
5
+
6
+ The predicate itself lives in specs/url-assert.ts (template: specs-url-assert.ts) — read it
7
+ for the semantics. It is a runtime helper rather than an inline lambda so that correcting
8
+ what "the URL matches" MEANS is a runtime patch, not a regenerate-the-world breaking change
9
+ for every suite in the field.
10
+
11
+ Only `pathRegex` is the generator's business: it alone knows the page selector's value and
12
+ its `:id` wildcards, and it is pre-escaped by pathToRegexSource. `\/?$` normalises a
13
+ trailing slash so `/vi/search/` does not read as a different page. The declared value stays
14
+ a plain string literal so the runtime-data marker pass can rewrite it to testData.get(…). --}}
15
+ await expect(page).toHaveURL(urlMatches({ pathname: /^{{pathRegex}}\/?$/{{#if query}}, dataRef: '{{escapeQuotes dataRef}}', declared: '{{escapeQuotes query}}'{{/if}} }));