create-appraisejs 0.2.0-alpha.6 → 0.3.0-alpha.0

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 (485) hide show
  1. package/README.md +12 -1
  2. package/package.json +1 -1
  3. package/templates/default/.appraise-template-meta.json +2 -2
  4. package/templates/default/.env.example +2 -2
  5. package/templates/default/README.md +57 -53
  6. package/templates/default/automation/steps/actions/click.step.ts +58 -58
  7. package/templates/default/automation/steps/actions/hover.step.ts +27 -27
  8. package/templates/default/automation/steps/actions/navigation.step.ts +70 -70
  9. package/templates/default/automation/steps/actions/random_data.step.ts +142 -142
  10. package/templates/default/automation/steps/actions/store.step.ts +86 -86
  11. package/templates/default/automation/steps/actions/wait.step.ts +110 -90
  12. package/templates/default/automation/steps/validations/active_state_assertion.step.ts +30 -30
  13. package/templates/default/automation/steps/validations/navigation_assertion.step.ts +22 -22
  14. package/templates/default/automation/steps/validations/text_assertion.step.ts +107 -107
  15. package/templates/default/automation/steps/validations/visibility_assertion.step.ts +26 -26
  16. package/templates/default/components.json +24 -24
  17. package/templates/default/cucumber.mjs +16 -16
  18. package/templates/default/eslint.config.mjs +20 -16
  19. package/templates/default/next-env.d.ts +6 -6
  20. package/templates/default/next.config.ts +20 -11
  21. package/templates/default/package-lock.json +1775 -74
  22. package/templates/default/package.json +8 -1
  23. package/templates/default/packages/cucumber-runtime/package.json +13 -13
  24. package/templates/default/packages/cucumber-runtime/src/cache.util.ts +93 -93
  25. package/templates/default/packages/cucumber-runtime/src/cli.ts +68 -68
  26. package/templates/default/packages/cucumber-runtime/src/environment.util.ts +21 -21
  27. package/templates/default/packages/cucumber-runtime/src/executor.ts +32 -32
  28. package/templates/default/packages/cucumber-runtime/src/index.ts +17 -17
  29. package/templates/default/packages/cucumber-runtime/src/locator.util.ts +234 -234
  30. package/templates/default/packages/cucumber-runtime/src/parameter-types.ts +7 -7
  31. package/templates/default/packages/cucumber-runtime/src/random-data.util.ts +35 -35
  32. package/templates/default/packages/cucumber-runtime/src/types.ts +13 -13
  33. package/templates/default/packages/cucumber-runtime/src/world.ts +44 -44
  34. package/templates/default/packages/cucumber-runtime/tsconfig.json +11 -11
  35. package/templates/default/postcss.config.mjs +8 -8
  36. package/templates/default/prisma/dev.db +0 -0
  37. package/templates/default/prisma/migrations/20251104113456_add_type_for_template_step_groups/migration.sql +16 -16
  38. package/templates/default/prisma/migrations/20251104170946_add_tags_to_test_suite_and_test_case/migration.sql +27 -27
  39. package/templates/default/prisma/migrations/20251112190024_add_cascade_delete_to_test_run_test_case/migration.sql +17 -17
  40. package/templates/default/prisma/migrations/20251113181100_add_test_run_log/migration.sql +12 -12
  41. package/templates/default/prisma/migrations/20251119191838_add_tag_type/migration.sql +28 -28
  42. package/templates/default/prisma/migrations/20251121164059_add_conflict_resolution/migration.sql +12 -12
  43. package/templates/default/prisma/migrations/20251223183400_add_report_model_to_db_schema/migration.sql +10 -10
  44. package/templates/default/prisma/migrations/20251223183637_add_report_test_case_entity_for_storing_test_results_for_individual_test_cases/migration.sql +10 -10
  45. package/templates/default/prisma/migrations/20251224083549_add_comprehensive_report_storage/migration.sql +108 -108
  46. package/templates/default/prisma/migrations/20251229194422_migrate_duration_to_string/migration.sql +55 -55
  47. package/templates/default/prisma/migrations/20251230124637_add_unique_constraint_to_test_run_name/migration.sql +27 -27
  48. package/templates/default/prisma/migrations/20260115094436_add_dashboard_metrics/migration.sql +59 -59
  49. package/templates/default/prisma/migrations/20260127172022_add_cascade_delete_to_step_parameters/migration.sql +34 -34
  50. package/templates/default/prisma/migrations/20260313093000_add_report_step_screenshot_path/migration.sql +1 -1
  51. package/templates/default/scripts/build-step-registry.ts +33 -0
  52. package/templates/default/scripts/install-playwright.ts +17 -8
  53. package/templates/default/scripts/install-template-step.ts +128 -0
  54. package/templates/default/scripts/lib/filename-utils.test.ts +24 -0
  55. package/templates/default/scripts/lib/filename-utils.ts +24 -0
  56. package/templates/default/scripts/lib/jsdoc-parser.test.ts +63 -0
  57. package/templates/default/scripts/lib/jsdoc-parser.ts +88 -0
  58. package/templates/default/scripts/lib/step-file-parser.test.ts +71 -0
  59. package/templates/default/scripts/lib/step-file-parser.ts +315 -0
  60. package/templates/default/scripts/lib/step-matcher.test.ts +86 -0
  61. package/templates/default/scripts/lib/step-matcher.ts +120 -0
  62. package/templates/default/scripts/lib/sync-script-runner.ts +23 -0
  63. package/templates/default/scripts/lib/sync-summary.ts +29 -0
  64. package/templates/default/scripts/lib/tag-parsing.test.ts +20 -0
  65. package/templates/default/scripts/lib/tag-parsing.ts +10 -0
  66. package/templates/default/scripts/lib/template-step-installer.test.ts +225 -0
  67. package/templates/default/scripts/lib/template-step-installer.ts +404 -0
  68. package/templates/default/scripts/lib/template-step-registry.test.ts +118 -0
  69. package/templates/default/scripts/lib/template-step-registry.ts +137 -0
  70. package/templates/default/scripts/protect-seeded-files.ts +51 -38
  71. package/templates/default/scripts/regenerate-features.ts +98 -94
  72. package/templates/default/scripts/run-vitest.ts +59 -0
  73. package/templates/default/scripts/setup-env.ts +26 -19
  74. package/templates/default/scripts/sync-all.ts +44 -54
  75. package/templates/default/scripts/sync-appraise-base-template.ts +44 -16
  76. package/templates/default/scripts/sync-environments.ts +22 -66
  77. package/templates/default/scripts/sync-locator-groups.ts +358 -410
  78. package/templates/default/scripts/sync-locators.ts +348 -398
  79. package/templates/default/scripts/sync-modules.ts +302 -341
  80. package/templates/default/scripts/sync-tags.ts +29 -65
  81. package/templates/default/scripts/sync-template-step-groups.ts +24 -182
  82. package/templates/default/scripts/sync-template-steps.ts +36 -493
  83. package/templates/default/scripts/sync-test-cases.ts +296 -539
  84. package/templates/default/scripts/sync-test-suites.ts +32 -79
  85. package/templates/default/src/actions/dashboard/dashboard-actions.ts +70 -241
  86. package/templates/default/src/actions/environments/environment-actions.ts +102 -188
  87. package/templates/default/src/actions/locator/locator-actions.ts +77 -490
  88. package/templates/default/src/actions/locator-groups/locator-group-actions.ts +34 -212
  89. package/templates/default/src/actions/locator-picker/locator-picker-actions.test.ts +81 -0
  90. package/templates/default/src/actions/locator-picker/locator-picker-actions.ts +20 -161
  91. package/templates/default/src/actions/modules/module-actions.ts +99 -135
  92. package/templates/default/src/actions/reports/report-actions.ts +28 -565
  93. package/templates/default/src/actions/settings/sync-actions.test.ts +58 -0
  94. package/templates/default/src/actions/tags/tag-actions.ts +99 -107
  95. package/templates/default/src/actions/template-step/template-step-actions.ts +33 -194
  96. package/templates/default/src/actions/template-step-group/template-step-group-actions.ts +35 -92
  97. package/templates/default/src/actions/template-test-case/template-test-case-actions.ts +98 -238
  98. package/templates/default/src/actions/test-case/test-case-actions.ts +108 -356
  99. package/templates/default/src/actions/test-run/test-run-actions.ts +74 -1081
  100. package/templates/default/src/actions/test-suite/test-suite-actions.ts +35 -202
  101. package/templates/default/src/app/(base)/environments/create/page.tsx +28 -28
  102. package/templates/default/src/app/(base)/environments/environment-form.test.tsx +92 -0
  103. package/templates/default/src/app/(base)/environments/environment-form.tsx +228 -219
  104. package/templates/default/src/app/(base)/environments/environment-helpers.ts +58 -0
  105. package/templates/default/src/app/(base)/environments/environment-table-columns.tsx +96 -96
  106. package/templates/default/src/app/(base)/environments/environment-table.tsx +25 -24
  107. package/templates/default/src/app/(base)/environments/modify/[id]/page.tsx +49 -46
  108. package/templates/default/src/app/(base)/environments/page.tsx +59 -59
  109. package/templates/default/src/app/(base)/layout.tsx +10 -10
  110. package/templates/default/src/app/(base)/locator-groups/create/page.tsx +44 -44
  111. package/templates/default/src/app/(base)/locator-groups/locator-group-form.tsx +215 -215
  112. package/templates/default/src/app/(base)/locator-groups/locator-group-table-columns.tsx +77 -77
  113. package/templates/default/src/app/(base)/locator-groups/locator-group-table.tsx +28 -28
  114. package/templates/default/src/app/(base)/locator-groups/modify/[id]/page.tsx +46 -46
  115. package/templates/default/src/app/(base)/locators/create/create-locator-workspace-helpers.test.ts +71 -0
  116. package/templates/default/src/app/(base)/locators/create/create-locator-workspace-helpers.ts +333 -0
  117. package/templates/default/src/app/(base)/locators/create/create-locator-workspace.test.tsx +125 -0
  118. package/templates/default/src/app/(base)/locators/create/create-locator-workspace.tsx +56 -274
  119. package/templates/default/src/app/(base)/locators/create/page.tsx +8 -4
  120. package/templates/default/src/app/(base)/locators/create/use-locator-workspace.ts +183 -0
  121. package/templates/default/src/app/(base)/locators/locator-helpers.test.ts +28 -0
  122. package/templates/default/src/app/(base)/locators/locator-helpers.ts +59 -0
  123. package/templates/default/src/app/(base)/locators/locator-table-columns.tsx +74 -73
  124. package/templates/default/src/app/(base)/locators/locator-table.tsx +30 -28
  125. package/templates/default/src/app/(base)/locators/modify/[id]/page.tsx +20 -8
  126. package/templates/default/src/app/(base)/locators/page.tsx +3 -6
  127. package/templates/default/src/app/(base)/locators/sync-locators-button.tsx +67 -66
  128. package/templates/default/src/app/(base)/modules/create/page.tsx +33 -34
  129. package/templates/default/src/app/(base)/modules/modify/[id]/page.tsx +43 -46
  130. package/templates/default/src/app/(base)/modules/module-form.test.tsx +84 -0
  131. package/templates/default/src/app/(base)/modules/module-form.tsx +159 -126
  132. package/templates/default/src/app/(base)/modules/module-helpers.ts +64 -0
  133. package/templates/default/src/app/(base)/modules/module-table-columns.tsx +81 -85
  134. package/templates/default/src/app/(base)/modules/module-table.tsx +25 -24
  135. package/templates/default/src/app/(base)/modules/page.tsx +59 -59
  136. package/templates/default/src/app/(base)/reports/[id]/page.tsx +20 -260
  137. package/templates/default/src/app/(base)/reports/duration-chart.tsx +33 -33
  138. package/templates/default/src/app/(base)/reports/feature-chart.tsx +79 -78
  139. package/templates/default/src/app/(base)/reports/overview-chart.tsx +49 -49
  140. package/templates/default/src/app/(base)/reports/page.tsx +98 -98
  141. package/templates/default/src/app/(base)/reports/report-detail-helpers.test.ts +109 -0
  142. package/templates/default/src/app/(base)/reports/report-detail-helpers.ts +247 -0
  143. package/templates/default/src/app/(base)/reports/report-metric-card.tsx +78 -78
  144. package/templates/default/src/app/(base)/reports/report-table-columns.tsx +189 -189
  145. package/templates/default/src/app/(base)/reports/report-table.tsx +72 -72
  146. package/templates/default/src/app/(base)/reports/test-cases/page.tsx +40 -40
  147. package/templates/default/src/app/(base)/reports/test-cases/test-cases-metric-table-columns.tsx +115 -115
  148. package/templates/default/src/app/(base)/reports/test-cases/test-cases-metric-table.tsx +27 -27
  149. package/templates/default/src/app/(base)/reports/test-suites/page.tsx +42 -42
  150. package/templates/default/src/app/(base)/reports/test-suites/test-suites-metric-table-columns.tsx +79 -79
  151. package/templates/default/src/app/(base)/reports/test-suites/test-suites-metric-table.tsx +27 -27
  152. package/templates/default/src/app/(base)/reports/view-logs-button.tsx +58 -58
  153. package/templates/default/src/app/(base)/settings/settings-sync-panel-helpers.test.tsx +40 -0
  154. package/templates/default/src/app/(base)/settings/settings-sync-panel-helpers.tsx +110 -0
  155. package/templates/default/src/app/(base)/settings/settings-sync-panel.test.tsx +127 -0
  156. package/templates/default/src/app/(base)/settings/settings-sync-panel.tsx +19 -134
  157. package/templates/default/src/app/(base)/settings/use-settings-sync.ts +66 -0
  158. package/templates/default/src/app/(base)/tags/create/page.tsx +39 -39
  159. package/templates/default/src/app/(base)/tags/modify/[id]/page.tsx +50 -50
  160. package/templates/default/src/app/(base)/tags/page.tsx +58 -58
  161. package/templates/default/src/app/(base)/tags/tag-form-helpers.ts +13 -0
  162. package/templates/default/src/app/(base)/tags/tag-form.test.tsx +83 -0
  163. package/templates/default/src/app/(base)/tags/tag-form.tsx +143 -147
  164. package/templates/default/src/app/(base)/tags/tag-table-columns.tsx +63 -63
  165. package/templates/default/src/app/(base)/tags/tag-table.tsx +29 -29
  166. package/templates/default/src/app/(base)/template-step-groups/create/page.tsx +28 -28
  167. package/templates/default/src/app/(base)/template-step-groups/modify/[id]/page.tsx +43 -45
  168. package/templates/default/src/app/(base)/template-step-groups/page.tsx +60 -60
  169. package/templates/default/src/app/(base)/template-step-groups/template-step-group-form.test.tsx +82 -0
  170. package/templates/default/src/app/(base)/template-step-groups/template-step-group-form.tsx +181 -167
  171. package/templates/default/src/app/(base)/template-step-groups/template-step-group-helpers.ts +54 -0
  172. package/templates/default/src/app/(base)/template-step-groups/template-step-group-table-columns.tsx +89 -89
  173. package/templates/default/src/app/(base)/template-step-groups/template-step-group-table.tsx +34 -32
  174. package/templates/default/src/app/(base)/template-steps/create/page.tsx +40 -37
  175. package/templates/default/src/app/(base)/template-steps/modify/[id]/page.tsx +54 -49
  176. package/templates/default/src/app/(base)/template-steps/page.tsx +59 -58
  177. package/templates/default/src/app/(base)/template-steps/paramChip.tsx +233 -213
  178. package/templates/default/src/app/(base)/template-steps/template-step-form.test.tsx +132 -0
  179. package/templates/default/src/app/(base)/template-steps/template-step-form.tsx +342 -384
  180. package/templates/default/src/app/(base)/template-steps/template-step-helpers.test.ts +99 -0
  181. package/templates/default/src/app/(base)/template-steps/template-step-helpers.ts +176 -0
  182. package/templates/default/src/app/(base)/template-steps/template-step-table-columns.tsx +153 -158
  183. package/templates/default/src/app/(base)/template-steps/template-step-table.tsx +26 -24
  184. package/templates/default/src/app/(base)/template-test-cases/create/page.tsx +56 -56
  185. package/templates/default/src/app/(base)/template-test-cases/modify/[id]/page.tsx +89 -89
  186. package/templates/default/src/app/(base)/template-test-cases/page.tsx +58 -58
  187. package/templates/default/src/app/(base)/template-test-cases/template-test-case-flow.test.tsx +109 -0
  188. package/templates/default/src/app/(base)/template-test-cases/template-test-case-flow.tsx +45 -84
  189. package/templates/default/src/app/(base)/template-test-cases/template-test-case-form.test.tsx +140 -0
  190. package/templates/default/src/app/(base)/template-test-cases/template-test-case-form.tsx +154 -262
  191. package/templates/default/src/app/(base)/template-test-cases/template-test-case-table-columns.tsx +76 -76
  192. package/templates/default/src/app/(base)/template-test-cases/template-test-case-table.tsx +32 -32
  193. package/templates/default/src/app/(base)/test-cases/create/page.tsx +90 -76
  194. package/templates/default/src/app/(base)/test-cases/create-from-template/create-from-template-helpers.test.ts +94 -0
  195. package/templates/default/src/app/(base)/test-cases/create-from-template/create-from-template-helpers.ts +171 -0
  196. package/templates/default/src/app/(base)/test-cases/create-from-template/generate/[id]/page.tsx +105 -96
  197. package/templates/default/src/app/(base)/test-cases/create-from-template/page.tsx +40 -38
  198. package/templates/default/src/app/(base)/test-cases/create-from-template/template-selection-form.test.tsx +87 -0
  199. package/templates/default/src/app/(base)/test-cases/create-from-template/template-selection-form.tsx +83 -73
  200. package/templates/default/src/app/(base)/test-cases/modify/[id]/page.tsx +106 -106
  201. package/templates/default/src/app/(base)/test-cases/page.tsx +3 -2
  202. package/templates/default/src/app/(base)/test-cases/test-case-flow.test.tsx +108 -0
  203. package/templates/default/src/app/(base)/test-cases/test-case-flow.tsx +43 -82
  204. package/templates/default/src/app/(base)/test-cases/test-case-form.test.tsx +202 -0
  205. package/templates/default/src/app/(base)/test-cases/test-case-form.tsx +263 -395
  206. package/templates/default/src/app/(base)/test-cases/test-case-route-helpers.test.ts +95 -0
  207. package/templates/default/src/app/(base)/test-cases/test-case-route-helpers.ts +147 -0
  208. package/templates/default/src/app/(base)/test-cases/test-case-table.tsx +4 -2
  209. package/templates/default/src/app/(base)/test-runs/[id]/page.tsx +11 -10
  210. package/templates/default/src/app/(base)/test-runs/create/page.tsx +4 -5
  211. package/templates/default/src/app/(base)/test-runs/page.tsx +60 -60
  212. package/templates/default/src/app/(base)/test-runs/test-run-form-helpers.test.ts +50 -0
  213. package/templates/default/src/app/(base)/test-runs/test-run-form-helpers.ts +168 -0
  214. package/templates/default/src/app/(base)/test-runs/test-run-form.test.tsx +138 -0
  215. package/templates/default/src/app/(base)/test-runs/test-run-form.tsx +111 -256
  216. package/templates/default/src/app/(base)/test-runs/test-run-table-columns.tsx +229 -229
  217. package/templates/default/src/app/(base)/test-runs/test-run-table.tsx +127 -127
  218. package/templates/default/src/app/(base)/test-runs/use-test-run-name-validation.ts +74 -0
  219. package/templates/default/src/app/(base)/test-suites/create/page.tsx +17 -12
  220. package/templates/default/src/app/(base)/test-suites/modify/[id]/page.tsx +22 -19
  221. package/templates/default/src/app/(base)/test-suites/page.tsx +14 -56
  222. package/templates/default/src/app/(base)/test-suites/test-suite-form.test.tsx +127 -0
  223. package/templates/default/src/app/(base)/test-suites/test-suite-form.tsx +45 -64
  224. package/templates/default/src/app/(base)/test-suites/test-suite-helpers.test.ts +67 -0
  225. package/templates/default/src/app/(base)/test-suites/test-suite-helpers.ts +215 -0
  226. package/templates/default/src/app/(base)/test-suites/test-suite-table.tsx +32 -29
  227. package/templates/default/src/app/(dashboard-components)/app-drawer.tsx +187 -187
  228. package/templates/default/src/app/(dashboard-components)/data-card-grid.tsx +12 -12
  229. package/templates/default/src/app/(dashboard-components)/data-card.tsx +26 -26
  230. package/templates/default/src/app/(dashboard-components)/execution-health-panel.tsx +56 -56
  231. package/templates/default/src/app/(dashboard-components)/ongoing-test-runs-card.tsx +87 -87
  232. package/templates/default/src/app/(dashboard-components)/quick-actions-drawer.tsx +44 -44
  233. package/templates/default/src/app/api/reports/steps/[stepId]/screenshot/route.test.ts +83 -0
  234. package/templates/default/src/app/api/reports/steps/[stepId]/screenshot/route.ts +52 -52
  235. package/templates/default/src/app/api/test-runs/[runId]/download/route.test.ts +169 -0
  236. package/templates/default/src/app/api/test-runs/[runId]/download/route.ts +1 -1
  237. package/templates/default/src/app/api/test-runs/[runId]/trace/[testCaseId]/route.test.ts +135 -0
  238. package/templates/default/src/app/api/test-runs/[runId]/trace/[testCaseId]/route.ts +146 -146
  239. package/templates/default/src/app/globals.css +147 -147
  240. package/templates/default/src/app/page.tsx +1 -1
  241. package/templates/default/src/assets/icons/empty-tube.tsx +23 -23
  242. package/templates/default/src/assets/icons/tube-plus.tsx +29 -29
  243. package/templates/default/src/components/base-node.tsx +21 -21
  244. package/templates/default/src/components/chart/pie-chart.tsx +73 -73
  245. package/templates/default/src/components/data-extraction/locator-inspector-helpers.test.ts +32 -0
  246. package/templates/default/src/components/data-extraction/locator-inspector-helpers.ts +183 -0
  247. package/templates/default/src/components/data-extraction/locator-inspector.tsx +349 -460
  248. package/templates/default/src/components/data-state/empty-state.tsx +40 -40
  249. package/templates/default/src/components/data-visualization/info-card.tsx +70 -70
  250. package/templates/default/src/components/data-visualization/info-grid.tsx +22 -22
  251. package/templates/default/src/components/diagram/button-edge.tsx +54 -54
  252. package/templates/default/src/components/diagram/dynamic-parameters-helpers.test.ts +83 -0
  253. package/templates/default/src/components/diagram/dynamic-parameters-helpers.ts +158 -0
  254. package/templates/default/src/components/diagram/dynamic-parameters.tsx +350 -474
  255. package/templates/default/src/components/diagram/edit-header-option.tsx +36 -36
  256. package/templates/default/src/components/diagram/flow-diagram-helpers.test.ts +117 -0
  257. package/templates/default/src/components/diagram/flow-diagram-helpers.ts +251 -0
  258. package/templates/default/src/components/diagram/flow-diagram.tsx +247 -470
  259. package/templates/default/src/components/diagram/flow-host-helpers.test.ts +74 -0
  260. package/templates/default/src/components/diagram/flow-host-helpers.ts +51 -0
  261. package/templates/default/src/components/diagram/node-form-helpers.test.ts +92 -0
  262. package/templates/default/src/components/diagram/node-form-helpers.ts +100 -0
  263. package/templates/default/src/components/diagram/node-form.test.tsx +168 -0
  264. package/templates/default/src/components/diagram/node-form.tsx +199 -262
  265. package/templates/default/src/components/diagram/options-header-node.tsx +57 -57
  266. package/templates/default/src/components/diagram/template-step-combobox.tsx +155 -155
  267. package/templates/default/src/components/diagram/use-flow-node-order.ts +49 -0
  268. package/templates/default/src/components/form/error-message.tsx +7 -7
  269. package/templates/default/src/components/kokonutui/smooth-tab.tsx +453 -453
  270. package/templates/default/src/components/loading-skeleton/data-table/data-table-skeleton.tsx +30 -30
  271. package/templates/default/src/components/loading-skeleton/form/button-skeleton.tsx +8 -8
  272. package/templates/default/src/components/loading-skeleton/form/icon-button-skeleton.tsx +8 -8
  273. package/templates/default/src/components/loading-skeleton/form/text-input-skeleton.tsx +8 -8
  274. package/templates/default/src/components/loading-skeleton/visualization/table-skeleton.tsx +14 -14
  275. package/templates/default/src/components/navigation/command-badge.tsx +34 -34
  276. package/templates/default/src/components/navigation/command-chain-input.tsx +51 -51
  277. package/templates/default/src/components/navigation/entity-search-command.tsx +118 -116
  278. package/templates/default/src/components/navigation/nav-card.tsx +31 -31
  279. package/templates/default/src/components/navigation/nav-command-helpers.ts +122 -0
  280. package/templates/default/src/components/navigation/nav-command-search.tsx +125 -0
  281. package/templates/default/src/components/navigation/nav-command.test.tsx +106 -0
  282. package/templates/default/src/components/navigation/nav-command.tsx +49 -472
  283. package/templates/default/src/components/navigation/nav-link.tsx +60 -60
  284. package/templates/default/src/components/navigation/nav-menu-card-deck.tsx +112 -112
  285. package/templates/default/src/components/navigation/use-nav-command.ts +58 -0
  286. package/templates/default/src/components/node-header.tsx +159 -159
  287. package/templates/default/src/components/reports/test-case-logs-modal.tsx +310 -310
  288. package/templates/default/src/components/table/table-actions.tsx +174 -172
  289. package/templates/default/src/components/test-case/test-case-form-helpers.test.ts +100 -0
  290. package/templates/default/src/components/test-case/test-case-form-helpers.ts +140 -0
  291. package/templates/default/src/components/test-case/test-case-picker-helpers.test.ts +40 -0
  292. package/templates/default/src/components/test-case/test-case-picker-helpers.ts +41 -0
  293. package/templates/default/src/components/test-case/test-case-picker.test.tsx +44 -0
  294. package/templates/default/src/components/test-case/test-case-picker.tsx +16 -35
  295. package/templates/default/src/components/test-case/test-scenario-preview.tsx +34 -0
  296. package/templates/default/src/components/test-run/download-logs-button.tsx +92 -92
  297. package/templates/default/src/components/test-run/log-viewer-helpers.test.ts +37 -0
  298. package/templates/default/src/components/test-run/log-viewer-helpers.ts +80 -0
  299. package/templates/default/src/components/test-run/log-viewer.test.tsx +118 -0
  300. package/templates/default/src/components/test-run/log-viewer.tsx +51 -350
  301. package/templates/default/src/components/test-run/test-run-details-helpers.test.ts +31 -0
  302. package/templates/default/src/components/test-run/test-run-details-helpers.ts +208 -0
  303. package/templates/default/src/components/test-run/test-run-details.test.tsx +174 -0
  304. package/templates/default/src/components/test-run/test-run-details.tsx +155 -457
  305. package/templates/default/src/components/test-run/test-run-header-helpers.test.ts +31 -0
  306. package/templates/default/src/components/test-run/test-run-header-helpers.ts +23 -0
  307. package/templates/default/src/components/test-run/test-run-header.test.tsx +103 -0
  308. package/templates/default/src/components/test-run/test-run-header.tsx +27 -149
  309. package/templates/default/src/components/test-run/use-log-viewer.ts +213 -0
  310. package/templates/default/src/components/test-run/use-test-run-details.ts +184 -0
  311. package/templates/default/src/components/test-run/use-test-run-header.ts +89 -0
  312. package/templates/default/src/components/test-run/view-report-button.tsx +102 -102
  313. package/templates/default/src/components/test-suite/test-suite-picker-helpers.test.ts +68 -0
  314. package/templates/default/src/components/test-suite/test-suite-picker-helpers.ts +76 -0
  315. package/templates/default/src/components/test-suite/test-suite-picker.test.tsx +65 -0
  316. package/templates/default/src/components/test-suite/test-suite-picker.tsx +4 -72
  317. package/templates/default/src/components/theme/mode-toggle.tsx +54 -54
  318. package/templates/default/src/components/theme/theme-provider.tsx +8 -8
  319. package/templates/default/src/components/typography/page-header-subtitle.tsx +7 -7
  320. package/templates/default/src/components/typography/page-header.tsx +7 -7
  321. package/templates/default/src/components/ui/alert-dialog.tsx +106 -106
  322. package/templates/default/src/components/ui/alert.tsx +43 -43
  323. package/templates/default/src/components/ui/avatar.tsx +40 -40
  324. package/templates/default/src/components/ui/badge.tsx +29 -29
  325. package/templates/default/src/components/ui/button.tsx +47 -47
  326. package/templates/default/src/components/ui/calendar.tsx +158 -158
  327. package/templates/default/src/components/ui/card.tsx +43 -43
  328. package/templates/default/src/components/ui/checkbox.tsx +28 -28
  329. package/templates/default/src/components/ui/command.tsx +135 -135
  330. package/templates/default/src/components/ui/data-table-column-header.tsx +61 -61
  331. package/templates/default/src/components/ui/data-table-pagination.tsx +87 -87
  332. package/templates/default/src/components/ui/data-table-view-options.tsx +50 -50
  333. package/templates/default/src/components/ui/data-table.test.tsx +122 -0
  334. package/templates/default/src/components/ui/data-table.tsx +298 -261
  335. package/templates/default/src/components/ui/dialog.tsx +97 -97
  336. package/templates/default/src/components/ui/dropdown-menu.tsx +182 -182
  337. package/templates/default/src/components/ui/input.tsx +22 -22
  338. package/templates/default/src/components/ui/kbd.tsx +28 -28
  339. package/templates/default/src/components/ui/label.tsx +19 -19
  340. package/templates/default/src/components/ui/loading.tsx +12 -12
  341. package/templates/default/src/components/ui/multi-select-with-preview.tsx +116 -116
  342. package/templates/default/src/components/ui/multi-select.test.tsx +45 -0
  343. package/templates/default/src/components/ui/multi-select.tsx +158 -142
  344. package/templates/default/src/components/ui/navigation-menu.tsx +120 -120
  345. package/templates/default/src/components/ui/popover.tsx +33 -33
  346. package/templates/default/src/components/ui/progress.tsx +25 -25
  347. package/templates/default/src/components/ui/radio-group.tsx +44 -44
  348. package/templates/default/src/components/ui/scroll-area.tsx +40 -40
  349. package/templates/default/src/components/ui/select.tsx +151 -151
  350. package/templates/default/src/components/ui/separator.tsx +22 -22
  351. package/templates/default/src/components/ui/skeleton.tsx +7 -7
  352. package/templates/default/src/components/ui/table.tsx +76 -76
  353. package/templates/default/src/components/ui/tabs.tsx +55 -55
  354. package/templates/default/src/components/ui/textarea.tsx +21 -21
  355. package/templates/default/src/components/ui/toast.tsx +113 -113
  356. package/templates/default/src/components/ui/toaster.tsx +26 -26
  357. package/templates/default/src/components/user-prompt/delete-prompt.test.tsx +60 -0
  358. package/templates/default/src/components/user-prompt/delete-prompt.tsx +118 -87
  359. package/templates/default/src/constants/form-opts/diagram/node-form.ts +30 -30
  360. package/templates/default/src/constants/form-opts/environment-form-opts.ts +24 -24
  361. package/templates/default/src/constants/form-opts/locator-group-form-opts.ts +28 -28
  362. package/templates/default/src/constants/form-opts/module-form-opts.ts +21 -21
  363. package/templates/default/src/constants/form-opts/tag-form-opts.ts +42 -42
  364. package/templates/default/src/constants/form-opts/template-selection-form-opts.ts +16 -16
  365. package/templates/default/src/constants/form-opts/template-step-group-form-opts.ts +24 -24
  366. package/templates/default/src/constants/form-opts/template-test-case-form-opts.ts +39 -39
  367. package/templates/default/src/constants/form-opts/template-test-step-form-opts.ts +36 -36
  368. package/templates/default/src/constants/form-opts/test-case-form-opts.ts +43 -43
  369. package/templates/default/src/constants/form-opts/test-suite-form-opts.ts +24 -24
  370. package/templates/default/src/hooks/use-toast.ts +187 -187
  371. package/templates/default/src/lib/automation/automation-path-roots.ts +95 -0
  372. package/templates/default/src/lib/automation/automation-workspace.ts +147 -0
  373. package/templates/default/src/lib/automation/paths.ts +6 -211
  374. package/templates/default/src/lib/bidirectional-sync.ts +432 -432
  375. package/templates/default/src/lib/environment-file-utils.ts +2 -1
  376. package/templates/default/src/lib/executor/local-executor-adapter.ts +2 -5
  377. package/templates/default/src/lib/feature-file-generator.ts +2 -1
  378. package/templates/default/src/lib/gherkin-parser.ts +0 -2
  379. package/templates/default/src/lib/locator-group-file-utils.ts +304 -307
  380. package/templates/default/src/lib/locator-picker/session-manager.ts +0 -21
  381. package/templates/default/src/lib/locator-picker/suggestions.ts +13 -11
  382. package/templates/default/src/lib/metrics/metric-calculator.ts +2 -6
  383. package/templates/default/src/lib/module-hierarchy-builder.ts +205 -205
  384. package/templates/default/src/lib/path-helpers/module-path.ts +71 -71
  385. package/templates/default/src/lib/sync/sync-executor.test.ts +76 -0
  386. package/templates/default/src/lib/sync/sync-pending-counts.test.ts +227 -226
  387. package/templates/default/src/lib/sync/sync-pending-counts.ts +2 -5
  388. package/templates/default/src/lib/template-sync-utils.d.ts +6 -6
  389. package/templates/default/src/lib/template-sync-utils.js +46 -46
  390. package/templates/default/src/lib/template-sync-utils.ts +63 -63
  391. package/templates/default/src/lib/test-case-utils.ts +6 -6
  392. package/templates/default/src/lib/test-run/log-formatter.ts +83 -83
  393. package/templates/default/src/lib/test-run/report-parser.ts +352 -352
  394. package/templates/default/src/lib/test-run/test-run-executor.ts +13 -13
  395. package/templates/default/src/lib/test-run/winston-logger.ts +65 -64
  396. package/templates/default/src/lib/transformers/gherkin-converter.ts +42 -42
  397. package/templates/default/src/lib/transformers/key-to-icon-transformer.tsx +95 -95
  398. package/templates/default/src/lib/transformers/template-test-case-converter.ts +160 -160
  399. package/templates/default/src/lib/utils/node-param-validation.ts +81 -81
  400. package/templates/default/src/lib/utils/template-step-file-generator.ts +2 -2
  401. package/templates/default/src/lib/utils/template-step-file-manager.ts +166 -166
  402. package/templates/default/src/lib/utils.ts +31 -31
  403. package/templates/default/src/services/dashboard/dashboard-service.test.ts +106 -0
  404. package/templates/default/src/services/dashboard/dashboard-service.ts +173 -0
  405. package/templates/default/src/services/environment/environment-service.test.ts +137 -0
  406. package/templates/default/src/services/environment/environment-service.ts +96 -0
  407. package/templates/default/src/services/locator/locator-path-utils.test.ts +14 -0
  408. package/templates/default/src/services/locator/locator-path-utils.ts +14 -0
  409. package/templates/default/src/services/locator/locator-service.test.ts +63 -0
  410. package/templates/default/src/services/locator/locator-service.ts +479 -0
  411. package/templates/default/src/services/locator/locator-sync-utils.test.ts +19 -0
  412. package/templates/default/src/services/locator/locator-sync-utils.ts +21 -0
  413. package/templates/default/src/services/locator-group/locator-group-service.test.ts +123 -0
  414. package/templates/default/src/services/locator-group/locator-group-service.ts +180 -0
  415. package/templates/default/src/services/module/module-service.test.ts +89 -0
  416. package/templates/default/src/services/module/module-service.ts +66 -0
  417. package/templates/default/src/services/report/report-service.test.ts +244 -0
  418. package/templates/default/src/services/report/report-service.ts +438 -0
  419. package/templates/default/src/services/shared/constants.ts +2 -0
  420. package/templates/default/src/services/shared/errors.test.ts +38 -0
  421. package/templates/default/src/services/shared/errors.ts +44 -0
  422. package/templates/default/src/services/shared/index.ts +7 -0
  423. package/templates/default/src/services/tag/tag-service.test.ts +22 -0
  424. package/templates/default/src/services/tag/tag-service.ts +41 -0
  425. package/templates/default/src/services/template-step/template-step-service.test.ts +22 -0
  426. package/templates/default/src/services/template-step/template-step-service.ts +171 -0
  427. package/templates/default/src/services/template-step-group/template-step-group-service.test.ts +22 -0
  428. package/templates/default/src/services/template-step-group/template-step-group-service.ts +81 -0
  429. package/templates/default/src/services/template-test-case/template-test-case-service.test.ts +22 -0
  430. package/templates/default/src/services/template-test-case/template-test-case-service.ts +128 -0
  431. package/templates/default/src/services/test-case/test-case-service.test.ts +175 -0
  432. package/templates/default/src/services/test-case/test-case-service.ts +298 -0
  433. package/templates/default/src/services/test-run/test-run-helpers.ts +61 -0
  434. package/templates/default/src/services/test-run/test-run-service.test.ts +647 -0
  435. package/templates/default/src/services/test-run/test-run-service.ts +917 -0
  436. package/templates/default/src/services/test-suite/test-suite-service.test.ts +127 -0
  437. package/templates/default/src/services/test-suite/test-suite-service.ts +197 -0
  438. package/templates/default/src/types/diagram/diagram.ts +34 -34
  439. package/templates/default/src/types/diagram/template-step.ts +11 -11
  440. package/templates/default/src/types/executor/browser.type.ts +1 -1
  441. package/templates/default/src/types/form/actionHandler.ts +19 -6
  442. package/templates/default/src/types/locator/locator.type.ts +11 -11
  443. package/templates/default/src/types/step/step.type.ts +1 -1
  444. package/templates/default/src/types/table/data-table.ts +6 -6
  445. package/templates/default/tailwind.config.ts +62 -62
  446. package/templates/default/tsconfig.json +1 -1
  447. package/dist/cli.e2e.test.d.ts +0 -2
  448. package/dist/cli.e2e.test.d.ts.map +0 -1
  449. package/dist/cli.e2e.test.js +0 -73
  450. package/dist/cli.e2e.test.js.map +0 -1
  451. package/dist/config.test.d.ts +0 -2
  452. package/dist/config.test.d.ts.map +0 -1
  453. package/dist/config.test.js +0 -65
  454. package/dist/config.test.js.map +0 -1
  455. package/dist/copy-template.test.d.ts +0 -2
  456. package/dist/copy-template.test.d.ts.map +0 -1
  457. package/dist/copy-template.test.js +0 -71
  458. package/dist/copy-template.test.js.map +0 -1
  459. package/dist/download-repo.test.d.ts +0 -2
  460. package/dist/download-repo.test.d.ts.map +0 -1
  461. package/dist/download-repo.test.js +0 -14
  462. package/dist/download-repo.test.js.map +0 -1
  463. package/dist/install.test.d.ts +0 -2
  464. package/dist/install.test.d.ts.map +0 -1
  465. package/dist/install.test.js +0 -119
  466. package/dist/install.test.js.map +0 -1
  467. package/dist/prompts.test.d.ts +0 -2
  468. package/dist/prompts.test.d.ts.map +0 -1
  469. package/dist/prompts.test.js +0 -58
  470. package/dist/prompts.test.js.map +0 -1
  471. package/templates/default/src/actions/conflict/conflict.action.ts +0 -33
  472. package/templates/default/src/actions/review/review-actions.ts +0 -147
  473. package/templates/default/src/actions/user/user-actions.ts +0 -13
  474. package/templates/default/src/app/(base)/locators/locator-form.tsx +0 -163
  475. package/templates/default/src/app/(base)/reviews/create/page.tsx +0 -26
  476. package/templates/default/src/app/(base)/reviews/created-reviews-table.tsx +0 -15
  477. package/templates/default/src/app/(base)/reviews/modify/[id]/page.tsx +0 -26
  478. package/templates/default/src/app/(base)/reviews/page.tsx +0 -26
  479. package/templates/default/src/app/(base)/reviews/review/[id]/page.tsx +0 -26
  480. package/templates/default/src/app/(base)/reviews/review-form.tsx +0 -11
  481. package/templates/default/src/app/(base)/reviews/review-table-by-creator-columns.tsx +0 -9
  482. package/templates/default/src/app/(base)/reviews/review-table-by-reviewer-columns.tsx +0 -9
  483. package/templates/default/src/app/(base)/reviews/reviewer-reviews-table.tsx +0 -15
  484. package/templates/default/src/constants/form-opts/locator-form-opts.ts +0 -20
  485. package/templates/default/src/constants/form-opts/review-form-opts.ts +0 -23
