@sun-asterisk/sungen 3.2.29 → 3.2.31

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
@@ -1,11 +1,15 @@
1
1
  import { ParsedStep } from '../gherkin-parser';
2
+ import { PARENT_CONTAINER_TYPES } from '../gherkin-parser/parent-scope-clause';
2
3
  import { TemplateEngine } from './template-engine';
3
4
  import { adapterRegistry } from './adapters';
4
5
  import { PatternRegistry, PatternContext, Platform } from './patterns';
5
6
  import { SelectorResolver } from './utils/selector-resolver';
6
7
  import { DataResolver } from './utils/data-resolver';
8
+ import { locatorIdentityKey } from './utils/dialog-scope-identity';
7
9
  import { DiagnosticsCollector } from './diagnostics';
8
10
  import { unsupportedStepCode } from './patterns/unsupported-step-code';
11
+ import { maskRefsForMatching } from './utils/mask-refs-for-matching';
12
+ import { isKnownSelectorType } from './utils/selector-type-validation';
9
13
  import { loadAppProfile, ResolvedAppProfile } from './app-profile';
10
14
  import { capabilityRegistry } from '../../capabilities/registry';
11
15
 
@@ -38,6 +42,8 @@ export interface MappedStep {
38
42
  code: string; // Generated Playwright code
39
43
  comment?: string; // Optional comment
40
44
  imports?: string[]; // Required imports
45
+ pattern?: string; // Name of the step form that produced the code (post-map guards read it)
46
+ templateData?: Record<string, any>; // What the resolver handed the template (e.g. a recorded refusal)
41
47
  }
42
48
 
