@sun-asterisk/sungen 3.2.0-beta.142 → 3.2.0-beta.144
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 +11 -2
- package/dist/capabilities/context-router.d.ts.map +1 -1
- package/dist/capabilities/context-router.js +10 -3
- package/dist/capabilities/context-router.js.map +1 -1
- package/dist/capabilities/discover.js +1 -1
- package/dist/capabilities/discover.js.map +1 -1
- package/dist/cli/commands/audit.d.ts.map +1 -1
- package/dist/cli/commands/audit.js +5 -3
- package/dist/cli/commands/audit.js.map +1 -1
- package/dist/generators/test-generator/adapters/appium/appium-adapter.d.ts +54 -0
- package/dist/generators/test-generator/adapters/appium/appium-adapter.d.ts.map +1 -0
- package/dist/generators/test-generator/adapters/appium/appium-adapter.js +52 -0
- package/dist/generators/test-generator/adapters/appium/appium-adapter.js.map +1 -0
- package/dist/generators/test-generator/adapters/appium/templates/after-all.hbs +8 -0
- package/dist/generators/test-generator/adapters/appium/templates/after-each.hbs +8 -0
- package/dist/generators/test-generator/adapters/appium/templates/before-all.hbs +8 -0
- package/dist/generators/test-generator/adapters/appium/templates/before-each.hbs +8 -0
- package/dist/generators/test-generator/adapters/appium/templates/imports.hbs +8 -0
- package/dist/generators/test-generator/adapters/appium/templates/scenario.hbs +8 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/alert-accept-action.hbs +4 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/alert-dismiss-action.hbs +2 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/alert-fill-action.hbs +2 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/check-action.hbs +10 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/clear-action.hbs +1 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/click-action.hbs +1 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/click-element-with-text.hbs +2 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/click-select-action.hbs +4 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/dismiss-action.hbs +3 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/double-click-action.hbs +6 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/drag-action.hbs +2 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/expand-action.hbs +2 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/fill-action.hbs +14 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/fill-editor-action.hbs +12 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/frame-enter-action.hbs +6 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/frame-exit-action.hbs +2 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/hide-keyboard-action.hbs +4 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/hover-action.hbs +2 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/keyboard-global-action.hbs +2 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/press-action.hbs +4 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/radio-select-action.hbs +4 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/scroll-action.hbs +19 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/select-action.hbs +5 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/table-action-in-row.hbs +2 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/toggle-action.hbs +3 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/uncheck-action.hbs +8 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/unknown-element-action.hbs +2 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/upload-action.hbs +3 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/wait-for-page.hbs +3 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/wait-for-role-with-data.hbs +2 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/wait-for-role.hbs +3 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/alert-text-assertion.hbs +2 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/attribute-assertion.hbs +1 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/checked-assertion.hbs +1 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/clipboard-text-assertion.hbs +2 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/column-cell-assertion.hbs +2 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/contain-text-assertion.hbs +14 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/count-assertion.hbs +1 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/disabled-assertion.hbs +1 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/empty-assertion.hbs +1 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/enabled-assertion.hbs +1 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/focused-assertion.hbs +1 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/have-text-assertion.hbs +15 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/have-value-assertion.hbs +1 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/is-hidden-assertion.hbs +1 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/label-value-assertion.hbs +12 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/list-item-count-assertion.hbs +2 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/loading-assertion.hbs +2 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/not-checked-assertion.hbs +1 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/page-assertion.hbs +1 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/route-assertion.hbs +2 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/selected-assertion.hbs +1 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/sorted-assertion.hbs +2 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/table-column-exists.hbs +2 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/table-empty.hbs +2 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/table-match-data.hbs +2 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/table-row-count.hbs +2 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/table-row-exists.hbs +2 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/table-row-not-exists.hbs +2 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/visible-assertion.hbs +1 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/gestures/background-action.hbs +1 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/gestures/grant-permission-action.hbs +11 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/gestures/long-press-action.hbs +5 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/gestures/open-notifications-action.hbs +3 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/gestures/pinch-zoom-action.hbs +5 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/gestures/pull-to-refresh-action.hbs +5 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/gestures/rotate-action.hbs +1 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/gestures/set-clipboard-action.hbs +2 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/gestures/set-geolocation-action.hbs +2 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/gestures/swipe-action.hbs +5 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/gestures/tap-top-action.hbs +24 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/navigation/navigation.hbs +1 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/navigation/wait-for-element-with-text.hbs +1 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/navigation/wait-for-element.hbs +1 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/navigation/wait-timeout.hbs +1 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/partials/appium-selector-expr.hbs +8 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/partials/appium-selector.hbs +15 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/partials/locator.hbs +8 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/setup/application-running.hbs +1 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/setup/clear-auth.hbs +2 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/setup/clear-browser-state.hbs +1 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/setup/clear-database.hbs +1 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/setup/user-login-todo.hbs +2 -0
- package/dist/generators/test-generator/adapters/appium/templates/test-file.hbs +226 -0
- package/dist/generators/test-generator/adapters/index.d.ts +1 -0
- package/dist/generators/test-generator/adapters/index.d.ts.map +1 -1
- package/dist/generators/test-generator/adapters/index.js +9 -1
- package/dist/generators/test-generator/adapters/index.js.map +1 -1
- package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
- package/dist/generators/test-generator/code-generator.js +3 -2
- package/dist/generators/test-generator/code-generator.js.map +1 -1
- package/dist/generators/test-generator/step-mapper.d.ts +1 -0
- package/dist/generators/test-generator/step-mapper.d.ts.map +1 -1
- package/dist/generators/test-generator/step-mapper.js +7 -37
- package/dist/generators/test-generator/step-mapper.js.map +1 -1
- package/dist/generators/test-generator/template-engine.d.ts.map +1 -1
- package/dist/generators/test-generator/template-engine.js +13 -1
- package/dist/generators/test-generator/template-engine.js.map +1 -1
- package/dist/harness/audit.d.ts +16 -2
- package/dist/harness/audit.d.ts.map +1 -1
- package/dist/harness/audit.js +74 -11
- package/dist/harness/audit.js.map +1 -1
- package/dist/harness/capability-plan.d.ts +2 -0
- package/dist/harness/capability-plan.d.ts.map +1 -1
- package/dist/harness/capability-plan.js +4 -1
- package/dist/harness/capability-plan.js.map +1 -1
- package/dist/harness/catalog/drivers.yaml +1 -1
- package/dist/harness/flow-check.d.ts.map +1 -1
- package/dist/harness/flow-check.js +13 -4
- package/dist/harness/flow-check.js.map +1 -1
- package/dist/harness/parse.d.ts +2 -0
- package/dist/harness/parse.d.ts.map +1 -1
- package/dist/harness/parse.js +10 -2
- package/dist/harness/parse.js.map +1 -1
- package/dist/harness/quality-gates.d.ts +6 -0
- package/dist/harness/quality-gates.d.ts.map +1 -1
- package/dist/harness/quality-gates.js +15 -1
- package/dist/harness/quality-gates.js.map +1 -1
- package/dist/harness/sensors.d.ts +27 -0
- package/dist/harness/sensors.d.ts.map +1 -1
- package/dist/harness/sensors.js +91 -21
- package/dist/harness/sensors.js.map +1 -1
- package/dist/orchestrator/ai-rules-updater.d.ts.map +1 -1
- package/dist/orchestrator/ai-rules-updater.js +8 -0
- package/dist/orchestrator/ai-rules-updater.js.map +1 -1
- package/dist/orchestrator/templates/ai-instructions/claude-skill-capture-mobile.md +184 -0
- package/dist/orchestrator/templates/ai-instructions/claude-skill-mobile-gestures.md +109 -0
- package/dist/orchestrator/templates/ai-instructions/claude-skill-selector-fix-mobile.md +316 -0
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-capture-mobile.md +184 -0
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-mobile-gestures.md +109 -0
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-selector-fix-mobile.md +316 -0
- package/dist/orchestrator/templates/env.appium.example +25 -0
- package/dist/orchestrator/templates/specs-pw-shape-reporter.ts +92 -0
- package/dist/orchestrator/templates/wdio.conf.ts +295 -0
- 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 +5 -0
- package/dist/utils/selector-types.js.map +1 -1
- package/package.json +3 -3
- package/src/capabilities/context-router.ts +15 -3
- package/src/capabilities/discover.ts +1 -1
- package/src/cli/commands/audit.ts +5 -3
- package/src/generators/test-generator/adapters/appium/appium-adapter.ts +57 -0
- package/src/generators/test-generator/adapters/appium/templates/after-all.hbs +8 -0
- package/src/generators/test-generator/adapters/appium/templates/after-each.hbs +8 -0
- package/src/generators/test-generator/adapters/appium/templates/before-all.hbs +8 -0
- package/src/generators/test-generator/adapters/appium/templates/before-each.hbs +8 -0
- package/src/generators/test-generator/adapters/appium/templates/imports.hbs +8 -0
- package/src/generators/test-generator/adapters/appium/templates/scenario.hbs +8 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/alert-accept-action.hbs +4 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/alert-dismiss-action.hbs +2 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/alert-fill-action.hbs +2 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/check-action.hbs +10 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/clear-action.hbs +1 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/click-action.hbs +1 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/click-element-with-text.hbs +2 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/click-select-action.hbs +4 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/dismiss-action.hbs +3 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/double-click-action.hbs +6 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/drag-action.hbs +2 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/expand-action.hbs +2 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/fill-action.hbs +14 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/fill-editor-action.hbs +12 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/frame-enter-action.hbs +6 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/frame-exit-action.hbs +2 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/hide-keyboard-action.hbs +4 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/hover-action.hbs +2 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/keyboard-global-action.hbs +2 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/press-action.hbs +4 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/radio-select-action.hbs +4 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/scroll-action.hbs +19 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/select-action.hbs +5 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/table-action-in-row.hbs +2 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/toggle-action.hbs +3 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/uncheck-action.hbs +8 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/unknown-element-action.hbs +2 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/upload-action.hbs +3 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/wait-for-page.hbs +3 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/wait-for-role-with-data.hbs +2 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/wait-for-role.hbs +3 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/alert-text-assertion.hbs +2 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/attribute-assertion.hbs +1 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/checked-assertion.hbs +1 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/clipboard-text-assertion.hbs +2 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/column-cell-assertion.hbs +2 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/contain-text-assertion.hbs +14 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/count-assertion.hbs +1 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/disabled-assertion.hbs +1 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/empty-assertion.hbs +1 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/enabled-assertion.hbs +1 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/focused-assertion.hbs +1 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/have-text-assertion.hbs +15 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/have-value-assertion.hbs +1 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/is-hidden-assertion.hbs +1 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/label-value-assertion.hbs +12 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/list-item-count-assertion.hbs +2 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/loading-assertion.hbs +2 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/not-checked-assertion.hbs +1 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/page-assertion.hbs +1 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/route-assertion.hbs +2 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/selected-assertion.hbs +1 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/sorted-assertion.hbs +2 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/table-column-exists.hbs +2 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/table-empty.hbs +2 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/table-match-data.hbs +2 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/table-row-count.hbs +2 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/table-row-exists.hbs +2 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/table-row-not-exists.hbs +2 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/visible-assertion.hbs +1 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/gestures/background-action.hbs +1 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/gestures/grant-permission-action.hbs +11 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/gestures/long-press-action.hbs +5 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/gestures/open-notifications-action.hbs +3 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/gestures/pinch-zoom-action.hbs +5 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/gestures/pull-to-refresh-action.hbs +5 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/gestures/rotate-action.hbs +1 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/gestures/set-clipboard-action.hbs +2 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/gestures/set-geolocation-action.hbs +2 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/gestures/swipe-action.hbs +5 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/gestures/tap-top-action.hbs +24 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/navigation/navigation.hbs +1 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/navigation/wait-for-element-with-text.hbs +1 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/navigation/wait-for-element.hbs +1 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/navigation/wait-timeout.hbs +1 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/partials/appium-selector-expr.hbs +8 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/partials/appium-selector.hbs +15 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/partials/locator.hbs +8 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/setup/application-running.hbs +1 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/setup/clear-auth.hbs +2 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/setup/clear-browser-state.hbs +1 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/setup/clear-database.hbs +1 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/setup/user-login-todo.hbs +2 -0
- package/src/generators/test-generator/adapters/appium/templates/test-file.hbs +226 -0
- package/src/generators/test-generator/adapters/index.ts +7 -0
- package/src/generators/test-generator/code-generator.ts +3 -2
- package/src/generators/test-generator/step-mapper.ts +8 -5
- package/src/generators/test-generator/template-engine.ts +13 -1
- package/src/harness/audit.ts +84 -14
- package/src/harness/capability-plan.ts +5 -2
- package/src/harness/catalog/drivers.yaml +1 -1
- package/src/harness/flow-check.ts +13 -4
- package/src/harness/parse.ts +12 -2
- package/src/harness/quality-gates.ts +14 -1
- package/src/harness/sensors.ts +110 -22
- package/src/orchestrator/ai-rules-updater.ts +8 -0
- package/src/orchestrator/templates/ai-instructions/claude-skill-capture-mobile.md +184 -0
- package/src/orchestrator/templates/ai-instructions/claude-skill-mobile-gestures.md +109 -0
- package/src/orchestrator/templates/ai-instructions/claude-skill-selector-fix-mobile.md +316 -0
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-capture-mobile.md +184 -0
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-mobile-gestures.md +109 -0
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-selector-fix-mobile.md +316 -0
- package/src/orchestrator/templates/env.appium.example +25 -0
- package/src/orchestrator/templates/specs-pw-shape-reporter.ts +92 -0
- package/src/orchestrator/templates/wdio.conf.ts +295 -0
- package/src/utils/selector-types.ts +5 -0
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
{{#if platform}}
|
|
2
|
+
// sungen:platform={{platform}}
|
|
3
|
+
{{/if}}
|
|
4
|
+
{{#if runtimeData}}
|
|
5
|
+
|
|
6
|
+
const testData = TestDataLoader.load('{{screenName}}', '{{featureFileName}}');
|
|
7
|
+
{{/if}}
|
|
8
|
+
{{#if singleAuthRole}}
|
|
9
|
+
import * as fs from 'node:fs';
|
|
10
|
+
import * as path from 'node:path';
|
|
11
|
+
|
|
12
|
+
// ── @auth (mobile) ──────────────────────────────────────────────────────────
|
|
13
|
+
// App-agnostic auth: the framework supplies the MECHANISM; each app supplies the DATA via a
|
|
14
|
+
// gitignored contract at specs/.auth-mobile/<role>.json:
|
|
15
|
+
// { "authedMarker": {type,value}, // present ⟺ already logged in
|
|
16
|
+
// "credentials": { ... }, // referenced as {{key}} in step values
|
|
17
|
+
// "loginSteps": [ {action:"tap"|"fill"|"wait", selector:{type,value}, value?} ] }
|
|
18
|
+
// ensureAuth is idempotent (skips when already logged in) and fail-loud (throws → the @auth spec is
|
|
19
|
+
// blocked, never silently run logged-out). See MOBILE_INTEGRATION_PLAN.md §Phase 5.1.
|
|
20
|
+
function __authSelector(s: { type: string; value: string }): string {
|
|
21
|
+
switch (s.type) {
|
|
22
|
+
case 'accessibility-id': case 'testid': case 'label': case 'placeholder': case 'text': return '~' + s.value;
|
|
23
|
+
case 'android-uiautomator': return 'android=' + s.value;
|
|
24
|
+
case 'ios-predicate': return '-ios predicate string:' + s.value;
|
|
25
|
+
case 'id': return 'id=' + s.value;
|
|
26
|
+
case 'xpath': case 'locator': default: return s.value;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function __authInterpolate(v: string, creds: Record<string, unknown> = {}): string {
|
|
30
|
+
return String(v).replace(/\{\{\s*(\w+)\s*\}\}/g, (_m, k) => (creds[k] != null ? String(creds[k]) : ''));
|
|
31
|
+
}
|
|
32
|
+
// Robust mobile text entry: Flutter often ignores a single setValue, and W3C key-streams can drop a
|
|
33
|
+
// char — so we type, read back, and retry once. Masked (password) fields can't be verified → trusted.
|
|
34
|
+
async function __robustFill(el: WebdriverIO.Element, value: string): Promise<void> {
|
|
35
|
+
await el.click();
|
|
36
|
+
try { await el.clearValue(); } catch { /* not clearable */ }
|
|
37
|
+
await el.setValue(value);
|
|
38
|
+
let got = '';
|
|
39
|
+
try { got = (await el.getText()) || (await el.getAttribute('text')) || ''; } catch { /* ignore */ }
|
|
40
|
+
const masked = /[•●*]/.test(got);
|
|
41
|
+
if (!masked && got.replace(/\s/g, '') !== value.replace(/\s/g, '')) {
|
|
42
|
+
try { await el.clearValue(); } catch { /* ignore */ }
|
|
43
|
+
await el.addValue(value);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
async function __ensureAuth(role: string): Promise<void> {
|
|
47
|
+
const cfgPath = path.join(process.cwd(), 'specs', '.auth-mobile', role + '.json');
|
|
48
|
+
const rel = path.relative(process.cwd(), cfgPath);
|
|
49
|
+
if (!fs.existsSync(cfgPath)) {
|
|
50
|
+
throw new Error(`@auth:${role} requires ${rel} (loginSteps + credentials + a login-state marker). See MOBILE_INTEGRATION_PLAN.md §Phase 5.1.`);
|
|
51
|
+
}
|
|
52
|
+
const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf-8'));
|
|
53
|
+
const present = async (marker: { type: string; value: string } | undefined, timeout = 6000): Promise<boolean> => {
|
|
54
|
+
if (!marker) return false;
|
|
55
|
+
try { await $(__authSelector(marker)).waitForDisplayed({ timeout }); return true; }
|
|
56
|
+
catch { return false; }
|
|
57
|
+
};
|
|
58
|
+
const absent = async (marker: { type: string; value: string } | undefined, timeout = 8000): Promise<boolean> => {
|
|
59
|
+
if (!marker) return true;
|
|
60
|
+
try { await $(__authSelector(marker)).waitForDisplayed({ reverse: true, timeout }); return true; }
|
|
61
|
+
catch { return false; }
|
|
62
|
+
};
|
|
63
|
+
const runSteps = async (steps: any[]): Promise<void> => {
|
|
64
|
+
for (const step of (steps || [])) {
|
|
65
|
+
// Selector-less actions first (no element to find).
|
|
66
|
+
if (step.action === 'hideKeyboard') { try { await driver.hideKeyboard(); } catch { /* no keyboard shown */ } continue; }
|
|
67
|
+
if (step.action === 'back') { try { await driver.back(); } catch { /* nothing to dismiss */ } continue; }
|
|
68
|
+
const el = await $(__authSelector(step.selector));
|
|
69
|
+
await el.waitForDisplayed();
|
|
70
|
+
if (step.action === 'fill') await __robustFill(el, __authInterpolate(step.value, cfg.credentials));
|
|
71
|
+
else if (step.action === 'tap') await el.click();
|
|
72
|
+
// 'wait' = just the waitForDisplayed above
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// ── Mode A: navigation-based (preferred) ──────────────────────────────────
|
|
77
|
+
// For apps whose login state is NOT visible on the post-relaunch landing screen (e.g. a Flutter
|
|
78
|
+
// app that always opens on Home). We navigate to a DECISION screen (loginEntrySteps, e.g. My Page
|
|
79
|
+
// → Account Settings) and read `loggedOutMarker` there (present ⟺ logged out, e.g. the Login
|
|
80
|
+
// button). loginSteps then start FROM that decision screen.
|
|
81
|
+
if (cfg.loginEntrySteps && cfg.loggedOutMarker) {
|
|
82
|
+
await runSteps(cfg.loginEntrySteps);
|
|
83
|
+
if (!(await present(cfg.loggedOutMarker, 8000))) return; // logged-out marker absent ⟹ already logged in (noReset)
|
|
84
|
+
await runSteps(cfg.loginSteps);
|
|
85
|
+
// Verify: a credentials error means rejection; the login form staying put (logged-out marker
|
|
86
|
+
// never disappears) means it didn't submit. Success = the form navigated away with no error.
|
|
87
|
+
if (await present(cfg.errorMarker, 3000)) {
|
|
88
|
+
throw new Error(`@auth:${role} login rejected (error shown) — check credentials.email/password in ${rel}.`);
|
|
89
|
+
}
|
|
90
|
+
if (!(await absent(cfg.loggedOutMarker, 8000))) {
|
|
91
|
+
throw new Error(`@auth:${role} login did not complete (still on the login screen) — check loginSteps in ${rel}.`);
|
|
92
|
+
}
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ── Mode B: legacy authedMarker (present on the landing screen ⟺ logged in) ──
|
|
97
|
+
const markerPresent = async (): Promise<boolean> => present(cfg.authedMarker, 6000);
|
|
98
|
+
if (await markerPresent()) return; // idempotent — already authenticated (session kept by noReset)
|
|
99
|
+
await runSteps(cfg.loginSteps);
|
|
100
|
+
if (!(await markerPresent())) {
|
|
101
|
+
throw new Error(`@auth:${role} login failed — verify credentials / loginSteps / authedMarker in ${rel}.`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
{{/if}}
|
|
105
|
+
|
|
106
|
+
{{#if featureDescription}}
|
|
107
|
+
/**
|
|
108
|
+
* Feature: {{featureName}}
|
|
109
|
+
* {{featureDescription}}
|
|
110
|
+
*/
|
|
111
|
+
{{/if}}
|
|
112
|
+
describe('{{featureName}}', () => {
|
|
113
|
+
{{#if singleAuthRole}}
|
|
114
|
+
// @auth:{{singleAuthRole}} — authenticate ONCE before any scenario; the session persists across
|
|
115
|
+
// the per-scenario relaunch below (noReset:true). Throws (blocks the spec) if login can't be done.
|
|
116
|
+
before(async () => {
|
|
117
|
+
{{#if appPackage}}
|
|
118
|
+
// Relaunch to a known launch screen first — the session may attach to the app in any state.
|
|
119
|
+
// Platform-aware id (NOT the Android literal): on iOS a @platform:mobile feature's app is the
|
|
120
|
+
// dual-id bundle id — a literal appPackage here would throw and block the whole spec.
|
|
121
|
+
// (__resolveAppId is a hoisted function declaration in this describe scope.)
|
|
122
|
+
const __AUTH_APP__ = __resolveAppId();
|
|
123
|
+
try { await driver.terminateApp(__AUTH_APP__); } catch { /* not running */ }
|
|
124
|
+
await driver.activateApp(__AUTH_APP__);
|
|
125
|
+
{{/if}}
|
|
126
|
+
await __ensureAuth('{{singleAuthRole}}');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
{{/if}}
|
|
130
|
+
{{#if appPackage}}
|
|
131
|
+
// Multi-app: this feature's app (from the `Path:` line). Reset to its launch screen before each
|
|
132
|
+
// scenario so one `wdio run` can host screens from different apps — each spec launches its own.
|
|
133
|
+
// The `Path:` holds the ANDROID package (or, for an @platform:ios feature, the iOS bundle id).
|
|
134
|
+
const __APP_PKG_DEFAULT__ = '{{appPackage}}';
|
|
135
|
+
{{#if iosBundleId}}
|
|
136
|
+
// Per-feature iOS bundle id from the dual-id `Path: <androidPkg>/<activity> | <iosBundleId>` —
|
|
137
|
+
// app ids differ per OS (e.g. an iOS `.dev` flavor), and keeping it in the feature file makes the
|
|
138
|
+
// spec self-describing + lets one iOS run host multiple apps.
|
|
139
|
+
const __IOS_BUNDLE__ = '{{iosBundleId}}';
|
|
140
|
+
{{/if}}
|
|
141
|
+
// Platform-aware app id: an @platform:mobile spec runs on BOTH OSes. On iOS prefer the feature's own
|
|
142
|
+
// bundle id (dual-id Path), then the session cap (IOS_BUNDLE_ID in wdio.conf), then the Path value;
|
|
143
|
+
// on Android (or any non-iOS session) use the Path package unchanged.
|
|
144
|
+
function __resolveAppId(): string {
|
|
145
|
+
if (typeof driver !== 'undefined' && driver.isIOS) {
|
|
146
|
+
const caps = (driver.requestedCapabilities || {}) as Record<string, string>;
|
|
147
|
+
return {{#if iosBundleId}}__IOS_BUNDLE__ || {{/if}}caps['appium:bundleId'] || caps['bundleId'] || __APP_PKG_DEFAULT__;
|
|
148
|
+
}
|
|
149
|
+
return __APP_PKG_DEFAULT__;
|
|
150
|
+
}
|
|
151
|
+
beforeEach(async () => {
|
|
152
|
+
const __APP_PKG__ = __resolveAppId();
|
|
153
|
+
try { await driver.terminateApp(__APP_PKG__); } catch { /* app may not be running yet */ }
|
|
154
|
+
await driver.activateApp(__APP_PKG__);
|
|
155
|
+
// Settle after a cold (re)launch: a Flutter app first renders a BLANK accessibility skeleton
|
|
156
|
+
// (nested FrameLayout/View, no content-desc) for several seconds before the real tree appears.
|
|
157
|
+
// That skeleton is static, so a "two equal reads" check alone false-positives on it and we'd
|
|
158
|
+
// proceed before any element exists (→ every find fails). So require the tree to (a) contain
|
|
159
|
+
// rendered content — at least one non-empty content-desc/text — AND (b) be stable across two
|
|
160
|
+
// reads. A generous timeout covers slow cold renders on a stressed emulator. Best-effort: a
|
|
161
|
+
// timeout never fails the scenario, it just falls through to the step's own auto-wait.
|
|
162
|
+
let __prevSrc = '';
|
|
163
|
+
await driver
|
|
164
|
+
.waitUntil(
|
|
165
|
+
async () => {
|
|
166
|
+
const src = await driver.getPageSource();
|
|
167
|
+
const hasContent = /content-desc="[^"]+"|\btext="[^"]+"/.test(src);
|
|
168
|
+
const stable = hasContent && src === __prevSrc;
|
|
169
|
+
__prevSrc = src;
|
|
170
|
+
return stable;
|
|
171
|
+
},
|
|
172
|
+
{ timeout: 30000, interval: 400, timeoutMsg: 'UI did not settle (no rendered content)' },
|
|
173
|
+
)
|
|
174
|
+
.catch(() => { /* best-effort — fall through to per-step auto-wait */ });
|
|
175
|
+
{{#if backgroundSteps}}
|
|
176
|
+
// Background runs here (inside beforeEach) — the app resets to its launch screen every scenario,
|
|
177
|
+
// so Background setup (nav, dismiss launch promos…) must re-run after each relaunch, not once.
|
|
178
|
+
{{#each backgroundSteps}}
|
|
179
|
+
{{#if comment}}
|
|
180
|
+
// {{comment}}
|
|
181
|
+
{{/if}}
|
|
182
|
+
{{code}}
|
|
183
|
+
{{/each}}
|
|
184
|
+
{{/if}}
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
{{/if}}
|
|
188
|
+
{{#unless appPackage}}
|
|
189
|
+
{{#if backgroundSteps}}
|
|
190
|
+
before(async () => {
|
|
191
|
+
{{#each backgroundSteps}}
|
|
192
|
+
{{#if comment}}
|
|
193
|
+
// {{comment}}
|
|
194
|
+
{{/if}}
|
|
195
|
+
{{code}}
|
|
196
|
+
{{/each}}
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
{{/if}}
|
|
200
|
+
{{/unless}}
|
|
201
|
+
{{#if beforeAll}}
|
|
202
|
+
{{beforeAll}}
|
|
203
|
+
|
|
204
|
+
{{/if}}
|
|
205
|
+
{{#if afterEach}}
|
|
206
|
+
{{afterEach}}
|
|
207
|
+
|
|
208
|
+
{{/if}}
|
|
209
|
+
{{#if afterAll}}
|
|
210
|
+
{{afterAll}}
|
|
211
|
+
|
|
212
|
+
{{/if}}
|
|
213
|
+
{{#if authGroups}}
|
|
214
|
+
{{#each authGroups}}
|
|
215
|
+
{{#each scenarios}}
|
|
216
|
+
{{this}}
|
|
217
|
+
|
|
218
|
+
{{/each}}
|
|
219
|
+
{{/each}}
|
|
220
|
+
{{else}}
|
|
221
|
+
{{#each scenarios}}
|
|
222
|
+
{{this}}
|
|
223
|
+
|
|
224
|
+
{{/each}}
|
|
225
|
+
{{/if}}
|
|
226
|
+
});
|
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
export { TestGeneratorAdapter, TestFileData, ScenarioData, StepTemplateData, LocatorExpression } from './adapter-interface';
|
|
2
2
|
export { AdapterRegistry, adapterRegistry, DriverNotInstalledError } from './adapter-registry';
|
|
3
3
|
export { PlaywrightAdapter } from './playwright/playwright-adapter';
|
|
4
|
+
export { AppiumAdapter } from './appium/appium-adapter';
|
|
4
5
|
|
|
5
6
|
// Auto-register built-in adapters
|
|
6
7
|
import { adapterRegistry } from './adapter-registry';
|
|
7
8
|
import { PlaywrightAdapter } from './playwright/playwright-adapter';
|
|
9
|
+
import { AppiumAdapter } from './appium/appium-adapter';
|
|
8
10
|
|
|
9
11
|
adapterRegistry.register('playwright', () => new PlaywrightAdapter());
|
|
10
12
|
// Phase 2a: platform alias. `web` is the bundled Playwright adapter (back-compat
|
|
11
13
|
// baseline) until it is externalized to @sungen/driver-web in a later cut.
|
|
12
14
|
adapterRegistry.register('web', () => new PlaywrightAdapter());
|
|
15
|
+
// MOB-2: the Appium codegen adapter — template strings only, NO appium/wdio dep in core.
|
|
16
|
+
// `platform: mobile` (qa/capabilities.yaml) loads @sungen/driver-mobile (the SPI capability)
|
|
17
|
+
// and selects this adapter (the runtime codegen). `appium` is the explicit alias.
|
|
18
|
+
adapterRegistry.register('mobile', () => new AppiumAdapter());
|
|
19
|
+
adapterRegistry.register('appium', () => new AppiumAdapter());
|
|
@@ -76,7 +76,7 @@ function extractCleanupFlags(tags: string[]): { overlay?: boolean; forms?: boole
|
|
|
76
76
|
const FUNCTIONAL_TAG_PREFIXES = [
|
|
77
77
|
'@parallel', '@cleanup:', '@auth:', '@manual', '@no-auth',
|
|
78
78
|
'@steps:', '@extend:', '@screenshot:', '@beforeAll', '@afterEach', '@afterAll',
|
|
79
|
-
'@flow', '@cases:',
|
|
79
|
+
'@flow', '@cases:', '@deferred:', '@owned-by:',
|
|
80
80
|
];
|
|
81
81
|
|
|
82
82
|
function extractPassThroughTags(scenarioTags: string[], featureTags: string[]): string | undefined {
|
|
@@ -173,7 +173,8 @@ function getEffectiveAuthRole(
|
|
|
173
173
|
* @manual at scenario level → skip that scenario
|
|
174
174
|
*/
|
|
175
175
|
function isManual(tags: string[]): boolean {
|
|
176
|
-
|
|
176
|
+
// @deferred:flow is owned by a flow → not automated on this screen, so it skips like @manual (H6).
|
|
177
|
+
return tags.some(tag => tag === '@manual' || tag === '@deferred:flow');
|
|
177
178
|
}
|
|
178
179
|
|
|
179
180
|
/**
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { ParsedStep } from '../gherkin-parser';
|
|
2
2
|
import { TemplateEngine } from './template-engine';
|
|
3
|
+
import { adapterRegistry } from './adapters';
|
|
3
4
|
import { PatternRegistry, PatternContext } from './patterns';
|
|
4
5
|
import { SelectorResolver } from './utils/selector-resolver';
|
|
5
6
|
import { DataResolver } from './utils/data-resolver';
|
|
6
|
-
import * as path from 'path';
|
|
7
7
|
|
|
8
8
|
export interface MappedStep {
|
|
9
9
|
code: string; // Generated Playwright code
|
|
@@ -35,15 +35,18 @@ export class StepMapper {
|
|
|
35
35
|
private inRowScope: boolean = false;
|
|
36
36
|
private rowScopeTable: string = '';
|
|
37
37
|
|
|
38
|
-
constructor(options: { verbose?: boolean; baseURL?: string; featureName?: string; screenName?: string; featurePath?: string; runtimeData?: boolean } = {}) {
|
|
38
|
+
constructor(options: { verbose?: boolean; baseURL?: string; featureName?: string; screenName?: string; featurePath?: string; runtimeData?: boolean; framework?: string } = {}) {
|
|
39
39
|
this.verbose = options.verbose ?? false;
|
|
40
40
|
this.baseURL = options.baseURL || null; // null means path-only navigation
|
|
41
41
|
this.featureName = options.featureName;
|
|
42
42
|
this.screenName = options.screenName;
|
|
43
43
|
this.featurePath = options.featurePath;
|
|
44
|
-
//
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
// Render step templates from the ACTIVE adapter's template set (web→Playwright,
|
|
45
|
+
// mobile→Appium). The adapter owns its templatesDir; resolving it here keeps step
|
|
46
|
+
// rendering and the file skeleton (code-generator) on the same framework.
|
|
47
|
+
const framework = options.framework || 'web';
|
|
48
|
+
const templatesDir = adapterRegistry.getAdapter(framework).templatesDir;
|
|
49
|
+
this.templateEngine = new TemplateEngine(templatesDir);
|
|
47
50
|
this.patternRegistry = new PatternRegistry();
|
|
48
51
|
this.selectorResolver = new SelectorResolver(undefined, options.screenName);
|
|
49
52
|
this.dataResolver = new DataResolver(undefined, options.screenName, options.runtimeData);
|
|
@@ -168,6 +168,18 @@ export class TemplateEngine {
|
|
|
168
168
|
const dialogRootContent = fs.readFileSync(dialogRootPath, 'utf-8');
|
|
169
169
|
Handlebars.registerPartial('dialog-root', dialogRootContent);
|
|
170
170
|
}
|
|
171
|
+
|
|
172
|
+
// Adapter-agnostic: register any OTHER top-level partial in partials/ by its basename
|
|
173
|
+
// (the named registrations above are playwright's; the appium adapter ships
|
|
174
|
+
// appium-selector / appium-selector-expr). Idempotent — re-registering by name is harmless.
|
|
175
|
+
const partialsDir = path.join(this.stepsTemplatesDir, 'partials');
|
|
176
|
+
if (fs.existsSync(partialsDir)) {
|
|
177
|
+
for (const f of fs.readdirSync(partialsDir)) {
|
|
178
|
+
if (!f.endsWith('.hbs')) continue;
|
|
179
|
+
const name = f.replace(/\.hbs$/, '');
|
|
180
|
+
Handlebars.registerPartial(name, fs.readFileSync(path.join(partialsDir, f), 'utf-8'));
|
|
181
|
+
}
|
|
182
|
+
}
|
|
171
183
|
}
|
|
172
184
|
|
|
173
185
|
private loadTemplate(templateName: string, isStepTemplate: boolean = false): HandlebarsTemplateDelegate {
|
|
@@ -179,7 +191,7 @@ export class TemplateEngine {
|
|
|
179
191
|
|
|
180
192
|
// Try to find template in organized folders
|
|
181
193
|
if (isStepTemplate) {
|
|
182
|
-
const folders = ['actions', 'assertions', 'navigation', 'setup', 'partials'];
|
|
194
|
+
const folders = ['actions', 'assertions', 'navigation', 'setup', 'gestures', 'partials'];
|
|
183
195
|
|
|
184
196
|
for (const folder of folders) {
|
|
185
197
|
const templatePath = path.join(baseDir, folder, `${templateName}.hbs`);
|
package/src/harness/audit.ts
CHANGED
|
@@ -12,8 +12,8 @@ import { loadScenarios, parseViewpointOverview, ScenarioInfo, ViewpointEntry } f
|
|
|
12
12
|
import { featureBasename } from './unit-paths';
|
|
13
13
|
import {
|
|
14
14
|
loadCatalog, viewpointGate, assertionDepth, dataThemesFor, depthThresholdFor, coverageBalance, duplicateClusters, traceability, claimProof, taxonomyLint,
|
|
15
|
-
automatableManual, flowCoveredThemes,
|
|
16
|
-
GateResult, DepthResult, BalanceResult, DuplicateResult, TraceResult, ClaimProofResult, TaxonomyResult, Catalog, AutomatableManualResult,
|
|
15
|
+
automatableManual, flowCoveredThemes, flowRegressionDepth, oracleStrength,
|
|
16
|
+
GateResult, DepthResult, BalanceResult, DuplicateResult, TraceResult, ClaimProofResult, TaxonomyResult, Catalog, AutomatableManualResult, FlowDepthResult, OracleStrengthResult,
|
|
17
17
|
} from './sensors';
|
|
18
18
|
import { loadFlowScenarios } from './flow-check';
|
|
19
19
|
import { manualReasonMismatches, MANUAL_REASONS, buildPlan } from './capability-plan';
|
|
@@ -22,7 +22,7 @@ import { readIntent, projectRootFromScreenDir, IntentProfile } from './intent';
|
|
|
22
22
|
import { getProvenance, Provenance } from './provenance';
|
|
23
23
|
import { specCoverage, SpecCoverageResult, parseSpecClauses } from './spec-coverage';
|
|
24
24
|
import { downstreamScope, manualOracle, readText, DownstreamResult, ManualOracleResult,
|
|
25
|
-
negativeSideEffect, sourceBacked, crossArtifactOwnership } from './quality-gates';
|
|
25
|
+
negativeSideEffect, sourceBacked, crossArtifactOwnership, isolationRisk } from './quality-gates';
|
|
26
26
|
import { viewpointLedger, parseViewpointItems, LedgerResult } from './viewpoint-ledger';
|
|
27
27
|
import { capabilityRegistry } from '../capabilities/registry';
|
|
28
28
|
import { discoverAndRegisterCapabilities } from '../capabilities/discover';
|
|
@@ -42,6 +42,8 @@ export interface AuditReport {
|
|
|
42
42
|
downstream: DownstreamResult; // downstream screens referenced but under-covered
|
|
43
43
|
manualOracle: ManualOracleResult; // @manual scenarios lacking setup/action/oracle
|
|
44
44
|
automatableManual: AutomatableManualResult; // @manual that is actually automatable (deferred, not judgment) — TQ-2
|
|
45
|
+
flowDepth: FlowDepthResult; // H3 — stateful-flow regression depth (count / teardown / multi-source)
|
|
46
|
+
oracle: OracleStrengthResult; // H4 — facet-oracle strength (weak name-substring vs title/detail/API/DB)
|
|
45
47
|
ledger: LedgerResult; // atomic viewpoint-item coverage (per-bullet status)
|
|
46
48
|
calibration: { // #8 — multi-axis score so a high overall can't hide a weak axis
|
|
47
49
|
axes: Record<string, number>;
|
|
@@ -81,9 +83,28 @@ function catalogIdFromScreenDir(screenDir: string): string {
|
|
|
81
83
|
* future `mobile/<x>` or `perf/<x>` unit routes to that capability with no core change. `flows/<flow>`
|
|
82
84
|
* has no `flows` capability → default (UI), which is correct (flows are a UI concept).
|
|
83
85
|
*/
|
|
84
|
-
export function scoringCapabilityFor(catalogScreenName: string, defaultCap: string | undefined): string | undefined {
|
|
86
|
+
export function scoringCapabilityFor(catalogScreenName: string, defaultCap: string | undefined, platform?: string): string | undefined {
|
|
85
87
|
const seg = catalogScreenName.split('/')[0];
|
|
86
|
-
|
|
88
|
+
// Path segment wins (api/<area> → api). Then the active platform capability (mobile → mobile),
|
|
89
|
+
// when a driver registered one with that id. Else the default (web/bare screen → ui — unchanged,
|
|
90
|
+
// since `web` has no capability of its own). See docs/spec/sungen-platform-capability-routing-spec.md.
|
|
91
|
+
if (seg && capabilityRegistry.get(seg)) return seg;
|
|
92
|
+
if (platform && capabilityRegistry.get(platform)) return platform;
|
|
93
|
+
return defaultCap;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* H7 — senior-grade band. The top decile (≥9) is reserved for suites that ALSO clear the senior
|
|
98
|
+
* axes: a stateful flow with FULL regression depth (count + teardown + multi-source), no weak facet
|
|
99
|
+
* oracle, and no parallel-cart isolation risk. Otherwise the score is held just below 9 (8.9), so
|
|
100
|
+
* "≥9" means senior-grade — not merely "themes covered". Neutral for screens/api (no signals → 10).
|
|
101
|
+
*/
|
|
102
|
+
export function seniorBandedOverall(
|
|
103
|
+
rawOverall: number,
|
|
104
|
+
s: { flowStateful: boolean; flowRatio: number; oracleWeak: number; isolationRisk: boolean },
|
|
105
|
+
): number {
|
|
106
|
+
const seniorGrade = (!s.flowStateful || s.flowRatio >= 1) && s.oracleWeak === 0 && !s.isolationRisk;
|
|
107
|
+
return Math.min(rawOverall, seniorGrade ? 10 : 8.9);
|
|
87
108
|
}
|
|
88
109
|
|
|
89
110
|
export function runAudit(screenDir: string, screenName: string): AuditReport {
|
|
@@ -106,8 +127,11 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
|
|
|
106
127
|
// UI capability). A capability that provides no catalog/gate falls back to the in-core UI
|
|
107
128
|
// functions, so UI units — and api units until AO-2 adds the api providers — are byte-identical.
|
|
108
129
|
discoverAndRegisterCapabilities();
|
|
130
|
+
// The active platform (web | mobile | …) activates its own capability for scoring + sensor routing.
|
|
131
|
+
// `web` has no capability of its own → scoringCap stays the default `ui` (byte-identical).
|
|
132
|
+
const platform = readCapabilities(projectRootFromScreenDir(screenDir)).platform;
|
|
109
133
|
const defaultCap = capabilityRegistry.defaultCapabilityId();
|
|
110
|
-
const scoringCapId = scoringCapabilityFor(catalogScreenName, defaultCap);
|
|
134
|
+
const scoringCapId = scoringCapabilityFor(catalogScreenName, defaultCap, platform);
|
|
111
135
|
const scoringCap = scoringCapId ? capabilityRegistry.get(scoringCapId) : undefined;
|
|
112
136
|
const catalog = (scoringCap?.viewpoints?.() as Catalog | undefined) || loadCatalog();
|
|
113
137
|
const spec = specCoverage(specPath, scenarios, featureText);
|
|
@@ -157,16 +181,43 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
|
|
|
157
181
|
const ownership = crossArtifactOwnership(screenDir, scenarios);
|
|
158
182
|
const unsourced = sourceBacked(scenarios, parseSpecClauses(specPath).frs.map((f) => f.id), parseViewpointItems(viewpointPath).map((i) => i.text), viewpoints.map((v) => v.id), featureText);
|
|
159
183
|
|
|
184
|
+
// H3 — stateful-flow regression depth. For a UI flow whose scenarios mutate a cart/checkout
|
|
185
|
+
// collection, the regression dimensions (count/quantity proof · teardown · multi-source) cap the
|
|
186
|
+
// businessDepth headroom: it can reach 1.0 only when all three are exercised, so a present-but-
|
|
187
|
+
// shallow flow can't claim a perfect score (floor 0.5 — assertion depth still dominates).
|
|
188
|
+
const isUiFlow = /^flows\//.test(catalogScreenName);
|
|
189
|
+
const flowDepth = isUiFlow ? flowRegressionDepth(scenarios) : { stateful: false, countProof: false, teardown: false, multiSource: false, ratio: 1, missing: [] } as FlowDepthResult;
|
|
190
|
+
const FLOW_DEPTH_FLOOR = 0.5;
|
|
191
|
+
// H4 — oracle strength: a weak facet oracle (name-substring "proves" category/brand membership)
|
|
192
|
+
// caps businessDepth the same way (floor 0.5). A suite with no facet claim, or a strong oracle, is
|
|
193
|
+
// neutral (ratio 1 → no cap), so existing snapshots are unaffected.
|
|
194
|
+
const oracle = oracleStrength(scenarios);
|
|
195
|
+
const ORACLE_FLOOR = 0.5;
|
|
196
|
+
|
|
160
197
|
// Sub-scores
|
|
161
198
|
const coverage = gate.coverageRatio;
|
|
162
|
-
const businessDepth =
|
|
163
|
-
|
|
164
|
-
?
|
|
165
|
-
: 1
|
|
199
|
+
const businessDepth = Math.min(
|
|
200
|
+
depth.bcDepthRatio,
|
|
201
|
+
flowDepth.stateful ? FLOW_DEPTH_FLOOR + (1 - FLOW_DEPTH_FLOOR) * flowDepth.ratio : 1,
|
|
202
|
+
oracle.weak.length ? ORACLE_FLOOR + (1 - ORACLE_FLOOR) * oracle.ratio : 1,
|
|
203
|
+
);
|
|
204
|
+
// When the taxonomy drifted (most scenarios unclassified), the balance axis is unreliable — cap it
|
|
205
|
+
// at 0.5 instead of awarding a vacuous 1.0 so a stale taxonomy fails loudly, not silently (H1).
|
|
206
|
+
const balanceScore = balance.unclassifiedRatio > 0.4
|
|
207
|
+
? 0.5
|
|
208
|
+
: balance.coreCount + balance.secondaryCount > 0
|
|
209
|
+
? Math.min(1, balance.coreCount / Math.max(1, balance.secondaryCount))
|
|
210
|
+
: 1;
|
|
166
211
|
const traceScore = 0.5 * trace.withVpCodeRatio + 0.5 * trace.mappedRatio;
|
|
167
212
|
|
|
168
213
|
// Business-weighted overall (coverage + depth dominate)
|
|
169
|
-
const
|
|
214
|
+
const rawOverall = (0.4 * coverage + 0.3 * businessDepth + 0.15 * balanceScore + 0.15 * traceScore) * 10;
|
|
215
|
+
// H7 — senior-grade band: the top decile (≥9) is reserved for suites that also clear the senior
|
|
216
|
+
// axes — a stateful flow with FULL regression depth (count + teardown + multi-source), no weak
|
|
217
|
+
// facet oracle, and no parallel-cart isolation risk. Otherwise the score is held just below 9, so
|
|
218
|
+
// "≥9" means senior-grade, not merely "themes covered". Neutral for screens/api (no signals).
|
|
219
|
+
const isoRisk = isolationRisk(featureText, flowDepth.stateful);
|
|
220
|
+
const overall = seniorBandedOverall(rawOverall, { flowStateful: flowDepth.stateful, flowRatio: flowDepth.ratio, oracleWeak: oracle.weak.length, isolationRisk: isoRisk });
|
|
170
221
|
|
|
171
222
|
const findings: string[] = [];
|
|
172
223
|
for (const c of flowCredits) {
|
|
@@ -187,6 +238,23 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
|
|
|
187
238
|
`add data assertions (\`... with {{value}}\`, \`see all ... contain {{v}}\`) or, if cross-screen, defer to a flow with @manual + reason.`,
|
|
188
239
|
);
|
|
189
240
|
}
|
|
241
|
+
// H5 — state isolation: a @parallel stateful flow that mutates the cart needs per-scenario
|
|
242
|
+
// isolation, else count/quantity asserts go flaky. Warn (advisory) when no mitigation is present
|
|
243
|
+
// (@cleanup:cart / @isolate / a "Given … empty cart" background / fresh context).
|
|
244
|
+
if (isoRisk) {
|
|
245
|
+
findings.push('ISOLATION-RISK: this @parallel flow mutates the cart but has no per-scenario isolation → cart/count/quantity asserts can go flaky when scenarios share state. Add `@cleanup:cart` (or `@isolate`, or a `Given User has an empty cart` background) so each scenario starts clean.');
|
|
246
|
+
}
|
|
247
|
+
if (flowDepth.stateful && flowDepth.missing.length) {
|
|
248
|
+
const how: Record<string, string> = {
|
|
249
|
+
'count-proof': 'assert the cart ROW COUNT / item QUANTITY (e.g. `... table with {{two_rows}}`, `Quantity column with {{qty}}`), not just the row presence',
|
|
250
|
+
'teardown': 'add a REMOVE/clear scenario that returns the cart to its empty state (the inverse operation)',
|
|
251
|
+
'multi-source': 'add to the cart from EVERY source on the page (the main list AND the recommended/related rail), not just one',
|
|
252
|
+
};
|
|
253
|
+
findings.push(`FLOW-DEPTH: this stateful flow exercises ${3 - flowDepth.missing.length}/3 regression dimensions — missing [${flowDepth.missing.join(', ')}] → ${flowDepth.missing.map((m) => how[m]).join('; ')}. (businessDepth is capped until covered.)`);
|
|
254
|
+
}
|
|
255
|
+
for (const w of oracle.weak) {
|
|
256
|
+
findings.push(`ORACLE-WEAK: "${w.name}" — ${w.hint}`);
|
|
257
|
+
}
|
|
190
258
|
for (const u of claim.unproven) {
|
|
191
259
|
const tag = u.severity === 'fail' ? 'CLAIM-UNPROVEN' : 'CLAIM-WEAK';
|
|
192
260
|
findings.push(`${tag}: "${u.name}" — title claims [${u.claim}] but steps lack ${u.need}. ${u.hint}`);
|
|
@@ -194,7 +262,9 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
|
|
|
194
262
|
for (const m of taxonomy.mislabeled) {
|
|
195
263
|
findings.push(`VP-MISLABEL: "${m.name}" is coded VP-${m.current} but reads as ${m.suggested} (signal: "${m.signal}") → re-tag VP-${m.suggested}-NNN so the coverage matrix isn't skewed.`);
|
|
196
264
|
}
|
|
197
|
-
if (balance.
|
|
265
|
+
if (balance.unclassifiedRatio > 0.4) {
|
|
266
|
+
findings.push(`TAXONOMY-UNCLASSIFIED: ${balance.note} → align the VP-<CATEGORY> codes with the catalog (or extend the bucket keywords) so coverage-balance is meaningful.`);
|
|
267
|
+
} else if (balance.imbalanced) {
|
|
198
268
|
findings.push(`BALANCE: ${balance.note} Stop expanding secondary viewpoints until business-core gaps are filled.`);
|
|
199
269
|
}
|
|
200
270
|
if (trace.mappedRatio < 0.5) {
|
|
@@ -273,7 +343,7 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
|
|
|
273
343
|
...(scenarios.some((s) => s.queryRefs && s.queryRefs.length) ? ['@query'] : []),
|
|
274
344
|
...(scenarios.some((s) => s.apiRefs && s.apiRefs.length) ? ['@api'] : []),
|
|
275
345
|
];
|
|
276
|
-
const routedGateIds = contextRouter.route({ target: { kind: 'screen', id: screenName }, artifact: 'feature', tags: featureTags }).gateSensorIds;
|
|
346
|
+
const routedGateIds = contextRouter.route({ target: { kind: 'screen', id: screenName }, artifact: 'feature', tags: featureTags, platform }).gateSensorIds;
|
|
277
347
|
const gateSensorFindings = capabilityRegistry.sensors('gate')
|
|
278
348
|
.filter((s) => routedGateIds.includes(s.id))
|
|
279
349
|
.flatMap((s) => s.run({ screenName: catalogScreenName, cwd: projectRootFromScreenDir(screenDir), featureText, scenarios, universalGaps: gate.universalGaps }));
|
|
@@ -315,7 +385,7 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
|
|
|
315
385
|
screen: screenName,
|
|
316
386
|
scenarioCount: scenarios.length,
|
|
317
387
|
gate, depth, claim, taxonomy, balance, duplicates, trace, spec,
|
|
318
|
-
taxonomyMismatch, downstream, manualOracle: manualOracleResult, automatableManual: autoManual, ledger, calibration,
|
|
388
|
+
taxonomyMismatch, downstream, manualOracle: manualOracleResult, automatableManual: autoManual, flowDepth, oracle, ledger, calibration,
|
|
319
389
|
score: {
|
|
320
390
|
overall: Math.round(overall * 10) / 10,
|
|
321
391
|
coverage: Math.round(coverage * 100) / 100,
|
|
@@ -56,7 +56,7 @@ export function classifyReason(text: string): string {
|
|
|
56
56
|
return '';
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
interface ParsedScenario { name: string; tags: string[]; manual: boolean; reason: string }
|
|
59
|
+
interface ParsedScenario { name: string; tags: string[]; manual: boolean; reason: string; deferredToFlow: boolean; ownedBy?: string }
|
|
60
60
|
|
|
61
61
|
/** Parse scenarios with their tags + the reason comment line above (for @manual). */
|
|
62
62
|
export function parseScenarios(featurePath: string): ParsedScenario[] {
|
|
@@ -84,7 +84,10 @@ export function parseScenarios(featurePath: string): ParsedScenario[] {
|
|
|
84
84
|
else if (l === '') continue;
|
|
85
85
|
else break; // a real step → stop
|
|
86
86
|
}
|
|
87
|
-
|
|
87
|
+
const deferredToFlow = tags.some((t) => /^@deferred:flow$/i.test(t));
|
|
88
|
+
const ownedBy = (tags.find((t) => /^@owned-by:/i.test(t)) || '').slice('@owned-by:'.length) || undefined;
|
|
89
|
+
// @deferred:flow accounts like @manual on the screen (owned by a flow, not automated here) (H6).
|
|
90
|
+
out.push({ name: m[1].trim(), tags, manual: tags.some((t) => /^@manual\b/i.test(t)) || deferredToFlow, reason, deferredToFlow, ownedBy });
|
|
88
91
|
}
|
|
89
92
|
return out;
|
|
90
93
|
}
|
|
@@ -31,7 +31,7 @@ drivers:
|
|
|
31
31
|
mobile:
|
|
32
32
|
kind: platform
|
|
33
33
|
package: "@sungen/driver-mobile"
|
|
34
|
-
status:
|
|
34
|
+
status: shipped # @sungen/driver-mobile — Appium/WebdriverIO, gesture steps + web-parity gate
|
|
35
35
|
runtime: appium
|
|
36
36
|
adapter: mobile
|
|
37
37
|
capabilities: ["@ui"]
|
|
@@ -74,14 +74,23 @@ export function buildFlowCheck(cwd: string, onlyFlow?: string): FlowCheckReport
|
|
|
74
74
|
const deferrals: Deferral[] = [];
|
|
75
75
|
for (const sc of screens) {
|
|
76
76
|
for (const s of parseScenarios(featurePath(cwd, 'screens', sc))) {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const
|
|
77
|
+
// A deferral is the first-class `@deferred:flow` tag (H6) OR the legacy `@manual` + a
|
|
78
|
+
// "deferred to a flow" comment (back-compat). Either marks a cross-screen case owned by a flow.
|
|
79
|
+
const isDeferral = s.deferredToFlow || (s.manual && /deferred to a flow/i.test(s.reason));
|
|
80
|
+
if (!isDeferral) continue;
|
|
81
|
+
// Targets come from the comment hint; a tag-only @deferred:flow (no comment) falls back to the
|
|
82
|
+
// scenario TITLE so the covering flow scenario can still be located.
|
|
83
|
+
const targets = targetsFromHint([s.reason, s.name].join(' '));
|
|
84
|
+
// `@owned-by:<flow>` names the owner explicitly → only that flow's scenarios can cover it
|
|
85
|
+
// (a false @owned-by is then surfaced as missing). Else any flow may cover it (legacy).
|
|
86
|
+
const pool = s.ownedBy ? flowScenarios.filter((fs2) => fs2.flow === s.ownedBy) : flowScenarios;
|
|
87
|
+
const matches = pool.filter((fs2) => targets.some((t) => fs2.haystack.includes(t)));
|
|
80
88
|
let verdict: Deferral['verdict'] = 'missing';
|
|
81
89
|
let via: string | undefined;
|
|
82
90
|
if (matches.some((m) => m.deep)) { verdict = 'covered'; via = matches.find((m) => m.deep)!.flow; }
|
|
83
91
|
else if (matches.length) { verdict = 'shallow'; via = matches[0].flow; }
|
|
84
|
-
|
|
92
|
+
const hint = s.ownedBy ? `${s.reason || 'deferred to a flow'} (owned-by: ${s.ownedBy})` : s.reason;
|
|
93
|
+
deferrals.push({ screen: sc, scenario: s.name, hint, targets, verdict, via });
|
|
85
94
|
}
|
|
86
95
|
}
|
|
87
96
|
|
package/src/harness/parse.ts
CHANGED
|
@@ -34,6 +34,8 @@ export interface ScenarioInfo {
|
|
|
34
34
|
queryRefs?: string[]; // named queries referenced by this scenario (inline `query [name]` + @query: tags)
|
|
35
35
|
apiRefs?: string[]; // named API endpoints referenced by this scenario (@api: tags)
|
|
36
36
|
requiresCaps?: string[]; // @requires:<cap> — automation-ready but needs an opt-in driver (TQ-11)
|
|
37
|
+
deferredToFlow?: boolean; // @deferred:flow — owned by a flow, not automated on this screen (H6)
|
|
38
|
+
ownedByFlow?: string; // @owned-by:<flow> — the flow that owns this deferred scenario (H6)
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
/** Format-tolerant: is this token an ID (project's scheme), not a prose word?
|
|
@@ -101,7 +103,10 @@ const PRIORITY_TAGS: Record<string, Priority> = { '@high': 'high', '@normal': 'n
|
|
|
101
103
|
|
|
102
104
|
function classifyScenario(sc: ParsedScenario): ScenarioInfo {
|
|
103
105
|
const tags = sc.tags || [];
|
|
104
|
-
const
|
|
106
|
+
const deferredToFlow = tags.includes('@deferred:flow');
|
|
107
|
+
const ownedByFlow = (tags.find((t: string) => /^@owned-by:/i.test(t)) || '').slice('@owned-by:'.length) || undefined;
|
|
108
|
+
// @deferred:flow is owned by a flow → not automated on this screen, so it accounts like @manual (H6).
|
|
109
|
+
const manual = tags.includes('@manual') || deferredToFlow;
|
|
105
110
|
const casesTag = tags.find((t) => t.startsWith('@cases:'));
|
|
106
111
|
const casesDataset = casesTag ? casesTag.slice('@cases:'.length).trim() : undefined;
|
|
107
112
|
// Named-query references: @query:<name>[(overrides)] tags + inline `query [name]` step refs.
|
|
@@ -118,7 +123,10 @@ function classifyScenario(sc: ParsedScenario): ScenarioInfo {
|
|
|
118
123
|
let priority: Priority = 'unknown';
|
|
119
124
|
for (const t of tags) if (PRIORITY_TAGS[t]) priority = PRIORITY_TAGS[t];
|
|
120
125
|
|
|
121
|
-
|
|
126
|
+
// Category is everything between `VP-` and the final `-<sequence>` — INCLUDING hyphens, so
|
|
127
|
+
// compound categories (VP-LIST-DISPLAY-01, VP-ADD-TO-CART-03, VP-PRODUCT-DISCOVERY-02) parse,
|
|
128
|
+
// not just single-word ones. A single-word category (VP-CART-001) still works. (H1)
|
|
129
|
+
const codeMatch = sc.name.match(/\bVP-([A-Z]+(?:-[A-Z]+)*)-\d+/i);
|
|
122
130
|
const vpCode = codeMatch ? codeMatch[0].toUpperCase() : undefined;
|
|
123
131
|
const category = codeMatch ? codeMatch[1].toUpperCase() : undefined;
|
|
124
132
|
// Project-scheme ID: the leading token of the title (VP0-001 / MS-HP-001 / VP-LIST-001).
|
|
@@ -173,6 +181,8 @@ function classifyScenario(sc: ParsedScenario): ScenarioInfo {
|
|
|
173
181
|
queryRefs: queryRefs.size ? [...queryRefs] : undefined,
|
|
174
182
|
apiRefs: apiRefs.size ? [...apiRefs] : undefined,
|
|
175
183
|
requiresCaps: requiresCaps.length ? requiresCaps : undefined,
|
|
184
|
+
deferredToFlow: deferredToFlow || undefined,
|
|
185
|
+
ownedByFlow,
|
|
176
186
|
};
|
|
177
187
|
}
|
|
178
188
|
|
|
@@ -20,7 +20,10 @@ function downstreamRoutes(specText: string): string[] {
|
|
|
20
20
|
const routes = new Set<string>();
|
|
21
21
|
for (const line of specText.split('\n')) {
|
|
22
22
|
if (!/success|navigat|to \(|→/i.test(line)) continue;
|
|
23
|
-
|
|
23
|
+
// A real route's leading `/` sits at a path boundary (start, whitespace, backtick, quote, paren),
|
|
24
|
+
// NOT after a letter/digit. The lookbehind rejects prose slashes like "text/icon" or
|
|
25
|
+
// "category/brand" that aren't routes at all (H2 — they produced /icon, /button, /brand).
|
|
26
|
+
for (const m of line.matchAll(/(?<![A-Za-z0-9])(\/[a-z][a-z0-9/_-]+)`?/gi)) {
|
|
24
27
|
const r = m[1];
|
|
25
28
|
if (r !== ownRoute && r.split('/').length > ownRoute.split('/').length - 0) routes.add(r);
|
|
26
29
|
}
|
|
@@ -29,6 +32,16 @@ function downstreamRoutes(specText: string): string[] {
|
|
|
29
32
|
return [...routes].filter((r) => r !== ownRoute && (!ownRoute || r.startsWith(ownRoute + '/') || r.split('/').length >= 3));
|
|
30
33
|
}
|
|
31
34
|
|
|
35
|
+
/**
|
|
36
|
+
* H5 — a @parallel stateful (cart-mutating) flow with NO per-scenario isolation is flaky: scenarios
|
|
37
|
+
* share state, so cart count/quantity asserts race. Mitigations: @cleanup:cart, @isolate, a fresh
|
|
38
|
+
* browser context, or a "Given … empty cart" background. Returns true when the risk is unmitigated.
|
|
39
|
+
*/
|
|
40
|
+
export function isolationRisk(featureText: string, stateful: boolean): boolean {
|
|
41
|
+
if (!stateful || !/@parallel\b/i.test(featureText)) return false;
|
|
42
|
+
return !/@cleanup:cart\b|@isolate\b|empty cart|fresh (?:browser )?context|new context/i.test(featureText);
|
|
43
|
+
}
|
|
44
|
+
|
|
32
45
|
export function downstreamScope(specText: string, scenarios: ScenarioInfo[]): DownstreamResult {
|
|
33
46
|
const routes = downstreamRoutes(specText);
|
|
34
47
|
const underCovered: { route: string; slug: string }[] = [];
|