@sun-asterisk/sungen 3.2.31 → 3.2.33

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 (363) hide show
  1. package/dist/cli/commands/audit.d.ts +1 -0
  2. package/dist/cli/commands/audit.d.ts.map +1 -1
  3. package/dist/cli/commands/audit.js +13 -2
  4. package/dist/cli/commands/audit.js.map +1 -1
  5. package/dist/cli/commands/capability.d.ts.map +1 -1
  6. package/dist/cli/commands/capability.js +46 -17
  7. package/dist/cli/commands/capability.js.map +1 -1
  8. package/dist/cli/commands/delivery.d.ts.map +1 -1
  9. package/dist/cli/commands/delivery.js +39 -18
  10. package/dist/cli/commands/delivery.js.map +1 -1
  11. package/dist/cli/commands/generate.d.ts.map +1 -1
  12. package/dist/cli/commands/generate.js +14 -7
  13. package/dist/cli/commands/generate.js.map +1 -1
  14. package/dist/cli/commands/init.d.ts.map +1 -1
  15. package/dist/cli/commands/init.js +2 -0
  16. package/dist/cli/commands/init.js.map +1 -1
  17. package/dist/cli/commands/results-merge.d.ts.map +1 -1
  18. package/dist/cli/commands/results-merge.js +2 -12
  19. package/dist/cli/commands/results-merge.js.map +1 -1
  20. package/dist/cli/commands/results-sanitize.d.ts.map +1 -1
  21. package/dist/cli/commands/results-sanitize.js +2 -13
  22. package/dist/cli/commands/results-sanitize.js.map +1 -1
  23. package/dist/cli/commands/setup-mobile-env.d.ts +15 -0
  24. package/dist/cli/commands/setup-mobile-env.d.ts.map +1 -0
  25. package/dist/cli/commands/setup-mobile-env.js +65 -0
  26. package/dist/cli/commands/setup-mobile-env.js.map +1 -0
  27. package/dist/cli/index.js +2 -0
  28. package/dist/cli/index.js.map +1 -1
  29. package/dist/dashboard/snapshot-builder.d.ts.map +1 -1
  30. package/dist/dashboard/snapshot-builder.js +6 -10
  31. package/dist/dashboard/snapshot-builder.js.map +1 -1
  32. package/dist/exporters/feature-parser.d.ts +21 -0
  33. package/dist/exporters/feature-parser.d.ts.map +1 -1
  34. package/dist/exporters/feature-parser.js +32 -1
  35. package/dist/exporters/feature-parser.js.map +1 -1
  36. package/dist/exporters/locale-variants.d.ts +7 -1
  37. package/dist/exporters/locale-variants.d.ts.map +1 -1
  38. package/dist/exporters/locale-variants.js +23 -16
  39. package/dist/exporters/locale-variants.js.map +1 -1
  40. package/dist/exporters/matrix/build.d.ts.map +1 -1
  41. package/dist/exporters/matrix/build.js +13 -7
  42. package/dist/exporters/matrix/build.js.map +1 -1
  43. package/dist/exporters/matrix/carry-manual-results.d.ts +70 -0
  44. package/dist/exporters/matrix/carry-manual-results.d.ts.map +1 -0
  45. package/dist/exporters/matrix/carry-manual-results.js +232 -0
  46. package/dist/exporters/matrix/carry-manual-results.js.map +1 -0
  47. package/dist/exporters/matrix/export.d.ts +2 -0
  48. package/dist/exporters/matrix/export.d.ts.map +1 -1
  49. package/dist/exporters/matrix/export.js +7 -1
  50. package/dist/exporters/matrix/export.js.map +1 -1
  51. package/dist/exporters/matrix/wording.d.ts +7 -0
  52. package/dist/exporters/matrix/wording.d.ts.map +1 -1
  53. package/dist/exporters/matrix/wording.js +27 -1
  54. package/dist/exporters/matrix/wording.js.map +1 -1
  55. package/dist/exporters/xlsx-exporter.d.ts +2 -0
  56. package/dist/exporters/xlsx-exporter.d.ts.map +1 -1
  57. package/dist/exporters/xlsx-exporter.js +7 -2
  58. package/dist/exporters/xlsx-exporter.js.map +1 -1
  59. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/reinstall-app-action.hbs +2 -2
  60. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/all-contain-assertion.hbs +1 -2
  61. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/all-contain-element.hbs +8 -5
  62. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/count-assertion.hbs +5 -1
  63. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/disabled-with-filter-assertion.hbs +1 -1
  64. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/disabled-with-role-variable-assertion.hbs +1 -1
  65. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/hidden-with-filter-assertion.hbs +1 -1
  66. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/hidden-with-role-variable-assertion.hbs +1 -1
  67. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/label-value-assertion.hbs +8 -2
  68. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/list-item-count-assertion.hbs +5 -2
  69. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/state-with-filter-assertion.hbs +1 -1
  70. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/visible-filtered-assertion.hbs +1 -1
  71. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/visible-with-role-variable-assertion.hbs +7 -3
  72. package/dist/generators/test-generator/adapters/appium/templates/steps/partials/await-elements.hbs +12 -0
  73. package/dist/generators/test-generator/adapters/appium/templates/test-file.hbs +94 -4
  74. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/capture-variable.hbs +7 -1
  75. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/label-value-assertion.hbs +7 -1
  76. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/row-scoped-column-assertion.hbs +23 -1
  77. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/visible-with-role-variable-assertion.hbs +8 -4
  78. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator-base.hbs +4 -4
  79. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator-strategies/default.hbs +1 -1
  80. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator-strategies/id.hbs +1 -1
  81. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator.hbs +4 -4
  82. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/url-assertion.hbs +17 -2
  83. package/dist/generators/test-generator/adapters/playwright/templates/test-file.hbs +14 -0
  84. package/dist/generators/test-generator/code-generator.d.ts +2 -1
  85. package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
  86. package/dist/generators/test-generator/code-generator.js +122 -15
  87. package/dist/generators/test-generator/code-generator.js.map +1 -1
  88. package/dist/generators/test-generator/diagnostics.d.ts +70 -4
  89. package/dist/generators/test-generator/diagnostics.d.ts.map +1 -1
  90. package/dist/generators/test-generator/diagnostics.js +69 -3
  91. package/dist/generators/test-generator/diagnostics.js.map +1 -1
  92. package/dist/generators/test-generator/patterns/index.d.ts +13 -3
  93. package/dist/generators/test-generator/patterns/index.d.ts.map +1 -1
  94. package/dist/generators/test-generator/patterns/index.js +16 -10
  95. package/dist/generators/test-generator/patterns/index.js.map +1 -1
  96. package/dist/generators/test-generator/patterns/types.d.ts +15 -0
  97. package/dist/generators/test-generator/patterns/types.d.ts.map +1 -1
  98. package/dist/generators/test-generator/patterns/with-parent-scope.d.ts +5 -0
  99. package/dist/generators/test-generator/patterns/with-parent-scope.d.ts.map +1 -1
  100. package/dist/generators/test-generator/patterns/with-parent-scope.js +6 -0
  101. package/dist/generators/test-generator/patterns/with-parent-scope.js.map +1 -1
  102. package/dist/generators/test-generator/step-mapper.d.ts +81 -0
  103. package/dist/generators/test-generator/step-mapper.d.ts.map +1 -1
  104. package/dist/generators/test-generator/step-mapper.js +187 -13
  105. package/dist/generators/test-generator/step-mapper.js.map +1 -1
  106. package/dist/generators/test-generator/template-engine.d.ts.map +1 -1
  107. package/dist/generators/test-generator/template-engine.js +18 -0
  108. package/dist/generators/test-generator/template-engine.js.map +1 -1
  109. package/dist/generators/test-generator/utils/data-resolver.d.ts +25 -0
  110. package/dist/generators/test-generator/utils/data-resolver.d.ts.map +1 -1
  111. package/dist/generators/test-generator/utils/data-resolver.js +39 -8
  112. package/dist/generators/test-generator/utils/data-resolver.js.map +1 -1
  113. package/dist/generators/test-generator/utils/selector-entry-fields.d.ts.map +1 -1
  114. package/dist/generators/test-generator/utils/selector-entry-fields.js +1 -0
  115. package/dist/generators/test-generator/utils/selector-entry-fields.js.map +1 -1
  116. package/dist/generators/test-generator/utils/selector-resolver.d.ts +84 -4
  117. package/dist/generators/test-generator/utils/selector-resolver.d.ts.map +1 -1
  118. package/dist/generators/test-generator/utils/selector-resolver.js +229 -82
  119. package/dist/generators/test-generator/utils/selector-resolver.js.map +1 -1
  120. package/dist/harness/audit.d.ts.map +1 -1
  121. package/dist/harness/audit.js +35 -8
  122. package/dist/harness/audit.js.map +1 -1
  123. package/dist/harness/capability.d.ts +7 -1
  124. package/dist/harness/capability.d.ts.map +1 -1
  125. package/dist/harness/capability.js +15 -12
  126. package/dist/harness/capability.js.map +1 -1
  127. package/dist/harness/catalog/universal-viewpoints.yaml +5 -5
  128. package/dist/harness/config-file.d.ts +17 -0
  129. package/dist/harness/config-file.d.ts.map +1 -0
  130. package/dist/harness/config-file.js +100 -0
  131. package/dist/harness/config-file.js.map +1 -0
  132. package/dist/harness/depth-lint.d.ts.map +1 -1
  133. package/dist/harness/depth-lint.js +2 -1
  134. package/dist/harness/depth-lint.js.map +1 -1
  135. package/dist/harness/flow-check.d.ts.map +1 -1
  136. package/dist/harness/flow-check.js +6 -2
  137. package/dist/harness/flow-check.js.map +1 -1
  138. package/dist/harness/keyword-match.d.ts +59 -0
  139. package/dist/harness/keyword-match.d.ts.map +1 -0
  140. package/dist/harness/keyword-match.js +86 -0
  141. package/dist/harness/keyword-match.js.map +1 -0
  142. package/dist/harness/next-step.d.ts.map +1 -1
  143. package/dist/harness/next-step.js +2 -1
  144. package/dist/harness/next-step.js.map +1 -1
  145. package/dist/harness/parse.d.ts.map +1 -1
  146. package/dist/harness/parse.js +42 -7
  147. package/dist/harness/parse.js.map +1 -1
  148. package/dist/harness/perf.d.ts +7 -1
  149. package/dist/harness/perf.d.ts.map +1 -1
  150. package/dist/harness/perf.js +13 -10
  151. package/dist/harness/perf.js.map +1 -1
  152. package/dist/harness/quality-gates.d.ts +9 -2
  153. package/dist/harness/quality-gates.d.ts.map +1 -1
  154. package/dist/harness/quality-gates.js +16 -4
  155. package/dist/harness/quality-gates.js.map +1 -1
  156. package/dist/harness/query-catalog.js +0 -0
  157. package/dist/harness/query-catalog.js.map +1 -1
  158. package/dist/harness/read-text.d.ts +6 -0
  159. package/dist/harness/read-text.d.ts.map +1 -1
  160. package/dist/harness/read-text.js +7 -1
  161. package/dist/harness/read-text.js.map +1 -1
  162. package/dist/harness/secret-scan.d.ts +27 -1
  163. package/dist/harness/secret-scan.d.ts.map +1 -1
  164. package/dist/harness/secret-scan.js +57 -4
  165. package/dist/harness/secret-scan.js.map +1 -1
  166. package/dist/harness/sensors.d.ts +12 -0
  167. package/dist/harness/sensors.d.ts.map +1 -1
  168. package/dist/harness/sensors.js +41 -18
  169. package/dist/harness/sensors.js.map +1 -1
  170. package/dist/harness/spec-branches.d.ts +7 -20
  171. package/dist/harness/spec-branches.d.ts.map +1 -1
  172. package/dist/harness/spec-branches.js +85 -13
  173. package/dist/harness/spec-branches.js.map +1 -1
  174. package/dist/harness/spec-coverage.d.ts +16 -0
  175. package/dist/harness/spec-coverage.d.ts.map +1 -1
  176. package/dist/harness/spec-coverage.js +131 -12
  177. package/dist/harness/spec-coverage.js.map +1 -1
  178. package/dist/harness/viewpoint-ledger.d.ts.map +1 -1
  179. package/dist/harness/viewpoint-ledger.js +2 -1
  180. package/dist/harness/viewpoint-ledger.js.map +1 -1
  181. package/dist/harness/write-atomic.d.ts +2 -0
  182. package/dist/harness/write-atomic.d.ts.map +1 -0
  183. package/dist/harness/write-atomic.js +62 -0
  184. package/dist/harness/write-atomic.js.map +1 -0
  185. package/dist/orchestrator/ai-skills/writer.d.ts.map +1 -1
  186. package/dist/orchestrator/ai-skills/writer.js +15 -7
  187. package/dist/orchestrator/ai-skills/writer.js.map +1 -1
  188. package/dist/orchestrator/appium-environment-setup.d.ts +24 -0
  189. package/dist/orchestrator/appium-environment-setup.d.ts.map +1 -0
  190. package/dist/orchestrator/appium-environment-setup.js +157 -0
  191. package/dist/orchestrator/appium-environment-setup.js.map +1 -0
  192. package/dist/orchestrator/assets-drift.d.ts.map +1 -1
  193. package/dist/orchestrator/assets-drift.js +11 -6
  194. package/dist/orchestrator/assets-drift.js.map +1 -1
  195. package/dist/orchestrator/mobile-runtime-scaffolder.d.ts +2 -0
  196. package/dist/orchestrator/mobile-runtime-scaffolder.d.ts.map +1 -1
  197. package/dist/orchestrator/mobile-runtime-scaffolder.js.map +1 -1
  198. package/dist/orchestrator/project-deps-sync.d.ts.map +1 -1
  199. package/dist/orchestrator/project-deps-sync.js +22 -1
  200. package/dist/orchestrator/project-deps-sync.js.map +1 -1
  201. package/dist/orchestrator/project-initializer.d.ts +9 -0
  202. package/dist/orchestrator/project-initializer.d.ts.map +1 -1
  203. package/dist/orchestrator/project-initializer.js +63 -7
  204. package/dist/orchestrator/project-initializer.js.map +1 -1
  205. package/dist/orchestrator/security-overrides-catalog.d.ts +39 -0
  206. package/dist/orchestrator/security-overrides-catalog.d.ts.map +1 -0
  207. package/dist/orchestrator/security-overrides-catalog.js +63 -0
  208. package/dist/orchestrator/security-overrides-catalog.js.map +1 -0
  209. package/dist/orchestrator/security-overrides.d.ts +51 -0
  210. package/dist/orchestrator/security-overrides.d.ts.map +1 -0
  211. package/dist/orchestrator/security-overrides.js +165 -0
  212. package/dist/orchestrator/security-overrides.js.map +1 -0
  213. package/dist/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +14 -0
  214. package/dist/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +8 -2
  215. package/dist/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +64 -2
  216. package/dist/orchestrator/templates/ai-src/skills/sungen-selector-keys/SKILL.md +13 -1
  217. package/dist/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +20 -5
  218. package/dist/orchestrator/templates/ai-src/skills/sungen-tc-review/SKILL.md +3 -3
  219. package/dist/orchestrator/templates/appium-environment/README.md +83 -0
  220. package/dist/orchestrator/templates/appium-environment/scripts/common/install-appium.ps1 +44 -0
  221. package/dist/orchestrator/templates/appium-environment/scripts/common/install-appium.sh +50 -0
  222. package/dist/orchestrator/templates/appium-environment/scripts/common/lib.ps1 +108 -0
  223. package/dist/orchestrator/templates/appium-environment/scripts/common/lib.sh +141 -0
  224. package/dist/orchestrator/templates/appium-environment/scripts/common/verify-appium.ps1 +39 -0
  225. package/dist/orchestrator/templates/appium-environment/scripts/common/verify-appium.sh +43 -0
  226. package/dist/orchestrator/templates/appium-environment/scripts/common/verify-node.ps1 +13 -0
  227. package/dist/orchestrator/templates/appium-environment/scripts/common/verify-node.sh +16 -0
  228. package/dist/orchestrator/templates/appium-environment/scripts/macos/install-android.sh +110 -0
  229. package/dist/orchestrator/templates/appium-environment/scripts/macos/install-ios.sh +32 -0
  230. package/dist/orchestrator/templates/appium-environment/scripts/macos/verify-android.sh +32 -0
  231. package/dist/orchestrator/templates/appium-environment/scripts/macos/verify-ios.sh +15 -0
  232. package/dist/orchestrator/templates/appium-environment/scripts/windows/install-android.ps1 +153 -0
  233. package/dist/orchestrator/templates/appium-environment/scripts/windows/verify-android.ps1 +26 -0
  234. package/dist/orchestrator/templates/appium-environment/setup.ps1 +58 -0
  235. package/dist/orchestrator/templates/appium-environment/setup.sh +88 -0
  236. package/dist/orchestrator/templates/specs-checked-state.d.ts +1 -1
  237. package/dist/orchestrator/templates/specs-checked-state.d.ts.map +1 -1
  238. package/dist/orchestrator/templates/specs-checked-state.js +26 -3
  239. package/dist/orchestrator/templates/specs-checked-state.js.map +1 -1
  240. package/dist/orchestrator/templates/specs-checked-state.ts +31 -6
  241. package/dist/orchestrator/templates/specs-locale-fixture.ts +21 -9
  242. package/dist/orchestrator/templates/specs-mock.d.ts +2 -1
  243. package/dist/orchestrator/templates/specs-mock.d.ts.map +1 -1
  244. package/dist/orchestrator/templates/specs-mock.js +10 -3
  245. package/dist/orchestrator/templates/specs-mock.js.map +1 -1
  246. package/dist/orchestrator/templates/specs-mock.ts +12 -4
  247. package/dist/orchestrator/templates/specs-pw-shape-reporter.ts +0 -0
  248. package/dist/orchestrator/templates/specs-url-assert.d.ts +17 -1
  249. package/dist/orchestrator/templates/specs-url-assert.d.ts.map +1 -1
  250. package/dist/orchestrator/templates/specs-url-assert.js +14 -2
  251. package/dist/orchestrator/templates/specs-url-assert.js.map +1 -1
  252. package/dist/orchestrator/templates/specs-url-assert.ts +24 -2
  253. package/dist/orchestrator/templates/wdio.conf.ts +24 -1
  254. package/package.json +3 -3
  255. package/src/cli/commands/audit.ts +12 -3
  256. package/src/cli/commands/capability.ts +63 -19
  257. package/src/cli/commands/delivery.ts +38 -16
  258. package/src/cli/commands/generate.ts +15 -7
  259. package/src/cli/commands/init.ts +3 -0
  260. package/src/cli/commands/results-merge.ts +2 -8
  261. package/src/cli/commands/results-sanitize.ts +2 -9
  262. package/src/cli/commands/setup-mobile-env.ts +64 -0
  263. package/src/cli/index.ts +2 -0
  264. package/src/dashboard/snapshot-builder.ts +6 -8
  265. package/src/exporters/feature-parser.ts +32 -2
  266. package/src/exporters/locale-variants.ts +23 -17
  267. package/src/exporters/matrix/build.ts +14 -8
  268. package/src/exporters/matrix/carry-manual-results.ts +229 -0
  269. package/src/exporters/matrix/export.ts +10 -4
  270. package/src/exporters/matrix/wording.ts +26 -1
  271. package/src/exporters/xlsx-exporter.ts +7 -2
  272. package/src/generators/test-generator/adapters/adapter-interface.ts +2 -2
  273. package/src/generators/test-generator/adapters/appium/templates/steps/actions/reinstall-app-action.hbs +2 -2
  274. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/all-contain-assertion.hbs +1 -2
  275. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/all-contain-element.hbs +8 -5
  276. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/count-assertion.hbs +5 -1
  277. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/disabled-with-filter-assertion.hbs +1 -1
  278. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/disabled-with-role-variable-assertion.hbs +1 -1
  279. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/hidden-with-filter-assertion.hbs +1 -1
  280. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/hidden-with-role-variable-assertion.hbs +1 -1
  281. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/label-value-assertion.hbs +8 -2
  282. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/list-item-count-assertion.hbs +5 -2
  283. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/state-with-filter-assertion.hbs +1 -1
  284. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/visible-filtered-assertion.hbs +1 -1
  285. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/visible-with-role-variable-assertion.hbs +7 -3
  286. package/src/generators/test-generator/adapters/appium/templates/steps/partials/await-elements.hbs +12 -0
  287. package/src/generators/test-generator/adapters/appium/templates/test-file.hbs +94 -4
  288. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/capture-variable.hbs +7 -1
  289. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/label-value-assertion.hbs +7 -1
  290. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/row-scoped-column-assertion.hbs +23 -1
  291. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/visible-with-role-variable-assertion.hbs +8 -4
  292. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator-base.hbs +4 -4
  293. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator-strategies/default.hbs +1 -1
  294. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator-strategies/id.hbs +1 -1
  295. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator.hbs +4 -4
  296. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/url-assertion.hbs +17 -2
  297. package/src/generators/test-generator/adapters/playwright/templates/test-file.hbs +14 -0
  298. package/src/generators/test-generator/code-generator.ts +119 -19
  299. package/src/generators/test-generator/diagnostics.ts +70 -4
  300. package/src/generators/test-generator/patterns/index.ts +16 -9
  301. package/src/generators/test-generator/patterns/types.ts +15 -0
  302. package/src/generators/test-generator/patterns/with-parent-scope.ts +6 -0
  303. package/src/generators/test-generator/step-mapper.ts +196 -13
  304. package/src/generators/test-generator/template-engine.ts +18 -0
  305. package/src/generators/test-generator/utils/data-resolver.ts +41 -12
  306. package/src/generators/test-generator/utils/selector-entry-fields.ts +1 -0
  307. package/src/generators/test-generator/utils/selector-resolver.ts +256 -85
  308. package/src/harness/audit.ts +41 -12
  309. package/src/harness/capability.ts +23 -12
  310. package/src/harness/catalog/universal-viewpoints.yaml +5 -5
  311. package/src/harness/config-file.ts +66 -0
  312. package/src/harness/depth-lint.ts +2 -1
  313. package/src/harness/flow-check.ts +6 -2
  314. package/src/harness/keyword-match.ts +85 -0
  315. package/src/harness/next-step.ts +2 -1
  316. package/src/harness/parse.ts +42 -7
  317. package/src/harness/perf.ts +15 -7
  318. package/src/harness/quality-gates.ts +16 -4
  319. package/src/harness/query-catalog.ts +0 -0
  320. package/src/harness/read-text.ts +7 -1
  321. package/src/harness/secret-scan.ts +60 -4
  322. package/src/harness/sensors.ts +50 -19
  323. package/src/harness/spec-branches.ts +72 -14
  324. package/src/harness/spec-coverage.ts +133 -13
  325. package/src/harness/viewpoint-ledger.ts +2 -1
  326. package/src/harness/write-atomic.ts +22 -0
  327. package/src/orchestrator/ai-skills/writer.ts +18 -3
  328. package/src/orchestrator/appium-environment-setup.ts +127 -0
  329. package/src/orchestrator/assets-drift.ts +10 -5
  330. package/src/orchestrator/mobile-runtime-scaffolder.ts +2 -0
  331. package/src/orchestrator/project-deps-sync.ts +23 -1
  332. package/src/orchestrator/project-initializer.ts +69 -7
  333. package/src/orchestrator/security-overrides-catalog.ts +69 -0
  334. package/src/orchestrator/security-overrides.ts +149 -0
  335. package/src/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +14 -0
  336. package/src/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +8 -2
  337. package/src/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +64 -2
  338. package/src/orchestrator/templates/ai-src/skills/sungen-selector-keys/SKILL.md +13 -1
  339. package/src/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +20 -5
  340. package/src/orchestrator/templates/ai-src/skills/sungen-tc-review/SKILL.md +3 -3
  341. package/src/orchestrator/templates/appium-environment/README.md +83 -0
  342. package/src/orchestrator/templates/appium-environment/scripts/common/install-appium.ps1 +44 -0
  343. package/src/orchestrator/templates/appium-environment/scripts/common/install-appium.sh +50 -0
  344. package/src/orchestrator/templates/appium-environment/scripts/common/lib.ps1 +108 -0
  345. package/src/orchestrator/templates/appium-environment/scripts/common/lib.sh +141 -0
  346. package/src/orchestrator/templates/appium-environment/scripts/common/verify-appium.ps1 +39 -0
  347. package/src/orchestrator/templates/appium-environment/scripts/common/verify-appium.sh +43 -0
  348. package/src/orchestrator/templates/appium-environment/scripts/common/verify-node.ps1 +13 -0
  349. package/src/orchestrator/templates/appium-environment/scripts/common/verify-node.sh +16 -0
  350. package/src/orchestrator/templates/appium-environment/scripts/macos/install-android.sh +110 -0
  351. package/src/orchestrator/templates/appium-environment/scripts/macos/install-ios.sh +32 -0
  352. package/src/orchestrator/templates/appium-environment/scripts/macos/verify-android.sh +32 -0
  353. package/src/orchestrator/templates/appium-environment/scripts/macos/verify-ios.sh +15 -0
  354. package/src/orchestrator/templates/appium-environment/scripts/windows/install-android.ps1 +153 -0
  355. package/src/orchestrator/templates/appium-environment/scripts/windows/verify-android.ps1 +26 -0
  356. package/src/orchestrator/templates/appium-environment/setup.ps1 +58 -0
  357. package/src/orchestrator/templates/appium-environment/setup.sh +88 -0
  358. package/src/orchestrator/templates/specs-checked-state.ts +31 -6
  359. package/src/orchestrator/templates/specs-locale-fixture.ts +21 -9
  360. package/src/orchestrator/templates/specs-mock.ts +12 -4
  361. package/src/orchestrator/templates/specs-pw-shape-reporter.ts +0 -0
  362. package/src/orchestrator/templates/specs-url-assert.ts +24 -2
  363. package/src/orchestrator/templates/wdio.conf.ts +24 -1
