execbro 0.0.1-reserve → 1.10.12
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 +186 -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 +769 -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 +98 -0
- package/build/core/screenState.d.ts.map +1 -0
- package/build/core/screenState.js +1121 -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 +295 -0
- package/build/pro/tap.d.ts.map +1 -0
- package/build/pro/tap.js +1937 -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 +745 -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,1705 @@
|
|
|
1
|
+
import { execFile } from "child_process";
|
|
2
|
+
import { promisify } from "util";
|
|
3
|
+
import { existsSync } from "fs";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import os from "os";
|
|
6
|
+
import sharp from "sharp";
|
|
7
|
+
import { getActiveSimulatorUdid } from "./state.js";
|
|
8
|
+
import { notifyDriverMissing } from "./logbox.js";
|
|
9
|
+
import { execAsync } from "./exec.js";
|
|
10
|
+
const execFileAsync = promisify(execFile);
|
|
11
|
+
// simctl command timeout in milliseconds
|
|
12
|
+
const SIMCTL_TIMEOUT = 30000;
|
|
13
|
+
// IDB command timeout in milliseconds
|
|
14
|
+
const IDB_TIMEOUT = 30000;
|
|
15
|
+
// AXe command timeout in milliseconds
|
|
16
|
+
const AXE_TIMEOUT = 30000;
|
|
17
|
+
// Valid button types for IDB ui button command
|
|
18
|
+
export const IOS_BUTTON_TYPES = [
|
|
19
|
+
"HOME",
|
|
20
|
+
"LOCK",
|
|
21
|
+
"SIDE_BUTTON",
|
|
22
|
+
"SIRI",
|
|
23
|
+
"APPLE_PAY",
|
|
24
|
+
];
|
|
25
|
+
// Track connected IDB simulators to avoid redundant connect calls
|
|
26
|
+
const connectedIdbSimulators = new Set();
|
|
27
|
+
/**
|
|
28
|
+
* Get the IDB executable path
|
|
29
|
+
* Supports IDB_PATH environment variable for custom installations
|
|
30
|
+
*/
|
|
31
|
+
function getIdbPath() {
|
|
32
|
+
return process.env.IDB_PATH || "idb";
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Get the active iOS UI driver.
|
|
36
|
+
* Defaults to AXe; set IOS_DRIVER=idb in MCP server env to use IDB instead.
|
|
37
|
+
*/
|
|
38
|
+
function getIosDriver() {
|
|
39
|
+
const driver = process.env.IOS_DRIVER?.toLowerCase();
|
|
40
|
+
if (driver === "idb")
|
|
41
|
+
return "idb";
|
|
42
|
+
return "axe";
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Get the AXe executable path.
|
|
46
|
+
* Supports AXE_PATH environment variable for custom installations.
|
|
47
|
+
*/
|
|
48
|
+
function getAxePath() {
|
|
49
|
+
return process.env.AXE_PATH || "axe";
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Run AXe command with execFile (no shell) for proper argument handling.
|
|
53
|
+
*/
|
|
54
|
+
async function runAxe(...args) {
|
|
55
|
+
const axePath = getAxePath();
|
|
56
|
+
const { stdout, stderr } = await execFileAsync(axePath, args, {
|
|
57
|
+
timeout: AXE_TIMEOUT,
|
|
58
|
+
});
|
|
59
|
+
return {
|
|
60
|
+
stdout: stdout.trim(),
|
|
61
|
+
stderr: stderr.trim(),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Run IDB command with execFile (no shell) for proper argument handling
|
|
66
|
+
* This matches the original ios-simulator-mcp implementation
|
|
67
|
+
*/
|
|
68
|
+
async function runIdb(...args) {
|
|
69
|
+
const idbPath = getIdbPath();
|
|
70
|
+
const { stdout, stderr } = await execFileAsync(idbPath, args, {
|
|
71
|
+
timeout: IDB_TIMEOUT,
|
|
72
|
+
});
|
|
73
|
+
return {
|
|
74
|
+
stdout: stdout.trim(),
|
|
75
|
+
stderr: stderr.trim(),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Check if IDB is available
|
|
80
|
+
*/
|
|
81
|
+
export async function isIdbAvailable() {
|
|
82
|
+
try {
|
|
83
|
+
await runIdb("--help");
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Check if AXe CLI is available
|
|
92
|
+
*/
|
|
93
|
+
export async function isAxeAvailable() {
|
|
94
|
+
try {
|
|
95
|
+
await runAxe("--version");
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Check if the active iOS UI driver is available.
|
|
104
|
+
* Used by tap.ts for early fail-fast checks.
|
|
105
|
+
*/
|
|
106
|
+
export async function isUiDriverAvailable() {
|
|
107
|
+
const driver = getIosDriver();
|
|
108
|
+
return driver === "axe" ? isAxeAvailable() : isIdbAvailable();
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Get install instructions for the active iOS UI driver.
|
|
112
|
+
*/
|
|
113
|
+
export function getUiDriverInstallHint() {
|
|
114
|
+
const driver = getIosDriver();
|
|
115
|
+
if (driver === "axe") {
|
|
116
|
+
return "AXe is not installed. Install with: brew install cameroncooke/axe/axe\nAXe is the recommended iOS UI driver. See: https://github.com/cameroncooke/AXe";
|
|
117
|
+
}
|
|
118
|
+
return "IDB is not installed. Install with: brew install idb-companion\nTip: Consider switching to AXe (recommended) — brew install cameroncooke/axe/axe and set IOS_DRIVER=axe";
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Map IDB button names (uppercase/underscores) to AXe button names (lowercase/hyphens).
|
|
122
|
+
*/
|
|
123
|
+
function toAxeButtonName(idbButton) {
|
|
124
|
+
const map = {
|
|
125
|
+
HOME: "home",
|
|
126
|
+
LOCK: "lock",
|
|
127
|
+
SIDE_BUTTON: "side-button",
|
|
128
|
+
SIRI: "siri",
|
|
129
|
+
APPLE_PAY: "apple-pay",
|
|
130
|
+
};
|
|
131
|
+
return map[idbButton];
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Ensure IDB is connected to the specified simulator
|
|
135
|
+
* IDB requires `idb connect <UDID>` before any UI commands work
|
|
136
|
+
*/
|
|
137
|
+
async function ensureIdbConnected(udid) {
|
|
138
|
+
// Skip if already connected in this session
|
|
139
|
+
if (connectedIdbSimulators.has(udid)) {
|
|
140
|
+
return { success: true };
|
|
141
|
+
}
|
|
142
|
+
try {
|
|
143
|
+
await runIdb("connect", udid);
|
|
144
|
+
connectedIdbSimulators.add(udid);
|
|
145
|
+
return { success: true };
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
149
|
+
// "Already connected" is not an error
|
|
150
|
+
if (errorMessage.includes("already connected") ||
|
|
151
|
+
errorMessage.includes("Connected")) {
|
|
152
|
+
connectedIdbSimulators.add(udid);
|
|
153
|
+
return { success: true };
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
success: false,
|
|
157
|
+
error: `Failed to connect IDB to simulator: ${errorMessage}`,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Shared pre-flight check for all UI driver functions:
|
|
163
|
+
* 1. Check driver availability
|
|
164
|
+
* 2. Resolve target simulator UDID
|
|
165
|
+
* 3. Ensure IDB connection (IDB only)
|
|
166
|
+
*/
|
|
167
|
+
async function ensureUiDriverReady(udid) {
|
|
168
|
+
const driver = getIosDriver();
|
|
169
|
+
if (driver === "axe") {
|
|
170
|
+
if (!(await isAxeAvailable())) {
|
|
171
|
+
notifyDriverMissing("ios");
|
|
172
|
+
return {
|
|
173
|
+
ready: false,
|
|
174
|
+
error: { success: false, error: getUiDriverInstallHint() },
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
if (!(await isIdbAvailable())) {
|
|
180
|
+
notifyDriverMissing("ios");
|
|
181
|
+
return {
|
|
182
|
+
ready: false,
|
|
183
|
+
error: { success: false, error: getUiDriverInstallHint() },
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
const targetUdid = udid || (await getActiveOrBootedSimulatorUdid());
|
|
188
|
+
if (!targetUdid) {
|
|
189
|
+
return {
|
|
190
|
+
ready: false,
|
|
191
|
+
error: {
|
|
192
|
+
success: false,
|
|
193
|
+
error: await buildNoSimulatorError(),
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
if (driver === "idb") {
|
|
198
|
+
const connectResult = await ensureIdbConnected(targetUdid);
|
|
199
|
+
if (!connectResult.success) {
|
|
200
|
+
return {
|
|
201
|
+
ready: false,
|
|
202
|
+
error: { success: false, error: connectResult.error },
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return { ready: true, driver, targetUdid };
|
|
207
|
+
}
|
|
208
|
+
// Shared message for callers that hit a missing-Xcode preflight. Includes
|
|
209
|
+
// the actual install command so the agent (or user) can act without guessing.
|
|
210
|
+
export const XCODE_MISSING_ERROR = "Xcode command line tools are not installed or not selected. " +
|
|
211
|
+
"Run `xcode-select --install` for the CLT, or install full Xcode from the App Store " +
|
|
212
|
+
"and run `sudo xcode-select -s /Applications/Xcode.app/Contents/Developer`. " +
|
|
213
|
+
"simctl ships with full Xcode (the standalone CLT does not include it).";
|
|
214
|
+
// Memoized result — Xcode CLT presence does not change mid-session in
|
|
215
|
+
// practice, and `xcrun simctl help` adds 50–200 ms per ios_* call without
|
|
216
|
+
// caching. Re-resolve once per process; restart the MCP server after install.
|
|
217
|
+
let simctlAvailableCache = null;
|
|
218
|
+
/**
|
|
219
|
+
* Check if simctl is available (cached for the lifetime of the process).
|
|
220
|
+
*/
|
|
221
|
+
export async function isSimctlAvailable() {
|
|
222
|
+
if (simctlAvailableCache)
|
|
223
|
+
return simctlAvailableCache;
|
|
224
|
+
simctlAvailableCache = (async () => {
|
|
225
|
+
try {
|
|
226
|
+
await execAsync("xcrun simctl help", { timeout: 5000 });
|
|
227
|
+
return true;
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
})();
|
|
233
|
+
return simctlAvailableCache;
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Reset the cached simctl availability check. Test-only.
|
|
237
|
+
*/
|
|
238
|
+
export function resetSimctlAvailableCache() {
|
|
239
|
+
simctlAvailableCache = null;
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* List iOS simulators
|
|
243
|
+
*/
|
|
244
|
+
export async function listIOSSimulators(onlyBooted = false) {
|
|
245
|
+
try {
|
|
246
|
+
const simctlAvailable = await isSimctlAvailable();
|
|
247
|
+
if (!simctlAvailable) {
|
|
248
|
+
return {
|
|
249
|
+
success: false,
|
|
250
|
+
error: XCODE_MISSING_ERROR,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
const { stdout } = await execAsync("xcrun simctl list devices -j", {
|
|
254
|
+
timeout: SIMCTL_TIMEOUT,
|
|
255
|
+
});
|
|
256
|
+
const data = JSON.parse(stdout);
|
|
257
|
+
const simulators = [];
|
|
258
|
+
// Parse devices from each runtime
|
|
259
|
+
for (const [runtime, devices] of Object.entries(data.devices)) {
|
|
260
|
+
if (!Array.isArray(devices))
|
|
261
|
+
continue;
|
|
262
|
+
for (const device of devices) {
|
|
263
|
+
if (!device.isAvailable)
|
|
264
|
+
continue;
|
|
265
|
+
if (onlyBooted && device.state !== "Booted")
|
|
266
|
+
continue;
|
|
267
|
+
// Extract iOS version from runtime string
|
|
268
|
+
const runtimeMatch = runtime.match(/iOS[- ](\d+[.-]\d+)/i);
|
|
269
|
+
const runtimeVersion = runtimeMatch
|
|
270
|
+
? `iOS ${runtimeMatch[1].replace("-", ".")}`
|
|
271
|
+
: runtime;
|
|
272
|
+
simulators.push({
|
|
273
|
+
udid: device.udid,
|
|
274
|
+
name: device.name,
|
|
275
|
+
state: device.state,
|
|
276
|
+
runtime: runtimeVersion,
|
|
277
|
+
deviceType: device.deviceTypeIdentifier,
|
|
278
|
+
isAvailable: device.isAvailable,
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
if (simulators.length === 0) {
|
|
283
|
+
return {
|
|
284
|
+
success: true,
|
|
285
|
+
result: onlyBooted
|
|
286
|
+
? "No booted iOS simulators. Start a simulator first."
|
|
287
|
+
: "No available iOS simulators found.",
|
|
288
|
+
simulators: [],
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
// Sort: Booted first, then by name
|
|
292
|
+
simulators.sort((a, b) => {
|
|
293
|
+
if (a.state === "Booted" && b.state !== "Booted")
|
|
294
|
+
return -1;
|
|
295
|
+
if (a.state !== "Booted" && b.state === "Booted")
|
|
296
|
+
return 1;
|
|
297
|
+
return a.name.localeCompare(b.name);
|
|
298
|
+
});
|
|
299
|
+
const formatted = simulators
|
|
300
|
+
.map((s) => {
|
|
301
|
+
const status = s.state === "Booted" ? "🟢 Booted" : "⚪ Shutdown";
|
|
302
|
+
return `${s.name} (${s.runtime}) - ${status}\n UDID: ${s.udid}`;
|
|
303
|
+
})
|
|
304
|
+
.join("\n\n");
|
|
305
|
+
return {
|
|
306
|
+
success: true,
|
|
307
|
+
result: `iOS Simulators:\n\n${formatted}`,
|
|
308
|
+
simulators,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
catch (error) {
|
|
312
|
+
return {
|
|
313
|
+
success: false,
|
|
314
|
+
error: `Failed to list simulators: ${error instanceof Error ? error.message : String(error)}`,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Build the actionable "no booted simulator" error message. Lists up to 6
|
|
320
|
+
* recently-available shutdown simulators by name+UDID and points the agent
|
|
321
|
+
* at `ios_boot_simulator`, so it can recover instead of giving up.
|
|
322
|
+
*/
|
|
323
|
+
export async function buildNoSimulatorError() {
|
|
324
|
+
const base = "No iOS simulator is currently running. " +
|
|
325
|
+
"Boot one with `ios_boot_simulator` (pass udid or name), " +
|
|
326
|
+
"or run `xcrun simctl boot <udid>` directly.";
|
|
327
|
+
try {
|
|
328
|
+
const { stdout } = await execAsync("xcrun simctl list devices -j", {
|
|
329
|
+
timeout: SIMCTL_TIMEOUT,
|
|
330
|
+
});
|
|
331
|
+
const data = JSON.parse(stdout);
|
|
332
|
+
const shutdown = [];
|
|
333
|
+
for (const list of Object.values(data.devices)) {
|
|
334
|
+
if (!Array.isArray(list))
|
|
335
|
+
continue;
|
|
336
|
+
for (const d of list) {
|
|
337
|
+
if (d.isAvailable === false)
|
|
338
|
+
continue;
|
|
339
|
+
if (d.state === "Shutdown")
|
|
340
|
+
shutdown.push({ name: d.name, udid: d.udid });
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
if (shutdown.length === 0)
|
|
344
|
+
return base;
|
|
345
|
+
const top = shutdown.slice(0, 6).map((d) => ` - ${d.name} (${d.udid})`).join("\n");
|
|
346
|
+
const more = shutdown.length > 6 ? `\n ...and ${shutdown.length - 6} more (run list_ios_simulators)` : "";
|
|
347
|
+
return `${base}\n\nAvailable simulators (shutdown):\n${top}${more}`;
|
|
348
|
+
}
|
|
349
|
+
catch {
|
|
350
|
+
return base;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Get the booted simulator UDID
|
|
355
|
+
*/
|
|
356
|
+
export async function getBootedSimulatorUdid() {
|
|
357
|
+
try {
|
|
358
|
+
const { stdout } = await execAsync("xcrun simctl list devices booted -j", {
|
|
359
|
+
timeout: SIMCTL_TIMEOUT,
|
|
360
|
+
});
|
|
361
|
+
const data = JSON.parse(stdout);
|
|
362
|
+
for (const devices of Object.values(data.devices)) {
|
|
363
|
+
if (!Array.isArray(devices))
|
|
364
|
+
continue;
|
|
365
|
+
for (const device of devices) {
|
|
366
|
+
if (device.state === "Booted") {
|
|
367
|
+
return device.udid;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
373
|
+
catch {
|
|
374
|
+
return null;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Find a booted simulator's UDID by its device name
|
|
379
|
+
* Matches Metro's deviceName against simulator names from simctl
|
|
380
|
+
*/
|
|
381
|
+
export async function findSimulatorByName(deviceName) {
|
|
382
|
+
try {
|
|
383
|
+
const { stdout } = await execAsync("xcrun simctl list devices booted -j", {
|
|
384
|
+
timeout: SIMCTL_TIMEOUT,
|
|
385
|
+
});
|
|
386
|
+
const data = JSON.parse(stdout);
|
|
387
|
+
const normalizedDeviceName = deviceName.toLowerCase().trim();
|
|
388
|
+
for (const devices of Object.values(data.devices)) {
|
|
389
|
+
if (!Array.isArray(devices))
|
|
390
|
+
continue;
|
|
391
|
+
for (const device of devices) {
|
|
392
|
+
if (device.state !== "Booted")
|
|
393
|
+
continue;
|
|
394
|
+
const normalizedSimName = device.name.toLowerCase().trim();
|
|
395
|
+
// Exact match
|
|
396
|
+
if (normalizedSimName === normalizedDeviceName) {
|
|
397
|
+
return device.udid;
|
|
398
|
+
}
|
|
399
|
+
// Partial match (deviceName contains simulator name or vice versa)
|
|
400
|
+
if (normalizedSimName.includes(normalizedDeviceName) ||
|
|
401
|
+
normalizedDeviceName.includes(normalizedSimName)) {
|
|
402
|
+
return device.udid;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
return null;
|
|
407
|
+
}
|
|
408
|
+
catch {
|
|
409
|
+
return null;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Get the active simulator UDID (Metro-connected) or fall back to first booted simulator
|
|
414
|
+
* This enables automatic device scoping based on Metro connection
|
|
415
|
+
*/
|
|
416
|
+
export async function getActiveOrBootedSimulatorUdid() {
|
|
417
|
+
// First, check if there's an active Metro-connected simulator
|
|
418
|
+
const activeUdid = getActiveSimulatorUdid();
|
|
419
|
+
if (activeUdid) {
|
|
420
|
+
return activeUdid;
|
|
421
|
+
}
|
|
422
|
+
// Fall back to first booted simulator
|
|
423
|
+
return getBootedSimulatorUdid();
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Build device selector for simctl command
|
|
427
|
+
*/
|
|
428
|
+
function buildDeviceArg(udid) {
|
|
429
|
+
return udid || "booted";
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Take a screenshot from an iOS simulator
|
|
433
|
+
*/
|
|
434
|
+
export async function iosScreenshot(outputPath, udid) {
|
|
435
|
+
try {
|
|
436
|
+
const simctlAvailable = await isSimctlAvailable();
|
|
437
|
+
if (!simctlAvailable) {
|
|
438
|
+
return {
|
|
439
|
+
success: false,
|
|
440
|
+
error: XCODE_MISSING_ERROR,
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
// Resolve target UDID (prefer Metro-connected simulator)
|
|
444
|
+
const targetUdid = udid || (await getActiveOrBootedSimulatorUdid());
|
|
445
|
+
if (!targetUdid) {
|
|
446
|
+
return {
|
|
447
|
+
success: false,
|
|
448
|
+
error: await buildNoSimulatorError(),
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
// Generate output path if not provided
|
|
452
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
453
|
+
const finalOutputPath = outputPath || path.join(os.tmpdir(), `ios-screenshot-${timestamp}.png`);
|
|
454
|
+
await execAsync(`xcrun simctl io ${targetUdid} screenshot "${finalOutputPath}"`, {
|
|
455
|
+
timeout: SIMCTL_TIMEOUT,
|
|
456
|
+
});
|
|
457
|
+
// Resize image if needed (API limit: 2000px max for multi-image requests)
|
|
458
|
+
// Return scale factor so AI can convert image coords to device coords
|
|
459
|
+
const MAX_DIMENSION = 2000;
|
|
460
|
+
const image = sharp(finalOutputPath);
|
|
461
|
+
const metadata = await image.metadata();
|
|
462
|
+
const originalWidth = metadata.width || 0;
|
|
463
|
+
const originalHeight = metadata.height || 0;
|
|
464
|
+
let imageData;
|
|
465
|
+
let scaleFactor = 1;
|
|
466
|
+
if (originalWidth > MAX_DIMENSION || originalHeight > MAX_DIMENSION) {
|
|
467
|
+
// Calculate scale to fit within MAX_DIMENSION
|
|
468
|
+
scaleFactor = Math.max(originalWidth, originalHeight) / MAX_DIMENSION;
|
|
469
|
+
imageData = await image
|
|
470
|
+
.resize(MAX_DIMENSION, MAX_DIMENSION, {
|
|
471
|
+
fit: "inside",
|
|
472
|
+
withoutEnlargement: true,
|
|
473
|
+
})
|
|
474
|
+
.jpeg({ quality: 85 })
|
|
475
|
+
.toBuffer();
|
|
476
|
+
}
|
|
477
|
+
else {
|
|
478
|
+
imageData = await image.jpeg({ quality: 85 }).toBuffer();
|
|
479
|
+
}
|
|
480
|
+
return {
|
|
481
|
+
success: true,
|
|
482
|
+
result: finalOutputPath,
|
|
483
|
+
data: imageData,
|
|
484
|
+
scaleFactor,
|
|
485
|
+
originalWidth,
|
|
486
|
+
originalHeight,
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
catch (error) {
|
|
490
|
+
return {
|
|
491
|
+
success: false,
|
|
492
|
+
error: `Failed to capture screenshot: ${error instanceof Error ? error.message : String(error)}`,
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* Install an app on an iOS simulator
|
|
498
|
+
*/
|
|
499
|
+
export async function iosInstallApp(appPath, udid) {
|
|
500
|
+
try {
|
|
501
|
+
const simctlAvailable = await isSimctlAvailable();
|
|
502
|
+
if (!simctlAvailable) {
|
|
503
|
+
return {
|
|
504
|
+
success: false,
|
|
505
|
+
error: XCODE_MISSING_ERROR,
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
// Verify app exists
|
|
509
|
+
if (!existsSync(appPath)) {
|
|
510
|
+
return {
|
|
511
|
+
success: false,
|
|
512
|
+
error: `App bundle not found: ${appPath}`,
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
// Resolve target UDID (prefer Metro-connected simulator)
|
|
516
|
+
const targetUdid = udid || (await getActiveOrBootedSimulatorUdid());
|
|
517
|
+
if (!targetUdid) {
|
|
518
|
+
return {
|
|
519
|
+
success: false,
|
|
520
|
+
error: await buildNoSimulatorError(),
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
await execAsync(`xcrun simctl install ${targetUdid} "${appPath}"`, {
|
|
524
|
+
timeout: 120000, // 2 minute timeout for install
|
|
525
|
+
});
|
|
526
|
+
return {
|
|
527
|
+
success: true,
|
|
528
|
+
result: `Successfully installed ${path.basename(appPath)}`,
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
catch (error) {
|
|
532
|
+
return {
|
|
533
|
+
success: false,
|
|
534
|
+
error: `Failed to install app: ${error instanceof Error ? error.message : String(error)}`,
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* Launch an app on an iOS simulator
|
|
540
|
+
*/
|
|
541
|
+
export async function iosLaunchApp(bundleId, udid) {
|
|
542
|
+
try {
|
|
543
|
+
const simctlAvailable = await isSimctlAvailable();
|
|
544
|
+
if (!simctlAvailable) {
|
|
545
|
+
return {
|
|
546
|
+
success: false,
|
|
547
|
+
error: XCODE_MISSING_ERROR,
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
// Resolve target UDID (prefer Metro-connected simulator)
|
|
551
|
+
const targetUdid = udid || (await getActiveOrBootedSimulatorUdid());
|
|
552
|
+
if (!targetUdid) {
|
|
553
|
+
return {
|
|
554
|
+
success: false,
|
|
555
|
+
error: await buildNoSimulatorError(),
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
await execAsync(`xcrun simctl launch ${targetUdid} ${bundleId}`, {
|
|
559
|
+
timeout: SIMCTL_TIMEOUT,
|
|
560
|
+
});
|
|
561
|
+
return {
|
|
562
|
+
success: true,
|
|
563
|
+
result: `Launched ${bundleId}`,
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
catch (error) {
|
|
567
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
568
|
+
let hint = "";
|
|
569
|
+
if (msg.includes("FBSOpenApplicationErrorDomain")) {
|
|
570
|
+
hint =
|
|
571
|
+
"\n\nCommon causes:\n- App is not installed on this simulator (install it first with ios_install_app)\n- Bundle ID is incorrect (check with: xcrun simctl listapps booted)";
|
|
572
|
+
}
|
|
573
|
+
return {
|
|
574
|
+
success: false,
|
|
575
|
+
error: `Failed to launch app: ${msg}${hint}`,
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* Open a URL in the iOS simulator
|
|
581
|
+
*/
|
|
582
|
+
export async function iosOpenUrl(url, udid) {
|
|
583
|
+
try {
|
|
584
|
+
const simctlAvailable = await isSimctlAvailable();
|
|
585
|
+
if (!simctlAvailable) {
|
|
586
|
+
return {
|
|
587
|
+
success: false,
|
|
588
|
+
error: XCODE_MISSING_ERROR,
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
// Resolve target UDID (prefer Metro-connected simulator)
|
|
592
|
+
const targetUdid = udid || (await getActiveOrBootedSimulatorUdid());
|
|
593
|
+
if (!targetUdid) {
|
|
594
|
+
return {
|
|
595
|
+
success: false,
|
|
596
|
+
error: await buildNoSimulatorError(),
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
await execAsync(`xcrun simctl openurl ${targetUdid} "${url}"`, {
|
|
600
|
+
timeout: SIMCTL_TIMEOUT,
|
|
601
|
+
});
|
|
602
|
+
return {
|
|
603
|
+
success: true,
|
|
604
|
+
result: `Opened URL: ${url}`,
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
catch (error) {
|
|
608
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
609
|
+
let hint = "";
|
|
610
|
+
if (msg.includes("LSApplicationWorkspaceError") ||
|
|
611
|
+
msg.includes("OpenApplicationErrorDomain")) {
|
|
612
|
+
hint =
|
|
613
|
+
"\n\nCommon causes:\n- Custom URL scheme not registered in the app's Info.plist\n- App that handles this URL scheme is not installed\n- For Expo apps, use the exp:// URL format";
|
|
614
|
+
}
|
|
615
|
+
return {
|
|
616
|
+
success: false,
|
|
617
|
+
error: `Failed to open URL: ${msg}${hint}`,
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
/**
|
|
622
|
+
* Terminate an app on an iOS simulator
|
|
623
|
+
*/
|
|
624
|
+
export async function iosTerminateApp(bundleId, udid) {
|
|
625
|
+
try {
|
|
626
|
+
const simctlAvailable = await isSimctlAvailable();
|
|
627
|
+
if (!simctlAvailable) {
|
|
628
|
+
return {
|
|
629
|
+
success: false,
|
|
630
|
+
error: XCODE_MISSING_ERROR,
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
// Resolve target UDID (prefer Metro-connected simulator)
|
|
634
|
+
const targetUdid = udid || (await getActiveOrBootedSimulatorUdid());
|
|
635
|
+
if (!targetUdid) {
|
|
636
|
+
return {
|
|
637
|
+
success: false,
|
|
638
|
+
error: await buildNoSimulatorError(),
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
await execAsync(`xcrun simctl terminate ${targetUdid} ${bundleId}`, {
|
|
642
|
+
timeout: SIMCTL_TIMEOUT,
|
|
643
|
+
});
|
|
644
|
+
return {
|
|
645
|
+
success: true,
|
|
646
|
+
result: `Terminated ${bundleId}`,
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
catch (error) {
|
|
650
|
+
return {
|
|
651
|
+
success: false,
|
|
652
|
+
error: `Failed to terminate app: ${error instanceof Error ? error.message : String(error)}`,
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
/**
|
|
657
|
+
* Boot an iOS simulator
|
|
658
|
+
*/
|
|
659
|
+
export async function iosBootSimulator(udid) {
|
|
660
|
+
try {
|
|
661
|
+
const simctlAvailable = await isSimctlAvailable();
|
|
662
|
+
if (!simctlAvailable) {
|
|
663
|
+
return {
|
|
664
|
+
success: false,
|
|
665
|
+
error: XCODE_MISSING_ERROR,
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
await execAsync(`xcrun simctl boot ${udid}`, {
|
|
669
|
+
timeout: 60000, // 1 minute timeout for boot
|
|
670
|
+
});
|
|
671
|
+
// Open Simulator app
|
|
672
|
+
await execAsync("open -a Simulator", { timeout: 10000 }).catch(() => {
|
|
673
|
+
// Ignore if Simulator app doesn't open
|
|
674
|
+
});
|
|
675
|
+
return {
|
|
676
|
+
success: true,
|
|
677
|
+
result: `Simulator ${udid} is now booting`,
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
catch (error) {
|
|
681
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
682
|
+
// Already booted is not an error
|
|
683
|
+
if (errorMessage.includes("Unable to boot device in current state: Booted")) {
|
|
684
|
+
return {
|
|
685
|
+
success: true,
|
|
686
|
+
result: "Simulator is already booted",
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
return {
|
|
690
|
+
success: false,
|
|
691
|
+
error: `Failed to boot simulator: ${errorMessage}`,
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
// ============================================================================
|
|
696
|
+
// iOS UI Interaction Tools
|
|
697
|
+
// These tools require an iOS UI driver to be installed:
|
|
698
|
+
// Recommended: AXe — brew install cameroncooke/axe/axe
|
|
699
|
+
// Alternative: IDB — brew install idb-companion
|
|
700
|
+
// AXe is the default driver. Set IOS_DRIVER=idb to fall back to IDB.
|
|
701
|
+
// ============================================================================
|
|
702
|
+
/**
|
|
703
|
+
* Tap at coordinates on an iOS simulator using IDB
|
|
704
|
+
*/
|
|
705
|
+
export async function iosTap(x, y, options) {
|
|
706
|
+
try {
|
|
707
|
+
const preflight = await ensureUiDriverReady(options?.udid);
|
|
708
|
+
if (!preflight.ready)
|
|
709
|
+
return preflight.error;
|
|
710
|
+
const { driver, targetUdid } = preflight;
|
|
711
|
+
const xRounded = Math.round(x);
|
|
712
|
+
const yRounded = Math.round(y);
|
|
713
|
+
if (driver === "axe") {
|
|
714
|
+
if (options?.duration !== undefined) {
|
|
715
|
+
// Long press: use axe touch with delay (convert ms to seconds)
|
|
716
|
+
const delaySecs = String(options.duration / 1000);
|
|
717
|
+
await runAxe("touch", "-x", String(xRounded), "-y", String(yRounded), "--down", "--up", "--delay", delaySecs, "--udid", targetUdid);
|
|
718
|
+
}
|
|
719
|
+
else {
|
|
720
|
+
await runAxe("tap", "-x", String(xRounded), "-y", String(yRounded), "--udid", targetUdid);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
else {
|
|
724
|
+
const args = ["ui", "tap", "--udid", targetUdid];
|
|
725
|
+
if (options?.duration !== undefined) {
|
|
726
|
+
args.push("--duration", String(options.duration));
|
|
727
|
+
}
|
|
728
|
+
args.push("--json", "--", String(xRounded), String(yRounded));
|
|
729
|
+
const { stderr } = await runIdb(...args);
|
|
730
|
+
if (stderr)
|
|
731
|
+
throw new Error(stderr);
|
|
732
|
+
}
|
|
733
|
+
return {
|
|
734
|
+
success: true,
|
|
735
|
+
result: `Tapped at (${xRounded}, ${yRounded})`,
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
catch (error) {
|
|
739
|
+
return {
|
|
740
|
+
success: false,
|
|
741
|
+
error: `Failed to tap: ${error instanceof Error ? error.message : String(error)}`,
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
/**
|
|
746
|
+
* Swipe gesture on an iOS simulator
|
|
747
|
+
*/
|
|
748
|
+
export async function iosSwipe(startX, startY, endX, endY, options) {
|
|
749
|
+
try {
|
|
750
|
+
const preflight = await ensureUiDriverReady(options?.udid);
|
|
751
|
+
if (!preflight.ready)
|
|
752
|
+
return preflight.error;
|
|
753
|
+
const { driver, targetUdid } = preflight;
|
|
754
|
+
const x1 = Math.round(startX);
|
|
755
|
+
const y1 = Math.round(startY);
|
|
756
|
+
const x2 = Math.round(endX);
|
|
757
|
+
const y2 = Math.round(endY);
|
|
758
|
+
if (driver === "axe") {
|
|
759
|
+
const args = [
|
|
760
|
+
"swipe",
|
|
761
|
+
"--start-x",
|
|
762
|
+
String(x1),
|
|
763
|
+
"--start-y",
|
|
764
|
+
String(y1),
|
|
765
|
+
"--end-x",
|
|
766
|
+
String(x2),
|
|
767
|
+
"--end-y",
|
|
768
|
+
String(y2),
|
|
769
|
+
];
|
|
770
|
+
if (options?.duration !== undefined) {
|
|
771
|
+
// Both axe and idb expect duration in seconds (matches the MCP tool schema).
|
|
772
|
+
args.push("--duration", String(options.duration));
|
|
773
|
+
}
|
|
774
|
+
if (options?.delta !== undefined) {
|
|
775
|
+
args.push("--delta", String(options.delta));
|
|
776
|
+
}
|
|
777
|
+
args.push("--udid", targetUdid);
|
|
778
|
+
await runAxe(...args);
|
|
779
|
+
}
|
|
780
|
+
else {
|
|
781
|
+
const args = ["ui", "swipe", "--udid", targetUdid];
|
|
782
|
+
if (options?.duration !== undefined) {
|
|
783
|
+
args.push("--duration", String(options.duration));
|
|
784
|
+
}
|
|
785
|
+
if (options?.delta !== undefined) {
|
|
786
|
+
args.push("--delta", String(options.delta));
|
|
787
|
+
}
|
|
788
|
+
args.push("--json", "--", String(x1), String(y1), String(x2), String(y2));
|
|
789
|
+
const { stderr } = await runIdb(...args);
|
|
790
|
+
if (stderr)
|
|
791
|
+
throw new Error(stderr);
|
|
792
|
+
}
|
|
793
|
+
return {
|
|
794
|
+
success: true,
|
|
795
|
+
result: `Swiped from (${x1}, ${y1}) to (${x2}, ${y2})`,
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
catch (error) {
|
|
799
|
+
return {
|
|
800
|
+
success: false,
|
|
801
|
+
error: `Failed to swipe: ${error instanceof Error ? error.message : String(error)}`,
|
|
802
|
+
};
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
/**
|
|
806
|
+
* Input text into the active field on an iOS simulator
|
|
807
|
+
*/
|
|
808
|
+
export async function iosInputText(text, udid) {
|
|
809
|
+
try {
|
|
810
|
+
const preflight = await ensureUiDriverReady(udid);
|
|
811
|
+
if (!preflight.ready)
|
|
812
|
+
return preflight.error;
|
|
813
|
+
const { driver, targetUdid } = preflight;
|
|
814
|
+
if (driver === "axe") {
|
|
815
|
+
await runAxe("type", text, "--udid", targetUdid);
|
|
816
|
+
}
|
|
817
|
+
else {
|
|
818
|
+
const { stderr } = await runIdb("ui", "text", "--udid", targetUdid, text);
|
|
819
|
+
if (stderr)
|
|
820
|
+
throw new Error(stderr);
|
|
821
|
+
}
|
|
822
|
+
return {
|
|
823
|
+
success: true,
|
|
824
|
+
result: `Typed text: "${text}"`,
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
catch (error) {
|
|
828
|
+
return {
|
|
829
|
+
success: false,
|
|
830
|
+
error: `Failed to input text: ${error instanceof Error ? error.message : String(error)}`,
|
|
831
|
+
};
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
/**
|
|
835
|
+
* Press a hardware button on an iOS simulator
|
|
836
|
+
*/
|
|
837
|
+
export async function iosButton(button, options) {
|
|
838
|
+
try {
|
|
839
|
+
// Validate button type
|
|
840
|
+
if (!IOS_BUTTON_TYPES.includes(button)) {
|
|
841
|
+
return {
|
|
842
|
+
success: false,
|
|
843
|
+
error: `Invalid button type: ${button}. Valid options: ${IOS_BUTTON_TYPES.join(", ")}`,
|
|
844
|
+
};
|
|
845
|
+
}
|
|
846
|
+
const preflight = await ensureUiDriverReady(options?.udid);
|
|
847
|
+
if (!preflight.ready)
|
|
848
|
+
return preflight.error;
|
|
849
|
+
const { driver, targetUdid } = preflight;
|
|
850
|
+
if (driver === "axe") {
|
|
851
|
+
const args = ["button", toAxeButtonName(button)];
|
|
852
|
+
if (options?.duration !== undefined) {
|
|
853
|
+
args.push("--duration", String(options.duration));
|
|
854
|
+
}
|
|
855
|
+
args.push("--udid", targetUdid);
|
|
856
|
+
await runAxe(...args);
|
|
857
|
+
}
|
|
858
|
+
else {
|
|
859
|
+
const args = ["ui", "button", "--udid", targetUdid];
|
|
860
|
+
if (options?.duration !== undefined) {
|
|
861
|
+
args.push("--duration", String(options.duration));
|
|
862
|
+
}
|
|
863
|
+
args.push(button);
|
|
864
|
+
const { stderr } = await runIdb(...args);
|
|
865
|
+
if (stderr)
|
|
866
|
+
throw new Error(stderr);
|
|
867
|
+
}
|
|
868
|
+
return {
|
|
869
|
+
success: true,
|
|
870
|
+
result: `Pressed ${button} button`,
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
catch (error) {
|
|
874
|
+
return {
|
|
875
|
+
success: false,
|
|
876
|
+
error: `Failed to press button: ${error instanceof Error ? error.message : String(error)}`,
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
/**
|
|
881
|
+
* Send a key event to an iOS simulator
|
|
882
|
+
*/
|
|
883
|
+
export async function iosKeyEvent(keycode, options) {
|
|
884
|
+
try {
|
|
885
|
+
const preflight = await ensureUiDriverReady(options?.udid);
|
|
886
|
+
if (!preflight.ready)
|
|
887
|
+
return preflight.error;
|
|
888
|
+
const { driver, targetUdid } = preflight;
|
|
889
|
+
if (driver === "axe") {
|
|
890
|
+
const args = ["key", String(keycode)];
|
|
891
|
+
if (options?.duration !== undefined) {
|
|
892
|
+
args.push("--duration", String(options.duration));
|
|
893
|
+
}
|
|
894
|
+
args.push("--udid", targetUdid);
|
|
895
|
+
await runAxe(...args);
|
|
896
|
+
}
|
|
897
|
+
else {
|
|
898
|
+
const args = ["ui", "key", "--udid", targetUdid];
|
|
899
|
+
if (options?.duration !== undefined) {
|
|
900
|
+
args.push("--duration", String(options.duration));
|
|
901
|
+
}
|
|
902
|
+
args.push(String(keycode));
|
|
903
|
+
const { stderr } = await runIdb(...args);
|
|
904
|
+
if (stderr)
|
|
905
|
+
throw new Error(stderr);
|
|
906
|
+
}
|
|
907
|
+
return {
|
|
908
|
+
success: true,
|
|
909
|
+
result: `Sent key event: ${keycode}`,
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
catch (error) {
|
|
913
|
+
return {
|
|
914
|
+
success: false,
|
|
915
|
+
error: `Failed to send key event: ${error instanceof Error ? error.message : String(error)}`,
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
/**
|
|
920
|
+
* Send a sequence of key events to an iOS simulator
|
|
921
|
+
*/
|
|
922
|
+
export async function iosKeySequence(keycodes, udid) {
|
|
923
|
+
try {
|
|
924
|
+
if (!keycodes || keycodes.length === 0) {
|
|
925
|
+
return {
|
|
926
|
+
success: false,
|
|
927
|
+
error: "At least one keycode is required",
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
const preflight = await ensureUiDriverReady(udid);
|
|
931
|
+
if (!preflight.ready)
|
|
932
|
+
return preflight.error;
|
|
933
|
+
const { driver, targetUdid } = preflight;
|
|
934
|
+
if (driver === "axe") {
|
|
935
|
+
await runAxe("key-sequence", "--keycodes", keycodes.join(","), "--udid", targetUdid);
|
|
936
|
+
}
|
|
937
|
+
else {
|
|
938
|
+
const args = [
|
|
939
|
+
"ui",
|
|
940
|
+
"key-sequence",
|
|
941
|
+
"--udid",
|
|
942
|
+
targetUdid,
|
|
943
|
+
...keycodes.map(String),
|
|
944
|
+
];
|
|
945
|
+
const { stderr } = await runIdb(...args);
|
|
946
|
+
if (stderr)
|
|
947
|
+
throw new Error(stderr);
|
|
948
|
+
}
|
|
949
|
+
return {
|
|
950
|
+
success: true,
|
|
951
|
+
result: `Sent key sequence: ${keycodes.join(", ")}`,
|
|
952
|
+
};
|
|
953
|
+
}
|
|
954
|
+
catch (error) {
|
|
955
|
+
return {
|
|
956
|
+
success: false,
|
|
957
|
+
error: `Failed to send key sequence: ${error instanceof Error ? error.message : String(error)}`,
|
|
958
|
+
};
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
/**
|
|
962
|
+
* Format accessibility tree for human-readable output
|
|
963
|
+
*/
|
|
964
|
+
function formatAccessibilityTree(elements, depth = 0) {
|
|
965
|
+
const lines = [];
|
|
966
|
+
const indent = " ".repeat(depth);
|
|
967
|
+
for (const element of elements) {
|
|
968
|
+
const parts = [];
|
|
969
|
+
if (element.type)
|
|
970
|
+
parts.push(`[${element.type}]`);
|
|
971
|
+
if (element.AXLabel)
|
|
972
|
+
parts.push(`"${element.AXLabel}"`);
|
|
973
|
+
if (element.AXValue)
|
|
974
|
+
parts.push(`value="${element.AXValue}"`);
|
|
975
|
+
if (element.frame) {
|
|
976
|
+
const f = element.frame;
|
|
977
|
+
const centerX = Math.round(f.x + f.width / 2);
|
|
978
|
+
const centerY = Math.round(f.y + f.height / 2);
|
|
979
|
+
parts.push(`frame=(${f.x}, ${f.y}, ${f.width}x${f.height}) tap=(${centerX}, ${centerY})`);
|
|
980
|
+
}
|
|
981
|
+
if (parts.length > 0) {
|
|
982
|
+
lines.push(`${indent}${parts.join(" ")}`);
|
|
983
|
+
}
|
|
984
|
+
if (element.children && element.children.length > 0) {
|
|
985
|
+
lines.push(formatAccessibilityTree(element.children, depth + 1));
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
return lines.join("\n");
|
|
989
|
+
}
|
|
990
|
+
/**
|
|
991
|
+
* Probe the iOS root accessibility element for the top safe-area inset in points.
|
|
992
|
+
* React-native-screens modal/sheet presentations cause measureInWindow to return y
|
|
993
|
+
* relative to the content origin (below the inset) instead of the window. Knowing
|
|
994
|
+
* the inset lets us detect and correct the shifted coordinate space.
|
|
995
|
+
*
|
|
996
|
+
* Cached briefly so a single tap flow doesn't incur multiple describe-ui calls.
|
|
997
|
+
*/
|
|
998
|
+
const SAFE_AREA_CACHE_TTL_MS = 5000;
|
|
999
|
+
const safeAreaTopCache = new Map();
|
|
1000
|
+
export async function getIOSSafeAreaTop(udid) {
|
|
1001
|
+
const cacheKey = udid || "__default__";
|
|
1002
|
+
const cached = safeAreaTopCache.get(cacheKey);
|
|
1003
|
+
if (cached && cached.expires > Date.now())
|
|
1004
|
+
return cached.value;
|
|
1005
|
+
let value = 0;
|
|
1006
|
+
try {
|
|
1007
|
+
const preflight = await ensureUiDriverReady(udid);
|
|
1008
|
+
if (preflight.ready) {
|
|
1009
|
+
const { driver, targetUdid } = preflight;
|
|
1010
|
+
let stdout;
|
|
1011
|
+
if (driver === "axe") {
|
|
1012
|
+
stdout = (await runAxe("describe-ui", "--udid", targetUdid)).stdout;
|
|
1013
|
+
}
|
|
1014
|
+
else {
|
|
1015
|
+
const r = await runIdb("ui", "describe-all", "--udid", targetUdid, "--json", "--nested");
|
|
1016
|
+
stdout = r.stdout;
|
|
1017
|
+
}
|
|
1018
|
+
const data = JSON.parse(stdout);
|
|
1019
|
+
const root = Array.isArray(data) ? data[0] : data;
|
|
1020
|
+
const y = root?.frame?.y;
|
|
1021
|
+
if (typeof y === "number" && y > 0)
|
|
1022
|
+
value = Math.round(y);
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
catch {
|
|
1026
|
+
// Fall through: value stays 0 (no correction)
|
|
1027
|
+
}
|
|
1028
|
+
safeAreaTopCache.set(cacheKey, { value, expires: Date.now() + SAFE_AREA_CACHE_TTL_MS });
|
|
1029
|
+
return value;
|
|
1030
|
+
}
|
|
1031
|
+
/**
|
|
1032
|
+
* Get accessibility info for the entire screen
|
|
1033
|
+
*/
|
|
1034
|
+
export async function iosDescribeAll(udid) {
|
|
1035
|
+
try {
|
|
1036
|
+
const preflight = await ensureUiDriverReady(udid);
|
|
1037
|
+
if (!preflight.ready)
|
|
1038
|
+
return preflight.error;
|
|
1039
|
+
const { driver, targetUdid } = preflight;
|
|
1040
|
+
let stdout;
|
|
1041
|
+
if (driver === "axe") {
|
|
1042
|
+
const result = await runAxe("describe-ui", "--udid", targetUdid);
|
|
1043
|
+
stdout = result.stdout;
|
|
1044
|
+
}
|
|
1045
|
+
else {
|
|
1046
|
+
const result = await runIdb("ui", "describe-all", "--udid", targetUdid, "--json", "--nested");
|
|
1047
|
+
if (result.stderr)
|
|
1048
|
+
throw new Error(result.stderr);
|
|
1049
|
+
stdout = result.stdout;
|
|
1050
|
+
}
|
|
1051
|
+
// Parse JSON response
|
|
1052
|
+
const elements = JSON.parse(stdout);
|
|
1053
|
+
// Format for human-readable output
|
|
1054
|
+
const formatted = formatAccessibilityTree(elements);
|
|
1055
|
+
const header = 'Note: All coordinates are in iOS points. Use tap(text="...") for tapping — do not pass these coordinates to tap(x, y) directly.\n\n';
|
|
1056
|
+
return {
|
|
1057
|
+
success: true,
|
|
1058
|
+
result: header + (formatted || "No accessibility elements found"),
|
|
1059
|
+
elements,
|
|
1060
|
+
};
|
|
1061
|
+
}
|
|
1062
|
+
catch (error) {
|
|
1063
|
+
return {
|
|
1064
|
+
success: false,
|
|
1065
|
+
error: `Failed to describe screen: ${error instanceof Error ? error.message : String(error)}`,
|
|
1066
|
+
};
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
/**
|
|
1070
|
+
* I1 (2026-05-16): when a native iOS system sheet (auth prompt, permission request,
|
|
1071
|
+
* Safari sign-in sheet) is on screen, the RN app's fiber tree still describes the
|
|
1072
|
+
* underlying screen — but those pressables aren't interactable until the sheet is
|
|
1073
|
+
* dismissed. Detect the overlay so the screenshot output can warn the agent and
|
|
1074
|
+
* point it at tap(x, y, native=true) for the overlay's actual buttons.
|
|
1075
|
+
*
|
|
1076
|
+
* Heuristic: scan the top three levels of the AXe accessibility tree for an
|
|
1077
|
+
* element whose type matches an iOS sheet/alert role, OR whose AXLabel matches a
|
|
1078
|
+
* well-known system-overlay phrase. False positives are rare because the patterns
|
|
1079
|
+
* are specific to system-presented sheets — RN content rarely uses these labels.
|
|
1080
|
+
*/
|
|
1081
|
+
const IOS_SYSTEM_OVERLAY_TYPES = /^(Alert|ActionSheet|Sheet|Dialog|SystemDialog)$/i;
|
|
1082
|
+
const IOS_SYSTEM_OVERLAY_LABEL_PATTERNS = /Wants to Use|Sign in to|Sign In to|Allow .* to (use|access)|Open in Safari|Continue with|Use Face ID|Use Touch ID|Use Passcode/i;
|
|
1083
|
+
/**
|
|
1084
|
+
* Telltale of an RN accessibilityLabel rollup vs a system-presented sheet:
|
|
1085
|
+
* a rollup concatenates multiple child labels with commas / newlines (3+ separators),
|
|
1086
|
+
* while a system sheet's label is a single sentence. Without this guard the label
|
|
1087
|
+
* patterns above false-positive on screens like gifted.co's login wrapper whose
|
|
1088
|
+
* rollup happens to contain "Continue with" amongst many other phrases.
|
|
1089
|
+
*/
|
|
1090
|
+
function looksLikeRNLabelRollup(label) {
|
|
1091
|
+
const commas = (label.match(/,/g) ?? []).length;
|
|
1092
|
+
const newlines = (label.match(/\n/g) ?? []).length;
|
|
1093
|
+
return commas + newlines >= 3;
|
|
1094
|
+
}
|
|
1095
|
+
export async function detectIOSSystemOverlay(udid) {
|
|
1096
|
+
try {
|
|
1097
|
+
const desc = await iosDescribeAll(udid);
|
|
1098
|
+
if (!desc.success || !desc.elements)
|
|
1099
|
+
return null;
|
|
1100
|
+
const found = (function walk(elements, depth) {
|
|
1101
|
+
if (depth > 2)
|
|
1102
|
+
return null;
|
|
1103
|
+
for (const el of elements) {
|
|
1104
|
+
if (el.type && IOS_SYSTEM_OVERLAY_TYPES.test(el.type)) {
|
|
1105
|
+
return { type: el.type, label: el.AXLabel ?? "" };
|
|
1106
|
+
}
|
|
1107
|
+
if (el.AXLabel &&
|
|
1108
|
+
IOS_SYSTEM_OVERLAY_LABEL_PATTERNS.test(el.AXLabel) &&
|
|
1109
|
+
!looksLikeRNLabelRollup(el.AXLabel)) {
|
|
1110
|
+
return { type: el.type ?? "Unknown", label: el.AXLabel };
|
|
1111
|
+
}
|
|
1112
|
+
if (el.children && el.children.length > 0) {
|
|
1113
|
+
const nested = walk(el.children, depth + 1);
|
|
1114
|
+
if (nested)
|
|
1115
|
+
return nested;
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
return null;
|
|
1119
|
+
})(desc.elements, 0);
|
|
1120
|
+
if (!found)
|
|
1121
|
+
return { detected: false };
|
|
1122
|
+
return { detected: true, description: found.label || found.type };
|
|
1123
|
+
}
|
|
1124
|
+
catch {
|
|
1125
|
+
return null;
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
export function formatIOSSystemOverlayWarning(detection) {
|
|
1129
|
+
if (!detection.detected)
|
|
1130
|
+
return "";
|
|
1131
|
+
const desc = detection.description?.slice(0, 120) ?? "system sheet";
|
|
1132
|
+
return (`\n\n⚠️ Native iOS overlay detected: "${desc}". The pressables list above reflects the ` +
|
|
1133
|
+
`React Native screen UNDERNEATH and may be stale. For overlay buttons, take the coordinates ` +
|
|
1134
|
+
`from the screenshot and call tap(x, y, native=true, platform="ios").`);
|
|
1135
|
+
}
|
|
1136
|
+
/**
|
|
1137
|
+
* Get accessibility info at a specific point
|
|
1138
|
+
*/
|
|
1139
|
+
export async function iosDescribePoint(x, y, udid) {
|
|
1140
|
+
try {
|
|
1141
|
+
const preflight = await ensureUiDriverReady(udid);
|
|
1142
|
+
if (!preflight.ready)
|
|
1143
|
+
return preflight.error;
|
|
1144
|
+
const { driver, targetUdid } = preflight;
|
|
1145
|
+
const xRounded = Math.round(x);
|
|
1146
|
+
const yRounded = Math.round(y);
|
|
1147
|
+
let stdout;
|
|
1148
|
+
if (driver === "axe") {
|
|
1149
|
+
const result = await runAxe("describe-ui", "--point", `${xRounded},${yRounded}`, "--udid", targetUdid);
|
|
1150
|
+
stdout = result.stdout;
|
|
1151
|
+
}
|
|
1152
|
+
else {
|
|
1153
|
+
const result = await runIdb("ui", "describe-point", "--udid", targetUdid, "--json", "--", String(xRounded), String(yRounded));
|
|
1154
|
+
if (result.stderr)
|
|
1155
|
+
throw new Error(result.stderr);
|
|
1156
|
+
stdout = result.stdout;
|
|
1157
|
+
}
|
|
1158
|
+
// Parse JSON response - may be single element or array
|
|
1159
|
+
let element;
|
|
1160
|
+
try {
|
|
1161
|
+
const parsed = JSON.parse(stdout);
|
|
1162
|
+
element = Array.isArray(parsed) ? parsed[0] : parsed;
|
|
1163
|
+
}
|
|
1164
|
+
catch {
|
|
1165
|
+
return {
|
|
1166
|
+
success: true,
|
|
1167
|
+
result: `No accessibility element found at (${xRounded}, ${yRounded})`,
|
|
1168
|
+
elements: [],
|
|
1169
|
+
};
|
|
1170
|
+
}
|
|
1171
|
+
// Format for human-readable output
|
|
1172
|
+
const parts = [];
|
|
1173
|
+
if (element.type)
|
|
1174
|
+
parts.push(`Type: ${element.type}`);
|
|
1175
|
+
if (element.AXLabel)
|
|
1176
|
+
parts.push(`Label: "${element.AXLabel}"`);
|
|
1177
|
+
if (element.AXValue)
|
|
1178
|
+
parts.push(`Value: "${element.AXValue}"`);
|
|
1179
|
+
if (element.frame) {
|
|
1180
|
+
const f = element.frame;
|
|
1181
|
+
const centerX = Math.round(f.x + f.width / 2);
|
|
1182
|
+
const centerY = Math.round(f.y + f.height / 2);
|
|
1183
|
+
parts.push(`Frame: (${f.x}, ${f.y}) ${f.width}x${f.height}`);
|
|
1184
|
+
parts.push(`Tap: (${centerX}, ${centerY})`);
|
|
1185
|
+
}
|
|
1186
|
+
return {
|
|
1187
|
+
success: true,
|
|
1188
|
+
result: parts.length > 0
|
|
1189
|
+
? `Element at (${xRounded}, ${yRounded}):\n${parts.join("\n")}`
|
|
1190
|
+
: `No accessibility element found at (${xRounded}, ${yRounded})`,
|
|
1191
|
+
elements: element ? [element] : [],
|
|
1192
|
+
};
|
|
1193
|
+
}
|
|
1194
|
+
catch (error) {
|
|
1195
|
+
return {
|
|
1196
|
+
success: false,
|
|
1197
|
+
error: `Failed to describe point: ${error instanceof Error ? error.message : String(error)}`,
|
|
1198
|
+
};
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
/**
|
|
1202
|
+
* Helper to flatten nested accessibility elements
|
|
1203
|
+
*/
|
|
1204
|
+
function flattenElements(elements) {
|
|
1205
|
+
const result = [];
|
|
1206
|
+
for (const el of elements) {
|
|
1207
|
+
result.push(el);
|
|
1208
|
+
if (el.children && el.children.length > 0) {
|
|
1209
|
+
result.push(...flattenElements(el.children));
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
return result;
|
|
1213
|
+
}
|
|
1214
|
+
/**
|
|
1215
|
+
* Tap an element by its accessibility label using IDB
|
|
1216
|
+
* This simplifies the workflow: no need to manually find coordinates
|
|
1217
|
+
*/
|
|
1218
|
+
export async function iosTapElement(options) {
|
|
1219
|
+
try {
|
|
1220
|
+
const { label, labelContains, index = 0, duration, udid } = options;
|
|
1221
|
+
if (!label && !labelContains) {
|
|
1222
|
+
return {
|
|
1223
|
+
success: false,
|
|
1224
|
+
error: "Either 'label' (exact match) or 'labelContains' (partial match) is required",
|
|
1225
|
+
};
|
|
1226
|
+
}
|
|
1227
|
+
// Get all accessibility elements
|
|
1228
|
+
const describeResult = await iosDescribeAll(udid);
|
|
1229
|
+
if (!describeResult.success || !describeResult.elements) {
|
|
1230
|
+
return {
|
|
1231
|
+
success: false,
|
|
1232
|
+
error: describeResult.error || "Failed to get accessibility elements",
|
|
1233
|
+
};
|
|
1234
|
+
}
|
|
1235
|
+
// Flatten the tree and find matching elements
|
|
1236
|
+
const allElements = flattenElements(describeResult.elements);
|
|
1237
|
+
const matches = allElements.filter((el) => {
|
|
1238
|
+
if (!el.AXLabel)
|
|
1239
|
+
return false;
|
|
1240
|
+
if (label)
|
|
1241
|
+
return el.AXLabel === label;
|
|
1242
|
+
if (labelContains)
|
|
1243
|
+
return el.AXLabel.toLowerCase().includes(labelContains.toLowerCase());
|
|
1244
|
+
return false;
|
|
1245
|
+
});
|
|
1246
|
+
if (matches.length === 0) {
|
|
1247
|
+
const searchTerm = label
|
|
1248
|
+
? `label="${label}"`
|
|
1249
|
+
: `labelContains="${labelContains}"`;
|
|
1250
|
+
return {
|
|
1251
|
+
success: false,
|
|
1252
|
+
error: `No element found with ${searchTerm}`,
|
|
1253
|
+
};
|
|
1254
|
+
}
|
|
1255
|
+
// Select element by index (default 0 = first match)
|
|
1256
|
+
if (index >= matches.length) {
|
|
1257
|
+
return {
|
|
1258
|
+
success: false,
|
|
1259
|
+
error: `Index ${index} out of range. Found ${matches.length} matching element(s).`,
|
|
1260
|
+
};
|
|
1261
|
+
}
|
|
1262
|
+
const element = matches[index];
|
|
1263
|
+
// Check if element has frame coordinates
|
|
1264
|
+
if (!element.frame) {
|
|
1265
|
+
return {
|
|
1266
|
+
success: false,
|
|
1267
|
+
error: `Element "${element.AXLabel}" has no frame coordinates`,
|
|
1268
|
+
};
|
|
1269
|
+
}
|
|
1270
|
+
// Calculate center
|
|
1271
|
+
const centerX = Math.round(element.frame.x + element.frame.width / 2);
|
|
1272
|
+
const centerY = Math.round(element.frame.y + element.frame.height / 2);
|
|
1273
|
+
// Tap at center
|
|
1274
|
+
const tapResult = await iosTap(centerX, centerY, { duration, udid });
|
|
1275
|
+
if (tapResult.success) {
|
|
1276
|
+
return {
|
|
1277
|
+
success: true,
|
|
1278
|
+
result: `Tapped "${element.AXLabel}" at (${centerX}, ${centerY})`,
|
|
1279
|
+
};
|
|
1280
|
+
}
|
|
1281
|
+
return tapResult;
|
|
1282
|
+
}
|
|
1283
|
+
catch (error) {
|
|
1284
|
+
return {
|
|
1285
|
+
success: false,
|
|
1286
|
+
error: `Failed to tap element: ${error instanceof Error ? error.message : String(error)}`,
|
|
1287
|
+
};
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
/**
|
|
1291
|
+
* Parse IDB accessibility output into simplified element array
|
|
1292
|
+
*/
|
|
1293
|
+
function parseIdbAccessibilityForFindElement(output) {
|
|
1294
|
+
const elements = [];
|
|
1295
|
+
try {
|
|
1296
|
+
const data = JSON.parse(output);
|
|
1297
|
+
const extractElements = (node) => {
|
|
1298
|
+
const frame = node.frame;
|
|
1299
|
+
if (frame) {
|
|
1300
|
+
const element = {
|
|
1301
|
+
label: node.AXLabel || node.label || "",
|
|
1302
|
+
value: node.AXValue || node.value || "",
|
|
1303
|
+
type: node.type || node.AXType || "",
|
|
1304
|
+
identifier: node.AXIdentifier ||
|
|
1305
|
+
node.identifier ||
|
|
1306
|
+
node.AXUniqueId ||
|
|
1307
|
+
"",
|
|
1308
|
+
frame: {
|
|
1309
|
+
x: frame.x || 0,
|
|
1310
|
+
y: frame.y || 0,
|
|
1311
|
+
width: frame.width || 0,
|
|
1312
|
+
height: frame.height || 0,
|
|
1313
|
+
},
|
|
1314
|
+
center: {
|
|
1315
|
+
x: Math.round((frame.x || 0) + (frame.width || 0) / 2),
|
|
1316
|
+
y: Math.round((frame.y || 0) + (frame.height || 0) / 2),
|
|
1317
|
+
},
|
|
1318
|
+
enabled: node.enabled !== false,
|
|
1319
|
+
traits: node.traits || [],
|
|
1320
|
+
};
|
|
1321
|
+
if (element.label ||
|
|
1322
|
+
element.value ||
|
|
1323
|
+
element.type ||
|
|
1324
|
+
element.identifier) {
|
|
1325
|
+
elements.push(element);
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
const children = node.children;
|
|
1329
|
+
if (children && Array.isArray(children)) {
|
|
1330
|
+
for (const child of children) {
|
|
1331
|
+
extractElements(child);
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
};
|
|
1335
|
+
if (Array.isArray(data)) {
|
|
1336
|
+
for (const item of data) {
|
|
1337
|
+
extractElements(item);
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
else {
|
|
1341
|
+
extractElements(data);
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
catch {
|
|
1345
|
+
// If JSON parsing fails, return empty array
|
|
1346
|
+
}
|
|
1347
|
+
return elements;
|
|
1348
|
+
}
|
|
1349
|
+
/**
|
|
1350
|
+
* Check if an element's center is within the screen viewport.
|
|
1351
|
+
* Filters out off-screen elements in scroll views, horizontal pagers, etc.
|
|
1352
|
+
*/
|
|
1353
|
+
function isIOSElementInViewport(element, screenWidth, screenHeight) {
|
|
1354
|
+
const { center } = element;
|
|
1355
|
+
return (center.x >= 0 &&
|
|
1356
|
+
center.x <= screenWidth &&
|
|
1357
|
+
center.y >= 0 &&
|
|
1358
|
+
center.y <= screenHeight);
|
|
1359
|
+
}
|
|
1360
|
+
/**
|
|
1361
|
+
* Extract screen dimensions from the parsed UI tree.
|
|
1362
|
+
* The root Application element's frame represents the screen size.
|
|
1363
|
+
*/
|
|
1364
|
+
function extractIOSScreenSize(rawOutput) {
|
|
1365
|
+
try {
|
|
1366
|
+
const data = JSON.parse(rawOutput);
|
|
1367
|
+
const root = Array.isArray(data) ? data[0] : data;
|
|
1368
|
+
const frame = root?.frame;
|
|
1369
|
+
if (frame?.width && frame?.height) {
|
|
1370
|
+
return { width: frame.width, height: frame.height };
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
catch {
|
|
1374
|
+
// Fall through
|
|
1375
|
+
}
|
|
1376
|
+
return null;
|
|
1377
|
+
}
|
|
1378
|
+
/**
|
|
1379
|
+
* Match iOS element against find options
|
|
1380
|
+
*/
|
|
1381
|
+
function matchesIOSFindElement(element, options) {
|
|
1382
|
+
if (options.label !== undefined) {
|
|
1383
|
+
if (element.label !== options.label)
|
|
1384
|
+
return false;
|
|
1385
|
+
}
|
|
1386
|
+
if (options.labelContains !== undefined) {
|
|
1387
|
+
if (!element.label.toLowerCase().includes(options.labelContains.toLowerCase()))
|
|
1388
|
+
return false;
|
|
1389
|
+
}
|
|
1390
|
+
if (options.value !== undefined) {
|
|
1391
|
+
if (element.value !== options.value)
|
|
1392
|
+
return false;
|
|
1393
|
+
}
|
|
1394
|
+
if (options.valueContains !== undefined) {
|
|
1395
|
+
if (!element.value.toLowerCase().includes(options.valueContains.toLowerCase()))
|
|
1396
|
+
return false;
|
|
1397
|
+
}
|
|
1398
|
+
if (options.type !== undefined) {
|
|
1399
|
+
if (!element.type.toLowerCase().includes(options.type.toLowerCase()))
|
|
1400
|
+
return false;
|
|
1401
|
+
}
|
|
1402
|
+
if (options.identifier !== undefined) {
|
|
1403
|
+
if (element.identifier !== options.identifier)
|
|
1404
|
+
return false;
|
|
1405
|
+
}
|
|
1406
|
+
if (options.identifierContains !== undefined) {
|
|
1407
|
+
if (!element.identifier
|
|
1408
|
+
.toLowerCase()
|
|
1409
|
+
.includes(options.identifierContains.toLowerCase()))
|
|
1410
|
+
return false;
|
|
1411
|
+
}
|
|
1412
|
+
return true;
|
|
1413
|
+
}
|
|
1414
|
+
/**
|
|
1415
|
+
* Get UI accessibility tree from iOS simulator using IDB (for find_element)
|
|
1416
|
+
*/
|
|
1417
|
+
export async function iosGetUITree(udid) {
|
|
1418
|
+
try {
|
|
1419
|
+
const preflight = await ensureUiDriverReady(udid);
|
|
1420
|
+
if (!preflight.ready)
|
|
1421
|
+
return preflight.error;
|
|
1422
|
+
const { driver, targetUdid } = preflight;
|
|
1423
|
+
let stdout;
|
|
1424
|
+
if (driver === "axe") {
|
|
1425
|
+
const result = await runAxe("describe-ui", "--udid", targetUdid);
|
|
1426
|
+
stdout = result.stdout;
|
|
1427
|
+
}
|
|
1428
|
+
else {
|
|
1429
|
+
const result = await runIdb("ui", "describe-all", "--udid", targetUdid);
|
|
1430
|
+
stdout = result.stdout;
|
|
1431
|
+
}
|
|
1432
|
+
const elements = parseIdbAccessibilityForFindElement(stdout);
|
|
1433
|
+
return {
|
|
1434
|
+
success: true,
|
|
1435
|
+
elements,
|
|
1436
|
+
rawOutput: stdout,
|
|
1437
|
+
};
|
|
1438
|
+
}
|
|
1439
|
+
catch (error) {
|
|
1440
|
+
return {
|
|
1441
|
+
success: false,
|
|
1442
|
+
error: `Failed to get UI tree: ${error instanceof Error ? error.message : String(error)}`,
|
|
1443
|
+
};
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
/**
|
|
1447
|
+
* Bug 3 (2026-05-16): RN rolls up child Text labels into a parent View's
|
|
1448
|
+
* accessibilityLabel when the parent is marked `accessible`. Result: a screen-root
|
|
1449
|
+
* View whose label is a 200+ char concatenation of every visible text. `labelContains`
|
|
1450
|
+
* matches it, frame is the whole screen, centroid happens to land on whatever button
|
|
1451
|
+
* sits near the middle — and the tap silently lands on the wrong target.
|
|
1452
|
+
*
|
|
1453
|
+
* Heuristic filter: an element is an aggregator wrapper when EITHER
|
|
1454
|
+
* (a) its label is much longer than the query AND contains concatenation hints
|
|
1455
|
+
* (newlines, commas, semicolons — RN rollup typically joins with `, ` or `\n`)
|
|
1456
|
+
* (b) its frame covers >50% of the viewport (a real button never does this)
|
|
1457
|
+
*
|
|
1458
|
+
* Both heuristics together catch the gifted.co LoginScreen wrapper case the doc cites.
|
|
1459
|
+
*/
|
|
1460
|
+
function isAggregatorWrapper(element, query, screenSize) {
|
|
1461
|
+
const labelLen = element.label?.length ?? 0;
|
|
1462
|
+
const queryLen = query.length;
|
|
1463
|
+
const isLongConcatenatedLabel = queryLen > 0 &&
|
|
1464
|
+
labelLen > queryLen * 5 &&
|
|
1465
|
+
labelLen > 60 &&
|
|
1466
|
+
/[,;\n]/.test(element.label ?? "");
|
|
1467
|
+
let isFullScreenFrame = false;
|
|
1468
|
+
if (screenSize && element.frame) {
|
|
1469
|
+
const elArea = (element.frame.width ?? 0) * (element.frame.height ?? 0);
|
|
1470
|
+
const screenArea = screenSize.width * screenSize.height;
|
|
1471
|
+
if (screenArea > 0) {
|
|
1472
|
+
isFullScreenFrame = elArea > screenArea * 0.5;
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
return isLongConcatenatedLabel || isFullScreenFrame;
|
|
1476
|
+
}
|
|
1477
|
+
/**
|
|
1478
|
+
* Find element(s) in the iOS UI tree matching the given criteria
|
|
1479
|
+
*/
|
|
1480
|
+
export async function iosFindElement(options, udid) {
|
|
1481
|
+
try {
|
|
1482
|
+
if (!options.label &&
|
|
1483
|
+
!options.labelContains &&
|
|
1484
|
+
!options.value &&
|
|
1485
|
+
!options.valueContains &&
|
|
1486
|
+
!options.type &&
|
|
1487
|
+
!options.identifier &&
|
|
1488
|
+
!options.identifierContains) {
|
|
1489
|
+
return {
|
|
1490
|
+
success: false,
|
|
1491
|
+
found: false,
|
|
1492
|
+
error: "At least one search criteria (label, labelContains, value, valueContains, identifier, identifierContains, or type) must be provided",
|
|
1493
|
+
};
|
|
1494
|
+
}
|
|
1495
|
+
const treeResult = await iosGetUITree(udid);
|
|
1496
|
+
if (!treeResult.success || !treeResult.elements) {
|
|
1497
|
+
return {
|
|
1498
|
+
success: false,
|
|
1499
|
+
found: false,
|
|
1500
|
+
error: treeResult.error,
|
|
1501
|
+
};
|
|
1502
|
+
}
|
|
1503
|
+
// Filter to viewport-visible elements first, then match criteria.
|
|
1504
|
+
// This prevents selecting off-screen duplicates in scroll views / pagers.
|
|
1505
|
+
const screenSize = treeResult.rawOutput
|
|
1506
|
+
? extractIOSScreenSize(treeResult.rawOutput)
|
|
1507
|
+
: null;
|
|
1508
|
+
const visibleElements = screenSize
|
|
1509
|
+
? treeResult.elements.filter((el) => isIOSElementInViewport(el, screenSize.width, screenSize.height))
|
|
1510
|
+
: treeResult.elements;
|
|
1511
|
+
let matches = visibleElements.filter((el) => matchesIOSFindElement(el, options));
|
|
1512
|
+
// If no visible matches, fall back to all elements (element may be partially visible
|
|
1513
|
+
// or the screen size extraction failed)
|
|
1514
|
+
if (matches.length === 0) {
|
|
1515
|
+
matches = treeResult.elements.filter((el) => matchesIOSFindElement(el, options));
|
|
1516
|
+
}
|
|
1517
|
+
// Bug 3 (2026-05-16): when matching by partial string (labelContains / identifierContains),
|
|
1518
|
+
// drop aggregator-wrapper elements that match only because their label rolls up every
|
|
1519
|
+
// visible child's text. Without this filter, tapping such a wrapper's centroid
|
|
1520
|
+
// silently lands on the wrong on-screen element.
|
|
1521
|
+
//
|
|
1522
|
+
// When ALL matches are aggregator wrappers (the common case — RN often rolls labels
|
|
1523
|
+
// up to a single accessible wrapper, leaving no individual element with a matching
|
|
1524
|
+
// label), refuse with a clear error pointing the agent at component= or coordinates.
|
|
1525
|
+
// This is safer than silently tapping the wrapper centroid (which lands wherever the
|
|
1526
|
+
// wrapper's geometric center happens to be — usually NOT on the requested button).
|
|
1527
|
+
if (options.labelContains !== undefined && matches.length > 0) {
|
|
1528
|
+
const filtered = matches.filter((el) => !isAggregatorWrapper(el, options.labelContains, screenSize));
|
|
1529
|
+
if (filtered.length > 0) {
|
|
1530
|
+
matches = filtered;
|
|
1531
|
+
}
|
|
1532
|
+
else {
|
|
1533
|
+
return {
|
|
1534
|
+
success: true,
|
|
1535
|
+
found: false,
|
|
1536
|
+
matchCount: 0,
|
|
1537
|
+
error: `Only matches for labelContains="${options.labelContains}" were aggregator wrappers ` +
|
|
1538
|
+
`(RN accessibilityLabel rollup of a parent View covering the whole screen). ` +
|
|
1539
|
+
`Tapping the wrapper would land on the wrong element. ` +
|
|
1540
|
+
`Try component="ComponentName" instead, or pass coordinates (x, y) from a screenshot.`,
|
|
1541
|
+
};
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
if (options.identifierContains !== undefined && matches.length > 0) {
|
|
1545
|
+
const filtered = matches.filter((el) => !isAggregatorWrapper(el, options.identifierContains, screenSize));
|
|
1546
|
+
if (filtered.length > 0) {
|
|
1547
|
+
matches = filtered;
|
|
1548
|
+
}
|
|
1549
|
+
else {
|
|
1550
|
+
return {
|
|
1551
|
+
success: true,
|
|
1552
|
+
found: false,
|
|
1553
|
+
matchCount: 0,
|
|
1554
|
+
error: `Only matches for identifierContains="${options.identifierContains}" were aggregator wrappers. ` +
|
|
1555
|
+
`Try component="ComponentName" or pass coordinates (x, y) from a screenshot.`,
|
|
1556
|
+
};
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
if (matches.length === 0) {
|
|
1560
|
+
return {
|
|
1561
|
+
success: true,
|
|
1562
|
+
found: false,
|
|
1563
|
+
matchCount: 0,
|
|
1564
|
+
};
|
|
1565
|
+
}
|
|
1566
|
+
const index = options.index ?? 0;
|
|
1567
|
+
const selectedElement = matches[index];
|
|
1568
|
+
if (!selectedElement) {
|
|
1569
|
+
return {
|
|
1570
|
+
success: true,
|
|
1571
|
+
found: false,
|
|
1572
|
+
matchCount: matches.length,
|
|
1573
|
+
error: `Index ${index} out of bounds. Found ${matches.length} matching element(s).`,
|
|
1574
|
+
};
|
|
1575
|
+
}
|
|
1576
|
+
return {
|
|
1577
|
+
success: true,
|
|
1578
|
+
found: true,
|
|
1579
|
+
element: selectedElement,
|
|
1580
|
+
allMatches: matches,
|
|
1581
|
+
matchCount: matches.length,
|
|
1582
|
+
};
|
|
1583
|
+
}
|
|
1584
|
+
catch (error) {
|
|
1585
|
+
return {
|
|
1586
|
+
success: false,
|
|
1587
|
+
found: false,
|
|
1588
|
+
error: `Failed to find element: ${error instanceof Error ? error.message : String(error)}`,
|
|
1589
|
+
};
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
// DPR cache: UDID → device pixel ratio (never changes during session)
|
|
1593
|
+
const dprCache = new Map();
|
|
1594
|
+
/**
|
|
1595
|
+
* Pure calculation: DPR = screenshotPixelWidth / screenPointWidth.
|
|
1596
|
+
* Exported for unit testing.
|
|
1597
|
+
*/
|
|
1598
|
+
export function calculateDPR(screenshotPixelWidth, screenPointWidth) {
|
|
1599
|
+
return Math.round(screenshotPixelWidth / screenPointWidth);
|
|
1600
|
+
}
|
|
1601
|
+
/**
|
|
1602
|
+
* Query the actual device pixel ratio for an iOS simulator.
|
|
1603
|
+
* Uses screenshot pixel width vs accessibility root frame width.
|
|
1604
|
+
* Result is cached per UDID for the session.
|
|
1605
|
+
*/
|
|
1606
|
+
export async function getDevicePixelRatio(udid) {
|
|
1607
|
+
const cacheKey = udid || "__default__";
|
|
1608
|
+
const cached = dprCache.get(cacheKey);
|
|
1609
|
+
if (cached)
|
|
1610
|
+
return cached;
|
|
1611
|
+
const resolvedUdid = udid || (await getActiveOrBootedSimulatorUdid());
|
|
1612
|
+
if (!resolvedUdid) {
|
|
1613
|
+
throw new Error(await buildNoSimulatorError());
|
|
1614
|
+
}
|
|
1615
|
+
// Take screenshot to get pixel dimensions
|
|
1616
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
1617
|
+
const screenshotPath = path.join(os.tmpdir(), `ios-dpr-screenshot-${timestamp}.png`);
|
|
1618
|
+
await execAsync(`xcrun simctl io ${resolvedUdid} screenshot "${screenshotPath}"`, {
|
|
1619
|
+
timeout: SIMCTL_TIMEOUT,
|
|
1620
|
+
});
|
|
1621
|
+
const metadata = await sharp(screenshotPath).metadata();
|
|
1622
|
+
const pixelWidth = metadata.width;
|
|
1623
|
+
const pixelHeight = metadata.height;
|
|
1624
|
+
// Try accessibility tree for exact DPR, fall back to inference
|
|
1625
|
+
const driver = getIosDriver();
|
|
1626
|
+
const driverOk = driver === "axe" ? await isAxeAvailable() : await isIdbAvailable();
|
|
1627
|
+
if (driverOk) {
|
|
1628
|
+
try {
|
|
1629
|
+
let stdout;
|
|
1630
|
+
if (driver === "axe") {
|
|
1631
|
+
const result = await runAxe("describe-ui", "--udid", resolvedUdid);
|
|
1632
|
+
stdout = result.stdout;
|
|
1633
|
+
}
|
|
1634
|
+
else {
|
|
1635
|
+
await ensureIdbConnected(resolvedUdid);
|
|
1636
|
+
const result = await runIdb("ui", "describe-all", "--udid", resolvedUdid, "--json", "--nested");
|
|
1637
|
+
stdout = result.stdout;
|
|
1638
|
+
}
|
|
1639
|
+
const screenSize = extractIOSScreenSize(stdout);
|
|
1640
|
+
if (screenSize) {
|
|
1641
|
+
const dpr = calculateDPR(pixelWidth, screenSize.width);
|
|
1642
|
+
dprCache.set(cacheKey, dpr);
|
|
1643
|
+
return dpr;
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
catch {
|
|
1647
|
+
// driver failed — fall through to inference
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
// Fallback: infer DPR from screenshot pixel dimensions
|
|
1651
|
+
const { inferIOSDevicePixelRatio } = await import("./ocr.js");
|
|
1652
|
+
const dpr = inferIOSDevicePixelRatio(pixelWidth, pixelHeight);
|
|
1653
|
+
dprCache.set(cacheKey, dpr);
|
|
1654
|
+
return dpr;
|
|
1655
|
+
}
|
|
1656
|
+
/** Clear DPR cache (for testing or when devices change) */
|
|
1657
|
+
export function clearDPRCache() {
|
|
1658
|
+
dprCache.clear();
|
|
1659
|
+
}
|
|
1660
|
+
/**
|
|
1661
|
+
* Wait for element to appear on iOS screen with polling
|
|
1662
|
+
*/
|
|
1663
|
+
export async function iosWaitForElement(options, udid) {
|
|
1664
|
+
const timeoutMs = options.timeoutMs ?? 10000;
|
|
1665
|
+
const pollIntervalMs = options.pollIntervalMs ?? 500;
|
|
1666
|
+
const startTime = Date.now();
|
|
1667
|
+
if (!options.label &&
|
|
1668
|
+
!options.labelContains &&
|
|
1669
|
+
!options.value &&
|
|
1670
|
+
!options.valueContains &&
|
|
1671
|
+
!options.type) {
|
|
1672
|
+
return {
|
|
1673
|
+
success: false,
|
|
1674
|
+
found: false,
|
|
1675
|
+
timedOut: false,
|
|
1676
|
+
error: "At least one search criteria (label, labelContains, value, valueContains, or type) must be provided",
|
|
1677
|
+
};
|
|
1678
|
+
}
|
|
1679
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
1680
|
+
const result = await iosFindElement(options, udid);
|
|
1681
|
+
if (result.found && result.element) {
|
|
1682
|
+
return {
|
|
1683
|
+
...result,
|
|
1684
|
+
elapsedMs: Date.now() - startTime,
|
|
1685
|
+
timedOut: false,
|
|
1686
|
+
};
|
|
1687
|
+
}
|
|
1688
|
+
if (!result.success) {
|
|
1689
|
+
return {
|
|
1690
|
+
...result,
|
|
1691
|
+
elapsedMs: Date.now() - startTime,
|
|
1692
|
+
timedOut: false,
|
|
1693
|
+
};
|
|
1694
|
+
}
|
|
1695
|
+
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
|
|
1696
|
+
}
|
|
1697
|
+
return {
|
|
1698
|
+
success: true,
|
|
1699
|
+
found: false,
|
|
1700
|
+
elapsedMs: Date.now() - startTime,
|
|
1701
|
+
timedOut: true,
|
|
1702
|
+
error: `Timed out after ${timeoutMs}ms waiting for element`,
|
|
1703
|
+
};
|
|
1704
|
+
}
|
|
1705
|
+
//# sourceMappingURL=ios.js.map
|