@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
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mutually-exclusive branches declared in the spec, and whether each one has a scenario (#630).
|
|
3
|
+
*
|
|
4
|
+
* A field report hit the same gap three times in three different flows, in three different
|
|
5
|
+
* shapes: an `### Exception Flow A` heading left entirely blank; a decision table
|
|
6
|
+
* (`DEC-003_EmptyStateCascade`) whose three arms produced ONE covered arm and two
|
|
7
|
+
* scenarios verifying that same arm under different names; and a pseudocode block with two
|
|
8
|
+
* attention-hint branches where only the first got a case.
|
|
9
|
+
*
|
|
10
|
+
* The narrative rule already existed — "AND condition → test each branch failing independently",
|
|
11
|
+
* "business rule → 1 behavioural TC per rule". It was missed anyway, three times. So the
|
|
12
|
+
* reviewers' own conclusion is the design here: what works is an ENFORCED enumeration, the way
|
|
13
|
+
* `Viewpoint items` are enumerated and completeness-checked, not another sentence of guidance.
|
|
14
|
+
*
|
|
15
|
+
* Deliberately conservative. A branch is only claimed when the spec makes the enumeration
|
|
16
|
+
* explicit — a named heading, a table that declares conditions and outcomes, or an if/else
|
|
17
|
+
* cascade — because a false "you missed a branch" costs more trust than it buys coverage.
|
|
18
|
+
*/
|
|
19
|
+
import { ScenarioInfo } from './parse';
|
|
20
|
+
|
|
21
|
+
export interface SpecBranch {
|
|
22
|
+
/** Stable id for the message: the block name plus the arm. */
|
|
23
|
+
id: string;
|
|
24
|
+
/** The distinctive token the suite is expected to cite. */
|
|
25
|
+
label: string;
|
|
26
|
+
kind: 'exception-flow' | 'alternative-flow' | 'decision-arm' | 'pseudocode-arm';
|
|
27
|
+
/** Where it was found, so the author can go and read it. */
|
|
28
|
+
source: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* CamelCase identifiers only — an outcome LABEL (`StateNoItems`, `HintPrimary`).
|
|
33
|
+
*
|
|
34
|
+
* SCREAMING_SNAKE was in this pattern and produced two false branches on a real spec:
|
|
35
|
+
* `DEFAULT_FILTER` from `conditions !== DEFAULT_FILTER` and `MESSAGE_READ` from a permission check.
|
|
36
|
+
* Those are OPERANDS inside a condition, not arms of it — a constant names a value, an outcome
|
|
37
|
+
* names a result. Shape alone cannot tell them apart, so the shape that only outcomes use is the
|
|
38
|
+
* one to match (#651 follow-up).
|
|
39
|
+
*/
|
|
40
|
+
const IDENTIFIER = /\b([A-Z][a-z0-9]+(?:[A-Z][a-z0-9]+)+)\b/g;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Code references are not branch labels. A table cell citing
|
|
44
|
+
* `src/components/molecules/HomeBoard/AlertPanel.tsx` yielded four "branches" —
|
|
45
|
+
* AlertPanel, TaskPanel, FeaturedList, HomeBoard — none of which is a branch at all.
|
|
46
|
+
* Backticked spans and anything path-shaped go before identifiers are read.
|
|
47
|
+
*/
|
|
48
|
+
function withoutCodeRefs(text: string): string {
|
|
49
|
+
return text
|
|
50
|
+
.replace(/`[^`]*`/g, ' ') // inline code spans
|
|
51
|
+
.replace(/\[[^\]]*\]\([^)]*\)/g, ' ') // markdown links
|
|
52
|
+
.replace(/\S*\/\S*/g, ' ') // any path-like token
|
|
53
|
+
.replace(/\b\w+\.(?:tsx?|jsx?|vue|py|rb|go|java|kt|swift|md|ya?ml|json)\b/gi, ' ');
|
|
54
|
+
}
|
|
55
|
+
/** A named decision/rule block: `DEC-003_Foo`, `BR-001_Bar`, `ALG-002`. */
|
|
56
|
+
const NAMED_BLOCK = /\b((?:DEC|BR|ALG|RULE|DISC)-\d+)(?:_([A-Za-z0-9]+))?\b/;
|
|
57
|
+
|
|
58
|
+
const HEADING = /^(#{1,6})\s+(.*)$/;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Split a markdown document into `{ heading, lines }` sections, so a table or fence can be
|
|
62
|
+
* attributed to the block that names it.
|
|
63
|
+
*/
|
|
64
|
+
function sections(text: string): Array<{ heading: string; lines: string[] }> {
|
|
65
|
+
const out: Array<{ heading: string; lines: string[] }> = [{ heading: '', lines: [] }];
|
|
66
|
+
for (const raw of text.split('\n')) {
|
|
67
|
+
const h = raw.match(HEADING);
|
|
68
|
+
if (h) out.push({ heading: h[2].replace(/[*`]/g, '').trim(), lines: [] });
|
|
69
|
+
else out[out.length - 1].lines.push(raw);
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Does this table declare CONDITIONS mapping to OUTCOMES (a decision table)? */
|
|
75
|
+
function isDecisionTable(header: string[], sectionHeading: string): boolean {
|
|
76
|
+
const cells = header.map((c) => c.toLowerCase());
|
|
77
|
+
// A Given/When/Then table is a list of TEST CASES, not a decision to enumerate — each row is
|
|
78
|
+
// already a case. It matched on `when`+`then` and turned a real spec's test-case table into
|
|
79
|
+
// five phantom branches (#651 follow-up).
|
|
80
|
+
const isGherkinTable = cells.some((c) => /\bgiven\b/.test(c)) && cells.some((c) => /\bwhen\b/.test(c))
|
|
81
|
+
&& cells.some((c) => /\bthen\b/.test(c));
|
|
82
|
+
const isTestCaseTable = cells.some((c) => /test[- ]?id|tc[- ]?id|case[- ]?id/.test(c));
|
|
83
|
+
if (isGherkinTable || isTestCaseTable) return false;
|
|
84
|
+
const hasCondition = cells.some((c) => /condition|case|when|input|state|flag|criteria|nhánh|điều kiện/.test(c));
|
|
85
|
+
const hasOutcome = cells.some((c) => /outcome|result|then|display|shows?|expected|action|behaviou?r|kết quả/.test(c));
|
|
86
|
+
// A block the spec itself NAMES as a decision counts even if its columns are worded oddly.
|
|
87
|
+
return (hasCondition && hasOutcome) || /\b(?:DEC|decision|cascade|matrix)\b/i.test(sectionHeading);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function specBranches(specText: string): SpecBranch[] {
|
|
91
|
+
const out: SpecBranch[] = [];
|
|
92
|
+
const seen = new Set<string>();
|
|
93
|
+
const push = (b: SpecBranch): void => {
|
|
94
|
+
const k = `${b.id}|${b.label}`;
|
|
95
|
+
if (seen.has(k)) return;
|
|
96
|
+
seen.add(k);
|
|
97
|
+
out.push(b);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
for (const sec of sections(specText)) {
|
|
101
|
+
// 1. An `### Exception Flow A` / `### Alternative Flow 2` heading IS one branch. Unambiguous.
|
|
102
|
+
const flowH = sec.heading.match(/^(exception|alternative|alternate)\s+flow\s*([A-Z0-9][\w-]*)?/i);
|
|
103
|
+
if (flowH) {
|
|
104
|
+
const label = sec.heading;
|
|
105
|
+
push({
|
|
106
|
+
id: label, label,
|
|
107
|
+
kind: /^exception/i.test(flowH[1]) ? 'exception-flow' : 'alternative-flow',
|
|
108
|
+
source: `heading "${sec.heading}"`,
|
|
109
|
+
});
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const named = sec.heading.match(NAMED_BLOCK);
|
|
114
|
+
const blockName = named ? named[0] : sec.heading;
|
|
115
|
+
|
|
116
|
+
// 2. A decision table: each data row is one arm. Matched on the identifier the row names.
|
|
117
|
+
const rows = sec.lines.filter((l) => l.trim().startsWith('|') && !/^\|[\s|:-]+\|?$/.test(l.trim()));
|
|
118
|
+
if (rows.length >= 3) { // header + at least two arms
|
|
119
|
+
const header = rows[0].split('|').map((c) => c.trim()).filter(Boolean);
|
|
120
|
+
if (isDecisionTable(header, sec.heading)) {
|
|
121
|
+
for (const row of rows.slice(1)) {
|
|
122
|
+
const ids = [...withoutCodeRefs(row).matchAll(IDENTIFIER)].map((m) => m[1]);
|
|
123
|
+
// Only an identifier is distinctive enough to look for in a suite; a prose condition
|
|
124
|
+
// would match half the feature file.
|
|
125
|
+
for (const id of ids) push({ id: `${blockName}:${id}`, label: id, kind: 'decision-arm', source: `decision table under "${sec.heading}"` });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// 3. An if/elif/else cascade in a fenced block: each arm that NAMES an outcome identifier.
|
|
131
|
+
let inFence = false;
|
|
132
|
+
let fence: string[] = [];
|
|
133
|
+
for (const l of sec.lines) {
|
|
134
|
+
if (l.trim().startsWith('```')) {
|
|
135
|
+
if (inFence) {
|
|
136
|
+
const body = fence.join('\n');
|
|
137
|
+
const arms = (body.match(/\b(?:else\s+if|elif|if|else)\b/g) ?? []).length;
|
|
138
|
+
if (arms >= 2) {
|
|
139
|
+
for (const m of withoutCodeRefs(body).matchAll(IDENTIFIER)) {
|
|
140
|
+
push({ id: `${blockName}:${m[1]}`, label: m[1], kind: 'pseudocode-arm', source: `pseudocode under "${sec.heading}"` });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
fence = [];
|
|
144
|
+
}
|
|
145
|
+
inFence = !inFence;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (inFence) fence.push(l);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return out;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Branches with no scenario citing them. A citation counts anywhere in the feature text —
|
|
156
|
+
* title, steps or a comment — because an author who deliberately excludes a branch writes the
|
|
157
|
+
* reason as a comment, and that is an accounting, not a gap.
|
|
158
|
+
*/
|
|
159
|
+
const GENERIC_WORD = new Set([
|
|
160
|
+
'flow', 'exception', 'alternative', 'alternate', 'the', 'and', 'with', 'without', 'when', 'then',
|
|
161
|
+
'user', 'visitor', 'screen', 'page', 'from', 'that', 'this', 'case', 'gate', 'step',
|
|
162
|
+
]);
|
|
163
|
+
|
|
164
|
+
export function unenumeratedBranches(branches: SpecBranch[], featureText: string): SpecBranch[] {
|
|
165
|
+
const hay = featureText.toLowerCase();
|
|
166
|
+
return branches.filter((b) => {
|
|
167
|
+
// An IDENTIFIER is precise — require it verbatim. A HEADING is prose the author will
|
|
168
|
+
// rephrase ("Exception Flow A — drawer confirm gate" becomes "closing the drawer without
|
|
169
|
+
// confirming"), so match on its distinctive words instead: demanding the heading verbatim
|
|
170
|
+
// would report a branch that is covered under a better name.
|
|
171
|
+
if (b.kind === 'decision-arm' || b.kind === 'pseudocode-arm') return !hay.includes(b.label.toLowerCase());
|
|
172
|
+
const words = [...new Set((b.label.toLowerCase().match(/[a-z][a-z-]{3,}/g) ?? []))]
|
|
173
|
+
.filter((w) => !GENERIC_WORD.has(w));
|
|
174
|
+
if (words.length === 0) return !hay.includes(b.label.toLowerCase());
|
|
175
|
+
const hits = words.filter((w) => hay.includes(w)).length;
|
|
176
|
+
return hits < Math.min(2, words.length);
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Scenarios whose assertions are IDENTICAL — "N cases" in a coverage sheet that verify one arm N
|
|
182
|
+
* times. The field report's decision table produced exactly this: two cases under different
|
|
183
|
+
* names, both asserting the same message, reported as two covered branches.
|
|
184
|
+
*
|
|
185
|
+
* Compares only the assertion side: two scenarios may legitimately share setup and differ in
|
|
186
|
+
* what they prove, but if what they PROVE is identical they are one test.
|
|
187
|
+
*/
|
|
188
|
+
export function sameOracleClusters(scenarios: ScenarioInfo[]): Array<{ oracle: string; scenarios: string[] }> {
|
|
189
|
+
const byOracle = new Map<string, string[]>();
|
|
190
|
+
for (const s of scenarios) {
|
|
191
|
+
if (s.manual) continue; // a procedure's oracle is prose
|
|
192
|
+
const asserts = [...(s.steps ?? [])]
|
|
193
|
+
.filter((st) => st.bucket === 'then')
|
|
194
|
+
.map((st) => st.text.toLowerCase().replace(/\s+/g, ' ').trim())
|
|
195
|
+
.sort();
|
|
196
|
+
if (asserts.length === 0) continue;
|
|
197
|
+
const key = asserts.join(' ; ');
|
|
198
|
+
byOracle.set(key, [...(byOracle.get(key) ?? []), s.name]);
|
|
199
|
+
}
|
|
200
|
+
return [...byOracle.entries()]
|
|
201
|
+
.filter(([, names]) => names.length > 1)
|
|
202
|
+
.map(([oracle, names]) => ({ oracle: oracle.slice(0, 100), scenarios: names }))
|
|
203
|
+
.sort((a, b) => b.scenarios.length - a.scenarios.length);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* A "denied → hidden" scenario with no "allowed → shown" counterpart (#630).
|
|
208
|
+
*
|
|
209
|
+
* Two security cases proved that chat content is hidden when `MESSAGE_READ` is denied. If the code
|
|
210
|
+
* hid it unconditionally — ignoring the permission entirely — both still pass. The deny side alone
|
|
211
|
+
* cannot distinguish "the guard works" from "the feature is broken for everyone", so the pair is
|
|
212
|
+
* the test, exactly as `min-1 / min / max / max+1` is the test for a boundary.
|
|
213
|
+
*
|
|
214
|
+
* The mirror of the negative-claim rule the skill already has, on the other side.
|
|
215
|
+
*/
|
|
216
|
+
const DENY_SHAPE = /\b(?:denied|deny|denies|forbidden|not authoriz\w*|unauthoriz\w*|no permission|without (?:the )?(?:permission|right|access|flag)|permission (?:is )?off|flag (?:is )?off|revoked|restricted)\b/i;
|
|
217
|
+
const HIDDEN_SHAPE = /\b(?:is hidden|are hidden|not (?:visible|shown|displayed)|blocked|masked|redacted|does not (?:show|render|appear))\b/i;
|
|
218
|
+
const ALLOW_SHAPE = /\b(?:granted|allowed|permitted|authoriz\w*|has (?:the )?(?:permission|right|access)|permission (?:is )?on|flag (?:is )?on|with (?:the )?(?:permission|right|access))\b/i;
|
|
219
|
+
|
|
220
|
+
export function permissionPairGaps(
|
|
221
|
+
scenarios: ScenarioInfo[],
|
|
222
|
+
): Array<{ scenario: string; subject: string[] }> {
|
|
223
|
+
const denies = scenarios.filter((s) => DENY_SHAPE.test(s.haystack) && HIDDEN_SHAPE.test(s.haystack));
|
|
224
|
+
if (denies.length === 0) return [];
|
|
225
|
+
const allows = scenarios.filter((s) => ALLOW_SHAPE.test(s.haystack) && !HIDDEN_SHAPE.test(s.stepsText));
|
|
226
|
+
// Words only — the scenario ID is stripped first, because `[a-z][a-z-]{4,}` happily matches
|
|
227
|
+
// `fl-er-` and an id fragment can never appear in the counterpart's title, so demanding it made
|
|
228
|
+
// the pair impossible to find.
|
|
229
|
+
const distinctive = (s: ScenarioInfo): string[] =>
|
|
230
|
+
[...new Set((s.name.replace(/^\S+\s*/, '').toLowerCase().match(/[a-z]{5,}/g) ?? []))]
|
|
231
|
+
.filter((w) => !GENERIC_WORD.has(w) && !/denied|denies|hidden|permission|scenario/.test(w));
|
|
232
|
+
const out: Array<{ scenario: string; subject: string[] }> = [];
|
|
233
|
+
for (const d of denies) {
|
|
234
|
+
const subject = distinctive(d);
|
|
235
|
+
// The counterpart has to be about the SAME thing — two shared distinctive words, the same
|
|
236
|
+
// bar the ledger's prose matching uses.
|
|
237
|
+
const paired = allows.some((a) => subject.filter((w) => a.haystack.includes(w)).length >= Math.min(2, subject.length));
|
|
238
|
+
if (!paired) out.push({ scenario: d.name, subject: subject.slice(0, 4) });
|
|
239
|
+
}
|
|
240
|
+
return out;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Scenarios blocked on test data that is still a placeholder (#630).
|
|
245
|
+
*
|
|
246
|
+
* A deliverable reported "Pending" for a case whose design was finished and whose only blocker was
|
|
247
|
+
* an unseeded ENV-BOUND value — indistinguishable, on the report, from a case with an unresolved
|
|
248
|
+
* design problem. One is a data task, the other is test work; they should not read the same.
|
|
249
|
+
*/
|
|
250
|
+
const SEED_PLACEHOLDER = /^(?:TODO[_-]?SEED\w*|TODO|TBD|<[^>]+>|REPLACE[_-]?ME|FILL[_-]?ME|CHANGE[_-]?ME|XXX+)$/i;
|
|
251
|
+
|
|
252
|
+
export function awaitingSeedData(
|
|
253
|
+
testDataText: string, scenarios: ScenarioInfo[],
|
|
254
|
+
): Array<{ key: string; scenarios: string[] }> {
|
|
255
|
+
// One placeholder key is ONE data task, however many files or lines mention it — a real project
|
|
256
|
+
// reported the same key three times because its overlays each declare it (#651 follow-up).
|
|
257
|
+
const placeholders = [...new Set(
|
|
258
|
+
[...testDataText.matchAll(/^\s*([A-Za-z0-9_.-]+)\s*:\s*["']?([^"'\n#]+?)["']?\s*(?:#.*)?$/gm)]
|
|
259
|
+
.filter((m) => SEED_PLACEHOLDER.test(m[2].trim()))
|
|
260
|
+
.map((m) => m[1]),
|
|
261
|
+
)];
|
|
262
|
+
if (placeholders.length === 0) return [];
|
|
263
|
+
return placeholders
|
|
264
|
+
.map((key) => ({
|
|
265
|
+
key,
|
|
266
|
+
scenarios: scenarios.filter((s) => s.stepsText.includes(`{{${key.toLowerCase()}`)
|
|
267
|
+
|| new RegExp(`\\{\\{[a-z0-9_.-]*\\.${key.toLowerCase()}\\}\\}`).test(s.stepsText)).map((s) => s.name),
|
|
268
|
+
}))
|
|
269
|
+
.filter((p) => p.scenarios.length > 0);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* The fixed risk catalog for a multi-screen journey, reconciled against the flow (#651 F1-7).
|
|
274
|
+
*
|
|
275
|
+
* `SPEC-BRANCH-UNCOVERED` enforces the branches a spec DECLARES. This is the other half, and the
|
|
276
|
+
* one a field report asked for explicitly: the risks a spec is often SILENT about, which a
|
|
277
|
+
* reviewer notices and a generator does not. Their words — *"cả 2 flow đều thiếu 2-3 nhóm trong
|
|
278
|
+
* danh sách này, không phải vì không áp dụng mà vì không có bước bắt buộc rà lại checklist đó
|
|
279
|
+
* trước khi coi flow 'đủ'"*.
|
|
280
|
+
*
|
|
281
|
+
* The same families as the step × risk matrix in `sungen-tc-generation`, so guidance and gate
|
|
282
|
+
* cannot drift apart. A family counts as CONSIDERED when it is mentioned anywhere the author
|
|
283
|
+
* reasons: a declared flow's branch point, outcome or reason; a scenario; or the viewpoint. That
|
|
284
|
+
* makes "we thought about it and it does not apply" a one-line answer — the accounting the flow
|
|
285
|
+
* inventory already asks for — rather than a scenario nobody needs.
|
|
286
|
+
*/
|
|
287
|
+
export interface RiskFamily {
|
|
288
|
+
id: string;
|
|
289
|
+
label: string;
|
|
290
|
+
/** What the author writes when they HAVE considered it. */
|
|
291
|
+
re: RegExp;
|
|
292
|
+
/** Why it bites on a multi-screen journey specifically. */
|
|
293
|
+
why: string;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export const MULTI_SCREEN_RISKS: RiskFamily[] = [
|
|
297
|
+
{ id: 'double-submit', label: 'double submit / double activation',
|
|
298
|
+
re: /\b(?:double[- ]?(?:submit|click|tap|activation)|twice|two taps?|idempoten\w*|re-?submit|duplicate request)\b/i,
|
|
299
|
+
why: 'a second activation before the first response is the classic way one journey creates two records' },
|
|
300
|
+
{ id: 'concurrency', label: 'two tabs / two devices at once',
|
|
301
|
+
re: /\b(?:two tabs?|second tab|another tab|multi[- ]?tab|two devices|second device|concurrent\w*|simultaneous\w*|race)\b/i,
|
|
302
|
+
why: 'a journey that buffers state client-side behaves differently when two of it run at once' },
|
|
303
|
+
{ id: 'client-buffer', label: 'client-side buffer kept / lost',
|
|
304
|
+
re: /\b(?:session ?storage|local ?storage|buffer\w*|draft|unsaved|re-?hydrat\w*|restore[sd]?)\b/i,
|
|
305
|
+
why: 'state held between two steps must be proved BOTH ways — it survives a legitimate return, and it is lost when the spec says it is' },
|
|
306
|
+
{ id: 'server-error', label: 'a mid-journey server error',
|
|
307
|
+
re: /\b(?:server error|5\d{2}\b|network (?:error|drop|failure)|offline|timeout|unavailable|fails? server-?side|retry)\b/i,
|
|
308
|
+
why: 'the interesting question is what survives the failure, not that an error appeared' },
|
|
309
|
+
{ id: 'abandonment', label: 'abandonment / TTL of partial state',
|
|
310
|
+
re: /\b(?:abandon\w*|lapse[sd]?|expir\w*|ttl\b|stale|cleanup|24 ?h|dormant|never (?:completed|finished))\b/i,
|
|
311
|
+
why: 'a journey stopped halfway leaves state somebody owns; a flow test is where that becomes visible' },
|
|
312
|
+
{ id: 'direct-access', label: 'direct URL access to a later step',
|
|
313
|
+
re: /\b(?:direct(?:ly)? access|deep[- ]?link|without (?:a |an |the )?(?:context|session|prior)|guard|not skippable|redirect\w*)\b/i,
|
|
314
|
+
why: 'every step after the first is reachable by URL, and only a journey test can check the guard' },
|
|
315
|
+
{ id: 'auth-transition', label: 'where the session begins',
|
|
316
|
+
re: /\b(?:authenticat\w*|unauthenticat\w*|signed[- ]in|logged[- ]in|session (?:is )?(?:established|created|begins)|auto[- ]?login)\b/i,
|
|
317
|
+
why: 'the step where an actor stops being anonymous is a boundary no single screen owns' },
|
|
318
|
+
{ id: 'escape-hatch', label: 'a documented way out of the journey',
|
|
319
|
+
re: /\b(?:cancel|back to top|exit|leave[sd]? the (?:journey|flow)|opt[- ]?out|skip|dismiss)\b/i,
|
|
320
|
+
why: 'a designed-for exit is an alternate flow with its own outcome, not an error' },
|
|
321
|
+
{ id: 'side-effect-order', label: 'ordering of a delayed side effect',
|
|
322
|
+
re: /\b(?:mail|email|notification|webhook|queue|async|delayed|eventual\w*|dispatch\w*|side[- ]effect)\b/i,
|
|
323
|
+
why: 'a journey that triggers an out-of-band effect is where its ordering and its absence both matter' },
|
|
324
|
+
];
|
|
325
|
+
|
|
326
|
+
export function unconsideredRisks(
|
|
327
|
+
contract: FlowContractLike, scenarios: ScenarioInfo[], viewpointText: string,
|
|
328
|
+
): RiskFamily[] {
|
|
329
|
+
// Everywhere the author reasons about the journey. A mention in ANY of them is consideration —
|
|
330
|
+
// the check asks "did you think about this?", not "did you write a scenario for it?".
|
|
331
|
+
const corpus = [
|
|
332
|
+
viewpointText,
|
|
333
|
+
...scenarios.map((s) => s.haystack),
|
|
334
|
+
...(contract.flows ?? []).flatMap((f) => [f.id, f.branchFrom ?? '', f.outcome ?? '', f.reason ?? '']),
|
|
335
|
+
contract.minimalGuarantee ?? '', contract.successGuarantee ?? '', contract.precondition ?? '',
|
|
336
|
+
].join('\n').toLowerCase();
|
|
337
|
+
return MULTI_SCREEN_RISKS.filter((r) => !r.re.test(corpus));
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/** The slice of the contract this needs — kept structural so the module stays import-light. */
|
|
341
|
+
export interface FlowContractLike {
|
|
342
|
+
flows?: Array<{ id: string; branchFrom?: string; outcome?: string; reason?: string }>;
|
|
343
|
+
minimalGuarantee?: string;
|
|
344
|
+
successGuarantee?: string;
|
|
345
|
+
precondition?: string;
|
|
346
|
+
}
|
|
@@ -206,7 +206,7 @@ export function specCoverage(specPath: string, scenarios: ScenarioInfo[], featur
|
|
|
206
206
|
/**
|
|
207
207
|
* A flow's requirement list that is a HAND RESTATEMENT of the screen specs it traverses.
|
|
208
208
|
*
|
|
209
|
-
* A flow spec typically cites its requirements as belonging elsewhere — "
|
|
209
|
+
* A flow spec typically cites its requirements as belonging elsewhere — "SCREEN_A_002 FR-001",
|
|
210
210
|
* "restated here in flow terms". That restatement is lossy by construction, and nothing checked
|
|
211
211
|
* it: on a real run the flow spec restated two of the screens' FRs, `specFR` read **2/2 = 100%**,
|
|
212
212
|
* and the guard clause the flow most needed (a double-submit rule in one of those screen specs)
|
|
@@ -221,12 +221,12 @@ export function specCoverage(specPath: string, scenarios: ScenarioInfo[], featur
|
|
|
221
221
|
export function restatedRequirementSources(specText: string, availableUnits: string[]): {
|
|
222
222
|
restated: boolean; sources: string[]; missing: string[];
|
|
223
223
|
} {
|
|
224
|
-
// "these ... originate in the SCREEN specs", "restated here", "per
|
|
224
|
+
// "these ... originate in the SCREEN specs", "restated here", "per SCREEN_A_002 FR-001".
|
|
225
225
|
const restated = /\brestate[sd]?\b|\boriginate[sd]? in\b|\bderived from the (?:screen|per-screen) spec/i.test(specText);
|
|
226
|
-
// External document ids carrying their own requirement number: `
|
|
226
|
+
// External document ids carrying their own requirement number: `SCREEN_A_002 FR-001`,
|
|
227
227
|
// `SCR-1-SYS-0001.FR-3`. Two+ segments and an uppercase head, so a bare `FR-001` (the flow's
|
|
228
228
|
// own) never matches.
|
|
229
|
-
// A citation may name SEVERAL documents at once — "
|
|
229
|
+
// A citation may name SEVERAL documents at once — "SCREEN_A_002/SCREEN_A_004 FR-001" — so match
|
|
230
230
|
// the whole slash/comma-joined run and split it. Capturing only the token adjacent to the
|
|
231
231
|
// requirement number silently dropped every sibling.
|
|
232
232
|
const DOC = '[A-Z][A-Z0-9]*(?:[_-][A-Z0-9]+)+';
|
|
@@ -35,11 +35,18 @@ export interface ViewpointBaseline {
|
|
|
35
35
|
added?: string[];
|
|
36
36
|
removed?: string[];
|
|
37
37
|
recordedAt?: string;
|
|
38
|
+
/**
|
|
39
|
+
* The recorded ids came from an EARLIER parser and the file itself is untouched, so the two
|
|
40
|
+
* id sets cannot be compared. Not a change — and not a confirmation either.
|
|
41
|
+
*/
|
|
42
|
+
reparsed?: boolean;
|
|
43
|
+
/** sha1 of the file's own bytes — the only fingerprint a parser upgrade cannot move. */
|
|
44
|
+
fileHash?: string;
|
|
38
45
|
}
|
|
39
46
|
|
|
40
47
|
interface BaselineFile {
|
|
41
48
|
version: 1;
|
|
42
|
-
units: Record<string, { hash: string; ids: string[]; recordedAt: string }>;
|
|
49
|
+
units: Record<string, { hash: string; ids: string[]; recordedAt: string; fileHash?: string }>;
|
|
43
50
|
}
|
|
44
51
|
|
|
45
52
|
function baselinePath(projectRoot: string): string {
|
|
@@ -71,6 +78,21 @@ function fingerprint(ids: string[]): string {
|
|
|
71
78
|
return crypto.createHash('sha1').update(ids.join('\n')).digest('hex').slice(0, 12);
|
|
72
79
|
}
|
|
73
80
|
|
|
81
|
+
/**
|
|
82
|
+
* Fingerprint the FILE, not the parse.
|
|
83
|
+
*
|
|
84
|
+
* The baseline compared the ids the CURRENT parser extracts against the ids the parser
|
|
85
|
+
* recorded — so improving the parser looked exactly like the author rewriting the declaration.
|
|
86
|
+
* A real project upgrading across versions was told its viewpoint "no longer declares what it
|
|
87
|
+
* did", had two axes withheld and its score capped, over a file nobody had touched. The bytes
|
|
88
|
+
* are the one thing only an edit can move (#657).
|
|
89
|
+
*/
|
|
90
|
+
function fileFingerprint(viewpointPath: string): string {
|
|
91
|
+
try {
|
|
92
|
+
return crypto.createHash('sha1').update(fs.readFileSync(viewpointPath)).digest('hex').slice(0, 12);
|
|
93
|
+
} catch { return ''; }
|
|
94
|
+
}
|
|
95
|
+
|
|
74
96
|
/**
|
|
75
97
|
* Compare the unit's current test-viewpoint declaration to the recorded baseline.
|
|
76
98
|
* Read-only — recording is the caller's decision, so an audit stays a measurement
|
|
@@ -85,12 +107,23 @@ export function checkViewpointBaseline(
|
|
|
85
107
|
if (!fs.existsSync(viewpointPath)) return { status: 'absent', hash: '', ids: [] };
|
|
86
108
|
const ids = parseViewpointOverview(viewpointPath).map((v) => v.id).sort();
|
|
87
109
|
const hash = fingerprint(ids);
|
|
110
|
+
const fileHash = fileFingerprint(viewpointPath);
|
|
88
111
|
if (ids.length === 0) return { status: 'absent', hash: '', ids: [] };
|
|
89
112
|
|
|
90
113
|
const file = readBaselineFile(projectRoot);
|
|
91
114
|
const recorded = file.units[unitId];
|
|
92
|
-
if (!recorded) return { status: 'new', hash, ids };
|
|
93
|
-
if (recorded.hash === hash) return { status: 'unchanged', hash, ids, recordedAt: recorded.recordedAt };
|
|
115
|
+
if (!recorded) return { status: 'new', hash, ids, fileHash };
|
|
116
|
+
if (recorded.hash === hash) return { status: 'unchanged', hash, ids, fileHash, recordedAt: recorded.recordedAt };
|
|
117
|
+
// Same bytes, different parse — the parser moved, the declaration did not.
|
|
118
|
+
if (recorded.fileHash && fileHash && recorded.fileHash === fileHash) {
|
|
119
|
+
return { status: 'unchanged', hash, ids, fileHash, reparsed: true, recordedAt: recorded.recordedAt };
|
|
120
|
+
}
|
|
121
|
+
// A baseline recorded before file fingerprinting cannot distinguish the two, and every
|
|
122
|
+
// project upgrading across that boundary would be accused once. Re-baseline instead of
|
|
123
|
+
// accusing, and say why: the alternative is a certain false alarm for everyone.
|
|
124
|
+
if (!recorded.fileHash) {
|
|
125
|
+
return { status: 'unchanged', hash, ids, fileHash, reparsed: true, recordedAt: recorded.recordedAt };
|
|
126
|
+
}
|
|
94
127
|
|
|
95
128
|
const before = new Set(recorded.ids ?? []);
|
|
96
129
|
const now = new Set(ids);
|
|
@@ -98,6 +131,7 @@ export function checkViewpointBaseline(
|
|
|
98
131
|
status: 'changed',
|
|
99
132
|
hash,
|
|
100
133
|
ids,
|
|
134
|
+
fileHash,
|
|
101
135
|
added: ids.filter((i) => !before.has(i)),
|
|
102
136
|
removed: (recorded.ids ?? []).filter((i) => !now.has(i)),
|
|
103
137
|
recordedAt: recorded.recordedAt,
|
|
@@ -108,21 +142,22 @@ export function checkViewpointBaseline(
|
|
|
108
142
|
export function acceptViewpointBaseline(
|
|
109
143
|
projectRoot: string,
|
|
110
144
|
unitId: string,
|
|
111
|
-
current: { hash: string; ids: string[] },
|
|
145
|
+
current: { hash: string; ids: string[]; fileHash?: string },
|
|
112
146
|
): void {
|
|
113
147
|
const file = readBaselineFile(projectRoot);
|
|
114
148
|
file.units[unitId] = {
|
|
115
149
|
hash: current.hash,
|
|
116
150
|
ids: current.ids,
|
|
117
151
|
recordedAt: new Date().toISOString(),
|
|
152
|
+
...(current.fileHash ? { fileHash: current.fileHash } : {}),
|
|
118
153
|
};
|
|
119
154
|
writeBaselineFile(projectRoot, file);
|
|
120
155
|
}
|
|
121
156
|
|
|
122
157
|
/** Read the declaration without recording anything (used by `--accept-viewpoint`). */
|
|
123
|
-
export function readViewpointDeclaration(viewpointPath: string): { hash: string; ids: string[] } | null {
|
|
158
|
+
export function readViewpointDeclaration(viewpointPath: string): { hash: string; ids: string[]; fileHash: string } | null {
|
|
124
159
|
if (!fs.existsSync(viewpointPath)) return null;
|
|
125
160
|
const ids = parseViewpointOverview(viewpointPath).map((v) => v.id).sort();
|
|
126
161
|
if (ids.length === 0) return null;
|
|
127
|
-
return { hash: fingerprint(ids), ids };
|
|
162
|
+
return { hash: fingerprint(ids), ids, fileHash: fileFingerprint(viewpointPath) };
|
|
128
163
|
}
|
|
@@ -69,6 +69,19 @@ const PLACEHOLDER_ITEM = /^(none\b|n\/a\b|tbd\b|no known\b|nothing\b|do not inve
|
|
|
69
69
|
/** A priority-DECLARATION row: `| VP-LOGIC | High | <reason prose> |`. The category id and its
|
|
70
70
|
* priority are consumed by the traceability + balance axes; the reason is rationale. */
|
|
71
71
|
const PRIORITY_ROW = /^(?:VP|FL)[A-Z0-9._-]*\s+—\s+(?:critical|high|medium|normal|low|deferred)\b/i;
|
|
72
|
+
/**
|
|
73
|
+
* Whole SECTIONS that declare something ABOUT the checklist rather than listing claims to prove.
|
|
74
|
+
*
|
|
75
|
+
* The row-shape filter above only caught a Priority row that happened to look like one, so a
|
|
76
|
+
* project that fenced its prose still had its `Known Issues` TABLE rows counted as uncovered
|
|
77
|
+
* claims (PC-06/07/08/10 in a field report) — and one id listed in both `Known Issues` and
|
|
78
|
+
* `Deferred` was counted as two separate gaps (#630). Excluding by section covers rows and prose
|
|
79
|
+
* uniformly, which is what the row filter could never do.
|
|
80
|
+
*
|
|
81
|
+
* `Design Decisions` deliberately stays IN: "values are buffered between step 3 and 4, a reload
|
|
82
|
+
* loses them" is a real claim, and it is what the continuity check reads.
|
|
83
|
+
*/
|
|
84
|
+
const DECLARATION_SECTION = /^(?:known issues?|open questions?|deferred|out of scope|viewpoint grouping|traceability|metadata|scope|journey phases?|revision history|references?|sources?|changelog)\b/i;
|
|
72
85
|
const GENERIC = new Set(['display', 'shown', 'value', 'field', 'input', 'page', 'screen', 'button', 'link', 'text', 'check', 'verify', 'should', 'with', 'when', 'then', 'user', 'this', 'that', 'each', 'item', 'items']);
|
|
73
86
|
|
|
74
87
|
/** Extract atomic checklist items from a viewpoint file (format-tolerant). */
|
|
@@ -77,26 +90,60 @@ export function parseViewpointItems(viewpointPath: string): { id?: string; text:
|
|
|
77
90
|
const lines = readTextFile(viewpointPath).split('\n');
|
|
78
91
|
const items: { id?: string; text: string }[] = [];
|
|
79
92
|
let inFence = false;
|
|
93
|
+
let inDeclarationSection = false;
|
|
94
|
+
// The level an excluded section was opened at. A SUBSECTION inherits it: `### Optional`
|
|
95
|
+
// nested under `## 8. Traceability Index` used to re-open the section and hand its rows
|
|
96
|
+
// back as claims, because exclusion was recomputed from scratch at every heading.
|
|
97
|
+
let excludedAt: number | null = null;
|
|
80
98
|
for (const raw of lines) {
|
|
81
99
|
const line = raw.trim();
|
|
82
100
|
if (line.startsWith('```')) { inFence = !inFence; continue; }
|
|
83
101
|
if (inFence || !line) continue;
|
|
84
|
-
|
|
102
|
+
const heading = line.match(/^(#{1,6})\s+(.*)$/);
|
|
103
|
+
if (heading) {
|
|
104
|
+
const level = heading[1].length;
|
|
105
|
+
if (excludedAt !== null && level > excludedAt) continue; // still inside the excluded section
|
|
106
|
+
// A heading both ends the previous section and decides whether this one declares or claims.
|
|
107
|
+
// Strip an ordinal prefix ("## 5. Known Issues", "## 8. Viewpoint Grouping") before
|
|
108
|
+
// testing. The regex anchors at the start, so a NUMBERED heading matched nothing and
|
|
109
|
+
// every excluded section leaked its rows back in as claims — a real viewpoint numbers
|
|
110
|
+
// its sections, so in practice the exclusion list was doing nothing at all (#657).
|
|
111
|
+
const headText = heading[2].replace(/[*`]/g, '').replace(/^\s*(?:\d+|[A-Z])\s*[.):]\s*/, '').trim();
|
|
112
|
+
inDeclarationSection = DECLARATION_SECTION.test(headText);
|
|
113
|
+
excludedAt = inDeclarationSection ? level : null;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (inDeclarationSection) continue;
|
|
85
117
|
let text = '';
|
|
118
|
+
let lead = '';
|
|
86
119
|
const bullet = line.match(/^(?:[-*+]|\d+[.)])\s+(.*)$/);
|
|
87
|
-
if (bullet) text = bullet[1];
|
|
120
|
+
if (bullet) { text = bullet[1]; lead = bullet[1]; }
|
|
88
121
|
else if (line.startsWith('|')) { // table data row
|
|
89
122
|
if (/^\|[\s|:-]+\|?$/.test(line)) continue; // separator
|
|
90
123
|
const cells = line.split('|').map((c) => c.trim()).filter(Boolean);
|
|
91
124
|
if (/^(vp|id|viewpoint|priority|reason|no\.?|category|item|trigger|#|pattern|applicable|notes|field|constraint|code|description|status|step|flow|ref|level|question|screen|actor|branches from|own steps|component|thành phần|trường|bước)$/i.test(cells[0] || '')) continue; // header
|
|
92
125
|
text = cells.join(' — ');
|
|
126
|
+
lead = cells[0] ?? '';
|
|
93
127
|
} else continue;
|
|
94
128
|
text = text.replace(/[*`]/g, '').trim();
|
|
95
129
|
if (!text) continue;
|
|
96
130
|
if (PLACEHOLDER_ITEM.test(text)) continue; // "None on file yet" is not a claim
|
|
97
131
|
if (PRIORITY_ROW.test(text)) continue; // priority declaration, not a checklist item
|
|
98
|
-
|
|
132
|
+
// WHICH id a row declares is decided by its LEAD — the first table cell, or the start of
|
|
133
|
+
// the bullet. Ids further along are references ("KHÔNG kiểm nội dung — đã test ở VP-X-001"),
|
|
134
|
+
// and a well-formed claim cites its neighbours all the time. Reading the whole row instead
|
|
135
|
+
// made a citation look like a second declaration of somebody else's id.
|
|
136
|
+
const leadClean = lead.replace(/[*`]/g, '').trim();
|
|
137
|
+
// Several ids IN THE LEAD is an index line, not a claim — `- VP-A-001, VP-A-002, VP-A-003`
|
|
138
|
+
// under a grouping section. It declares nothing; it points at things declared elsewhere.
|
|
139
|
+
const leadIds = new Set((leadClean.match(new RegExp(ID_RE.source, 'g')) || []).filter((x) => /\d/.test(x)));
|
|
140
|
+
if (leadIds.size > 1) continue;
|
|
141
|
+
const idM = leadClean.match(ID_RE) ?? text.match(ID_RE);
|
|
99
142
|
const id = idM && /\d/.test(idM[1]) ? idM[1] : undefined; // require a digit so prose words aren't IDs
|
|
143
|
+
// A row keyed by a bare GROUP prefix ("VP-LOGIC — transitions — High — why the flow exists")
|
|
144
|
+
// declares a family and its priority, not a checklist item: nothing can "cover" it, and
|
|
145
|
+
// demanding coverage sends the author looking for a test that was never meant to exist.
|
|
146
|
+
if (/^VP-[A-Z]+\s*(?:—|-|\||$)/.test(leadClean) && !/\d/.test(leadClean)) continue;
|
|
100
147
|
const words = (text.toLowerCase().match(/[a-z][a-z-]{3,}/g) || []).filter((w) => !GENERIC.has(w));
|
|
101
148
|
if (!id && words.length < 2) continue; // not substantive enough to track
|
|
102
149
|
items.push({ id, text: text.slice(0, 100) });
|
|
@@ -105,7 +152,12 @@ export function parseViewpointItems(viewpointPath: string): { id?: string; text:
|
|
|
105
152
|
}
|
|
106
153
|
|
|
107
154
|
export function viewpointLedger(viewpointPath: string, scenarios: ScenarioInfo[], featureText: string): LedgerResult {
|
|
108
|
-
|
|
155
|
+
// A literally repeated line is ONE claim. Deduped here rather than in the parser, because
|
|
156
|
+
// `viewpointIntegrity` needs the raw list to report an id that labels SEVERAL DIFFERENT claims
|
|
157
|
+
// (#622) — silently collapsing those would remove its input.
|
|
158
|
+
const seen = new Set<string>();
|
|
159
|
+
const items = parseViewpointItems(viewpointPath)
|
|
160
|
+
.filter((i) => { const k = `${i.id ?? ''}|${i.text}`; if (seen.has(k)) return false; seen.add(k); return true; });
|
|
109
161
|
if (!fs.existsSync(viewpointPath) || items.length === 0) {
|
|
110
162
|
return { hasViewpoint: fs.existsSync(viewpointPath), total: 0, covered: 0, ratio: 1, missing: [], manualOnly: [], partial: [] };
|
|
111
163
|
}
|
|
@@ -170,19 +222,39 @@ export function viewpointLedger(viewpointPath: string, scenarios: ScenarioInfo[]
|
|
|
170
222
|
* The language has no reload/back step at all (#593), so the honest outcome is a `@manual`
|
|
171
223
|
* deferral naming the gesture — not a silent swap.
|
|
172
224
|
*/
|
|
173
|
-
const BROWSER_GESTURES: Array<{ gesture: string; re: RegExp; appSubstitute: RegExp }> = [
|
|
225
|
+
const BROWSER_GESTURES: Array<{ gesture: string; re: RegExp; appSubstitute: RegExp; platform?: string }> = [
|
|
174
226
|
{ gesture: 'browser back', re: /\bbrowser(?:'s)?[ -]?(?:back|history)\b|\bhistory[ -]back\b/i,
|
|
175
|
-
appSubstitute: /\bclick \[[^\]]*back[^\]]*\]/i },
|
|
227
|
+
appSubstitute: /\bclick \[[^\]]*back[^\]]*\]/i, platform: 'web' },
|
|
176
228
|
{ gesture: 'browser reload/refresh', re: /\b(?:page |browser )?(?:reload|refresh)(?:ing|ed|es)?\b/i,
|
|
177
|
-
appSubstitute: /\bis on \[[^\]]+\] page\b/i },
|
|
229
|
+
appSubstitute: /\bis on \[[^\]]+\] page\b/i, platform: 'web' },
|
|
178
230
|
{ gesture: 'closing and reopening the tab', re: /\bclos(?:e|ing) (?:and reopen\w*\s*)?the tab\b|\breopen\w* the tab\b/i,
|
|
179
|
-
appSubstitute: /\bis on \[[^\]]+\] page\b/i },
|
|
231
|
+
appSubstitute: /\bis on \[[^\]]+\] page\b/i, platform: 'web' },
|
|
232
|
+
|
|
233
|
+
// The same class on mobile, where the vocabulary is different but the trap is identical: an
|
|
234
|
+
// in-app control is not the OS gesture. Android's hardware/system back does not run the
|
|
235
|
+
// screen's own handler unless the app wires it, and backgrounding can destroy an Activity that
|
|
236
|
+
// an in-app navigation never touches — so a defect that only appears on the OS path lives
|
|
237
|
+
// exactly in the substitution (#626). The check was web-only, so mobile flows were silent about
|
|
238
|
+
// the very risk it exists to catch.
|
|
239
|
+
{ gesture: 'the OS/hardware back gesture', platform: 'mobile',
|
|
240
|
+
re: /\b(?:hardware|system|os|device|android|physical)[ -]?back\b|\bback (?:gesture|button|swipe)\b|\bswipe[ -]back\b/i,
|
|
241
|
+
appSubstitute: /\b(?:tap|click) \[[^\]]*back[^\]]*\]/i },
|
|
242
|
+
{ gesture: 'backgrounding and resuming the app', platform: 'mobile',
|
|
243
|
+
re: /\b(?:background(?:ing|ed)?|foreground(?:ing|ed)?|app switcher|recent apps|minimi[sz]|resum(?:e|ing)|kill(?:ed|ing)? the app|force[ -]stop|process death)\b/i,
|
|
244
|
+
appSubstitute: /\b(?:tap|click) \[[^\]]+\]|is on \[[^\]]+\] screen\b/i },
|
|
245
|
+
{ gesture: 'rotating the device', platform: 'mobile',
|
|
246
|
+
re: /\b(?:rotat(?:e|ing|ion)|landscape|portrait|orientation change)\b/i,
|
|
247
|
+
appSubstitute: /\bis on \[[^\]]+\] screen\b/i },
|
|
180
248
|
];
|
|
181
249
|
|
|
182
250
|
export function browserGestureSubstitutions(
|
|
183
|
-
viewpointPath: string, scenarios: ScenarioInfo[],
|
|
251
|
+
viewpointPath: string, scenarios: ScenarioInfo[], platform?: string,
|
|
184
252
|
): Array<{ gesture: string; item: string }> {
|
|
185
253
|
const out: Array<{ gesture: string; item: string }> = [];
|
|
254
|
+
// A gesture only applies where its platform does: "reload" means nothing on a native app, and
|
|
255
|
+
// "backgrounding" means nothing in a browser tab. An unknown platform checks everything, which
|
|
256
|
+
// is how the web-only set behaved before.
|
|
257
|
+
const applies = (g: { platform?: string }): boolean => !platform || !g.platform || g.platform === platform;
|
|
186
258
|
// Judged PER SCENARIO, not over the whole file: a @manual scenario elsewhere that merely
|
|
187
259
|
// mentions the gesture in passing (explaining a tooling limit) silenced the check for an
|
|
188
260
|
// item that an automated scenario had quietly substituted.
|
|
@@ -190,6 +262,7 @@ export function browserGestureSubstitutions(
|
|
|
190
262
|
const automated = scenarios.filter((s) => !s.manual).map((s) => s.haystack);
|
|
191
263
|
for (const item of parseViewpointItems(viewpointPath)) {
|
|
192
264
|
for (const g of BROWSER_GESTURES) {
|
|
265
|
+
if (!applies(g)) continue;
|
|
193
266
|
if (!g.re.test(item.text)) continue;
|
|
194
267
|
// A @manual scenario whose own subject IS the gesture is the honest answer.
|
|
195
268
|
if (deferred.some((h) => g.re.test(h))) break;
|
package/src/index.ts
CHANGED
|
@@ -20,6 +20,7 @@ export type { ResolvedAppProfile } from './generators/test-generator/app-profile
|
|
|
20
20
|
export type { MappedStep } from './generators/test-generator/step-mapper';
|
|
21
21
|
export type { ParsedStep } from './generators/gherkin-parser';
|
|
22
22
|
export { getPathCode, inferPath, resolvePathVariables } from './generators/test-generator/utils/path-inference';
|
|
23
|
+
export { formatToRegexSource, formatToRegexLiteral, hasFormatToken } from './generators/test-generator/utils/format-pattern';
|
|
23
24
|
export { LIST_MEMBER_ROLES } from './generators/test-generator/utils/selector-resolver';
|
|
24
25
|
|
|
25
26
|
// --- Precondition-annotation override grammar (shared by the @query / @api driver codegen) ---
|