@sun-asterisk/sungen 3.2.24 → 3.2.26
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/dist/capabilities/context-router.d.ts.map +1 -1
- package/dist/capabilities/context-router.js +15 -7
- package/dist/capabilities/context-router.js.map +1 -1
- package/dist/cli/commands/audit.d.ts.map +1 -1
- package/dist/cli/commands/audit.js +23 -4
- package/dist/cli/commands/audit.js.map +1 -1
- package/dist/cli/commands/capability.d.ts.map +1 -1
- package/dist/cli/commands/capability.js +6 -48
- package/dist/cli/commands/capability.js.map +1 -1
- package/dist/cli/commands/changed.d.ts +14 -0
- package/dist/cli/commands/changed.d.ts.map +1 -0
- package/dist/cli/commands/changed.js +128 -0
- package/dist/cli/commands/changed.js.map +1 -0
- package/dist/cli/commands/delivery.d.ts.map +1 -1
- package/dist/cli/commands/delivery.js +25 -1
- package/dist/cli/commands/delivery.js.map +1 -1
- package/dist/cli/commands/generate.js +1 -1
- package/dist/cli/commands/generate.js.map +1 -1
- package/dist/cli/commands/results-merge.d.ts +19 -0
- package/dist/cli/commands/results-merge.d.ts.map +1 -0
- package/dist/cli/commands/results-merge.js +191 -0
- package/dist/cli/commands/results-merge.js.map +1 -0
- package/dist/cli/commands/update.d.ts.map +1 -1
- package/dist/cli/commands/update.js +100 -12
- package/dist/cli/commands/update.js.map +1 -1
- package/dist/cli/index.js +4 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/exporters/api-catalog-loader.d.ts.map +1 -1
- package/dist/exporters/api-catalog-loader.js +10 -3
- package/dist/exporters/api-catalog-loader.js.map +1 -1
- package/dist/exporters/api-testcase-formatter.d.ts +9 -0
- package/dist/exporters/api-testcase-formatter.d.ts.map +1 -1
- package/dist/exporters/api-testcase-formatter.js +55 -3
- package/dist/exporters/api-testcase-formatter.js.map +1 -1
- package/dist/exporters/csv-exporter.d.ts.map +1 -1
- package/dist/exporters/csv-exporter.js +10 -1
- package/dist/exporters/csv-exporter.js.map +1 -1
- package/dist/exporters/json-exporter.d.ts.map +1 -1
- package/dist/exporters/json-exporter.js +6 -1
- package/dist/exporters/json-exporter.js.map +1 -1
- package/dist/exporters/matrix/build.d.ts.map +1 -1
- package/dist/exporters/matrix/build.js +14 -1
- package/dist/exporters/matrix/build.js.map +1 -1
- package/dist/exporters/matrix/export.js +1 -1
- package/dist/exporters/matrix/export.js.map +1 -1
- package/dist/exporters/matrix/map-loader.d.ts.map +1 -1
- package/dist/exporters/matrix/map-loader.js +5 -0
- package/dist/exporters/matrix/map-loader.js.map +1 -1
- package/dist/exporters/matrix/render-csv.d.ts.map +1 -1
- package/dist/exporters/matrix/render-csv.js +5 -1
- package/dist/exporters/matrix/render-csv.js.map +1 -1
- package/dist/exporters/matrix/render-xlsx.d.ts.map +1 -1
- package/dist/exporters/matrix/render-xlsx.js +4 -1
- package/dist/exporters/matrix/render-xlsx.js.map +1 -1
- package/dist/exporters/matrix/types.d.ts +18 -0
- package/dist/exporters/matrix/types.d.ts.map +1 -1
- package/dist/exporters/matrix/types.js.map +1 -1
- package/dist/exporters/playwright-report-parser.d.ts +17 -2
- package/dist/exporters/playwright-report-parser.d.ts.map +1 -1
- package/dist/exporters/playwright-report-parser.js +68 -8
- package/dist/exporters/playwright-report-parser.js.map +1 -1
- package/dist/exporters/results-merge.d.ts +46 -0
- package/dist/exporters/results-merge.d.ts.map +1 -0
- package/dist/exporters/results-merge.js +148 -0
- package/dist/exporters/results-merge.js.map +1 -0
- package/dist/exporters/selector-key-resolver.d.ts +4 -0
- package/dist/exporters/selector-key-resolver.d.ts.map +1 -1
- package/dist/exporters/selector-key-resolver.js +101 -1
- package/dist/exporters/selector-key-resolver.js.map +1 -1
- package/dist/exporters/types.d.ts +8 -1
- package/dist/exporters/types.d.ts.map +1 -1
- package/dist/exporters/xlsx-report-builder.d.ts.map +1 -1
- package/dist/exporters/xlsx-report-builder.js +5 -4
- package/dist/exporters/xlsx-report-builder.js.map +1 -1
- package/dist/generators/gherkin-parser/index.d.ts +2 -0
- package/dist/generators/gherkin-parser/index.d.ts.map +1 -1
- package/dist/generators/gherkin-parser/index.js +52 -1
- package/dist/generators/gherkin-parser/index.js.map +1 -1
- package/dist/generators/test-generator/adapters/adapter-interface.d.ts +20 -10
- package/dist/generators/test-generator/adapters/adapter-interface.d.ts.map +1 -1
- package/dist/generators/test-generator/adapters/appium/appium-adapter.d.ts +2 -11
- package/dist/generators/test-generator/adapters/appium/appium-adapter.d.ts.map +1 -1
- package/dist/generators/test-generator/adapters/appium/appium-adapter.js.map +1 -1
- package/dist/generators/test-generator/adapters/appium/templates/imports.hbs +3 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/date-picker-unsupported.hbs +4 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/lifecycle-missing-app-id.hbs +5 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/matches-format-assertion.hbs +15 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/visible-filtered-assertion.hbs +5 -2
- package/dist/generators/test-generator/adapters/appium/templates/test-file.hbs +185 -26
- package/dist/generators/test-generator/adapters/playwright/playwright-adapter.d.ts +2 -11
- package/dist/generators/test-generator/adapters/playwright/playwright-adapter.d.ts.map +1 -1
- package/dist/generators/test-generator/adapters/playwright/playwright-adapter.js.map +1 -1
- package/dist/generators/test-generator/adapters/playwright/templates/imports.hbs +3 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/select-action.hbs +17 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/attribute-assertion.hbs +6 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/have-value-assertion.hbs +18 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/hidden-with-filter-assertion.hbs +5 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/matches-format-assertion.hbs +5 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/state-with-filter-assertion.hbs +4 -2
- package/dist/generators/test-generator/adapters/playwright/templates/steps/navigation/wait-for-element-with-text.hbs +4 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-expect-text.hbs +1 -1
- package/dist/generators/test-generator/code-generator.d.ts +6 -0
- package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
- package/dist/generators/test-generator/code-generator.js +80 -7
- package/dist/generators/test-generator/code-generator.js.map +1 -1
- package/dist/generators/test-generator/diagnostics.d.ts +60 -6
- package/dist/generators/test-generator/diagnostics.d.ts.map +1 -1
- package/dist/generators/test-generator/diagnostics.js +59 -5
- package/dist/generators/test-generator/diagnostics.js.map +1 -1
- package/dist/generators/test-generator/patterns/expect-patterns.d.ts +7 -0
- package/dist/generators/test-generator/patterns/expect-patterns.d.ts.map +1 -1
- package/dist/generators/test-generator/patterns/expect-patterns.js +36 -2
- package/dist/generators/test-generator/patterns/expect-patterns.js.map +1 -1
- package/dist/generators/test-generator/patterns/index.d.ts +19 -12
- package/dist/generators/test-generator/patterns/index.d.ts.map +1 -1
- package/dist/generators/test-generator/patterns/index.js +58 -38
- package/dist/generators/test-generator/patterns/index.js.map +1 -1
- package/dist/generators/test-generator/patterns/types.d.ts +23 -0
- package/dist/generators/test-generator/patterns/types.d.ts.map +1 -1
- package/dist/generators/test-generator/patterns/unsupported-step-code.d.ts +18 -0
- package/dist/generators/test-generator/patterns/unsupported-step-code.d.ts.map +1 -0
- package/dist/generators/test-generator/patterns/unsupported-step-code.js +23 -0
- package/dist/generators/test-generator/patterns/unsupported-step-code.js.map +1 -0
- package/dist/generators/test-generator/recipe-expander.js +9 -1
- package/dist/generators/test-generator/recipe-expander.js.map +1 -1
- package/dist/generators/test-generator/reset-tier.d.ts +68 -0
- package/dist/generators/test-generator/reset-tier.d.ts.map +1 -0
- package/dist/generators/test-generator/reset-tier.js +94 -0
- package/dist/generators/test-generator/reset-tier.js.map +1 -0
- package/dist/generators/test-generator/step-mapper.d.ts +39 -0
- package/dist/generators/test-generator/step-mapper.d.ts.map +1 -1
- package/dist/generators/test-generator/step-mapper.js +144 -5
- package/dist/generators/test-generator/step-mapper.js.map +1 -1
- package/dist/generators/test-generator/template-engine.d.ts +2 -10
- package/dist/generators/test-generator/template-engine.d.ts.map +1 -1
- package/dist/generators/test-generator/template-engine.js +16 -2
- package/dist/generators/test-generator/template-engine.js.map +1 -1
- package/dist/generators/test-generator/utils/format-pattern.d.ts +35 -0
- package/dist/generators/test-generator/utils/format-pattern.d.ts.map +1 -0
- package/dist/generators/test-generator/utils/format-pattern.js +82 -0
- package/dist/generators/test-generator/utils/format-pattern.js.map +1 -0
- package/dist/generators/test-generator/utils/js-literal-scanner.d.ts +42 -0
- package/dist/generators/test-generator/utils/js-literal-scanner.d.ts.map +1 -0
- package/dist/generators/test-generator/utils/js-literal-scanner.js +142 -0
- package/dist/generators/test-generator/utils/js-literal-scanner.js.map +1 -0
- package/dist/generators/test-generator/utils/mask-refs-for-matching.d.ts +18 -0
- package/dist/generators/test-generator/utils/mask-refs-for-matching.d.ts.map +1 -0
- package/dist/generators/test-generator/utils/mask-refs-for-matching.js +25 -0
- package/dist/generators/test-generator/utils/mask-refs-for-matching.js.map +1 -0
- package/dist/generators/test-generator/utils/runtime-data-transformer.d.ts +9 -1
- package/dist/generators/test-generator/utils/runtime-data-transformer.d.ts.map +1 -1
- package/dist/generators/test-generator/utils/runtime-data-transformer.js +124 -63
- package/dist/generators/test-generator/utils/runtime-data-transformer.js.map +1 -1
- package/dist/generators/test-generator/utils/selector-resolver.d.ts.map +1 -1
- package/dist/generators/test-generator/utils/selector-resolver.js +20 -1
- package/dist/generators/test-generator/utils/selector-resolver.js.map +1 -1
- package/dist/generators/test-generator/utils/spec-syntax-check.d.ts +14 -0
- package/dist/generators/test-generator/utils/spec-syntax-check.d.ts.map +1 -0
- package/dist/generators/test-generator/utils/spec-syntax-check.js +26 -0
- package/dist/generators/test-generator/utils/spec-syntax-check.js.map +1 -0
- package/dist/harness/annotation-overrides.d.ts +0 -12
- package/dist/harness/annotation-overrides.d.ts.map +1 -1
- package/dist/harness/annotation-overrides.js +9 -5
- package/dist/harness/annotation-overrides.js.map +1 -1
- package/dist/harness/audit.d.ts +7 -0
- package/dist/harness/audit.d.ts.map +1 -1
- package/dist/harness/audit.js +78 -10
- package/dist/harness/audit.js.map +1 -1
- package/dist/harness/capability-plan.d.ts +2 -1
- package/dist/harness/capability-plan.d.ts.map +1 -1
- package/dist/harness/capability-plan.js +8 -3
- package/dist/harness/capability-plan.js.map +1 -1
- package/dist/harness/capability.d.ts +11 -0
- package/dist/harness/capability.d.ts.map +1 -1
- package/dist/harness/capability.js +8 -0
- package/dist/harness/capability.js.map +1 -1
- package/dist/harness/catalog/drivers.yaml +5 -0
- package/dist/harness/flow-contract.d.ts +18 -1
- package/dist/harness/flow-contract.d.ts.map +1 -1
- package/dist/harness/flow-contract.js +72 -9
- package/dist/harness/flow-contract.js.map +1 -1
- package/dist/harness/parse.d.ts.map +1 -1
- package/dist/harness/parse.js +5 -1
- package/dist/harness/parse.js.map +1 -1
- package/dist/harness/quality-gates.d.ts +12 -1
- package/dist/harness/quality-gates.d.ts.map +1 -1
- package/dist/harness/quality-gates.js +62 -7
- package/dist/harness/quality-gates.js.map +1 -1
- package/dist/harness/run-manifest.d.ts +58 -0
- package/dist/harness/run-manifest.d.ts.map +1 -0
- package/dist/harness/run-manifest.js +264 -0
- package/dist/harness/run-manifest.js.map +1 -0
- package/dist/harness/spec-branches.d.ts +88 -0
- package/dist/harness/spec-branches.d.ts.map +1 -0
- package/dist/harness/spec-branches.js +280 -0
- package/dist/harness/spec-branches.js.map +1 -0
- package/dist/harness/spec-coverage.d.ts +1 -1
- package/dist/harness/spec-coverage.js +4 -4
- package/dist/harness/spec-coverage.js.map +1 -1
- package/dist/harness/viewpoint-baseline.d.ts +9 -0
- package/dist/harness/viewpoint-baseline.d.ts.map +1 -1
- package/dist/harness/viewpoint-baseline.js +33 -3
- package/dist/harness/viewpoint-baseline.js.map +1 -1
- package/dist/harness/viewpoint-ledger.d.ts +1 -1
- package/dist/harness/viewpoint-ledger.d.ts.map +1 -1
- package/dist/harness/viewpoint-ledger.js +88 -9
- package/dist/harness/viewpoint-ledger.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/orchestrator/lockstep-version.d.ts +22 -0
- package/dist/orchestrator/lockstep-version.d.ts.map +1 -0
- package/dist/orchestrator/lockstep-version.js +58 -0
- package/dist/orchestrator/lockstep-version.js.map +1 -0
- package/dist/orchestrator/project-deps-sync.d.ts +39 -0
- package/dist/orchestrator/project-deps-sync.d.ts.map +1 -0
- package/dist/orchestrator/project-deps-sync.js +175 -0
- package/dist/orchestrator/project-deps-sync.js.map +1 -0
- package/dist/orchestrator/project-initializer.d.ts.map +1 -1
- package/dist/orchestrator/project-initializer.js +5 -2
- package/dist/orchestrator/project-initializer.js.map +1 -1
- package/dist/orchestrator/templates/ai-src/commands/add-flow.md +16 -0
- package/dist/orchestrator/templates/ai-src/commands/create-test.md +9 -0
- package/dist/orchestrator/templates/ai-src/commands/delivery.md +9 -2
- package/dist/orchestrator/templates/ai-src/commands/run-test.md +109 -0
- package/dist/orchestrator/templates/ai-src/config/claude.md +1 -1
- package/dist/orchestrator/templates/ai-src/config/copilot.md +1 -1
- package/dist/orchestrator/templates/ai-src/skills/sungen-api-design/SKILL.md +49 -0
- package/dist/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +14 -3
- package/dist/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +110 -6
- package/dist/orchestrator/templates/ai-src/skills/sungen-locale/SKILL.md +1 -1
- package/dist/orchestrator/templates/ai-src/skills/sungen-mobile-gestures/SKILL.md +8 -0
- package/dist/orchestrator/templates/ai-src/skills/sungen-selector-fix/SKILL.md +1 -1
- package/dist/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +40 -1
- package/dist/orchestrator/templates/ai-src/skills/sungen-tc-review/SKILL.md +15 -0
- package/dist/orchestrator/templates/env.appium.example +10 -0
- package/dist/orchestrator/templates/specs-api.d.ts +4 -2
- package/dist/orchestrator/templates/specs-api.d.ts.map +1 -1
- package/dist/orchestrator/templates/specs-api.js +233 -10
- package/dist/orchestrator/templates/specs-api.js.map +1 -1
- package/dist/orchestrator/templates/specs-api.ts +214 -11
- package/dist/orchestrator/templates/specs-regex-escape.d.ts +13 -0
- package/dist/orchestrator/templates/specs-regex-escape.d.ts.map +1 -0
- package/dist/orchestrator/templates/specs-regex-escape.js +19 -0
- package/dist/orchestrator/templates/specs-regex-escape.js.map +1 -0
- package/dist/orchestrator/templates/specs-regex-escape.ts +16 -0
- package/dist/orchestrator/templates/specs-test-data.ts +53 -6
- package/dist/orchestrator/templates/wdio.conf.ts +28 -9
- package/package.json +8 -6
- package/src/capabilities/context-router.ts +13 -5
- package/src/cli/commands/audit.ts +22 -3
- package/src/cli/commands/capability.ts +4 -38
- package/src/cli/commands/changed.ts +104 -0
- package/src/cli/commands/delivery.ts +27 -2
- package/src/cli/commands/generate.ts +1 -1
- package/src/cli/commands/results-merge.ts +153 -0
- package/src/cli/commands/update.ts +72 -13
- package/src/cli/index.ts +4 -0
- package/src/exporters/api-catalog-loader.ts +10 -2
- package/src/exporters/api-testcase-formatter.ts +59 -3
- package/src/exporters/csv-exporter.ts +11 -2
- package/src/exporters/json-exporter.ts +7 -2
- package/src/exporters/matrix/build.ts +13 -1
- package/src/exporters/matrix/export.ts +1 -1
- package/src/exporters/matrix/map-loader.ts +5 -0
- package/src/exporters/matrix/render-csv.ts +5 -1
- package/src/exporters/matrix/render-xlsx.ts +4 -1
- package/src/exporters/matrix/types.ts +18 -0
- package/src/exporters/playwright-report-parser.ts +68 -9
- package/src/exporters/results-merge.ts +150 -0
- package/src/exporters/selector-key-resolver.ts +81 -1
- package/src/exporters/types.ts +8 -1
- package/src/exporters/xlsx-report-builder.ts +5 -4
- package/src/generators/gherkin-parser/index.ts +55 -1
- package/src/generators/test-generator/adapters/adapter-interface.ts +23 -1
- package/src/generators/test-generator/adapters/appium/appium-adapter.ts +2 -2
- package/src/generators/test-generator/adapters/appium/templates/imports.hbs +3 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/date-picker-unsupported.hbs +4 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/lifecycle-missing-app-id.hbs +5 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/matches-format-assertion.hbs +15 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/visible-filtered-assertion.hbs +5 -2
- package/src/generators/test-generator/adapters/appium/templates/test-file.hbs +185 -26
- package/src/generators/test-generator/adapters/playwright/playwright-adapter.ts +2 -2
- package/src/generators/test-generator/adapters/playwright/templates/imports.hbs +3 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/actions/select-action.hbs +17 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/attribute-assertion.hbs +6 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/have-value-assertion.hbs +18 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/hidden-with-filter-assertion.hbs +5 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/matches-format-assertion.hbs +5 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/state-with-filter-assertion.hbs +4 -2
- package/src/generators/test-generator/adapters/playwright/templates/steps/navigation/wait-for-element-with-text.hbs +4 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-expect-text.hbs +1 -1
- package/src/generators/test-generator/code-generator.ts +86 -7
- package/src/generators/test-generator/diagnostics.ts +60 -6
- package/src/generators/test-generator/patterns/expect-patterns.ts +37 -2
- package/src/generators/test-generator/patterns/index.ts +58 -33
- package/src/generators/test-generator/patterns/types.ts +36 -0
- package/src/generators/test-generator/patterns/unsupported-step-code.ts +19 -0
- package/src/generators/test-generator/recipe-expander.ts +9 -1
- package/src/generators/test-generator/reset-tier.ts +93 -0
- package/src/generators/test-generator/step-mapper.ts +149 -6
- package/src/generators/test-generator/template-engine.ts +18 -3
- package/src/generators/test-generator/utils/format-pattern.ts +79 -0
- package/src/generators/test-generator/utils/js-literal-scanner.ts +132 -0
- package/src/generators/test-generator/utils/mask-refs-for-matching.ts +21 -0
- package/src/generators/test-generator/utils/runtime-data-transformer.ts +126 -76
- package/src/generators/test-generator/utils/selector-resolver.ts +22 -1
- package/src/generators/test-generator/utils/spec-syntax-check.ts +22 -0
- package/src/harness/annotation-overrides.ts +10 -5
- package/src/harness/audit.ts +82 -12
- package/src/harness/capability-plan.ts +10 -5
- package/src/harness/capability.ts +16 -0
- package/src/harness/catalog/drivers.yaml +5 -0
- package/src/harness/flow-contract.ts +87 -9
- package/src/harness/parse.ts +5 -1
- package/src/harness/quality-gates.ts +64 -6
- package/src/harness/run-manifest.ts +254 -0
- package/src/harness/spec-branches.ts +346 -0
- package/src/harness/spec-coverage.ts +4 -4
- package/src/harness/viewpoint-baseline.ts +41 -6
- package/src/harness/viewpoint-ledger.ts +82 -9
- package/src/index.ts +1 -0
- package/src/orchestrator/lockstep-version.ts +46 -0
- package/src/orchestrator/project-deps-sync.ts +175 -0
- package/src/orchestrator/project-initializer.ts +5 -2
- package/src/orchestrator/templates/ai-src/commands/add-flow.md +16 -0
- package/src/orchestrator/templates/ai-src/commands/create-test.md +9 -0
- package/src/orchestrator/templates/ai-src/commands/delivery.md +9 -2
- package/src/orchestrator/templates/ai-src/commands/run-test.md +109 -0
- package/src/orchestrator/templates/ai-src/config/claude.md +1 -1
- package/src/orchestrator/templates/ai-src/config/copilot.md +1 -1
- package/src/orchestrator/templates/ai-src/skills/sungen-api-design/SKILL.md +49 -0
- package/src/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +14 -3
- package/src/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +110 -6
- package/src/orchestrator/templates/ai-src/skills/sungen-locale/SKILL.md +1 -1
- package/src/orchestrator/templates/ai-src/skills/sungen-mobile-gestures/SKILL.md +8 -0
- package/src/orchestrator/templates/ai-src/skills/sungen-selector-fix/SKILL.md +1 -1
- package/src/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +40 -1
- package/src/orchestrator/templates/ai-src/skills/sungen-tc-review/SKILL.md +15 -0
- package/src/orchestrator/templates/env.appium.example +10 -0
- package/src/orchestrator/templates/specs-api.ts +214 -11
- package/src/orchestrator/templates/specs-regex-escape.ts +16 -0
- package/src/orchestrator/templates/specs-test-data.ts +53 -6
- package/src/orchestrator/templates/wdio.conf.ts +28 -9
|
@@ -18,6 +18,10 @@
|
|
|
18
18
|
* • `value` for `type: 'role'` (role name like 'button', not display)
|
|
19
19
|
* • Entries with no `name` and no usable `value`
|
|
20
20
|
* • Composite templates with 2+ vars (ambiguous which to use)
|
|
21
|
+
* • Locators whose single var is SCOPE text — inside `:has(...)`/`:not(...)`,
|
|
22
|
+
* in a non-final segment, or in an XPath nested predicate / pre-step
|
|
23
|
+
* position (see locatorVarIsScopeText) — substituting those renames the
|
|
24
|
+
* ref to text of a DIFFERENT element (a column header naming a table)
|
|
21
25
|
*/
|
|
22
26
|
|
|
23
27
|
import * as fs from 'fs';
|
|
@@ -90,13 +94,89 @@ function resolveField(value: string, isLocator: boolean): SelectorResolution | n
|
|
|
90
94
|
}
|
|
91
95
|
if (vars.length === 1) {
|
|
92
96
|
// Sole `{{var}}` or composite like `has-text("{{var}}")`. Both resolve
|
|
93
|
-
// to the same var's locale-specific display text
|
|
97
|
+
// to the same var's locale-specific display text — but ONLY when the var
|
|
98
|
+
// is the target element's OWN text. A var inside a `:has(...)` argument
|
|
99
|
+
// or in an ancestor segment names a DIFFERENT element (a column header
|
|
100
|
+
// scoping a table), and substituting it renames the ref to unrelated text.
|
|
101
|
+
if (isLocator && locatorVarIsScopeText(value)) return null;
|
|
94
102
|
return { varName: vars[0] };
|
|
95
103
|
}
|
|
96
104
|
// 2+ vars in one template — ambiguous which to use; skip rather than guess.
|
|
97
105
|
return null;
|
|
98
106
|
}
|
|
99
107
|
|
|
108
|
+
/**
|
|
109
|
+
* True when a locator's single `{{var}}` (FIRST occurrence — repeats of the
|
|
110
|
+
* same var judge by that one, erring toward fidelity) belongs to a SCOPE
|
|
111
|
+
* rather than the target element's own display text.
|
|
112
|
+
*
|
|
113
|
+
* CSS / Playwright engines:
|
|
114
|
+
* - inside a `:has(...)` or `:not(...)` argument at any depth — that text
|
|
115
|
+
* describes ANOTHER element (or the text this one must NOT contain);
|
|
116
|
+
* `:has-text(...)` / `:text(...)` ARE the element's own text and pass;
|
|
117
|
+
* - in a non-final segment — a combinator (space, `>`, `>>`, `+`, `~`) at
|
|
118
|
+
* top level (outside parens, brackets and quotes) follows the var with a
|
|
119
|
+
* real selector after it. A bare trailing match flag (`"{{x}}" i`) or an
|
|
120
|
+
* attribute-internal space (`[name="{{x}}" i]`) is NOT a combinator.
|
|
121
|
+
*
|
|
122
|
+
* XPath (`xpath=` / `//…`):
|
|
123
|
+
* - the var sits in a NESTED predicate (bracket depth ≥ 2, e.g.
|
|
124
|
+
* `//table[.//th[text()="{{col}}"]]` — the th labels the table); or
|
|
125
|
+
* - a location step (`/`) follows the var at top level
|
|
126
|
+
* (`//th[text()="{{col}}"]/ancestor::table`).
|
|
127
|
+
*
|
|
128
|
+
* One left-to-right scan tracking quote state (with backslash-run counting so
|
|
129
|
+
* a literal trailing `\\` can't wedge the scanner in "quoted"), paren frames,
|
|
130
|
+
* and bracket depth.
|
|
131
|
+
*/
|
|
132
|
+
function locatorVarIsScopeText(locator: string): boolean {
|
|
133
|
+
const m = varPattern().exec(locator);
|
|
134
|
+
if (!m) return false;
|
|
135
|
+
const varIndex = m.index;
|
|
136
|
+
const isXpath = /^\s*(?:xpath=|\(*\/\/)/i.test(locator);
|
|
137
|
+
let quote: string | null = null;
|
|
138
|
+
const scopeFrames: boolean[] = []; // one entry per open paren: is it a :has(/:not( frame?
|
|
139
|
+
let bracketDepth = 0;
|
|
140
|
+
let passedVar = false;
|
|
141
|
+
const escaped = (i: number): boolean => {
|
|
142
|
+
let n = 0;
|
|
143
|
+
for (let j = i - 1; j >= 0 && locator[j] === '\\'; j--) n++;
|
|
144
|
+
return n % 2 === 1;
|
|
145
|
+
};
|
|
146
|
+
for (let i = 0; i < locator.length; i++) {
|
|
147
|
+
if (i === varIndex) {
|
|
148
|
+
passedVar = true;
|
|
149
|
+
if (!isXpath && scopeFrames.some(Boolean)) return true;
|
|
150
|
+
if (isXpath && bracketDepth >= 2) return true;
|
|
151
|
+
}
|
|
152
|
+
const c = locator[i];
|
|
153
|
+
if (quote) {
|
|
154
|
+
if (c === quote && !escaped(i)) quote = null;
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if (c === '"' || c === "'") { quote = c; continue; }
|
|
158
|
+
if (c === '(') {
|
|
159
|
+
const pseudo = locator.slice(Math.max(0, i - 4), i).toLowerCase();
|
|
160
|
+
scopeFrames.push(!isXpath && (pseudo === ':has' || pseudo === ':not'));
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (c === ')') { scopeFrames.pop(); continue; }
|
|
164
|
+
if (c === '[') { bracketDepth++; continue; }
|
|
165
|
+
if (c === ']') { bracketDepth = Math.max(0, bracketDepth - 1); continue; }
|
|
166
|
+
if (!passedVar || scopeFrames.length > 0 || bracketDepth > 0) continue;
|
|
167
|
+
if (isXpath) {
|
|
168
|
+
if (c === '/') return true; // a location step after the var → the target comes later
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (c === ' ' || c === '>' || c === '+' || c === '~') {
|
|
172
|
+
const rest = locator.slice(i).replace(/^[\s>+~]+/, '');
|
|
173
|
+
// Bare Playwright match flags after a quoted value are not a new segment.
|
|
174
|
+
if (rest.length > 0 && !/^[is](?:\s|$)/.test(rest)) return true;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
|
|
100
180
|
/**
|
|
101
181
|
* Build the SelectorKey → resolution map from a selectors YAML file.
|
|
102
182
|
* Returns an empty map when the file is missing or malformed.
|
package/src/exporters/types.ts
CHANGED
|
@@ -125,6 +125,13 @@ export interface PlaywrightResult {
|
|
|
125
125
|
duration?: number;
|
|
126
126
|
startTime?: string;
|
|
127
127
|
error?: string;
|
|
128
|
+
/**
|
|
129
|
+
* Every distinct failure message of the final attempt (ANSI/stack stripped).
|
|
130
|
+
* Playwright's top-level `error` is only the FIRST error — when a timeout
|
|
131
|
+
* wraps an assertion, the assertion detail (Locator/Expected/Received) lives
|
|
132
|
+
* in `results[].errors[1+]` and would otherwise never reach the deliverable.
|
|
133
|
+
*/
|
|
134
|
+
errorMessages?: string[];
|
|
128
135
|
tracePath?: string;
|
|
129
136
|
projectName?: string;
|
|
130
137
|
}
|
|
@@ -179,7 +186,7 @@ export interface ApiCatalogEntry {
|
|
|
179
186
|
/** Request-body wire format — decides the Content-Type the request sends.
|
|
180
187
|
* Absent/`json` → application/json, `form` → application/x-www-form-urlencoded,
|
|
181
188
|
* `multipart` → multipart/form-data. */
|
|
182
|
-
encoding?: 'json' | 'form' | 'multipart';
|
|
189
|
+
encoding?: 'json' | 'form' | 'multipart' | 'xml';
|
|
183
190
|
/** Multipart file parts (multipart/form-data). A field value may be a single file spec, its string
|
|
184
191
|
* shorthand, or an array of specs (multiple files under one field name). Typed loosely — guard before use. */
|
|
185
192
|
files?: Record<string, unknown>;
|
|
@@ -640,10 +640,11 @@ export function buildApiDetailRows(
|
|
|
640
640
|
const uniqueAuth = [...new Set(authTags.map((t) => t.slice('@auth:'.length)))];
|
|
641
641
|
const authDatasource = [datasource, ...uniqueAuth].filter(Boolean).join('; ');
|
|
642
642
|
|
|
643
|
-
// Request shape: compose from body + params + encoding.
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
643
|
+
// Request shape: compose from body + params + encoding. A raw XML template can
|
|
644
|
+
// run long — cap the cell so one entry can't blow up the sheet's row height.
|
|
645
|
+
const rawBody = entry.body ? (typeof entry.body === 'string' ? entry.body : JSON.stringify(entry.body)) : '';
|
|
646
|
+
const cappedBody = rawBody.length > 500 ? `${rawBody.slice(0, 499)}…` : rawBody;
|
|
647
|
+
const bodyStr = cappedBody ? `body: ${cappedBody}` : '';
|
|
647
648
|
const paramsArr = Array.isArray(entry.params) ? entry.params as string[] : [];
|
|
648
649
|
const paramsStr = paramsArr.length > 0 ? `params: [${paramsArr.join(', ')}]` : '';
|
|
649
650
|
const requestShape = [bodyStr, paramsStr].filter(Boolean).join('; ') || '—';
|
|
@@ -4,6 +4,51 @@ import * as Messages from '@cucumber/messages';
|
|
|
4
4
|
import fs from 'fs';
|
|
5
5
|
import path from 'path';
|
|
6
6
|
import { SelectorResolver } from '../test-generator/utils/selector-resolver';
|
|
7
|
+
import { maskRefsForMatching } from '../test-generator/utils/mask-refs-for-matching';
|
|
8
|
+
import type { StateClause } from '../test-generator/patterns/types';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Canonical state clauses, longest-first. Order is load-bearing twice over: `sorted ascending` must
|
|
12
|
+
* be tried before any bare word so it is not split, and `unchecked` before `checked` so the
|
|
13
|
+
* substring never misclassifies — the same hazard, and the same fix, as `qualifiedState()` in
|
|
14
|
+
* driver-ui's assertion-patterns.
|
|
15
|
+
*/
|
|
16
|
+
const STATE_CLAUSES: ReadonlyArray<StateClause> = [
|
|
17
|
+
'sorted ascending', 'sorted descending',
|
|
18
|
+
'unchecked', 'checked',
|
|
19
|
+
'hidden', 'visible', 'disabled', 'enabled', 'focused', 'empty', 'loading', 'selected',
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Lift a trailing state clause out of a step sentence so the pattern layer can tell whether the
|
|
24
|
+
* pattern that won actually asserts it (SG-E021). Before this the clause lived ONLY inside
|
|
25
|
+
* `step.text`, every pattern re-regexed it privately, and a clause swallowed by a higher-priority
|
|
26
|
+
* pattern was undetectable by construction.
|
|
27
|
+
*
|
|
28
|
+
* Narrow on purpose — everything this rejects is a step that must keep compiling exactly as it does
|
|
29
|
+
* today:
|
|
30
|
+
* - requires the literal `is` — `wait for [X] to be hidden` is a wait, not a state assertion;
|
|
31
|
+
* - requires TRAILING position — `… is hidden after the modal closes` is prose, not a clause;
|
|
32
|
+
* - rejects `is not <state>` — `in local storage is not empty` is a real storage grammar (D5);
|
|
33
|
+
* - masks `[Ref]` and `{{data}}` CONTENT first, so `[Sorted Ascending]` (a button) and
|
|
34
|
+
* `{{is_empty}}` (a variable) are not read as states.
|
|
35
|
+
*/
|
|
36
|
+
function extractStateClause(text: string): StateClause | undefined {
|
|
37
|
+
// Collapse runs of whitespace and drop a trailing period, so `… is hidden .` reads the same as
|
|
38
|
+
// `… is hidden`. Compared as a plain string tail rather than a built regex: the clause list holds
|
|
39
|
+
// a two-word member (`sorted ascending`), and interpolating it into a RegExp means escaping the
|
|
40
|
+
// separator, which is exactly the kind of quiet breakage `\b`-in-a-template-literal already is.
|
|
41
|
+
const tail = maskRefsForMatching(text)
|
|
42
|
+
.replace(/\s+/g, ' ')
|
|
43
|
+
.replace(/ ?\.$/, '')
|
|
44
|
+
.toLowerCase();
|
|
45
|
+
// ` is ` must sit IMMEDIATELY before the clause. That single space is what excludes `is not empty`
|
|
46
|
+
// — the storage grammar — without a negative lookbehind.
|
|
47
|
+
for (const state of STATE_CLAUSES) {
|
|
48
|
+
if (tail.endsWith(` is ${state}`)) return state;
|
|
49
|
+
}
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
7
52
|
|
|
8
53
|
export interface DataTableRow {
|
|
9
54
|
cells: string[]; // Cell values in order
|
|
@@ -28,6 +73,7 @@ export interface ParsedStep {
|
|
|
28
73
|
parentRef?: string; // Parent scope reference: "in [Parent Name] parentType" → parentRef = "Parent Name"
|
|
29
74
|
parentType?: string; // Parent scope type: table, list, section, dialog, form
|
|
30
75
|
dataTable?: ParsedDataTable; // Inline data table (Cucumber DataTable syntax)
|
|
76
|
+
state?: StateClause; // Trailing canonical state clause, e.g. "hidden" from "… is hidden" (drives SG-E021)
|
|
31
77
|
}
|
|
32
78
|
|
|
33
79
|
export interface ParsedScenario {
|
|
@@ -204,7 +250,14 @@ export class GherkinParser {
|
|
|
204
250
|
const selectorRef = selectorMatch ? selectorMatch[1] : undefined;
|
|
205
251
|
|
|
206
252
|
// Extract data reference: {{login.email.valid}} or <valid_user.email> (legacy)
|
|
207
|
-
|
|
253
|
+
// A `{{var}}` inside a [Reference] is NEVER the step's data ref: a bracket carries a
|
|
254
|
+
// selectors.yaml key or a browser-storage key, both matched verbatim. Blank the brackets
|
|
255
|
+
// out first — the match is un-anchored and takes the FIRST hit, so a step with braces on
|
|
256
|
+
// both sides (`see [{{code}}_TOKEN] in session storage has length {{n}}`, or
|
|
257
|
+
// `remember [X] text as {{v}}` written with a bracket var) would otherwise bind the
|
|
258
|
+
// bracket's variable and silently lose the real ref.
|
|
259
|
+
const textOutsideRefs = text.replace(/\[[^\]]*\]/g, '');
|
|
260
|
+
const dataMatch = textOutsideRefs.match(/\{\{([a-z0-9\-\.\_]+)\}\}|<([^>]+)>/i);
|
|
208
261
|
const dataRef = dataMatch ? (dataMatch[1] || dataMatch[2]) : undefined;
|
|
209
262
|
|
|
210
263
|
// Extract quoted value: "Hello, how are you?"
|
|
@@ -269,6 +322,7 @@ export class GherkinParser {
|
|
|
269
322
|
parentRef,
|
|
270
323
|
parentType,
|
|
271
324
|
dataTable,
|
|
325
|
+
state: extractStateClause(step.text),
|
|
272
326
|
};
|
|
273
327
|
}
|
|
274
328
|
|
|
@@ -31,6 +31,13 @@ export interface TestFileData {
|
|
|
31
31
|
singleAuthRole?: string; // Auth role when all scenarios share the same role
|
|
32
32
|
appPackage?: string; // Mobile (appium): Android package from the feature `Path:` → drives the per-scenario relaunch reset (beforeEach terminate+activate)
|
|
33
33
|
iosBundleId?: string; // Mobile (appium): iOS bundle id from a dual-id `Path: <pkg>/<activity> | <bundleId>`
|
|
34
|
+
resetTier?: string; // Mobile (appium): `@reset:none|soft|data|full` — how much state each scenario starts from. Default 'soft' = the terminate+activate relaunch this adapter always did, so an untagged feature is unchanged. See generators/test-generator/reset-tier.ts
|
|
35
|
+
// Mobile (appium): per-scenario `@reset:` overrides, keyed by the scenario's BASE name (not the
|
|
36
|
+
// rendered it() title — a @cases scenario builds its title at runtime from the row label). An
|
|
37
|
+
// ordered array rather than a map so duplicate names stay visible instead of silently collapsing.
|
|
38
|
+
// `cases` = the scenario's it() title is built at RUNTIME from a row label, so it is the only shape
|
|
39
|
+
// whose title may legitimately carry a ` — <label>` suffix the lookup has to strip.
|
|
40
|
+
scenarioResetTiers?: Array<{ name: string; tier: string; cases: boolean }>;
|
|
34
41
|
}
|
|
35
42
|
|
|
36
43
|
export interface ScenarioData {
|
|
@@ -40,6 +47,7 @@ export interface ScenarioData {
|
|
|
40
47
|
isParallel?: boolean; // @parallel: use fresh page from fixture
|
|
41
48
|
tags?: string; // Pass-through tags for Playwright { tag: [...] }, e.g. "'@smoke', '@high'"
|
|
42
49
|
casesDataset?: string; // @cases:<dataset> — emit one test() per row of the runtime dataset
|
|
50
|
+
resetTier?: string; // Mobile (appium): scenario-level `@reset:` override; raises (never lowers) the feature tier for this scenario only
|
|
43
51
|
}
|
|
44
52
|
|
|
45
53
|
export interface StepTemplateData {
|
|
@@ -59,6 +67,20 @@ export interface LocatorExpression {
|
|
|
59
67
|
* TestGeneratorAdapter
|
|
60
68
|
* Framework-specific code generation using template engine
|
|
61
69
|
*/
|
|
70
|
+
|
|
71
|
+
/** Options for the imports block — one shape shared by every adapter and the template engine. */
|
|
72
|
+
export interface RenderImportsOptions {
|
|
73
|
+
runtimeData?: boolean;
|
|
74
|
+
basePath?: string;
|
|
75
|
+
isParallel?: boolean;
|
|
76
|
+
needsCleanupImport?: boolean;
|
|
77
|
+
needsDb?: boolean;
|
|
78
|
+
needsApi?: boolean;
|
|
79
|
+
needsMock?: boolean;
|
|
80
|
+
urlAssertImports?: string;
|
|
81
|
+
regexEscapeImport?: boolean;
|
|
82
|
+
}
|
|
83
|
+
|
|
62
84
|
export interface TestGeneratorAdapter {
|
|
63
85
|
readonly frameworkName: string;
|
|
64
86
|
readonly fileExtension: string;
|
|
@@ -67,7 +89,7 @@ export interface TestGeneratorAdapter {
|
|
|
67
89
|
// Template rendering methods
|
|
68
90
|
renderTestFile(data: TestFileData): string;
|
|
69
91
|
renderScenario(data: ScenarioData): string;
|
|
70
|
-
renderImports(options?:
|
|
92
|
+
renderImports(options?: RenderImportsOptions): string;
|
|
71
93
|
renderBeforeEach(data: { steps: Array<{ comment?: string; code: string }> }): string;
|
|
72
94
|
renderBeforeAll(data: { steps: Array<{ comment?: string; code: string }> }): string;
|
|
73
95
|
renderAfterEach(data: { steps: Array<{ comment?: string; code: string }> }): string;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
|
-
import { TestGeneratorAdapter, TestFileData, ScenarioData } from '../adapter-interface';
|
|
2
|
+
import { TestGeneratorAdapter, TestFileData, ScenarioData, RenderImportsOptions } from '../adapter-interface';
|
|
3
3
|
import { TemplateEngine } from '../../template-engine';
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -31,7 +31,7 @@ export class AppiumAdapter implements TestGeneratorAdapter {
|
|
|
31
31
|
return this.templateEngine.renderScenario(data);
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
renderImports(options?:
|
|
34
|
+
renderImports(options?: RenderImportsOptions): string {
|
|
35
35
|
return this.templateEngine.renderImports(options);
|
|
36
36
|
}
|
|
37
37
|
|
|
@@ -11,6 +11,9 @@ import { api } from '{{basePath}}/api';
|
|
|
11
11
|
{{#if needsMock}}
|
|
12
12
|
import { mock } from '{{basePath}}/mock';
|
|
13
13
|
{{/if}}
|
|
14
|
+
{{#if regexEscapeImport}}
|
|
15
|
+
import { escapeRegExp } from '{{basePath}}/regex-escape';
|
|
16
|
+
{{/if}}
|
|
14
17
|
|
|
15
18
|
// This file is auto-generated from Gherkin feature files
|
|
16
19
|
// DO NOT EDIT MANUALLY - changes will be overwritten
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
{{!-- A date-picker on native mobile is a DIALOG (spinner/calendar), not a text field: the generic
|
|
2
|
+
fill would click the trigger and setValue into it, selecting nothing while reporting success.
|
|
3
|
+
Same treatment as the upload step — fail loud, and name both ways forward. --}}
|
|
4
|
+
throw new Error("[sungen] {{escapeQuotes reason}}");
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
{{!-- A lifecycle step (restart / reinstall) in a feature with no `Path:` app id. The lifecycle
|
|
2
|
+
templates resolve the app through a helper the test-file emits only when that id is present,
|
|
3
|
+
so without it the spec used to compile clean and die at runtime with a ReferenceError —
|
|
4
|
+
taking the whole spec down, not the step. Fail at the step, naming the missing line. --}}
|
|
5
|
+
throw new Error("[sungen] {{escapeQuotes reason}}");
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{{!-- Format assertion (mobile): same multi-source read as have-text/contain-text
|
|
2
|
+
(@text / @content-desc on Android, label/value on iOS), matched against the shape the spec
|
|
3
|
+
states. The error quotes the FORMAT and the actual string — the compiled regex is noise to
|
|
4
|
+
the person who wrote the requirement. --}}
|
|
5
|
+
{
|
|
6
|
+
const __el = await {{> locator}};
|
|
7
|
+
await __el.waitForDisplayed();
|
|
8
|
+
const __sources = driver.isIOS
|
|
9
|
+
? [await __el.getAttribute('label'), await __el.getAttribute('value')]
|
|
10
|
+
: [await __el.getText(), await __el.getAttribute('content-desc')];
|
|
11
|
+
const __actual = String(__sources.find(s => s && String(s).trim() !== '') ?? '').trim();
|
|
12
|
+
if (!{{{formatRegex}}}.test(__actual)) {
|
|
13
|
+
throw new Error(`[sungen] Expected [{{escapeQuotes selectorRef}}] to match the format "{{escapeQuotes format}}" but it reads "${__actual}"`);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
{{!-- Filtered visible (mobile): $$ over the base selector, per-OS text read matched against
|
|
2
2
|
dataValue — no CSS filter() combinator exists on Appium, so the match is hand-rolled
|
|
3
3
|
(covers both the locator- and role-strategy call sites, since both resolve through
|
|
4
|
-
appium-selector-expr). Assert at least one match is displayed.
|
|
4
|
+
appium-selector-expr). Assert at least one match is displayed. Substring match by
|
|
5
|
+
default, matching the web hasText string contract: case-insensitive with collapsed
|
|
6
|
+
whitespace; `exact` opts into full case-sensitive equality (the anchored-regex twin). --}}
|
|
5
7
|
{
|
|
6
8
|
const __expected = String('{{escapeQuotes dataValue}}').trim();
|
|
9
|
+
const __norm = (s: string) => s.replace(/\s+/g, ' ').trim().toLowerCase();
|
|
7
10
|
const __items = await $$({{> appium-selector-expr}});
|
|
8
11
|
let __found = false;
|
|
9
12
|
for (const __el of __items) {
|
|
@@ -11,7 +14,7 @@
|
|
|
11
14
|
? [await __el.getAttribute('label'), await __el.getAttribute('value')]
|
|
12
15
|
: [await __el.getText(), await __el.getAttribute('content-desc')];
|
|
13
16
|
const __actual = String(__sources.find(s => s && String(s).trim() !== '') ?? '').trim();
|
|
14
|
-
if (__actual === __expected && await __el.isDisplayed()) { __found = true; break; }
|
|
17
|
+
if ({{#if exact}}__actual === __expected{{else}}__norm(__actual).includes(__norm(__expected)){{/if}} && await __el.isDisplayed()) { __found = true; break; }
|
|
15
18
|
}
|
|
16
19
|
if (!__found) throw new Error(`[sungen] Expected a visible match with text "${__expected}"`);
|
|
17
20
|
}
|
|
@@ -366,31 +366,117 @@ describe('{{featureName}}', () => {
|
|
|
366
366
|
}
|
|
367
367
|
return __APP_PKG_DEFAULT__;
|
|
368
368
|
}
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
//
|
|
386
|
-
//
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
369
|
+
// ── Reset tier ────────────────────────────────────────────────────────────────────────────────
|
|
370
|
+
// How much state each scenario starts from (`@reset:` on the Feature; default `soft`):
|
|
371
|
+
// none — nothing reset; state carries over BY DESIGN (a cart accumulating across scenarios)
|
|
372
|
+
// soft — terminate + activate: in-memory state and the nav stack
|
|
373
|
+
// data — soft + wipe the app's data layer (local DB, cache, prefs, keychain)
|
|
374
|
+
// full — remove + reinstall: also permissions and install-time state
|
|
375
|
+
// Unlike web, no cheap "reload the URL" exists here, and a cold start is 5–30s — hence a ladder
|
|
376
|
+
// rather than one all-or-nothing switch.
|
|
377
|
+
const __DECLARED_TIER = '{{resetTier}}';
|
|
378
|
+
const __TIER_ORDER = ['none', 'soft', 'data', 'full'];
|
|
379
|
+
// SUNGEN_RESET_TIER RAISES the floor for a whole run — CI at `data`, local dev at `soft` — without
|
|
380
|
+
// regenerating a single spec (same runtime-resolution model as the test-data overlays). It never
|
|
381
|
+
// LOWERS a declared tier: a feature that asked for `full` needs it. Resolved once, so an
|
|
382
|
+
// unrecognized value warns once rather than per scenario — a silently ignored typo here would
|
|
383
|
+
// reintroduce exactly the false-confidence this tier system exists to remove.
|
|
384
|
+
const __RUN_TIER: string = (() => {
|
|
385
|
+
// Defensive only — the compiler validates the tag — but fall back to `soft`, NOT to index 0
|
|
386
|
+
// (`none`). A safety net that silently drops to "reset nothing" points the wrong way.
|
|
387
|
+
const declaredIdx = __TIER_ORDER.indexOf(__DECLARED_TIER);
|
|
388
|
+
const declared = declaredIdx < 0 ? __TIER_ORDER.indexOf('soft') : declaredIdx;
|
|
389
|
+
const raw = (process.env.SUNGEN_RESET_TIER || '').trim().toLowerCase();
|
|
390
|
+
if (!raw) return __TIER_ORDER[declared];
|
|
391
|
+
const floor = __TIER_ORDER.indexOf(raw);
|
|
392
|
+
if (floor < 0) {
|
|
393
|
+
console.warn(` ! SUNGEN_RESET_TIER='${raw}' is not a tier (${__TIER_ORDER.join(', ')}) — ignored`);
|
|
394
|
+
return __TIER_ORDER[declared];
|
|
395
|
+
}
|
|
396
|
+
return __TIER_ORDER[Math.max(declared, floor)];
|
|
397
|
+
})();
|
|
398
|
+
|
|
399
|
+
// A scenario that FAILED left unknown state behind — a half-written local DB, an open modal — that a
|
|
400
|
+
// plain process relaunch does not undo. So the NEXT scenario resets one rung harder than it asked
|
|
401
|
+
// for: this is the "A fails mid-flow, B fails for A's reasons" case that no amount of relaunching
|
|
402
|
+
// fixes. One rung, one scenario, never sticky — otherwise a single flake would silently promote the
|
|
403
|
+
// whole run to `full` and put 30s on every remaining scenario.
|
|
404
|
+
// `none` is a FIXED POINT by design: it is an explicit state-carryover contract (a cart built up
|
|
405
|
+
// across scenarios), and escalating would silently break the very thing it was asked for. The chain
|
|
406
|
+
// is dead once one link fails anyway, so a relaunch would change only how the rest fail.
|
|
407
|
+
// Mirrors escalateResetTier() in the compiler; a test asserts the two ladders agree.
|
|
408
|
+
const __ESCALATE: Record<string, string> = { none: 'none', soft: 'data', data: 'full', full: 'full' };
|
|
409
|
+
// Holds the TITLE of the scenario that failed, not a boolean — because escalation must reach the next
|
|
410
|
+
// DIFFERENT scenario and must NOT reach a RETRY of the failed one.
|
|
411
|
+
//
|
|
412
|
+
// `mochaOpts.retries` re-runs a failed scenario, and the retry is also "the next run". Escalating it
|
|
413
|
+
// would make attempt 2 reset harder than attempt 1, which breaks the guarantee this config is built
|
|
414
|
+
// on: race victims recover on attempt 2 while REAL failures fail both attempts. A dirty-state failure
|
|
415
|
+
// is a real failure — if the retry silently got a wiped data layer it would pass, the reporter takes
|
|
416
|
+
// the last attempt, and a scenario that only works after a wipe would be delivered as a clean
|
|
417
|
+
// `Passed` under a feature file still reading its weaker declared tier. That false pass is worse than
|
|
418
|
+
// the flakiness escalation was added to fix, so the retry keeps attempt 1's tier and fails honestly.
|
|
419
|
+
//
|
|
420
|
+
// Comparing titles rather than reading mocha's retry counter keeps this independent of the runner's
|
|
421
|
+
// API, and it fails in the SAFE direction: if a title is unavailable both sides compare equal, so no
|
|
422
|
+
// escalation happens and the invariant above still holds. (Two scenarios sharing one title in a file
|
|
423
|
+
// lose the escalation between them — SG-W024 already reports that shape when tiers are involved.)
|
|
424
|
+
let __escalateAfter: string | null = null;
|
|
425
|
+
|
|
426
|
+
// Registered BEFORE any user @afterEach block so a throwing user hook cannot skip the bookkeeping.
|
|
427
|
+
afterEach(async function () {
|
|
428
|
+
__escalateAfter = this.currentTest?.state === 'failed' ? (this.currentTest?.title ?? '') : null;
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
{{#if scenarioResetTiers}}
|
|
432
|
+
// Per-scenario `@reset:` overrides, keyed on the scenario's BASE name. Not on the rendered title:
|
|
433
|
+
// a @cases scenario builds its title at runtime (`name + ' — ' + row.__label`) and appium appends
|
|
434
|
+
// pass-through tags as a `[…]` suffix, so no compile-time key could match what mocha reports.
|
|
435
|
+
// __tierFor therefore strips those two suffixes back off — and MEMBERSHIP-TESTS at every step, so a
|
|
436
|
+
// name it cannot resolve returns 'none' (inherit the feature tier) instead of guessing a wrong one.
|
|
437
|
+
// Silently applying the wrong tier is the failure this whole feature exists to remove.
|
|
438
|
+
const __SCENARIO_TIERS: Record<string, string> = {
|
|
439
|
+
{{#each scenarioResetTiers}}
|
|
440
|
+
'{{escapeQuotes name}}': '{{tier}}',
|
|
441
|
+
{{/each}}
|
|
442
|
+
};
|
|
443
|
+
// Only a @cases scenario builds its it() title at RUNTIME (`name + ' \u2014 ' + row.__label`), so only
|
|
444
|
+
// these names may shed a trailing ` \u2014 <label>` during lookup. Stripping that from EVERY title let a
|
|
445
|
+
// scenario merely NAMED `<something> \u2014 <something>` resolve to a different scenario's tier.
|
|
446
|
+
const __CASES_NAMES = new Set<string>([{{#each scenarioResetTiers}}{{#if cases}}'{{escapeQuotes name}}', {{/if}}{{/each}}]);
|
|
447
|
+
function __tierFor(title: string): string {
|
|
448
|
+
if (Object.prototype.hasOwnProperty.call(__SCENARIO_TIERS, title)) return __SCENARIO_TIERS[title];
|
|
449
|
+
// `\[@` — must be a TAG group, not a trailing selector ref. Without the `@`, a scenario named
|
|
450
|
+
// `VP-001 tap [Save]` would reduce to `VP-001 tap` and could pick up a DIFFERENT scenario's tier.
|
|
451
|
+
const noTags = title.replace(/ \[@[^\]]*\]$/, '');
|
|
452
|
+
if (Object.prototype.hasOwnProperty.call(__SCENARIO_TIERS, noTags)) return __SCENARIO_TIERS[noTags];
|
|
453
|
+
// Shed row-label segments from the RIGHT, one at a time, accepting only a @cases name. Right-to-left
|
|
454
|
+
// and iterative so an em dash inside the scenario NAME or inside the row LABEL still resolves; every
|
|
455
|
+
// step is membership-tested against __CASES_NAMES, so this narrows toward a declared name and never
|
|
456
|
+
// guesses at one.
|
|
457
|
+
let base = noTags;
|
|
458
|
+
for (let i = base.lastIndexOf(' \u2014 '); i >= 0; i = base.lastIndexOf(' \u2014 ')) {
|
|
459
|
+
base = base.slice(0, i);
|
|
460
|
+
if (__CASES_NAMES.has(base)) return __SCENARIO_TIERS[base];
|
|
461
|
+
}
|
|
462
|
+
// Unresolvable ⟹ inherit the feature tier. Never a guess: a wrong tier applied silently is the
|
|
463
|
+
// failure this whole lookup exists to avoid.
|
|
464
|
+
return 'none';
|
|
465
|
+
}
|
|
466
|
+
{{/if}}
|
|
467
|
+
|
|
468
|
+
// Settle after a cold (re)launch: a Flutter app first renders a BLANK accessibility skeleton
|
|
469
|
+
// (nested FrameLayout/View, no content-desc) for several seconds before the real tree appears.
|
|
470
|
+
// So require rendered content — at least one non-empty content-desc/text — on two consecutive
|
|
471
|
+
// reads before proceeding. A generous timeout covers slow cold renders on a stressed emulator.
|
|
472
|
+
// Best-effort: a timeout never fails the scenario, it just falls through to the step's own
|
|
473
|
+
// auto-wait.
|
|
474
|
+
// "Settled" = rendered content present on TWO consecutive reads. Deliberately NOT full source
|
|
475
|
+
// equality: a UI with any looping animation (a rotating promo carousel is enough) never yields
|
|
476
|
+
// two identical snapshots, so an equality oracle starves and burns the whole timeout on every
|
|
477
|
+
// scenario. Two content-bearing reads still reject the blank first-frame skeleton (no
|
|
478
|
+
// content-desc/text at all) that a single read could false-positive on.
|
|
479
|
+
async function __settleUi(timeout: number): Promise<void> {
|
|
394
480
|
let __prevHadContent = false;
|
|
395
481
|
await driver
|
|
396
482
|
.waitUntil(
|
|
@@ -401,9 +487,82 @@ describe('{{featureName}}', () => {
|
|
|
401
487
|
__prevHadContent = hasContent;
|
|
402
488
|
return settled;
|
|
403
489
|
},
|
|
404
|
-
{ timeout
|
|
490
|
+
{ timeout, interval: 400, timeoutMsg: 'UI did not settle (no rendered content)' },
|
|
405
491
|
)
|
|
406
492
|
.catch(() => { /* best-effort — fall through to per-step auto-wait */ });
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// Wipe the app's data layer. `mobile: clearApp` is a UiAutomator2 (Android) command — it maps to
|
|
496
|
+
// `pm clear`, which drops the local DB, cache, SharedPreferences and keychain entries in one call
|
|
497
|
+
// for ~2–5s. XCUITest has NO equivalent, so iOS degrades to a reinstall: slower, but the only way
|
|
498
|
+
// to reach the same layers. That needs a build to install from, hence the named throw.
|
|
499
|
+
async function __clearAppData(pkg: string): Promise<void> {
|
|
500
|
+
if (driver.isIOS) {
|
|
501
|
+
await __reinstallApp(pkg);
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
await driver.execute('mobile: clearApp', { appId: pkg });
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
async function __reinstallApp(pkg: string): Promise<void> {
|
|
508
|
+
const caps = (driver.requestedCapabilities || {}) as Record<string, string>;
|
|
509
|
+
const appPath = caps['appium:app'] || caps['app'];
|
|
510
|
+
if (!appPath) {
|
|
511
|
+
throw new Error(
|
|
512
|
+
`reset tier needs the app's build file to reinstall — set APP_APK (Android) / IOS_APP (iOS) so the appium:app capability is present, or drop to @reset:soft.`,
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
try { await driver.removeApp(pkg); } catch { /* not installed */ }
|
|
516
|
+
await driver.installApp(appPath);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// One reset, at the given tier. `none` still restores the NATIVE context and portrait: both leak at
|
|
520
|
+
// OS/session level rather than app level, so skipping them alongside the relaunch would make `none`
|
|
521
|
+
// outright buggy instead of merely stateful. It skips the settle wait, though — nothing relaunched,
|
|
522
|
+
// so there is no blank first frame to wait through, and burning up to 30s per scenario would defeat
|
|
523
|
+
// the whole point of asking for `none`.
|
|
524
|
+
async function __resetApp(tier: string): Promise<void> {
|
|
525
|
+
const __APP_PKG__ = __resolveAppId();
|
|
526
|
+
if (tier !== 'none') {
|
|
527
|
+
if (tier === 'full') {
|
|
528
|
+
try { await driver.terminateApp(__APP_PKG__); } catch { /* app may not be running yet */ }
|
|
529
|
+
await __reinstallApp(__APP_PKG__);
|
|
530
|
+
} else {
|
|
531
|
+
try { await driver.terminateApp(__APP_PKG__); } catch { /* app may not be running yet */ }
|
|
532
|
+
if (tier === 'data') await __clearAppData(__APP_PKG__);
|
|
533
|
+
}
|
|
534
|
+
// Let the termination fully land before relaunching — activateApp racing a still-dying
|
|
535
|
+
// process can RESUME the old instance (prior scenario's scroll position and all) instead
|
|
536
|
+
// of cold-starting, which strands top-of-screen elements above the viewport.
|
|
537
|
+
await driver.pause(500);
|
|
538
|
+
await driver.activateApp(__APP_PKG__);
|
|
539
|
+
}
|
|
540
|
+
// Restore the NATIVE context — a prior scenario that entered a WebView (`switch to [X] frame`)
|
|
541
|
+
// without switching back would otherwise poison every following native find.
|
|
542
|
+
try { await driver.switchContext('NATIVE_APP'); } catch { /* single-context session */ }
|
|
543
|
+
// Restore portrait so a prior scenario's `rotate to landscape` can't leak into this one — the
|
|
544
|
+
// app relaunch above resets in-app state, but device orientation is OS-level and persists.
|
|
545
|
+
try { await driver.setOrientation('PORTRAIT'); } catch { /* orientation control not supported */ }
|
|
546
|
+
// A fresh install paints its first frame slower than a plain relaunch — same oracle, more rope.
|
|
547
|
+
if (tier !== 'none') await __settleUi(tier === 'full' || tier === 'data' ? 45000 : 30000);
|
|
548
|
+
}
|
|
549
|
+
beforeEach(async function () {
|
|
550
|
+
{{#if scenarioResetTiers}}
|
|
551
|
+
// A scenario tier RAISES the feature tier, never lowers it: a @reset:full feature must not be
|
|
552
|
+
// weakened by one scenario asking for less. Use a separate feature when you genuinely need that.
|
|
553
|
+
const __scenarioTier = __tierFor(this.currentTest?.title ?? '');
|
|
554
|
+
const __base = __TIER_ORDER.indexOf(__scenarioTier) > __TIER_ORDER.indexOf(__RUN_TIER) ? __scenarioTier : __RUN_TIER;
|
|
555
|
+
{{else}}
|
|
556
|
+
const __base = __RUN_TIER;
|
|
557
|
+
{{/if}}
|
|
558
|
+
// Same title ⟹ this is a retry of the scenario that just failed: keep its declared tier so a real
|
|
559
|
+
// failure fails both attempts (see __escalateAfter above).
|
|
560
|
+
const __escalate = __escalateAfter !== null && __escalateAfter !== (this.currentTest?.title ?? '');
|
|
561
|
+
const __tier = __escalate ? __ESCALATE[__base] : __base;
|
|
562
|
+
if (__tier !== __base) {
|
|
563
|
+
console.log(` \u2191 reset escalated ${__base} \u2192 ${__tier} (previous scenario failed: "${__escalateAfter}")`);
|
|
564
|
+
}
|
|
565
|
+
await __resetApp(__tier);
|
|
407
566
|
{{#if backgroundSteps}}
|
|
408
567
|
// Background runs here (inside beforeEach) — the app resets to its launch screen every scenario,
|
|
409
568
|
// so Background setup (nav, dismiss launch promos…) must re-run after each relaunch, not once.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
|
-
import { TestGeneratorAdapter, TestFileData, ScenarioData } from '../adapter-interface';
|
|
2
|
+
import { TestGeneratorAdapter, TestFileData, ScenarioData, RenderImportsOptions } from '../adapter-interface';
|
|
3
3
|
import { TemplateEngine } from '../../template-engine';
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -26,7 +26,7 @@ export class PlaywrightAdapter implements TestGeneratorAdapter {
|
|
|
26
26
|
return this.templateEngine.renderScenario(data);
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
renderImports(options?:
|
|
29
|
+
renderImports(options?: RenderImportsOptions): string {
|
|
30
30
|
return this.templateEngine.renderImports(options);
|
|
31
31
|
}
|
|
32
32
|
|
|
@@ -15,6 +15,9 @@ import { mock } from '{{basePath}}/mock';
|
|
|
15
15
|
{{#if urlAssertImports}}
|
|
16
16
|
import { {{urlAssertImports}} } from '{{basePath}}/url-assert';
|
|
17
17
|
{{/if}}
|
|
18
|
+
{{#if regexEscapeImport}}
|
|
19
|
+
import { escapeRegExp } from '{{basePath}}/regex-escape';
|
|
20
|
+
{{/if}}
|
|
18
21
|
|
|
19
22
|
// This file is auto-generated from Gherkin feature files
|
|
20
23
|
// DO NOT EDIT MANUALLY - changes will be overwritten
|