execbro 0.0.1-reserve → 1.10.13
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/LICENSE +32 -0
- package/README.md +496 -0
- package/build/__tests__/helpers/fake-cdp-server.d.ts +56 -0
- package/build/__tests__/helpers/fake-cdp-server.d.ts.map +1 -0
- package/build/__tests__/helpers/fake-cdp-server.js +120 -0
- package/build/__tests__/helpers/fake-cdp-server.js.map +1 -0
- package/build/__tests__/helpers/tap-test-helpers.d.ts +36 -0
- package/build/__tests__/helpers/tap-test-helpers.d.ts.map +1 -0
- package/build/__tests__/helpers/tap-test-helpers.js +130 -0
- package/build/__tests__/helpers/tap-test-helpers.js.map +1 -0
- package/build/__tests__/integration/auto-reconnect.test.d.ts +2 -0
- package/build/__tests__/integration/auto-reconnect.test.d.ts.map +1 -0
- package/build/__tests__/integration/auto-reconnect.test.js +118 -0
- package/build/__tests__/integration/auto-reconnect.test.js.map +1 -0
- package/build/__tests__/integration/connection-health.test.d.ts +2 -0
- package/build/__tests__/integration/connection-health.test.d.ts.map +1 -0
- package/build/__tests__/integration/connection-health.test.js +157 -0
- package/build/__tests__/integration/connection-health.test.js.map +1 -0
- package/build/__tests__/integration/execute-in-app.test.d.ts +2 -0
- package/build/__tests__/integration/execute-in-app.test.d.ts.map +1 -0
- package/build/__tests__/integration/execute-in-app.test.js +124 -0
- package/build/__tests__/integration/execute-in-app.test.js.map +1 -0
- package/build/__tests__/integration/per-call-timeout.test.d.ts +2 -0
- package/build/__tests__/integration/per-call-timeout.test.d.ts.map +1 -0
- package/build/__tests__/integration/per-call-timeout.test.js +103 -0
- package/build/__tests__/integration/per-call-timeout.test.js.map +1 -0
- package/build/__tests__/integration/tap-strategies.test.d.ts +2 -0
- package/build/__tests__/integration/tap-strategies.test.d.ts.map +1 -0
- package/build/__tests__/integration/tap-strategies.test.js +330 -0
- package/build/__tests__/integration/tap-strategies.test.js.map +1 -0
- package/build/__tests__/integration/tools.test.d.ts +2 -0
- package/build/__tests__/integration/tools.test.d.ts.map +1 -0
- package/build/__tests__/integration/tools.test.js +227 -0
- package/build/__tests__/integration/tools.test.js.map +1 -0
- package/build/__tests__/setup.d.ts +2 -0
- package/build/__tests__/setup.d.ts.map +1 -0
- package/build/__tests__/setup.js +15 -0
- package/build/__tests__/setup.js.map +1 -0
- package/build/__tests__/unit/androidEmulatorAvds.test.d.ts +2 -0
- package/build/__tests__/unit/androidEmulatorAvds.test.d.ts.map +1 -0
- package/build/__tests__/unit/androidEmulatorAvds.test.js +98 -0
- package/build/__tests__/unit/androidEmulatorAvds.test.js.map +1 -0
- package/build/__tests__/unit/buildToken.test.d.ts +2 -0
- package/build/__tests__/unit/buildToken.test.d.ts.map +1 -0
- package/build/__tests__/unit/buildToken.test.js +22 -0
- package/build/__tests__/unit/buildToken.test.js.map +1 -0
- package/build/__tests__/unit/bundle.test.d.ts +2 -0
- package/build/__tests__/unit/bundle.test.d.ts.map +1 -0
- package/build/__tests__/unit/bundle.test.js +53 -0
- package/build/__tests__/unit/bundle.test.js.map +1 -0
- package/build/__tests__/unit/burst.test.d.ts +2 -0
- package/build/__tests__/unit/burst.test.d.ts.map +1 -0
- package/build/__tests__/unit/burst.test.js +54 -0
- package/build/__tests__/unit/burst.test.js.map +1 -0
- package/build/__tests__/unit/connection-health.test.d.ts +2 -0
- package/build/__tests__/unit/connection-health.test.d.ts.map +1 -0
- package/build/__tests__/unit/connection-health.test.js +38 -0
- package/build/__tests__/unit/connection-health.test.js.map +1 -0
- package/build/__tests__/unit/debugGlobals.test.d.ts +2 -0
- package/build/__tests__/unit/debugGlobals.test.d.ts.map +1 -0
- package/build/__tests__/unit/debugGlobals.test.js +14 -0
- package/build/__tests__/unit/debugGlobals.test.js.map +1 -0
- package/build/__tests__/unit/deviceDiscovery.test.d.ts +2 -0
- package/build/__tests__/unit/deviceDiscovery.test.d.ts.map +1 -0
- package/build/__tests__/unit/deviceDiscovery.test.js +119 -0
- package/build/__tests__/unit/deviceDiscovery.test.js.map +1 -0
- package/build/__tests__/unit/deviceResolver.test.d.ts +2 -0
- package/build/__tests__/unit/deviceResolver.test.d.ts.map +1 -0
- package/build/__tests__/unit/deviceResolver.test.js +261 -0
- package/build/__tests__/unit/deviceResolver.test.js.map +1 -0
- package/build/__tests__/unit/escapeNonAscii.test.d.ts +2 -0
- package/build/__tests__/unit/escapeNonAscii.test.d.ts.map +1 -0
- package/build/__tests__/unit/escapeNonAscii.test.js +76 -0
- package/build/__tests__/unit/escapeNonAscii.test.js.map +1 -0
- package/build/__tests__/unit/exec.test.d.ts +2 -0
- package/build/__tests__/unit/exec.test.d.ts.map +1 -0
- package/build/__tests__/unit/exec.test.js +54 -0
- package/build/__tests__/unit/exec.test.js.map +1 -0
- package/build/__tests__/unit/executor.test.d.ts +2 -0
- package/build/__tests__/unit/executor.test.d.ts.map +1 -0
- package/build/__tests__/unit/executor.test.js +220 -0
- package/build/__tests__/unit/executor.test.js.map +1 -0
- package/build/__tests__/unit/failureArtifact.test.d.ts +2 -0
- package/build/__tests__/unit/failureArtifact.test.d.ts.map +1 -0
- package/build/__tests__/unit/failureArtifact.test.js +207 -0
- package/build/__tests__/unit/failureArtifact.test.js.map +1 -0
- package/build/__tests__/unit/fastRefreshRecorder.test.d.ts +2 -0
- package/build/__tests__/unit/fastRefreshRecorder.test.d.ts.map +1 -0
- package/build/__tests__/unit/fastRefreshRecorder.test.js +68 -0
- package/build/__tests__/unit/fastRefreshRecorder.test.js.map +1 -0
- package/build/__tests__/unit/fastRefreshTools.test.d.ts +2 -0
- package/build/__tests__/unit/fastRefreshTools.test.d.ts.map +1 -0
- package/build/__tests__/unit/fastRefreshTools.test.js +112 -0
- package/build/__tests__/unit/fastRefreshTools.test.js.map +1 -0
- package/build/__tests__/unit/fiberMatch.test.d.ts +2 -0
- package/build/__tests__/unit/fiberMatch.test.d.ts.map +1 -0
- package/build/__tests__/unit/fiberMatch.test.js +114 -0
- package/build/__tests__/unit/fiberMatch.test.js.map +1 -0
- package/build/__tests__/unit/fingerprint.test.d.ts +2 -0
- package/build/__tests__/unit/fingerprint.test.d.ts.map +1 -0
- package/build/__tests__/unit/fingerprint.test.js +99 -0
- package/build/__tests__/unit/fingerprint.test.js.map +1 -0
- package/build/__tests__/unit/focusedInput.test.d.ts +2 -0
- package/build/__tests__/unit/focusedInput.test.d.ts.map +1 -0
- package/build/__tests__/unit/focusedInput.test.js +58 -0
- package/build/__tests__/unit/focusedInput.test.js.map +1 -0
- package/build/__tests__/unit/focusedInputTools.test.d.ts +2 -0
- package/build/__tests__/unit/focusedInputTools.test.d.ts.map +1 -0
- package/build/__tests__/unit/focusedInputTools.test.js +79 -0
- package/build/__tests__/unit/focusedInputTools.test.js.map +1 -0
- package/build/__tests__/unit/iconSemantics.test.d.ts +2 -0
- package/build/__tests__/unit/iconSemantics.test.d.ts.map +1 -0
- package/build/__tests__/unit/iconSemantics.test.js +80 -0
- package/build/__tests__/unit/iconSemantics.test.js.map +1 -0
- package/build/__tests__/unit/imageBuffer.test.d.ts +2 -0
- package/build/__tests__/unit/imageBuffer.test.d.ts.map +1 -0
- package/build/__tests__/unit/imageBuffer.test.js +128 -0
- package/build/__tests__/unit/imageBuffer.test.js.map +1 -0
- package/build/__tests__/unit/logs.test.d.ts +2 -0
- package/build/__tests__/unit/logs.test.d.ts.map +1 -0
- package/build/__tests__/unit/logs.test.js +90 -0
- package/build/__tests__/unit/logs.test.js.map +1 -0
- package/build/__tests__/unit/measureComponent.test.d.ts +2 -0
- package/build/__tests__/unit/measureComponent.test.d.ts.map +1 -0
- package/build/__tests__/unit/measureComponent.test.js +66 -0
- package/build/__tests__/unit/measureComponent.test.js.map +1 -0
- package/build/__tests__/unit/measureComponentTools.test.d.ts +2 -0
- package/build/__tests__/unit/measureComponentTools.test.d.ts.map +1 -0
- package/build/__tests__/unit/measureComponentTools.test.js +94 -0
- package/build/__tests__/unit/measureComponentTools.test.js.map +1 -0
- package/build/__tests__/unit/metro.test.d.ts +2 -0
- package/build/__tests__/unit/metro.test.d.ts.map +1 -0
- package/build/__tests__/unit/metro.test.js +61 -0
- package/build/__tests__/unit/metro.test.js.map +1 -0
- package/build/__tests__/unit/network.test.d.ts +2 -0
- package/build/__tests__/unit/network.test.d.ts.map +1 -0
- package/build/__tests__/unit/network.test.js +102 -0
- package/build/__tests__/unit/network.test.js.map +1 -0
- package/build/__tests__/unit/networkInterceptor.test.d.ts +2 -0
- package/build/__tests__/unit/networkInterceptor.test.d.ts.map +1 -0
- package/build/__tests__/unit/networkInterceptor.test.js +104 -0
- package/build/__tests__/unit/networkInterceptor.test.js.map +1 -0
- package/build/__tests__/unit/probe.test.d.ts +2 -0
- package/build/__tests__/unit/probe.test.d.ts.map +1 -0
- package/build/__tests__/unit/probe.test.js +62 -0
- package/build/__tests__/unit/probe.test.js.map +1 -0
- package/build/__tests__/unit/rnGlobalsBootstrap.test.d.ts +2 -0
- package/build/__tests__/unit/rnGlobalsBootstrap.test.d.ts.map +1 -0
- package/build/__tests__/unit/rnGlobalsBootstrap.test.js +29 -0
- package/build/__tests__/unit/rnGlobalsBootstrap.test.js.map +1 -0
- package/build/__tests__/unit/screenState.test.d.ts +2 -0
- package/build/__tests__/unit/screenState.test.d.ts.map +1 -0
- package/build/__tests__/unit/screenState.test.js +384 -0
- package/build/__tests__/unit/screenState.test.js.map +1 -0
- package/build/__tests__/unit/screenshot-diff.test.d.ts +2 -0
- package/build/__tests__/unit/screenshot-diff.test.d.ts.map +1 -0
- package/build/__tests__/unit/screenshot-diff.test.js +97 -0
- package/build/__tests__/unit/screenshot-diff.test.js.map +1 -0
- package/build/__tests__/unit/tap-evidence.test.d.ts +2 -0
- package/build/__tests__/unit/tap-evidence.test.d.ts.map +1 -0
- package/build/__tests__/unit/tap-evidence.test.js +89 -0
- package/build/__tests__/unit/tap-evidence.test.js.map +1 -0
- package/build/__tests__/unit/tap.test.d.ts +2 -0
- package/build/__tests__/unit/tap.test.d.ts.map +1 -0
- package/build/__tests__/unit/tap.test.js +767 -0
- package/build/__tests__/unit/tap.test.js.map +1 -0
- package/build/__tests__/unit/timeoutClamp.test.d.ts +2 -0
- package/build/__tests__/unit/timeoutClamp.test.d.ts.map +1 -0
- package/build/__tests__/unit/timeoutClamp.test.js +17 -0
- package/build/__tests__/unit/timeoutClamp.test.js.map +1 -0
- package/build/__tests__/unit/toolDescriptions.test.d.ts +2 -0
- package/build/__tests__/unit/toolDescriptions.test.d.ts.map +1 -0
- package/build/__tests__/unit/toolDescriptions.test.js +145 -0
- package/build/__tests__/unit/toolDescriptions.test.js.map +1 -0
- package/build/__tests__/unit/transportClassify.test.d.ts +2 -0
- package/build/__tests__/unit/transportClassify.test.d.ts.map +1 -0
- package/build/__tests__/unit/transportClassify.test.js +60 -0
- package/build/__tests__/unit/transportClassify.test.js.map +1 -0
- package/build/__tests__/unit/verifyAction.test.d.ts +2 -0
- package/build/__tests__/unit/verifyAction.test.d.ts.map +1 -0
- package/build/__tests__/unit/verifyAction.test.js +26 -0
- package/build/__tests__/unit/verifyAction.test.js.map +1 -0
- package/build/core/android.d.ts +299 -0
- package/build/core/android.d.ts.map +1 -0
- package/build/core/android.js +1582 -0
- package/build/core/android.js.map +1 -0
- package/build/core/appDetection.d.ts +20 -0
- package/build/core/appDetection.d.ts.map +1 -0
- package/build/core/appDetection.js +167 -0
- package/build/core/appDetection.js.map +1 -0
- package/build/core/bundle.d.ts +49 -0
- package/build/core/bundle.d.ts.map +1 -0
- package/build/core/bundle.js +356 -0
- package/build/core/bundle.js.map +1 -0
- package/build/core/componentSearch.d.ts +41 -0
- package/build/core/componentSearch.d.ts.map +1 -0
- package/build/core/componentSearch.js +588 -0
- package/build/core/componentSearch.js.map +1 -0
- package/build/core/componentTree.d.ts +13 -0
- package/build/core/componentTree.d.ts.map +1 -0
- package/build/core/componentTree.js +275 -0
- package/build/core/componentTree.js.map +1 -0
- package/build/core/config.d.ts +9 -0
- package/build/core/config.d.ts.map +1 -0
- package/build/core/config.js +28 -0
- package/build/core/config.js.map +1 -0
- package/build/core/connection.d.ts +102 -0
- package/build/core/connection.d.ts.map +1 -0
- package/build/core/connection.js +1710 -0
- package/build/core/connection.js.map +1 -0
- package/build/core/connectionState.d.ts +122 -0
- package/build/core/connectionState.d.ts.map +1 -0
- package/build/core/connectionState.js +313 -0
- package/build/core/connectionState.js.map +1 -0
- package/build/core/debugGlobals.d.ts +6 -0
- package/build/core/debugGlobals.d.ts.map +1 -0
- package/build/core/debugGlobals.js +370 -0
- package/build/core/debugGlobals.js.map +1 -0
- package/build/core/deviceDiscovery.d.ts +52 -0
- package/build/core/deviceDiscovery.d.ts.map +1 -0
- package/build/core/deviceDiscovery.js +74 -0
- package/build/core/deviceDiscovery.js.map +1 -0
- package/build/core/deviceResolver.d.ts +44 -0
- package/build/core/deviceResolver.d.ts.map +1 -0
- package/build/core/deviceResolver.js +225 -0
- package/build/core/deviceResolver.js.map +1 -0
- package/build/core/errorScreenParser.d.ts +30 -0
- package/build/core/errorScreenParser.d.ts.map +1 -0
- package/build/core/errorScreenParser.js +198 -0
- package/build/core/errorScreenParser.js.map +1 -0
- package/build/core/errors.d.ts +14 -0
- package/build/core/errors.d.ts.map +1 -0
- package/build/core/errors.js +17 -0
- package/build/core/errors.js.map +1 -0
- package/build/core/escapeNonAscii.d.ts +18 -0
- package/build/core/escapeNonAscii.d.ts.map +1 -0
- package/build/core/escapeNonAscii.js +352 -0
- package/build/core/escapeNonAscii.js.map +1 -0
- package/build/core/exec.d.ts +22 -0
- package/build/core/exec.d.ts.map +1 -0
- package/build/core/exec.js +54 -0
- package/build/core/exec.js.map +1 -0
- package/build/core/executor.d.ts +9 -0
- package/build/core/executor.d.ts.map +1 -0
- package/build/core/executor.js +12 -0
- package/build/core/executor.js.map +1 -0
- package/build/core/failureArtifact.d.ts +120 -0
- package/build/core/failureArtifact.d.ts.map +1 -0
- package/build/core/failureArtifact.js +113 -0
- package/build/core/failureArtifact.js.map +1 -0
- package/build/core/fastRefreshRecorder.d.ts +23 -0
- package/build/core/fastRefreshRecorder.d.ts.map +1 -0
- package/build/core/fastRefreshRecorder.js +83 -0
- package/build/core/fastRefreshRecorder.js.map +1 -0
- package/build/core/fastRefreshTools.d.ts +19 -0
- package/build/core/fastRefreshTools.d.ts.map +1 -0
- package/build/core/fastRefreshTools.js +71 -0
- package/build/core/fastRefreshTools.js.map +1 -0
- package/build/core/feedback.d.ts +18 -0
- package/build/core/feedback.d.ts.map +1 -0
- package/build/core/feedback.js +73 -0
- package/build/core/feedback.js.map +1 -0
- package/build/core/fiberMatch.d.ts +25 -0
- package/build/core/fiberMatch.d.ts.map +1 -0
- package/build/core/fiberMatch.js +35 -0
- package/build/core/fiberMatch.js.map +1 -0
- package/build/core/fingerprint.d.ts +4 -0
- package/build/core/fingerprint.d.ts.map +1 -0
- package/build/core/fingerprint.js +47 -0
- package/build/core/fingerprint.js.map +1 -0
- package/build/core/focusedInput.d.ts +27 -0
- package/build/core/focusedInput.d.ts.map +1 -0
- package/build/core/focusedInput.js +110 -0
- package/build/core/focusedInput.js.map +1 -0
- package/build/core/focusedInputTools.d.ts +21 -0
- package/build/core/focusedInputTools.d.ts.map +1 -0
- package/build/core/focusedInputTools.js +51 -0
- package/build/core/focusedInputTools.js.map +1 -0
- package/build/core/format.d.ts +8 -0
- package/build/core/format.d.ts.map +1 -0
- package/build/core/format.js +34 -0
- package/build/core/format.js.map +1 -0
- package/build/core/guides.d.ts +21 -0
- package/build/core/guides.d.ts.map +1 -0
- package/build/core/guides.js +397 -0
- package/build/core/guides.js.map +1 -0
- package/build/core/iconSemantics.d.ts +18 -0
- package/build/core/iconSemantics.d.ts.map +1 -0
- package/build/core/iconSemantics.js +114 -0
- package/build/core/iconSemantics.js.map +1 -0
- package/build/core/imageBuffer.d.ts +37 -0
- package/build/core/imageBuffer.d.ts.map +1 -0
- package/build/core/imageBuffer.js +58 -0
- package/build/core/imageBuffer.js.map +1 -0
- package/build/core/index.d.ts +37 -0
- package/build/core/index.d.ts.map +1 -0
- package/build/core/index.js +66 -0
- package/build/core/index.js.map +1 -0
- package/build/core/inspector.d.ts +55 -0
- package/build/core/inspector.d.ts.map +1 -0
- package/build/core/inspector.js +603 -0
- package/build/core/inspector.js.map +1 -0
- package/build/core/ios.d.ts +263 -0
- package/build/core/ios.d.ts.map +1 -0
- package/build/core/ios.js +1705 -0
- package/build/core/ios.js.map +1 -0
- package/build/core/jsExecute.d.ts +62 -0
- package/build/core/jsExecute.d.ts.map +1 -0
- package/build/core/jsExecute.js +696 -0
- package/build/core/jsExecute.js.map +1 -0
- package/build/core/license.d.ts +48 -0
- package/build/core/license.d.ts.map +1 -0
- package/build/core/license.js +267 -0
- package/build/core/license.js.map +1 -0
- package/build/core/logbox.d.ts +58 -0
- package/build/core/logbox.d.ts.map +1 -0
- package/build/core/logbox.js +330 -0
- package/build/core/logbox.js.map +1 -0
- package/build/core/logs.d.ts +44 -0
- package/build/core/logs.d.ts.map +1 -0
- package/build/core/logs.js +149 -0
- package/build/core/logs.js.map +1 -0
- package/build/core/measureComponent.d.ts +19 -0
- package/build/core/measureComponent.d.ts.map +1 -0
- package/build/core/measureComponent.js +94 -0
- package/build/core/measureComponent.js.map +1 -0
- package/build/core/measureComponentTools.d.ts +6 -0
- package/build/core/measureComponentTools.d.ts.map +1 -0
- package/build/core/measureComponentTools.js +43 -0
- package/build/core/measureComponentTools.js.map +1 -0
- package/build/core/metro.d.ts +31 -0
- package/build/core/metro.d.ts.map +1 -0
- package/build/core/metro.js +124 -0
- package/build/core/metro.js.map +1 -0
- package/build/core/nativeOnlyHints.d.ts +14 -0
- package/build/core/nativeOnlyHints.d.ts.map +1 -0
- package/build/core/nativeOnlyHints.js +58 -0
- package/build/core/nativeOnlyHints.js.map +1 -0
- package/build/core/network.d.ts +43 -0
- package/build/core/network.d.ts.map +1 -0
- package/build/core/network.js +217 -0
- package/build/core/network.js.map +1 -0
- package/build/core/networkInterceptor.d.ts +30 -0
- package/build/core/networkInterceptor.d.ts.map +1 -0
- package/build/core/networkInterceptor.js +211 -0
- package/build/core/networkInterceptor.js.map +1 -0
- package/build/core/ocr.d.ts +87 -0
- package/build/core/ocr.d.ts.map +1 -0
- package/build/core/ocr.js +335 -0
- package/build/core/ocr.js.map +1 -0
- package/build/core/paths.d.ts +2 -0
- package/build/core/paths.d.ts.map +1 -0
- package/build/core/paths.js +28 -0
- package/build/core/paths.js.map +1 -0
- package/build/core/posthog.d.ts +5 -0
- package/build/core/posthog.d.ts.map +1 -0
- package/build/core/posthog.js +52 -0
- package/build/core/posthog.js.map +1 -0
- package/build/core/pressables.d.ts +46 -0
- package/build/core/pressables.d.ts.map +1 -0
- package/build/core/pressables.js +1719 -0
- package/build/core/pressables.js.map +1 -0
- package/build/core/probe.d.ts +12 -0
- package/build/core/probe.d.ts.map +1 -0
- package/build/core/probe.js +56 -0
- package/build/core/probe.js.map +1 -0
- package/build/core/redux.d.ts +22 -0
- package/build/core/redux.d.ts.map +1 -0
- package/build/core/redux.js +116 -0
- package/build/core/redux.js.map +1 -0
- package/build/core/register.d.ts +8 -0
- package/build/core/register.d.ts.map +1 -0
- package/build/core/register.js +202 -0
- package/build/core/register.js.map +1 -0
- package/build/core/rnGlobalsBootstrap.d.ts +16 -0
- package/build/core/rnGlobalsBootstrap.d.ts.map +1 -0
- package/build/core/rnGlobalsBootstrap.js +116 -0
- package/build/core/rnGlobalsBootstrap.js.map +1 -0
- package/build/core/screenLayout.d.ts +53 -0
- package/build/core/screenLayout.d.ts.map +1 -0
- package/build/core/screenLayout.js +878 -0
- package/build/core/screenLayout.js.map +1 -0
- package/build/core/screenState.d.ts +166 -0
- package/build/core/screenState.d.ts.map +1 -0
- package/build/core/screenState.js +1352 -0
- package/build/core/screenState.js.map +1 -0
- package/build/core/sdkBridge.d.ts +67 -0
- package/build/core/sdkBridge.d.ts.map +1 -0
- package/build/core/sdkBridge.js +148 -0
- package/build/core/sdkBridge.js.map +1 -0
- package/build/core/state.d.ts +37 -0
- package/build/core/state.d.ts.map +1 -0
- package/build/core/state.js +124 -0
- package/build/core/state.js.map +1 -0
- package/build/core/telemetry.d.ts +50 -0
- package/build/core/telemetry.d.ts.map +1 -0
- package/build/core/telemetry.js +653 -0
- package/build/core/telemetry.js.map +1 -0
- package/build/core/toolHelpers.d.ts +24 -0
- package/build/core/toolHelpers.d.ts.map +1 -0
- package/build/core/toolHelpers.js +118 -0
- package/build/core/toolHelpers.js.map +1 -0
- package/build/core/types.d.ts +162 -0
- package/build/core/types.d.ts.map +1 -0
- package/build/core/types.js +2 -0
- package/build/core/types.js.map +1 -0
- package/build/index.d.ts +4 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +132 -0
- package/build/index.js.map +1 -0
- package/build/pro/screenshot-diff.d.ts +10 -0
- package/build/pro/screenshot-diff.d.ts.map +1 -0
- package/build/pro/screenshot-diff.js +38 -0
- package/build/pro/screenshot-diff.js.map +1 -0
- package/build/pro/tap.d.ts +291 -0
- package/build/pro/tap.d.ts.map +1 -0
- package/build/pro/tap.js +1839 -0
- package/build/pro/tap.js.map +1 -0
- package/build/pro/verifyAction.d.ts +50 -0
- package/build/pro/verifyAction.d.ts.map +1 -0
- package/build/pro/verifyAction.js +285 -0
- package/build/pro/verifyAction.js.map +1 -0
- package/build/tools/_deviceArg.d.ts +45 -0
- package/build/tools/_deviceArg.d.ts.map +1 -0
- package/build/tools/_deviceArg.js +58 -0
- package/build/tools/_deviceArg.js.map +1 -0
- package/build/tools/accountTools.d.ts +46 -0
- package/build/tools/accountTools.d.ts.map +1 -0
- package/build/tools/accountTools.js +223 -0
- package/build/tools/accountTools.js.map +1 -0
- package/build/tools/bundleTools.d.ts +3 -0
- package/build/tools/bundleTools.d.ts.map +1 -0
- package/build/tools/bundleTools.js +375 -0
- package/build/tools/bundleTools.js.map +1 -0
- package/build/tools/componentTools.d.ts +3 -0
- package/build/tools/componentTools.d.ts.map +1 -0
- package/build/tools/componentTools.js +746 -0
- package/build/tools/componentTools.js.map +1 -0
- package/build/tools/connectionTools.d.ts +3 -0
- package/build/tools/connectionTools.d.ts.map +1 -0
- package/build/tools/connectionTools.js +625 -0
- package/build/tools/connectionTools.js.map +1 -0
- package/build/tools/deviceTools.d.ts +3 -0
- package/build/tools/deviceTools.d.ts.map +1 -0
- package/build/tools/deviceTools.js +423 -0
- package/build/tools/deviceTools.js.map +1 -0
- package/build/tools/executionTools.d.ts +3 -0
- package/build/tools/executionTools.d.ts.map +1 -0
- package/build/tools/executionTools.js +243 -0
- package/build/tools/executionTools.js.map +1 -0
- package/build/tools/interactionTools.d.ts +3 -0
- package/build/tools/interactionTools.d.ts.map +1 -0
- package/build/tools/interactionTools.js +539 -0
- package/build/tools/interactionTools.js.map +1 -0
- package/build/tools/logTools.d.ts +3 -0
- package/build/tools/logTools.d.ts.map +1 -0
- package/build/tools/logTools.js +371 -0
- package/build/tools/logTools.js.map +1 -0
- package/build/tools/metaTools.d.ts +7 -0
- package/build/tools/metaTools.d.ts.map +1 -0
- package/build/tools/metaTools.js +186 -0
- package/build/tools/metaTools.js.map +1 -0
- package/build/tools/networkTools.d.ts +3 -0
- package/build/tools/networkTools.d.ts.map +1 -0
- package/build/tools/networkTools.js +478 -0
- package/build/tools/networkTools.js.map +1 -0
- package/build/tools/reduxTools.d.ts +3 -0
- package/build/tools/reduxTools.d.ts.map +1 -0
- package/build/tools/reduxTools.js +83 -0
- package/build/tools/reduxTools.js.map +1 -0
- package/build/tools/screenshotTools.d.ts +3 -0
- package/build/tools/screenshotTools.d.ts.map +1 -0
- package/build/tools/screenshotTools.js +703 -0
- package/build/tools/screenshotTools.js.map +1 -0
- package/package.json +63 -2
|
@@ -0,0 +1,1582 @@
|
|
|
1
|
+
import { existsSync } from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import os from "os";
|
|
4
|
+
import sharp from "sharp";
|
|
5
|
+
import { notifyDriverMissing } from "./logbox.js";
|
|
6
|
+
import { execAsync } from "./exec.js";
|
|
7
|
+
// XML parsing for uiautomator dump
|
|
8
|
+
import { XMLParser } from "fast-xml-parser";
|
|
9
|
+
// ADB command timeout in milliseconds
|
|
10
|
+
const ADB_TIMEOUT = 30000;
|
|
11
|
+
// Module-level cache for ADB availability — checked once per session.
|
|
12
|
+
// `null` = unchecked, boolean = decided. Reset only via `resetAdbAvailabilityCache()` (tests).
|
|
13
|
+
let adbAvailableCache = null;
|
|
14
|
+
let adbPathFixApplied = false;
|
|
15
|
+
/**
|
|
16
|
+
* Standard locations where the Android SDK platform-tools may live when adb is
|
|
17
|
+
* installed but not on the spawned process's PATH (very common on macOS where
|
|
18
|
+
* users add platform-tools only to ~/.zshrc, which GUI-spawned processes don't read).
|
|
19
|
+
*/
|
|
20
|
+
function candidateAdbDirs() {
|
|
21
|
+
const dirs = [];
|
|
22
|
+
const env = process.env;
|
|
23
|
+
if (env.ANDROID_HOME)
|
|
24
|
+
dirs.push(path.join(env.ANDROID_HOME, "platform-tools"));
|
|
25
|
+
if (env.ANDROID_SDK_ROOT)
|
|
26
|
+
dirs.push(path.join(env.ANDROID_SDK_ROOT, "platform-tools"));
|
|
27
|
+
const home = os.homedir();
|
|
28
|
+
if (process.platform === "darwin") {
|
|
29
|
+
dirs.push(path.join(home, "Library", "Android", "sdk", "platform-tools"));
|
|
30
|
+
}
|
|
31
|
+
else if (process.platform === "win32") {
|
|
32
|
+
if (env.LOCALAPPDATA)
|
|
33
|
+
dirs.push(path.join(env.LOCALAPPDATA, "Android", "Sdk", "platform-tools"));
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
dirs.push(path.join(home, "Android", "Sdk", "platform-tools"));
|
|
37
|
+
}
|
|
38
|
+
return dirs;
|
|
39
|
+
}
|
|
40
|
+
function adbBinaryName() {
|
|
41
|
+
return process.platform === "win32" ? "adb.exe" : "adb";
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Check if ADB is reachable. Caches the result for the lifetime of the process.
|
|
45
|
+
*
|
|
46
|
+
* Resolution order:
|
|
47
|
+
* 1. `adb version` against the inherited PATH.
|
|
48
|
+
* 2. Standard SDK install paths (ANDROID_HOME, ~/Library/Android/sdk/..., etc).
|
|
49
|
+
* If found, prepend the directory to process.env.PATH so subsequent bare
|
|
50
|
+
* `adb` invocations work without any call-site changes.
|
|
51
|
+
*
|
|
52
|
+
* The recovery path covers the common case of macOS users with Android Studio
|
|
53
|
+
* installed but adb missing from PATH because ~/.zshrc isn't sourced by GUI-spawned
|
|
54
|
+
* processes (Claude Code, VS Code MCP, etc).
|
|
55
|
+
*/
|
|
56
|
+
export async function isAdbAvailable() {
|
|
57
|
+
if (adbAvailableCache !== null)
|
|
58
|
+
return adbAvailableCache;
|
|
59
|
+
try {
|
|
60
|
+
await execAsync("adb version", { timeout: 5000 });
|
|
61
|
+
adbAvailableCache = true;
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
// Fall through to path discovery below.
|
|
66
|
+
}
|
|
67
|
+
if (!adbPathFixApplied) {
|
|
68
|
+
for (const dir of candidateAdbDirs()) {
|
|
69
|
+
const candidate = path.join(dir, adbBinaryName());
|
|
70
|
+
if (!existsSync(candidate))
|
|
71
|
+
continue;
|
|
72
|
+
const sep = process.platform === "win32" ? ";" : ":";
|
|
73
|
+
process.env.PATH = `${dir}${sep}${process.env.PATH ?? ""}`;
|
|
74
|
+
adbPathFixApplied = true;
|
|
75
|
+
try {
|
|
76
|
+
await execAsync("adb version", { timeout: 5000 });
|
|
77
|
+
adbAvailableCache = true;
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// Found the binary but it didn't run — try next candidate.
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
adbAvailableCache = false;
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Test-only: clear the ADB availability cache so the next call re-probes.
|
|
91
|
+
*/
|
|
92
|
+
export function resetAdbAvailabilityCache() {
|
|
93
|
+
adbAvailableCache = null;
|
|
94
|
+
adbPathFixApplied = false;
|
|
95
|
+
}
|
|
96
|
+
const ADB_MISSING_ERROR = "ADB is not installed or not in PATH. To fix:\n1. Install Android SDK Platform Tools: https://developer.android.com/tools/releases/platform-tools\n2. Add to PATH: export PATH=$PATH:~/Library/Android/sdk/platform-tools\n3. Verify: run 'adb devices' in terminal";
|
|
97
|
+
/**
|
|
98
|
+
* Check ADB availability and push a LogBox notification if missing.
|
|
99
|
+
* Returns an AdbResult error if ADB is unavailable, or null if ready.
|
|
100
|
+
*/
|
|
101
|
+
async function requireAdb() {
|
|
102
|
+
if (await isAdbAvailable())
|
|
103
|
+
return null;
|
|
104
|
+
notifyDriverMissing("android");
|
|
105
|
+
return { success: false, error: ADB_MISSING_ERROR };
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* List connected Android devices
|
|
109
|
+
*/
|
|
110
|
+
export async function listAndroidDevices() {
|
|
111
|
+
try {
|
|
112
|
+
const adbMissing = await requireAdb();
|
|
113
|
+
if (adbMissing)
|
|
114
|
+
return adbMissing;
|
|
115
|
+
const { stdout } = await execAsync("adb devices -l", { timeout: ADB_TIMEOUT });
|
|
116
|
+
const lines = stdout.trim().split("\n");
|
|
117
|
+
// Skip the "List of devices attached" header
|
|
118
|
+
const deviceLines = lines.slice(1).filter((line) => line.trim().length > 0);
|
|
119
|
+
if (deviceLines.length === 0) {
|
|
120
|
+
return {
|
|
121
|
+
success: true,
|
|
122
|
+
result: "No Android devices connected."
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
const devices = deviceLines.map((line) => {
|
|
126
|
+
const parts = line.trim().split(/\s+/);
|
|
127
|
+
const id = parts[0];
|
|
128
|
+
const status = parts[1];
|
|
129
|
+
const device = { id, status };
|
|
130
|
+
// Parse additional info like product:xxx model:xxx device:xxx transport_id:xxx
|
|
131
|
+
for (let i = 2; i < parts.length; i++) {
|
|
132
|
+
const [key, value] = parts[i].split(":");
|
|
133
|
+
if (key === "product")
|
|
134
|
+
device.product = value;
|
|
135
|
+
else if (key === "model")
|
|
136
|
+
device.model = value;
|
|
137
|
+
else if (key === "device")
|
|
138
|
+
device.device = value;
|
|
139
|
+
else if (key === "transport_id")
|
|
140
|
+
device.transportId = value;
|
|
141
|
+
}
|
|
142
|
+
return device;
|
|
143
|
+
});
|
|
144
|
+
const formatted = devices
|
|
145
|
+
.map((d) => {
|
|
146
|
+
let info = `${d.id} (${d.status})`;
|
|
147
|
+
if (d.model)
|
|
148
|
+
info += ` - ${d.model.replace(/_/g, " ")}`;
|
|
149
|
+
if (d.product)
|
|
150
|
+
info += ` [${d.product}]`;
|
|
151
|
+
return info;
|
|
152
|
+
})
|
|
153
|
+
.join("\n");
|
|
154
|
+
return {
|
|
155
|
+
success: true,
|
|
156
|
+
result: `Connected Android devices:\n${formatted}`,
|
|
157
|
+
devices
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
return {
|
|
162
|
+
success: false,
|
|
163
|
+
error: `Failed to list devices: ${error instanceof Error ? error.message : String(error)}`
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Get the first connected Android device ID
|
|
169
|
+
*/
|
|
170
|
+
export async function getDefaultAndroidDevice() {
|
|
171
|
+
try {
|
|
172
|
+
const { stdout } = await execAsync("adb devices", { timeout: ADB_TIMEOUT });
|
|
173
|
+
const lines = stdout.trim().split("\n");
|
|
174
|
+
const deviceLines = lines.slice(1).filter((line) => line.trim().length > 0);
|
|
175
|
+
for (const line of deviceLines) {
|
|
176
|
+
const [id, status] = line.trim().split(/\s+/);
|
|
177
|
+
if (status === "device") {
|
|
178
|
+
return id;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Locate the `emulator` binary. Returns the command string to invoke it
|
|
189
|
+
* (either bare "emulator" if it's on PATH, or a quoted absolute path).
|
|
190
|
+
* Returns null when the binary cannot be located. Mirrors `isAdbAvailable`'s
|
|
191
|
+
* PATH → ANDROID_HOME → common-install-paths discovery order.
|
|
192
|
+
*/
|
|
193
|
+
async function findEmulatorBinary() {
|
|
194
|
+
try {
|
|
195
|
+
await execAsync("emulator -help", { timeout: 3000 });
|
|
196
|
+
return "emulator";
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
// not on PATH — try standard install locations
|
|
200
|
+
}
|
|
201
|
+
const env = process.env;
|
|
202
|
+
const candidates = [];
|
|
203
|
+
if (env.ANDROID_HOME)
|
|
204
|
+
candidates.push(path.join(env.ANDROID_HOME, "emulator", "emulator"));
|
|
205
|
+
if (env.ANDROID_SDK_ROOT)
|
|
206
|
+
candidates.push(path.join(env.ANDROID_SDK_ROOT, "emulator", "emulator"));
|
|
207
|
+
candidates.push(path.join(os.homedir(), "Library", "Android", "sdk", "emulator", "emulator"));
|
|
208
|
+
for (const candidate of candidates) {
|
|
209
|
+
try {
|
|
210
|
+
await execAsync(`"${candidate}" -help`, { timeout: 3000 });
|
|
211
|
+
return `"${candidate}"`;
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
// try next
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* List all Android Virtual Devices defined on the host machine, regardless of
|
|
221
|
+
* whether they are currently running.
|
|
222
|
+
*
|
|
223
|
+
* Returns `[]` when adb is missing, the emulator binary cannot be located, or
|
|
224
|
+
* the listing fails. Callers should treat that as "no inventory available"
|
|
225
|
+
* rather than an error so the surrounding tool can still surface other
|
|
226
|
+
* platforms.
|
|
227
|
+
*/
|
|
228
|
+
export async function getAndroidEmulatorAvds() {
|
|
229
|
+
const adbMissing = await requireAdb();
|
|
230
|
+
if (adbMissing)
|
|
231
|
+
return [];
|
|
232
|
+
const emulatorBin = await findEmulatorBinary();
|
|
233
|
+
if (!emulatorBin)
|
|
234
|
+
return [];
|
|
235
|
+
try {
|
|
236
|
+
const { stdout } = await execAsync(`${emulatorBin} -list-avds`, { timeout: ADB_TIMEOUT });
|
|
237
|
+
return stdout
|
|
238
|
+
.split("\n")
|
|
239
|
+
.map((line) => line.trim())
|
|
240
|
+
.filter((line) => line.length > 0 && !line.includes("Storing crashdata"));
|
|
241
|
+
}
|
|
242
|
+
catch {
|
|
243
|
+
return [];
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Map a running emulator's AVD name to its adb serial (e.g. "emulator-5554").
|
|
248
|
+
*
|
|
249
|
+
* Strategy: list adb devices, filter to `emulator-NNNN` serials, then call
|
|
250
|
+
* `adb -s <serial> emu avd name` per serial until one matches the requested
|
|
251
|
+
* AVD name. Returns `null` if no running emulator matches or if adb is
|
|
252
|
+
* unavailable. This is the slow path for the resolver — callers should prefer
|
|
253
|
+
* the cached registry entry first.
|
|
254
|
+
*/
|
|
255
|
+
export async function getAdbIdForAvd(avdName) {
|
|
256
|
+
const adbMissing = await requireAdb();
|
|
257
|
+
if (adbMissing)
|
|
258
|
+
return null;
|
|
259
|
+
try {
|
|
260
|
+
const { stdout: devicesOut } = await execAsync("adb devices", { timeout: ADB_TIMEOUT });
|
|
261
|
+
const lines = devicesOut.trim().split("\n").slice(1); // skip "List of devices attached" header
|
|
262
|
+
const emulatorSerials = lines
|
|
263
|
+
.map((line) => line.trim().split(/\s+/)[0])
|
|
264
|
+
.filter((id) => /^emulator-\d+$/.test(id));
|
|
265
|
+
const target = avdName.trim();
|
|
266
|
+
for (const serial of emulatorSerials) {
|
|
267
|
+
try {
|
|
268
|
+
const { stdout } = await execAsync(`adb -s ${serial} emu avd name`, {
|
|
269
|
+
timeout: ADB_TIMEOUT
|
|
270
|
+
});
|
|
271
|
+
const reported = stdout.split("\n")[0]?.trim() ?? "";
|
|
272
|
+
if (reported === target)
|
|
273
|
+
return serial;
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
// skip this serial, try next
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
catch {
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Build device selector for ADB command
|
|
287
|
+
*/
|
|
288
|
+
function buildDeviceArg(deviceId) {
|
|
289
|
+
return deviceId ? `-s ${deviceId}` : "";
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Take a screenshot from an Android device
|
|
293
|
+
*/
|
|
294
|
+
export async function androidScreenshot(outputPath, deviceId, signal) {
|
|
295
|
+
try {
|
|
296
|
+
const adbMissing = await requireAdb();
|
|
297
|
+
if (adbMissing)
|
|
298
|
+
return adbMissing;
|
|
299
|
+
const device = deviceId || (await getDefaultAndroidDevice());
|
|
300
|
+
if (!device) {
|
|
301
|
+
return {
|
|
302
|
+
success: false,
|
|
303
|
+
error: "No Android device connected. Connect a device or start an emulator."
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
const deviceArg = buildDeviceArg(device);
|
|
307
|
+
// Generate output path if not provided
|
|
308
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
309
|
+
const finalOutputPath = outputPath || path.join(os.tmpdir(), `android-screenshot-${timestamp}.png`);
|
|
310
|
+
// Capture screenshot on device
|
|
311
|
+
const remotePath = "/sdcard/screenshot-temp.png";
|
|
312
|
+
await execAsync(`adb ${deviceArg} shell screencap -p ${remotePath}`, {
|
|
313
|
+
timeout: ADB_TIMEOUT,
|
|
314
|
+
signal
|
|
315
|
+
});
|
|
316
|
+
// Pull screenshot to local machine
|
|
317
|
+
await execAsync(`adb ${deviceArg} pull ${remotePath} "${finalOutputPath}"`, {
|
|
318
|
+
timeout: ADB_TIMEOUT,
|
|
319
|
+
signal
|
|
320
|
+
});
|
|
321
|
+
// Clean up remote file
|
|
322
|
+
await execAsync(`adb ${deviceArg} shell rm ${remotePath}`, {
|
|
323
|
+
timeout: ADB_TIMEOUT,
|
|
324
|
+
signal
|
|
325
|
+
}).catch(() => {
|
|
326
|
+
// Ignore cleanup errors
|
|
327
|
+
});
|
|
328
|
+
// Resize image if needed (API limit: 2000px max for multi-image requests)
|
|
329
|
+
// Return scale factor so AI can convert image coords to device coords
|
|
330
|
+
const MAX_DIMENSION = 2000;
|
|
331
|
+
const image = sharp(finalOutputPath);
|
|
332
|
+
const metadata = await image.metadata();
|
|
333
|
+
const originalWidth = metadata.width || 0;
|
|
334
|
+
const originalHeight = metadata.height || 0;
|
|
335
|
+
let imageData;
|
|
336
|
+
let scaleFactor = 1;
|
|
337
|
+
if (originalWidth > MAX_DIMENSION || originalHeight > MAX_DIMENSION) {
|
|
338
|
+
// Calculate scale to fit within MAX_DIMENSION
|
|
339
|
+
scaleFactor = Math.max(originalWidth, originalHeight) / MAX_DIMENSION;
|
|
340
|
+
imageData = await image
|
|
341
|
+
.resize(MAX_DIMENSION, MAX_DIMENSION, {
|
|
342
|
+
fit: "inside",
|
|
343
|
+
withoutEnlargement: true
|
|
344
|
+
})
|
|
345
|
+
.jpeg({ quality: 85 })
|
|
346
|
+
.toBuffer();
|
|
347
|
+
}
|
|
348
|
+
else {
|
|
349
|
+
imageData = await image
|
|
350
|
+
.jpeg({ quality: 85 })
|
|
351
|
+
.toBuffer();
|
|
352
|
+
}
|
|
353
|
+
return {
|
|
354
|
+
success: true,
|
|
355
|
+
result: finalOutputPath,
|
|
356
|
+
data: imageData,
|
|
357
|
+
scaleFactor,
|
|
358
|
+
originalWidth,
|
|
359
|
+
originalHeight
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
catch (error) {
|
|
363
|
+
return {
|
|
364
|
+
success: false,
|
|
365
|
+
error: `Failed to capture screenshot: ${error instanceof Error ? error.message : String(error)}`
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Install an APK on an Android device
|
|
371
|
+
*/
|
|
372
|
+
export async function androidInstallApp(apkPath, deviceId, options) {
|
|
373
|
+
try {
|
|
374
|
+
const adbMissing = await requireAdb();
|
|
375
|
+
if (adbMissing)
|
|
376
|
+
return adbMissing;
|
|
377
|
+
// Verify APK exists
|
|
378
|
+
if (!existsSync(apkPath)) {
|
|
379
|
+
return {
|
|
380
|
+
success: false,
|
|
381
|
+
error: `APK file not found: ${apkPath}`
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
const device = deviceId || (await getDefaultAndroidDevice());
|
|
385
|
+
if (!device) {
|
|
386
|
+
return {
|
|
387
|
+
success: false,
|
|
388
|
+
error: "No Android device connected. Connect a device or start an emulator."
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
const deviceArg = buildDeviceArg(device);
|
|
392
|
+
// Build install flags
|
|
393
|
+
const flags = [];
|
|
394
|
+
if (options?.replace)
|
|
395
|
+
flags.push("-r");
|
|
396
|
+
if (options?.grantPermissions)
|
|
397
|
+
flags.push("-g");
|
|
398
|
+
const flagsStr = flags.length > 0 ? flags.join(" ") + " " : "";
|
|
399
|
+
const { stdout, stderr } = await execAsync(`adb ${deviceArg} install ${flagsStr}"${apkPath}"`, { timeout: 120000 } // 2 minute timeout for install
|
|
400
|
+
);
|
|
401
|
+
const output = stdout + stderr;
|
|
402
|
+
if (output.includes("Success")) {
|
|
403
|
+
return {
|
|
404
|
+
success: true,
|
|
405
|
+
result: `Successfully installed ${path.basename(apkPath)}`
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
else {
|
|
409
|
+
return {
|
|
410
|
+
success: false,
|
|
411
|
+
error: output.trim() || "Installation failed with unknown error"
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
catch (error) {
|
|
416
|
+
return {
|
|
417
|
+
success: false,
|
|
418
|
+
error: `Failed to install app: ${error instanceof Error ? error.message : String(error)}`
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Launch an app on an Android device
|
|
424
|
+
*/
|
|
425
|
+
export async function androidLaunchApp(packageName, activityName, deviceId) {
|
|
426
|
+
try {
|
|
427
|
+
const adbMissing = await requireAdb();
|
|
428
|
+
if (adbMissing)
|
|
429
|
+
return adbMissing;
|
|
430
|
+
const device = deviceId || (await getDefaultAndroidDevice());
|
|
431
|
+
if (!device) {
|
|
432
|
+
return {
|
|
433
|
+
success: false,
|
|
434
|
+
error: "No Android device connected. Connect a device or start an emulator."
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
const deviceArg = buildDeviceArg(device);
|
|
438
|
+
let command;
|
|
439
|
+
if (activityName) {
|
|
440
|
+
// Launch specific activity
|
|
441
|
+
command = `adb ${deviceArg} shell am start -n ${packageName}/${activityName}`;
|
|
442
|
+
}
|
|
443
|
+
else {
|
|
444
|
+
// Launch main/launcher activity
|
|
445
|
+
command = `adb ${deviceArg} shell monkey -p ${packageName} -c android.intent.category.LAUNCHER 1`;
|
|
446
|
+
}
|
|
447
|
+
const { stdout, stderr } = await execAsync(command, { timeout: ADB_TIMEOUT });
|
|
448
|
+
const output = stdout + stderr;
|
|
449
|
+
// Check for errors
|
|
450
|
+
if (output.includes("Error") || output.includes("Exception")) {
|
|
451
|
+
return {
|
|
452
|
+
success: false,
|
|
453
|
+
error: output.trim()
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
return {
|
|
457
|
+
success: true,
|
|
458
|
+
result: `Launched ${packageName}${activityName ? `/${activityName}` : ""}`
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
catch (error) {
|
|
462
|
+
return {
|
|
463
|
+
success: false,
|
|
464
|
+
error: `Failed to launch app: ${error instanceof Error ? error.message : String(error)}`
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Get list of installed packages on the device
|
|
470
|
+
*/
|
|
471
|
+
export async function androidListPackages(deviceId, filter) {
|
|
472
|
+
try {
|
|
473
|
+
const adbMissing = await requireAdb();
|
|
474
|
+
if (adbMissing)
|
|
475
|
+
return adbMissing;
|
|
476
|
+
const device = deviceId || (await getDefaultAndroidDevice());
|
|
477
|
+
if (!device) {
|
|
478
|
+
return {
|
|
479
|
+
success: false,
|
|
480
|
+
error: "No Android device connected. Connect a device or start an emulator."
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
const deviceArg = buildDeviceArg(device);
|
|
484
|
+
const { stdout } = await execAsync(`adb ${deviceArg} shell pm list packages`, {
|
|
485
|
+
timeout: ADB_TIMEOUT
|
|
486
|
+
});
|
|
487
|
+
let packages = stdout
|
|
488
|
+
.trim()
|
|
489
|
+
.split("\n")
|
|
490
|
+
.map((line) => line.replace("package:", "").trim())
|
|
491
|
+
.filter((pkg) => pkg.length > 0);
|
|
492
|
+
if (filter) {
|
|
493
|
+
const filterLower = filter.toLowerCase();
|
|
494
|
+
packages = packages.filter((pkg) => pkg.toLowerCase().includes(filterLower));
|
|
495
|
+
}
|
|
496
|
+
if (packages.length === 0) {
|
|
497
|
+
return {
|
|
498
|
+
success: true,
|
|
499
|
+
result: filter ? `No packages found matching "${filter}"` : "No packages found"
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
return {
|
|
503
|
+
success: true,
|
|
504
|
+
result: `Installed packages${filter ? ` matching "${filter}"` : ""}:\n${packages.join("\n")}`
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
catch (error) {
|
|
508
|
+
return {
|
|
509
|
+
success: false,
|
|
510
|
+
error: `Failed to list packages: ${error instanceof Error ? error.message : String(error)}`
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
// ============================================================================
|
|
515
|
+
// UI Input Functions (Phase 2)
|
|
516
|
+
// ============================================================================
|
|
517
|
+
/**
|
|
518
|
+
* Common key event codes for Android
|
|
519
|
+
*/
|
|
520
|
+
export const ANDROID_KEY_EVENTS = {
|
|
521
|
+
HOME: 3,
|
|
522
|
+
BACK: 4,
|
|
523
|
+
CALL: 5,
|
|
524
|
+
END_CALL: 6,
|
|
525
|
+
VOLUME_UP: 24,
|
|
526
|
+
VOLUME_DOWN: 25,
|
|
527
|
+
POWER: 26,
|
|
528
|
+
CAMERA: 27,
|
|
529
|
+
CLEAR: 28,
|
|
530
|
+
TAB: 61,
|
|
531
|
+
ENTER: 66,
|
|
532
|
+
DEL: 67,
|
|
533
|
+
MENU: 82,
|
|
534
|
+
SEARCH: 84,
|
|
535
|
+
MEDIA_PLAY_PAUSE: 85,
|
|
536
|
+
MEDIA_STOP: 86,
|
|
537
|
+
MEDIA_NEXT: 87,
|
|
538
|
+
MEDIA_PREVIOUS: 88,
|
|
539
|
+
MOVE_HOME: 122,
|
|
540
|
+
MOVE_END: 123,
|
|
541
|
+
APP_SWITCH: 187,
|
|
542
|
+
ESCAPE: 111
|
|
543
|
+
};
|
|
544
|
+
/**
|
|
545
|
+
* Tap at coordinates on an Android device
|
|
546
|
+
*/
|
|
547
|
+
export async function androidTap(x, y, deviceId) {
|
|
548
|
+
try {
|
|
549
|
+
const adbMissing = await requireAdb();
|
|
550
|
+
if (adbMissing)
|
|
551
|
+
return adbMissing;
|
|
552
|
+
const device = deviceId || (await getDefaultAndroidDevice());
|
|
553
|
+
if (!device) {
|
|
554
|
+
return {
|
|
555
|
+
success: false,
|
|
556
|
+
error: "No Android device connected. Connect a device or start an emulator."
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
const deviceArg = buildDeviceArg(device);
|
|
560
|
+
await execAsync(`adb ${deviceArg} shell input tap ${Math.round(x)} ${Math.round(y)}`, {
|
|
561
|
+
timeout: ADB_TIMEOUT
|
|
562
|
+
});
|
|
563
|
+
return {
|
|
564
|
+
success: true,
|
|
565
|
+
result: `Tapped at (${Math.round(x)}, ${Math.round(y)})`
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
catch (error) {
|
|
569
|
+
return {
|
|
570
|
+
success: false,
|
|
571
|
+
error: `Failed to tap: ${error instanceof Error ? error.message : String(error)}`
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
/**
|
|
576
|
+
* Long press at coordinates on an Android device
|
|
577
|
+
*/
|
|
578
|
+
export async function androidLongPress(x, y, durationMs = 1000, deviceId) {
|
|
579
|
+
try {
|
|
580
|
+
const adbMissing = await requireAdb();
|
|
581
|
+
if (adbMissing)
|
|
582
|
+
return adbMissing;
|
|
583
|
+
const device = deviceId || (await getDefaultAndroidDevice());
|
|
584
|
+
if (!device) {
|
|
585
|
+
return {
|
|
586
|
+
success: false,
|
|
587
|
+
error: "No Android device connected. Connect a device or start an emulator."
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
const deviceArg = buildDeviceArg(device);
|
|
591
|
+
// Long press is implemented as a swipe from the same point to the same point
|
|
592
|
+
const xRounded = Math.round(x);
|
|
593
|
+
const yRounded = Math.round(y);
|
|
594
|
+
await execAsync(`adb ${deviceArg} shell input swipe ${xRounded} ${yRounded} ${xRounded} ${yRounded} ${durationMs}`, { timeout: ADB_TIMEOUT + durationMs });
|
|
595
|
+
return {
|
|
596
|
+
success: true,
|
|
597
|
+
result: `Long pressed at (${xRounded}, ${yRounded}) for ${durationMs}ms`
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
catch (error) {
|
|
601
|
+
return {
|
|
602
|
+
success: false,
|
|
603
|
+
error: `Failed to long press: ${error instanceof Error ? error.message : String(error)}`
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
/**
|
|
608
|
+
* Swipe on an Android device
|
|
609
|
+
*/
|
|
610
|
+
export async function androidSwipe(startX, startY, endX, endY, durationMs = 300, deviceId) {
|
|
611
|
+
try {
|
|
612
|
+
const adbMissing = await requireAdb();
|
|
613
|
+
if (adbMissing)
|
|
614
|
+
return adbMissing;
|
|
615
|
+
const device = deviceId || (await getDefaultAndroidDevice());
|
|
616
|
+
if (!device) {
|
|
617
|
+
return {
|
|
618
|
+
success: false,
|
|
619
|
+
error: "No Android device connected. Connect a device or start an emulator."
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
const deviceArg = buildDeviceArg(device);
|
|
623
|
+
const x1 = Math.round(startX);
|
|
624
|
+
const y1 = Math.round(startY);
|
|
625
|
+
const x2 = Math.round(endX);
|
|
626
|
+
const y2 = Math.round(endY);
|
|
627
|
+
await execAsync(`adb ${deviceArg} shell input swipe ${x1} ${y1} ${x2} ${y2} ${durationMs}`, { timeout: ADB_TIMEOUT + durationMs });
|
|
628
|
+
return {
|
|
629
|
+
success: true,
|
|
630
|
+
result: `Swiped from (${x1}, ${y1}) to (${x2}, ${y2}) in ${durationMs}ms`
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
catch (error) {
|
|
634
|
+
return {
|
|
635
|
+
success: false,
|
|
636
|
+
error: `Failed to swipe: ${error instanceof Error ? error.message : String(error)}`
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
/**
|
|
641
|
+
* Input text on an Android device
|
|
642
|
+
*
|
|
643
|
+
* ADB input text has limitations with special characters.
|
|
644
|
+
* This function handles escaping properly for URLs, emails, and special strings.
|
|
645
|
+
*/
|
|
646
|
+
export async function androidInputText(text, deviceId) {
|
|
647
|
+
try {
|
|
648
|
+
const adbMissing = await requireAdb();
|
|
649
|
+
if (adbMissing)
|
|
650
|
+
return adbMissing;
|
|
651
|
+
const device = deviceId || (await getDefaultAndroidDevice());
|
|
652
|
+
if (!device) {
|
|
653
|
+
return {
|
|
654
|
+
success: false,
|
|
655
|
+
error: "No Android device connected. Connect a device or start an emulator."
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
const deviceArg = buildDeviceArg(device);
|
|
659
|
+
// For complex strings with special characters, type character by character
|
|
660
|
+
// using key events for reliability
|
|
661
|
+
const hasComplexChars = /[/:?=&#@%+]/.test(text);
|
|
662
|
+
if (hasComplexChars) {
|
|
663
|
+
// Use character-by-character input for strings with special chars
|
|
664
|
+
// This is slower but more reliable for URLs, emails, etc.
|
|
665
|
+
for (const char of text) {
|
|
666
|
+
let keyCmd;
|
|
667
|
+
// Map special characters to their escaped form or use direct input
|
|
668
|
+
switch (char) {
|
|
669
|
+
case " ":
|
|
670
|
+
keyCmd = `adb ${deviceArg} shell input text "%s"`;
|
|
671
|
+
break;
|
|
672
|
+
case "'":
|
|
673
|
+
// Single quote needs special handling
|
|
674
|
+
keyCmd = `adb ${deviceArg} shell input text "\\'"`;
|
|
675
|
+
break;
|
|
676
|
+
case '"':
|
|
677
|
+
keyCmd = `adb ${deviceArg} shell input text '\\"'`;
|
|
678
|
+
break;
|
|
679
|
+
case "\\":
|
|
680
|
+
keyCmd = `adb ${deviceArg} shell input text "\\\\"`;
|
|
681
|
+
break;
|
|
682
|
+
case "&":
|
|
683
|
+
keyCmd = `adb ${deviceArg} shell input text "\\&"`;
|
|
684
|
+
break;
|
|
685
|
+
case "|":
|
|
686
|
+
keyCmd = `adb ${deviceArg} shell input text "\\|"`;
|
|
687
|
+
break;
|
|
688
|
+
case ";":
|
|
689
|
+
keyCmd = `adb ${deviceArg} shell input text "\\;"`;
|
|
690
|
+
break;
|
|
691
|
+
case "<":
|
|
692
|
+
keyCmd = `adb ${deviceArg} shell input text "\\<"`;
|
|
693
|
+
break;
|
|
694
|
+
case ">":
|
|
695
|
+
keyCmd = `adb ${deviceArg} shell input text "\\>"`;
|
|
696
|
+
break;
|
|
697
|
+
case "(":
|
|
698
|
+
keyCmd = `adb ${deviceArg} shell input text "\\("`;
|
|
699
|
+
break;
|
|
700
|
+
case ")":
|
|
701
|
+
keyCmd = `adb ${deviceArg} shell input text "\\)"`;
|
|
702
|
+
break;
|
|
703
|
+
case "$":
|
|
704
|
+
keyCmd = `adb ${deviceArg} shell input text "\\$"`;
|
|
705
|
+
break;
|
|
706
|
+
case "`":
|
|
707
|
+
keyCmd = `adb ${deviceArg} shell input text "\\\`"`;
|
|
708
|
+
break;
|
|
709
|
+
case "#":
|
|
710
|
+
// # is a shell comment character — single quotes don't prevent interpretation
|
|
711
|
+
// on some Android shell versions, so use backslash escaping in double quotes
|
|
712
|
+
keyCmd = `adb ${deviceArg} shell input text "\\#"`;
|
|
713
|
+
break;
|
|
714
|
+
default:
|
|
715
|
+
// For most characters, wrap in single quotes to prevent shell interpretation
|
|
716
|
+
// Single quotes preserve literal meaning of all characters except single quote itself
|
|
717
|
+
keyCmd = `adb ${deviceArg} shell input text '${char}'`;
|
|
718
|
+
}
|
|
719
|
+
await execAsync(keyCmd, { timeout: 5000 });
|
|
720
|
+
}
|
|
721
|
+
return {
|
|
722
|
+
success: true,
|
|
723
|
+
result: `Typed: "${text}"`
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
// For simple alphanumeric strings, use the faster bulk input
|
|
727
|
+
// Escape basic special characters
|
|
728
|
+
const escapedText = text
|
|
729
|
+
.replace(/\\/g, "\\\\")
|
|
730
|
+
.replace(/"/g, '\\"')
|
|
731
|
+
.replace(/'/g, "\\'")
|
|
732
|
+
.replace(/`/g, "\\`")
|
|
733
|
+
.replace(/\$/g, "\\$")
|
|
734
|
+
.replace(/ /g, "%s");
|
|
735
|
+
await execAsync(`adb ${deviceArg} shell input text "${escapedText}"`, {
|
|
736
|
+
timeout: ADB_TIMEOUT
|
|
737
|
+
});
|
|
738
|
+
return {
|
|
739
|
+
success: true,
|
|
740
|
+
result: `Typed: "${text}"`
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
catch (error) {
|
|
744
|
+
return {
|
|
745
|
+
success: false,
|
|
746
|
+
error: `Failed to input text: ${error instanceof Error ? error.message : String(error)}`
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
/**
|
|
751
|
+
* Send a key event to an Android device
|
|
752
|
+
*/
|
|
753
|
+
export async function androidKeyEvent(keyCode, deviceId) {
|
|
754
|
+
try {
|
|
755
|
+
const adbMissing = await requireAdb();
|
|
756
|
+
if (adbMissing)
|
|
757
|
+
return adbMissing;
|
|
758
|
+
const device = deviceId || (await getDefaultAndroidDevice());
|
|
759
|
+
if (!device) {
|
|
760
|
+
return {
|
|
761
|
+
success: false,
|
|
762
|
+
error: "No Android device connected. Connect a device or start an emulator."
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
const deviceArg = buildDeviceArg(device);
|
|
766
|
+
// Resolve key code from name if needed
|
|
767
|
+
const resolvedKeyCode = typeof keyCode === "string" ? ANDROID_KEY_EVENTS[keyCode] : keyCode;
|
|
768
|
+
if (resolvedKeyCode === undefined) {
|
|
769
|
+
return {
|
|
770
|
+
success: false,
|
|
771
|
+
error: `Invalid key code: ${keyCode}`
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
await execAsync(`adb ${deviceArg} shell input keyevent ${resolvedKeyCode}`, {
|
|
775
|
+
timeout: ADB_TIMEOUT
|
|
776
|
+
});
|
|
777
|
+
// Get key name for display
|
|
778
|
+
const keyName = typeof keyCode === "string"
|
|
779
|
+
? keyCode
|
|
780
|
+
: Object.entries(ANDROID_KEY_EVENTS).find(([_, v]) => v === keyCode)?.[0] ||
|
|
781
|
+
`keycode ${keyCode}`;
|
|
782
|
+
return {
|
|
783
|
+
success: true,
|
|
784
|
+
result: `Sent key event: ${keyName}`
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
catch (error) {
|
|
788
|
+
return {
|
|
789
|
+
success: false,
|
|
790
|
+
error: `Failed to send key event: ${error instanceof Error ? error.message : String(error)}`
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
/**
|
|
795
|
+
* Parse bounds string like "[0,0][1080,1920]" to AndroidUIElement bounds
|
|
796
|
+
*/
|
|
797
|
+
function parseBoundsForUIElement(boundsStr) {
|
|
798
|
+
const match = boundsStr.match(/\[(\d+),(\d+)\]\[(\d+),(\d+)\]/);
|
|
799
|
+
if (!match)
|
|
800
|
+
return null;
|
|
801
|
+
const left = parseInt(match[1], 10);
|
|
802
|
+
const top = parseInt(match[2], 10);
|
|
803
|
+
const right = parseInt(match[3], 10);
|
|
804
|
+
const bottom = parseInt(match[4], 10);
|
|
805
|
+
return {
|
|
806
|
+
left,
|
|
807
|
+
top,
|
|
808
|
+
right,
|
|
809
|
+
bottom,
|
|
810
|
+
width: right - left,
|
|
811
|
+
height: bottom - top
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
/**
|
|
815
|
+
* Parse uiautomator XML dump into element array
|
|
816
|
+
*/
|
|
817
|
+
function parseUIAutomatorXML(xml) {
|
|
818
|
+
const elements = [];
|
|
819
|
+
// Match all node elements with their attributes
|
|
820
|
+
const nodeRegex = /<node\s+([^>]+)\/?>|<node\s+([^>]+)>/g;
|
|
821
|
+
let match;
|
|
822
|
+
while ((match = nodeRegex.exec(xml)) !== null) {
|
|
823
|
+
const attrStr = match[1] || match[2];
|
|
824
|
+
if (!attrStr)
|
|
825
|
+
continue;
|
|
826
|
+
// Extract attributes
|
|
827
|
+
const getAttr = (name) => {
|
|
828
|
+
const attrMatch = attrStr.match(new RegExp(`${name}="([^"]*)"`));
|
|
829
|
+
return attrMatch ? attrMatch[1] : "";
|
|
830
|
+
};
|
|
831
|
+
const boundsStr = getAttr("bounds");
|
|
832
|
+
const bounds = parseBoundsForUIElement(boundsStr);
|
|
833
|
+
if (!bounds)
|
|
834
|
+
continue;
|
|
835
|
+
const element = {
|
|
836
|
+
text: getAttr("text"),
|
|
837
|
+
contentDesc: getAttr("content-desc"),
|
|
838
|
+
resourceId: getAttr("resource-id"),
|
|
839
|
+
className: getAttr("class"),
|
|
840
|
+
bounds,
|
|
841
|
+
center: {
|
|
842
|
+
x: Math.round((bounds.left + bounds.right) / 2),
|
|
843
|
+
y: Math.round((bounds.top + bounds.bottom) / 2)
|
|
844
|
+
},
|
|
845
|
+
clickable: getAttr("clickable") === "true",
|
|
846
|
+
enabled: getAttr("enabled") === "true",
|
|
847
|
+
focused: getAttr("focused") === "true",
|
|
848
|
+
scrollable: getAttr("scrollable") === "true",
|
|
849
|
+
selected: getAttr("selected") === "true"
|
|
850
|
+
};
|
|
851
|
+
elements.push(element);
|
|
852
|
+
}
|
|
853
|
+
return elements;
|
|
854
|
+
}
|
|
855
|
+
/**
|
|
856
|
+
* Match element against find options
|
|
857
|
+
*/
|
|
858
|
+
/**
|
|
859
|
+
* Check if an element's center is within the screen viewport.
|
|
860
|
+
* Filters out off-screen elements in scroll views, pagers, etc.
|
|
861
|
+
*/
|
|
862
|
+
function isAndroidElementInViewport(element, screenWidth, screenHeight) {
|
|
863
|
+
const { center } = element;
|
|
864
|
+
return center.x >= 0 && center.x <= screenWidth && center.y >= 0 && center.y <= screenHeight;
|
|
865
|
+
}
|
|
866
|
+
/**
|
|
867
|
+
* Extract screen dimensions from the uiautomator XML.
|
|
868
|
+
* The root hierarchy node's bounds represent the screen size.
|
|
869
|
+
*/
|
|
870
|
+
function extractAndroidScreenSize(rawXml) {
|
|
871
|
+
// Match the first node's bounds (root element = screen)
|
|
872
|
+
const match = rawXml.match(/<(?:hierarchy|node)[^>]*bounds="\[0,0\]\[(\d+),(\d+)\]"/);
|
|
873
|
+
if (match) {
|
|
874
|
+
return { width: parseInt(match[1], 10), height: parseInt(match[2], 10) };
|
|
875
|
+
}
|
|
876
|
+
return null;
|
|
877
|
+
}
|
|
878
|
+
function matchesElement(element, options) {
|
|
879
|
+
if (options.text !== undefined) {
|
|
880
|
+
if (element.text !== options.text)
|
|
881
|
+
return false;
|
|
882
|
+
}
|
|
883
|
+
if (options.textContains !== undefined) {
|
|
884
|
+
if (!element.text.toLowerCase().includes(options.textContains.toLowerCase()))
|
|
885
|
+
return false;
|
|
886
|
+
}
|
|
887
|
+
if (options.contentDesc !== undefined) {
|
|
888
|
+
if (element.contentDesc !== options.contentDesc)
|
|
889
|
+
return false;
|
|
890
|
+
}
|
|
891
|
+
if (options.contentDescContains !== undefined) {
|
|
892
|
+
if (!element.contentDesc.toLowerCase().includes(options.contentDescContains.toLowerCase()))
|
|
893
|
+
return false;
|
|
894
|
+
}
|
|
895
|
+
if (options.resourceId !== undefined) {
|
|
896
|
+
// Support both full "com.app:id/button" and short "button" forms
|
|
897
|
+
const shortId = element.resourceId.split("/").pop() || "";
|
|
898
|
+
if (element.resourceId !== options.resourceId && shortId !== options.resourceId)
|
|
899
|
+
return false;
|
|
900
|
+
}
|
|
901
|
+
return true;
|
|
902
|
+
}
|
|
903
|
+
/**
|
|
904
|
+
* Get UI accessibility tree from Android device using uiautomator
|
|
905
|
+
*/
|
|
906
|
+
export async function androidGetUITree(deviceId, signal) {
|
|
907
|
+
try {
|
|
908
|
+
const adbMissing = await requireAdb();
|
|
909
|
+
if (adbMissing)
|
|
910
|
+
return adbMissing;
|
|
911
|
+
const device = deviceId || (await getDefaultAndroidDevice());
|
|
912
|
+
if (!device) {
|
|
913
|
+
return {
|
|
914
|
+
success: false,
|
|
915
|
+
error: "No Android device connected. Connect a device or start an emulator."
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
const deviceArg = buildDeviceArg(device);
|
|
919
|
+
// Dump UI hierarchy to device. The `signal` is passed so a strategy-level
|
|
920
|
+
// timeout in tap.ts can SIGKILL the host adb process — the device-side
|
|
921
|
+
// uiautomator dump is independent and finishes silently, but we stop
|
|
922
|
+
// waiting on the ADB channel so the next strategy can use it.
|
|
923
|
+
const remotePath = "/sdcard/ui_dump.xml";
|
|
924
|
+
await execAsync(`adb ${deviceArg} shell uiautomator dump ${remotePath}`, {
|
|
925
|
+
timeout: ADB_TIMEOUT,
|
|
926
|
+
signal
|
|
927
|
+
});
|
|
928
|
+
// Read the XML content
|
|
929
|
+
const { stdout } = await execAsync(`adb ${deviceArg} shell cat ${remotePath}`, {
|
|
930
|
+
timeout: ADB_TIMEOUT,
|
|
931
|
+
signal
|
|
932
|
+
});
|
|
933
|
+
// Clean up remote file
|
|
934
|
+
await execAsync(`adb ${deviceArg} shell rm ${remotePath}`, {
|
|
935
|
+
timeout: ADB_TIMEOUT,
|
|
936
|
+
signal
|
|
937
|
+
}).catch(() => { });
|
|
938
|
+
const elements = parseUIAutomatorXML(stdout);
|
|
939
|
+
return {
|
|
940
|
+
success: true,
|
|
941
|
+
elements,
|
|
942
|
+
rawXml: stdout
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
catch (error) {
|
|
946
|
+
return {
|
|
947
|
+
success: false,
|
|
948
|
+
error: `Failed to get UI tree: ${error instanceof Error ? error.message : String(error)}`
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
/**
|
|
953
|
+
* Find element(s) in the UI tree matching the given criteria
|
|
954
|
+
*/
|
|
955
|
+
export async function androidFindElement(options, deviceId, signal) {
|
|
956
|
+
try {
|
|
957
|
+
// Validate that at least one search criteria is provided
|
|
958
|
+
if (!options.text && !options.textContains && !options.contentDesc &&
|
|
959
|
+
!options.contentDescContains && !options.resourceId) {
|
|
960
|
+
return {
|
|
961
|
+
success: false,
|
|
962
|
+
found: false,
|
|
963
|
+
error: "At least one search criteria (text, textContains, contentDesc, contentDescContains, or resourceId) must be provided"
|
|
964
|
+
};
|
|
965
|
+
}
|
|
966
|
+
const treeResult = await androidGetUITree(deviceId, signal);
|
|
967
|
+
if (!treeResult.success || !treeResult.elements) {
|
|
968
|
+
return {
|
|
969
|
+
success: false,
|
|
970
|
+
found: false,
|
|
971
|
+
error: treeResult.error
|
|
972
|
+
};
|
|
973
|
+
}
|
|
974
|
+
// Filter to viewport-visible elements first, then match criteria.
|
|
975
|
+
// This prevents selecting off-screen duplicates in scroll views / pagers.
|
|
976
|
+
const screenSize = treeResult.rawXml ? extractAndroidScreenSize(treeResult.rawXml) : null;
|
|
977
|
+
const visibleElements = screenSize
|
|
978
|
+
? treeResult.elements.filter(el => isAndroidElementInViewport(el, screenSize.width, screenSize.height))
|
|
979
|
+
: treeResult.elements;
|
|
980
|
+
let matches = visibleElements.filter(el => matchesElement(el, options));
|
|
981
|
+
// If no visible matches, fall back to all elements (element may be partially visible
|
|
982
|
+
// or the screen size extraction failed)
|
|
983
|
+
if (matches.length === 0) {
|
|
984
|
+
matches = treeResult.elements.filter(el => matchesElement(el, options));
|
|
985
|
+
}
|
|
986
|
+
if (matches.length === 0) {
|
|
987
|
+
return {
|
|
988
|
+
success: true,
|
|
989
|
+
found: false,
|
|
990
|
+
matchCount: 0
|
|
991
|
+
};
|
|
992
|
+
}
|
|
993
|
+
// Select the element at the specified index (default 0)
|
|
994
|
+
const index = options.index ?? 0;
|
|
995
|
+
const selectedElement = matches[index];
|
|
996
|
+
if (!selectedElement) {
|
|
997
|
+
return {
|
|
998
|
+
success: true,
|
|
999
|
+
found: false,
|
|
1000
|
+
matchCount: matches.length,
|
|
1001
|
+
error: `Index ${index} out of bounds. Found ${matches.length} matching element(s).`
|
|
1002
|
+
};
|
|
1003
|
+
}
|
|
1004
|
+
return {
|
|
1005
|
+
success: true,
|
|
1006
|
+
found: true,
|
|
1007
|
+
element: selectedElement,
|
|
1008
|
+
allMatches: matches,
|
|
1009
|
+
matchCount: matches.length
|
|
1010
|
+
};
|
|
1011
|
+
}
|
|
1012
|
+
catch (error) {
|
|
1013
|
+
return {
|
|
1014
|
+
success: false,
|
|
1015
|
+
found: false,
|
|
1016
|
+
error: `Failed to find element: ${error instanceof Error ? error.message : String(error)}`
|
|
1017
|
+
};
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
/**
|
|
1021
|
+
* Wait for element to appear on screen with polling
|
|
1022
|
+
*/
|
|
1023
|
+
export async function androidWaitForElement(options, deviceId) {
|
|
1024
|
+
const timeoutMs = options.timeoutMs ?? 10000;
|
|
1025
|
+
const pollIntervalMs = options.pollIntervalMs ?? 500;
|
|
1026
|
+
const startTime = Date.now();
|
|
1027
|
+
// Validate that at least one search criteria is provided
|
|
1028
|
+
if (!options.text && !options.textContains && !options.contentDesc &&
|
|
1029
|
+
!options.contentDescContains && !options.resourceId) {
|
|
1030
|
+
return {
|
|
1031
|
+
success: false,
|
|
1032
|
+
found: false,
|
|
1033
|
+
timedOut: false,
|
|
1034
|
+
error: "At least one search criteria (text, textContains, contentDesc, contentDescContains, or resourceId) must be provided"
|
|
1035
|
+
};
|
|
1036
|
+
}
|
|
1037
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
1038
|
+
const result = await androidFindElement(options, deviceId);
|
|
1039
|
+
if (result.found && result.element) {
|
|
1040
|
+
return {
|
|
1041
|
+
...result,
|
|
1042
|
+
elapsedMs: Date.now() - startTime,
|
|
1043
|
+
timedOut: false
|
|
1044
|
+
};
|
|
1045
|
+
}
|
|
1046
|
+
// If there was an error (not just "not found"), return it
|
|
1047
|
+
if (!result.success) {
|
|
1048
|
+
return {
|
|
1049
|
+
...result,
|
|
1050
|
+
elapsedMs: Date.now() - startTime,
|
|
1051
|
+
timedOut: false
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
1054
|
+
// Wait before next poll
|
|
1055
|
+
await new Promise(resolve => setTimeout(resolve, pollIntervalMs));
|
|
1056
|
+
}
|
|
1057
|
+
return {
|
|
1058
|
+
success: true,
|
|
1059
|
+
found: false,
|
|
1060
|
+
elapsedMs: Date.now() - startTime,
|
|
1061
|
+
timedOut: true,
|
|
1062
|
+
error: `Timed out after ${timeoutMs}ms waiting for element`
|
|
1063
|
+
};
|
|
1064
|
+
}
|
|
1065
|
+
/**
|
|
1066
|
+
* Get device screen size
|
|
1067
|
+
*/
|
|
1068
|
+
export async function androidGetScreenSize(deviceId) {
|
|
1069
|
+
try {
|
|
1070
|
+
const adbMissing = await requireAdb();
|
|
1071
|
+
if (adbMissing)
|
|
1072
|
+
return adbMissing;
|
|
1073
|
+
const device = deviceId || (await getDefaultAndroidDevice());
|
|
1074
|
+
if (!device) {
|
|
1075
|
+
return {
|
|
1076
|
+
success: false,
|
|
1077
|
+
error: "No Android device connected. Connect a device or start an emulator."
|
|
1078
|
+
};
|
|
1079
|
+
}
|
|
1080
|
+
const deviceArg = buildDeviceArg(device);
|
|
1081
|
+
const { stdout } = await execAsync(`adb ${deviceArg} shell wm size`, {
|
|
1082
|
+
timeout: ADB_TIMEOUT
|
|
1083
|
+
});
|
|
1084
|
+
// Parse output like "Physical size: 1080x1920"
|
|
1085
|
+
const match = stdout.match(/(\d+)x(\d+)/);
|
|
1086
|
+
if (match) {
|
|
1087
|
+
return {
|
|
1088
|
+
success: true,
|
|
1089
|
+
width: parseInt(match[1], 10),
|
|
1090
|
+
height: parseInt(match[2], 10)
|
|
1091
|
+
};
|
|
1092
|
+
}
|
|
1093
|
+
return {
|
|
1094
|
+
success: false,
|
|
1095
|
+
error: `Could not parse screen size from: ${stdout.trim()}`
|
|
1096
|
+
};
|
|
1097
|
+
}
|
|
1098
|
+
catch (error) {
|
|
1099
|
+
return {
|
|
1100
|
+
success: false,
|
|
1101
|
+
error: `Failed to get screen size: ${error instanceof Error ? error.message : String(error)}`
|
|
1102
|
+
};
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
/**
|
|
1106
|
+
* Get device display density (dpi)
|
|
1107
|
+
*/
|
|
1108
|
+
export async function androidGetDensity(deviceId) {
|
|
1109
|
+
try {
|
|
1110
|
+
const adbMissing = await requireAdb();
|
|
1111
|
+
if (adbMissing)
|
|
1112
|
+
return adbMissing;
|
|
1113
|
+
const device = deviceId || (await getDefaultAndroidDevice());
|
|
1114
|
+
if (!device) {
|
|
1115
|
+
return {
|
|
1116
|
+
success: false,
|
|
1117
|
+
error: "No Android device connected. Connect a device or start an emulator."
|
|
1118
|
+
};
|
|
1119
|
+
}
|
|
1120
|
+
const deviceArg = buildDeviceArg(device);
|
|
1121
|
+
const { stdout } = await execAsync(`adb ${deviceArg} shell wm density`, {
|
|
1122
|
+
timeout: ADB_TIMEOUT
|
|
1123
|
+
});
|
|
1124
|
+
// Parse output like "Physical density: 440" or "Override density: 440"
|
|
1125
|
+
const match = stdout.match(/density:\s*(\d+)/i);
|
|
1126
|
+
if (match) {
|
|
1127
|
+
return {
|
|
1128
|
+
success: true,
|
|
1129
|
+
density: parseInt(match[1], 10)
|
|
1130
|
+
};
|
|
1131
|
+
}
|
|
1132
|
+
return {
|
|
1133
|
+
success: false,
|
|
1134
|
+
error: `Could not parse density from: ${stdout.trim()}`
|
|
1135
|
+
};
|
|
1136
|
+
}
|
|
1137
|
+
catch (error) {
|
|
1138
|
+
return {
|
|
1139
|
+
success: false,
|
|
1140
|
+
error: `Failed to get density: ${error instanceof Error ? error.message : String(error)}`
|
|
1141
|
+
};
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
/**
|
|
1145
|
+
* Get status bar height in pixels
|
|
1146
|
+
* Android status bar is typically 24dp, but can vary by device/OS version
|
|
1147
|
+
*/
|
|
1148
|
+
export async function androidGetStatusBarHeight(deviceId) {
|
|
1149
|
+
try {
|
|
1150
|
+
// Get density first
|
|
1151
|
+
const densityResult = await androidGetDensity(deviceId);
|
|
1152
|
+
if (!densityResult.success || !densityResult.density) {
|
|
1153
|
+
// Fallback to common estimate
|
|
1154
|
+
return {
|
|
1155
|
+
success: true,
|
|
1156
|
+
heightPixels: 63, // Common for 420dpi devices (24dp * 2.625)
|
|
1157
|
+
heightDp: 24
|
|
1158
|
+
};
|
|
1159
|
+
}
|
|
1160
|
+
const density = densityResult.density;
|
|
1161
|
+
const densityScale = density / 160; // Android baseline is 160dpi
|
|
1162
|
+
// Try to get actual status bar height from resources
|
|
1163
|
+
const deviceArg = buildDeviceArg(deviceId);
|
|
1164
|
+
try {
|
|
1165
|
+
const { stdout } = await execAsync(`adb ${deviceArg} shell "dumpsys window | grep -E 'statusBars|mStatusBarLayer|InsetsSource.*statusBars'"`, { timeout: ADB_TIMEOUT });
|
|
1166
|
+
// Try to parse status bar height from dumpsys output
|
|
1167
|
+
// Look for patterns like "statusBars frame=[0,0][1080,63]"
|
|
1168
|
+
const frameMatch = stdout.match(/statusBars.*frame=\[[\d,]+\]\[(\d+),(\d+)\]/);
|
|
1169
|
+
if (frameMatch) {
|
|
1170
|
+
const heightPixels = parseInt(frameMatch[2], 10);
|
|
1171
|
+
return {
|
|
1172
|
+
success: true,
|
|
1173
|
+
heightPixels,
|
|
1174
|
+
heightDp: Math.round(heightPixels / densityScale)
|
|
1175
|
+
};
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
catch {
|
|
1179
|
+
// Fallback to standard calculation
|
|
1180
|
+
}
|
|
1181
|
+
// Standard status bar height is 24dp on most devices
|
|
1182
|
+
const statusBarDp = 24;
|
|
1183
|
+
const statusBarPixels = Math.round(statusBarDp * densityScale);
|
|
1184
|
+
return {
|
|
1185
|
+
success: true,
|
|
1186
|
+
heightPixels: statusBarPixels,
|
|
1187
|
+
heightDp: statusBarDp
|
|
1188
|
+
};
|
|
1189
|
+
}
|
|
1190
|
+
catch (error) {
|
|
1191
|
+
return {
|
|
1192
|
+
success: false,
|
|
1193
|
+
error: `Failed to get status bar height: ${error instanceof Error ? error.message : String(error)}`
|
|
1194
|
+
};
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
/**
|
|
1198
|
+
* Simplify Android class name for display
|
|
1199
|
+
* android.widget.Button -> Button
|
|
1200
|
+
* android.widget.TextView -> TextView
|
|
1201
|
+
*/
|
|
1202
|
+
function simplifyClassName(className) {
|
|
1203
|
+
if (!className)
|
|
1204
|
+
return "Unknown";
|
|
1205
|
+
const parts = className.split(".");
|
|
1206
|
+
return parts[parts.length - 1];
|
|
1207
|
+
}
|
|
1208
|
+
/**
|
|
1209
|
+
* Parse bounds string "[left,top][right,bottom]" to object
|
|
1210
|
+
*/
|
|
1211
|
+
function parseBounds(boundsStr) {
|
|
1212
|
+
const match = boundsStr?.match(/\[(\d+),(\d+)\]\[(\d+),(\d+)\]/);
|
|
1213
|
+
if (!match)
|
|
1214
|
+
return null;
|
|
1215
|
+
return {
|
|
1216
|
+
left: parseInt(match[1], 10),
|
|
1217
|
+
top: parseInt(match[2], 10),
|
|
1218
|
+
right: parseInt(match[3], 10),
|
|
1219
|
+
bottom: parseInt(match[4], 10)
|
|
1220
|
+
};
|
|
1221
|
+
}
|
|
1222
|
+
/**
|
|
1223
|
+
* Parse a single node from uiautomator XML
|
|
1224
|
+
*/
|
|
1225
|
+
function parseUiNode(node) {
|
|
1226
|
+
const attrs = node["@_bounds"]
|
|
1227
|
+
? node
|
|
1228
|
+
: node.node
|
|
1229
|
+
? (Array.isArray(node.node) ? node.node[0] : node.node)
|
|
1230
|
+
: null;
|
|
1231
|
+
if (!attrs)
|
|
1232
|
+
return null;
|
|
1233
|
+
const boundsStr = attrs["@_bounds"];
|
|
1234
|
+
const bounds = parseBounds(boundsStr);
|
|
1235
|
+
if (!bounds)
|
|
1236
|
+
return null;
|
|
1237
|
+
const width = bounds.right - bounds.left;
|
|
1238
|
+
const height = bounds.bottom - bounds.top;
|
|
1239
|
+
const centerX = Math.round(bounds.left + width / 2);
|
|
1240
|
+
const centerY = Math.round(bounds.top + height / 2);
|
|
1241
|
+
const element = {
|
|
1242
|
+
class: simplifyClassName(attrs["@_class"] || ""),
|
|
1243
|
+
bounds,
|
|
1244
|
+
frame: {
|
|
1245
|
+
x: bounds.left,
|
|
1246
|
+
y: bounds.top,
|
|
1247
|
+
width,
|
|
1248
|
+
height
|
|
1249
|
+
},
|
|
1250
|
+
tap: {
|
|
1251
|
+
x: centerX,
|
|
1252
|
+
y: centerY
|
|
1253
|
+
},
|
|
1254
|
+
children: []
|
|
1255
|
+
};
|
|
1256
|
+
// Add optional attributes
|
|
1257
|
+
if (attrs["@_text"])
|
|
1258
|
+
element.text = attrs["@_text"];
|
|
1259
|
+
if (attrs["@_content-desc"])
|
|
1260
|
+
element.contentDesc = attrs["@_content-desc"];
|
|
1261
|
+
if (attrs["@_resource-id"])
|
|
1262
|
+
element.resourceId = attrs["@_resource-id"];
|
|
1263
|
+
if (attrs["@_checkable"] === "true")
|
|
1264
|
+
element.checkable = true;
|
|
1265
|
+
if (attrs["@_checked"] === "true")
|
|
1266
|
+
element.checked = true;
|
|
1267
|
+
if (attrs["@_clickable"] === "true")
|
|
1268
|
+
element.clickable = true;
|
|
1269
|
+
if (attrs["@_enabled"] === "true")
|
|
1270
|
+
element.enabled = true;
|
|
1271
|
+
if (attrs["@_focusable"] === "true")
|
|
1272
|
+
element.focusable = true;
|
|
1273
|
+
if (attrs["@_focused"] === "true")
|
|
1274
|
+
element.focused = true;
|
|
1275
|
+
if (attrs["@_scrollable"] === "true")
|
|
1276
|
+
element.scrollable = true;
|
|
1277
|
+
if (attrs["@_selected"] === "true")
|
|
1278
|
+
element.selected = true;
|
|
1279
|
+
return element;
|
|
1280
|
+
}
|
|
1281
|
+
/**
|
|
1282
|
+
* Recursively parse UI hierarchy from XML node
|
|
1283
|
+
*/
|
|
1284
|
+
function parseHierarchy(node) {
|
|
1285
|
+
const results = [];
|
|
1286
|
+
// Handle the node itself
|
|
1287
|
+
if (node["@_bounds"]) {
|
|
1288
|
+
const element = parseUiNode(node);
|
|
1289
|
+
if (element) {
|
|
1290
|
+
// Parse children
|
|
1291
|
+
if (node.node) {
|
|
1292
|
+
const children = Array.isArray(node.node) ? node.node : [node.node];
|
|
1293
|
+
for (const child of children) {
|
|
1294
|
+
element.children.push(...parseHierarchy(child));
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
results.push(element);
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
else if (node.node) {
|
|
1301
|
+
// This is a container without bounds (like hierarchy root)
|
|
1302
|
+
const children = Array.isArray(node.node) ? node.node : [node.node];
|
|
1303
|
+
for (const child of children) {
|
|
1304
|
+
results.push(...parseHierarchy(child));
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
return results;
|
|
1308
|
+
}
|
|
1309
|
+
/**
|
|
1310
|
+
* Format accessibility tree for display (similar to iOS format)
|
|
1311
|
+
*/
|
|
1312
|
+
function formatAndroidAccessibilityTree(elements, indent = 0) {
|
|
1313
|
+
const lines = [];
|
|
1314
|
+
const prefix = " ".repeat(indent);
|
|
1315
|
+
for (const element of elements) {
|
|
1316
|
+
const parts = [];
|
|
1317
|
+
// [ClassName] "text" or "content-desc"
|
|
1318
|
+
parts.push(`[${element.class}]`);
|
|
1319
|
+
// Add label (text or content-desc)
|
|
1320
|
+
const label = element.text || element.contentDesc;
|
|
1321
|
+
if (label) {
|
|
1322
|
+
parts.push(`"${label}"`);
|
|
1323
|
+
}
|
|
1324
|
+
// Add frame and tap coordinates
|
|
1325
|
+
const f = element.frame;
|
|
1326
|
+
parts.push(`frame=(${f.x}, ${f.y}, ${f.width}x${f.height}) tap=(${element.tap.x}, ${element.tap.y})`);
|
|
1327
|
+
lines.push(`${prefix}${parts.join(" ")}`);
|
|
1328
|
+
// Recurse into children
|
|
1329
|
+
if (element.children.length > 0) {
|
|
1330
|
+
lines.push(formatAndroidAccessibilityTree(element.children, indent + 1));
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
return lines.join("\n");
|
|
1334
|
+
}
|
|
1335
|
+
/**
|
|
1336
|
+
* Flatten element tree to array for searching
|
|
1337
|
+
*/
|
|
1338
|
+
function flattenElements(elements) {
|
|
1339
|
+
const result = [];
|
|
1340
|
+
for (const element of elements) {
|
|
1341
|
+
result.push(element);
|
|
1342
|
+
if (element.children.length > 0) {
|
|
1343
|
+
result.push(...flattenElements(element.children));
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
return result;
|
|
1347
|
+
}
|
|
1348
|
+
/**
|
|
1349
|
+
* Get the UI hierarchy from the connected Android device using uiautomator dump
|
|
1350
|
+
*/
|
|
1351
|
+
export async function androidDescribeAll(deviceId) {
|
|
1352
|
+
try {
|
|
1353
|
+
const adbMissing = await requireAdb();
|
|
1354
|
+
if (adbMissing)
|
|
1355
|
+
return adbMissing;
|
|
1356
|
+
const device = deviceId || (await getDefaultAndroidDevice());
|
|
1357
|
+
if (!device) {
|
|
1358
|
+
return {
|
|
1359
|
+
success: false,
|
|
1360
|
+
error: "No Android device connected. Connect a device or start an emulator."
|
|
1361
|
+
};
|
|
1362
|
+
}
|
|
1363
|
+
const deviceArg = buildDeviceArg(device);
|
|
1364
|
+
// Use file-based approach (most reliable across devices)
|
|
1365
|
+
// /dev/tty doesn't work on most emulators/devices
|
|
1366
|
+
const remotePath = "/sdcard/ui_dump.xml";
|
|
1367
|
+
await execAsync(`adb ${deviceArg} shell uiautomator dump ${remotePath}`, {
|
|
1368
|
+
timeout: ADB_TIMEOUT
|
|
1369
|
+
});
|
|
1370
|
+
const { stdout } = await execAsync(`adb ${deviceArg} shell cat ${remotePath}`, {
|
|
1371
|
+
timeout: ADB_TIMEOUT,
|
|
1372
|
+
maxBuffer: 10 * 1024 * 1024
|
|
1373
|
+
});
|
|
1374
|
+
const xmlContent = stdout.trim();
|
|
1375
|
+
// Clean up
|
|
1376
|
+
await execAsync(`adb ${deviceArg} shell rm ${remotePath}`, {
|
|
1377
|
+
timeout: 5000
|
|
1378
|
+
}).catch(() => { });
|
|
1379
|
+
if (!xmlContent || !xmlContent.includes("<hierarchy")) {
|
|
1380
|
+
return {
|
|
1381
|
+
success: false,
|
|
1382
|
+
error: "Failed to get UI hierarchy. Make sure the device screen is unlocked and the app is in foreground."
|
|
1383
|
+
};
|
|
1384
|
+
}
|
|
1385
|
+
// Parse XML
|
|
1386
|
+
const parser = new XMLParser({
|
|
1387
|
+
ignoreAttributes: false,
|
|
1388
|
+
attributeNamePrefix: "@_"
|
|
1389
|
+
});
|
|
1390
|
+
const parsed = parser.parse(xmlContent);
|
|
1391
|
+
if (!parsed.hierarchy) {
|
|
1392
|
+
return {
|
|
1393
|
+
success: false,
|
|
1394
|
+
error: "Invalid UI hierarchy XML structure"
|
|
1395
|
+
};
|
|
1396
|
+
}
|
|
1397
|
+
const elements = parseHierarchy(parsed.hierarchy);
|
|
1398
|
+
const formatted = formatAndroidAccessibilityTree(elements);
|
|
1399
|
+
return {
|
|
1400
|
+
success: true,
|
|
1401
|
+
elements,
|
|
1402
|
+
formatted
|
|
1403
|
+
};
|
|
1404
|
+
}
|
|
1405
|
+
catch (error) {
|
|
1406
|
+
return {
|
|
1407
|
+
success: false,
|
|
1408
|
+
error: `Failed to get UI hierarchy: ${error instanceof Error ? error.message : String(error)}`
|
|
1409
|
+
};
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
/**
|
|
1413
|
+
* Get accessibility info for the UI element at specific coordinates
|
|
1414
|
+
*/
|
|
1415
|
+
export async function androidDescribePoint(x, y, deviceId) {
|
|
1416
|
+
try {
|
|
1417
|
+
// First get the full hierarchy
|
|
1418
|
+
const result = await androidDescribeAll(deviceId);
|
|
1419
|
+
if (!result.success || !result.elements) {
|
|
1420
|
+
return result;
|
|
1421
|
+
}
|
|
1422
|
+
// Flatten and find elements containing the point
|
|
1423
|
+
const allElements = flattenElements(result.elements);
|
|
1424
|
+
// Find all elements whose bounds contain the point
|
|
1425
|
+
const matchingElements = allElements.filter((el) => {
|
|
1426
|
+
const b = el.bounds;
|
|
1427
|
+
return x >= b.left && x <= b.right && y >= b.top && y <= b.bottom;
|
|
1428
|
+
});
|
|
1429
|
+
if (matchingElements.length === 0) {
|
|
1430
|
+
return {
|
|
1431
|
+
success: true,
|
|
1432
|
+
formatted: `No element found at (${x}, ${y})`
|
|
1433
|
+
};
|
|
1434
|
+
}
|
|
1435
|
+
// Return the deepest (smallest) element that contains the point
|
|
1436
|
+
// Sort by area (smallest first) to get the most specific element
|
|
1437
|
+
matchingElements.sort((a, b) => {
|
|
1438
|
+
const areaA = a.frame.width * a.frame.height;
|
|
1439
|
+
const areaB = b.frame.width * b.frame.height;
|
|
1440
|
+
return areaA - areaB;
|
|
1441
|
+
});
|
|
1442
|
+
const element = matchingElements[0];
|
|
1443
|
+
// Format detailed output
|
|
1444
|
+
const lines = [];
|
|
1445
|
+
const label = element.text || element.contentDesc;
|
|
1446
|
+
lines.push(`[${element.class}]${label ? ` "${label}"` : ""} frame=(${element.frame.x}, ${element.frame.y}, ${element.frame.width}x${element.frame.height}) tap=(${element.tap.x}, ${element.tap.y})`);
|
|
1447
|
+
if (element.resourceId) {
|
|
1448
|
+
lines.push(` resource-id: ${element.resourceId}`);
|
|
1449
|
+
}
|
|
1450
|
+
if (element.contentDesc && element.text) {
|
|
1451
|
+
// Show content-desc separately if we showed text as label
|
|
1452
|
+
lines.push(` content-desc: ${element.contentDesc}`);
|
|
1453
|
+
}
|
|
1454
|
+
if (element.text && element.contentDesc) {
|
|
1455
|
+
// Show text separately if we showed content-desc as label
|
|
1456
|
+
lines.push(` text: ${element.text}`);
|
|
1457
|
+
}
|
|
1458
|
+
// Show state flags
|
|
1459
|
+
const flags = [];
|
|
1460
|
+
if (element.clickable)
|
|
1461
|
+
flags.push("clickable");
|
|
1462
|
+
if (element.enabled)
|
|
1463
|
+
flags.push("enabled");
|
|
1464
|
+
if (element.focusable)
|
|
1465
|
+
flags.push("focusable");
|
|
1466
|
+
if (element.focused)
|
|
1467
|
+
flags.push("focused");
|
|
1468
|
+
if (element.scrollable)
|
|
1469
|
+
flags.push("scrollable");
|
|
1470
|
+
if (element.selected)
|
|
1471
|
+
flags.push("selected");
|
|
1472
|
+
if (element.checked)
|
|
1473
|
+
flags.push("checked");
|
|
1474
|
+
if (flags.length > 0) {
|
|
1475
|
+
lines.push(` state: ${flags.join(", ")}`);
|
|
1476
|
+
}
|
|
1477
|
+
return {
|
|
1478
|
+
success: true,
|
|
1479
|
+
elements: [element],
|
|
1480
|
+
formatted: lines.join("\n")
|
|
1481
|
+
};
|
|
1482
|
+
}
|
|
1483
|
+
catch (error) {
|
|
1484
|
+
return {
|
|
1485
|
+
success: false,
|
|
1486
|
+
error: `Failed to describe point: ${error instanceof Error ? error.message : String(error)}`
|
|
1487
|
+
};
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
/**
|
|
1491
|
+
* Tap an element by its text, content-description, or resource-id
|
|
1492
|
+
*/
|
|
1493
|
+
export async function androidTapElement(options) {
|
|
1494
|
+
try {
|
|
1495
|
+
const { text, textContains, contentDesc, contentDescContains, resourceId, index = 0, deviceId } = options;
|
|
1496
|
+
// Validate that at least one search criterion is provided
|
|
1497
|
+
if (!text && !textContains && !contentDesc && !contentDescContains && !resourceId) {
|
|
1498
|
+
return {
|
|
1499
|
+
success: false,
|
|
1500
|
+
error: "At least one of text, textContains, contentDesc, contentDescContains, or resourceId must be provided"
|
|
1501
|
+
};
|
|
1502
|
+
}
|
|
1503
|
+
// Get the UI hierarchy
|
|
1504
|
+
const result = await androidDescribeAll(deviceId);
|
|
1505
|
+
if (!result.success || !result.elements) {
|
|
1506
|
+
return {
|
|
1507
|
+
success: false,
|
|
1508
|
+
error: result.error || "Failed to get UI hierarchy"
|
|
1509
|
+
};
|
|
1510
|
+
}
|
|
1511
|
+
// Flatten and search
|
|
1512
|
+
const allElements = flattenElements(result.elements);
|
|
1513
|
+
// Filter elements based on search criteria
|
|
1514
|
+
const matchingElements = allElements.filter((el) => {
|
|
1515
|
+
if (text && el.text !== text)
|
|
1516
|
+
return false;
|
|
1517
|
+
if (textContains && (!el.text || !el.text.toLowerCase().includes(textContains.toLowerCase())))
|
|
1518
|
+
return false;
|
|
1519
|
+
if (contentDesc && el.contentDesc !== contentDesc)
|
|
1520
|
+
return false;
|
|
1521
|
+
if (contentDescContains && (!el.contentDesc || !el.contentDesc.toLowerCase().includes(contentDescContains.toLowerCase())))
|
|
1522
|
+
return false;
|
|
1523
|
+
if (resourceId) {
|
|
1524
|
+
// Support both full resource-id and short form
|
|
1525
|
+
if (!el.resourceId)
|
|
1526
|
+
return false;
|
|
1527
|
+
if (el.resourceId !== resourceId && !el.resourceId.endsWith(`:id/${resourceId}`))
|
|
1528
|
+
return false;
|
|
1529
|
+
}
|
|
1530
|
+
return true;
|
|
1531
|
+
});
|
|
1532
|
+
if (matchingElements.length === 0) {
|
|
1533
|
+
const criteria = [];
|
|
1534
|
+
if (text)
|
|
1535
|
+
criteria.push(`text="${text}"`);
|
|
1536
|
+
if (textContains)
|
|
1537
|
+
criteria.push(`textContains="${textContains}"`);
|
|
1538
|
+
if (contentDesc)
|
|
1539
|
+
criteria.push(`contentDesc="${contentDesc}"`);
|
|
1540
|
+
if (contentDescContains)
|
|
1541
|
+
criteria.push(`contentDescContains="${contentDescContains}"`);
|
|
1542
|
+
if (resourceId)
|
|
1543
|
+
criteria.push(`resourceId="${resourceId}"`);
|
|
1544
|
+
return {
|
|
1545
|
+
success: false,
|
|
1546
|
+
error: `Element not found: ${criteria.join(", ")}`
|
|
1547
|
+
};
|
|
1548
|
+
}
|
|
1549
|
+
if (index >= matchingElements.length) {
|
|
1550
|
+
return {
|
|
1551
|
+
success: false,
|
|
1552
|
+
error: `Index ${index} out of range. Found ${matchingElements.length} matching element(s).`
|
|
1553
|
+
};
|
|
1554
|
+
}
|
|
1555
|
+
const element = matchingElements[index];
|
|
1556
|
+
const label = element.text || element.contentDesc || element.resourceId || element.class;
|
|
1557
|
+
// Log if multiple matches
|
|
1558
|
+
let resultMessage;
|
|
1559
|
+
if (matchingElements.length > 1) {
|
|
1560
|
+
resultMessage = `Found ${matchingElements.length} elements, tapping "${label}" (index ${index}) at (${element.tap.x}, ${element.tap.y})`;
|
|
1561
|
+
}
|
|
1562
|
+
else {
|
|
1563
|
+
resultMessage = `Tapped "${label}" at (${element.tap.x}, ${element.tap.y})`;
|
|
1564
|
+
}
|
|
1565
|
+
// Perform the tap
|
|
1566
|
+
const tapResult = await androidTap(element.tap.x, element.tap.y, deviceId);
|
|
1567
|
+
if (!tapResult.success) {
|
|
1568
|
+
return tapResult;
|
|
1569
|
+
}
|
|
1570
|
+
return {
|
|
1571
|
+
success: true,
|
|
1572
|
+
result: resultMessage
|
|
1573
|
+
};
|
|
1574
|
+
}
|
|
1575
|
+
catch (error) {
|
|
1576
|
+
return {
|
|
1577
|
+
success: false,
|
|
1578
|
+
error: `Failed to tap element: ${error instanceof Error ? error.message : String(error)}`
|
|
1579
|
+
};
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
//# sourceMappingURL=android.js.map
|