@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.
- package/README.md +1 -1
- package/dist/commands/assert-not-visible.js +4 -1
- package/dist/commands/assert-visible.js +5 -2
- package/dist/commands/back.js +2 -1
- package/dist/commands/capture-ui.js +7 -3
- package/dist/commands/clipboard.js +9 -0
- package/dist/commands/copy-text-from.js +2 -1
- package/dist/commands/crashes.js +5 -0
- package/dist/commands/delete-device.js +5 -0
- package/dist/commands/download-app.js +4 -0
- package/dist/commands/erase-text.js +4 -1
- package/dist/commands/focused.js +5 -2
- package/dist/commands/foreground-app.js +4 -1
- package/dist/commands/gestures.js +7 -0
- package/dist/commands/hide-keyboard.js +5 -0
- package/dist/commands/inspect.js +10 -3
- package/dist/commands/install-app.js +5 -0
- package/dist/commands/launch-app.js +3 -2
- package/dist/commands/list-apps.js +5 -0
- package/dist/commands/list-devices.js +12 -0
- package/dist/commands/memory.js +5 -0
- package/dist/commands/press-key.js +15 -0
- package/dist/commands/profile-frames.js +4 -2
- package/dist/commands/profile.js +111 -7
- package/dist/commands/screenshot.js +5 -2
- package/dist/commands/scroll-until-visible.js +5 -2
- package/dist/commands/scroll.js +4 -1
- package/dist/commands/start-device.js +27 -3
- package/dist/commands/stop-app.js +2 -1
- package/dist/commands/stop-device.js +12 -1
- package/dist/commands/swipe.js +4 -1
- package/dist/commands/tap.js +3 -2
- package/dist/commands/uninstall-app.js +4 -0
- package/dist/daemon/server.js +14 -4
- package/dist/drivers/bootstrap.js +10 -0
- package/dist/drivers/flow-runner.js +16 -3
- package/dist/drivers/roku/app-ui-parser.js +122 -0
- package/dist/drivers/roku/discovery.js +136 -0
- package/dist/drivers/roku/ecp-client.js +396 -0
- package/dist/drivers/roku/key-mapping.js +67 -0
- package/dist/drivers/roku.js +237 -0
- package/dist/drivers/vega/page-source-parser.js +5 -118
- package/dist/drivers/xml.js +128 -0
- package/dist/enum-options.js +3 -1
- package/dist/index.js +1 -1
- package/dist/runner.js +32 -0
- package/package.json +1 -1
- package/skills/conductor-device-interact/SKILL.md +4 -3
- package/skills/conductor-device-setup/SKILL.md +35 -2
- package/skills/conductor-profiler/SKILL.md +9 -4
package/dist/commands/profile.js
CHANGED
|
@@ -4,6 +4,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.REACT_PROFILER_STOP = exports.REACT_PROFILER_READ = exports.REACT_PROFILER_INSTALL = exports.HELP = void 0;
|
|
7
|
+
exports.isUnsymbolised = isUnsymbolised;
|
|
8
|
+
exports.rollupByDso = rollupByDso;
|
|
9
|
+
exports.unsymbolisedPercent = unsymbolisedPercent;
|
|
7
10
|
exports.parseSimpleperfReport = parseSimpleperfReport;
|
|
8
11
|
exports.profileCpu = profileCpu;
|
|
9
12
|
exports.heapGrowth = heapGrowth;
|
|
@@ -37,9 +40,80 @@ const output_js_1 = require("../output.js");
|
|
|
37
40
|
const bootstrap_js_1 = require("../drivers/bootstrap.js");
|
|
38
41
|
const sdk_js_1 = require("../android/sdk.js");
|
|
39
42
|
const runner_js_1 = require("../runner.js");
|
|
43
|
+
const device_js_1 = require("../android/device.js");
|
|
40
44
|
const memory_js_1 = require("./memory.js");
|
|
41
45
|
const metro_cdp_js_1 = require("../drivers/metro-cdp.js");
|
|
42
46
|
const profile_gc_js_1 = require("./profile-gc.js");
|
|
47
|
+
/** simpleperf renders an unresolved address as `libfoo.so[+1a62f8]`. */
|
|
48
|
+
function isUnsymbolised(symbol) {
|
|
49
|
+
return /\[\+[0-9a-fx]+\]\s*$/i.test(symbol);
|
|
50
|
+
}
|
|
51
|
+
function shortDso(dso) {
|
|
52
|
+
return dso.includes('/') ? dso.slice(dso.lastIndexOf('/') + 1) : dso;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Sum a flat symbol table by library.
|
|
56
|
+
*
|
|
57
|
+
* A stripped app library shows up as a dozen `libfoo.so[+offset]` rows that a
|
|
58
|
+
* reader cannot tell apart from a dozen unrelated hotspots. Rolled up, one
|
|
59
|
+
* library dominating is immediately visible — which is the actual finding when
|
|
60
|
+
* a profile has no single hot symbol.
|
|
61
|
+
*/
|
|
62
|
+
function rollupByDso(entries) {
|
|
63
|
+
const byDso = new Map();
|
|
64
|
+
for (const e of entries) {
|
|
65
|
+
const key = shortDso(e.dso);
|
|
66
|
+
const slot = byDso.get(key) ?? { percent: 0, symbols: 0, unsym: 0 };
|
|
67
|
+
slot.percent += e.percent;
|
|
68
|
+
slot.symbols++;
|
|
69
|
+
if (isUnsymbolised(e.symbol))
|
|
70
|
+
slot.unsym++;
|
|
71
|
+
byDso.set(key, slot);
|
|
72
|
+
}
|
|
73
|
+
return [...byDso.entries()]
|
|
74
|
+
.map(([dso, v]) => ({
|
|
75
|
+
dso,
|
|
76
|
+
percent: Math.round(v.percent * 100) / 100,
|
|
77
|
+
symbols: v.symbols,
|
|
78
|
+
unsymbolised: v.unsym === v.symbols,
|
|
79
|
+
}))
|
|
80
|
+
.sort((a, b) => b.percent - a.percent);
|
|
81
|
+
}
|
|
82
|
+
/** Share of the sampled overhead that resolved to no function name. */
|
|
83
|
+
function unsymbolisedPercent(entries) {
|
|
84
|
+
const total = entries.reduce((a, e) => a + e.percent, 0);
|
|
85
|
+
if (total <= 0)
|
|
86
|
+
return 0;
|
|
87
|
+
const unsym = entries.filter((e) => isUnsymbolised(e.symbol)).reduce((a, e) => a + e.percent, 0);
|
|
88
|
+
return Math.round((unsym / total) * 10000) / 100;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Turn a simpleperf failure into something actionable.
|
|
92
|
+
*
|
|
93
|
+
* simpleperf needs the target to be `android:debuggable` or to declare
|
|
94
|
+
* `<profileable android:shell="true"/>`. A stock release APK is neither, and
|
|
95
|
+
* the resulting error says only `exited with 1`.
|
|
96
|
+
*/
|
|
97
|
+
async function explainSimpleperfFailure(deviceId, appId, stderr) {
|
|
98
|
+
// Drop simpleperf's PMU probing chatter; it is present on every run.
|
|
99
|
+
const meaningful = stderr
|
|
100
|
+
.split(/\r?\n/)
|
|
101
|
+
.filter((l) => l.trim() && !/cannot read event type|Failed to read event type/i.test(l))
|
|
102
|
+
.slice(-4)
|
|
103
|
+
.join('\n');
|
|
104
|
+
if (appId) {
|
|
105
|
+
const pkg = await (0, device_js_1.adbShell)(deviceId, ['dumpsys', 'package', appId]);
|
|
106
|
+
if (pkg.success && /^\s*flags=\[/m.test(pkg.stdout) && !/\bDEBUGGABLE\b/.test(pkg.stdout)) {
|
|
107
|
+
return (`simpleperf cannot profile ${appId}: the installed APK is neither debuggable nor ` +
|
|
108
|
+
`profileable.\nsimpleperf needs android:debuggable, or ` +
|
|
109
|
+
`\`<profileable android:shell="true"/>\` inside <application> — the latter is a ` +
|
|
110
|
+
`one-line manifest change that keeps the build a release build and is the right fix ` +
|
|
111
|
+
`for a perf build.\n` +
|
|
112
|
+
(meaningful ? `simpleperf said:\n${meaningful}` : ''));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return `simpleperf record failed.\n${meaningful || '(no diagnostic output)'}`;
|
|
116
|
+
}
|
|
43
117
|
/**
|
|
44
118
|
* Parse `simpleperf report --sort dso,symbol`. The header row is located by its
|
|
45
119
|
* `Overhead` column rather than by line number, since simpleperf prefixes the
|
|
@@ -105,11 +179,13 @@ async function recordAndroidCpu(deviceId, appId, durationSec, out, report) {
|
|
|
105
179
|
else {
|
|
106
180
|
recordArgs.push('-a');
|
|
107
181
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
182
|
+
// Capture rather than inherit: simpleperf prints a wall of `cannot read event
|
|
183
|
+
// type` lines while probing PMU support, and those are not the error. Letting
|
|
184
|
+
// them through buries whatever actually went wrong.
|
|
185
|
+
const rec = await (0, runner_js_1.spawnCommand)(adb, recordArgs, { env });
|
|
186
|
+
if (!rec.success) {
|
|
187
|
+
throw new Error(await explainSimpleperfFailure(deviceId, appId, rec.stderr));
|
|
188
|
+
}
|
|
113
189
|
// Symbolize on-device before pulling: /system/bin/simpleperf resolves against
|
|
114
190
|
// the libraries actually loaded there, which a host-side report cannot do
|
|
115
191
|
// without a matching symfs.
|
|
@@ -168,6 +244,10 @@ async function profileCpu(opts, sessionName, profileOpts) {
|
|
|
168
244
|
else if (platform === 'android') {
|
|
169
245
|
entries = await recordAndroidCpu(sessionName, profileOpts.appId, profileOpts.durationSec, out, profileOpts.report ? { top: profileOpts.top ?? 30 } : undefined);
|
|
170
246
|
}
|
|
247
|
+
else if (platform === 'roku') {
|
|
248
|
+
(0, output_js_1.printError)('profile cpu is not supported on roku — ECP exposes no CPU counters.', opts);
|
|
249
|
+
return 1;
|
|
250
|
+
}
|
|
171
251
|
else if (platform === 'vega') {
|
|
172
252
|
// Vega is Amazon's own OS, not Android — no simpleperf, no dumpsys. A
|
|
173
253
|
// physical Fire TV Stick runs Fire OS (Android) over adb and is a
|
|
@@ -182,17 +262,41 @@ async function profileCpu(opts, sessionName, profileOpts) {
|
|
|
182
262
|
(0, output_js_1.printError)(`profile cpu is not supported on platform ${platform ?? '(unknown)'}`, opts);
|
|
183
263
|
return 1;
|
|
184
264
|
}
|
|
265
|
+
const byDso = entries ? rollupByDso(entries) : undefined;
|
|
266
|
+
const unsymPercent = entries ? unsymbolisedPercent(entries) : undefined;
|
|
185
267
|
if (opts.json) {
|
|
186
|
-
(0, output_js_1.printData)({
|
|
268
|
+
(0, output_js_1.printData)({
|
|
269
|
+
out,
|
|
270
|
+
durationSec: profileOpts.durationSec,
|
|
271
|
+
platform,
|
|
272
|
+
symbols: entries,
|
|
273
|
+
byDso,
|
|
274
|
+
unsymbolisedPercent: unsymPercent,
|
|
275
|
+
}, opts);
|
|
187
276
|
}
|
|
188
277
|
else {
|
|
189
278
|
(0, output_js_1.printSuccess)(`profile cpu — recorded ${profileOpts.durationSec}s → ${out}`, opts);
|
|
279
|
+
if (byDso && byDso.length > 0) {
|
|
280
|
+
// Lead with the rollup: a flat profile has no hot symbol, and the real
|
|
281
|
+
// finding is usually which library the samples are spread across.
|
|
282
|
+
console.log(`\n by library`);
|
|
283
|
+
for (const d of byDso.slice(0, 10)) {
|
|
284
|
+
console.log(` ${`${d.percent}%`.padStart(9)} ${d.dso} (${d.symbols} symbol${d.symbols === 1 ? '' : 's'}` +
|
|
285
|
+
`${d.unsymbolised ? ', unsymbolised' : ''})`);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
190
288
|
if (entries) {
|
|
191
|
-
console.log(
|
|
289
|
+
console.log(`\n ${'overhead'.padStart(9)} symbol`);
|
|
192
290
|
for (const e of entries) {
|
|
193
291
|
console.log(` ${`${e.percent}%`.padStart(9)} ${e.symbol} [${e.dso}]`);
|
|
194
292
|
}
|
|
195
293
|
}
|
|
294
|
+
if (unsymPercent !== undefined && unsymPercent > 10) {
|
|
295
|
+
console.log(`\n note: ${unsymPercent}% of sampled overhead resolved to a raw address rather ` +
|
|
296
|
+
`than a function — those libraries are stripped. Read the by-library rollup above ` +
|
|
297
|
+
`instead of the symbol table; a dozen \`lib.so[+offset]\` rows are one library, ` +
|
|
298
|
+
`not a dozen findings.`);
|
|
299
|
+
}
|
|
196
300
|
}
|
|
197
301
|
return 0;
|
|
198
302
|
}
|
|
@@ -28,6 +28,7 @@ const ios_js_1 = require("../drivers/ios.js");
|
|
|
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
|
const wait_js_1 = require("../drivers/wait.js");
|
|
32
33
|
const direct_ios_selector_js_1 = require("../drivers/direct-ios-selector.js");
|
|
33
34
|
const png_crop_js_1 = require("../png-crop.js");
|
|
@@ -85,8 +86,10 @@ async function screenshot(outputPath, opts = {}, sessionName = 'default', fullPa
|
|
|
85
86
|
hierarchyH = info.heightPixels;
|
|
86
87
|
el = await (0, wait_js_1.waitForWebElement)(() => driver.viewHierarchy(), sel);
|
|
87
88
|
}
|
|
88
|
-
else if (driver instanceof android_js_1.AndroidDriver ||
|
|
89
|
-
|
|
89
|
+
else if (driver instanceof android_js_1.AndroidDriver ||
|
|
90
|
+
driver instanceof vega_js_1.VegaDriver ||
|
|
91
|
+
driver instanceof roku_js_1.RokuDriver) {
|
|
92
|
+
// Vega and Roku emit uiautomator-style XML, so they reuse the Android resolver.
|
|
90
93
|
const xml = await driver.viewHierarchy();
|
|
91
94
|
// Android XML root bounds: derive from the first parseable <node bounds="[0,0][W,H]">
|
|
92
95
|
const m = xml.match(/<node[^>]*bounds="\[0,0\]\[(\d+),(\d+)\]"/);
|
|
@@ -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 element_resolver_js_1 = require("../drivers/element-resolver.js");
|
|
17
18
|
const direct_ios_selector_js_1 = require("../drivers/direct-ios-selector.js");
|
|
18
19
|
const utils_js_1 = require("../utils.js");
|
|
@@ -78,8 +79,10 @@ async function scrollUntilVisible(element, opts = {}, sessionName = 'default', f
|
|
|
78
79
|
const { widthPixels: w, heightPixels: h } = info;
|
|
79
80
|
await driver.swipe(coords.startX * w, coords.startY * h, coords.endX * w, coords.endY * h, 500);
|
|
80
81
|
}
|
|
81
|
-
else if (driver instanceof android_js_1.AndroidDriver ||
|
|
82
|
-
|
|
82
|
+
else if (driver instanceof android_js_1.AndroidDriver ||
|
|
83
|
+
driver instanceof vega_js_1.VegaDriver ||
|
|
84
|
+
driver instanceof roku_js_1.RokuDriver) {
|
|
85
|
+
// Vega and Roku emit uiautomator-style XML, so they reuse the Android resolver.
|
|
83
86
|
const xml = await driver.viewHierarchy();
|
|
84
87
|
if ((0, element_resolver_js_1.findAndroidElement)(xml, sel)) {
|
|
85
88
|
(0, output_js_1.printSuccess)(`scroll-until-visible ${label} — found`, opts);
|
package/dist/commands/scroll.js
CHANGED
|
@@ -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
|
const utils_js_1 = require("../utils.js");
|
|
13
14
|
async function scroll(direction = 'down', opts = {}, sessionName = 'default') {
|
|
14
15
|
const valid = ['down', 'up', 'left', 'right'];
|
|
@@ -32,7 +33,9 @@ async function scroll(direction = 'down', opts = {}, sessionName = 'default') {
|
|
|
32
33
|
const { widthPixels: w, heightPixels: h } = info;
|
|
33
34
|
await driver.swipe(coords.startX * w, coords.startY * h, coords.endX * w, coords.endY * h, 500);
|
|
34
35
|
}
|
|
35
|
-
else if (driver instanceof android_js_1.AndroidDriver ||
|
|
36
|
+
else if (driver instanceof android_js_1.AndroidDriver ||
|
|
37
|
+
driver instanceof vega_js_1.VegaDriver ||
|
|
38
|
+
driver instanceof roku_js_1.RokuDriver) {
|
|
36
39
|
const info = await driver.deviceInfo();
|
|
37
40
|
const { widthPixels: w, heightPixels: h } = info;
|
|
38
41
|
await driver.swipe(coords.startX * w, coords.startY * h, coords.endX * w, coords.endY * h, 500);
|
|
@@ -11,7 +11,7 @@ exports.buildAvdmanagerCreateArgs = buildAvdmanagerCreateArgs;
|
|
|
11
11
|
exports.raiseAvdConfigRam = raiseAvdConfigRam;
|
|
12
12
|
exports.startDevice = startDevice;
|
|
13
13
|
exports.HELP = ` start-device
|
|
14
|
-
--platform <ios|android|tvos|web|vega> Boot a simulator/emulator, start the web driver (Playwright),
|
|
14
|
+
--platform <ios|android|tvos|web|vega|roku> Boot a simulator/emulator, start the web driver (Playwright), attach a Vega VVD, or check a Roku device
|
|
15
15
|
--os-version <n> iOS/tvOS version (e.g. 18) or Android API level (e.g. 33)
|
|
16
16
|
--avd <name> Android AVD name (default: first available; created if missing + --device-type)
|
|
17
17
|
--name <name> Set a custom name on the device after creation (iOS/tvOS/web)
|
|
@@ -34,6 +34,7 @@ const bootstrap_js_1 = require("../drivers/bootstrap.js");
|
|
|
34
34
|
const output_js_1 = require("../output.js");
|
|
35
35
|
const utils_js_1 = require("../utils.js");
|
|
36
36
|
const cli_js_1 = require("../drivers/vega/cli.js");
|
|
37
|
+
const discovery_js_1 = require("../drivers/roku/discovery.js");
|
|
37
38
|
const IOS_BOOT_TIMEOUT_MS = 120000;
|
|
38
39
|
const ANDROID_BOOT_TIMEOUT_MS = 120000;
|
|
39
40
|
const POLL_MS = 1000;
|
|
@@ -807,10 +808,31 @@ async function startVega(opts, deviceName) {
|
|
|
807
808
|
(0, output_js_1.printSuccess)(`Vega device ready: ${device.serial}`, opts);
|
|
808
809
|
return 0;
|
|
809
810
|
}
|
|
811
|
+
// ── Roku ────────────────────────────────────────────────────────────────────
|
|
812
|
+
/**
|
|
813
|
+
* Roku is physical hardware with no emulator, so there is nothing to boot: this
|
|
814
|
+
* resolves the target device (explicit `--name <host>`, else `CONDUCTOR_ROKU_HOST`
|
|
815
|
+
* or an SSDP hit) and confirms it answers on ECP, so a misconfigured device is
|
|
816
|
+
* reported here rather than on the first `tap-on`.
|
|
817
|
+
*/
|
|
818
|
+
async function startRoku(opts, deviceName) {
|
|
819
|
+
const host = deviceName?.replace(/^roku:/, '') ?? process.env.CONDUCTOR_ROKU_HOST?.trim();
|
|
820
|
+
const device = host ? await (0, discovery_js_1.describe)(host) : (await (0, discovery_js_1.discoverRokuDevices)())[0];
|
|
821
|
+
if (!device) {
|
|
822
|
+
(0, output_js_1.printError)(host
|
|
823
|
+
? `Cannot reach the Roku device at ${host} on ECP port 8060. Check that it is on ` +
|
|
824
|
+
`this network and in developer mode.`
|
|
825
|
+
: 'No Roku device found. Set CONDUCTOR_ROKU_HOST=<device-ip>, pass --name <device-ip>, ' +
|
|
826
|
+
'or set CONDUCTOR_ROKU_DISCOVERY=true to scan the LAN.', opts);
|
|
827
|
+
return 1;
|
|
828
|
+
}
|
|
829
|
+
(0, output_js_1.printSuccess)(`Roku device ready: ${device.friendlyName || device.modelName} (roku:${device.host})`, opts);
|
|
830
|
+
return 0;
|
|
831
|
+
}
|
|
810
832
|
// ── Entry point ───────────────────────────────────────────────────────────────
|
|
811
833
|
async function startDevice(platform, opts, flags) {
|
|
812
834
|
if (!platform) {
|
|
813
|
-
(0, output_js_1.printError)('start-device requires --platform ios|android|tvos|web|vega', opts);
|
|
835
|
+
(0, output_js_1.printError)('start-device requires --platform ios|android|tvos|web|vega|roku', opts);
|
|
814
836
|
return 1;
|
|
815
837
|
}
|
|
816
838
|
switch (platform.toLowerCase()) {
|
|
@@ -824,8 +846,10 @@ async function startDevice(platform, opts, flags) {
|
|
|
824
846
|
return startWebDriver(opts, flags.browser, flags.name);
|
|
825
847
|
case 'vega':
|
|
826
848
|
return startVega(opts, flags.name);
|
|
849
|
+
case 'roku':
|
|
850
|
+
return startRoku(opts, flags.name);
|
|
827
851
|
default:
|
|
828
|
-
(0, output_js_1.printError)(`Unknown platform "${platform}". Use ios, android, tvos, web, or
|
|
852
|
+
(0, output_js_1.printError)(`Unknown platform "${platform}". Use ios, android, tvos, web, vega, or roku.`, opts);
|
|
829
853
|
return 1;
|
|
830
854
|
}
|
|
831
855
|
}
|
|
@@ -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 stopApp(appId, opts = {}, sessionName = 'default') {
|
|
14
15
|
const session = await (0, session_js_1.getSession)(sessionName);
|
|
15
16
|
const resolvedAppId = appId ?? session.appId;
|
|
@@ -24,7 +25,7 @@ async function stopApp(appId, opts = {}, sessionName = 'default') {
|
|
|
24
25
|
else if (driver instanceof web_js_1.WebDriver) {
|
|
25
26
|
await driver.terminateApp();
|
|
26
27
|
}
|
|
27
|
-
else if (driver instanceof vega_js_1.VegaDriver) {
|
|
28
|
+
else if (driver instanceof vega_js_1.VegaDriver || driver instanceof roku_js_1.RokuDriver) {
|
|
28
29
|
await driver.stopApp(resolvedAppId);
|
|
29
30
|
}
|
|
30
31
|
else if (driver instanceof android_js_1.AndroidDriver) {
|
|
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.HELP = void 0;
|
|
4
4
|
exports.stopDevice = stopDevice;
|
|
5
5
|
exports.HELP = ` stop-device [<name-or-id>]
|
|
6
|
-
--platform <ios|tvos|android|web|vega> Scope to a single platform
|
|
6
|
+
--platform <ios|tvos|android|web|vega|roku> Scope to a single platform
|
|
7
7
|
--all Stop all booted simulators / running emulators / web sessions`;
|
|
8
8
|
const runner_js_1 = require("../runner.js");
|
|
9
9
|
const sdk_js_1 = require("../android/sdk.js");
|
|
@@ -36,6 +36,7 @@ async function stopDevice(nameOrId, opts, flags) {
|
|
|
36
36
|
const includeAndroid = !platform || platform === 'android';
|
|
37
37
|
const includeWeb = !platform || platform === 'web';
|
|
38
38
|
const includeVega = !platform || platform === 'vega';
|
|
39
|
+
const includeRoku = !platform || platform === 'roku';
|
|
39
40
|
const stopped = [];
|
|
40
41
|
// ── --all mode ───────────────────────────────────────────────────────────
|
|
41
42
|
if (flags.all) {
|
|
@@ -51,6 +52,8 @@ async function stopDevice(nameOrId, opts, flags) {
|
|
|
51
52
|
continue;
|
|
52
53
|
if (d.platform === 'vega' && !includeVega)
|
|
53
54
|
continue;
|
|
55
|
+
if (d.platform === 'roku' && !includeRoku)
|
|
56
|
+
continue;
|
|
54
57
|
try {
|
|
55
58
|
if (d.platform === 'ios' || d.platform === 'tvos') {
|
|
56
59
|
await shutdownSimulator(d.id);
|
|
@@ -62,6 +65,10 @@ async function stopDevice(nameOrId, opts, flags) {
|
|
|
62
65
|
// Vega VVD lifecycle is owned by Amazon's tooling — we only stop our log daemon.
|
|
63
66
|
await (0, client_js_1.stopDaemon)(d.id);
|
|
64
67
|
}
|
|
68
|
+
else if (d.platform === 'roku') {
|
|
69
|
+
// A Roku is physical hardware we never booted; there is nothing to stop.
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
65
72
|
stopped.push({ id: d.id, name: d.name, platform: d.platform });
|
|
66
73
|
}
|
|
67
74
|
catch (e) {
|
|
@@ -104,6 +111,10 @@ async function stopDevice(nameOrId, opts, flags) {
|
|
|
104
111
|
// Vega VVD lifecycle is owned by Amazon's tooling — we only stop our log daemon.
|
|
105
112
|
await (0, client_js_1.stopDaemon)(match.id);
|
|
106
113
|
}
|
|
114
|
+
else if (match.platform === 'roku') {
|
|
115
|
+
(0, output_js_1.printError)(`${match.name} is a physical Roku device — conductor never booted it, so there is nothing to stop.`, opts);
|
|
116
|
+
return 1;
|
|
117
|
+
}
|
|
107
118
|
}
|
|
108
119
|
catch (e) {
|
|
109
120
|
(0, output_js_1.printError)(e instanceof Error ? e.message : String(e), opts);
|
package/dist/commands/swipe.js
CHANGED
|
@@ -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 utils_js_1 = require("../utils.js");
|
|
17
18
|
function parseCoordPair(s) {
|
|
18
19
|
const [xs, ys] = s.split(',').map((p) => p.trim());
|
|
@@ -71,7 +72,9 @@ async function swipe(direction, opts = {}, sessionName = 'default', flags = {})
|
|
|
71
72
|
}
|
|
72
73
|
await driver.swipe(startX, startY, endX, endY, durationMs);
|
|
73
74
|
}
|
|
74
|
-
else if (driver instanceof android_js_1.AndroidDriver ||
|
|
75
|
+
else if (driver instanceof android_js_1.AndroidDriver ||
|
|
76
|
+
driver instanceof vega_js_1.VegaDriver ||
|
|
77
|
+
driver instanceof roku_js_1.RokuDriver) {
|
|
75
78
|
const { widthPixels: w, heightPixels: h } = await driver.deviceInfo();
|
|
76
79
|
const durationMs = flags.duration ?? 500;
|
|
77
80
|
if (flags.start && flags.end) {
|
package/dist/commands/tap.js
CHANGED
|
@@ -28,6 +28,7 @@ const ios_js_1 = require("../drivers/ios.js");
|
|
|
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
|
const wait_js_1 = require("../drivers/wait.js");
|
|
32
33
|
const direct_ios_selector_js_1 = require("../drivers/direct-ios-selector.js");
|
|
33
34
|
const snapshot_store_js_1 = require("../snapshot-store.js");
|
|
@@ -88,8 +89,8 @@ async function tap(query, opts = {}, sessionName = 'default', flags = {}) {
|
|
|
88
89
|
else if (driver instanceof web_js_1.WebDriver) {
|
|
89
90
|
el = await (0, wait_js_1.waitForWebElement)(() => driver.viewHierarchy(), sel);
|
|
90
91
|
}
|
|
91
|
-
else if (driver instanceof vega_js_1.VegaDriver) {
|
|
92
|
-
// Vega
|
|
92
|
+
else if (driver instanceof vega_js_1.VegaDriver || driver instanceof roku_js_1.RokuDriver) {
|
|
93
|
+
// Vega and Roku emit uiautomator-style XML, so they reuse the Android resolver.
|
|
93
94
|
el = await (0, wait_js_1.waitForAndroidElement)(() => driver.viewHierarchy(), sel);
|
|
94
95
|
}
|
|
95
96
|
else if (driver instanceof android_js_1.AndroidDriver) {
|
|
@@ -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 uninstallApp(appId, opts = {}, sessionName = 'default') {
|
|
13
14
|
if (!appId) {
|
|
14
15
|
(0, output_js_1.printError)('uninstall-app requires <appId>', opts);
|
|
@@ -24,6 +25,9 @@ async function uninstallApp(appId, opts = {}, sessionName = 'default') {
|
|
|
24
25
|
else if (driver instanceof vega_js_1.VegaDriver) {
|
|
25
26
|
throw new Error('uninstall-app is not supported on vega (Amazon Fire TV)');
|
|
26
27
|
}
|
|
28
|
+
else if (driver instanceof roku_js_1.RokuDriver) {
|
|
29
|
+
throw new Error('uninstall-app is not supported on roku');
|
|
30
|
+
}
|
|
27
31
|
else if (driver instanceof android_js_1.AndroidDriver) {
|
|
28
32
|
await driver.uninstallApp(appId);
|
|
29
33
|
}
|
package/dist/daemon/server.js
CHANGED
|
@@ -177,8 +177,9 @@ let _driverStartError = null;
|
|
|
177
177
|
async function ensureDriverRunning() {
|
|
178
178
|
if (_restartInProgress || !_driverStarted)
|
|
179
179
|
return;
|
|
180
|
-
// Vega
|
|
181
|
-
|
|
180
|
+
// Vega and Roku have no driver process/port to health-check — control is
|
|
181
|
+
// host-side (the vega CLI) or over the network (Roku ECP).
|
|
182
|
+
if (driverPlatform === 'vega' || driverPlatform === 'roku')
|
|
182
183
|
return;
|
|
183
184
|
let alive;
|
|
184
185
|
if (driverPlatform === 'android') {
|
|
@@ -317,6 +318,9 @@ async function main() {
|
|
|
317
318
|
else if (driverPlatform === 'vega') {
|
|
318
319
|
dlog('vega: no driver process to stop (control is host-side via the CLI)');
|
|
319
320
|
}
|
|
321
|
+
else if (driverPlatform === 'roku') {
|
|
322
|
+
dlog('roku: no driver process to stop (control is ECP over the network)');
|
|
323
|
+
}
|
|
320
324
|
else if (driverPlatform === 'web') {
|
|
321
325
|
dlog('Stopping web driver');
|
|
322
326
|
try {
|
|
@@ -449,6 +453,12 @@ async function main() {
|
|
|
449
453
|
_driverStarted = true;
|
|
450
454
|
dlog('vega: no driver process to start; collecting logs only');
|
|
451
455
|
}
|
|
456
|
+
else if (platform === 'roku') {
|
|
457
|
+
// Roku has neither a driver process nor a log stream to collect, so
|
|
458
|
+
// the daemon has nothing to do — commands drive the device directly.
|
|
459
|
+
_driverStarted = true;
|
|
460
|
+
dlog('roku: no driver process and no device logs; daemon is idle');
|
|
461
|
+
}
|
|
452
462
|
else {
|
|
453
463
|
await startDriverForPlatform(platform);
|
|
454
464
|
}
|
|
@@ -487,8 +497,8 @@ async function main() {
|
|
|
487
497
|
});
|
|
488
498
|
}
|
|
489
499
|
/**
|
|
490
|
-
* Bring up the driver process for a
|
|
491
|
-
* `_driverStartError`. Extracted so the vega
|
|
500
|
+
* Bring up the driver process for a platform that has one. Sets `_driverStarted` /
|
|
501
|
+
* `_driverStartError`. Extracted so the vega and roku paths can skip it entirely.
|
|
492
502
|
*/
|
|
493
503
|
async function startDriverForPlatform(platform) {
|
|
494
504
|
let driverAlive;
|
|
@@ -63,6 +63,11 @@ async function detectPlatform(deviceId) {
|
|
|
63
63
|
_platformCache.set(deviceId, 'vega');
|
|
64
64
|
return 'vega';
|
|
65
65
|
}
|
|
66
|
+
// Roku: "roku:<host>" (e.g. "roku:192.168.1.100")
|
|
67
|
+
if (deviceId === 'roku' || deviceId.startsWith('roku:')) {
|
|
68
|
+
_platformCache.set(deviceId, 'roku');
|
|
69
|
+
return 'roku';
|
|
70
|
+
}
|
|
66
71
|
// Check if it looks like an iOS/tvOS simulator UUID (8-4-4-4-12 hex chars)
|
|
67
72
|
const iosUuidRe = /^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$/i;
|
|
68
73
|
if (iosUuidRe.test(deviceId)) {
|
|
@@ -174,6 +179,11 @@ async function getDriverPort(platform, deviceId) {
|
|
|
174
179
|
else if (platform === 'vega') {
|
|
175
180
|
port = state.nextVegaPort++;
|
|
176
181
|
}
|
|
182
|
+
else if (platform === 'roku') {
|
|
183
|
+
// Roku is driven entirely over the network (ECP on the device's own port
|
|
184
|
+
// 8060) — there is no host-side driver process, so no port to reserve.
|
|
185
|
+
return 0;
|
|
186
|
+
}
|
|
177
187
|
else {
|
|
178
188
|
port = state.nextAndroidPort++;
|
|
179
189
|
}
|
|
@@ -21,6 +21,8 @@ const ios_js_1 = require("./ios.js");
|
|
|
21
21
|
const android_js_1 = require("./android.js");
|
|
22
22
|
const web_js_1 = require("./web.js");
|
|
23
23
|
const vega_js_1 = require("./vega.js");
|
|
24
|
+
const roku_js_1 = require("./roku.js");
|
|
25
|
+
const key_mapping_js_1 = require("./roku/key-mapping.js");
|
|
24
26
|
const wait_js_1 = require("./wait.js");
|
|
25
27
|
const direct_ios_selector_js_1 = require("./direct-ios-selector.js");
|
|
26
28
|
const perf_hooks_1 = require("perf_hooks");
|
|
@@ -552,7 +554,9 @@ function getConductorObj(driver, output) {
|
|
|
552
554
|
? 'web'
|
|
553
555
|
: driver instanceof vega_js_1.VegaDriver
|
|
554
556
|
? 'vega'
|
|
555
|
-
:
|
|
557
|
+
: driver instanceof roku_js_1.RokuDriver
|
|
558
|
+
? 'roku'
|
|
559
|
+
: 'android';
|
|
556
560
|
return {
|
|
557
561
|
platform,
|
|
558
562
|
copiedText: output['__copiedText'] ?? '',
|
|
@@ -684,7 +688,7 @@ async function executeCommandBody(key, val, driver, opts) {
|
|
|
684
688
|
await driver.pressKey('delete');
|
|
685
689
|
}
|
|
686
690
|
else {
|
|
687
|
-
// Android, web, and
|
|
691
|
+
// Android, web, vega, and roku all expose eraseAllText.
|
|
688
692
|
await driver.eraseAllText(n);
|
|
689
693
|
}
|
|
690
694
|
break;
|
|
@@ -825,7 +829,7 @@ async function executeCommandBody(key, val, driver, opts) {
|
|
|
825
829
|
await driver.back();
|
|
826
830
|
else if (driver instanceof web_js_1.WebDriver)
|
|
827
831
|
await driver.goBack();
|
|
828
|
-
else if (driver instanceof vega_js_1.VegaDriver)
|
|
832
|
+
else if (driver instanceof vega_js_1.VegaDriver || driver instanceof roku_js_1.RokuDriver)
|
|
829
833
|
await driver.back();
|
|
830
834
|
// iOS has no hardware back button — noop
|
|
831
835
|
break;
|
|
@@ -1113,6 +1117,11 @@ async function executeCommandBody(key, val, driver, opts) {
|
|
|
1113
1117
|
throw new Error(`pressKey: key "${val}" is not supported on vega`);
|
|
1114
1118
|
await driver.pressButton(button);
|
|
1115
1119
|
}
|
|
1120
|
+
else if (driver instanceof roku_js_1.RokuDriver) {
|
|
1121
|
+
if (!(0, key_mapping_js_1.rokuEcpKey)(keyName))
|
|
1122
|
+
throw new Error(`pressKey: key "${val}" is not supported on roku`);
|
|
1123
|
+
await driver.pressKeyNamed(keyName);
|
|
1124
|
+
}
|
|
1116
1125
|
else {
|
|
1117
1126
|
const keycode = ANDROID_KEYCODES[keyName];
|
|
1118
1127
|
if (keycode === undefined)
|
|
@@ -1133,6 +1142,10 @@ async function executeCommandBody(key, val, driver, opts) {
|
|
|
1133
1142
|
else if (driver instanceof vega_js_1.VegaDriver) {
|
|
1134
1143
|
// No reliable keyboard-hide primitive on vega — noop
|
|
1135
1144
|
}
|
|
1145
|
+
else if (driver instanceof roku_js_1.RokuDriver) {
|
|
1146
|
+
// Roku dismisses its on-screen keyboard with Back.
|
|
1147
|
+
await driver.back();
|
|
1148
|
+
}
|
|
1136
1149
|
else {
|
|
1137
1150
|
await driver.pressKeyEvent(111); // KEYCODE_ESCAPE
|
|
1138
1151
|
}
|