create-appraisejs 0.1.10-alpha.8 → 0.2.0-alpha.1

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 (414) hide show
  1. package/README.md +52 -52
  2. package/dist/cli.e2e.test.d.ts +2 -0
  3. package/dist/cli.e2e.test.d.ts.map +1 -0
  4. package/dist/cli.e2e.test.js +73 -0
  5. package/dist/cli.e2e.test.js.map +1 -0
  6. package/dist/config.test.d.ts +2 -0
  7. package/dist/config.test.d.ts.map +1 -0
  8. package/dist/config.test.js +65 -0
  9. package/dist/config.test.js.map +1 -0
  10. package/dist/copy-template.test.d.ts +2 -0
  11. package/dist/copy-template.test.d.ts.map +1 -0
  12. package/dist/copy-template.test.js +71 -0
  13. package/dist/copy-template.test.js.map +1 -0
  14. package/dist/download-repo.test.d.ts +2 -0
  15. package/dist/download-repo.test.d.ts.map +1 -0
  16. package/dist/download-repo.test.js +14 -0
  17. package/dist/download-repo.test.js.map +1 -0
  18. package/dist/install.test.d.ts +2 -0
  19. package/dist/install.test.d.ts.map +1 -0
  20. package/dist/install.test.js +119 -0
  21. package/dist/install.test.js.map +1 -0
  22. package/dist/prepare-template-utils.d.ts.map +1 -1
  23. package/dist/prepare-template-utils.js +5 -0
  24. package/dist/prepare-template-utils.js.map +1 -1
  25. package/dist/prompts.test.d.ts +2 -0
  26. package/dist/prompts.test.d.ts.map +1 -0
  27. package/dist/prompts.test.js +58 -0
  28. package/dist/prompts.test.js.map +1 -0
  29. package/dist/scaffold-gitignore.d.ts +1 -0
  30. package/dist/scaffold-gitignore.d.ts.map +1 -1
  31. package/dist/scaffold-gitignore.js +3 -0
  32. package/dist/scaffold-gitignore.js.map +1 -1
  33. package/package.json +3 -2
  34. package/templates/default/.appraise-template-meta.json +2 -2
  35. package/templates/default/.env.example +2 -2
  36. package/templates/default/.vscode/settings.json +2 -1
  37. package/templates/default/README.md +53 -53
  38. package/templates/default/automation/mapping/locator-map.json +1 -14
  39. package/templates/default/automation/steps/actions/click.step.ts +58 -58
  40. package/templates/default/automation/steps/actions/hover.step.ts +27 -27
  41. package/templates/default/automation/steps/actions/input.step.ts +84 -84
  42. package/templates/default/automation/steps/actions/navigation.step.ts +70 -70
  43. package/templates/default/automation/steps/actions/random_data.step.ts +142 -142
  44. package/templates/default/automation/steps/actions/store.step.ts +86 -86
  45. package/templates/default/automation/steps/actions/wait.step.ts +90 -90
  46. package/templates/default/automation/steps/validations/active_state_assertion.step.ts +30 -30
  47. package/templates/default/automation/steps/validations/navigation_assertion.step.ts +22 -22
  48. package/templates/default/automation/steps/validations/text_assertion.step.ts +107 -107
  49. package/templates/default/automation/steps/validations/visibility_assertion.step.ts +26 -26
  50. package/templates/default/components.json +24 -24
  51. package/templates/default/cucumber.mjs +16 -16
  52. package/templates/default/eslint.config.mjs +16 -16
  53. package/templates/default/gitignore +3 -0
  54. package/templates/default/next-env.d.ts +6 -6
  55. package/templates/default/next.config.ts +11 -11
  56. package/templates/default/package-lock.json +146 -75
  57. package/templates/default/package.json +2 -1
  58. package/templates/default/packages/cucumber-runtime/package.json +13 -13
  59. package/templates/default/packages/cucumber-runtime/src/cache.util.ts +93 -93
  60. package/templates/default/packages/cucumber-runtime/src/cli.ts +68 -68
  61. package/templates/default/packages/cucumber-runtime/src/environment.util.ts +21 -21
  62. package/templates/default/packages/cucumber-runtime/src/executor.ts +32 -32
  63. package/templates/default/packages/cucumber-runtime/src/hooks.ts +25 -3
  64. package/templates/default/packages/cucumber-runtime/src/index.ts +17 -17
  65. package/templates/default/packages/cucumber-runtime/src/locator.util.ts +234 -234
  66. package/templates/default/packages/cucumber-runtime/src/parameter-types.ts +7 -7
  67. package/templates/default/packages/cucumber-runtime/src/paths.ts +66 -1
  68. package/templates/default/packages/cucumber-runtime/src/random-data.util.ts +35 -35
  69. package/templates/default/packages/cucumber-runtime/src/types.ts +13 -13
  70. package/templates/default/packages/cucumber-runtime/src/world.ts +44 -44
  71. package/templates/default/packages/cucumber-runtime/tsconfig.json +11 -11
  72. package/templates/default/packages/locator-picker-companion/dist/cli.d.ts +1 -0
  73. package/templates/default/packages/locator-picker-companion/dist/cli.js +302 -0
  74. package/templates/default/packages/locator-picker-companion/dist/index.d.ts +3 -0
  75. package/templates/default/packages/locator-picker-companion/dist/index.js +3 -0
  76. package/templates/default/packages/locator-picker-companion/dist/injected-picker-script.d.ts +1 -0
  77. package/templates/default/packages/locator-picker-companion/dist/injected-picker-script.js +615 -0
  78. package/templates/default/packages/locator-picker-companion/dist/launcher.d.ts +11 -0
  79. package/templates/default/packages/locator-picker-companion/dist/launcher.js +59 -0
  80. package/templates/default/packages/locator-picker-companion/dist/selector-generator.d.ts +3 -0
  81. package/templates/default/packages/locator-picker-companion/dist/selector-generator.js +257 -0
  82. package/templates/default/packages/locator-picker-companion/dist/session-file.d.ts +22 -0
  83. package/templates/default/packages/locator-picker-companion/dist/session-file.js +150 -0
  84. package/templates/default/packages/locator-picker-companion/dist/types.d.ts +31 -0
  85. package/templates/default/packages/locator-picker-companion/dist/types.js +1 -0
  86. package/templates/default/packages/locator-picker-companion/package.json +17 -0
  87. package/templates/default/packages/locator-picker-companion/src/cli.ts +428 -0
  88. package/templates/default/packages/locator-picker-companion/src/index.ts +3 -0
  89. package/templates/default/packages/locator-picker-companion/src/injected-picker-script.ts +771 -0
  90. package/templates/default/packages/locator-picker-companion/src/launcher.ts +77 -0
  91. package/templates/default/packages/locator-picker-companion/src/selector-generator.ts +339 -0
  92. package/templates/default/packages/locator-picker-companion/src/session-file.ts +226 -0
  93. package/templates/default/packages/locator-picker-companion/src/types.ts +42 -0
  94. package/templates/default/packages/locator-picker-companion/tsconfig.json +14 -0
  95. package/templates/default/postcss.config.mjs +8 -8
  96. package/templates/default/prisma/dev.db +0 -0
  97. package/templates/default/prisma/migrations/20251104113456_add_type_for_template_step_groups/migration.sql +16 -16
  98. package/templates/default/prisma/migrations/20251104170946_add_tags_to_test_suite_and_test_case/migration.sql +27 -27
  99. package/templates/default/prisma/migrations/20251112190024_add_cascade_delete_to_test_run_test_case/migration.sql +17 -17
  100. package/templates/default/prisma/migrations/20251113181100_add_test_run_log/migration.sql +12 -12
  101. package/templates/default/prisma/migrations/20251119191838_add_tag_type/migration.sql +28 -28
  102. package/templates/default/prisma/migrations/20251121164059_add_conflict_resolution/migration.sql +12 -12
  103. package/templates/default/prisma/migrations/20251223183400_add_report_model_to_db_schema/migration.sql +10 -10
  104. package/templates/default/prisma/migrations/20251223183637_add_report_test_case_entity_for_storing_test_results_for_individual_test_cases/migration.sql +10 -10
  105. package/templates/default/prisma/migrations/20251224083549_add_comprehensive_report_storage/migration.sql +108 -108
  106. package/templates/default/prisma/migrations/20251229194422_migrate_duration_to_string/migration.sql +55 -55
  107. package/templates/default/prisma/migrations/20251230124637_add_unique_constraint_to_test_run_name/migration.sql +27 -27
  108. package/templates/default/prisma/migrations/20260115094436_add_dashboard_metrics/migration.sql +59 -59
  109. package/templates/default/prisma/migrations/20260127172022_add_cascade_delete_to_step_parameters/migration.sql +34 -34
  110. package/templates/default/prisma/migrations/20260313093000_add_report_step_screenshot_path/migration.sql +1 -0
  111. package/templates/default/prisma/migrations/20260318120000_add_test_suite_context_to_test_run_test_case/migration.sql +20 -0
  112. package/templates/default/prisma/migrations/20260318173512_add_support_of_test_suite_level_runs/migration.sql +2 -0
  113. package/templates/default/prisma/schema.prisma +4 -0
  114. package/templates/default/scripts/install-playwright.ts +20 -0
  115. package/templates/default/scripts/protect-seeded-files.ts +38 -38
  116. package/templates/default/scripts/regenerate-features.ts +94 -94
  117. package/templates/default/scripts/setup-env.ts +19 -19
  118. package/templates/default/scripts/sync-all.ts +22 -66
  119. package/templates/default/scripts/sync-appraise-base-template.ts +57 -19
  120. package/templates/default/scripts/sync-environments.ts +40 -21
  121. package/templates/default/scripts/sync-locator-groups.ts +410 -410
  122. package/templates/default/scripts/sync-locators.ts +398 -398
  123. package/templates/default/scripts/sync-modules.ts +341 -341
  124. package/templates/default/scripts/sync-tags.ts +2 -3
  125. package/templates/default/scripts/sync-template-step-groups.ts +0 -3
  126. package/templates/default/scripts/sync-template-steps.ts +37 -11
  127. package/templates/default/scripts/sync-test-cases.ts +68 -16
  128. package/templates/default/scripts/sync-test-suites.ts +7 -5
  129. package/templates/default/src/actions/conflict/conflict.action.ts +33 -33
  130. package/templates/default/src/actions/dashboard/dashboard-actions.ts +240 -240
  131. package/templates/default/src/actions/environments/environment-actions.ts +188 -188
  132. package/templates/default/src/actions/locator/locator-actions.ts +490 -490
  133. package/templates/default/src/actions/locator-groups/locator-group-actions.ts +6 -3
  134. package/templates/default/src/actions/locator-picker/locator-picker-actions.ts +243 -0
  135. package/templates/default/src/actions/modules/module-actions.ts +135 -135
  136. package/templates/default/src/actions/reports/report-actions.ts +51 -49
  137. package/templates/default/src/actions/review/review-actions.ts +147 -147
  138. package/templates/default/src/actions/settings/sync-actions.ts +46 -0
  139. package/templates/default/src/actions/tags/tag-actions.ts +107 -107
  140. package/templates/default/src/actions/template-step/template-step-actions.ts +38 -4
  141. package/templates/default/src/actions/template-step-group/template-step-group-actions.ts +10 -3
  142. package/templates/default/src/actions/template-test-case/template-test-case-actions.ts +238 -238
  143. package/templates/default/src/actions/test-case/test-case-actions.ts +356 -356
  144. package/templates/default/src/actions/test-run/test-run-actions.ts +222 -129
  145. package/templates/default/src/actions/test-suite/test-suite-actions.ts +95 -17
  146. package/templates/default/src/actions/user/user-actions.ts +13 -13
  147. package/templates/default/src/app/(base)/environments/create/page.tsx +28 -28
  148. package/templates/default/src/app/(base)/environments/environment-form.tsx +219 -219
  149. package/templates/default/src/app/(base)/environments/environment-table-columns.tsx +96 -96
  150. package/templates/default/src/app/(base)/environments/environment-table.tsx +24 -24
  151. package/templates/default/src/app/(base)/environments/modify/[id]/page.tsx +46 -46
  152. package/templates/default/src/app/(base)/environments/page.tsx +59 -59
  153. package/templates/default/src/app/(base)/layout.tsx +10 -10
  154. package/templates/default/src/app/(base)/locator-groups/create/page.tsx +44 -44
  155. package/templates/default/src/app/(base)/locator-groups/locator-group-form.tsx +215 -215
  156. package/templates/default/src/app/(base)/locator-groups/locator-group-table-columns.tsx +77 -77
  157. package/templates/default/src/app/(base)/locator-groups/locator-group-table.tsx +28 -28
  158. package/templates/default/src/app/(base)/locator-groups/modify/[id]/page.tsx +46 -46
  159. package/templates/default/src/app/(base)/locator-groups/page.tsx +20 -8
  160. package/templates/default/src/app/(base)/locators/create/create-locator-workspace.tsx +542 -0
  161. package/templates/default/src/app/(base)/locators/create/page.tsx +32 -19
  162. package/templates/default/src/app/(base)/locators/locator-form.tsx +163 -163
  163. package/templates/default/src/app/(base)/locators/locator-table-columns.tsx +73 -73
  164. package/templates/default/src/app/(base)/locators/locator-table.tsx +28 -28
  165. package/templates/default/src/app/(base)/locators/modify/[id]/page.tsx +56 -27
  166. package/templates/default/src/app/(base)/locators/page.tsx +13 -13
  167. package/templates/default/src/app/(base)/locators/picker/page.tsx +7 -0
  168. package/templates/default/src/app/(base)/locators/sync-locators-button.tsx +66 -66
  169. package/templates/default/src/app/(base)/modules/create/page.tsx +34 -34
  170. package/templates/default/src/app/(base)/modules/modify/[id]/page.tsx +46 -46
  171. package/templates/default/src/app/(base)/modules/module-form.tsx +126 -126
  172. package/templates/default/src/app/(base)/modules/module-table-columns.tsx +85 -85
  173. package/templates/default/src/app/(base)/modules/module-table.tsx +24 -24
  174. package/templates/default/src/app/(base)/modules/page.tsx +59 -59
  175. package/templates/default/src/app/(base)/reports/[id]/page.tsx +40 -33
  176. package/templates/default/src/app/(base)/reports/duration-chart.tsx +33 -33
  177. package/templates/default/src/app/(base)/reports/feature-chart.tsx +78 -78
  178. package/templates/default/src/app/(base)/reports/overview-chart.tsx +49 -46
  179. package/templates/default/src/app/(base)/reports/page.tsx +98 -98
  180. package/templates/default/src/app/(base)/reports/report-metric-card.tsx +78 -16
  181. package/templates/default/src/app/(base)/reports/report-table-columns.tsx +189 -189
  182. package/templates/default/src/app/(base)/reports/report-table.tsx +72 -72
  183. package/templates/default/src/app/(base)/reports/report-view-table-columns.tsx +12 -1
  184. package/templates/default/src/app/(base)/reports/report-view-table.tsx +1 -0
  185. package/templates/default/src/app/(base)/reports/test-cases/page.tsx +40 -42
  186. package/templates/default/src/app/(base)/reports/test-cases/test-cases-metric-table-columns.tsx +115 -115
  187. package/templates/default/src/app/(base)/reports/test-cases/test-cases-metric-table.tsx +27 -27
  188. package/templates/default/src/app/(base)/reports/test-suites/page.tsx +42 -42
  189. package/templates/default/src/app/(base)/reports/test-suites/test-suites-metric-table-columns.tsx +79 -79
  190. package/templates/default/src/app/(base)/reports/test-suites/test-suites-metric-table.tsx +27 -27
  191. package/templates/default/src/app/(base)/reports/view-logs-button.tsx +58 -60
  192. package/templates/default/src/app/(base)/reviews/create/page.tsx +26 -26
  193. package/templates/default/src/app/(base)/reviews/created-reviews-table.tsx +15 -15
  194. package/templates/default/src/app/(base)/reviews/modify/[id]/page.tsx +26 -26
  195. package/templates/default/src/app/(base)/reviews/page.tsx +26 -26
  196. package/templates/default/src/app/(base)/reviews/review/[id]/page.tsx +26 -26
  197. package/templates/default/src/app/(base)/reviews/review-form.tsx +11 -11
  198. package/templates/default/src/app/(base)/reviews/review-table-by-creator-columns.tsx +9 -9
  199. package/templates/default/src/app/(base)/reviews/review-table-by-reviewer-columns.tsx +9 -9
  200. package/templates/default/src/app/(base)/reviews/reviewer-reviews-table.tsx +15 -15
  201. package/templates/default/src/app/(base)/settings/page.tsx +30 -0
  202. package/templates/default/src/app/(base)/settings/settings-sync-panel.tsx +261 -0
  203. package/templates/default/src/app/(base)/tags/create/page.tsx +39 -39
  204. package/templates/default/src/app/(base)/tags/modify/[id]/page.tsx +50 -50
  205. package/templates/default/src/app/(base)/tags/page.tsx +58 -58
  206. package/templates/default/src/app/(base)/tags/tag-form.tsx +147 -147
  207. package/templates/default/src/app/(base)/tags/tag-table-columns.tsx +63 -63
  208. package/templates/default/src/app/(base)/tags/tag-table.tsx +29 -29
  209. package/templates/default/src/app/(base)/template-step-groups/create/page.tsx +28 -28
  210. package/templates/default/src/app/(base)/template-step-groups/modify/[id]/page.tsx +45 -45
  211. package/templates/default/src/app/(base)/template-step-groups/page.tsx +60 -60
  212. package/templates/default/src/app/(base)/template-step-groups/template-step-group-form.tsx +167 -167
  213. package/templates/default/src/app/(base)/template-step-groups/template-step-group-table-columns.tsx +89 -89
  214. package/templates/default/src/app/(base)/template-step-groups/template-step-group-table.tsx +32 -32
  215. package/templates/default/src/app/(base)/template-steps/create/page.tsx +37 -37
  216. package/templates/default/src/app/(base)/template-steps/modify/[id]/page.tsx +49 -49
  217. package/templates/default/src/app/(base)/template-steps/page.tsx +58 -58
  218. package/templates/default/src/app/(base)/template-steps/paramChip.tsx +213 -213
  219. package/templates/default/src/app/(base)/template-steps/template-step-form.tsx +384 -384
  220. package/templates/default/src/app/(base)/template-steps/template-step-table-columns.tsx +158 -158
  221. package/templates/default/src/app/(base)/template-steps/template-step-table.tsx +24 -24
  222. package/templates/default/src/app/(base)/template-test-cases/create/page.tsx +56 -56
  223. package/templates/default/src/app/(base)/template-test-cases/modify/[id]/page.tsx +89 -89
  224. package/templates/default/src/app/(base)/template-test-cases/page.tsx +58 -58
  225. package/templates/default/src/app/(base)/template-test-cases/template-test-case-flow.tsx +84 -84
  226. package/templates/default/src/app/(base)/template-test-cases/template-test-case-form.tsx +262 -262
  227. package/templates/default/src/app/(base)/template-test-cases/template-test-case-table-columns.tsx +76 -76
  228. package/templates/default/src/app/(base)/template-test-cases/template-test-case-table.tsx +32 -32
  229. package/templates/default/src/app/(base)/test-cases/create/page.tsx +76 -76
  230. package/templates/default/src/app/(base)/test-cases/create-from-template/generate/[id]/page.tsx +96 -96
  231. package/templates/default/src/app/(base)/test-cases/create-from-template/page.tsx +38 -38
  232. package/templates/default/src/app/(base)/test-cases/create-from-template/template-selection-form.tsx +73 -73
  233. package/templates/default/src/app/(base)/test-cases/modify/[id]/page.tsx +106 -106
  234. package/templates/default/src/app/(base)/test-cases/page.tsx +2 -2
  235. package/templates/default/src/app/(base)/test-cases/test-case-flow.tsx +82 -82
  236. package/templates/default/src/app/(base)/test-cases/test-case-form.tsx +395 -395
  237. package/templates/default/src/app/(base)/test-cases/test-case-table-columns.tsx +6 -71
  238. package/templates/default/src/app/(base)/test-cases/test-case-table.tsx +2 -2
  239. package/templates/default/src/app/(base)/test-runs/[id]/page.tsx +4 -1
  240. package/templates/default/src/app/(base)/test-runs/create/page.tsx +11 -9
  241. package/templates/default/src/app/(base)/test-runs/page.tsx +60 -60
  242. package/templates/default/src/app/(base)/test-runs/test-run-form.tsx +37 -44
  243. package/templates/default/src/app/(base)/test-runs/test-run-table-columns.tsx +229 -229
  244. package/templates/default/src/app/(base)/test-runs/test-run-table.tsx +127 -127
  245. package/templates/default/src/app/(base)/test-suites/create/page.tsx +3 -2
  246. package/templates/default/src/app/(base)/test-suites/modify/[id]/page.tsx +2 -1
  247. package/templates/default/src/app/(base)/test-suites/page.tsx +57 -21
  248. package/templates/default/src/app/(base)/test-suites/test-suite-form.tsx +16 -17
  249. package/templates/default/src/app/(base)/test-suites/test-suite-table-columns.tsx +2 -1
  250. package/templates/default/src/app/(base)/test-suites/test-suite-table.tsx +29 -29
  251. package/templates/default/src/app/(dashboard-components)/app-drawer.tsx +187 -187
  252. package/templates/default/src/app/(dashboard-components)/data-card-grid.tsx +12 -12
  253. package/templates/default/src/app/(dashboard-components)/data-card.tsx +26 -26
  254. package/templates/default/src/app/(dashboard-components)/execution-health-panel.tsx +56 -56
  255. package/templates/default/src/app/(dashboard-components)/ongoing-test-runs-card.tsx +87 -87
  256. package/templates/default/src/app/(dashboard-components)/quick-actions-drawer.tsx +44 -44
  257. package/templates/default/src/app/api/reports/steps/[stepId]/screenshot/route.ts +52 -0
  258. package/templates/default/src/app/api/test-runs/[runId]/download/route.ts +97 -34
  259. package/templates/default/src/app/api/test-runs/[runId]/logs/route.ts +4 -1
  260. package/templates/default/src/app/api/test-runs/[runId]/trace/[testCaseId]/route.ts +146 -147
  261. package/templates/default/src/app/globals.css +147 -147
  262. package/templates/default/src/app/layout.tsx +4 -0
  263. package/templates/default/src/app/page.tsx +2 -0
  264. package/templates/default/src/assets/icons/empty-tube.tsx +23 -23
  265. package/templates/default/src/assets/icons/tube-plus.tsx +29 -29
  266. package/templates/default/src/components/base-node.tsx +21 -21
  267. package/templates/default/src/components/chart/pie-chart.tsx +73 -73
  268. package/templates/default/src/components/data-extraction/locator-inspector.tsx +460 -460
  269. package/templates/default/src/components/data-state/empty-state.tsx +40 -40
  270. package/templates/default/src/components/data-visualization/info-card.tsx +70 -70
  271. package/templates/default/src/components/data-visualization/info-grid.tsx +22 -22
  272. package/templates/default/src/components/diagram/button-edge.tsx +54 -54
  273. package/templates/default/src/components/diagram/dynamic-parameters.tsx +474 -474
  274. package/templates/default/src/components/diagram/edit-header-option.tsx +36 -36
  275. package/templates/default/src/components/diagram/flow-diagram.tsx +470 -470
  276. package/templates/default/src/components/diagram/node-form.tsx +262 -262
  277. package/templates/default/src/components/diagram/options-header-node.tsx +57 -57
  278. package/templates/default/src/components/diagram/template-step-combobox.tsx +155 -155
  279. package/templates/default/src/components/form/error-message.tsx +7 -7
  280. package/templates/default/src/components/kokonutui/smooth-tab.tsx +453 -453
  281. package/templates/default/src/components/loading-skeleton/data-table/data-table-skeleton.tsx +30 -30
  282. package/templates/default/src/components/loading-skeleton/form/button-skeleton.tsx +8 -8
  283. package/templates/default/src/components/loading-skeleton/form/icon-button-skeleton.tsx +8 -8
  284. package/templates/default/src/components/loading-skeleton/form/text-input-skeleton.tsx +8 -8
  285. package/templates/default/src/components/loading-skeleton/visualization/table-skeleton.tsx +14 -14
  286. package/templates/default/src/components/logo.tsx +15 -15
  287. package/templates/default/src/components/navigation/command-badge.tsx +34 -34
  288. package/templates/default/src/components/navigation/command-chain-input.tsx +51 -51
  289. package/templates/default/src/components/navigation/entity-search-command.tsx +116 -116
  290. package/templates/default/src/components/navigation/nav-card.tsx +31 -31
  291. package/templates/default/src/components/navigation/nav-command.tsx +13 -0
  292. package/templates/default/src/components/navigation/nav-link.tsx +60 -60
  293. package/templates/default/src/components/navigation/nav-menu-card-deck.tsx +112 -112
  294. package/templates/default/src/components/node-header.tsx +159 -159
  295. package/templates/default/src/components/reports/test-case-logs-modal.tsx +310 -253
  296. package/templates/default/src/components/table/table-actions.tsx +172 -172
  297. package/templates/default/src/components/test-case/test-case-columns.tsx +76 -0
  298. package/templates/default/src/components/test-case/test-case-picker.tsx +244 -0
  299. package/templates/default/src/components/test-run/download-logs-button.tsx +92 -99
  300. package/templates/default/src/components/test-run/log-viewer.tsx +1 -14
  301. package/templates/default/src/components/test-run/test-run-details.tsx +15 -2
  302. package/templates/default/src/components/test-run/test-run-header.tsx +149 -149
  303. package/templates/default/src/components/test-run/view-report-button.tsx +102 -102
  304. package/templates/default/src/components/test-suite/test-suite-picker.tsx +444 -0
  305. package/templates/default/src/components/theme/mode-toggle.tsx +54 -54
  306. package/templates/default/src/components/theme/theme-provider.tsx +8 -8
  307. package/templates/default/src/components/typography/page-header-subtitle.tsx +7 -7
  308. package/templates/default/src/components/typography/page-header.tsx +7 -7
  309. package/templates/default/src/components/ui/alert-dialog.tsx +106 -106
  310. package/templates/default/src/components/ui/alert.tsx +43 -43
  311. package/templates/default/src/components/ui/avatar.tsx +40 -40
  312. package/templates/default/src/components/ui/badge.tsx +29 -29
  313. package/templates/default/src/components/ui/button.tsx +47 -47
  314. package/templates/default/src/components/ui/calendar.tsx +158 -158
  315. package/templates/default/src/components/ui/card.tsx +43 -43
  316. package/templates/default/src/components/ui/checkbox.tsx +28 -28
  317. package/templates/default/src/components/ui/command.tsx +135 -135
  318. package/templates/default/src/components/ui/data-table-column-header.tsx +61 -61
  319. package/templates/default/src/components/ui/data-table-pagination.tsx +87 -87
  320. package/templates/default/src/components/ui/data-table-view-options.tsx +50 -50
  321. package/templates/default/src/components/ui/data-table.tsx +261 -261
  322. package/templates/default/src/components/ui/dialog.tsx +97 -97
  323. package/templates/default/src/components/ui/dropdown-menu.tsx +182 -182
  324. package/templates/default/src/components/ui/input.tsx +22 -22
  325. package/templates/default/src/components/ui/kbd.tsx +28 -28
  326. package/templates/default/src/components/ui/label.tsx +19 -19
  327. package/templates/default/src/components/ui/loading.tsx +12 -12
  328. package/templates/default/src/components/ui/multi-select-with-preview.tsx +116 -116
  329. package/templates/default/src/components/ui/multi-select.tsx +142 -142
  330. package/templates/default/src/components/ui/navigation-menu.tsx +120 -120
  331. package/templates/default/src/components/ui/popover.tsx +33 -33
  332. package/templates/default/src/components/ui/progress.tsx +25 -25
  333. package/templates/default/src/components/ui/radio-group.tsx +44 -44
  334. package/templates/default/src/components/ui/scroll-area.tsx +40 -40
  335. package/templates/default/src/components/ui/select.tsx +151 -151
  336. package/templates/default/src/components/ui/separator.tsx +22 -22
  337. package/templates/default/src/components/ui/skeleton.tsx +7 -7
  338. package/templates/default/src/components/ui/table.tsx +76 -76
  339. package/templates/default/src/components/ui/tabs.tsx +55 -55
  340. package/templates/default/src/components/ui/textarea.tsx +21 -21
  341. package/templates/default/src/components/ui/toast.tsx +113 -113
  342. package/templates/default/src/components/ui/toaster.tsx +26 -26
  343. package/templates/default/src/components/user-prompt/delete-prompt.tsx +87 -87
  344. package/templates/default/src/config/db-config.ts +10 -10
  345. package/templates/default/src/constants/form-opts/diagram/node-form.ts +30 -30
  346. package/templates/default/src/constants/form-opts/environment-form-opts.ts +24 -24
  347. package/templates/default/src/constants/form-opts/locator-form-opts.ts +20 -20
  348. package/templates/default/src/constants/form-opts/locator-group-form-opts.ts +28 -28
  349. package/templates/default/src/constants/form-opts/module-form-opts.ts +21 -21
  350. package/templates/default/src/constants/form-opts/review-form-opts.ts +23 -23
  351. package/templates/default/src/constants/form-opts/tag-form-opts.ts +42 -42
  352. package/templates/default/src/constants/form-opts/template-selection-form-opts.ts +16 -16
  353. package/templates/default/src/constants/form-opts/template-step-group-form-opts.ts +24 -24
  354. package/templates/default/src/constants/form-opts/template-test-case-form-opts.ts +39 -39
  355. package/templates/default/src/constants/form-opts/template-test-step-form-opts.ts +36 -36
  356. package/templates/default/src/constants/form-opts/test-case-form-opts.ts +43 -43
  357. package/templates/default/src/constants/form-opts/test-run-form-opts.ts +5 -3
  358. package/templates/default/src/constants/form-opts/test-suite-form-opts.ts +24 -24
  359. package/templates/default/src/hooks/use-toast.ts +187 -187
  360. package/templates/default/src/lib/automation/paths.ts +42 -12
  361. package/templates/default/src/lib/automation/projection-service.ts +2 -1
  362. package/templates/default/src/lib/bidirectional-sync.ts +432 -432
  363. package/templates/default/src/lib/database-sync.ts +2 -7
  364. package/templates/default/src/lib/environment-file-utils.ts +184 -184
  365. package/templates/default/src/lib/executor/local-executor-adapter.ts +24 -25
  366. package/templates/default/src/lib/executor/types.ts +2 -2
  367. package/templates/default/src/lib/feature-file-generator.ts +5 -66
  368. package/templates/default/src/lib/gherkin-parser.ts +259 -259
  369. package/templates/default/src/lib/locator-group-file-utils.ts +307 -307
  370. package/templates/default/src/lib/locator-picker/session-manager.ts +452 -0
  371. package/templates/default/src/lib/locator-picker/suggestions.ts +124 -0
  372. package/templates/default/src/lib/metrics/metric-calculator.ts +5 -0
  373. package/templates/default/src/lib/module-hierarchy-builder.ts +205 -205
  374. package/templates/default/src/lib/path-helpers/module-path.ts +71 -71
  375. package/templates/default/src/lib/process/task-spawner.ts +39 -2
  376. package/templates/default/src/lib/sync/projected-feature-utils.ts +113 -0
  377. package/templates/default/src/lib/sync/sync-executor.ts +130 -0
  378. package/templates/default/src/lib/sync/sync-pending-counts.test.ts +239 -0
  379. package/templates/default/src/lib/sync/sync-pending-counts.ts +1358 -0
  380. package/templates/default/src/lib/sync/sync-registry.ts +149 -0
  381. package/templates/default/src/lib/tag-utils.ts +28 -0
  382. package/templates/default/src/lib/template-sync-utils.d.ts +6 -6
  383. package/templates/default/src/lib/template-sync-utils.js +46 -46
  384. package/templates/default/src/lib/template-sync-utils.ts +63 -63
  385. package/templates/default/src/lib/test-case-utils.ts +6 -6
  386. package/templates/default/src/lib/test-run/log-formatter.ts +83 -83
  387. package/templates/default/src/lib/test-run/matching.ts +117 -0
  388. package/templates/default/src/lib/test-run/process-manager.ts +2 -0
  389. package/templates/default/src/lib/test-run/report-parser.ts +352 -316
  390. package/templates/default/src/lib/test-run/test-run-executor.ts +13 -15
  391. package/templates/default/src/lib/test-run/winston-logger.ts +64 -66
  392. package/templates/default/src/lib/test-suite-identifier-service.ts +76 -0
  393. package/templates/default/src/lib/test-suite-utils.ts +6 -0
  394. package/templates/default/src/lib/transformers/gherkin-converter.ts +42 -42
  395. package/templates/default/src/lib/transformers/key-to-icon-transformer.tsx +95 -95
  396. package/templates/default/src/lib/transformers/template-test-case-converter.ts +160 -160
  397. package/templates/default/src/lib/utils/node-param-validation.ts +81 -81
  398. package/templates/default/src/lib/utils/template-step-file-generator.ts +104 -104
  399. package/templates/default/src/lib/utils/template-step-file-manager-intelligent.ts +0 -8
  400. package/templates/default/src/lib/utils/template-step-file-manager.ts +166 -166
  401. package/templates/default/src/lib/utils.ts +31 -31
  402. package/templates/default/src/types/diagram/diagram.ts +34 -34
  403. package/templates/default/src/types/diagram/template-step.ts +11 -11
  404. package/templates/default/src/types/executor/browser.type.ts +1 -1
  405. package/templates/default/src/types/form/actionHandler.ts +6 -6
  406. package/templates/default/src/types/locator/locator.type.ts +11 -11
  407. package/templates/default/src/types/locator-picker.ts +65 -0
  408. package/templates/default/src/types/step/step.type.ts +1 -1
  409. package/templates/default/src/types/table/data-table.ts +6 -6
  410. package/templates/default/src/types/test-case-picker.ts +6 -0
  411. package/templates/default/src/types/test-suite-picker.ts +18 -0
  412. package/templates/default/tailwind.config.ts +62 -62
  413. package/templates/default/tsconfig.json +4 -1
  414. package/templates/default/appraisejs.config.json +0 -1
