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,411 +1,411 @@
|
|
|
1
|
-
import { promises as fs } from 'fs'
|
|
2
|
-
import { join, dirname } from 'path'
|
|
3
|
-
import prisma from '@/config/db-config'
|
|
4
|
-
import { buildModulePath } from '@/lib/path-helpers/module-path'
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Checks if a directory is empty (no files or subdirectories)
|
|
8
|
-
* @param dirPath - Path to the directory to check
|
|
9
|
-
* @returns Promise<boolean> - True if directory is empty, false otherwise
|
|
10
|
-
*/
|
|
11
|
-
async function isDirectoryEmpty(dirPath: string): Promise<boolean> {
|
|
12
|
-
try {
|
|
13
|
-
const entries = await fs.readdir(dirPath)
|
|
14
|
-
return entries.length === 0
|
|
15
|
-
} catch (error) {
|
|
16
|
-
// If directory doesn't exist or can't be read, consider it empty
|
|
17
|
-
console.warn(`Could not read directory ${dirPath}:`, error)
|
|
18
|
-
return true
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Removes empty directories up the hierarchy until a non-empty directory is found
|
|
24
|
-
* @param dirPath - Starting directory path to clean up
|
|
25
|
-
* @param basePath - Base path to stop cleaning (e.g., features directory)
|
|
26
|
-
* @returns Promise<void>
|
|
27
|
-
*/
|
|
28
|
-
async function removeEmptyDirectoriesUp(dirPath: string, basePath: string): Promise<void> {
|
|
29
|
-
let currentPath = dirPath
|
|
30
|
-
|
|
31
|
-
// Keep going up the directory tree until we reach the base path
|
|
32
|
-
while (currentPath !== basePath && currentPath !== dirname(currentPath)) {
|
|
33
|
-
try {
|
|
34
|
-
// Check if current directory is empty
|
|
35
|
-
if (await isDirectoryEmpty(currentPath)) {
|
|
36
|
-
await fs.rmdir(currentPath)
|
|
37
|
-
console.log(`Removed empty directory: ${currentPath}`)
|
|
38
|
-
// Move up one level
|
|
39
|
-
currentPath = dirname(currentPath)
|
|
40
|
-
} else {
|
|
41
|
-
// Directory is not empty, stop cleaning
|
|
42
|
-
break
|
|
43
|
-
}
|
|
44
|
-
} catch (error) {
|
|
45
|
-
// If we can't remove the directory or it doesn't exist, stop
|
|
46
|
-
console.warn(`Could not remove directory ${currentPath}:`, error)
|
|
47
|
-
break
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Generates a Gherkin feature file for a test suite
|
|
54
|
-
* @param testSuiteId - The ID of the test suite
|
|
55
|
-
* @param testSuiteName - The name of the test suite
|
|
56
|
-
* @param testSuiteDescription - The description of the test suite
|
|
57
|
-
* @param moduleName - The name of the module the test suite belongs to
|
|
58
|
-
* @returns Promise<string> - The path to the generated feature file
|
|
59
|
-
*/
|
|
60
|
-
export async function generateFeatureFile(
|
|
61
|
-
testSuiteId: string,
|
|
62
|
-
testSuiteName: string,
|
|
63
|
-
testSuiteDescription?: string,
|
|
64
|
-
): Promise<string> {
|
|
65
|
-
try {
|
|
66
|
-
// Fetch test suite with test cases, steps, tags, and all modules for path building
|
|
67
|
-
const [testSuite, allModules] = await Promise.all([
|
|
68
|
-
prisma.testSuite.findUnique({
|
|
69
|
-
where: { id: testSuiteId },
|
|
70
|
-
include: {
|
|
71
|
-
testCases: {
|
|
72
|
-
include: {
|
|
73
|
-
steps: {
|
|
74
|
-
include: {
|
|
75
|
-
parameters: true,
|
|
76
|
-
},
|
|
77
|
-
orderBy: {
|
|
78
|
-
order: 'asc',
|
|
79
|
-
},
|
|
80
|
-
},
|
|
81
|
-
tags: true,
|
|
82
|
-
},
|
|
83
|
-
},
|
|
84
|
-
module: true,
|
|
85
|
-
tags: true,
|
|
86
|
-
},
|
|
87
|
-
}),
|
|
88
|
-
prisma.module.findMany(), // Get all modules to build hierarchy path
|
|
89
|
-
])
|
|
90
|
-
|
|
91
|
-
if (!testSuite) {
|
|
92
|
-
throw new Error(`Test suite with ID ${testSuiteId} not found`)
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// Build the module path for directory structure
|
|
96
|
-
const modulePath = buildModulePath(allModules, testSuite.module)
|
|
97
|
-
|
|
98
|
-
// Generate feature file content
|
|
99
|
-
const featureContent = generateFeatureContent(
|
|
100
|
-
testSuiteDescription || testSuiteName, // Use description as feature title, fallback to name
|
|
101
|
-
testSuite.testCases,
|
|
102
|
-
testSuite.tags,
|
|
103
|
-
)
|
|
104
|
-
|
|
105
|
-
// Create the features directory with module path
|
|
106
|
-
const featuresBaseDir = join(process.cwd(), 'src', 'tests', 'features')
|
|
107
|
-
const moduleDir = join(featuresBaseDir, modulePath.substring(1)) // Remove leading slash
|
|
108
|
-
await fs.mkdir(moduleDir, { recursive: true })
|
|
109
|
-
|
|
110
|
-
// Generate a safe filename from the test suite name only
|
|
111
|
-
const safeFileName = generateSafeFileName(testSuiteName)
|
|
112
|
-
const featureFilePath = join(moduleDir, `${safeFileName}.feature`)
|
|
113
|
-
|
|
114
|
-
// Write the feature file
|
|
115
|
-
await fs.writeFile(featureFilePath, featureContent, 'utf8')
|
|
116
|
-
|
|
117
|
-
return featureFilePath
|
|
118
|
-
} catch (error) {
|
|
119
|
-
console.error('Error generating feature file:', error)
|
|
120
|
-
throw error
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Generates the content for a Gherkin feature file
|
|
126
|
-
*/
|
|
127
|
-
function generateFeatureContent(
|
|
128
|
-
featureTitle: string,
|
|
129
|
-
testCases: Array<{
|
|
130
|
-
title: string
|
|
131
|
-
description: string
|
|
132
|
-
steps: Array<{
|
|
133
|
-
gherkinStep: string
|
|
134
|
-
order: number
|
|
135
|
-
}>
|
|
136
|
-
tags?: Array<{
|
|
137
|
-
tagExpression: string
|
|
138
|
-
}>
|
|
139
|
-
}>,
|
|
140
|
-
testSuiteTags?: Array<{
|
|
141
|
-
tagExpression: string
|
|
142
|
-
}>,
|
|
143
|
-
): string {
|
|
144
|
-
const lines: string[] = []
|
|
145
|
-
|
|
146
|
-
// Warning header
|
|
147
|
-
lines.push('# AUTO-GENERATED FILE - DO NOT EDIT MANUALLY')
|
|
148
|
-
lines.push('# This file is automatically generated from Test Suite data.')
|
|
149
|
-
lines.push('# Any manual changes will be overwritten when the Test Suite is updated.')
|
|
150
|
-
lines.push('# To modify this feature, update the corresponding Test Suite in the application.')
|
|
151
|
-
lines.push('')
|
|
152
|
-
|
|
153
|
-
// Add feature-level tags (one tag per line)
|
|
154
|
-
if (testSuiteTags && testSuiteTags.length > 0) {
|
|
155
|
-
testSuiteTags.forEach(tag => {
|
|
156
|
-
lines.push(tag.tagExpression)
|
|
157
|
-
})
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// Feature header - use description as feature title
|
|
161
|
-
lines.push(`Feature: ${featureTitle}`)
|
|
162
|
-
lines.push('')
|
|
163
|
-
|
|
164
|
-
// Get test suite tag expressions for deduplication
|
|
165
|
-
const testSuiteTagExpressions = new Set((testSuiteTags || []).map(tag => tag.tagExpression.toLowerCase()))
|
|
166
|
-
|
|
167
|
-
// Generate scenarios for each test case that has steps
|
|
168
|
-
let scenarioCount = 0
|
|
169
|
-
testCases.forEach(testCase => {
|
|
170
|
-
// Only generate scenario if test case has steps
|
|
171
|
-
if (testCase.steps && testCase.steps.length > 0) {
|
|
172
|
-
// Generate Gherkin steps from test case steps
|
|
173
|
-
const gherkinSteps = generateGherkinStepsFromTestCase(testCase.steps)
|
|
174
|
-
|
|
175
|
-
// Only add scenario if there are actual gherkin steps
|
|
176
|
-
if (gherkinSteps.length > 0) {
|
|
177
|
-
if (scenarioCount > 0) {
|
|
178
|
-
lines.push('') // Add blank line between scenarios
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// Add scenario-level tags (skip if already present at feature level)
|
|
182
|
-
if (testCase.tags && testCase.tags.length > 0) {
|
|
183
|
-
testCase.tags.forEach(tag => {
|
|
184
|
-
// Only add tag if it's not already present at feature level
|
|
185
|
-
if (!testSuiteTagExpressions.has(tag.tagExpression.toLowerCase())) {
|
|
186
|
-
lines.push(` ${tag.tagExpression}`)
|
|
187
|
-
}
|
|
188
|
-
})
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
lines.push(` Scenario: [${testCase.title}] ${testCase.description}`)
|
|
192
|
-
|
|
193
|
-
gherkinSteps.forEach(step => {
|
|
194
|
-
lines.push(` ${step}`)
|
|
195
|
-
})
|
|
196
|
-
|
|
197
|
-
scenarioCount++
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
})
|
|
201
|
-
|
|
202
|
-
return lines.join('\n') + '\n'
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* Generates Gherkin steps from test case steps using the same logic as the frontend
|
|
207
|
-
*/
|
|
208
|
-
function generateGherkinStepsFromTestCase(
|
|
209
|
-
steps: Array<{
|
|
210
|
-
gherkinStep: string
|
|
211
|
-
order: number
|
|
212
|
-
}>,
|
|
213
|
-
): string[] {
|
|
214
|
-
if (!steps || steps.length === 0) {
|
|
215
|
-
return []
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
// Sort steps by order
|
|
219
|
-
const sortedSteps = steps.sort((a, b) => a.order - b.order)
|
|
220
|
-
|
|
221
|
-
let hasThenInPrevious = false
|
|
222
|
-
let hasWhenInPrevious = false
|
|
223
|
-
|
|
224
|
-
return sortedSteps.map((step, index) => {
|
|
225
|
-
const gherkinStep = step.gherkinStep?.trim() || ''
|
|
226
|
-
const firstWord = gherkinStep.split(' ')[0].toLowerCase()
|
|
227
|
-
const hasGherkinKeyword = ['given', 'when', 'then', 'and', 'but'].includes(firstWord)
|
|
228
|
-
const stepWithoutKeyword = hasGherkinKeyword ? gherkinStep.split(' ').slice(1).join(' ') : gherkinStep
|
|
229
|
-
|
|
230
|
-
// First step always starts with Given
|
|
231
|
-
if (index === 0) {
|
|
232
|
-
return `Given ${stepWithoutKeyword}`
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// Check if this step should be a Then statement
|
|
236
|
-
const isThenStatement =
|
|
237
|
-
firstWord === 'then' ||
|
|
238
|
-
stepWithoutKeyword.toLowerCase().startsWith('should') ||
|
|
239
|
-
stepWithoutKeyword.toLowerCase().startsWith('must') ||
|
|
240
|
-
stepWithoutKeyword.toLowerCase().startsWith('will')
|
|
241
|
-
|
|
242
|
-
// If we haven't seen a Then yet
|
|
243
|
-
if (!hasThenInPrevious) {
|
|
244
|
-
// If this is a Then statement
|
|
245
|
-
if (isThenStatement) {
|
|
246
|
-
hasThenInPrevious = true
|
|
247
|
-
return `Then ${stepWithoutKeyword}`
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
// If we haven't seen a When yet, use When
|
|
251
|
-
if (!hasWhenInPrevious) {
|
|
252
|
-
hasWhenInPrevious = true
|
|
253
|
-
return `When ${stepWithoutKeyword}`
|
|
254
|
-
}
|
|
255
|
-
// After When, use And
|
|
256
|
-
return `And ${stepWithoutKeyword}`
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
// After Then
|
|
260
|
-
if (isThenStatement) {
|
|
261
|
-
// If it's another Then statement, use And
|
|
262
|
-
return `And ${stepWithoutKeyword}`
|
|
263
|
-
}
|
|
264
|
-
// After Then, use When for new actions
|
|
265
|
-
hasThenInPrevious = false
|
|
266
|
-
hasWhenInPrevious = true
|
|
267
|
-
return `When ${stepWithoutKeyword}`
|
|
268
|
-
})
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
/**
|
|
272
|
-
* Deletes a feature file for a test suite
|
|
273
|
-
* @param testSuiteId - The ID of the test suite
|
|
274
|
-
* @returns Promise<boolean> - True if file was deleted, false if file didn't exist
|
|
275
|
-
*/
|
|
276
|
-
export async function deleteFeatureFile(testSuiteId: string): Promise<boolean> {
|
|
277
|
-
try {
|
|
278
|
-
// Fetch test suite and all modules to build the correct path
|
|
279
|
-
const [testSuite, allModules] = await Promise.all([
|
|
280
|
-
prisma.testSuite.findUnique({
|
|
281
|
-
where: { id: testSuiteId },
|
|
282
|
-
include: {
|
|
283
|
-
module: true,
|
|
284
|
-
},
|
|
285
|
-
}),
|
|
286
|
-
prisma.module.findMany(),
|
|
287
|
-
])
|
|
288
|
-
|
|
289
|
-
if (!testSuite) {
|
|
290
|
-
console.warn(`Test suite with ID ${testSuiteId} not found for feature file deletion`)
|
|
291
|
-
return false
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
// Build the module path for directory structure
|
|
295
|
-
const modulePath = buildModulePath(allModules, testSuite.module)
|
|
296
|
-
const safeFileName = generateSafeFileName(testSuite.name)
|
|
297
|
-
|
|
298
|
-
const featuresBaseDir = join(process.cwd(), 'src', 'tests', 'features')
|
|
299
|
-
const moduleDir = join(featuresBaseDir, modulePath.substring(1)) // Remove leading slash
|
|
300
|
-
const featureFilePath = join(moduleDir, `${safeFileName}.feature`)
|
|
301
|
-
|
|
302
|
-
try {
|
|
303
|
-
await fs.unlink(featureFilePath)
|
|
304
|
-
console.log(`Feature file deleted: ${featureFilePath}`)
|
|
305
|
-
|
|
306
|
-
// Clean up empty directories up the module hierarchy
|
|
307
|
-
await removeEmptyDirectoriesUp(moduleDir, featuresBaseDir)
|
|
308
|
-
|
|
309
|
-
return true
|
|
310
|
-
} catch (error: unknown) {
|
|
311
|
-
if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT') {
|
|
312
|
-
console.warn(`Feature file not found for deletion: ${featureFilePath}`)
|
|
313
|
-
return false
|
|
314
|
-
}
|
|
315
|
-
throw error
|
|
316
|
-
}
|
|
317
|
-
} catch (error) {
|
|
318
|
-
console.error('Error deleting feature file:', error)
|
|
319
|
-
throw error
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
/**
|
|
324
|
-
* Regenerates all feature files from the current database state
|
|
325
|
-
* This is useful after merging changes or database migrations to ensure sync
|
|
326
|
-
* @returns Promise<string[]> - Array of generated feature file paths
|
|
327
|
-
*/
|
|
328
|
-
export async function regenerateAllFeatureFiles(): Promise<string[]> {
|
|
329
|
-
try {
|
|
330
|
-
console.log('Starting regeneration of all feature files...')
|
|
331
|
-
|
|
332
|
-
// Clear existing feature files directory
|
|
333
|
-
const featuresBaseDir = join(process.cwd(), 'src', 'tests', 'features')
|
|
334
|
-
try {
|
|
335
|
-
await fs.rm(featuresBaseDir, { recursive: true, force: true })
|
|
336
|
-
} catch (error) {
|
|
337
|
-
console.warn('Could not clear features directory:', error)
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
// Fetch all test suites from database
|
|
341
|
-
const testSuites = await prisma.testSuite.findMany({
|
|
342
|
-
include: {
|
|
343
|
-
testCases: {
|
|
344
|
-
include: {
|
|
345
|
-
steps: {
|
|
346
|
-
include: {
|
|
347
|
-
parameters: true,
|
|
348
|
-
},
|
|
349
|
-
orderBy: {
|
|
350
|
-
order: 'asc',
|
|
351
|
-
},
|
|
352
|
-
},
|
|
353
|
-
tags: true,
|
|
354
|
-
},
|
|
355
|
-
},
|
|
356
|
-
module: true,
|
|
357
|
-
tags: true,
|
|
358
|
-
},
|
|
359
|
-
})
|
|
360
|
-
|
|
361
|
-
// Fetch all modules for path building
|
|
362
|
-
const allModules = await prisma.module.findMany()
|
|
363
|
-
|
|
364
|
-
const generatedFiles: string[] = []
|
|
365
|
-
|
|
366
|
-
// Generate feature file for each test suite
|
|
367
|
-
for (const testSuite of testSuites) {
|
|
368
|
-
try {
|
|
369
|
-
const modulePath = buildModulePath(allModules, testSuite.module)
|
|
370
|
-
const featureContent = generateFeatureContent(
|
|
371
|
-
testSuite.description || testSuite.name,
|
|
372
|
-
testSuite.testCases,
|
|
373
|
-
testSuite.tags,
|
|
374
|
-
)
|
|
375
|
-
|
|
376
|
-
// Create the features directory with module path
|
|
377
|
-
const moduleDir = join(featuresBaseDir, modulePath.substring(1)) // Remove leading slash
|
|
378
|
-
await fs.mkdir(moduleDir, { recursive: true })
|
|
379
|
-
|
|
380
|
-
// Generate filename and write file
|
|
381
|
-
const safeFileName = generateSafeFileName(testSuite.name)
|
|
382
|
-
const featureFilePath = join(moduleDir, `${safeFileName}.feature`)
|
|
383
|
-
|
|
384
|
-
await fs.writeFile(featureFilePath, featureContent, 'utf8')
|
|
385
|
-
generatedFiles.push(featureFilePath)
|
|
386
|
-
|
|
387
|
-
console.log(`Generated: ${featureFilePath}`)
|
|
388
|
-
} catch (error) {
|
|
389
|
-
console.error(`Error generating feature file for test suite ${testSuite.name}:`, error)
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
console.log(`Regeneration complete. Generated ${generatedFiles.length} feature files.`)
|
|
394
|
-
return generatedFiles
|
|
395
|
-
} catch (error) {
|
|
396
|
-
console.error('Error during feature files regeneration:', error)
|
|
397
|
-
throw error
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
/**
|
|
402
|
-
* Generates a safe filename from test suite name
|
|
403
|
-
*/
|
|
404
|
-
function generateSafeFileName(testSuiteName: string): string {
|
|
405
|
-
// Convert to lowercase and replace spaces and special characters with hyphens
|
|
406
|
-
return testSuiteName
|
|
407
|
-
.toLowerCase()
|
|
408
|
-
.replace(/[^a-z0-9]+/g, '-')
|
|
409
|
-
.replace(/^-+|-+$/g, '') // Remove leading/trailing hyphens
|
|
410
|
-
.replace(/-+/g, '-') // Replace multiple consecutive hyphens with single hyphen
|
|
411
|
-
}
|
|
1
|
+
import { promises as fs } from 'fs'
|
|
2
|
+
import { join, dirname } from 'path'
|
|
3
|
+
import prisma from '@/config/db-config'
|
|
4
|
+
import { buildModulePath } from '@/lib/path-helpers/module-path'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Checks if a directory is empty (no files or subdirectories)
|
|
8
|
+
* @param dirPath - Path to the directory to check
|
|
9
|
+
* @returns Promise<boolean> - True if directory is empty, false otherwise
|
|
10
|
+
*/
|
|
11
|
+
async function isDirectoryEmpty(dirPath: string): Promise<boolean> {
|
|
12
|
+
try {
|
|
13
|
+
const entries = await fs.readdir(dirPath)
|
|
14
|
+
return entries.length === 0
|
|
15
|
+
} catch (error) {
|
|
16
|
+
// If directory doesn't exist or can't be read, consider it empty
|
|
17
|
+
console.warn(`Could not read directory ${dirPath}:`, error)
|
|
18
|
+
return true
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Removes empty directories up the hierarchy until a non-empty directory is found
|
|
24
|
+
* @param dirPath - Starting directory path to clean up
|
|
25
|
+
* @param basePath - Base path to stop cleaning (e.g., features directory)
|
|
26
|
+
* @returns Promise<void>
|
|
27
|
+
*/
|
|
28
|
+
async function removeEmptyDirectoriesUp(dirPath: string, basePath: string): Promise<void> {
|
|
29
|
+
let currentPath = dirPath
|
|
30
|
+
|
|
31
|
+
// Keep going up the directory tree until we reach the base path
|
|
32
|
+
while (currentPath !== basePath && currentPath !== dirname(currentPath)) {
|
|
33
|
+
try {
|
|
34
|
+
// Check if current directory is empty
|
|
35
|
+
if (await isDirectoryEmpty(currentPath)) {
|
|
36
|
+
await fs.rmdir(currentPath)
|
|
37
|
+
console.log(`Removed empty directory: ${currentPath}`)
|
|
38
|
+
// Move up one level
|
|
39
|
+
currentPath = dirname(currentPath)
|
|
40
|
+
} else {
|
|
41
|
+
// Directory is not empty, stop cleaning
|
|
42
|
+
break
|
|
43
|
+
}
|
|
44
|
+
} catch (error) {
|
|
45
|
+
// If we can't remove the directory or it doesn't exist, stop
|
|
46
|
+
console.warn(`Could not remove directory ${currentPath}:`, error)
|
|
47
|
+
break
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Generates a Gherkin feature file for a test suite
|
|
54
|
+
* @param testSuiteId - The ID of the test suite
|
|
55
|
+
* @param testSuiteName - The name of the test suite
|
|
56
|
+
* @param testSuiteDescription - The description of the test suite
|
|
57
|
+
* @param moduleName - The name of the module the test suite belongs to
|
|
58
|
+
* @returns Promise<string> - The path to the generated feature file
|
|
59
|
+
*/
|
|
60
|
+
export async function generateFeatureFile(
|
|
61
|
+
testSuiteId: string,
|
|
62
|
+
testSuiteName: string,
|
|
63
|
+
testSuiteDescription?: string,
|
|
64
|
+
): Promise<string> {
|
|
65
|
+
try {
|
|
66
|
+
// Fetch test suite with test cases, steps, tags, and all modules for path building
|
|
67
|
+
const [testSuite, allModules] = await Promise.all([
|
|
68
|
+
prisma.testSuite.findUnique({
|
|
69
|
+
where: { id: testSuiteId },
|
|
70
|
+
include: {
|
|
71
|
+
testCases: {
|
|
72
|
+
include: {
|
|
73
|
+
steps: {
|
|
74
|
+
include: {
|
|
75
|
+
parameters: true,
|
|
76
|
+
},
|
|
77
|
+
orderBy: {
|
|
78
|
+
order: 'asc',
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
tags: true,
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
module: true,
|
|
85
|
+
tags: true,
|
|
86
|
+
},
|
|
87
|
+
}),
|
|
88
|
+
prisma.module.findMany(), // Get all modules to build hierarchy path
|
|
89
|
+
])
|
|
90
|
+
|
|
91
|
+
if (!testSuite) {
|
|
92
|
+
throw new Error(`Test suite with ID ${testSuiteId} not found`)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Build the module path for directory structure
|
|
96
|
+
const modulePath = buildModulePath(allModules, testSuite.module)
|
|
97
|
+
|
|
98
|
+
// Generate feature file content
|
|
99
|
+
const featureContent = generateFeatureContent(
|
|
100
|
+
testSuiteDescription || testSuiteName, // Use description as feature title, fallback to name
|
|
101
|
+
testSuite.testCases,
|
|
102
|
+
testSuite.tags,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
// Create the features directory with module path
|
|
106
|
+
const featuresBaseDir = join(process.cwd(), 'src', 'tests', 'features')
|
|
107
|
+
const moduleDir = join(featuresBaseDir, modulePath.substring(1)) // Remove leading slash
|
|
108
|
+
await fs.mkdir(moduleDir, { recursive: true })
|
|
109
|
+
|
|
110
|
+
// Generate a safe filename from the test suite name only
|
|
111
|
+
const safeFileName = generateSafeFileName(testSuiteName)
|
|
112
|
+
const featureFilePath = join(moduleDir, `${safeFileName}.feature`)
|
|
113
|
+
|
|
114
|
+
// Write the feature file
|
|
115
|
+
await fs.writeFile(featureFilePath, featureContent, 'utf8')
|
|
116
|
+
|
|
117
|
+
return featureFilePath
|
|
118
|
+
} catch (error) {
|
|
119
|
+
console.error('Error generating feature file:', error)
|
|
120
|
+
throw error
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Generates the content for a Gherkin feature file
|
|
126
|
+
*/
|
|
127
|
+
function generateFeatureContent(
|
|
128
|
+
featureTitle: string,
|
|
129
|
+
testCases: Array<{
|
|
130
|
+
title: string
|
|
131
|
+
description: string
|
|
132
|
+
steps: Array<{
|
|
133
|
+
gherkinStep: string
|
|
134
|
+
order: number
|
|
135
|
+
}>
|
|
136
|
+
tags?: Array<{
|
|
137
|
+
tagExpression: string
|
|
138
|
+
}>
|
|
139
|
+
}>,
|
|
140
|
+
testSuiteTags?: Array<{
|
|
141
|
+
tagExpression: string
|
|
142
|
+
}>,
|
|
143
|
+
): string {
|
|
144
|
+
const lines: string[] = []
|
|
145
|
+
|
|
146
|
+
// Warning header
|
|
147
|
+
lines.push('# AUTO-GENERATED FILE - DO NOT EDIT MANUALLY')
|
|
148
|
+
lines.push('# This file is automatically generated from Test Suite data.')
|
|
149
|
+
lines.push('# Any manual changes will be overwritten when the Test Suite is updated.')
|
|
150
|
+
lines.push('# To modify this feature, update the corresponding Test Suite in the application.')
|
|
151
|
+
lines.push('')
|
|
152
|
+
|
|
153
|
+
// Add feature-level tags (one tag per line)
|
|
154
|
+
if (testSuiteTags && testSuiteTags.length > 0) {
|
|
155
|
+
testSuiteTags.forEach(tag => {
|
|
156
|
+
lines.push(tag.tagExpression)
|
|
157
|
+
})
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Feature header - use description as feature title
|
|
161
|
+
lines.push(`Feature: ${featureTitle}`)
|
|
162
|
+
lines.push('')
|
|
163
|
+
|
|
164
|
+
// Get test suite tag expressions for deduplication
|
|
165
|
+
const testSuiteTagExpressions = new Set((testSuiteTags || []).map(tag => tag.tagExpression.toLowerCase()))
|
|
166
|
+
|
|
167
|
+
// Generate scenarios for each test case that has steps
|
|
168
|
+
let scenarioCount = 0
|
|
169
|
+
testCases.forEach(testCase => {
|
|
170
|
+
// Only generate scenario if test case has steps
|
|
171
|
+
if (testCase.steps && testCase.steps.length > 0) {
|
|
172
|
+
// Generate Gherkin steps from test case steps
|
|
173
|
+
const gherkinSteps = generateGherkinStepsFromTestCase(testCase.steps)
|
|
174
|
+
|
|
175
|
+
// Only add scenario if there are actual gherkin steps
|
|
176
|
+
if (gherkinSteps.length > 0) {
|
|
177
|
+
if (scenarioCount > 0) {
|
|
178
|
+
lines.push('') // Add blank line between scenarios
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Add scenario-level tags (skip if already present at feature level)
|
|
182
|
+
if (testCase.tags && testCase.tags.length > 0) {
|
|
183
|
+
testCase.tags.forEach(tag => {
|
|
184
|
+
// Only add tag if it's not already present at feature level
|
|
185
|
+
if (!testSuiteTagExpressions.has(tag.tagExpression.toLowerCase())) {
|
|
186
|
+
lines.push(` ${tag.tagExpression}`)
|
|
187
|
+
}
|
|
188
|
+
})
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
lines.push(` Scenario: [${testCase.title}] ${testCase.description}`)
|
|
192
|
+
|
|
193
|
+
gherkinSteps.forEach(step => {
|
|
194
|
+
lines.push(` ${step}`)
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
scenarioCount++
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
return lines.join('\n') + '\n'
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Generates Gherkin steps from test case steps using the same logic as the frontend
|
|
207
|
+
*/
|
|
208
|
+
function generateGherkinStepsFromTestCase(
|
|
209
|
+
steps: Array<{
|
|
210
|
+
gherkinStep: string
|
|
211
|
+
order: number
|
|
212
|
+
}>,
|
|
213
|
+
): string[] {
|
|
214
|
+
if (!steps || steps.length === 0) {
|
|
215
|
+
return []
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Sort steps by order
|
|
219
|
+
const sortedSteps = steps.sort((a, b) => a.order - b.order)
|
|
220
|
+
|
|
221
|
+
let hasThenInPrevious = false
|
|
222
|
+
let hasWhenInPrevious = false
|
|
223
|
+
|
|
224
|
+
return sortedSteps.map((step, index) => {
|
|
225
|
+
const gherkinStep = step.gherkinStep?.trim() || ''
|
|
226
|
+
const firstWord = gherkinStep.split(' ')[0].toLowerCase()
|
|
227
|
+
const hasGherkinKeyword = ['given', 'when', 'then', 'and', 'but'].includes(firstWord)
|
|
228
|
+
const stepWithoutKeyword = hasGherkinKeyword ? gherkinStep.split(' ').slice(1).join(' ') : gherkinStep
|
|
229
|
+
|
|
230
|
+
// First step always starts with Given
|
|
231
|
+
if (index === 0) {
|
|
232
|
+
return `Given ${stepWithoutKeyword}`
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Check if this step should be a Then statement
|
|
236
|
+
const isThenStatement =
|
|
237
|
+
firstWord === 'then' ||
|
|
238
|
+
stepWithoutKeyword.toLowerCase().startsWith('should') ||
|
|
239
|
+
stepWithoutKeyword.toLowerCase().startsWith('must') ||
|
|
240
|
+
stepWithoutKeyword.toLowerCase().startsWith('will')
|
|
241
|
+
|
|
242
|
+
// If we haven't seen a Then yet
|
|
243
|
+
if (!hasThenInPrevious) {
|
|
244
|
+
// If this is a Then statement
|
|
245
|
+
if (isThenStatement) {
|
|
246
|
+
hasThenInPrevious = true
|
|
247
|
+
return `Then ${stepWithoutKeyword}`
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// If we haven't seen a When yet, use When
|
|
251
|
+
if (!hasWhenInPrevious) {
|
|
252
|
+
hasWhenInPrevious = true
|
|
253
|
+
return `When ${stepWithoutKeyword}`
|
|
254
|
+
}
|
|
255
|
+
// After When, use And
|
|
256
|
+
return `And ${stepWithoutKeyword}`
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// After Then
|
|
260
|
+
if (isThenStatement) {
|
|
261
|
+
// If it's another Then statement, use And
|
|
262
|
+
return `And ${stepWithoutKeyword}`
|
|
263
|
+
}
|
|
264
|
+
// After Then, use When for new actions
|
|
265
|
+
hasThenInPrevious = false
|
|
266
|
+
hasWhenInPrevious = true
|
|
267
|
+
return `When ${stepWithoutKeyword}`
|
|
268
|
+
})
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Deletes a feature file for a test suite
|
|
273
|
+
* @param testSuiteId - The ID of the test suite
|
|
274
|
+
* @returns Promise<boolean> - True if file was deleted, false if file didn't exist
|
|
275
|
+
*/
|
|
276
|
+
export async function deleteFeatureFile(testSuiteId: string): Promise<boolean> {
|
|
277
|
+
try {
|
|
278
|
+
// Fetch test suite and all modules to build the correct path
|
|
279
|
+
const [testSuite, allModules] = await Promise.all([
|
|
280
|
+
prisma.testSuite.findUnique({
|
|
281
|
+
where: { id: testSuiteId },
|
|
282
|
+
include: {
|
|
283
|
+
module: true,
|
|
284
|
+
},
|
|
285
|
+
}),
|
|
286
|
+
prisma.module.findMany(),
|
|
287
|
+
])
|
|
288
|
+
|
|
289
|
+
if (!testSuite) {
|
|
290
|
+
console.warn(`Test suite with ID ${testSuiteId} not found for feature file deletion`)
|
|
291
|
+
return false
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Build the module path for directory structure
|
|
295
|
+
const modulePath = buildModulePath(allModules, testSuite.module)
|
|
296
|
+
const safeFileName = generateSafeFileName(testSuite.name)
|
|
297
|
+
|
|
298
|
+
const featuresBaseDir = join(process.cwd(), 'src', 'tests', 'features')
|
|
299
|
+
const moduleDir = join(featuresBaseDir, modulePath.substring(1)) // Remove leading slash
|
|
300
|
+
const featureFilePath = join(moduleDir, `${safeFileName}.feature`)
|
|
301
|
+
|
|
302
|
+
try {
|
|
303
|
+
await fs.unlink(featureFilePath)
|
|
304
|
+
console.log(`Feature file deleted: ${featureFilePath}`)
|
|
305
|
+
|
|
306
|
+
// Clean up empty directories up the module hierarchy
|
|
307
|
+
await removeEmptyDirectoriesUp(moduleDir, featuresBaseDir)
|
|
308
|
+
|
|
309
|
+
return true
|
|
310
|
+
} catch (error: unknown) {
|
|
311
|
+
if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT') {
|
|
312
|
+
console.warn(`Feature file not found for deletion: ${featureFilePath}`)
|
|
313
|
+
return false
|
|
314
|
+
}
|
|
315
|
+
throw error
|
|
316
|
+
}
|
|
317
|
+
} catch (error) {
|
|
318
|
+
console.error('Error deleting feature file:', error)
|
|
319
|
+
throw error
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Regenerates all feature files from the current database state
|
|
325
|
+
* This is useful after merging changes or database migrations to ensure sync
|
|
326
|
+
* @returns Promise<string[]> - Array of generated feature file paths
|
|
327
|
+
*/
|
|
328
|
+
export async function regenerateAllFeatureFiles(): Promise<string[]> {
|
|
329
|
+
try {
|
|
330
|
+
console.log('Starting regeneration of all feature files...')
|
|
331
|
+
|
|
332
|
+
// Clear existing feature files directory
|
|
333
|
+
const featuresBaseDir = join(process.cwd(), 'src', 'tests', 'features')
|
|
334
|
+
try {
|
|
335
|
+
await fs.rm(featuresBaseDir, { recursive: true, force: true })
|
|
336
|
+
} catch (error) {
|
|
337
|
+
console.warn('Could not clear features directory:', error)
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Fetch all test suites from database
|
|
341
|
+
const testSuites = await prisma.testSuite.findMany({
|
|
342
|
+
include: {
|
|
343
|
+
testCases: {
|
|
344
|
+
include: {
|
|
345
|
+
steps: {
|
|
346
|
+
include: {
|
|
347
|
+
parameters: true,
|
|
348
|
+
},
|
|
349
|
+
orderBy: {
|
|
350
|
+
order: 'asc',
|
|
351
|
+
},
|
|
352
|
+
},
|
|
353
|
+
tags: true,
|
|
354
|
+
},
|
|
355
|
+
},
|
|
356
|
+
module: true,
|
|
357
|
+
tags: true,
|
|
358
|
+
},
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
// Fetch all modules for path building
|
|
362
|
+
const allModules = await prisma.module.findMany()
|
|
363
|
+
|
|
364
|
+
const generatedFiles: string[] = []
|
|
365
|
+
|
|
366
|
+
// Generate feature file for each test suite
|
|
367
|
+
for (const testSuite of testSuites) {
|
|
368
|
+
try {
|
|
369
|
+
const modulePath = buildModulePath(allModules, testSuite.module)
|
|
370
|
+
const featureContent = generateFeatureContent(
|
|
371
|
+
testSuite.description || testSuite.name,
|
|
372
|
+
testSuite.testCases,
|
|
373
|
+
testSuite.tags,
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
// Create the features directory with module path
|
|
377
|
+
const moduleDir = join(featuresBaseDir, modulePath.substring(1)) // Remove leading slash
|
|
378
|
+
await fs.mkdir(moduleDir, { recursive: true })
|
|
379
|
+
|
|
380
|
+
// Generate filename and write file
|
|
381
|
+
const safeFileName = generateSafeFileName(testSuite.name)
|
|
382
|
+
const featureFilePath = join(moduleDir, `${safeFileName}.feature`)
|
|
383
|
+
|
|
384
|
+
await fs.writeFile(featureFilePath, featureContent, 'utf8')
|
|
385
|
+
generatedFiles.push(featureFilePath)
|
|
386
|
+
|
|
387
|
+
console.log(`Generated: ${featureFilePath}`)
|
|
388
|
+
} catch (error) {
|
|
389
|
+
console.error(`Error generating feature file for test suite ${testSuite.name}:`, error)
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
console.log(`Regeneration complete. Generated ${generatedFiles.length} feature files.`)
|
|
394
|
+
return generatedFiles
|
|
395
|
+
} catch (error) {
|
|
396
|
+
console.error('Error during feature files regeneration:', error)
|
|
397
|
+
throw error
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Generates a safe filename from test suite name
|
|
403
|
+
*/
|
|
404
|
+
function generateSafeFileName(testSuiteName: string): string {
|
|
405
|
+
// Convert to lowercase and replace spaces and special characters with hyphens
|
|
406
|
+
return testSuiteName
|
|
407
|
+
.toLowerCase()
|
|
408
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
409
|
+
.replace(/^-+|-+$/g, '') // Remove leading/trailing hyphens
|
|
410
|
+
.replace(/-+/g, '-') // Replace multiple consecutive hyphens with single hyphen
|
|
411
|
+
}
|