@sun-asterisk/sungen 3.2.29 → 3.2.30

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 (335) hide show
  1. package/dist/cli/commands/delivery.d.ts.map +1 -1
  2. package/dist/cli/commands/delivery.js +2 -1
  3. package/dist/cli/commands/delivery.js.map +1 -1
  4. package/dist/cli/commands/generate.d.ts.map +1 -1
  5. package/dist/cli/commands/generate.js +6 -2
  6. package/dist/cli/commands/generate.js.map +1 -1
  7. package/dist/cli/commands/inspect.js +1 -1
  8. package/dist/cli/commands/inspect.js.map +1 -1
  9. package/dist/cli/commands/results-merge.d.ts.map +1 -1
  10. package/dist/cli/commands/results-merge.js +9 -2
  11. package/dist/cli/commands/results-merge.js.map +1 -1
  12. package/dist/cli/commands/results-sanitize.d.ts +24 -0
  13. package/dist/cli/commands/results-sanitize.d.ts.map +1 -0
  14. package/dist/cli/commands/results-sanitize.js +126 -0
  15. package/dist/cli/commands/results-sanitize.js.map +1 -0
  16. package/dist/cli/commands/script-check.d.ts.map +1 -1
  17. package/dist/cli/commands/script-check.js +3 -1
  18. package/dist/cli/commands/script-check.js.map +1 -1
  19. package/dist/cli/commands/update.d.ts.map +1 -1
  20. package/dist/cli/commands/update.js +3 -0
  21. package/dist/cli/commands/update.js.map +1 -1
  22. package/dist/exporters/api-testcase-formatter.d.ts +3 -6
  23. package/dist/exporters/api-testcase-formatter.d.ts.map +1 -1
  24. package/dist/exporters/api-testcase-formatter.js +7 -10
  25. package/dist/exporters/api-testcase-formatter.js.map +1 -1
  26. package/dist/exporters/matrix/build.d.ts.map +1 -1
  27. package/dist/exporters/matrix/build.js +4 -2
  28. package/dist/exporters/matrix/build.js.map +1 -1
  29. package/dist/exporters/matrix/render-coverage-sheet.d.ts.map +1 -1
  30. package/dist/exporters/matrix/render-coverage-sheet.js +3 -1
  31. package/dist/exporters/matrix/render-coverage-sheet.js.map +1 -1
  32. package/dist/exporters/matrix/render-csv.d.ts.map +1 -1
  33. package/dist/exporters/matrix/render-csv.js +3 -1
  34. package/dist/exporters/matrix/render-csv.js.map +1 -1
  35. package/dist/exporters/matrix/render-overview-sheet.d.ts.map +1 -1
  36. package/dist/exporters/matrix/render-overview-sheet.js +5 -9
  37. package/dist/exporters/matrix/render-overview-sheet.js.map +1 -1
  38. package/dist/exporters/matrix/result-totals.d.ts +23 -0
  39. package/dist/exporters/matrix/result-totals.d.ts.map +1 -0
  40. package/dist/exporters/matrix/result-totals.js +21 -0
  41. package/dist/exporters/matrix/result-totals.js.map +1 -0
  42. package/dist/exporters/matrix/testcase-summary-band.d.ts.map +1 -1
  43. package/dist/exporters/matrix/testcase-summary-band.js +2 -6
  44. package/dist/exporters/matrix/testcase-summary-band.js.map +1 -1
  45. package/dist/exporters/qa-env-secret-denylist.js +2 -2
  46. package/dist/exporters/qa-env-secret-denylist.js.map +1 -1
  47. package/dist/exporters/report-path-sanitizer.d.ts +38 -0
  48. package/dist/exporters/report-path-sanitizer.d.ts.map +1 -0
  49. package/dist/exporters/report-path-sanitizer.js +137 -0
  50. package/dist/exporters/report-path-sanitizer.js.map +1 -0
  51. package/dist/exporters/result-variants.d.ts.map +1 -1
  52. package/dist/exporters/result-variants.js +2 -1
  53. package/dist/exporters/result-variants.js.map +1 -1
  54. package/dist/exporters/sensitive-key.d.ts +31 -0
  55. package/dist/exporters/sensitive-key.d.ts.map +1 -0
  56. package/dist/exporters/sensitive-key.js +50 -0
  57. package/dist/exporters/sensitive-key.js.map +1 -0
  58. package/dist/exporters/test-data-resolver.d.ts +4 -1
  59. package/dist/exporters/test-data-resolver.d.ts.map +1 -1
  60. package/dist/exporters/test-data-resolver.js +12 -2
  61. package/dist/exporters/test-data-resolver.js.map +1 -1
  62. package/dist/generators/gherkin-parser/index.d.ts +3 -0
  63. package/dist/generators/gherkin-parser/index.d.ts.map +1 -1
  64. package/dist/generators/gherkin-parser/index.js +17 -9
  65. package/dist/generators/gherkin-parser/index.js.map +1 -1
  66. package/dist/generators/gherkin-parser/parent-scope-clause.d.ts +54 -0
  67. package/dist/generators/gherkin-parser/parent-scope-clause.d.ts.map +1 -0
  68. package/dist/generators/gherkin-parser/parent-scope-clause.js +109 -0
  69. package/dist/generators/gherkin-parser/parent-scope-clause.js.map +1 -0
  70. package/dist/generators/test-generator/adapters/adapter-interface.d.ts +23 -18
  71. package/dist/generators/test-generator/adapters/adapter-interface.d.ts.map +1 -1
  72. package/dist/generators/test-generator/adapters/appium/appium-adapter.d.ts +4 -19
  73. package/dist/generators/test-generator/adapters/appium/appium-adapter.d.ts.map +1 -1
  74. package/dist/generators/test-generator/adapters/appium/appium-adapter.js.map +1 -1
  75. package/dist/generators/test-generator/adapters/appium/templates/after-all.hbs +18 -0
  76. package/dist/generators/test-generator/adapters/appium/templates/after-each.hbs +18 -0
  77. package/dist/generators/test-generator/adapters/appium/templates/before-all.hbs +18 -0
  78. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/click-at-position-action.hbs +1 -0
  79. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/click-element-with-text.hbs +1 -1
  80. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/double-click-action.hbs +7 -0
  81. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/frame-enter-action.hbs +3 -6
  82. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/table-paginate-action.hbs +3 -0
  83. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/visible-assertion.hbs +1 -1
  84. package/dist/generators/test-generator/adapters/appium/templates/steps/gestures/long-press-action.hbs +4 -0
  85. package/dist/generators/test-generator/adapters/appium/templates/steps/gestures/pinch-zoom-action.hbs +4 -0
  86. package/dist/generators/test-generator/adapters/appium/templates/steps/partials/locator.hbs +9 -1
  87. package/dist/generators/test-generator/adapters/appium/templates/test-file.hbs +54 -23
  88. package/dist/generators/test-generator/adapters/index.d.ts +1 -1
  89. package/dist/generators/test-generator/adapters/index.d.ts.map +1 -1
  90. package/dist/generators/test-generator/adapters/playwright/playwright-adapter.d.ts +4 -19
  91. package/dist/generators/test-generator/adapters/playwright/playwright-adapter.d.ts.map +1 -1
  92. package/dist/generators/test-generator/adapters/playwright/playwright-adapter.js.map +1 -1
  93. package/dist/generators/test-generator/adapters/playwright/templates/after-all.hbs +19 -1
  94. package/dist/generators/test-generator/adapters/playwright/templates/after-each.hbs +18 -0
  95. package/dist/generators/test-generator/adapters/playwright/templates/before-all.hbs +19 -1
  96. package/dist/generators/test-generator/adapters/playwright/templates/imports.hbs +3 -0
  97. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/check-action.hbs +6 -1
  98. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/click-at-position-action.hbs +23 -0
  99. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/expand-action.hbs +8 -8
  100. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/fill-editor-action.hbs +7 -2
  101. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/table-action-in-row-nth.hbs +3 -1
  102. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/table-action-in-row.hbs +3 -1
  103. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/table-paginate-action.hbs +5 -0
  104. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/uncheck-action.hbs +3 -1
  105. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/unknown-element-action.hbs +1 -1
  106. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/wait-for-page.hbs +18 -1
  107. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/all-contain-assertion.hbs +32 -4
  108. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/all-contain-element.hbs +5 -2
  109. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/checked-assertion.hbs +5 -1
  110. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/column-cell-assertion.hbs +13 -2
  111. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/contain-text-assertion.hbs +1 -1
  112. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/disabled-with-role-variable-assertion.hbs +2 -2
  113. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/disabled-with-variable-assertion.hbs +1 -1
  114. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/have-text-assertion.hbs +1 -1
  115. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/hidden-with-role-variable-assertion.hbs +2 -2
  116. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/hidden-with-variable-assertion.hbs +1 -1
  117. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/not-checked-assertion.hbs +2 -1
  118. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/selected-assertion.hbs +14 -3
  119. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/state-with-filter-assertion.hbs +13 -2
  120. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/visible-with-role-variable-assertion.hbs +3 -3
  121. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/visible-with-value-assertion.hbs +1 -1
  122. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/visible-with-variable-assertion.hbs +1 -1
  123. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/checked-opts.hbs +8 -0
  124. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator-nth.hbs +1 -1
  125. package/dist/generators/test-generator/adapters/playwright/templates/test-file.hbs +10 -8
  126. package/dist/generators/test-generator/code-generator.d.ts +37 -0
  127. package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
  128. package/dist/generators/test-generator/code-generator.js +160 -39
  129. package/dist/generators/test-generator/code-generator.js.map +1 -1
  130. package/dist/generators/test-generator/diagnostics.d.ts +79 -5
  131. package/dist/generators/test-generator/diagnostics.d.ts.map +1 -1
  132. package/dist/generators/test-generator/diagnostics.js +78 -4
  133. package/dist/generators/test-generator/diagnostics.js.map +1 -1
  134. package/dist/generators/test-generator/patterns/index.d.ts +24 -5
  135. package/dist/generators/test-generator/patterns/index.d.ts.map +1 -1
  136. package/dist/generators/test-generator/patterns/index.js +92 -14
  137. package/dist/generators/test-generator/patterns/index.js.map +1 -1
  138. package/dist/generators/test-generator/patterns/with-parent-scope.d.ts +18 -0
  139. package/dist/generators/test-generator/patterns/with-parent-scope.d.ts.map +1 -0
  140. package/dist/generators/test-generator/patterns/with-parent-scope.js +40 -0
  141. package/dist/generators/test-generator/patterns/with-parent-scope.js.map +1 -0
  142. package/dist/generators/test-generator/step-mapper.d.ts +63 -9
  143. package/dist/generators/test-generator/step-mapper.d.ts.map +1 -1
  144. package/dist/generators/test-generator/step-mapper.js +166 -28
  145. package/dist/generators/test-generator/step-mapper.js.map +1 -1
  146. package/dist/generators/test-generator/template-engine.d.ts +4 -19
  147. package/dist/generators/test-generator/template-engine.d.ts.map +1 -1
  148. package/dist/generators/test-generator/template-engine.js +17 -12
  149. package/dist/generators/test-generator/template-engine.js.map +1 -1
  150. package/dist/generators/test-generator/utils/data-resolver.d.ts +7 -0
  151. package/dist/generators/test-generator/utils/data-resolver.d.ts.map +1 -1
  152. package/dist/generators/test-generator/utils/data-resolver.js +19 -2
  153. package/dist/generators/test-generator/utils/data-resolver.js.map +1 -1
  154. package/dist/generators/test-generator/utils/dialog-scope-identity.d.ts +41 -0
  155. package/dist/generators/test-generator/utils/dialog-scope-identity.d.ts.map +1 -0
  156. package/dist/generators/test-generator/utils/dialog-scope-identity.js +61 -0
  157. package/dist/generators/test-generator/utils/dialog-scope-identity.js.map +1 -0
  158. package/dist/generators/test-generator/utils/escape-js-string.d.ts +17 -0
  159. package/dist/generators/test-generator/utils/escape-js-string.d.ts.map +1 -0
  160. package/dist/generators/test-generator/utils/escape-js-string.js +29 -0
  161. package/dist/generators/test-generator/utils/escape-js-string.js.map +1 -0
  162. package/dist/generators/test-generator/utils/nearest-match.d.ts +24 -0
  163. package/dist/generators/test-generator/utils/nearest-match.d.ts.map +1 -0
  164. package/dist/generators/test-generator/utils/nearest-match.js +56 -0
  165. package/dist/generators/test-generator/utils/nearest-match.js.map +1 -0
  166. package/dist/generators/test-generator/utils/selector-entry-fields.d.ts +58 -0
  167. package/dist/generators/test-generator/utils/selector-entry-fields.d.ts.map +1 -0
  168. package/dist/generators/test-generator/utils/selector-entry-fields.js +119 -0
  169. package/dist/generators/test-generator/utils/selector-entry-fields.js.map +1 -0
  170. package/dist/generators/test-generator/utils/selector-entry-walk.d.ts +25 -0
  171. package/dist/generators/test-generator/utils/selector-entry-walk.d.ts.map +1 -0
  172. package/dist/generators/test-generator/utils/selector-entry-walk.js +54 -0
  173. package/dist/generators/test-generator/utils/selector-entry-walk.js.map +1 -0
  174. package/dist/generators/test-generator/utils/selector-nth-validation.d.ts +59 -0
  175. package/dist/generators/test-generator/utils/selector-nth-validation.d.ts.map +1 -0
  176. package/dist/generators/test-generator/utils/selector-nth-validation.js +91 -0
  177. package/dist/generators/test-generator/utils/selector-nth-validation.js.map +1 -0
  178. package/dist/generators/test-generator/utils/selector-resolver.d.ts +30 -1
  179. package/dist/generators/test-generator/utils/selector-resolver.d.ts.map +1 -1
  180. package/dist/generators/test-generator/utils/selector-resolver.js +195 -15
  181. package/dist/generators/test-generator/utils/selector-resolver.js.map +1 -1
  182. package/dist/generators/test-generator/utils/selector-type-validation.d.ts +51 -0
  183. package/dist/generators/test-generator/utils/selector-type-validation.d.ts.map +1 -0
  184. package/dist/generators/test-generator/utils/selector-type-validation.js +77 -0
  185. package/dist/generators/test-generator/utils/selector-type-validation.js.map +1 -0
  186. package/dist/harness/data-driven-lint.d.ts.map +1 -1
  187. package/dist/harness/data-driven-lint.js +217 -33
  188. package/dist/harness/data-driven-lint.js.map +1 -1
  189. package/dist/harness/script-check.d.ts +2 -2
  190. package/dist/harness/script-check.d.ts.map +1 -1
  191. package/dist/harness/script-check.js +186 -25
  192. package/dist/harness/script-check.js.map +1 -1
  193. package/dist/index.d.ts +2 -0
  194. package/dist/index.d.ts.map +1 -1
  195. package/dist/index.js +10 -1
  196. package/dist/index.js.map +1 -1
  197. package/dist/orchestrator/project-initializer.d.ts +6 -0
  198. package/dist/orchestrator/project-initializer.d.ts.map +1 -1
  199. package/dist/orchestrator/project-initializer.js +16 -0
  200. package/dist/orchestrator/project-initializer.js.map +1 -1
  201. package/dist/orchestrator/report-sanitize-reporter-install.d.ts +24 -0
  202. package/dist/orchestrator/report-sanitize-reporter-install.d.ts.map +1 -0
  203. package/dist/orchestrator/report-sanitize-reporter-install.js +161 -0
  204. package/dist/orchestrator/report-sanitize-reporter-install.js.map +1 -0
  205. package/dist/orchestrator/templates/ai-src/commands/run-test.md +9 -1
  206. package/dist/orchestrator/templates/ai-src/skills/sungen-api-coverage-model/SKILL.md +10 -0
  207. package/dist/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +14 -2
  208. package/dist/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +108 -8
  209. package/dist/orchestrator/templates/ai-src/skills/sungen-selector-fix/SKILL.md +1 -0
  210. package/dist/orchestrator/templates/ai-src/skills/sungen-selector-keys/SKILL.md +9 -2
  211. package/dist/orchestrator/templates/ai-src/skills/sungen-tc-review/SKILL.md +4 -4
  212. package/dist/orchestrator/templates/playwright.config.ts +7 -0
  213. package/dist/orchestrator/templates/specs-checked-state.d.ts +83 -0
  214. package/dist/orchestrator/templates/specs-checked-state.d.ts.map +1 -0
  215. package/dist/orchestrator/templates/specs-checked-state.js +261 -0
  216. package/dist/orchestrator/templates/specs-checked-state.js.map +1 -0
  217. package/dist/orchestrator/templates/specs-checked-state.ts +324 -0
  218. package/dist/orchestrator/templates/specs-report-path-sanitize-reporter.d.ts +36 -0
  219. package/dist/orchestrator/templates/specs-report-path-sanitize-reporter.d.ts.map +1 -0
  220. package/dist/orchestrator/templates/specs-report-path-sanitize-reporter.js +155 -0
  221. package/dist/orchestrator/templates/specs-report-path-sanitize-reporter.js.map +1 -0
  222. package/dist/orchestrator/templates/specs-report-path-sanitize-reporter.ts +142 -0
  223. package/dist/orchestrator/templates/specs-url-assert.d.ts +50 -25
  224. package/dist/orchestrator/templates/specs-url-assert.d.ts.map +1 -1
  225. package/dist/orchestrator/templates/specs-url-assert.js +65 -24
  226. package/dist/orchestrator/templates/specs-url-assert.js.map +1 -1
  227. package/dist/orchestrator/templates/specs-url-assert.ts +101 -28
  228. package/dist/utils/selector-types.d.ts +43 -1
  229. package/dist/utils/selector-types.d.ts.map +1 -1
  230. package/dist/utils/selector-types.js +51 -7
  231. package/dist/utils/selector-types.js.map +1 -1
  232. package/package.json +3 -3
  233. package/src/cli/commands/delivery.ts +2 -1
  234. package/src/cli/commands/generate.ts +5 -2
  235. package/src/cli/commands/inspect.ts +1 -1
  236. package/src/cli/commands/results-merge.ts +9 -2
  237. package/src/cli/commands/results-sanitize.ts +98 -0
  238. package/src/cli/commands/script-check.ts +3 -1
  239. package/src/cli/commands/update.ts +3 -0
  240. package/src/exporters/api-testcase-formatter.ts +5 -7
  241. package/src/exporters/matrix/build.ts +4 -2
  242. package/src/exporters/matrix/render-coverage-sheet.ts +3 -1
  243. package/src/exporters/matrix/render-csv.ts +3 -1
  244. package/src/exporters/matrix/render-overview-sheet.ts +5 -10
  245. package/src/exporters/matrix/result-totals.ts +37 -0
  246. package/src/exporters/matrix/testcase-summary-band.ts +2 -7
  247. package/src/exporters/qa-env-secret-denylist.ts +1 -1
  248. package/src/exporters/report-path-sanitizer.ts +144 -0
  249. package/src/exporters/result-variants.ts +2 -1
  250. package/src/exporters/sensitive-key.ts +48 -0
  251. package/src/exporters/test-data-resolver.ts +12 -2
  252. package/src/generators/gherkin-parser/index.ts +21 -10
  253. package/src/generators/gherkin-parser/parent-scope-clause.ts +130 -0
  254. package/src/generators/test-generator/adapters/adapter-interface.ts +25 -3
  255. package/src/generators/test-generator/adapters/appium/appium-adapter.ts +4 -4
  256. package/src/generators/test-generator/adapters/appium/templates/after-all.hbs +18 -0
  257. package/src/generators/test-generator/adapters/appium/templates/after-each.hbs +18 -0
  258. package/src/generators/test-generator/adapters/appium/templates/before-all.hbs +18 -0
  259. package/src/generators/test-generator/adapters/appium/templates/steps/actions/click-at-position-action.hbs +1 -0
  260. package/src/generators/test-generator/adapters/appium/templates/steps/actions/click-element-with-text.hbs +1 -1
  261. package/src/generators/test-generator/adapters/appium/templates/steps/actions/double-click-action.hbs +7 -0
  262. package/src/generators/test-generator/adapters/appium/templates/steps/actions/frame-enter-action.hbs +3 -6
  263. package/src/generators/test-generator/adapters/appium/templates/steps/actions/table-paginate-action.hbs +3 -0
  264. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/visible-assertion.hbs +1 -1
  265. package/src/generators/test-generator/adapters/appium/templates/steps/gestures/long-press-action.hbs +4 -0
  266. package/src/generators/test-generator/adapters/appium/templates/steps/gestures/pinch-zoom-action.hbs +4 -0
  267. package/src/generators/test-generator/adapters/appium/templates/steps/partials/locator.hbs +9 -1
  268. package/src/generators/test-generator/adapters/appium/templates/test-file.hbs +54 -23
  269. package/src/generators/test-generator/adapters/index.ts +1 -1
  270. package/src/generators/test-generator/adapters/playwright/playwright-adapter.ts +4 -4
  271. package/src/generators/test-generator/adapters/playwright/templates/after-all.hbs +19 -1
  272. package/src/generators/test-generator/adapters/playwright/templates/after-each.hbs +18 -0
  273. package/src/generators/test-generator/adapters/playwright/templates/before-all.hbs +19 -1
  274. package/src/generators/test-generator/adapters/playwright/templates/imports.hbs +3 -0
  275. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/check-action.hbs +6 -1
  276. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/click-at-position-action.hbs +23 -0
  277. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/expand-action.hbs +8 -8
  278. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/fill-editor-action.hbs +7 -2
  279. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/table-action-in-row-nth.hbs +3 -1
  280. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/table-action-in-row.hbs +3 -1
  281. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/table-paginate-action.hbs +5 -0
  282. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/uncheck-action.hbs +3 -1
  283. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/unknown-element-action.hbs +1 -1
  284. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/wait-for-page.hbs +18 -1
  285. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/all-contain-assertion.hbs +32 -4
  286. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/all-contain-element.hbs +5 -2
  287. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/checked-assertion.hbs +5 -1
  288. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/column-cell-assertion.hbs +13 -2
  289. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/contain-text-assertion.hbs +1 -1
  290. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/disabled-with-role-variable-assertion.hbs +2 -2
  291. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/disabled-with-variable-assertion.hbs +1 -1
  292. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/have-text-assertion.hbs +1 -1
  293. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/hidden-with-role-variable-assertion.hbs +2 -2
  294. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/hidden-with-variable-assertion.hbs +1 -1
  295. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/not-checked-assertion.hbs +2 -1
  296. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/selected-assertion.hbs +14 -3
  297. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/state-with-filter-assertion.hbs +13 -2
  298. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/visible-with-role-variable-assertion.hbs +3 -3
  299. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/visible-with-value-assertion.hbs +1 -1
  300. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/visible-with-variable-assertion.hbs +1 -1
  301. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/checked-opts.hbs +8 -0
  302. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator-nth.hbs +1 -1
  303. package/src/generators/test-generator/adapters/playwright/templates/test-file.hbs +10 -8
  304. package/src/generators/test-generator/code-generator.ts +176 -45
  305. package/src/generators/test-generator/diagnostics.ts +79 -5
  306. package/src/generators/test-generator/patterns/index.ts +97 -16
  307. package/src/generators/test-generator/patterns/with-parent-scope.ts +41 -0
  308. package/src/generators/test-generator/step-mapper.ts +170 -26
  309. package/src/generators/test-generator/template-engine.ts +21 -21
  310. package/src/generators/test-generator/utils/data-resolver.ts +23 -2
  311. package/src/generators/test-generator/utils/dialog-scope-identity.ts +71 -0
  312. package/src/generators/test-generator/utils/escape-js-string.ts +24 -0
  313. package/src/generators/test-generator/utils/nearest-match.ts +55 -0
  314. package/src/generators/test-generator/utils/selector-entry-fields.ts +131 -0
  315. package/src/generators/test-generator/utils/selector-entry-walk.ts +58 -0
  316. package/src/generators/test-generator/utils/selector-nth-validation.ts +99 -0
  317. package/src/generators/test-generator/utils/selector-resolver.ts +245 -16
  318. package/src/generators/test-generator/utils/selector-type-validation.ts +87 -0
  319. package/src/harness/data-driven-lint.ts +192 -32
  320. package/src/harness/script-check.ts +186 -21
  321. package/src/index.ts +7 -0
  322. package/src/orchestrator/project-initializer.ts +17 -0
  323. package/src/orchestrator/report-sanitize-reporter-install.ts +109 -0
  324. package/src/orchestrator/templates/ai-src/commands/run-test.md +9 -1
  325. package/src/orchestrator/templates/ai-src/skills/sungen-api-coverage-model/SKILL.md +10 -0
  326. package/src/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +14 -2
  327. package/src/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +108 -8
  328. package/src/orchestrator/templates/ai-src/skills/sungen-selector-fix/SKILL.md +1 -0
  329. package/src/orchestrator/templates/ai-src/skills/sungen-selector-keys/SKILL.md +9 -2
  330. package/src/orchestrator/templates/ai-src/skills/sungen-tc-review/SKILL.md +4 -4
  331. package/src/orchestrator/templates/playwright.config.ts +7 -0
  332. package/src/orchestrator/templates/specs-checked-state.ts +324 -0
  333. package/src/orchestrator/templates/specs-report-path-sanitize-reporter.ts +142 -0
  334. package/src/orchestrator/templates/specs-url-assert.ts +101 -28
  335. package/src/utils/selector-types.ts +54 -5
