@sun-asterisk/sungen 3.2.24 → 3.2.25

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 (305) hide show
  1. package/dist/capabilities/context-router.d.ts.map +1 -1
  2. package/dist/capabilities/context-router.js +15 -7
  3. package/dist/capabilities/context-router.js.map +1 -1
  4. package/dist/cli/commands/capability.d.ts.map +1 -1
  5. package/dist/cli/commands/capability.js +6 -48
  6. package/dist/cli/commands/capability.js.map +1 -1
  7. package/dist/cli/commands/changed.d.ts +14 -0
  8. package/dist/cli/commands/changed.d.ts.map +1 -0
  9. package/dist/cli/commands/changed.js +128 -0
  10. package/dist/cli/commands/changed.js.map +1 -0
  11. package/dist/cli/commands/delivery.d.ts.map +1 -1
  12. package/dist/cli/commands/delivery.js +25 -1
  13. package/dist/cli/commands/delivery.js.map +1 -1
  14. package/dist/cli/commands/generate.js +1 -1
  15. package/dist/cli/commands/generate.js.map +1 -1
  16. package/dist/cli/commands/results-merge.d.ts +19 -0
  17. package/dist/cli/commands/results-merge.d.ts.map +1 -0
  18. package/dist/cli/commands/results-merge.js +191 -0
  19. package/dist/cli/commands/results-merge.js.map +1 -0
  20. package/dist/cli/commands/update.d.ts.map +1 -1
  21. package/dist/cli/commands/update.js +100 -12
  22. package/dist/cli/commands/update.js.map +1 -1
  23. package/dist/cli/index.js +4 -0
  24. package/dist/cli/index.js.map +1 -1
  25. package/dist/exporters/api-catalog-loader.d.ts.map +1 -1
  26. package/dist/exporters/api-catalog-loader.js +10 -3
  27. package/dist/exporters/api-catalog-loader.js.map +1 -1
  28. package/dist/exporters/api-testcase-formatter.d.ts +9 -0
  29. package/dist/exporters/api-testcase-formatter.d.ts.map +1 -1
  30. package/dist/exporters/api-testcase-formatter.js +55 -3
  31. package/dist/exporters/api-testcase-formatter.js.map +1 -1
  32. package/dist/exporters/csv-exporter.d.ts.map +1 -1
  33. package/dist/exporters/csv-exporter.js +10 -1
  34. package/dist/exporters/csv-exporter.js.map +1 -1
  35. package/dist/exporters/json-exporter.d.ts.map +1 -1
  36. package/dist/exporters/json-exporter.js +6 -1
  37. package/dist/exporters/json-exporter.js.map +1 -1
  38. package/dist/exporters/matrix/build.d.ts.map +1 -1
  39. package/dist/exporters/matrix/build.js +10 -0
  40. package/dist/exporters/matrix/build.js.map +1 -1
  41. package/dist/exporters/matrix/export.js +1 -1
  42. package/dist/exporters/matrix/export.js.map +1 -1
  43. package/dist/exporters/matrix/render-csv.d.ts.map +1 -1
  44. package/dist/exporters/matrix/render-csv.js +5 -1
  45. package/dist/exporters/matrix/render-csv.js.map +1 -1
  46. package/dist/exporters/matrix/render-xlsx.d.ts.map +1 -1
  47. package/dist/exporters/matrix/render-xlsx.js +4 -1
  48. package/dist/exporters/matrix/render-xlsx.js.map +1 -1
  49. package/dist/exporters/matrix/types.d.ts +7 -0
  50. package/dist/exporters/matrix/types.d.ts.map +1 -1
  51. package/dist/exporters/matrix/types.js.map +1 -1
  52. package/dist/exporters/playwright-report-parser.d.ts +17 -2
  53. package/dist/exporters/playwright-report-parser.d.ts.map +1 -1
  54. package/dist/exporters/playwright-report-parser.js +68 -8
  55. package/dist/exporters/playwright-report-parser.js.map +1 -1
  56. package/dist/exporters/results-merge.d.ts +46 -0
  57. package/dist/exporters/results-merge.d.ts.map +1 -0
  58. package/dist/exporters/results-merge.js +148 -0
  59. package/dist/exporters/results-merge.js.map +1 -0
  60. package/dist/exporters/selector-key-resolver.d.ts +4 -0
  61. package/dist/exporters/selector-key-resolver.d.ts.map +1 -1
  62. package/dist/exporters/selector-key-resolver.js +101 -1
  63. package/dist/exporters/selector-key-resolver.js.map +1 -1
  64. package/dist/exporters/types.d.ts +8 -1
  65. package/dist/exporters/types.d.ts.map +1 -1
  66. package/dist/exporters/xlsx-report-builder.d.ts.map +1 -1
  67. package/dist/exporters/xlsx-report-builder.js +5 -4
  68. package/dist/exporters/xlsx-report-builder.js.map +1 -1
  69. package/dist/generators/gherkin-parser/index.d.ts +2 -0
  70. package/dist/generators/gherkin-parser/index.d.ts.map +1 -1
  71. package/dist/generators/gherkin-parser/index.js +52 -1
  72. package/dist/generators/gherkin-parser/index.js.map +1 -1
  73. package/dist/generators/test-generator/adapters/adapter-interface.d.ts +20 -10
  74. package/dist/generators/test-generator/adapters/adapter-interface.d.ts.map +1 -1
  75. package/dist/generators/test-generator/adapters/appium/appium-adapter.d.ts +2 -11
  76. package/dist/generators/test-generator/adapters/appium/appium-adapter.d.ts.map +1 -1
  77. package/dist/generators/test-generator/adapters/appium/appium-adapter.js.map +1 -1
  78. package/dist/generators/test-generator/adapters/appium/templates/imports.hbs +3 -0
  79. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/date-picker-unsupported.hbs +4 -0
  80. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/lifecycle-missing-app-id.hbs +5 -0
  81. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/matches-format-assertion.hbs +15 -0
  82. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/visible-filtered-assertion.hbs +5 -2
  83. package/dist/generators/test-generator/adapters/appium/templates/test-file.hbs +185 -26
  84. package/dist/generators/test-generator/adapters/playwright/playwright-adapter.d.ts +2 -11
  85. package/dist/generators/test-generator/adapters/playwright/playwright-adapter.d.ts.map +1 -1
  86. package/dist/generators/test-generator/adapters/playwright/playwright-adapter.js.map +1 -1
  87. package/dist/generators/test-generator/adapters/playwright/templates/imports.hbs +3 -0
  88. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/select-action.hbs +17 -1
  89. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/attribute-assertion.hbs +6 -1
  90. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/have-value-assertion.hbs +18 -1
  91. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/hidden-with-filter-assertion.hbs +5 -1
  92. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/matches-format-assertion.hbs +5 -0
  93. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/state-with-filter-assertion.hbs +4 -2
  94. package/dist/generators/test-generator/adapters/playwright/templates/steps/navigation/wait-for-element-with-text.hbs +4 -1
  95. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-expect-text.hbs +1 -1
  96. package/dist/generators/test-generator/code-generator.d.ts +6 -0
  97. package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
  98. package/dist/generators/test-generator/code-generator.js +80 -7
  99. package/dist/generators/test-generator/code-generator.js.map +1 -1
  100. package/dist/generators/test-generator/diagnostics.d.ts +60 -6
  101. package/dist/generators/test-generator/diagnostics.d.ts.map +1 -1
  102. package/dist/generators/test-generator/diagnostics.js +59 -5
  103. package/dist/generators/test-generator/diagnostics.js.map +1 -1
  104. package/dist/generators/test-generator/patterns/expect-patterns.d.ts +7 -0
  105. package/dist/generators/test-generator/patterns/expect-patterns.d.ts.map +1 -1
  106. package/dist/generators/test-generator/patterns/expect-patterns.js +36 -2
  107. package/dist/generators/test-generator/patterns/expect-patterns.js.map +1 -1
  108. package/dist/generators/test-generator/patterns/index.d.ts +19 -12
  109. package/dist/generators/test-generator/patterns/index.d.ts.map +1 -1
  110. package/dist/generators/test-generator/patterns/index.js +58 -38
  111. package/dist/generators/test-generator/patterns/index.js.map +1 -1
  112. package/dist/generators/test-generator/patterns/types.d.ts +23 -0
  113. package/dist/generators/test-generator/patterns/types.d.ts.map +1 -1
  114. package/dist/generators/test-generator/patterns/unsupported-step-code.d.ts +18 -0
  115. package/dist/generators/test-generator/patterns/unsupported-step-code.d.ts.map +1 -0
  116. package/dist/generators/test-generator/patterns/unsupported-step-code.js +23 -0
  117. package/dist/generators/test-generator/patterns/unsupported-step-code.js.map +1 -0
  118. package/dist/generators/test-generator/recipe-expander.js +9 -1
  119. package/dist/generators/test-generator/recipe-expander.js.map +1 -1
  120. package/dist/generators/test-generator/reset-tier.d.ts +68 -0
  121. package/dist/generators/test-generator/reset-tier.d.ts.map +1 -0
  122. package/dist/generators/test-generator/reset-tier.js +94 -0
  123. package/dist/generators/test-generator/reset-tier.js.map +1 -0
  124. package/dist/generators/test-generator/step-mapper.d.ts +39 -0
  125. package/dist/generators/test-generator/step-mapper.d.ts.map +1 -1
  126. package/dist/generators/test-generator/step-mapper.js +144 -5
  127. package/dist/generators/test-generator/step-mapper.js.map +1 -1
  128. package/dist/generators/test-generator/template-engine.d.ts +2 -10
  129. package/dist/generators/test-generator/template-engine.d.ts.map +1 -1
  130. package/dist/generators/test-generator/template-engine.js +16 -2
  131. package/dist/generators/test-generator/template-engine.js.map +1 -1
  132. package/dist/generators/test-generator/utils/format-pattern.d.ts +35 -0
  133. package/dist/generators/test-generator/utils/format-pattern.d.ts.map +1 -0
  134. package/dist/generators/test-generator/utils/format-pattern.js +82 -0
  135. package/dist/generators/test-generator/utils/format-pattern.js.map +1 -0
  136. package/dist/generators/test-generator/utils/js-literal-scanner.d.ts +42 -0
  137. package/dist/generators/test-generator/utils/js-literal-scanner.d.ts.map +1 -0
  138. package/dist/generators/test-generator/utils/js-literal-scanner.js +142 -0
  139. package/dist/generators/test-generator/utils/js-literal-scanner.js.map +1 -0
  140. package/dist/generators/test-generator/utils/mask-refs-for-matching.d.ts +18 -0
  141. package/dist/generators/test-generator/utils/mask-refs-for-matching.d.ts.map +1 -0
  142. package/dist/generators/test-generator/utils/mask-refs-for-matching.js +25 -0
  143. package/dist/generators/test-generator/utils/mask-refs-for-matching.js.map +1 -0
  144. package/dist/generators/test-generator/utils/runtime-data-transformer.d.ts +9 -1
  145. package/dist/generators/test-generator/utils/runtime-data-transformer.d.ts.map +1 -1
  146. package/dist/generators/test-generator/utils/runtime-data-transformer.js +124 -63
  147. package/dist/generators/test-generator/utils/runtime-data-transformer.js.map +1 -1
  148. package/dist/generators/test-generator/utils/selector-resolver.d.ts.map +1 -1
  149. package/dist/generators/test-generator/utils/selector-resolver.js +20 -1
  150. package/dist/generators/test-generator/utils/selector-resolver.js.map +1 -1
  151. package/dist/generators/test-generator/utils/spec-syntax-check.d.ts +14 -0
  152. package/dist/generators/test-generator/utils/spec-syntax-check.d.ts.map +1 -0
  153. package/dist/generators/test-generator/utils/spec-syntax-check.js +26 -0
  154. package/dist/generators/test-generator/utils/spec-syntax-check.js.map +1 -0
  155. package/dist/harness/annotation-overrides.d.ts +0 -12
  156. package/dist/harness/annotation-overrides.d.ts.map +1 -1
  157. package/dist/harness/annotation-overrides.js +9 -5
  158. package/dist/harness/annotation-overrides.js.map +1 -1
  159. package/dist/harness/audit.js +4 -4
  160. package/dist/harness/audit.js.map +1 -1
  161. package/dist/harness/capability-plan.d.ts +2 -1
  162. package/dist/harness/capability-plan.d.ts.map +1 -1
  163. package/dist/harness/capability-plan.js +8 -3
  164. package/dist/harness/capability-plan.js.map +1 -1
  165. package/dist/harness/capability.d.ts +11 -0
  166. package/dist/harness/capability.d.ts.map +1 -1
  167. package/dist/harness/capability.js +8 -0
  168. package/dist/harness/capability.js.map +1 -1
  169. package/dist/harness/catalog/drivers.yaml +5 -0
  170. package/dist/harness/parse.d.ts.map +1 -1
  171. package/dist/harness/parse.js +5 -1
  172. package/dist/harness/parse.js.map +1 -1
  173. package/dist/harness/run-manifest.d.ts +58 -0
  174. package/dist/harness/run-manifest.d.ts.map +1 -0
  175. package/dist/harness/run-manifest.js +264 -0
  176. package/dist/harness/run-manifest.js.map +1 -0
  177. package/dist/harness/viewpoint-ledger.d.ts +1 -1
  178. package/dist/harness/viewpoint-ledger.d.ts.map +1 -1
  179. package/dist/harness/viewpoint-ledger.js +25 -4
  180. package/dist/harness/viewpoint-ledger.js.map +1 -1
  181. package/dist/index.d.ts +1 -0
  182. package/dist/index.d.ts.map +1 -1
  183. package/dist/index.js +5 -1
  184. package/dist/index.js.map +1 -1
  185. package/dist/orchestrator/lockstep-version.d.ts +22 -0
  186. package/dist/orchestrator/lockstep-version.d.ts.map +1 -0
  187. package/dist/orchestrator/lockstep-version.js +58 -0
  188. package/dist/orchestrator/lockstep-version.js.map +1 -0
  189. package/dist/orchestrator/project-deps-sync.d.ts +39 -0
  190. package/dist/orchestrator/project-deps-sync.d.ts.map +1 -0
  191. package/dist/orchestrator/project-deps-sync.js +175 -0
  192. package/dist/orchestrator/project-deps-sync.js.map +1 -0
  193. package/dist/orchestrator/project-initializer.d.ts.map +1 -1
  194. package/dist/orchestrator/project-initializer.js +5 -2
  195. package/dist/orchestrator/project-initializer.js.map +1 -1
  196. package/dist/orchestrator/templates/ai-src/commands/run-test.md +109 -0
  197. package/dist/orchestrator/templates/ai-src/config/claude.md +1 -1
  198. package/dist/orchestrator/templates/ai-src/config/copilot.md +1 -1
  199. package/dist/orchestrator/templates/ai-src/skills/sungen-api-design/SKILL.md +49 -0
  200. package/dist/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +14 -3
  201. package/dist/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +110 -6
  202. package/dist/orchestrator/templates/ai-src/skills/sungen-locale/SKILL.md +1 -1
  203. package/dist/orchestrator/templates/ai-src/skills/sungen-mobile-gestures/SKILL.md +8 -0
  204. package/dist/orchestrator/templates/ai-src/skills/sungen-selector-fix/SKILL.md +1 -1
  205. package/dist/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +9 -0
  206. package/dist/orchestrator/templates/ai-src/skills/sungen-tc-review/SKILL.md +15 -0
  207. package/dist/orchestrator/templates/env.appium.example +10 -0
  208. package/dist/orchestrator/templates/specs-api.d.ts +4 -2
  209. package/dist/orchestrator/templates/specs-api.d.ts.map +1 -1
  210. package/dist/orchestrator/templates/specs-api.js +233 -10
  211. package/dist/orchestrator/templates/specs-api.js.map +1 -1
  212. package/dist/orchestrator/templates/specs-api.ts +214 -11
  213. package/dist/orchestrator/templates/specs-regex-escape.d.ts +13 -0
  214. package/dist/orchestrator/templates/specs-regex-escape.d.ts.map +1 -0
  215. package/dist/orchestrator/templates/specs-regex-escape.js +19 -0
  216. package/dist/orchestrator/templates/specs-regex-escape.js.map +1 -0
  217. package/dist/orchestrator/templates/specs-regex-escape.ts +16 -0
  218. package/dist/orchestrator/templates/specs-test-data.ts +53 -6
  219. package/dist/orchestrator/templates/wdio.conf.ts +28 -9
  220. package/package.json +8 -6
  221. package/src/capabilities/context-router.ts +13 -5
  222. package/src/cli/commands/capability.ts +4 -38
  223. package/src/cli/commands/changed.ts +104 -0
  224. package/src/cli/commands/delivery.ts +27 -2
  225. package/src/cli/commands/generate.ts +1 -1
  226. package/src/cli/commands/results-merge.ts +153 -0
  227. package/src/cli/commands/update.ts +72 -13
  228. package/src/cli/index.ts +4 -0
  229. package/src/exporters/api-catalog-loader.ts +10 -2
  230. package/src/exporters/api-testcase-formatter.ts +59 -3
  231. package/src/exporters/csv-exporter.ts +11 -2
  232. package/src/exporters/json-exporter.ts +7 -2
  233. package/src/exporters/matrix/build.ts +9 -0
  234. package/src/exporters/matrix/export.ts +1 -1
  235. package/src/exporters/matrix/render-csv.ts +5 -1
  236. package/src/exporters/matrix/render-xlsx.ts +4 -1
  237. package/src/exporters/matrix/types.ts +7 -0
  238. package/src/exporters/playwright-report-parser.ts +68 -9
  239. package/src/exporters/results-merge.ts +150 -0
  240. package/src/exporters/selector-key-resolver.ts +81 -1
  241. package/src/exporters/types.ts +8 -1
  242. package/src/exporters/xlsx-report-builder.ts +5 -4
  243. package/src/generators/gherkin-parser/index.ts +55 -1
  244. package/src/generators/test-generator/adapters/adapter-interface.ts +23 -1
  245. package/src/generators/test-generator/adapters/appium/appium-adapter.ts +2 -2
  246. package/src/generators/test-generator/adapters/appium/templates/imports.hbs +3 -0
  247. package/src/generators/test-generator/adapters/appium/templates/steps/actions/date-picker-unsupported.hbs +4 -0
  248. package/src/generators/test-generator/adapters/appium/templates/steps/actions/lifecycle-missing-app-id.hbs +5 -0
  249. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/matches-format-assertion.hbs +15 -0
  250. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/visible-filtered-assertion.hbs +5 -2
  251. package/src/generators/test-generator/adapters/appium/templates/test-file.hbs +185 -26
  252. package/src/generators/test-generator/adapters/playwright/playwright-adapter.ts +2 -2
  253. package/src/generators/test-generator/adapters/playwright/templates/imports.hbs +3 -0
  254. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/select-action.hbs +17 -1
  255. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/attribute-assertion.hbs +6 -1
  256. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/have-value-assertion.hbs +18 -1
  257. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/hidden-with-filter-assertion.hbs +5 -1
  258. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/matches-format-assertion.hbs +5 -0
  259. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/state-with-filter-assertion.hbs +4 -2
  260. package/src/generators/test-generator/adapters/playwright/templates/steps/navigation/wait-for-element-with-text.hbs +4 -1
  261. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-expect-text.hbs +1 -1
  262. package/src/generators/test-generator/code-generator.ts +86 -7
  263. package/src/generators/test-generator/diagnostics.ts +60 -6
  264. package/src/generators/test-generator/patterns/expect-patterns.ts +37 -2
  265. package/src/generators/test-generator/patterns/index.ts +58 -33
  266. package/src/generators/test-generator/patterns/types.ts +36 -0
  267. package/src/generators/test-generator/patterns/unsupported-step-code.ts +19 -0
  268. package/src/generators/test-generator/recipe-expander.ts +9 -1
  269. package/src/generators/test-generator/reset-tier.ts +93 -0
  270. package/src/generators/test-generator/step-mapper.ts +149 -6
  271. package/src/generators/test-generator/template-engine.ts +18 -3
  272. package/src/generators/test-generator/utils/format-pattern.ts +79 -0
  273. package/src/generators/test-generator/utils/js-literal-scanner.ts +132 -0
  274. package/src/generators/test-generator/utils/mask-refs-for-matching.ts +21 -0
  275. package/src/generators/test-generator/utils/runtime-data-transformer.ts +126 -76
  276. package/src/generators/test-generator/utils/selector-resolver.ts +22 -1
  277. package/src/generators/test-generator/utils/spec-syntax-check.ts +22 -0
  278. package/src/harness/annotation-overrides.ts +10 -5
  279. package/src/harness/audit.ts +4 -4
  280. package/src/harness/capability-plan.ts +10 -5
  281. package/src/harness/capability.ts +16 -0
  282. package/src/harness/catalog/drivers.yaml +5 -0
  283. package/src/harness/parse.ts +5 -1
  284. package/src/harness/run-manifest.ts +254 -0
  285. package/src/harness/viewpoint-ledger.ts +26 -5
  286. package/src/index.ts +1 -0
  287. package/src/orchestrator/lockstep-version.ts +46 -0
  288. package/src/orchestrator/project-deps-sync.ts +175 -0
  289. package/src/orchestrator/project-initializer.ts +5 -2
  290. package/src/orchestrator/templates/ai-src/commands/run-test.md +109 -0
  291. package/src/orchestrator/templates/ai-src/config/claude.md +1 -1
  292. package/src/orchestrator/templates/ai-src/config/copilot.md +1 -1
  293. package/src/orchestrator/templates/ai-src/skills/sungen-api-design/SKILL.md +49 -0
  294. package/src/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +14 -3
  295. package/src/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +110 -6
  296. package/src/orchestrator/templates/ai-src/skills/sungen-locale/SKILL.md +1 -1
  297. package/src/orchestrator/templates/ai-src/skills/sungen-mobile-gestures/SKILL.md +8 -0
  298. package/src/orchestrator/templates/ai-src/skills/sungen-selector-fix/SKILL.md +1 -1
  299. package/src/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +9 -0
  300. package/src/orchestrator/templates/ai-src/skills/sungen-tc-review/SKILL.md +15 -0
  301. package/src/orchestrator/templates/env.appium.example +10 -0
  302. package/src/orchestrator/templates/specs-api.ts +214 -11
  303. package/src/orchestrator/templates/specs-regex-escape.ts +16 -0
  304. package/src/orchestrator/templates/specs-test-data.ts +53 -6
  305. package/src/orchestrator/templates/wdio.conf.ts +28 -9