@@ -7,7 +7,7 @@
7
7
  {
8
8
  const __expected = String('{{escapeQuotes dataValue}}').trim();
9
9
  const __norm = (s: string) => s.replace(/\s+/g, ' ').trim().toLowerCase();
10
- const __items = await $$({{> appium-selector-expr}});
10
+ const __items = {{> await-elements mustExist=true}};
11
11
  let __found = false;
12
12
  for (const __el of __items) {
13
13
  const __sources = driver.isIOS
@@ -1,13 +1,17 @@
1
1
  {{!-- Role variable, visible (mobile best-effort): ARIA role has no native-mobile analog — reuse
2
2
  the role→accessibility-id mapping in appium-selector.hbs (`~name`) when a static name
3
3
  exists; the resolver already folds dataValue into name when there is none, so this is only
4
- the fallback for a caller that omits both. --}}
4
+ the fallback for a caller that omits both.
5
+
6
+ SCOPE (#747): an `in [Parent] <type>` clause chains this under the parent element. Both
7
+ branches locate by accessibility id / predicate, neither of which carries the absolute-xpath
8
+ trap the label-value twin has to work around. --}}
5
9
  {{#if name}}
6
- await expect($('~{{escapeQuotes name}}')).toBeDisplayed();
10
+ await expect({{#if parentLocator}}{{parentLocator}}.$('~{{escapeQuotes name}}'){{else}}$('~{{escapeQuotes name}}'){{/if}}).toBeDisplayed();
7
11
  {{else}}
8
12
  {
9
13
  const __expected = String('{{escapeQuotes dataValue}}').trim();
10
14
  const __sel = driver.isIOS ? `-ios predicate string:label == "${__expected}" OR value == "${__expected}"` : `android=new UiSelector().text("${__expected}")`;
11
- await expect($(__sel)).toBeDisplayed();
15
+ await expect({{#if parentLocator}}{{parentLocator}}.$(__sel){{else}}$(__sel){{/if}}).toBeDisplayed();
12
16
  }
13
17
  {{/if}}
@@ -0,0 +1,12 @@
1
+ {{~!--
2
+ The settled read behind every collection assertion on this adapter. `await $$(sel)` is a one-shot
3
+ read, so a loop over its result asserts nothing at all when the list has not painted yet — the
4
+ false-green class #672 closed on the Playwright side and left here.
5
+
6
+ Hash params:
7
+ mustExist the assertion is meaningless over an empty set (positive assertions). Omitted means
8
+ emptiness is a legitimate verdict (the negated `is hidden` forms).
9
+ useRoleName the two role-variable templates search by accessibility id built from `name:`
10
+ rather than through the selector entry.
11
+ --~}}
12
+ await __settleElements({{#if useRoleName}}'~{{escapeQuotes name}}'{{else}}{{> appium-selector-expr}}{{/if}}, {{#if mustExist}}true{{else}}false{{/if}}, '{{escapeQuotes selectorRef}}')
@@ -119,6 +119,32 @@ async function __ensureVisible(selector: string): Promise<WebdriverIO.Element> {
119
119
  return $(selector);
120
120
  }
121
121
 
122
+ // Read a live element ARRAY after giving the tree a chance to render.
123
+ //
124
+ // `await $$(sel)` is a ONE-SHOT read — unlike a Playwright Locator it does not re-query, so an
125
+ // assertion that loops over the result silently asserts NOTHING when the list has not painted yet:
126
+ // zero matches, zero iterations, green. #672 fixed that class on the Playwright side and left the
127
+ // appium adapter untouched, so every collection assertion here still raced the first render.
128
+ //
129
+ // `mustExist` says which verdict emptiness is. A positive assertion ("all of them contain X",
130
+ // "every match is disabled") is meaningless over an empty set, so it waits the full budget and
131
+ // then fails naming the reference. A negated one ("none of them is displayed") is legitimately
132
+ // satisfied by absence, so it only waits long enough to outlast a render race and then proceeds
133
+ // with whatever it found — waiting the full budget there would add 15s to every correct `is
134
+ // hidden` step, which is a worse trade than the race it would close.
135
+ async function __settleElements(
136
+ selector: string, mustExist: boolean, ref: string,
137
+ ): Promise<WebdriverIO.Element[]> {
138
+ try {
139
+ await (await $(selector)).waitForExist({ timeout: mustExist ? 15000 : 3000 });
140
+ } catch {
141
+ if (mustExist) {
142
+ throw new Error(`[sungen] Expected at least one ${ref ? `[${ref}]` : `element matching ${selector}`} — none appeared within 15s`);
143
+ }
144
+ }
145
+ return $$(selector);
146
+ }
147
+
122
148
  // Visibility oracle for simple-case `see [X]` assertions. Android: WDIO's toBeDisplayed()
123
149
  // (reliable on UiAutomator2). iOS: XCUITest's `visible` attribute false-negatives on real,
124
150
  // on-screen elements (snapshot artifact — seen live even on a screen title every prior run
@@ -476,9 +502,36 @@ describe('{{featureName}}', () => {
476
502
  // lose the escalation between them — SG-W024 already reports that shape when tiers are involved.)
477
503
  let __escalateAfter: string | null = null;
478
504
 
505
+ // `@known-defect` marks a scenario that is EXPECTED to be red — it asserts a live defect. The
506
+ // scaffolded /sungen:run-test command tells authors exactly that ("expected-red scenarios stay red;
507
+ // never 'fix' them"), so the ladder honours it here: such a failure is the suite working as
508
+ // designed, not evidence of leftover state. Arming escalation off it would make the NEXT scenario
509
+ // pay a harder reset for nothing — on every run, forever, because the defect is known.
510
+ //
511
+ // Read off the TITLE because `@known-defect` is a pass-through tag: appium appends it there as the
512
+ // `[@a @b]` suffix (which is what keeps it greppable via `--grep`), and that is how the field
513
+ // report shows it. Anchored to that trailing group, so a scenario merely NAMED `… known-defect …`
514
+ // cannot exempt itself — the same reason __tierFor refuses to strip a trailing `[Selector]` ref.
515
+ //
516
+ // Matched as a whole TAG TOKEN, never as a substring. `@known-defect-list` is a DIFFERENT tag, and
517
+ // reading it as this one would silently stop the next scenario's real dirty-state failure from
518
+ // getting the escalated reset it needs — with no log line at all, because "never armed" is not the
519
+ // "capped" path and prints nothing. The `:<ref>` parameter form is accepted because that is what
520
+ // the rest of the vocabulary does (`@manual:M10`, `@reset:data`).
521
+ //
522
+ // Caveat, inherited rather than introduced: a @cases scenario builds its title at runtime as
523
+ // `name — <row label>`, so a row LABEL that itself ends in `[@…]` would be read as the tag group.
524
+ // Same trailing-bracket ambiguity __tierFor accepts for a `[Selector]` ref.
525
+ function __isExpectedFail(title: string): boolean {
526
+ const tagGroup = title.match(/ \[@[^\]]*\]$/);
527
+ if (!tagGroup) return false;
528
+ return tagGroup[0].slice(2, -1).split(/\s+/)
529
+ .some((t) => t === '@known-defect' || t.startsWith('@known-defect:'));
530
+ }
531
+
479
532
  // Registered BEFORE any user @afterEach block so a throwing user hook cannot skip the bookkeeping.
480
533
  afterEach(async function () {
481
- __escalateAfter = this.currentTest?.state === 'failed' ? (this.currentTest?.title ?? '') : null;
534
+ __escalateAfter = this.currentTest?.state === 'failed' && !__isExpectedFail(this.currentTest?.title ?? '') ? (this.currentTest?.title ?? '') : null;
482
535
  });
483
536
 
484
537
  {{#if scenarioResetTiers}}
@@ -557,9 +610,31 @@ describe('{{featureName}}', () => {
557
610
  await driver.execute('mobile: clearApp', { appId: pkg });
558
611
  }
559
612
 
560
- async function __reinstallApp(pkg: string): Promise<void> {
613
+ /** The app's build file, when the session was given one. Absent when the app was attached BY PACKAGE. */
614
+ function __appBuildPath(): string | undefined {
561
615
  const caps = (driver.requestedCapabilities || {}) as Record<string, string>;
562
- const appPath = caps['appium:app'] || caps['app'];
616
+ return caps['appium:app'] || caps['app'];
617
+ }
618
+
619
+ /**
620
+ * The strongest tier THIS SESSION can actually carry out.
621
+ *
622
+ * Reinstalling needs the app's build file, and `data` on iOS IS a reinstall (no `mobile: clearApp`
623
+ * outside UiAutomator2). Attaching the app by package — the common local setup — leaves no build
624
+ * file, so those tiers cannot run at all.
625
+ *
626
+ * This exists so ESCALATION can cap itself. Escalation is a recovery the runner CHOSE; nothing
627
+ * asked for it. Escalating into a tier that throws turns one failed scenario into a dead spec file,
628
+ * because the throw lands in `beforeEach` and mocha then skips every remaining test in the file.
629
+ * A DECLARED tier is never capped — that is an unmet contract and must still throw by name.
630
+ */
631
+ function __performableTier(): string {
632
+ if (__appBuildPath()) return 'full';
633
+ return driver.isIOS ? 'soft' : 'data';
634
+ }
635
+
636
+ async function __reinstallApp(pkg: string): Promise<void> {
637
+ const appPath = __appBuildPath();
563
638
  if (!appPath) {
564
639
  throw new Error(
565
640
  `reset tier needs the app's build file to reinstall — set APP_APK (Android) / IOS_APP (iOS) so the appium:app capability is present, or drop to @reset:soft.`,
@@ -611,9 +686,24 @@ describe('{{featureName}}', () => {
611
686
  // Same title ⟹ this is a retry of the scenario that just failed: keep its declared tier so a real
612
687
  // failure fails both attempts (see __escalateAfter above).
613
688
  const __escalate = __escalateAfter !== null && __escalateAfter !== (this.currentTest?.title ?? '');
614
- const __tier = __escalate ? __ESCALATE[__base] : __base;
689
+ // Escalation caps at what this session can perform, with the DECLARED tier as the FLOOR.
690
+ // The cap stops a tier NOBODY ASKED FOR from throwing in this hook and taking the rest of the
691
+ // spec file with it. The floor stops the cap from silently WEAKENING what the feature declared:
692
+ // a declared tier this session cannot perform still reaches __resetApp and still throws by name,
693
+ // because that one IS an unmet contract.
694
+ const __want = __escalate ? __ESCALATE[__base] : __base;
695
+ const __tier = __escalate
696
+ ? __TIER_ORDER[Math.max(
697
+ __TIER_ORDER.indexOf(__base),
698
+ Math.min(__TIER_ORDER.indexOf(__want), __TIER_ORDER.indexOf(__performableTier())),
699
+ )]
700
+ : __want;
615
701
  if (__tier !== __base) {
616
702
  console.log(` \u2191 reset escalated ${__base} \u2192 ${__tier} (previous scenario failed: "${__escalateAfter}")`);
703
+ } else if (__want !== __base) {
704
+ // Said per occurrence, not once per run: this names the scenario that did NOT get the stronger
705
+ // reset it was owed, which is a fact about THIS scenario, not just about the session.
706
+ console.warn(` ! reset could not escalate ${__base} \u2192 ${__want} — this session has no app build file to reinstall from (the app was attached by package). Staying at ${__base}; set APP_APK (Android) / IOS_APP (iOS) to enable the stronger reset.`);
617
707
  }
618
708
  await __resetApp(__tier);
619
709
  {{#if backgroundSteps}}
@@ -1 +1,7 @@
1
- testData.set('{{varName}}', ((await {{> locator}}.{{capture}}()) ?? '').trim());
1
+ {{!-- `.first()` unless the entry pinned an index. A capture reads ONE element; without it,
2
+ Playwright's strict mode turned any selector matching more than one node into a hard failure
3
+ naming the count rather than the step — and the documented workaround was to add `nth: 0`,
4
+ i.e. to ask for exactly this. `{{> locator}}` already appends `.nth(n)` when `nth:` is
5
+ declared, so the two never stack. The compiler warns (SG-W036) when the selector is a SHAPE
6
+ that routinely matches a list, so "the first one" stays a decision rather than an accident. --}}
7
+ testData.set('{{varName}}', ((await {{> locator}}{{#unless (isDefined nth)}}.first(){{/unless}}.{{capture}}()) ?? '').trim());
@@ -1 +1,7 @@
1
- await expect(page.getByText(/{{#if label}}{{escapeRegex label}}\s*{{else}}(?:^|\s){{/if}}{{escapeRegex dataValue}}$/)).toBeVisible();
1
+ {{!-- Caption+value read: the ref names on-screen TEXT ("学科 -"), not an element, so the locator is
2
+ built inline from the caption rather than through the `locator` partial. That is why the scope
3
+ clause needs its own slot here — the partial's `parentLocator` handling never runs (#747).
4
+ Unscoped, `… in [Education Card 6] card` compiled away to nothing: on a screen repeating ten
5
+ 学歴・職歴 cards the assertion strict-mode-failed where the caption repeats, and where it
6
+ happened to be unique elsewhere it passed having read a DIFFERENT card than the sentence names. --}}
7
+ await expect({{#if parentLocator}}{{parentLocator}}{{else}}page{{/if}}.getByText(/{{#if label}}{{escapeRegex label}}\s*{{else}}(?:^|\s){{/if}}{{escapeRegex dataValue}}$/)).toBeVisible();
@@ -1 +1,23 @@
1
- {{!-- Column assertion scoped to the current row (`tableRow`, bound by the row-in-table entry step that ran before this one). columnIndex uses `isDefined` rather than a bare truthy check: column 0 is a valid index. --}}{{#if (isDefined columnIndex)}}await expect(tableRow.getByRole('cell', { includeHidden: true }).filter({ visible: true }).nth({{columnIndex}})).toHaveText('{{escapeQuotes cellValue}}');{{else if isGiven}}await tableRow.getByRole('cell', { includeHidden: true }).filter({ hasText: '{{escapeQuotes cellValue}}' }).filter({ visible: true }).waitFor();{{else}}await expect(tableRow.getByRole('cell', { includeHidden: true }).filter({ hasText: '{{escapeQuotes cellValue}}' }).filter({ visible: true })).toBeVisible();{{/if}}
1
+ {{!-- Column assertion scoped to the current row (`tableRow`, bound by the row-in-table entry step
2
+ that ran before this one).
3
+
4
+ `cellLocator` is the DECLARED cell — `columns.<col>.elements.cell` on the table entry, already
5
+ rendered against `tableRow`. It comes first because it is the only branch that works on a row
6
+ built from <div>s: those rows carry no ARIA `cell` role, so both branches below match zero
7
+ elements there, and `columns.<col>.index` cannot help — it indexes that same empty set. The
8
+ compiler warns (SG-W034) when a `rows:` shape is declared and no cell is.
9
+
10
+ columnIndex uses `isDefined` rather than a bare truthy check: column 0 is a valid index.
11
+
12
+ includeHidden + visible filter: when a modal (e.g. Radix Dialog) is open the background gets
13
+ aria-hidden="true", which empties plain getByRole() even though the cells are still rendered
14
+ — measure what the user SEES, not the a11y tree. --}}
15
+ {{~#if cellLocator~}}
16
+ {{#if isGiven}}await {{cellLocator}}.filter({ hasText: '{{escapeQuotes cellValue}}' }).waitFor();{{else}}await expect({{cellLocator}}).toHaveText('{{escapeQuotes cellValue}}');{{/if}}
17
+ {{~else if (isDefined columnIndex)~}}
18
+ await expect(tableRow.getByRole('cell', { includeHidden: true }).filter({ visible: true }).nth({{columnIndex}})).toHaveText('{{escapeQuotes cellValue}}');
19
+ {{~else if isGiven~}}
20
+ await tableRow.getByRole('cell', { includeHidden: true }).filter({ hasText: '{{escapeQuotes cellValue}}' }).filter({ visible: true }).waitFor();
21
+ {{~else~}}
22
+ await expect(tableRow.getByRole('cell', { includeHidden: true }).filter({ hasText: '{{escapeQuotes cellValue}}' }).filter({ visible: true })).toBeVisible();
23
+ {{~/if~}}
@@ -1,9 +1,13 @@
1
+ {{!-- `getByRole` is built inline from role/name rather than through the `locator` partial, so the
2
+ scope clause needs its own slot here exactly as in label-value-assertion — the label branch's
3
+ twin (#747). `see [Logo] image with {{alt}} in [Product Card 3] card` searched every card on
4
+ the page before this. --}}
1
5
  {{#if name}}
2
6
  {{#if dataValue}}
3
- await expect(page.getByRole('{{role}}', { name: '{{escapeQuotes name}}'{{#if exact}}, exact: true{{/if}} }).filter({ hasText: /^{{escapeRegex dataValue}}$/ }){{> locator-nth}}).toBeVisible();
7
+ await expect({{#if parentLocator}}{{parentLocator}}{{else}}page{{/if}}.getByRole('{{role}}', { name: '{{escapeQuotes name}}'{{#if exact}}, exact: true{{/if}} }).filter({ hasText: /^{{escapeRegex dataValue}}$/ }){{> locator-nth}}).toBeVisible();
4
8
  {{else}}
5
- await expect(page.getByRole('{{role}}', { name: '{{escapeQuotes name}}'{{#if exact}}, exact: true{{/if}} }){{> locator-nth}}).toBeVisible();
9
+ await expect({{#if parentLocator}}{{parentLocator}}{{else}}page{{/if}}.getByRole('{{role}}', { name: '{{escapeQuotes name}}'{{#if exact}}, exact: true{{/if}} }){{> locator-nth}}).toBeVisible();
6
10
  {{/if}}
7
11
  {{else}}
8
- await expect(page.getByRole('{{role}}'{{#if exact}}, { exact: true }{{/if}}).filter({ hasText: /^{{escapeRegex dataValue}}$/ }){{> locator-nth}}).toBeVisible();
9
- {{/if}}
12
+ await expect({{#if parentLocator}}{{parentLocator}}{{else}}page{{/if}}.getByRole('{{role}}'{{#if exact}}, { exact: true }{{/if}}).filter({ hasText: /^{{escapeRegex dataValue}}$/ }){{> locator-nth}}).toBeVisible();
13
+ {{/if}}
@@ -5,8 +5,8 @@
5
5
  {{~#case 'label'}}getByLabel('{{escapeQuotes value}}'{{#if exact}}, { exact: true }{{/if}}){{/case~}}
6
6
  {{~#case 'text'}}getByText('{{escapeQuotes value}}'{{#if exact}}, { exact: true }{{/if}}){{/case~}}
7
7
  {{~#case 'locator'}}locator('{{escapeQuotes value}}'){{/case~}}
8
- {{~#case 'id'}}locator('{{escapeQuotes value}}'){{/case~}}
9
- {{~#default}}locator('{{escapeQuotes value}}'){{/default~}}
8
+ {{~#case 'id'}}locator('{{escapeQuotes (cssIdSelector locator value)}}'){{/case~}}
9
+ {{~#default}}locator('{{#if locator}}{{escapeQuotes locator}}{{else}}{{escapeQuotes value}}{{/if}}'){{/default~}}
10
10
  {{~/switch}}{{else~}}
11
11
  {{#if scope}}{{#if (eq scope "dialog")}}{{pageRoot}}.{{else}}{{pageRoot}}.getByLabel('{{escapeQuotes scope}}').{{/if}}{{#switch strategy~}}
12
12
  {{~#case 'testid'}}getByTestId('{{escapeQuotes value}}'){{/case~}}
@@ -15,8 +15,8 @@
15
15
  {{~#case 'label'}}getByLabel('{{escapeQuotes value}}'{{#if exact}}, { exact: true }{{/if}}){{/case~}}
16
16
  {{~#case 'text'}}getByText('{{escapeQuotes value}}'{{#if exact}}, { exact: true }{{/if}}){{/case~}}
17
17
  {{~#case 'locator'}}locator('{{escapeQuotes value}}'){{/case~}}
18
- {{~#case 'id'}}locator('{{escapeQuotes value}}'){{/case~}}
19
- {{~#default}}locator('{{escapeQuotes value}}'){{/default~}}
18
+ {{~#case 'id'}}locator('{{escapeQuotes (cssIdSelector locator value)}}'){{/case~}}
19
+ {{~#default}}locator('{{#if locator}}{{escapeQuotes locator}}{{else}}{{escapeQuotes value}}{{/if}}'){{/default~}}
20
20
  {{~/switch}}{{else}}{{#switch strategy~}}
21
21
  {{~#case 'testid'}}{{> locator-strategies/testid}}{{/case~}}
22
22
  {{~#case 'role'}}{{> locator-strategies/role}}{{/case~}}
@@ -1 +1 @@
1
- page.locator('{{escapeQuotes selector}}')
1
+ {{pageRoot}}.locator('{{#if locator}}{{escapeQuotes locator}}{{else}}{{escapeQuotes value}}{{/if}}')
@@ -1 +1 @@
1
- page.locator('{{escapeQuotes selector}}')
1
+ {{pageRoot}}.locator('{{escapeQuotes (cssIdSelector locator value)}}')
@@ -5,8 +5,8 @@
5
5
  {{~#case 'label'}}getByLabel('{{escapeQuotes value}}'{{#if exact}}, { exact: true }{{/if}}){{/case~}}
6
6
  {{~#case 'text'}}getByText('{{escapeQuotes value}}'{{#if exact}}, { exact: true }{{/if}}){{/case~}}
7
7
  {{~#case 'locator'}}locator('{{escapeQuotes value}}'){{/case~}}
8
- {{~#case 'id'}}locator('{{escapeQuotes value}}'){{/case~}}
9
- {{~#default}}locator('{{escapeQuotes value}}'){{/default~}}
8
+ {{~#case 'id'}}locator('{{escapeQuotes (cssIdSelector locator value)}}'){{/case~}}
9
+ {{~#default}}locator('{{#if locator}}{{escapeQuotes locator}}{{else}}{{escapeQuotes value}}{{/if}}'){{/default~}}
10
10
  {{~/switch}}{{> locator-nth}}{{else~}}
11
11
  {{#if scope}}{{#if (eq scope "dialog")}}{{pageRoot}}.{{else}}{{pageRoot}}.getByLabel('{{escapeQuotes scope}}').{{/if}}{{#switch strategy~}}
12
12
  {{~#case 'testid'}}getByTestId('{{escapeQuotes value}}'){{/case~}}
@@ -15,8 +15,8 @@
15
15
  {{~#case 'label'}}getByLabel('{{escapeQuotes value}}'{{#if exact}}, { exact: true }{{/if}}){{/case~}}
16
16
  {{~#case 'text'}}getByText('{{escapeQuotes value}}'{{#if exact}}, { exact: true }{{/if}}){{/case~}}
17
17
  {{~#case 'locator'}}locator('{{escapeQuotes value}}'){{/case~}}
18
- {{~#case 'id'}}locator('{{escapeQuotes value}}'){{/case~}}
19
- {{~#default}}locator('{{escapeQuotes value}}'){{/default~}}
18
+ {{~#case 'id'}}locator('{{escapeQuotes (cssIdSelector locator value)}}'){{/case~}}
19
+ {{~#default}}locator('{{#if locator}}{{escapeQuotes locator}}{{else}}{{escapeQuotes value}}{{/if}}'){{/default~}}
20
20
  {{~/switch}}{{> locator-nth}}{{else}}{{#switch strategy~}}
21
21
  {{~#case 'testid'}}{{> locator-strategies/testid}}{{/case~}}
22
22
  {{~#case 'role'}}{{> locator-strategies/role}}{{/case~}}
@@ -15,5 +15,20 @@
15
15
 
16
16
  `expectPage`, not a bare `toHaveURL`: a polling URL assertion passes on the first transient
17
17
  match, before a route guard has bounced the user away (#662). The helper arrives, lets the
18
- page settle, holds, and re-asserts — read specs/url-assert.ts. --}}
19
- await expectPage(page, { pathname: /^{{pathRegex}}\/?$/{{#if query}}, dataRef: '{{escapeQuotes dataRef}}', declared: '{{escapeQuotes query}}'{{/if}} });
18
+ page settle, holds, and re-asserts — read specs/url-assert.ts.
19
+
20
+ `hostname` rides separately and only for an absolute page value, exactly as in the
21
+ `wait-for-page` twin: it is read off `u.hostname`, which never appears in `u.pathname`, so
22
+ one concatenated regex cannot carry both. Without it a page declared as
23
+ `https://admin.example.com/jobs` asserted the PATH on any host, and the public site's /jobs
24
+ satisfied it.
25
+
26
+ `ignoreQuery` comes from the page ENTRY's `query: any`, not from the step, because it is a fact
27
+ about the page rather than about one assertion (#748). It relaxes only the bare-page rule; the
28
+ declared-value half below is untouched, so the two compose rather than override each other.
29
+
30
+ The declared-value half is gated on `dataRef`, not on `query`. The step wrote `with {{v}}`
31
+ either way; gating on the resolved VALUE meant an empty test-data entry silently dropped the
32
+ whole check and fell back to the bare-page rule, which is the one case specs/url-assert.ts
33
+ exists to throw on. --}}
34
+ await expectPage(page, { pathname: /^{{pathRegex}}\/?$/{{#if hostRegex}}, hostname: /^{{hostRegex}}$/{{/if}}{{#if ignoreQuery}}, ignoreQuery: true{{/if}}{{#if dataRef}}, dataRef: '{{escapeQuotes dataRef}}', declared: '{{escapeQuotes query}}'{{/if}} });
@@ -49,6 +49,20 @@ test.describe('{{featureName}}', () => {
49
49
  {{#each scenarios}}
50
50
  {{indent this 2}}
51
51
 
52
+ {{/each}}
53
+ });
54
+
55
+ {{else if noAuth}}
56
+ {{!-- @no-auth demands an EMPTY session (#735). Leaving the group bare inherits whatever
57
+ playwright.config.ts set through `use.storageState`, so "an anonymous visitor is redirected to
58
+ sign-in" ran signed in and passed for the wrong reason. Omitting storageState inherits it —
59
+ an inline empty state is the only way to say "no session". --}}
60
+ test.describe('no-auth', () => {
61
+ test.use({ storageState: { cookies: [], origins: [] } });
62
+
63
+ {{#each scenarios}}
64
+ {{indent this 2}}
65
+
52
66
  {{/each}}
53
67
  });
54
68
 
@@ -167,23 +167,51 @@ function extractAuthRole(tags: string[]): string | undefined {
167
167
  * Precedence: Scenario > Feature > None
168
168
  */
169
169
  function getEffectiveAuthRole(
170
- scenarioTags: string[],
170
+ scenarioTags: string[],
171
171
  featureTags: string[]
172
172
  ): string | undefined {
173
173
  // Check for explicit @no-auth override
174
174
  if (scenarioTags.some(tag => tag === '@no-auth')) {
175
175
  return undefined;
176
176
  }
177
-
177
+
178
178
  // Scenario-level takes precedence
179
179
  const scenarioRole = extractAuthRole(scenarioTags);
180
180
  if (scenarioRole) return scenarioRole;
181
-
181
+
182
182
  // Fall back to feature-level
183
183
  const featureRole = extractAuthRole(featureTags);
184
184
  return featureRole;
185
185
  }
186
186
 
187
+ /**
188
+ * The three auth states a scenario can be in — they are NOT two (#735).
189
+ *
190
+ * `@no-auth` and "no auth tag at all" both leave `getEffectiveAuthRole` undefined, but they mean
191
+ * opposite things: the first demands an EMPTY session, the second inherits whatever
192
+ * `playwright.config.ts` set via `use.storageState`. Collapsing them put both in one group, so a
193
+ * scenario written to prove "an anonymous visitor is bounced to sign-in" ran signed in and passed
194
+ * for the wrong reason — while the exported test case still printed "Not authenticated".
195
+ *
196
+ * Returned as a flag beside the role rather than a tagged union so every existing `authRole` reader
197
+ * keeps working unchanged; only the grouping and the template consult `noAuth`.
198
+ */
199
+ function getAuthMode(
200
+ scenarioTags: string[],
201
+ featureTags: string[],
202
+ ): { noAuth: boolean; authRole: string | undefined } {
203
+ if (scenarioTags.some(tag => tag === '@no-auth')) {
204
+ return { noAuth: true, authRole: undefined };
205
+ }
206
+ return { noAuth: false, authRole: getEffectiveAuthRole(scenarioTags, featureTags) };
207
+ }
208
+
209
+ /**
210
+ * Grouping key for the anonymous bucket. A literal that no `@auth:<role>` can produce, so an
211
+ * imaginative role name can never land in the same group as `@no-auth`.
212
+ */
213
+ const NO_AUTH_GROUP_KEY = '\0no-auth';
214
+
187
215
 
188
216
  /**
189
217
  * True when the scenario has at least one step in the Then bucket —
@@ -238,6 +266,56 @@ function warnMultipleAuthTags(tags: string[], context: string): void {
238
266
  }
239
267
  }
240
268
 
269
+ /**
270
+ * Drop the YAML comment from ONE line: a `#` at line start or preceded by whitespace,
271
+ * and not inside a quoted scalar. Line-level only — see `yamlWithoutComments` for the
272
+ * block-scalar case, which this function cannot see from a single line.
273
+ */
274
+ function stripYamlComment(line: string): string {
275
+ let quote: string | null = null;
276
+ for (let i = 0; i < line.length; i++) {
277
+ const c = line[i];
278
+ if (quote) {
279
+ if (quote === '"' && c === '\\') { i++; continue; }
280
+ if (c === quote) quote = null;
281
+ continue;
282
+ }
283
+ if (c === '"' || c === "'") { quote = c; continue; }
284
+ if (c === '#' && (i === 0 || /\s/.test(line[i - 1]))) return line.slice(0, i);
285
+ }
286
+ return line;
287
+ }
288
+
289
+ /**
290
+ * The YAML text a `${QA_*}` scan may look at: comments removed, block scalars kept whole.
291
+ *
292
+ * SG-W009's reference scan used to run over the whole raw file, so a `${QA_*}` that only ever
293
+ * appeared in a `#` comment counted as a live reference — including the one sungen's own
294
+ * scaffolded "HOW TO FILL VALUES" block writes into every test-data file. The declared side has
295
+ * always been line-wise; this brings the reference side to the same footing.
296
+ *
297
+ * Inside a `|`/`>` block scalar every character is literal content, `#` included, so those lines
298
+ * pass through untouched. Stripping them would send the check the other way — a real, undeclared
299
+ * `${QA_*}` written after a `#` in a block scalar would go unreported, and the run would only fail
300
+ * later with "environment variable not set". A missed warning is worse than a noisy one.
301
+ */
302
+ function yamlWithoutComments(text: string): string {
303
+ const out: string[] = [];
304
+ let blockIndent: number | null = null;
305
+ for (const line of text.split('\n')) {
306
+ if (blockIndent !== null) {
307
+ const indent = line.search(/\S/);
308
+ // blank lines and anything indented deeper than the key stay inside the scalar
309
+ if (indent === -1 || indent > blockIndent) { out.push(line); continue; }
310
+ blockIndent = null;
311
+ }
312
+ const block = /^(\s*)[^#\s][^:]*:\s*[|>][+-]?\d*[+-]?\s*$/.exec(line);
313
+ if (block) { blockIndent = block[1].length; out.push(line); continue; }
314
+ out.push(stripYamlComment(line));
315
+ }
316
+ return out.join('\n');
317
+ }
318
+
241
319
  /**
242
320
  * Result of test generation for a single feature file
243
321
  */
@@ -699,7 +777,7 @@ export class CodeGenerator {
699
777
  // absent compiles to a skip stub instead of a real test (read once per feature).
700
778
  const enabledCaps = new Set<string>(readCapabilities(process.cwd()).enabled.map(d => d.toLowerCase()));
701
779
 
702
- const renderedScenarios: Array<{ code: string; authRole?: string }> = [];
780
+ const renderedScenarios: Array<{ code: string; authRole?: string; noAuth?: boolean }> = [];
703
781
  for (const scenario of feature.scenarios) {
704
782
  if (isManual(scenario.tags)) {
705
783
  // SG-W008 — two-direction rule for @manual scenarios:
@@ -758,7 +836,14 @@ export class CodeGenerator {
758
836
  const reqAbsent = requiresCaps(scenario.tags).filter(c => !enabledCaps.has(c));
759
837
  if (reqAbsent.length) {
760
838
  if (this.options.verbose) console.log(` ⏸ Pending capability (${reqAbsent.join(', ')}): ${scenario.name}`);
761
- renderedScenarios.push({ code: this.generateRequiresSkipStub(scenario.name, reqAbsent), authRole: undefined });
839
+ // Carries its `@no-auth` tag like any other scenario so the stub lands in the group the
840
+ // author declared — a skipped test that reappears once the capability is enabled must not
841
+ // quietly change auth bucket.
842
+ renderedScenarios.push({
843
+ code: this.generateRequiresSkipStub(scenario.name, reqAbsent),
844
+ authRole: undefined,
845
+ noAuth: scenario.tags.includes('@no-auth'),
846
+ });
762
847
  continue;
763
848
  }
764
849
 
@@ -770,7 +855,7 @@ export class CodeGenerator {
770
855
  authFeatureTags = [...baseScenario.tags, ...authFeatureTags];
771
856
  }
772
857
  }
773
- const authRole = getEffectiveAuthRole(scenario.tags, authFeatureTags);
858
+ const { authRole, noAuth } = getAuthMode(scenario.tags, authFeatureTags);
774
859
 
775
860
  const code = await this.generateScenario(
776
861
  scenario,
@@ -778,15 +863,16 @@ export class CodeGenerator {
778
863
  feature.tags || [],
779
864
  isParallel
780
865
  );
781
- renderedScenarios.push({ code, authRole });
866
+ renderedScenarios.push({ code, authRole, noAuth });
782
867
  }
783
868
 
784
869
  // Group scenarios by auth role for nested test.describe blocks
785
- // This ensures test.use({ storageState }) only applies to its group
870
+ // This ensures test.use({ storageState }) only applies to its group. `@no-auth` gets its OWN
871
+ // bucket (#735) — it is a demand for an empty session, not the absence of a demand.
786
872
  const authGroupMap = new Map<string, string[]>();
787
873
  const groupOrder: string[] = [];
788
- for (const { code, authRole } of renderedScenarios) {
789
- const key = authRole || '';
874
+ for (const { code, authRole, noAuth } of renderedScenarios) {
875
+ const key = noAuth ? NO_AUTH_GROUP_KEY : (authRole || '');
790
876
  if (!authGroupMap.has(key)) {
791
877
  authGroupMap.set(key, []);
792
878
  groupOrder.push(key);
@@ -795,7 +881,8 @@ export class CodeGenerator {
795
881
  }
796
882
 
797
883
  const authGroups = groupOrder.map(key => ({
798
- authRole: key || undefined,
884
+ authRole: key === NO_AUTH_GROUP_KEY ? undefined : (key || undefined),
885
+ noAuth: key === NO_AUTH_GROUP_KEY,
799
886
  scenarios: authGroupMap.get(key)!,
800
887
  }));
801
888
 
@@ -804,6 +891,15 @@ export class CodeGenerator {
804
891
  // - Multiple groups: nested describes per auth role
805
892
  const needsGrouping = authGroups.length > 1;
806
893
 
894
+ // Rendering the per-group blocks is a SEPARATE question from the guards below (#735). A lone
895
+ // `@no-auth` group is one group — `needsGrouping` is false — yet it still has to emit its empty
896
+ // storageState, or an all-anonymous feature silently runs on the config's session. Keeping the
897
+ // guards keyed on `needsGrouping` means this never trips the appium/serial errors, which are
898
+ // about MIXING roles. Serial is excluded because its layout builds the context by hand with
899
+ // `browser.newContext()`, which never picks up `use.storageState` — already anonymous, and a
900
+ // `test.use` there would apply to nothing.
901
+ const renderAuthBlocks = needsGrouping || (isParallel && authGroups.some(g => g.noAuth));
902
+
807
903
  // D6 — the Appium test-file template has no per-group before-hook (only `singleAuthRole` runs
808
904
  // `__ensureAuth`); >1 auth group would render every group's scenarios with NO authentication at
809
905
  // all, silently. Mirrors the web `@parallel` guard below, but unconditional (isParallel has no
@@ -819,15 +915,18 @@ export class CodeGenerator {
819
915
 
820
916
  if (needsGrouping && !isParallel) {
821
917
  throw new Error(
822
- `Feature "${feature.name}" has multiple auth groups but no @parallel tag.\n` +
918
+ `Feature "${feature.name}" has multiple auth groups but runs serially.\n` +
823
919
  `Serial mode uses a shared browser context — it cannot mix different auth roles.\n` +
824
- `Fix: add @parallel tag to the feature.`
920
+ `Fix: remove the @serial tag (features are parallel by default), or split "${feature.name}" ` +
921
+ `into one feature file per auth role.`
825
922
  );
826
923
  }
827
924
  const scenarios = renderedScenarios.map(s => s.code);
828
925
 
829
- // For single group, extract the auth role to put test.use at describe level
830
- const singleAuthRole = !needsGrouping && authGroups.length === 1
926
+ // For single group, extract the auth role to put test.use at describe level. Keyed on
927
+ // `renderAuthBlocks`, not `needsGrouping`: a lone `@no-auth` group now renders its own block, so
928
+ // hoisting anything to the feature describe would double up. Its authRole is undefined anyway.
929
+ const singleAuthRole = !renderAuthBlocks && authGroups.length === 1
831
930
  ? authGroups[0].authRole
832
931
  : undefined;
833
932
 
@@ -932,8 +1031,8 @@ export class CodeGenerator {
932
1031
  platform,
933
1032
  cleanup,
934
1033
  backgroundSteps,
935
- scenarios: needsGrouping ? [] : scenarios,
936
- authGroups: needsGrouping ? authGroups : undefined,
1034
+ scenarios: renderAuthBlocks ? [] : scenarios,
1035
+ authGroups: renderAuthBlocks ? authGroups : undefined,
937
1036
  singleAuthRole,
938
1037
  appPackage,
939
1038
  iosBundleId,
@@ -1265,7 +1364,8 @@ export class CodeGenerator {
1265
1364
  * a name in the committed .env.qa.example. An undeclared ref means the test throws
1266
1365
  * "environment variable not set" the moment the key is used — surface it at compile
1267
1366
  * time instead. Only the `${QA_*}` shape is checked: any other `${...}` text is
1268
- * ordinary literal data by design (never resolved, never warned about).
1367
+ * ordinary literal data by design (never resolved, never warned about). A `${QA_*}`
1368
+ * that appears only inside a `#` comment is prose, not a reference, and is ignored.
1269
1369
  */
1270
1370
  private reportedEnvRefs = new Set<string>();
1271
1371
  private checkEnvSecretRefs(featureFile: string): void {
@@ -1273,7 +1373,7 @@ export class CodeGenerator {
1273
1373
  if (!fs.existsSync(testDataDir)) return;
1274
1374
  const referenced = new Set<string>();
1275
1375
  for (const f of fs.readdirSync(testDataDir).filter((x) => x.endsWith('.yaml') || x.endsWith('.yml'))) {
1276
- const text = fs.readFileSync(path.join(testDataDir, f), 'utf-8');
1376
+ const text = yamlWithoutComments(fs.readFileSync(path.join(testDataDir, f), 'utf-8'));
1277
1377
  for (const m of text.matchAll(/\$\{(QA_[A-Z0-9_]+)\}/g)) referenced.add(m[1]);
1278
1378
  }
1279
1379
  if (!referenced.size) return;