@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
|
@@ -8,6 +8,7 @@ const sdk_js_1 = require("../android/sdk.js");
|
|
|
8
8
|
const session_js_1 = require("../session.js");
|
|
9
9
|
const output_js_1 = require("../output.js");
|
|
10
10
|
const bootstrap_js_1 = require("../drivers/bootstrap.js");
|
|
11
|
+
const devicectl_js_1 = require("../drivers/devicectl.js");
|
|
11
12
|
const cli_js_1 = require("../drivers/vega/cli.js");
|
|
12
13
|
async function resolveDeviceId(sessionName) {
|
|
13
14
|
if (sessionName !== 'default')
|
|
@@ -46,10 +47,21 @@ async function installApp(appPath, opts = {}, sessionName = 'default') {
|
|
|
46
47
|
}
|
|
47
48
|
}
|
|
48
49
|
else if (platform === 'ios' || platform === 'tvos') {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
if ((await (0, bootstrap_js_1.detectDeviceKind)(deviceId)) === 'physical') {
|
|
51
|
+
try {
|
|
52
|
+
await (0, devicectl_js_1.installApp)(deviceId, appPath);
|
|
53
|
+
}
|
|
54
|
+
catch (e) {
|
|
55
|
+
(0, output_js_1.printError)(`install-app failed: ${e instanceof Error ? e.message : String(e)}`, opts);
|
|
56
|
+
return 1;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
const result = await (0, runner_js_1.spawnCommand)('xcrun', ['simctl', 'install', deviceId, appPath]);
|
|
61
|
+
if (!result.success) {
|
|
62
|
+
(0, output_js_1.printError)(`install-app failed: ${result.stderr}`, opts);
|
|
63
|
+
return 1;
|
|
64
|
+
}
|
|
53
65
|
}
|
|
54
66
|
}
|
|
55
67
|
else {
|
|
@@ -8,6 +8,7 @@ const sdk_js_1 = require("../android/sdk.js");
|
|
|
8
8
|
const session_js_1 = require("../session.js");
|
|
9
9
|
const output_js_1 = require("../output.js");
|
|
10
10
|
const bootstrap_js_1 = require("../drivers/bootstrap.js");
|
|
11
|
+
const devicectl_js_1 = require("../drivers/devicectl.js");
|
|
11
12
|
const cli_js_1 = require("../drivers/vega/cli.js");
|
|
12
13
|
async function resolveDeviceId(sessionName) {
|
|
13
14
|
if (sessionName !== 'default')
|
|
@@ -38,6 +39,16 @@ async function listApps(opts = {}, sessionName = 'default') {
|
|
|
38
39
|
// Vega is driven through Amazon's CLI, not adb; strip the `vega:` id prefix.
|
|
39
40
|
appIds = (await new cli_js_1.VegaCli(deviceId.replace(/^vega:/, '')).listInstalledApps()).sort();
|
|
40
41
|
}
|
|
42
|
+
else if ((platform === 'ios' || platform === 'tvos') &&
|
|
43
|
+
(await (0, bootstrap_js_1.detectDeviceKind)(deviceId)) === 'physical') {
|
|
44
|
+
try {
|
|
45
|
+
appIds = (await (0, devicectl_js_1.listApps)(deviceId)).map((a) => a.id).sort();
|
|
46
|
+
}
|
|
47
|
+
catch (e) {
|
|
48
|
+
(0, output_js_1.printError)(`list-apps failed: ${e instanceof Error ? e.message : String(e)}`, opts);
|
|
49
|
+
return 1;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
41
52
|
else if (platform === 'ios' || platform === 'tvos') {
|
|
42
53
|
const result = await (0, runner_js_1.spawnCommand)('bash', [
|
|
43
54
|
'-c',
|
|
@@ -18,6 +18,7 @@ const protocol_js_1 = require("../daemon/protocol.js");
|
|
|
18
18
|
const cli_js_1 = require("../drivers/vega/cli.js");
|
|
19
19
|
const discovery_js_1 = require("../drivers/roku/discovery.js");
|
|
20
20
|
const cdp_discovery_js_1 = require("../drivers/cdp-discovery.js");
|
|
21
|
+
const devicectl_js_1 = require("../drivers/devicectl.js");
|
|
21
22
|
// Captured during discoverAvailableDevices so listDevices() can report it.
|
|
22
23
|
// Module-scoped because the discover function returns Device[]; bolting an
|
|
23
24
|
// extra return field onto the public type would ripple beyond this fix.
|
|
@@ -110,6 +111,18 @@ async function discoverBootedDevices() {
|
|
|
110
111
|
}
|
|
111
112
|
}
|
|
112
113
|
}
|
|
114
|
+
// Physical iOS/tvOS devices: paired and reachable is the closest analogue to
|
|
115
|
+
// a booted simulator — that's when conductor can actually drive them.
|
|
116
|
+
for (const d of await (0, devicectl_js_1.listPhysicalDevices)()) {
|
|
117
|
+
if (!d.available)
|
|
118
|
+
continue;
|
|
119
|
+
devices.push({
|
|
120
|
+
id: d.identifier,
|
|
121
|
+
name: d.name,
|
|
122
|
+
platform: d.platform,
|
|
123
|
+
status: d.developerModeEnabled ? 'connected' : 'developer mode off',
|
|
124
|
+
});
|
|
125
|
+
}
|
|
113
126
|
// Vega (Amazon Fire TV): booted devices reported by the vega CLI. Best-effort —
|
|
114
127
|
// the CLI is absent unless the Vega SDK is installed.
|
|
115
128
|
try {
|
|
@@ -166,6 +179,18 @@ async function discoverAvailableDevices() {
|
|
|
166
179
|
// ignore parse errors
|
|
167
180
|
}
|
|
168
181
|
}
|
|
182
|
+
// Physical devices that aren't currently reachable — listed so users can see
|
|
183
|
+
// why a device they expect is missing.
|
|
184
|
+
for (const d of await (0, devicectl_js_1.listPhysicalDevices)()) {
|
|
185
|
+
if (d.available)
|
|
186
|
+
continue;
|
|
187
|
+
devices.push({
|
|
188
|
+
id: d.identifier,
|
|
189
|
+
name: d.name,
|
|
190
|
+
platform: d.platform,
|
|
191
|
+
status: 'unavailable',
|
|
192
|
+
});
|
|
193
|
+
}
|
|
169
194
|
// Android: list available AVDs
|
|
170
195
|
const emu = await (0, runner_js_1.spawnCommand)((0, sdk_js_1.resolveAndroidTool)('emulator'), ['-list-avds'], {
|
|
171
196
|
env: (0, sdk_js_1.androidSpawnEnv)(),
|
package/dist/commands/metro.js
CHANGED
|
@@ -3,12 +3,18 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.HELP = void 0;
|
|
4
4
|
exports.metroStop = metroStop;
|
|
5
5
|
exports.metroReload = metroReload;
|
|
6
|
+
exports.normalizeMetroLocation = normalizeMetroLocation;
|
|
7
|
+
exports.metroUse = metroUse;
|
|
6
8
|
exports.HELP = ` metro stop [--port N] Stop the Metro bundler process on a port (default 8081)
|
|
7
|
-
metro reload [--port N] [--target N] Reload the JS bundle without restarting native
|
|
9
|
+
metro reload [--port N] [--target N] Reload the JS bundle without restarting native
|
|
10
|
+
metro use <port|host:port> [<appId>] Point an RN app at a Metro other than its compiled-in one
|
|
11
|
+
metro use --reset [<appId>] Drop the override, back to the compiled-in port`;
|
|
8
12
|
const child_process_1 = require("child_process");
|
|
9
13
|
const output_js_1 = require("../output.js");
|
|
10
14
|
const metro_cdp_js_1 = require("../drivers/metro-cdp.js");
|
|
11
15
|
const bootstrap_js_1 = require("../drivers/bootstrap.js");
|
|
16
|
+
const runner_js_1 = require("../runner.js");
|
|
17
|
+
const session_js_1 = require("../session.js");
|
|
12
18
|
async function pidsOnPort(port) {
|
|
13
19
|
return new Promise((resolve) => {
|
|
14
20
|
const proc = (0, child_process_1.spawn)('lsof', ['-ti', `tcp:${port}`], {
|
|
@@ -109,3 +115,133 @@ async function metroReload(opts, sessionName, metroOpts) {
|
|
|
109
115
|
}
|
|
110
116
|
}
|
|
111
117
|
}
|
|
118
|
+
/**
|
|
119
|
+
* NSUserDefaults key RCTBundleURLProvider reads before falling back to the
|
|
120
|
+
* RCT_METRO_PORT baked into React-Core at pod-install time. Writing it is the
|
|
121
|
+
* only way to move a build onto another port without recompiling.
|
|
122
|
+
*/
|
|
123
|
+
const JS_LOCATION_KEY = 'RCT_jsLocation';
|
|
124
|
+
/**
|
|
125
|
+
* `8102` → `localhost:8102`. Also tolerates a pasted `http://host:port/`.
|
|
126
|
+
*
|
|
127
|
+
* Takes a number too: minimist turns a bare port argument into one.
|
|
128
|
+
*/
|
|
129
|
+
function normalizeMetroLocation(raw) {
|
|
130
|
+
const trimmed = String(raw)
|
|
131
|
+
.trim()
|
|
132
|
+
.replace(/^https?:\/\//, '')
|
|
133
|
+
.replace(/\/+$/, '');
|
|
134
|
+
const match = trimmed.match(/^(?:([A-Za-z0-9._-]+):)?(\d+)$/);
|
|
135
|
+
if (!match) {
|
|
136
|
+
throw new Error(`invalid location "${raw}" — expected <port> or <host:port>`);
|
|
137
|
+
}
|
|
138
|
+
const [, host = 'localhost', rawPort] = match;
|
|
139
|
+
const port = Number(rawPort);
|
|
140
|
+
if (port < 1 || port > 65535) {
|
|
141
|
+
throw new Error(`invalid port "${rawPort}" in "${raw}" — expected 1-65535`);
|
|
142
|
+
}
|
|
143
|
+
return `${host}:${port}`;
|
|
144
|
+
}
|
|
145
|
+
async function isPackagerRunning(location) {
|
|
146
|
+
try {
|
|
147
|
+
const res = await fetch(`http://${location}/status`, {
|
|
148
|
+
signal: AbortSignal.timeout(2000),
|
|
149
|
+
});
|
|
150
|
+
return res.ok && (await res.text()).includes('packager-status:running');
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Point a React Native app at a specific Metro by writing RCTBundleURLProvider's
|
|
158
|
+
* `RCT_jsLocation` default into the app's preferences on a simulator.
|
|
159
|
+
*
|
|
160
|
+
* The port an app asks for is compiled into React-Core from `RCT_METRO_PORT` at
|
|
161
|
+
* pod-install time, so a build made against the wrong port (a git worktree with
|
|
162
|
+
* its own Metro, a pod install from a shell missing the env var) keeps asking
|
|
163
|
+
* for the old one until it is recompiled. This override survives reinstalls and
|
|
164
|
+
* needs no rebuild.
|
|
165
|
+
*/
|
|
166
|
+
async function metroUse(opts, sessionName, useOpts) {
|
|
167
|
+
const deviceId = sessionName && sessionName !== 'default' ? sessionName : await (0, runner_js_1.detectFirstDevice)();
|
|
168
|
+
if (!deviceId) {
|
|
169
|
+
(0, output_js_1.printError)('metro use: no device found. Pass --device <id> or boot a simulator.', opts);
|
|
170
|
+
return 1;
|
|
171
|
+
}
|
|
172
|
+
const platform = await (0, bootstrap_js_1.detectPlatform)(deviceId).catch(() => undefined);
|
|
173
|
+
if (platform !== 'ios' && platform !== 'tvos') {
|
|
174
|
+
(0, output_js_1.printError)(`metro use: only iOS/tvOS simulators are supported (device is ${platform ?? 'unknown'}).\n` +
|
|
175
|
+
'On Android, map the port instead: adb -s <serial> reverse tcp:8081 tcp:<metro-port>', opts);
|
|
176
|
+
return 1;
|
|
177
|
+
}
|
|
178
|
+
const kind = await (0, bootstrap_js_1.detectDeviceKind)(deviceId).catch(() => undefined);
|
|
179
|
+
if (kind === 'physical') {
|
|
180
|
+
(0, output_js_1.printError)('metro use: physical devices read their packager host from the bundled ip.txt, ' +
|
|
181
|
+
'not from simulator defaults. Set it from the in-app dev menu instead.', opts);
|
|
182
|
+
return 1;
|
|
183
|
+
}
|
|
184
|
+
const appId = useOpts.appId ?? (await (0, session_js_1.getSession)(sessionName)).appId;
|
|
185
|
+
if (!appId) {
|
|
186
|
+
(0, output_js_1.printError)('metro use: no appId given and no active session. Run launch-app first.', opts);
|
|
187
|
+
return 1;
|
|
188
|
+
}
|
|
189
|
+
if (useOpts.reset) {
|
|
190
|
+
// `defaults delete` exits non-zero when the key was never written, which is
|
|
191
|
+
// the same end state the caller asked for.
|
|
192
|
+
await (0, runner_js_1.spawnCommand)('xcrun', [
|
|
193
|
+
'simctl',
|
|
194
|
+
'spawn',
|
|
195
|
+
deviceId,
|
|
196
|
+
'defaults',
|
|
197
|
+
'delete',
|
|
198
|
+
appId,
|
|
199
|
+
JS_LOCATION_KEY,
|
|
200
|
+
]);
|
|
201
|
+
if (opts.json)
|
|
202
|
+
(0, output_js_1.printData)({ appId, deviceId, location: null, reset: true }, opts);
|
|
203
|
+
else
|
|
204
|
+
(0, output_js_1.printSuccess)(`Cleared Metro override for "${appId}" — relaunch the app to apply`, opts);
|
|
205
|
+
return 0;
|
|
206
|
+
}
|
|
207
|
+
if (!useOpts.location) {
|
|
208
|
+
(0, output_js_1.printError)('Usage: conductor metro use <port|host:port> [<appId>] | metro use --reset', opts);
|
|
209
|
+
return 1;
|
|
210
|
+
}
|
|
211
|
+
let location;
|
|
212
|
+
try {
|
|
213
|
+
location = normalizeMetroLocation(useOpts.location);
|
|
214
|
+
}
|
|
215
|
+
catch (err) {
|
|
216
|
+
(0, output_js_1.printError)(`metro use: ${err instanceof Error ? err.message : String(err)}`, opts);
|
|
217
|
+
return 1;
|
|
218
|
+
}
|
|
219
|
+
const running = await isPackagerRunning(location);
|
|
220
|
+
const result = await (0, runner_js_1.spawnCommand)('xcrun', [
|
|
221
|
+
'simctl',
|
|
222
|
+
'spawn',
|
|
223
|
+
deviceId,
|
|
224
|
+
'defaults',
|
|
225
|
+
'write',
|
|
226
|
+
appId,
|
|
227
|
+
JS_LOCATION_KEY,
|
|
228
|
+
location,
|
|
229
|
+
]);
|
|
230
|
+
if (!result.success) {
|
|
231
|
+
(0, output_js_1.printError)(`metro use: could not write ${JS_LOCATION_KEY} for "${appId}"\n${result.stderr}`, opts);
|
|
232
|
+
return 1;
|
|
233
|
+
}
|
|
234
|
+
if (!running) {
|
|
235
|
+
// RCTBundleURLProvider drops a stored location when nothing answers there,
|
|
236
|
+
// so the app would silently fall back to its compiled-in port.
|
|
237
|
+
process.stderr.write(`warning: no packager answered at ${location}; the app falls back to its ` +
|
|
238
|
+
'compiled-in port until Metro is up there.\n');
|
|
239
|
+
}
|
|
240
|
+
if (opts.json) {
|
|
241
|
+
(0, output_js_1.printData)({ appId, deviceId, location, packagerRunning: running, reset: false }, opts);
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
(0, output_js_1.printSuccess)(`"${appId}" → ${location} — relaunch the app to apply`, opts);
|
|
245
|
+
}
|
|
246
|
+
return 0;
|
|
247
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.ANDROID_KEYCODE = exports.VALID_KEYS = exports.HELP = void 0;
|
|
3
|
+
exports.ANDROID_KEYCODE = exports.TVOS_REMOTE_BUTTONS = exports.VALID_KEYS = exports.HELP = void 0;
|
|
4
4
|
exports.dispatchKey = dispatchKey;
|
|
5
5
|
exports.pressKey = pressKey;
|
|
6
6
|
exports.HELP = ` press-key <key> Press a key (Enter, Backspace, Home, ...)
|
|
@@ -59,6 +59,12 @@ exports.VALID_KEYS = [
|
|
|
59
59
|
'Remote Info',
|
|
60
60
|
'Remote Instant Replay',
|
|
61
61
|
'Remote Search',
|
|
62
|
+
'Remote Page Up',
|
|
63
|
+
'Remote Page Down',
|
|
64
|
+
'Remote Guide',
|
|
65
|
+
'Remote TV Provider',
|
|
66
|
+
'Remote One Two Three',
|
|
67
|
+
'Remote Four Colors',
|
|
62
68
|
];
|
|
63
69
|
// iOS XCTest pressKey accepts these values (maps to XCUIKeyboardKey)
|
|
64
70
|
const IOS_KEY_MAP = {
|
|
@@ -74,7 +80,9 @@ const IOS_BUTTON_MAP = {
|
|
|
74
80
|
Power: 'lock',
|
|
75
81
|
};
|
|
76
82
|
// tvOS remote: map key names to pressButton values
|
|
77
|
-
|
|
83
|
+
// Page Up/Down and Guide need tvOS 14.3; the last three need tvOS 18.1. The
|
|
84
|
+
// driver reports a precondition error when the device's OS is older.
|
|
85
|
+
exports.TVOS_REMOTE_BUTTONS = {
|
|
78
86
|
'Remote Dpad Up': 'up',
|
|
79
87
|
'Remote Dpad Down': 'down',
|
|
80
88
|
'Remote Dpad Left': 'left',
|
|
@@ -82,6 +90,12 @@ const TVOS_REMOTE_BUTTONS = {
|
|
|
82
90
|
'Remote Dpad Center': 'select',
|
|
83
91
|
'Remote Menu': 'menu',
|
|
84
92
|
'Remote Media Play Pause': 'playPause',
|
|
93
|
+
'Remote Page Up': 'pageUp',
|
|
94
|
+
'Remote Page Down': 'pageDown',
|
|
95
|
+
'Remote Guide': 'guide',
|
|
96
|
+
'Remote TV Provider': 'tvProvider',
|
|
97
|
+
'Remote One Two Three': 'oneTwoThree',
|
|
98
|
+
'Remote Four Colors': 'fourColors',
|
|
85
99
|
};
|
|
86
100
|
// vega (Amazon Fire TV) remote: map key names to VegaDriver button values.
|
|
87
101
|
const VEGA_REMOTE_BUTTONS = {
|
|
@@ -139,6 +153,10 @@ exports.ANDROID_KEYCODE = {
|
|
|
139
153
|
'Remote Info': 165,
|
|
140
154
|
'Remote Instant Replay': 273, // KEYCODE_MEDIA_SKIP_BACKWARD
|
|
141
155
|
'Remote Search': 84,
|
|
156
|
+
// Paging keys — the fast way through a long list. Whether a given app honours
|
|
157
|
+
// them varies, the same as on tvOS.
|
|
158
|
+
'Remote Page Up': 92,
|
|
159
|
+
'Remote Page Down': 93,
|
|
142
160
|
};
|
|
143
161
|
/**
|
|
144
162
|
* Send `key` on an already-connected driver. Split out of `pressKey` so
|
|
@@ -148,7 +166,7 @@ exports.ANDROID_KEYCODE = {
|
|
|
148
166
|
async function dispatchKey(driver, matched, holdSeconds) {
|
|
149
167
|
if (driver instanceof ios_js_1.IOSDriver) {
|
|
150
168
|
if (driver.platform === 'tvos') {
|
|
151
|
-
const tvosButton = TVOS_REMOTE_BUTTONS[matched];
|
|
169
|
+
const tvosButton = exports.TVOS_REMOTE_BUTTONS[matched];
|
|
152
170
|
const iosButton = IOS_BUTTON_MAP[matched];
|
|
153
171
|
if (tvosButton) {
|
|
154
172
|
await driver.pressButton(tvosButton, holdSeconds);
|
|
@@ -10,6 +10,7 @@ const sdk_js_1 = require("../android/sdk.js");
|
|
|
10
10
|
const output_js_1 = require("../output.js");
|
|
11
11
|
const client_js_1 = require("../daemon/client.js");
|
|
12
12
|
const list_devices_js_1 = require("./list-devices.js");
|
|
13
|
+
const bootstrap_js_1 = require("../drivers/bootstrap.js");
|
|
13
14
|
// ── iOS / tvOS ───────────────────────────────────────────────────────────────
|
|
14
15
|
async function shutdownSimulator(udid) {
|
|
15
16
|
const result = await (0, runner_js_1.spawnCommand)('xcrun', ['simctl', 'shutdown', udid]);
|
|
@@ -55,7 +56,12 @@ async function stopDevice(nameOrId, opts, flags) {
|
|
|
55
56
|
if (d.platform === 'roku' && !includeRoku)
|
|
56
57
|
continue;
|
|
57
58
|
try {
|
|
58
|
-
if (d.platform === 'ios' || d.platform === 'tvos')
|
|
59
|
+
if ((d.platform === 'ios' || d.platform === 'tvos') &&
|
|
60
|
+
(await (0, bootstrap_js_1.detectDeviceKind)(d.id)) === 'physical') {
|
|
61
|
+
// Real hardware can't be shut down from here — just release our driver.
|
|
62
|
+
await (0, client_js_1.stopDaemon)(d.id);
|
|
63
|
+
}
|
|
64
|
+
else if (d.platform === 'ios' || d.platform === 'tvos') {
|
|
59
65
|
await shutdownSimulator(d.id);
|
|
60
66
|
}
|
|
61
67
|
else if (d.platform === 'android') {
|
|
@@ -101,7 +107,12 @@ async function stopDevice(nameOrId, opts, flags) {
|
|
|
101
107
|
return 1;
|
|
102
108
|
}
|
|
103
109
|
try {
|
|
104
|
-
if (match.platform === 'ios' || match.platform === 'tvos')
|
|
110
|
+
if ((match.platform === 'ios' || match.platform === 'tvos') &&
|
|
111
|
+
(await (0, bootstrap_js_1.detectDeviceKind)(match.id)) === 'physical') {
|
|
112
|
+
// Real hardware can't be shut down from here — just release our driver.
|
|
113
|
+
await (0, client_js_1.stopDaemon)(match.id);
|
|
114
|
+
}
|
|
115
|
+
else if (match.platform === 'ios' || match.platform === 'tvos') {
|
|
105
116
|
await shutdownSimulator(match.id);
|
|
106
117
|
}
|
|
107
118
|
else if (match.platform === 'android') {
|
package/dist/commands/swipe.js
CHANGED
|
@@ -26,8 +26,10 @@ async function swipe(direction, opts = {}, sessionName = 'default', flags = {})
|
|
|
26
26
|
}
|
|
27
27
|
const result = await (0, runner_js_1.runDirect)(async (driver) => {
|
|
28
28
|
if (driver instanceof ios_js_1.IOSDriver && driver.platform === 'tvos') {
|
|
29
|
-
throw new Error('swipe is not supported on tvOS —
|
|
30
|
-
'
|
|
29
|
+
throw new Error('swipe is not supported on tvOS — XCTest has no Siri Remote touch-surface\n' +
|
|
30
|
+
'gesture ("Swipe events are only implemented for iOS, visionOS, and watchOS").\n' +
|
|
31
|
+
'Navigate with press-key "Remote Dpad Left"/"Right"/"Up"/"Down", or cover\n' +
|
|
32
|
+
'long lists faster with press-key "Remote Page Up"/"Remote Page Down".');
|
|
31
33
|
}
|
|
32
34
|
let startX, startY, endX, endY;
|
|
33
35
|
if (driver instanceof ios_js_1.IOSDriver) {
|
|
@@ -34,6 +34,15 @@ const IOS_BUTTON = {
|
|
|
34
34
|
ArrowDown: 'down',
|
|
35
35
|
ArrowLeft: 'left',
|
|
36
36
|
ArrowRight: 'right',
|
|
37
|
+
// Newer remote buttons; the driver rejects these on older tvOS.
|
|
38
|
+
pageUp: 'pageUp',
|
|
39
|
+
PageUp: 'pageUp',
|
|
40
|
+
pageDown: 'pageDown',
|
|
41
|
+
PageDown: 'pageDown',
|
|
42
|
+
guide: 'guide',
|
|
43
|
+
tvProvider: 'tvProvider',
|
|
44
|
+
oneTwoThree: 'oneTwoThree',
|
|
45
|
+
fourColors: 'fourColors',
|
|
37
46
|
};
|
|
38
47
|
function iosBackend(driver) {
|
|
39
48
|
let size = null;
|
|
@@ -52,7 +61,23 @@ function iosBackend(driver) {
|
|
|
52
61
|
touch: true,
|
|
53
62
|
drag: true,
|
|
54
63
|
multitouch: true,
|
|
55
|
-
|
|
64
|
+
// Advertise what this platform actually accepts so clients can hide
|
|
65
|
+
// controls rather than discover failures at press time.
|
|
66
|
+
buttons: tvos
|
|
67
|
+
? [
|
|
68
|
+
'home',
|
|
69
|
+
'menu',
|
|
70
|
+
'select',
|
|
71
|
+
'up',
|
|
72
|
+
'down',
|
|
73
|
+
'left',
|
|
74
|
+
'right',
|
|
75
|
+
'playPause',
|
|
76
|
+
'pageUp',
|
|
77
|
+
'pageDown',
|
|
78
|
+
'guide',
|
|
79
|
+
]
|
|
80
|
+
: ['home', 'lock'],
|
|
56
81
|
keyboard: true,
|
|
57
82
|
text: true,
|
|
58
83
|
tvRemote: tvos,
|
|
@@ -35,6 +35,7 @@ const android_js_1 = require("../drivers/log-sources/android.js");
|
|
|
35
35
|
const vega_js_1 = require("../drivers/log-sources/vega.js");
|
|
36
36
|
const metro_js_1 = require("../drivers/log-sources/metro.js");
|
|
37
37
|
const metro_discovery_js_1 = require("../drivers/log-sources/metro-discovery.js");
|
|
38
|
+
const bootstrap_js_1 = require("../drivers/bootstrap.js");
|
|
38
39
|
const MAX_BUFFER = 5000;
|
|
39
40
|
const RESTART_DELAY_MS = 2000;
|
|
40
41
|
const WEB_POLL_INTERVAL_MS = 500;
|
|
@@ -145,6 +146,11 @@ class LogCollector {
|
|
|
145
146
|
return;
|
|
146
147
|
try {
|
|
147
148
|
if (this.platform === 'ios' || this.platform === 'tvos') {
|
|
149
|
+
// `simctl spawn ... log stream` has no devicectl equivalent, so OS logs
|
|
150
|
+
// are simulator-only. Metro still streams over the network below, which
|
|
151
|
+
// is the useful source for React Native apps anyway.
|
|
152
|
+
if ((await (0, bootstrap_js_1.detectDeviceKind)(this.deviceId)) === 'physical')
|
|
153
|
+
return;
|
|
148
154
|
this.source = new ios_js_1.IOSLogSource(this.deviceId, this.appId);
|
|
149
155
|
}
|
|
150
156
|
else if (this.platform === 'android') {
|
package/dist/daemon/server.js
CHANGED
|
@@ -84,6 +84,20 @@ function dlog(msg) {
|
|
|
84
84
|
// ── Driver lifecycle ──────────────────────────────────────────────────────────
|
|
85
85
|
let driverPort = 1075;
|
|
86
86
|
let driverPlatform = 'ios';
|
|
87
|
+
/**
|
|
88
|
+
* Host the driver is reachable on. Simulators and every non-Apple platform use
|
|
89
|
+
* the host's loopback; a physical device runs the driver on its own, so we talk
|
|
90
|
+
* to it over the network. Memoised — resolution goes through mDNS.
|
|
91
|
+
*/
|
|
92
|
+
let _driverHost = null;
|
|
93
|
+
async function driverHost() {
|
|
94
|
+
if (_driverHost)
|
|
95
|
+
return _driverHost;
|
|
96
|
+
if (driverPlatform !== 'ios' && driverPlatform !== 'tvos')
|
|
97
|
+
return '127.0.0.1';
|
|
98
|
+
_driverHost = await (0, bootstrap_js_1.resolveDriverHost)(sessionName).catch(() => '127.0.0.1');
|
|
99
|
+
return _driverHost;
|
|
100
|
+
}
|
|
87
101
|
let logCollector = null;
|
|
88
102
|
let inputServer = null;
|
|
89
103
|
let inputPort = null;
|
|
@@ -109,7 +123,7 @@ async function startInputServerForPlatform() {
|
|
|
109
123
|
makeBackend = () => (0, input_backends_js_1.androidBackend)(driver);
|
|
110
124
|
}
|
|
111
125
|
else {
|
|
112
|
-
const driver = new ios_js_1.IOSDriver(driverPort,
|
|
126
|
+
const driver = new ios_js_1.IOSDriver(driverPort, await driverHost(), sessionName, driverPlatform, (await (0, bootstrap_js_1.detectDeviceKind)(sessionName)) === 'physical');
|
|
113
127
|
makeBackend = () => (0, input_backends_js_1.iosBackend)(driver);
|
|
114
128
|
// Opt-in native held-touch backend for live drags (iOS only; single-touch).
|
|
115
129
|
if (driverPlatform === 'ios' && process.env.CONDUCTOR_IOS_HID === '1') {
|
|
@@ -154,6 +168,12 @@ async function startVideoServerForPlatform() {
|
|
|
154
168
|
return;
|
|
155
169
|
if (driverPlatform !== 'ios' && driverPlatform !== 'tvos')
|
|
156
170
|
return;
|
|
171
|
+
// Capture attaches to the Simulator's framebuffer via SimulatorKit, which has
|
|
172
|
+
// no counterpart on real hardware.
|
|
173
|
+
if ((await (0, bootstrap_js_1.detectDeviceKind)(sessionName)) === 'physical') {
|
|
174
|
+
dlog('video capture is simulator-only — stream server disabled for this device');
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
157
177
|
const binary = await (0, bootstrap_js_1.getCaptureBinaryPath)();
|
|
158
178
|
if (!binary) {
|
|
159
179
|
dlog('video capture binary not present — stream server disabled for this device');
|
|
@@ -189,11 +209,13 @@ async function ensureDriverRunning() {
|
|
|
189
209
|
probe.close();
|
|
190
210
|
}
|
|
191
211
|
else {
|
|
192
|
-
// 'ios', 'tvos', and 'web' all use an HTTP server — port open = alive
|
|
193
|
-
|
|
212
|
+
// 'ios', 'tvos', and 'web' all use an HTTP server — port open = alive.
|
|
213
|
+
// Physical devices answer on the LAN rather than the host's loopback.
|
|
214
|
+
alive = await (0, bootstrap_js_1.isPortOpen)(driverPort, await driverHost());
|
|
194
215
|
}
|
|
195
216
|
if (!alive) {
|
|
196
217
|
if ((driverPlatform === 'ios' || driverPlatform === 'tvos') &&
|
|
218
|
+
(await (0, bootstrap_js_1.detectDeviceKind)(sessionName)) === 'simulator' &&
|
|
197
219
|
!(await (0, bootstrap_js_1.isSimulatorBooted)(sessionName))) {
|
|
198
220
|
dlog(`Simulator ${sessionName} is not booted — skipping driver restart`);
|
|
199
221
|
return;
|
|
@@ -201,7 +223,11 @@ async function ensureDriverRunning() {
|
|
|
201
223
|
_restartInProgress = true;
|
|
202
224
|
dlog(`Driver on port ${driverPort} not responding — restarting`);
|
|
203
225
|
try {
|
|
204
|
-
if (driverPlatform === 'ios')
|
|
226
|
+
if ((driverPlatform === 'ios' || driverPlatform === 'tvos') &&
|
|
227
|
+
(await (0, bootstrap_js_1.detectDeviceKind)(sessionName)) === 'physical') {
|
|
228
|
+
await (0, bootstrap_js_1.startDeviceDriver)(sessionName, driverPlatform, driverPort);
|
|
229
|
+
}
|
|
230
|
+
else if (driverPlatform === 'ios') {
|
|
205
231
|
await (0, bootstrap_js_1.startIOSDriver)(sessionName, driverPort);
|
|
206
232
|
}
|
|
207
233
|
else if (driverPlatform === 'tvos') {
|
|
@@ -333,7 +359,10 @@ async function main() {
|
|
|
333
359
|
else {
|
|
334
360
|
dlog(`Stopping driver on port ${driverPort}`);
|
|
335
361
|
try {
|
|
336
|
-
if (driverPlatform === 'ios') {
|
|
362
|
+
if (driverPlatform === 'ios' && (await (0, bootstrap_js_1.detectDeviceKind)(sessionName)) === 'physical') {
|
|
363
|
+
await (0, bootstrap_js_1.stopDeviceDriver)(sessionName, 'ios');
|
|
364
|
+
}
|
|
365
|
+
else if (driverPlatform === 'ios') {
|
|
337
366
|
await (0, bootstrap_js_1.stopIOSDriver)(sessionName);
|
|
338
367
|
}
|
|
339
368
|
else {
|
|
@@ -510,7 +539,7 @@ async function startDriverForPlatform(platform) {
|
|
|
510
539
|
}
|
|
511
540
|
else {
|
|
512
541
|
// 'ios', 'tvos', and 'web' all use an HTTP server — port open = alive
|
|
513
|
-
driverAlive = await (0, bootstrap_js_1.isPortOpen)(driverPort);
|
|
542
|
+
driverAlive = await (0, bootstrap_js_1.isPortOpen)(driverPort, await driverHost());
|
|
514
543
|
}
|
|
515
544
|
if (driverAlive) {
|
|
516
545
|
_driverStarted = true;
|
|
@@ -533,7 +562,11 @@ async function startDriverForPlatform(platform) {
|
|
|
533
562
|
}
|
|
534
563
|
dlog(`Starting ${platform} driver on port ${driverPort}`);
|
|
535
564
|
try {
|
|
536
|
-
if (platform === 'ios')
|
|
565
|
+
if ((platform === 'ios' || platform === 'tvos') &&
|
|
566
|
+
(await (0, bootstrap_js_1.detectDeviceKind)(sessionName)) === 'physical') {
|
|
567
|
+
await (0, bootstrap_js_1.startDeviceDriver)(sessionName, platform, driverPort);
|
|
568
|
+
}
|
|
569
|
+
else if (platform === 'ios') {
|
|
537
570
|
await (0, bootstrap_js_1.startIOSDriver)(sessionName, driverPort);
|
|
538
571
|
}
|
|
539
572
|
else if (platform === 'tvos') {
|