@sun-asterisk/sungen 3.2.24-beta.6 → 3.2.25
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/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 +10 -0
- 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/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 +7 -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/biometric-action.hbs +9 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/check-action.hbs +11 -6
- 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/deep-link-action.hbs +8 -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/actions/location-services-action.hbs +11 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/reinstall-app-action.hbs +21 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/restart-app-action.hbs +16 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/set-connectivity-action.hbs +11 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/uncheck-action.hbs +9 -5
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/checked-assertion.hbs +10 -4
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/device-log-assertion.hbs +16 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/disappears-within-assertion.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/not-checked-assertion.hbs +6 -4
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/state-with-filter-assertion.hbs +4 -3
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/still-visible-after-assertion.hbs +4 -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/steps/gestures/pull-to-refresh-action.hbs +3 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/gestures/swipe-action.hbs +3 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/partials/appium-selector.hbs +7 -1
- package/dist/generators/test-generator/adapters/appium/templates/test-file.hbs +302 -29
- 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 +19 -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 +24 -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.map +1 -1
- package/dist/harness/audit.js +7 -5
- 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 +14 -4
- 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/parse.d.ts +1 -0
- package/dist/harness/parse.d.ts.map +1 -1
- package/dist/harness/parse.js +6 -1
- package/dist/harness/parse.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/script-check.d.ts.map +1 -1
- package/dist/harness/script-check.js +6 -2
- package/dist/harness/script-check.js.map +1 -1
- package/dist/harness/sensors.d.ts.map +1 -1
- package/dist/harness/sensors.js +5 -1
- package/dist/harness/sensors.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 +25 -4
- 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/mobile-runtime-scaffolder.d.ts.map +1 -1
- package/dist/orchestrator/mobile-runtime-scaffolder.js +5 -1
- package/dist/orchestrator/mobile-runtime-scaffolder.js.map +1 -1
- 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/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 +120 -10
- 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 +31 -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 +9 -0
- package/dist/orchestrator/templates/ai-src/skills/sungen-tc-review/SKILL.md +15 -0
- package/dist/orchestrator/templates/env.appium.example +21 -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-db.d.ts.map +1 -1
- package/dist/orchestrator/templates/specs-db.js +79 -6
- package/dist/orchestrator/templates/specs-db.js.map +1 -1
- package/dist/orchestrator/templates/specs-db.ts +70 -7
- 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 +126 -10
- 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 +1 -0
- package/dist/utils/selector-types.js.map +1 -1
- package/package.json +8 -6
- package/src/capabilities/context-router.ts +13 -5
- 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 +9 -0
- package/src/exporters/matrix/export.ts +1 -1
- package/src/exporters/matrix/render-csv.ts +5 -1
- package/src/exporters/matrix/render-xlsx.ts +4 -1
- package/src/exporters/matrix/types.ts +7 -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/biometric-action.hbs +9 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/check-action.hbs +11 -6
- 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/deep-link-action.hbs +8 -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/actions/location-services-action.hbs +11 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/reinstall-app-action.hbs +21 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/restart-app-action.hbs +16 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/set-connectivity-action.hbs +11 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/uncheck-action.hbs +9 -5
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/checked-assertion.hbs +10 -4
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/device-log-assertion.hbs +16 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/disappears-within-assertion.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/not-checked-assertion.hbs +6 -4
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/state-with-filter-assertion.hbs +4 -3
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/still-visible-after-assertion.hbs +4 -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/steps/gestures/pull-to-refresh-action.hbs +3 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/gestures/swipe-action.hbs +3 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/partials/appium-selector.hbs +7 -1
- package/src/generators/test-generator/adapters/appium/templates/test-file.hbs +302 -29
- 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 +22 -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 +26 -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 +7 -5
- package/src/harness/capability-plan.ts +13 -6
- package/src/harness/capability.ts +16 -0
- package/src/harness/catalog/drivers.yaml +5 -0
- package/src/harness/parse.ts +7 -1
- package/src/harness/run-manifest.ts +254 -0
- package/src/harness/script-check.ts +6 -2
- package/src/harness/sensors.ts +5 -1
- package/src/harness/viewpoint-ledger.ts +26 -5
- package/src/index.ts +1 -0
- package/src/orchestrator/lockstep-version.ts +46 -0
- package/src/orchestrator/mobile-runtime-scaffolder.ts +5 -1
- package/src/orchestrator/project-deps-sync.ts +175 -0
- package/src/orchestrator/project-initializer.ts +5 -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 +120 -10
- 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 +31 -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 +9 -0
- package/src/orchestrator/templates/ai-src/skills/sungen-tc-review/SKILL.md +15 -0
- package/src/orchestrator/templates/env.appium.example +21 -0
- package/src/orchestrator/templates/specs-api.ts +214 -11
- package/src/orchestrator/templates/specs-db.ts +70 -7
- 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 +126 -10
- package/src/utils/selector-types.ts +1 -0
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
} from './feature-parser';
|
|
17
17
|
import { formatPrecondition, groupExpectedByAnchor, keepActor, stripActor } from './step-formatter';
|
|
18
18
|
import { formatTestData, substituteTestDataVars } from './test-data-resolver';
|
|
19
|
-
import { collectSecretValues, formatApiExpected, formatApiRequest, hasApiCalls, redactSecrets } from './api-testcase-formatter';
|
|
19
|
+
import { collectSecretValues, formatApiCallSummaryLines, formatApiExpected, formatApiRequest, hasApiCalls, redactSecrets } from './api-testcase-formatter';
|
|
20
20
|
import {
|
|
21
21
|
formatExecutedDate,
|
|
22
22
|
statusToTestResult,
|
|
@@ -99,9 +99,14 @@ export function buildScreenSnapshot(input: BuildScreenSnapshotInput): ScreenSnap
|
|
|
99
99
|
};
|
|
100
100
|
const formatStep = (s: { text: string; bucket: 'given' | 'when' | 'then' }) =>
|
|
101
101
|
localize(s.bucket === 'given' ? keepActor(s.text) : stripActor(s.text));
|
|
102
|
+
// Same `@api:` precondition lines as csv-exporter's buildTestCaseRows — the
|
|
103
|
+
// dashboard and the delivery CSV must not disagree about what a row does.
|
|
104
|
+
const apiCallLines = (input.kind === 'screen' || input.kind === 'flow') && input.apiCatalog && hasApiCalls(m.feature.tags)
|
|
105
|
+
? formatApiCallSummaryLines(m.feature.tags, input.apiCatalog)
|
|
106
|
+
: [];
|
|
102
107
|
const precondition = formatPrecondition(
|
|
103
108
|
authRole,
|
|
104
|
-
(m.resolvedBackgroundSteps ?? []).map(formatStep).filter(Boolean)
|
|
109
|
+
[...(m.resolvedBackgroundSteps ?? []).map(formatStep).filter(Boolean), ...apiCallLines]
|
|
105
110
|
);
|
|
106
111
|
let steps = m.resolvedSteps.map(formatStep).filter(Boolean);
|
|
107
112
|
// Same anchor rule as csv-exporter (shared groupExpectedByAnchor) so the
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import { ApiCatalogEntry, FeatureMetadata, PlaywrightResult } from '../types';
|
|
9
9
|
import { formatApiCallLines, parseApiCalls } from '../api-testcase-formatter';
|
|
10
|
+
import { collectQaEnvSecretDenylist } from '../qa-env-secret-denylist';
|
|
10
11
|
import { bucketTexts } from './step-buckets';
|
|
11
12
|
import { MergedScenario } from '../scenario-merger';
|
|
12
13
|
import {
|
|
@@ -140,6 +141,10 @@ export function deriveVariants(inputs: Pick<BuildInputs, 'feature' | 'merged' |
|
|
|
140
141
|
const { vpId, category1 } = splitVpAndName(m.feature.name);
|
|
141
142
|
if (!vpId) continue; // Gate A reports it — no variant can be addressed without a VP-id
|
|
142
143
|
const tags = m.feature.tags;
|
|
144
|
+
// @exploration probes live outside the official suite (the same contract the audit's
|
|
145
|
+
// source-tracing exemption follows) — they never become deliverable coverage variants,
|
|
146
|
+
// so the gates and the map never ask for them.
|
|
147
|
+
if (tags.some((t) => /^@exploration\b/i.test(t))) continue;
|
|
143
148
|
const mode = extractTestcaseType(tags) === 'Manual' ? 'manual' : 'auto';
|
|
144
149
|
const authRole = extractAuthRole(tags);
|
|
145
150
|
// Category segment of the id, whatever the project's scheme:
|
|
@@ -513,6 +518,9 @@ export function expandMapRef(ref: string, variantsByVp: Map<string, CoverageVari
|
|
|
513
518
|
export function buildMatrix(inputs: BuildInputs): MatrixModel {
|
|
514
519
|
const { unit, map, transformerVersion } = inputs;
|
|
515
520
|
const variants = deriveVariants(inputs);
|
|
521
|
+
// Same denylist the testcases CSV applies to its Note column — the matrix error
|
|
522
|
+
// cells render the same failure messages and go into the same client hand-off.
|
|
523
|
+
const secretValues = inputs.testData ? collectQaEnvSecretDenylist(inputs.testData).values : [];
|
|
516
524
|
|
|
517
525
|
const variantsByVp = new Map<string, CoverageVariant[]>();
|
|
518
526
|
for (const v of variants) {
|
|
@@ -612,6 +620,7 @@ export function buildMatrix(inputs: BuildInputs): MatrixModel {
|
|
|
612
620
|
dispositions,
|
|
613
621
|
requirements,
|
|
614
622
|
findings,
|
|
623
|
+
secretValues,
|
|
615
624
|
manifest: {
|
|
616
625
|
unit,
|
|
617
626
|
transformerVersion,
|
|
@@ -53,7 +53,7 @@ function readSpecText(paths: MatrixTargetPaths): string {
|
|
|
53
53
|
: '';
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
/** apis.yaml endpoint catalog for
|
|
56
|
+
/** apis.yaml endpoint catalog — resolved for EVERY kind (screen/flow read their unit-scoped catalog + the shared one), so hybrid @api preconditions render their real request contract. */
|
|
57
57
|
function catalogFor(paths: MatrixTargetPaths): Record<string, ApiCatalogEntry> {
|
|
58
58
|
return loadApiCatalog(paths.cwd, paths.kind ?? 'screen', paths.area ?? paths.unit);
|
|
59
59
|
}
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* and result), so downstream pipelines can re-derive both views from one file.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { redactSecrets } from '../api-testcase-formatter';
|
|
9
|
+
import { clipNoteField } from '../playwright-report-parser';
|
|
8
10
|
import { variantState } from './build';
|
|
9
11
|
import { coverageSummary, itemModeLabel, itemResultLabel } from './render-xlsx';
|
|
10
12
|
import { MatrixModel } from './types';
|
|
@@ -75,7 +77,9 @@ export function renderMatrixCsv(model: MatrixModel): string {
|
|
|
75
77
|
v.traces.join(' '),
|
|
76
78
|
variantState(v),
|
|
77
79
|
isoDate(v.result?.startTime),
|
|
78
|
-
|
|
80
|
+
// Redact BEFORE clipping — a clip boundary must never split a secret in
|
|
81
|
+
// a way that lets its prefix through. Same denylist as the Note column.
|
|
82
|
+
v.result?.error ? clipNoteField(redactSecrets(String(v.result.error), model.secretValues)) : '',
|
|
79
83
|
'',
|
|
80
84
|
].map(esc).join(','));
|
|
81
85
|
}
|
|
@@ -27,6 +27,8 @@ import {
|
|
|
27
27
|
BLACK,
|
|
28
28
|
LAVENDER,
|
|
29
29
|
} from '../xlsx-report-builder';
|
|
30
|
+
import { redactSecrets } from '../api-testcase-formatter';
|
|
31
|
+
import { clipNoteField } from '../playwright-report-parser';
|
|
30
32
|
import { variantState } from './build';
|
|
31
33
|
import { DeliveryItem, MatrixModel, MapCategory } from './types';
|
|
32
34
|
|
|
@@ -229,7 +231,8 @@ function addMatrixSheet(wb: ExcelJS.Workbook, model: MatrixModel, sungenVersion?
|
|
|
229
231
|
variantState(v),
|
|
230
232
|
isoDate(v.result?.startTime),
|
|
231
233
|
'',
|
|
232
|
-
|
|
234
|
+
// Redact BEFORE clipping — same denylist as the testcases CSV Note column.
|
|
235
|
+
v.result?.error ? clipNoteField(redactSecrets(String(v.result.error), model.secretValues)) : '',
|
|
233
236
|
'',
|
|
234
237
|
];
|
|
235
238
|
vValues.forEach((val, i) => dataCell(vr.getCell(2 + i), val, { center: i === 9 }));
|
|
@@ -235,6 +235,13 @@ export interface MatrixModel {
|
|
|
235
235
|
manifest: MatrixManifest;
|
|
236
236
|
/** Perf verdict (#569) — present when qa/perf.yaml sets a budget AND the run produced durations. */
|
|
237
237
|
perf?: import('../../harness/perf').PerfVerdict | null;
|
|
238
|
+
/**
|
|
239
|
+
* Resolved ${QA_*} secret values referenced by this unit's test data. Failure
|
|
240
|
+
* messages quote runtime values (a Received: line can echo a typed password),
|
|
241
|
+
* so every error cell the renderers emit must be redacted against this list —
|
|
242
|
+
* the deliverable ships to the client.
|
|
243
|
+
*/
|
|
244
|
+
secretValues: string[];
|
|
238
245
|
}
|
|
239
246
|
|
|
240
247
|
/** Complexity warning threshold (rules draft Gate I; raised for compact grouping) — echoed in the manifest. */
|
|
@@ -16,14 +16,22 @@ import { PlaywrightResult } from './types';
|
|
|
16
16
|
* Candidate order (first existing wins):
|
|
17
17
|
* 1. <genDir>/<featureBaseName>-test-result.<SUNGEN_ENV>.json (env runs)
|
|
18
18
|
* 2. <genDir>/<featureBaseName>-test-result.json (per-feature reporter)
|
|
19
|
-
* 3. <
|
|
20
|
-
*
|
|
19
|
+
* 3. <genDir>/<featureBaseName>-test-result.<os>[.<SUNGEN_ENV>].json — mobile per-OS
|
|
20
|
+
* reporter files (wdio writes one per capability; a mobile project has NO base file).
|
|
21
|
+
* MOBILE_PLATFORM picks the OS explicitly; otherwise android, then ios.
|
|
22
|
+
* 4. <cwd>/test-results/results.<SUNGEN_ENV>.json
|
|
23
|
+
* 5. <cwd>/test-results/results.json (playwright default)
|
|
21
24
|
*/
|
|
22
25
|
export function resolveResultsPath(cwd: string, genDir: string, featureBaseName: string): string | null {
|
|
23
26
|
const env = process.env.SUNGEN_ENV;
|
|
24
27
|
const candidates: string[] = [];
|
|
25
28
|
if (env) candidates.push(path.join(genDir, `${featureBaseName}-test-result.${env}.json`));
|
|
26
29
|
candidates.push(path.join(genDir, `${featureBaseName}-test-result.json`));
|
|
30
|
+
const mobileOs = (process.env.MOBILE_PLATFORM || '').toLowerCase();
|
|
31
|
+
for (const os of mobileOs === 'android' || mobileOs === 'ios' ? [mobileOs] : ['android', 'ios']) {
|
|
32
|
+
if (env) candidates.push(path.join(genDir, `${featureBaseName}-test-result.${os}.${env}.json`));
|
|
33
|
+
candidates.push(path.join(genDir, `${featureBaseName}-test-result.${os}.json`));
|
|
34
|
+
}
|
|
27
35
|
if (env) candidates.push(path.join(cwd, 'test-results', `results.${env}.json`));
|
|
28
36
|
candidates.push(path.join(cwd, 'test-results', 'results.json'));
|
|
29
37
|
for (const c of candidates) {
|
|
@@ -57,6 +65,9 @@ interface PlaywrightSpec {
|
|
|
57
65
|
duration?: number; // ms — Playwright emits this on every result (#569 perf budgets)
|
|
58
66
|
startTime?: string;
|
|
59
67
|
error?: { message?: string; stack?: string };
|
|
68
|
+
// ALL errors of the attempt. `error` above is only the first one — when a
|
|
69
|
+
// timeout interrupts an assertion, the assertion detail is errors[1+].
|
|
70
|
+
errors?: Array<{ message?: string; stack?: string }>;
|
|
60
71
|
attachments?: Array<{ name?: string; path?: string; contentType?: string }>;
|
|
61
72
|
}>;
|
|
62
73
|
}>;
|
|
@@ -110,7 +121,30 @@ export function loadPlaywrightReport(reportPath: string): Map<string, Playwright
|
|
|
110
121
|
const results = test?.results || [];
|
|
111
122
|
const res = results[results.length - 1];
|
|
112
123
|
const status = normalizeStatus(res?.status);
|
|
113
|
-
|
|
124
|
+
// Collect EVERY failure message of the attempt, not just the first: a test
|
|
125
|
+
// timeout wraps the real assertion failure, so `error` alone holds only
|
|
126
|
+
// "Test timeout of 15000ms exceeded." while the Locator/Expected/Received
|
|
127
|
+
// detail QA needs sits in errors[1+]. Stack frames are stripped PER message
|
|
128
|
+
// before joining — otherwise the first message's stack swallows the rest
|
|
129
|
+
// when downstream consumers cut at the first "at " frame.
|
|
130
|
+
const rawMessages = (res?.errors?.length ? res.errors.map((e) => e.message) : [res?.error?.message])
|
|
131
|
+
.map((m) => stripAnsi(m || ''))
|
|
132
|
+
.filter(Boolean);
|
|
133
|
+
const distinct: string[] = [];
|
|
134
|
+
const seenKeys = new Set<string>();
|
|
135
|
+
for (const m of rawMessages) {
|
|
136
|
+
const stripped = m.split(/\n\s*at\s/)[0].trim();
|
|
137
|
+
// Key ignores a leading `Error:` — "Error: page closed" and "page closed"
|
|
138
|
+
// are the same failure and must not render as two Note bullets.
|
|
139
|
+
const key = stripped.replace(/^Error:\s*/i, '');
|
|
140
|
+
if (!key || seenKeys.has(key)) continue;
|
|
141
|
+
seenKeys.add(key);
|
|
142
|
+
distinct.push(stripped);
|
|
143
|
+
}
|
|
144
|
+
// Single message: keep the legacy shape (stack intact) so every existing
|
|
145
|
+
// consumer renders byte-identical output. Multiple: join the stack-stripped
|
|
146
|
+
// distinct messages so none can hide behind another's stack.
|
|
147
|
+
const errorMsg = distinct.length > 1 ? distinct.join('\n') : stripAnsi(res?.error?.message || rawMessages[0] || '');
|
|
114
148
|
const trace = res?.attachments?.find((a) => a.name === 'trace' || a.contentType === 'application/zip')?.path;
|
|
115
149
|
|
|
116
150
|
// Strip the leading file-path suite title if present.
|
|
@@ -124,6 +158,7 @@ export function loadPlaywrightReport(reportPath: string): Map<string, Playwright
|
|
|
124
158
|
duration: typeof res?.duration === 'number' ? res.duration : undefined,
|
|
125
159
|
startTime: res?.startTime,
|
|
126
160
|
error: errorMsg,
|
|
161
|
+
errorMessages: distinct.length > 0 ? distinct : undefined,
|
|
127
162
|
tracePath: trace,
|
|
128
163
|
projectName: test?.projectName,
|
|
129
164
|
});
|
|
@@ -178,7 +213,7 @@ function normalizeStatus(s: string | undefined): PlaywrightResult['status'] {
|
|
|
178
213
|
* "specs/generated/kudos/kudos.spec.ts > Feature Name > ...",
|
|
179
214
|
* strip that leading segment so matching works against "Feature Name > ...".
|
|
180
215
|
*/
|
|
181
|
-
function stripLeadingFilePathSuite(fullTitle: string): string {
|
|
216
|
+
export function stripLeadingFilePathSuite(fullTitle: string): string {
|
|
182
217
|
const parts = fullTitle.split(' > ');
|
|
183
218
|
if (parts.length > 1 && /\.spec\.ts$/i.test(parts[0])) {
|
|
184
219
|
return parts.slice(1).join(' > ');
|
|
@@ -256,11 +291,23 @@ export function parseExpectedReceived(raw: string): { expected?: string; receive
|
|
|
256
291
|
export function formatNote(result: PlaywrightResult | undefined, expectedFallback = ''): string {
|
|
257
292
|
if (!result) return '';
|
|
258
293
|
if (result.status !== 'failed' && result.status !== 'timedOut') return '';
|
|
259
|
-
//
|
|
260
|
-
//
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
|
|
294
|
+
// Per-message: stack frames dropped (frames start with "at ") + a leading `Error:` stripped.
|
|
295
|
+
// Keep newlines inside each message so parseExpectedReceived can delimit Expected/Received.
|
|
296
|
+
const messages = (result.errorMessages?.length ? result.errorMessages : [result.error || ''])
|
|
297
|
+
.map((m) => m.split(/\n\s*at\s/)[0].replace(/^Error:\s*/i, '').trim())
|
|
298
|
+
.filter(Boolean);
|
|
299
|
+
// The message that carries the assertion detail drives Expected/Actual; the other
|
|
300
|
+
// messages (e.g. the "Test timeout of 15000ms exceeded." wrapper) must still be
|
|
301
|
+
// shown — a real assertion failure rendered as a bare timeout reads as flaky.
|
|
302
|
+
// Prefer a message carrying BOTH halves: a half-match (Expected in one message,
|
|
303
|
+
// Received in another) would otherwise put the wrong text in Actual. With
|
|
304
|
+
// expect.soft the first assertion drives the split and the later assertion
|
|
305
|
+
// messages render as `• Error:` bullets — content preserved, columns describe #1.
|
|
306
|
+
const halves = (m: string): number => { const er = parseExpectedReceived(m); return (er.expected ? 1 : 0) + (er.received ? 1 : 0); };
|
|
307
|
+
const assertion = messages.find((m) => halves(m) === 2) ?? messages.find((m) => halves(m) >= 1) ?? messages[0] ?? '';
|
|
308
|
+
const wrappers = messages.filter((m) => m !== assertion);
|
|
309
|
+
const { expected, received } = parseExpectedReceived(assertion);
|
|
310
|
+
const actual = received || clip(assertion); // received value, else the raw error
|
|
264
311
|
|
|
265
312
|
// Newline-separated bullets so each field lands on its own row when the cell wraps — matches the
|
|
266
313
|
// Test Data column's `• key: value` formatting. Excel/CSV honour `\n` inside quoted cells; the
|
|
@@ -276,6 +323,18 @@ export function formatNote(result: PlaywrightResult | undefined, expectedFallbac
|
|
|
276
323
|
else if (lines.length > 1) parts.push(`• Expected result:\n${lines.map((l) => ` ${l}`).join('\n')}`);
|
|
277
324
|
}
|
|
278
325
|
if (actual) parts.push(`• Actual result: ${actual}`);
|
|
326
|
+
// Non-assertion companions of an assertion failure (timeout wrapper etc.), one
|
|
327
|
+
// clipped bullet each so no cell field can blow past the XLSX-safe cap.
|
|
328
|
+
for (const w of wrappers) parts.push(`• Error: ${clip(w)}`);
|
|
279
329
|
if (result.tracePath) parts.push(`• Trace: ${result.tracePath}`);
|
|
280
330
|
return parts.join('\n');
|
|
281
331
|
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* One-cell error rendering for the matrix deliverable: newlines collapsed and
|
|
335
|
+
* clipped to the same cap as Note fields. Replaces ad-hoc `slice(0, 200)` calls
|
|
336
|
+
* that could cut a joined multi-error message mid-assertion.
|
|
337
|
+
*/
|
|
338
|
+
export function clipNoteField(s: string): string {
|
|
339
|
+
return clip(s);
|
|
340
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Merge two Playwright JSON reports by FULL TEST TITLE — the fix for subset
|
|
3
|
+
* re-runs: Playwright's JSON reporter overwrites the per-target result file,
|
|
4
|
+
* so re-running the 3 failed cases of a 100-case screen used to throw away
|
|
5
|
+
* the 97 passing results (QA then re-ran everything, or asked an AI to merge
|
|
6
|
+
* by hand — non-deterministic).
|
|
7
|
+
*
|
|
8
|
+
* Semantics (deterministic):
|
|
9
|
+
* - same title in both → the NEW spec wins wholesale (its results, its
|
|
10
|
+
* attempts, its timing);
|
|
11
|
+
* - title only in base → kept as-is;
|
|
12
|
+
* - title only in new → appended (its suite structure preserved).
|
|
13
|
+
* Titles compare AFTER stripping the leading file-path suite segment, so a
|
|
14
|
+
* base and a new report generated from different cwd/suite paths still match.
|
|
15
|
+
* `@cases` rows carry a ` — <label>` title suffix and merge as independent
|
|
16
|
+
* entries — re-running one dataset row keeps the other rows.
|
|
17
|
+
*
|
|
18
|
+
* The merge never deletes: a base title missing from the current spec (a
|
|
19
|
+
* scenario renamed by a spec edit) is a GHOST the caller may WARN about, but
|
|
20
|
+
* pruning is not this function's job — the delivery builds rows from the
|
|
21
|
+
* feature, so ghosts never render; only the dead `stats` block would drift,
|
|
22
|
+
* and it is recomputed below.
|
|
23
|
+
*
|
|
24
|
+
* Everything outside suites/specs/tests is passed through untouched from the
|
|
25
|
+
* BASE report (config, metadata, unknown future fields) — tolerance over
|
|
26
|
+
* strictness, the shape belongs to Playwright.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { stripLeadingFilePathSuite } from './playwright-report-parser';
|
|
30
|
+
|
|
31
|
+
interface SpecRef {
|
|
32
|
+
title: string; // stripped full title
|
|
33
|
+
parent: any[]; // the specs array holding the node
|
|
34
|
+
index: number;
|
|
35
|
+
node: any;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface MergeOutcome {
|
|
39
|
+
merged: any;
|
|
40
|
+
replaced: string[]; // titles taken from the new report over the base
|
|
41
|
+
added: string[]; // titles only in the new report (appended)
|
|
42
|
+
kept: string[]; // titles only in the base (untouched)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Collect every spec node with its stripped full title and parent array. */
|
|
46
|
+
function collectSpecs(report: any): SpecRef[] {
|
|
47
|
+
const out: SpecRef[] = [];
|
|
48
|
+
const walk = (suites: any[] | undefined, stack: string[]): void => {
|
|
49
|
+
if (!Array.isArray(suites)) return;
|
|
50
|
+
for (const suite of suites) {
|
|
51
|
+
if (!suite || typeof suite !== 'object') continue;
|
|
52
|
+
const nextStack = typeof suite.title === 'string' && suite.title ? [...stack, suite.title] : stack;
|
|
53
|
+
if (Array.isArray(suite.specs)) {
|
|
54
|
+
suite.specs.forEach((spec: any, index: number) => {
|
|
55
|
+
if (!spec || typeof spec.title !== 'string') return;
|
|
56
|
+
const fullTitle = stripLeadingFilePathSuite([...nextStack, spec.title].join(' > '));
|
|
57
|
+
out.push({ title: fullTitle, parent: suite.specs, index, node: spec });
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
walk(suite.suites, nextStack);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
walk(report?.suites, []);
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Drop consumed specs from a suite tree; prune suites left with nothing. */
|
|
68
|
+
function pruneConsumed(suites: any[], consumed: Set<any>): any[] {
|
|
69
|
+
const kept: any[] = [];
|
|
70
|
+
for (const suite of suites) {
|
|
71
|
+
if (!suite || typeof suite !== 'object') continue;
|
|
72
|
+
if (Array.isArray(suite.specs)) suite.specs = suite.specs.filter((s: any) => !consumed.has(s));
|
|
73
|
+
if (Array.isArray(suite.suites)) suite.suites = pruneConsumed(suite.suites, consumed);
|
|
74
|
+
const hasSpecs = Array.isArray(suite.specs) && suite.specs.length > 0;
|
|
75
|
+
const hasSuites = Array.isArray(suite.suites) && suite.suites.length > 0;
|
|
76
|
+
if (hasSpecs || hasSuites) kept.push(suite);
|
|
77
|
+
}
|
|
78
|
+
return kept;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Recompute the report-level stats counters from the merged tree's final attempts. */
|
|
82
|
+
function recomputeStats(merged: any): void {
|
|
83
|
+
if (!merged.stats || typeof merged.stats !== 'object') return;
|
|
84
|
+
let expected = 0, unexpected = 0, skipped = 0, flaky = 0;
|
|
85
|
+
for (const ref of collectSpecs(merged)) {
|
|
86
|
+
for (const test of ref.node.tests ?? []) {
|
|
87
|
+
const results = test?.results ?? [];
|
|
88
|
+
const last = results[results.length - 1];
|
|
89
|
+
const status = last?.status;
|
|
90
|
+
if (status === 'passed') { if (results.some((r: any) => r?.status === 'failed' || r?.status === 'timedOut')) flaky++; else expected++; }
|
|
91
|
+
else if (status === 'skipped') skipped++;
|
|
92
|
+
else if (status) unexpected++;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
merged.stats = { ...merged.stats, expected, unexpected, skipped, flaky };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Merge `next` into a DEEP COPY of `base` (neither input is mutated).
|
|
100
|
+
* Throws on inputs that are not report-shaped objects.
|
|
101
|
+
*/
|
|
102
|
+
export function mergeResultsReports(base: any, next: any): MergeOutcome {
|
|
103
|
+
for (const [label, r] of [['base', base], ['new', next]] as const) {
|
|
104
|
+
if (!r || typeof r !== 'object' || Array.isArray(r)) {
|
|
105
|
+
throw new Error(`results merge: the ${label} report is not a JSON object.`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const merged = JSON.parse(JSON.stringify(base));
|
|
109
|
+
const nextCopy = JSON.parse(JSON.stringify(next));
|
|
110
|
+
|
|
111
|
+
// Last occurrence wins within one report — the same tie-break walkSuites uses.
|
|
112
|
+
const newByTitle = new Map<string, SpecRef>();
|
|
113
|
+
for (const ref of collectSpecs(nextCopy)) newByTitle.set(ref.title, ref);
|
|
114
|
+
|
|
115
|
+
const replaced: string[] = [];
|
|
116
|
+
const kept: string[] = [];
|
|
117
|
+
const consumed = new Set<any>();
|
|
118
|
+
for (const ref of collectSpecs(merged)) {
|
|
119
|
+
const incoming = newByTitle.get(ref.title);
|
|
120
|
+
if (incoming) {
|
|
121
|
+
ref.parent[ref.index] = incoming.node; // the new spec wins wholesale
|
|
122
|
+
consumed.add(incoming.node);
|
|
123
|
+
replaced.push(ref.title);
|
|
124
|
+
} else {
|
|
125
|
+
kept.push(ref.title);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const added: string[] = [];
|
|
130
|
+
for (const [title, ref] of newByTitle) {
|
|
131
|
+
if (!consumed.has(ref.node)) added.push(title);
|
|
132
|
+
}
|
|
133
|
+
if (added.length > 0 && Array.isArray(nextCopy.suites)) {
|
|
134
|
+
merged.suites = [...(Array.isArray(merged.suites) ? merged.suites : []), ...pruneConsumed(nextCopy.suites, consumed)];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
recomputeStats(merged);
|
|
138
|
+
return { merged, replaced, added, kept };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Ghost detection for the WARN (never a prune): base-kept titles that no
|
|
143
|
+
* longer correspond to a CURRENT spec title. A `@cases` report title carries
|
|
144
|
+
* a ` — <label>` suffix while the spec title is the base form, so a ghost is
|
|
145
|
+
* a title that neither equals a spec title nor extends one with that suffix.
|
|
146
|
+
*/
|
|
147
|
+
export function findGhostTitles(keptTitles: string[], specTitles: string[]): string[] {
|
|
148
|
+
return keptTitles.filter((title) =>
|
|
149
|
+
!specTitles.some((spec) => title === spec || title.startsWith(`${spec} — `)));
|
|
150
|
+
}
|
|
@@ -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
|
|