@houwert/conductor 0.29.2 → 0.30.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 (50) hide show
  1. package/README.md +1 -1
  2. package/dist/commands/assert-not-visible.js +4 -1
  3. package/dist/commands/assert-visible.js +5 -2
  4. package/dist/commands/back.js +2 -1
  5. package/dist/commands/capture-ui.js +7 -3
  6. package/dist/commands/clipboard.js +9 -0
  7. package/dist/commands/copy-text-from.js +2 -1
  8. package/dist/commands/crashes.js +5 -0
  9. package/dist/commands/delete-device.js +5 -0
  10. package/dist/commands/download-app.js +4 -0
  11. package/dist/commands/erase-text.js +4 -1
  12. package/dist/commands/focused.js +5 -2
  13. package/dist/commands/foreground-app.js +4 -1
  14. package/dist/commands/gestures.js +7 -0
  15. package/dist/commands/hide-keyboard.js +5 -0
  16. package/dist/commands/inspect.js +10 -3
  17. package/dist/commands/install-app.js +5 -0
  18. package/dist/commands/launch-app.js +3 -2
  19. package/dist/commands/list-apps.js +5 -0
  20. package/dist/commands/list-devices.js +12 -0
  21. package/dist/commands/memory.js +5 -0
  22. package/dist/commands/press-key.js +15 -0
  23. package/dist/commands/profile-frames.js +4 -2
  24. package/dist/commands/profile.js +111 -7
  25. package/dist/commands/screenshot.js +5 -2
  26. package/dist/commands/scroll-until-visible.js +5 -2
  27. package/dist/commands/scroll.js +4 -1
  28. package/dist/commands/start-device.js +27 -3
  29. package/dist/commands/stop-app.js +2 -1
  30. package/dist/commands/stop-device.js +12 -1
  31. package/dist/commands/swipe.js +4 -1
  32. package/dist/commands/tap.js +3 -2
  33. package/dist/commands/uninstall-app.js +4 -0
  34. package/dist/daemon/server.js +14 -4
  35. package/dist/drivers/bootstrap.js +10 -0
  36. package/dist/drivers/flow-runner.js +16 -3
  37. package/dist/drivers/roku/app-ui-parser.js +122 -0
  38. package/dist/drivers/roku/discovery.js +136 -0
  39. package/dist/drivers/roku/ecp-client.js +396 -0
  40. package/dist/drivers/roku/key-mapping.js +67 -0
  41. package/dist/drivers/roku.js +237 -0
  42. package/dist/drivers/vega/page-source-parser.js +5 -118
  43. package/dist/drivers/xml.js +128 -0
  44. package/dist/enum-options.js +3 -1
  45. package/dist/index.js +1 -1
  46. package/dist/runner.js +32 -0
  47. package/package.json +1 -1
  48. package/skills/conductor-device-interact/SKILL.md +4 -3
  49. package/skills/conductor-device-setup/SKILL.md +35 -2
  50. package/skills/conductor-profiler/SKILL.md +9 -4
package/README.md CHANGED
@@ -26,7 +26,7 @@ conductor assert-visible "Dashboard"
26
26
  conductor take-screenshot --output /tmp/screen.png
