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