@keepsnappy/agent 0.0.0-stage → 0.1.0-alpha.11

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.
@@ -0,0 +1,1227 @@
1
+ #!/usr/bin/env node
2
+
3
+ // packages/appium-driver/src/load.ts
4
+ import { isAbsolute, resolve } from "node:path";
5
+ import { pathToFileURL } from "node:url";
6
+
7
+ // packages/appium-driver/src/run.ts
8
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
9
+ import { join } from "node:path";
10
+ import { parseArgs } from "node:util";
11
+
12
+ // packages/appium-driver/src/browserstack.ts
13
+ import { openAsBlob } from "node:fs";
14
+ import { basename } from "node:path";
15
+
16
+ // packages/appium-driver/src/links.ts
17
+ var ALERT_POLL_MS = 250;
18
+ var RUN_DEFAULTS = { ttl: 3600, profile: 1, diag: 1 };
19
+ var END_GC_DEFAULT = true;
20
+ var route = (scheme) => `${scheme}devtools/perf-lab`;
21
+ function parseSdkArgs(args) {
22
+ return args.split("&").filter(Boolean).map((kv) => {
23
+ const i = kv.indexOf("=");
24
+ return i < 0 ? [kv, "1"] : [kv.slice(0, i), kv.slice(i + 1)];
25
+ });
26
+ }
27
+ function linkArgs(args = "") {
28
+ const fields = new Map(Object.entries(RUN_DEFAULTS).map(([k, v]) => [k, String(v)]));
29
+ for (const [k, v] of parseSdkArgs(args))
30
+ if (k !== "activate" && k !== "runToken")
31
+ fields.set(k, v);
32
+ return [...fields].map(([k, v]) => `${k}=${v}`).join("&");
33
+ }
34
+ var activationLink = (scheme, token, args = "") => `${route(scheme)}?activate=1&${linkArgs(args)}&runToken=${token}`;
35
+ var flushLink = (scheme) => `${route(scheme)}?flush=1&activate=0`;
36
+ var gcLink = (scheme, n) => `${route(scheme)}?cmd=gc&n=${n}`;
37
+ async function acceptAlert(driver, labels, waitMs) {
38
+ const deadline = driver.now() + waitMs;
39
+ for (;; ) {
40
+ let buttons = null;
41
+ try {
42
+ buttons = await driver.execute("mobile: alert", { action: "getButtons" });
43
+ } catch {
44
+ buttons = null;
45
+ }
46
+ const hit = Array.isArray(buttons) ? labels.find((l) => buttons.includes(l)) : undefined;
47
+ if (hit) {
48
+ await driver.execute("mobile: alert", { action: "accept", buttonLabel: hit });
49
+ return hit;
50
+ }
51
+ if (driver.now() > deadline)
52
+ return null;
53
+ await driver.sleep(ALERT_POLL_MS);
54
+ }
55
+ }
56
+ async function openLink(driver, url, state) {
57
+ if (state.linkMode !== "safari") {
58
+ try {
59
+ await driver.execute("mobile: deepLink", { url, bundleId: driver.bundleId });
60
+ state.linkMode = "deepLink";
61
+ } catch (e) {
62
+ if (state.linkMode === "deepLink")
63
+ throw e;
64
+ state.linkMode = "safari";
65
+ state.linkError = String(e.message);
66
+ }
67
+ }
68
+ if (state.linkMode === "safari") {
69
+ await driver.execute("mobile: launchApp", { bundleId: "com.apple.mobilesafari" });
70
+ const bar = await driver.find("accessibility id", "TabBarItemTitle") ?? await driver.find("accessibility id", "URL");
71
+ if (bar)
72
+ await driver.click(bar);
73
+ const field = await driver.find("-ios predicate string", 'type == "XCUIElementTypeTextField" AND name == "URL"');
74
+ if (!field)
75
+ throw new Error("Safari address bar not found");
76
+ await driver.cmd("POST", `/element/${field}/value`, { text: `${url}
77
+ ` });
78
+ }
79
+ return acceptAlert(driver, ["Open"], state.linkMode === "safari" ? 8000 : 0);
80
+ }
81
+
82
+ // packages/appium-driver/src/selectors.ts
83
+ var toSelector = (s) => typeof s === "string" ? { id: s } : s;
84
+ var id = (value) => ({ id: value });
85
+ var REGEX_CHARS = /[\\()[\]*+?|^$]/;
86
+ var quote = (s) => `'${s.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
87
+ var matches = (attr, re) => `${attr} MATCHES ${quote(`(?is)${re}`)}`;
88
+ var textMatch = (re) => `(${matches("label", re)} OR ${matches("value", re)})`;
89
+ function locator(sel) {
90
+ if (sel.id !== undefined && sel.text === undefined && !REGEX_CHARS.test(sel.id)) {
91
+ return { using: "accessibility id", value: sel.id };
92
+ }
93
+ const parts = [];
94
+ if (sel.id !== undefined)
95
+ parts.push(REGEX_CHARS.test(sel.id) ? matches("name", sel.id) : `name == ${quote(sel.id)}`);
96
+ if (sel.text !== undefined)
97
+ parts.push(textMatch(sel.text));
98
+ return { using: "-ios predicate string", value: parts.join(" AND ") };
99
+ }
100
+ var describeSelector = (sel) => [sel.id !== undefined ? `id=${sel.id}` : null, sel.text !== undefined ? `text=${sel.text}` : null, sel.childOf !== undefined ? `childOf=${sel.childOf}` : null].filter(Boolean).join(" ");
101
+ var javaString = (s) => `"${s.replace(/"/g, "\\\"")}"`;
102
+ var javaRegex = (re) => {
103
+ const trailing = /\\*$/.exec(re)[0].length;
104
+ if (trailing % 2 === 1)
105
+ throw new Error(`selector ${JSON.stringify(re)} ends in a lone backslash`);
106
+ return javaString(`(?is)${re}${trailing > 0 ? "(?:)" : ""}`);
107
+ };
108
+ function androidLocators(sel) {
109
+ const base = () => {
110
+ if (sel.id === undefined)
111
+ return [];
112
+ return [REGEX_CHARS.test(sel.id) ? `resourceIdMatches(${javaRegex(sel.id)})` : `resourceId(${javaString(sel.id)})`];
113
+ };
114
+ const using = "-android uiautomator";
115
+ const build = (extra) => ({ using, value: `new UiSelector()${[...base(), ...extra].map((c) => `.${c}`).join("")}` });
116
+ if (sel.text === undefined)
117
+ return [build([])];
118
+ return [build([`textMatches(${javaRegex(sel.text)})`]), build([`descriptionMatches(${javaRegex(sel.text)})`])];
119
+ }
120
+ var escapeRegex = (s) => s.replace(/[\\^$.|?*+()[\]{}]/g, "\\$&");
121
+ function androidAnyOfIds(ids) {
122
+ const alternatives = ids.map((id) => `(?:${REGEX_CHARS.test(id) ? id : escapeRegex(id)})`).join("|");
123
+ return { using: "-android uiautomator", value: `new UiSelector().resourceIdMatches(${javaRegex(alternatives)})` };
124
+ }
125
+
126
+ // packages/appium-driver/src/snapshot.ts
127
+ var ENTITIES = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'" };
128
+ var unescapeXml = (s) => s.replace(/&(#x[0-9a-f]+|#\d+|amp|lt|gt|quot|apos);/gi, (_, e) => e[0] === "#" ? String.fromCodePoint(e[1].toLowerCase() === "x" ? parseInt(e.slice(2), 16) : parseInt(e.slice(1), 10)) : ENTITIES[e.toLowerCase()]);
129
+ var TAG = /<(\/?)([\w.$:-]+)((?:\s+[\w:.-]+=(?:"[^"]*"|'[^']*'))*)\s*(\/?)>/g;
130
+ var ATTR = /([\w:.-]+)=(?:"([^"]*)"|'([^']*)')/g;
131
+ var BOUNDS = /^\[(-?\d+),(-?\d+)\]\[(-?\d+),(-?\d+)\]$/;
132
+ function parseAndroidSource(xml) {
133
+ const nodes = [];
134
+ const open = [];
135
+ for (const m of xml.matchAll(TAG)) {
136
+ if (m[1] === "/") {
137
+ open.pop();
138
+ continue;
139
+ }
140
+ const attrs = {};
141
+ for (const a of m[3].matchAll(ATTR))
142
+ attrs[a[1]] = unescapeXml(a[2] ?? a[3]);
143
+ const b = BOUNDS.exec(attrs.bounds ?? "");
144
+ const node = {
145
+ parent: open.length ? open[open.length - 1] : -1,
146
+ id: attrs["resource-id"] ?? "",
147
+ text: attrs.text ?? "",
148
+ desc: attrs["content-desc"] ?? "",
149
+ displayed: attrs.displayed === "true",
150
+ scrollable: attrs.scrollable === "true",
151
+ left: b ? +b[1] : 0,
152
+ top: b ? +b[2] : 0,
153
+ right: b ? +b[3] : 0,
154
+ bottom: b ? +b[4] : 0
155
+ };
156
+ nodes.push(node);
157
+ if (m[4] !== "/")
158
+ open.push(nodes.length - 1);
159
+ }
160
+ return nodes;
161
+ }
162
+ var inverted = (n) => n.bottom < n.top;
163
+ function nodeShown(n, w, full = false) {
164
+ if (!n.displayed || inverted(n))
165
+ return false;
166
+ if (n.right - n.left <= 0 || n.bottom - n.top <= 0)
167
+ return false;
168
+ if (full)
169
+ return n.left >= 0 && n.top >= 0 && n.right <= w.width && n.bottom <= w.height;
170
+ return n.left < w.width && n.top < w.height && n.right > 0 && n.bottom > 0;
171
+ }
172
+ function wholeMatch(re) {
173
+ if (/\(\?[^:=!<]/.test(re) || /\\[AZzGhHRVXpPk]|\\Q|[*+?}]\+/.test(re))
174
+ return null;
175
+ try {
176
+ return new RegExp(`^(?:${re})$`, "is");
177
+ } catch {
178
+ return null;
179
+ }
180
+ }
181
+ function nodeMatcher(sel) {
182
+ let idTest = null;
183
+ if (sel.id !== undefined) {
184
+ if (REGEX_CHARS.test(sel.id)) {
185
+ const re = wholeMatch(sel.id);
186
+ if (!re)
187
+ return null;
188
+ idTest = (s) => re.test(s);
189
+ } else {
190
+ const want = sel.id;
191
+ idTest = (s) => s === want;
192
+ }
193
+ }
194
+ let textRe = null;
195
+ if (sel.text !== undefined) {
196
+ textRe = wholeMatch(sel.text);
197
+ if (!textRe)
198
+ return null;
199
+ }
200
+ return (n) => (idTest ? idTest(n.id) : true) && (textRe ? textRe.test(n.text) || textRe.test(n.desc) : true);
201
+ }
202
+ function idMatcher(id) {
203
+ const m = nodeMatcher({ id });
204
+ return m && ((resourceId) => m({ parent: -1, id: resourceId, text: "", desc: "", displayed: true, scrollable: false, left: 0, top: 0, right: 0, bottom: 0 }));
205
+ }
206
+
207
+ class Screen {
208
+ nodes;
209
+ window;
210
+ edges = new Map;
211
+ container;
212
+ constructor(nodes, window) {
213
+ this.nodes = nodes;
214
+ this.window = window;
215
+ this.container = nodes.map((_, i) => this.scrollAncestor(i));
216
+ const byContainer = new Map;
217
+ nodes.forEach((n, i) => {
218
+ const c = this.container[i];
219
+ if (c >= 0 && inverted(nodes[c]) && inverted(n))
220
+ (byContainer.get(c) ?? byContainer.set(c, []).get(c)).push(i);
221
+ });
222
+ for (const [c, members] of byContainer) {
223
+ const edge = this.clipEdge(members);
224
+ if (edge !== null)
225
+ this.edges.set(c, edge);
226
+ }
227
+ }
228
+ scrollAncestor(i) {
229
+ let p = this.nodes[i].parent;
230
+ while (p >= 0 && !this.nodes[p].scrollable)
231
+ p = this.nodes[p].parent;
232
+ return p;
233
+ }
234
+ clipEdge(members) {
235
+ const heads = new Map;
236
+ for (const i of members) {
237
+ const n = this.nodes[i];
238
+ const parent = this.nodes[n.parent];
239
+ if (parent && inverted(parent) && parent.bottom === n.bottom)
240
+ continue;
241
+ heads.set(n.bottom, (heads.get(n.bottom) ?? 0) + 1);
242
+ }
243
+ let edge = null;
244
+ let most = 1;
245
+ for (const [bottom, count] of heads) {
246
+ if (count > most || count === most && edge !== null && bottom > edge) {
247
+ edge = bottom;
248
+ most = count;
249
+ }
250
+ }
251
+ return edge;
252
+ }
253
+ nodeShown(i, full) {
254
+ const n = this.nodes[i];
255
+ if (!inverted(n))
256
+ return nodeShown(n, this.window, full);
257
+ const edge = this.edges.get(this.container[i]);
258
+ if (full || edge === undefined)
259
+ return false;
260
+ return n.bottom > edge && n.bottom <= this.window.height && n.right > n.left && n.left < this.window.width && n.right > 0;
261
+ }
262
+ shown(sel, full = false) {
263
+ let inside;
264
+ if (sel.childOf !== undefined) {
265
+ const parent = this.shown({ id: sel.childOf });
266
+ if (parent === null)
267
+ return null;
268
+ if (parent.length === 0)
269
+ return [];
270
+ inside = parent[0];
271
+ }
272
+ const match = nodeMatcher({ ...sel.id !== undefined ? { id: sel.id } : {}, ...sel.text !== undefined ? { text: sel.text } : {} });
273
+ if (!match)
274
+ return null;
275
+ const hits = [];
276
+ this.nodes.forEach((n, i) => {
277
+ if (!match(n) || !this.nodeShown(i, full))
278
+ return;
279
+ if (inside !== undefined && !this.within(i, inside))
280
+ return;
281
+ hits.push(i);
282
+ });
283
+ return hits;
284
+ }
285
+ within(i, ancestor) {
286
+ for (let p = this.nodes[i].parent;p >= 0; p = this.nodes[p].parent)
287
+ if (p === ancestor)
288
+ return true;
289
+ return false;
290
+ }
291
+ }
292
+
293
+ // packages/appium-driver/src/webdriver.ts
294
+ var ELEMENT_KEY = "element-6066-11e4-a52e-4f735466cecf";
295
+ var TAP_HOLD_MS = 100;
296
+ var realSleep = (ms) => new Promise((r) => setTimeout(r, ms));
297
+
298
+ class WdError extends Error {
299
+ wdError;
300
+ constructor(message, wdError) {
301
+ super(message);
302
+ this.wdError = wdError;
303
+ }
304
+ }
305
+
306
+ class Driver {
307
+ bundleId;
308
+ platform;
309
+ headers;
310
+ hub;
311
+ fetch;
312
+ log;
313
+ now;
314
+ sleep;
315
+ sessionId = null;
316
+ context = {};
317
+ viewport = null;
318
+ rects = new Map;
319
+ constructor(o) {
320
+ this.bundleId = o.bundleId;
321
+ this.platform = o.platform ?? IOS;
322
+ this.headers = o.headers ?? {};
323
+ this.hub = o.hub.replace(/\/+$/, "");
324
+ this.fetch = o.fetch ?? fetch;
325
+ this.log = o.log;
326
+ this.now = o.now ?? Date.now;
327
+ this.sleep = o.sleep ?? realSleep;
328
+ }
329
+ async cmd(method, path, body) {
330
+ const url = `${this.hub}${path.startsWith("/session") || !this.sessionId ? path : `/session/${this.sessionId}${path}`}`;
331
+ const t0 = this.now();
332
+ let res;
333
+ let json;
334
+ try {
335
+ res = await this.fetch(url, {
336
+ method,
337
+ headers: { "Content-Type": "application/json", ...this.headers },
338
+ ...body === undefined ? {} : { body: JSON.stringify(body) }
339
+ });
340
+ json = await res.json().catch(() => ({}));
341
+ } finally {
342
+ this.log.push({ ...this.context, cmd: `${method} ${path.replace(/\/element\/[^/]+/, "/element/:id")}`, t: t0, ms: this.now() - t0, status: res?.status ?? 0 });
343
+ }
344
+ const value = json?.value;
345
+ if (!res.ok || value && typeof value === "object" && value.error) {
346
+ throw new WdError(`${method} ${path}: ${value?.error ?? res.status} ${String(value?.message ?? "").slice(0, 300)}`.trim(), value?.error ?? String(res.status));
347
+ }
348
+ return value;
349
+ }
350
+ async start(capabilities) {
351
+ const value = await this.cmd("POST", "/session", { capabilities: { alwaysMatch: capabilities } });
352
+ this.sessionId = value.sessionId;
353
+ return value;
354
+ }
355
+ execute(script, arg) {
356
+ return this.cmd("POST", "/execute/sync", { script, args: arg === undefined ? [] : [arg] });
357
+ }
358
+ async find(using, value, parent) {
359
+ try {
360
+ const el = await this.cmd("POST", parent ? `/element/${parent}/element` : "/element", { using, value });
361
+ return el[ELEMENT_KEY] ?? Object.values(el)[0];
362
+ } catch (e) {
363
+ if (e instanceof WdError && e.wdError === "no such element")
364
+ return null;
365
+ throw e;
366
+ }
367
+ }
368
+ async findAll(using, value, parent) {
369
+ const path = parent ? `/element/${parent}/elements` : "/elements";
370
+ const list = await this.cmd("POST", path, { using, value });
371
+ return (list ?? []).map((el) => el[ELEMENT_KEY] ?? Object.values(el)[0]);
372
+ }
373
+ async screen() {
374
+ this.viewport ??= await this.cmd("GET", "/window/rect");
375
+ return this.viewport;
376
+ }
377
+ async onScreen(el, full = false) {
378
+ let r;
379
+ try {
380
+ r = await this.cmd("GET", `/element/${el}/rect`);
381
+ } catch (e) {
382
+ if (e instanceof WdError && e.wdError === "stale element reference")
383
+ return false;
384
+ throw e;
385
+ }
386
+ const w = await this.screen();
387
+ if (!r || r.width <= 0 || r.height <= 0)
388
+ return false;
389
+ this.rects.set(el, r);
390
+ if (full)
391
+ return r.x >= 0 && r.y >= 0 && r.x + r.width <= w.width && r.y + r.height <= w.height;
392
+ return r.x < w.width && r.y < w.height && r.x + r.width > 0 && r.y + r.height > 0;
393
+ }
394
+ async findShown(sel, full = false) {
395
+ let parent;
396
+ if (sel.childOf !== undefined) {
397
+ const p = await this.findShown(id(sel.childOf));
398
+ if (!p)
399
+ return null;
400
+ parent = p;
401
+ }
402
+ for (const { using, value } of this.platform.locators(sel)) {
403
+ if (this.platform.singleFindFirst) {
404
+ const first = await this.find(using, value, parent);
405
+ if (!first)
406
+ continue;
407
+ if (await this.onScreen(first, full))
408
+ return first;
409
+ }
410
+ for (const el of await this.findAll(using, value, parent)) {
411
+ if (await this.onScreen(el, full))
412
+ return el;
413
+ }
414
+ }
415
+ return null;
416
+ }
417
+ async shownMany(sels, group) {
418
+ const out = sels.map(() => false);
419
+ const tests = sels.map((s) => s.id !== undefined && s.text === undefined && s.childOf === undefined ? idMatcher(s.id) : null);
420
+ const grouped = group && this.platform.anyOfIds ? tests.flatMap((t, i) => t ? [i] : []) : [];
421
+ if (grouped.length >= 2) {
422
+ const { using, value } = this.platform.anyOfIds(grouped.map((i) => sels[i].id));
423
+ for (const el of await this.findAll(using, value)) {
424
+ const wanted = grouped.filter((i) => !out[i]);
425
+ if (wanted.length === 0)
426
+ break;
427
+ let resourceId;
428
+ try {
429
+ resourceId = String(await this.cmd("GET", `/element/${el}/attribute/resource-id`));
430
+ } catch (e) {
431
+ if (e instanceof WdError && e.wdError === "stale element reference")
432
+ continue;
433
+ throw e;
434
+ }
435
+ const hits = wanted.filter((i) => tests[i](resourceId));
436
+ if (hits.length > 0 && await this.onScreen(el))
437
+ for (const i of hits)
438
+ out[i] = true;
439
+ }
440
+ }
441
+ for (let i = 0;i < sels.length; i++)
442
+ if (!grouped.includes(i) || grouped.length < 2)
443
+ out[i] = Boolean(await this.findShown(sels[i]));
444
+ return out;
445
+ }
446
+ async click(el) {
447
+ await this.cmd("POST", `/element/${el}/click`, {});
448
+ }
449
+ async tapElement(el) {
450
+ const r = this.rects.get(el) ?? await this.cmd("GET", `/element/${el}/rect`);
451
+ const w = await this.screen();
452
+ const x = Math.round(Math.min(Math.max(r.x + r.width / 2, 1), w.width - 1));
453
+ const y = Math.round(Math.min(Math.max(r.y + r.height / 2, 1), w.height - 1));
454
+ await this.cmd("POST", "/actions", {
455
+ actions: [
456
+ {
457
+ type: "pointer",
458
+ id: "finger1",
459
+ parameters: { pointerType: "touch" },
460
+ actions: [
461
+ { type: "pointerMove", duration: 0, x, y },
462
+ { type: "pointerDown", button: 0 },
463
+ { type: "pause", duration: TAP_HOLD_MS },
464
+ { type: "pointerUp", button: 0 }
465
+ ]
466
+ }
467
+ ]
468
+ });
469
+ await this.cmd("DELETE", "/actions").catch(() => null);
470
+ }
471
+ async drag(from, to, durationMs) {
472
+ await this.cmd("POST", "/actions", {
473
+ actions: [
474
+ {
475
+ type: "pointer",
476
+ id: "finger1",
477
+ parameters: { pointerType: "touch" },
478
+ actions: [
479
+ { type: "pointerMove", duration: 0, x: from.x, y: from.y },
480
+ { type: "pointerDown", button: 0 },
481
+ { type: "pointerMove", duration: durationMs, x: to.x, y: to.y },
482
+ { type: "pointerUp", button: 0 }
483
+ ]
484
+ }
485
+ ]
486
+ });
487
+ await this.cmd("DELETE", "/actions").catch(() => null);
488
+ }
489
+ async relaunch(bundleId = this.bundleId) {
490
+ await this.execute("mobile: terminateApp", this.platform.appArg(bundleId));
491
+ await this.execute("mobile: activateApp", this.platform.appArg(bundleId));
492
+ }
493
+ activate(bundleId = this.bundleId) {
494
+ return this.execute("mobile: activateApp", this.platform.appArg(bundleId));
495
+ }
496
+ async end() {
497
+ if (!this.sessionId)
498
+ return;
499
+ await this.cmd("DELETE", `/session/${this.sessionId}`).catch(() => null);
500
+ }
501
+ }
502
+
503
+ // packages/appium-driver/src/platform.ts
504
+ var modelOf = (device) => device.replace(/-[^-]*$/, "");
505
+ var versionOf = (device) => device.replace(/^.*-/, "");
506
+ var FLUSH_ID = "perflab.flushed";
507
+ var ACK_ID = "perflab.ack";
508
+ var GC_ANSWER_WAIT_MS = 1e4;
509
+ var GC_POLL_MS = 100;
510
+ function parseBroadcastAnswer(out, n) {
511
+ const data = /data="([^"]*)"/.exec(String(out ?? ""))?.[1];
512
+ return data !== undefined && data.startsWith(`gc:${n} `) ? data : null;
513
+ }
514
+ var NO_WAIT_MS = 0;
515
+ var NEW_COMMAND_TIMEOUT_S = 300;
516
+ var IOS = {
517
+ name: "ios",
518
+ defaultDevice: "iPhone 15 Pro-17",
519
+ caps: (device) => ({
520
+ platformName: "iOS",
521
+ "appium:automationName": "XCUITest",
522
+ "appium:deviceName": modelOf(device),
523
+ "appium:platformVersion": versionOf(device),
524
+ "appium:autoAcceptAlerts": true,
525
+ "appium:newCommandTimeout": NEW_COMMAND_TIMEOUT_S,
526
+ "appium:waitForIdleTimeout": NO_WAIT_MS
527
+ }),
528
+ settings: { animationCoolOffTimeout: NO_WAIT_MS, waitForIdleTimeout: NO_WAIT_MS, shouldUseCompactResponses: true },
529
+ locators: (sel) => [locator(sel)],
530
+ keyboardShown: async (driver) => Boolean(await driver.findShown(id("Return"))),
531
+ appArg: (bundleId) => ({ bundleId }),
532
+ openLink: (driver, url, state) => openLink(driver, url, state),
533
+ acceptAlert: (driver, labels, waitMs) => acceptAlert(driver, labels, waitMs),
534
+ flush: { lookup: { using: "accessibility id", value: FLUSH_ID }, statusAttribute: "label" },
535
+ uploadFields: { ios_keychain_support: "true" },
536
+ async gcCommand(driver, n, scheme, state) {
537
+ await openLink(driver, gcLink(scheme, n), state);
538
+ const want = `type == "XCUIElementTypeStaticText" AND name == "${ACK_ID}" AND label BEGINSWITH "gc:${n} "`;
539
+ const deadline = driver.now() + GC_ANSWER_WAIT_MS;
540
+ for (;; ) {
541
+ const el = await driver.find("-ios predicate string", want);
542
+ if (el)
543
+ return driver.cmd("GET", `/element/${el}/attribute/label`).then(String).catch(() => {
544
+ driver.log.push({ ...driver.context, cmd: "gc-ack-label", t: driver.now(), ms: 0, status: 1 });
545
+ return `gc:${n} ok`;
546
+ });
547
+ if (driver.now() > deadline)
548
+ return null;
549
+ await driver.sleep(GC_POLL_MS);
550
+ }
551
+ }
552
+ };
553
+ var ANDROID = {
554
+ name: "android",
555
+ defaultDevice: "Google Pixel 7-13.0",
556
+ caps: (device) => ({
557
+ platformName: "Android",
558
+ "appium:automationName": "UiAutomator2",
559
+ "appium:deviceName": modelOf(device),
560
+ "appium:platformVersion": versionOf(device),
561
+ "appium:autoGrantPermissions": true,
562
+ "appium:newCommandTimeout": NEW_COMMAND_TIMEOUT_S
563
+ }),
564
+ settings: { waitForIdleTimeout: NO_WAIT_MS, actionAcknowledgmentTimeout: NO_WAIT_MS, waitForSelectorTimeout: NO_WAIT_MS, enableMultiWindows: true, ignoreUnimportantViews: true },
565
+ locators: androidLocators,
566
+ appArg: (appId) => ({ appId }),
567
+ async openLink(driver, url) {
568
+ await driver.execute("mobile: deepLink", { url, package: driver.bundleId });
569
+ return null;
570
+ },
571
+ async acceptAlert(driver, _labels, waitMs) {
572
+ const deadline = driver.now() + waitMs;
573
+ for (;; ) {
574
+ try {
575
+ const alertText = await driver.cmd("GET", "/alert/text");
576
+ await driver.cmd("POST", "/alert/accept", {});
577
+ return String(alertText);
578
+ } catch (e) {
579
+ if (!(e instanceof WdError && e.wdError === "no such alert"))
580
+ throw e;
581
+ }
582
+ if (driver.now() > deadline)
583
+ return null;
584
+ await driver.sleep(ALERT_POLL_MS);
585
+ }
586
+ },
587
+ flush: {
588
+ lookup: { using: "-android uiautomator", value: `new UiSelector().description("${FLUSH_ID}")` },
589
+ statusAttribute: "text",
590
+ fallback: { lookup: { using: "xpath", value: `//*[@content-desc="${FLUSH_ID}"]` }, setting: "allowInvisibleElements" }
591
+ },
592
+ async readScreen(driver) {
593
+ await driver.cmd("POST", "/appium/settings", { settings: { allowInvisibleElements: true } });
594
+ let xml;
595
+ try {
596
+ xml = await driver.cmd("GET", "/source");
597
+ } finally {
598
+ await driver.cmd("POST", "/appium/settings", { settings: { allowInvisibleElements: false } }).catch(() => null);
599
+ }
600
+ return new Screen(parseAndroidSource(xml), await driver.screen());
601
+ },
602
+ keyboardShown: async (driver) => Boolean(await driver.execute("mobile: isKeyboardShown")),
603
+ singleFindFirst: true,
604
+ anyOfIds: androidAnyOfIds,
605
+ uploadFields: {},
606
+ async gcCommand(driver, n) {
607
+ const out = await driver.execute("mobile: broadcast", {
608
+ action: `${driver.bundleId}.perflab.COMMAND`,
609
+ package: driver.bundleId,
610
+ extras: [["s", "cmd", "gc"], ["i", "n", String(n)]],
611
+ flags: "0x10000000"
612
+ });
613
+ return parseBroadcastAnswer(out, n);
614
+ }
615
+ };
616
+ var PLATFORMS = { ios: IOS, android: ANDROID };
617
+ function platformOf(name) {
618
+ if (!Object.hasOwn(PLATFORMS, name))
619
+ throw new Error(`--platform ${name}: the Appium driver drives ios and android`);
620
+ return PLATFORMS[name];
621
+ }
622
+
623
+ // packages/appium-driver/src/browserstack.ts
624
+ var HUB = "https://hub-cloud.browserstack.com/wd/hub";
625
+ var BS_API = "https://api-cloud.browserstack.com/app-automate";
626
+ var basicAuth = (user, key) => `Basic ${Buffer.from(`${user}:${key}`).toString("base64")}`;
627
+ async function resolveApp(app, customId, auth, fetchImpl, platform = IOS) {
628
+ if (app.startsWith("bs://"))
629
+ return app;
630
+ const headers = { Authorization: auth };
631
+ if (customId) {
632
+ const res = await fetchImpl(`${BS_API}/recent_apps/${encodeURIComponent(customId)}`, { headers });
633
+ const list = res.ok ? await res.json().catch(() => []) : [];
634
+ const url = Array.isArray(list) ? list[0]?.app_url : undefined;
635
+ if (url)
636
+ return url;
637
+ }
638
+ const form = new FormData;
639
+ form.append("file", await openAsBlob(app), basename(app));
640
+ if (customId)
641
+ form.append("custom_id", customId);
642
+ for (const [k, v] of Object.entries(platform.uploadFields))
643
+ form.append(k, v);
644
+ const res = await fetchImpl(`${BS_API}/upload`, { method: "POST", headers, body: form });
645
+ const body = await res.json().catch(() => ({}));
646
+ if (!body.app_url)
647
+ throw new Error(`app upload failed: HTTP ${res.status} ${JSON.stringify(body)}`);
648
+ return body.app_url;
649
+ }
650
+ function sessionCaps(i) {
651
+ const tunnel = i.localIdentifier ? { local: true, localIdentifier: i.localIdentifier } : {};
652
+ return {
653
+ ...(i.platform ?? IOS).caps(i.device),
654
+ "appium:app": i.appUrl,
655
+ "bstack:options": {
656
+ projectName: "perflab",
657
+ buildName: i.buildName,
658
+ sessionName: i.sessionName,
659
+ ...tunnel,
660
+ idleTimeout: 300,
661
+ appiumVersion: i.appiumVersion
662
+ }
663
+ };
664
+ }
665
+ async function sessionDetail(sessionId, auth, fetchImpl) {
666
+ const res = await fetchImpl(`${BS_API}/sessions/${sessionId}.json`, { headers: { Authorization: auth } }).catch(() => null);
667
+ if (!res?.ok)
668
+ return {};
669
+ return (await res.json()).automation_session ?? {};
670
+ }
671
+
672
+ // packages/appium-driver/src/session.ts
673
+ var MARKERS_PER_REQUEST = 500;
674
+ var MARKER_TRIES = 3;
675
+ var HOST_CLOCK_TRIPS = 6;
676
+ var RETRY_BASE_MS = 1000;
677
+ var RETRY_AFTER_CAP_MS = 30000;
678
+ async function post(fetchImpl, url, token, body, now) {
679
+ const t0 = now();
680
+ try {
681
+ const res = await fetchImpl(url, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` }, body: JSON.stringify(body) });
682
+ const t3 = now();
683
+ const after = Number(res.headers?.get?.("retry-after"));
684
+ const parsed = await res.json().catch(() => null);
685
+ const clock = parsed?.clock;
686
+ const trip = typeof clock?.recv_ms === "number" && typeof clock.send_ms === "number" ? { t0, t1: clock.recv_ms, t2: clock.send_ms, t3 } : null;
687
+ return { status: res.status, retryAfterMs: Number.isFinite(after) && after > 0 ? after * 1000 : null, body: parsed, trip };
688
+ } catch {
689
+ return null;
690
+ }
691
+ }
692
+ async function postWithRetry(fetchImpl, url, token, body, sleep, now) {
693
+ let answer = null;
694
+ for (let attempt = 0;attempt < MARKER_TRIES; attempt++) {
695
+ if (attempt > 0)
696
+ await sleep(Math.min(answer?.retryAfterMs ?? RETRY_BASE_MS * 2 ** (attempt - 1), RETRY_AFTER_CAP_MS));
697
+ answer = await post(fetchImpl, url, token, body, now);
698
+ if (answer && answer.status !== 429 && answer.status < 500)
699
+ return answer;
700
+ }
701
+ return answer;
702
+ }
703
+ var itemsArrived = (body) => {
704
+ const results = body?.results;
705
+ return Array.isArray(results) ? results.filter((r) => r?.ok === true).length : null;
706
+ };
707
+ async function sendMarkers(fetchImpl, api, markers, opts = {}) {
708
+ const sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
709
+ const now = opts.now ?? Date.now;
710
+ const byToken = new Map;
711
+ for (const m of markers.splice(0)) {
712
+ if (!m.token || !api)
713
+ continue;
714
+ const list = byToken.get(m.token) ?? [];
715
+ list.push(m);
716
+ byToken.set(m.token, list);
717
+ }
718
+ const base = api.replace(/\/+$/, "");
719
+ const url = `${base}/v1/markers`;
720
+ let sent = 0;
721
+ for (const [token, list] of byToken) {
722
+ const trips = [];
723
+ const withClock = (body) => {
724
+ const carried = trips.splice(0);
725
+ return carried.length ? { ...body, clock: carried } : body;
726
+ };
727
+ const send = async (body) => {
728
+ const answer = await postWithRetry(fetchImpl, url, token, withClock(body), sleep, now);
729
+ if (answer?.trip)
730
+ trips.push(answer.trip);
731
+ return answer;
732
+ };
733
+ for (let i = 0;i < list.length; i += MARKERS_PER_REQUEST) {
734
+ const chunk = list.slice(i, i + MARKERS_PER_REQUEST).map((m) => ({ step: m.step, phase: m.phase, t_host: m.t_host, ...m.gc_n !== undefined ? { gc_n: m.gc_n } : {} }));
735
+ const answer = await send({ markers: chunk });
736
+ const arrived = answer && answer.status < 300 ? itemsArrived(answer.body) : null;
737
+ if (arrived !== null) {
738
+ sent += arrived;
739
+ continue;
740
+ }
741
+ if (answer?.status !== 400 && answer?.status !== 413 || itemsArrived(answer.body) !== null)
742
+ continue;
743
+ for (const one of chunk) {
744
+ const single = await send(one);
745
+ if (single && single.status < 300)
746
+ sent++;
747
+ }
748
+ }
749
+ for (let i = 0;i < HOST_CLOCK_TRIPS && trips.length; i++) {
750
+ const answer = await post(fetchImpl, `${base}/v1/clock`, token, { source: "host", samples: trips.splice(0) }, now);
751
+ if (answer?.trip)
752
+ trips.push(answer.trip);
753
+ }
754
+ if (trips.length)
755
+ await post(fetchImpl, `${base}/v1/clock`, token, { source: "host", samples: trips.splice(0) }, now);
756
+ }
757
+ return sent;
758
+ }
759
+ var SDK_FLUSH_WORST_CASE_MS = 315000;
760
+ var FLUSH_MARGIN_MS = 30000;
761
+ var FLUSH_WAIT_MS = SDK_FLUSH_WORST_CASE_MS + FLUSH_MARGIN_MS;
762
+ var FLUSH_POLL_MS = 500;
763
+ var FLUSH_FALLBACK_EVERY = 10;
764
+ var RESCUE_EVERY_MS = 3000;
765
+ var noop = async () => {
766
+ return;
767
+ };
768
+
769
+ class Session {
770
+ driver;
771
+ scheme;
772
+ fetch;
773
+ api;
774
+ markers = [];
775
+ token = "";
776
+ linkState = {};
777
+ pendingReady = null;
778
+ readyWaits = [];
779
+ actionGc;
780
+ endGc;
781
+ gcN = 0;
782
+ stepDepth = 0;
783
+ constructor(o) {
784
+ this.driver = o.driver;
785
+ this.scheme = o.scheme;
786
+ this.fetch = o.fetch ?? fetch;
787
+ this.api = o.api ?? "";
788
+ this.actionGc = o.actionGc ? "step" : "off";
789
+ this.endGc = o.endGc ?? END_GC_DEFAULT;
790
+ }
791
+ async gc(atEnd = false) {
792
+ if (this.actionGc !== "step" && !(atEnd && this.endGc && this.actionGc === "off"))
793
+ return null;
794
+ if (this.linkState.linkMode === "safari") {
795
+ this.actionGc = "safari";
796
+ return null;
797
+ }
798
+ const n = ++this.gcN;
799
+ const start = this.now();
800
+ const answer = await this.driver.platform.gcCommand(this.driver, n, this.scheme, this.linkState).catch(() => null);
801
+ this.driver.log.push({ ...this.driver.context, cmd: "gc", t: this.now(), ms: this.now() - start, status: answer === `gc:${n} ok` ? 0 : 1, ...answer !== null ? { answer } : {} });
802
+ if (answer === `gc:${n} ok`)
803
+ return n;
804
+ this.actionGc = answer === `gc:${n} unavailable` ? "unavailable" : "unsupported";
805
+ return null;
806
+ }
807
+ now() {
808
+ return this.driver.now();
809
+ }
810
+ expectNext(sel, timeoutMs = 1e4) {
811
+ this.pendingReady = { sel, timeoutMs };
812
+ }
813
+ async ready() {
814
+ const p = this.pendingReady;
815
+ if (!p)
816
+ return 0;
817
+ this.pendingReady = null;
818
+ const start = this.now();
819
+ await this.waitVisible(p.sel, p.timeoutMs);
820
+ const took = this.now() - start;
821
+ this.readyWaits.push(took);
822
+ this.driver.log.push({ ...this.driver.context, cmd: "ready", t: this.now(), ms: took, status: 0 });
823
+ return took;
824
+ }
825
+ marker(step, phase, gcN = null) {
826
+ this.markers.push({ token: this.token, step, phase, t_host: this.now(), ...gcN !== null ? { gc_n: gcN } : {} });
827
+ }
828
+ async step(name, action, checks = noop, once = noop) {
829
+ await this.ready();
830
+ this.driver.context = { ...this.driver.context, step: name };
831
+ this.stepDepth++;
832
+ try {
833
+ const gcN = this.stepDepth === 1 ? await this.gc() : null;
834
+ this.marker(name, "start", gcN);
835
+ await action();
836
+ await checks();
837
+ await once();
838
+ this.marker(name, "end");
839
+ await checks();
840
+ } finally {
841
+ this.stepDepth--;
842
+ const { step: _done, ...rest } = this.driver.context;
843
+ this.driver.context = rest;
844
+ }
845
+ }
846
+ isVisible(sel) {
847
+ return this.driver.findShown(toSelector(sel)).then(Boolean);
848
+ }
849
+ async waitVisible(sel, timeoutMs = 1e4) {
850
+ const s = toSelector(sel);
851
+ const deadline = this.now() + timeoutMs;
852
+ for (;; ) {
853
+ const el = await this.driver.findShown(s);
854
+ if (el)
855
+ return el;
856
+ if (this.now() > deadline)
857
+ throw new Error(`${describeSelector(s)} not visible within ${timeoutMs} ms`);
858
+ await this.driver.sleep(200);
859
+ }
860
+ }
861
+ async waitGone(sel, timeoutMs = 1e4) {
862
+ const s = toSelector(sel);
863
+ const deadline = this.now() + timeoutMs;
864
+ for (;; ) {
865
+ if (!await this.driver.findShown(s))
866
+ return;
867
+ if (this.now() > deadline)
868
+ throw new Error(`${describeSelector(s)} still visible after ${timeoutMs} ms`);
869
+ await this.driver.sleep(200);
870
+ }
871
+ }
872
+ async waitKeyboard(want, timeoutMs = 1e4) {
873
+ const deadline = this.now() + timeoutMs;
874
+ while (await this.driver.platform.keyboardShown(this.driver) !== (want === "up")) {
875
+ if (this.now() > deadline)
876
+ throw new Error(`keyboard not ${want} within ${timeoutMs} ms`);
877
+ await this.driver.sleep(200);
878
+ }
879
+ }
880
+ async waitAll(conditions, timeoutMs = 1e4) {
881
+ const platform = this.driver.platform;
882
+ if (!platform.anyOfIds) {
883
+ for (const c of conditions) {
884
+ if ("shown" in c)
885
+ await this.waitVisible(c.shown, timeoutMs);
886
+ else if ("gone" in c)
887
+ await this.waitGone(c.gone, timeoutMs);
888
+ else
889
+ await this.waitKeyboard(c.keyboard, timeoutMs);
890
+ }
891
+ return;
892
+ }
893
+ const elements = conditions.flatMap((c, at) => ("keyboard" in c) ? [] : [{ at, want: "shown" in c, sel: toSelector("shown" in c ? c.shown : c.gone) }]);
894
+ const deadline = this.now() + timeoutMs;
895
+ let lastRescue = -Infinity;
896
+ const group = conditions.some((c) => ("gone" in c));
897
+ for (;; ) {
898
+ const found = await this.driver.shownMany(elements.map((e) => e.sel), group);
899
+ const met = (e, i) => found[i] === e.want;
900
+ let unmet = elements.flatMap((e, i) => met(e, i) ? [] : [conditions[e.at]]);
901
+ const missing = elements.flatMap((e, i) => e.want && !found[i] ? [i] : []);
902
+ if (missing.length > 0 && platform.readScreen && this.now() - lastRescue >= RESCUE_EVERY_MS && await platform.keyboardShown(this.driver)) {
903
+ try {
904
+ const screen = await platform.readScreen(this.driver);
905
+ for (const i of missing)
906
+ if (screen.shown(elements[i].sel)?.length)
907
+ found[i] = true;
908
+ } catch {} finally {
909
+ lastRescue = this.now();
910
+ }
911
+ unmet = elements.flatMap((e, i) => met(e, i) ? [] : [conditions[e.at]]);
912
+ }
913
+ for (const c of conditions)
914
+ if ("keyboard" in c && await platform.keyboardShown(this.driver) !== (c.keyboard === "up"))
915
+ unmet.push(c);
916
+ if (unmet.length === 0)
917
+ return;
918
+ if (this.now() > deadline) {
919
+ throw new Error(`${unmet.map((c) => ("shown" in c) ? `not visible: ${describeSelector(toSelector(c.shown))}` : ("gone" in c) ? `still visible: ${describeSelector(toSelector(c.gone))}` : `keyboard not ${c.keyboard}`).join("; ")} within ${timeoutMs} ms`);
920
+ }
921
+ await this.driver.sleep(200);
922
+ }
923
+ }
924
+ async tap(sel, timeoutMs = 1e4) {
925
+ await this.ready();
926
+ const el = await this.waitVisible(sel, timeoutMs);
927
+ await this.driver.tapElement(el);
928
+ }
929
+ async tapUntil(sel, target, timeoutMs = 15000, retapAfterMs = 2000) {
930
+ const s = toSelector(sel);
931
+ const deadline = this.now() + timeoutMs;
932
+ let retaps = 0;
933
+ await this.tap(s, timeoutMs);
934
+ let retapAt = this.now() + retapAfterMs;
935
+ for (;; ) {
936
+ if (await this.isVisible(target))
937
+ return retaps;
938
+ if (this.now() > deadline) {
939
+ throw new Error(`${describeSelector(toSelector(target))} not visible within ${timeoutMs} ms of tapping ${describeSelector(s)} (${retaps + 1} taps)`);
940
+ }
941
+ if (this.now() > retapAt) {
942
+ const el = await this.driver.findShown(s);
943
+ if (el) {
944
+ await this.ready();
945
+ await this.driver.tapElement(el);
946
+ retaps++;
947
+ }
948
+ retapAt = this.now() + retapAfterMs;
949
+ }
950
+ await this.driver.sleep(100);
951
+ }
952
+ }
953
+ async swipe(from, to, durationMs = 400, target) {
954
+ await this.ready();
955
+ if (target !== undefined)
956
+ await this.waitVisible(target);
957
+ const w = await this.driver.screen();
958
+ const pt = ([x, y]) => ({ x: Math.round(w.width * x / 100), y: Math.round(w.height * y / 100) });
959
+ await this.driver.drag(pt(from), pt(to), durationMs);
960
+ }
961
+ async swipeFrom(sel, direction, durationMs = 400) {
962
+ await this.ready();
963
+ const el = await this.waitVisible(sel);
964
+ const r = await this.driver.cmd("GET", `/element/${el}/rect`);
965
+ const w = await this.driver.screen();
966
+ const from = { x: Math.round(r.x + r.width / 2), y: Math.round(r.y + r.height / 2) };
967
+ const to = { x: from.x, y: Math.round(w.height * (direction === "down" ? 0.9 : 0.1)) };
968
+ await this.driver.drag(from, to, durationMs);
969
+ }
970
+ async swipeUntilVisible(sel, from, to, durationMs, times) {
971
+ for (let i = 0;i < times && !await this.isVisible(sel); i++)
972
+ await this.swipe(from, to, durationMs);
973
+ }
974
+ async swipeFromUntilVisible(from, direction, target, times, durationMs = 400) {
975
+ for (let i = 0;i < times; i++) {
976
+ if (await this.isVisible(target))
977
+ return;
978
+ await this.swipeFrom(from, direction, durationMs);
979
+ }
980
+ if (!await this.isVisible(target))
981
+ throw new Error(`${describeSelector(toSelector(target))} not visible after ${times} swipes from ${describeSelector(toSelector(from))}`);
982
+ }
983
+ async scrollUntil(sel, { direction = "down", timeoutMs = 15000, speed = 40 } = {}) {
984
+ const s = toSelector(sel);
985
+ const deadline = this.now() + timeoutMs;
986
+ const durationMs = Math.round(1000 * (100 - speed) / 100) + 1;
987
+ for (;; ) {
988
+ if (await this.driver.findShown(s, true))
989
+ return;
990
+ if (this.now() > deadline)
991
+ throw new Error(`${describeSelector(s)} not scrolled into view within ${timeoutMs} ms`);
992
+ await this.swipe([50, 50], direction === "down" ? [50, 10] : [50, 90], durationMs);
993
+ }
994
+ }
995
+ async type(sel, value) {
996
+ await this.ready();
997
+ const el = await this.waitVisible(sel);
998
+ await this.driver.cmd("POST", `/element/${el}/value`, { text: value });
999
+ }
1000
+ acceptAlert(labels, waitMs) {
1001
+ return this.driver.platform.acceptAlert(this.driver, labels, waitMs);
1002
+ }
1003
+ openLink(url) {
1004
+ return this.driver.platform.openLink(this.driver, url, this.linkState);
1005
+ }
1006
+ relaunch() {
1007
+ this.pendingReady = null;
1008
+ return this.driver.relaunch();
1009
+ }
1010
+ activate() {
1011
+ return this.driver.activate();
1012
+ }
1013
+ async beginRun({ token, args = "", home }) {
1014
+ this.token = token;
1015
+ this.pendingReady = null;
1016
+ await this.relaunch();
1017
+ await this.waitVisible(home, 30000);
1018
+ return this.openLink(activationLink(this.scheme, token, args));
1019
+ }
1020
+ async endRun() {
1021
+ if (this.markers.some((m) => m.phase === "start"))
1022
+ await this.gc(true);
1023
+ await sendMarkers(this.fetch, this.api, this.markers, { sleep: (ms) => this.driver.sleep(ms), now: () => this.now() });
1024
+ await this.openLink(flushLink(this.scheme));
1025
+ const flush = this.driver.platform.flush;
1026
+ const deadline = this.now() + FLUSH_WAIT_MS;
1027
+ for (let poll = 1;this.now() < deadline; poll++) {
1028
+ let el = await this.driver.find(flush.lookup.using, flush.lookup.value);
1029
+ if (!el && flush.fallback && poll % FLUSH_FALLBACK_EVERY === 0)
1030
+ el = await this.findFallback(flush.fallback);
1031
+ if (el) {
1032
+ const label = await this.driver.cmd("GET", `/element/${el}/attribute/${flush.statusAttribute}`).catch(() => null);
1033
+ return label ?? "shown";
1034
+ }
1035
+ await this.driver.sleep(FLUSH_POLL_MS);
1036
+ }
1037
+ return null;
1038
+ }
1039
+ async findFallback(f) {
1040
+ await this.driver.cmd("POST", "/appium/settings", { settings: { [f.setting]: true } });
1041
+ try {
1042
+ return await this.driver.find(f.lookup.using, f.lookup.value);
1043
+ } finally {
1044
+ await this.driver.cmd("POST", "/appium/settings", { settings: { [f.setting]: false } }).catch(() => null);
1045
+ }
1046
+ }
1047
+ }
1048
+
1049
+ // packages/appium-driver/src/tokens.ts
1050
+ function parseTokensJs(text) {
1051
+ const pick = (name) => {
1052
+ const m = text.match(new RegExp(`output\\.${name}\\s*=\\s*(.+);\\s*$`, "m"));
1053
+ return m ? JSON.parse(m[1]) : undefined;
1054
+ };
1055
+ return { api: pick("perflabApi") ?? "", tokens: pick("perflabTokens") ?? {} };
1056
+ }
1057
+ function plan(journeys, repeat, tokens) {
1058
+ const out = [];
1059
+ for (let i = 0;i < repeat; i++) {
1060
+ for (const journey of journeys)
1061
+ out.push({ journey, iteration: i + 1, token: tokens[journey]?.[i] ?? "" });
1062
+ }
1063
+ return out;
1064
+ }
1065
+
1066
+ // packages/appium-driver/src/run.ts
1067
+ var realRunDeps = () => ({
1068
+ env: process.env,
1069
+ fetch,
1070
+ now: Date.now,
1071
+ sleep: realSleep,
1072
+ print: (l) => console.log(l),
1073
+ error: (l) => console.error(l)
1074
+ });
1075
+ var DRIVER_OPTIONS = {
1076
+ "journey-module": { type: "string" },
1077
+ app: { type: "string" },
1078
+ "app-custom-id": { type: "string", default: "" },
1079
+ "tokens-js": { type: "string" },
1080
+ journeys: { type: "string" },
1081
+ repeat: { type: "string", default: "1" },
1082
+ device: { type: "string" },
1083
+ "local-identifier": { type: "string" },
1084
+ "build-name": { type: "string", default: "perflab-appium" },
1085
+ "appium-version": { type: "string", default: "2.19.0" },
1086
+ out: { type: "string", default: "farm-run" },
1087
+ platform: { type: "string", default: "ios" },
1088
+ "action-gc": { type: "string", default: "off" },
1089
+ "end-gc": { type: "string", default: END_GC_DEFAULT ? "on" : "off" }
1090
+ };
1091
+ function parseDriverArgs(argv, module) {
1092
+ const values = parseArgs({ args: [...argv], options: { ...module.options ?? {}, ...DRIVER_OPTIONS } }).values;
1093
+ values.device ??= platformOf(String(values.platform)).defaultDevice;
1094
+ return values;
1095
+ }
1096
+ function journeyModulePath(argv) {
1097
+ return parseArgs({ args: [...argv], options: { "journey-module": { type: "string" } }, strict: false }).values["journey-module"];
1098
+ }
1099
+ async function runDriver(argv, module, deps = realRunDeps()) {
1100
+ const a = parseDriverArgs(argv, module);
1101
+ const user = deps.env.BROWSERSTACK_USERNAME;
1102
+ const key = deps.env.BROWSERSTACK_ACCESS_KEY;
1103
+ if (!user || !key)
1104
+ throw new Error("BROWSERSTACK_USERNAME / BROWSERSTACK_ACCESS_KEY are not set");
1105
+ if (!a.app || !a["tokens-js"] || !a.journeys)
1106
+ throw new Error("--app, --tokens-js and --journeys are required");
1107
+ const platform = platformOf(String(a.platform));
1108
+ const journeys = String(a.journeys).split(",").map((s) => s.trim()).filter(Boolean);
1109
+ const unknown = journeys.filter((j) => !Object.hasOwn(module.journeys, j));
1110
+ if (unknown.length)
1111
+ throw new Error(`the journey module has no journey ${unknown.map((j) => `"${j}"`).join(", ")} (it has ${Object.keys(module.journeys).join(", ") || "none"})`);
1112
+ const repeat = Number(a.repeat);
1113
+ if (!Number.isInteger(repeat) || repeat < 1)
1114
+ throw new Error("--repeat must be a whole number >= 1");
1115
+ if (a["action-gc"] !== "on" && a["action-gc"] !== "off")
1116
+ throw new Error("--action-gc must be on or off");
1117
+ if (a["end-gc"] !== "on" && a["end-gc"] !== "off")
1118
+ throw new Error("--end-gc must be on or off");
1119
+ const out = String(a.out);
1120
+ const auth = basicAuth(user, key);
1121
+ mkdirSync(out, { recursive: true });
1122
+ const { api, tokens } = parseTokensJs(readFileSync(String(a["tokens-js"]), "utf8"));
1123
+ const appUrl = await resolveApp(String(a.app), String(a["app-custom-id"] ?? ""), auth, deps.fetch, platform);
1124
+ deps.print(`app: ${appUrl}`);
1125
+ const log = [];
1126
+ const driver = new Driver({ hub: HUB, log, headers: { Authorization: auth }, bundleId: module.app.bundleId, platform, fetch: deps.fetch, now: deps.now, sleep: deps.sleep });
1127
+ const t = new Session({ driver, scheme: module.app.scheme, fetch: deps.fetch, api, actionGc: a["action-gc"] === "on", endGc: a["end-gc"] === "on" });
1128
+ const sessionName = module.sessionName ?? "perflab (appium)";
1129
+ const device = String(a.device);
1130
+ const ctx = { fetch: deps.fetch, tokens, journeys, repeat, args: a };
1131
+ const caps = sessionCaps({ appUrl, device, buildName: String(a["build-name"]), sessionName, appiumVersion: String(a["appium-version"]), platform, localIdentifier: a["local-identifier"] });
1132
+ const startedAt = deps.now();
1133
+ let error = null;
1134
+ const iterations = [];
1135
+ try {
1136
+ await driver.start(caps);
1137
+ deps.print(`session ${driver.sessionId}`);
1138
+ await driver.cmd("POST", "/appium/settings", { settings: platform.settings }).catch(() => null);
1139
+ await module.setup?.(t, ctx);
1140
+ await runIterations(t, module, ctx, iterations);
1141
+ } catch (e) {
1142
+ error = e instanceof Error ? e : new Error(String(e));
1143
+ deps.error(`::error::${error.message}`);
1144
+ if (driver.sessionId) {
1145
+ const source = await driver.cmd("GET", "/source").catch(() => null);
1146
+ if (typeof source === "string")
1147
+ writeFileSync(join(out, "failure-source.xml"), source);
1148
+ const png = await driver.cmd("GET", "/screenshot").catch(() => null);
1149
+ if (typeof png === "string")
1150
+ writeFileSync(join(out, "failure-screen.png"), Buffer.from(png, "base64"));
1151
+ }
1152
+ }
1153
+ if (driver.sessionId) {
1154
+ driver.context = { phase: "final-flush" };
1155
+ await t.endRun().catch(() => null);
1156
+ const reason = error ? error.message.slice(0, 250) : `${sessionName} done`;
1157
+ await driver.execute(`browserstack_executor: ${JSON.stringify({ action: "setSessionStatus", arguments: { status: error ? "failed" : "passed", reason } })}`).catch(() => null);
1158
+ }
1159
+ const endedAt = deps.now();
1160
+ await driver.end();
1161
+ const detail = driver.sessionId ? await sessionDetail(driver.sessionId, auth, deps.fetch) : {};
1162
+ const buildId = detail.build_hashed_id ?? null;
1163
+ writeFileSync(join(out, `browserstack.${String(a.platform)}.json`), `${JSON.stringify({
1164
+ driver: "appium",
1165
+ status: error ? "failed" : "passed",
1166
+ buildId,
1167
+ dashboard: detail.public_url ?? detail.browser_url ?? null,
1168
+ devices: [{ device, sessions: [{ id: driver.sessionId, ...error ? { error: { message: error.message } } : {} }] }]
1169
+ }, null, 2)}
1170
+ `);
1171
+ writeFileSync(join(out, "driver-log.json"), `${JSON.stringify({ driver: "appium", device, appUrl, session: driver.sessionId, buildId, linkMode: t.linkState.linkMode ?? null, actionGc: t.actionGc, endGc: t.endGc, linkError: t.linkState.linkError ?? null, startedAt, endedAt, total_ms: endedAt - startedAt, error: error?.message ?? null, iterations, commands: log }, null, 2)}
1172
+ `);
1173
+ deps.print(`driver-log.json: ${log.length} commands, ${iterations.length} iterations, ${Math.round((endedAt - startedAt) / 1000)} s`);
1174
+ for (const r of iterations)
1175
+ deps.print(` ${r.journey} #${r.iteration}: ${r.total_ms ?? "?"} ms, flushed=${r.flushed}`);
1176
+ return error ? 1 : 0;
1177
+ }
1178
+ async function runIterations(t, module, ctx, iterations) {
1179
+ const { driver } = t;
1180
+ for (const it of plan(ctx.journeys, ctx.repeat, ctx.tokens)) {
1181
+ const rec = { journey: it.journey, iteration: it.iteration, token: it.token !== "" };
1182
+ iterations.push(rec);
1183
+ driver.context = { journey: it.journey, iteration: it.iteration, phase: "begin" };
1184
+ const t0 = t.now();
1185
+ rec.alert = await t.beginRun({ token: it.token, home: module.home, ...module.sdkArgs ? { args: module.sdkArgs } : {} });
1186
+ const t1 = t.now();
1187
+ driver.context = { ...driver.context, phase: "journey" };
1188
+ const waitsBefore = t.readyWaits.length;
1189
+ await module.journeys[it.journey](t, { ...ctx, journey: it.journey, iteration: it.iteration });
1190
+ await t.ready();
1191
+ const t2 = t.now();
1192
+ t.marker("run:complete", "end");
1193
+ rec.steps = t.markers.filter((m) => m.phase === "start").length;
1194
+ const waits = t.readyWaits.slice(waitsBefore);
1195
+ rec.ready = { n: waits.length, ms: waits.reduce((x, y) => x + y, 0) };
1196
+ driver.context = { ...driver.context, phase: "end" };
1197
+ rec.flushed = await t.endRun();
1198
+ const t3 = t.now();
1199
+ Object.assign(rec, { t_start: t0, begin_ms: t1 - t0, journey_ms: t2 - t1, end_ms: t3 - t2, total_ms: t3 - t0 });
1200
+ }
1201
+ }
1202
+
1203
+ // packages/appium-driver/src/load.ts
1204
+ async function loadJourneyModule(path, cwd = process.cwd()) {
1205
+ const file = isAbsolute(path) ? path : resolve(cwd, path);
1206
+ const mod = await import(pathToFileURL(file).href);
1207
+ const m = mod.default ?? mod;
1208
+ if (!m.app?.bundleId || !m.app.scheme)
1209
+ throw new Error(`${path}: the journey module must export app: { bundleId, scheme }`);
1210
+ if (m.home === undefined)
1211
+ throw new Error(`${path}: the journey module must export home (the selector visible after launch)`);
1212
+ if (!m.journeys || Object.keys(m.journeys).length === 0)
1213
+ throw new Error(`${path}: the journey module must export journeys: { <name>: async (t, ctx) => {...} }`);
1214
+ return m;
1215
+ }
1216
+ async function main(argv) {
1217
+ const path = journeyModulePath(argv);
1218
+ if (!path)
1219
+ throw new Error("--journey-module <file> is required");
1220
+ return runDriver(argv, await loadJourneyModule(path));
1221
+ }
1222
+
1223
+ // packages/appium-driver/src/cli.ts
1224
+ main(process.argv.slice(2)).then((code) => process.exit(code), (e) => {
1225
+ console.error(`::error::${e instanceof Error ? e.message : String(e)}`);
1226
+ process.exit(1);
1227
+ });