@@ -16,13 +16,22 @@ import {
16
16
  ParsedStep,
17
17
  } from '../src/lib/gherkin-parser'
18
18
  import { buildModuleHierarchy, findModuleByPath } from '../src/lib/module-hierarchy-builder'
19
- import { TemplateStepType, TemplateStepIcon, StepParameterType, TagType } from '@prisma/client'
19
+ import { StepParameterType, TagType } from '@prisma/client'
20
20
  import { ensureAutomationWorkspaceReady, getAutomationFeaturesDir } from '../src/lib/automation/paths'
21
21
  import {
22
22
  determineProjectedStepIcon,
23
23
  getTestSuiteFilesystemKey,
24
24
  normalizeProjectedDbTestCaseSteps,
25
25
  } from '../src/lib/sync/projected-feature-utils'
26
+ import { extractTestSuiteNameFromFilename } from './lib/filename-utils'
27
+ import { splitTagLine } from './lib/tag-parsing'
28
+ import {
29
+ determineStepTypeAndIcon,
30
+ findMatchingTemplateStep,
31
+ sameResolvedParameters,
32
+ } from './lib/step-matcher'
33
+ import { printSyncSummary } from './lib/sync-summary'
34
+ import { runSyncScript } from './lib/sync-script-runner'
26
35
 
