@loadmill/droid-cua 2.43.0 → 2.44.0
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/README.md +114 -7
- package/build/index.js +39 -15
- package/build/src/cli/device-selector.js +13 -6
- package/build/src/cli/headless-target.js +24 -0
- package/build/src/cli/ios-device-command.js +121 -0
- package/build/src/cli/ios-device-list.js +32 -0
- package/build/src/cli/network-capture-device.js +21 -0
- package/build/src/commands/help.js +14 -3
- package/build/src/core/execution-engine.js +3 -0
- package/build/src/desktop-shared/ios-real-device.js +82 -0
- package/build/src/device/factory.js +4 -0
- package/build/src/device/ios/actions.js +48 -12
- package/build/src/device/ios/appium-server.js +7 -18
- package/build/src/device/ios/connection.js +71 -12
- package/build/src/device/ios/node-cli-env.js +55 -0
- package/build/src/device/ios/real-device/device-state.js +40 -0
- package/build/src/device/ios/real-device/devicectl.js +154 -0
- package/build/src/device/ios/real-device/readiness.js +439 -0
- package/build/src/device/ios/real-device/session.js +176 -0
- package/build/src/device/ios/real-device/signing.js +466 -0
- package/build/src/device/ios/real-device/wda-provisioning.js +229 -0
- package/build/src/network/certificate-authority.js +21 -1
- package/build/src/network/certificate-install.js +38 -0
- package/build/src/network/local-proxy.js +21 -1
- package/build/src/network/run-network-capture.js +16 -5
- package/package.json +1 -1
- package/skill/droid-cua/SKILL.md +2 -2
- package/skill/droid-cua/references/authoring-tests.md +1 -1
- package/skill/droid-cua/references/onboarding-and-troubleshooting.md +31 -0
- package/skill/droid-cua/references/running-and-healing.md +11 -2
package/README.md
CHANGED
|
@@ -69,10 +69,10 @@ You can also keep project run history in a results folder and review past runs f
|
|
|
69
69
|
<h2 id="platforms">📱 Platforms</h2>
|
|
70
70
|
|
|
71
71
|
- **Android** - Physical devices and emulators
|
|
72
|
-
- **iOS** - Simulators on macOS
|
|
72
|
+
- **iOS** - Simulators on macOS, and iPhones connected to your Mac by USB
|
|
73
73
|
|
|
74
74
|
Desktop app support:
|
|
75
|
-
- macOS: Android
|
|
75
|
+
- macOS: Android, iOS simulator, and iPhone (USB) workflows
|
|
76
76
|
- Windows: Android workflows only
|
|
77
77
|
|
|
78
78
|
## Requirements
|
|
@@ -90,6 +90,8 @@ Desktop app support:
|
|
|
90
90
|
- Appium
|
|
91
91
|
- XCUITest driver
|
|
92
92
|
|
|
93
|
+
**iPhone over USB (macOS only):** see [Testing on a real iPhone](#testing-on-a-real-iphone).
|
|
94
|
+
|
|
93
95
|
---
|
|
94
96
|
|
|
95
97
|
<h2 id="features">✨ Features</h2>
|
|
@@ -97,7 +99,7 @@ Desktop app support:
|
|
|
97
99
|
- **Desktop-first workflow** - Create, run, and manage tests from one app
|
|
98
100
|
- **Setup guidance** - Configure API access and platform prerequisites in the app
|
|
99
101
|
- **Device and simulator connection** - Connect Android targets and iOS simulators
|
|
100
|
-
- **Network certificate preparation** - Manage one active root CA and install it on a connected local simulator or supported emulator from the desktop app's Tools menu
|
|
102
|
+
- **Network certificate preparation** - Manage one active root CA and install it on a connected local simulator or supported emulator, or get manual setup steps for a physical iPhone or Android phone, from the desktop app's Tools menu
|
|
101
103
|
- **Natural-language test creation** - Describe flows in plain English
|
|
102
104
|
- **Test management** - Create, edit, save, and rerun reusable scripts, including tests organized in nested project folders
|
|
103
105
|
- **Live execution logs** - Watch actions and progress as tests run
|
|
@@ -106,15 +108,19 @@ Desktop app support:
|
|
|
106
108
|
- **Run video export** - Generate and download a polished MP4 replay from a desktop run's screenshots and instruction timeline
|
|
107
109
|
- **Headless support** - Reuse scripts in CLI and automation workflows
|
|
108
110
|
|
|
109
|
-
**Network certificate preparation:** Use **Tools → Manage Certificates** to see the active CA, upload a single `.pem` containing a CA certificate and its matching unencrypted private key, generate a new Droid CA, or remove the active CA. Droid stores one active pair in `~/.droid-cua/network/`; upload copies the PEM material without changing the source file, and removal does not change device trust. Connect a local iOS Simulator or a rootable Android Emulator, then choose **Tools → Install Network Certificate**. This installs the active CA, generating a Droid CA if none exists. Devices prepared with a previous CA may need installation again. Droid may reapply Android system trust when a prepared emulator reconnects after a cold boot. Restart an already running Android app after installation so it loads the updated trust store. Android certificate setup supports capable rootable images through Android 16; Google Play images without root access and Android 17 are not supported.
|
|
111
|
+
**Network certificate preparation:** Use **Tools → Manage Certificates** to see the active CA, upload a single `.pem` containing a CA certificate and its matching unencrypted private key, generate a new Droid CA, or remove the active CA. Droid stores one active pair in `~/.droid-cua/network/`; upload copies the PEM material without changing the source file, and removal does not change device trust. Connect a local iOS Simulator or a rootable Android Emulator, then choose **Tools → Install Network Certificate**. This installs the active CA, generating a Droid CA if none exists. Devices prepared with a previous CA may need installation again. Droid may reapply Android system trust when a prepared emulator reconnects after a cold boot. Restart an already running Android app after installation so it loads the updated trust store. With a local iPhone connected by USB, **Tools → Install Network Certificate** does not change the phone; it shows the manual steps instead. **Send to iPhone** opens the macOS share menu with a DER `.cer` copy of the active CA named after its common name (for example `Droid CUA Root fbd877ab.cer`, written to `~/.droid-cua/network/iphone/`); choose AirDrop and your iPhone, install it from **Settings → Profile Downloaded**, then turn on full trust for that common name under **Settings → General → About → Certificate Trust Settings**. Android Emulator certificate setup supports capable rootable images through Android 16; Google Play images without root access and Android 17 are not supported. With a physical Android phone connected over ADB, **Tools → Install Network Certificate** writes a PEM `.crt` copy of the active CA named after its common name (for example `Droid CUA Root fbd877ab.crt`, in `~/.droid-cua/network/android/`), copies it to the phone's `/sdcard/Download/` folder with `adb push`, and shows the manual steps; it changes no phone settings. Install it from **Settings → Security → Encryption & credentials → Install a certificate → CA certificate** (names vary by vendor), then restart the tested app. If the copy fails, Droid shows the local file path so you can copy it to the phone yourself.
|
|
110
112
|
|
|
111
113
|
**Network capture:** Enable **Network Capture** in desktop Settings or pass `--capture-network` for CLI tests. Local Chrome and Edge capture through Playwright with no certificate or proxy setup, using the same network queries and report cards as mobile. Capture covers the test browser context, including frames and popups, from run start. Previously loaded traffic is not recovered and existing pages are not automatically reloaded. CLI `--url` navigation happens after capture starts. Cloud browser capture, WebSocket messages and HAR export are not supported.
|
|
112
114
|
|
|
113
115
|
**Mobile setup:** Manually route a prepared local simulator or emulator to `127.0.0.1:8765` (`10.0.2.2:8765` from Android Emulator). Enable **Network Capture** in desktop Settings to keep the proxy forwarding while Droid is open and automatically capture every local mobile test, including each project or scenario case. Existing **Network Proxy** settings do not turn capture on; opt in to the new setting. For CLI tests, pass `--capture-network`; this starts the proxy and writes one JSONL log per test. `--network-proxy` remains a forwarding-only CLI flag. Droid prints or logs each capture path and a best-effort health summary. Desktop artifacts are in the project's results folder; CLI artifacts are beside `--report` output when enabled, or under `logs/`. A missing CA stops mobile capture before test actions. No observed traffic or failed HTTPS decryption is reported separately from test assertions, but capture health does not prove that all traffic was observed. WebSocket messages are unsupported. Manual proxy setup is the V1 workflow. Droid does not change device or Mac proxy settings; turn off manually configured routing when finished. If Droid exits while routing remains enabled, restart Droid or clear the proxy to restore connectivity.
|
|
114
116
|
|
|
117
|
+
**Physical iPhone capture:** A USB-connected iPhone reaches the proxy over Wi-Fi, so Droid listens on all Mac network interfaces (`0.0.0.0:8765`) instead of `127.0.0.1` while an iPhone is the target: in the CLI when `--avd ios-device:<udid|auto>` is used with `--capture-network` or `--network-proxy`, and in the desktop app while Network Capture is on and a local iPhone is connected (the proxy switches back to `127.0.0.1` on disconnect, but not during a run). Droid prints the setting to use, for example `On the iPhone, set Wi-Fi → Configure Proxy → Manual: server 192.168.1.23, port 8765.` Put the Mac and iPhone on the same Wi-Fi network with device-to-device traffic allowed, install and fully trust the Droid CA manually as described above, and set the iPhone proxy by hand; Droid never changes iPhone settings. Turn the iPhone proxy off after testing. Anyone on the same network can use the proxy while it listens on `0.0.0.0`, so use a trusted network. Apps that pin certificates still connect through the proxy, but their HTTPS is not decrypted.
|
|
118
|
+
|
|
119
|
+
**Physical Android phone capture:** A phone connected over ADB (any serial other than `emulator-<port>`) is handled the same way as a physical iPhone: Droid listens on `0.0.0.0:8765`, in the CLI once the connected device turns out to be a phone (with `--capture-network` or `--network-proxy`) and in the desktop app while Network Capture is on and the phone is connected. Droid prints the setting to use, for example `On the Android phone, set Wi-Fi → Edit network → Proxy → Manual: hostname 192.168.1.23, port 8765.` Install the CA as a user certificate as described above and set the phone's Wi-Fi proxy by hand; Droid does not root the phone or change its settings, and turn the proxy off after testing. Since Android 7, apps trust user-installed CAs only when their network security config allows it, for example `<certificates src="user"/>` in `network_security_config.xml` or a `<debug-overrides>` block in a debuggable build. Debug and QA builds usually opt in; Play Store builds usually do not, so their HTTPS is not decrypted. Chrome trusts user CAs. Rooted-device system trust is not supported.
|
|
120
|
+
|
|
115
121
|
Captures stop recording at 100 MiB per run, retaining a partial JSONL with a `CAPTURE_LIMIT` error. Traffic continues forwarding; live network queries report incomplete evidence. Captured bodies are limited to 1 MiB, both on the wire and after decompression. Compressed bodies that expand beyond that limit are omitted with `decoded-body-too-large`; this is incomplete evidence, not a missing response. The separate query evidence limit is 1,000,000 serialized characters. A capture limit or missing traffic is not proof that an application request never occurred.
|
|
116
122
|
|
|
117
|
-
For troubleshooting, “No traffic observed” points to routing or an app that did not send traffic; it does not prove an expected request was absent. HTTPS connections with no decrypted requests point to certificate trust or interception compatibility. A port-in-use error means another Droid instance or proxy already owns the listener. Stop that listener before starting the CLI. A capture-size or write failure retains only partial evidence.
|
|
123
|
+
For troubleshooting, “No traffic observed” points to routing or an app that did not send traffic; it does not prove an expected request was absent. HTTPS connections with no decrypted requests point to certificate trust or interception compatibility. A port-in-use error means another Droid instance or proxy already owns the listener. Stop that listener before starting the CLI. A capture-size or write failure retains only partial evidence. Ordinary iOS runs without capture do not require proxy or certificate preparation.
|
|
118
124
|
|
|
119
125
|
**Network questions and assertions:** With capture enabled, tests can ask Droid to inspect HTTP traffic and compare values across requests in natural language. For example: “Assert that the POST to `/pageview` after opening Home contains the `item_id` returned by `/home`.” The `query_network` tool searches by URL substring, method, status, flow IDs, request timestamps, or a starting step. It considers all matching exchanges in time order and sends only selected headers/bodies to one side call using the selected run model. Large results require a narrower search; they are not silently truncated. Optional observation windows wait the full duration, up to 30 seconds per query. Absence means only “no matching traffic observed during this window”; unavailable capture or insufficient evidence cannot establish absence. Desktop and HTML reports show one collapsed Network query card per call; expand it to see the question and answer. The agent receives structured flow references and capture diagnostics, while model details stay out of the timeline.
|
|
120
126
|
|
|
@@ -207,6 +213,10 @@ droid-cua run tests/auth/login.dcua tests/checkout/purchase.dcua admin/refund.dc
|
|
|
207
213
|
# Headless iOS simulator run
|
|
208
214
|
droid-cua run tests/login.dcua --platform ios --avd "iPhone 16"
|
|
209
215
|
|
|
216
|
+
# Check an iPhone connected by USB, then run on it
|
|
217
|
+
droid-cua ios-device check --fix
|
|
218
|
+
droid-cua run tests/login.dcua --avd ios-device:auto
|
|
219
|
+
|
|
210
220
|
# List running desktop applications and their window IDs (macOS or Windows)
|
|
211
221
|
droid-cua --target desktop --list-desktop-apps
|
|
212
222
|
|
|
@@ -287,7 +297,8 @@ droid-cua \
|
|
|
287
297
|
```
|
|
288
298
|
|
|
289
299
|
Supported CLI options include:
|
|
290
|
-
- `--avd`
|
|
300
|
+
- `--avd` (including `ios-device:<udid>` or `ios-device:auto` for an iPhone connected by USB)
|
|
301
|
+
- `--ios-team`
|
|
291
302
|
- `--platform`
|
|
292
303
|
- `--device-source`
|
|
293
304
|
- `--device-name`
|
|
@@ -314,7 +325,7 @@ Supported CLI options include:
|
|
|
314
325
|
- `--execution-prompt-file`
|
|
315
326
|
- `--secrets`
|
|
316
327
|
- `--record`
|
|
317
|
-
- `--network-proxy` (local mobile sessions only; starts idle forwarding at `127.0.0.1:8765`)
|
|
328
|
+
- `--network-proxy` (local mobile sessions only; starts idle forwarding at `127.0.0.1:8765`, or `0.0.0.0:8765` for `--avd ios-device:<udid|auto>` and physical Android phones)
|
|
318
329
|
- `--capture-network` (local mobile and Chrome/Edge test runs; writes a JSONL log for each test, using a proxy only for mobile)
|
|
319
330
|
- `--report`
|
|
320
331
|
- `--report-metadata <path>`
|
|
@@ -375,6 +386,102 @@ HTML report (`--report`):
|
|
|
375
386
|
droid-cua run tests/login.dcua --avd adb:emulator-5554 --report ./reports
|
|
376
387
|
```
|
|
377
388
|
|
|
389
|
+
<h3 id="testing-on-a-real-iphone">Testing on a real iPhone</h3>
|
|
390
|
+
|
|
391
|
+
The desktop app and the CLI can run tests on your own iPhone connected to your Mac by USB, on the
|
|
392
|
+
apps already installed on it (TestFlight, App Store, or your team's builds).
|
|
393
|
+
droid-cua installs a small helper app (WebDriverAgent) on the iPhone, signed
|
|
394
|
+
with your company's Apple team; nothing is built from source at run time.
|
|
395
|
+
|
|
396
|
+
Requirements:
|
|
397
|
+
- macOS with an Xcode that supports the iPhone's iOS version, opened once.
|
|
398
|
+
Its first launch installs the device components and asks for a Mac admin password.
|
|
399
|
+
- The iOS platform installed in that Xcode (**Xcode > Settings > Components**, or
|
|
400
|
+
`xcodebuild -downloadPlatform iOS`; about 10 GB). Xcode 26 doesn't include it.
|
|
401
|
+
- Your Apple ID signed into Xcode (**Xcode > Settings > Apple Accounts**), as a
|
|
402
|
+
member of your company's **paid** Apple Developer team, with at least the
|
|
403
|
+
**Developer** role and **Access to Certificates, Identifiers & Profiles**.
|
|
404
|
+
Free Apple IDs are not supported.
|
|
405
|
+
- The Appium XCUITest driver, as for Simulators.
|
|
406
|
+
- On the iPhone, once: connect it by USB, unlock it and tap **Trust**; turn on
|
|
407
|
+
**Developer Mode** (**Settings > Privacy & Security > Developer Mode**, then let it
|
|
408
|
+
restart and confirm); set **Auto-Lock** to **Never** (**Settings > Display & Brightness**).
|
|
409
|
+
- A test-only iPhone is recommended: droid-cua tests the apps and data already on
|
|
410
|
+
the phone and never resets it.
|
|
411
|
+
|
|
412
|
+
Check the iPhone and set it up:
|
|
413
|
+
|
|
414
|
+
```sh
|
|
415
|
+
droid-cua ios-device check # report each check and how to fix it
|
|
416
|
+
droid-cua ios-device check --fix # also set up signing and install the helper app
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
`ios-device check` prints every check as passed (`✔`), failed (`✖`), or a
|
|
420
|
+
reminder (`!`), with the exact fix for each failure, and exits `0` only when the
|
|
421
|
+
iPhone is ready. Use `--udid <udid>` to choose an iPhone (the first one connected
|
|
422
|
+
by USB is used otherwise). The first `--fix` on a Mac runs a one-time Xcode signing
|
|
423
|
+
build (about a minute); installing the helper app takes a few seconds. A test run
|
|
424
|
+
performs the same checks and fixes automatically before it starts.
|
|
425
|
+
|
|
426
|
+
**Xcode installed** names what's wrong: Xcode isn't in `/Applications`, the
|
|
427
|
+
Command Line Tools are selected instead of Xcode (fix: `sudo xcode-select -s
|
|
428
|
+
/Applications/Xcode.app`), or the Xcode license isn't accepted (fix: `sudo
|
|
429
|
+
xcodebuild -license accept`). **iOS platform installed** checks that Xcode has the
|
|
430
|
+
iOS platform for its iOS SDK, without which Xcode can't build for an iPhone (fix:
|
|
431
|
+
install it from **Xcode > Settings > Components**, or run `xcodebuild
|
|
432
|
+
-downloadPlatform iOS`, about 10 GB). **Apple team access** reads your teams from Xcode;
|
|
433
|
+
on a fresh Mac, where Xcode hasn't saved them yet, droid-cua fetches them from
|
|
434
|
+
Apple with a short Xcode build (a few seconds, at most once a minute). It then
|
|
435
|
+
says whether no Apple account is signed into Xcode, or you're signed in but none
|
|
436
|
+
of your accounts is in a paid Apple Developer team.
|
|
437
|
+
|
|
438
|
+
Run tests on the iPhone:
|
|
439
|
+
|
|
440
|
+
```sh
|
|
441
|
+
droid-cua run tests/login.dcua --avd ios-device:auto
|
|
442
|
+
droid-cua run tests/login.dcua --avd ios-device:00008120-001C18501A81A01E --ios-team ABCDE12345
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
- `--avd ios-device:<udid>` selects an iPhone; `ios-device:auto` picks the first
|
|
446
|
+
iPhone connected by USB. The platform is detected as iOS from the token.
|
|
447
|
+
Running `droid-cua` without `--avd` lists connected iPhones next to Simulators.
|
|
448
|
+
- `--ios-team <TEAMID>` chooses the Apple team that signs the helper app when you
|
|
449
|
+
belong to several paid teams. `DROID_CUA_IOS_TEAM_ID` works too; the flag wins.
|
|
450
|
+
- `--app` is not used for a local iPhone: tests run against apps already installed
|
|
451
|
+
on it, so open the app in your test instructions (for example, `Open the Settings app`).
|
|
452
|
+
- After the iPhone restarts, unlock it. On the first run after a restart or a new
|
|
453
|
+
helper install, the iPhone may ask for your passcode to allow automation; enter
|
|
454
|
+
it and droid-cua continues. If the iPhone locks during a run, droid-cua pauses
|
|
455
|
+
until you unlock it.
|
|
456
|
+
- After a major iOS update, droid-cua tells you which Xcode to install and open once.
|
|
457
|
+
|
|
458
|
+
In the desktop app:
|
|
459
|
+
|
|
460
|
+
- Run the Setup Wizard (**Help > Run Setup Wizard**) and choose **iPhone (USB)**.
|
|
461
|
+
The **Connect iPhone** checklist runs the same checks as `ios-device check`.
|
|
462
|
+
Steps you do on the iPhone or in Xcode (plug in, Trust, Developer Mode, unlock,
|
|
463
|
+
device components) re-check every few seconds and turn green on their own.
|
|
464
|
+
**Set up signing** and **Install helper app** run the fix and stream its progress.
|
|
465
|
+
When the Command Line Tools are selected or the Xcode license isn't accepted,
|
|
466
|
+
**Xcode installed** offers **Set Xcode Developer Path** or **Accept Xcode License**
|
|
467
|
+
(both ask for a Mac admin password). When the iOS platform is missing,
|
|
468
|
+
**iOS platform installed** offers **Download iOS Platform**, which runs
|
|
469
|
+
`xcodebuild -downloadPlatform iOS` (about 10 GB).
|
|
470
|
+
Auto-Lock and the dedicated test iPhone are shown as reminders and don't block.
|
|
471
|
+
- If you belong to several paid Apple teams, choose the team in the wizard. The
|
|
472
|
+
desktop app remembers it and uses it for every iPhone run.
|
|
473
|
+
- Connected iPhones appear in the iOS device picker next to Simulators, with a
|
|
474
|
+
hint when the iPhone isn't trusted yet or Developer Mode is off. droid tests the
|
|
475
|
+
apps already installed on the iPhone and never installs any.
|
|
476
|
+
- The connection log and the run view highlight what needs you on the iPhone:
|
|
477
|
+
*Enter your passcode on the iPhone to allow automation*, *iPhone locked — unlock
|
|
478
|
+
to continue*, and *Your iPhone is on iOS N. Install Xcode N and open it once.*
|
|
479
|
+
|
|
480
|
+
Known limits: USB only (no Wi-Fi), one iPhone at a time, attended runs (someone
|
|
481
|
+
needs to unlock the phone and approve prompts), macOS only, and no `.ipa`
|
|
482
|
+
installation. Verified on iOS 26; iOS 17 to 26 are expected to work. iOS betas
|
|
483
|
+
and iOS 27 are not supported yet.
|
|
484
|
+
|
|
378
485
|
LambdaTest CLI runs:
|
|
379
486
|
- Use `--device-source lambdatest` with `--platform`, `--device-name`, `--os-version`, `--app`, and `--instructions`.
|
|
380
487
|
- Set `LAMBDATEST_USERNAME` and `LAMBDATEST_ACCESS_KEY` in the environment.
|
package/build/index.js
CHANGED
|
@@ -19,7 +19,7 @@ import { resolveExecutionTestData } from "./src/core/test-data.js";
|
|
|
19
19
|
import { getDefaultSecretsPath, resolveExecutionSecrets } from "./src/core/secrets.js";
|
|
20
20
|
import { assertRuntimeCredentialsForProvider, createRunContext, resolveRuntimeConfigFromEnv, resolveRuntimeCredentialsFromEnv, } from "./src/core/runtime-config.js";
|
|
21
21
|
import { resolveHeadlessExecutionConfig } from "./src/cli/headless-execution-config.js";
|
|
22
|
-
import { artifactKindsForPool, resolveHeadlessTarget, scopeArtifactKindsToPool, shouldWaitForLoadmillCloudCompletion, } from "./src/cli/headless-target.js";
|
|
22
|
+
import { artifactKindsForPool, resolveHeadlessTarget, resolveIosTeamId, scopeArtifactKindsToPool, shouldWaitForLoadmillCloudCompletion, } from "./src/cli/headless-target.js";
|
|
23
23
|
import { createHeadlessDebugArtifacts } from "./src/cli/headless-debug.js";
|
|
24
24
|
import { loadHeadlessInstructions } from "./src/cli/headless-instructions.js";
|
|
25
25
|
import { clearLoadmillReportMetadata } from "./src/cli/loadmill-report-metadata.js";
|
|
@@ -38,6 +38,7 @@ import { droidReportUploadManager } from "./src/integrations/loadmill/report-upl
|
|
|
38
38
|
import { getCloudProviderAdapter } from "./src/device/cloud/registry.js";
|
|
39
39
|
import { setCloudProgressListener } from "./src/device/cloud/progress.js";
|
|
40
40
|
import { runSkillCommand } from "./src/cli/skill-command.js";
|
|
41
|
+
import { runIosDeviceCommand } from "./src/cli/ios-device-command.js";
|
|
41
42
|
import { formatLoadmillCloudNotEntitledMessage, isLoadmillCloudNotEntitledError, isLoadmillCloudUsageLimitError, } from "./src/device/cloud/loadmill-cloud/errors.js";
|
|
42
43
|
import { fetchAndDownloadArtifacts, loadmillCloudAdapter } from "./src/device/cloud/loadmill-cloud/adapter.js";
|
|
43
44
|
import { resolveRunPool } from "./src/device/cloud/loadmill-cloud/run-pool.js";
|
|
@@ -46,10 +47,26 @@ import { formatDuration } from "./src/core/report/run-log-model.js";
|
|
|
46
47
|
import { createBrowserSessionTeardown, withLoadmillCloudSession } from "./src/cli/loadmill-cloud-session.js";
|
|
47
48
|
import { createDesktopController, listDesktopApplications } from "./src/desktop/computer-use.js";
|
|
48
49
|
import { createRunNetworkCapture } from "./src/network/run-network-capture.js";
|
|
50
|
+
import { cliNetworkCaptureDeviceKind, cliNetworkProxyTarget } from "./src/cli/network-capture-device.js";
|
|
49
51
|
import { loadCertificateAuthority } from "./src/network/certificate-authority.js";
|
|
50
52
|
import { ensureCliAndroidCertificate, inspectNetworkDevice } from "./src/network/certificate-install.js";
|
|
51
53
|
dotenv.config();
|
|
52
54
|
let networkCapture = null;
|
|
55
|
+
let cliNetworkProxyHost = null;
|
|
56
|
+
async function startCliNetworkProxy(deviceName) {
|
|
57
|
+
const { host, instruction } = cliNetworkProxyTarget(deviceName);
|
|
58
|
+
if (host === cliNetworkProxyHost)
|
|
59
|
+
return;
|
|
60
|
+
try {
|
|
61
|
+
await networkCapture.startProxy({ host });
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
throw new Error(`Droid network proxy could not start: ${error.message}`, { cause: error });
|
|
65
|
+
}
|
|
66
|
+
cliNetworkProxyHost = host;
|
|
67
|
+
if (instruction)
|
|
68
|
+
console.log(instruction);
|
|
69
|
+
}
|
|
53
70
|
process.once("SIGTERM", async () => {
|
|
54
71
|
process.exitCode = 143;
|
|
55
72
|
await networkCapture?.shutdown().catch(() => { });
|
|
@@ -78,12 +95,23 @@ if (args._[0] === "skill") {
|
|
|
78
95
|
process.exit(1);
|
|
79
96
|
}
|
|
80
97
|
}
|
|
98
|
+
if (args._[0] === "ios-device") {
|
|
99
|
+
try {
|
|
100
|
+
process.exit(await runIosDeviceCommand({ args }));
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
104
|
+
console.error(`\niOS device command failed: ${message}`);
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
81
108
|
const reportMetadataPath = typeof args["report-metadata"] === "string"
|
|
82
109
|
&& args["report-metadata"].trim().length > 0
|
|
83
110
|
? args["report-metadata"].trim()
|
|
84
111
|
: null;
|
|
85
112
|
await clearLoadmillReportMetadata(reportMetadataPath);
|
|
86
113
|
const targetConfig = resolveHeadlessTarget(args);
|
|
114
|
+
const iosTeamId = resolveIosTeamId(args, targetConfig);
|
|
87
115
|
let avdName = targetConfig.avdName;
|
|
88
116
|
let platform = targetConfig.platform || null; // 'ios' or 'android'
|
|
89
117
|
const recordScreenshots = args["record"] || false;
|
|
@@ -413,14 +441,8 @@ async function main() {
|
|
|
413
441
|
process.exit(130);
|
|
414
442
|
});
|
|
415
443
|
}
|
|
416
|
-
if (networkCapture && !isBrowserTarget)
|
|
417
|
-
|
|
418
|
-
await networkCapture.startProxy();
|
|
419
|
-
}
|
|
420
|
-
catch (error) {
|
|
421
|
-
throw new Error(`Droid network proxy could not start: ${error.message}`, { cause: error });
|
|
422
|
-
}
|
|
423
|
-
}
|
|
444
|
+
if (networkCapture && !isBrowserTarget)
|
|
445
|
+
await startCliNetworkProxy(avdName);
|
|
424
446
|
let preloadedHeadlessConfig = null;
|
|
425
447
|
let preloadedHeadlessTarget = null;
|
|
426
448
|
let headlessInstructionRuns = [];
|
|
@@ -463,6 +485,8 @@ async function main() {
|
|
|
463
485
|
const selection = await selectDevice();
|
|
464
486
|
platform = selection.platform;
|
|
465
487
|
avdName = selection.deviceName;
|
|
488
|
+
if (networkCapture)
|
|
489
|
+
await startCliNetworkProxy(avdName);
|
|
466
490
|
}
|
|
467
491
|
let targetId;
|
|
468
492
|
let deviceInfo;
|
|
@@ -801,18 +825,18 @@ async function main() {
|
|
|
801
825
|
}
|
|
802
826
|
}
|
|
803
827
|
else {
|
|
804
|
-
targetId = await connectToDevice(avdName, platform);
|
|
828
|
+
targetId = await connectToDevice(avdName, platform, { teamId: iosTeamId });
|
|
829
|
+
const deviceKind = networkCapture ? cliNetworkCaptureDeviceKind(getConnectedDevicePlatform(), targetId) : null;
|
|
830
|
+
if (deviceKind === "android-device")
|
|
831
|
+
await startCliNetworkProxy(`adb:${targetId}`);
|
|
805
832
|
if (captureNetwork) {
|
|
806
|
-
|
|
807
|
-
if (connectedPlatform === "android") {
|
|
808
|
-
if (!/^emulator-\d+$/.test(targetId))
|
|
809
|
-
throw new Error("--capture-network supports Android Emulators, not physical Android devices.");
|
|
833
|
+
if (deviceKind === "android-emulator") {
|
|
810
834
|
const ca = await loadCertificateAuthority();
|
|
811
835
|
const trust = await ensureCliAndroidCertificate(targetId, ca);
|
|
812
836
|
if (trust.changed)
|
|
813
837
|
console.log(`Reapplied Droid's network certificate on ${targetId}.`);
|
|
814
838
|
}
|
|
815
|
-
else {
|
|
839
|
+
else if (deviceKind === "ios-simulator") {
|
|
816
840
|
await inspectNetworkDevice({ platform: "ios", deviceId: targetId });
|
|
817
841
|
}
|
|
818
842
|
}
|
|
@@ -5,6 +5,7 @@ import React, { useState } from 'react';
|
|
|
5
5
|
import { render, Box, Text, useInput, useApp } from 'ink';
|
|
6
6
|
import { exec } from "child_process";
|
|
7
7
|
import { promisify } from "util";
|
|
8
|
+
import { getConnectedIphones } from "./ios-device-list.js";
|
|
8
9
|
const execAsync = promisify(exec);
|
|
9
10
|
/**
|
|
10
11
|
* Interactive selection component with arrow key navigation
|
|
@@ -171,16 +172,18 @@ async function getIOSDevices() {
|
|
|
171
172
|
*/
|
|
172
173
|
export async function selectDevice() {
|
|
173
174
|
// Check what's available
|
|
174
|
-
const [androidDevices,
|
|
175
|
+
const [androidDevices, iphones, simulators] = await Promise.all([
|
|
175
176
|
getAndroidDevices(),
|
|
177
|
+
getConnectedIphones(),
|
|
176
178
|
getIOSDevices(),
|
|
177
179
|
]);
|
|
180
|
+
const iosDevices = [...iphones, ...simulators];
|
|
178
181
|
const hasAndroid = androidDevices.length > 0;
|
|
179
182
|
const hasIOS = iosDevices.length > 0;
|
|
180
183
|
if (!hasAndroid && !hasIOS) {
|
|
181
184
|
console.error("\nNo devices found!");
|
|
182
185
|
console.error(" Android: Connect a device or create an AVD, then run droid-cua again");
|
|
183
|
-
console.error(" iOS: Xcode Simulator
|
|
186
|
+
console.error(" iOS: Connect an iPhone by USB or make an Xcode Simulator available");
|
|
184
187
|
process.exit(1);
|
|
185
188
|
}
|
|
186
189
|
// Build platform options
|
|
@@ -193,9 +196,13 @@ export async function selectDevice() {
|
|
|
193
196
|
});
|
|
194
197
|
}
|
|
195
198
|
if (hasIOS) {
|
|
196
|
-
const runningCount =
|
|
199
|
+
const runningCount = simulators.filter((d) => d.running).length;
|
|
200
|
+
const counts = [
|
|
201
|
+
iphones.length > 0 ? `${iphones.length} iPhone(s)` : null,
|
|
202
|
+
simulators.length > 0 ? `${simulators.length} simulator(s)` : null,
|
|
203
|
+
].filter(Boolean).join(", ");
|
|
197
204
|
platformOptions.push({
|
|
198
|
-
label: `iOS${runningCount > 0 ? ` (${runningCount} running)` : ""} - ${
|
|
205
|
+
label: `iOS${runningCount > 0 ? ` (${runningCount} running)` : ""} - ${counts}`,
|
|
199
206
|
value: "ios",
|
|
200
207
|
});
|
|
201
208
|
}
|
|
@@ -211,11 +218,11 @@ export async function selectDevice() {
|
|
|
211
218
|
}
|
|
212
219
|
// Select device
|
|
213
220
|
const deviceList = platform === "ios" ? iosDevices : androidDevices;
|
|
214
|
-
const deviceType = platform === "ios" ? "Simulator" : "Android Target";
|
|
221
|
+
const deviceType = platform === "ios" ? (iphones.length > 0 ? "iOS Device" : "Simulator") : "Android Target";
|
|
215
222
|
let deviceName;
|
|
216
223
|
if (deviceList.length === 1) {
|
|
217
224
|
deviceName = deviceList[0].value;
|
|
218
|
-
console.log(`Using ${
|
|
225
|
+
console.log(`Using ${deviceList[0].label} (only available ${deviceType.toLowerCase()})\n`);
|
|
219
226
|
}
|
|
220
227
|
else {
|
|
221
228
|
const selected = await renderSelection(`Select ${deviceType}`, deviceList);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { CLOUD_BROWSER_FAMILIES, CLOUD_BROWSER_VERSION_ALIASES, LOCAL_BROWSER_FAMILIES, } from "../browser/cloud-browser-catalog.js";
|
|
2
|
+
import { parseIosDeviceToken } from "../device/ios/real-device/session.js";
|
|
2
3
|
function normalizeTarget(value) {
|
|
3
4
|
if (value == null || value === "") {
|
|
4
5
|
return "mobile";
|
|
@@ -332,6 +333,9 @@ export function resolveHeadlessTarget(args = {}) {
|
|
|
332
333
|
}
|
|
333
334
|
if (deviceSource === "local") {
|
|
334
335
|
assertNoCloudFlags(args);
|
|
336
|
+
if (platform === "android" && parseIosDeviceToken(avdName)) {
|
|
337
|
+
throw new Error("--avd ios-device:<udid> is an iPhone; use --platform ios or omit --platform.");
|
|
338
|
+
}
|
|
335
339
|
}
|
|
336
340
|
if (isCloudDeviceSource(deviceSource)) {
|
|
337
341
|
const deviceName = hasNonEmptyString(args["device-name"]) ? args["device-name"].trim() : null;
|
|
@@ -393,3 +397,23 @@ export function resolveHeadlessTarget(args = {}) {
|
|
|
393
397
|
listDevices: false,
|
|
394
398
|
};
|
|
395
399
|
}
|
|
400
|
+
const APPLE_TEAM_ID_PATTERN = /^[A-Z0-9]{10}$/;
|
|
401
|
+
export function normalizeIosTeamId(value) {
|
|
402
|
+
const teamId = readNonEmptyCliValue(value)?.toUpperCase() ?? null;
|
|
403
|
+
if (!teamId || !APPLE_TEAM_ID_PATTERN.test(teamId)) {
|
|
404
|
+
throw new Error("--ios-team must be a 10-character Apple team ID, for example ABCDE12345.");
|
|
405
|
+
}
|
|
406
|
+
return teamId;
|
|
407
|
+
}
|
|
408
|
+
export function resolveIosTeamId(args = {}, targetConfig = {}) {
|
|
409
|
+
if (args["ios-team"] == null) {
|
|
410
|
+
return null;
|
|
411
|
+
}
|
|
412
|
+
const teamId = normalizeIosTeamId(args["ios-team"]);
|
|
413
|
+
const isLocalMobile = targetConfig.target === "mobile" && targetConfig.deviceSource === "local";
|
|
414
|
+
const isOtherDevice = targetConfig.avdName != null && !parseIosDeviceToken(targetConfig.avdName);
|
|
415
|
+
if (!isLocalMobile || targetConfig.platform === "android" || isOtherDevice) {
|
|
416
|
+
throw new Error("--ios-team can only be used with a local iPhone (--avd ios-device:<udid> or ios-device:auto).");
|
|
417
|
+
}
|
|
418
|
+
return teamId;
|
|
419
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { runChecks as defaultRunChecks } from "../device/ios/real-device/readiness.js";
|
|
2
|
+
import { IOS_DEVICE_PREFIX, parseIosDeviceToken } from "../device/ios/real-device/session.js";
|
|
3
|
+
import { createStatusLine as defaultCreateStatusLine } from "./cloud-status-line.js";
|
|
4
|
+
import { normalizeIosTeamId } from "./headless-target.js";
|
|
5
|
+
const ALLOWED_OPTIONS = new Set(["_", "udid", "fix", "ios-team", "h", "help"]);
|
|
6
|
+
const SYMBOLS = {
|
|
7
|
+
pass: { text: "✔", color: 32 },
|
|
8
|
+
fail: { text: "✖", color: 31 },
|
|
9
|
+
warn: { text: "!", color: 33 },
|
|
10
|
+
blocked: { text: "·", color: 90 },
|
|
11
|
+
};
|
|
12
|
+
export function iosDeviceCommandHelp() {
|
|
13
|
+
return [
|
|
14
|
+
"Usage: droid-cua ios-device check [options]",
|
|
15
|
+
"",
|
|
16
|
+
"Check that a real iPhone connected by USB is ready for droid-cua tests",
|
|
17
|
+
"",
|
|
18
|
+
"Options:",
|
|
19
|
+
" --udid <udid> Check this iPhone (default: the first iPhone connected by USB)",
|
|
20
|
+
" --ios-team <TEAMID> Apple team that signs the helper app (default: DROID_CUA_IOS_TEAM_ID, or your only paid team)",
|
|
21
|
+
" --fix Set up signing and install the helper app (WebDriverAgent) when needed",
|
|
22
|
+
" -h, --help display help for command",
|
|
23
|
+
"",
|
|
24
|
+
"Examples:",
|
|
25
|
+
" droid-cua ios-device check",
|
|
26
|
+
" droid-cua ios-device check --udid 00008120-001C18501A81A01E --ios-team ABCDE12345 --fix",
|
|
27
|
+
"",
|
|
28
|
+
"Run tests on the iPhone with --avd ios-device:<udid> or --avd ios-device:auto.",
|
|
29
|
+
].join("\n");
|
|
30
|
+
}
|
|
31
|
+
function parseIosDeviceCommandArgs(args, env) {
|
|
32
|
+
const unknownOption = Object.keys(args).find((key) => !ALLOWED_OPTIONS.has(key));
|
|
33
|
+
if (unknownOption)
|
|
34
|
+
throw new Error(`Unknown ios-device option --${unknownOption}.`);
|
|
35
|
+
if (args.help === true || args.h === true || args._.length === 1) {
|
|
36
|
+
return { help: iosDeviceCommandHelp() };
|
|
37
|
+
}
|
|
38
|
+
if (args._[1] !== "check" || args._.length !== 2) {
|
|
39
|
+
throw new Error(`Unknown ios-device command "${args._.slice(1).join(" ")}". Expected: droid-cua ios-device check`);
|
|
40
|
+
}
|
|
41
|
+
if (args.fix != null && typeof args.fix !== "boolean") {
|
|
42
|
+
throw new Error("--fix does not accept a value.");
|
|
43
|
+
}
|
|
44
|
+
let udid = null;
|
|
45
|
+
if (args.udid != null) {
|
|
46
|
+
const value = typeof args.udid === "string" ? args.udid.trim() : "";
|
|
47
|
+
if (!value)
|
|
48
|
+
throw new Error("--udid requires a value.");
|
|
49
|
+
udid = parseIosDeviceToken(value) ? parseIosDeviceToken(value).udid : value;
|
|
50
|
+
}
|
|
51
|
+
const teamValue = args["ios-team"] ?? env.DROID_CUA_IOS_TEAM_ID;
|
|
52
|
+
const teamId = teamValue == null || teamValue === "" ? null : normalizeIosTeamId(teamValue);
|
|
53
|
+
return { udid, teamId, fix: args.fix === true };
|
|
54
|
+
}
|
|
55
|
+
function paint(text, code, color) {
|
|
56
|
+
return color ? `\u001b[${code}m${text}\u001b[39m` : text;
|
|
57
|
+
}
|
|
58
|
+
function symbolFor(result, color) {
|
|
59
|
+
const symbol = SYMBOLS[result.blockedBy ? "blocked" : result.status];
|
|
60
|
+
return paint(symbol.text, symbol.color, color);
|
|
61
|
+
}
|
|
62
|
+
function guidanceLines(result, { fix }) {
|
|
63
|
+
if (result.blockedBy || result.status === "pass")
|
|
64
|
+
return [];
|
|
65
|
+
const lines = [];
|
|
66
|
+
if (result.details) {
|
|
67
|
+
lines.push(result.status === "fail" ? `Fix: ${result.details}` : result.details);
|
|
68
|
+
}
|
|
69
|
+
if (result.id === "apple_team" && result.status === "fail" && /paid Apple teams/.test(result.summary)) {
|
|
70
|
+
lines.push("Choose the team with --ios-team <TEAMID> (or set DROID_CUA_IOS_TEAM_ID).");
|
|
71
|
+
}
|
|
72
|
+
if (!fix && result.remediation?.some((step) => step.kind === "run_fix")) {
|
|
73
|
+
lines.push("Run droid-cua ios-device check --fix to do it now (a test run also does it automatically).");
|
|
74
|
+
}
|
|
75
|
+
return lines.map((line) => ` ${line}`);
|
|
76
|
+
}
|
|
77
|
+
export function formatReadinessReport({ ready, results, context }, { fix = false, color = false } = {}) {
|
|
78
|
+
const device = context?.device;
|
|
79
|
+
const heading = device
|
|
80
|
+
? `iPhone readiness: ${device.name} (${device.model}, iOS ${device.iosVersion})`
|
|
81
|
+
: "iPhone readiness";
|
|
82
|
+
const lines = [heading, ""];
|
|
83
|
+
for (const result of results) {
|
|
84
|
+
lines.push(`${symbolFor(result, color)} ${result.title}: ${result.summary}`);
|
|
85
|
+
lines.push(...guidanceLines(result, { fix }));
|
|
86
|
+
}
|
|
87
|
+
lines.push("");
|
|
88
|
+
if (ready) {
|
|
89
|
+
lines.push(`Ready. Run tests on this iPhone with --avd ${IOS_DEVICE_PREFIX}${context.udid}`);
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
lines.push("Not ready. Fix the first ✖ above, then run droid-cua ios-device check again.");
|
|
93
|
+
}
|
|
94
|
+
return lines;
|
|
95
|
+
}
|
|
96
|
+
function shouldUseColor() {
|
|
97
|
+
return Boolean(process.stdout.isTTY) && !Object.hasOwn(process.env, "NO_COLOR");
|
|
98
|
+
}
|
|
99
|
+
export async function runIosDeviceCommand({ args, env = process.env, output = console, runChecks = defaultRunChecks, createStatusLine = defaultCreateStatusLine, color = shouldUseColor(), } = {}) {
|
|
100
|
+
const command = parseIosDeviceCommandArgs(args ?? { _: ["ios-device"] }, env);
|
|
101
|
+
if (command.help) {
|
|
102
|
+
output.log(command.help);
|
|
103
|
+
return 0;
|
|
104
|
+
}
|
|
105
|
+
const statusLine = createStatusLine();
|
|
106
|
+
statusLine.start(command.fix ? "Checking and preparing the iPhone" : "Checking the iPhone");
|
|
107
|
+
let result;
|
|
108
|
+
try {
|
|
109
|
+
result = await runChecks({
|
|
110
|
+
udid: command.udid,
|
|
111
|
+
teamId: command.teamId,
|
|
112
|
+
autoFix: command.fix,
|
|
113
|
+
onProgress: (progress) => statusLine.update(progress.message),
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
finally {
|
|
117
|
+
statusLine.stop();
|
|
118
|
+
}
|
|
119
|
+
output.log(formatReadinessReport(result, { fix: command.fix, color }).join("\n"));
|
|
120
|
+
return result.ready ? 0 : 1;
|
|
121
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { listDevices } from "../device/ios/real-device/devicectl.js";
|
|
2
|
+
import { IOS_DEVICE_PREFIX } from "../device/ios/real-device/session.js";
|
|
3
|
+
export function iosDeviceStateHint(device) {
|
|
4
|
+
if (device.pairingState !== "paired")
|
|
5
|
+
return "not trusted";
|
|
6
|
+
if (device.developerModeStatus === "disabled")
|
|
7
|
+
return "Developer Mode off";
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
export function iosDeviceLabel(device) {
|
|
11
|
+
const details = [device.model, device.iosVersion ? `iOS ${device.iosVersion}` : null].filter(Boolean).join(" · ");
|
|
12
|
+
const hint = iosDeviceStateHint(device);
|
|
13
|
+
return `${device.name || device.udid} — ${details ? `${details} ` : ""}(USB)${hint ? ` — ${hint}` : ""}`;
|
|
14
|
+
}
|
|
15
|
+
export async function getConnectedIphones({ list = listDevices } = {}) {
|
|
16
|
+
let devices;
|
|
17
|
+
try {
|
|
18
|
+
devices = await list();
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return [];
|
|
22
|
+
}
|
|
23
|
+
return devices
|
|
24
|
+
.filter((device) => device.transport === "wired" && device.udid)
|
|
25
|
+
.map((device) => ({
|
|
26
|
+
label: iosDeviceLabel(device),
|
|
27
|
+
value: `${IOS_DEVICE_PREFIX}${device.udid}`,
|
|
28
|
+
running: true,
|
|
29
|
+
realDevice: true,
|
|
30
|
+
}))
|
|
31
|
+
.sort((a, b) => a.label.localeCompare(b.label));
|
|
32
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { parseIosDeviceToken } from "../device/ios/real-device/session.js";
|
|
2
|
+
import { androidProxyInstruction, iphoneProxyInstruction, LAN_PROXY_HOST, macLanAddresses, NETWORK_PROXY_HOST } from "../network/local-proxy.js";
|
|
3
|
+
function isAndroidEmulatorSerial(serial) {
|
|
4
|
+
return /^emulator-\d+$/.test(serial || "");
|
|
5
|
+
}
|
|
6
|
+
function physicalAndroidSerial(deviceName) {
|
|
7
|
+
const serial = typeof deviceName === "string" && deviceName.startsWith("adb:") ? deviceName.slice(4) : null;
|
|
8
|
+
return serial && !isAndroidEmulatorSerial(serial) ? serial : null;
|
|
9
|
+
}
|
|
10
|
+
export function cliNetworkProxyTarget(deviceName, addresses = macLanAddresses()) {
|
|
11
|
+
if (parseIosDeviceToken(deviceName))
|
|
12
|
+
return { host: LAN_PROXY_HOST, instruction: iphoneProxyInstruction(addresses) };
|
|
13
|
+
if (physicalAndroidSerial(deviceName))
|
|
14
|
+
return { host: LAN_PROXY_HOST, instruction: androidProxyInstruction(addresses) };
|
|
15
|
+
return { host: NETWORK_PROXY_HOST, instruction: null };
|
|
16
|
+
}
|
|
17
|
+
export function cliNetworkCaptureDeviceKind(platform, targetId) {
|
|
18
|
+
if (platform === "android")
|
|
19
|
+
return isAndroidEmulatorSerial(targetId) ? "android-emulator" : "android-device";
|
|
20
|
+
return parseIosDeviceToken(targetId) ? "iphone" : "ios-simulator";
|
|
21
|
+
}
|