43
49
  /**
@@ -203,6 +209,19 @@ export class StepMapper {
203
209
  this.dataResolver.registerCaptured(name);
204
210
  }
205
211
 
212
+ /**
213
+ * Register the head of a variable bound by the feature's `@beforeAll` hook. Feature-scoped: it
214
+ * survives setScenarioContext() so every scenario can read `{{head.*}}` as a runtime get().
215
+ */
216
+ registerFeatureCaptured(head: string): void {
217
+ this.dataResolver.registerFeatureCaptured(head);
218
+ }
219
+
220
+ /** Drop feature-scoped captured vars — call once per feature, before its hooks and scenarios map. */
221
+ clearFeatureCaptured(): void {
222
+ this.dataResolver.clearFeatureCaptured();
223
+ }
224
+
206
225
  /**
207
226
  * Map a Gherkin step to Playwright code
208
227
  * Uses pattern registry first, falls back to AI if enabled
@@ -216,6 +235,8 @@ export class StepMapper {
216
235
  this.lastPrimaryKeyword = keyword;
217
236
  }
218
237
 
238
+ this.warnScopeClauseDiscarded(step);
239
+
219
240
  // SG-W015 — `see [X] card with {{n}}` where n is a bare number compiles to a TEXT
220
241
  // assertion (toHaveText('3')), not a count. An author reaching for "3 cards are shown"
221
242
  // gets an assertion that can only pass if the card's TEXT is the digit — a wrong test
@@ -295,20 +316,25 @@ export class StepMapper {
295
316
  this.templateEngine.setBaseContext({ inFrame: true, frameSelector });
296
317
  // C1 — routed through the adapter instead of building `page.frameLocator(...)` inline:
297
318
  // the Appium adapter's own template performs its webview-context switch instead.
298
- const code = this.templateEngine.renderStep('frame-enter-action', { frameSelector });
319
+ // `frameRef` carries the name as WRITTEN in the Gherkin: the Appium twin can fail (#705),
320
+ // and its error has to quote the step the author would go and look at, not the resolved
321
+ // xpath they never typed.
322
+ const code = this.templateEngine.renderStep('frame-enter-action', { frameSelector, frameRef: selectorRef });
299
323
  return { code, comment: `Switch to ${selectorRef} frame` };
300
324
  }
301
325
 
302
326
  // Dialog scope directives — intercept before pattern matching
303
327
  if (/\buse dialog\b/i.test(step.text)) {
304
328
  this.inDialogScope = true;
305
- this.templateEngine.setBaseContext({ inDialog: true });
329
+ // setBaseContext MERGES — an earlier scope's root keys in the same scenario would
330
+ // otherwise leak into this unnamed one.
331
+ this.templateEngine.setBaseContext({ inDialog: true, dialogScopeKey: undefined });
306
332
  this.warnDialogScopeIgnoredOnMobile(step);
307
333
  return { code: '', comment: 'Enter dialog scope' };
308
334
  }
309
335
  if (/\b(?:close|dismiss|exit)\s+dialog\b/i.test(step.text)) {
310
336
  this.inDialogScope = false;
311
- this.templateEngine.setBaseContext({ inDialog: false });
337
+ this.templateEngine.setBaseContext({ inDialog: false, dialogScopeKey: undefined });
312
338
  return { code: '', comment: 'Exit dialog scope' };
313
339
  }
314
340
  // `is on [X] <type>` supports exactly two shapes: `page` (URL from the page-type selector
@@ -344,6 +370,7 @@ export class StepMapper {
344
370
  dialogRootExpr: undefined,
345
371
  dialogRoleName: undefined,
346
372
  dialogFilterText: undefined,
373
+ dialogScopeKey: undefined,
347
374
  };
348
375
 
349
376
  if (step.selectorRef && step.dataRef) {
@@ -372,6 +399,16 @@ export class StepMapper {
372
399
  }
373
400
  // Case 1: no selectorRef → plain page.getByRole('dialog')
374
401
 
402
+ // Record what this root IS, so a later step naming the same element is recognized as the
403
+ // root rather than nested inside it (issue #692). The declared entry when there is one;
404
+ // otherwise the named-dialog fallback the element itself will resolve to.
405
+ if (step.selectorRef) {
406
+ const entry = this.resolveDialogEntry(step.selectorRef);
407
+ contextVars.dialogScopeKey = entry
408
+ ? locatorIdentityKey(entry)
409
+ : locatorIdentityKey({ strategy: 'role', role: 'dialog', name: step.selectorRef });
410
+ }
411
+
375
412
  this.templateEngine.setBaseContext(contextVars);
376
413
  return { code: '', comment: `Enter dialog scope${step.selectorRef ? ': ' + step.selectorRef : ''}` };
377
414
  }
@@ -427,10 +464,14 @@ export class StepMapper {
427
464
  this.checkPageAssertionNoOpOnMobile(step, mappedStep);
428
465
  this.checkSetupNoOpOnMobile(step, mappedStep);
429
466
  this.checkNavigationNoOpOnMobile(step, mappedStep);
430
- this.checkPageAssertionFallback(step, mappedStep);
467
+ this.checkPageStepFallback(step, mappedStep);
431
468
  this.checkDeclaredUrlHasNoQuery(step, mappedStep);
469
+ const positionDropped = this.checkClickPositionDropped(step, mappedStep);
470
+ if (positionDropped) return positionDropped;
432
471
  const swallowed = this.checkAssertionClauseSwallowed(step, mappedStep);
433
472
  if (swallowed) return swallowed;
473
+ const badType = this.checkUnknownSelectorType(step, mappedStep);
474
+ if (badType) return badType;
434
475
  return mappedStep;
435
476
  }
436
477
 
@@ -487,6 +528,27 @@ export class StepMapper {
487
528
  * falls back to the legacy named-dialog root, so behavior is unchanged for those entries.
488
529
  */