@@ -0,0 +1,4 @@
1
+ {{!-- A date-picker on native mobile is a DIALOG (spinner/calendar), not a text field: the generic
2
+ fill would click the trigger and setValue into it, selecting nothing while reporting success.
3
+ Same treatment as the upload step — fail loud, and name both ways forward. --}}
4
+ throw new Error("[sungen] {{escapeQuotes reason}}");
@@ -0,0 +1,5 @@
1
+ {{!-- A lifecycle step (restart / reinstall) in a feature with no `Path:` app id. The lifecycle
2
+ templates resolve the app through a helper the test-file emits only when that id is present,
3
+ so without it the spec used to compile clean and die at runtime with a ReferenceError —
4
+ taking the whole spec down, not the step. Fail at the step, naming the missing line. --}}
5
+ throw new Error("[sungen] {{escapeQuotes reason}}");
@@ -0,0 +1,15 @@
1
+ {{!-- Format assertion (mobile): same multi-source read as have-text/contain-text
2
+ (@text / @content-desc on Android, label/value on iOS), matched against the shape the spec
3
+ states. The error quotes the FORMAT and the actual string — the compiled regex is noise to
4
+ the person who wrote the requirement. --}}
5
+ {
6
+ const __el = await {{> locator}};
7
+ await __el.waitForDisplayed();
8
+ const __sources = driver.isIOS
9
+ ? [await __el.getAttribute('label'), await __el.getAttribute('value')]
10
+ : [await __el.getText(), await __el.getAttribute('content-desc')];
11
+ const __actual = String(__sources.find(s => s && String(s).trim() !== '') ?? '').trim();
12
+ if (!{{{formatRegex}}}.test(__actual)) {
13
+ throw new Error(`[sungen] Expected [{{escapeQuotes selectorRef}}] to match the format "{{escapeQuotes format}}" but it reads "${__actual}"`);
14
+ }
15
+ }
@@ -1,9 +1,12 @@
1
1
  {{!-- Filtered visible (mobile): $$ over the base selector, per-OS text read matched against
2
2
  dataValue — no CSS filter() combinator exists on Appium, so the match is hand-rolled
3
3
  (covers both the locator- and role-strategy call sites, since both resolve through
4
- appium-selector-expr). Assert at least one match is displayed. --}}
4
+ appium-selector-expr). Assert at least one match is displayed. Substring match by
5
+ default, matching the web hasText string contract: case-insensitive with collapsed
6
+ whitespace; `exact` opts into full case-sensitive equality (the anchored-regex twin). --}}
5
7
  {
6
8
  const __expected = String('{{escapeQuotes dataValue}}').trim();
9
+ const __norm = (s: string) => s.replace(/\s+/g, ' ').trim().toLowerCase();
7
10
  const __items = await $$({{> appium-selector-expr}});
8
11
  let __found = false;
9
12
  for (const __el of __items) {
@@ -11,7 +14,7 @@
11
14
  ? [await __el.getAttribute('label'), await __el.getAttribute('value')]
12
15
  : [await __el.getText(), await __el.getAttribute('content-desc')];
13
16
  const __actual = String(__sources.find(s => s && String(s).trim() !== '') ?? '').trim();
14
- if (__actual === __expected && await __el.isDisplayed()) { __found = true; break; }
17
+ if ({{#if exact}}__actual === __expected{{else}}__norm(__actual).includes(__norm(__expected)){{/if}} && await __el.isDisplayed()) { __found = true; break; }
15
18
  }
16
19
  if (!__found) throw new Error(`[sungen] Expected a visible match with text "${__expected}"`);
17
20
  }
@@ -366,31 +366,117 @@ describe('{{featureName}}', () => {
366
366
  }
367
367
  return __APP_PKG_DEFAULT__;
368
368
  }
369
- beforeEach(async () => {
370
- const __APP_PKG__ = __resolveAppId();
371
- try { await driver.terminateApp(__APP_PKG__); } catch { /* app may not be running yet */ }
372
- // Let the termination fully land before relaunching activateApp racing a still-dying
373
- // process can RESUME the old instance (prior scenario's scroll position and all) instead
374
- // of cold-starting, which strands top-of-screen elements above the viewport.
375
- await driver.pause(500);
376
- await driver.activateApp(__APP_PKG__);
377
- // Restore the NATIVE context — a prior scenario that entered a WebView (`switch to [X] frame`)
378
- // without switching back would otherwise poison every following native find.
379
- try { await driver.switchContext('NATIVE_APP'); } catch { /* single-context session */ }
380
- // Restore portrait so a prior scenario's `rotate to landscape` can't leak into this one — the
381
- // app relaunch above resets in-app state, but device orientation is OS-level and persists.
382
- try { await driver.setOrientation('PORTRAIT'); } catch { /* orientation control not supported */ }
383
- // Settle after a cold (re)launch: a Flutter app first renders a BLANK accessibility skeleton
384
- // (nested FrameLayout/View, no content-desc) for several seconds before the real tree appears.
385
- // So require rendered contentat least one non-empty content-desc/text on two consecutive
386
- // reads before proceeding. A generous timeout covers slow cold renders on a stressed emulator.
387
- // Best-effort: a timeout never fails the scenario, it just falls through to the step's own
388
- // auto-wait.
389
- // "Settled" = rendered content present on TWO consecutive reads. Deliberately NOT full source
390
- // equality: a UI with any looping animation (a rotating promo carousel is enough) never yields
391
- // two identical snapshots, so an equality oracle starves and burns the whole timeout on every
392
- // scenario. Two content-bearing reads still reject the blank first-frame skeleton (no
393
- // content-desc/text at all) that a single read could false-positive on.
369
+ // ── Reset tier ────────────────────────────────────────────────────────────────────────────────
370
+ // How much state each scenario starts from (`@reset:` on the Feature; default `soft`):
371
+ // none nothing reset; state carries over BY DESIGN (a cart accumulating across scenarios)
372
+ // soft terminate + activate: in-memory state and the nav stack
373
+ // data soft + wipe the app's data layer (local DB, cache, prefs, keychain)
374
+ // full remove + reinstall: also permissions and install-time state
375
+ // Unlike web, no cheap "reload the URL" exists here, and a cold start is 5–30s — hence a ladder
376
+ // rather than one all-or-nothing switch.
377
+ const __DECLARED_TIER = '{{resetTier}}';
378
+ const __TIER_ORDER = ['none', 'soft', 'data', 'full'];
379
+ // SUNGEN_RESET_TIER RAISES the floor for a whole run CI at `data`, local dev at `soft` — without
380
+ // regenerating a single spec (same runtime-resolution model as the test-data overlays). It never
381
+ // LOWERS a declared tier: a feature that asked for `full` needs it. Resolved once, so an
382
+ // unrecognized value warns once rather than per scenario a silently ignored typo here would
383
+ // reintroduce exactly the false-confidence this tier system exists to remove.
384
+ const __RUN_TIER: string = (() => {
385
+ // Defensive only the compiler validates the tag but fall back to `soft`, NOT to index 0
386
+ // (`none`). A safety net that silently drops to "reset nothing" points the wrong way.
387
+ const declaredIdx = __TIER_ORDER.indexOf(__DECLARED_TIER);
388
+ const declared = declaredIdx < 0 ? __TIER_ORDER.indexOf('soft') : declaredIdx;
389
+ const raw = (process.env.SUNGEN_RESET_TIER || '').trim().toLowerCase();
390
+ if (!raw) return __TIER_ORDER[declared];
391
+ const floor = __TIER_ORDER.indexOf(raw);
392
+ if (floor < 0) {
393
+ console.warn(` ! SUNGEN_RESET_TIER='${raw}' is not a tier (${__TIER_ORDER.join(', ')}) ignored`);
394
+ return __TIER_ORDER[declared];
395
+ }
396
+ return __TIER_ORDER[Math.max(declared, floor)];
397
+ })();
398
+
399
+ // A scenario that FAILED left unknown state behind — a half-written local DB, an open modal — that a
400
+ // plain process relaunch does not undo. So the NEXT scenario resets one rung harder than it asked
401
+ // for: this is the "A fails mid-flow, B fails for A's reasons" case that no amount of relaunching
402
+ // fixes. One rung, one scenario, never sticky — otherwise a single flake would silently promote the
403
+ // whole run to `full` and put 30s on every remaining scenario.
404
+ // `none` is a FIXED POINT by design: it is an explicit state-carryover contract (a cart built up
405
+ // across scenarios), and escalating would silently break the very thing it was asked for. The chain
406
+ // is dead once one link fails anyway, so a relaunch would change only how the rest fail.
407
+ // Mirrors escalateResetTier() in the compiler; a test asserts the two ladders agree.
408
+ const __ESCALATE: Record<string, string> = { none: 'none', soft: 'data', data: 'full', full: 'full' };
409
+ // Holds the TITLE of the scenario that failed, not a boolean — because escalation must reach the next
410
+ // DIFFERENT scenario and must NOT reach a RETRY of the failed one.
411
+ //
412
+ // `mochaOpts.retries` re-runs a failed scenario, and the retry is also "the next run". Escalating it
413
+ // would make attempt 2 reset harder than attempt 1, which breaks the guarantee this config is built
414
+ // on: race victims recover on attempt 2 while REAL failures fail both attempts. A dirty-state failure
415
+ // is a real failure — if the retry silently got a wiped data layer it would pass, the reporter takes
416
+ // the last attempt, and a scenario that only works after a wipe would be delivered as a clean
417
+ // `Passed` under a feature file still reading its weaker declared tier. That false pass is worse than
418
+ // the flakiness escalation was added to fix, so the retry keeps attempt 1's tier and fails honestly.
419
+ //
420
+ // Comparing titles rather than reading mocha's retry counter keeps this independent of the runner's
421
+ // API, and it fails in the SAFE direction: if a title is unavailable both sides compare equal, so no
422
+ // escalation happens and the invariant above still holds. (Two scenarios sharing one title in a file
423
+ // lose the escalation between them — SG-W024 already reports that shape when tiers are involved.)
424
+ let __escalateAfter: string | null = null;
425
+
426
+ // Registered BEFORE any user @afterEach block so a throwing user hook cannot skip the bookkeeping.
427
+ afterEach(async function () {
428
+ __escalateAfter = this.currentTest?.state === 'failed' ? (this.currentTest?.title ?? '') : null;
429
+ });
430
+
431
+ {{#if scenarioResetTiers}}
432
+ // Per-scenario `@reset:` overrides, keyed on the scenario's BASE name. Not on the rendered title:
433
+ // a @cases scenario builds its title at runtime (`name + ' — ' + row.__label`) and appium appends
434
+ // pass-through tags as a `[…]` suffix, so no compile-time key could match what mocha reports.
435
+ // __tierFor therefore strips those two suffixes back off — and MEMBERSHIP-TESTS at every step, so a
436
+ // name it cannot resolve returns 'none' (inherit the feature tier) instead of guessing a wrong one.
437
+ // Silently applying the wrong tier is the failure this whole feature exists to remove.
438
+ const __SCENARIO_TIERS: Record<string, string> = {
439
+ {{#each scenarioResetTiers}}
440
+ '{{escapeQuotes name}}': '{{tier}}',
441
+ {{/each}}
442
+ };
443
+ // Only a @cases scenario builds its it() title at RUNTIME (`name + ' \u2014 ' + row.__label`), so only
444
+ // these names may shed a trailing ` \u2014 <label>` during lookup. Stripping that from EVERY title let a
445
+ // scenario merely NAMED `<something> \u2014 <something>` resolve to a different scenario's tier.
446
+ const __CASES_NAMES = new Set<string>([{{#each scenarioResetTiers}}{{#if cases}}'{{escapeQuotes name}}', {{/if}}{{/each}}]);
447
+ function __tierFor(title: string): string {
448
+ if (Object.prototype.hasOwnProperty.call(__SCENARIO_TIERS, title)) return __SCENARIO_TIERS[title];
449
+ // `\[@` — must be a TAG group, not a trailing selector ref. Without the `@`, a scenario named
450
+ // `VP-001 tap [Save]` would reduce to `VP-001 tap` and could pick up a DIFFERENT scenario's tier.
451
+ const noTags = title.replace(/ \[@[^\]]*\]$/, '');
452
+ if (Object.prototype.hasOwnProperty.call(__SCENARIO_TIERS, noTags)) return __SCENARIO_TIERS[noTags];
453
+ // Shed row-label segments from the RIGHT, one at a time, accepting only a @cases name. Right-to-left
454
+ // and iterative so an em dash inside the scenario NAME or inside the row LABEL still resolves; every
455
+ // step is membership-tested against __CASES_NAMES, so this narrows toward a declared name and never
456
+ // guesses at one.
457
+ let base = noTags;
458
+ for (let i = base.lastIndexOf(' \u2014 '); i >= 0; i = base.lastIndexOf(' \u2014 ')) {
459
+ base = base.slice(0, i);
460
+ if (__CASES_NAMES.has(base)) return __SCENARIO_TIERS[base];
461
+ }
462
+ // Unresolvable ⟹ inherit the feature tier. Never a guess: a wrong tier applied silently is the
463
+ // failure this whole lookup exists to avoid.
464
+ return 'none';
465
+ }
466
+ {{/if}}
467
+
468
+ // Settle after a cold (re)launch: a Flutter app first renders a BLANK accessibility skeleton
469
+ // (nested FrameLayout/View, no content-desc) for several seconds before the real tree appears.
470
+ // So require rendered content — at least one non-empty content-desc/text — on two consecutive
471
+ // reads before proceeding. A generous timeout covers slow cold renders on a stressed emulator.
472
+ // Best-effort: a timeout never fails the scenario, it just falls through to the step's own
473
+ // auto-wait.
474
+ // "Settled" = rendered content present on TWO consecutive reads. Deliberately NOT full source
475
+ // equality: a UI with any looping animation (a rotating promo carousel is enough) never yields
476
+ // two identical snapshots, so an equality oracle starves and burns the whole timeout on every
477
+ // scenario. Two content-bearing reads still reject the blank first-frame skeleton (no
478
+ // content-desc/text at all) that a single read could false-positive on.
479
+ async function __settleUi(timeout: number): Promise<void> {
394
480
  let __prevHadContent = false;
395
481
  await driver
396
482
  .waitUntil(
@@ -401,9 +487,82 @@ describe('{{featureName}}', () => {
401
487
  __prevHadContent = hasContent;
402
488
  return settled;
403
489
  },
404
- { timeout: 30000, interval: 400, timeoutMsg: 'UI did not settle (no rendered content)' },
490
+ { timeout, interval: 400, timeoutMsg: 'UI did not settle (no rendered content)' },
405
491
  )
406
492
  .catch(() => { /* best-effort — fall through to per-step auto-wait */ });
493
+ }
494
+
495
+ // Wipe the app's data layer. `mobile: clearApp` is a UiAutomator2 (Android) command — it maps to
496
+ // `pm clear`, which drops the local DB, cache, SharedPreferences and keychain entries in one call
497
+ // for ~2–5s. XCUITest has NO equivalent, so iOS degrades to a reinstall: slower, but the only way
498
+ // to reach the same layers. That needs a build to install from, hence the named throw.
499
+ async function __clearAppData(pkg: string): Promise<void> {
500
+ if (driver.isIOS) {
501
+ await __reinstallApp(pkg);
502
+ return;
503
+ }
504
+ await driver.execute('mobile: clearApp', { appId: pkg });
505
+ }
506
+
507
+ async function __reinstallApp(pkg: string): Promise<void> {
508
+ const caps = (driver.requestedCapabilities || {}) as Record<string, string>;
509
+ const appPath = caps['appium:app'] || caps['app'];
510
+ if (!appPath) {
511
+ throw new Error(
512
+ `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.`,
513
+ );
514
+ }
515
+ try { await driver.removeApp(pkg); } catch { /* not installed */ }
516
+ await driver.installApp(appPath);
517
+ }
518
+
519
+ // One reset, at the given tier. `none` still restores the NATIVE context and portrait: both leak at
520
+ // OS/session level rather than app level, so skipping them alongside the relaunch would make `none`
521
+ // outright buggy instead of merely stateful. It skips the settle wait, though — nothing relaunched,
522
+ // so there is no blank first frame to wait through, and burning up to 30s per scenario would defeat
523
+ // the whole point of asking for `none`.
524
+ async function __resetApp(tier: string): Promise<void> {
525
+ const __APP_PKG__ = __resolveAppId();
526
+ if (tier !== 'none') {
527
+ if (tier === 'full') {
528
+ try { await driver.terminateApp(__APP_PKG__); } catch { /* app may not be running yet */ }
529
+ await __reinstallApp(__APP_PKG__);
530
+ } else {
531
+ try { await driver.terminateApp(__APP_PKG__); } catch { /* app may not be running yet */ }
532
+ if (tier === 'data') await __clearAppData(__APP_PKG__);
533
+ }
534
+ // Let the termination fully land before relaunching — activateApp racing a still-dying
535
+ // process can RESUME the old instance (prior scenario's scroll position and all) instead
536
+ // of cold-starting, which strands top-of-screen elements above the viewport.
537
+ await driver.pause(500);
538
+ await driver.activateApp(__APP_PKG__);
539
+ }
540
+ // Restore the NATIVE context — a prior scenario that entered a WebView (`switch to [X] frame`)
541
+ // without switching back would otherwise poison every following native find.
542
+ try { await driver.switchContext('NATIVE_APP'); } catch { /* single-context session */ }
543
+ // Restore portrait so a prior scenario's `rotate to landscape` can't leak into this one — the
544
+ // app relaunch above resets in-app state, but device orientation is OS-level and persists.
545
+ try { await driver.setOrientation('PORTRAIT'); } catch { /* orientation control not supported */ }
546
+ // A fresh install paints its first frame slower than a plain relaunch — same oracle, more rope.
547
+ if (tier !== 'none') await __settleUi(tier === 'full' || tier === 'data' ? 45000 : 30000);
548
+ }
549
+ beforeEach(async function () {
550
+ {{#if scenarioResetTiers}}
551
+ // A scenario tier RAISES the feature tier, never lowers it: a @reset:full feature must not be
552
+ // weakened by one scenario asking for less. Use a separate feature when you genuinely need that.
553
+ const __scenarioTier = __tierFor(this.currentTest?.title ?? '');
554
+ const __base = __TIER_ORDER.indexOf(__scenarioTier) > __TIER_ORDER.indexOf(__RUN_TIER) ? __scenarioTier : __RUN_TIER;
555
+ {{else}}
556
+ const __base = __RUN_TIER;
557
+ {{/if}}
558
+ // Same title ⟹ this is a retry of the scenario that just failed: keep its declared tier so a real
559
+ // failure fails both attempts (see __escalateAfter above).
560
+ const __escalate = __escalateAfter !== null && __escalateAfter !== (this.currentTest?.title ?? '');
561
+ const __tier = __escalate ? __ESCALATE[__base] : __base;
562
+ if (__tier !== __base) {
563
+ console.log(` \u2191 reset escalated ${__base} \u2192 ${__tier} (previous scenario failed: "${__escalateAfter}")`);
564
+ }
565
+ await __resetApp(__tier);
407
566
  {{#if backgroundSteps}}
408
567
  // Background runs here (inside beforeEach) — the app resets to its launch screen every scenario,
409
568
  // so Background setup (nav, dismiss launch promos…) must re-run after each relaunch, not once.
@@ -1,4 +1,4 @@
1
- import { TestGeneratorAdapter, TestFileData, ScenarioData } from '../adapter-interface';
1
+ import { TestGeneratorAdapter, TestFileData, ScenarioData, RenderImportsOptions } from '../adapter-interface';
2
2
  /**
3
3
  * PlaywrightAdapter
4
4
  * Generates Playwright TypeScript test code using Handlebars templates
@@ -11,16 +11,7 @@ export declare class PlaywrightAdapter implements TestGeneratorAdapter {
11
11
  constructor();
12
12
  renderTestFile(data: TestFileData): string;
13
13
  renderScenario(data: ScenarioData): string;
14
- renderImports(options?: {
15
- runtimeData?: boolean;
16
- basePath?: string;
17
- isParallel?: boolean;
18
- needsCleanupImport?: boolean;
19
- needsDb?: boolean;
20
- needsApi?: boolean;
21
- needsMock?: boolean;
22
- urlAssertImports?: string;
23
- }): string;
14
+ renderImports(options?: RenderImportsOptions): string;
24
15
  renderBeforeEach(data: {
25
16
  steps: Array<{
26
17
  comment?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"playwright-adapter.d.ts","sourceRoot":"","sources":["../../../../../src/generators/test-generator/adapters/playwright/playwright-adapter.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,oBAAoB,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAGxF;;;GAGG;AACH,qBAAa,iBAAkB,YAAW,oBAAoB;IAC5D,QAAQ,CAAC,aAAa,gBAAgB;IACtC,QAAQ,CAAC,aAAa,cAAc;IACpC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,OAAO,CAAC,cAAc,CAAiB;;IAQvC,cAAc,CAAC,IAAI,EAAE,YAAY,GAAG,MAAM;IAI1C,cAAc,CAAC,IAAI,EAAE,YAAY,GAAG,MAAM;IAI1C,aAAa,CAAC,OAAO,CAAC,EAAE;QAAE,WAAW,CAAC,EAAE,OAAO,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,OAAO,CAAC;QAAC,kBAAkB,CAAC,EAAE,OAAO,CAAE;QAAC,OAAO,CAAC,EAAE,OAAO,CAAC;QAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;QAAC,SAAS,CAAC,EAAE,OAAO,CAAC;QAAC,gBAAgB,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,MAAM;IAIzN,gBAAgB,CAAC,IAAI,EAAE;QAAE,KAAK,EAAE,KAAK,CAAC;YAAE,OAAO,CAAC,EAAE,MAAM,CAAC;YAAC,IAAI,EAAE,MAAM,CAAA;SAAE,CAAC,CAAA;KAAE,GAAG,MAAM;IAIpF,eAAe,CAAC,IAAI,EAAE;QAAE,KAAK,EAAE,KAAK,CAAC;YAAE,OAAO,CAAC,EAAE,MAAM,CAAC;YAAC,IAAI,EAAE,MAAM,CAAA;SAAE,CAAC,CAAA;KAAE,GAAG,MAAM;IAInF,eAAe,CAAC,IAAI,EAAE;QAAE,KAAK,EAAE,KAAK,CAAC;YAAE,OAAO,CAAC,EAAE,MAAM,CAAC;YAAC,IAAI,EAAE,MAAM,CAAA;SAAE,CAAC,CAAA;KAAE,GAAG,MAAM;IAInF,cAAc,CAAC,IAAI,EAAE;QAAE,KAAK,EAAE,KAAK,CAAC;YAAE,OAAO,CAAC,EAAE,MAAM,CAAC;YAAC,IAAI,EAAE,MAAM,CAAA;SAAE,CAAC,CAAA;KAAE,GAAG,MAAM;IAIlF,UAAU,CAAC,YAAY,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,GAAG,MAAM;CAGpD"}
1
+ {"version":3,"file":"playwright-adapter.d.ts","sourceRoot":"","sources":["../../../../../src/generators/test-generator/adapters/playwright/playwright-adapter.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,oBAAoB,EAAE,YAAY,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAG9G;;;GAGG;AACH,qBAAa,iBAAkB,YAAW,oBAAoB;IAC5D,QAAQ,CAAC,aAAa,gBAAgB;IACtC,QAAQ,CAAC,aAAa,cAAc;IACpC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,OAAO,CAAC,cAAc,CAAiB;;IAQvC,cAAc,CAAC,IAAI,EAAE,YAAY,GAAG,MAAM;IAI1C,cAAc,CAAC,IAAI,EAAE,YAAY,GAAG,MAAM;IAI1C,aAAa,CAAC,OAAO,CAAC,EAAE,oBAAoB,GAAG,MAAM;IAIrD,gBAAgB,CAAC,IAAI,EAAE;QAAE,KAAK,EAAE,KAAK,CAAC;YAAE,OAAO,CAAC,EAAE,MAAM,CAAC;YAAC,IAAI,EAAE,MAAM,CAAA;SAAE,CAAC,CAAA;KAAE,GAAG,MAAM;IAIpF,eAAe,CAAC,IAAI,EAAE;QAAE,KAAK,EAAE,KAAK,CAAC;YAAE,OAAO,CAAC,EAAE,MAAM,CAAC;YAAC,IAAI,EAAE,MAAM,CAAA;SAAE,CAAC,CAAA;KAAE,GAAG,MAAM;IAInF,eAAe,CAAC,IAAI,EAAE;QAAE,KAAK,EAAE,KAAK,CAAC;YAAE,OAAO,CAAC,EAAE,MAAM,CAAC;YAAC,IAAI,EAAE,MAAM,CAAA;SAAE,CAAC,CAAA;KAAE,GAAG,MAAM;IAInF,cAAc,CAAC,IAAI,EAAE;QAAE,KAAK,EAAE,KAAK,CAAC;YAAE,OAAO,CAAC,EAAE,MAAM,CAAC;YAAC,IAAI,EAAE,MAAM,CAAA;SAAE,CAAC,CAAA;KAAE,GAAG,MAAM;IAIlF,UAAU,CAAC,YAAY,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,GAAG,MAAM;CAGpD"}
@@ -1 +1 @@
1
- {"version":3,"file":"playwright-adapter.js","sourceRoot":"","sources":["../../../../../src/generators/test-generator/adapters/playwright/playwright-adapter.ts"],"names":[],"mappings":";;;;;;AAAA,gDAAwB;AAExB,2DAAuD;AAEvD;;;GAGG;AACH,MAAa,iBAAiB;IAM5B;QALS,kBAAa,GAAG,YAAY,CAAC;QAC7B,kBAAa,GAAG,UAAU,CAAC;QAKlC,2CAA2C;QAC3C,IAAI,CAAC,YAAY,GAAG,cAAI,CAAC,IAAI,CAAC,SAAS,EAAE,WAAW,CAAC,CAAC;QACtD,IAAI,CAAC,cAAc,GAAG,IAAI,gCAAc,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;IAC9D,CAAC;IAED,cAAc,CAAC,IAAkB;QAC/B,OAAO,IAAI,CAAC,cAAc,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC;IAClD,CAAC;IAED,cAAc,CAAC,IAAkB;QAC/B,OAAO,IAAI,CAAC,cAAc,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC;IAClD,CAAC;IAED,aAAa,CAAC,OAAkM;QAC9M,OAAO,IAAI,CAAC,cAAc,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;IACpD,CAAC;IAED,gBAAgB,CAAC,IAA0D;QACzE,OAAO,IAAI,CAAC,cAAc,CAAC,gBAAgB,CAAC,IAAI,CAAC,CAAC;IACpD,CAAC;IAED,eAAe,CAAC,IAA0D;QACxE,OAAO,IAAI,CAAC,cAAc,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC;IACnD,CAAC;IAED,eAAe,CAAC,IAA0D;QACxE,OAAO,IAAI,CAAC,cAAc,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC;IACnD,CAAC;IAED,cAAc,CAAC,IAA0D;QACvE,OAAO,IAAI,CAAC,cAAc,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC;IAClD,CAAC;IAED,UAAU,CAAC,YAAoB,EAAE,IAAS;QACxC,OAAO,IAAI,CAAC,cAAc,CAAC,UAAU,CAAC,YAAY,EAAE,IAAI,CAAC,CAAC;IAC5D,CAAC;CACF;AA3CD,8CA2CC"}
1
+ {"version":3,"file":"playwright-adapter.js","sourceRoot":"","sources":["../../../../../src/generators/test-generator/adapters/playwright/playwright-adapter.ts"],"names":[],"mappings":";;;;;;AAAA,gDAAwB;AAExB,2DAAuD;AAEvD;;;GAGG;AACH,MAAa,iBAAiB;IAM5B;QALS,kBAAa,GAAG,YAAY,CAAC;QAC7B,kBAAa,GAAG,UAAU,CAAC;QAKlC,2CAA2C;QAC3C,IAAI,CAAC,YAAY,GAAG,cAAI,CAAC,IAAI,CAAC,SAAS,EAAE,WAAW,CAAC,CAAC;QACtD,IAAI,CAAC,cAAc,GAAG,IAAI,gCAAc,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;IAC9D,CAAC;IAED,cAAc,CAAC,IAAkB;QAC/B,OAAO,IAAI,CAAC,cAAc,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC;IAClD,CAAC;IAED,cAAc,CAAC,IAAkB;QAC/B,OAAO,IAAI,CAAC,cAAc,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC;IAClD,CAAC;IAED,aAAa,CAAC,OAA8B;QAC1C,OAAO,IAAI,CAAC,cAAc,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;IACpD,CAAC;IAED,gBAAgB,CAAC,IAA0D;QACzE,OAAO,IAAI,CAAC,cAAc,CAAC,gBAAgB,CAAC,IAAI,CAAC,CAAC;IACpD,CAAC;IAED,eAAe,CAAC,IAA0D;QACxE,OAAO,IAAI,CAAC,cAAc,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC;IACnD,CAAC;IAED,eAAe,CAAC,IAA0D;QACxE,OAAO,IAAI,CAAC,cAAc,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC;IACnD,CAAC;IAED,cAAc,CAAC,IAA0D;QACvE,OAAO,IAAI,CAAC,cAAc,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC;IAClD,CAAC;IAED,UAAU,CAAC,YAAoB,EAAE,IAAS;QACxC,OAAO,IAAI,CAAC,cAAc,CAAC,UAAU,CAAC,YAAY,EAAE,IAAI,CAAC,CAAC;IAC5D,CAAC;CACF;AA3CD,8CA2CC"}
@@ -15,6 +15,9 @@ import { mock } from '{{basePath}}/mock';
15
15
  {{#if urlAssertImports}}
16
16
  import { {{urlAssertImports}} } from '{{basePath}}/url-assert';
17
17
  {{/if}}
18
+ {{#if regexEscapeImport}}
19
+ import { escapeRegExp } from '{{basePath}}/regex-escape';
20
+ {{/if}}
18
21
 
19
22
  // This file is auto-generated from Gherkin feature files
20
23
  // DO NOT EDIT MANUALLY - changes will be overwritten
@@ -1 +1,17 @@
1
- await {{> locator}}.selectOption('{{selectValue}}');
1
+ {{#if probeNative}}
2
+ {{!-- The selector resolved to an ARIA role that a native <select> and a custom trigger share
3
+ (`combobox`/`listbox`). .selectOption() only works on a real <select>; on a Radix/shadcn or
4
+ MUI trigger it throws "Element is not a <select> element". Probe the tag, then take the
5
+ native path or the open-then-pick path. Pin `variant: native` (or bind a `select` recipe in
6
+ qa/app.yaml) to compile one branch only. --}}
7
+ { const el = {{> locator}};
8
+ if ((await el.evaluate((n) => n.tagName.toLowerCase())) === 'select') {
9
+ await el.selectOption('{{selectValue}}');
10
+ } else {
11
+ await el.click();
12
+ await page.getByText('{{selectValue}}', { exact: true }).first().click();
13
+ }
14
+ }
15
+ {{~else~}}
16
+ await {{> locator}}.selectOption('{{selectValue}}');
17
+ {{~/if}}
@@ -1,3 +1,8 @@
1
- {{#if pattern}}await expect({{> locator}}).toHaveAttribute('{{attribute}}', /{{dataValue}}/);
1
+ {{!-- `pattern` opts the value into REGEX semantics: the value IS the pattern, so it must
2
+ NOT be regex-escaped. A string container (not a /…/ literal) keeps it that way in
3
+ runtime-data mode — the transformer escapes values landing in regex LITERALS but
4
+ leaves a whole-marker string as a bare accessor call. escapeQuotes doubles
5
+ backslashes, so `\d` in a static pattern reaches RegExp intact. --}}
6
+ {{#if pattern}}await expect({{> locator}}).toHaveAttribute('{{attribute}}', new RegExp('{{escapeQuotes dataValue}}'));
2
7
  {{else}}await expect({{> locator}}).toHaveAttribute('{{attribute}}', '{{dataValue}}');
3
8
  {{/if}}
@@ -1 +1,18 @@
1
- await expect({{> locator}}).toHaveValue('{{escapeQuotes dataValue}}');
1
+ {{#if probeNative}}
2
+ {{!-- Ambiguous value target (dropdown/select/combobox): ARIA role `combobox` is worn both by a
3
+ native <select> and by a `<button role="combobox">` trigger (Radix/shadcn, MUI, Headless UI).
4
+ toHaveValue() reads the DOM `value` property — a button has none, so it throws "Not an input
5
+ element". Probe the tag and assert whatever the element actually exposes. Same runtime-probe
6
+ idiom as selected-assertion.hbs. Pin a real native control with `variant: native`, or bind an
7
+ `assertValue` recipe, to skip the probe entirely. --}}
8
+ { const el = {{> locator}};
9
+ const tag = await el.evaluate((n) => n.tagName.toLowerCase());
10
+ if (tag === 'input' || tag === 'select' || tag === 'textarea') {
11
+ await expect(el).toHaveValue('{{escapeQuotes dataValue}}');
12
+ } else {
13
+ await expect(el).toHaveText('{{escapeQuotes dataValue}}');
14
+ }
15
+ }
16
+ {{~else~}}
17
+ await expect({{> locator}}).toHaveValue('{{escapeQuotes dataValue}}');
18
+ {{~/if}}
@@ -1 +1,5 @@
1
- await expect({{> locator-base}}.filter({ hasText: /^{{escapeRegex dataValue}}$/ }){{> locator-nth}}).toBeHidden();
1
+ {{!-- Same filter contract as the visible/contains siblings (has-text-value): substring by
2
+ default, `exact: true` in the selector YAML anchors. Divergent defaults let the pair
3
+ `see [X] with {{v}}` (substring, matches) + `… is hidden` (anchored, 0 elements)
4
+ BOTH pass on one element — a vacuous-pass contradiction. --}}
5
+ await expect({{> locator-base}}.filter({ hasText: {{> has-text-value dataValue=dataValue exact=exact}} }){{> locator-nth}}).toBeHidden();
@@ -0,0 +1,5 @@
1
+ {{!-- Format assertion: the label must match the SHAPE the spec states, not a fixed value.
2
+ toHaveText accepts a RegExp and retries under the standard expect timeout, so a label that
3
+ renders late still passes. The message names the FORMAT (not the compiled regex) because the
4
+ failure is read by whoever wrote the requirement. --}}
5
+ await expect({{> locator}}, 'Expected [{{escapeQuotes selectorRef}}] to match the format "{{escapeQuotes format}}"').toHaveText({{{formatRegex}}});
@@ -1,5 +1,7 @@
1
+ {{!-- Filter contract shared with the visible/hidden/contains siblings (has-text-value):
2
+ substring by default, `exact: true` in the selector YAML anchors. --}}
1
3
  {{#if selectedProbe}}
2
- { const el = {{> locator-base}}.filter({ hasText: /^{{escapeRegex dataValue}}$/ }){{> locator-nth}};
4
+ { const el = {{> locator-base}}.filter({ hasText: {{> has-text-value dataValue=dataValue exact=exact}} }){{> locator-nth}};
3
5
  if ((await el.getAttribute('aria-selected')) !== null) {
4
6
  await expect(el).toHaveAttribute('aria-selected', 'true');
5
7
  } else {
@@ -7,5 +9,5 @@ await expect(el).toBeChecked();
7
9
  }
8
10
  }
9
11
  {{else}}
10
- await expect({{> locator-base}}.filter({ hasText: /^{{escapeRegex dataValue}}$/ }){{> locator-nth}}).{{{stateAssertion}}};
12
+ await expect({{> locator-base}}.filter({ hasText: {{> has-text-value dataValue=dataValue exact=exact}} }){{> locator-nth}}).{{{stateAssertion}}};
11
13
  {{/if}}
@@ -1 +1,4 @@
1
- await {{> locator-base}}.filter({ hasText: /^{{escapeRegex dataValue}}$/ }){{> locator-nth}}.waitFor({ state: '{{state}}' });
1
+ {{!-- Filter contract shared with the see-assertion siblings (has-text-value): substring by
2
+ default, `exact: true` in the selector YAML anchors — `wait for [X] with {{v}}` and
3
+ `see [X] … with {{v}}` must match the same element for the same input. --}}
4
+ await {{> locator-base}}.filter({ hasText: {{> has-text-value dataValue=dataValue exact=exact}} }){{> locator-nth}}.waitFor({ state: '{{state}}' });
@@ -1 +1 @@
1
- await expect({{{targetExpr}}}).toContainText({{{valueExpr}}});
1
+ await expect({{{targetExpr}}}).{{#if exact}}toHaveText{{else}}toContainText{{/if}}({{{valueExpr}}});
@@ -33,6 +33,12 @@ export declare class CodeGenerator {
33
33
  * Generate test file from parsed feature
34
34
  */
35
35
  generateTest(feature: ParsedFeature, outputDir: string): Promise<GeneratedTest>;
36
+ /**
37
+ * Report SG-E022 when the emitted spec is not parseable. Reported, never thrown: the
38
+ * caller still writes the file, so the user keeps the artifact to inspect (and `--strict`
39
+ * turns it into a non-zero exit like every other diagnostic).
40
+ */
41
+ private assertParseable;
36
42
  /**
37
43
  * Count total steps in feature (excluding background).
38
44
  * For @extend scenarios, counts base steps + own steps (merged total).
@@ -1 +1 @@
1
- {"version":3,"file":"code-generator.d.ts","sourceRoot":"","sources":["../../../src/generators/test-generator/code-generator.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,aAAa,EAA8B,MAAM,mBAAmB,CAAC;AAI9E,OAAO,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAqOrD;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,qBAAa,aAAa;IACxB,OAAO,CAAC,UAAU,CAAa;IAC/B,OAAO,CAAC,OAAO,CAAuB;IACtC,OAAO,CAAC,UAAU,CAAC,CAAS;IAC5B,OAAO,CAAC,OAAO,CAAM;IAErB,OAAO,CAAC,eAAe,CAAc;IAErC,OAAO,CAAC,aAAa,CAAqC;IAE1D,OAAO,CAAC,WAAW,CAA8B;gBAErC,OAAO,GAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAC;QAAC,OAAO,CAAC,EAAE,OAAO,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,OAAO,CAAC;QAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;KAAO;IAiBtK,oFAAoF;IACpF,cAAc,IAAI,oBAAoB;IAItC;;OAEG;IACG,YAAY,CAChB,OAAO,EAAE,aAAa,EACtB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,aAAa,CAAC;IA0EzB;;;OAGG;IACH,OAAO,CAAC,UAAU;IAclB;;OAEG;IACH,SAAS,CAAC,aAAa,EAAE,aAAa,GAAG,IAAI;IAS7C;;OAEG;IACH;;;;;OAKG;IACH,OAAO,CAAC,mBAAmB;IA0C3B;;;;OAIG;IACH,OAAO,CAAC,uBAAuB;IAoB/B;;;;;OAKG;IACH,OAAO,CAAC,mBAAmB;IAY3B,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;YASzB,gBAAgB;YA0ThB,uBAAuB;YAYvB,kBAAkB;YAoBlB,YAAY;IAqB1B;;;;;OAKG;IACH,OAAO,CAAC,wBAAwB;YASlB,gBAAgB;IAoJ9B;;OAEG;IACH,OAAO,CAAC,UAAU;IAKlB;;;OAGG;IACH,OAAO,CAAC,qBAAqB;IAS7B;;OAEG;IACH;;;;;;;OAOG;IACH,OAAO,CAAC,eAAe,CAAqB;IAC5C,OAAO,CAAC,kBAAkB;IAyBpB,gBAAgB,CACpB,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,MAAM,EAAE,GACrB,OAAO,CAAC,aAAa,EAAE,CAAC;CA+D5B"}
1
+ {"version":3,"file":"code-generator.d.ts","sourceRoot":"","sources":["../../../src/generators/test-generator/code-generator.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,aAAa,EAA8B,MAAM,mBAAmB,CAAC;AAI9E,OAAO,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAuOrD;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,qBAAa,aAAa;IACxB,OAAO,CAAC,UAAU,CAAa;IAC/B,OAAO,CAAC,OAAO,CAAuB;IACtC,OAAO,CAAC,UAAU,CAAC,CAAS;IAC5B,OAAO,CAAC,OAAO,CAAM;IAErB,OAAO,CAAC,eAAe,CAAc;IAErC,OAAO,CAAC,aAAa,CAAqC;IAE1D,OAAO,CAAC,WAAW,CAA8B;gBAErC,OAAO,GAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAC;QAAC,OAAO,CAAC,EAAE,OAAO,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,OAAO,CAAC;QAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;KAAO;IAiBtK,oFAAoF;IACpF,cAAc,IAAI,oBAAoB;IAItC;;OAEG;IACG,YAAY,CAChB,OAAO,EAAE,aAAa,EACtB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,aAAa,CAAC;IA4FzB;;;;OAIG;IACH,OAAO,CAAC,eAAe;IAavB;;;OAGG;IACH,OAAO,CAAC,UAAU;IAclB;;OAEG;IACH,SAAS,CAAC,aAAa,EAAE,aAAa,GAAG,IAAI;IAS7C;;OAEG;IACH;;;;;OAKG;IACH,OAAO,CAAC,mBAAmB;IA0C3B;;;;OAIG;IACH,OAAO,CAAC,uBAAuB;IAoB/B;;;;;OAKG;IACH,OAAO,CAAC,mBAAmB;IAY3B,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;YASzB,gBAAgB;YAmWhB,uBAAuB;YAYvB,kBAAkB;YAoBlB,YAAY;IAqB1B;;;;;OAKG;IACH,OAAO,CAAC,wBAAwB;YASlB,gBAAgB;IAoJ9B;;OAEG;IACH,OAAO,CAAC,UAAU;IAKlB;;;OAGG;IACH,OAAO,CAAC,qBAAqB;IAS7B;;OAEG;IACH;;;;;;;OAOG;IACH,OAAO,CAAC,eAAe,CAAqB;IAC5C,OAAO,CAAC,kBAAkB;IAyBpB,gBAAgB,CACpB,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,MAAM,EAAE,GACrB,OAAO,CAAC,aAAa,EAAE,CAAC;CA+D5B"}
@@ -10,10 +10,12 @@ const step_mapper_1 = require("./step-mapper");
10
10
  const adapters_1 = require("./adapters");
11
11
  const runtime_data_transformer_1 = require("./utils/runtime-data-transformer");
12
12
  const diagnostics_1 = require("./diagnostics");
13
+ const reset_tier_1 = require("./reset-tier");
13
14
  const registry_1 = require("../../capabilities/registry");
14
15
  const discover_1 = require("../../capabilities/discover");
15
16
  const capability_1 = require("../../harness/capability");
16
17
  const scenario_merger_1 = require("../../exporters/scenario-merger");
18
+ const spec_syntax_check_1 = require("./utils/spec-syntax-check");
17
19
  /**
18
20
  * Filter base scenario steps for @extend: only keep Given→When steps.
19
21
  * Skips Then and any And/But that follow Then (since And/But inherit
@@ -82,7 +84,7 @@ function extractCleanupFlags(tags) {
82
84
  const FUNCTIONAL_TAG_PREFIXES = [
83
85
  '@parallel', '@serial', '@cleanup:', '@auth:', '@manual', '@no-auth',
84
86
  '@steps:', '@extend:', '@screenshot:', '@beforeAll', '@afterEach', '@afterAll',
85
- '@flow', '@cases:', '@deferred:', '@owned-by:', '@platform:',
87
+ '@flow', '@cases:', '@deferred:', '@owned-by:', '@platform:', '@reset:',
86
88
  ];
87
89
  function extractPassThroughTags(scenarioTags, featureTags) {
88
90
  const allTags = [...featureTags, ...scenarioTags];
@@ -298,7 +300,15 @@ class CodeGenerator {
298
300
  this.syncGeneratedHelper(outputDir, h.file, h.template);
299
301
  }
300
302
  // Generate test code (async now to support AI mapping)
301
- const testCode = await this.generateTestCode(feature);
303
+ let testCode = await this.generateTestCode(feature);
304
+ // Runtime data: replace __SUNGEN_TD$…$__ markers with testData.get() calls.
305
+ // Runs BEFORE the imports are rendered so they can reflect what the transformed
306
+ // body actually references (a marker inside a regex literal becomes an
307
+ // escapeRegExp(...) call that needs its helper imported). Imports carry no
308
+ // markers, so transforming only the body loses nothing.
309
+ if (this.options.runtimeData) {
310
+ testCode = (0, runtime_data_transformer_1.transformToRuntimeData)(testCode);
311
+ }
302
312
  // `url-assert` is imported from what the body actually emitted, not from an active capability:
303
313
  // page assertions belong to the DEFAULT ui capability, so a capability flag would import the
304
314
  // helper into every UI spec — including the many that touch no URL at all. Hence the body is
@@ -307,14 +317,23 @@ class CodeGenerator {
307
317
  const urlAssertImports = ['urlMatches', 'joinPath'].filter((n) => testCode.includes(`${n}(`)).join(', ');
308
318
  if (urlAssertImports)
309
319
  this.syncGeneratedHelper(outputDir, 'url-assert.ts', 'specs-url-assert.ts');
310
- const imports = this.adapter.renderImports({ runtimeData: this.options.runtimeData, basePath, needsCleanupImport, needsDb, needsApi, needsMock, urlAssertImports });
320
+ // Same body-first rule for the runtime regex-escape helper: only a spec whose
321
+ // transformed body wraps a value in escapeRegExp() imports (and syncs) it.
322
+ const regexEscapeImport = /\bescapeRegExp\(/.test(testCode);
323
+ if (regexEscapeImport)
324
+ this.syncGeneratedHelper(outputDir, 'regex-escape.ts', 'specs-regex-escape.ts');
325
+ const imports = this.adapter.renderImports({ runtimeData: this.options.runtimeData, basePath, needsCleanupImport, needsDb, needsApi, needsMock, urlAssertImports, regexEscapeImport });
311
326
  // Combine and collapse any runs of 3+ newlines down to 2 (one blank line max)
312
327
  const raw = `${imports}\n\n${testCode}`;
313
328
  let code = raw.replace(/\n{3,}/g, '\n\n');
314
- // Runtime data: replace __SUNGEN_TD$…$__ markers with testData.get() calls
315
- if (this.options.runtimeData) {
316
- code = (0, runtime_data_transformer_1.transformToRuntimeData)(code);
317
- }
329
+ // SG-E022 compiler self-check. Every template and post-processor above assembles
330
+ // TEXT, so a delimiter bug in any of them yields a file Playwright cannot parse at all:
331
+ // one SyntaxError, zero scenarios run. That is exactly how the runtime-data quote-pairing
332
+ // defect reached a user — `sungen generate` reported success and the breakage only
333
+ // surfaced at `npx playwright test`, a step further from the cause. Parsing what we are
334
+ // about to write makes the compiler name its own bad output, at the moment it makes it.
335
+ // (The runtime-data transform itself ran on the body above, before the imports.)
336
+ this.assertParseable(code, feature.name, filePath);
318
337
  return {
319
338
  featureName: feature.name,
320
339
  filePath,
@@ -323,6 +342,20 @@ class CodeGenerator {
323
342
  stepCount: this.countSteps(feature),
324
343
  };
325
344
  }
345
+ /**
346
+ * Report SG-E022 when the emitted spec is not parseable. Reported, never thrown: the
347
+ * caller still writes the file, so the user keeps the artifact to inspect (and `--strict`
348
+ * turns it into a non-zero exit like every other diagnostic).
349
+ */
350
+ assertParseable(code, featureName, filePath) {
351
+ const detail = (0, spec_syntax_check_1.findSyntaxError)(code);
352
+ if (!detail)
353
+ return;
354
+ this.diagnostics.add('SG-E022', `generated spec "${filePath}" is not parseable TypeScript (${detail}) — Playwright will `
355
+ + 'fail the whole file with one SyntaxError and run NONE of its scenarios. This is a sungen '
356
+ + 'compiler bug, not a problem with the .feature: please report it with the step this file '
357
+ + 'was generated from.', { feature: featureName });
358
+ }
326
359
  /**
327
360
  * Count total steps in feature (excluding background).
328
361
  * For @extend scenarios, counts base steps + own steps (merged total).
@@ -696,6 +729,44 @@ class CodeGenerator {
696
729
  // package (activity stripped, it isn't needed for terminateApp/activateApp); iosBundleId is the part
697
730
  // after `|` when present. Without these the test-file template's `{{#if appPackage}}` stays false and
698
731
  // the beforeEach (terminate+activate) reset is never emitted → nav scenarios cascade-fail.
732
+ // Mobile: how much state each scenario starts from — `@reset:none|soft|data|full`, default 'soft'
733
+ // (the terminate+activate relaunch this adapter always did, so an untagged feature is unchanged).
734
+ // Appium-only: web features get a clean slate from navigation and carry no tier.
735
+ const resetTier = this.adapter.frameworkName === 'appium'
736
+ ? ((0, reset_tier_1.parseResetTier)(feature.tags || []) ?? reset_tier_1.DEFAULT_RESET_TIER)
737
+ : undefined;
738
+ // Per-scenario `@reset:` overrides. Keyed on the scenario's BASE name — NOT the rendered it()
739
+ // title, because a @cases scenario concatenates its title at runtime from the row label, so no
740
+ // compile-time key could match it. The generated spec normalizes a title back to a base name and
741
+ // membership-tests each step, so a miss just inherits the feature tier rather than guessing wrong.
742
+ // `cases` marks a scenario whose it() title is BUILT AT RUNTIME (`name + ' — ' + row.__label`).
743
+ // Only those titles may have a row suffix stripped when looking a tier up; applying that strip to
744
+ // every scenario let a name that merely LOOKED like `<base> — <suffix>` resolve to a different
745
+ // scenario's tier.
746
+ const scenarioResetTiers = this.adapter.frameworkName === 'appium'
747
+ ? (feature.scenarios || []).reduce((acc, sc) => {
748
+ const tier = (0, reset_tier_1.parseResetTier)(sc.tags || []);
749
+ if (tier)
750
+ acc.push({ name: sc.name, tier, cases: (sc.tags || []).some(t => t.startsWith('@cases:')) });
751
+ return acc;
752
+ }, [])
753
+ : undefined;
754
+ // SG-W024 — the runtime lookup is keyed on the scenario NAME, so two scenarios sharing a name
755
+ // cannot be told apart. Counted over EVERY scenario, not just the tagged ones: an UNTAGGED
756
+ // scenario sharing a name with a tagged one silently inherits that tier (weakening a
757
+ // `@reset:none` chain, or paying a reinstall it never asked for), and scanning only the tagged
758
+ // subset missed exactly that case.
759
+ if (scenarioResetTiers && scenarioResetTiers.length) {
760
+ const tiered = new Set(scenarioResetTiers.map(t => t.name));
761
+ const counts = new Map();
762
+ for (const sc of feature.scenarios || [])
763
+ counts.set(sc.name, (counts.get(sc.name) || 0) + 1);
764
+ for (const [name, n] of counts) {
765
+ if (n < 2 || !tiered.has(name))
766
+ continue;
767
+ this.diagnostics.add('SG-W024', `${n} scenarios in this feature are named "${name}" and at least one carries a @reset: tag — the generated lookup is keyed on the scenario NAME, so every one of them resolves to the SAME tier, including any that declared none. Rename them so each is unique.`, { feature: feature.name, step: name });
768
+ }
769
+ }
699
770
  let appPackage;
700
771
  let iosBundleId;
701
772
  if (this.adapter.frameworkName === 'appium' && feature.path) {
@@ -731,6 +802,8 @@ class CodeGenerator {
731
802
  singleAuthRole,
732
803
  appPackage,
733
804
  iosBundleId,
805
+ resetTier,
806
+ scenarioResetTiers: scenarioResetTiers && scenarioResetTiers.length ? scenarioResetTiers : undefined,
734
807
  });
735
808
  }
736
809
  async generateBackgroundSteps(background) {