@houwert/conductor 0.16.0 → 0.17.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/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/start-device.js +18 -0
- package/dist/commands/tap.js +2 -1
- package/dist/commands/workspace.js +162 -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/metro-cdp.js +291 -0
- package/dist/drivers/metro-scripts.js +399 -0
- package/dist/drivers/wait.js +20 -2
- package/dist/index.js +262 -1
- package/dist/runner.js +18 -0
- package/package.json +1 -1
- package/proto/conductor_android.proto +29 -0
|
@@ -0,0 +1,300 @@
|
|
|
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.HELP = void 0;
|
|
7
|
+
exports.profileCpu = profileCpu;
|
|
8
|
+
exports.profileMemory = profileMemory;
|
|
9
|
+
exports.profileReactStart = profileReactStart;
|
|
10
|
+
exports.profileReactStop = profileReactStop;
|
|
11
|
+
exports.HELP = ` profile cpu --duration <s> [--out <path>]
|
|
12
|
+
Record a CPU trace (iOS: xctrace, Android: simpleperf)
|
|
13
|
+
profile memory --track <s> [--interval <ms>] [<appId>]
|
|
14
|
+
Sample memory for N seconds, report deltas
|
|
15
|
+
profile react start Install a React commit-profiler hook in the JS runtime
|
|
16
|
+
profile react stop [--top N] Stop and summarise captured React commits`;
|
|
17
|
+
const child_process_1 = require("child_process");
|
|
18
|
+
const os_1 = __importDefault(require("os"));
|
|
19
|
+
const path_1 = __importDefault(require("path"));
|
|
20
|
+
const output_js_1 = require("../output.js");
|
|
21
|
+
const bootstrap_js_1 = require("../drivers/bootstrap.js");
|
|
22
|
+
const sdk_js_1 = require("../android/sdk.js");
|
|
23
|
+
const memory_js_1 = require("./memory.js");
|
|
24
|
+
const metro_cdp_js_1 = require("../drivers/metro-cdp.js");
|
|
25
|
+
function defaultTracePath(prefix, ext) {
|
|
26
|
+
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
|
27
|
+
return path_1.default.join(os_1.default.tmpdir(), `${prefix}-${ts}.${ext}`);
|
|
28
|
+
}
|
|
29
|
+
async function recordIosCpu(deviceId, appId, durationSec, out) {
|
|
30
|
+
const args = [
|
|
31
|
+
'xctrace',
|
|
32
|
+
'record',
|
|
33
|
+
'--template',
|
|
34
|
+
'Time Profiler',
|
|
35
|
+
'--device',
|
|
36
|
+
deviceId,
|
|
37
|
+
'--time-limit',
|
|
38
|
+
`${durationSec}s`,
|
|
39
|
+
'--output',
|
|
40
|
+
out,
|
|
41
|
+
];
|
|
42
|
+
if (appId)
|
|
43
|
+
args.push('--attach', appId);
|
|
44
|
+
await new Promise((resolve, reject) => {
|
|
45
|
+
const proc = (0, child_process_1.spawn)('xcrun', args, { stdio: 'inherit' });
|
|
46
|
+
proc.on('close', (code) => code === 0 ? resolve() : reject(new Error(`xctrace exited with code ${code}`)));
|
|
47
|
+
proc.on('error', reject);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
async function recordAndroidCpu(deviceId, appId, durationSec, out) {
|
|
51
|
+
const adb = (0, sdk_js_1.resolveAndroidTool)('adb');
|
|
52
|
+
const env = (0, sdk_js_1.androidSpawnEnv)();
|
|
53
|
+
const remote = `/data/local/tmp/conductor-perf-${Date.now()}.data`;
|
|
54
|
+
const recordArgs = [
|
|
55
|
+
'-s',
|
|
56
|
+
deviceId,
|
|
57
|
+
'shell',
|
|
58
|
+
'simpleperf',
|
|
59
|
+
'record',
|
|
60
|
+
'-o',
|
|
61
|
+
remote,
|
|
62
|
+
'--duration',
|
|
63
|
+
String(durationSec),
|
|
64
|
+
];
|
|
65
|
+
if (appId) {
|
|
66
|
+
recordArgs.push('--app', appId);
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
recordArgs.push('-a');
|
|
70
|
+
}
|
|
71
|
+
await new Promise((resolve, reject) => {
|
|
72
|
+
const proc = (0, child_process_1.spawn)(adb, recordArgs, { stdio: 'inherit', env });
|
|
73
|
+
proc.on('close', (code) => code === 0 ? resolve() : reject(new Error(`simpleperf record exited with ${code}`)));
|
|
74
|
+
proc.on('error', reject);
|
|
75
|
+
});
|
|
76
|
+
await new Promise((resolve, reject) => {
|
|
77
|
+
const proc = (0, child_process_1.spawn)(adb, ['-s', deviceId, 'pull', remote, out], { stdio: 'inherit', env });
|
|
78
|
+
proc.on('close', (code) => code === 0 ? resolve() : reject(new Error(`adb pull exited with ${code}`)));
|
|
79
|
+
proc.on('error', reject);
|
|
80
|
+
});
|
|
81
|
+
await new Promise((resolve) => {
|
|
82
|
+
const proc = (0, child_process_1.spawn)(adb, ['-s', deviceId, 'shell', 'rm', remote], { stdio: 'ignore', env });
|
|
83
|
+
proc.on('close', () => resolve());
|
|
84
|
+
proc.on('error', () => resolve());
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
async function profileCpu(opts, sessionName, profileOpts) {
|
|
88
|
+
if (sessionName === 'default') {
|
|
89
|
+
(0, output_js_1.printError)('profile cpu requires a --device', opts);
|
|
90
|
+
return 1;
|
|
91
|
+
}
|
|
92
|
+
const platform = await (0, bootstrap_js_1.detectPlatform)(sessionName).catch(() => null);
|
|
93
|
+
const isIos = platform === 'ios' || platform === 'tvos';
|
|
94
|
+
const out = profileOpts.out ?? defaultTracePath('cpu', isIos ? 'trace' : 'perf.data');
|
|
95
|
+
try {
|
|
96
|
+
if (isIos) {
|
|
97
|
+
await recordIosCpu(sessionName, profileOpts.appId, profileOpts.durationSec, out);
|
|
98
|
+
}
|
|
99
|
+
else if (platform === 'android') {
|
|
100
|
+
await recordAndroidCpu(sessionName, profileOpts.appId, profileOpts.durationSec, out);
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
(0, output_js_1.printError)(`profile cpu is not supported on platform ${platform ?? '(unknown)'}`, opts);
|
|
104
|
+
return 1;
|
|
105
|
+
}
|
|
106
|
+
if (opts.json)
|
|
107
|
+
(0, output_js_1.printData)({ out, durationSec: profileOpts.durationSec, platform }, opts);
|
|
108
|
+
else
|
|
109
|
+
(0, output_js_1.printSuccess)(`profile cpu — recorded ${profileOpts.durationSec}s → ${out}`, opts);
|
|
110
|
+
return 0;
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
(0, output_js_1.printError)(`profile cpu — ${err instanceof Error ? err.message : String(err)}`, opts);
|
|
114
|
+
return 1;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
async function profileMemory(opts, sessionName, profileOpts) {
|
|
118
|
+
const samples = [];
|
|
119
|
+
const start = Date.now();
|
|
120
|
+
const end = start + profileOpts.trackSec * 1000;
|
|
121
|
+
while (Date.now() < end) {
|
|
122
|
+
const at = Date.now() - start;
|
|
123
|
+
// Capture memory output for this sample by intercepting stdout.
|
|
124
|
+
const captured = await captureStdout(async () => {
|
|
125
|
+
await (0, memory_js_1.memory)(profileOpts.appId, { json: true }, sessionName, {});
|
|
126
|
+
});
|
|
127
|
+
samples.push({ at, sample: captured });
|
|
128
|
+
if (Date.now() < end) {
|
|
129
|
+
await new Promise((r) => setTimeout(r, profileOpts.intervalMs));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const parsed = samples.map((s) => {
|
|
133
|
+
try {
|
|
134
|
+
return { at: s.at, data: JSON.parse(s.sample) };
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
return { at: s.at, data: null };
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
if (opts.json) {
|
|
141
|
+
(0, output_js_1.printData)({ samples: parsed, durationMs: Date.now() - start }, opts);
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
console.log(`profile memory — ${samples.length} samples over ${profileOpts.trackSec}s`);
|
|
145
|
+
for (const p of parsed) {
|
|
146
|
+
const summary = p.data && typeof p.data === 'object'
|
|
147
|
+
? Object.entries(p.data)
|
|
148
|
+
.slice(0, 4)
|
|
149
|
+
.map(([k, v]) => `${k}=${typeof v === 'object' ? '…' : String(v)}`)
|
|
150
|
+
.join(' ')
|
|
151
|
+
: '(parse error)';
|
|
152
|
+
console.log(` t+${(p.at / 1000).toFixed(1)}s ${summary}`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return 0;
|
|
156
|
+
}
|
|
157
|
+
async function captureStdout(fn) {
|
|
158
|
+
const chunks = [];
|
|
159
|
+
const origWrite = process.stdout.write.bind(process.stdout);
|
|
160
|
+
process.stdout.write = ((c) => {
|
|
161
|
+
chunks.push(typeof c === 'string' ? c : Buffer.from(c).toString());
|
|
162
|
+
return true;
|
|
163
|
+
});
|
|
164
|
+
try {
|
|
165
|
+
await fn();
|
|
166
|
+
}
|
|
167
|
+
finally {
|
|
168
|
+
process.stdout.write = origWrite;
|
|
169
|
+
}
|
|
170
|
+
return chunks.join('');
|
|
171
|
+
}
|
|
172
|
+
// ── React profiler ────────────────────────────────────────────────────────────
|
|
173
|
+
const REACT_PROFILER_INSTALL = `
|
|
174
|
+
(() => {
|
|
175
|
+
if (globalThis.__CONDUCTOR_REACT_PROFILER__) {
|
|
176
|
+
return { installed: true, already: true };
|
|
177
|
+
}
|
|
178
|
+
const hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
179
|
+
if (!hook) return { installed: false, error: 'No React DevTools hook (Hermes only?)' };
|
|
180
|
+
const commits = [];
|
|
181
|
+
const MAX = 500;
|
|
182
|
+
const orig = hook.onCommitFiberRoot;
|
|
183
|
+
hook.onCommitFiberRoot = function(rendererID, root, priorityLevel) {
|
|
184
|
+
try {
|
|
185
|
+
const entry = { at: Date.now(), rendererID, components: [] };
|
|
186
|
+
let node = root.current;
|
|
187
|
+
const stack = [{ fiber: node, depth: 0 }];
|
|
188
|
+
let count = 0;
|
|
189
|
+
while (stack.length && count < 200) {
|
|
190
|
+
const { fiber, depth } = stack.pop();
|
|
191
|
+
if (!fiber) continue;
|
|
192
|
+
const dur = fiber.actualDuration ?? 0;
|
|
193
|
+
if (dur > 0) {
|
|
194
|
+
const name = (fiber.type && (fiber.type.displayName || fiber.type.name)) || (typeof fiber.type === 'string' ? fiber.type : null);
|
|
195
|
+
if (name) {
|
|
196
|
+
entry.components.push({ name, depth, actualDuration: dur, selfDuration: fiber.selfBaseDuration ?? 0 });
|
|
197
|
+
count++;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (fiber.child) stack.push({ fiber: fiber.child, depth: depth + 1 });
|
|
201
|
+
if (fiber.sibling) stack.push({ fiber: fiber.sibling, depth });
|
|
202
|
+
}
|
|
203
|
+
commits.push(entry);
|
|
204
|
+
if (commits.length > MAX) commits.shift();
|
|
205
|
+
} catch (e) {}
|
|
206
|
+
if (typeof orig === 'function') return orig.apply(this, arguments);
|
|
207
|
+
};
|
|
208
|
+
globalThis.__CONDUCTOR_REACT_PROFILER__ = {
|
|
209
|
+
installed: true,
|
|
210
|
+
commits,
|
|
211
|
+
uninstall: () => { hook.onCommitFiberRoot = orig; }
|
|
212
|
+
};
|
|
213
|
+
return { installed: true, already: false };
|
|
214
|
+
})()
|
|
215
|
+
`;
|
|
216
|
+
const REACT_PROFILER_READ = (top) => `
|
|
217
|
+
(() => {
|
|
218
|
+
const p = globalThis.__CONDUCTOR_REACT_PROFILER__;
|
|
219
|
+
if (!p) return { installed: false, commits: [] };
|
|
220
|
+
const commits = p.commits.slice();
|
|
221
|
+
const byName = {};
|
|
222
|
+
for (const c of commits) {
|
|
223
|
+
for (const comp of c.components) {
|
|
224
|
+
byName[comp.name] = byName[comp.name] ?? { name: comp.name, totalMs: 0, renders: 0 };
|
|
225
|
+
byName[comp.name].totalMs += comp.actualDuration;
|
|
226
|
+
byName[comp.name].renders += 1;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
const top = Object.values(byName).sort((a, b) => b.totalMs - a.totalMs).slice(0, ${top});
|
|
230
|
+
return { installed: true, commits, totalCommits: commits.length, top };
|
|
231
|
+
})()
|
|
232
|
+
`;
|
|
233
|
+
const REACT_PROFILER_STOP = `
|
|
234
|
+
(() => {
|
|
235
|
+
const p = globalThis.__CONDUCTOR_REACT_PROFILER__;
|
|
236
|
+
if (!p) return { installed: false };
|
|
237
|
+
if (typeof p.uninstall === 'function') p.uninstall();
|
|
238
|
+
delete globalThis.__CONDUCTOR_REACT_PROFILER__;
|
|
239
|
+
return { installed: true, stopped: true };
|
|
240
|
+
})()
|
|
241
|
+
`;
|
|
242
|
+
async function profileReactStart(opts, sessionName, cdpOpts) {
|
|
243
|
+
try {
|
|
244
|
+
const platform = await (0, bootstrap_js_1.detectPlatform)(sessionName).catch(() => undefined);
|
|
245
|
+
const client = new metro_cdp_js_1.MetroCdpClient();
|
|
246
|
+
await client.connect({
|
|
247
|
+
port: cdpOpts.port ?? 8081,
|
|
248
|
+
deviceId: sessionName !== 'default' ? sessionName : undefined,
|
|
249
|
+
platform,
|
|
250
|
+
targetIndex: cdpOpts.targetIndex,
|
|
251
|
+
});
|
|
252
|
+
const result = await client.evaluate(REACT_PROFILER_INSTALL);
|
|
253
|
+
client.close();
|
|
254
|
+
if (!result.installed) {
|
|
255
|
+
(0, output_js_1.printError)(`profile react start — ${result.error ?? 'install failed'}`, opts);
|
|
256
|
+
return 1;
|
|
257
|
+
}
|
|
258
|
+
if (opts.json)
|
|
259
|
+
(0, output_js_1.printData)(result, opts);
|
|
260
|
+
else
|
|
261
|
+
(0, output_js_1.printSuccess)(`profile react start — ${result.already ? 'already installed' : 'installed'}`, opts);
|
|
262
|
+
return 0;
|
|
263
|
+
}
|
|
264
|
+
catch (err) {
|
|
265
|
+
(0, output_js_1.printError)(`profile react start — ${err instanceof Error ? err.message : String(err)}`, opts);
|
|
266
|
+
return 1;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
async function profileReactStop(opts, sessionName, cdpOpts, top) {
|
|
270
|
+
try {
|
|
271
|
+
const platform = await (0, bootstrap_js_1.detectPlatform)(sessionName).catch(() => undefined);
|
|
272
|
+
const client = new metro_cdp_js_1.MetroCdpClient();
|
|
273
|
+
await client.connect({
|
|
274
|
+
port: cdpOpts.port ?? 8081,
|
|
275
|
+
deviceId: sessionName !== 'default' ? sessionName : undefined,
|
|
276
|
+
platform,
|
|
277
|
+
targetIndex: cdpOpts.targetIndex,
|
|
278
|
+
});
|
|
279
|
+
const read = await client.evaluate(REACT_PROFILER_READ(top));
|
|
280
|
+
await client.evaluate(REACT_PROFILER_STOP);
|
|
281
|
+
client.close();
|
|
282
|
+
if (!read.installed) {
|
|
283
|
+
(0, output_js_1.printError)('profile react stop — profiler was not installed', opts);
|
|
284
|
+
return 1;
|
|
285
|
+
}
|
|
286
|
+
if (opts.json)
|
|
287
|
+
(0, output_js_1.printData)(read, opts);
|
|
288
|
+
else {
|
|
289
|
+
console.log(`profile react — ${read.totalCommits ?? 0} commit(s)`);
|
|
290
|
+
for (const t of read.top ?? []) {
|
|
291
|
+
console.log(` ${t.totalMs.toFixed(1)}ms ${t.renders}x ${t.name}`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return 0;
|
|
295
|
+
}
|
|
296
|
+
catch (err) {
|
|
297
|
+
(0, output_js_1.printError)(`profile react stop — ${err instanceof Error ? err.message : String(err)}`, opts);
|
|
298
|
+
return 1;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
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.HELP = void 0;
|
|
7
|
+
exports.runSequence = runSequence;
|
|
8
|
+
exports.HELP = ` run-sequence [--file path.json] Run a sequence of conductor commands serially against one session
|
|
9
|
+
JSON shape: {"steps":[{"cmd":"tap-on","args":["Login"]}, ...]}
|
|
10
|
+
Reads stdin when --file is omitted. Stops on first non-zero exit.`;
|
|
11
|
+
const child_process_1 = require("child_process");
|
|
12
|
+
const fs_1 = __importDefault(require("fs"));
|
|
13
|
+
const path_1 = __importDefault(require("path"));
|
|
14
|
+
const output_js_1 = require("../output.js");
|
|
15
|
+
function flagsToArgs(flags) {
|
|
16
|
+
if (!flags)
|
|
17
|
+
return [];
|
|
18
|
+
const out = [];
|
|
19
|
+
for (const [key, value] of Object.entries(flags)) {
|
|
20
|
+
if (value === false)
|
|
21
|
+
continue;
|
|
22
|
+
out.push(`--${key}`);
|
|
23
|
+
if (value !== true)
|
|
24
|
+
out.push(String(value));
|
|
25
|
+
}
|
|
26
|
+
return out;
|
|
27
|
+
}
|
|
28
|
+
function readStdin() {
|
|
29
|
+
return new Promise((resolve, reject) => {
|
|
30
|
+
let buf = '';
|
|
31
|
+
process.stdin.setEncoding('utf-8');
|
|
32
|
+
process.stdin.on('data', (chunk) => {
|
|
33
|
+
buf += chunk;
|
|
34
|
+
});
|
|
35
|
+
process.stdin.on('end', () => resolve(buf));
|
|
36
|
+
process.stdin.on('error', reject);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
async function runSequence(filePath, opts = {}, sessionName = 'default') {
|
|
40
|
+
let raw;
|
|
41
|
+
if (filePath) {
|
|
42
|
+
const resolved = path_1.default.resolve(filePath);
|
|
43
|
+
if (!fs_1.default.existsSync(resolved)) {
|
|
44
|
+
(0, output_js_1.printError)(`run-sequence: file not found: ${resolved}`, opts);
|
|
45
|
+
return 1;
|
|
46
|
+
}
|
|
47
|
+
raw = fs_1.default.readFileSync(resolved, 'utf-8');
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
raw = await readStdin();
|
|
51
|
+
}
|
|
52
|
+
let parsed;
|
|
53
|
+
try {
|
|
54
|
+
parsed = JSON.parse(raw);
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
(0, output_js_1.printError)(`run-sequence: invalid JSON\n${err instanceof Error ? err.message : String(err)}`, opts);
|
|
58
|
+
return 1;
|
|
59
|
+
}
|
|
60
|
+
if (!parsed.steps || !Array.isArray(parsed.steps)) {
|
|
61
|
+
(0, output_js_1.printError)('run-sequence: input must be {"steps":[...]}', opts);
|
|
62
|
+
return 1;
|
|
63
|
+
}
|
|
64
|
+
const results = [];
|
|
65
|
+
const conductorBin = process.argv[1] ?? 'conductor';
|
|
66
|
+
const deviceArgs = sessionName !== 'default' ? ['--device', sessionName] : [];
|
|
67
|
+
for (let i = 0; i < parsed.steps.length; i++) {
|
|
68
|
+
const step = parsed.steps[i];
|
|
69
|
+
const args = [step.cmd, ...(step.args ?? []), ...flagsToArgs(step.flags), ...deviceArgs];
|
|
70
|
+
if (!opts.json) {
|
|
71
|
+
console.log(`[${i + 1}/${parsed.steps.length}] conductor ${args.join(' ')}`);
|
|
72
|
+
}
|
|
73
|
+
const result = await runStep(conductorBin, args);
|
|
74
|
+
results.push({
|
|
75
|
+
cmd: step.cmd,
|
|
76
|
+
args: step.args ?? [],
|
|
77
|
+
exitCode: result.exitCode,
|
|
78
|
+
stdout: result.stdout,
|
|
79
|
+
stderr: result.stderr,
|
|
80
|
+
});
|
|
81
|
+
if (!opts.json && result.stdout)
|
|
82
|
+
process.stdout.write(result.stdout);
|
|
83
|
+
if (!opts.json && result.stderr)
|
|
84
|
+
process.stderr.write(result.stderr);
|
|
85
|
+
if (result.exitCode !== 0) {
|
|
86
|
+
if (opts.json) {
|
|
87
|
+
(0, output_js_1.printData)({ ok: false, completed: i, total: parsed.steps.length, results }, opts);
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
(0, output_js_1.printError)(`run-sequence aborted at step ${i + 1}/${parsed.steps.length} (${step.cmd}, exit ${result.exitCode})`, opts);
|
|
91
|
+
}
|
|
92
|
+
return result.exitCode;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (opts.json) {
|
|
96
|
+
(0, output_js_1.printData)({ ok: true, completed: parsed.steps.length, total: parsed.steps.length, results }, opts);
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
console.log(`run-sequence — completed ${parsed.steps.length} step(s)`);
|
|
100
|
+
}
|
|
101
|
+
return 0;
|
|
102
|
+
}
|
|
103
|
+
function runStep(bin, args) {
|
|
104
|
+
return new Promise((resolve) => {
|
|
105
|
+
const proc = (0, child_process_1.spawn)(process.execPath, [bin, ...args], {
|
|
106
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
107
|
+
env: process.env,
|
|
108
|
+
});
|
|
109
|
+
let stdout = '';
|
|
110
|
+
let stderr = '';
|
|
111
|
+
proc.stdout.on('data', (c) => {
|
|
112
|
+
stdout += c.toString();
|
|
113
|
+
});
|
|
114
|
+
proc.stderr.on('data', (c) => {
|
|
115
|
+
stderr += c.toString();
|
|
116
|
+
});
|
|
117
|
+
proc.on('close', (code) => {
|
|
118
|
+
resolve({ exitCode: code ?? 1, stdout, stderr });
|
|
119
|
+
});
|
|
120
|
+
proc.on('error', (err) => {
|
|
121
|
+
resolve({ exitCode: 1, stdout: '', stderr: err.message });
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
}
|
|
@@ -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 element_resolver_js_1 = require("../drivers/element-resolver.js");
|
|
16
|
+
const direct_ios_selector_js_1 = require("../drivers/direct-ios-selector.js");
|
|
16
17
|
const utils_js_1 = require("../utils.js");
|
|
17
18
|
async function scrollUntilVisible(element, opts = {}, sessionName = 'default', flags = {}) {
|
|
18
19
|
if (!element && !flags.id && !flags.text) {
|
|
@@ -38,9 +39,25 @@ async function scrollUntilVisible(element, opts = {}, sessionName = 'default', f
|
|
|
38
39
|
try {
|
|
39
40
|
const driver = await (0, runner_js_1.getDriver)(sessionName);
|
|
40
41
|
const deadline = Date.now() + timeoutMs;
|
|
42
|
+
// Resolve simple selectors with a direct runner query, skipping the
|
|
43
|
+
// full-tree snapshot on every scroll iteration.
|
|
44
|
+
const iosDirectResolve = driver instanceof ios_js_1.IOSDriver ? (0, direct_ios_selector_js_1.makeIOSDirectResolver)(driver, sel) : undefined;
|
|
41
45
|
while (Date.now() < deadline) {
|
|
42
46
|
try {
|
|
43
47
|
if (driver instanceof ios_js_1.IOSDriver) {
|
|
48
|
+
if (iosDirectResolve) {
|
|
49
|
+
let fast = null;
|
|
50
|
+
try {
|
|
51
|
+
fast = await iosDirectResolve();
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// direct query failed — fall back to the snapshot check below
|
|
55
|
+
}
|
|
56
|
+
if (fast) {
|
|
57
|
+
(0, output_js_1.printSuccess)(`scroll-until-visible ${label} — found`, opts);
|
|
58
|
+
return 0;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
44
61
|
const root = await driver.viewHierarchy().then((h) => h.axElement);
|
|
45
62
|
if ((0, element_resolver_js_1.findIOSElement)(root, sel)) {
|
|
46
63
|
(0, output_js_1.printSuccess)(`scroll-until-visible ${label} — found`, opts);
|
|
@@ -165,6 +165,9 @@ async function startIOS(osVersion, opts, name, deviceType) {
|
|
|
165
165
|
}
|
|
166
166
|
}
|
|
167
167
|
const displayName = name ?? sim.name;
|
|
168
|
+
// Prewarm the driver so the first interaction command is not the
|
|
169
|
+
// one that pays the XCTest runner startup cost.
|
|
170
|
+
await (0, runner_js_1.prewarmDriver)(sim.udid);
|
|
168
171
|
(0, output_js_1.printSuccess)(`Simulator already booted: ${displayName} (${sim.udid})`, opts);
|
|
169
172
|
return 0;
|
|
170
173
|
}
|
|
@@ -203,6 +206,9 @@ async function startIOS(osVersion, opts, name, deviceType) {
|
|
|
203
206
|
}
|
|
204
207
|
(0, child_process_1.spawn)('open', ['-a', 'Simulator'], { detached: true, stdio: 'ignore' }).unref();
|
|
205
208
|
const displayName = name ?? deviceType;
|
|
209
|
+
// Prewarm the driver so the first interaction command is not the
|
|
210
|
+
// one that pays the XCTest runner startup cost.
|
|
211
|
+
await (0, runner_js_1.prewarmDriver)(udid);
|
|
206
212
|
(0, output_js_1.printSuccess)(`Booted: ${displayName} (${udid})`, opts);
|
|
207
213
|
return 0;
|
|
208
214
|
}
|
|
@@ -238,6 +244,9 @@ async function startIOS(osVersion, opts, name, deviceType) {
|
|
|
238
244
|
// Open the Simulator.app so the window appears
|
|
239
245
|
(0, child_process_1.spawn)('open', ['-a', 'Simulator'], { detached: true, stdio: 'ignore' }).unref();
|
|
240
246
|
const displayName = name ?? device.name;
|
|
247
|
+
// Prewarm the driver so the first interaction command is not the
|
|
248
|
+
// one that pays the XCTest runner startup cost.
|
|
249
|
+
await (0, runner_js_1.prewarmDriver)(device.udid);
|
|
241
250
|
(0, output_js_1.printSuccess)(`Booted: ${displayName} (${device.udid})`, opts);
|
|
242
251
|
return 0;
|
|
243
252
|
}
|
|
@@ -320,6 +329,9 @@ async function startTvOS(osVersion, opts, name, deviceType) {
|
|
|
320
329
|
}
|
|
321
330
|
}
|
|
322
331
|
const displayName = name ?? sim.name;
|
|
332
|
+
// Prewarm the driver so the first interaction command is not the
|
|
333
|
+
// one that pays the XCTest runner startup cost.
|
|
334
|
+
await (0, runner_js_1.prewarmDriver)(sim.udid);
|
|
323
335
|
(0, output_js_1.printSuccess)(`Simulator already booted: ${displayName} (${sim.udid})`, opts);
|
|
324
336
|
return 0;
|
|
325
337
|
}
|
|
@@ -358,6 +370,9 @@ async function startTvOS(osVersion, opts, name, deviceType) {
|
|
|
358
370
|
}
|
|
359
371
|
(0, child_process_1.spawn)('open', ['-a', 'Simulator'], { detached: true, stdio: 'ignore' }).unref();
|
|
360
372
|
const displayName = name ?? deviceType;
|
|
373
|
+
// Prewarm the driver so the first interaction command is not the
|
|
374
|
+
// one that pays the XCTest runner startup cost.
|
|
375
|
+
await (0, runner_js_1.prewarmDriver)(udid);
|
|
361
376
|
(0, output_js_1.printSuccess)(`Booted: ${displayName} (${udid})`, opts);
|
|
362
377
|
return 0;
|
|
363
378
|
}
|
|
@@ -393,6 +408,9 @@ async function startTvOS(osVersion, opts, name, deviceType) {
|
|
|
393
408
|
// Open the Simulator.app so the window appears
|
|
394
409
|
(0, child_process_1.spawn)('open', ['-a', 'Simulator'], { detached: true, stdio: 'ignore' }).unref();
|
|
395
410
|
const displayName = name ?? device.name;
|
|
411
|
+
// Prewarm the driver so the first interaction command is not the
|
|
412
|
+
// one that pays the XCTest runner startup cost.
|
|
413
|
+
await (0, runner_js_1.prewarmDriver)(device.udid);
|
|
396
414
|
(0, output_js_1.printSuccess)(`Booted: ${displayName} (${device.udid})`, opts);
|
|
397
415
|
return 0;
|
|
398
416
|
}
|
package/dist/commands/tap.js
CHANGED
|
@@ -23,6 +23,7 @@ const ios_js_1 = require("../drivers/ios.js");
|
|
|
23
23
|
const android_js_1 = require("../drivers/android.js");
|
|
24
24
|
const web_js_1 = require("../drivers/web.js");
|
|
25
25
|
const wait_js_1 = require("../drivers/wait.js");
|
|
26
|
+
const direct_ios_selector_js_1 = require("../drivers/direct-ios-selector.js");
|
|
26
27
|
const utils_js_1 = require("../utils.js");
|
|
27
28
|
async function tap(query, opts = {}, sessionName = 'default', flags = {}) {
|
|
28
29
|
if (!query && !flags.id && !flags.text) {
|
|
@@ -49,7 +50,7 @@ async function tap(query, opts = {}, sessionName = 'default', flags = {}) {
|
|
|
49
50
|
}
|
|
50
51
|
let el;
|
|
51
52
|
if (driver instanceof ios_js_1.IOSDriver) {
|
|
52
|
-
el = await (0, wait_js_1.waitForIOSElement)(() => driver.viewHierarchy().then((h) => h.axElement), sel);
|
|
53
|
+
el = await (0, wait_js_1.waitForIOSElement)((o) => driver.viewHierarchy(false, [], { cache: o?.cached }).then((h) => h.axElement), sel, undefined, undefined, (0, direct_ios_selector_js_1.makeIOSDirectResolver)(driver, sel));
|
|
53
54
|
}
|
|
54
55
|
else if (driver instanceof web_js_1.WebDriver) {
|
|
55
56
|
el = await (0, wait_js_1.waitForWebElement)(() => driver.viewHierarchy(), sel);
|