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,1185 @@
1
+ 'use server'
2
+
3
+ import prisma from '@/config/db-config'
4
+ import { testRunSchema } from '@/constants/form-opts/test-run-form-opts'
5
+ import { ActionResponse } from '@/types/form/actionHandler'
6
+ import { z } from 'zod'
7
+ import {
8
+ TestRunStatus,
9
+ TestRunResult,
10
+ TestRunTestCaseStatus,
11
+ TestRunTestCaseResult,
12
+ TagType,
13
+ Tag,
14
+ } from '@prisma/client'
15
+ import { executeTestRun } from '@/lib/test-run/test-run-executor'
16
+ import { waitForTask, taskSpawner, killTask } from '@/tests/utils/spawner.util'
17
+ import { revalidatePath } from 'next/cache'
18
+ import { formatLogsForStorage, parseLogsFromStorage, type LogEntry } from '@/lib/test-run/log-formatter'
19
+ import { processManager } from '@/lib/test-run/process-manager'
20
+ import { createTestRunLogger, closeLogger, getLogFilePath } from '@/lib/test-run/winston-logger'
21
+ import { promises as fs } from 'fs'
22
+ import path from 'path'
23
+ import { Prisma } from '@prisma/client'
24
+ import { updateTestCaseMetrics, updateMetricsForTestRun } from '@/lib/metrics/metric-calculator'
25
+
26
+ /**
27
+ * Check if a test run name already exists
28
+ */
29
+ async function checkUniqueName(name: string, excludeId?: string): Promise<boolean> {
30
+ const existing = await prisma.testRun.findFirst({
31
+ where: {
32
+ name: name,
33
+ ...(excludeId && { id: { not: excludeId } }),
34
+ },
35
+ })
36
+ return !!existing
37
+ }
38
+
39
+ export async function getAllTestRunsAction(filter?: string): Promise<ActionResponse> {
40
+ try {
41
+ // Build the where clause based on filter
42
+ const whereClause: Prisma.TestRunWhereInput = {}
43
+
44
+ if (filter === 'recentFailed') {
45
+ // Calculate the date 7 days ago
46
+ const sevenDaysAgo = new Date()
47
+ sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7)
48
+
49
+ whereClause.result = TestRunResult.FAILED
50
+ whereClause.completedAt = {
51
+ not: null,
52
+ gte: sevenDaysAgo,
53
+ }
54
+ }
55
+
56
+ const testRuns = await prisma.testRun.findMany({
57
+ where: whereClause,
58
+ include: {
59
+ testCases: true,
60
+ tags: true,
61
+ environment: true,
62
+ },
63
+ })
64
+ return {
65
+ status: 200,
66
+ data: testRuns,
67
+ }
68
+ } catch (error) {
69
+ return {
70
+ status: 500,
71
+ error: `Server error occurred: ${error}`,
72
+ }
73
+ }
74
+ }
75
+
76
+ export async function getTestRunByIdAction(id: string): Promise<ActionResponse> {
77
+ try {
78
+ const testRun = await prisma.testRun.findUnique({
79
+ where: { id },
80
+ include: {
81
+ testCases: {
82
+ include: {
83
+ testCase: true,
84
+ },
85
+ },
86
+ tags: true,
87
+ environment: true,
88
+ reports: true,
89
+ },
90
+ })
91
+
92
+ if (!testRun) {
93
+ return {
94
+ status: 404,
95
+ error: 'Test run not found',
96
+ }
97
+ }
98
+
99
+ return {
100
+ status: 200,
101
+ data: testRun,
102
+ }
103
+ } catch (error) {
104
+ return {
105
+ status: 500,
106
+ error: `Server error occurred: ${error}`,
107
+ }
108
+ }
109
+ }
110
+
111
+ export async function deleteTestRunAction(id: string[]): Promise<ActionResponse> {
112
+ try {
113
+ // Get all unique test case IDs from test runs being deleted (before deletion)
114
+ // This is needed to recalculate metrics after deletion
115
+ const testRunTestCases = await prisma.testRunTestCase.findMany({
116
+ where: {
117
+ testRunId: { in: id },
118
+ },
119
+ select: {
120
+ testCaseId: true,
121
+ },
122
+ })
123
+ const _affectedTestCaseIds = [...new Set(testRunTestCases.map(trtc => trtc.testCaseId))]
124
+
125
+ // find all trace paths for the test runs
126
+ const tracePaths = await prisma.testRunTestCase.findMany({
127
+ where: {
128
+ testRunId: { in: id },
129
+ },
130
+ select: {
131
+ tracePath: true,
132
+ },
133
+ })
134
+ // delete the trace paths
135
+ for (const tracePath of tracePaths) {
136
+ if (tracePath.tracePath) {
137
+ await fs.unlink(tracePath.tracePath)
138
+ }
139
+ }
140
+
141
+ // find all report paths for the test runs
142
+ const reportPaths = await prisma.testRun.findMany({
143
+ where: { id: { in: id } },
144
+ select: {
145
+ reportPath: true,
146
+ },
147
+ })
148
+ // delete the report paths
149
+ for (const reportPath of reportPaths) {
150
+ if (reportPath.reportPath) {
151
+ await fs.unlink(reportPath.reportPath)
152
+ }
153
+ }
154
+
155
+ // delete the test runs
156
+ await prisma.testRun.deleteMany({
157
+ where: { id: { in: id } },
158
+ })
159
+
160
+ // Recalculate metrics for affected test cases and dashboard metrics
161
+ // Note: We recalculate all test case metrics, not just affected ones, because
162
+ // deleting a test run might affect consecutive failure counts for any test case
163
+ // that had recent runs (e.g., if a test case had 3 consecutive failures and we
164
+ // delete one of those failures, it might no longer be "repeatedly failing")
165
+ const { recalculateMetricsForTestCases, updateDashboardMetrics } = await import(
166
+ '@/lib/metrics/metric-calculator'
167
+ )
168
+
169
+ // Get all test case IDs that have recent test runs (last 7 days)
170
+ // These are the ones that might be affected by the deletion
171
+ // We recalculate all of them because deleting a test run might affect
172
+ // consecutive failure counts for any test case
173
+ const recentPeriodDate = new Date()
174
+ recentPeriodDate.setDate(recentPeriodDate.getDate() - 7)
175
+
176
+ const allRecentTestRunTestCases = await prisma.testRunTestCase.findMany({
177
+ where: {
178
+ status: TestRunTestCaseStatus.COMPLETED,
179
+ testRun: {
180
+ completedAt: {
181
+ gte: recentPeriodDate,
182
+ },
183
+ },
184
+ },
185
+ select: {
186
+ testCaseId: true,
187
+ },
188
+ })
189
+
190
+ // Get unique test case IDs
191
+ const allAffectedTestCaseIds = [
192
+ ...new Set(allRecentTestRunTestCases.map(trtc => trtc.testCaseId)),
193
+ ]
194
+
195
+ // Recalculate metrics for all test cases with recent runs
196
+ if (allAffectedTestCaseIds.length > 0) {
197
+ await recalculateMetricsForTestCases(allAffectedTestCaseIds)
198
+ }
199
+
200
+ // Always update dashboard metrics (e.g., failedRecentRunsCount might change)
201
+ await updateDashboardMetrics()
202
+
203
+ // Revalidate paths
204
+ revalidatePath('/test-runs')
205
+ revalidatePath('/')
206
+ return {
207
+ status: 200,
208
+ message: 'Test run(s) deleted successfully',
209
+ }
210
+ } catch (error) {
211
+ return {
212
+ status: 500,
213
+ error: `Server error occurred: ${error}`,
214
+ }
215
+ }
216
+ }
217
+
218
+ export async function getAllTestSuiteTestCasesAction(): Promise<ActionResponse> {
219
+ try {
220
+ const testSuiteTestCases = await prisma.testSuite.findMany({
221
+ include: {
222
+ testCases: true,
223
+ },
224
+ })
225
+ return {
226
+ status: 200,
227
+ data: testSuiteTestCases,
228
+ }
229
+ } catch (error) {
230
+ return {
231
+ status: 500,
232
+ error: `Server error occurred: ${error}`,
233
+ }
234
+ }
235
+ }
236
+
237
+ /**
238
+ * Stores test run logs in the database
239
+ * @param testRunId - The test run ID (runId, not id)
240
+ * @param logs - Array of log entries to store
241
+ */
242
+ export async function storeTestRunLogsAction(testRunId: string, logs: LogEntry[]): Promise<ActionResponse> {
243
+ try {
244
+ if (logs.length === 0) {
245
+ return {
246
+ status: 200,
247
+ message: 'No logs to store',
248
+ }
249
+ }
250
+
251
+ // Format logs for storage
252
+ const formattedLogs = formatLogsForStorage(logs)
253
+
254
+ // Upsert logs in TestRunLog table
255
+ await prisma.testRunLog.upsert({
256
+ where: { testRunId },
257
+ create: {
258
+ testRunId,
259
+ logs: formattedLogs,
260
+ },
261
+ update: {
262
+ logs: formattedLogs,
263
+ },
264
+ })
265
+
266
+ return {
267
+ status: 200,
268
+ message: 'Logs stored successfully',
269
+ }
270
+ } catch (error) {
271
+ console.error(`[TestRunAction] Error storing logs for testRunId: ${testRunId}:`, error)
272
+ return {
273
+ status: 500,
274
+ error: `Server error occurred: ${error instanceof Error ? error.message : 'Unknown error'}`,
275
+ }
276
+ }
277
+ }
278
+
279
+ /**
280
+ * Retrieves test run logs from the database
281
+ * @param testRunId - The test run ID (runId, not id)
282
+ */
283
+ export async function getTestRunLogsAction(testRunId: string): Promise<ActionResponse> {
284
+ try {
285
+ const testRunLog = await prisma.testRunLog.findUnique({
286
+ where: { testRunId },
287
+ })
288
+
289
+ if (!testRunLog) {
290
+ return {
291
+ status: 200,
292
+ data: [],
293
+ }
294
+ }
295
+
296
+ // Parse logs from storage
297
+ const logs = parseLogsFromStorage(testRunLog.logs)
298
+
299
+ return {
300
+ status: 200,
301
+ data: logs,
302
+ }
303
+ } catch (error) {
304
+ console.error(`[TestRunAction] Error retrieving logs for testRunId: ${testRunId}:`, error)
305
+ return {
306
+ status: 500,
307
+ error: `Server error occurred: ${error instanceof Error ? error.message : 'Unknown error'}`,
308
+ }
309
+ }
310
+ }
311
+
312
+ export async function createTestRunAction(
313
+ _prev: unknown,
314
+ value: z.infer<typeof testRunSchema>,
315
+ ): Promise<ActionResponse> {
316
+ try {
317
+ // Validate input
318
+ testRunSchema.parse(value)
319
+
320
+ // Check if name already exists
321
+ const nameExists = await checkUniqueName(value.name)
322
+ if (nameExists) {
323
+ return {
324
+ status: 400,
325
+ error: 'A test run with this name already exists. Please choose a different name.',
326
+ }
327
+ }
328
+
329
+ // Fetch environment and tags from database
330
+ const environment = await prisma.environment.findUnique({
331
+ where: { id: value.environmentId },
332
+ })
333
+
334
+ if (!environment) {
335
+ return {
336
+ status: 400,
337
+ error: 'Environment not found',
338
+ }
339
+ }
340
+
341
+ // Determine if we're filtering by tags or test cases
342
+ const isFilteringByTags = value.tags.length > 0
343
+ const isFilteringByTestCases = value.testCases.length > 0 && value.tags.length === 0
344
+
345
+ // Validate that at least one filtering option is provided
346
+ if (!isFilteringByTags && !isFilteringByTestCases) {
347
+ return {
348
+ status: 400,
349
+ error: 'Either tags or test cases must be provided to filter the test run.',
350
+ }
351
+ }
352
+
353
+ let tags: Tag[] = []
354
+ let testRunTestCases: Array<{ testCaseId: string }> = []
355
+
356
+ if (isFilteringByTags) {
357
+ // Existing behavior: filter by tags
358
+ tags = await prisma.tag.findMany({
359
+ where: { id: { in: value.tags } },
360
+ })
361
+
362
+ // Find test cases that have tags directly OR belong to test suites with tags
363
+ const tagFilteredTestCases = await prisma.testCase.findMany({
364
+ where: {
365
+ OR: [
366
+ // Test cases with tags directly
367
+ {
368
+ tags: {
369
+ some: { id: { in: value.tags } },
370
+ },
371
+ },
372
+ // Test cases in test suites with tags
373
+ {
374
+ TestSuite: {
375
+ some: {
376
+ tags: {
377
+ some: { id: { in: value.tags } },
378
+ },
379
+ },
380
+ },
381
+ },
382
+ ],
383
+ },
384
+ })
385
+
386
+ testRunTestCases = tagFilteredTestCases.map(tc => ({
387
+ testCaseId: tc.id,
388
+ }))
389
+ } else if (isFilteringByTestCases) {
390
+ // New behavior: filter by test cases - extract identifier tags
391
+ const selectedTestCases = await prisma.testCase.findMany({
392
+ where: {
393
+ id: { in: value.testCases.map(tc => tc.testCaseId) },
394
+ },
395
+ include: {
396
+ tags: true,
397
+ },
398
+ })
399
+
400
+ // Extract identifier tags from selected test cases
401
+ const identifierTags = selectedTestCases
402
+ .flatMap(tc => tc.tags)
403
+ .filter(tag => tag.type === TagType.IDENTIFIER)
404
+ // Remove duplicates by id
405
+ .filter((tag, index, self) => index === self.findIndex(t => t.id === tag.id))
406
+
407
+ // Safety check: if no identifier tags found, this would run all tests
408
+ // which is not what the user expects when they select specific test cases
409
+ if (identifierTags.length === 0) {
410
+ return {
411
+ status: 400,
412
+ error: 'Selected test cases do not have identifier tags. Cannot execute specific test cases.',
413
+ }
414
+ }
415
+
416
+ // Filter to only include test cases that have identifier tags
417
+ // Test cases without identifier tags cannot be executed and should be excluded
418
+ const testCasesWithIdentifierTags = selectedTestCases.filter(tc =>
419
+ tc.tags.some(tag => tag.type === TagType.IDENTIFIER),
420
+ )
421
+
422
+ // Log warning if some test cases don't have identifier tags
423
+ const testCasesWithoutIdentifierTags = selectedTestCases.filter(
424
+ tc => !tc.tags.some(tag => tag.type === TagType.IDENTIFIER),
425
+ )
426
+ if (testCasesWithoutIdentifierTags.length > 0) {
427
+ console.warn(
428
+ `[TestRunAction] Some selected test cases (${testCasesWithoutIdentifierTags.length}) do not have identifier tags and will not be executed.`,
429
+ )
430
+ }
431
+
432
+ tags = identifierTags
433
+
434
+ // Only include test cases that have identifier tags
435
+ testRunTestCases = testCasesWithIdentifierTags.map(tc => ({
436
+ testCaseId: tc.id,
437
+ }))
438
+ }
439
+
440
+ // Create TestRun record in database with RUNNING status
441
+ const testRun = await prisma.testRun.create({
442
+ data: {
443
+ name: value.name,
444
+ environmentId: value.environmentId,
445
+ testWorkersCount: value.testWorkersCount || 1,
446
+ browserEngine: value.browserEngine,
447
+ status: TestRunStatus.RUNNING,
448
+ result: TestRunResult.PENDING,
449
+ tags: {
450
+ connect: tags.map(tag => ({ id: tag.id })),
451
+ },
452
+ testCases: {
453
+ create: testRunTestCases.map(tc => ({
454
+ testCaseId: tc.testCaseId,
455
+ })),
456
+ },
457
+ },
458
+ })
459
+
460
+ // Initialize Winston logger for this test run
461
+ const logger = await createTestRunLogger(testRun.runId)
462
+ const logFilePath = getLogFilePath(testRun.runId)
463
+
464
+ // Store log file path in database
465
+ await prisma.testRun.update({
466
+ where: { id: testRun.id },
467
+ data: {
468
+ logPath: logFilePath,
469
+ },
470
+ })
471
+
472
+ // Execute test run asynchronously (don't await, let it run in background)
473
+ try {
474
+ const { process: spawnedProcess, reportPath } = await executeTestRun({
475
+ testRunId: testRun.runId,
476
+ environment,
477
+ tags,
478
+ testWorkersCount: value.testWorkersCount || 1,
479
+ browserEngine: value.browserEngine,
480
+ headless: true, // Default to headless
481
+ })
482
+
483
+ // Store report path in TestRun record
484
+ await prisma.testRun.update({
485
+ where: { id: testRun.id },
486
+ data: {
487
+ reportPath,
488
+ },
489
+ })
490
+
491
+ const executePromise = Promise.resolve(spawnedProcess)
492
+
493
+ // Set up server-side listener for scenario::end events to update test case statuses
494
+ // This ensures status updates happen even if no client is connected
495
+ const onScenarioEnd = async (eventData: {
496
+ testRunId: string
497
+ scenarioName: string
498
+ status: string
499
+ tracePath?: string
500
+ }) => {
501
+ // Only process events for this test run
502
+ if (eventData.testRunId === testRun.runId) {
503
+ console.log(
504
+ `[TestRunAction] Server-side scenario::end event for testRunId: ${testRun.runId}, scenario: ${eventData.scenarioName}, status: ${eventData.status}${eventData.tracePath ? `, tracePath: ${eventData.tracePath}` : ''}`,
505
+ )
506
+ // Map the status string to the expected format
507
+ const statusMap: Record<string, 'passed' | 'failed' | 'skipped' | 'unknown'> = {
508
+ passed: 'passed',
509
+ failed: 'failed',
510
+ skipped: 'skipped',
511
+ }
512
+ const mappedStatus = statusMap[eventData.status] || 'unknown'
513
+ // Update test case status in database
514
+ await updateTestRunTestCaseStatusAction(
515
+ testRun.runId,
516
+ eventData.scenarioName,
517
+ mappedStatus,
518
+ eventData.tracePath,
519
+ )
520
+ }
521
+ }
522
+
523
+ // Register the server-side listener
524
+ processManager.on('scenario::end', onScenarioEnd)
525
+ console.log(`[TestRunAction] Registered server-side scenario::end listener for testRunId: ${testRun.runId}`)
526
+
527
+ // Cleanup function to remove the listener
528
+ const cleanupListener = () => {
529
+ processManager.removeListener('scenario::end', onScenarioEnd)
530
+ console.log(`[TestRunAction] Removed server-side scenario::end listener for testRunId: ${testRun.runId}`)
531
+ }
532
+
533
+ executePromise
534
+ .then(async spawnedProcess => {
535
+ // Wait for process to complete
536
+ const exitCode = await waitForTask(spawnedProcess.name)
537
+
538
+ // Collect all logs from the process output
539
+ const logEntries: LogEntry[] = []
540
+
541
+ // Add stdout logs
542
+ if (spawnedProcess.output.stdout.length > 0) {
543
+ const stdoutText = spawnedProcess.output.stdout.join('')
544
+ const stdoutLines = stdoutText.split('\n').filter(line => line.trim() !== '')
545
+ stdoutLines.forEach((line, index) => {
546
+ const timestamp = new Date(spawnedProcess.startTime.getTime() + index * 10)
547
+ logEntries.push({
548
+ type: 'stdout',
549
+ message: line,
550
+ timestamp,
551
+ })
552
+ // Log to Winston logger
553
+ logger.info(line)
554
+ })
555
+ }
556
+
557
+ // Add stderr logs
558
+ if (spawnedProcess.output.stderr.length > 0) {
559
+ const stderrText = spawnedProcess.output.stderr.join('')
560
+ const stderrLines = stderrText.split('\n').filter(line => line.trim() !== '')
561
+ const stdoutCount = logEntries.filter(e => e.type === 'stdout').length
562
+ stderrLines.forEach((line, index) => {
563
+ const timestamp = new Date(spawnedProcess.startTime.getTime() + stdoutCount * 10 + index * 10)
564
+ logEntries.push({
565
+ type: 'stderr',
566
+ message: line,
567
+ timestamp,
568
+ })
569
+ // Log to Winston logger
570
+ logger.error(line)
571
+ })
572
+ }
573
+
574
+ // Add exit status log
575
+ const exitMessage = `Process exited with code ${exitCode}`
576
+ logEntries.push({
577
+ type: 'status',
578
+ message: exitMessage,
579
+ timestamp: spawnedProcess.endTime || new Date(),
580
+ })
581
+ // Log exit status to Winston logger
582
+ logger.info(exitMessage)
583
+
584
+ // Store logs in database
585
+ await storeTestRunLogsAction(testRun.runId, logEntries)
586
+
587
+ // Close Winston logger
588
+ await closeLogger(logger)
589
+
590
+ // Check current status before updating - preserve CANCELLED status if already set
591
+ const currentTestRun = await prisma.testRun.findUnique({
592
+ where: { id: testRun.id },
593
+ select: { status: true, result: true },
594
+ })
595
+
596
+ // Only update to COMPLETED if not already CANCELLED or CANCELLING
597
+ if (
598
+ currentTestRun &&
599
+ currentTestRun.status !== TestRunStatus.CANCELLED &&
600
+ currentTestRun.status !== TestRunStatus.CANCELLING
601
+ ) {
602
+ // Update TestRun status based on exit code
603
+ const status = exitCode === 0 ? TestRunStatus.COMPLETED : TestRunStatus.COMPLETED
604
+ const result = exitCode === 0 ? TestRunResult.PASSED : TestRunResult.FAILED
605
+
606
+ await prisma.testRun.update({
607
+ where: { id: testRun.id },
608
+ data: {
609
+ status,
610
+ result,
611
+ completedAt: new Date(),
612
+ },
613
+ })
614
+
615
+ // Update metrics for the completed test run
616
+ try {
617
+ await updateMetricsForTestRun(testRun.id)
618
+ } catch (error) {
619
+ console.error(`[TestRunAction] Error updating metrics for test run ${testRun.id}:`, error)
620
+ // Don't fail the test run if metrics update fails
621
+ }
622
+ } else {
623
+ // Status is already CANCELLED or CANCELLING, just update completedAt if not set
624
+ if (currentTestRun && !currentTestRun.result) {
625
+ await prisma.testRun.update({
626
+ where: { id: testRun.id },
627
+ data: {
628
+ completedAt: new Date(),
629
+ },
630
+ })
631
+ }
632
+ }
633
+
634
+ // Clean up the server-side event listener
635
+ cleanupListener()
636
+
637
+ // Store report in database if report path exists and test run is not cancelled
638
+ // Check current status again to ensure we don't generate reports for cancelled runs
639
+ const finalTestRunStatus = await prisma.testRun.findUnique({
640
+ where: { id: testRun.id },
641
+ select: { status: true },
642
+ })
643
+
644
+ if (
645
+ finalTestRunStatus &&
646
+ (finalTestRunStatus.status === TestRunStatus.CANCELLED ||
647
+ finalTestRunStatus.status === TestRunStatus.CANCELLING)
648
+ ) {
649
+ console.log(
650
+ `[TestRunAction] Skipping report generation for testRunId: ${testRun.runId} - test run was cancelled`,
651
+ )
652
+ } else if (reportPath) {
653
+ try {
654
+ const { storeReportFromFile } = await import('@/actions/reports/report-actions')
655
+ const reportResult = await storeReportFromFile(testRun.runId, reportPath)
656
+ if (reportResult.status === 200) {
657
+ console.log(`[TestRunAction] Report stored successfully for testRunId: ${testRun.runId}`)
658
+ } else {
659
+ console.warn(
660
+ `[TestRunAction] Failed to store report for testRunId: ${testRun.runId}: ${reportResult.error}`,
661
+ )
662
+ }
663
+ } catch (error) {
664
+ console.error(`[TestRunAction] Error storing report for testRunId: ${testRun.runId}:`, error)
665
+ // Don't fail the test run if report storage fails
666
+ }
667
+ } else {
668
+ console.warn(`[TestRunAction] No report path available for testRunId: ${testRun.runId}`)
669
+ }
670
+ })
671
+ .catch(async error => {
672
+ console.error(`[TestRunAction] Error executing test run for testRunId: ${testRun.runId}:`, error)
673
+
674
+ // Log error to Winston logger
675
+ logger.error(`Error executing test run: ${error instanceof Error ? error.message : String(error)}`)
676
+ if (error instanceof Error && error.stack) {
677
+ logger.error(error.stack)
678
+ }
679
+
680
+ // Close Winston logger
681
+ await closeLogger(logger).catch(err => {
682
+ console.error(`[TestRunAction] Error closing logger for testRunId: ${testRun.runId}:`, err)
683
+ })
684
+
685
+ // Check current status before updating - preserve CANCELLED status if already set
686
+ const currentTestRun = await prisma.testRun.findUnique({
687
+ where: { id: testRun.id },
688
+ select: { status: true, result: true },
689
+ })
690
+
691
+ // Only update to COMPLETED if not already CANCELLED or CANCELLING
692
+ if (
693
+ currentTestRun &&
694
+ currentTestRun.status !== TestRunStatus.CANCELLED &&
695
+ currentTestRun.status !== TestRunStatus.CANCELLING
696
+ ) {
697
+ // Update TestRun status to indicate failure
698
+ await prisma.testRun.update({
699
+ where: { id: testRun.id },
700
+ data: {
701
+ status: TestRunStatus.COMPLETED,
702
+ result: TestRunResult.FAILED,
703
+ completedAt: new Date(),
704
+ },
705
+ })
706
+ } else {
707
+ // Status is already CANCELLED or CANCELLING, just update completedAt if not set
708
+ if (currentTestRun && !currentTestRun.result) {
709
+ await prisma.testRun.update({
710
+ where: { id: testRun.id },
711
+ data: {
712
+ completedAt: new Date(),
713
+ },
714
+ })
715
+ }
716
+ }
717
+
718
+ // Clean up the server-side event listener
719
+ cleanupListener()
720
+ })
721
+ } catch (error) {
722
+ // Catch any synchronous errors
723
+ console.error(`[TestRunAction] Synchronous error calling executeTestRun for testRunId: ${testRun.runId}:`, error)
724
+ console.error(`[TestRunAction] Error stack:`, error instanceof Error ? error.stack : 'No stack trace')
725
+ // Note: If executeTestRun throws synchronously, the listener won't be set up, so no cleanup needed
726
+ }
727
+
728
+ return {
729
+ status: 200,
730
+ message: 'Test run created successfully',
731
+ data: { testRunId: testRun.runId, id: testRun.id },
732
+ }
733
+ } catch (error) {
734
+ console.error('Error creating test run:', error)
735
+ // Handle Prisma unique constraint error
736
+ if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === 'P2002') {
737
+ return {
738
+ status: 400,
739
+ error: 'A test run with this name already exists. Please choose a different name.',
740
+ }
741
+ }
742
+ return {
743
+ status: 500,
744
+ error: `Server error occurred: ${error instanceof Error ? error.message : 'Unknown error'}`,
745
+ }
746
+ }
747
+ }
748
+
749
+ /**
750
+ * Updates a test case status and result in a test run based on scenario completion
751
+ * @param testRunId - The test run ID (runId, not id)
752
+ * @param scenarioName - The scenario name from cucumber (format: "[Test Case Title] Description")
753
+ * @param status - The scenario status (passed, failed, skipped)
754
+ * @param tracePath - Optional trace path for failed scenarios
755
+ */
756
+ export async function updateTestRunTestCaseStatusAction(
757
+ testRunId: string,
758
+ scenarioName: string,
759
+ status: 'passed' | 'failed' | 'skipped' | 'unknown',
760
+ tracePath?: string,
761
+ ): Promise<ActionResponse> {
762
+ try {
763
+ // Find the test run by runId
764
+ const testRun = await prisma.testRun.findUnique({
765
+ where: { runId: testRunId },
766
+ include: {
767
+ testCases: {
768
+ include: {
769
+ testCase: true,
770
+ },
771
+ },
772
+ },
773
+ })
774
+
775
+ if (!testRun) {
776
+ return {
777
+ status: 404,
778
+ error: 'Test run not found',
779
+ }
780
+ }
781
+
782
+ // Parse scenario name to extract test case title
783
+ // Format: "[Test Case Title] Description" or just "Test Case Title"
784
+ let testCaseTitle: string | null = null
785
+
786
+ // Try to extract title from [brackets]
787
+ const bracketMatch = scenarioName.match(/^\[([^\]]+)\]/)
788
+ if (bracketMatch) {
789
+ testCaseTitle = bracketMatch[1].trim()
790
+ } else {
791
+ // If no brackets, use the full scenario name (might be just the title)
792
+ testCaseTitle = scenarioName.trim()
793
+ }
794
+
795
+ if (!testCaseTitle) {
796
+ return {
797
+ status: 400,
798
+ error: 'Could not extract test case title from scenario name',
799
+ }
800
+ }
801
+
802
+ // Find matching test case by title
803
+ const matchingTestCase = testRun.testCases.find(trtc => trtc.testCase.title === testCaseTitle)
804
+
805
+ if (!matchingTestCase) {
806
+ // This is expected when scenarios run without corresponding test cases (e.g., when filtered by tags)
807
+ // Return success status to indicate this was handled gracefully
808
+ console.log(
809
+ `[TestRunAction] No matching test case found for scenario: ${scenarioName} (extracted title: ${testCaseTitle}). This is expected when scenarios run without corresponding test cases.`,
810
+ )
811
+ return {
812
+ status: 200,
813
+ message: `Scenario "${scenarioName}" completed but has no corresponding test case in this test run (likely filtered by tags)`,
814
+ }
815
+ }
816
+
817
+ // Map status to TestRunTestCaseStatus and TestRunTestCaseResult
818
+ const testCaseStatus: TestRunTestCaseStatus = TestRunTestCaseStatus.COMPLETED
819
+ let testCaseResult: TestRunTestCaseResult
820
+
821
+ switch (status) {
822
+ case 'passed':
823
+ testCaseResult = TestRunTestCaseResult.PASSED
824
+ break
825
+ case 'failed':
826
+ testCaseResult = TestRunTestCaseResult.FAILED
827
+ break
828
+ case 'skipped':
829
+ testCaseResult = TestRunTestCaseResult.UNTESTED // Skipped is treated as untested
830
+ break
831
+ default:
832
+ testCaseResult = TestRunTestCaseResult.UNTESTED
833
+ }
834
+
835
+ // Update the TestRunTestCase
836
+ await prisma.testRunTestCase.update({
837
+ where: { id: matchingTestCase.id },
838
+ data: {
839
+ status: testCaseStatus,
840
+ result: testCaseResult,
841
+ tracePath: tracePath || null,
842
+ },
843
+ })
844
+
845
+ // Update test case metrics
846
+ try {
847
+ await updateTestCaseMetrics(
848
+ matchingTestCase.testCaseId,
849
+ testCaseResult,
850
+ testRun.completedAt || testRun.startedAt || new Date(),
851
+ )
852
+ } catch (error) {
853
+ console.error(`[TestRunAction] Error updating metrics for test case ${matchingTestCase.testCaseId}:`, error)
854
+ // Don't fail the action if metrics update fails
855
+ }
856
+
857
+ return {
858
+ status: 200,
859
+ message: 'Test case status updated successfully',
860
+ }
861
+ } catch (error) {
862
+ console.error(
863
+ `[TestRunAction] Error updating test case status for testRunId: ${testRunId}, scenario: ${scenarioName}:`,
864
+ error,
865
+ )
866
+ return {
867
+ status: 500,
868
+ error: `Server error occurred: ${error instanceof Error ? error.message : 'Unknown error'}`,
869
+ }
870
+ }
871
+ }
872
+
873
+ /**
874
+ * Checks if a trace viewer is currently running for a test case
875
+ * @param testRunId - The test run ID (runId, not id)
876
+ * @param testCaseId - The test case ID (TestRunTestCase id, not TestCase id)
877
+ * @returns ActionResponse with isRunning status
878
+ */
879
+ export async function checkTraceViewerStatusAction(testRunId: string, testCaseId: string): Promise<ActionResponse> {
880
+ try {
881
+ // Verify test run exists
882
+ const testRun = await prisma.testRun.findUnique({
883
+ where: { runId: testRunId },
884
+ include: {
885
+ testCases: {
886
+ where: { id: testCaseId },
887
+ },
888
+ },
889
+ })
890
+
891
+ if (!testRun) {
892
+ return {
893
+ status: 404,
894
+ error: 'Test run not found',
895
+ }
896
+ }
897
+
898
+ // Verify test case belongs to this test run
899
+ const testRunTestCase = testRun.testCases.find(tc => tc.id === testCaseId)
900
+ if (!testRunTestCase) {
901
+ return {
902
+ status: 404,
903
+ error: 'Test case not found in this test run',
904
+ }
905
+ }
906
+
907
+ // Check if trace viewer process is running
908
+ const processName = `trace-viewer-${testCaseId}`
909
+ const process = taskSpawner.getProcess(processName)
910
+ const isRunning = process?.isRunning ?? false
911
+
912
+ return {
913
+ status: 200,
914
+ data: {
915
+ isRunning,
916
+ processName: isRunning ? processName : null,
917
+ },
918
+ }
919
+ } catch (error) {
920
+ console.error(
921
+ `[TestRunAction] Error checking trace viewer status for testRunId: ${testRunId}, testCaseId: ${testCaseId}:`,
922
+ error,
923
+ )
924
+ return {
925
+ status: 500,
926
+ error: `Server error occurred: ${error instanceof Error ? error.message : 'Unknown error'}`,
927
+ }
928
+ }
929
+ }
930
+
931
+ /**
932
+ * Spawns Playwright trace viewer for a failed test case
933
+ * @param testRunId - The test run ID (runId, not id)
934
+ * @param testCaseId - The test case ID (TestRunTestCase id, not TestCase id)
935
+ * @returns ActionResponse indicating success or failure
936
+ */
937
+ export async function spawnTraceViewerAction(testRunId: string, testCaseId: string): Promise<ActionResponse> {
938
+ try {
939
+ // Verify test run exists
940
+ const testRun = await prisma.testRun.findUnique({
941
+ where: { runId: testRunId },
942
+ include: {
943
+ testCases: {
944
+ where: { id: testCaseId },
945
+ include: {
946
+ testCase: true,
947
+ },
948
+ },
949
+ },
950
+ })
951
+
952
+ if (!testRun) {
953
+ return {
954
+ status: 404,
955
+ error: 'Test run not found',
956
+ }
957
+ }
958
+
959
+ // Verify test case belongs to this test run
960
+ const testRunTestCase = testRun.testCases.find(tc => tc.id === testCaseId)
961
+ if (!testRunTestCase) {
962
+ return {
963
+ status: 404,
964
+ error: 'Test case not found in this test run',
965
+ }
966
+ }
967
+
968
+ // Get trace path from database
969
+ const tracePath = testRunTestCase.tracePath
970
+ if (!tracePath) {
971
+ return {
972
+ status: 400,
973
+ error: 'No trace path available for this test case',
974
+ }
975
+ }
976
+
977
+ // Validate trace file exists
978
+ try {
979
+ await fs.access(tracePath)
980
+ } catch {
981
+ return {
982
+ status: 404,
983
+ error: `Trace file not found at path: ${tracePath}`,
984
+ }
985
+ }
986
+
987
+ // Resolve absolute path if relative
988
+ const absoluteTracePath = path.isAbsolute(tracePath) ? tracePath : path.join(process.cwd(), tracePath)
989
+
990
+ // Spawn playwright show-trace command
991
+ // The process is self-closing when the user closes the trace viewer
992
+ const spawnedProcess = await taskSpawner.spawn('npx', ['playwright', 'show-trace', absoluteTracePath], {
993
+ streamLogs: true,
994
+ prefixLogs: true,
995
+ logPrefix: `trace-viewer-${testCaseId}`,
996
+ captureOutput: false, // No need to capture output for trace viewer
997
+ })
998
+
999
+ console.log(
1000
+ `[TestRunAction] Spawned trace viewer process for testCaseId: ${testCaseId}, tracePath: ${absoluteTracePath}`,
1001
+ )
1002
+
1003
+ return {
1004
+ status: 200,
1005
+ message: 'Trace viewer launched successfully',
1006
+ data: {
1007
+ processName: spawnedProcess.name,
1008
+ },
1009
+ }
1010
+ } catch (error) {
1011
+ console.error(
1012
+ `[TestRunAction] Error spawning trace viewer for testRunId: ${testRunId}, testCaseId: ${testCaseId}:`,
1013
+ error,
1014
+ )
1015
+ return {
1016
+ status: 500,
1017
+ error: `Server error occurred: ${error instanceof Error ? error.message : 'Unknown error'}`,
1018
+ }
1019
+ }
1020
+ }
1021
+
1022
+ export async function cancelTestRunAction(testRunId: string): Promise<ActionResponse> {
1023
+ try {
1024
+ const testRun = await prisma.testRun.findUnique({
1025
+ where: { runId: testRunId },
1026
+ })
1027
+ if (!testRun) {
1028
+ return {
1029
+ status: 404,
1030
+ error: 'Test run not found',
1031
+ }
1032
+ }
1033
+
1034
+ if (
1035
+ testRun.status !== TestRunStatus.RUNNING &&
1036
+ testRun.status !== TestRunStatus.QUEUED &&
1037
+ testRun.status !== TestRunStatus.CANCELLING
1038
+ ) {
1039
+ return {
1040
+ status: 400,
1041
+ error: 'Test run is not running, queued, or already being cancelled',
1042
+ }
1043
+ }
1044
+
1045
+ // If already cancelling, don't proceed
1046
+ if (testRun.status === TestRunStatus.CANCELLING) {
1047
+ return {
1048
+ status: 200,
1049
+ message: 'Test run cancellation is already in progress',
1050
+ }
1051
+ }
1052
+
1053
+ // Set status to CANCELLING immediately
1054
+ await prisma.testRun.update({
1055
+ where: { id: testRun.id },
1056
+ data: {
1057
+ status: TestRunStatus.CANCELLING,
1058
+ },
1059
+ })
1060
+
1061
+ const process = processManager.get(testRunId)
1062
+ console.log(`[TestRunAction] Process: ${JSON.stringify(process)}`)
1063
+
1064
+ if (!process) {
1065
+ console.warn(`[TestRunAction] No process found for testRunId: ${testRunId}`)
1066
+ await prisma.testRun.update({
1067
+ where: { id: testRun.id },
1068
+ data: {
1069
+ status: TestRunStatus.CANCELLED,
1070
+ result: TestRunResult.CANCELLED,
1071
+ completedAt: new Date(),
1072
+ },
1073
+ })
1074
+ return {
1075
+ status: 200,
1076
+ message: 'Test run cancelled successfully',
1077
+ }
1078
+ }
1079
+
1080
+ const killed = killTask(process.name, 'SIGTERM')
1081
+ console.log(`[TestRunAction] Killed: ${killed}`)
1082
+ if (!killed) {
1083
+ const forceKilled = killTask(process.name, 'SIGKILL')
1084
+ if (!forceKilled) {
1085
+ console.warn(`[TestRunAction] Failed to force kill process for testRunId: ${testRunId}`)
1086
+ }
1087
+ }
1088
+
1089
+ await prisma.testRun.update({
1090
+ where: { id: testRun.id },
1091
+ data: {
1092
+ status: TestRunStatus.CANCELLED,
1093
+ result: TestRunResult.CANCELLED,
1094
+ completedAt: new Date(),
1095
+ },
1096
+ })
1097
+
1098
+ await prisma.testRunTestCase.updateMany({
1099
+ where: {
1100
+ testRunId: testRun.id,
1101
+ status: {
1102
+ in: [TestRunTestCaseStatus.PENDING, TestRunTestCaseStatus.RUNNING],
1103
+ },
1104
+ },
1105
+ data: {
1106
+ status: TestRunTestCaseStatus.CANCELLED,
1107
+ result: TestRunTestCaseResult.UNTESTED,
1108
+ },
1109
+ })
1110
+
1111
+ revalidatePath('/test-runs')
1112
+ revalidatePath(`/test-runs/${testRunId}`)
1113
+
1114
+ return {
1115
+ status: 200,
1116
+ message: 'Test run stopped successfully',
1117
+ }
1118
+ } catch (error) {
1119
+ console.error(`[TestRunAction] Error stopping test run ${testRunId}:`, error)
1120
+ return {
1121
+ status: 500,
1122
+ error: `Server error occurred: ${error instanceof Error ? error.message : 'Unknown error'}`,
1123
+ }
1124
+ }
1125
+ }
1126
+
1127
+ export async function getMostRecentTestRunAction(): Promise<ActionResponse> {
1128
+ try {
1129
+ const testRun = await prisma.testRun.findFirst({
1130
+ orderBy: { completedAt: 'desc' },
1131
+ where: {
1132
+ completedAt: { not: null },
1133
+ status: TestRunStatus.COMPLETED,
1134
+ },
1135
+ include: {
1136
+ testCases: {
1137
+ include: {
1138
+ testCase: {
1139
+ include: {
1140
+ metrics: true, // Include metrics if needed
1141
+ },
1142
+ },
1143
+ },
1144
+ },
1145
+ environment: true,
1146
+ tags: true,
1147
+ },
1148
+ })
1149
+
1150
+ if (!testRun) {
1151
+ return {
1152
+ status: 404,
1153
+ error: 'No completed test run found',
1154
+ }
1155
+ }
1156
+
1157
+ return {
1158
+ status: 200,
1159
+ data: testRun,
1160
+ }
1161
+ } catch (error) {
1162
+ return {
1163
+ status: 500,
1164
+ error: `Server error occurred: ${error}`,
1165
+ }
1166
+ }
1167
+ }
1168
+
1169
+ /**
1170
+ * Check if a test run name is unique
1171
+ */
1172
+ export async function checkTestRunNameUniqueAction(name: string, excludeId?: string): Promise<ActionResponse> {
1173
+ try {
1174
+ const nameExists = await checkUniqueName(name, excludeId)
1175
+ return {
1176
+ status: 200,
1177
+ data: { isUnique: !nameExists },
1178
+ }
1179
+ } catch (error) {
1180
+ return {
1181
+ status: 500,
1182
+ error: `Server error occurred: ${error}`,
1183
+ }
1184
+ }
1185
+ }