@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.
Files changed (53) hide show
  1. package/.claude-plugin/plugin.json +4 -1
  2. package/README.md +121 -21
  3. package/dist/commands/assert-not-visible.js +5 -0
  4. package/dist/commands/assert-visible.js +15 -0
  5. package/dist/commands/back.js +6 -1
  6. package/dist/commands/cheat-sheet.js +2 -0
  7. package/dist/commands/copy-app.js +55 -0
  8. package/dist/commands/daemon.js +4 -0
  9. package/dist/commands/device-pool.js +4 -0
  10. package/dist/commands/erase-text.js +2 -0
  11. package/dist/commands/focused.js +233 -0
  12. package/dist/commands/foreground-app.js +2 -0
  13. package/dist/commands/hide-keyboard.js +2 -0
  14. package/dist/commands/inspect.js +23 -1
  15. package/dist/commands/install.js +69 -4
  16. package/dist/commands/launch-app.js +6 -0
  17. package/dist/commands/list-apps.js +2 -0
  18. package/dist/commands/list-devices.js +70 -7
  19. package/dist/commands/open-link.js +2 -0
  20. package/dist/commands/press-key.js +73 -7
  21. package/dist/commands/run-flow-inline.js +3 -0
  22. package/dist/commands/run-flow.js +4 -0
  23. package/dist/commands/run-parallel.js +2 -0
  24. package/dist/commands/screenshot.js +2 -0
  25. package/dist/commands/scroll-until-visible.js +6 -0
  26. package/dist/commands/scroll.js +6 -0
  27. package/dist/commands/session.js +2 -0
  28. package/dist/commands/set-location.js +2 -0
  29. package/dist/commands/set-orientation.js +2 -0
  30. package/dist/commands/start-device.js +311 -9
  31. package/dist/commands/stop-app.js +2 -0
  32. package/dist/commands/swipe.js +10 -0
  33. package/dist/commands/tap.js +20 -0
  34. package/dist/commands/type.js +2 -0
  35. package/dist/daemon/server.js +64 -22
  36. package/dist/device-picker.js +65 -0
  37. package/dist/drivers/android.js +14 -13
  38. package/dist/drivers/bootstrap.js +179 -6
  39. package/dist/drivers/element-resolver.js +10 -0
  40. package/dist/drivers/flow-runner.js +6 -5
  41. package/dist/drivers/ios.js +2 -1
  42. package/dist/index.js +120 -97
  43. package/dist/postinstall.js +2 -2
  44. package/dist/runner.js +40 -6
  45. package/drivers/ios/conductor-driver-ios.zip +0 -0
  46. package/drivers/ios/conductor-driver-iosUITests-Runner.zip +0 -0
  47. package/drivers/tvos/conductor-driver-tvos-config.xctestrun +121 -0
  48. package/drivers/tvos/conductor-driver-tvos.zip +0 -0
  49. package/drivers/tvos/conductor-driver-tvosUITests-Runner.zip +0 -0
  50. package/package.json +1 -1
  51. package/skills/conductor/SKILL.md +44 -6
  52. package/skills/conductor/references/flow-syntax.md +1 -0
  53. 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
+ }
@@ -84,29 +84,21 @@ class AndroidDriver {
84
84
  'grpc.keepalive_timeout_ms': 20000,
85
85
  });
86
86
  }
87
- async close() {
87
+ close() {
88
88
  if (this.client) {
89
- await new Promise((resolve) => this.client.close(() => resolve()));
89
+ this.client.close();
90
90
  this.client = null;
91
91
  }
92
92
  }
93
- async isAlive() {
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 { assignments: {}, nextIosPort: IOS_BASE_PORT, nextAndroidPort: ANDROID_BASE_PORT };
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
- const port = platform === 'ios' ? state.nextIosPort++ : state.nextAndroidPort++;
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', 'tcp:3763']).catch(() => { });
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
- DPAD_UP: 19,
334
- DPAD_DOWN: 20,
335
- DPAD_LEFT: 21,
336
- DPAD_RIGHT: 22,
337
- DPAD_CENTER: 23,
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 ?? {};
@@ -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) {