@sun-asterisk/sungen 1.0.24 → 2.0.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 +198 -74
- package/dist/cli/commands/add.d.ts.map +1 -1
- package/dist/cli/commands/add.js +5 -3
- package/dist/cli/commands/add.js.map +1 -1
- package/dist/cli/commands/generate.d.ts.map +1 -1
- package/dist/cli/commands/generate.js +110 -35
- package/dist/cli/commands/generate.js.map +1 -1
- package/dist/cli/index.d.ts +2 -2
- package/dist/cli/index.js +5 -16
- package/dist/cli/index.js.map +1 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/frame-enter-action.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/frame-exit-action.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/keyboard-global-action.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/scroll-action.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/table-action-in-row.hbs +2 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/table-cell-by-filter.hbs +3 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/table-cell-by-index.hbs +3 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/table-column-exists.hbs +2 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/table-empty.hbs +2 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/table-row-count.hbs +2 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/table-row-exists.hbs +2 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/table-row-not-exists.hbs +2 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/visible-with-value-assertion.hbs +1 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator-base.hbs +11 -2
- package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator.hbs +11 -2
- package/dist/generators/test-generator/code-generator.d.ts +0 -1
- package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
- package/dist/generators/test-generator/code-generator.js +0 -47
- package/dist/generators/test-generator/code-generator.js.map +1 -1
- package/dist/generators/test-generator/patterns/assertion-patterns.d.ts.map +1 -1
- package/dist/generators/test-generator/patterns/assertion-patterns.js +2 -0
- package/dist/generators/test-generator/patterns/assertion-patterns.js.map +1 -1
- package/dist/generators/test-generator/patterns/index.d.ts +4 -1
- package/dist/generators/test-generator/patterns/index.d.ts.map +1 -1
- package/dist/generators/test-generator/patterns/index.js +17 -5
- package/dist/generators/test-generator/patterns/index.js.map +1 -1
- package/dist/generators/test-generator/patterns/interaction-patterns.js +1 -1
- package/dist/generators/test-generator/patterns/interaction-patterns.js.map +1 -1
- package/dist/generators/test-generator/patterns/keyboard-patterns.d.ts +7 -0
- package/dist/generators/test-generator/patterns/keyboard-patterns.d.ts.map +1 -0
- package/dist/generators/test-generator/patterns/keyboard-patterns.js +47 -0
- package/dist/generators/test-generator/patterns/keyboard-patterns.js.map +1 -0
- package/dist/generators/test-generator/patterns/scope-patterns.d.ts +7 -0
- package/dist/generators/test-generator/patterns/scope-patterns.d.ts.map +1 -0
- package/dist/generators/test-generator/patterns/scope-patterns.js +36 -0
- package/dist/generators/test-generator/patterns/scope-patterns.js.map +1 -0
- package/dist/generators/test-generator/patterns/scroll-patterns.d.ts +7 -0
- package/dist/generators/test-generator/patterns/scroll-patterns.d.ts.map +1 -0
- package/dist/generators/test-generator/patterns/scroll-patterns.js +25 -0
- package/dist/generators/test-generator/patterns/scroll-patterns.js.map +1 -0
- package/dist/generators/test-generator/patterns/table-patterns.d.ts +7 -0
- package/dist/generators/test-generator/patterns/table-patterns.d.ts.map +1 -0
- package/dist/generators/test-generator/patterns/table-patterns.js +192 -0
- package/dist/generators/test-generator/patterns/table-patterns.js.map +1 -0
- package/dist/generators/test-generator/step-mapper.d.ts +1 -3
- package/dist/generators/test-generator/step-mapper.d.ts.map +1 -1
- package/dist/generators/test-generator/step-mapper.js +30 -27
- package/dist/generators/test-generator/step-mapper.js.map +1 -1
- package/dist/generators/test-generator/template-engine.d.ts.map +1 -1
- package/dist/generators/test-generator/template-engine.js +4 -1
- package/dist/generators/test-generator/template-engine.js.map +1 -1
- package/dist/generators/test-generator/types.d.ts +7 -24
- package/dist/generators/test-generator/types.d.ts.map +1 -1
- package/dist/generators/test-generator/types.js +2 -101
- package/dist/generators/test-generator/types.js.map +1 -1
- package/dist/generators/test-generator/utils/selector-resolver.d.ts +14 -0
- package/dist/generators/test-generator/utils/selector-resolver.d.ts.map +1 -1
- package/dist/generators/test-generator/utils/selector-resolver.js +37 -11
- package/dist/generators/test-generator/utils/selector-resolver.js.map +1 -1
- package/dist/orchestrator/project-initializer.d.ts +8 -0
- package/dist/orchestrator/project-initializer.d.ts.map +1 -1
- package/dist/orchestrator/project-initializer.js +343 -32
- package/dist/orchestrator/project-initializer.js.map +1 -1
- package/dist/orchestrator/screen-manager.d.ts +1 -32
- package/dist/orchestrator/screen-manager.d.ts.map +1 -1
- package/dist/orchestrator/screen-manager.js +55 -216
- package/dist/orchestrator/screen-manager.js.map +1 -1
- package/dist/utils/selector-types.d.ts +1 -1
- package/dist/utils/selector-types.d.ts.map +1 -1
- package/dist/utils/selector-types.js +3 -0
- package/dist/utils/selector-types.js.map +1 -1
- package/package.json +2 -2
- package/src/cli/commands/add.ts +5 -3
- package/src/cli/commands/generate.ts +90 -38
- package/src/cli/index.ts +5 -16
- package/src/generators/test-generator/adapters/playwright/templates/steps/actions/frame-enter-action.hbs +1 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/actions/frame-exit-action.hbs +1 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/actions/keyboard-global-action.hbs +1 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/actions/scroll-action.hbs +1 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/actions/table-action-in-row.hbs +2 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/table-cell-by-filter.hbs +3 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/table-cell-by-index.hbs +3 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/table-column-exists.hbs +2 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/table-empty.hbs +2 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/table-row-count.hbs +2 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/table-row-exists.hbs +2 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/table-row-not-exists.hbs +2 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/visible-with-value-assertion.hbs +1 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator-base.hbs +11 -2
- package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator.hbs +11 -2
- package/src/generators/test-generator/code-generator.ts +0 -59
- package/src/generators/test-generator/patterns/assertion-patterns.ts +2 -0
- package/src/generators/test-generator/patterns/index.ts +12 -3
- package/src/generators/test-generator/patterns/interaction-patterns.ts +1 -1
- package/src/generators/test-generator/patterns/keyboard-patterns.ts +51 -0
- package/src/generators/test-generator/patterns/scope-patterns.ts +40 -0
- package/src/generators/test-generator/patterns/scroll-patterns.ts +27 -0
- package/src/generators/test-generator/patterns/table-patterns.ts +232 -0
- package/src/generators/test-generator/step-mapper.ts +33 -27
- package/src/generators/test-generator/template-engine.ts +3 -1
- package/src/generators/test-generator/types.ts +7 -112
- package/src/generators/test-generator/utils/selector-resolver.ts +70 -25
- package/src/orchestrator/project-initializer.ts +345 -32
- package/src/orchestrator/screen-manager.ts +61 -233
- package/src/utils/selector-types.ts +3 -0
- package/dist/cli/commands/cache-clear.d.ts +0 -3
- package/dist/cli/commands/cache-clear.d.ts.map +0 -1
- package/dist/cli/commands/cache-clear.js +0 -24
- package/dist/cli/commands/cache-clear.js.map +0 -1
- package/dist/cli/commands/full.d.ts +0 -3
- package/dist/cli/commands/full.d.ts.map +0 -1
- package/dist/cli/commands/full.js +0 -37
- package/dist/cli/commands/full.js.map +0 -1
- package/dist/cli/commands/live-scan.d.ts +0 -3
- package/dist/cli/commands/live-scan.d.ts.map +0 -1
- package/dist/cli/commands/live-scan.js +0 -78
- package/dist/cli/commands/live-scan.js.map +0 -1
- package/dist/cli/commands/map.d.ts +0 -3
- package/dist/cli/commands/map.d.ts.map +0 -1
- package/dist/cli/commands/map.js +0 -93
- package/dist/cli/commands/map.js.map +0 -1
- package/dist/cli/commands/validate.d.ts +0 -3
- package/dist/cli/commands/validate.d.ts.map +0 -1
- package/dist/cli/commands/validate.js +0 -43
- package/dist/cli/commands/validate.js.map +0 -1
- package/dist/cli/utils.d.ts +0 -6
- package/dist/cli/utils.d.ts.map +0 -1
- package/dist/cli/utils.js +0 -101
- package/dist/cli/utils.js.map +0 -1
- package/dist/config/config-loader.d.ts +0 -51
- package/dist/config/config-loader.d.ts.map +0 -1
- package/dist/config/config-loader.js +0 -216
- package/dist/config/config-loader.js.map +0 -1
- package/dist/config/config-schema.d.ts +0 -121
- package/dist/config/config-schema.d.ts.map +0 -1
- package/dist/config/config-schema.js +0 -7
- package/dist/config/config-schema.js.map +0 -1
- package/dist/core/live-scanner/config-reader.d.ts +0 -10
- package/dist/core/live-scanner/config-reader.d.ts.map +0 -1
- package/dist/core/live-scanner/config-reader.js +0 -87
- package/dist/core/live-scanner/config-reader.js.map +0 -1
- package/dist/core/live-scanner/element-finder.d.ts +0 -20
- package/dist/core/live-scanner/element-finder.d.ts.map +0 -1
- package/dist/core/live-scanner/element-finder.js +0 -481
- package/dist/core/live-scanner/element-finder.js.map +0 -1
- package/dist/core/live-scanner/index.d.ts +0 -8
- package/dist/core/live-scanner/index.d.ts.map +0 -1
- package/dist/core/live-scanner/index.js +0 -33
- package/dist/core/live-scanner/index.js.map +0 -1
- package/dist/core/live-scanner/matrix-reader.d.ts +0 -17
- package/dist/core/live-scanner/matrix-reader.d.ts.map +0 -1
- package/dist/core/live-scanner/matrix-reader.js +0 -60
- package/dist/core/live-scanner/matrix-reader.js.map +0 -1
- package/dist/core/live-scanner/matrix-writer.d.ts +0 -7
- package/dist/core/live-scanner/matrix-writer.d.ts.map +0 -1
- package/dist/core/live-scanner/matrix-writer.js +0 -103
- package/dist/core/live-scanner/matrix-writer.js.map +0 -1
- package/dist/core/live-scanner/role-fallback.d.ts +0 -15
- package/dist/core/live-scanner/role-fallback.d.ts.map +0 -1
- package/dist/core/live-scanner/role-fallback.js +0 -46
- package/dist/core/live-scanner/role-fallback.js.map +0 -1
- package/dist/core/live-scanner/scanner.d.ts +0 -22
- package/dist/core/live-scanner/scanner.d.ts.map +0 -1
- package/dist/core/live-scanner/scanner.js +0 -303
- package/dist/core/live-scanner/scanner.js.map +0 -1
- package/dist/core/live-scanner/step-replayer.d.ts +0 -26
- package/dist/core/live-scanner/step-replayer.d.ts.map +0 -1
- package/dist/core/live-scanner/step-replayer.js +0 -473
- package/dist/core/live-scanner/step-replayer.js.map +0 -1
- package/dist/core/live-scanner/types.d.ts +0 -52
- package/dist/core/live-scanner/types.d.ts.map +0 -1
- package/dist/core/live-scanner/types.js +0 -14
- package/dist/core/live-scanner/types.js.map +0 -1
- package/dist/core/selector-base/annotation-handler.d.ts +0 -45
- package/dist/core/selector-base/annotation-handler.d.ts.map +0 -1
- package/dist/core/selector-base/annotation-handler.js +0 -102
- package/dist/core/selector-base/annotation-handler.js.map +0 -1
- package/dist/core/selector-base/base-generator.d.ts +0 -49
- package/dist/core/selector-base/base-generator.d.ts.map +0 -1
- package/dist/core/selector-base/base-generator.js +0 -214
- package/dist/core/selector-base/base-generator.js.map +0 -1
- package/dist/core/selector-base/gherkin-parser.d.ts +0 -24
- package/dist/core/selector-base/gherkin-parser.d.ts.map +0 -1
- package/dist/core/selector-base/gherkin-parser.js +0 -42
- package/dist/core/selector-base/gherkin-parser.js.map +0 -1
- package/dist/core/selector-mapper/priority-mapper.d.ts +0 -74
- package/dist/core/selector-mapper/priority-mapper.d.ts.map +0 -1
- package/dist/core/selector-mapper/priority-mapper.js +0 -477
- package/dist/core/selector-mapper/priority-mapper.js.map +0 -1
- package/dist/core/ui-scanner/heuristics/base-heuristic.d.ts +0 -91
- package/dist/core/ui-scanner/heuristics/base-heuristic.d.ts.map +0 -1
- package/dist/core/ui-scanner/heuristics/base-heuristic.js +0 -175
- package/dist/core/ui-scanner/heuristics/base-heuristic.js.map +0 -1
- package/dist/core/ui-scanner/react-scanner.d.ts +0 -32
- package/dist/core/ui-scanner/react-scanner.d.ts.map +0 -1
- package/dist/core/ui-scanner/react-scanner.js +0 -163
- package/dist/core/ui-scanner/react-scanner.js.map +0 -1
- package/dist/core/ui-scanner/scanner-interface.d.ts +0 -94
- package/dist/core/ui-scanner/scanner-interface.d.ts.map +0 -1
- package/dist/core/ui-scanner/scanner-interface.js +0 -33
- package/dist/core/ui-scanner/scanner-interface.js.map +0 -1
- package/dist/core/ui-scanner/strict-scanner.d.ts +0 -81
- package/dist/core/ui-scanner/strict-scanner.d.ts.map +0 -1
- package/dist/core/ui-scanner/strict-scanner.js +0 -511
- package/dist/core/ui-scanner/strict-scanner.js.map +0 -1
- package/dist/core/validator/data-validator.d.ts +0 -38
- package/dist/core/validator/data-validator.d.ts.map +0 -1
- package/dist/core/validator/data-validator.js +0 -212
- package/dist/core/validator/data-validator.js.map +0 -1
- package/dist/core/validator/feature-validator.d.ts +0 -27
- package/dist/core/validator/feature-validator.d.ts.map +0 -1
- package/dist/core/validator/feature-validator.js +0 -182
- package/dist/core/validator/feature-validator.js.map +0 -1
- package/dist/core/validator/index.d.ts +0 -46
- package/dist/core/validator/index.d.ts.map +0 -1
- package/dist/core/validator/index.js +0 -17
- package/dist/core/validator/index.js.map +0 -1
- package/dist/core/validator/screen-validator.d.ts +0 -35
- package/dist/core/validator/screen-validator.d.ts.map +0 -1
- package/dist/core/validator/screen-validator.js +0 -195
- package/dist/core/validator/screen-validator.js.map +0 -1
- package/dist/core/validator/selector-validator.d.ts +0 -36
- package/dist/core/validator/selector-validator.d.ts.map +0 -1
- package/dist/core/validator/selector-validator.js +0 -210
- package/dist/core/validator/selector-validator.js.map +0 -1
- package/dist/external/ai-provider.d.ts +0 -60
- package/dist/external/ai-provider.d.ts.map +0 -1
- package/dist/external/ai-provider.js +0 -30
- package/dist/external/ai-provider.js.map +0 -1
- package/dist/external/anthropic-provider.d.ts +0 -29
- package/dist/external/anthropic-provider.d.ts.map +0 -1
- package/dist/external/anthropic-provider.js +0 -85
- package/dist/external/anthropic-provider.js.map +0 -1
- package/dist/generators/cache/cache-manager.d.ts +0 -66
- package/dist/generators/cache/cache-manager.d.ts.map +0 -1
- package/dist/generators/cache/cache-manager.js +0 -286
- package/dist/generators/cache/cache-manager.js.map +0 -1
- package/dist/generators/dsl-writer/index.d.ts +0 -33
- package/dist/generators/dsl-writer/index.d.ts.map +0 -1
- package/dist/generators/dsl-writer/index.js +0 -226
- package/dist/generators/dsl-writer/index.js.map +0 -1
- package/dist/generators/scaffold-generator/index.d.ts +0 -162
- package/dist/generators/scaffold-generator/index.d.ts.map +0 -1
- package/dist/generators/scaffold-generator/index.js +0 -877
- package/dist/generators/scaffold-generator/index.js.map +0 -1
- package/dist/generators/selector-mapper/ai-mapper.d.ts +0 -56
- package/dist/generators/selector-mapper/ai-mapper.d.ts.map +0 -1
- package/dist/generators/selector-mapper/ai-mapper.js +0 -457
- package/dist/generators/selector-mapper/ai-mapper.js.map +0 -1
- package/dist/generators/selector-mapper/hybrid-mapper.d.ts +0 -67
- package/dist/generators/selector-mapper/hybrid-mapper.d.ts.map +0 -1
- package/dist/generators/selector-mapper/hybrid-mapper.js +0 -349
- package/dist/generators/selector-mapper/hybrid-mapper.js.map +0 -1
- package/dist/generators/selector-mapper/index.d.ts +0 -8
- package/dist/generators/selector-mapper/index.d.ts.map +0 -1
- package/dist/generators/selector-mapper/index.js +0 -12
- package/dist/generators/selector-mapper/index.js.map +0 -1
- package/dist/generators/selector-mapper/intelligent-mapper.d.ts +0 -125
- package/dist/generators/selector-mapper/intelligent-mapper.d.ts.map +0 -1
- package/dist/generators/selector-mapper/intelligent-mapper.js +0 -391
- package/dist/generators/selector-mapper/intelligent-mapper.js.map +0 -1
- package/dist/generators/test-generator/ai-step-mapper.d.ts +0 -27
- package/dist/generators/test-generator/ai-step-mapper.d.ts.map +0 -1
- package/dist/generators/test-generator/ai-step-mapper.js +0 -204
- package/dist/generators/test-generator/ai-step-mapper.js.map +0 -1
- package/dist/generators/test-generator/auth-setup-generator.d.ts +0 -18
- package/dist/generators/test-generator/auth-setup-generator.d.ts.map +0 -1
- package/dist/generators/test-generator/auth-setup-generator.js +0 -82
- package/dist/generators/test-generator/auth-setup-generator.js.map +0 -1
- package/dist/generators/test-generator/patterns/legacy-patterns.d.ts +0 -7
- package/dist/generators/test-generator/patterns/legacy-patterns.d.ts.map +0 -1
- package/dist/generators/test-generator/patterns/legacy-patterns.js +0 -98
- package/dist/generators/test-generator/patterns/legacy-patterns.js.map +0 -1
- package/dist/generators/test-generator/templates/auth-setup.ts.hbs +0 -36
- package/dist/generators/ui-model-builder/deep-scanner.d.ts +0 -121
- package/dist/generators/ui-model-builder/deep-scanner.d.ts.map +0 -1
- package/dist/generators/ui-model-builder/deep-scanner.js +0 -1113
- package/dist/generators/ui-model-builder/deep-scanner.js.map +0 -1
- package/dist/generators/ui-model-builder/enhanced-deep-scanner.d.ts +0 -110
- package/dist/generators/ui-model-builder/enhanced-deep-scanner.d.ts.map +0 -1
- package/dist/generators/ui-model-builder/enhanced-deep-scanner.js +0 -608
- package/dist/generators/ui-model-builder/enhanced-deep-scanner.js.map +0 -1
- package/dist/generators/ui-model-builder/react-scanner.d.ts +0 -107
- package/dist/generators/ui-model-builder/react-scanner.d.ts.map +0 -1
- package/dist/generators/ui-model-builder/react-scanner.js +0 -797
- package/dist/generators/ui-model-builder/react-scanner.js.map +0 -1
- package/dist/orchestrator/cache-manager.d.ts +0 -15
- package/dist/orchestrator/cache-manager.d.ts.map +0 -1
- package/dist/orchestrator/cache-manager.js +0 -62
- package/dist/orchestrator/cache-manager.js.map +0 -1
- package/dist/orchestrator/pipeline.d.ts +0 -56
- package/dist/orchestrator/pipeline.d.ts.map +0 -1
- package/dist/orchestrator/pipeline.js +0 -298
- package/dist/orchestrator/pipeline.js.map +0 -1
- package/dist/orchestrator/reporter.d.ts +0 -15
- package/dist/orchestrator/reporter.d.ts.map +0 -1
- package/dist/orchestrator/reporter.js +0 -30
- package/dist/orchestrator/reporter.js.map +0 -1
- package/src/cli/commands/cache-clear.ts +0 -22
- package/src/cli/commands/full.ts +0 -35
- package/src/cli/commands/live-scan.ts +0 -82
- package/src/cli/commands/map.ts +0 -97
- package/src/cli/commands/validate.ts +0 -43
- package/src/cli/utils.ts +0 -106
- package/src/config/ai-providers.yaml +0 -56
- package/src/config/config-loader.ts +0 -248
- package/src/config/config-schema.ts +0 -148
- package/src/config/default.config.yaml +0 -107
- package/src/config/framework.config.yaml +0 -52
- package/src/config/routes.yaml +0 -31
- package/src/core/live-scanner/config-reader.ts +0 -57
- package/src/core/live-scanner/element-finder.ts +0 -534
- package/src/core/live-scanner/index.ts +0 -7
- package/src/core/live-scanner/matrix-reader.ts +0 -65
- package/src/core/live-scanner/matrix-writer.ts +0 -77
- package/src/core/live-scanner/role-fallback.ts +0 -44
- package/src/core/live-scanner/scanner.ts +0 -321
- package/src/core/live-scanner/step-replayer.ts +0 -503
- package/src/core/live-scanner/types.ts +0 -58
- package/src/core/selector-base/annotation-handler.ts +0 -127
- package/src/core/selector-base/base-generator.ts +0 -234
- package/src/core/selector-base/gherkin-parser.ts +0 -57
- package/src/core/selector-mapper/priority-mapper.ts +0 -607
- package/src/core/ui-scanner/heuristics/base-heuristic.ts +0 -216
- package/src/core/ui-scanner/react-scanner.ts +0 -156
- package/src/core/ui-scanner/scanner-interface.ts +0 -133
- package/src/core/ui-scanner/strict-scanner.ts +0 -629
- package/src/core/validator/data-validator.ts +0 -202
- package/src/core/validator/feature-validator.ts +0 -176
- package/src/core/validator/index.ts +0 -57
- package/src/core/validator/screen-validator.ts +0 -209
- package/src/core/validator/selector-validator.ts +0 -209
- package/src/external/ai-provider.ts +0 -90
- package/src/external/anthropic-provider.ts +0 -114
- package/src/generators/README.md +0 -410
- package/src/generators/cache/cache-manager.ts +0 -322
- package/src/generators/dsl-writer/index.ts +0 -253
- package/src/generators/scaffold-generator/index.ts +0 -1029
- package/src/generators/selector-mapper/ai-mapper.ts +0 -528
- package/src/generators/selector-mapper/hybrid-mapper.ts +0 -427
- package/src/generators/selector-mapper/index.ts +0 -10
- package/src/generators/selector-mapper/intelligent-mapper.ts +0 -530
- package/src/generators/test-generator/ai-step-mapper.ts +0 -224
- package/src/generators/test-generator/auth-setup-generator.ts +0 -59
- package/src/generators/test-generator/patterns/legacy-patterns.ts +0 -104
- package/src/generators/test-generator/templates/auth-setup.ts.hbs +0 -36
- package/src/generators/ui-model-builder/deep-scanner.ts +0 -1244
- package/src/generators/ui-model-builder/enhanced-deep-scanner.ts +0 -731
- package/src/generators/ui-model-builder/react-scanner.ts +0 -959
- package/src/orchestrator/cache-manager.ts +0 -32
- package/src/orchestrator/pipeline.ts +0 -354
- package/src/orchestrator/reporter.ts +0 -36
|
@@ -1,1029 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Scaffold Generator
|
|
3
|
-
* Parses Gherkin feature files and generates selector YAML scaffolds
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import fs from 'fs';
|
|
7
|
-
import path from 'path';
|
|
8
|
-
import yaml from 'yaml';
|
|
9
|
-
import { glob } from 'glob';
|
|
10
|
-
import { SelectorResolver } from '../test-generator/utils/selector-resolver';
|
|
11
|
-
import { SelectorType } from '../../utils/selector-types';
|
|
12
|
-
|
|
13
|
-
export interface ScaffoldElement {
|
|
14
|
-
locator: string;
|
|
15
|
-
type: SelectorType;
|
|
16
|
-
value: string;
|
|
17
|
-
name?: string; // For role type (accessible name), or label text
|
|
18
|
-
nth: number;
|
|
19
|
-
exact?: boolean; // Whether to use exact matching (default: false)
|
|
20
|
-
inputMethod?: string; // 'pressSequentially' for contenteditable/rich-text elements
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export interface ScaffoldResult {
|
|
24
|
-
[key: string]: ScaffoldElement;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export interface TestDataResult {
|
|
28
|
-
[key: string]: string | TestDataResult;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export interface MapResult {
|
|
32
|
-
featureFile: string;
|
|
33
|
-
selectorsPath: string;
|
|
34
|
-
testDataPath: string;
|
|
35
|
-
elementCount: number;
|
|
36
|
-
dataRefCount: number;
|
|
37
|
-
selectorsSkipped?: boolean; // True if selectors file already existed (and force was not set)
|
|
38
|
-
testDataMerged?: boolean; // True if test-data file existed and was smart-merged
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
interface ExtractedDataRef {
|
|
42
|
-
rawRef: string; // Original ref e.g., "valid_login_email" or "valid.login_password"
|
|
43
|
-
lineText: string; // Full line for context
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
interface ExtractedElement {
|
|
47
|
-
rawText: string; // Original text in brackets e.g., "Email Address"
|
|
48
|
-
key: string; // Converted key e.g., "email.address"
|
|
49
|
-
action: string; // Action type: fill, click, see, open, select
|
|
50
|
-
targetType: string; // Target type from Gherkin: button, link, field, radio, dropdown, text, etc.
|
|
51
|
-
lineText: string; // Full line for context
|
|
52
|
-
nth: number; // Index from "field 1", "button 2", etc. (0 = no index specified)
|
|
53
|
-
featurePath?: string; // NEW: Feature path for page type (e.g., "/" or "/dashboard")
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export class ScaffoldGenerator {
|
|
57
|
-
/**
|
|
58
|
-
* Generate scaffold YAML from a feature file
|
|
59
|
-
*/
|
|
60
|
-
generateFromFile(featureFilePath: string): ScaffoldResult {
|
|
61
|
-
const content = fs.readFileSync(featureFilePath, 'utf-8');
|
|
62
|
-
return this.generateFromContent(content);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Generate scaffold YAML from feature content
|
|
67
|
-
*/
|
|
68
|
-
generateFromContent(content: string): ScaffoldResult {
|
|
69
|
-
// Extract feature path from Gherkin metadata (e.g., "Path: login-guest?user=inactive")
|
|
70
|
-
const featurePath = this.extractFeaturePath(content);
|
|
71
|
-
|
|
72
|
-
const elements = this.extractElements(content, featurePath);
|
|
73
|
-
return this.buildScaffold(elements);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Extract elements from Gherkin content with their actions
|
|
78
|
-
*/
|
|
79
|
-
private extractElements(content: string, featurePath?: string): ExtractedElement[] {
|
|
80
|
-
const elements: ExtractedElement[] = [];
|
|
81
|
-
const lines = content.split('\n');
|
|
82
|
-
|
|
83
|
-
// Regex to match elements in brackets [...] with optional nth index
|
|
84
|
-
// Captures: [Element] field 1, [Element] button 2, [Element] text 3, etc.
|
|
85
|
-
const elementRegex = /\[([^\]]+)\]/g;
|
|
86
|
-
|
|
87
|
-
for (const line of lines) {
|
|
88
|
-
const trimmedLine = line.trim();
|
|
89
|
-
|
|
90
|
-
// Skip comments and empty lines
|
|
91
|
-
if (trimmedLine.startsWith('#') || !trimmedLine) {
|
|
92
|
-
continue;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// Determine action from the line
|
|
96
|
-
const action = this.detectAction(trimmedLine);
|
|
97
|
-
|
|
98
|
-
let match: RegExpExecArray | null;
|
|
99
|
-
while ((match = elementRegex.exec(trimmedLine)) !== null) {
|
|
100
|
-
const rawText = match[1];
|
|
101
|
-
const baseKey = SelectorResolver.generateKey(rawText);
|
|
102
|
-
|
|
103
|
-
// Extract nth index and target type from text after the element reference
|
|
104
|
-
// Matches patterns like: [Email] field 1, [Submit] button 2, [Name] 3
|
|
105
|
-
const afterElement = trimmedLine.slice(match.index + match[0].length);
|
|
106
|
-
const nth = SelectorResolver.extractNthFromStep(afterElement);
|
|
107
|
-
const targetType = this.extractTargetType(afterElement, action);
|
|
108
|
-
|
|
109
|
-
// Append nth to key so elements at different positions get distinct keys
|
|
110
|
-
// e.g., [Hãy gửi lời cảm ơn] field 3 → "hay.gui.loi.cam.on--3"
|
|
111
|
-
const key = nth > 0 ? `${baseKey}--${nth}` : baseKey;
|
|
112
|
-
|
|
113
|
-
// Check if element starts with "target" (semantic indicator for dynamic content)
|
|
114
|
-
// e.g., [target order], [Target Item] → element should have empty value
|
|
115
|
-
const isTargetElement = /^target\s/i.test(rawText);
|
|
116
|
-
|
|
117
|
-
elements.push({
|
|
118
|
-
rawText: isTargetElement ? '' : rawText, // Empty value when element is a target element
|
|
119
|
-
key,
|
|
120
|
-
action,
|
|
121
|
-
targetType,
|
|
122
|
-
lineText: trimmedLine,
|
|
123
|
-
nth,
|
|
124
|
-
featurePath: targetType === 'page' ? featurePath : undefined,
|
|
125
|
-
});
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
return elements;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* Extract target type from text after element reference
|
|
134
|
-
* e.g., " button" -> "button", " link 1" -> "link", " field with" -> "field"
|
|
135
|
-
*/
|
|
136
|
-
private extractTargetType(text: string, action: string): string {
|
|
137
|
-
const lowerText = text.toLowerCase().trim();
|
|
138
|
-
|
|
139
|
-
// NEW: Check for 'page' keyword FIRST (highest priority)
|
|
140
|
-
if (/^page\b/.test(lowerText) || /^\d*\s*page\b/.test(lowerText)) {
|
|
141
|
-
return 'page';
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// Check for specific target types in order of specificity
|
|
145
|
-
if (/^link\b/.test(lowerText) || /^\d*\s*link\b/.test(lowerText)) {
|
|
146
|
-
return 'link';
|
|
147
|
-
}
|
|
148
|
-
if (/^button\b/.test(lowerText) || /^\d*\s*button\b/.test(lowerText)) {
|
|
149
|
-
return 'button';
|
|
150
|
-
}
|
|
151
|
-
if (/^radio\b/.test(lowerText) || /^\d*\s*radio\b/.test(lowerText)) {
|
|
152
|
-
return 'radio';
|
|
153
|
-
}
|
|
154
|
-
if (/^dropdown\b/.test(lowerText) || /^\d*\s*dropdown\b/.test(lowerText) || /^select\b/.test(lowerText)) {
|
|
155
|
-
return 'dropdown';
|
|
156
|
-
}
|
|
157
|
-
if (/^checkbox\b/.test(lowerText) || /^\d*\s*checkbox\b/.test(lowerText)) {
|
|
158
|
-
return 'checkbox';
|
|
159
|
-
}
|
|
160
|
-
if (/^field\b/.test(lowerText) || /^\d*\s*field\b/.test(lowerText) || /^input\b/.test(lowerText)) {
|
|
161
|
-
return 'field';
|
|
162
|
-
}
|
|
163
|
-
if (/^textbox\b/.test(lowerText) || /^\d*\s*textbox\b/.test(lowerText)) {
|
|
164
|
-
return 'textbox';
|
|
165
|
-
}
|
|
166
|
-
if (/^textarea\b/.test(lowerText) || /^\d*\s*textarea\b/.test(lowerText)) {
|
|
167
|
-
return 'textarea';
|
|
168
|
-
}
|
|
169
|
-
if (/^editor\b/.test(lowerText) || /^\d*\s*editor\b/.test(lowerText)) {
|
|
170
|
-
return 'editor';
|
|
171
|
-
}
|
|
172
|
-
if (/^text\b/.test(lowerText) || /^\d*\s*text\b/.test(lowerText)) {
|
|
173
|
-
return 'text';
|
|
174
|
-
}
|
|
175
|
-
if (/^label\b/.test(lowerText) || /^\d*\s*label\b/.test(lowerText)) {
|
|
176
|
-
return 'label';
|
|
177
|
-
}
|
|
178
|
-
if (/^uploader\b/.test(lowerText) || /^\d*\s*uploader\b/.test(lowerText)) {
|
|
179
|
-
return 'uploader';
|
|
180
|
-
}
|
|
181
|
-
if (/^element\b/.test(lowerText) || /^\d*\s*element\b/.test(lowerText)) {
|
|
182
|
-
return 'element';
|
|
183
|
-
}
|
|
184
|
-
if (/^(column|columnheader)\b/.test(lowerText) || /^\d*\s*(column|columnheader)\b/.test(lowerText)) {
|
|
185
|
-
return 'column';
|
|
186
|
-
}
|
|
187
|
-
if (/^(logo|image|img|icon)\b/.test(lowerText) || /^\d*\s*(logo|image|img|icon)\b/.test(lowerText)) {
|
|
188
|
-
return 'img';
|
|
189
|
-
}
|
|
190
|
-
if (/^(option|item|listitem)\b/.test(lowerText) || /^\d*\s*(option|item|listitem)\b/.test(lowerText)) {
|
|
191
|
-
return 'option';
|
|
192
|
-
}
|
|
193
|
-
if (/^(dialog|modal)\b/.test(lowerText) || /^\d*\s*(dialog|modal)\b/.test(lowerText)) {
|
|
194
|
-
return 'dialog';
|
|
195
|
-
}
|
|
196
|
-
if (/^(heading|header)\b/.test(lowerText) || /^\d*\s*(heading|header)\b/.test(lowerText)) {
|
|
197
|
-
return 'heading';
|
|
198
|
-
}
|
|
199
|
-
if (/^(title|caption|message)\b/.test(lowerText) || /^\d*\s*(title|caption|message)\b/.test(lowerText)) {
|
|
200
|
-
return 'text';
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// Default based on action
|
|
204
|
-
switch (action) {
|
|
205
|
-
case 'click':
|
|
206
|
-
return 'text'; // Default click to text
|
|
207
|
-
case 'fill':
|
|
208
|
-
return 'field'; // Default fill to field
|
|
209
|
-
case 'see':
|
|
210
|
-
return 'text'; // Default see to text
|
|
211
|
-
case 'select':
|
|
212
|
-
return 'dropdown'; // Default select to dropdown
|
|
213
|
-
default:
|
|
214
|
-
return 'element';
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
* Extract nth index from text after element reference
|
|
220
|
-
* e.g., " field 1 with" -> 1, " button 2" -> 2, " row 1" -> 1, " text" -> 0
|
|
221
|
-
*/
|
|
222
|
-
/**
|
|
223
|
-
* Detect the action type from a Gherkin step line
|
|
224
|
-
*/
|
|
225
|
-
private detectAction(line: string): string {
|
|
226
|
-
const lowerLine = line.toLowerCase();
|
|
227
|
-
|
|
228
|
-
// Check for specific action keywords
|
|
229
|
-
// Order matters: check "see" before "fill" because text like "fill in all fields" may appear in assertions
|
|
230
|
-
if (/\b(see|verify|check|expect|should see|visible|displayed)\b/.test(lowerLine)) {
|
|
231
|
-
return 'see';
|
|
232
|
-
}
|
|
233
|
-
if (/\b(fill|enter|type|input)\b/.test(lowerLine)) {
|
|
234
|
-
return 'fill';
|
|
235
|
-
}
|
|
236
|
-
if (/\b(click|press|tap|submit)\b/.test(lowerLine)) {
|
|
237
|
-
return 'click';
|
|
238
|
-
}
|
|
239
|
-
if (/\b(open|navigate|go to|visit)\b/.test(lowerLine)) {
|
|
240
|
-
return 'open';
|
|
241
|
-
}
|
|
242
|
-
if (/\b(select|choose|pick)\b/.test(lowerLine)) {
|
|
243
|
-
return 'select';
|
|
244
|
-
}
|
|
245
|
-
if (/\b(wait|wait for|waits for)\b/.test(lowerLine)) {
|
|
246
|
-
return 'wait';
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
// Default to placeholder for unknown actions
|
|
250
|
-
return 'default';
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
/**
|
|
254
|
-
* Extract feature path from Gherkin metadata
|
|
255
|
-
* Looks for "Path: <value>" line in the feature file
|
|
256
|
-
* e.g., "Path: login-guest?user=inactive" -> "login-guest?user=inactive"
|
|
257
|
-
*/
|
|
258
|
-
private extractFeaturePath(content: string): string | undefined {
|
|
259
|
-
const lines = content.split('\n');
|
|
260
|
-
|
|
261
|
-
for (const line of lines) {
|
|
262
|
-
const trimmedLine = line.trim();
|
|
263
|
-
|
|
264
|
-
// Look for Path: pattern (case-insensitive)
|
|
265
|
-
const match = trimmedLine.match(/^Path:\s*(.+?)$/i);
|
|
266
|
-
if (match) {
|
|
267
|
-
return match[1].trim();
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
return undefined;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
/**
|
|
275
|
-
* Convert element text to YAML key format
|
|
276
|
-
* "Email Address" -> "email.address"
|
|
277
|
-
* "Your email or password is incorrect!" -> "your.email.or.password.is.incorrect"
|
|
278
|
-
*/
|
|
279
|
-
/**
|
|
280
|
-
* Get action priority (higher number = higher priority)
|
|
281
|
-
*/
|
|
282
|
-
private getActionPriority(action: string): number {
|
|
283
|
-
const priorities: { [key: string]: number } = {
|
|
284
|
-
'click': 4, // Highest priority - most specific interaction
|
|
285
|
-
'fill': 3, // Form input
|
|
286
|
-
'select': 3, // Form select
|
|
287
|
-
'see': 2, // Assertion
|
|
288
|
-
'open': 1, // Navigation - lowest priority
|
|
289
|
-
'default': 0,
|
|
290
|
-
};
|
|
291
|
-
return priorities[action] ?? 0;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
/**
|
|
295
|
-
* Build scaffold result from extracted elements
|
|
296
|
-
*/
|
|
297
|
-
private buildScaffold(elements: ExtractedElement[]): ScaffoldResult {
|
|
298
|
-
const result: ScaffoldResult = {};
|
|
299
|
-
|
|
300
|
-
// Group elements by key to detect duplicates with different target types
|
|
301
|
-
const elementsByKey: { [key: string]: ExtractedElement[] } = {};
|
|
302
|
-
for (const element of elements) {
|
|
303
|
-
if (!elementsByKey[element.key]) {
|
|
304
|
-
elementsByKey[element.key] = [];
|
|
305
|
-
}
|
|
306
|
-
elementsByKey[element.key].push(element);
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
for (const key of Object.keys(elementsByKey)) {
|
|
310
|
-
const group = elementsByKey[key];
|
|
311
|
-
|
|
312
|
-
// Deduplicate by targetType, keeping highest priority action per type
|
|
313
|
-
const byType: { [type: string]: ExtractedElement } = {};
|
|
314
|
-
for (const element of group) {
|
|
315
|
-
const existing = byType[element.targetType];
|
|
316
|
-
if (!existing ||
|
|
317
|
-
this.getActionPriority(element.action) > this.getActionPriority(existing.action)) {
|
|
318
|
-
byType[element.targetType] = element;
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
const uniqueTypes = Object.keys(byType);
|
|
323
|
-
|
|
324
|
-
if (uniqueTypes.length <= 1) {
|
|
325
|
-
// No type conflict — use base key as before
|
|
326
|
-
const element = byType[uniqueTypes[0]];
|
|
327
|
-
result[key] = this.createScaffoldElement(element);
|
|
328
|
-
} else {
|
|
329
|
-
// Multiple types for same name — generate type-suffixed keys
|
|
330
|
-
for (const type of uniqueTypes) {
|
|
331
|
-
const suffixedKey = `${key}--${type}`;
|
|
332
|
-
result[suffixedKey] = this.createScaffoldElement(byType[type]);
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
return result;
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
/**
|
|
341
|
-
* Create a scaffold element based on the action type and target type
|
|
342
|
-
*/
|
|
343
|
-
private createScaffoldElement(element: ExtractedElement): ScaffoldElement {
|
|
344
|
-
const { nth, targetType } = element;
|
|
345
|
-
|
|
346
|
-
// NEW: Handle page type
|
|
347
|
-
if (targetType === 'page') {
|
|
348
|
-
return {
|
|
349
|
-
locator: '',
|
|
350
|
-
type: 'page',
|
|
351
|
-
value: element.featurePath || '/',
|
|
352
|
-
nth,
|
|
353
|
-
};
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
switch (element.action) {
|
|
357
|
-
case 'fill':
|
|
358
|
-
// For fill action with uploader type, use label type (file input triggered by label)
|
|
359
|
-
if (targetType === 'uploader') {
|
|
360
|
-
return {
|
|
361
|
-
locator: '',
|
|
362
|
-
type: 'label',
|
|
363
|
-
value: element.rawText,
|
|
364
|
-
nth,
|
|
365
|
-
};
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
// For fill action with label type, use label type
|
|
369
|
-
if (targetType === 'label') {
|
|
370
|
-
return {
|
|
371
|
-
locator: '',
|
|
372
|
-
type: 'label',
|
|
373
|
-
value: element.rawText,
|
|
374
|
-
nth,
|
|
375
|
-
};
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
// For fill action with field/textbox/textarea types, use role-based locator
|
|
379
|
-
return {
|
|
380
|
-
locator: '',
|
|
381
|
-
type: 'role',
|
|
382
|
-
value: this.getFillRoleValue(targetType),
|
|
383
|
-
name: element.rawText,
|
|
384
|
-
nth,
|
|
385
|
-
};
|
|
386
|
-
|
|
387
|
-
case 'click':
|
|
388
|
-
// Special case: label and text clicks use text locator
|
|
389
|
-
if (targetType === 'label' || targetType === 'text') {
|
|
390
|
-
return {
|
|
391
|
-
locator: '',
|
|
392
|
-
type: 'text',
|
|
393
|
-
value: element.rawText,
|
|
394
|
-
nth,
|
|
395
|
-
};
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
// Regular click actions use role
|
|
399
|
-
const clickRoleValue = this.getClickRoleValue(targetType);
|
|
400
|
-
return {
|
|
401
|
-
locator: '',
|
|
402
|
-
type: 'role',
|
|
403
|
-
value: clickRoleValue,
|
|
404
|
-
name: element.rawText, // TODO: Update with actual accessible name from inspection
|
|
405
|
-
nth,
|
|
406
|
-
};
|
|
407
|
-
|
|
408
|
-
case 'see':
|
|
409
|
-
// Column type: used for table column cell assertions, no DOM selector needed
|
|
410
|
-
if (targetType === 'column') {
|
|
411
|
-
return {
|
|
412
|
-
locator: '',
|
|
413
|
-
type: 'column',
|
|
414
|
-
value: element.rawText,
|
|
415
|
-
nth,
|
|
416
|
-
};
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
// For assertions - respect targetType for role-based elements
|
|
420
|
-
if (targetType === 'img' || targetType === 'button' || targetType === 'link' ||
|
|
421
|
-
targetType === 'checkbox' || targetType === 'radio' || targetType === 'heading' || targetType === 'dialog' || targetType === 'dropdown') {
|
|
422
|
-
const seeRoleValue = this.getRoleValue(targetType);
|
|
423
|
-
return {
|
|
424
|
-
locator: '',
|
|
425
|
-
type: 'role',
|
|
426
|
-
value: seeRoleValue,
|
|
427
|
-
name: element.rawText,
|
|
428
|
-
nth,
|
|
429
|
-
};
|
|
430
|
-
}
|
|
431
|
-
return {
|
|
432
|
-
locator: '',
|
|
433
|
-
type: 'text',
|
|
434
|
-
value: element.rawText,
|
|
435
|
-
nth,
|
|
436
|
-
};
|
|
437
|
-
|
|
438
|
-
case 'select':
|
|
439
|
-
// Determine role based on target type (radio, dropdown/combobox, checkbox)
|
|
440
|
-
const selectRoleValue = this.getSelectRoleValue(targetType);
|
|
441
|
-
return {
|
|
442
|
-
locator: '',
|
|
443
|
-
type: 'role',
|
|
444
|
-
value: selectRoleValue,
|
|
445
|
-
name: element.rawText, // TODO: Update with actual accessible name from inspection
|
|
446
|
-
nth,
|
|
447
|
-
};
|
|
448
|
-
|
|
449
|
-
case 'open':
|
|
450
|
-
// For page navigation, we typically don't need a selector
|
|
451
|
-
// But include it for reference
|
|
452
|
-
return {
|
|
453
|
-
locator: '',
|
|
454
|
-
type: 'text',
|
|
455
|
-
value: element.rawText,
|
|
456
|
-
nth,
|
|
457
|
-
};
|
|
458
|
-
|
|
459
|
-
default:
|
|
460
|
-
// Default to placeholder type
|
|
461
|
-
return {
|
|
462
|
-
locator: '',
|
|
463
|
-
type: 'placeholder',
|
|
464
|
-
value: element.rawText,
|
|
465
|
-
nth,
|
|
466
|
-
};
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
/**
|
|
471
|
-
* Get ARIA role value from target type (shared across actions)
|
|
472
|
-
*/
|
|
473
|
-
private getRoleValue(targetType: string): string {
|
|
474
|
-
switch (targetType) {
|
|
475
|
-
case 'link':
|
|
476
|
-
return 'link';
|
|
477
|
-
case 'button':
|
|
478
|
-
return 'button';
|
|
479
|
-
case 'checkbox':
|
|
480
|
-
return 'checkbox';
|
|
481
|
-
case 'radio':
|
|
482
|
-
return 'radio';
|
|
483
|
-
case 'tab':
|
|
484
|
-
return 'tab';
|
|
485
|
-
case 'menuitem':
|
|
486
|
-
return 'menuitem';
|
|
487
|
-
case 'img':
|
|
488
|
-
return 'img';
|
|
489
|
-
case 'heading':
|
|
490
|
-
return 'heading';
|
|
491
|
-
case 'dialog':
|
|
492
|
-
return 'dialog';
|
|
493
|
-
case 'dropdown':
|
|
494
|
-
return 'button';
|
|
495
|
-
default:
|
|
496
|
-
return 'button'; // Default to button
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
/**
|
|
501
|
-
* Get role value for click actions based on target type
|
|
502
|
-
*/
|
|
503
|
-
private getClickRoleValue(targetType: string): string {
|
|
504
|
-
return this.getRoleValue(targetType);
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
/**
|
|
508
|
-
* Get role value for select actions based on target type
|
|
509
|
-
*/
|
|
510
|
-
private getSelectRoleValue(targetType: string): string {
|
|
511
|
-
switch (targetType) {
|
|
512
|
-
case 'radio':
|
|
513
|
-
return 'radio';
|
|
514
|
-
case 'checkbox':
|
|
515
|
-
return 'checkbox';
|
|
516
|
-
case 'dropdown':
|
|
517
|
-
case 'select':
|
|
518
|
-
return 'combobox';
|
|
519
|
-
case 'listbox':
|
|
520
|
-
return 'listbox';
|
|
521
|
-
default:
|
|
522
|
-
return 'combobox'; // Default to combobox
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
/**
|
|
527
|
-
* Get ARIA role value for fill actions — all fillable inputs use 'textbox'
|
|
528
|
-
*/
|
|
529
|
-
private getFillRoleValue(targetType: string): string {
|
|
530
|
-
switch (targetType) {
|
|
531
|
-
case 'field':
|
|
532
|
-
case 'textbox':
|
|
533
|
-
case 'textarea':
|
|
534
|
-
default:
|
|
535
|
-
return 'textbox';
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
/**
|
|
540
|
-
* Generate YAML content from scaffold result
|
|
541
|
-
*/
|
|
542
|
-
generateYamlContent(scaffold: ScaffoldResult, screenName?: string): string {
|
|
543
|
-
// For header, use name (for role type) or value (for other types)
|
|
544
|
-
const getDisplayValue = (e: ScaffoldElement) => e.name || e.value;
|
|
545
|
-
|
|
546
|
-
const header = screenName
|
|
547
|
-
? `# ${this.capitalizeFirst(screenName)} Screen Selectors (Auto-generated)
|
|
548
|
-
# Reference in features: ${Object.values(scaffold).map(e => `[${getDisplayValue(e)}]`).join(', ')}
|
|
549
|
-
# Directory: qa/selectors/screens/
|
|
550
|
-
|
|
551
|
-
`
|
|
552
|
-
: `# Screen Selectors (Auto-generated)
|
|
553
|
-
|
|
554
|
-
`;
|
|
555
|
-
|
|
556
|
-
// Convert to YAML with proper formatting
|
|
557
|
-
const yamlContent = yaml.stringify(scaffold, {
|
|
558
|
-
indent: 2,
|
|
559
|
-
lineWidth: 100,
|
|
560
|
-
defaultKeyType: 'PLAIN',
|
|
561
|
-
defaultStringType: 'QUOTE_SINGLE',
|
|
562
|
-
});
|
|
563
|
-
|
|
564
|
-
return header + yamlContent;
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
/**
|
|
568
|
-
* Write scaffold to file
|
|
569
|
-
*/
|
|
570
|
-
writeScaffold(
|
|
571
|
-
scaffold: ScaffoldResult,
|
|
572
|
-
outputPath: string,
|
|
573
|
-
screenName?: string
|
|
574
|
-
): void {
|
|
575
|
-
const dir = path.dirname(outputPath);
|
|
576
|
-
if (!fs.existsSync(dir)) {
|
|
577
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
const content = this.generateYamlContent(scaffold, screenName);
|
|
581
|
-
fs.writeFileSync(outputPath, content, 'utf-8');
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
/**
|
|
585
|
-
* Process a feature file and write scaffold to output
|
|
586
|
-
*/
|
|
587
|
-
processFeatureFile(
|
|
588
|
-
featureFilePath: string,
|
|
589
|
-
outputDir: string,
|
|
590
|
-
forceOverwrite: boolean = false
|
|
591
|
-
): { outputPath: string; elementCount: number } {
|
|
592
|
-
// Extract screen name from feature file name
|
|
593
|
-
const featureFileName = path.basename(featureFilePath, '.feature');
|
|
594
|
-
const screenName = featureFileName;
|
|
595
|
-
|
|
596
|
-
// Generate scaffold
|
|
597
|
-
const scaffold = this.generateFromFile(featureFilePath);
|
|
598
|
-
const elementCount = Object.keys(scaffold).length;
|
|
599
|
-
|
|
600
|
-
// Write to output
|
|
601
|
-
const outputPath = path.join(outputDir, `${screenName}.yaml`);
|
|
602
|
-
|
|
603
|
-
// Check if file exists and skip if not forcing overwrite
|
|
604
|
-
if (fs.existsSync(outputPath) && !forceOverwrite) {
|
|
605
|
-
return { outputPath, elementCount };
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
this.writeScaffold(scaffold, outputPath, screenName);
|
|
609
|
-
|
|
610
|
-
return { outputPath, elementCount };
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
/**
|
|
614
|
-
* Process all feature files in a directory
|
|
615
|
-
*/
|
|
616
|
-
processAllFeatures(
|
|
617
|
-
featuresDir: string,
|
|
618
|
-
outputDir: string
|
|
619
|
-
): Array<{ file: string; outputPath: string; elementCount: number }> {
|
|
620
|
-
const featureFiles = glob.sync('**/*.feature', {
|
|
621
|
-
cwd: featuresDir,
|
|
622
|
-
absolute: true,
|
|
623
|
-
});
|
|
624
|
-
|
|
625
|
-
const results: Array<{ file: string; outputPath: string; elementCount: number }> = [];
|
|
626
|
-
|
|
627
|
-
for (const featureFile of featureFiles) {
|
|
628
|
-
const result = this.processFeatureFile(featureFile, outputDir);
|
|
629
|
-
results.push({
|
|
630
|
-
file: featureFile,
|
|
631
|
-
...result,
|
|
632
|
-
});
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
return results;
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
/**
|
|
639
|
-
* Enrich scaffold with live-scan data if a .live-scan.yaml file exists.
|
|
640
|
-
* Uses the Playwright locator strategy detected by live-scan:
|
|
641
|
-
* testid → type: testid, value: <testid>
|
|
642
|
-
* role → type: role, value: <aria-role>, name: <accessible-name>
|
|
643
|
-
* label → type: label, value: <label-text>
|
|
644
|
-
* placeholder → type: placeholder, value: <placeholder-text>
|
|
645
|
-
* text → type: text, value: <visible-text>
|
|
646
|
-
*/
|
|
647
|
-
private enrichWithLiveScan(scaffold: ScaffoldResult, liveScanPath: string): ScaffoldResult {
|
|
648
|
-
if (!fs.existsSync(liveScanPath)) {
|
|
649
|
-
return scaffold;
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
let matrixData: any;
|
|
653
|
-
try {
|
|
654
|
-
const { readMatrix, mergeMatrixElements } = require('../../core/live-scanner/matrix-reader');
|
|
655
|
-
matrixData = readMatrix(liveScanPath);
|
|
656
|
-
if (!matrixData) return scaffold;
|
|
657
|
-
|
|
658
|
-
const elements = mergeMatrixElements(matrixData);
|
|
659
|
-
if (!elements || Object.keys(elements).length === 0) return scaffold;
|
|
660
|
-
|
|
661
|
-
console.log(` 🔗 Using live-scan data from ${path.basename(liveScanPath)}`);
|
|
662
|
-
|
|
663
|
-
let enrichedCount = 0;
|
|
664
|
-
let warningCount = 0;
|
|
665
|
-
|
|
666
|
-
const enriched: ScaffoldResult = { ...scaffold };
|
|
667
|
-
|
|
668
|
-
for (const [key, scaffoldElement] of Object.entries(enriched)) {
|
|
669
|
-
// Column-type elements use header text matching, skip live-scan enrichment
|
|
670
|
-
if (scaffoldElement.type === 'column') continue;
|
|
671
|
-
// Target elements (empty value) are matched by data value at runtime, skip enrichment
|
|
672
|
-
if (!scaffoldElement.value && !scaffoldElement.name) continue;
|
|
673
|
-
|
|
674
|
-
// Try exact key first, then base key without --N suffix or --type suffix
|
|
675
|
-
let liveElement = elements[key];
|
|
676
|
-
if (!liveElement && key.includes('--')) {
|
|
677
|
-
const baseKey = key.replace(/--.*$/, '');
|
|
678
|
-
liveElement = elements[baseKey];
|
|
679
|
-
}
|
|
680
|
-
if (!liveElement) {
|
|
681
|
-
continue;
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
// If live-scan couldn't find the element, clear the identifying field so user must fill manually
|
|
685
|
-
if (liveElement.matchMethod === 'unresolved') {
|
|
686
|
-
if (scaffoldElement.name !== undefined) {
|
|
687
|
-
enriched[key] = { ...scaffoldElement, name: '' };
|
|
688
|
-
} else {
|
|
689
|
-
enriched[key] = { ...scaffoldElement, value: '' };
|
|
690
|
-
}
|
|
691
|
-
continue;
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
// Use the Playwright locator strategy that live-scan detected
|
|
695
|
-
const selectorType = liveElement.selectorType;
|
|
696
|
-
|
|
697
|
-
// Only set exact when true (omit from YAML when false for cleaner output)
|
|
698
|
-
const exactField = liveElement.exact ? { exact: true } : {};
|
|
699
|
-
|
|
700
|
-
// Strip 'name' from scaffold base — only 'role' type uses name
|
|
701
|
-
const { name: _scaffoldName, ...scaffoldBase } = scaffoldElement as any;
|
|
702
|
-
|
|
703
|
-
if (selectorType === 'testid' && liveElement.testid) {
|
|
704
|
-
// page.getByTestId('value')
|
|
705
|
-
enriched[key] = {
|
|
706
|
-
...scaffoldBase,
|
|
707
|
-
type: 'testid' as any,
|
|
708
|
-
value: liveElement.testid,
|
|
709
|
-
};
|
|
710
|
-
} else if (selectorType === 'role' && liveElement.role) {
|
|
711
|
-
// page.getByRole('button', { name: 'Submit' })
|
|
712
|
-
// When name is empty (e.g. data-value fallback), keep it as '' —
|
|
713
|
-
// the actual name comes from test-data at runtime
|
|
714
|
-
enriched[key] = {
|
|
715
|
-
...scaffoldBase,
|
|
716
|
-
type: 'role',
|
|
717
|
-
value: liveElement.role,
|
|
718
|
-
name: liveElement.name,
|
|
719
|
-
...exactField,
|
|
720
|
-
};
|
|
721
|
-
} else if (selectorType === 'label' && liveElement.label) {
|
|
722
|
-
// page.getByLabel('Username')
|
|
723
|
-
enriched[key] = {
|
|
724
|
-
...scaffoldBase,
|
|
725
|
-
type: 'label',
|
|
726
|
-
value: liveElement.label,
|
|
727
|
-
...exactField,
|
|
728
|
-
};
|
|
729
|
-
} else if (selectorType === 'placeholder' && (liveElement.placeholder || liveElement.selectorValue)) {
|
|
730
|
-
// page.getByPlaceholder('Enter email')
|
|
731
|
-
enriched[key] = {
|
|
732
|
-
...scaffoldBase,
|
|
733
|
-
type: 'placeholder',
|
|
734
|
-
value: liveElement.placeholder || liveElement.selectorValue,
|
|
735
|
-
...exactField,
|
|
736
|
-
};
|
|
737
|
-
} else if (selectorType === 'text') {
|
|
738
|
-
// page.getByText('Welcome')
|
|
739
|
-
// When name is empty (data-value fallback), keep value empty —
|
|
740
|
-
// the actual text comes from test-data at runtime
|
|
741
|
-
enriched[key] = {
|
|
742
|
-
...scaffoldBase,
|
|
743
|
-
type: 'text',
|
|
744
|
-
value: liveElement.name,
|
|
745
|
-
...exactField,
|
|
746
|
-
};
|
|
747
|
-
} else if (selectorType === 'locator' && liveElement.selectorValue) {
|
|
748
|
-
// page.locator('[contenteditable="true"]') — CSS locator
|
|
749
|
-
// Reset nth to 0: the original nth was for text matches, not this locator type
|
|
750
|
-
enriched[key] = {
|
|
751
|
-
...scaffoldBase,
|
|
752
|
-
type: 'locator' as any,
|
|
753
|
-
value: liveElement.selectorValue,
|
|
754
|
-
nth: 0,
|
|
755
|
-
};
|
|
756
|
-
} else {
|
|
757
|
-
continue;
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
// Detect contenteditable div-based editors (rich text editors like TipTap, Quill, ProseMirror)
|
|
761
|
-
// When the DOM element is contenteditable but NOT a native input/textarea,
|
|
762
|
-
// mark it so the generator uses click + pressSequentially instead of .fill()
|
|
763
|
-
if (liveElement.contenteditable && liveElement.tag !== 'textarea' && liveElement.tag !== 'input') {
|
|
764
|
-
enriched[key] = { ...enriched[key], inputMethod: 'pressSequentially' };
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
enrichedCount++;
|
|
768
|
-
if (liveElement.warning) {
|
|
769
|
-
warningCount++;
|
|
770
|
-
console.log(` ⚠️ ${key}: ${liveElement.warning}`);
|
|
771
|
-
}
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
const textOnlyCount = Object.keys(enriched).length - enrichedCount;
|
|
775
|
-
console.log(` 📊 Live-scan: ${enrichedCount} enriched, ${textOnlyCount} text-inferred${warningCount > 0 ? `, ${warningCount} warnings` : ''}`);
|
|
776
|
-
|
|
777
|
-
return enriched;
|
|
778
|
-
} catch (error: any) {
|
|
779
|
-
console.warn(` ⚠️ Failed to read live-scan data: ${error.message}`);
|
|
780
|
-
return scaffold;
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
/**
|
|
785
|
-
* Helper to capitalize first letter
|
|
786
|
-
*/
|
|
787
|
-
private capitalizeFirst(str: string): string {
|
|
788
|
-
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
/**
|
|
792
|
-
* Process a screen and generate both selectors and test-data YAML
|
|
793
|
-
* Folder structure: qa/screens/<screen>/features/, selectors/, test-data/
|
|
794
|
-
*/
|
|
795
|
-
processScreen(screenName: string, screensDir: string = 'qa/screens', forceOverwrite: boolean = false): MapResult[] {
|
|
796
|
-
const screenDir = path.join(screensDir, screenName);
|
|
797
|
-
const featuresDir = path.join(screenDir, 'features');
|
|
798
|
-
const selectorsDir = path.join(screenDir, 'selectors');
|
|
799
|
-
const testDataDir = path.join(screenDir, 'test-data');
|
|
800
|
-
|
|
801
|
-
// Find all feature files in the screen's features directory
|
|
802
|
-
const featureFiles = glob.sync('**/*.feature', {
|
|
803
|
-
cwd: featuresDir,
|
|
804
|
-
absolute: true,
|
|
805
|
-
});
|
|
806
|
-
|
|
807
|
-
if (featureFiles.length === 0) {
|
|
808
|
-
throw new Error(`No feature files found in ${featuresDir}`);
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
// Ensure directories exist
|
|
812
|
-
fs.mkdirSync(selectorsDir, { recursive: true });
|
|
813
|
-
fs.mkdirSync(testDataDir, { recursive: true });
|
|
814
|
-
|
|
815
|
-
const results: MapResult[] = [];
|
|
816
|
-
|
|
817
|
-
// Process each feature file individually
|
|
818
|
-
for (const featureFile of featureFiles) {
|
|
819
|
-
const filename = path.basename(featureFile, '.feature');
|
|
820
|
-
const content = fs.readFileSync(featureFile, 'utf-8');
|
|
821
|
-
|
|
822
|
-
// Extract feature path from Gherkin metadata
|
|
823
|
-
const featurePath = this.extractFeaturePath(content);
|
|
824
|
-
|
|
825
|
-
// Extract elements and data refs from this file
|
|
826
|
-
const elements = this.extractElements(content, featurePath);
|
|
827
|
-
const dataRefs = this.extractDataRefs(content);
|
|
828
|
-
|
|
829
|
-
// Build scaffolds from this file
|
|
830
|
-
const selectorScaffold = this.buildScaffold(elements);
|
|
831
|
-
const testDataScaffold = this.buildTestData(dataRefs);
|
|
832
|
-
|
|
833
|
-
// Enrich scaffold with live-scan data if available
|
|
834
|
-
const liveScanPath = path.join(selectorsDir, `${filename}.live-scan.yaml`);
|
|
835
|
-
const liveScanEnriched = this.enrichWithLiveScan(selectorScaffold, liveScanPath);
|
|
836
|
-
|
|
837
|
-
// Check if files exist
|
|
838
|
-
const selectorsPath = path.join(selectorsDir, `${filename}.yaml`);
|
|
839
|
-
const selectorsOverridePath = path.join(selectorsDir, `${filename}.override.yaml`);
|
|
840
|
-
const selectorsExists = fs.existsSync(selectorsPath);
|
|
841
|
-
|
|
842
|
-
const testDataPath = path.join(testDataDir, `${filename}.yaml`);
|
|
843
|
-
const testDataOverridePath = path.join(testDataDir, `${filename}.override.yaml`);
|
|
844
|
-
const testDataExists = fs.existsSync(testDataPath);
|
|
845
|
-
|
|
846
|
-
// Write selectors YAML (BASE ONLY - never touch override files)
|
|
847
|
-
if (!selectorsExists || forceOverwrite) {
|
|
848
|
-
this.writeSelectorsYaml(liveScanEnriched, selectorsPath, filename);
|
|
849
|
-
}
|
|
850
|
-
// Override file is NEVER touched by map command
|
|
851
|
-
|
|
852
|
-
// Write test-data YAML — always smart-merge regardless of --force:
|
|
853
|
-
// • existing keys keep their current value
|
|
854
|
-
// • new keys from features are added (empty string)
|
|
855
|
-
// • keys no longer referenced in features are removed
|
|
856
|
-
if (testDataExists) {
|
|
857
|
-
const existingRaw = yaml.parse(fs.readFileSync(testDataPath, 'utf-8')) as TestDataResult || {};
|
|
858
|
-
const merged = this.mergeTestData(existingRaw, testDataScaffold);
|
|
859
|
-
this.writeTestDataYaml(merged, testDataPath, filename);
|
|
860
|
-
} else {
|
|
861
|
-
this.writeTestDataYaml(testDataScaffold, testDataPath, filename);
|
|
862
|
-
}
|
|
863
|
-
// Override file is NEVER touched by map command
|
|
864
|
-
|
|
865
|
-
results.push({
|
|
866
|
-
featureFile: filename,
|
|
867
|
-
selectorsPath,
|
|
868
|
-
testDataPath,
|
|
869
|
-
elementCount: Object.keys(liveScanEnriched).length,
|
|
870
|
-
dataRefCount: dataRefs.length,
|
|
871
|
-
selectorsSkipped: selectorsExists && !forceOverwrite,
|
|
872
|
-
testDataMerged: testDataExists,
|
|
873
|
-
});
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
return results;
|
|
877
|
-
}
|
|
878
|
-
|
|
879
|
-
/**
|
|
880
|
-
* Extract data references {{...}} from Gherkin content
|
|
881
|
-
*/
|
|
882
|
-
private extractDataRefs(content: string): ExtractedDataRef[] {
|
|
883
|
-
const dataRefs: ExtractedDataRef[] = [];
|
|
884
|
-
const lines = content.split('\n');
|
|
885
|
-
const seen = new Set<string>();
|
|
886
|
-
|
|
887
|
-
// Regex to match data refs {{...}}
|
|
888
|
-
const dataRefRegex = /\{\{([^}]+)\}\}/g;
|
|
889
|
-
|
|
890
|
-
for (const line of lines) {
|
|
891
|
-
const trimmedLine = line.trim();
|
|
892
|
-
|
|
893
|
-
// Skip comments and empty lines
|
|
894
|
-
if (trimmedLine.startsWith('#') || !trimmedLine) {
|
|
895
|
-
continue;
|
|
896
|
-
}
|
|
897
|
-
|
|
898
|
-
let match: RegExpExecArray | null;
|
|
899
|
-
while ((match = dataRefRegex.exec(trimmedLine)) !== null) {
|
|
900
|
-
const rawRef = match[1].trim();
|
|
901
|
-
|
|
902
|
-
// Skip duplicates
|
|
903
|
-
if (seen.has(rawRef)) {
|
|
904
|
-
continue;
|
|
905
|
-
}
|
|
906
|
-
seen.add(rawRef);
|
|
907
|
-
|
|
908
|
-
dataRefs.push({
|
|
909
|
-
rawRef,
|
|
910
|
-
lineText: trimmedLine,
|
|
911
|
-
});
|
|
912
|
-
}
|
|
913
|
-
}
|
|
914
|
-
|
|
915
|
-
return dataRefs;
|
|
916
|
-
}
|
|
917
|
-
|
|
918
|
-
/**
|
|
919
|
-
* Build test data structure from data refs
|
|
920
|
-
* Supports nested structure: "valid.login_password" -> { valid: { login_password: '' } }
|
|
921
|
-
*/
|
|
922
|
-
private buildTestData(dataRefs: ExtractedDataRef[]): TestDataResult {
|
|
923
|
-
const result: TestDataResult = {};
|
|
924
|
-
|
|
925
|
-
for (const dataRef of dataRefs) {
|
|
926
|
-
const parts = dataRef.rawRef.split('.');
|
|
927
|
-
|
|
928
|
-
if (parts.length === 1) {
|
|
929
|
-
// Simple variable: "valid_login_email" -> { valid_login_email: '' }
|
|
930
|
-
result[parts[0]] = '';
|
|
931
|
-
} else {
|
|
932
|
-
// Nested variable: "valid.login_password" -> { valid: { login_password: '' } }
|
|
933
|
-
let current: TestDataResult = result;
|
|
934
|
-
for (let i = 0; i < parts.length - 1; i++) {
|
|
935
|
-
const part = parts[i];
|
|
936
|
-
if (!current[part] || typeof current[part] === 'string') {
|
|
937
|
-
current[part] = {};
|
|
938
|
-
}
|
|
939
|
-
current = current[part] as TestDataResult;
|
|
940
|
-
}
|
|
941
|
-
current[parts[parts.length - 1]] = '';
|
|
942
|
-
}
|
|
943
|
-
}
|
|
944
|
-
|
|
945
|
-
return result;
|
|
946
|
-
}
|
|
947
|
-
|
|
948
|
-
/**
|
|
949
|
-
* Write selectors YAML file
|
|
950
|
-
*/
|
|
951
|
-
private writeSelectorsYaml(scaffold: ScaffoldResult, outputPath: string, screenName: string): void {
|
|
952
|
-
const getDisplayValue = (e: ScaffoldElement) => e.name || e.value;
|
|
953
|
-
|
|
954
|
-
// Deduplicate display values for header comment
|
|
955
|
-
const displayValues = [...new Set(Object.values(scaffold).map(e => `[${getDisplayValue(e)}]`))];
|
|
956
|
-
const header = `# ${this.capitalizeFirst(screenName)} Screen Selectors (Auto-generated)
|
|
957
|
-
# ⚠️ AUTO-GENERATED - Do not edit directly
|
|
958
|
-
# To override values, edit ${screenName}.override.yaml
|
|
959
|
-
# Manual edits to this file will be lost on regeneration
|
|
960
|
-
#
|
|
961
|
-
# Reference in features: ${displayValues.join(', ')}
|
|
962
|
-
|
|
963
|
-
`;
|
|
964
|
-
|
|
965
|
-
const yamlContent = yaml.stringify(scaffold, {
|
|
966
|
-
indent: 2,
|
|
967
|
-
lineWidth: 100,
|
|
968
|
-
defaultKeyType: 'PLAIN',
|
|
969
|
-
defaultStringType: 'QUOTE_SINGLE',
|
|
970
|
-
});
|
|
971
|
-
|
|
972
|
-
fs.writeFileSync(outputPath, header + yamlContent, 'utf-8');
|
|
973
|
-
}
|
|
974
|
-
|
|
975
|
-
/**
|
|
976
|
-
* Smart-merge test-data objects:
|
|
977
|
-
* - Keys present in both: keep the existing value (user may have filled it in)
|
|
978
|
-
* - Keys only in fresh (new refs in features): add with empty value
|
|
979
|
-
* - Keys only in existing (removed from features): drop them
|
|
980
|
-
*/
|
|
981
|
-
private mergeTestData(existing: TestDataResult, fresh: TestDataResult): TestDataResult {
|
|
982
|
-
const merged: TestDataResult = {};
|
|
983
|
-
|
|
984
|
-
for (const key of Object.keys(fresh)) {
|
|
985
|
-
if (key in existing) {
|
|
986
|
-
const existingVal = existing[key];
|
|
987
|
-
const freshVal = fresh[key];
|
|
988
|
-
if (typeof freshVal === 'object' && typeof existingVal === 'object') {
|
|
989
|
-
// Both are nested — recurse
|
|
990
|
-
merged[key] = this.mergeTestData(existingVal as TestDataResult, freshVal as TestDataResult);
|
|
991
|
-
} else {
|
|
992
|
-
// Keep the existing (user-supplied) value
|
|
993
|
-
merged[key] = existingVal;
|
|
994
|
-
}
|
|
995
|
-
} else {
|
|
996
|
-
// New key — add with the scaffold default (empty string or nested object)
|
|
997
|
-
merged[key] = fresh[key];
|
|
998
|
-
}
|
|
999
|
-
// Keys present in existing but absent from fresh are intentionally dropped (no longer referenced)
|
|
1000
|
-
}
|
|
1001
|
-
|
|
1002
|
-
return merged;
|
|
1003
|
-
}
|
|
1004
|
-
|
|
1005
|
-
/**
|
|
1006
|
-
* Write test-data YAML file
|
|
1007
|
-
*/
|
|
1008
|
-
private writeTestDataYaml(testData: TestDataResult, outputPath: string, screenName: string): void {
|
|
1009
|
-
const header = `# ${this.capitalizeFirst(screenName)} Screen Test Data
|
|
1010
|
-
# Managed by: sungen map (smart-merge)
|
|
1011
|
-
# • New {{variable}} refs are added automatically (empty value)
|
|
1012
|
-
# • Removed refs are dropped on the next sungen map run
|
|
1013
|
-
# • Your existing values are never overwritten
|
|
1014
|
-
# To fix a value permanently, copy the key to ${screenName}.override.yaml
|
|
1015
|
-
#
|
|
1016
|
-
# Reference in features using {{variable}} syntax
|
|
1017
|
-
|
|
1018
|
-
`;
|
|
1019
|
-
|
|
1020
|
-
const yamlContent = yaml.stringify(testData, {
|
|
1021
|
-
indent: 2,
|
|
1022
|
-
lineWidth: 100,
|
|
1023
|
-
defaultKeyType: 'PLAIN',
|
|
1024
|
-
defaultStringType: 'QUOTE_SINGLE',
|
|
1025
|
-
});
|
|
1026
|
-
|
|
1027
|
-
fs.writeFileSync(outputPath, header + yamlContent, 'utf-8');
|
|
1028
|
-
}
|
|
1029
|
-
}
|