@sun-asterisk/sungen 3.2.0-beta.141 → 3.2.0-beta.143
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/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/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 +1 -1
- package/dist/harness/audit.d.ts.map +1 -1
- package/dist/harness/audit.js +14 -4
- package/dist/harness/audit.js.map +1 -1
- package/dist/harness/catalog/drivers.yaml +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/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/step-mapper.ts +8 -5
- package/src/generators/test-generator/template-engine.ts +13 -1
- package/src/harness/audit.ts +12 -4
- package/src/harness/catalog/drivers.yaml +1 -1
- 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());
|
|
@@ -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
|
@@ -81,9 +81,14 @@ function catalogIdFromScreenDir(screenDir: string): string {
|
|
|
81
81
|
* future `mobile/<x>` or `perf/<x>` unit routes to that capability with no core change. `flows/<flow>`
|
|
82
82
|
* has no `flows` capability → default (UI), which is correct (flows are a UI concept).
|
|
83
83
|
*/
|
|
84
|
-
export function scoringCapabilityFor(catalogScreenName: string, defaultCap: string | undefined): string | undefined {
|
|
84
|
+
export function scoringCapabilityFor(catalogScreenName: string, defaultCap: string | undefined, platform?: string): string | undefined {
|
|
85
85
|
const seg = catalogScreenName.split('/')[0];
|
|
86
|
-
|
|
86
|
+
// Path segment wins (api/<area> → api). Then the active platform capability (mobile → mobile),
|
|
87
|
+
// when a driver registered one with that id. Else the default (web/bare screen → ui — unchanged,
|
|
88
|
+
// since `web` has no capability of its own). See docs/spec/sungen-platform-capability-routing-spec.md.
|
|
89
|
+
if (seg && capabilityRegistry.get(seg)) return seg;
|
|
90
|
+
if (platform && capabilityRegistry.get(platform)) return platform;
|
|
91
|
+
return defaultCap;
|
|
87
92
|
}
|
|
88
93
|
|
|
89
94
|
export function runAudit(screenDir: string, screenName: string): AuditReport {
|
|
@@ -106,8 +111,11 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
|
|
|
106
111
|
// UI capability). A capability that provides no catalog/gate falls back to the in-core UI
|
|
107
112
|
// functions, so UI units — and api units until AO-2 adds the api providers — are byte-identical.
|
|
108
113
|
discoverAndRegisterCapabilities();
|
|
114
|
+
// The active platform (web | mobile | …) activates its own capability for scoring + sensor routing.
|
|
115
|
+
// `web` has no capability of its own → scoringCap stays the default `ui` (byte-identical).
|
|
116
|
+
const platform = readCapabilities(projectRootFromScreenDir(screenDir)).platform;
|
|
109
117
|
const defaultCap = capabilityRegistry.defaultCapabilityId();
|
|
110
|
-
const scoringCapId = scoringCapabilityFor(catalogScreenName, defaultCap);
|
|
118
|
+
const scoringCapId = scoringCapabilityFor(catalogScreenName, defaultCap, platform);
|
|
111
119
|
const scoringCap = scoringCapId ? capabilityRegistry.get(scoringCapId) : undefined;
|
|
112
120
|
const catalog = (scoringCap?.viewpoints?.() as Catalog | undefined) || loadCatalog();
|
|
113
121
|
const spec = specCoverage(specPath, scenarios, featureText);
|
|
@@ -273,7 +281,7 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
|
|
|
273
281
|
...(scenarios.some((s) => s.queryRefs && s.queryRefs.length) ? ['@query'] : []),
|
|
274
282
|
...(scenarios.some((s) => s.apiRefs && s.apiRefs.length) ? ['@api'] : []),
|
|
275
283
|
];
|
|
276
|
-
const routedGateIds = contextRouter.route({ target: { kind: 'screen', id: screenName }, artifact: 'feature', tags: featureTags }).gateSensorIds;
|
|
284
|
+
const routedGateIds = contextRouter.route({ target: { kind: 'screen', id: screenName }, artifact: 'feature', tags: featureTags, platform }).gateSensorIds;
|
|
277
285
|
const gateSensorFindings = capabilityRegistry.sensors('gate')
|
|
278
286
|
.filter((s) => routedGateIds.includes(s.id))
|
|
279
287
|
.flatMap((s) => s.run({ screenName: catalogScreenName, cwd: projectRootFromScreenDir(screenDir), featureText, scenarios, universalGaps: gate.universalGaps }));
|
|
@@ -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"]
|
|
@@ -65,6 +65,10 @@ export const AI_RULES_FILE_MAPPING: [string, string][] = [
|
|
|
65
65
|
['claude-skill-capture-mode-live.md', '.claude/skills/sungen-capture/mode-live.md'],
|
|
66
66
|
['claude-skill-capture-mode-local.md', '.claude/skills/sungen-capture/mode-local.md'],
|
|
67
67
|
['claude-skill-locale.md', '.claude/skills/sungen-locale/SKILL.md'],
|
|
68
|
+
// Mobile (Appium) skills — relevant for platform: mobile (MOB-5).
|
|
69
|
+
['claude-skill-mobile-gestures.md', '.claude/skills/sungen-mobile-gestures/SKILL.md'],
|
|
70
|
+
['claude-skill-capture-mobile.md', '.claude/skills/sungen-capture-mobile/SKILL.md'],
|
|
71
|
+
['claude-skill-selector-fix-mobile.md', '.claude/skills/sungen-selector-fix-mobile/SKILL.md'],
|
|
68
72
|
|
|
69
73
|
// Agents — Claude Code sub-agents (isolated context). Copilot runs these inline.
|
|
70
74
|
['claude-agent-reviewer.md', '.claude/agents/sungen-reviewer.md'],
|
|
@@ -96,6 +100,10 @@ export const AI_RULES_FILE_MAPPING: [string, string][] = [
|
|
|
96
100
|
['github-skill-sungen-capture-mode-live.md', '.github/skills/sungen-capture/mode-live.md'],
|
|
97
101
|
['github-skill-sungen-capture-mode-local.md', '.github/skills/sungen-capture/mode-local.md'],
|
|
98
102
|
['github-skill-sungen-locale.md', '.github/skills/sungen-locale/SKILL.md'],
|
|
103
|
+
// Mobile (Appium) skills — relevant for platform: mobile (MOB-5).
|
|
104
|
+
['github-skill-sungen-mobile-gestures.md', '.github/skills/sungen-mobile-gestures/SKILL.md'],
|
|
105
|
+
['github-skill-sungen-capture-mobile.md', '.github/skills/sungen-capture-mobile/SKILL.md'],
|
|
106
|
+
['github-skill-sungen-selector-fix-mobile.md', '.github/skills/sungen-selector-fix-mobile/SKILL.md'],
|
|
99
107
|
];
|
|
100
108
|
|
|
101
109
|
// Skill/asset directories retired in a previous refactor. `sungen update` removes
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: sungen-capture-mobile
|
|
3
|
+
description: 'Capture a live mobile app screen via Appium MCP — locator tree + screenshot for visual context. Auto-loaded by create-test when the screen is @platform:mobile/android/ios.'
|
|
4
|
+
user-invocable: false
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Purpose
|
|
8
|
+
|
|
9
|
+
Launch a mobile app on a device/emulator, capture **one locator tree** (`generate_locators`) and **one screenshot**, and save them as visual context for test generation. The mobile analogue of `sungen-capture-live` — same job, Appium MCP instead of Playwright MCP.
|
|
10
|
+
|
|
11
|
+
Use when the target is a native/Flutter/React-Native app (`@platform:android` or `@platform:ios`) running on a connected device or emulator.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Prerequisites
|
|
16
|
+
|
|
17
|
+
- `appium-mcp` connected (see `/mcp`). It runs Appium **embedded** — no separate server needed.
|
|
18
|
+
- A device/emulator booted: `adb devices` shows one for Android; a Simulator for iOS.
|
|
19
|
+
- The app **installed** on the device (`adb install -r <app>.apk` for Android).
|
|
20
|
+
- The screen's `Path:` (in `<screen>/features/*.feature` or `spec.md`) gives the app entry as
|
|
21
|
+
`<appPackage>/<appActivity>` (Android) or bundleId (iOS), e.g. `com.kngroup.media.converter/.MainActivity`.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Steps
|
|
26
|
+
|
|
27
|
+
### 1. Resolve target app + entry
|
|
28
|
+
|
|
29
|
+
Resolve in this order:
|
|
30
|
+
1. `Path:` line in the feature / `App ID` in `spec.md` → split into `appPackage` + `appActivity`.
|
|
31
|
+
A dual-id Path (`<pkg>/<activity> | <iosBundleId>`) → take the part for the OS you are exploring
|
|
32
|
+
(left of `|` = Android pkg/activity, right = iOS bundle id).
|
|
33
|
+
2. If missing → `AskUserQuestion`: *"What is the appPackage/appActivity (Android) or bundleId (iOS)?"*
|
|
34
|
+
3. **Navigation recipe** — read the feature's `Background:` (the in-app web-path analog). Line 1
|
|
35
|
+
`Given User is on [Home] screen` is the launcher/landing screen the app opens on; each following
|
|
36
|
+
`When/And User tap [...]` (or gesture) is one hop toward the target screen. You will **replay**
|
|
37
|
+
these in step 4.5 so you scan the screen this feature targets — not the launcher. An anchor-only
|
|
38
|
+
Background (no nav steps) ⇒ the target IS the launcher screen ⇒ no navigation needed.
|
|
39
|
+
|
|
40
|
+
### 2. Select device
|
|
41
|
+
|
|
42
|
+
`select_device` with `platform: android` (or `ios`). If exactly one device, it auto-selects.
|
|
43
|
+
If several, list them and ask the user which `deviceUdid`.
|
|
44
|
+
|
|
45
|
+
### 3. Create the session
|
|
46
|
+
|
|
47
|
+
`appium_session_management` `action=create`, `platform=android`, capabilities (JSON string):
|
|
48
|
+
```
|
|
49
|
+
appium:appPackage, appium:appActivity, appium:udid,
|
|
50
|
+
appium:noReset=true, appium:autoGrantPermissions=true, appium:newCommandTimeout=300
|
|
51
|
+
```
|
|
52
|
+
For iOS, call `prepare_ios_simulator` first, then create with `appium:bundleId`.
|
|
53
|
+
|
|
54
|
+
### 4. ⚠️ Wait for the screen to actually render
|
|
55
|
+
|
|
56
|
+
**This is the #1 gotcha for Flutter / RN apps.** Right after launch the app often shows a blank
|
|
57
|
+
splash — and the locator tree will contain **zero usable elements** (just an empty `FrameLayout`).
|
|
58
|
+
Do **not** capture yet. Take a quick `appium_screenshot`; if it's blank/splash, wait and re-check
|
|
59
|
+
until real content is drawn. Only then capture. (A Flutter app on a loading screen looks "invisible"
|
|
60
|
+
to Appium; the same app on a rendered screen exposes its full Semantics tree.)
|
|
61
|
+
|
|
62
|
+
### 4.5. ⚠️ Navigate to the target screen (replay the navigation recipe)
|
|
63
|
+
|
|
64
|
+
**This is what makes capture land on the RIGHT screen** — the mobile equivalent of `page.goto(url)`
|
|
65
|
+
on web. After the launcher screen has rendered, replay the `Background:` nav steps so Appium ends up
|
|
66
|
+
on the screen this feature targets, *then* capture there.
|
|
67
|
+
|
|
68
|
+
For each nav step **after** the `Given User is on [Home] screen` anchor, in order:
|
|
69
|
+
1. `generate_locators` (or `appium_get_page_source`) on the CURRENT screen.
|
|
70
|
+
2. Resolve the step's `[Label]` against the live tree — prefer `accessibility id`, then visible
|
|
71
|
+
`text`/`content-desc`, then an `xpath` on a distinctive substring. Gestures (`scroll to [X]`,
|
|
72
|
+
`swipe …`) follow `sungen-mobile-gestures`.
|
|
73
|
+
3. Perform the tap/gesture, then **wait for the next screen to render** (same blank-splash guard as
|
|
74
|
+
step 4) before the next step.
|
|
75
|
+
|
|
76
|
+
After the LAST step you are on the target screen — capture HERE (steps 5–6).
|
|
77
|
+
|
|
78
|
+
- **Anchor-only Background** (no nav steps) ⇒ the target IS the launcher screen ⇒ skip this step.
|
|
79
|
+
- **A step won't resolve** (its `[Label]` matches no element) ⇒ **STOP. Do not capture the wrong
|
|
80
|
+
screen.** Screenshot where you are and report: *"recipe step N: [Label] not found — fix the
|
|
81
|
+
Background nav path."* Fail loud (run-test would fail at the same step anyway).
|
|
82
|
+
- **No recipe yet** (guided-stub Background, but the target is NOT the launcher) ⇒ drive to the
|
|
83
|
+
screen interactively off the live tree (if unsure which control leads there, `AskUserQuestion`),
|
|
84
|
+
then **report the exact nav steps you took** so create-test/add-screen can write them into the
|
|
85
|
+
Background. Do **not** edit the `.feature` yourself — Gherkin authorship stays with those commands.
|
|
86
|
+
|
|
87
|
+
### 5. Capture the locator tree (primary AI context)
|
|
88
|
+
|
|
89
|
+
Call **`generate_locators`** — this is the mobile equivalent of the web accessibility snapshot. It
|
|
90
|
+
returns priority-ranked locators per interactable element: `accessibility id`, `id`, platform-native
|
|
91
|
+
(`-android uiautomator` / `-ios predicate string`), and `xpath`, plus `content-desc`/`text`,
|
|
92
|
+
`clickable`, `enabled`. This is what `sungen-tc-generation` and `sungen-selector-keys` consume.
|
|
93
|
+
|
|
94
|
+
If the tree is huge, also `appium_get_page_source` (saved to a file) and grep it — never dump the
|
|
95
|
+
full XML inline.
|
|
96
|
+
|
|
97
|
+
### 6. Screenshot
|
|
98
|
+
|
|
99
|
+
`appium_screenshot` → save to:
|
|
100
|
+
```
|
|
101
|
+
qa/screens/<screen>/requirements/ui/mobile-<timestamp>.png
|
|
102
|
+
```
|
|
103
|
+
`<timestamp>` = `YYYYMMDD-HHMM` local (e.g. `mobile-20260605-1645.png`).
|
|
104
|
+
|
|
105
|
+
### 7. Detect discrepancies vs spec
|
|
106
|
+
|
|
107
|
+
If `spec.md` exists, cross-check `generate_locators` labels against spec sections (fields in spec but
|
|
108
|
+
not on screen, and vice-versa). Report; do **not** auto-edit `spec.md`.
|
|
109
|
+
|
|
110
|
+
### 8. End the session
|
|
111
|
+
|
|
112
|
+
`appium_session_management` `action=delete` to free the device. Always clean up.
|
|
113
|
+
|
|
114
|
+
### 9. Report back
|
|
115
|
+
|
|
116
|
+
> Captured mobile screen `<appPackage>`:
|
|
117
|
+
> - Reached via: <N nav steps replayed from the Background recipe, or "launcher screen — no nav">
|
|
118
|
+
> - Locators: <N> interactable elements (M with accessibility-id)
|
|
119
|
+
> - Screenshot: `requirements/ui/mobile-<timestamp>.png`
|
|
120
|
+
> - Discrepancies vs spec: <count, or "none">
|
|
121
|
+
> - Discovered nav steps (only if the recipe was empty): <the taps you took, for create-test/add-screen to write into the Background>
|
|
122
|
+
|
|
123
|
+
Hand back to the calling command.
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## Cross-platform discovery (`@platform:mobile` — explore one OS, layer the rest)
|
|
128
|
+
|
|
129
|
+
When the screen is **`@platform:mobile`** (write-once-run-both), discover the **shared** cases first,
|
|
130
|
+
then peel off only what is genuinely OS-specific. You do **not** need to explore both OSes up front —
|
|
131
|
+
Flutter/RN expose the same Semantics on each, so one exploration covers most of it.
|
|
132
|
+
|
|
133
|
+
1. **Explore whichever OS has a device up** (Android or iOS — symmetric for Flutter). Capture the tree.
|
|
134
|
+
2. **Classify each component** into THREE buckets (not two):
|
|
135
|
+
- **Shared-exact** → a stable `accessibility id` equal on both OSes (Flutter Semantics → same value on
|
|
136
|
+
Android `content-desc` AND iOS `name`) → flat `type: accessibility-id` key in `<screen>.feature`.
|
|
137
|
+
- **Shared-variant** → SAME step/assertion on both OSes but the LOCATOR differs (composite
|
|
138
|
+
`content-desc` vs `-ios predicate`; partial/dynamic text via `descriptionContains` vs predicate
|
|
139
|
+
`CONTAINS`; a different attribute) → STILL `<screen>.feature`, with `android:`/`ios:` variant keys in
|
|
140
|
+
selectors.yaml. **A native locator alone is NOT a reason to split** — default for composite/partial text.
|
|
141
|
+
- **Sub-feature** → ONLY for genuine SCENARIO divergence: an element on ONE OS only, divergent
|
|
142
|
+
steps/flow, an OS-exclusive gesture/permission/dialog flow, or a case needing a per-platform
|
|
143
|
+
`nth`/`scope` (a variant can't diverge those). → `<screen>-android.feature` / `<screen>-ios.feature`.
|
|
144
|
+
3. **Report the classification** so create-test/run-test scaffolds the right thing:
|
|
145
|
+
- `<screen>.feature` `@platform:mobile` ← shared-exact AND shared-variant (the vast majority)
|
|
146
|
+
- `<screen>-android.feature` `@platform:android` ← ONLY scenario-divergent Android cases
|
|
147
|
+
- `<screen>-ios.feature` `@platform:ios` ← ONLY scenario-divergent iOS cases
|
|
148
|
+
4. **Verifying the other OS needs no second exploration** — just running the shared spec there
|
|
149
|
+
(`MOBILE_PLATFORM=both`) IS the check. Re-capture on the 2nd OS only for components that fail, and
|
|
150
|
+
move those into that OS's sub-feature. (Asking devs to add a Semantics label to an unlabeled element
|
|
151
|
+
converts an OS-specific case back into a shared one — prefer that when feasible.)
|
|
152
|
+
|
|
153
|
+
## Selector-quality notes (carry into selectors.yaml)
|
|
154
|
+
|
|
155
|
+
- **Prefer `accessibility id`** (Android `content-desc` / iOS `accessibilityIdentifier`) — most stable.
|
|
156
|
+
- For **Flutter**, the Semantics label surfaces as `content-desc` ⇒ it usually equals the **visible
|
|
157
|
+
text**, so it is **locale-dependent** ("Good afternoon!", "English"). Keep such values in test-data
|
|
158
|
+
and use `{{variable}}` + locale overlays (see `sungen-locale`).
|
|
159
|
+
- **Composite nodes**: cards may concatenate child text into one `content-desc` with `\n` → prefer an
|
|
160
|
+
xpath `contains(@content-desc,'…')` on a distinctive substring.
|
|
161
|
+
- **Duplicate labels** → use `nth`. **Unlabelled controls** (search fields, icon buttons) → fall back
|
|
162
|
+
to `xpath` / class.
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## What this skill does NOT do
|
|
167
|
+
|
|
168
|
+
- Does not run tests or generate `selectors.yaml` (that's `/sungen:run-test` + `sungen-selector-fix-mobile`)
|
|
169
|
+
- Does not generate Gherkin (that's `sungen-tc-generation`) — it may *report* discovered nav steps,
|
|
170
|
+
but writing them into the Background is create-test/add-screen's job
|
|
171
|
+
- Does not handle login/auth state (mobile auth is a separate concern — deep link / test build)
|
|
172
|
+
- Captures **the feature's target screen** per invocation — reached by replaying the Background
|
|
173
|
+
navigation recipe (step 4.5), not just the launcher. For a *different* screen, give it its own
|
|
174
|
+
feature + recipe and re-invoke.
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## Relationship to other capture skills
|
|
179
|
+
|
|
180
|
+
- `sungen-capture-figma` / `sungen-capture-local` — design/image sources (platform-agnostic)
|
|
181
|
+
- `sungen-capture-live` — web (Playwright MCP)
|
|
182
|
+
- `sungen-capture-mobile` — this skill, mobile (Appium MCP)
|
|
183
|
+
|
|
184
|
+
All write to `requirements/ui/` and report back to the caller.
|