@houwert/conductor 0.2.0 → 0.3.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/.claude-plugin/plugin.json +4 -1
- package/README.md +121 -21
- package/dist/commands/assert-not-visible.js +5 -0
- package/dist/commands/assert-visible.js +15 -0
- package/dist/commands/back.js +6 -1
- package/dist/commands/cheat-sheet.js +2 -0
- package/dist/commands/copy-app.js +55 -0
- package/dist/commands/daemon.js +4 -0
- package/dist/commands/device-pool.js +4 -0
- package/dist/commands/erase-text.js +2 -0
- package/dist/commands/focused.js +233 -0
- package/dist/commands/foreground-app.js +2 -0
- package/dist/commands/hide-keyboard.js +2 -0
- package/dist/commands/inspect.js +23 -1
- package/dist/commands/install.js +69 -4
- package/dist/commands/launch-app.js +6 -0
- package/dist/commands/list-apps.js +2 -0
- package/dist/commands/list-devices.js +70 -7
- package/dist/commands/open-link.js +2 -0
- package/dist/commands/press-key.js +73 -7
- package/dist/commands/run-flow-inline.js +3 -0
- package/dist/commands/run-flow.js +4 -0
- package/dist/commands/run-parallel.js +2 -0
- package/dist/commands/screenshot.js +2 -0
- package/dist/commands/scroll-until-visible.js +6 -0
- package/dist/commands/scroll.js +6 -0
- package/dist/commands/session.js +2 -0
- package/dist/commands/set-location.js +2 -0
- package/dist/commands/set-orientation.js +2 -0
- package/dist/commands/start-device.js +311 -9
- package/dist/commands/stop-app.js +2 -0
- package/dist/commands/swipe.js +10 -0
- package/dist/commands/tap.js +20 -0
- package/dist/commands/type.js +2 -0
- package/dist/daemon/server.js +64 -22
- package/dist/device-picker.js +65 -0
- package/dist/drivers/android.js +14 -13
- package/dist/drivers/bootstrap.js +179 -6
- package/dist/drivers/element-resolver.js +10 -0
- package/dist/drivers/flow-runner.js +6 -5
- package/dist/drivers/ios.js +2 -1
- package/dist/index.js +120 -97
- package/dist/postinstall.js +2 -2
- package/dist/runner.js +40 -6
- package/drivers/ios/conductor-driver-ios.zip +0 -0
- package/drivers/ios/conductor-driver-iosUITests-Runner.zip +0 -0
- package/drivers/tvos/conductor-driver-tvos-config.xctestrun +121 -0
- package/drivers/tvos/conductor-driver-tvos.zip +0 -0
- package/drivers/tvos/conductor-driver-tvosUITests-Runner.zip +0 -0
- package/package.json +1 -1
- package/skills/conductor/SKILL.md +44 -6
- package/skills/conductor/references/flow-syntax.md +1 -0
- package/skills/skills.yaml +8 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.pickDevice = pickDevice;
|
|
4
|
+
const readline_1 = require("readline");
|
|
5
|
+
const list_devices_js_1 = require("./commands/list-devices.js");
|
|
6
|
+
/**
|
|
7
|
+
* Discover booted devices and let the user pick one interactively
|
|
8
|
+
* when multiple are found.
|
|
9
|
+
*
|
|
10
|
+
* - 0 devices → returns undefined
|
|
11
|
+
* - 1 device → returns it automatically
|
|
12
|
+
* - N devices + TTY → shows a numbered picker
|
|
13
|
+
* - N devices + no TTY → returns undefined (caller should error)
|
|
14
|
+
*/
|
|
15
|
+
async function pickDevice() {
|
|
16
|
+
const devices = await (0, list_devices_js_1.discoverBootedDevices)();
|
|
17
|
+
if (devices.length === 0)
|
|
18
|
+
return undefined;
|
|
19
|
+
if (devices.length === 1)
|
|
20
|
+
return devices[0].id;
|
|
21
|
+
// Non-interactive context — can't prompt
|
|
22
|
+
if (!process.stdin.isTTY) {
|
|
23
|
+
console.error(`Multiple devices found but stdin is not a TTY. Use --device to specify one:\n` +
|
|
24
|
+
devices.map((d) => ` ${d.id} ${d.name} (${d.platform})`).join('\n'));
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
console.log('Multiple devices detected — pick one:\n');
|
|
28
|
+
for (let i = 0; i < devices.length; i++) {
|
|
29
|
+
const d = devices[i];
|
|
30
|
+
console.log(` ${i + 1}) ${d.name} (${d.platform}, ${d.id})`);
|
|
31
|
+
}
|
|
32
|
+
console.log();
|
|
33
|
+
const choice = await promptChoice(devices.length);
|
|
34
|
+
if (choice === null)
|
|
35
|
+
return undefined;
|
|
36
|
+
const picked = devices[choice];
|
|
37
|
+
console.log(`Using ${picked.name} (${picked.id})\n`);
|
|
38
|
+
return picked.id;
|
|
39
|
+
}
|
|
40
|
+
function promptChoice(max) {
|
|
41
|
+
return new Promise((resolve) => {
|
|
42
|
+
const rl = (0, readline_1.createInterface)({ input: process.stdin, output: process.stdout });
|
|
43
|
+
let resolved = false;
|
|
44
|
+
const done = (value) => {
|
|
45
|
+
if (resolved)
|
|
46
|
+
return;
|
|
47
|
+
resolved = true;
|
|
48
|
+
rl.close();
|
|
49
|
+
resolve(value);
|
|
50
|
+
};
|
|
51
|
+
const ask = () => {
|
|
52
|
+
rl.question(`Enter 1–${max}: `, (answer) => {
|
|
53
|
+
const n = parseInt(answer.trim(), 10);
|
|
54
|
+
if (n >= 1 && n <= max) {
|
|
55
|
+
done(n - 1);
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
ask();
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
};
|
|
62
|
+
rl.on('close', () => done(null));
|
|
63
|
+
ask();
|
|
64
|
+
});
|
|
65
|
+
}
|
package/dist/drivers/android.js
CHANGED
|
@@ -84,29 +84,21 @@ class AndroidDriver {
|
|
|
84
84
|
'grpc.keepalive_timeout_ms': 20000,
|
|
85
85
|
});
|
|
86
86
|
}
|
|
87
|
-
|
|
87
|
+
close() {
|
|
88
88
|
if (this.client) {
|
|
89
|
-
|
|
89
|
+
this.client.close();
|
|
90
90
|
this.client = null;
|
|
91
91
|
}
|
|
92
92
|
}
|
|
93
|
-
|
|
94
|
-
try {
|
|
95
|
-
await this.deviceInfo();
|
|
96
|
-
return true;
|
|
97
|
-
}
|
|
98
|
-
catch {
|
|
99
|
-
return false;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
call(method, req) {
|
|
93
|
+
call(method, req, timeoutMs = 30000) {
|
|
103
94
|
return new Promise((resolve, reject) => {
|
|
104
95
|
if (!this.client) {
|
|
105
96
|
reject(new Error('AndroidDriver: not connected'));
|
|
106
97
|
return;
|
|
107
98
|
}
|
|
99
|
+
const deadline = new Date(Date.now() + timeoutMs);
|
|
108
100
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
109
|
-
this.client[method](req, (err, resp) => {
|
|
101
|
+
this.client[method](req, { deadline }, (err, resp) => {
|
|
110
102
|
if (err)
|
|
111
103
|
reject(err);
|
|
112
104
|
else
|
|
@@ -114,6 +106,15 @@ class AndroidDriver {
|
|
|
114
106
|
});
|
|
115
107
|
});
|
|
116
108
|
}
|
|
109
|
+
async isAlive() {
|
|
110
|
+
try {
|
|
111
|
+
await this.call('deviceInfo', {}, 5000);
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
117
118
|
async deviceInfo() {
|
|
118
119
|
const resp = await this.call('deviceInfo', {});
|
|
119
120
|
return { widthPixels: resp.widthPixels, heightPixels: resp.heightPixels };
|
|
@@ -10,6 +10,9 @@ exports.isSimulatorBooted = isSimulatorBooted;
|
|
|
10
10
|
exports.isPortOpen = isPortOpen;
|
|
11
11
|
exports.startIOSDriver = startIOSDriver;
|
|
12
12
|
exports.stopIOSDriver = stopIOSDriver;
|
|
13
|
+
exports.setupTvOSDriverCache = setupTvOSDriverCache;
|
|
14
|
+
exports.startTvOSDriver = startTvOSDriver;
|
|
15
|
+
exports.stopTvOSDriver = stopTvOSDriver;
|
|
13
16
|
exports.startAndroidDriver = startAndroidDriver;
|
|
14
17
|
exports.stopAndroidDriver = stopAndroidDriver;
|
|
15
18
|
exports.uninstallDriver = uninstallDriver;
|
|
@@ -24,6 +27,7 @@ exports.uninstallDriver = uninstallDriver;
|
|
|
24
27
|
* drivers/ios/ — no separate Conductor/JVM installation required.
|
|
25
28
|
*/
|
|
26
29
|
const child_process_1 = require("child_process");
|
|
30
|
+
const http_1 = __importDefault(require("http"));
|
|
27
31
|
const net_1 = __importDefault(require("net"));
|
|
28
32
|
const os_1 = __importDefault(require("os"));
|
|
29
33
|
const fs_1 = __importDefault(require("fs"));
|
|
@@ -35,9 +39,24 @@ const _platformCache = new Map();
|
|
|
35
39
|
async function detectPlatform(deviceId) {
|
|
36
40
|
if (_platformCache.has(deviceId))
|
|
37
41
|
return _platformCache.get(deviceId);
|
|
38
|
-
// Check if it looks like an iOS simulator UUID (8-4-4-4-12 hex chars)
|
|
42
|
+
// Check if it looks like an iOS/tvOS simulator UUID (8-4-4-4-12 hex chars)
|
|
39
43
|
const iosUuidRe = /^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$/i;
|
|
40
44
|
if (iosUuidRe.test(deviceId)) {
|
|
45
|
+
// Query simctl to determine whether this UUID belongs to a tvOS runtime
|
|
46
|
+
try {
|
|
47
|
+
const out = await spawnCapture('xcrun', ['simctl', 'list', 'devices', '--json']);
|
|
48
|
+
const parsed = JSON.parse(out);
|
|
49
|
+
for (const [runtime, sims] of Object.entries(parsed.devices)) {
|
|
50
|
+
if (sims.some((s) => s.udid === deviceId)) {
|
|
51
|
+
const platform = runtime.includes('tvOS') ? 'tvos' : 'ios';
|
|
52
|
+
_platformCache.set(deviceId, platform);
|
|
53
|
+
return platform;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
/* fall through to ios default */
|
|
59
|
+
}
|
|
41
60
|
_platformCache.set(deviceId, 'ios');
|
|
42
61
|
return 'ios';
|
|
43
62
|
}
|
|
@@ -47,6 +66,7 @@ async function detectPlatform(deviceId) {
|
|
|
47
66
|
}
|
|
48
67
|
// ── Port management ───────────────────────────────────────────────────────────
|
|
49
68
|
const IOS_BASE_PORT = 1075;
|
|
69
|
+
const TVOS_BASE_PORT = 2075;
|
|
50
70
|
const ANDROID_BASE_PORT = 3763;
|
|
51
71
|
const PORT_FILE = path_1.default.join(os_1.default.homedir(), '.conductor', 'ports.json');
|
|
52
72
|
const PORT_LOCK = PORT_FILE + '.lock';
|
|
@@ -56,7 +76,12 @@ function readPortState() {
|
|
|
56
76
|
return JSON.parse(fs_1.default.readFileSync(PORT_FILE, 'utf-8'));
|
|
57
77
|
}
|
|
58
78
|
catch {
|
|
59
|
-
return {
|
|
79
|
+
return {
|
|
80
|
+
assignments: {},
|
|
81
|
+
nextIosPort: IOS_BASE_PORT,
|
|
82
|
+
nextTvosPort: TVOS_BASE_PORT,
|
|
83
|
+
nextAndroidPort: ANDROID_BASE_PORT,
|
|
84
|
+
};
|
|
60
85
|
}
|
|
61
86
|
}
|
|
62
87
|
function writePortState(state) {
|
|
@@ -99,7 +124,19 @@ async function getDriverPort(platform, deviceId) {
|
|
|
99
124
|
if (state.assignments[deviceId] !== undefined) {
|
|
100
125
|
return state.assignments[deviceId];
|
|
101
126
|
}
|
|
102
|
-
|
|
127
|
+
// Ensure tvos counter is initialised for port files created before tvOS support
|
|
128
|
+
if (state.nextTvosPort === undefined)
|
|
129
|
+
state.nextTvosPort = TVOS_BASE_PORT;
|
|
130
|
+
let port;
|
|
131
|
+
if (platform === 'ios') {
|
|
132
|
+
port = state.nextIosPort++;
|
|
133
|
+
}
|
|
134
|
+
else if (platform === 'tvos') {
|
|
135
|
+
port = state.nextTvosPort++;
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
port = state.nextAndroidPort++;
|
|
139
|
+
}
|
|
103
140
|
state.assignments[deviceId] = port;
|
|
104
141
|
writePortState(state);
|
|
105
142
|
return port;
|
|
@@ -235,10 +272,19 @@ async function startIOSDriver(deviceId, port = IOS_BASE_PORT) {
|
|
|
235
272
|
(0, verbose_js_1.log)(`Starting iOS XCTest driver for device ${deviceId} on port ${port}`);
|
|
236
273
|
await setupIOSDriverCache();
|
|
237
274
|
const xctestrun = path_1.default.join(IOS_DRIVER_CACHE, 'conductor-driver-ios-config.xctestrun');
|
|
275
|
+
// Inject PORT into the xctestrun EnvironmentVariables so the XCTest runner
|
|
276
|
+
// picks it up. Env vars on the spawn call don't reach the test process —
|
|
277
|
+
// xcodebuild only passes what's declared in the xctestrun plist.
|
|
278
|
+
await spawnAndWait('plutil', [
|
|
279
|
+
'-replace',
|
|
280
|
+
'conductor-driver-iosUITests.EnvironmentVariables.PORT',
|
|
281
|
+
'-string',
|
|
282
|
+
String(port),
|
|
283
|
+
xctestrun,
|
|
284
|
+
]);
|
|
238
285
|
const proc = (0, child_process_1.spawn)('xcodebuild', ['test-without-building', '-xctestrun', xctestrun, '-destination', `id=${deviceId}`], {
|
|
239
286
|
detached: true,
|
|
240
287
|
stdio: ['ignore', 'ignore', 'ignore'],
|
|
241
|
-
env: { ...process.env, TEST_RUNNER_PORT: String(port) },
|
|
242
288
|
});
|
|
243
289
|
proc.unref();
|
|
244
290
|
const deadline = Date.now() + IOS_STARTUP_TIMEOUT_MS;
|
|
@@ -257,6 +303,108 @@ async function startIOSDriver(deviceId, port = IOS_BASE_PORT) {
|
|
|
257
303
|
async function stopIOSDriver(deviceId) {
|
|
258
304
|
await spawnAndWait('xcrun', ['simctl', 'terminate', deviceId, IOS_RUNNER_BUNDLE_ID]);
|
|
259
305
|
}
|
|
306
|
+
// ── tvOS bootstrap ────────────────────────────────────────────────────────────
|
|
307
|
+
const TVOS_RUNNER_BUNDLE_ID = 'dev.houwert.conductor-driver-tvosUITests.xctrunner';
|
|
308
|
+
const TVOS_STARTUP_TIMEOUT_MS = 120000;
|
|
309
|
+
const TVOS_STARTUP_POLL_MS = 500;
|
|
310
|
+
// Persistent cache for extracted tvOS driver files (~/.conductor/tvos-driver/).
|
|
311
|
+
// __TESTROOT__ in the xctestrun resolves to this directory, so both the xctestrun
|
|
312
|
+
// and the Debug-appletvsimulator/ folder must live here.
|
|
313
|
+
const TVOS_DRIVER_CACHE = path_1.default.join(os_1.default.homedir(), '.conductor', 'tvos-driver');
|
|
314
|
+
/**
|
|
315
|
+
* Ensure the tvOS driver files are extracted from the bundled zips into the cache
|
|
316
|
+
* dir. Re-extracts only when the bundled xctestrun has changed (tracked by mtime).
|
|
317
|
+
*/
|
|
318
|
+
async function setupTvOSDriverCache() {
|
|
319
|
+
const bundledXctestrun = path_1.default.join(BUNDLED_DRIVERS_DIR, 'tvos', 'conductor-driver-tvos-config.xctestrun');
|
|
320
|
+
const bundledDriverZip = path_1.default.join(BUNDLED_DRIVERS_DIR, 'tvos', 'conductor-driver-tvos.zip');
|
|
321
|
+
const bundledRunnerZip = path_1.default.join(BUNDLED_DRIVERS_DIR, 'tvos', 'conductor-driver-tvosUITests-Runner.zip');
|
|
322
|
+
if (!fs_1.default.existsSync(bundledXctestrun) ||
|
|
323
|
+
!fs_1.default.existsSync(bundledDriverZip) ||
|
|
324
|
+
!fs_1.default.existsSync(bundledRunnerZip)) {
|
|
325
|
+
throw new Error(`Conductor tvOS driver files not found at ${path_1.default.join(BUNDLED_DRIVERS_DIR, 'tvos')}.\n` +
|
|
326
|
+
`Run 'make package-cli' from the repo root to build and bundle the drivers.`);
|
|
327
|
+
}
|
|
328
|
+
const versionFile = path_1.default.join(TVOS_DRIVER_CACHE, '.version');
|
|
329
|
+
const xctestrunMtime = String(fs_1.default.statSync(bundledXctestrun).mtimeMs);
|
|
330
|
+
let cachedMtime = '';
|
|
331
|
+
try {
|
|
332
|
+
cachedMtime = fs_1.default.readFileSync(versionFile, 'utf-8').trim();
|
|
333
|
+
}
|
|
334
|
+
catch {
|
|
335
|
+
/* first run */
|
|
336
|
+
}
|
|
337
|
+
const runnerApp = path_1.default.join(TVOS_DRIVER_CACHE, 'Debug-appletvsimulator', 'conductor-driver-tvosUITests-Runner.app');
|
|
338
|
+
if (cachedMtime === xctestrunMtime && fs_1.default.existsSync(runnerApp))
|
|
339
|
+
return;
|
|
340
|
+
(0, verbose_js_1.log)('Extracting tvOS driver files to cache...');
|
|
341
|
+
fs_1.default.rmSync(TVOS_DRIVER_CACHE, { recursive: true, force: true });
|
|
342
|
+
fs_1.default.mkdirSync(TVOS_DRIVER_CACHE, { recursive: true });
|
|
343
|
+
// Copy xctestrun directly
|
|
344
|
+
fs_1.default.copyFileSync(bundledXctestrun, path_1.default.join(TVOS_DRIVER_CACHE, 'conductor-driver-tvos-config.xctestrun'));
|
|
345
|
+
// Unzip the two .app bundles
|
|
346
|
+
const appsDir = path_1.default.join(TVOS_DRIVER_CACHE, 'Debug-appletvsimulator');
|
|
347
|
+
await spawnAndWait('unzip', ['-q', '-o', bundledDriverZip, '-d', appsDir]);
|
|
348
|
+
await spawnAndWait('unzip', ['-q', '-o', bundledRunnerZip, '-d', appsDir]);
|
|
349
|
+
fs_1.default.writeFileSync(versionFile, xctestrunMtime);
|
|
350
|
+
(0, verbose_js_1.log)('tvOS driver cache ready');
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Start the tvOS XCTest driver via `xcodebuild test-without-building`.
|
|
354
|
+
* Mirrors startIOSDriver but targets the tvOS xctestrun.
|
|
355
|
+
*
|
|
356
|
+
* On first launch the runner app appears in the foreground, so we press the
|
|
357
|
+
* home button to dismiss it. On subsequent restarts (e.g. health-check recovery)
|
|
358
|
+
* we skip the dismiss to avoid disrupting the user's navigation state.
|
|
359
|
+
*/
|
|
360
|
+
async function startTvOSDriver(deviceId, port = TVOS_BASE_PORT, dismissAfterLaunch = false) {
|
|
361
|
+
if (await isPortOpen(port)) {
|
|
362
|
+
(0, verbose_js_1.log)(`tvOS driver already running on port ${port}`);
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
(0, verbose_js_1.log)(`Starting tvOS XCTest driver for device ${deviceId} on port ${port}`);
|
|
366
|
+
await setupTvOSDriverCache();
|
|
367
|
+
const xctestrun = path_1.default.join(TVOS_DRIVER_CACHE, 'conductor-driver-tvos-config.xctestrun');
|
|
368
|
+
// Inject PORT into the xctestrun EnvironmentVariables so the XCTest runner
|
|
369
|
+
// picks it up. Env vars on the spawn call don't reach the test process —
|
|
370
|
+
// xcodebuild only passes what's declared in the xctestrun plist.
|
|
371
|
+
await spawnAndWait('plutil', [
|
|
372
|
+
'-replace',
|
|
373
|
+
'conductor-driver-tvosUITests.EnvironmentVariables.PORT',
|
|
374
|
+
'-string',
|
|
375
|
+
String(port),
|
|
376
|
+
xctestrun,
|
|
377
|
+
]);
|
|
378
|
+
const proc = (0, child_process_1.spawn)('xcodebuild', ['test-without-building', '-xctestrun', xctestrun, '-destination', `id=${deviceId}`], {
|
|
379
|
+
detached: true,
|
|
380
|
+
stdio: ['ignore', 'ignore', 'ignore'],
|
|
381
|
+
});
|
|
382
|
+
proc.unref();
|
|
383
|
+
const deadline = Date.now() + TVOS_STARTUP_TIMEOUT_MS;
|
|
384
|
+
while (Date.now() < deadline) {
|
|
385
|
+
await (0, utils_js_1.sleep)(TVOS_STARTUP_POLL_MS);
|
|
386
|
+
if (await isPortOpen(port)) {
|
|
387
|
+
(0, verbose_js_1.log)(`tvOS driver ready on port ${port}`);
|
|
388
|
+
if (dismissAfterLaunch) {
|
|
389
|
+
try {
|
|
390
|
+
await pressButtonViaDriver(port, 'home');
|
|
391
|
+
(0, verbose_js_1.log)('Dismissed tvOS driver app');
|
|
392
|
+
}
|
|
393
|
+
catch {
|
|
394
|
+
(0, verbose_js_1.log)('Could not dismiss tvOS driver app (non-fatal)');
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
throw new Error(`tvOS XCTest driver did not start within ${TVOS_STARTUP_TIMEOUT_MS / 1000}s on port ${port}.`);
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Stop the tvOS XCTest driver by terminating the runner app.
|
|
404
|
+
*/
|
|
405
|
+
async function stopTvOSDriver(deviceId) {
|
|
406
|
+
await spawnAndWait('xcrun', ['simctl', 'terminate', deviceId, TVOS_RUNNER_BUNDLE_ID]);
|
|
407
|
+
}
|
|
260
408
|
// ── Android bootstrap ─────────────────────────────────────────────────────────
|
|
261
409
|
const ANDROID_STARTUP_TIMEOUT_MS = 30000;
|
|
262
410
|
const ANDROID_STARTUP_POLL_MS = 500;
|
|
@@ -321,7 +469,7 @@ async function startAndroidDriver(deviceId, port = ANDROID_BASE_PORT) {
|
|
|
321
469
|
`Make sure the Conductor driver APK is installed on device ${deviceId}.\n` +
|
|
322
470
|
`Try running: conductor install --device ${deviceId}`);
|
|
323
471
|
}
|
|
324
|
-
async function stopAndroidDriver(deviceId) {
|
|
472
|
+
async function stopAndroidDriver(deviceId, port = ANDROID_BASE_PORT) {
|
|
325
473
|
await spawnAndWait('adb', [
|
|
326
474
|
'-s',
|
|
327
475
|
deviceId,
|
|
@@ -330,7 +478,7 @@ async function stopAndroidDriver(deviceId) {
|
|
|
330
478
|
'force-stop',
|
|
331
479
|
'dev.houwert.conductor',
|
|
332
480
|
]).catch(() => { });
|
|
333
|
-
await spawnAndWait('adb', ['-s', deviceId, 'forward', '--remove',
|
|
481
|
+
await spawnAndWait('adb', ['-s', deviceId, 'forward', '--remove', `tcp:${port}`]).catch(() => { });
|
|
334
482
|
}
|
|
335
483
|
/**
|
|
336
484
|
* Uninstall the Conductor driver app(s) from the device.
|
|
@@ -345,12 +493,37 @@ async function uninstallDriver(deviceId, platform) {
|
|
|
345
493
|
'dev.houwert.conductor-driver-iosUITests.xctrunner',
|
|
346
494
|
]).catch(() => { });
|
|
347
495
|
}
|
|
496
|
+
else if (platform === 'tvos') {
|
|
497
|
+
await spawnAndWait('xcrun', ['simctl', 'uninstall', deviceId, TVOS_RUNNER_BUNDLE_ID]).catch(() => { });
|
|
498
|
+
}
|
|
348
499
|
else {
|
|
349
500
|
await spawnAndWait('adb', ['-s', deviceId, 'uninstall', 'dev.houwert.conductor']).catch(() => { });
|
|
350
501
|
await spawnAndWait('adb', ['-s', deviceId, 'uninstall', 'dev.houwert.conductor.test']).catch(() => { });
|
|
351
502
|
}
|
|
352
503
|
}
|
|
353
504
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
505
|
+
/** Send a pressButton command directly to the driver HTTP server. */
|
|
506
|
+
function pressButtonViaDriver(port, button) {
|
|
507
|
+
return new Promise((resolve, reject) => {
|
|
508
|
+
const body = JSON.stringify({ button });
|
|
509
|
+
const options = {
|
|
510
|
+
hostname: '127.0.0.1',
|
|
511
|
+
port,
|
|
512
|
+
path: '/pressButton',
|
|
513
|
+
method: 'POST',
|
|
514
|
+
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) },
|
|
515
|
+
};
|
|
516
|
+
const req = http_1.default.request(options, (res) => {
|
|
517
|
+
res.resume();
|
|
518
|
+
res.on('end', () => res.statusCode && res.statusCode < 300
|
|
519
|
+
? resolve()
|
|
520
|
+
: reject(new Error(`HTTP ${res.statusCode}`)));
|
|
521
|
+
});
|
|
522
|
+
req.on('error', reject);
|
|
523
|
+
req.write(body);
|
|
524
|
+
req.end();
|
|
525
|
+
});
|
|
526
|
+
}
|
|
354
527
|
function spawnAndWait(cmd, args) {
|
|
355
528
|
return new Promise((resolve, reject) => {
|
|
356
529
|
const proc = (0, child_process_1.spawn)(cmd, args, { stdio: 'ignore' });
|
|
@@ -212,12 +212,22 @@ function parseAndroidHierarchy(xml) {
|
|
|
212
212
|
text: attrs['text'] ?? '',
|
|
213
213
|
resourceId: attrs['resource-id'] ?? '',
|
|
214
214
|
contentDesc: attrs['content-desc'] ?? '',
|
|
215
|
+
className: attrs['class'] ?? '',
|
|
216
|
+
packageName: attrs['package'] ?? '',
|
|
215
217
|
bounds,
|
|
216
218
|
clickable: attrs['clickable'] === 'true',
|
|
217
219
|
enabled: attrs['enabled'] === 'true',
|
|
218
220
|
checked: attrs['checked'] === 'true',
|
|
219
221
|
focused: attrs['focused'] === 'true',
|
|
222
|
+
focusable: attrs['focusable'] === 'true',
|
|
220
223
|
selected: attrs['selected'] === 'true',
|
|
224
|
+
checkable: attrs['checkable'] === 'true',
|
|
225
|
+
longClickable: attrs['long-clickable'] === 'true',
|
|
226
|
+
scrollable: attrs['scrollable'] === 'true',
|
|
227
|
+
password: attrs['password'] === 'true',
|
|
228
|
+
visibleToUser: attrs['visible-to-user'] === 'true',
|
|
229
|
+
hintText: attrs['hintText'] ?? '',
|
|
230
|
+
error: attrs['error'] ?? '',
|
|
221
231
|
children: [],
|
|
222
232
|
index: nodes.length,
|
|
223
233
|
});
|
|
@@ -330,11 +330,12 @@ const ANDROID_KEYCODES = {
|
|
|
330
330
|
VOLUME_UP: 24,
|
|
331
331
|
VOLUME_DOWN: 25,
|
|
332
332
|
POWER: 26,
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
333
|
+
// Android TV remote D-pad keys only
|
|
334
|
+
'REMOTE DPAD UP': 19,
|
|
335
|
+
'REMOTE DPAD DOWN': 20,
|
|
336
|
+
'REMOTE DPAD LEFT': 21,
|
|
337
|
+
'REMOTE DPAD RIGHT': 22,
|
|
338
|
+
'REMOTE DPAD CENTER': 23,
|
|
338
339
|
};
|
|
339
340
|
async function executeFlow(flow, driver, opts = {}) {
|
|
340
341
|
const cliEnv = opts.env ?? {};
|
package/dist/drivers/ios.js
CHANGED
|
@@ -17,10 +17,11 @@ const os_1 = __importDefault(require("os"));
|
|
|
17
17
|
const path_1 = __importDefault(require("path"));
|
|
18
18
|
const child_process_1 = require("child_process");
|
|
19
19
|
class IOSDriver {
|
|
20
|
-
constructor(port = 1075, host = '127.0.0.1', deviceId) {
|
|
20
|
+
constructor(port = 1075, host = '127.0.0.1', deviceId, platform = 'ios') {
|
|
21
21
|
this.port = port;
|
|
22
22
|
this.host = host;
|
|
23
23
|
this.deviceId = deviceId;
|
|
24
|
+
this.platform = platform;
|
|
24
25
|
this._recordingProcess = null;
|
|
25
26
|
}
|
|
26
27
|
request(method, path, body) {
|