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