27
36
  interface TestCaseFromFS {
28
37
  identifierTag: string // @tc_... tag
@@ -35,19 +44,6 @@ interface TestCaseFromFS {
35
44
  filePath: string // Feature file path
36
45
  }
37
46
 
38
- interface ParameterMatch {
39
- name: string
40
- value: string
41
- order: number
42
- type: StepParameterType
43
- }
44
-
45
- interface TemplateStepMatch {
46
- templateStepId: string
47
- signature: string
48
- parameters: ParameterMatch[]
49
- }
50
-
51
47
  interface SyncResult {
52
48
  testCasesScanned: number
53
49
  testCasesExisting: number
@@ -61,26 +57,6 @@ interface SyncResult {
61
57
  deletedTestCases: Array<{ identifierTag: string; title: string }>
62
58
  }
63
59
 
64
- /**
65
- * Extracts test suite name from filename
66
- * Example: "login-validation.feature" -> "login-validation"
67
- */
68
- function extractTestSuiteNameFromFilename(filePath: string): string {
69
- const fileName = filePath.split(/[/\\]/).pop() || ''
70
- return fileName.replace(/\.feature$/, '')
71
- }
72
-
73
- /**
74
- * Splits a tag line that may contain multiple tags separated by spaces
75
- * Example: "@smoke @demo" -> ["@smoke", "@demo"]
76
- */
77
- function splitTagLine(tagLine: string): string[] {
78
- return tagLine
79
- .split(/\s+/)
80
- .filter(tag => tag.trim().startsWith('@'))
81
- .map(tag => tag.trim())
82
- }
83
-
84
60
  /**
85
61
  * Normalizes a tag expression to ensure it has the @ prefix
86
62
  * Example: "tc_123" -> "@tc_123", "@tc_123" -> "@tc_123"
@@ -171,149 +147,6 @@ async function scanTestCasesFromFilesystem(featuresDir: string): Promise<TestCas
171
147
  return testCases
172
148
  }
173
149
 
174
- /**
175
- * Converts template step signature to regex pattern
176
- * Replaces placeholders like {string}, {int}, {boolean} with regex patterns
177
- */
178
- function signatureToRegex(signature: string): RegExp {
179
- // Escape special regex characters except placeholders
180
- let pattern = signature.replace(/[.*+?^${}()|[\]\\]/g, match => {
181
- // Don't escape { and } as they're our placeholders
182
- if (match === '{' || match === '}') return match
183
- return '\\' + match
184
- })
185
-
186
- // Replace placeholders with regex patterns
187
- pattern = pattern.replace(/\{string\}/g, '"([^"]+)"') // Matches quoted strings
188
- pattern = pattern.replace(/\{int\}/g, '(\\d+)') // Matches integers
189
- pattern = pattern.replace(/\{boolean\}/g, '(true|false)') // Matches booleans
190
- pattern = pattern.replace(/\{number\}/g, '(\\d+(?:\\.\\d+)?)') // Matches numbers (int or float)
191
-
192
- // Create regex with case-insensitive matching and word boundaries
193
- return new RegExp(`^${pattern}$`, 'i')
194
- }
195
-
196
- /**
197
- * Extracts parameters from gherkin step text based on template step signature
198
- */
199
- function extractParametersFromGherkinStep(
200
- gherkinText: string,
201
- signature: string,
202
- templateStepParameters: Array<{ name: string; order: number; type: StepParameterType }>,
203
- ): ParameterMatch[] | null {
204
- const regex = signatureToRegex(signature)
205
- const match = gherkinText.match(regex)
206
-
207
- if (!match) {
208
- return null
209
- }
210
-
211
- // Extract captured groups (skip index 0 which is the full match)
212
- const capturedValues = match.slice(1)
213
- const parameters: ParameterMatch[] = []
214
-
215
- // Map captured values to template step parameters by order
216
- for (let i = 0; i < capturedValues.length && i < templateStepParameters.length; i++) {
217
- const param = templateStepParameters[i]
218
- const value = capturedValues[i]
219
-
220
- if (value !== undefined) {
221
- parameters.push({
222
- name: param.name,
223
- value: value,
224
- order: param.order,
225
- type: param.type,
226
- })
227
- }
228
- }
229
-
230
- return parameters
231
- }
232
-
233
- /**
234
- * Matches a gherkin step to a template step by pattern matching
235
- * Returns the template step ID and extracted parameters, or null if no match found
236
- * Note: Template step signatures don't include the keyword, so we match against step.text only
237
- */
238
- async function matchGherkinStepToTemplateStep(gherkinStep: ParsedStep): Promise<TemplateStepMatch | null> {
239
- try {
240
- // Get all template steps from database
241
- const templateSteps = await prisma.templateStep.findMany({
242
- include: {
243
- parameters: {
244
- orderBy: {
245
- order: 'asc',
246
- },
247
- },
248
- },
249
- })
250
-
251
- // Try to match against each template step signature
252
- // Template step signatures don't include the keyword, so match against step.text
253
- for (const templateStep of templateSteps) {
254
- // Try to match the gherkin step text (without keyword) against the signature
255
- const parameters = extractParametersFromGherkinStep(
256
- gherkinStep.text,
257
- templateStep.signature,
258
- templateStep.parameters.map(p => ({
259
- name: p.name,
260
- order: p.order,
261
- type: p.type,
262
- })),
263
- )
264
-
265
- if (parameters !== null) {
266
- return {
267
- templateStepId: templateStep.id,
268
- signature: templateStep.signature,
269
- parameters,
270
- }
271
- }
272
- }
273
-
274
- return null
275
- } catch (error) {
276
- console.error(`Error matching gherkin step to template step:`, error)
277
- return null
278
- }
279
- }
280
-
281
- /**
282
- * Determines the step type and icon based on the Gherkin keyword
283
- */
284
- function determineStepTypeAndIcon(keyword: string): { type: TemplateStepType; icon: TemplateStepIcon } {
285
- const lowerKeyword = keyword.toLowerCase().trim()
286
-
287
- if (lowerKeyword === 'given') {
288
- return { type: 'ACTION', icon: 'NAVIGATION' }
289
- } else if (lowerKeyword === 'when') {
290
- return { type: 'ACTION', icon: 'MOUSE' }
291
- } else if (lowerKeyword === 'then') {
292
- return { type: 'ASSERTION', icon: 'VALIDATION' }
293
- } else if (lowerKeyword === 'and' || lowerKeyword === 'but') {
294
- return { type: 'ACTION', icon: 'MOUSE' }
295
- } else {
296
- // Default fallback
297
- return { type: 'ACTION', icon: 'MOUSE' }
298
- }
299
- }
300
-
301
- function sameResolvedParameters(left: ParameterMatch[], right: ParameterMatch[]): boolean {
302
- if (left.length !== right.length) {
303
- return false
304
- }
305
-
306
- return left.every((parameter, index) => {
307
- const other = right[index]
308
- return (
309
- parameter.name === other?.name &&
310
- parameter.value === other?.value &&
311
- parameter.order === other?.order &&
312
- parameter.type === other?.type
313
- )
314
- })
315
- }
316
-
317
150
  /**
318
151
  * Finds or creates a tag by tag expression
319
152
  * If the tag exists but has a different type, updates it to the correct type
@@ -354,12 +187,67 @@ async function findOrCreateTag(tagExpression: string, type: TagType): Promise<st
354
187
  }
355
188
  }
356
189
 
190
+ async function deleteTestCaseWithCascade(
191
+ testCaseId: string,
192
+ identifierTagId?: string,
193
+ ): Promise<void> {
194
+ // Keep deletes in dependency order to satisfy RESTRICT constraints and
195
+ // mirror domain-level delete behavior in a single transactional boundary.
196
+ await prisma.$transaction(async tx => {
197
+ await tx.testRunTestCase.deleteMany({
198
+ where: { testCaseId },
199
+ })
200
+ await tx.review.deleteMany({
201
+ where: { testCaseId },
202
+ })
203
+ await tx.linkedJiraTicket.deleteMany({
204
+ where: { testCaseId },
205
+ })
206
+ await tx.testCaseStepParameter.deleteMany({
207
+ where: {
208
+ testCaseStep: { testCaseId },
209
+ },
210
+ })
211
+ await tx.testCaseStep.deleteMany({
212
+ where: { testCaseId },
213
+ })
214
+
215
+ if (identifierTagId) {
216
+ const otherTestCasesWithTag = await tx.testCase.findMany({
217
+ where: {
218
+ tags: { some: { id: identifierTagId } },
219
+ id: { not: testCaseId },
220
+ },
221
+ })
222
+ if (otherTestCasesWithTag.length === 0) {
223
+ await tx.tag.delete({
224
+ where: { id: identifierTagId },
225
+ })
226
+ }
227
+ }
228
+
229
+ await tx.testCase.delete({
230
+ where: { id: testCaseId },
231
+ })
232
+ })
233
+ }
234
+
357
235
  /**
358
236
  * Syncs test case steps to database
359
237
  */
360
- async function syncTestCaseSteps(testCaseId: string, steps: ParsedStep[], result: SyncResult): Promise<void> {
238
+ async function syncTestCaseSteps(
239
+ testCaseId: string,
240
+ steps: ParsedStep[],
241
+ templateSteps: Array<{
242
+ id: string
243
+ signature: string
244
+ parameters: Array<{ name: string; order: number; type: StepParameterType }>
245
+ }>,
246
+ result: SyncResult,
247
+ ): Promise<void> {
361
248
  try {
362
- // Get existing steps
249
+ // Load current persisted step state once so we can diff by order and apply
250
+ // minimal mutations for idempotent sync runs.
363
251
  const existingSteps = await prisma.testCaseStep.findMany({
364
252
  where: { testCaseId },
365
253
  orderBy: { order: 'asc' },
@@ -373,13 +261,13 @@ async function syncTestCaseSteps(testCaseId: string, steps: ParsedStep[], result
373
261
  },
374
262
  })
375
263
 
376
- // Create a map of existing steps by order
264
+ // Order is the stable identity within a scenario for synchronization.
377
265
  const existingStepsMap = new Map(existingSteps.map(step => [step.order, step]))
378
266
  const projectedExistingStepsMap = new Map(normalizeProjectedDbTestCaseSteps(existingSteps).map(step => [step.order, step]))
379
267
 
380
268
  // Process each step from filesystem
381
269
  for (const step of steps) {
382
- const match = await matchGherkinStepToTemplateStep(step)
270
+ const match = findMatchingTemplateStep(step, templateSteps)
383
271
 
384
272
  if (!match) {
385
273
  result.warnings.push(
@@ -403,7 +291,8 @@ async function syncTestCaseSteps(testCaseId: string, steps: ParsedStep[], result
403
291
  projectedExistingStep.templateStepSignature === match.signature &&
404
292
  sameResolvedParameters(projectedExistingStep.parameters, match.parameters)
405
293
 
406
- // Update existing step if needed
294
+ // First compare with projected state (normalizes icon/signature/params) to
295
+ // avoid redundant writes from representational differences.
407
296
  const needsUpdate =
408
297
  !matchesProjectedState &&
409
298
  (existingStep.gherkinStep !== gherkinStep ||
@@ -483,207 +372,195 @@ async function syncTestCaseSteps(testCaseId: string, steps: ParsedStep[], result
483
372
  }
484
373
  }
485
374
 
486
- /**
487
- * Syncs test cases from filesystem to database
488
- */
489
- async function syncTestCasesToDatabase(testCasesFromFS: TestCaseFromFS[], result: SyncResult): Promise<void> {
490
- console.log('\nāœ… Syncing test cases to database...')
375
+ type TemplateStepForMatch = Array<{
376
+ id: string
377
+ signature: string
378
+ parameters: Array<{ name: string; order: number; type: StepParameterType }>
379
+ }>
380
+
381
+ async function upsertTestCase(
382
+ testCase: TestCaseFromFS,
383
+ templateSteps: TemplateStepForMatch,
384
+ suitesByModuleId: Map<string, Array<{ id: string; name: string }>>,
385
+ result: SyncResult,
386
+ ): Promise<void> {
387
+ // Resolve module + suite first so create/update paths share the same identity anchor.
388
+ // Ensure module exists
389
+ let moduleId = await findModuleByPath(testCase.modulePath)
390
+
391
+ if (!moduleId) {
392
+ console.log(` šŸ“¦ Creating module hierarchy for path: ${testCase.modulePath}`)
393
+ moduleId = await buildModuleHierarchy(testCase.modulePath)
394
+ }
491
395
 
492
- // Track test cases from filesystem (by identifier tag)
493
- const fsTestCaseTags = new Set<string>()
494
- const suitesByModuleId = new Map<string, Array<{ id: string; name: string }>>()
396
+ // Find test suite
397
+ let moduleSuites = suitesByModuleId.get(moduleId)
398
+ if (!moduleSuites) {
399
+ moduleSuites = await prisma.testSuite.findMany({
400
+ where: {
401
+ moduleId: moduleId,
402
+ },
403
+ select: {
404
+ id: true,
405
+ name: true,
406
+ },
407
+ })
408
+ suitesByModuleId.set(moduleId, moduleSuites)
409
+ }
495
410
 
496
- for (const testCase of testCasesFromFS) {
497
- try {
498
- fsTestCaseTags.add(testCase.identifierTag)
411
+ const testSuite = moduleSuites.find(
412
+ suite => getTestSuiteFilesystemKey(suite.name) === getTestSuiteFilesystemKey(testCase.testSuiteName),
413
+ )
499
414
 
500
- // Ensure module exists
501
- let moduleId = await findModuleByPath(testCase.modulePath)
415
+ if (!testSuite) {
416
+ result.errors.push(`Test suite '${testCase.testSuiteName}' not found in module '${testCase.modulePath}'`)
417
+ console.error(` āŒ Test suite '${testCase.testSuiteName}' not found in module '${testCase.modulePath}'`)
418
+ return
419
+ }
502
420
 
503
- if (!moduleId) {
504
- console.log(` šŸ“¦ Creating module hierarchy for path: ${testCase.modulePath}`)
505
- moduleId = await buildModuleHierarchy(testCase.modulePath)
506
- }
421
+ const identifierTagName = testCase.identifierTag.startsWith('@')
422
+ ? testCase.identifierTag.substring(1)
423
+ : testCase.identifierTag
507
424
 
508
- // Find test suite
509
- let moduleSuites = suitesByModuleId.get(moduleId)
510
- if (!moduleSuites) {
511
- moduleSuites = await prisma.testSuite.findMany({
512
- where: {
513
- moduleId: moduleId,
514
- },
515
- select: {
516
- id: true,
517
- name: true,
518
- },
519
- })
520
- suitesByModuleId.set(moduleId, moduleSuites)
521
- }
425
+ const identifierTag = await prisma.tag.findFirst({
426
+ where: {
427
+ name: identifierTagName,
428
+ type: TagType.IDENTIFIER,
429
+ },
430
+ include: {
431
+ testCases: {
432
+ include: {
433
+ TestSuite: true,
434
+ },
435
+ },
436
+ },
437
+ })
522
438
 
523
- const testSuite = moduleSuites.find(
524
- suite => getTestSuiteFilesystemKey(suite.name) === getTestSuiteFilesystemKey(testCase.testSuiteName),
525
- )
439
+ const filterTagIds: string[] = []
440
+ // FILTER tags are opportunistically created during test-case sync; identifier tags
441
+ // are handled separately because they define test-case identity.
442
+ for (const filterTagExpr of testCase.filterTags) {
443
+ const tagId = await findOrCreateTag(filterTagExpr, TagType.FILTER)
444
+ if (tagId) {
445
+ filterTagIds.push(tagId)
446
+ }
447
+ }
526
448
 
527
- if (!testSuite) {
528
- result.errors.push(`Test suite '${testCase.testSuiteName}' not found in module '${testCase.modulePath}'`)
529
- console.error(` āŒ Test suite '${testCase.testSuiteName}' not found in module '${testCase.modulePath}'`)
530
- continue
531
- }
449
+ if (identifierTag && identifierTag.testCases.length > 0) {
450
+ // Prefer a suite-matching case when tags are reused; fallback preserves legacy data.
451
+ const matchedExistingTestCaseSummary =
452
+ identifierTag.testCases.find(existingCase => existingCase.TestSuite.some(suite => suite.id === testSuite.id)) ??
453
+ identifierTag.testCases[0]
454
+ const existingTestCase = await prisma.testCase.findUnique({
455
+ where: { id: matchedExistingTestCaseSummary.id },
456
+ include: {
457
+ tags: true,
458
+ TestSuite: true,
459
+ },
460
+ })
532
461
 
533
- // Find identifier tag
534
- const identifierTagName = testCase.identifierTag.startsWith('@')
535
- ? testCase.identifierTag.substring(1)
536
- : testCase.identifierTag
462
+ if (!existingTestCase) {
463
+ result.errors.push(`Test case with identifier tag '${testCase.identifierTag}' not found`)
464
+ return
465
+ }
537
466
 
538
- // Find test case by identifier tag
539
- const identifierTag = await prisma.tag.findFirst({
540
- where: {
541
- name: identifierTagName,
542
- type: TagType.IDENTIFIER,
543
- },
544
- include: {
545
- testCases: {
546
- include: {
547
- TestSuite: true,
548
- },
467
+ const currentFilterTagIds =
468
+ existingTestCase.tags
469
+ .filter(t => t.type === TagType.FILTER)
470
+ .map(t => t.id)
471
+ .sort() || []
472
+
473
+ const newFilterTagIds = filterTagIds.sort()
474
+ const tagsChanged = JSON.stringify(currentFilterTagIds) !== JSON.stringify(newFilterTagIds)
475
+ const isAssociated = existingTestCase.TestSuite.some(ts => ts.id === testSuite.id)
476
+
477
+ const needsUpdate =
478
+ existingTestCase.title !== testCase.title ||
479
+ existingTestCase.description !== testCase.description ||
480
+ tagsChanged ||
481
+ !isAssociated
482
+
483
+ if (needsUpdate) {
484
+ await prisma.testCase.update({
485
+ where: { id: existingTestCase.id },
486
+ data: {
487
+ title: testCase.title,
488
+ description: testCase.description,
489
+ tags: {
490
+ set: [identifierTag.id, ...filterTagIds].map(id => ({ id })),
549
491
  },
492
+ TestSuite: isAssociated
493
+ ? undefined
494
+ : {
495
+ connect: [{ id: testSuite.id }],
496
+ },
550
497
  },
551
498
  })
552
499
 
553
- // Find filter tag IDs
554
- const filterTagIds: string[] = []
555
- for (const filterTagExpr of testCase.filterTags) {
556
- const tagId = await findOrCreateTag(filterTagExpr, TagType.FILTER)
557
- if (tagId) {
558
- filterTagIds.push(tagId)
559
- }
560
- }
561
-
562
- if (identifierTag && identifierTag.testCases.length > 0) {
563
- // Test case exists - update it
564
- const matchedExistingTestCaseSummary =
565
- identifierTag.testCases.find(existingCase => existingCase.TestSuite.some(suite => suite.id === testSuite.id)) ??
566
- identifierTag.testCases[0]
567
- const existingTestCase = await prisma.testCase.findUnique({
568
- where: { id: matchedExistingTestCaseSummary.id },
569
- include: {
570
- tags: true,
571
- TestSuite: true,
572
- },
573
- })
574
-
575
- if (!existingTestCase) {
576
- result.errors.push(`Test case with identifier tag '${testCase.identifierTag}' not found`)
577
- continue
578
- }
579
-
580
- // Check if update is needed
581
- const currentFilterTagIds =
582
- existingTestCase.tags
583
- .filter(t => t.type === TagType.FILTER)
584
- .map(t => t.id)
585
- .sort() || []
586
-
587
- const newFilterTagIds = filterTagIds.sort()
588
- const tagsChanged = JSON.stringify(currentFilterTagIds) !== JSON.stringify(newFilterTagIds)
589
- const isAssociated = existingTestCase.TestSuite.some(ts => ts.id === testSuite.id)
590
-
591
- const needsUpdate =
592
- existingTestCase.title !== testCase.title ||
593
- existingTestCase.description !== testCase.description ||
594
- tagsChanged ||
595
- !isAssociated
596
-
597
- if (needsUpdate) {
598
- await prisma.testCase.update({
599
- where: { id: existingTestCase.id },
600
- data: {
601
- title: testCase.title,
602
- description: testCase.description,
603
- tags: {
604
- set: [identifierTag.id, ...filterTagIds].map(id => ({ id })),
605
- },
606
- TestSuite: isAssociated
607
- ? undefined // Don't change associations if already connected
608
- : {
609
- connect: [{ id: testSuite.id }], // Add test suite if not already connected
610
- },
611
- },
612
- })
500
+ result.testCasesUpdated++
501
+ result.updatedTestCases.push({
502
+ identifierTag: testCase.identifierTag,
503
+ title: testCase.title,
504
+ })
505
+ console.log(` šŸ”„ Updated test case '${testCase.title}' (${testCase.identifierTag})`)
506
+ } else {
507
+ result.testCasesExisting++
508
+ console.log(` āœ“ Test case '${testCase.title}' (${testCase.identifierTag}) already up to date`)
509
+ }
613
510
 
614
- result.testCasesUpdated++
615
- result.updatedTestCases.push({
616
- identifierTag: testCase.identifierTag,
617
- title: testCase.title,
618
- })
619
- console.log(` šŸ”„ Updated test case '${testCase.title}' (${testCase.identifierTag})`)
620
- } else {
621
- result.testCasesExisting++
622
- console.log(` āœ“ Test case '${testCase.title}' (${testCase.identifierTag}) already up to date`)
623
- }
511
+ await syncTestCaseSteps(existingTestCase.id, testCase.steps, templateSteps, result)
512
+ return
513
+ }
624
514
 
625
- // Sync steps
626
- await syncTestCaseSteps(existingTestCase.id, testCase.steps, result)
627
- } else {
628
- // Test case doesn't exist - create it
629
- // First ensure identifier tag exists
630
- const identifierTagId = await findOrCreateTag(testCase.identifierTag, TagType.IDENTIFIER)
515
+ const identifierTagId = await findOrCreateTag(testCase.identifierTag, TagType.IDENTIFIER)
631
516
 
632
- if (!identifierTagId) {
633
- result.errors.push(`Failed to create identifier tag '${testCase.identifierTag}'`)
634
- console.error(` āŒ Failed to create identifier tag '${testCase.identifierTag}'`)
635
- continue
636
- }
517
+ if (!identifierTagId) {
518
+ result.errors.push(`Failed to create identifier tag '${testCase.identifierTag}'`)
519
+ console.error(` āŒ Failed to create identifier tag '${testCase.identifierTag}'`)
520
+ return
521
+ }
637
522
 
638
- const newTestCase = await prisma.testCase.create({
639
- data: {
640
- title: testCase.title,
641
- description: testCase.description,
642
- tags: {
643
- connect: [identifierTagId, ...filterTagIds].map(id => ({ id })),
644
- },
645
- TestSuite: {
646
- connect: [{ id: testSuite.id }],
647
- },
648
- },
649
- include: {
650
- tags: true,
651
- },
652
- })
523
+ const newTestCase = await prisma.testCase.create({
524
+ data: {
525
+ title: testCase.title,
526
+ description: testCase.description,
527
+ tags: {
528
+ connect: [identifierTagId, ...filterTagIds].map(id => ({ id })),
529
+ },
530
+ TestSuite: {
531
+ connect: [{ id: testSuite.id }],
532
+ },
533
+ },
534
+ include: {
535
+ tags: true,
536
+ },
537
+ })
653
538
 
654
- // Verify identifier tag is associated
655
- const hasIdentifierTag = newTestCase.tags.some(t => t.type === TagType.IDENTIFIER)
656
- if (!hasIdentifierTag) {
657
- result.errors.push(
658
- `Test case '${testCase.title}' was created but identifier tag '${testCase.identifierTag}' was not associated`,
659
- )
660
- console.error(
661
- ` āŒ Test case '${testCase.title}' was created but identifier tag '${testCase.identifierTag}' was not associated`,
662
- )
663
- }
539
+ const hasIdentifierTag = newTestCase.tags.some(t => t.type === TagType.IDENTIFIER)
540
+ if (!hasIdentifierTag) {
541
+ result.errors.push(
542
+ `Test case '${testCase.title}' was created but identifier tag '${testCase.identifierTag}' was not associated`,
543
+ )
544
+ console.error(
545
+ ` āŒ Test case '${testCase.title}' was created but identifier tag '${testCase.identifierTag}' was not associated`,
546
+ )
547
+ }
664
548
 
665
- result.testCasesCreated++
666
- result.createdTestCases.push({
667
- identifierTag: testCase.identifierTag,
668
- title: testCase.title,
669
- })
670
- console.log(` āž• Created test case '${testCase.title}' (${testCase.identifierTag})`)
549
+ result.testCasesCreated++
550
+ result.createdTestCases.push({
551
+ identifierTag: testCase.identifierTag,
552
+ title: testCase.title,
553
+ })
554
+ console.log(` āž• Created test case '${testCase.title}' (${testCase.identifierTag})`)
671
555
 
672
- // Sync steps
673
- await syncTestCaseSteps(newTestCase.id, testCase.steps, result)
674
- }
675
- } catch (error) {
676
- const errorMsg = `Error processing test case '${testCase.title}' from ${testCase.filePath}: ${error}`
677
- result.errors.push(errorMsg)
678
- console.error(` āŒ ${errorMsg}`)
679
- }
680
- }
556
+ await syncTestCaseSteps(newTestCase.id, testCase.steps, templateSteps, result)
557
+ }
681
558
 
682
- // Delete orphaned test cases (test cases in DB but not in FS)
559
+ async function deleteOrphanedTestCases(fsTestCaseTags: Set<string>, result: SyncResult): Promise<void> {
683
560
  console.log('\nšŸ” Checking for orphaned test cases (not in filesystem)...')
684
561
  const allDbTestCases = await prisma.testCase.findMany({
685
562
  include: {
686
- tags: true, // Include all tags, not just identifier tags
563
+ tags: true,
687
564
  },
688
565
  })
689
566
 
@@ -691,56 +568,9 @@ async function syncTestCasesToDatabase(testCasesFromFS: TestCaseFromFS[], result
691
568
  try {
692
569
  const identifierTag = dbTestCase.tags.find(t => t.type === TagType.IDENTIFIER)
693
570
 
694
- // Test cases without identifier tags cannot be synced from filesystem
695
- // and should be deleted as orphaned
696
571
  if (!identifierTag) {
697
572
  console.log(` āš ļø Test case '${dbTestCase.title}' has no identifier tag - will be deleted as orphaned`)
698
-
699
- // Delete the test case and all related records in a transaction
700
- await prisma.$transaction(async tx => {
701
- // Delete all test run test cases (has RESTRICT constraint, must be deleted first)
702
- await tx.testRunTestCase.deleteMany({
703
- where: {
704
- testCaseId: dbTestCase.id,
705
- },
706
- })
707
-
708
- // Delete all reviews
709
- await tx.review.deleteMany({
710
- where: {
711
- testCaseId: dbTestCase.id,
712
- },
713
- })
714
-
715
- // Delete all linked Jira tickets
716
- await tx.linkedJiraTicket.deleteMany({
717
- where: {
718
- testCaseId: dbTestCase.id,
719
- },
720
- })
721
-
722
- // Delete all step parameters
723
- await tx.testCaseStepParameter.deleteMany({
724
- where: {
725
- testCaseStep: {
726
- testCaseId: dbTestCase.id,
727
- },
728
- },
729
- })
730
-
731
- // Delete all test case steps
732
- await tx.testCaseStep.deleteMany({
733
- where: {
734
- testCaseId: dbTestCase.id,
735
- },
736
- })
737
-
738
- // Delete the test case
739
- await tx.testCase.delete({
740
- where: { id: dbTestCase.id },
741
- })
742
- })
743
-
573
+ await deleteTestCaseWithCascade(dbTestCase.id)
744
574
  result.testCasesDeleted++
745
575
  result.deletedTestCases.push({
746
576
  identifierTag: '(no identifier tag)',
@@ -750,12 +580,10 @@ async function syncTestCasesToDatabase(testCasesFromFS: TestCaseFromFS[], result
750
580
  continue
751
581
  }
752
582
 
753
- // Normalize tagExpression to ensure consistent format comparison
754
- // fsTestCaseTags contains normalized tags (with @ prefix), so we need to normalize
755
- // the database tagExpression before comparing
756
583
  const identifierTagExpr = normalizeTagExpression(identifierTag.tagExpression)
584
+ // Compare normalized expressions because filesystem tags are always normalized
585
+ // with '@', while historical DB entries may not be.
757
586
  if (!fsTestCaseTags.has(identifierTagExpr)) {
758
- // Check if test case has test runs (for logging)
759
587
  const testRunTestCases = await prisma.testRunTestCase.findMany({
760
588
  where: { testCaseId: dbTestCase.id },
761
589
  })
@@ -766,72 +594,7 @@ async function syncTestCasesToDatabase(testCasesFromFS: TestCaseFromFS[], result
766
594
  )
767
595
  }
768
596
 
769
- // Delete the test case and all related records in a transaction
770
- // Following the same pattern as deleteTestCaseAction
771
- await prisma.$transaction(async tx => {
772
- // Delete all test run test cases (has RESTRICT constraint, must be deleted first)
773
- await tx.testRunTestCase.deleteMany({
774
- where: {
775
- testCaseId: dbTestCase.id,
776
- },
777
- })
778
-
779
- // Delete all reviews
780
- await tx.review.deleteMany({
781
- where: {
782
- testCaseId: dbTestCase.id,
783
- },
784
- })
785
-
786
- // Delete all linked Jira tickets
787
- await tx.linkedJiraTicket.deleteMany({
788
- where: {
789
- testCaseId: dbTestCase.id,
790
- },
791
- })
792
-
793
- // Delete all step parameters
794
- await tx.testCaseStepParameter.deleteMany({
795
- where: {
796
- testCaseStep: {
797
- testCaseId: dbTestCase.id,
798
- },
799
- },
800
- })
801
-
802
- // Delete all test case steps
803
- await tx.testCaseStep.deleteMany({
804
- where: {
805
- testCaseId: dbTestCase.id,
806
- },
807
- })
808
-
809
- // Delete the identifier tag (only if it's not used by other test cases)
810
- // Check if any other test case uses this tag
811
- const otherTestCasesWithTag = await tx.testCase.findMany({
812
- where: {
813
- tags: {
814
- some: {
815
- id: identifierTag.id,
816
- },
817
- },
818
- id: {
819
- not: dbTestCase.id,
820
- },
821
- },
822
- })
823
-
824
- if (otherTestCasesWithTag.length === 0) {
825
- await tx.tag.delete({
826
- where: { id: identifierTag.id },
827
- })
828
- }
829
-
830
- // Delete the test case
831
- await tx.testCase.delete({
832
- where: { id: dbTestCase.id },
833
- })
834
- })
597
+ await deleteTestCaseWithCascade(dbTestCase.id, identifierTag.id)
835
598
 
836
599
  result.testCasesDeleted++
837
600
  result.deletedTestCases.push({
@@ -849,59 +612,41 @@ async function syncTestCasesToDatabase(testCasesFromFS: TestCaseFromFS[], result
849
612
  }
850
613
 
851
614
  /**
852
- * Generates and displays sync summary
615
+ * Syncs test cases from filesystem to database
853
616
  */
854
- function generateSummary(result: SyncResult): void {
855
- console.log('\nšŸ“Š Sync Summary:')
856
- console.log(` šŸ“ Test cases scanned: ${result.testCasesScanned}`)
857
- console.log(` āœ… Test cases existing: ${result.testCasesExisting}`)
858
- console.log(` āž• Test cases created: ${result.testCasesCreated}`)
859
- console.log(` šŸ”„ Test cases updated: ${result.testCasesUpdated}`)
860
- console.log(` šŸ—‘ļø Test cases deleted: ${result.testCasesDeleted}`)
861
- console.log(` āš ļø Warnings: ${result.warnings.length}`)
862
- console.log(` āŒ Errors: ${result.errors.length}`)
863
-
864
- if (result.createdTestCases.length > 0) {
865
- console.log('\n Created test cases:')
866
- result.createdTestCases.forEach((tc, index) => {
867
- console.log(` ${index + 1}. ${tc.title} (${tc.identifierTag})`)
868
- })
869
- }
870
-
871
- if (result.updatedTestCases.length > 0) {
872
- console.log('\n Updated test cases:')
873
- result.updatedTestCases.forEach((tc, index) => {
874
- console.log(` ${index + 1}. ${tc.title} (${tc.identifierTag})`)
875
- })
876
- }
877
-
878
- if (result.deletedTestCases.length > 0) {
879
- console.log('\n Deleted test cases:')
880
- result.deletedTestCases.forEach((tc, index) => {
881
- console.log(` ${index + 1}. ${tc.title} (${tc.identifierTag})`)
882
- })
883
- }
617
+ async function syncTestCasesToDatabase(testCasesFromFS: TestCaseFromFS[], result: SyncResult): Promise<void> {
618
+ console.log('\nāœ… Syncing test cases to database...')
619
+ // Fixes prior N+1 behavior: template steps are loaded once and reused for
620
+ // every gherkin step match in this sync run.
621
+ const templateSteps = await prisma.templateStep.findMany({
622
+ include: {
623
+ parameters: {
624
+ orderBy: { order: 'asc' },
625
+ },
626
+ },
627
+ })
884
628
 
885
- if (result.warnings.length > 0) {
886
- console.log('\n Warnings:')
887
- result.warnings.forEach((warning, index) => {
888
- console.log(` ${index + 1}. ${warning}`)
889
- })
890
- }
629
+ // Track test cases from filesystem (by identifier tag)
630
+ const fsTestCaseTags = new Set<string>()
631
+ const suitesByModuleId = new Map<string, Array<{ id: string; name: string }>>()
891
632
 
892
- if (result.errors.length > 0) {
893
- console.log('\n Errors:')
894
- result.errors.forEach((error, index) => {
895
- console.log(` ${index + 1}. ${error}`)
896
- })
633
+ for (const testCase of testCasesFromFS) {
634
+ try {
635
+ fsTestCaseTags.add(testCase.identifierTag)
636
+ await upsertTestCase(testCase, templateSteps, suitesByModuleId, result)
637
+ } catch (error) {
638
+ const errorMsg = `Error processing test case '${testCase.title}' from ${testCase.filePath}: ${error}`
639
+ result.errors.push(errorMsg)
640
+ console.error(` āŒ ${errorMsg}`)
641
+ }
897
642
  }
643
+ await deleteOrphanedTestCases(fsTestCaseTags, result)
898
644
  }
899
645
 
900
646
  /**
901
- * Main function
647
+ * Generates and displays sync summary
902
648
  */
903
- async function main() {
904
- try {
649
+ async function main(): Promise<SyncResult | void> {
905
650
  console.log('šŸ”„ Starting test cases sync...')
906
651
  console.log('This will scan feature files and sync test cases to database.')
907
652
  console.log('Filesystem is the source of truth - test cases in DB but not in FS will be deleted.\n')
@@ -939,22 +684,34 @@ async function main() {
939
684
  // Sync to database
940
685
  await syncTestCasesToDatabase(testCasesFromFS, result)
941
686
 
942
- // Generate summary
943
- generateSummary(result)
944
-
945
- if (result.errors.length === 0) {
946
- console.log('\nāœ… Sync completed successfully!')
947
- } else {
948
- console.log('\nāš ļø Sync completed with errors. Please review the errors above.')
949
- process.exit(1)
950
- }
951
- } catch (error) {
952
- console.error('\nāŒ Error during sync:', error)
953
- process.exit(1)
954
- } finally {
955
- await prisma.$disconnect()
956
- }
687
+ printSyncSummary(
688
+ [
689
+ { label: 'šŸ“ Test cases scanned', value: result.testCasesScanned },
690
+ { label: 'āœ… Test cases existing', value: result.testCasesExisting },
691
+ { label: 'āž• Test cases created', value: result.testCasesCreated },
692
+ { label: 'šŸ”„ Test cases updated', value: result.testCasesUpdated },
693
+ { label: 'šŸ—‘ļø Test cases deleted', value: result.testCasesDeleted },
694
+ { label: 'āš ļø Warnings', value: result.warnings.length },
695
+ { label: 'āŒ Errors', value: result.errors.length },
696
+ ],
697
+ [
698
+ {
699
+ title: 'Created test cases',
700
+ items: result.createdTestCases.map(tc => `${tc.title} (${tc.identifierTag})`),
701
+ },
702
+ {
703
+ title: 'Updated test cases',
704
+ items: result.updatedTestCases.map(tc => `${tc.title} (${tc.identifierTag})`),
705
+ },
706
+ {
707
+ title: 'Deleted test cases',
708
+ items: result.deletedTestCases.map(tc => `${tc.title} (${tc.identifierTag})`),
709
+ },
710
+ { title: 'Warnings', items: result.warnings },
711
+ { title: 'Errors', items: result.errors },
712
+ ],
713
+ )
714
+ return result
957
715
  }
958
716
 
959
- main()
960
-
717
+ runSyncScript(main)