@@ -0,0 +1,1358 @@
1
+ import { promises as fs } from 'fs'
2
+ import { join } from 'path'
3
+ import { glob } from 'glob'
4
+ import prettier from 'prettier'
5
+ import { parse } from '@babel/parser'
6
+ import * as t from '@babel/types'
7
+ import _traverse from '@babel/traverse'
8
+ import type { NodePath } from '@babel/traverse'
9
+ import {
10
+ StepParameterType,
11
+ TagType,
12
+ TemplateStepGroupType,
13
+ TemplateStepIcon,
14
+ TemplateStepType,
15
+ } from '@prisma/client'
16
+ import prisma from '@/config/db-config'
17
+ import {
18
+ ensureAutomationWorkspaceReady,
19
+ getAutomationEnvironmentsDir,
20
+ getAutomationFeaturesDir,
21
+ } from '@/lib/automation/paths'
22
+ import { extractModulePathFromFilePath, scanFeatureFiles, type ParsedFeature, type ParsedStep } from '@/lib/gherkin-parser'
23
+ import { getAllModulesWithPaths } from '@/lib/module-hierarchy-builder'
24
+ import { SYNC_ALL_REQUEST_ID, syncScriptDefinitions, type SyncRequestId, type SyncScriptId } from '@/lib/sync/sync-registry'
25
+ import { getTagTypeFromName } from '@/lib/tag-utils'
26
+ import { extractModulePathFromAutomationFile, getAutomationLocatorMapPath } from '@/lib/template-sync-utils'
27
+ import {
28
+ determineProjectedStepIcon,
29
+ getTestSuiteSyncIdentity,
30
+ normalizeProjectedDbTestCaseSteps,
31
+ } from '@/lib/sync/projected-feature-utils'
32
+
33
+ const traverse = (_traverse as { default?: typeof _traverse }).default ?? _traverse
34
+
35
+ export type SyncPendingCounts = Record<SyncRequestId, number>
36
+
37
+ type EnvironmentConfig = {
38
+ baseUrl: string
39
+ apiBaseUrl: string
40
+ email: string
41
+ password: string
42
+ }
43
+
44
+ type EnvironmentData = {
45
+ name: string
46
+ baseUrl: string
47
+ apiBaseUrl: string | null
48
+ username: string | null
49
+ password: string | null
50
+ }
51
+
52
+ type LocatorMapEntry = {
53
+ name: string
54
+ path: string
55
+ }
56
+
57
+ type LocatorGroupFromFs = {
58
+ name: string
59
+ route: string
60
+ modulePath: string
61
+ }
62
+
63
+ type LocatorFileData = {
64
+ groupName: string
65
+ modulePath: string
66
+ locators: Record<string, string>
67
+ }
68
+
69
+ type StepGroupJSDoc = {
70
+ name: string
71
+ description: string | null
72
+ type: TemplateStepGroupType
73
+ }
74
+
75
+ type StepJSDoc = {
76
+ name: string
77
+ description: string | null
78
+ icon: TemplateStepIcon
79
+ }
80
+
81
+ type StepParameter = {
82
+ name: string
83
+ type: StepParameterType
84
+ order: number
85
+ }
86
+
87
+ type ParsedTemplateStep = {
88
+ jsdoc: StepJSDoc
89
+ signature: string
90
+ functionDefinition: string
91
+ normalizedFunctionDefinition: string
92
+ parameters: StepParameter[]
93
+ keyword: 'When' | 'Then' | 'Given'
94
+ }
95
+
96
+ type ParsedStepFile = {
97
+ group: StepGroupJSDoc
98
+ steps: ParsedTemplateStep[]
99
+ }
100
+
101
+ type TemplateStepFromFs = {
102
+ step: ParsedTemplateStep
103
+ groupName: string
104
+ groupType: TemplateStepGroupType
105
+ }
106
+
107
+ type TestSuiteFromFs = {
108
+ name: string
109
+ description: string | null
110
+ modulePath: string
111
+ tags: string[]
112
+ }
113
+
114
+ type TestCaseFromFs = {
115
+ identifierTag: string
116
+ title: string
117
+ description: string
118
+ testSuiteName: string
119
+ modulePath: string
120
+ filterTags: string[]
121
+ steps: ParsedStep[]
122
+ }
123
+
124
+ type ParameterMatch = {
125
+ name: string
126
+ value: string
127
+ order: number
128
+ type: StepParameterType
129
+ }
130
+
131
+ type FilesystemSnapshot = {
132
+ environments: EnvironmentData[]
133
+ modulePaths: Set<string>
134
+ locatorGroups: LocatorGroupFromFs[]
135
+ locatorFiles: LocatorFileData[]
136
+ tagObjects: Array<{ name: string; tagExpression: string; type: TagType }>
137
+ templateStepGroups: StepGroupJSDoc[]
138
+ templateSteps: TemplateStepFromFs[]
139
+ testSuites: TestSuiteFromFs[]
140
+ testCases: TestCaseFromFs[]
141
+ }
142
+
143
+ function normalizeEnvironmentName(name: string): string {
144
+ if (!name || name.trim() === '') {
145
+ return name
146
+ }
147
+
148
+ return name
149
+ .trim()
150
+ .replace(/[_\s]+/g, ' ')
151
+ .split(' ')
152
+ .filter(Boolean)
153
+ .map(segment => segment.charAt(0).toUpperCase() + segment.slice(1).toLowerCase())
154
+ .join(' ')
155
+ }
156
+
157
+ function getEnvironmentIdentityKey(name: string): string {
158
+ return name
159
+ .trim()
160
+ .replace(/[_\s]+/g, ' ')
161
+ .toLowerCase()
162
+ }
163
+
164
+ async function normalizeFunctionDefinition(functionDefinition: string | null | undefined): Promise<string> {
165
+ const source = functionDefinition?.trim()
166
+ if (!source) {
167
+ return ''
168
+ }
169
+
170
+ try {
171
+ return (
172
+ await prettier.format(source, {
173
+ parser: 'typescript',
174
+ semi: true,
175
+ singleQuote: true,
176
+ trailingComma: 'es5',
177
+ printWidth: 80,
178
+ tabWidth: 2,
179
+ })
180
+ ).trim()
181
+ } catch {
182
+ return source
183
+ }
184
+ }
185
+
186
+ async function readEnvironmentsFromFile(): Promise<EnvironmentData[]> {
187
+ const filePath = join(getAutomationEnvironmentsDir(), 'environments.json')
188
+
189
+ try {
190
+ const fileContent = await fs.readFile(filePath, 'utf-8')
191
+ const jsonContent = JSON.parse(fileContent) as Record<string, EnvironmentConfig>
192
+
193
+ return Object.entries(jsonContent).map(([key, config]) => ({
194
+ name: normalizeEnvironmentName(key),
195
+ baseUrl: config.baseUrl.trim(),
196
+ apiBaseUrl: config.apiBaseUrl?.trim() ? config.apiBaseUrl.trim() : null,
197
+ username: config.email?.trim() ? config.email.trim() : null,
198
+ password: config.password?.trim() ? config.password.trim() : null,
199
+ }))
200
+ } catch (error) {
201
+ console.error('Unable to read environments for sync counts:', error)
202
+ return []
203
+ }
204
+ }
205
+
206
+ function splitTagLine(tagLine: string): string[] {
207
+ return tagLine
208
+ .split(/\s+/)
209
+ .filter(tag => tag.trim().startsWith('@'))
210
+ .map(tag => tag.trim())
211
+ }
212
+
213
+ function normalizeTagExpression(tagExpression: string): string {
214
+ return tagExpression.startsWith('@') ? tagExpression : `@${tagExpression}`
215
+ }
216
+
217
+ function extractUniqueTags(parsedFeatures: ParsedFeature[]): Set<string> {
218
+ const uniqueTags = new Set<string>()
219
+
220
+ for (const feature of parsedFeatures) {
221
+ for (const tagLine of feature.tags) {
222
+ for (const tag of splitTagLine(tagLine)) {
223
+ uniqueTags.add(tag)
224
+ }
225
+ }
226
+
227
+ for (const scenario of feature.scenarios) {
228
+ for (const tagLine of scenario.tags) {
229
+ for (const tag of splitTagLine(tagLine)) {
230
+ uniqueTags.add(tag)
231
+ }
232
+ }
233
+ }
234
+ }
235
+
236
+ return uniqueTags
237
+ }
238
+
239
+ function buildTagObjects(tagExpressions: Set<string>): Array<{ name: string; tagExpression: string; type: TagType }> {
240
+ return Array.from(tagExpressions).map(tagExpression => {
241
+ const name = tagExpression.startsWith('@') ? tagExpression.substring(1) : tagExpression
242
+ return {
243
+ name,
244
+ tagExpression,
245
+ type: getTagTypeFromName(name),
246
+ }
247
+ })
248
+ }
249
+
250
+ function buildModuleTreePaths(modulePaths: Iterable<string>): Set<string> {
251
+ const tree = new Set<string>()
252
+
253
+ for (const modulePath of modulePaths) {
254
+ if (modulePath === '/') {
255
+ tree.add('/')
256
+ continue
257
+ }
258
+
259
+ const parts = modulePath.split('/').filter(Boolean)
260
+ let currentPath = ''
261
+
262
+ for (const part of parts) {
263
+ currentPath += `/${part}`
264
+ tree.add(currentPath)
265
+ }
266
+ }
267
+
268
+ return tree
269
+ }
270
+
271
+ async function scanLocatorFiles(baseDir: string): Promise<string[]> {
272
+ const files = await glob('automation/locators/**/*.json', { cwd: baseDir })
273
+ return files.map(file => join(baseDir, file))
274
+ }
275
+
276
+ async function readLocatorMap(baseDir: string): Promise<LocatorMapEntry[]> {
277
+ try {
278
+ const fileContent = await fs.readFile(getAutomationLocatorMapPath(baseDir), 'utf-8')
279
+ const locatorMap = JSON.parse(fileContent) as LocatorMapEntry[]
280
+ return Array.isArray(locatorMap) ? locatorMap : []
281
+ } catch {
282
+ return []
283
+ }
284
+ }
285
+
286
+ function extractLocatorGroupName(filePath: string): string {
287
+ const fileName = filePath.split(/[/\\]/).pop() ?? ''
288
+ return fileName.replace(/\.json$/, '')
289
+ }
290
+
291
+ async function readLocatorFile(filePath: string): Promise<Record<string, string>> {
292
+ try {
293
+ const fileContent = await fs.readFile(filePath, 'utf-8')
294
+ const content = JSON.parse(fileContent) as Record<string, string>
295
+ return typeof content === 'object' && content !== null ? content : {}
296
+ } catch (error) {
297
+ console.error(`Unable to read locator file ${filePath}:`, error)
298
+ return {}
299
+ }
300
+ }
301
+
302
+ function parseGroupJSDoc(content: string): StepGroupJSDoc | null {
303
+ const lines = content.split('\n')
304
+ let startLine = 0
305
+
306
+ while (startLine < lines.length) {
307
+ const line = lines[startLine].trim()
308
+ if (line === '' || line.startsWith('import ')) {
309
+ startLine++
310
+ continue
311
+ }
312
+ break
313
+ }
314
+
315
+ if (startLine >= lines.length || !lines[startLine].trim().startsWith('/**')) {
316
+ return null
317
+ }
318
+
319
+ let hasType = false
320
+ let endLine = -1
321
+ let name: string | null = null
322
+ let description: string | null = null
323
+ let type: string | null = null
324
+
325
+ const maxLines = Math.min(lines.length, startLine + 50)
326
+ for (let i = startLine; i < maxLines; i++) {
327
+ const line = lines[i].trim()
328
+
329
+ if (line.includes('*/')) {
330
+ const beforeClose = line.split('*/')[0].trim()
331
+
332
+ if (beforeClose.startsWith('* @name') || beforeClose.startsWith('*@name')) {
333
+ const match = beforeClose.match(/@name\s+(.+)/)
334
+ if (match) name = match[1].trim()
335
+ } else if (beforeClose.startsWith('* @description') || beforeClose.startsWith('*@description')) {
336
+ const match = beforeClose.match(/@description\s+(.+)/)
337
+ if (match) description = match[1].trim() || null
338
+ } else if (beforeClose.startsWith('* @type') || beforeClose.startsWith('*@type')) {
339
+ const match = beforeClose.match(/@type\s+(.+)/)
340
+ if (match) {
341
+ hasType = true
342
+ type = match[1].trim()
343
+ }
344
+ }
345
+
346
+ endLine = i
347
+ break
348
+ }
349
+
350
+ if (line.startsWith('* @name') || line.startsWith('*@name')) {
351
+ const match = line.match(/@name\s+(.+)/)
352
+ if (match) name = match[1].trim()
353
+ } else if (line.startsWith('* @description') || line.startsWith('*@description')) {
354
+ const match = line.match(/@description\s+(.+)/)
355
+ if (match) description = match[1].trim() || null
356
+ } else if (line.startsWith('* @type') || line.startsWith('*@type')) {
357
+ const match = line.match(/@type\s+(.+)/)
358
+ if (match) {
359
+ hasType = true
360
+ type = match[1].trim()
361
+ }
362
+ }
363
+ }
364
+
365
+ if (!hasType || endLine < 0 || !name || !type) {
366
+ return null
367
+ }
368
+
369
+ const normalizedType = type.toUpperCase()
370
+ if (normalizedType !== 'ACTION' && normalizedType !== 'VALIDATION') {
371
+ return null
372
+ }
373
+
374
+ return {
375
+ name: name.trim(),
376
+ description: description ? description.trim() : null,
377
+ type: normalizedType as TemplateStepGroupType,
378
+ }
379
+ }
380
+
381
+ function parseStepJSDoc(content: string, startLine: number): StepJSDoc | null {
382
+ const lines = content.split('\n')
383
+ let jsdocStart = -1
384
+
385
+ for (let i = startLine - 1; i >= 0 && i >= startLine - 20; i--) {
386
+ const line = lines[i]?.trim()
387
+ if (line?.includes('*/')) {
388
+ jsdocStart = i
389
+ for (let j = i - 1; j >= 0 && j >= i - 10; j--) {
390
+ const previousLine = lines[j]?.trim()
391
+ if (previousLine?.startsWith('/**')) {
392
+ jsdocStart = j
393
+ break
394
+ }
395
+ }
396
+ break
397
+ }
398
+
399
+ if (line?.startsWith('/**')) {
400
+ jsdocStart = i
401
+ break
402
+ }
403
+ }
404
+
405
+ if (jsdocStart === -1) {
406
+ return null
407
+ }
408
+
409
+ let name: string | null = null
410
+ let description: string | null = null
411
+ let icon: string | null = null
412
+ let foundJSDoc = false
413
+
414
+ for (let i = jsdocStart; i < Math.min(lines.length, jsdocStart + 20); i++) {
415
+ const line = lines[i]?.trim()
416
+
417
+ if (line?.startsWith('/**')) {
418
+ foundJSDoc = true
419
+ continue
420
+ }
421
+
422
+ if (line?.includes('*/')) {
423
+ const beforeClose = line.split('*/')[0].trim()
424
+ if (beforeClose.startsWith('* @name') || beforeClose.startsWith('*@name')) {
425
+ const match = beforeClose.match(/@name\s+(.+)/)
426
+ if (match) name = match[1].trim()
427
+ } else if (beforeClose.startsWith('* @description') || beforeClose.startsWith('*@description')) {
428
+ const match = beforeClose.match(/@description\s+(.+)/)
429
+ if (match) description = match[1].trim() || null
430
+ } else if (beforeClose.startsWith('* @icon') || beforeClose.startsWith('*@icon')) {
431
+ const match = beforeClose.match(/@icon\s+(.+)/)
432
+ if (match) icon = match[1].trim()
433
+ }
434
+ break
435
+ }
436
+
437
+ if (!foundJSDoc) {
438
+ continue
439
+ }
440
+
441
+ if (line?.startsWith('* @name') || line?.startsWith('*@name')) {
442
+ const match = line.match(/@name\s+(.+)/)
443
+ if (match) name = match[1].trim()
444
+ } else if (line?.startsWith('* @description') || line?.startsWith('*@description')) {
445
+ const match = line.match(/@description\s+(.+)/)
446
+ if (match) description = match[1].trim() || null
447
+ } else if (line?.startsWith('* @icon') || line?.startsWith('*@icon')) {
448
+ const match = line.match(/@icon\s+(.+)/)
449
+ if (match) icon = match[1].trim()
450
+ }
451
+ }
452
+
453
+ if (!name || !icon) {
454
+ return null
455
+ }
456
+
457
+ const iconUpper = icon.toUpperCase()
458
+ const validIcons = Object.values(TemplateStepIcon)
459
+ if (!validIcons.includes(iconUpper as TemplateStepIcon)) {
460
+ return null
461
+ }
462
+
463
+ return {
464
+ name: name.trim(),
465
+ description: description ? description.trim() : null,
466
+ icon: iconUpper as TemplateStepIcon,
467
+ }
468
+ }
469
+
470
+ function mapTypeToParameterType(typeName: string): StepParameterType {
471
+ const normalized = typeName.trim()
472
+
473
+ if (normalized === 'SelectorName') return StepParameterType.LOCATOR
474
+ if (normalized === 'string') return StepParameterType.STRING
475
+ if (normalized === 'number' || normalized === 'int') return StepParameterType.NUMBER
476
+ if (normalized === 'boolean') return StepParameterType.BOOLEAN
477
+ if (normalized === 'Date') return StepParameterType.DATE
478
+
479
+ throw new Error(`Unsupported parameter type: ${typeName}`)
480
+ }
481
+
482
+ function extractFunctionDefinition(callExpression: t.CallExpression, sourceCode: string): string {
483
+ const start = callExpression.start
484
+ const end = callExpression.end
485
+
486
+ if (start == null || end == null) {
487
+ throw new Error('Cannot extract function definition.')
488
+ }
489
+
490
+ let code = sourceCode.slice(start, end).trim()
491
+ const trailingSource = sourceCode.slice(end, end + 10).trim()
492
+ if (trailingSource.startsWith(';')) {
493
+ code += ';'
494
+ }
495
+
496
+ return code
497
+ }
498
+
499
+ function parseStepFile(content: string): ParsedStepFile | null {
500
+ const group = parseGroupJSDoc(content)
501
+ if (!group) {
502
+ return null
503
+ }
504
+
505
+ const ast = parse(content, {
506
+ sourceType: 'module',
507
+ plugins: ['typescript', 'decorators-legacy'],
508
+ })
509
+
510
+ const steps: ParsedTemplateStep[] = []
511
+
512
+ traverse(ast, {
513
+ CallExpression(path: NodePath<t.CallExpression>) {
514
+ const node = path.node
515
+ const callee = node.callee
516
+ let keyword: 'When' | 'Then' | 'Given' | null = null
517
+
518
+ if (t.isIdentifier(callee) && (callee.name === 'When' || callee.name === 'Then' || callee.name === 'Given')) {
519
+ keyword = callee.name as 'When' | 'Then' | 'Given'
520
+ }
521
+
522
+ if (!keyword || node.arguments.length < 2) {
523
+ return
524
+ }
525
+
526
+ const patternArg = node.arguments[0]
527
+ const functionArg = node.arguments[1]
528
+
529
+ if (!t.isStringLiteral(patternArg) || !t.isFunction(functionArg)) {
530
+ return
531
+ }
532
+
533
+ const lineNumber = node.loc?.start?.line
534
+ if (lineNumber == null) {
535
+ return
536
+ }
537
+
538
+ const jsdoc = parseStepJSDoc(content, lineNumber - 1)
539
+ if (!jsdoc) {
540
+ return
541
+ }
542
+
543
+ const parameters: StepParameter[] = []
544
+ let order = 0
545
+
546
+ for (const parameter of functionArg.params) {
547
+ if (t.isIdentifier(parameter) && parameter.name === 'this') {
548
+ continue
549
+ }
550
+
551
+ if (t.isObjectPattern(parameter)) {
552
+ continue
553
+ }
554
+
555
+ if (!t.isIdentifier(parameter) || !parameter.typeAnnotation || !t.isTSTypeAnnotation(parameter.typeAnnotation)) {
556
+ continue
557
+ }
558
+
559
+ const annotation = parameter.typeAnnotation.typeAnnotation
560
+ let typeName: string | null = null
561
+
562
+ if (t.isTSTypeReference(annotation) && t.isIdentifier(annotation.typeName)) {
563
+ typeName = annotation.typeName.name
564
+ } else if (t.isTSStringKeyword(annotation)) {
565
+ typeName = 'string'
566
+ } else if (t.isTSNumberKeyword(annotation)) {
567
+ typeName = 'number'
568
+ } else if (t.isTSBooleanKeyword(annotation)) {
569
+ typeName = 'boolean'
570
+ }
571
+
572
+ if (!typeName) {
573
+ continue
574
+ }
575
+
576
+ try {
577
+ parameters.push({
578
+ name: parameter.name,
579
+ type: mapTypeToParameterType(typeName),
580
+ order: order++,
581
+ })
582
+ } catch {
583
+ return
584
+ }
585
+ }
586
+
587
+ steps.push({
588
+ jsdoc,
589
+ signature: patternArg.value,
590
+ functionDefinition: extractFunctionDefinition(node, content),
591
+ normalizedFunctionDefinition: '',
592
+ parameters,
593
+ keyword,
594
+ })
595
+ },
596
+ })
597
+
598
+ return { group, steps }
599
+ }
600
+
601
+ async function scanStepFiles(baseDir: string): Promise<string[]> {
602
+ const actionFiles = await glob('automation/steps/actions/**/*.step.ts', { cwd: baseDir })
603
+ const validationFiles = await glob('automation/steps/validations/**/*.step.ts', { cwd: baseDir })
604
+ return [...actionFiles, ...validationFiles]
605
+ }
606
+
607
+ function extractTestSuiteNameFromFilename(filePath: string): string {
608
+ const fileName = filePath.split(/[/\\]/).pop() ?? ''
609
+ return fileName.replace(/\.feature$/, '')
610
+ }
611
+
612
+ function extractFeatureLevelTags(parsedFeature: ParsedFeature): string[] {
613
+ return parsedFeature.tags.flatMap(splitTagLine)
614
+ }
615
+
616
+ function parseScenarioTitle(scenarioName: string, scenarioDescription?: string): { title: string; description: string } {
617
+ if (scenarioDescription) {
618
+ return {
619
+ title: scenarioDescription.trim(),
620
+ description: scenarioName.trim(),
621
+ }
622
+ }
623
+
624
+ return {
625
+ title: scenarioName.trim(),
626
+ description: '',
627
+ }
628
+ }
629
+
630
+ function signatureToRegex(signature: string): RegExp {
631
+ let pattern = signature.replace(/[.*+?^${}()|[\]\\]/g, match => {
632
+ if (match === '{' || match === '}') return match
633
+ return `\\${match}`
634
+ })
635
+
636
+ pattern = pattern.replace(/\{string\}/g, '"([^"]+)"')
637
+ pattern = pattern.replace(/\{int\}/g, '(\\d+)')
638
+ pattern = pattern.replace(/\{boolean\}/g, '(true|false)')
639
+ pattern = pattern.replace(/\{number\}/g, '(\\d+(?:\\.\\d+)?)')
640
+
641
+ return new RegExp(`^${pattern}$`, 'i')
642
+ }
643
+
644
+ function extractParametersFromGherkinStep(
645
+ gherkinText: string,
646
+ signature: string,
647
+ templateStepParameters: Array<{ name: string; order: number; type: StepParameterType }>,
648
+ ): ParameterMatch[] | null {
649
+ const match = gherkinText.match(signatureToRegex(signature))
650
+ if (!match) {
651
+ return null
652
+ }
653
+
654
+ return match.slice(1).flatMap((value, index) => {
655
+ const parameter = templateStepParameters[index]
656
+ if (!parameter || value == null) {
657
+ return []
658
+ }
659
+
660
+ return [
661
+ {
662
+ name: parameter.name,
663
+ value,
664
+ order: parameter.order,
665
+ type: parameter.type,
666
+ },
667
+ ]
668
+ })
669
+ }
670
+
671
+ function matchGherkinStepToTemplateStep(
672
+ gherkinStep: ParsedStep,
673
+ templateSteps: Array<{
674
+ signature: string
675
+ parameters: Array<{ name: string; order: number; type: StepParameterType }>
676
+ }>,
677
+ ): { signature: string; parameters: ParameterMatch[] } | null {
678
+ for (const templateStep of templateSteps) {
679
+ const parameters = extractParametersFromGherkinStep(gherkinStep.text, templateStep.signature, templateStep.parameters)
680
+ if (parameters) {
681
+ return {
682
+ signature: templateStep.signature,
683
+ parameters,
684
+ }
685
+ }
686
+ }
687
+
688
+ return null
689
+ }
690
+
691
+ function groupRecordsByKey<T>(records: T[], getKey: (record: T) => string | null | undefined): Map<string, T[]> {
692
+ const recordsByKey = new Map<string, T[]>()
693
+
694
+ for (const record of records) {
695
+ const key = getKey(record)
696
+ if (!key) {
697
+ continue
698
+ }
699
+
700
+ const existing = recordsByKey.get(key)
701
+ if (existing) {
702
+ existing.push(record)
703
+ continue
704
+ }
705
+
706
+ recordsByKey.set(key, [record])
707
+ }
708
+
709
+ return recordsByKey
710
+ }
711
+
712
+ function sameStringSet(left: string[], right: string[]): boolean {
713
+ const sortedLeft = [...left].sort()
714
+ const sortedRight = [...right].sort()
715
+
716
+ return JSON.stringify(sortedLeft) === JSON.stringify(sortedRight)
717
+ }
718
+
719
+ function sameStepParameters(
720
+ left: Array<{ name: string; order: number; type: StepParameterType }>,
721
+ right: Array<{ name: string; order: number; type: StepParameterType }>,
722
+ ): boolean {
723
+ if (left.length !== right.length) {
724
+ return false
725
+ }
726
+
727
+ return left.every((parameter, index) => {
728
+ const other = right[index]
729
+ return (
730
+ parameter.name === other?.name &&
731
+ parameter.order === other?.order &&
732
+ parameter.type === other?.type
733
+ )
734
+ })
735
+ }
736
+
737
+ function sameResolvedParameters(
738
+ left: Array<{ name: string; value: string; order: number; type: StepParameterType }>,
739
+ right: Array<{ name: string; value: string; order: number; type: StepParameterType }>,
740
+ ): boolean {
741
+ if (left.length !== right.length) {
742
+ return false
743
+ }
744
+
745
+ return left.every((parameter, index) => {
746
+ const other = right[index]
747
+ return (
748
+ parameter.name === other?.name &&
749
+ parameter.value === other?.value &&
750
+ parameter.order === other?.order &&
751
+ parameter.type === other?.type
752
+ )
753
+ })
754
+ }
755
+
756
+ async function buildFilesystemSnapshot(baseDir: string): Promise<FilesystemSnapshot> {
757
+ const featuresDir = getAutomationFeaturesDir()
758
+ const [environments, parsedFeatures, locatorFiles, locatorMap, stepFiles] = await Promise.all([
759
+ readEnvironmentsFromFile(),
760
+ scanFeatureFiles(featuresDir),
761
+ scanLocatorFiles(baseDir),
762
+ readLocatorMap(baseDir),
763
+ scanStepFiles(baseDir),
764
+ ])
765
+
766
+ const locatorRouteMap = new Map(locatorMap.map(entry => [entry.name, entry.path]))
767
+ const locatorGroups: LocatorGroupFromFs[] = []
768
+ const locatorFileData: LocatorFileData[] = []
769
+ const directModulePaths = new Set<string>()
770
+
771
+ for (const filePath of locatorFiles) {
772
+ const modulePath = extractModulePathFromAutomationFile(filePath, baseDir, 'locators')
773
+ const groupName = extractLocatorGroupName(filePath)
774
+ const locators = await readLocatorFile(filePath)
775
+
776
+ locatorGroups.push({
777
+ name: groupName,
778
+ route: locatorRouteMap.get(groupName) ?? `/${groupName}`,
779
+ modulePath,
780
+ })
781
+ locatorFileData.push({
782
+ groupName,
783
+ modulePath,
784
+ locators,
785
+ })
786
+ directModulePaths.add(modulePath)
787
+ }
788
+
789
+ for (const feature of parsedFeatures) {
790
+ directModulePaths.add(extractModulePathFromFilePath(feature.filePath, featuresDir))
791
+ }
792
+
793
+ const tagObjects = buildTagObjects(extractUniqueTags(parsedFeatures))
794
+
795
+ const templateStepGroups: StepGroupJSDoc[] = []
796
+ const templateSteps: TemplateStepFromFs[] = []
797
+
798
+ for (const file of stepFiles) {
799
+ try {
800
+ const filePath = join(baseDir, file)
801
+ const content = await fs.readFile(filePath, 'utf-8')
802
+ const parsed = parseStepFile(content)
803
+ if (!parsed) {
804
+ continue
805
+ }
806
+
807
+ await Promise.all(
808
+ parsed.steps.map(async step => {
809
+ step.normalizedFunctionDefinition = await normalizeFunctionDefinition(step.functionDefinition)
810
+ }),
811
+ )
812
+
813
+ templateStepGroups.push(parsed.group)
814
+ for (const step of parsed.steps) {
815
+ templateSteps.push({
816
+ step,
817
+ groupName: parsed.group.name,
818
+ groupType: parsed.group.type,
819
+ })
820
+ }
821
+ } catch (error) {
822
+ console.error(`Unable to parse step file ${file} for sync counts:`, error)
823
+ }
824
+ }
825
+
826
+ const testSuites: TestSuiteFromFs[] = parsedFeatures.map(feature => ({
827
+ name: extractTestSuiteNameFromFilename(feature.filePath),
828
+ description: feature.featureDescription ?? null,
829
+ modulePath: extractModulePathFromFilePath(feature.filePath, featuresDir),
830
+ tags: extractFeatureLevelTags(feature),
831
+ }))
832
+
833
+ const testCases: TestCaseFromFs[] = []
834
+ for (const feature of parsedFeatures) {
835
+ const modulePath = extractModulePathFromFilePath(feature.filePath, featuresDir)
836
+ const testSuiteName = extractTestSuiteNameFromFilename(feature.filePath)
837
+
838
+ for (const scenario of feature.scenarios) {
839
+ const flattenedTags = scenario.tags.flatMap(splitTagLine)
840
+ const identifierTag = flattenedTags.find(tag => tag.replace(/^@/, '').startsWith('tc_'))
841
+ if (!identifierTag) {
842
+ continue
843
+ }
844
+
845
+ const { title, description } = parseScenarioTitle(scenario.name, scenario.description)
846
+ testCases.push({
847
+ identifierTag: normalizeTagExpression(identifierTag),
848
+ title,
849
+ description,
850
+ testSuiteName,
851
+ modulePath,
852
+ filterTags: flattenedTags.filter(tag => normalizeTagExpression(tag) !== normalizeTagExpression(identifierTag)),
853
+ steps: scenario.steps,
854
+ })
855
+ }
856
+ }
857
+
858
+ return {
859
+ environments,
860
+ modulePaths: buildModuleTreePaths(directModulePaths),
861
+ locatorGroups,
862
+ locatorFiles: locatorFileData,
863
+ tagObjects,
864
+ templateStepGroups,
865
+ templateSteps,
866
+ testSuites,
867
+ testCases,
868
+ }
869
+ }
870
+
871
+ export function countModuleMismatches(
872
+ filesystemPaths: Set<string>,
873
+ dbModules: Array<{ id: string; name: string; path: string; parentId: string | null }>,
874
+ ): number {
875
+ const dbPaths = new Set<string>()
876
+ let rootExists = false
877
+
878
+ for (const dbModule of dbModules) {
879
+ if (dbModule.name === 'root' && dbModule.parentId === null) {
880
+ rootExists = true
881
+ continue
882
+ }
883
+
884
+ dbPaths.add(dbModule.path)
885
+ }
886
+
887
+ let count = 0
888
+ for (const modulePath of filesystemPaths) {
889
+ if (modulePath === '/') {
890
+ if (!rootExists) count++
891
+ continue
892
+ }
893
+
894
+ if (!dbPaths.has(modulePath)) {
895
+ count++
896
+ }
897
+ }
898
+
899
+ return count
900
+ }
901
+
902
+ function countEnvironmentMismatches(
903
+ filesystemEnvironments: EnvironmentData[],
904
+ dbEnvironments: Array<{ name: string }>,
905
+ ): number {
906
+ const fsByNormalizedName = new Map(
907
+ filesystemEnvironments.map(environment => [getEnvironmentIdentityKey(environment.name), environment]),
908
+ )
909
+ const dbByNormalizedName = new Map<string, { name: string }>()
910
+
911
+ for (const environment of dbEnvironments) {
912
+ const normalizedName = getEnvironmentIdentityKey(environment.name)
913
+ if (!dbByNormalizedName.has(normalizedName)) {
914
+ dbByNormalizedName.set(normalizedName, environment)
915
+ }
916
+ }
917
+
918
+ let count = 0
919
+ for (const environment of filesystemEnvironments) {
920
+ const existing = dbByNormalizedName.get(getEnvironmentIdentityKey(environment.name))
921
+ if (!existing) {
922
+ count++
923
+ }
924
+ }
925
+
926
+ for (const environment of dbEnvironments) {
927
+ if (!fsByNormalizedName.has(getEnvironmentIdentityKey(environment.name))) {
928
+ count++
929
+ }
930
+ }
931
+
932
+ return count
933
+ }
934
+
935
+ export function countTagMismatches(
936
+ filesystemTags: Array<{ name: string; tagExpression: string; type: TagType }>,
937
+ dbTags: Array<{ name: string; type: TagType }>,
938
+ ): number {
939
+ const dbByName = groupRecordsByKey(dbTags, tag => tag.name)
940
+
941
+ let count = 0
942
+ for (const tag of filesystemTags) {
943
+ const hasMatch = (dbByName.get(tag.name) ?? []).some(existing => existing.type === tag.type)
944
+ if (!hasMatch) {
945
+ count++
946
+ }
947
+ }
948
+
949
+ return count
950
+ }
951
+
952
+ export function countTemplateStepGroupMismatches(
953
+ filesystemGroups: StepGroupJSDoc[],
954
+ dbGroups: Array<{ name: string; description: string | null; type: TemplateStepGroupType }>,
955
+ ): number {
956
+ const dbByName = groupRecordsByKey(dbGroups, group => group.name)
957
+ let count = 0
958
+
959
+ for (const group of filesystemGroups) {
960
+ const hasMatch = (dbByName.get(group.name) ?? []).some(existing => {
961
+ return (existing.description ?? null) === (group.description ?? null) && existing.type === group.type
962
+ })
963
+
964
+ if (!hasMatch) {
965
+ count++
966
+ }
967
+ }
968
+
969
+ return count
970
+ }
971
+
972
+ export function countTemplateStepMismatches(
973
+ filesystemSteps: TemplateStepFromFs[],
974
+ dbSteps: Array<{
975
+ signature: string
976
+ name: string
977
+ description: string | null
978
+ functionDefinition: string | null
979
+ icon: TemplateStepIcon
980
+ type: TemplateStepType
981
+ templateStepGroup: { name: string }
982
+ parameters: Array<{ name: string; order: number; type: StepParameterType }>
983
+ }>,
984
+ ): number {
985
+ const dbBySignature = groupRecordsByKey(dbSteps, step => step.signature)
986
+ let count = 0
987
+
988
+ for (const item of filesystemSteps) {
989
+ const expectedType =
990
+ item.groupType === TemplateStepGroupType.ACTION ? TemplateStepType.ACTION : TemplateStepType.ASSERTION
991
+
992
+ const hasMatch = (dbBySignature.get(item.step.signature) ?? []).some(existing => {
993
+ return (
994
+ existing.name === item.step.jsdoc.name &&
995
+ (existing.description ?? '') === (item.step.jsdoc.description ?? '') &&
996
+ (existing.functionDefinition ?? '') === item.step.normalizedFunctionDefinition &&
997
+ existing.icon === item.step.jsdoc.icon &&
998
+ existing.type === expectedType &&
999
+ existing.templateStepGroup.name === item.groupName &&
1000
+ sameStepParameters(existing.parameters, item.step.parameters)
1001
+ )
1002
+ })
1003
+
1004
+ if (!hasMatch) {
1005
+ count++
1006
+ }
1007
+ }
1008
+
1009
+ return count
1010
+ }
1011
+
1012
+ function countLocatorGroupMismatches(
1013
+ filesystemGroups: LocatorGroupFromFs[],
1014
+ dbGroups: Array<{ name: string; route: string; moduleId: string }>,
1015
+ modulePathMap: Map<string, string>,
1016
+ ): number {
1017
+ const fsByName = new Map(filesystemGroups.map(group => [group.name, group]))
1018
+ const dbByName = new Map(dbGroups.map(group => [group.name, group]))
1019
+ let count = 0
1020
+
1021
+ for (const group of filesystemGroups) {
1022
+ const existing = dbByName.get(group.name)
1023
+ if (!existing) {
1024
+ count++
1025
+ continue
1026
+ }
1027
+
1028
+ const existingModulePath = modulePathMap.get(existing.moduleId) ?? '/'
1029
+ if (existing.route !== group.route || existingModulePath !== group.modulePath) {
1030
+ count++
1031
+ }
1032
+ }
1033
+
1034
+ for (const group of dbGroups) {
1035
+ if (!fsByName.has(group.name)) {
1036
+ count++
1037
+ }
1038
+ }
1039
+
1040
+ return count
1041
+ }
1042
+
1043
+ function countLocatorMismatches(
1044
+ filesystemLocatorFiles: LocatorFileData[],
1045
+ dbGroups: Array<{
1046
+ name: string
1047
+ locators: Array<{ name: string; value: string }>
1048
+ }>,
1049
+ ): number {
1050
+ const dbGroupsByName = new Map(dbGroups.map(group => [group.name, group]))
1051
+ let count = 0
1052
+
1053
+ for (const file of filesystemLocatorFiles) {
1054
+ const dbGroup = dbGroupsByName.get(file.groupName)
1055
+ const fileLocators = Object.entries(file.locators)
1056
+
1057
+ if (!dbGroup) {
1058
+ count += fileLocators.length
1059
+ continue
1060
+ }
1061
+
1062
+ const dbLocators = new Map(dbGroup.locators.map(locator => [locator.name, locator.value]))
1063
+ const fileLocatorNames = new Set<string>()
1064
+
1065
+ for (const [name, value] of fileLocators) {
1066
+ fileLocatorNames.add(name)
1067
+ if (!dbLocators.has(name) || dbLocators.get(name) !== value) {
1068
+ count++
1069
+ }
1070
+ }
1071
+
1072
+ for (const locator of dbGroup.locators) {
1073
+ if (!fileLocatorNames.has(locator.name)) {
1074
+ count++
1075
+ }
1076
+ }
1077
+ }
1078
+
1079
+ return count
1080
+ }
1081
+
1082
+ export function countTestSuiteMismatches(
1083
+ filesystemSuites: TestSuiteFromFs[],
1084
+ dbSuites: Array<{
1085
+ name: string
1086
+ description: string | null
1087
+ moduleId: string
1088
+ tags: Array<{ tagExpression: string }>
1089
+ }>,
1090
+ modulePathMap: Map<string, string>,
1091
+ ): number {
1092
+ const dbByKey = groupRecordsByKey(dbSuites, suite =>
1093
+ getTestSuiteSyncIdentity(suite.name, modulePathMap.get(suite.moduleId) ?? '/'),
1094
+ )
1095
+ let count = 0
1096
+
1097
+ for (const suite of filesystemSuites) {
1098
+ const suiteKey = getTestSuiteSyncIdentity(suite.name, suite.modulePath)
1099
+ const fsTagExpressions = suite.tags.map(normalizeTagExpression)
1100
+ const hasMatch = (dbByKey.get(suiteKey) ?? []).some(existing => {
1101
+ const dbTagExpressions = existing.tags.map(tag => normalizeTagExpression(tag.tagExpression))
1102
+ return (existing.description ?? null) === (suite.description ?? null) && sameStringSet(dbTagExpressions, fsTagExpressions)
1103
+ })
1104
+
1105
+ if (!hasMatch) {
1106
+ count++
1107
+ }
1108
+ }
1109
+
1110
+ return count
1111
+ }
1112
+
1113
+ function hasProjectedTestCaseStepMismatch(
1114
+ stepsFromFs: ParsedStep[],
1115
+ dbSteps: Array<{
1116
+ order: number
1117
+ gherkinStep: string
1118
+ label: string
1119
+ icon: TemplateStepIcon
1120
+ TemplateStep: { signature: string } | null
1121
+ parameters: Array<{ name: string; value: string; order: number; type: StepParameterType }>
1122
+ }>,
1123
+ dbTemplateSteps: Array<{
1124
+ signature: string
1125
+ parameters: Array<{ name: string; order: number; type: StepParameterType }>
1126
+ }>,
1127
+ ): boolean {
1128
+ const projectedDbSteps = normalizeProjectedDbTestCaseSteps(dbSteps)
1129
+ const dbStepsByOrder = new Map(projectedDbSteps.map(step => [step.order, step]))
1130
+
1131
+ for (const step of stepsFromFs) {
1132
+ const existing = dbStepsByOrder.get(step.order)
1133
+ const matchedTemplateStep = matchGherkinStepToTemplateStep(step, dbTemplateSteps)
1134
+
1135
+ if (!existing || !matchedTemplateStep) {
1136
+ return true
1137
+ }
1138
+
1139
+ const expectedIcon = determineProjectedStepIcon(step.keyword)
1140
+ const expectedGherkinStep = `${step.keyword} ${step.text}`
1141
+
1142
+ if (
1143
+ existing.gherkinStep !== expectedGherkinStep ||
1144
+ existing.label !== step.text ||
1145
+ existing.icon !== expectedIcon ||
1146
+ existing.templateStepSignature !== matchedTemplateStep.signature ||
1147
+ !sameResolvedParameters(existing.parameters, matchedTemplateStep.parameters)
1148
+ ) {
1149
+ return true
1150
+ }
1151
+ }
1152
+
1153
+ const fsOrders = new Set(stepsFromFs.map(step => step.order))
1154
+ return projectedDbSteps.some(step => !fsOrders.has(step.order))
1155
+ }
1156
+
1157
+ export function countTestCaseMismatches(
1158
+ filesystemTestCases: TestCaseFromFs[],
1159
+ dbTestCases: Array<{
1160
+ title: string
1161
+ description: string
1162
+ tags: Array<{ tagExpression: string; type: TagType }>
1163
+ TestSuite: Array<{ name: string; moduleId: string }>
1164
+ steps: Array<{
1165
+ order: number
1166
+ gherkinStep: string
1167
+ label: string
1168
+ icon: TemplateStepIcon
1169
+ TemplateStep: { signature: string } | null
1170
+ parameters: Array<{ name: string; value: string; order: number; type: StepParameterType }>
1171
+ }>
1172
+ }>,
1173
+ modulePathMap: Map<string, string>,
1174
+ dbTemplateSteps: Array<{
1175
+ signature: string
1176
+ parameters: Array<{ name: string; order: number; type: StepParameterType }>
1177
+ }>,
1178
+ ): number {
1179
+ const dbByIdentifier = new Map<string, Array<(typeof dbTestCases)[number]>>()
1180
+ let count = 0
1181
+
1182
+ for (const testCase of dbTestCases) {
1183
+ const identifierTag = testCase.tags.find(tag => tag.type === TagType.IDENTIFIER)
1184
+ if (!identifierTag) {
1185
+ continue
1186
+ }
1187
+
1188
+ const normalizedIdentifier = normalizeTagExpression(identifierTag.tagExpression)
1189
+ const existing = dbByIdentifier.get(normalizedIdentifier)
1190
+ if (existing) {
1191
+ existing.push(testCase)
1192
+ continue
1193
+ }
1194
+
1195
+ dbByIdentifier.set(normalizedIdentifier, [testCase])
1196
+ }
1197
+
1198
+ for (const testCase of filesystemTestCases) {
1199
+ const expectedSuiteIdentity = getTestSuiteSyncIdentity(testCase.testSuiteName, testCase.modulePath)
1200
+ const normalizedFilterTags = testCase.filterTags.map(normalizeTagExpression)
1201
+ const hasMatch = (dbByIdentifier.get(testCase.identifierTag) ?? []).some(existing => {
1202
+ const existingFilterTags = existing.tags
1203
+ .filter(tag => tag.type === TagType.FILTER)
1204
+ .map(tag => normalizeTagExpression(tag.tagExpression))
1205
+ const isLinkedToExpectedSuite = existing.TestSuite.some(
1206
+ suite => getTestSuiteSyncIdentity(suite.name, modulePathMap.get(suite.moduleId) ?? '/') === expectedSuiteIdentity,
1207
+ )
1208
+
1209
+ return (
1210
+ existing.title === testCase.title &&
1211
+ existing.description === testCase.description &&
1212
+ isLinkedToExpectedSuite &&
1213
+ sameStringSet(existingFilterTags, normalizedFilterTags) &&
1214
+ !hasProjectedTestCaseStepMismatch(testCase.steps, existing.steps, dbTemplateSteps)
1215
+ )
1216
+ })
1217
+
1218
+ if (!hasMatch) {
1219
+ count++
1220
+ }
1221
+ }
1222
+
1223
+ return count
1224
+ }
1225
+
1226
+ function emptyCounts(): SyncPendingCounts {
1227
+ const counts = Object.fromEntries(syncScriptDefinitions.map(definition => [definition.id, 0])) as Record<SyncScriptId, number>
1228
+ return {
1229
+ ...counts,
1230
+ [SYNC_ALL_REQUEST_ID]: 0,
1231
+ }
1232
+ }
1233
+
1234
+ export async function getSyncPendingCounts(): Promise<SyncPendingCounts> {
1235
+ try {
1236
+ await ensureAutomationWorkspaceReady()
1237
+
1238
+ const baseDir = process.cwd()
1239
+ const filesystem = await buildFilesystemSnapshot(baseDir)
1240
+ const [
1241
+ dbModules,
1242
+ dbEnvironments,
1243
+ dbTags,
1244
+ dbTemplateStepGroups,
1245
+ dbTemplateSteps,
1246
+ dbLocatorGroups,
1247
+ dbTestSuites,
1248
+ dbTestCases,
1249
+ ] = await Promise.all([
1250
+ getAllModulesWithPaths(),
1251
+ prisma.environment.findMany({ select: { name: true } }),
1252
+ prisma.tag.findMany({ select: { name: true, type: true } }),
1253
+ prisma.templateStepGroup.findMany({ select: { name: true, description: true, type: true } }),
1254
+ prisma.templateStep.findMany({
1255
+ select: {
1256
+ signature: true,
1257
+ name: true,
1258
+ description: true,
1259
+ functionDefinition: true,
1260
+ icon: true,
1261
+ type: true,
1262
+ parameters: {
1263
+ select: { name: true, order: true, type: true },
1264
+ orderBy: { order: 'asc' },
1265
+ },
1266
+ templateStepGroup: {
1267
+ select: { name: true },
1268
+ },
1269
+ },
1270
+ }),
1271
+ prisma.locatorGroup.findMany({
1272
+ select: {
1273
+ name: true,
1274
+ route: true,
1275
+ moduleId: true,
1276
+ locators: {
1277
+ select: { name: true, value: true },
1278
+ },
1279
+ },
1280
+ }),
1281
+ prisma.testSuite.findMany({
1282
+ select: {
1283
+ name: true,
1284
+ description: true,
1285
+ moduleId: true,
1286
+ tags: {
1287
+ select: { tagExpression: true },
1288
+ },
1289
+ },
1290
+ }),
1291
+ prisma.testCase.findMany({
1292
+ select: {
1293
+ title: true,
1294
+ description: true,
1295
+ tags: {
1296
+ select: { tagExpression: true, type: true },
1297
+ },
1298
+ TestSuite: {
1299
+ select: { name: true, moduleId: true },
1300
+ },
1301
+ steps: {
1302
+ orderBy: { order: 'asc' },
1303
+ select: {
1304
+ order: true,
1305
+ gherkinStep: true,
1306
+ label: true,
1307
+ icon: true,
1308
+ TemplateStep: {
1309
+ select: { signature: true },
1310
+ },
1311
+ parameters: {
1312
+ select: { name: true, value: true, order: true, type: true },
1313
+ orderBy: { order: 'asc' },
1314
+ },
1315
+ },
1316
+ },
1317
+ },
1318
+ }),
1319
+ ])
1320
+
1321
+ const normalizedDbTemplateSteps = await Promise.all(
1322
+ dbTemplateSteps.map(async step => ({
1323
+ ...step,
1324
+ functionDefinition: await normalizeFunctionDefinition(step.functionDefinition),
1325
+ })),
1326
+ )
1327
+
1328
+ const modulePathMap = new Map(dbModules.map(module => [module.id, module.name === 'root' && module.parentId === null ? '/' : module.path]))
1329
+
1330
+ const counts: Record<SyncScriptId, number> = {
1331
+ 'sync-modules': countModuleMismatches(filesystem.modulePaths, dbModules),
1332
+ 'sync-environments': countEnvironmentMismatches(filesystem.environments, dbEnvironments),
1333
+ 'sync-tags': countTagMismatches(filesystem.tagObjects, dbTags),
1334
+ 'sync-template-step-groups': countTemplateStepGroupMismatches(filesystem.templateStepGroups, dbTemplateStepGroups),
1335
+ 'sync-template-steps': countTemplateStepMismatches(filesystem.templateSteps, normalizedDbTemplateSteps),
1336
+ 'sync-locator-groups': countLocatorGroupMismatches(filesystem.locatorGroups, dbLocatorGroups, modulePathMap),
1337
+ 'sync-locators': countLocatorMismatches(filesystem.locatorFiles, dbLocatorGroups),
1338
+ 'sync-test-suites': countTestSuiteMismatches(filesystem.testSuites, dbTestSuites, modulePathMap),
1339
+ 'sync-test-cases': countTestCaseMismatches(
1340
+ filesystem.testCases,
1341
+ dbTestCases,
1342
+ modulePathMap,
1343
+ normalizedDbTemplateSteps.map(step => ({
1344
+ signature: step.signature,
1345
+ parameters: step.parameters,
1346
+ })),
1347
+ ),
1348
+ }
1349
+
1350
+ return {
1351
+ ...counts,
1352
+ [SYNC_ALL_REQUEST_ID]: Object.values(counts).reduce((sum, count) => sum + count, 0),
1353
+ }
1354
+ } catch (error) {
1355
+ console.error('Unable to compute sync pending counts:', error)
1356
+ return emptyCounts()
1357
+ }
1358
+ }