@houwert/conductor 0.16.0 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/assert-visible.js +2 -1
- package/dist/commands/capture-ui.js +4 -0
- package/dist/commands/clipboard.js +84 -0
- package/dist/commands/crashes.js +262 -0
- package/dist/commands/debug.js +244 -0
- package/dist/commands/flow-record.js +58 -0
- package/dist/commands/gestures.js +196 -0
- package/dist/commands/inspect.js +41 -1
- package/dist/commands/metro.js +109 -0
- package/dist/commands/network.js +194 -0
- package/dist/commands/profile.js +300 -0
- package/dist/commands/run-sequence.js +124 -0
- package/dist/commands/scroll-until-visible.js +17 -0
- package/dist/commands/set-viewport.js +73 -0
- package/dist/commands/start-device.js +18 -0
- package/dist/commands/tap.js +17 -3
- package/dist/commands/workspace.js +162 -0
- package/dist/drivers/a11y.js +3 -0
- package/dist/drivers/android.js +9 -0
- package/dist/drivers/direct-ios-selector.js +70 -0
- package/dist/drivers/element-resolver.js +189 -0
- package/dist/drivers/flow-recorder.js +135 -0
- package/dist/drivers/flow-runner.js +5 -3
- package/dist/drivers/ios.js +87 -2
- package/dist/drivers/log-sources/metro-discovery.js +12 -6
- package/dist/drivers/metro-cdp.js +305 -0
- package/dist/drivers/metro-scripts.js +399 -0
- package/dist/drivers/wait.js +20 -2
- package/dist/drivers/web.js +3 -0
- package/dist/index.js +289 -1
- package/dist/runner.js +18 -0
- package/dist/snapshot-store.js +96 -0
- package/package.json +1 -1
- package/proto/conductor_android.proto +29 -0
package/dist/index.js
CHANGED
|
@@ -30,6 +30,15 @@ const daemon_js_1 = require("./commands/daemon.js");
|
|
|
30
30
|
const install_js_1 = require("./commands/install.js");
|
|
31
31
|
const device_pool_js_1 = require("./commands/device-pool.js");
|
|
32
32
|
const run_parallel_js_1 = require("./commands/run-parallel.js");
|
|
33
|
+
const run_sequence_js_1 = require("./commands/run-sequence.js");
|
|
34
|
+
const gestures_js_1 = require("./commands/gestures.js");
|
|
35
|
+
const workspace_js_1 = require("./commands/workspace.js");
|
|
36
|
+
const debug_js_1 = require("./commands/debug.js");
|
|
37
|
+
const network_js_1 = require("./commands/network.js");
|
|
38
|
+
const flow_record_js_1 = require("./commands/flow-record.js");
|
|
39
|
+
const profile_js_1 = require("./commands/profile.js");
|
|
40
|
+
const crashes_js_1 = require("./commands/crashes.js");
|
|
41
|
+
const flow_recorder_js_1 = require("./drivers/flow-recorder.js");
|
|
33
42
|
const foreground_app_js_1 = require("./commands/foreground-app.js");
|
|
34
43
|
const list_apps_js_1 = require("./commands/list-apps.js");
|
|
35
44
|
const copy_app_js_1 = require("./commands/copy-app.js");
|
|
@@ -42,11 +51,14 @@ const hide_keyboard_js_1 = require("./commands/hide-keyboard.js");
|
|
|
42
51
|
const scroll_until_visible_js_1 = require("./commands/scroll-until-visible.js");
|
|
43
52
|
const set_location_js_1 = require("./commands/set-location.js");
|
|
44
53
|
const set_orientation_js_1 = require("./commands/set-orientation.js");
|
|
54
|
+
const set_viewport_js_1 = require("./commands/set-viewport.js");
|
|
45
55
|
const start_device_js_1 = require("./commands/start-device.js");
|
|
46
56
|
const stop_device_js_1 = require("./commands/stop-device.js");
|
|
47
57
|
const delete_device_js_1 = require("./commands/delete-device.js");
|
|
48
58
|
const logs_js_1 = require("./commands/logs.js");
|
|
49
59
|
const memory_js_1 = require("./commands/memory.js");
|
|
60
|
+
const metro_js_1 = require("./commands/metro.js");
|
|
61
|
+
const clipboard_js_1 = require("./commands/clipboard.js");
|
|
50
62
|
const device_picker_js_1 = require("./device-picker.js");
|
|
51
63
|
const update_check_js_1 = require("./update-check.js");
|
|
52
64
|
const pkg_root_js_1 = require("./pkg-root.js");
|
|
@@ -80,6 +92,7 @@ const COMMAND_HELP = {
|
|
|
80
92
|
'open-link': open_link_js_1.HELP,
|
|
81
93
|
'set-location': set_location_js_1.HELP,
|
|
82
94
|
'set-orientation': set_orientation_js_1.HELP,
|
|
95
|
+
'set-viewport': set_viewport_js_1.HELP,
|
|
83
96
|
'take-screenshot': screenshot_js_1.HELP,
|
|
84
97
|
'capture-ui': capture_ui_js_1.HELP,
|
|
85
98
|
inspect: inspect_js_1.HELP,
|
|
@@ -93,8 +106,19 @@ const COMMAND_HELP = {
|
|
|
93
106
|
'daemon-status': daemon_js_1.HELP_DAEMON_STATUS,
|
|
94
107
|
'device-pool': device_pool_js_1.HELP,
|
|
95
108
|
'run-parallel': run_parallel_js_1.HELP,
|
|
109
|
+
'run-sequence': run_sequence_js_1.HELP,
|
|
110
|
+
gestures: gestures_js_1.HELP,
|
|
111
|
+
workspace: workspace_js_1.HELP,
|
|
112
|
+
debug: debug_js_1.HELP,
|
|
113
|
+
network: network_js_1.HELP,
|
|
114
|
+
'flow record': flow_record_js_1.HELP,
|
|
115
|
+
profile: profile_js_1.HELP,
|
|
116
|
+
crashes: crashes_js_1.HELP,
|
|
96
117
|
logs: logs_js_1.HELP,
|
|
97
118
|
memory: memory_js_1.HELP,
|
|
119
|
+
metro: metro_js_1.HELP,
|
|
120
|
+
clipboard: clipboard_js_1.HELP,
|
|
121
|
+
paste: ' paste Trigger OS-level paste (or type clipboard on iOS)',
|
|
98
122
|
};
|
|
99
123
|
const OPTIONS_HELP = `Options:
|
|
100
124
|
--device <id> Target device ID (also keys the session and daemon)
|
|
@@ -133,6 +157,7 @@ async function main() {
|
|
|
133
157
|
'optional',
|
|
134
158
|
'benchmark',
|
|
135
159
|
'dump',
|
|
160
|
+
'tappable',
|
|
136
161
|
'objects',
|
|
137
162
|
'heap',
|
|
138
163
|
'leaks',
|
|
@@ -173,6 +198,29 @@ async function main() {
|
|
|
173
198
|
'vs',
|
|
174
199
|
'top',
|
|
175
200
|
'filter',
|
|
201
|
+
'port',
|
|
202
|
+
'target',
|
|
203
|
+
'at',
|
|
204
|
+
'file',
|
|
205
|
+
'scale',
|
|
206
|
+
'center',
|
|
207
|
+
'degrees',
|
|
208
|
+
'angle',
|
|
209
|
+
'limit',
|
|
210
|
+
'method',
|
|
211
|
+
'body',
|
|
212
|
+
'header',
|
|
213
|
+
'url',
|
|
214
|
+
'out',
|
|
215
|
+
'track',
|
|
216
|
+
'interval',
|
|
217
|
+
'app',
|
|
218
|
+
'since',
|
|
219
|
+
'preset',
|
|
220
|
+
'width',
|
|
221
|
+
'height',
|
|
222
|
+
'user-agent',
|
|
223
|
+
'color-scheme',
|
|
176
224
|
],
|
|
177
225
|
alias: { h: 'help', v: 'verbose', V: 'version' },
|
|
178
226
|
});
|
|
@@ -207,6 +255,8 @@ async function main() {
|
|
|
207
255
|
'copy-app',
|
|
208
256
|
'device-pool',
|
|
209
257
|
'run-parallel',
|
|
258
|
+
'metro',
|
|
259
|
+
'workspace',
|
|
210
260
|
// `logs --list` and `logs --source metro` only query Metro on localhost — no device needed
|
|
211
261
|
// `logs` always needs a device session — Metro discovery is device-scoped.
|
|
212
262
|
// `daemon-stop --all` stops every daemon — no device needed
|
|
@@ -459,6 +509,26 @@ async function main() {
|
|
|
459
509
|
exitCode = await (0, set_orientation_js_1.setOrientation)(orientation, opts, sessionName);
|
|
460
510
|
break;
|
|
461
511
|
}
|
|
512
|
+
case 'set-viewport': {
|
|
513
|
+
exitCode = await (0, set_viewport_js_1.setViewport)({
|
|
514
|
+
preset: argv['preset'],
|
|
515
|
+
width: argv['width'] !== undefined
|
|
516
|
+
? Number(argv['width'])
|
|
517
|
+
: rest[0] !== undefined
|
|
518
|
+
? Number(rest[0])
|
|
519
|
+
: undefined,
|
|
520
|
+
height: argv['height'] !== undefined
|
|
521
|
+
? Number(argv['height'])
|
|
522
|
+
: rest[1] !== undefined
|
|
523
|
+
? Number(rest[1])
|
|
524
|
+
: undefined,
|
|
525
|
+
scale: argv['scale'] !== undefined ? Number(argv['scale']) : undefined,
|
|
526
|
+
mobile: argv['mobile'],
|
|
527
|
+
userAgent: argv['user-agent'],
|
|
528
|
+
colorScheme: argv['color-scheme'],
|
|
529
|
+
}, opts, sessionName);
|
|
530
|
+
break;
|
|
531
|
+
}
|
|
462
532
|
case 'take-screenshot': {
|
|
463
533
|
const outPath = argv['output'];
|
|
464
534
|
const fullPage = Boolean(argv['full-page']);
|
|
@@ -471,7 +541,11 @@ async function main() {
|
|
|
471
541
|
break;
|
|
472
542
|
}
|
|
473
543
|
case 'inspect':
|
|
474
|
-
exitCode = await (0, inspect_js_1.inspect)(opts, sessionName, {
|
|
544
|
+
exitCode = await (0, inspect_js_1.inspect)(opts, sessionName, {
|
|
545
|
+
dump: argv['dump'],
|
|
546
|
+
at: argv['at'],
|
|
547
|
+
tappableOnly: argv['tappable'],
|
|
548
|
+
});
|
|
475
549
|
break;
|
|
476
550
|
case 'focused':
|
|
477
551
|
exitCode = await (0, focused_js_1.focused)(opts, sessionName, {
|
|
@@ -542,17 +616,231 @@ async function main() {
|
|
|
542
616
|
exitCode = await (0, device_pool_js_1.devicePool)(action, releaseId, opts);
|
|
543
617
|
break;
|
|
544
618
|
}
|
|
619
|
+
case 'run-sequence': {
|
|
620
|
+
const file = argv['file'] ?? rest[0];
|
|
621
|
+
exitCode = await (0, run_sequence_js_1.runSequence)(file, opts, sessionName);
|
|
622
|
+
break;
|
|
623
|
+
}
|
|
545
624
|
case 'run-parallel': {
|
|
546
625
|
const flowsDir = argv['flows-dir'] ?? rest[0] ?? '';
|
|
547
626
|
exitCode = await (0, run_parallel_js_1.runParallel)(flowsDir, opts);
|
|
548
627
|
break;
|
|
549
628
|
}
|
|
629
|
+
case 'crashes': {
|
|
630
|
+
const sub = (rest[0] ?? 'list').toLowerCase();
|
|
631
|
+
if (sub === 'list') {
|
|
632
|
+
exitCode = await (0, crashes_js_1.crashesList)(opts, sessionName, {
|
|
633
|
+
app: argv['app'],
|
|
634
|
+
since: argv['since'],
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
else if (sub === 'show') {
|
|
638
|
+
exitCode = await (0, crashes_js_1.crashesShow)(rest[1] ?? '', opts);
|
|
639
|
+
}
|
|
640
|
+
else if (sub === 'tail') {
|
|
641
|
+
exitCode = await (0, crashes_js_1.crashesTail)(opts, sessionName);
|
|
642
|
+
}
|
|
643
|
+
else {
|
|
644
|
+
console.error('Usage: conductor crashes <list|show|tail>');
|
|
645
|
+
exitCode = 1;
|
|
646
|
+
}
|
|
647
|
+
break;
|
|
648
|
+
}
|
|
649
|
+
case 'profile': {
|
|
650
|
+
const sub = (rest[0] ?? '').toLowerCase();
|
|
651
|
+
const port = argv['port'] !== undefined ? Number(argv['port']) : undefined;
|
|
652
|
+
const targetIndex = argv['target'] !== undefined ? Number(argv['target']) : undefined;
|
|
653
|
+
if (sub === 'cpu') {
|
|
654
|
+
const durationSec = argv['duration'] !== undefined ? Number(argv['duration']) : 10;
|
|
655
|
+
exitCode = await (0, profile_js_1.profileCpu)(opts, sessionName, {
|
|
656
|
+
durationSec,
|
|
657
|
+
out: argv['out'],
|
|
658
|
+
appId: rest[1],
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
else if (sub === 'memory') {
|
|
662
|
+
const trackSec = argv['track'] !== undefined ? Number(argv['track']) : 10;
|
|
663
|
+
const intervalMs = argv['interval'] !== undefined ? Number(argv['interval']) : 1000;
|
|
664
|
+
exitCode = await (0, profile_js_1.profileMemory)(opts, sessionName, {
|
|
665
|
+
trackSec,
|
|
666
|
+
intervalMs,
|
|
667
|
+
appId: rest[1],
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
else if (sub === 'react') {
|
|
671
|
+
const sub2 = (rest[1] ?? '').toLowerCase();
|
|
672
|
+
const top = argv['top'] !== undefined ? Number(argv['top']) : 20;
|
|
673
|
+
if (sub2 === 'start') {
|
|
674
|
+
exitCode = await (0, profile_js_1.profileReactStart)(opts, sessionName, { port, targetIndex });
|
|
675
|
+
}
|
|
676
|
+
else if (sub2 === 'stop') {
|
|
677
|
+
exitCode = await (0, profile_js_1.profileReactStop)(opts, sessionName, { port, targetIndex }, top);
|
|
678
|
+
}
|
|
679
|
+
else {
|
|
680
|
+
console.error('Usage: conductor profile react <start|stop>');
|
|
681
|
+
exitCode = 1;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
else {
|
|
685
|
+
console.error('Usage: conductor profile <cpu|memory|react> [args]');
|
|
686
|
+
exitCode = 1;
|
|
687
|
+
}
|
|
688
|
+
break;
|
|
689
|
+
}
|
|
690
|
+
case 'flow': {
|
|
691
|
+
const sub1 = (rest[0] ?? '').toLowerCase();
|
|
692
|
+
if (sub1 !== 'record') {
|
|
693
|
+
console.error('Usage: conductor flow record <start|finish|echo|status>');
|
|
694
|
+
exitCode = 1;
|
|
695
|
+
break;
|
|
696
|
+
}
|
|
697
|
+
const sub2 = (rest[1] ?? '').toLowerCase();
|
|
698
|
+
exitCode = await (0, flow_record_js_1.flowRecord)(sub2, rest.slice(2).map(String), opts, sessionName, argv);
|
|
699
|
+
break;
|
|
700
|
+
}
|
|
701
|
+
case 'network': {
|
|
702
|
+
const sub = (rest[0] ?? '').toLowerCase();
|
|
703
|
+
const port = argv['port'] !== undefined ? Number(argv['port']) : undefined;
|
|
704
|
+
const targetIndex = argv['target'] !== undefined ? Number(argv['target']) : undefined;
|
|
705
|
+
if (sub === 'logs') {
|
|
706
|
+
const limit = argv['limit'] !== undefined ? Number(argv['limit']) : undefined;
|
|
707
|
+
exitCode = await (0, network_js_1.networkLogs)(opts, sessionName, { port, targetIndex, limit });
|
|
708
|
+
}
|
|
709
|
+
else if (sub === 'request') {
|
|
710
|
+
const url = rest[1] ?? argv['url'] ?? '';
|
|
711
|
+
const rawHeaders = argv['header'];
|
|
712
|
+
const headers = Array.isArray(rawHeaders)
|
|
713
|
+
? rawHeaders
|
|
714
|
+
: rawHeaders
|
|
715
|
+
? [rawHeaders]
|
|
716
|
+
: [];
|
|
717
|
+
exitCode = await (0, network_js_1.networkRequest)(url, opts, sessionName, {
|
|
718
|
+
port,
|
|
719
|
+
targetIndex,
|
|
720
|
+
method: argv['method'],
|
|
721
|
+
body: argv['body'],
|
|
722
|
+
headers,
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
else {
|
|
726
|
+
console.error('Usage: conductor network <logs|request> [args]');
|
|
727
|
+
exitCode = 1;
|
|
728
|
+
}
|
|
729
|
+
break;
|
|
730
|
+
}
|
|
731
|
+
case 'debug': {
|
|
732
|
+
const sub = (rest[0] ?? '').toLowerCase();
|
|
733
|
+
const debugOpts = {
|
|
734
|
+
port: argv['port'] !== undefined ? Number(argv['port']) : undefined,
|
|
735
|
+
targetIndex: argv['target'] !== undefined ? Number(argv['target']) : undefined,
|
|
736
|
+
};
|
|
737
|
+
if (sub === 'status') {
|
|
738
|
+
exitCode = await (0, debug_js_1.debugStatus)(opts, sessionName, debugOpts);
|
|
739
|
+
}
|
|
740
|
+
else if (sub === 'evaluate' || sub === 'eval') {
|
|
741
|
+
const expr = rest.slice(1).join(' ');
|
|
742
|
+
exitCode = await (0, debug_js_1.debugEvaluate)(expr, opts, sessionName, debugOpts);
|
|
743
|
+
}
|
|
744
|
+
else if (sub === 'component-tree') {
|
|
745
|
+
exitCode = await (0, debug_js_1.debugComponentTree)(opts, sessionName, debugOpts);
|
|
746
|
+
}
|
|
747
|
+
else if (sub === 'inspect-element') {
|
|
748
|
+
const at = rest[1] ?? argv['at'] ?? '';
|
|
749
|
+
exitCode = await (0, debug_js_1.debugInspectElement)(at, opts, sessionName, debugOpts);
|
|
750
|
+
}
|
|
751
|
+
else if (sub === 'log-registry') {
|
|
752
|
+
exitCode = await (0, debug_js_1.debugLogRegistry)(opts, sessionName);
|
|
753
|
+
}
|
|
754
|
+
else if (sub === 'reload') {
|
|
755
|
+
exitCode = await (0, debug_js_1.debugReload)(opts, sessionName, debugOpts);
|
|
756
|
+
}
|
|
757
|
+
else {
|
|
758
|
+
console.error('Usage: conductor debug <status|evaluate|component-tree|inspect-element|log-registry|reload>');
|
|
759
|
+
exitCode = 1;
|
|
760
|
+
}
|
|
761
|
+
break;
|
|
762
|
+
}
|
|
763
|
+
case 'workspace': {
|
|
764
|
+
const sub = (rest[0] ?? 'info').toLowerCase();
|
|
765
|
+
exitCode = await (0, workspace_js_1.workspaceCmd)(sub, opts);
|
|
766
|
+
break;
|
|
767
|
+
}
|
|
768
|
+
case 'pinch':
|
|
769
|
+
exitCode = await (0, gestures_js_1.pinch)(opts, sessionName, {
|
|
770
|
+
scale: argv['scale'] !== undefined ? Number(argv['scale']) : undefined,
|
|
771
|
+
center: argv['center'],
|
|
772
|
+
duration: argv['duration'] !== undefined ? Number(argv['duration']) : undefined,
|
|
773
|
+
angle: argv['angle'] !== undefined ? Number(argv['angle']) : undefined,
|
|
774
|
+
});
|
|
775
|
+
break;
|
|
776
|
+
case 'rotate-gesture':
|
|
777
|
+
exitCode = await (0, gestures_js_1.rotateGesture)(opts, sessionName, {
|
|
778
|
+
degrees: argv['degrees'] !== undefined ? Number(argv['degrees']) : undefined,
|
|
779
|
+
center: argv['center'],
|
|
780
|
+
duration: argv['duration'] !== undefined ? Number(argv['duration']) : undefined,
|
|
781
|
+
});
|
|
782
|
+
break;
|
|
783
|
+
case 'gesture': {
|
|
784
|
+
const file = argv['file'];
|
|
785
|
+
const rawJson = !file ? rest.join(' ') : undefined;
|
|
786
|
+
exitCode = await (0, gestures_js_1.gesture)(rawJson, file, opts, sessionName);
|
|
787
|
+
break;
|
|
788
|
+
}
|
|
789
|
+
case 'clipboard': {
|
|
790
|
+
const sub = (rest[0] ?? '').toLowerCase();
|
|
791
|
+
if (sub === 'read') {
|
|
792
|
+
exitCode = await (0, clipboard_js_1.clipboardRead)(opts, sessionName);
|
|
793
|
+
}
|
|
794
|
+
else if (sub === 'write') {
|
|
795
|
+
const text = rest.slice(1).join(' ');
|
|
796
|
+
exitCode = await (0, clipboard_js_1.clipboardWrite)(text, opts, sessionName);
|
|
797
|
+
}
|
|
798
|
+
else {
|
|
799
|
+
console.error('Usage: conductor clipboard <read|write> [text]');
|
|
800
|
+
exitCode = 1;
|
|
801
|
+
}
|
|
802
|
+
break;
|
|
803
|
+
}
|
|
804
|
+
case 'paste':
|
|
805
|
+
exitCode = await (0, clipboard_js_1.paste)(opts, sessionName);
|
|
806
|
+
break;
|
|
807
|
+
case 'metro': {
|
|
808
|
+
const sub = (rest[0] ?? '').toLowerCase();
|
|
809
|
+
const port = argv['port'] !== undefined ? Number(argv['port']) : undefined;
|
|
810
|
+
const targetIndex = argv['target'] !== undefined ? Number(argv['target']) : undefined;
|
|
811
|
+
const metroSession = argv['device'] ?? 'default';
|
|
812
|
+
if (sub === 'stop') {
|
|
813
|
+
exitCode = await (0, metro_js_1.metroStop)(opts, { port });
|
|
814
|
+
}
|
|
815
|
+
else if (sub === 'reload') {
|
|
816
|
+
exitCode = await (0, metro_js_1.metroReload)(opts, metroSession, { port, targetIndex });
|
|
817
|
+
}
|
|
818
|
+
else {
|
|
819
|
+
console.error('Usage: conductor metro <stop|reload> [--port N] [--target N]');
|
|
820
|
+
exitCode = 1;
|
|
821
|
+
}
|
|
822
|
+
break;
|
|
823
|
+
}
|
|
550
824
|
default:
|
|
551
825
|
// Should be unreachable — unknown commands are caught before device resolution.
|
|
552
826
|
console.error(`Unknown command: ${command}`);
|
|
553
827
|
console.error('Run `conductor --help` for usage.');
|
|
554
828
|
exitCode = 1;
|
|
555
829
|
}
|
|
830
|
+
// Flow recording — append a YAML step for action commands that succeeded.
|
|
831
|
+
if (exitCode === 0 && !NO_DEVICE_COMMANDS.has(command) && command !== 'flow') {
|
|
832
|
+
try {
|
|
833
|
+
const active = await (0, flow_recorder_js_1.getActiveRecording)(sessionName);
|
|
834
|
+
if (active) {
|
|
835
|
+
const step = (0, flow_recorder_js_1.commandToYamlStep)(command, rest.map(String), argv);
|
|
836
|
+
if (step)
|
|
837
|
+
(0, flow_recorder_js_1.appendStep)(active, step);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
catch {
|
|
841
|
+
// Recording is best-effort — never fail the command for a bookkeeping issue.
|
|
842
|
+
}
|
|
843
|
+
}
|
|
556
844
|
process.exit(exitCode);
|
|
557
845
|
}
|
|
558
846
|
main().catch((err) => {
|
package/dist/runner.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.detectFirstDevice = detectFirstDevice;
|
|
4
4
|
exports.getDriver = getDriver;
|
|
5
|
+
exports.prewarmDriver = prewarmDriver;
|
|
5
6
|
exports.runDirect = runDirect;
|
|
6
7
|
exports.spawnCommand = spawnCommand;
|
|
7
8
|
exports.runInlineFlow = runInlineFlow;
|
|
@@ -194,6 +195,23 @@ async function getDriver(sessionName = 'default') {
|
|
|
194
195
|
_driverCache.set(sessionName, driver);
|
|
195
196
|
return driver;
|
|
196
197
|
}
|
|
198
|
+
/**
|
|
199
|
+
* Eagerly start the device daemon (and its XCTest driver) for `deviceId` so
|
|
200
|
+
* the runner is already warm by the time the first interaction command runs.
|
|
201
|
+
*
|
|
202
|
+
* `start-device` is the natural prewarm point: bringing the driver up there
|
|
203
|
+
* moves the one-off startup cost off the first `tap-on` / `assert-visible`.
|
|
204
|
+
* Best-effort — failures are swallowed since a real command will surface any
|
|
205
|
+
* genuine startup error with a proper message.
|
|
206
|
+
*/
|
|
207
|
+
async function prewarmDriver(deviceId) {
|
|
208
|
+
try {
|
|
209
|
+
await (0, client_js_1.startDaemon)(deviceId);
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
/* best-effort: a later command will report a real failure */
|
|
213
|
+
}
|
|
214
|
+
}
|
|
197
215
|
/**
|
|
198
216
|
* Execute a function with the driver for the given session.
|
|
199
217
|
* Returns a RunResult for consistent error handling across commands.
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.SNAPSHOT_STALE_MS = void 0;
|
|
7
|
+
exports.snapshotFilePath = snapshotFilePath;
|
|
8
|
+
exports.isRefQuery = isRefQuery;
|
|
9
|
+
exports.buildStoredSnapshot = buildStoredSnapshot;
|
|
10
|
+
exports.saveSnapshot = saveSnapshot;
|
|
11
|
+
exports.loadSnapshot = loadSnapshot;
|
|
12
|
+
exports.resolveRef = resolveRef;
|
|
13
|
+
/**
|
|
14
|
+
* Snapshot-scoped ephemeral element refs.
|
|
15
|
+
*
|
|
16
|
+
* `capture-ui` assigns each accessible element a short ref (`@e1`, `@e2`, …) and
|
|
17
|
+
* persists its resolved screen coordinates here, keyed by session. `tap-on @e3`
|
|
18
|
+
* then taps the cached point directly — no fuzzy text/id matching.
|
|
19
|
+
*
|
|
20
|
+
* Refs are deliberately ephemeral: a stale snapshot warns (it does not hard-fail),
|
|
21
|
+
* and the agent is expected to re-run `capture-ui` and act on fresh refs.
|
|
22
|
+
*/
|
|
23
|
+
const promises_1 = __importDefault(require("fs/promises"));
|
|
24
|
+
const os_1 = __importDefault(require("os"));
|
|
25
|
+
const path_1 = __importDefault(require("path"));
|
|
26
|
+
const SNAPSHOTS_DIR = path_1.default.join(os_1.default.homedir(), '.conductor', 'snapshots');
|
|
27
|
+
/** A snapshot older than this is considered stale. */
|
|
28
|
+
exports.SNAPSHOT_STALE_MS = 60000;
|
|
29
|
+
function snapshotFilePath(sessionName = 'default') {
|
|
30
|
+
return path_1.default.join(SNAPSHOTS_DIR, `${sessionName}.json`);
|
|
31
|
+
}
|
|
32
|
+
/** True when `s` looks like an ephemeral element ref (`@e3`). */
|
|
33
|
+
function isRefQuery(s) {
|
|
34
|
+
return /^@e\d+$/i.test(s.trim());
|
|
35
|
+
}
|
|
36
|
+
/** Build a `StoredSnapshot` from a freshly built a11y snapshot. */
|
|
37
|
+
function buildStoredSnapshot(entries, device) {
|
|
38
|
+
const refs = {};
|
|
39
|
+
for (const e of entries) {
|
|
40
|
+
refs[e.ref] = {
|
|
41
|
+
ref: e.ref,
|
|
42
|
+
centerX: e.frame.x + e.frame.w / 2,
|
|
43
|
+
centerY: e.frame.y + e.frame.h / 2,
|
|
44
|
+
frame: e.frame,
|
|
45
|
+
label: e.label,
|
|
46
|
+
nodeId: e.nodeId,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
version: 1,
|
|
51
|
+
capturedAt: new Date().toISOString(),
|
|
52
|
+
deviceId: device.deviceId,
|
|
53
|
+
platform: device.platform,
|
|
54
|
+
refs,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
async function saveSnapshot(sessionName, snapshot) {
|
|
58
|
+
await promises_1.default.mkdir(SNAPSHOTS_DIR, { recursive: true });
|
|
59
|
+
await promises_1.default.writeFile(snapshotFilePath(sessionName), JSON.stringify(snapshot, null, 2));
|
|
60
|
+
}
|
|
61
|
+
async function loadSnapshot(sessionName) {
|
|
62
|
+
try {
|
|
63
|
+
const data = await promises_1.default.readFile(snapshotFilePath(sessionName), 'utf-8');
|
|
64
|
+
return JSON.parse(data);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Resolve an `@eN` ref against the session's last `capture-ui` snapshot.
|
|
72
|
+
* Throws when there is no snapshot or the ref is unknown. `staleReason` is
|
|
73
|
+
* advisory — callers warn but still act, since refs are explicitly ephemeral.
|
|
74
|
+
*/
|
|
75
|
+
function resolveRef(snapshot, ref, ctx) {
|
|
76
|
+
if (!snapshot) {
|
|
77
|
+
throw new Error(`no snapshot for this session — run \`conductor capture-ui\` before using ${ref}`);
|
|
78
|
+
}
|
|
79
|
+
const norm = ref.trim().toLowerCase();
|
|
80
|
+
const key = Object.keys(snapshot.refs).find((k) => k.toLowerCase() === norm);
|
|
81
|
+
if (!key) {
|
|
82
|
+
const avail = Object.keys(snapshot.refs);
|
|
83
|
+
const shown = avail.slice(0, 8).join(', ');
|
|
84
|
+
throw new Error(`${ref} is not in the last snapshot ` +
|
|
85
|
+
`(${avail.length} ref${avail.length === 1 ? '' : 's'}: ${shown}${avail.length > 8 ? ', …' : ''})`);
|
|
86
|
+
}
|
|
87
|
+
let staleReason = null;
|
|
88
|
+
const ageMs = Date.now() - new Date(snapshot.capturedAt).getTime();
|
|
89
|
+
if (ageMs > exports.SNAPSHOT_STALE_MS) {
|
|
90
|
+
staleReason = `snapshot is ${Math.round(ageMs / 1000)}s old`;
|
|
91
|
+
}
|
|
92
|
+
else if (ctx?.deviceId && snapshot.deviceId && ctx.deviceId !== snapshot.deviceId) {
|
|
93
|
+
staleReason = `snapshot was captured on a different device (${snapshot.deviceId})`;
|
|
94
|
+
}
|
|
95
|
+
return { entry: snapshot.refs[key], staleReason };
|
|
96
|
+
}
|
package/package.json
CHANGED
|
@@ -12,6 +12,8 @@ service ConductorDriver {
|
|
|
12
12
|
|
|
13
13
|
rpc tap(TapRequest) returns (TapResponse) {}
|
|
14
14
|
|
|
15
|
+
rpc gesturePath(GesturePathRequest) returns (GesturePathResponse) {}
|
|
16
|
+
|
|
15
17
|
rpc inputText(InputTextRequest) returns (InputTextResponse) {}
|
|
16
18
|
|
|
17
19
|
rpc eraseAllText(EraseAllTextRequest) returns (EraseAllTextResponse) {}
|
|
@@ -76,6 +78,33 @@ message TapRequest {
|
|
|
76
78
|
|
|
77
79
|
message TapResponse {}
|
|
78
80
|
|
|
81
|
+
// One step in a finger's path.
|
|
82
|
+
// x, y pixel coordinates on the device's natural orientation.
|
|
83
|
+
// dt_ms delay in milliseconds since the previous step. For the first
|
|
84
|
+
// step in a path, this is the initial offset before the touch
|
|
85
|
+
// goes down.
|
|
86
|
+
message GestureStep {
|
|
87
|
+
double x = 1;
|
|
88
|
+
double y = 2;
|
|
89
|
+
uint32 dt_ms = 3;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// A single finger's traced path. Must have at least one step. The first step
|
|
93
|
+
// is the down position; subsequent steps are moves; the finger lifts after
|
|
94
|
+
// the last step.
|
|
95
|
+
message GestureFingerPath {
|
|
96
|
+
repeated GestureStep steps = 1;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Multi-finger gesture request. paths.size() == 1 is a single-finger gesture;
|
|
100
|
+
// paths.size() >= 2 dispatches a true multi-touch gesture via
|
|
101
|
+
// UiAutomation.dispatchGesture (API 24+).
|
|
102
|
+
message GesturePathRequest {
|
|
103
|
+
repeated GestureFingerPath paths = 1;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
message GesturePathResponse {}
|
|
107
|
+
|
|
79
108
|
message InputTextRequest {
|
|
80
109
|
string text = 1;
|
|
81
110
|
}
|