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.
- package/README.md +52 -0
- package/package.json +63 -0
- package/templates/default/.env.example +2 -0
- package/templates/default/README.md +51 -0
- package/templates/default/appraise.config.json +4 -0
- package/templates/default/components.json +24 -0
- package/templates/default/eslint.config.mjs +15 -0
- package/templates/default/next-env.d.ts +6 -0
- package/templates/default/next.config.ts +7 -0
- package/templates/default/package-lock.json +14321 -0
- package/templates/default/package.json +124 -0
- package/templates/default/postcss.config.mjs +8 -0
- package/templates/default/prisma/migrations/20251026202316_migrate_back_to_sqlite/migration.sql +257 -0
- package/templates/default/prisma/migrations/20251104113456_add_type_for_template_step_groups/migration.sql +16 -0
- package/templates/default/prisma/migrations/20251104170946_add_tags_to_test_suite_and_test_case/migration.sql +27 -0
- package/templates/default/prisma/migrations/20251112190024_add_cascade_delete_to_test_run_test_case/migration.sql +17 -0
- package/templates/default/prisma/migrations/20251113181100_add_test_run_log/migration.sql +12 -0
- package/templates/default/prisma/migrations/20251119191838_add_tag_type/migration.sql +28 -0
- package/templates/default/prisma/migrations/20251121164059_add_conflict_resolution/migration.sql +12 -0
- package/templates/default/prisma/migrations/20251130190737_add_trace_path_to_test_run_test_case/migration.sql +2 -0
- package/templates/default/prisma/migrations/20251213074835_add_log_path_to_test_run/migration.sql +2 -0
- package/templates/default/prisma/migrations/20251213183952_add_name_property_for_the_test_run_entities/migration.sql +30 -0
- package/templates/default/prisma/migrations/20251223183400_add_report_model_to_db_schema/migration.sql +10 -0
- package/templates/default/prisma/migrations/20251223183637_add_report_test_case_entity_for_storing_test_results_for_individual_test_cases/migration.sql +10 -0
- package/templates/default/prisma/migrations/20251224083549_add_comprehensive_report_storage/migration.sql +108 -0
- package/templates/default/prisma/migrations/20251229194422_migrate_duration_to_string/migration.sql +55 -0
- package/templates/default/prisma/migrations/20251230124637_add_unique_constraint_to_test_run_name/migration.sql +27 -0
- package/templates/default/prisma/migrations/20260115094436_add_dashboard_metrics/migration.sql +59 -0
- package/templates/default/prisma/migrations/20260127172022_add_cascade_delete_to_step_parameters/migration.sql +34 -0
- package/templates/default/prisma/migrations/migration_lock.toml +3 -0
- package/templates/default/prisma/schema.prisma +554 -0
- package/templates/default/public/favicon.ico +0 -0
- package/templates/default/public/file.svg +1 -0
- package/templates/default/public/globe.svg +1 -0
- package/templates/default/public/next.svg +1 -0
- package/templates/default/public/vercel.svg +1 -0
- package/templates/default/public/window.svg +1 -0
- package/templates/default/scripts/regenerate-features.ts +94 -0
- package/templates/default/scripts/setup-env.ts +19 -0
- package/templates/default/scripts/sync-all.ts +341 -0
- package/templates/default/scripts/sync-environments.ts +323 -0
- package/templates/default/scripts/sync-locator-groups.ts +413 -0
- package/templates/default/scripts/sync-locators.ts +402 -0
- package/templates/default/scripts/sync-modules.ts +349 -0
- package/templates/default/scripts/sync-tags.ts +292 -0
- package/templates/default/scripts/sync-template-step-groups.ts +399 -0
- package/templates/default/scripts/sync-template-steps.ts +806 -0
- package/templates/default/scripts/sync-test-cases.ts +905 -0
- package/templates/default/scripts/sync-test-suites.ts +411 -0
- package/templates/default/src/actions/conflict/conflict.action.ts +33 -0
- package/templates/default/src/actions/dashboard/dashboard-actions.ts +241 -0
- package/templates/default/src/actions/environments/environment-actions.ts +205 -0
- package/templates/default/src/actions/locator/locator-actions.ts +547 -0
- package/templates/default/src/actions/locator-groups/locator-group-actions.ts +344 -0
- package/templates/default/src/actions/modules/module-actions.ts +133 -0
- package/templates/default/src/actions/reports/report-actions.ts +614 -0
- package/templates/default/src/actions/review/review-actions.ts +147 -0
- package/templates/default/src/actions/tags/tag-actions.ts +104 -0
- package/templates/default/src/actions/template-step/template-step-actions.ts +332 -0
- package/templates/default/src/actions/template-step-group/template-step-group-actions.ts +278 -0
- package/templates/default/src/actions/template-test-case/template-test-case-actions.ts +238 -0
- package/templates/default/src/actions/test-case/test-case-actions.ts +419 -0
- package/templates/default/src/actions/test-run/test-run-actions.ts +1185 -0
- package/templates/default/src/actions/test-suite/test-suite-actions.ts +253 -0
- package/templates/default/src/actions/user/user-actions.ts +13 -0
- package/templates/default/src/app/(base)/environments/create/page.tsx +28 -0
- package/templates/default/src/app/(base)/environments/environment-form.tsx +219 -0
- package/templates/default/src/app/(base)/environments/environment-table-columns.tsx +96 -0
- package/templates/default/src/app/(base)/environments/environment-table.tsx +24 -0
- package/templates/default/src/app/(base)/environments/modify/[id]/page.tsx +46 -0
- package/templates/default/src/app/(base)/environments/page.tsx +59 -0
- package/templates/default/src/app/(base)/layout.tsx +10 -0
- package/templates/default/src/app/(base)/locator-groups/create/page.tsx +44 -0
- package/templates/default/src/app/(base)/locator-groups/locator-group-form.tsx +215 -0
- package/templates/default/src/app/(base)/locator-groups/locator-group-table-columns.tsx +77 -0
- package/templates/default/src/app/(base)/locator-groups/locator-group-table.tsx +28 -0
- package/templates/default/src/app/(base)/locator-groups/modify/[id]/page.tsx +46 -0
- package/templates/default/src/app/(base)/locator-groups/page.tsx +61 -0
- package/templates/default/src/app/(base)/locators/create/page.tsx +38 -0
- package/templates/default/src/app/(base)/locators/locator-form.tsx +163 -0
- package/templates/default/src/app/(base)/locators/locator-table-columns.tsx +90 -0
- package/templates/default/src/app/(base)/locators/locator-table.tsx +28 -0
- package/templates/default/src/app/(base)/locators/modify/[id]/page.tsx +45 -0
- package/templates/default/src/app/(base)/locators/page.tsx +65 -0
- package/templates/default/src/app/(base)/locators/sync-locators-button.tsx +66 -0
- package/templates/default/src/app/(base)/modules/create/page.tsx +34 -0
- package/templates/default/src/app/(base)/modules/modify/[id]/page.tsx +46 -0
- package/templates/default/src/app/(base)/modules/module-form.tsx +126 -0
- package/templates/default/src/app/(base)/modules/module-table-columns.tsx +85 -0
- package/templates/default/src/app/(base)/modules/module-table.tsx +24 -0
- package/templates/default/src/app/(base)/modules/page.tsx +59 -0
- package/templates/default/src/app/(base)/reports/[id]/page.tsx +517 -0
- package/templates/default/src/app/(base)/reports/duration-chart.tsx +33 -0
- package/templates/default/src/app/(base)/reports/feature-chart.tsx +78 -0
- package/templates/default/src/app/(base)/reports/overview-chart.tsx +46 -0
- package/templates/default/src/app/(base)/reports/page.tsx +98 -0
- package/templates/default/src/app/(base)/reports/report-metric-card.tsx +16 -0
- package/templates/default/src/app/(base)/reports/report-table-columns.tsx +189 -0
- package/templates/default/src/app/(base)/reports/report-table.tsx +72 -0
- package/templates/default/src/app/(base)/reports/report-view-table-columns.tsx +131 -0
- package/templates/default/src/app/(base)/reports/report-view-table.tsx +82 -0
- package/templates/default/src/app/(base)/reports/test-cases/page.tsx +42 -0
- package/templates/default/src/app/(base)/reports/test-cases/test-cases-metric-table-columns.tsx +115 -0
- package/templates/default/src/app/(base)/reports/test-cases/test-cases-metric-table.tsx +27 -0
- package/templates/default/src/app/(base)/reports/test-suites/page.tsx +42 -0
- package/templates/default/src/app/(base)/reports/test-suites/test-suites-metric-table-columns.tsx +79 -0
- package/templates/default/src/app/(base)/reports/test-suites/test-suites-metric-table.tsx +27 -0
- package/templates/default/src/app/(base)/reports/view-logs-button.tsx +60 -0
- package/templates/default/src/app/(base)/reviews/create/page.tsx +26 -0
- package/templates/default/src/app/(base)/reviews/created-reviews-table.tsx +15 -0
- package/templates/default/src/app/(base)/reviews/modify/[id]/page.tsx +26 -0
- package/templates/default/src/app/(base)/reviews/page.tsx +26 -0
- package/templates/default/src/app/(base)/reviews/review/[id]/page.tsx +26 -0
- package/templates/default/src/app/(base)/reviews/review-form.tsx +11 -0
- package/templates/default/src/app/(base)/reviews/review-table-by-creator-columns.tsx +9 -0
- package/templates/default/src/app/(base)/reviews/review-table-by-reviewer-columns.tsx +9 -0
- package/templates/default/src/app/(base)/reviews/reviewer-reviews-table.tsx +15 -0
- package/templates/default/src/app/(base)/tags/create/page.tsx +39 -0
- package/templates/default/src/app/(base)/tags/modify/[id]/page.tsx +50 -0
- package/templates/default/src/app/(base)/tags/page.tsx +58 -0
- package/templates/default/src/app/(base)/tags/tag-form.tsx +147 -0
- package/templates/default/src/app/(base)/tags/tag-table-columns.tsx +63 -0
- package/templates/default/src/app/(base)/tags/tag-table.tsx +29 -0
- package/templates/default/src/app/(base)/template-step-groups/create/page.tsx +28 -0
- package/templates/default/src/app/(base)/template-step-groups/modify/[id]/page.tsx +45 -0
- package/templates/default/src/app/(base)/template-step-groups/page.tsx +60 -0
- package/templates/default/src/app/(base)/template-step-groups/template-step-group-form.tsx +167 -0
- package/templates/default/src/app/(base)/template-step-groups/template-step-group-table-columns.tsx +89 -0
- package/templates/default/src/app/(base)/template-step-groups/template-step-group-table.tsx +32 -0
- package/templates/default/src/app/(base)/template-steps/create/page.tsx +37 -0
- package/templates/default/src/app/(base)/template-steps/modify/[id]/page.tsx +49 -0
- package/templates/default/src/app/(base)/template-steps/page.tsx +59 -0
- package/templates/default/src/app/(base)/template-steps/paramChip.tsx +213 -0
- package/templates/default/src/app/(base)/template-steps/template-step-form.tsx +384 -0
- package/templates/default/src/app/(base)/template-steps/template-step-table-columns.tsx +158 -0
- package/templates/default/src/app/(base)/template-steps/template-step-table.tsx +24 -0
- package/templates/default/src/app/(base)/template-test-cases/create/page.tsx +56 -0
- package/templates/default/src/app/(base)/template-test-cases/modify/[id]/page.tsx +89 -0
- package/templates/default/src/app/(base)/template-test-cases/page.tsx +58 -0
- package/templates/default/src/app/(base)/template-test-cases/template-test-case-flow.tsx +84 -0
- package/templates/default/src/app/(base)/template-test-cases/template-test-case-form.tsx +262 -0
- package/templates/default/src/app/(base)/template-test-cases/template-test-case-table-columns.tsx +76 -0
- package/templates/default/src/app/(base)/template-test-cases/template-test-case-table.tsx +32 -0
- package/templates/default/src/app/(base)/test-cases/create/page.tsx +76 -0
- package/templates/default/src/app/(base)/test-cases/create-from-template/generate/[id]/page.tsx +96 -0
- package/templates/default/src/app/(base)/test-cases/create-from-template/page.tsx +38 -0
- package/templates/default/src/app/(base)/test-cases/create-from-template/template-selection-form.tsx +73 -0
- package/templates/default/src/app/(base)/test-cases/modify/[id]/page.tsx +106 -0
- package/templates/default/src/app/(base)/test-cases/page.tsx +60 -0
- package/templates/default/src/app/(base)/test-cases/test-case-flow.tsx +82 -0
- package/templates/default/src/app/(base)/test-cases/test-case-form.tsx +395 -0
- package/templates/default/src/app/(base)/test-cases/test-case-table-columns.tsx +90 -0
- package/templates/default/src/app/(base)/test-cases/test-case-table.tsx +35 -0
- package/templates/default/src/app/(base)/test-runs/[id]/page.tsx +56 -0
- package/templates/default/src/app/(base)/test-runs/create/page.tsx +47 -0
- package/templates/default/src/app/(base)/test-runs/page.tsx +60 -0
- package/templates/default/src/app/(base)/test-runs/test-run-form.tsx +512 -0
- package/templates/default/src/app/(base)/test-runs/test-run-table-columns.tsx +229 -0
- package/templates/default/src/app/(base)/test-runs/test-run-table.tsx +127 -0
- package/templates/default/src/app/(base)/test-suites/create/page.tsx +45 -0
- package/templates/default/src/app/(base)/test-suites/modify/[id]/page.tsx +55 -0
- package/templates/default/src/app/(base)/test-suites/page.tsx +82 -0
- package/templates/default/src/app/(base)/test-suites/test-suite-form.tsx +269 -0
- package/templates/default/src/app/(base)/test-suites/test-suite-table-columns.tsx +97 -0
- package/templates/default/src/app/(base)/test-suites/test-suite-table.tsx +29 -0
- package/templates/default/src/app/(dashboard-components)/app-drawer.tsx +187 -0
- package/templates/default/src/app/(dashboard-components)/data-card-grid.tsx +13 -0
- package/templates/default/src/app/(dashboard-components)/data-card.tsx +27 -0
- package/templates/default/src/app/(dashboard-components)/execution-health-panel.tsx +57 -0
- package/templates/default/src/app/(dashboard-components)/ongoing-test-runs-card.tsx +87 -0
- package/templates/default/src/app/(dashboard-components)/quick-actions-drawer.tsx +45 -0
- package/templates/default/src/app/api/test-runs/[runId]/download/route.ts +133 -0
- package/templates/default/src/app/api/test-runs/[runId]/logs/route.ts +420 -0
- package/templates/default/src/app/api/test-runs/[runId]/trace/[testCaseId]/route.ts +146 -0
- package/templates/default/src/app/favicon.ico +0 -0
- package/templates/default/src/app/globals.css +147 -0
- package/templates/default/src/app/layout.tsx +171 -0
- package/templates/default/src/app/page.tsx +64 -0
- package/templates/default/src/assets/icons/empty-tube.tsx +23 -0
- package/templates/default/src/assets/icons/tube-plus.tsx +29 -0
- package/templates/default/src/components/base-node.tsx +21 -0
- package/templates/default/src/components/chart/pie-chart.tsx +73 -0
- package/templates/default/src/components/data-extraction/locator-inspector.tsx +460 -0
- package/templates/default/src/components/data-state/empty-state.tsx +40 -0
- package/templates/default/src/components/data-visualization/info-card.tsx +70 -0
- package/templates/default/src/components/data-visualization/info-grid.tsx +22 -0
- package/templates/default/src/components/devtools/providers.tsx +13 -0
- package/templates/default/src/components/diagram/button-edge.tsx +54 -0
- package/templates/default/src/components/diagram/dynamic-parameters.tsx +438 -0
- package/templates/default/src/components/diagram/edit-header-option.tsx +36 -0
- package/templates/default/src/components/diagram/flow-diagram.tsx +470 -0
- package/templates/default/src/components/diagram/node-form.tsx +262 -0
- package/templates/default/src/components/diagram/options-header-node.tsx +57 -0
- package/templates/default/src/components/diagram/template-step-combobox.tsx +155 -0
- package/templates/default/src/components/form/error-message.tsx +7 -0
- package/templates/default/src/components/kokonutui/smooth-tab.tsx +453 -0
- package/templates/default/src/components/loading-skeleton/data-table/data-table-skeleton.tsx +30 -0
- package/templates/default/src/components/loading-skeleton/form/button-skeleton.tsx +8 -0
- package/templates/default/src/components/loading-skeleton/form/icon-button-skeleton.tsx +8 -0
- package/templates/default/src/components/loading-skeleton/form/text-input-skeleton.tsx +8 -0
- package/templates/default/src/components/loading-skeleton/visualization/table-skeleton.tsx +14 -0
- package/templates/default/src/components/logo.tsx +15 -0
- package/templates/default/src/components/navigation/command-badge.tsx +34 -0
- package/templates/default/src/components/navigation/command-chain-input.tsx +51 -0
- package/templates/default/src/components/navigation/entity-search-command.tsx +116 -0
- package/templates/default/src/components/navigation/nav-card.tsx +31 -0
- package/templates/default/src/components/navigation/nav-command.tsx +508 -0
- package/templates/default/src/components/navigation/nav-link.tsx +60 -0
- package/templates/default/src/components/navigation/nav-menu-card-deck.tsx +112 -0
- package/templates/default/src/components/node-header.tsx +159 -0
- package/templates/default/src/components/reports/test-case-logs-modal.tsx +253 -0
- package/templates/default/src/components/table/table-actions.tsx +172 -0
- package/templates/default/src/components/test-run/download-logs-button.tsx +99 -0
- package/templates/default/src/components/test-run/log-viewer.tsx +445 -0
- package/templates/default/src/components/test-run/test-run-details.tsx +611 -0
- package/templates/default/src/components/test-run/test-run-header.tsx +149 -0
- package/templates/default/src/components/test-run/view-report-button.tsx +102 -0
- package/templates/default/src/components/theme/mode-toggle.tsx +54 -0
- package/templates/default/src/components/theme/theme-provider.tsx +8 -0
- package/templates/default/src/components/typography/page-header-subtitle.tsx +7 -0
- package/templates/default/src/components/typography/page-header.tsx +7 -0
- package/templates/default/src/components/ui/alert-dialog.tsx +106 -0
- package/templates/default/src/components/ui/alert.tsx +43 -0
- package/templates/default/src/components/ui/avatar.tsx +40 -0
- package/templates/default/src/components/ui/badge.tsx +29 -0
- package/templates/default/src/components/ui/button.tsx +47 -0
- package/templates/default/src/components/ui/calendar.tsx +158 -0
- package/templates/default/src/components/ui/card.tsx +43 -0
- package/templates/default/src/components/ui/chart.tsx +369 -0
- package/templates/default/src/components/ui/checkbox.tsx +28 -0
- package/templates/default/src/components/ui/command.tsx +135 -0
- package/templates/default/src/components/ui/data-table-column-header.tsx +61 -0
- package/templates/default/src/components/ui/data-table-pagination.tsx +87 -0
- package/templates/default/src/components/ui/data-table-view-options.tsx +50 -0
- package/templates/default/src/components/ui/data-table.tsx +267 -0
- package/templates/default/src/components/ui/dialog.tsx +97 -0
- package/templates/default/src/components/ui/dropdown-menu.tsx +182 -0
- package/templates/default/src/components/ui/empty.tsx +104 -0
- package/templates/default/src/components/ui/input.tsx +22 -0
- package/templates/default/src/components/ui/kbd.tsx +28 -0
- package/templates/default/src/components/ui/label.tsx +19 -0
- package/templates/default/src/components/ui/loading.tsx +12 -0
- package/templates/default/src/components/ui/multi-select-with-preview.tsx +116 -0
- package/templates/default/src/components/ui/multi-select.tsx +142 -0
- package/templates/default/src/components/ui/navigation-menu.tsx +120 -0
- package/templates/default/src/components/ui/popover.tsx +33 -0
- package/templates/default/src/components/ui/progress.tsx +25 -0
- package/templates/default/src/components/ui/radio-group.tsx +44 -0
- package/templates/default/src/components/ui/scroll-area.tsx +40 -0
- package/templates/default/src/components/ui/select.tsx +144 -0
- package/templates/default/src/components/ui/separator.tsx +22 -0
- package/templates/default/src/components/ui/skeleton.tsx +7 -0
- package/templates/default/src/components/ui/table.tsx +76 -0
- package/templates/default/src/components/ui/tabs.tsx +55 -0
- package/templates/default/src/components/ui/textarea.tsx +21 -0
- package/templates/default/src/components/ui/toast.tsx +113 -0
- package/templates/default/src/components/ui/toaster.tsx +26 -0
- package/templates/default/src/components/ui/tooltip.tsx +32 -0
- package/templates/default/src/components/user-prompt/delete-prompt.tsx +87 -0
- package/templates/default/src/config/db-config.ts +10 -0
- package/templates/default/src/constants/form-opts/diagram/node-form.ts +30 -0
- package/templates/default/src/constants/form-opts/environment-form-opts.ts +24 -0
- package/templates/default/src/constants/form-opts/locator-form-opts.ts +20 -0
- package/templates/default/src/constants/form-opts/locator-group-form-opts.ts +28 -0
- package/templates/default/src/constants/form-opts/module-form-opts.ts +21 -0
- package/templates/default/src/constants/form-opts/review-form-opts.ts +23 -0
- package/templates/default/src/constants/form-opts/tag-form-opts.ts +42 -0
- package/templates/default/src/constants/form-opts/template-selection-form-opts.ts +16 -0
- package/templates/default/src/constants/form-opts/template-step-group-form-opts.ts +24 -0
- package/templates/default/src/constants/form-opts/template-test-case-form-opts.ts +39 -0
- package/templates/default/src/constants/form-opts/template-test-step-form-opts.ts +36 -0
- package/templates/default/src/constants/form-opts/test-case-form-opts.ts +43 -0
- package/templates/default/src/constants/form-opts/test-run-form-opts.ts +31 -0
- package/templates/default/src/constants/form-opts/test-suite-form-opts.ts +24 -0
- package/templates/default/src/hooks/use-toast.ts +187 -0
- package/templates/default/src/lib/bidirectional-sync.ts +432 -0
- package/templates/default/src/lib/database-sync.ts +531 -0
- package/templates/default/src/lib/environment-file-utils.ts +221 -0
- package/templates/default/src/lib/feature-file-generator.ts +411 -0
- package/templates/default/src/lib/gherkin-parser.ts +259 -0
- package/templates/default/src/lib/locator-group-file-utils.ts +370 -0
- package/templates/default/src/lib/metrics/metric-calculator.ts +613 -0
- package/templates/default/src/lib/module-hierarchy-builder.ts +205 -0
- package/templates/default/src/lib/path-helpers/module-path.ts +71 -0
- package/templates/default/src/lib/test-case-utils.ts +6 -0
- package/templates/default/src/lib/test-run/log-formatter.ts +83 -0
- package/templates/default/src/lib/test-run/process-manager.ts +191 -0
- package/templates/default/src/lib/test-run/report-parser.ts +316 -0
- package/templates/default/src/lib/test-run/test-run-executor.ts +144 -0
- package/templates/default/src/lib/test-run/winston-logger.ts +95 -0
- package/templates/default/src/lib/transformers/gherkin-converter.ts +42 -0
- package/templates/default/src/lib/transformers/key-to-icon-transformer.tsx +95 -0
- package/templates/default/src/lib/transformers/template-test-case-converter.ts +160 -0
- package/templates/default/src/lib/utils/node-param-validation.ts +81 -0
- package/templates/default/src/lib/utils/template-step-file-generator.ts +167 -0
- package/templates/default/src/lib/utils/template-step-file-manager-intelligent.ts +723 -0
- package/templates/default/src/lib/utils/template-step-file-manager.ts +166 -0
- package/templates/default/src/lib/utils.ts +31 -0
- package/templates/default/src/tests/config/environments/environments.json +14 -0
- package/templates/default/src/tests/config/executor/world.ts +41 -0
- package/templates/default/src/tests/executor.ts +80 -0
- package/templates/default/src/tests/hooks/hooks.ts +99 -0
- package/templates/default/src/tests/mapping/locator-map.json +1 -0
- package/templates/default/src/tests/steps/actions/click.step.ts +62 -0
- package/templates/default/src/tests/steps/actions/hover.step.ts +31 -0
- package/templates/default/src/tests/steps/actions/input.step.ts +149 -0
- package/templates/default/src/tests/steps/actions/navigation.step.ts +72 -0
- package/templates/default/src/tests/steps/actions/random_data.step.ts +146 -0
- package/templates/default/src/tests/steps/actions/store.step.ts +90 -0
- package/templates/default/src/tests/steps/actions/wait.step.ts +107 -0
- package/templates/default/src/tests/steps/validations/active_state_assertion.step.ts +34 -0
- package/templates/default/src/tests/steps/validations/navigation_assertion.step.ts +23 -0
- package/templates/default/src/tests/steps/validations/text_assertion.step.ts +111 -0
- package/templates/default/src/tests/steps/validations/visibility_assertion.step.ts +30 -0
- package/templates/default/src/tests/support/parameter-types.ts +12 -0
- package/templates/default/src/tests/utils/cache.util.ts +260 -0
- package/templates/default/src/tests/utils/cli.util.ts +177 -0
- package/templates/default/src/tests/utils/environment.util.ts +65 -0
- package/templates/default/src/tests/utils/locator.util.ts +248 -0
- package/templates/default/src/tests/utils/random-data.util.ts +45 -0
- package/templates/default/src/tests/utils/spawner.util.ts +617 -0
- package/templates/default/src/types/diagram/diagram.ts +34 -0
- package/templates/default/src/types/diagram/template-step.ts +11 -0
- package/templates/default/src/types/executor/browser.type.ts +1 -0
- package/templates/default/src/types/form/actionHandler.ts +6 -0
- package/templates/default/src/types/locator/locator.type.ts +11 -0
- package/templates/default/src/types/step/step.type.ts +1 -0
- package/templates/default/src/types/table/data-table.ts +6 -0
- package/templates/default/tailwind.config.ts +62 -0
- 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
|
+
}
|