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,87 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { Button } from '@/components/ui/button'
|
|
4
|
+
import { Card, CardContent, CardTitle, CardHeader } from '@/components/ui/card'
|
|
5
|
+
import { ExternalLink } from 'lucide-react'
|
|
6
|
+
import { useRouter } from 'next/navigation'
|
|
7
|
+
import { useEffect, useState, useRef } from 'react'
|
|
8
|
+
import { getRunningTestRunsCountAction } from '@/actions/dashboard/dashboard-actions'
|
|
9
|
+
|
|
10
|
+
interface OngoingTestRunsCardProps {
|
|
11
|
+
initialCount: number
|
|
12
|
+
link: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default function OngoingTestRunsCard({ initialCount, link }: OngoingTestRunsCardProps) {
|
|
16
|
+
const router = useRouter()
|
|
17
|
+
const [count, setCount] = useState(initialCount)
|
|
18
|
+
const pollingRef = useRef<NodeJS.Timeout | null>(null)
|
|
19
|
+
|
|
20
|
+
// Poll for count updates while there are ongoing test runs
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
// Clear any existing polling interval
|
|
23
|
+
if (pollingRef.current) {
|
|
24
|
+
clearInterval(pollingRef.current)
|
|
25
|
+
pollingRef.current = null
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// If no ongoing test runs, don't poll
|
|
29
|
+
if (count === 0) {
|
|
30
|
+
return
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
pollingRef.current = setInterval(async () => {
|
|
34
|
+
try {
|
|
35
|
+
const { data, error } = await getRunningTestRunsCountAction()
|
|
36
|
+
if (error) {
|
|
37
|
+
console.error('Error polling running test runs count:', error)
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const newCount = data as number
|
|
42
|
+
setCount(newCount)
|
|
43
|
+
|
|
44
|
+
// If count reaches 0, stop polling
|
|
45
|
+
if (newCount === 0 && pollingRef.current) {
|
|
46
|
+
clearInterval(pollingRef.current)
|
|
47
|
+
pollingRef.current = null
|
|
48
|
+
}
|
|
49
|
+
} catch (error) {
|
|
50
|
+
console.error('Error polling running test runs count:', error)
|
|
51
|
+
}
|
|
52
|
+
}, 2000) // Poll every 2 seconds
|
|
53
|
+
|
|
54
|
+
return () => {
|
|
55
|
+
if (pollingRef.current) {
|
|
56
|
+
clearInterval(pollingRef.current)
|
|
57
|
+
pollingRef.current = null
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}, [count])
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<Card className="h-fit border-gray-600/10 bg-gray-600/10 min-w-40">
|
|
64
|
+
<CardHeader className="flex items-center justify-between flex-row p-2">
|
|
65
|
+
<CardTitle className={`text-xs font-normal ${count > 0 ? 'text-primary' : 'text-gray-400'}`}>
|
|
66
|
+
Ongoing Test Runs
|
|
67
|
+
</CardTitle>
|
|
68
|
+
<div className="flex items-center gap-2">
|
|
69
|
+
<Button
|
|
70
|
+
variant="outline"
|
|
71
|
+
className="text-primary hover:text-primary/80 px-2 py-1 bg-inherit border-gray-600/15 hover:bg-emerald-400/10"
|
|
72
|
+
disabled={count === 0}
|
|
73
|
+
onClick={() => router.push(link)}
|
|
74
|
+
size="sm"
|
|
75
|
+
>
|
|
76
|
+
<ExternalLink className="h-4 w-4" />
|
|
77
|
+
</Button>
|
|
78
|
+
</div>
|
|
79
|
+
</CardHeader>
|
|
80
|
+
<CardContent className="h-full px-2 py-1">
|
|
81
|
+
<div className={`text-2xl h-full flex items-center font-bold ${count > 0 ? 'text-primary' : 'text-gray-400'}`}>
|
|
82
|
+
{count}
|
|
83
|
+
</div>
|
|
84
|
+
</CardContent>
|
|
85
|
+
</Card>
|
|
86
|
+
)
|
|
87
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
import { Button } from "@/components/ui/button";
|
|
3
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
4
|
+
import { Blocks, FileCheck, LayoutTemplate, ListChecks, TestTubeDiagonal, TestTubes } from "lucide-react";
|
|
5
|
+
import { useRouter } from "next/navigation";
|
|
6
|
+
|
|
7
|
+
export default function QuickActionsDrawer() {
|
|
8
|
+
const router = useRouter()
|
|
9
|
+
return (
|
|
10
|
+
<Card id="container" className="w-fit border-gray-600/10 bg-gray-600/10 h-fit">
|
|
11
|
+
<CardHeader id="header">
|
|
12
|
+
<CardTitle className="text-primary">Quick Actions</CardTitle>
|
|
13
|
+
<CardDescription>Quickly create new entities to get started</CardDescription>
|
|
14
|
+
</CardHeader>
|
|
15
|
+
<CardContent id="content">
|
|
16
|
+
<div className="grid grid-cols-5 gap-4">
|
|
17
|
+
<Button variant="outline" className="flex flex-col items-center gap-2 h-fit [&_svg]:!h-6 [&_svg]:!w-6 bg-emerald-500/10 hover:bg-emerald-500/25 text-emerald-500 hover:text-emerald-400 border-none" onClick={() => router.push('/test-suites/create')}>
|
|
18
|
+
<TestTubes />
|
|
19
|
+
<span className="text-xs font-medium">Create Suite</span>
|
|
20
|
+
</Button>
|
|
21
|
+
<Button variant="outline" className="flex flex-col items-center gap-2 h-fit [&_svg]:!h-6 [&_svg]:!w-6 bg-emerald-500/10 hover:bg-emerald-500/25 text-emerald-500 hover:text-emerald-400 border-none" onClick={() => router.push('/test-cases/create')}>
|
|
22
|
+
<TestTubeDiagonal />
|
|
23
|
+
<span className="text-xs font-medium">Create Test</span>
|
|
24
|
+
</Button>
|
|
25
|
+
<Button variant="outline" className="flex flex-col items-center gap-2 h-fit [&_svg]:!h-6 [&_svg]:!w-6 bg-emerald-500/10 hover:bg-emerald-500/25 text-emerald-500 hover:text-emerald-400 border-none" onClick={() => router.push('/template-steps/create')}>
|
|
26
|
+
<LayoutTemplate />
|
|
27
|
+
<span className="text-xs font-medium">Create Step</span>
|
|
28
|
+
</Button>
|
|
29
|
+
<Button variant="outline" className="flex flex-col items-center gap-2 h-fit [&_svg]:!h-6 [&_svg]:!w-6 bg-emerald-500/10 hover:bg-emerald-500/25 text-emerald-500 hover:text-emerald-400 border-none" onClick={() => router.push('/test-runs/create')}>
|
|
30
|
+
<ListChecks />
|
|
31
|
+
<span className="text-xs font-medium">Create Run</span>
|
|
32
|
+
</Button>
|
|
33
|
+
<Button variant="outline" className="flex flex-col items-center gap-2 h-fit [&_svg]:!h-6 [&_svg]:!w-6 bg-emerald-500/10 hover:bg-emerald-500/25 text-emerald-500 hover:text-emerald-400 border-none" onClick={() => router.push('/template-test-cases/create')}>
|
|
34
|
+
<Blocks />
|
|
35
|
+
<span className="text-xs font-medium">Create Template</span>
|
|
36
|
+
</Button>
|
|
37
|
+
<Button variant="outline" className="flex flex-col items-center gap-2 h-fit [&_svg]:!h-6 [&_svg]:!w-6 bg-emerald-500/10 hover:bg-emerald-500/25 text-emerald-500 hover:text-emerald-400 border-none" onClick={() => router.push('/reports')}>
|
|
38
|
+
<FileCheck />
|
|
39
|
+
<span className="text-xs font-medium">View Reports</span>
|
|
40
|
+
</Button>
|
|
41
|
+
</div>
|
|
42
|
+
</CardContent>
|
|
43
|
+
</Card>
|
|
44
|
+
)
|
|
45
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server'
|
|
2
|
+
import prisma from '@/config/db-config'
|
|
3
|
+
import archiver from 'archiver'
|
|
4
|
+
import { promises as fs } from 'fs'
|
|
5
|
+
import path from 'path'
|
|
6
|
+
|
|
7
|
+
// Ensure this route runs in Node.js runtime (not Edge) for file system access
|
|
8
|
+
export const runtime = 'nodejs'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* GET handler for downloading test run logs and traces as a zip file
|
|
12
|
+
*
|
|
13
|
+
* This endpoint:
|
|
14
|
+
* - Verifies the test run exists
|
|
15
|
+
* - Collects the log file (if exists)
|
|
16
|
+
* - Collects all trace files from test cases (if any)
|
|
17
|
+
* - Creates a zip file containing all files
|
|
18
|
+
* - Returns the zip file as a downloadable response
|
|
19
|
+
*/
|
|
20
|
+
export async function GET(request: NextRequest, { params }: { params: Promise<{ runId: string }> }) {
|
|
21
|
+
const { runId } = await params
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
// Verify test run exists
|
|
25
|
+
const testRun = await prisma.testRun.findUnique({
|
|
26
|
+
where: { runId },
|
|
27
|
+
include: {
|
|
28
|
+
testCases: {
|
|
29
|
+
select: {
|
|
30
|
+
id: true,
|
|
31
|
+
tracePath: true,
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
if (!testRun) {
|
|
38
|
+
return NextResponse.json({ error: 'Test run not found' }, { status: 404 })
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Create a zip archive
|
|
42
|
+
const archive = archiver('zip', {
|
|
43
|
+
zlib: { level: 9 }, // Maximum compression
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
// Track if we have any files to add
|
|
47
|
+
let hasFiles = false
|
|
48
|
+
|
|
49
|
+
// Add log file if it exists
|
|
50
|
+
if (testRun.logPath) {
|
|
51
|
+
try {
|
|
52
|
+
await fs.access(testRun.logPath)
|
|
53
|
+
const logFileName = path.basename(testRun.logPath)
|
|
54
|
+
archive.file(testRun.logPath, { name: `logs/${logFileName}` })
|
|
55
|
+
hasFiles = true
|
|
56
|
+
} catch {
|
|
57
|
+
// Log file doesn't exist, skip it
|
|
58
|
+
console.warn(`[Download] Log file not found at path: ${testRun.logPath}`)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Add trace files if they exist
|
|
63
|
+
const traceFiles = testRun.testCases.filter(tc => tc.tracePath).map(tc => tc.tracePath!)
|
|
64
|
+
for (const tracePath of traceFiles) {
|
|
65
|
+
try {
|
|
66
|
+
await fs.access(tracePath)
|
|
67
|
+
const traceFileName = path.basename(tracePath)
|
|
68
|
+
archive.file(tracePath, { name: `traces/${traceFileName}` })
|
|
69
|
+
hasFiles = true
|
|
70
|
+
} catch {
|
|
71
|
+
// Trace file doesn't exist, skip it
|
|
72
|
+
console.warn(`[Download] Trace file not found at path: ${tracePath}`)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// If no files to add, return an error
|
|
77
|
+
if (!hasFiles) {
|
|
78
|
+
return NextResponse.json(
|
|
79
|
+
{ error: 'No log or trace files available for this test run' },
|
|
80
|
+
{ status: 404 },
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Create a readable stream to collect the archive data
|
|
85
|
+
const chunks: Buffer[] = []
|
|
86
|
+
|
|
87
|
+
// Set up event handlers before finalizing
|
|
88
|
+
const archivePromise = new Promise<Buffer>((resolve, reject) => {
|
|
89
|
+
archive.on('data', (chunk: Buffer) => {
|
|
90
|
+
chunks.push(chunk)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
archive.on('end', () => {
|
|
94
|
+
const zipBuffer = Buffer.concat(chunks)
|
|
95
|
+
resolve(zipBuffer)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
archive.on('error', (err) => {
|
|
99
|
+
reject(err)
|
|
100
|
+
})
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
// Finalize the archive
|
|
104
|
+
archive.finalize()
|
|
105
|
+
|
|
106
|
+
// Wait for the archive to complete
|
|
107
|
+
const zipBuffer = await archivePromise
|
|
108
|
+
|
|
109
|
+
// Generate filename with timestamp
|
|
110
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5)
|
|
111
|
+
const filename = `test-run-${runId}-${timestamp}.zip`
|
|
112
|
+
|
|
113
|
+
// Return the zip file as a downloadable response (Uint8Array for BodyInit)
|
|
114
|
+
return new NextResponse(new Uint8Array(zipBuffer), {
|
|
115
|
+
status: 200,
|
|
116
|
+
headers: {
|
|
117
|
+
'Content-Type': 'application/zip',
|
|
118
|
+
'Content-Disposition': `attachment; filename="${filename}"`,
|
|
119
|
+
'Content-Length': zipBuffer.length.toString(),
|
|
120
|
+
},
|
|
121
|
+
})
|
|
122
|
+
} catch (error) {
|
|
123
|
+
console.error(`[Download] Error creating zip file for testRunId: ${runId}:`, error)
|
|
124
|
+
return NextResponse.json(
|
|
125
|
+
{
|
|
126
|
+
error: 'Failed to create download file',
|
|
127
|
+
details: error instanceof Error ? error.message : 'Unknown error',
|
|
128
|
+
},
|
|
129
|
+
{ status: 500 },
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
import { NextRequest } from 'next/server'
|
|
2
|
+
import { processManager } from '@/lib/test-run/process-manager'
|
|
3
|
+
import { taskSpawner } from '@/tests/utils/spawner.util'
|
|
4
|
+
import prisma from '@/config/db-config'
|
|
5
|
+
import { TestRunStatus } from '@prisma/client'
|
|
6
|
+
|
|
7
|
+
// Ensure this route runs in Node.js runtime (not Edge) for singleton to work
|
|
8
|
+
export const runtime = 'nodejs'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Server-Sent Events (SSE) route handler for streaming test run logs
|
|
12
|
+
*
|
|
13
|
+
* This endpoint streams logs from a running test process to the client
|
|
14
|
+
* using Server-Sent Events. It listens to TaskSpawner stdout/stderr events
|
|
15
|
+
* and forwards them as SSE messages.
|
|
16
|
+
*
|
|
17
|
+
* Security: Verifies test run exists in database before allowing access.
|
|
18
|
+
* TODO: Add user authentication check when authentication is implemented.
|
|
19
|
+
*/
|
|
20
|
+
export async function GET(request: NextRequest, { params }: { params: Promise<{ runId: string }> }) {
|
|
21
|
+
const { runId } = await params
|
|
22
|
+
|
|
23
|
+
// Verify test run exists in database and check status
|
|
24
|
+
// TODO: Add user authentication check here when authentication is implemented
|
|
25
|
+
// Example: where: { runId, userId: currentUser.id }
|
|
26
|
+
try {
|
|
27
|
+
const testRun = await prisma.testRun.findUnique({
|
|
28
|
+
where: { runId },
|
|
29
|
+
select: { id: true, status: true }, // Need status to check if completed
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
if (!testRun) {
|
|
33
|
+
console.error(`[SSE] Test run not found in database for runId: ${runId}`)
|
|
34
|
+
const errorStream = new ReadableStream({
|
|
35
|
+
start(controller) {
|
|
36
|
+
const encoder = new TextEncoder()
|
|
37
|
+
const message = `event: error\ndata: ${JSON.stringify({ error: 'Test run not found' })}\n\n`
|
|
38
|
+
controller.enqueue(encoder.encode(message))
|
|
39
|
+
setTimeout(() => {
|
|
40
|
+
controller.close()
|
|
41
|
+
}, 100)
|
|
42
|
+
},
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
return new Response(errorStream, {
|
|
46
|
+
status: 404,
|
|
47
|
+
headers: {
|
|
48
|
+
'Content-Type': 'text/event-stream',
|
|
49
|
+
'Cache-Control': 'no-cache, no-transform',
|
|
50
|
+
Connection: 'keep-alive',
|
|
51
|
+
},
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// If test run is completed, reject SSE connection - logs should be loaded from DB
|
|
56
|
+
if (testRun.status === TestRunStatus.COMPLETED || testRun.status === TestRunStatus.CANCELLED) {
|
|
57
|
+
console.log(`[SSE] Test run ${runId} is ${testRun.status}, rejecting SSE connection`)
|
|
58
|
+
const errorStream = new ReadableStream({
|
|
59
|
+
start(controller) {
|
|
60
|
+
const encoder = new TextEncoder()
|
|
61
|
+
const message = `event: error\ndata: ${JSON.stringify({ error: 'Test run has completed. Logs are available in the database.' })}\n\n`
|
|
62
|
+
controller.enqueue(encoder.encode(message))
|
|
63
|
+
setTimeout(() => {
|
|
64
|
+
controller.close()
|
|
65
|
+
}, 100)
|
|
66
|
+
},
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
return new Response(errorStream, {
|
|
70
|
+
status: 200, // Return 200 but with error event so client can handle it
|
|
71
|
+
headers: {
|
|
72
|
+
'Content-Type': 'text/event-stream',
|
|
73
|
+
'Cache-Control': 'no-cache, no-transform',
|
|
74
|
+
Connection: 'keep-alive',
|
|
75
|
+
},
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
} catch (error) {
|
|
79
|
+
console.error(`[SSE] Database error verifying test run for runId: ${runId}:`, error)
|
|
80
|
+
const errorStream = new ReadableStream({
|
|
81
|
+
start(controller) {
|
|
82
|
+
const encoder = new TextEncoder()
|
|
83
|
+
const message = `event: error\ndata: ${JSON.stringify({ error: 'Internal server error' })}\n\n`
|
|
84
|
+
controller.enqueue(encoder.encode(message))
|
|
85
|
+
setTimeout(() => {
|
|
86
|
+
controller.close()
|
|
87
|
+
}, 100)
|
|
88
|
+
},
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
return new Response(errorStream, {
|
|
92
|
+
status: 500,
|
|
93
|
+
headers: {
|
|
94
|
+
'Content-Type': 'text/event-stream',
|
|
95
|
+
'Cache-Control': 'no-cache, no-transform',
|
|
96
|
+
Connection: 'keep-alive',
|
|
97
|
+
},
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Wait for process to be registered (with timeout)
|
|
102
|
+
// This handles the race condition where the page loads before the process is spawned
|
|
103
|
+
let process = processManager.get(runId)
|
|
104
|
+
const maxWaitTime = 10000 // Increase to 10 seconds
|
|
105
|
+
const checkInterval = 200 // Check every 200ms
|
|
106
|
+
let waited = 0
|
|
107
|
+
|
|
108
|
+
console.log(`[SSE] Looking for process with runId: ${runId}, current processes: ${processManager.size()}`)
|
|
109
|
+
|
|
110
|
+
// Log all available process IDs for debugging
|
|
111
|
+
if (processManager.size() > 0) {
|
|
112
|
+
const availableProcesses = processManager.getAllTestRunIds()
|
|
113
|
+
console.log(`[SSE] Available process IDs:`, availableProcesses)
|
|
114
|
+
console.log(`[SSE] Looking for runId: "${runId}", available:`, availableProcesses.map(id => `"${id}"`).join(', '))
|
|
115
|
+
} else {
|
|
116
|
+
console.log(`[SSE] No processes registered yet. ProcessManager size: ${processManager.size()}`)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
while (!process && waited < maxWaitTime) {
|
|
120
|
+
await new Promise(resolve => setTimeout(resolve, checkInterval))
|
|
121
|
+
waited += checkInterval
|
|
122
|
+
process = processManager.get(runId)
|
|
123
|
+
|
|
124
|
+
// Log progress every 2 seconds
|
|
125
|
+
if (waited % 2000 === 0) {
|
|
126
|
+
console.log(`[SSE] Still waiting for process ${runId}... (${waited}ms elapsed)`)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (!process) {
|
|
131
|
+
const availableProcesses = processManager.getAllTestRunIds()
|
|
132
|
+
const errorMessage = `Process not found for runId: ${runId} after ${waited}ms. Available processes: ${processManager.size()}. Available IDs: ${availableProcesses.join(', ') || 'none'}`
|
|
133
|
+
console.error(`[SSE] ${errorMessage}`)
|
|
134
|
+
|
|
135
|
+
// Return an SSE stream with an error event instead of JSON response
|
|
136
|
+
// This allows EventSource to properly handle the error
|
|
137
|
+
const errorStream = new ReadableStream({
|
|
138
|
+
start(controller) {
|
|
139
|
+
const encoder = new TextEncoder()
|
|
140
|
+
const message = `event: error\ndata: ${JSON.stringify({
|
|
141
|
+
error: 'Test run process not found. The process may not have started yet or may have already completed.',
|
|
142
|
+
details: `Looking for: ${runId}, Available: ${availableProcesses.join(', ') || 'none'}`,
|
|
143
|
+
})}\n\n`
|
|
144
|
+
controller.enqueue(encoder.encode(message))
|
|
145
|
+
setTimeout(() => {
|
|
146
|
+
controller.close()
|
|
147
|
+
}, 100)
|
|
148
|
+
},
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
return new Response(errorStream, {
|
|
152
|
+
headers: {
|
|
153
|
+
'Content-Type': 'text/event-stream',
|
|
154
|
+
'Cache-Control': 'no-cache, no-transform',
|
|
155
|
+
Connection: 'keep-alive',
|
|
156
|
+
},
|
|
157
|
+
})
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Process found, proceed with SSE connection
|
|
161
|
+
|
|
162
|
+
// Store cleanup function reference for cancel handler
|
|
163
|
+
let cleanupRef: (() => void) | null = null
|
|
164
|
+
|
|
165
|
+
// Create a ReadableStream for SSE
|
|
166
|
+
const stream = new ReadableStream({
|
|
167
|
+
async start(controller) {
|
|
168
|
+
const encoder = new TextEncoder()
|
|
169
|
+
let isClosed = false
|
|
170
|
+
let errorOccurred = false
|
|
171
|
+
let errorLogged = false // Track if we've already logged the error to avoid spam
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Helper function to safely close the controller
|
|
175
|
+
*/
|
|
176
|
+
const safeClose = () => {
|
|
177
|
+
if (!isClosed) {
|
|
178
|
+
try {
|
|
179
|
+
controller.close()
|
|
180
|
+
isClosed = true
|
|
181
|
+
} catch {
|
|
182
|
+
// Controller may already be closed
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Helper function to send SSE message
|
|
189
|
+
* Flushes immediately to ensure real-time streaming
|
|
190
|
+
*/
|
|
191
|
+
const sendSSE = (event: string, data: string) => {
|
|
192
|
+
// Early return if stream is closed or error occurred
|
|
193
|
+
if (isClosed || errorOccurred) return
|
|
194
|
+
|
|
195
|
+
try {
|
|
196
|
+
const message = `event: ${event}\ndata: ${data}\n\n`
|
|
197
|
+
controller.enqueue(encoder.encode(message))
|
|
198
|
+
} catch (error) {
|
|
199
|
+
// Only log error once to avoid infinite spam
|
|
200
|
+
if (!errorLogged) {
|
|
201
|
+
console.error(`[SSE] Error sending ${event} event:`, error)
|
|
202
|
+
errorLogged = true
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Mark error occurred and immediately cleanup
|
|
206
|
+
errorOccurred = true
|
|
207
|
+
isClosed = true
|
|
208
|
+
|
|
209
|
+
// Immediately cleanup listeners to prevent further events
|
|
210
|
+
cleanup()
|
|
211
|
+
safeClose()
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Check if a line is an event JSON (should not be displayed in logs)
|
|
217
|
+
*/
|
|
218
|
+
const isEventJson = (line: string): boolean => {
|
|
219
|
+
try {
|
|
220
|
+
const trimmed = line.trim()
|
|
221
|
+
if (!trimmed.startsWith('{') || !trimmed.includes('"event"')) {
|
|
222
|
+
return false
|
|
223
|
+
}
|
|
224
|
+
const parsed = JSON.parse(trimmed)
|
|
225
|
+
return parsed.event === 'scenario::end' || parsed.event !== undefined
|
|
226
|
+
} catch {
|
|
227
|
+
return false
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Handler for stdout events
|
|
233
|
+
*/
|
|
234
|
+
const onStdout = ({ processName, data }: { processName: string; data: string }) => {
|
|
235
|
+
// Early return if error occurred to prevent infinite loops
|
|
236
|
+
if (errorOccurred) return
|
|
237
|
+
|
|
238
|
+
if (processName === process.name) {
|
|
239
|
+
// Filter out event JSON lines - they're for internal processing only
|
|
240
|
+
const lines = data.split('\n')
|
|
241
|
+
const filteredLines = lines.filter(line => {
|
|
242
|
+
const trimmed = line.trim()
|
|
243
|
+
return trimmed && !isEventJson(trimmed)
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
if (filteredLines.length > 0) {
|
|
247
|
+
sendSSE('log', JSON.stringify({ type: 'stdout', message: filteredLines.join('\n') }))
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Handler for stderr events
|
|
254
|
+
*/
|
|
255
|
+
const onStderr = ({ processName, data }: { processName: string; data: string }) => {
|
|
256
|
+
// Early return if error occurred to prevent infinite loops
|
|
257
|
+
if (errorOccurred) return
|
|
258
|
+
|
|
259
|
+
if (processName === process.name) {
|
|
260
|
+
// Filter out event JSON lines - they're for internal processing only
|
|
261
|
+
const lines = data.split('\n')
|
|
262
|
+
const filteredLines = lines.filter(line => {
|
|
263
|
+
const trimmed = line.trim()
|
|
264
|
+
return trimmed && !isEventJson(trimmed)
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
if (filteredLines.length > 0) {
|
|
268
|
+
sendSSE('log', JSON.stringify({ type: 'stderr', message: filteredLines.join('\n') }))
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Handler for process exit
|
|
275
|
+
*/
|
|
276
|
+
const onExit = ({ processName, code }: { processName: string; code: number | null }) => {
|
|
277
|
+
// Early return if error occurred to prevent infinite loops
|
|
278
|
+
if (errorOccurred) {
|
|
279
|
+
// Still cleanup on exit even if error occurred
|
|
280
|
+
cleanup()
|
|
281
|
+
safeClose()
|
|
282
|
+
return
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (processName === process.name) {
|
|
286
|
+
sendSSE('exit', JSON.stringify({ code }))
|
|
287
|
+
// Close the stream after sending exit event
|
|
288
|
+
setTimeout(() => {
|
|
289
|
+
cleanup()
|
|
290
|
+
safeClose()
|
|
291
|
+
}, 100)
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Handler for process errors
|
|
297
|
+
*/
|
|
298
|
+
const onError = ({ processName, error }: { processName: string; error: Error }) => {
|
|
299
|
+
// Early return if error occurred to prevent infinite loops
|
|
300
|
+
if (errorOccurred) return
|
|
301
|
+
|
|
302
|
+
if (processName === process.name) {
|
|
303
|
+
sendSSE('error', JSON.stringify({ message: error.message }))
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Handler for scenario::end events from ProcessManager
|
|
309
|
+
*/
|
|
310
|
+
const onScenarioEnd = (eventData: {
|
|
311
|
+
testRunId: string
|
|
312
|
+
scenarioName: string
|
|
313
|
+
status: string
|
|
314
|
+
tracePath?: string
|
|
315
|
+
}) => {
|
|
316
|
+
// Early return if error occurred to prevent infinite loops
|
|
317
|
+
if (errorOccurred) return
|
|
318
|
+
|
|
319
|
+
console.log(
|
|
320
|
+
`[SSE] Received scenario::end event for testRunId: ${eventData.testRunId}, runId: ${runId}`,
|
|
321
|
+
eventData,
|
|
322
|
+
)
|
|
323
|
+
if (eventData.testRunId === runId) {
|
|
324
|
+
console.log(`[SSE] Sending scenario::end SSE event for scenario: ${eventData.scenarioName}`)
|
|
325
|
+
sendSSE(
|
|
326
|
+
'scenario::end',
|
|
327
|
+
JSON.stringify({
|
|
328
|
+
scenarioName: eventData.scenarioName,
|
|
329
|
+
status: eventData.status,
|
|
330
|
+
tracePath: eventData.tracePath,
|
|
331
|
+
}),
|
|
332
|
+
)
|
|
333
|
+
} else {
|
|
334
|
+
console.log(`[SSE] Ignoring scenario::end event - testRunId mismatch: ${eventData.testRunId} !== ${runId}`)
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Clean up listeners helper (defined before use)
|
|
339
|
+
const cleanup = () => {
|
|
340
|
+
taskSpawner.removeListener('stdout', onStdout)
|
|
341
|
+
taskSpawner.removeListener('stderr', onStderr)
|
|
342
|
+
taskSpawner.removeListener('exit', onExit)
|
|
343
|
+
taskSpawner.removeListener('error', onError)
|
|
344
|
+
processManager.removeListener('scenario::end', onScenarioEnd)
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Store cleanup reference for cancel handler
|
|
348
|
+
cleanupRef = cleanup
|
|
349
|
+
|
|
350
|
+
// Set up event listeners on TaskSpawner
|
|
351
|
+
taskSpawner.on('stdout', onStdout)
|
|
352
|
+
taskSpawner.on('stderr', onStderr)
|
|
353
|
+
taskSpawner.on('exit', onExit)
|
|
354
|
+
taskSpawner.on('error', onError)
|
|
355
|
+
|
|
356
|
+
// Listen for scenario::end events from ProcessManager
|
|
357
|
+
processManager.on('scenario::end', onScenarioEnd)
|
|
358
|
+
|
|
359
|
+
console.log(`[SSE] Connected to log stream for runId: ${runId}`)
|
|
360
|
+
|
|
361
|
+
// Send initial connection message
|
|
362
|
+
sendSSE('connected', JSON.stringify({ message: 'Connected to log stream' }))
|
|
363
|
+
|
|
364
|
+
// Send any already captured output immediately (filter out event JSON)
|
|
365
|
+
if (process.output && (process.output.stdout.length > 0 || process.output.stderr.length > 0)) {
|
|
366
|
+
process.output.stdout.forEach(line => {
|
|
367
|
+
const trimmed = line.trim()
|
|
368
|
+
// Skip event JSON lines
|
|
369
|
+
if (trimmed && !isEventJson(trimmed)) {
|
|
370
|
+
sendSSE('log', JSON.stringify({ type: 'stdout', message: line }))
|
|
371
|
+
}
|
|
372
|
+
})
|
|
373
|
+
process.output.stderr.forEach(line => {
|
|
374
|
+
const trimmed = line.trim()
|
|
375
|
+
// Skip event JSON lines
|
|
376
|
+
if (trimmed && !isEventJson(trimmed)) {
|
|
377
|
+
sendSSE('log', JSON.stringify({ type: 'stderr', message: line }))
|
|
378
|
+
}
|
|
379
|
+
})
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Check if process has already exited (race condition check after listeners are set up)
|
|
383
|
+
// We check this after setting up listeners in case exit event was emitted during setup
|
|
384
|
+
if (!process.isRunning && process.exitCode !== null) {
|
|
385
|
+
sendSSE('exit', JSON.stringify({ code: process.exitCode }))
|
|
386
|
+
// Don't close immediately - let any pending events be sent first
|
|
387
|
+
setTimeout(() => {
|
|
388
|
+
cleanup()
|
|
389
|
+
safeClose()
|
|
390
|
+
}, 500)
|
|
391
|
+
return
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Handle client disconnect
|
|
395
|
+
request.signal.addEventListener('abort', () => {
|
|
396
|
+
errorOccurred = true
|
|
397
|
+
cleanup()
|
|
398
|
+
safeClose()
|
|
399
|
+
})
|
|
400
|
+
},
|
|
401
|
+
cancel() {
|
|
402
|
+
// Handle stream cancellation (client closes connection)
|
|
403
|
+
// Cleanup listeners if they were set up
|
|
404
|
+
if (cleanupRef) {
|
|
405
|
+
cleanupRef()
|
|
406
|
+
cleanupRef = null
|
|
407
|
+
}
|
|
408
|
+
},
|
|
409
|
+
})
|
|
410
|
+
|
|
411
|
+
// Return SSE response with appropriate headers
|
|
412
|
+
return new Response(stream, {
|
|
413
|
+
headers: {
|
|
414
|
+
'Content-Type': 'text/event-stream',
|
|
415
|
+
'Cache-Control': 'no-cache, no-transform',
|
|
416
|
+
Connection: 'keep-alive',
|
|
417
|
+
'X-Accel-Buffering': 'no', // Disable nginx buffering
|
|
418
|
+
},
|
|
419
|
+
})
|
|
420
|
+
}
|