489
530
  private renderDialogRoot(selectorRef: string, opts: { nonRoleOnly?: boolean } = {}): string | undefined {
531
+ const resolved = this.resolveDialogEntry(selectorRef);
532
+ if (!resolved) return undefined;
533
+ if (!StepMapper.DIALOG_ROOT_STRATEGIES.includes(resolved.strategy)) return undefined;
534
+ if (opts.nonRoleOnly && resolved.strategy === 'role') return undefined;
535
+
536
+ // Render the root PAGE-rooted: leaving inDialog set (or `scope: dialog` on the entry
537
+ // itself) would send the partial back through dialog-root and nest the dialog in itself.
538
+ const expr = this.templateEngine.renderStep('locator', {
539
+ ...resolved,
540
+ scope: resolved.scope === 'dialog' ? undefined : resolved.scope,
541
+ inDialog: false,
542
+ }).trim();
543
+ return expr || undefined;
544
+ }
545
+
546
+ /**
547
+ * The selectors.yaml entry behind a dialog reference, normalized the way the `locator` partial
548
+ * needs it. Shared by `renderDialogRoot` (which turns it into the root expression) and the
549
+ * scope-entry path (which keys off it to recognize the root again later — issue #692).
550
+ */
551
+ private resolveDialogEntry(selectorRef: string): any | undefined {
490
552
  let resolved: any;
491
553
  try {
492
554
  resolved = this.selectorResolver.resolveSelector(selectorRef, this.featureName, 'dialog', 0);
@@ -500,34 +562,27 @@ export class StepMapper {
500
562
  if (resolved.strategy === 'css' && resolved.locator) {
501
563
  resolved = { ...resolved, strategy: 'locator', value: resolved.locator };
502
564
  }
503
- if (!StepMapper.DIALOG_ROOT_STRATEGIES.includes(resolved.strategy)) return undefined;
504
- if (opts.nonRoleOnly && resolved.strategy === 'role') return undefined;
505
-
506
- // Render the root PAGE-rooted: leaving inDialog set (or `scope: dialog` on the entry
507
- // itself) would send the partial back through dialog-root and nest the dialog in itself.
508
- const expr = this.templateEngine.renderStep('locator', {
509
- ...resolved,
510
- scope: resolved.scope === 'dialog' ? undefined : resolved.scope,
511
- inDialog: false,
512
- }).trim();
513
- return expr || undefined;
565
+ return resolved;
514
566
  }
515
567
 
516
568
  /**
517
- * SG-W013 — a page assertion whose path came from a fallback rather than from a `type: page`
518
- * selector entry. Both resolvers degrade quietly when `[Ref]` does not resolve: `see [X] page`
519
- * falls back to the feature's own path, `is on [X] page` to `/<ref>/`. Before the assertion was
520
- * anchored that produced a test which always PASSED; anchored, it produces one that can never
521
- * pass. Neither is the assertion the author wrote, and nothing said so.
569
+ * SG-W013 — a page STEP whose path came from a fallback rather than from a `type: page`
570
+ * selector entry. Every resolver degrades quietly when `[Ref]` does not resolve: `see [X] page`
571
+ * and `wait for [X] page` fall back to the feature's own path, `is on [X] page` to `/<ref>/`.
572
+ * Before the match was anchored that produced a test which always PASSED; anchored, it produces
573
+ * one that can never pass — an assertion that always fails, or, since #677 anchored the wait
574
+ * too, a wait that can only time out. None is the step the author wrote, and nothing said so.
522
575
  *
523
- * Detected here rather than in the resolver on purpose: `MappedStep` carries only
524
- * `{code, comment, imports}`, and the resolvers live in the driver packages while the collector
525
- * lives in core. Re-asking the SAME resolver the step-mapper already owns costs three lines and
576
+ * Detected here rather than in the resolver on purpose: the resolvers live in the driver packages
577
+ * while the collector lives in core (the mapped step now also names its pattern and carries the
578
+ * resolver's data, but the fallback itself is only visible by re-resolving the selector). Re-asking the SAME resolver the step-mapper already owns costs three lines and
526
579
  * duplicates no logic — the alternative is widening the adapter contract for every driver.
527
580
  */
528
- private checkPageAssertionFallback(step: ParsedStep, mapped: MappedStep): void {
581
+ private checkPageStepFallback(step: ParsedStep, mapped: MappedStep): void {
529
582
  if (!this.diagnostics || !step.selectorRef || step.elementType !== 'page') return;
530
- if (!/\b(?:urlMatches|expectPage)\(/.test(mapped.code || '')) return; // both URL-assertion shapes (#662)
583
+ // every shape that compiles a page path into an ANCHORED match: the two assertions (#662)
584
+ // and the wait (#677). A step that emitted none of them has no path to get wrong.
585
+ if (!/\b(?:urlMatches|expectPage|waitForPage)\(/.test(mapped.code || '')) return;
531
586
  let resolvedAsPage = false;
532
587
  try {
533
588
  const resolved = this.selectorResolver.resolveSelector(
@@ -539,7 +594,7 @@ export class StepMapper {
539
594
  }
540
595
  if (resolvedAsPage) return;
541
596
  this.diagnostics.add('SG-W013',
542
- `page assertion for "[${step.selectorRef}]" has no "type: page" selector entry, so it asserts a FALLBACK path — the anchored assertion cannot match the real URL. Declare the page, e.g. "${step.selectorRef.toLowerCase()}:\\n type: page\\n value: '/your/path'"`,
597
+ `page step "[${step.selectorRef}]" has no "type: page" selector entry, so it targets a FALLBACK path — the anchored match can never reach the real URL (an assertion always fails; a wait can only time out). Declare the page, e.g. "${step.selectorRef.toLowerCase()}:\\n type: page\\n value: '/your/path'"`,
543
598
  { feature: this.featureName, step: step.text });
544
599
  }
545
600
 
@@ -588,6 +643,43 @@ export class StepMapper {
588
643
  * Deliberately narrow — it fires only on the bare-visibility shape, so a pattern that genuinely
589
644
  * consumed the clause (`is-hidden`, `contains`, `matches-format`, the state family) is untouched.
590
645
  */
646
+ /**
647
+ * SG-E027 (#711) — the step names an entry whose `type:` is not a selector type, so the resolver
648
+ * fell to its placeholder default and this step compiled to `getByPlaceholder('<the value>')`:
649
+ * a locator that cannot match, which would fail much later as a timeout naming placeholder text
650
+ * the author never wrote. Compile it to a `throw` instead — the same reasoning as SG-E023 above,
651
+ * that a step which reads as a check and can only ever fail obscurely is worse than one that
652
+ * fails loudly and says why.
653
+ *
654
+ * The DIAGNOSTIC is not added here. `SelectorResolver.loadNewSelectorFile` already reported this
655
+ * entry once for the whole file; re-reporting per step would print the same line twenty times for
656
+ * one typo. This method only replaces the code, so the compile-time message and the runtime
657
+ * failure stay one fact reported once.
658
+ *
659
+ * A resolve throw means the key is missing entirely — a different defect that the resolver's own
660
+ * error already owns — and an auto-inferred selector stamps no `selectorType`, so both skip.
661
+ */
662
+ private checkUnknownSelectorType(step: ParsedStep, mapped: MappedStep): MappedStep | null {
663
+ if (!step.selectorRef) return null;
664
+ let selectorType: unknown;
665
+ try {
666
+ selectorType = this.selectorResolver.resolveSelector(
667
+ step.selectorRef, this.featureName, step.elementType, step.nth,
668
+ ).selectorType;
669
+ } catch {
670
+ return null; // missing key — the resolver's own error owns that
671
+ }
672
+ if (selectorType === undefined || isKnownSelectorType(selectorType)) return null;
673
+ const reason = `step "${step.text}" names selector "${step.selectorRef}", whose selectors.yaml `
674
+ + `entry declares type: "${String(selectorType)}" — not a selector type, so the entry `
675
+ + `resolved to a placeholder locator that cannot match. See SG-E027 for the valid types and `
676
+ + `the nearest spelling to what was written.`;
677
+ return {
678
+ code: unsupportedStepCode(reason),
679
+ comment: `Unknown selector type "${String(selectorType)}" — see SG-E027: ${step.text}`,
680
+ };
681
+ }
682
+
591
683
  private checkAssertionClauseSwallowed(step: ParsedStep, mapped: MappedStep): MappedStep | null {
592
684
  const code = mapped.code || '';
593
685
  // Bare visibility: nothing but a visible/displayed assertion on one locator.
@@ -630,6 +722,42 @@ export class StepMapper {
630
722
  };
631
723
  }
632
724
 
725
+ /**
726
+ * A click step that names a POSITION (`at position "top-left"`, `at offset x=…, y=…`) must compile
727
+ * through the positioned-click form with a position it understood. Anything else — a typo in the
728
+ * keyword, a value the grammar rejects, or a combination another click form claims first (`try to
729
+ * click … at position`, `… at offset … and accept [OK] alert`, `… with {{v}} … at position`) —
730
+ * would otherwise click the element's CENTRE and pass, which for a backdrop-dismiss test is the
731
+ * one outcome that proves nothing. Owned by the compiler rather than the driver so it holds no
732
+ * matter which pattern wins.
733
+ */
734
+ private static readonly CLICK_POSITION_HINT =
735
+ /\bat\s+(?:position|offset)\b|\bat\s+\w+\s+(?:"(?:top|bottom)-(?:left|right)"|x\s*=)|\b(?:top|bottom)[- ](?:left|right)\b|\bat\s+x\s*=/i;
736
+
737
+ private checkClickPositionDropped(step: ParsedStep, mapped: MappedStep): MappedStep | null {
738
+ // Refs are masked; so is every quoted VALUE except a preset name — `with "Engineer at position 3"`
739
+ // is a text filter, not a position, while a mistyped `at postion "top-left"` must stay visible.
740
+ const masked = maskRefsForMatching(step.text)
741
+ .replace(/"([^"]*)"/g, (quoted, inner: string) => (/^(?:top|bottom)-(?:left|right)$/i.test(inner.trim()) ? quoted : '"~"'));
742
+ if (!/\bclick/i.test(masked) || !StepMapper.CLICK_POSITION_HINT.test(masked)) return null;
743
+ // Already refused upstream (off-platform SG-W020, a dropped state clause, a missing template…):
744
+ // that refusal names the real cause; this guard only exists to stop a SILENT centre click.
745
+ if (/^throw new Error\("\[sungen\]/.test(mapped.code.trim())) return null;
746
+ const refused = mapped.templateData?.positionError as string | undefined;
747
+ if (mapped.pattern === 'click-element-at-position' && !refused) return null;
748
+ const reason = refused
749
+ ? `step "${step.text}": ${refused}`
750
+ : `step "${step.text}" asks for a click position but compiled through the "${mapped.pattern ?? 'unknown'}" `
751
+ + 'step form, which clicks the element\'s centre — the position would be silently dropped. Write '
752
+ + 'it as `click [X] <type> at position "<preset>"` or `… at offset x=<n>px|%, y=<n>px|%` on its own: '
753
+ + '`try to click`, a `with {{v}}` text filter and `… and accept [OK] alert` do not take a position.';
754
+ this.diagnostics?.add('SG-E026', reason, { feature: this.featureName, step: step.text });
755
+ return {
756
+ code: unsupportedStepCode(reason),
757
+ comment: `Click position dropped — see SG-E026: ${step.text}`,
758
+ };
759
+ }
760
+
633
761
  private checkDataRefConsumed(step: ParsedStep, mapped: MappedStep): void {
634
762
  if (!step.dataRef || !this.diagnostics) return;
635
763
  const code = mapped.code || '';
@@ -839,6 +967,22 @@ export class StepMapper {
839
967
  { feature: this.featureName, step: step.text });
840
968
  }
841
969
 
970
+ /**
971
+ * SG-W028 — a trailing `in [X] <word>` scope clause whose `<word>` is not a container type.
972
+ *
973
+ * The clause is dropped, and everything downstream takes the FIRST `[ref]` and the FIRST
974
+ * `{{data}}` it finds, so the sentence compiles clean while the assertion silently widens to the
975
+ * whole screen (#667). Unlike an unrecognized STEP, which at least raises SG-W001, a partially
976
+ * understood step used to say nothing at all — and a check that runs against more than it names
977
+ * passes for the wrong reason in exactly the list-of-identical-cards case it was written for.
978
+ */
979
+ private warnScopeClauseDiscarded(step: ParsedStep): void {
980
+ if (!step.unsupportedScopeType) return;
981
+ this.diagnostics?.add('SG-W028',
982
+ `\`${step.unsupportedScopeType}\` is not a container type, so the scope clause was DROPPED and this step runs against the whole page, not the named container. Supported: ${PARENT_CONTAINER_TYPES.join(', ')}`,
983
+ { feature: this.featureName, step: step.text });
984
+ }
985
+
842
986
  /**
843
987
  * SG-W021 — dialog scope is a no-op on the Appium adapter.
844
988
  *
@@ -1,7 +1,9 @@
1
1
  import Handlebars from 'handlebars';
2
+ import { escapeSingleQuoted } from './utils/escape-js-string';
2
3
  import fs from 'fs';
3
4
  import path from 'path';
4
- import type { RenderImportsOptions } from './adapters/adapter-interface';
5
+ import type { RenderImportsOptions, HookRenderData } from './adapters/adapter-interface';
6
+ import { locatorIdentityKey } from './utils/dialog-scope-identity';
5
7
 
6
8
  export class TemplateEngine {
7
9
  private templates: Map<string, HandlebarsTemplateDelegate> = new Map();
@@ -28,16 +30,9 @@ export class TemplateEngine {
28
30
  // the result in '…' (selector values, attribute values, scope), so a value containing a newline,
29
31
  // backslash, or quote must be escaped or the generated spec is a syntax error (e.g. a Flutter
30
32
  // composite content-desc like "Quick Convert\nSelect File\n…" used as an accessibility-id).
31
- // Order matters: backslash first, then the rest.
32
- Handlebars.registerHelper('escapeQuotes', function(text: string) {
33
- if (text == null) return '';
34
- return String(text)
35
- .replace(/\\/g, '\\\\')
36
- .replace(/'/g, "\\'")
37
- .replace(/\n/g, '\\n')
38
- .replace(/\r/g, '\\r')
39
- .replace(/\t/g, '\\t');
40
- });
33
+ // The rule itself lives in utils/escape-js-string so the pattern layer, which builds some
34
+ // locator strings by hand, cannot drift from what the templates do.
35
+ Handlebars.registerHelper('escapeQuotes', (text: string) => escapeSingleQuoted(text));
41
36
 
42
37
  // Escape a raw string for embedding inside a REGEX LITERAL (/…/). Metacharacters first —
43
38
  // that class doubles a backslash, so the control-char passes below cannot be mis-read as
@@ -152,6 +147,17 @@ export class TemplateEngine {
152
147
  // element is dialog-rooted even if its role is portal-exempt (issue #465).
153
148
  const explicitDialog = this.scope === 'dialog';
154
149
  if (!this.inDialog && !explicitDialog) return 'page';
150
+ // The element being resolved IS the active scope's own dialog root (issue #692). Prefixing
151
+ // it with that root asks for a dialog nested inside itself —
152
+ // getByRole('dialog',{name:X}).getByRole('dialog',{name:X}) — which can never resolve, yet
153
+ // compiles clean and fails as a plain timeout reading "the dialog never opened". So the
154
+ // root is idempotent: re-asserting the dialog you are scoped inside means what it reads as.
155
+ // Skipped for a `with {{v}}` root, whose .filter({ hasText }) form is not expressible as
156
+ // the element's own locator — identity says nothing about it there.
157
+ if (this.inDialog && !this.dialogFilterText && this.dialogScopeKey &&
158
+ locatorIdentityKey(this) === this.dialogScopeKey) {
159
+ return 'page';
160
+ }
155
161
  // Portal-rendered roles (Radix/MUI/Headless UI/antd render option/menu/tooltip
156
162
  // layers onto document.body) live OUTSIDE the dialog's DOM subtree — wrapping
157
163
  // them in the dialog root guarantees a timeout. List comes from the app profile
@@ -297,7 +303,7 @@ export class TemplateEngine {
297
303
  }
298
304
 
299
305
  renderImports(options?: RenderImportsOptions): string {
300
- return this.render('imports', { runtimeData: options?.runtimeData, basePath: options?.basePath || '..', isParallel: options?.isParallel, needsCleanupImport: options?.needsCleanupImport, needsDb: options?.needsDb, needsApi: options?.needsApi, needsMock: options?.needsMock, urlAssertImports: options?.urlAssertImports, regexEscapeImport: options?.regexEscapeImport });
306
+ return this.render('imports', { runtimeData: options?.runtimeData, basePath: options?.basePath || '..', isParallel: options?.isParallel, needsCleanupImport: options?.needsCleanupImport, needsDb: options?.needsDb, needsApi: options?.needsApi, needsMock: options?.needsMock, urlAssertImports: options?.urlAssertImports, regexEscapeImport: options?.regexEscapeImport, checkedStateImports: options?.checkedStateImports });
301
307
  }
302
308
 
303
309
  renderTestFile(data: {
@@ -332,21 +338,15 @@ export class TemplateEngine {
332
338
  return this.render('before-each', data);
333
339
  }
334
340
 
335
- renderBeforeAll(data: {
336
- steps: Array<{ comment?: string; code: string }>;
337
- }): string {
341
+ renderBeforeAll(data: HookRenderData): string {
338
342
  return this.render('before-all', data);
339
343
  }
340
344
 
341
- renderAfterEach(data: {
342
- steps: Array<{ comment?: string; code: string }>;
343
- }): string {
345
+ renderAfterEach(data: HookRenderData): string {
344
346
  return this.render('after-each', data);
345
347
  }
346
348
 
347
- renderAfterAll(data: {
348
- steps: Array<{ comment?: string; code: string }>;
349
- }): string {
349
+ renderAfterAll(data: HookRenderData): string {
350
350
  return this.render('after-all', data);
351
351
  }
352
352
 
@@ -29,6 +29,27 @@ export class DataResolver {
29
29
  this.capturedVars.clear();
30
30
  }
31
31
 
32
+ // Vars bound by a `@beforeAll` hook's `@api:`/`@query:` preconditions. The hook runs before every
33
+ // test of the feature, so its bindings are readable from EVERY scenario — they survive the
34
+ // per-scenario clearCaptured(). Stored by head (`login`) and matched against the ref's head
35
+ // (`login.body.token` → `login`), since a scenario may read any sub-path of the bound response.
36
+ private featureCaptured = new Set<string>();
37
+
38
+ /** Register the head of a variable bound by the feature's `@beforeAll` hook (feature-scoped). */
39
+ registerFeatureCaptured(head: string): void {
40
+ this.featureCaptured.add(head);
41
+ }
42
+
43
+ /** Clear feature-scoped captured vars — call at the start of each feature. */
44
+ clearFeatureCaptured(): void {
45
+ this.featureCaptured.clear();
46
+ }
47
+
48
+ /** Runtime-only ref (scenario-captured, or bound by the feature's `@beforeAll` hook)? */
49
+ private isCaptured(dataRef: string): boolean {
50
+ return this.capturedVars.has(dataRef) || this.featureCaptured.has(dataRef.split(/[.[]/)[0]);
51
+ }
52
+
32
53
  constructor(testDataDir?: string, screenName?: string, runtimeMode: boolean = false) {
33
54
  this.testDataDir = testDataDir || path.join(process.cwd(), 'qa', 'test-data');
34
55
  this.screenName = screenName;
@@ -52,7 +73,7 @@ export class DataResolver {
52
73
  resolveData(dataRef: string, featureName?: string): string {
53
74
  // Captured vars exist only at runtime → emit a marker (→ testData.get) and
54
75
  // skip YAML validation. Requires runtime mode (capture writes testData.set).
55
- if (this.capturedVars.has(dataRef)) {
76
+ if (this.isCaptured(dataRef)) {
56
77
  return DataResolver.encodeMarker(dataRef);
57
78
  }
58
79
 
@@ -75,7 +96,7 @@ export class DataResolver {
75
96
  * anything said about it belongs in a warning, never a gate.
76
97
  */
77
98
  peekBaseValue(dataRef: string, featureName?: string): string | undefined {
78
- if (this.capturedVars.has(dataRef)) return undefined;
99
+ if (this.isCaptured(dataRef)) return undefined;
79
100
  try {
80
101
  return this.resolveValue(dataRef, featureName);
81
102
  } catch {
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Identity of a locator as the `locator` partial actually renders it — issue #692.
3
+ *
4
+ * `Given User is on [X] dialog` records a scope root, and the `pageRoot` helper prefixes every
5
+ * locator rendered afterwards with it. That is the whole point of the scope — except when a later
6
+ * step names the SAME element as the scope root, where the prefix nests the dialog inside itself:
7
+ *
8
+ * page.getByRole('dialog', { name: 'Confirm' }).getByRole('dialog', { name: 'Confirm' })
9
+ *
10
+ * A dialog cannot contain itself, so that query can never resolve. It compiles clean, emits no
11
+ * diagnostic, and fails as a plain timeout — which reads as "the dialog never opened" rather than
12
+ * "the query is impossible". Re-asserting the dialog you are scoped inside is the natural thing to
13
+ * write straight after the `Given`, so the scope root is made idempotent instead.
14
+ *
15
+ * The guard needs exactly one question answered: does this element render to the same locator as
16
+ * the active scope root? So the key carries the fields the partial reads FOR THAT STRATEGY and
17
+ * nothing else — two entries share a key iff they render the same expression. Comparing role+name
18
+ * alone (the shape the issue first suggested) would miss `type: locator` and `type: testid`
19
+ * dialogs, which self-nest just the same.
20
+ *
21
+ * Deliberately says nothing about a `with {{v}}` filtered root: that root is
22
+ * `getByRole('dialog').filter({ hasText })`, which is not expressible as the element's own
23
+ * locator, so an identity match there would be meaningless. The caller skips the guard in that
24
+ * case — see the `dialogFilterText` check in `pageRoot`.
25
+ */
26
+
27
+ /** The subset of a resolved selector that the `locator` partial reads. */
28
+ export interface LocatorIdentityFields {
29
+ strategy?: string;
30
+ value?: string;
31
+ role?: string;
32
+ name?: string;
33
+ exact?: boolean;
34
+ nth?: number;
35
+ scope?: string;
36
+ }
37
+
38
+ /**
39
+ * A stable key for the locator `fields` renders to, or undefined when there is no strategy to
40
+ * render (nothing to compare, so no guard).
41
+ */
42
+ export function locatorIdentityKey(fields: LocatorIdentityFields | undefined): string | undefined {
43
+ if (!fields || !fields.strategy) return undefined;
44
+
45
+ // `scope: dialog` is the dialog-rooting sentinel rather than part of the element's own
46
+ // identity — renderDialogRoot strips it before rendering the root for the same reason.
47
+ const scope = fields.scope && fields.scope !== 'dialog' ? fields.scope : '';
48
+ const nth = typeof fields.nth === 'number' && fields.nth >= 0 ? String(fields.nth) : '';
49
+ const exact = fields.exact ? '1' : '';
50
+
51
+ const parts: string[] = [];
52
+ switch (fields.strategy) {
53
+ case 'role':
54
+ // getByRole reads role/name/exact — never `value`.
55
+ parts.push('role', fields.role || '', fields.name || '', exact);
56
+ break;
57
+ case 'testid':
58
+ parts.push('testid', fields.value || '');
59
+ break;
60
+ case 'placeholder':
61
+ case 'label':
62
+ case 'text':
63
+ parts.push(fields.strategy, fields.value || '', exact);
64
+ break;
65
+ default:
66
+ // 'locator', 'id', 'css' and the partial's own default all render locator('<value>').
67
+ parts.push('locator', fields.value || '');
68
+ }
69
+
70
+ return [...parts, `nth=${nth}`, `scope=${scope}`].join('|');
71
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Escape a raw string for embedding inside a SINGLE-QUOTED JS string literal.
3
+ *
4
+ * Order matters: backslash first, or the escapes added afterwards get re-escaped. Getting this
5
+ * half-right is worse than not doing it — a value ending in `\` turns `'…\'` into an escaped
6
+ * closing quote, and the string literal runs on into the rest of the file, so the generated
7
+ * `.spec.ts` stops parsing and EVERY scenario in it is lost rather than the one step.
8
+ *
9
+ * The line terminators are not cosmetic either: a card's accessible text is routinely a composite
10
+ * of several lines (a Flutter content-desc, a multi-line heading), and a literal newline inside
11
+ * `'…'` is a syntax error.
12
+ *
13
+ * Lives here rather than inside the Handlebars helper because the pattern layer also builds locator
14
+ * strings by hand — the parent-scope chain among them — and two copies of this rule drift.
15
+ */
16
+ export function escapeSingleQuoted(text: unknown): string {
17
+ if (text == null) return '';
18
+ return String(text)
19
+ .replace(/\\/g, '\\\\')
20
+ .replace(/'/g, "\\'")
21
+ .replace(/\n/g, '\\n')
22
+ .replace(/\r/g, '\\r')
23
+ .replace(/\t/g, '\\t');
24
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Nearest-known-spelling suggestion, shared by the selector-surface checks.
3
+ *
4
+ * Two checks need the same judgement about the same class of author mistake — SG-W004 on an entry
5
+ * FIELD nobody reads (#712) and SG-E027 on an entry `type:` VALUE nothing dispatches (#711) — so
6
+ * the rule lives in one place rather than being re-tuned independently in each. The tuning is the
7
+ * substance here, not the edit distance:
8
+ *
9
+ * - A case-only difference is returned first and always. `testId` for `testid` and
10
+ * `contentEditable` for `contenteditable` are the single likeliest way to write either
11
+ * wrongly, and both surfaces are lowercase throughout, so this is a near-certainty rather
12
+ * than a guess.
13
+ * - Otherwise a distance of 1-2 is a typo worth naming.
14
+ * - Anything further is a DIFFERENT word, and guessing at it sends the author somewhere wrong.
15
+ * `selector` is not a misspelling of `locator` — it is someone reasonably assuming a field
16
+ * that does not exist, and "did you mean locator?" would be a worse answer than silence.
17
+ * The caller supplies its own hint for that case.
18
+ */
19
+
20
+ /** Levenshtein distance. Bounded use only — these are short identifiers. */
21
+ function distance(a: string, b: string): number {
22
+ const m = a.length, n = b.length;
23
+ let prev = Array.from({ length: n + 1 }, (_, j) => j);
24
+ for (let i = 1; i <= m; i++) {
25
+ const curr = [i];
26
+ for (let j = 1; j <= n; j++) {
27
+ curr[j] = Math.min(
28
+ prev[j] + 1,
29
+ curr[j - 1] + 1,
30
+ prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1),
31
+ );
32
+ }
33
+ prev = curr;
34
+ }
35
+ return prev[n];
36
+ }
37
+
38
+ /**
39
+ * The nearest candidate to `value`, when one is close enough to be a likely typo rather than a
40
+ * different word. Returns undefined when nothing is close enough to name honestly.
41
+ */
42
+ export function nearestMatch(
43
+ value: string, candidates: Iterable<string>, maxDistance = 2,
44
+ ): string | undefined {
45
+ const lower = value.toLowerCase();
46
+ const all = [...candidates];
47
+ for (const c of all) if (c.toLowerCase() === lower) return c;
48
+ let best: string | undefined;
49
+ let bestDist = Infinity;
50
+ for (const c of all) {
51
+ const d = distance(lower, c.toLowerCase());
52
+ if (d < bestDist) { bestDist = d; best = c; }
53
+ }
54
+ return bestDist <= maxDistance ? best : undefined;
55
+ }