@@ -0,0 +1,324 @@
1
+ /* eslint-disable */
2
+ /**
3
+ * Sungen checked-state helper — auto-generated into specs/checked-state.ts. DO NOT EDIT.
4
+ *
5
+ * Playwright's `.check()` / `.uncheck()` accept ONLY native checkbox/radio inputs and
6
+ * `role=checkbox|radio|switch`. Anything else throws "Not a checkbox or radio button" (#675).
7
+ * A toggle BUTTON — `<button aria-pressed>`, the correct ARIA pattern for a pressed/unpressed
8
+ * control — is none of those, so a bare `.check()` could never drive one.
9
+ *
10
+ * Rewriting the Gherkin to `click` is NOT an equivalent workaround, and that is the whole reason
11
+ * this helper exists: `click` flips UNCONDITIONALLY, so a scenario that re-runs, or starts from an
12
+ * unknown state, drives the toggle the WRONG way. Idempotence — end up in the requested state
13
+ * regardless of where you started — is the entire meaning of `check` / `uncheck`.
14
+ *
15
+ * The mechanism is chosen from the ELEMENT at run time, not from the selector's declared role,
16
+ * because the resolved selector is very often a testid/CSS locator carrying no role at all — so
17
+ * codegen cannot know what it will hit.
18
+ *
19
+ * Deliberately depends on NOTHING but the `Locator` TYPE: no `expect`, no test-runner context. That
20
+ * keeps the branching testable against a fake locator without a browser (tests/runtime/
21
+ * checked-state.run.ts) — this file runs inside every user's suite, so its logic is worth pinning.
22
+ */
23
+ import type { Locator } from '@playwright/test';
24
+
25
+ /**
26
+ * Budget for EACH PHASE of a check/uncheck step — classification, the action, and the post-click
27
+ * verification each get their own (deliberately: see setCheckedState). So the worst case is a small
28
+ * multiple of this, not this. Raise it for a slow CI box with `SUNGEN_TOGGLE_TIMEOUT_MS`; without an
29
+ * escape hatch this would silently override a project that raised Playwright's own `actionTimeout`.
30
+ */
31
+ const ENV_TIMEOUT = Number(process.env.SUNGEN_TOGGLE_TIMEOUT_MS);
32
+ // Guard the parse: `=0` means "no timeout" to Playwright (hang forever) and a negative is nonsense —
33
+ // neither should be inherited from a typo'd env var.
34
+ const TOGGLE_TIMEOUT_MS = Number.isFinite(ENV_TIMEOUT) && ENV_TIMEOUT > 0 ? ENV_TIMEOUT : 8_000;
35
+ const POLL_INTERVAL_MS = 100;
36
+
37
+ /** A signal DECLARED in selectors.yaml: the class token, or the boolean attribute, marking ON. */
38
+ export interface DeclaredSignal { class?: string; attribute?: string }
39
+
40
+ /** Where the control publishes its on/off state — decided in the page, from the real element. */
41
+ export type StateSource = 'native' | 'aria-pressed' | 'aria-checked' | 'class' | 'attribute' | 'none';
42
+ export interface ControlProbe {
43
+ source: StateSource;
44
+ /** Current state, already normalised. Meaningless when `source` is 'native' (Playwright reads it). */
45
+ on: boolean;
46
+ /** Button-like: a control a human would press. Decides how a MISSING state is read — see below. */
47
+ buttonLike: boolean;
48
+ }
49
+
50
+ /**
51
+ * Runs IN THE PAGE, so it must stay self-contained: Playwright serialises it and the browser has
52
+ * none of this module's scope.
53
+ *
54
+ * It also must contain NO inner function of its own — not even an arrow helper. A bundler with
55
+ * keepNames (esbuild, which is what Playwright itself transpiles specs with) rewrites every named
56
+ * inner function to `__name(fn, "…")`, that wrapper travels inside the serialised source, and the
57
+ * page throws `ReferenceError: __name is not defined` on EVERY check step. Straight-line code with
58
+ * the normalise repeated inline is the price of being serialisable; `tests/runtime/
59
+ * checked-state.run.ts` pins it so the next edit cannot quietly reintroduce a helper.
60
+ *
61
+ * Structurally typed rather than via the DOM lib names because this file is also typechecked
62
+ * inside the compiler build, whose tsconfig carries no DOM lib.
63
+ */
64
+ export function probeControl(node: unknown, declared?: DeclaredSignal): ControlProbe {
65
+ const el = node as {
66
+ tagName: string; type?: string;
67
+ getAttribute(n: string): string | null; hasAttribute(n: string): boolean;
68
+ classList: { contains(c: string): boolean };
69
+ querySelector(s: string): unknown;
70
+ };
71
+ const tag = String(el.tagName ?? '').trim().toLowerCase(); // lowercase: XHTML serves it so
72
+ const type = String(el.type ?? '').trim().toLowerCase();
73
+ const role = String(el.getAttribute('role') ?? '').trim().toLowerCase();
74
+ const ariaCheckableRole = role === 'checkbox' || role === 'radio' || role === 'switch';
75
+ const buttonLike = tag === 'button' || role === 'button'
76
+ || (tag === 'input' && (type === 'button' || type === 'submit' || type === 'reset'));
77
+
78
+ // A DECLARED signal wins over every ARIA reading below (issue #730). The author reached for this
79
+ // escape hatch precisely because the component publishes no usable ARIA, so second-guessing them
80
+ // from a stray role would put the step back on the path that could not work. Self OR descendant:
81
+ // a card picker swaps the class on the control itself, while a custom checkbox often mounts a
82
+ // checkmark <svg class="is-checked"> inside instead — both are the same declaration to the author.
83
+ // `classList.contains` is exact-token by definition; `[class~="…"]` is its selector equivalent,
84
+ // so neither needs the class escaped and neither can half-match ("is-checked-disabled").
85
+ if (declared && declared.class) {
86
+ const onSelf = el.classList != null && el.classList.contains(declared.class);
87
+ const onChild = !onSelf && el.querySelector('[class~="' + declared.class + '"]') != null;
88
+ return { source: 'class', on: onSelf || onChild, buttonLike };
89
+ }
90
+ // The same declaration published as a boolean attribute instead of a class. Self ONLY, unlike the
91
+ // class above: a class swap can legitimately live on a checkmark child, but `data-checked` on a
92
+ // descendant is a different control's state, and reading it would assert the wrong element.
93
+ if (declared && declared.attribute) {
94
+ const raw = el.getAttribute(declared.attribute);
95
+ return { source: 'attribute', on: String(raw ?? '').trim().toLowerCase() === 'true', buttonLike };
96
+ }
97
+
98
+ // A native input needs no ARIA attribute — Playwright reads `.checked` directly.
99
+ if (tag === 'input' && (type === 'checkbox' || type === 'radio')) return { source: 'native', on: false, buttonLike };
100
+ // An ARIA checkable publishes state through `aria-checked`, which is what Playwright reads. Only
101
+ // hand it over when that attribute is actually there: `<button role="switch" aria-pressed="…">`
102
+ // is a common authoring slip, and routing it to .check() makes Playwright click, re-read the
103
+ // aria-checked that does not exist, and die on "did not change its state".
104
+ if (ariaCheckableRole && el.hasAttribute('aria-checked')) return { source: 'native', on: false, buttonLike };
105
+ if (el.hasAttribute('aria-pressed')) {
106
+ return { source: 'aria-pressed', on: String(el.getAttribute('aria-pressed') ?? '').trim().toLowerCase() === 'true', buttonLike };
107
+ }
108
+ if (el.hasAttribute('aria-checked')) {
109
+ return { source: 'aria-checked', on: String(el.getAttribute('aria-checked') ?? '').trim().toLowerCase() === 'true', buttonLike };
110
+ }
111
+ // Role claims checkable but nothing is published — let Playwright raise its own, precise error.
112
+ if (ariaCheckableRole) return { source: 'native', on: false, buttonLike };
113
+ return { source: 'none', on: false, buttonLike };
114
+ }
115
+
116
+ /** The declaration both helpers forward into the page. Undefined when nothing was declared, so a
117
+ * control with real ARIA keeps being read exactly as it was before any of this existed. */
118
+ const declaredSignal = (
119
+ options?: { checkedClass?: string; checkedAttribute?: string },
120
+ ): DeclaredSignal | undefined => (
121
+ options?.checkedClass ? { class: options.checkedClass }
122
+ : options?.checkedAttribute ? { attribute: options.checkedAttribute }
123
+ : undefined
124
+ );
125
+
126
+ const sleep = (ms: number): Promise<void> => new Promise((r) => setTimeout(r, ms));
127
+ /**
128
+ * Time left on a budget, floored at 250ms. Never 0 — to Playwright that means "no timeout", i.e.
129
+ * hang forever. And never a sliver either: a 1ms floor makes the last poll fail with
130
+ * `Timeout 1ms exceeded`, which masks the real, explanatory error this helper is about to throw.
131
+ * The floor lets a budget overrun by at most 250ms; the loops below terminate on the deadline.
132
+ */
133
+ const left = (deadline: number): number => Math.max(250, deadline - Date.now());
134
+
135
+ /** Read the live state. A REMOVED attribute means off: `aria-pressed={on || undefined}` is a common
136
+ * React/Vue idiom, so requiring a literal "false" would fail a control that is correctly off. */
137
+ async function readOn(target: Locator, attr: 'aria-pressed' | 'aria-checked', deadline: number): Promise<boolean> {
138
+ const raw = await target.getAttribute(attr, { timeout: left(deadline) });
139
+ return String(raw ?? '').trim().toLowerCase() === 'true';
140
+ }
141
+
142
+ /**
143
+ * Drive `target` to the requested on/off state, idempotently.
144
+ *
145
+ * @param target the control
146
+ * @param want `true` = checked/pressed, `false` = unchecked/unpressed
147
+ * @param label the `[Selector Ref]` from the feature file, used only in error messages
148
+ * @param options.checkedClass `checked:`/`selected:` from selectors.yaml — the class marking ON,
149
+ * for a control that publishes no ARIA at all (issue #730). Overrides every ARIA reading.
150
+ * @param options.checkedAttribute the same declaration published as a boolean attribute
151
+ * ('true'/'false') instead of a class. A selector declares one or the other, never both.
152
+ */
153
+ export async function setCheckedState(
154
+ target: Locator, want: boolean, label: string,
155
+ options?: { timeout?: number; checkedClass?: string; checkedAttribute?: string },
156
+ ): Promise<void> {
157
+ const timeout = options?.timeout ?? TOGGLE_TIMEOUT_MS;
158
+ const declared = declaredSignal(options);
159
+ // TWO budgets, not one shared deadline: a control that hydrates late would otherwise spend the
160
+ // whole budget being classified and leave the post-click check a single sample — which then
161
+ // reports "the click landed on the label" when the real problem was the clock.
162
+ const classifyDeadline = Date.now() + timeout;
163
+
164
+ // `locator.evaluate` waits only for ATTACHED. Playwright's own .check() re-resolved the locator on
165
+ // every actionability retry, so a control that gains its role/aria-* on hydration still worked —
166
+ // classifying once at first attach would turn a slow mount into a hard failure.
167
+ let probe = await target.evaluate(probeControl, declared, { timeout: left(classifyDeadline) });
168
+ while (probe.source === 'none' && classifyDeadline - Date.now() > POLL_INTERVAL_MS) {
169
+ await sleep(POLL_INTERVAL_MS);
170
+ probe = await target.evaluate(probeControl, declared, { timeout: left(classifyDeadline) });
171
+ }
172
+
173
+ if (probe.source === 'native') {
174
+ if (want) await target.check({ timeout });
175
+ else await target.uncheck({ timeout });
176
+ return;
177
+ }
178
+
179
+ // A declared class: drive it the way the ARIA path below drives a toggle — click ONLY when the
180
+ // current state differs, then require the class to reach the requested state. Same idempotence,
181
+ // and the same refusal to report success on a blind click.
182
+ if (probe.source === 'class' || probe.source === 'attribute') {
183
+ if (probe.on === want) return;
184
+ await target.click({ timeout });
185
+ const classStart = Date.now();
186
+ const classDeadline = classStart + timeout;
187
+ for (;;) {
188
+ const after = await target.evaluate(probeControl, declared, { timeout: left(classDeadline) });
189
+ if (after.on === want) return;
190
+ if (classDeadline - Date.now() <= POLL_INTERVAL_MS) {
191
+ throw new Error(
192
+ `[${label}] did not become ${want ? 'checked' : 'unchecked'} within ${Date.now() - classStart}ms of `
193
+ + `being clicked — the declared ${probe.source} "${declared?.class ?? declared?.attribute}" still reads `
194
+ + `${want ? 'off' : 'on'}${probe.source === 'class' ? ' on the element and on its descendants' : ''}. `
195
+ + `Either the click landed on the label rather than the control, or that is not the `
196
+ + `${probe.source} this component swaps for its on/off state.`,
197
+ );
198
+ }
199
+ await sleep(POLL_INTERVAL_MS);
200
+ }
201
+ }
202
+
203
+ if (probe.source === 'none') {
204
+ // Absence on a PRESSABLE control means off — a toggle written as `aria-pressed={on || undefined}`
205
+ // (React/Vue, and several component libraries a QA team does not own) drops the attribute
206
+ // entirely when unpressed. Treating that as an error made `uncheck` pass on the first run and
207
+ // fail on the second, which is precisely the rerun-dependent flake this helper exists to stop.
208
+ if (probe.buttonLike) {
209
+ if (!want) {
210
+ // Nothing to do — but say so, because the alternative is a green step that did nothing at
211
+ // all (a mis-resolved selector pointing at a plain button looks exactly like this).
212
+ console.warn(
213
+ `[sungen] uncheck [${label}]: the control publishes no aria-pressed/aria-checked, so it was `
214
+ + `read as already unpressed and NOT clicked. If it should have been a toggle, expose `
215
+ + `aria-pressed in both states; if this is not a toggle at all, the step is a no-op.`,
216
+ );
217
+ return;
218
+ }
219
+ // Not a blind click: press once, then REQUIRE the control to publish the requested state.
220
+ // A plain button publishes nothing afterwards either, so it still fails — just one click later.
221
+ await target.click({ timeout });
222
+ const pressedStart = Date.now();
223
+ const pressedDeadline = pressedStart + timeout;
224
+ for (;;) {
225
+ const after = await target.evaluate(probeControl, undefined, { timeout: left(pressedDeadline) });
226
+ if (after.source !== 'none' && after.on === want) return;
227
+ if (pressedDeadline - Date.now() <= POLL_INTERVAL_MS) {
228
+ throw new Error(
229
+ `Cannot check [${label}] — it published no aria-pressed/aria-checked before the click and `
230
+ + `still none ${Date.now() - pressedStart}ms after it, so it is a plain button rather than a `
231
+ + `toggle. Give the control a real ARIA toggle state, or use "toggles [${label}]" if an `
232
+ + `unconditional flip is what you meant.`,
233
+ );
234
+ }
235
+ await sleep(POLL_INTERVAL_MS);
236
+ }
237
+ }
238
+ // Not pressable and publishes nothing — there is no reading of this that makes the step meaningful.
239
+ throw new Error(
240
+ `Cannot ${want ? 'check' : 'uncheck'} [${label}] — it is not a checkbox/radio/switch, is not a `
241
+ + `button, and publishes no aria-pressed/aria-checked, so it has no on/off state to set. Point the `
242
+ + `selector at the control itself, or give it a real ARIA toggle role.`,
243
+ );
244
+ }
245
+
246
+ const attr = probe.source;
247
+ // Already in the requested state → nothing to do. This is the idempotence `check` promises.
248
+ if (probe.on === want) return;
249
+ await target.click({ timeout });
250
+
251
+ // Fresh budget: this measures the CONTROL's reaction, not time already spent finding it.
252
+ const verifyStart = Date.now();
253
+ const verifyDeadline = verifyStart + timeout;
254
+ for (;;) {
255
+ if (await readOn(target, attr, verifyDeadline) === want) return;
256
+ if (verifyDeadline - Date.now() <= POLL_INTERVAL_MS) {
257
+ throw new Error(
258
+ `[${label}] did not become ${want ? 'pressed' : 'unpressed'} within ${Date.now() - verifyStart}ms of being `
259
+ + `clicked — ${attr} still reads ${want ? 'off' : 'on'}. Either the click landed on the label rather than `
260
+ + `the control, or the handler did not change the state.`,
261
+ );
262
+ }
263
+ await sleep(POLL_INTERVAL_MS);
264
+ }
265
+ }
266
+
267
+ /**
268
+ * Assert the on/off state of the same control family — the read side of #675.
269
+ *
270
+ * `expect(locator).toBeChecked()` throws the identical "Not a checkbox or radio button" on a
271
+ * `<button aria-pressed>`, so driving a toggle worked while asserting it did not. Polls like
272
+ * Playwright's own web-first assertions rather than sampling once, and reads state exactly the way
273
+ * setCheckedState does, so an action and the assertion after it can never disagree about what the
274
+ * control says. Uses `locator.isChecked()` (not `expect`) for the native family, which keeps this
275
+ * file free of any test-runner dependency.
276
+ */
277
+ export async function expectCheckedState(
278
+ target: Locator, expected: boolean, label: string,
279
+ options?: { timeout?: number; checkedClass?: string; checkedAttribute?: string },
280
+ ): Promise<void> {
281
+ const timeout = options?.timeout ?? TOGGLE_TIMEOUT_MS;
282
+ // Same signal the action side drives (issue #730) — passing it here is what keeps the invariant
283
+ // above true for a class-only control too: `source: 'class'` falls into the generic `probe.on`
284
+ // branch below, so check-then-assert cannot disagree.
285
+ const declared = declaredSignal(options);
286
+ const start = Date.now();
287
+ const deadline = start + timeout;
288
+ for (;;) {
289
+ const probe = await target.evaluate(probeControl, declared, { timeout: left(deadline) });
290
+ let actual: boolean | null = null;
291
+ if (probe.source === 'native') actual = await target.isChecked({ timeout: left(deadline) });
292
+ else if (probe.source !== 'none') actual = probe.on;
293
+ else if (probe.buttonLike) {
294
+ // Absence on a pressable control reads as off, mirroring setCheckedState. But a plain button
295
+ // and a correctly-off toggle are indistinguishable HERE, and this is an ASSERTION: returning
296
+ // green having verified nothing is worse than the loud "Not a checkbox or radio button" it
297
+ // replaced. So say so, exactly as the action side does for the same reading.
298
+ if (!expected) {
299
+ console.warn(
300
+ `[sungen] see [${label}] is unchecked: the control publishes no aria-pressed/aria-checked, `
301
+ + `so it was read as unpressed WITHOUT verifying anything. If it should be a toggle, expose `
302
+ + `aria-pressed in both states; if it is not a toggle, this assertion proves nothing.`,
303
+ );
304
+ }
305
+ actual = false;
306
+ }
307
+ if (actual === expected) return;
308
+ if (deadline - Date.now() <= POLL_INTERVAL_MS) {
309
+ if (actual === null) {
310
+ throw new Error(
311
+ `Cannot assert [${label}] is ${expected ? 'checked' : 'unchecked'} — it is not a `
312
+ + `checkbox/radio/switch, is not a button, and publishes no aria-pressed/aria-checked, so it `
313
+ + `reports no state to assert. Point the selector at the control itself, or give it a real `
314
+ + `ARIA toggle role.`,
315
+ );
316
+ }
317
+ throw new Error(
318
+ `Expected [${label}] to be ${expected ? 'checked' : 'unchecked'}, but it is `
319
+ + `${actual ? 'checked' : 'unchecked'} after ${Date.now() - start}ms.`,
320
+ );
321
+ }
322
+ await sleep(POLL_INTERVAL_MS);
323
+ }
324
+ }
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Playwright reporter that rewrites the operator's machine paths out of the JSON result file.
3
+ *
4
+ * The JSON reporter records absolute paths — `config.configFile`, `rootDir`, `projects[]`, every
5
+ * `error.location.file`, attachment paths, and the paths quoted inside stack traces and messages.
6
+ * Teams commit that file next to the spec (sungen delivery reads it), so a username and a machine
7
+ * layout travel into the repository and the deliverable. Nothing downstream needs them absolute.
8
+ *
9
+ * Runs in `onExit()` — after every reporter, the JSON one included, has finished writing — reads the
10
+ * JSON reporter's `outputFile`, rewrites it in place, and stays silent otherwise. Paths under the
11
+ * project root (the directory of `playwright.config.ts`) become project-relative; outside it a path that
12
+ * names a machine (a home directory, a drive) keeps only its basename while any other absolute string
13
+ * — a URL route — is left alone; home directories quoted inside text become `~/`. `sungen results
14
+ * sanitize` applies the same rules to files that already exist.
15
+ *
16
+ * Wired in playwright.config.ts: reporter: [['html'], ['json', {…}], ['./specs/generated/report-path-sanitize-reporter.ts']].
17
+ * Regenerated by `sungen generate` / `sungen update` — DO NOT EDIT.
18
+ */
19
+ import type { FullConfig, Reporter } from '@playwright/test/reporter';
20
+ import * as fs from 'node:fs';
21
+ import * as path from 'node:path';
22
+
23
+ class ReportPathSanitizeReporter implements Reporter {
24
+ private config?: FullConfig;
25
+
26
+ onBegin(config: FullConfig): void {
27
+ this.config = config;
28
+ }
29
+
30
+ printsToStdio(): boolean {
31
+ return false;
32
+ }
33
+
34
+ async onExit(): Promise<void> {
35
+ const file = this.jsonOutputFile();
36
+ if (!file || !fs.existsSync(file)) return;
37
+ const root = this.configDir();
38
+ let doc: unknown;
39
+ try {
40
+ doc = JSON.parse(fs.readFileSync(file, 'utf-8'));
41
+ } catch {
42
+ return; // not ours to repair — leave whatever the JSON reporter wrote
43
+ }
44
+ const { doc: clean, rewritten } = sanitizeReportPaths(doc, { root });
45
+ if (rewritten === 0) return;
46
+ const tmp = `${file}.tmp-${process.pid}`;
47
+ fs.writeFileSync(tmp, JSON.stringify(clean, null, 2));
48
+ fs.renameSync(tmp, file);
49
+ }
50
+
51
+ private configDir(): string {
52
+ return this.config?.configFile ? path.dirname(this.config.configFile) : process.cwd();
53
+ }
54
+
55
+ /** The JSON reporter's output file: its `outputFile` option, else the env var the config reads. */
56
+ private jsonOutputFile(): string | undefined {
57
+ const entry = (this.config?.reporter ?? []).find(([name]) => name === 'json');
58
+ const opt = (entry?.[1] as { outputFile?: string } | undefined)?.outputFile
59
+ ?? process.env.PLAYWRIGHT_JSON_OUTPUT_FILE // Playwright's own override
60
+ ?? process.env.PLAYWRIGHT_JSON_OUTPUT_NAME; // the name sungen's config template reads
61
+ if (!opt) return undefined;
62
+ if (path.isAbsolute(opt)) return opt;
63
+ // Playwright resolves a relative outputFile against the config directory; the env var against cwd.
64
+ const candidates = [path.resolve(this.configDir(), opt), path.resolve(process.cwd(), opt)];
65
+ return candidates.find((c) => fs.existsSync(c)) ?? candidates[0];
66
+ }
67
+ }
68
+
69
+ export default ReportPathSanitizeReporter;
70
+
71
+ // ── sanitizer — the same rules as sungen's `report-path-sanitizer.ts`, embedded so this file needs no
72
+ // import from the compiler package (a parity test in sungen keeps the two in step) ──────────────
73
+
74
+ const ABSOLUTE = /^(?:file:\/\/\/?|[A-Za-z]:[\\/]|\/)/;
75
+ const WINDOWS_DRIVE = /^[A-Za-z]:/;
76
+ const HOME_DIR = /(?<![\w.:/\\-])(?:file:\/\/\/?)?(?:\/(?:Users|home)\/[^/\\\s"'`)\]]+[/\\]|[A-Za-z]:[\\/]Users[\\/][^/\\\s"'`)\]]+[\\/])/g;
77
+
78
+ interface Root { posix: string; caseInsensitive: boolean; pattern: RegExp }
79
+
80
+ export function sanitizeReportPaths<T>(doc: T, opts: { root: string }): { doc: T; rewritten: number } {
81
+ const root = normalizeRoot(opts.root);
82
+ if (root.posix.length < 2) return { doc, rewritten: 0 }; // a `/` root would shred the document
83
+ let rewritten = 0;
84
+ const walk = (v: unknown): unknown => {
85
+ if (typeof v === 'string') {
86
+ const next = sanitizeString(v, root);
87
+ if (next !== v) rewritten++;
88
+ return next;
89
+ }
90
+ if (Array.isArray(v)) return v.map(walk);
91
+ if (v && typeof v === 'object') {
92
+ const out: Record<string, unknown> = {};
93
+ for (const [k, x] of Object.entries(v as Record<string, unknown>)) out[k] = walk(x);
94
+ return out;
95
+ }
96
+ return v;
97
+ };
98
+ return { doc: walk(doc) as T, rewritten };
99
+ }
100
+
101
+ function normalizeRoot(raw: string): Root {
102
+ const posix = toPosix(stripFileScheme(raw)).replace(/\/+$/, '');
103
+ const caseInsensitive = WINDOWS_DRIVE.test(posix);
104
+ const body = posix.split('/').map(escapeRegExp).join('[\\\\/]');
105
+ return { posix, caseInsensitive, pattern: new RegExp(`(?:file:\\/\\/\\/?)?${body}(?:[\\\\/](?![\\\\/])|(?=$|[\\s"'\`):,]))`, caseInsensitive ? 'gi' : 'g') };
106
+ }
107
+
108
+ function sanitizeString(value: string, root: Root): string {
109
+ const trimmed = value.trim();
110
+ if (ABSOLUTE.test(trimmed) && !/\s/.test(trimmed)) {
111
+ const p = toPosix(stripFileScheme(trimmed)).replace(/\/+$/, '');
112
+ const a = root.caseInsensitive ? p.toLowerCase() : p;
113
+ const b = root.caseInsensitive ? root.posix.toLowerCase() : root.posix;
114
+ if (a === b) return '.';
115
+ if (a.startsWith(b + '/')) return p.slice(root.posix.length + 1);
116
+ // outside the root: only a machine path (home dir, drive) is cut to its basename — `/products` is a route
117
+ if (WINDOWS_DRIVE.test(p) || /\/(?:Users|home)\/[^/]+\/./.test(p)) {
118
+ // another machine's checkout of this project → relative from the project directory; else basename
119
+ const marker = '/' + root.posix.slice(root.posix.lastIndexOf('/') + 1) + '/';
120
+ const i = marker.length > 2 ? (root.caseInsensitive ? p.toLowerCase().indexOf(marker.toLowerCase()) : p.indexOf(marker)) : -1;
121
+ if (i >= 0) return p.slice(i + marker.length);
122
+ return p.slice(p.lastIndexOf('/') + 1) || p;
123
+ }
124
+ return value;
125
+ }
126
+ const out = value.replace(root.pattern, (m: string) => (/[\\/]$/.test(m) ? '' : '.'));
127
+ return out.replace(HOME_DIR, '~/');
128
+ }
129
+
130
+ function stripFileScheme(p: string): string {
131
+ if (!p.startsWith('file://')) return p;
132
+ const rest = p.slice('file://'.length);
133
+ return /^\/[A-Za-z]:/.test(rest) ? rest.slice(1) : rest;
134
+ }
135
+
136
+ function toPosix(p: string): string {
137
+ return p.replace(/\\/g, '/');
138
+ }
139
+
140
+ function escapeRegExp(s: string): string {
141
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
142
+ }
@@ -2,7 +2,8 @@
2
2
  /**
3
3
  * Sungen URL assertion helper — auto-generated into specs/url-assert.ts. DO NOT EDIT.
4
4
  *
5
- * Every Then-side URL assertion (`see [X] page`, `is on [X] page`) compiles to one call here.
5
+ * Every URL step compiles to one call here: the Then-side assertions (`see [X] page`,
6
+ * `is on [X] page`) to `expectPage`, and `wait for [X] page` to `waitForPage`.
6
7
  *
7
8
  * Why a helper and not an inline predicate. The predicate used to be emitted inline into every
8
9
  * spec, so each correction to what "the URL matches" MEANS was a breaking change for every
@@ -28,6 +29,16 @@ export interface UrlExpectation {
28
29
  * against a full URL.
29
30
  */
30
31
  pathname: RegExp;
32
+ /**
33
+ * Anchored hostname pattern, emitted ONLY when the page selector's `value` is an ABSOLUTE URL
34
+ * (`https://app.example.com/reception/:id`) — a destination on a different host from the suite's
35
+ * baseURL. Absent means "whatever host we are on", which is what a relative page value says.
36
+ *
37
+ * It is a SEPARATE field rather than part of `pathname` because the two are read off different
38
+ * properties of the parsed URL: `u.hostname` never appears in `u.pathname`. One concatenated
39
+ * regex only worked while the match ran unanchored over the whole URL string (#677).
40
+ */
41
+ hostname?: RegExp;
31
42
  /**
32
43
  * The `with {{v}}` value, raw from test-data: '?q=1' · '/vi/search?q=1' · '#tab' · ''.
33
44
  * `undefined` means the step declared nothing, which is itself an assertion — see below.
@@ -83,6 +94,15 @@ function decodedPath(pathname: string): string {
83
94
  }
84
95
  }
85
96
 
97
+ /**
98
+ * Is the URL at the expected LOCATION — right host, right path? The half every URL step shares.
99
+ * The Then-side assertion goes on to judge the query; the wait stops here (see `waitForPage`).
100
+ */
101
+ function locationMatches(pathname: RegExp, hostname: RegExp | undefined, u: URL): boolean {
102
+ if (hostname && !hostname.test(u.hostname)) return false;
103
+ return pathname.test(u.pathname) || pathname.test(decodedPath(u.pathname));
104
+ }
105
+
86
106
  /**
87
107
  * Build the predicate for `expect(page).toHaveURL(...)`.
88
108
  *
@@ -94,9 +114,9 @@ function decodedPath(pathname: string): string {
94
114
  * A declared fragment is asserted too; a declared value carrying no query
95
115
  * at all constrains the path only (the compiler warns: SG-W014).
96
116
  */
97
- export function urlMatches({ pathname, declared, dataRef }: UrlExpectation): (u: URL) => boolean {
117
+ export function urlMatches({ pathname, hostname, declared, dataRef }: UrlExpectation): (u: URL) => boolean {
98
118
  return (u: URL): boolean => {
99
- if (!pathname.test(u.pathname) && !pathname.test(decodedPath(u.pathname))) return false;
119
+ if (!locationMatches(pathname, hostname, u)) return false;
100
120
 
101
121
  // `dataRef`, not `declared`, decides which rule applies. Both "the step declared nothing" and
102
122
  // "the step declared {{v}} but the key resolved to nothing at runtime" arrive here as
@@ -135,38 +155,43 @@ export function urlMatches({ pathname, declared, dataRef }: UrlExpectation): (u:
135
155
  };
136
156
  }
137
157
 
158
+ /** What `settleAndHold` needs of a page — Playwright's `Page` satisfies it; so does a test fake. */
159
+ type SettleablePage = {
160
+ url(): string;
161
+ waitForLoadState(state: 'networkidle', opts?: { timeout?: number }): Promise<void>;
162
+ };
163
+
164
+ /** The two runtime knobs, shared by both URL steps. */
165
+ interface SettleOpts { settleMs?: number; networkIdleMs?: number }
166
+
138
167
  /**
139
- * `toHaveURL` is a polling assertion: it passes on the FIRST sample that matches. A client-side
140
- * router pushes the destination URL before the route guard or the API has answered, so a user
141
- * with no permission is "on" `/items/detail` for a few hundred milliseconds — long enough for
142
- * the assertion to sample it, pass, and end the test green — and is then bounced to `/forbidden`
143
- * with no assertion left to see it. A real project shipped a permission matrix on that oracle;
144
- * the 403s were found by hand (#662).
168
+ * A URL that MATCHES is not a page the user is still on. A client-side router pushes the
169
+ * destination URL before the route guard or the API has answered, so a user with no permission is
170
+ * "on" `/items/detail` for a few hundred milliseconds — long enough for a polling assertion to
171
+ * sample it and pass, or for a `waitForURL` to resolve — and is then bounced to `/forbidden` with
172
+ * nothing left watching. A real project shipped a permission matrix on that oracle; the 403s were
173
+ * found by hand (#662).
145
174
  *
146
- * `expectPage` is the page oracle `see [X] page` compiles to now. Arriving is not enough: the URL
147
- * has to be STILL matching once the page has settled.
148
- * 1. arrive — the same predicate `toHaveURL` always used;
149
- * 2. settle — wait for network to go quiet (bounded: an app that polls forever must not hang
150
- * the test, so a timeout here is not a failure);
151
- * 3. hold — sample the URL over a settle window; the moment it stops matching, fail and
152
- * NAME where it went, because that destination is the finding;
153
- * 4. re-assert — the arrival predicate, once more, on the settled page.
175
+ * So both URL steps go through this: having arrived,
176
+ * 1. settle — wait for network to go quiet (bounded: an app that polls forever must not hang
177
+ * the test, so a timeout here is not a failure);
178
+ * 2. hold — sample the URL over a settle window; the moment it stops matching, fail and
179
+ * NAME where it went, because that destination is the finding.
180
+ * The caller decides what arriving meant and what to do afterwards.
154
181
  *
155
182
  * `SUNGEN_URL_SETTLE_MS` (default 1000) is the hold window; `SUNGEN_URL_NETWORKIDLE_MS` (default
156
- * 3000) bounds step 2. Both are runtime knobs, so a slow environment is a config change, not a
183
+ * 3000) bounds step 1. Both are runtime knobs, so a slow environment is a config change, not a
157
184
  * regenerate.
158
185
  */
159
- export async function expectPage(
160
- page: { url(): string; waitForLoadState(state: 'networkidle', opts?: { timeout?: number }): Promise<void> },
161
- expectation: UrlExpectation,
162
- opts: { settleMs?: number; networkIdleMs?: number; assertUrl?: (matches: (u: URL) => boolean) => Promise<void> } = {},
186
+ async function settleAndHold(
187
+ page: SettleablePage,
188
+ matches: (u: URL) => boolean,
189
+ opts: SettleOpts,
190
+ label: string,
191
+ consequence: string,
163
192
  ): Promise<void> {
164
193
  const settleMs = opts.settleMs ?? Number(process.env.SUNGEN_URL_SETTLE_MS ?? 1000);
165
194
  const networkIdleMs = opts.networkIdleMs ?? Number(process.env.SUNGEN_URL_NETWORKIDLE_MS ?? 3000);
166
- const matches = urlMatches(expectation);
167
- const assertUrl = opts.assertUrl ?? (async (m: (u: URL) => boolean) => { await expect(page as unknown as Page).toHaveURL(m); });
168
-
169
- await assertUrl(matches);
170
195
  const arrivedAt = page.url();
171
196
 
172
197
  await page.waitForLoadState('networkidle', { timeout: networkIdleMs }).catch(() => undefined);
@@ -176,13 +201,61 @@ export async function expectPage(
176
201
  const now = page.url();
177
202
  if (!matches(new URL(now))) {
178
203
  throw new Error(
179
- `page assertion: reached ${arrivedAt} and then left for ${now} after ${Date.now() - started}ms — `
204
+ `${label}: reached ${arrivedAt} and then left for ${now} after ${Date.now() - started}ms — `
180
205
  + 'a route guard, an error boundary or a redirect moved the user away after the URL had already '
181
- + 'matched. The page the test asserted is not the page the user ended on.',
206
+ + `matched. ${consequence}`,
182
207
  );
183
208
  }
184
209
  await new Promise((r) => setTimeout(r, 50));
185
210
  }
211
+ }
212
+
213
+ /**
214
+ * `expectPage` is the page oracle `see [X] page` / `is on [X] page` compile to. Arriving is not
215
+ * enough: the URL has to be STILL matching once the page has settled.
216
+ * 1. arrive — the same predicate `toHaveURL` always used;
217
+ * 2. settle + hold — see `settleAndHold`;
218
+ * 3. re-assert — the arrival predicate, once more, on the settled page.
219
+ */
220
+ export async function expectPage(
221
+ page: SettleablePage,
222
+ expectation: UrlExpectation,
223
+ opts: SettleOpts & { assertUrl?: (matches: (u: URL) => boolean) => Promise<void> } = {},
224
+ ): Promise<void> {
225
+ const matches = urlMatches(expectation);
226
+ const assertUrl = opts.assertUrl ?? (async (m: (u: URL) => boolean) => { await expect(page as unknown as Page).toHaveURL(m); });
186
227
 
187
228
  await assertUrl(matches);
229
+ await settleAndHold(page, matches, opts, 'page assertion',
230
+ 'The page the test asserted is not the page the user ended on.');
231
+ await assertUrl(matches);
232
+ }
233
+
234
+ /**
235
+ * `waitForPage` is what `When User wait for [X] page` compiles to (#677).
236
+ *
237
+ * A bare `page.waitForURL(/…/)` resolves on the first URL sample that matches — the transient URL
238
+ * the router pushed before its guard decided. The wait then returned, and every step AFTER it ran
239
+ * against a page mid-redirect, so their failures pointed at the wrong thing. It was also the third
240
+ * hand-rolled copy of "does this URL match", drifting from the pair #662 consolidated here.
241
+ *
242
+ * Where this deliberately DIFFERS from `expectPage`: the query is not constrained. A Then-side page
243
+ * assertion is an oracle, so a bare page must be bare — `/vi/search` fails on `/vi/search?x=1`. A
244
+ * wait is not an oracle: `wait for [Results] page` after submitting a search legitimately lands on
245
+ * `/results?q=abc`, and reddening there would assert something the step never said. Prove the query
246
+ * with the `Then User see [Results] page with {{v}}` that follows.
247
+ */
248
+ export async function waitForPage(
249
+ page: SettleablePage & { waitForURL(url: (u: URL) => boolean, opts?: { timeout?: number }): Promise<void> },
250
+ expectation: UrlExpectation,
251
+ opts: SettleOpts & { timeout?: number; waitUrl?: (matches: (u: URL) => boolean) => Promise<void> } = {},
252
+ ): Promise<void> {
253
+ const { pathname, hostname } = expectation;
254
+ const matches = (u: URL): boolean => locationMatches(pathname, hostname, u);
255
+ // Playwright's own navigation timeout governs the arrival; only the hold is sungen's.
256
+ const arrive = opts.waitUrl ?? (async (m: (u: URL) => boolean) => { await page.waitForURL(m, { timeout: opts.timeout }); });
257
+
258
+ await arrive(matches);
259
+ await settleAndHold(page, matches, opts, 'page wait',
260
+ 'The steps after this wait would have run against a page the user is already being moved off.');
188
261
  }