27
27
  ```
28
28
 
29
- Targets iOS and tvOS simulators, physical iOS/tvOS devices, Android emulators and devices, Amazon Fire TV, and web via Playwright.
29
+ Targets iOS and tvOS simulators, physical iOS/tvOS devices, Android emulators and devices, Amazon Fire TV, Roku, and web via Playwright.
30
30
 
31
31
  ## Quick start
32
32
 
@@ -12,6 +12,7 @@ const ios_js_1 = require("../drivers/ios.js");
12
12
  const android_js_1 = require("../drivers/android.js");
13
13
  const web_js_1 = require("../drivers/web.js");
14
14
  const vega_js_1 = require("../drivers/vega.js");
15
+ const roku_js_1 = require("../drivers/roku.js");
15
16
  const wait_js_1 = require("../drivers/wait.js");
16
17
  async function assertNotVisible(element, opts = {}, sessionName = 'default', flags = {}) {
17
18
  if (!element && !flags.id && !flags.text) {
@@ -43,7 +44,9 @@ async function assertNotVisible(element, opts = {}, sessionName = 'default', fla
43
44
  else if (driver instanceof web_js_1.WebDriver) {
44
45
  await (0, wait_js_1.waitUntilWebElementGone)(() => driver.viewHierarchy(), sel, flags.timeout);
45
46
  }
46
- else if (driver instanceof android_js_1.AndroidDriver || driver instanceof vega_js_1.VegaDriver) {
47
+ else if (driver instanceof android_js_1.AndroidDriver ||
48
+ driver instanceof vega_js_1.VegaDriver ||
49
+ driver instanceof roku_js_1.RokuDriver) {
47
50
  // Vega emits uiautomator-style XML, so it reuses the Android resolver.
48
51
  await (0, wait_js_1.waitUntilAndroidElementGone)(() => driver.viewHierarchy(), sel, flags.timeout);
49
52
  }
@@ -22,6 +22,7 @@ const ios_js_1 = require("../drivers/ios.js");
22
22
  const android_js_1 = require("../drivers/android.js");
23
23
  const web_js_1 = require("../drivers/web.js");
24
24
  const vega_js_1 = require("../drivers/vega.js");
25
+ const roku_js_1 = require("../drivers/roku.js");
25
26
  const wait_js_1 = require("../drivers/wait.js");
26
27
  const direct_ios_selector_js_1 = require("../drivers/direct-ios-selector.js");
27
28
  async function assertVisible(element, opts = {}, sessionName = 'default', flags = {}) {
@@ -56,8 +57,10 @@ async function assertVisible(element, opts = {}, sessionName = 'default', flags
56
57
  else if (driver instanceof web_js_1.WebDriver) {
57
58
  return (0, wait_js_1.waitForWebElement)(() => driver.viewHierarchy(), sel, timeoutMs);
58
59
  }
59
- else if (driver instanceof android_js_1.AndroidDriver || driver instanceof vega_js_1.VegaDriver) {
60
- // Vega emits uiautomator-style XML, so it reuses the Android resolver.
60
+ else if (driver instanceof android_js_1.AndroidDriver ||
61
+ driver instanceof vega_js_1.VegaDriver ||
62
+ driver instanceof roku_js_1.RokuDriver) {
63
+ // Vega and Roku emit uiautomator-style XML, so they reuse the Android resolver.
61
64
  return (0, wait_js_1.waitForAndroidElement)(() => driver.viewHierarchy(), sel, timeoutMs);
62
65
  }
63
66
  };
@@ -9,6 +9,7 @@ const ios_js_1 = require("../drivers/ios.js");
9
9
  const android_js_1 = require("../drivers/android.js");
10
10
  const web_js_1 = require("../drivers/web.js");
11
11
  const vega_js_1 = require("../drivers/vega.js");
12
+ const roku_js_1 = require("../drivers/roku.js");
12
13
  async function back(opts = {}, sessionName = 'default') {
13
14
  const result = await (0, runner_js_1.runDirect)(async (driver) => {
14
15
  if (driver instanceof ios_js_1.IOSDriver) {
@@ -20,7 +21,7 @@ async function back(opts = {}, sessionName = 'default') {
20
21
  else if (driver instanceof web_js_1.WebDriver) {
21
22
  await driver.goBack();
22
23
  }
23
- else if (driver instanceof vega_js_1.VegaDriver) {
24
+ else if (driver instanceof vega_js_1.VegaDriver || driver instanceof roku_js_1.RokuDriver) {
24
25
  await driver.back();
25
26
  }
26
27
  else if (driver instanceof android_js_1.AndroidDriver) {
@@ -14,6 +14,7 @@ const ios_js_1 = require("../drivers/ios.js");
14
14
  const android_js_1 = require("../drivers/android.js");
15
15
  const web_js_1 = require("../drivers/web.js");
16
16
  const vega_js_1 = require("../drivers/vega.js");
17
+ const roku_js_1 = require("../drivers/roku.js");
17
18
  const a11y_js_1 = require("../drivers/a11y.js");
18
19
  const snapshot_store_js_1 = require("../snapshot-store.js");
19
20
  async function captureUI(outputPath, opts = {}, sessionName = 'default') {
@@ -67,9 +68,12 @@ async function captureUI(outputPath, opts = {}, sessionName = 'default') {
67
68
  a11ySnapshot = built.a11ySnapshot;
68
69
  screenshotBuf = shot;
69
70
  }
70
- else if (driver instanceof android_js_1.AndroidDriver || driver instanceof vega_js_1.VegaDriver) {
71
- // Vega emits uiautomator-style XML, so it reuses the Android a11y builder.
72
- platform = driver instanceof vega_js_1.VegaDriver ? 'vega' : 'android';
71
+ else if (driver instanceof android_js_1.AndroidDriver ||
72
+ driver instanceof vega_js_1.VegaDriver ||
73
+ driver instanceof roku_js_1.RokuDriver) {
74
+ // Vega and Roku emit uiautomator-style XML, so they reuse the Android a11y builder.
75
+ platform =
76
+ driver instanceof vega_js_1.VegaDriver ? 'vega' : driver instanceof roku_js_1.RokuDriver ? 'roku' : 'android';
73
77
  const [info, xml, shot] = await Promise.all([
74
78
  driver.deviceInfo(),
75
79
  driver.viewHierarchy(),
@@ -13,6 +13,7 @@ const ios_js_1 = require("../drivers/ios.js");
13
13
  const android_js_1 = require("../drivers/android.js");
14
14
  const web_js_1 = require("../drivers/web.js");
15
15
  const vega_js_1 = require("../drivers/vega.js");
16
+ const roku_js_1 = require("../drivers/roku.js");
16
17
  const ANDROID_MSG = 'clipboard is iOS-only. On Android, use `conductor input-text` to type instead.';
17
18
  async function clipboardRead(opts = {}, sessionName = 'default') {
18
19
  const result = await (0, runner_js_1.runDirect)(async (driver) => {
@@ -24,6 +25,8 @@ async function clipboardRead(opts = {}, sessionName = 'default') {
24
25
  throw new Error('clipboard read is not supported on Web');
25
26
  if (driver instanceof vega_js_1.VegaDriver)
26
27
  throw new Error('clipboard read is not supported on vega');
28
+ if (driver instanceof roku_js_1.RokuDriver)
29
+ throw new Error('clipboard read is not supported on roku');
27
30
  return '';
28
31
  }, sessionName);
29
32
  if (result.success) {
@@ -52,6 +55,9 @@ async function clipboardWrite(text, opts = {}, sessionName = 'default') {
52
55
  else if (driver instanceof vega_js_1.VegaDriver) {
53
56
  throw new Error('clipboard write is not supported on vega');
54
57
  }
58
+ else if (driver instanceof roku_js_1.RokuDriver) {
59
+ throw new Error('clipboard write is not supported on roku');
60
+ }
55
61
  }, sessionName);
56
62
  if (result.success) {
57
63
  (0, output_js_1.printSuccess)('clipboard write — done', opts);
@@ -81,6 +87,9 @@ async function paste(opts = {}, sessionName = 'default') {
81
87
  else if (driver instanceof vega_js_1.VegaDriver) {
82
88
  throw new Error('paste is not supported on vega');
83
89
  }
90
+ else if (driver instanceof roku_js_1.RokuDriver) {
91
+ throw new Error('paste is not supported on roku');
92
+ }
84
93
  }, sessionName);
85
94
  if (result.success) {
86
95
  (0, output_js_1.printSuccess)('paste — done', opts);
@@ -12,6 +12,7 @@ const ios_js_1 = require("../drivers/ios.js");
12
12
  const android_js_1 = require("../drivers/android.js");
13
13
  const web_js_1 = require("../drivers/web.js");
14
14
  const vega_js_1 = require("../drivers/vega.js");
15
+ const roku_js_1 = require("../drivers/roku.js");
15
16
  const wait_js_1 = require("../drivers/wait.js");
16
17
  const direct_ios_selector_js_1 = require("../drivers/direct-ios-selector.js");
17
18
  async function copyTextFrom(query, opts = {}, sessionName = 'default', flags = {}) {
@@ -33,7 +34,7 @@ async function copyTextFrom(query, opts = {}, sessionName = 'default', flags = {
33
34
  else if (driver instanceof web_js_1.WebDriver) {
34
35
  el = await (0, wait_js_1.waitForWebElement)(() => driver.viewHierarchy(), sel);
35
36
  }
36
- else if (driver instanceof vega_js_1.VegaDriver) {
37
+ else if (driver instanceof vega_js_1.VegaDriver || driver instanceof roku_js_1.RokuDriver) {
37
38
  el = await (0, wait_js_1.waitForAndroidElement)(() => driver.viewHierarchy(), sel);
38
39
  }
39
40
  else if (driver instanceof android_js_1.AndroidDriver) {
@@ -180,6 +180,11 @@ async function crashesList(opts, sessionName, listOpts) {
180
180
  if (!opts.json)
181
181
  console.error('note: crash reports are not supported on vega (Amazon Fire TV)');
182
182
  }
183
+ else if (platform === 'roku') {
184
+ // ECP exposes no crash log; Roku's are read from the dev console by hand.
185
+ if (!opts.json)
186
+ console.error('note: crash reports are not supported on roku');
187
+ }
183
188
  if (opts.json) {
184
189
  (0, output_js_1.printData)({ count: reports.length, reports }, opts);
185
190
  }
@@ -110,6 +110,11 @@ async function deleteDevice(nameOrId, opts, flags) {
110
110
  (0, output_js_1.printError)("delete-device is not supported on vega (Amazon Fire TV) — manage VVDs via Amazon's tooling.", opts);
111
111
  return 1;
112
112
  }
113
+ // A Roku is physical hardware conductor never created.
114
+ if (platform === 'roku' || (nameOrId && nameOrId.startsWith('roku:'))) {
115
+ (0, output_js_1.printError)('delete-device is not supported on roku — it is physical hardware.', opts);
116
+ return 1;
117
+ }
113
118
  const includeIOS = !platform || platform === 'ios';
114
119
  const includeTvOS = !platform || platform === 'tvos';
115
120
  const includeAndroid = !platform || platform === 'android';
@@ -37,6 +37,10 @@ async function downloadApp(appId, output, opts = {}, sessionName = 'default') {
37
37
  (0, output_js_1.printError)('download-app is not supported on vega (Amazon Fire TV).', opts);
38
38
  return 1;
39
39
  }
40
+ if (platform === 'roku') {
41
+ (0, output_js_1.printError)('download-app is not supported on roku.', opts);
42
+ return 1;
43
+ }
40
44
  if (platform === 'ios' || platform === 'tvos') {
41
45
  // Get the .app bundle path from the simulator
42
46
  const getPath = await (0, runner_js_1.spawnCommand)('xcrun', [
@@ -9,9 +9,12 @@ const ios_js_1 = require("../drivers/ios.js");
9
9
  const android_js_1 = require("../drivers/android.js");
10
10
  const web_js_1 = require("../drivers/web.js");
11
11
  const vega_js_1 = require("../drivers/vega.js");
12
+ const roku_js_1 = require("../drivers/roku.js");
12
13
  async function eraseText(characters, opts = {}, sessionName = 'default') {
13
14
  const result = await (0, runner_js_1.runDirect)(async (driver) => {
14
- if (driver instanceof android_js_1.AndroidDriver || driver instanceof vega_js_1.VegaDriver) {
15
+ if (driver instanceof android_js_1.AndroidDriver ||
16
+ driver instanceof vega_js_1.VegaDriver ||
17
+ driver instanceof roku_js_1.RokuDriver) {
15
18
  await driver.eraseAllText(characters);
16
19
  }
17
20
  else if (driver instanceof web_js_1.WebDriver) {
@@ -12,6 +12,7 @@ const ios_js_1 = require("../drivers/ios.js");
12
12
  const android_js_1 = require("../drivers/android.js");
13
13
  const web_js_1 = require("../drivers/web.js");
14
14
  const vega_js_1 = require("../drivers/vega.js");
15
+ const roku_js_1 = require("../drivers/roku.js");
15
16
  const element_resolver_js_1 = require("../drivers/element-resolver.js");
16
17
  // XCUIElementType rawValue → human-readable name.
17
18
  // Source: XCUIElementType enum in XCTest framework.
@@ -209,8 +210,10 @@ async function queryFocused(driver) {
209
210
  const node = findFocusedWeb(hierarchy.elements);
210
211
  return node ? formatWebElement(node) : null;
211
212
  }
212
- else if (driver instanceof android_js_1.AndroidDriver || driver instanceof vega_js_1.VegaDriver) {
213
- // Vega emits uiautomator-style XML, so it reuses the Android parser.
213
+ else if (driver instanceof android_js_1.AndroidDriver ||
214
+ driver instanceof vega_js_1.VegaDriver ||
215
+ driver instanceof roku_js_1.RokuDriver) {
216
+ // Vega and Roku emit uiautomator-style XML, so they reuse the Android parser.
214
217
  const xml = await driver.viewHierarchy();
215
218
  const nodes = (0, element_resolver_js_1.parseAndroidHierarchy)(xml);
216
219
  const node = nodes.find((n) => n.focused);
@@ -10,6 +10,7 @@ const ios_js_1 = require("../drivers/ios.js");
10
10
  const android_js_1 = require("../drivers/android.js");
11
11
  const web_js_1 = require("../drivers/web.js");
12
12
  const vega_js_1 = require("../drivers/vega.js");
13
+ const roku_js_1 = require("../drivers/roku.js");
13
14
  async function resolveDeviceId(sessionName) {
14
15
  if (sessionName !== 'default')
15
16
  return sessionName;
@@ -41,7 +42,9 @@ async function foregroundApp(opts = {}, sessionName = 'default') {
41
42
  else if (driver instanceof web_js_1.WebDriver) {
42
43
  appId = await driver.runningApp();
43
44
  }
44
- else if (driver instanceof android_js_1.AndroidDriver || driver instanceof vega_js_1.VegaDriver) {
45
+ else if (driver instanceof android_js_1.AndroidDriver ||
46
+ driver instanceof vega_js_1.VegaDriver ||
47
+ driver instanceof roku_js_1.RokuDriver) {
45
48
  appId = await driver.getForegroundApp();
46
49
  }
47
50
  else {
@@ -20,6 +20,7 @@ const output_js_1 = require("../output.js");
20
20
  const ios_js_1 = require("../drivers/ios.js");
21
21
  const web_js_1 = require("../drivers/web.js");
22
22
  const vega_js_1 = require("../drivers/vega.js");
23
+ const roku_js_1 = require("../drivers/roku.js");
23
24
  function parseCenter(s) {
24
25
  if (!s)
25
26
  return null;
@@ -111,6 +112,8 @@ async function pinch(opts, sessionName, pinchOpts) {
111
112
  throw new Error('pinch is not supported on Web');
112
113
  if (driver instanceof vega_js_1.VegaDriver)
113
114
  throw new Error('pinch is not supported on vega (Amazon Fire TV)');
115
+ if (driver instanceof roku_js_1.RokuDriver)
116
+ throw new Error('pinch is not supported on roku — Roku is D-pad driven, with no touch input');
114
117
  const { width, height, cx: defCx, cy: defCy } = await screenCenter(driver);
115
118
  const center = parseCenter(pinchOpts.center) ?? { x: defCx, y: defCy };
116
119
  // Start span is half the shorter screen dimension; end is scaled.
@@ -135,6 +138,8 @@ async function rotateGesture(opts, sessionName, rotateOpts) {
135
138
  throw new Error('rotate-gesture is not supported on Web');
136
139
  if (driver instanceof vega_js_1.VegaDriver)
137
140
  throw new Error('rotate-gesture is not supported on vega (Amazon Fire TV)');
141
+ if (driver instanceof roku_js_1.RokuDriver)
142
+ throw new Error('rotate-gesture is not supported on roku — Roku is D-pad driven, with no touch input');
138
143
  const { width, height, cx: defCx, cy: defCy } = await screenCenter(driver);
139
144
  const center = parseCenter(rotateOpts.center) ?? { x: defCx, y: defCy };
140
145
  const radius = Math.min(width, height) * 0.25;
@@ -192,6 +197,8 @@ async function gesture(rawJson, filePath, opts, sessionName) {
192
197
  throw new Error('gesture is not supported on Web');
193
198
  if (driver instanceof vega_js_1.VegaDriver)
194
199
  throw new Error('gesture is not supported on vega (Amazon Fire TV)');
200
+ if (driver instanceof roku_js_1.RokuDriver)
201
+ throw new Error('gesture is not supported on roku — Roku is D-pad driven, with no touch input');
195
202
  await playPaths(driver, paths);
196
203
  }, sessionName);
197
204
  if (result.success) {
@@ -9,6 +9,7 @@ const ios_js_1 = require("../drivers/ios.js");
9
9
  const android_js_1 = require("../drivers/android.js");
10
10
  const web_js_1 = require("../drivers/web.js");
11
11
  const vega_js_1 = require("../drivers/vega.js");
12
+ const roku_js_1 = require("../drivers/roku.js");
12
13
  async function hideKeyboard(opts = {}, sessionName = 'default') {
13
14
  const result = await (0, runner_js_1.runDirect)(async (driver) => {
14
15
  if (driver instanceof ios_js_1.IOSDriver) {
@@ -22,6 +23,10 @@ async function hideKeyboard(opts = {}, sessionName = 'default') {
22
23
  else if (driver instanceof vega_js_1.VegaDriver) {
23
24
  // Vega has no keyboard-hide primitive — no-op
24
25
  }
26
+ else if (driver instanceof roku_js_1.RokuDriver) {
27
+ // Roku dismisses its on-screen keyboard with Back.
28
+ await driver.back();
29
+ }
25
30
  else if (driver instanceof android_js_1.AndroidDriver) {
26
31
  await driver.pressKeyEvent(111); // KEYCODE_ESCAPE
27
32
  }
@@ -10,6 +10,7 @@ const ios_js_1 = require("../drivers/ios.js");
10
10
  const android_js_1 = require("../drivers/android.js");
11
11
  const web_js_1 = require("../drivers/web.js");
12
12
  const vega_js_1 = require("../drivers/vega.js");
13
+ const roku_js_1 = require("../drivers/roku.js");
13
14
  const element_resolver_js_1 = require("../drivers/element-resolver.js");
14
15
  const a11y_js_1 = require("../drivers/a11y.js");
15
16
  async function inspect(opts = {}, sessionName = 'default', inspectOpts = {}) {
@@ -29,7 +30,9 @@ async function inspect(opts = {}, sessionName = 'default', inspectOpts = {}) {
29
30
  const hierarchy = await driver.viewHierarchy(false);
30
31
  hit = (0, element_resolver_js_1.findIOSViewAtPoint)(hierarchy.axElement, x, y, tappable);
31
32
  }
32
- else if (driver instanceof android_js_1.AndroidDriver || driver instanceof vega_js_1.VegaDriver) {
33
+ else if (driver instanceof android_js_1.AndroidDriver ||
34
+ driver instanceof vega_js_1.VegaDriver ||
35
+ driver instanceof roku_js_1.RokuDriver) {
33
36
  const xml = await driver.viewHierarchy();
34
37
  hit = (0, element_resolver_js_1.findAndroidViewAtPoint)(xml, x, y, tappable);
35
38
  }
@@ -68,7 +71,9 @@ async function inspect(opts = {}, sessionName = 'default', inspectOpts = {}) {
68
71
  const built = (0, a11y_js_1.buildWebA11y)(vh);
69
72
  raw = JSON.stringify({ ...vh, elements: built.hierarchy }, null, 2);
70
73
  }
71
- else if (driver instanceof android_js_1.AndroidDriver || driver instanceof vega_js_1.VegaDriver) {
74
+ else if (driver instanceof android_js_1.AndroidDriver ||
75
+ driver instanceof vega_js_1.VegaDriver ||
76
+ driver instanceof roku_js_1.RokuDriver) {
72
77
  const xml = await driver.viewHierarchy();
73
78
  const built = (0, a11y_js_1.buildAndroidA11y)(xml);
74
79
  raw = JSON.stringify(built.hierarchy, null, 2);
@@ -93,7 +98,9 @@ async function inspect(opts = {}, sessionName = 'default', inspectOpts = {}) {
93
98
  const hierarchy = await driver.viewHierarchy();
94
99
  text = (0, element_resolver_js_1.inspectWebToText)(hierarchy);
95
100
  }
96
- else if (driver instanceof android_js_1.AndroidDriver || driver instanceof vega_js_1.VegaDriver) {
101
+ else if (driver instanceof android_js_1.AndroidDriver ||
102
+ driver instanceof vega_js_1.VegaDriver ||
103
+ driver instanceof roku_js_1.RokuDriver) {
97
104
  const xml = await driver.viewHierarchy();
98
105
  text = (0, element_resolver_js_1.inspectAndroidToText)(xml);
99
106
  }
@@ -30,6 +30,11 @@ async function installApp(appPath, opts = {}, sessionName = 'default') {
30
30
  (0, output_js_1.printError)('install-app is not supported on web. Use launch-app with a URL instead.', opts);
31
31
  return 1;
32
32
  }
33
+ else if (platform === 'roku') {
34
+ (0, output_js_1.printError)('install-app is not supported on roku — sideload the channel through the ' +
35
+ "device's developer web server (http://<device-ip>).", opts);
36
+ return 1;
37
+ }
33
38
  else if (platform === 'vega') {
34
39
  // Vega installs a `.vpkg` through Amazon's CLI; strip the `vega:` id prefix.
35
40
  try {
@@ -28,6 +28,7 @@ const fs_1 = __importDefault(require("fs"));
28
28
  const android_js_1 = require("../drivers/android.js");
29
29
  const web_js_1 = require("../drivers/web.js");
30
30
  const vega_js_1 = require("../drivers/vega.js");
31
+ const roku_js_1 = require("../drivers/roku.js");
31
32
  async function launchApp(appId, deviceId, opts = {}, sessionName = 'default', flags = {}) {
32
33
  if (!appId) {
33
34
  (0, output_js_1.printError)('launch-app requires <appId>', opts);
@@ -50,7 +51,7 @@ async function launchApp(appId, deviceId, opts = {}, sessionName = 'default', fl
50
51
  await driver.terminateApp(appId);
51
52
  else if (driver instanceof web_js_1.WebDriver)
52
53
  await driver.terminateApp();
53
- else if (driver instanceof vega_js_1.VegaDriver)
54
+ else if (driver instanceof vega_js_1.VegaDriver || driver instanceof roku_js_1.RokuDriver)
54
55
  await driver.terminateApp(appId);
55
56
  else if (driver instanceof android_js_1.AndroidDriver)
56
57
  await driver.stopApp(appId);
@@ -79,7 +80,7 @@ async function launchApp(appId, deviceId, opts = {}, sessionName = 'default', fl
79
80
  else if (driver instanceof web_js_1.WebDriver) {
80
81
  await driver.launchApp(appId);
81
82
  }
82
- else if (driver instanceof vega_js_1.VegaDriver) {
83
+ else if (driver instanceof vega_js_1.VegaDriver || driver instanceof roku_js_1.RokuDriver) {
83
84
  await driver.launchApp(appId, flags.launchArgs);
84
85
  }
85
86
  else if (driver instanceof android_js_1.AndroidDriver) {
@@ -29,6 +29,11 @@ async function listApps(opts = {}, sessionName = 'default') {
29
29
  (0, output_js_1.printError)('list-apps is not supported on web. Use foreground-app to get the current URL.', opts);
30
30
  return 1;
31
31
  }
32
+ else if (platform === 'roku') {
33
+ // ECP can name only the *active* channel, not enumerate installed ones.
34
+ (0, output_js_1.printError)('list-apps is not supported on roku. Use foreground-app to get the active channel id.', opts);
35
+ return 1;
36
+ }
32
37
  else if (platform === 'vega') {
33
38
  // Vega is driven through Amazon's CLI, not adb; strip the `vega:` id prefix.
34
39
  appIds = (await new cli_js_1.VegaCli(deviceId.replace(/^vega:/, '')).listInstalledApps()).sort();
@@ -16,6 +16,7 @@ const bootstrap_js_1 = require("../drivers/bootstrap.js");
16
16
  const client_js_1 = require("../daemon/client.js");
17
17
  const protocol_js_1 = require("../daemon/protocol.js");
18
18
  const cli_js_1 = require("../drivers/vega/cli.js");
19
+ const discovery_js_1 = require("../drivers/roku/discovery.js");
19
20
  const cdp_discovery_js_1 = require("../drivers/cdp-discovery.js");
20
21
  // Captured during discoverAvailableDevices so listDevices() can report it.
21
22
  // Module-scoped because the discover function returns Device[]; bolting an
@@ -124,6 +125,17 @@ async function discoverBootedDevices() {
124
125
  catch {
125
126
  /* vega CLI not installed */
126
127
  }
128
+ // Roku: the pinned CONDUCTOR_ROKU_HOST device, plus SSDP hits when
129
+ // CONDUCTOR_ROKU_DISCOVERY is on. Physical hardware only — Roku has no emulator.
130
+ for (const d of await (0, discovery_js_1.discoverRokuDevices)().catch(() => [])) {
131
+ devices.push({
132
+ id: `roku:${d.host}`,
133
+ name: d.friendlyName || d.modelName,
134
+ platform: 'roku',
135
+ status: 'booted',
136
+ formFactor: 'tv',
137
+ });
138
+ }
127
139
  // External CDP endpoints (e.g. an Electron app started with
128
140
  // --remote-debugging-port): one device per discovered webview/tile.
129
141
  devices.push(...(await (0, cdp_discovery_js_1.discoverCdpDevices)()));
@@ -1121,6 +1121,11 @@ async function memory(appIdArg, opts = {}, sessionName = 'default', memOpts = {}
1121
1121
  (0, output_js_1.printError)('memory is not yet supported on vega (Amazon Fire TV)', opts);
1122
1122
  return 1;
1123
1123
  }
1124
+ if (platform === 'roku') {
1125
+ // ECP exposes no memory counters, and there is no host-side tooling to ask.
1126
+ (0, output_js_1.printError)('memory is not supported on roku', opts);
1127
+ return 1;
1128
+ }
1124
1129
  let report;
1125
1130
  if (platform === 'web') {
1126
1131
  report = await collectWeb(deviceId, sessionName, memOpts);
@@ -19,6 +19,8 @@ const ios_js_1 = require("../drivers/ios.js");
19
19
  const android_js_1 = require("../drivers/android.js");
20
20
  const web_js_1 = require("../drivers/web.js");
21
21
  const vega_js_1 = require("../drivers/vega.js");
22
+ const roku_js_1 = require("../drivers/roku.js");
23
+ const key_mapping_js_1 = require("../drivers/roku/key-mapping.js");
22
24
  exports.VALID_KEYS = [
23
25
  'Enter',
24
26
  'Backspace',
@@ -54,6 +56,9 @@ exports.VALID_KEYS = [
54
56
  'TV Input HDMI 1',
55
57
  'TV Input HDMI 2',
56
58
  'TV Input HDMI 3',
59
+ 'Remote Info',
60
+ 'Remote Instant Replay',
61
+ 'Remote Search',
57
62
  ];
58
63
  // iOS XCTest pressKey accepts these values (maps to XCUIKeyboardKey)
59
64
  const IOS_KEY_MAP = {
@@ -131,6 +136,9 @@ exports.ANDROID_KEYCODE = {
131
136
  'TV Input HDMI 1': 243,
132
137
  'TV Input HDMI 2': 244,
133
138
  'TV Input HDMI 3': 245,
139
+ 'Remote Info': 165,
140
+ 'Remote Instant Replay': 273, // KEYCODE_MEDIA_SKIP_BACKWARD
141
+ 'Remote Search': 84,
134
142
  };
135
143
  /**
136
144
  * Send `key` on an already-connected driver. Split out of `pressKey` so
@@ -191,6 +199,13 @@ async function dispatchKey(driver, matched, holdSeconds) {
191
199
  }
192
200
  // Keys not mapped on vega are silently ignored
193
201
  }
202
+ else if (driver instanceof roku_js_1.RokuDriver) {
203
+ // Keys not mapped on roku are silently ignored, as on tvOS and vega — but a
204
+ // mapped key that the device rejects must still fail.
205
+ if ((0, key_mapping_js_1.rokuEcpKey)(matched)) {
206
+ await driver.pressKeyNamed(matched);
207
+ }
208
+ }
194
209
  else if (driver instanceof android_js_1.AndroidDriver) {
195
210
  const code = exports.ANDROID_KEYCODE[matched];
196
211
  if (code !== undefined) {
@@ -647,8 +647,10 @@ async function profileFramesReport(opts, sessionName, frameOpts) {
647
647
  if (frameOpts.trackSec !== undefined && repeat === 1) {
648
648
  notes.push({
649
649
  code: 'single-window',
650
- message: 'One window carries no run-to-run variance, so a later --diff cannot tell a real ' +
651
- 'change from noise. Capture a baseline with --repeat 5 to record its own spread.',
650
+ message: 'One window carries no run-to-run variance, so nothing here can be called a change. ' +
651
+ 'Measured on a Fire TV Stick, five identical captures of the same screen ranged ' +
652
+ '37.8-72.9% janky and 6.2-13.0ms issueDraw p50 — wide enough to invent a regression ' +
653
+ 'or hide one. Treat --repeat 5 as the default on TV, not an option.',
652
654
  });
653
655
  }
654
656
  const report = {