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