@houwert/conductor 0.30.0 → 0.32.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/dist/commands/install-app.js +16 -4
- package/dist/commands/list-apps.js +11 -0
- package/dist/commands/list-devices.js +25 -0
- package/dist/commands/metro.js +137 -1
- package/dist/commands/press-key.js +21 -3
- package/dist/commands/stop-device.js +13 -2
- package/dist/commands/swipe.js +4 -2
- package/dist/daemon/input-backends.js +26 -1
- package/dist/daemon/log-collector.js +6 -0
- package/dist/daemon/server.js +40 -7
- package/dist/drivers/bootstrap.js +253 -4
- package/dist/drivers/devicectl.js +243 -0
- package/dist/drivers/flow-runner.js +10 -1
- package/dist/drivers/ios.js +96 -4
- package/dist/index.js +10 -1
- package/dist/runner.js +16 -8
- package/package.json +1 -1
- package/skills/conductor-device-interact/SKILL.md +11 -1
- package/skills/conductor-device-setup/SKILL.md +28 -1
- package/skills/conductor-metro-debugger/SKILL.md +15 -0
package/dist/drivers/ios.js
CHANGED
|
@@ -1,4 +1,37 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
2
35
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
36
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
37
|
};
|
|
@@ -16,6 +49,7 @@ const promises_1 = __importDefault(require("fs/promises"));
|
|
|
16
49
|
const os_1 = __importDefault(require("os"));
|
|
17
50
|
const path_1 = __importDefault(require("path"));
|
|
18
51
|
const child_process_1 = require("child_process");
|
|
52
|
+
const devicectl = __importStar(require("./devicectl.js"));
|
|
19
53
|
/**
|
|
20
54
|
* How long a captured view hierarchy may be reused. Bounds the staleness of a
|
|
21
55
|
* cached snapshot when the screen changes without a driver-issued command
|
|
@@ -24,11 +58,18 @@ const child_process_1 = require("child_process");
|
|
|
24
58
|
*/
|
|
25
59
|
const HIERARCHY_CACHE_TTL_MS = 750;
|
|
26
60
|
class IOSDriver {
|
|
27
|
-
constructor(port = 1075, host = '127.0.0.1', deviceId, platform = 'ios'
|
|
61
|
+
constructor(port = 1075, host = '127.0.0.1', deviceId, platform = 'ios',
|
|
62
|
+
/**
|
|
63
|
+
* Physical devices route app lifecycle through devicectl instead of simctl,
|
|
64
|
+
* and can't offer the simulator-only conveniences (clipboard, location,
|
|
65
|
+
* TCC pre-approval, video capture).
|
|
66
|
+
*/
|
|
67
|
+
isPhysical = false) {
|
|
28
68
|
this.port = port;
|
|
29
69
|
this.host = host;
|
|
30
70
|
this.deviceId = deviceId;
|
|
31
71
|
this.platform = platform;
|
|
72
|
+
this.isPhysical = isPhysical;
|
|
32
73
|
this._recordingProcess = null;
|
|
33
74
|
/**
|
|
34
75
|
* Short-lived cache of the most recent view hierarchy, keyed by request
|
|
@@ -38,6 +79,12 @@ class IOSDriver {
|
|
|
38
79
|
*/
|
|
39
80
|
this.hierarchyCache = null;
|
|
40
81
|
}
|
|
82
|
+
/** Reject a simulator-only operation with a message that names the alternative. */
|
|
83
|
+
unsupportedOnDevice(operation, alternative) {
|
|
84
|
+
throw new Error(`${operation} is not supported on physical ${this.platform === 'tvos' ? 'tvOS' : 'iOS'} devices` +
|
|
85
|
+
(alternative ? ` — ${alternative}` : '') +
|
|
86
|
+
'.');
|
|
87
|
+
}
|
|
41
88
|
request(method, path, body) {
|
|
42
89
|
return new Promise((resolve, reject) => {
|
|
43
90
|
const bodyBuf = body !== undefined ? Buffer.from(JSON.stringify(body), 'utf-8') : undefined;
|
|
@@ -193,7 +240,18 @@ class IOSDriver {
|
|
|
193
240
|
for (const [key, value] of Object.entries(args ?? {})) {
|
|
194
241
|
argPairs.push(`-${key}`, value);
|
|
195
242
|
}
|
|
196
|
-
if (inject) {
|
|
243
|
+
if (this.isPhysical && (inject || argPairs.length > 0)) {
|
|
244
|
+
// devicectl is the device-side equivalent of `simctl launch`: it takes
|
|
245
|
+
// both launch arguments and an environment dictionary.
|
|
246
|
+
const deviceId = this.requireDeviceId();
|
|
247
|
+
await devicectl.launchApp(deviceId, bundleId, argPairs, inject
|
|
248
|
+
? {
|
|
249
|
+
DYLD_INSERT_LIBRARIES: inject.dylibPath,
|
|
250
|
+
CONDUCTOR_INPROC_PORT: String(inject.inprocPort),
|
|
251
|
+
}
|
|
252
|
+
: undefined);
|
|
253
|
+
}
|
|
254
|
+
else if (inject) {
|
|
197
255
|
// Injection requires simctl launch with SIMCTL_CHILD_ env — the XCTest
|
|
198
256
|
// /launchApp path only activates and can't set environment.
|
|
199
257
|
const deviceId = this.requireDeviceId();
|
|
@@ -220,6 +278,13 @@ class IOSDriver {
|
|
|
220
278
|
}
|
|
221
279
|
async clearAppState(bundleId) {
|
|
222
280
|
const deviceId = this.requireDeviceId();
|
|
281
|
+
if (this.isPhysical) {
|
|
282
|
+
// No get_app_container on device, so there's no bundle to reinstall from;
|
|
283
|
+
// the caller has to supply the .app again via install-app.
|
|
284
|
+
await devicectl.uninstallApp(deviceId, bundleId);
|
|
285
|
+
this.invalidateHierarchyCache();
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
223
288
|
// Terminate first to prevent app from saving state after clear
|
|
224
289
|
await this.simctl(['terminate', deviceId, bundleId]).catch(() => { });
|
|
225
290
|
// Capture the .app bundle path before uninstalling — uninstall deletes the UUID directory
|
|
@@ -240,26 +305,40 @@ class IOSDriver {
|
|
|
240
305
|
}
|
|
241
306
|
async uninstallApp(bundleId) {
|
|
242
307
|
const deviceId = this.requireDeviceId();
|
|
243
|
-
|
|
244
|
-
|
|
308
|
+
if (this.isPhysical) {
|
|
309
|
+
await devicectl.uninstallApp(deviceId, bundleId);
|
|
310
|
+
}
|
|
311
|
+
else {
|
|
312
|
+
await this.simctl(['terminate', deviceId, bundleId]).catch(() => { });
|
|
313
|
+
await this.simctl(['uninstall', deviceId, bundleId]);
|
|
314
|
+
}
|
|
245
315
|
this.invalidateHierarchyCache();
|
|
246
316
|
}
|
|
247
317
|
async clearKeychain() {
|
|
318
|
+
if (this.isPhysical)
|
|
319
|
+
this.unsupportedOnDevice('clear-keychain');
|
|
248
320
|
const deviceId = this.requireDeviceId();
|
|
249
321
|
await this.simctl(['keychain', deviceId, 'reset']);
|
|
250
322
|
}
|
|
251
323
|
async openLink(url) {
|
|
324
|
+
if (this.isPhysical) {
|
|
325
|
+
this.unsupportedOnDevice('open-link', 'devicectl has no openurl equivalent');
|
|
326
|
+
}
|
|
252
327
|
const deviceId = this.requireDeviceId();
|
|
253
328
|
await this.simctl(['openurl', deviceId, url]);
|
|
254
329
|
this.invalidateHierarchyCache();
|
|
255
330
|
}
|
|
256
331
|
/** Read the simulator's clipboard. Uses `xcrun simctl pbpaste <udid>`. */
|
|
257
332
|
async clipboardRead() {
|
|
333
|
+
if (this.isPhysical)
|
|
334
|
+
this.unsupportedOnDevice('Reading the clipboard');
|
|
258
335
|
const deviceId = this.requireDeviceId();
|
|
259
336
|
return this.simctlCapture(['pbpaste', deviceId]);
|
|
260
337
|
}
|
|
261
338
|
/** Write to the simulator's clipboard. Uses `xcrun simctl pbcopy <udid>` over stdin. */
|
|
262
339
|
async clipboardWrite(text) {
|
|
340
|
+
if (this.isPhysical)
|
|
341
|
+
this.unsupportedOnDevice('Writing the clipboard');
|
|
263
342
|
const deviceId = this.requireDeviceId();
|
|
264
343
|
await new Promise((resolve, reject) => {
|
|
265
344
|
const proc = (0, child_process_1.spawn)('xcrun', ['simctl', 'pbcopy', deviceId], {
|
|
@@ -275,6 +354,8 @@ class IOSDriver {
|
|
|
275
354
|
});
|
|
276
355
|
}
|
|
277
356
|
async setLocation(latitude, longitude) {
|
|
357
|
+
if (this.isPhysical)
|
|
358
|
+
this.unsupportedOnDevice('set-location');
|
|
278
359
|
const deviceId = this.requireDeviceId();
|
|
279
360
|
await this.simctl(['location', deviceId, 'set', `${latitude},${longitude}`]);
|
|
280
361
|
}
|
|
@@ -339,6 +420,12 @@ class IOSDriver {
|
|
|
339
420
|
mediaLibrary: 'media-library',
|
|
340
421
|
siri: 'siri',
|
|
341
422
|
};
|
|
423
|
+
// On device there's no TCC pre-approval path, so the runner's interruption
|
|
424
|
+
// monitor is the only thing that can answer permission dialogs.
|
|
425
|
+
if (this.isPhysical) {
|
|
426
|
+
await this.post('setPermissions', { permissions: expanded });
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
342
429
|
const deviceId = this.requireDeviceId();
|
|
343
430
|
if (allValue !== undefined) {
|
|
344
431
|
// Best-effort bulk grant/revoke. 'all' covers TCC-managed permissions but
|
|
@@ -370,6 +457,8 @@ class IOSDriver {
|
|
|
370
457
|
await this.post('setPermissions', { permissions: expanded });
|
|
371
458
|
}
|
|
372
459
|
async addMedia(filePath) {
|
|
460
|
+
if (this.isPhysical)
|
|
461
|
+
this.unsupportedOnDevice('add-media');
|
|
373
462
|
const deviceId = this.requireDeviceId();
|
|
374
463
|
await this.simctl(['addmedia', deviceId, filePath]);
|
|
375
464
|
}
|
|
@@ -380,6 +469,9 @@ class IOSDriver {
|
|
|
380
469
|
throw new Error('getAirplaneMode is not supported on iOS simulators');
|
|
381
470
|
}
|
|
382
471
|
async startRecording(outputPath) {
|
|
472
|
+
if (this.isPhysical) {
|
|
473
|
+
this.unsupportedOnDevice('Screen recording', 'capture stills with `conductor screenshot`');
|
|
474
|
+
}
|
|
383
475
|
const deviceId = this.requireDeviceId();
|
|
384
476
|
if (this._recordingProcess)
|
|
385
477
|
await this.stopRecording();
|
package/dist/index.js
CHANGED
|
@@ -229,6 +229,7 @@ async function main() {
|
|
|
229
229
|
'report',
|
|
230
230
|
'timeline',
|
|
231
231
|
'baselines',
|
|
232
|
+
'reset',
|
|
232
233
|
],
|
|
233
234
|
string: [
|
|
234
235
|
'device',
|
|
@@ -1219,8 +1220,16 @@ async function main() {
|
|
|
1219
1220
|
else if (sub === 'reload') {
|
|
1220
1221
|
exitCode = await (0, metro_js_1.metroReload)(opts, metroSession, { port, targetIndex });
|
|
1221
1222
|
}
|
|
1223
|
+
else if (sub === 'use') {
|
|
1224
|
+
exitCode = await (0, metro_js_1.metroUse)(opts, metroSession, {
|
|
1225
|
+
location: rest[1],
|
|
1226
|
+
appId: rest[2],
|
|
1227
|
+
reset: argv['reset'],
|
|
1228
|
+
});
|
|
1229
|
+
}
|
|
1222
1230
|
else {
|
|
1223
|
-
console.error('Usage: conductor metro <stop|reload> [--port N] [--target N]'
|
|
1231
|
+
console.error('Usage: conductor metro <stop|reload|use> [--port N] [--target N]\n' +
|
|
1232
|
+
' conductor metro use <port|host:port> [<appId>] [--reset]');
|
|
1224
1233
|
exitCode = 1;
|
|
1225
1234
|
}
|
|
1226
1235
|
break;
|
package/dist/runner.js
CHANGED
|
@@ -151,12 +151,16 @@ async function getDriver(sessionName = 'default') {
|
|
|
151
151
|
(0, verbose_js_1.log)(`getDriver: platform=${platform} deviceId=${deviceId} port=${port || '(deferred)'}`);
|
|
152
152
|
let driver;
|
|
153
153
|
if (platform === 'ios') {
|
|
154
|
-
|
|
154
|
+
// Physical devices serve the driver from their own loopback, so the CLI
|
|
155
|
+
// has to reach them over the network instead of the host's.
|
|
156
|
+
const host = await (0, bootstrap_js_1.resolveDriverHost)(deviceId);
|
|
157
|
+
if (!(await (0, bootstrap_js_1.isPortOpen)(port, host))) {
|
|
155
158
|
(0, verbose_js_1.log)(`Driver not running — starting daemon for ${deviceId}...`);
|
|
156
159
|
await (0, client_js_1.startDaemon)(deviceId);
|
|
157
|
-
await waitForPort(port);
|
|
160
|
+
await waitForPort(port, undefined, undefined, host);
|
|
158
161
|
}
|
|
159
|
-
const
|
|
162
|
+
const isPhysical = (await (0, bootstrap_js_1.detectDeviceKind)(deviceId)) === 'physical';
|
|
163
|
+
const iosDriver = new ios_js_1.IOSDriver(port, host, deviceId, 'ios', isPhysical);
|
|
160
164
|
if (!(await iosDriver.isAlive())) {
|
|
161
165
|
throw new Error(`iOS XCTest driver on port ${port} is not responding.\n` +
|
|
162
166
|
`Run: conductor daemon-start --device ${deviceId}`);
|
|
@@ -164,12 +168,16 @@ async function getDriver(sessionName = 'default') {
|
|
|
164
168
|
driver = iosDriver;
|
|
165
169
|
}
|
|
166
170
|
else if (platform === 'tvos') {
|
|
167
|
-
|
|
171
|
+
// Physical devices serve the driver from their own loopback, so the CLI
|
|
172
|
+
// has to reach them over the network instead of the host's.
|
|
173
|
+
const host = await (0, bootstrap_js_1.resolveDriverHost)(deviceId);
|
|
174
|
+
if (!(await (0, bootstrap_js_1.isPortOpen)(port, host))) {
|
|
168
175
|
(0, verbose_js_1.log)(`tvOS driver not running — starting daemon for ${deviceId}...`);
|
|
169
176
|
await (0, client_js_1.startDaemon)(deviceId);
|
|
170
|
-
await waitForPort(port);
|
|
177
|
+
await waitForPort(port, undefined, undefined, host);
|
|
171
178
|
}
|
|
172
|
-
const
|
|
179
|
+
const isPhysical = (await (0, bootstrap_js_1.detectDeviceKind)(deviceId)) === 'physical';
|
|
180
|
+
const tvosDriver = new ios_js_1.IOSDriver(port, host, deviceId, 'tvos', isPhysical);
|
|
173
181
|
if (!(await tvosDriver.isAlive())) {
|
|
174
182
|
throw new Error(`tvOS XCTest driver on port ${port} is not responding.\n` +
|
|
175
183
|
`Run: conductor daemon-start --device ${deviceId}`);
|
|
@@ -403,10 +411,10 @@ async function spawnCommand(cmd, args, options) {
|
|
|
403
411
|
});
|
|
404
412
|
}
|
|
405
413
|
/** Poll until a TCP port is open, or throw after timeout. */
|
|
406
|
-
async function waitForPort(port, timeoutMs = 180000, pollMs = 500) {
|
|
414
|
+
async function waitForPort(port, timeoutMs = 180000, pollMs = 500, host = '127.0.0.1') {
|
|
407
415
|
const deadline = Date.now() + timeoutMs;
|
|
408
416
|
while (Date.now() < deadline) {
|
|
409
|
-
if (await (0, bootstrap_js_1.isPortOpen)(port))
|
|
417
|
+
if (await (0, bootstrap_js_1.isPortOpen)(port, host))
|
|
410
418
|
return;
|
|
411
419
|
await new Promise((r) => setTimeout(r, pollMs));
|
|
412
420
|
}
|
package/package.json
CHANGED
|
@@ -41,7 +41,7 @@ conductor assert-visible "Dashboard"
|
|
|
41
41
|
| `conductor copy-text-from <element>` | Print an element's text (and copy to the iOS clipboard) |
|
|
42
42
|
| `conductor input-text <text>` | Type into the focused field |
|
|
43
43
|
| `conductor erase-text [n]` | Erase n characters (default 50) |
|
|
44
|
-
| `conductor press-key <key>` | Press a key (Enter, Backspace, Home, …) or a remote button (`Remote Dpad Up/Down/Left/Right/Center`, `Remote Menu`) for tvOS / Android TV / vega / roku. `--long-press` / `--duration <seconds>` holds it; `--measure` times the response (see `conductor-profiler`) |
|
|
44
|
+
| `conductor press-key <key>` | Press a key (Enter, Backspace, Home, …) or a remote button (`Remote Dpad Up/Down/Left/Right/Center`, `Remote Menu`, `Remote Page Up/Down` on tvOS and Android TV, and `Remote Guide` on tvOS) for tvOS / Android TV / vega / roku. `--long-press` / `--duration <seconds>` holds it; `--measure` times the response (see `conductor-profiler`) |
|
|
45
45
|
| `conductor hide-keyboard` | Dismiss the on-screen keyboard |
|
|
46
46
|
| `conductor back` | Press back |
|
|
47
47
|
| `conductor scroll [--direction down\|up\|left\|right]` | Scroll |
|
|
@@ -127,6 +127,16 @@ relaunch without the flag. (See `conductor-device-setup`.)
|
|
|
127
127
|
|
|
128
128
|
- `--device <id>` / `--device-name <name>` targets a device; `--platform <ios|android|tvos|web|vega|roku>` scopes by platform.
|
|
129
129
|
- Vega (Amazon Fire TV) is D-pad driven: navigate with `press-key "Remote Dpad …"`; coordinate `tap-on` also works. `open-link`, `set-location`, gestures, and clipboard are unsupported. See `conductor-device-setup`.
|
|
130
|
+
- Apple TV (tvOS) is focus-driven and has **no touch surface automation**: XCTest
|
|
131
|
+
refuses remote swipe gestures ("Swipe events are only implemented for iOS,
|
|
132
|
+
visionOS, and watchOS"), so `swipe`/`scroll` are unavailable. Navigate with
|
|
133
|
+
`press-key "Remote Dpad Up/Down/Left/Right"` and `"Remote Dpad Center"`; for
|
|
134
|
+
long lists use `press-key "Remote Page Up"` / `"Remote Page Down"` (tvOS 14.3+;
|
|
135
|
+
also mapped on Android TV), which move a screenful at a time when the app
|
|
136
|
+
honours them. `"Remote Guide"` (14.3+) and
|
|
137
|
+
`"Remote TV Provider"` / `"Remote One Two Three"` / `"Remote Four Colors"`
|
|
138
|
+
(18.1+) are also available. `--duration <seconds>` holds a button for
|
|
139
|
+
accelerated scrolling.
|
|
130
140
|
- Roku is D-pad only — there is no touch. `tap-on <selector>` resolves the element but presses `Select`, which activates whatever currently holds **focus**, so navigate focus onto the target with `press-key "Remote Dpad …"` first and use `tap-on` to confirm. `scroll`/`swipe` become repeated D-pad presses in the direction the content moves. `open-link` needs an app id (it becomes a channel launch parameter). Only sideloaded dev-mode channels are inspectable. See `conductor-device-setup`.
|
|
131
141
|
- Add `--json` for machine-readable output; failed assertions exit non-zero.
|
|
132
142
|
- Run a per-session daemon for many commands (see `conductor-device-setup`).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: conductor-device-setup
|
|
3
|
-
description: Boot, list, and manage devices and app installs for the conductor CLI — iOS simulators, Android emulators, tvOS simulators, Vega (Amazon Fire TV) virtual devices, Roku devices, and Playwright web browsers — plus sessions, the warm-driver daemon, and the parallel device pool. Use when starting or stopping a simulator/emulator/browser, attaching to a Vega VVD or a Roku device, installing or launching an app, setting up the web driver, attaching to an already-running browser over CDP (e.g. an Electron app / its webviews), keeping the driver warm, or coordinating multiple devices for parallel agents.
|
|
3
|
+
description: Boot, list, and manage devices and app installs for the conductor CLI — iOS simulators, Android emulators, tvOS simulators, physical iOS/tvOS devices, Vega (Amazon Fire TV) virtual devices, Roku devices, and Playwright web browsers — plus sessions, the warm-driver daemon, and the parallel device pool. Use when starting or stopping a simulator/emulator/browser, attaching to a Vega VVD or a Roku device, driving a physical iPhone/iPad/Apple TV, installing or launching an app, setting up the web driver, attaching to an already-running browser over CDP (e.g. an Electron app / its webviews), keeping the driver warm, or coordinating multiple devices for parallel agents.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Conductor — device & app setup
|
|
@@ -102,6 +102,33 @@ Unsupported on Roku: `install-app`/`uninstall-app` (sideload via the device's de
|
|
|
102
102
|
web server at `http://<device-ip>`), `list-apps`, `clear-state`, gestures,
|
|
103
103
|
screen recording, clipboard, `set-location`, memory/CPU profiling, and device logs.
|
|
104
104
|
|
|
105
|
+
### Physical iOS / tvOS devices
|
|
106
|
+
|
|
107
|
+
Real iPhones, iPads, and Apple TVs work alongside simulators. They're discovered
|
|
108
|
+
through `devicectl`, show up in `list-devices` with status `connected`, and are
|
|
109
|
+
addressed by their CoreDevice identifier (`--device <uuid>`).
|
|
110
|
+
|
|
111
|
+
Requirements:
|
|
112
|
+
|
|
113
|
+
- The device is **paired** with this Mac and on the **same network** — conductor
|
|
114
|
+
reaches the driver over the LAN, not the host's loopback.
|
|
115
|
+
- **Developer Mode** is enabled on the device.
|
|
116
|
+
- A signing team: conductor builds and signs the XCTest driver locally on first
|
|
117
|
+
use (a few minutes; cached per team in `~/.conductor/<platform>-driver-device/`).
|
|
118
|
+
Set `CONDUCTOR_TEAM_ID=<team>` when the Mac has more than one development team
|
|
119
|
+
— conductor refuses to guess rather than sign with the wrong one.
|
|
120
|
+
|
|
121
|
+
Everything driven through the XCTest driver behaves the same as on a simulator:
|
|
122
|
+
`inspect`, `capture-ui`, `tap-on`, `press-key`, `swipe`, `launch-app`,
|
|
123
|
+
`terminate-app`, `install-app`, `uninstall-app`, flows.
|
|
124
|
+
|
|
125
|
+
Simulator-only — these fail with an explicit message on a physical device:
|
|
126
|
+
`set-location`, `open-link`, clipboard read/write, `clear-keychain`, `add-media`,
|
|
127
|
+
screen recording and the live video stream, and OS log collection (`conductor
|
|
128
|
+
logs` still gets **Metro** logs, which is the useful source for React Native).
|
|
129
|
+
`clear-state` uninstalls on device without reinstalling — reinstall with
|
|
130
|
+
`install-app` afterwards.
|
|
131
|
+
|
|
105
132
|
## App lifecycle
|
|
106
133
|
|
|
107
134
|
| Command | Purpose |
|
|
@@ -54,6 +54,21 @@ running indefinitely.
|
|
|
54
54
|
|---|---|
|
|
55
55
|
| `conductor metro reload [--port N] [--target N]` | Reload the JS bundle without restarting native |
|
|
56
56
|
| `conductor metro stop [--port N]` | Stop the Metro bundler on a port (default 8081) |
|
|
57
|
+
| `conductor metro use <port\|host:port> [<appId>]` | Point the app at a specific Metro (iOS/tvOS simulators) |
|
|
58
|
+
| `conductor metro use --reset [<appId>]` | Drop that override, back to the compiled-in port |
|
|
59
|
+
|
|
60
|
+
An app asks for the Metro port compiled into React-Core from `RCT_METRO_PORT` at
|
|
61
|
+
pod-install time, so a build can end up asking for a port nothing is serving —
|
|
62
|
+
typically a git worktree whose Metro runs elsewhere, or a pod install from a
|
|
63
|
+
shell that lacked the env var. Symptom: the app never loads a bundle (or keeps
|
|
64
|
+
running a stale one) while `conductor metro reload` works fine against the port
|
|
65
|
+
you expect. `metro use` overrides it via the `RCT_jsLocation` preference, so no
|
|
66
|
+
recompile is needed and the setting survives reinstalls. **Relaunch the app**
|
|
67
|
+
afterwards — it is read when the bridge starts. The app id defaults to the
|
|
68
|
+
current session's.
|
|
69
|
+
|
|
70
|
+
Android has no such preference; map the port with
|
|
71
|
+
`adb -s <serial> reverse tcp:8081 tcp:<metro-port>` instead.
|
|
57
72
|
|
|
58
73
|
## Tips
|
|
59
74
|
|