prowl-tools 0.1.7 → 0.1.9

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.
@@ -2,383 +2,13 @@ import {
2
2
  SUPPORTED_BROWSER_ENGINES,
3
3
  ensureAllowedDomain,
4
4
  huntSchema,
5
+ interpolateHunt,
5
6
  listHunts,
6
7
  loadConfig,
7
8
  loadHunt,
8
9
  loadHuntTags,
9
10
  resolveViewport
10
- } from "./chunk-O3OUTZ2P.js";
11
-
12
- // src/config/interpolate.ts
13
- import crypto from "crypto";
14
- var VAR_PATTERN = /\{\{([A-Z0-9_]+)\}\}/g;
15
- function collectInterpolatedValues(input, vars, values) {
16
- if (typeof input === "string") {
17
- for (const match of input.matchAll(VAR_PATTERN)) {
18
- const varValue = vars[match[1]];
19
- if (varValue) values.add(varValue);
20
- }
21
- return;
22
- }
23
- if (Array.isArray(input)) {
24
- for (const item of input) {
25
- collectInterpolatedValues(item, vars, values);
26
- }
27
- return;
28
- }
29
- if (input && typeof input === "object") {
30
- for (const [key, value] of Object.entries(input)) {
31
- collectInterpolatedValues(key, vars, values);
32
- collectInterpolatedValues(value, vars, values);
33
- }
34
- }
35
- }
36
- function interpolateString(input, vars) {
37
- const usedVars = [];
38
- const value = input.replace(VAR_PATTERN, (_, name) => {
39
- const varValue = vars[name];
40
- if (varValue === void 0) {
41
- throw new Error(`Missing variable: ${name}`);
42
- }
43
- usedVars.push(name);
44
- return varValue;
45
- });
46
- return { value, usedVars };
47
- }
48
- var RANDOM_FIRST_NAMES = ["Alex", "Jordan", "Morgan", "Taylor", "Casey", "Riley", "Quinn", "Avery"];
49
- var RANDOM_LAST_NAMES = ["Smith", "Johnson", "Brown", "Davis", "Wilson", "Clark", "Hall", "Young"];
50
- function generateRandomVars(randomSource) {
51
- const random = randomSource?.random ?? Math.random;
52
- const randomBytes = randomSource?.randomBytes ?? crypto.randomBytes;
53
- const randomUUID = randomSource?.randomUUID ?? crypto.randomUUID;
54
- const hex = randomBytes(4).toString("hex");
55
- const firstIndex = Math.floor(random() * RANDOM_FIRST_NAMES.length);
56
- const lastIndex = Math.floor(random() * RANDOM_LAST_NAMES.length);
57
- const num2 = Math.floor(random() * 9e3) + 1e3;
58
- const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
59
- let text = "";
60
- for (let i = 0; i < 8; i++) {
61
- text += chars[Math.floor(random() * chars.length)];
62
- }
63
- return {
64
- RANDOM_EMAIL: `prowl_${hex}@test.com`,
65
- RANDOM_NAME: `${RANDOM_FIRST_NAMES[firstIndex]} ${RANDOM_LAST_NAMES[lastIndex]}`,
66
- RANDOM_NUMBER: String(num2),
67
- RANDOM_UUID: randomUUID(),
68
- RANDOM_TEXT: text
69
- };
70
- }
71
- function interpolateStep(step, vars, stepPath2, redacted) {
72
- const isExplicitFill = (value) => typeof value.selector === "string" && typeof value.value === "string";
73
- const interpolateSinglePair = (record) => {
74
- const entries = Object.entries(record);
75
- if (entries.length !== 1) {
76
- throw new Error("Shorthand step expects exactly one key-value pair");
77
- }
78
- const [key, value] = entries[0];
79
- return {
80
- [interpolateString(key, vars).value]: interpolateString(value, vars).value
81
- };
82
- };
83
- if ("navigate" in step) {
84
- const result = interpolateString(step.navigate, vars);
85
- return { navigate: result.value };
86
- }
87
- if ("click" in step) {
88
- if (typeof step.click === "string") {
89
- return { click: interpolateString(step.click, vars).value };
90
- }
91
- const result = interpolateString(step.click.selector, vars);
92
- return { click: { selector: result.value } };
93
- }
94
- if ("fill" in step) {
95
- if (isExplicitFill(step.fill)) {
96
- const selectorResult = interpolateString(step.fill.selector, vars);
97
- const valueResult2 = interpolateString(step.fill.value, vars);
98
- if (valueResult2.usedVars.length > 0) {
99
- redacted.add(stepPath2);
100
- }
101
- return { fill: { selector: selectorResult.value, value: valueResult2.value } };
102
- }
103
- const [rawLabel, rawValue] = Object.entries(step.fill)[0] ?? [];
104
- if (rawLabel === void 0 || rawValue === void 0) {
105
- throw new Error("Shorthand fill expects exactly one key-value pair");
106
- }
107
- const labelResult = interpolateString(rawLabel, vars);
108
- const valueResult = interpolateString(rawValue, vars);
109
- if (valueResult.usedVars.length > 0) {
110
- redacted.add(stepPath2);
111
- }
112
- return {
113
- fill: {
114
- [labelResult.value]: valueResult.value
115
- }
116
- };
117
- }
118
- if ("type" in step) {
119
- const valueResult = interpolateString(step.type, vars);
120
- if (valueResult.usedVars.length > 0) {
121
- redacted.add(stepPath2);
122
- }
123
- return { type: valueResult.value };
124
- }
125
- if ("selectOption" in step) {
126
- const selectorResult = interpolateString(step.selectOption.selector, vars);
127
- const valueResult = interpolateString(step.selectOption.value, vars);
128
- return { selectOption: { selector: selectorResult.value, value: valueResult.value } };
129
- }
130
- if ("select" in step) {
131
- return { select: interpolateSinglePair(step.select) };
132
- }
133
- if ("press" in step) {
134
- const selectorResult = interpolateString(step.press.selector, vars);
135
- const keyResult = interpolateString(step.press.key, vars);
136
- return { press: { selector: selectorResult.value, key: keyResult.value } };
137
- }
138
- if ("onDialog" in step) {
139
- return { onDialog: { action: step.onDialog.action } };
140
- }
141
- if ("setInputFiles" in step) {
142
- const selectorResult = interpolateString(step.setInputFiles.selector, vars);
143
- const rawFiles = step.setInputFiles.files;
144
- const files = Array.isArray(rawFiles) ? rawFiles.map((f) => interpolateString(f, vars).value) : interpolateString(rawFiles, vars).value;
145
- return { setInputFiles: { selector: selectorResult.value, files } };
146
- }
147
- if ("runHunt" in step) {
148
- if (typeof step.runHunt === "string") {
149
- return { runHunt: interpolateString(step.runHunt, vars).value };
150
- }
151
- const nameResult = interpolateString(step.runHunt.name, vars);
152
- const interpolatedVars = {};
153
- for (const [key, value] of Object.entries(step.runHunt.vars ?? {})) {
154
- interpolatedVars[key] = interpolateString(value, vars).value;
155
- }
156
- return {
157
- runHunt: {
158
- name: nameResult.value,
159
- ...Object.keys(interpolatedVars).length > 0 ? { vars: interpolatedVars } : {}
160
- }
161
- };
162
- }
163
- if ("assert" in step) {
164
- if (step.assert.visible !== void 0) {
165
- return { assert: { visible: interpolateString(step.assert.visible, vars).value } };
166
- }
167
- if (step.assert.notVisible !== void 0) {
168
- return { assert: { notVisible: interpolateString(step.assert.notVisible, vars).value } };
169
- }
170
- if (step.assert.urlIncludes !== void 0) {
171
- return { assert: { urlIncludes: interpolateString(step.assert.urlIncludes, vars).value } };
172
- }
173
- if (step.assert.urlEquals !== void 0) {
174
- return { assert: { urlEquals: interpolateString(step.assert.urlEquals, vars).value } };
175
- }
176
- return step;
177
- }
178
- if ("wait" in step) {
179
- if (typeof step.wait === "string") {
180
- return { wait: interpolateString(step.wait, vars).value };
181
- }
182
- return {
183
- wait: {
184
- for: interpolateString(step.wait.for, vars).value,
185
- timeout: step.wait.timeout
186
- }
187
- };
188
- }
189
- if ("waitForSelector" in step) {
190
- const selectorResult = interpolateString(step.waitForSelector.selector, vars);
191
- return {
192
- waitForSelector: {
193
- selector: selectorResult.value,
194
- timeout: step.waitForSelector.timeout
195
- }
196
- };
197
- }
198
- if ("waitForUrl" in step) {
199
- const valueResult = interpolateString(step.waitForUrl.value, vars);
200
- return {
201
- waitForUrl: {
202
- value: valueResult.value,
203
- timeout: step.waitForUrl.timeout
204
- }
205
- };
206
- }
207
- if ("waitForNetworkIdle" in step) {
208
- return { waitForNetworkIdle: { timeout: step.waitForNetworkIdle.timeout } };
209
- }
210
- if ("hover" in step) {
211
- const selectorResult = interpolateString(step.hover.selector, vars);
212
- return { hover: { selector: selectorResult.value } };
213
- }
214
- if ("scroll" in step) {
215
- return { scroll: { direction: step.scroll.direction, amount: step.scroll.amount } };
216
- }
217
- if ("scrollTo" in step) {
218
- const selectorResult = interpolateString(step.scrollTo.selector, vars);
219
- return { scrollTo: { selector: selectorResult.value } };
220
- }
221
- if ("screenshot" in step) {
222
- return { screenshot: { name: step.screenshot.name } };
223
- }
224
- if ("if" in step) {
225
- const condition = step.if;
226
- const thenSteps = condition.then.map(
227
- (s, i) => interpolateStep(s, vars, `${stepPath2}.if.then.${i}`, redacted)
228
- );
229
- const elseSteps = condition.else?.map(
230
- (s, i) => interpolateStep(s, vars, `${stepPath2}.if.else.${i}`, redacted)
231
- );
232
- return {
233
- if: {
234
- ...condition.visible !== void 0 ? { visible: interpolateString(condition.visible, vars).value } : {},
235
- ...condition.notVisible !== void 0 ? { notVisible: interpolateString(condition.notVisible, vars).value } : {},
236
- then: thenSteps,
237
- ...elseSteps !== void 0 ? { else: elseSteps } : {}
238
- }
239
- };
240
- }
241
- if ("repeat" in step) {
242
- const repeat = step.repeat;
243
- const subSteps = repeat.steps.map(
244
- (s, i) => interpolateStep(s, vars, `${stepPath2}.repeat.steps.${i}`, redacted)
245
- );
246
- return {
247
- repeat: {
248
- ...repeat.times !== void 0 ? { times: repeat.times } : {},
249
- ...repeat.while !== void 0 ? {
250
- while: {
251
- ...repeat.while.visible !== void 0 ? { visible: interpolateString(repeat.while.visible, vars).value } : {},
252
- ...repeat.while.notVisible !== void 0 ? { notVisible: interpolateString(repeat.while.notVisible, vars).value } : {}
253
- }
254
- } : {},
255
- ...repeat.maxIterations !== void 0 ? { maxIterations: repeat.maxIterations } : {},
256
- steps: subSteps
257
- }
258
- };
259
- }
260
- if ("mockRoute" in step) {
261
- const mock = step.mockRoute;
262
- return {
263
- mockRoute: {
264
- url: interpolateString(mock.url, vars).value,
265
- response: {
266
- status: mock.response.status,
267
- ...mock.response.contentType !== void 0 ? { contentType: interpolateString(mock.response.contentType, vars).value } : {},
268
- ...mock.response.body !== void 0 ? { body: interpolateString(mock.response.body, vars).value } : {},
269
- ...mock.response.file !== void 0 ? { file: interpolateString(mock.response.file, vars).value } : {}
270
- }
271
- }
272
- };
273
- }
274
- if ("unmockRoute" in step) {
275
- if (typeof step.unmockRoute === "string") {
276
- return { unmockRoute: interpolateString(step.unmockRoute, vars).value };
277
- }
278
- return {
279
- unmockRoute: { url: interpolateString(step.unmockRoute.url, vars).value }
280
- };
281
- }
282
- if ("evalScript" in step) {
283
- if (typeof step.evalScript === "string") {
284
- return { evalScript: interpolateString(step.evalScript, vars).value };
285
- }
286
- return {
287
- evalScript: {
288
- expression: interpolateString(step.evalScript.expression, vars).value,
289
- ...step.evalScript.as !== void 0 ? { as: step.evalScript.as } : {}
290
- }
291
- };
292
- }
293
- if ("runScript" in step) {
294
- return {
295
- runScript: { file: interpolateString(step.runScript.file, vars).value }
296
- };
297
- }
298
- if ("assertScreenshot" in step) {
299
- return {
300
- assertScreenshot: {
301
- name: interpolateString(step.assertScreenshot.name, vars).value,
302
- ...step.assertScreenshot.threshold !== void 0 ? { threshold: step.assertScreenshot.threshold } : {}
303
- }
304
- };
305
- }
306
- if ("assertWithAI" in step) {
307
- return {
308
- assertWithAI: interpolateString(step.assertWithAI, vars).value
309
- };
310
- }
311
- if ("copyText" in step) {
312
- return {
313
- copyText: {
314
- selector: interpolateString(step.copyText.selector, vars).value,
315
- as: step.copyText.as
316
- }
317
- };
318
- }
319
- if ("waitForDownload" in step) {
320
- if (step.waitForDownload === null) {
321
- return { waitForDownload: null };
322
- }
323
- return {
324
- waitForDownload: {
325
- ...step.waitForDownload.filename !== void 0 ? { filename: interpolateString(step.waitForDownload.filename, vars).value } : {},
326
- ...step.waitForDownload.timeout !== void 0 ? { timeout: step.waitForDownload.timeout } : {}
327
- }
328
- };
329
- }
330
- return step;
331
- }
332
- function interpolateAssertion(assertion, vars) {
333
- if ("selectorExists" in assertion) {
334
- return { selectorExists: interpolateString(assertion.selectorExists, vars).value };
335
- }
336
- if ("selectorNotExists" in assertion) {
337
- return { selectorNotExists: interpolateString(assertion.selectorNotExists, vars).value };
338
- }
339
- if ("urlIncludes" in assertion) {
340
- return { urlIncludes: interpolateString(assertion.urlIncludes, vars).value };
341
- }
342
- if ("urlEquals" in assertion) {
343
- return { urlEquals: interpolateString(assertion.urlEquals, vars).value };
344
- }
345
- if ("noConsoleErrors" in assertion) {
346
- return { noConsoleErrors: assertion.noConsoleErrors };
347
- }
348
- if ("noNetworkErrors" in assertion) {
349
- return { noNetworkErrors: assertion.noNetworkErrors };
350
- }
351
- return assertion;
352
- }
353
- function interpolateHunt(hunt, env, randomVars = generateRandomVars()) {
354
- const redactedFillSteps = /* @__PURE__ */ new Set();
355
- const envVars = Object.fromEntries(
356
- Object.entries(env).filter(([, value]) => value !== void 0)
357
- );
358
- const baseVars = { ...randomVars, ...envVars };
359
- const resolvedHuntVars = {};
360
- for (const [key, value] of Object.entries(hunt.vars ?? {})) {
361
- resolvedHuntVars[key] = interpolateString(value, baseVars).value;
362
- }
363
- const vars = { ...baseVars, ...resolvedHuntVars };
364
- const redactionValues = /* @__PURE__ */ new Set();
365
- collectInterpolatedValues(hunt.steps, vars, redactionValues);
366
- collectInterpolatedValues(hunt.assertions, vars, redactionValues);
367
- const steps = hunt.steps.map(
368
- (step, index) => interpolateStep(step, vars, `${index}`, redactedFillSteps)
369
- );
370
- const assertions = hunt.assertions?.map((assertion) => interpolateAssertion(assertion, vars));
371
- return {
372
- hunt: {
373
- ...hunt,
374
- steps,
375
- assertions
376
- },
377
- redactedFillSteps,
378
- randomVars,
379
- redactionValues: [...redactionValues]
380
- };
381
- }
11
+ } from "./chunk-JFJQNJSJ.js";
382
12
 
383
13
  // src/config/target.ts
384
14
  import { execFileSync } from "child_process";
@@ -396,10 +26,17 @@ var WEB_ONLY_STEP_TYPES = /* @__PURE__ */ new Set([
396
26
  "select",
397
27
  "selectOption",
398
28
  "setInputFiles",
399
- "waitForDownload",
400
- "scroll"
401
- // directional scroll runs window.scrollBy (evaluate) — use scrollTo instead
29
+ "waitForDownload"
402
30
  ]);
31
+ var MACOS_UNSUPPORTED_STEP_TYPES = /* @__PURE__ */ new Set(["scroll"]);
32
+ function macosUnsupportedReason(step) {
33
+ for (const type of MACOS_UNSUPPORTED_STEP_TYPES) {
34
+ if (type in step) {
35
+ return type;
36
+ }
37
+ }
38
+ return null;
39
+ }
403
40
  function webOnlyReason(step) {
404
41
  for (const type of WEB_ONLY_STEP_TYPES) {
405
42
  if (type in step) {
@@ -435,6 +72,14 @@ function assertStepsSupportedByTarget(steps, target) {
435
72
  `Step "${reason}" is not supported by the ${label} target. It is web-only; use a portable step (click, fill, type, press, wait, assert visible, screenshot, etc.).`
436
73
  );
437
74
  }
75
+ if (target === "macos") {
76
+ const macReason = macosUnsupportedReason(step);
77
+ if (macReason) {
78
+ throw new Error(
79
+ `Step "${macReason}" is not supported by the macOS target. Directional scroll is a touch swipe available on the iOS and Android targets; there is no macOS accessibility equivalent (use scrollTo to bring a specific element into view instead).`
80
+ );
81
+ }
82
+ }
438
83
  if ("if" in step) {
439
84
  assertStepsSupportedByTarget(step.if.then, target);
440
85
  if (step.if.else) {
@@ -689,6 +334,9 @@ function createMacDriver(client, options = {}) {
689
334
  async hover(selector) {
690
335
  await query("hover", selector);
691
336
  },
337
+ scroll() {
338
+ return rejectUnsupported("scroll");
339
+ },
692
340
  async scrollIntoView(selector) {
693
341
  await query("scrollTo", selector);
694
342
  },
@@ -851,10 +499,15 @@ ${macdriverBuildInstructions()}`
851
499
  ${macdriverBuildInstructions()}`);
852
500
  }
853
501
  var DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
502
+ function errorMessage(error) {
503
+ return error instanceof Error ? error.message : String(error);
504
+ }
854
505
  var SpawnMacHelperClient = class {
855
506
  child;
856
507
  pending = /* @__PURE__ */ new Map();
857
508
  requestTimeoutMs;
509
+ onEvent;
510
+ onEventError;
858
511
  stdoutBuffer = "";
859
512
  stderrBuffer = "";
860
513
  nextId = 1;
@@ -862,6 +515,9 @@ var SpawnMacHelperClient = class {
862
515
  terminalError;
863
516
  constructor(binaryPath, options = {}) {
864
517
  this.requestTimeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
518
+ this.onEvent = options.onEvent;
519
+ this.onEventError = options.onEventError ?? ((message) => process.stderr.write(`${message}
520
+ `));
865
521
  this.child = spawn(binaryPath, ["serve"], { stdio: ["pipe", "pipe", "pipe"] });
866
522
  this.child.stdout?.setEncoding("utf-8");
867
523
  this.child.stderr?.setEncoding("utf-8");
@@ -898,6 +554,10 @@ var SpawnMacHelperClient = class {
898
554
  } catch {
899
555
  return;
900
556
  }
557
+ if (typeof message.event === "string" && typeof message.id !== "number") {
558
+ this.handleEvent(message);
559
+ return;
560
+ }
901
561
  const id = typeof message.id === "number" ? message.id : void 0;
902
562
  if (id === void 0) {
903
563
  return;
@@ -914,6 +574,18 @@ var SpawnMacHelperClient = class {
914
574
  pending.reject(new Error(typeof message.error === "string" ? message.error : "prowl-macdriver error"));
915
575
  }
916
576
  }
577
+ handleEvent(event) {
578
+ try {
579
+ this.onEvent?.(event);
580
+ } catch (error) {
581
+ try {
582
+ this.onEventError(
583
+ `prowl-macdriver event sink failed for event "${event.event}": ${errorMessage(error)}`
584
+ );
585
+ } catch {
586
+ }
587
+ }
588
+ }
917
589
  failAll(error) {
918
590
  for (const pending of this.pending.values()) {
919
591
  clearTimeout(pending.timer);
@@ -1295,6 +967,103 @@ function parseSnapshot(xml) {
1295
967
 
1296
968
  // src/browser/android-driver.ts
1297
969
  import fs3 from "fs";
970
+
971
+ // src/browser/touch-gestures.ts
972
+ var DEFAULT_SWIPE_FRACTION = 0.75;
973
+ var SCROLL_TO_PROBE_SWIPE_FRACTION = 0.6;
974
+ var MAX_SWIPE_FRACTION = 0.9;
975
+ var SWIPE_HOLD_MS = 100;
976
+ var SWIPE_MOVE_DURATION_MS = 300;
977
+ var SCROLL_TO_SWEEP_DEPTH = 10;
978
+ var SCROLL_TO_PROBE_DIRECTIONS = [
979
+ ...Array.from({ length: SCROLL_TO_SWEEP_DEPTH }, () => "down"),
980
+ ...Array.from({ length: SCROLL_TO_SWEEP_DEPTH * 2 }, () => "up")
981
+ ];
982
+ var MAX_SCROLL_TO_SWIPES = SCROLL_TO_PROBE_DIRECTIONS.length;
983
+ var OPPOSITE_SWIPE_DIRECTIONS = {
984
+ up: "down",
985
+ down: "up",
986
+ left: "right",
987
+ right: "left"
988
+ };
989
+ async function probeScrollIntoView({
990
+ isVisible,
991
+ swipe,
992
+ directions = SCROLL_TO_PROBE_DIRECTIONS
993
+ }) {
994
+ if (await isVisible()) {
995
+ return true;
996
+ }
997
+ for (const direction of directions) {
998
+ await swipe(direction);
999
+ if (await isVisible()) {
1000
+ return true;
1001
+ }
1002
+ }
1003
+ return false;
1004
+ }
1005
+ function toScreenSize(value, source) {
1006
+ const record = value ?? {};
1007
+ const width = Number(record.width);
1008
+ const height = Number(record.height);
1009
+ if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) {
1010
+ throw new Error(`${source} did not return a usable screen size`);
1011
+ }
1012
+ return { width, height };
1013
+ }
1014
+ function isVertical(direction) {
1015
+ return direction === "up" || direction === "down";
1016
+ }
1017
+ function swipeDistanceFor(direction, size, amount) {
1018
+ if (amount !== void 0 && !Number.isFinite(amount)) {
1019
+ throw new Error("scroll amount must be a finite number");
1020
+ }
1021
+ const axis = isVertical(direction) ? size.height : size.width;
1022
+ const requested = amount === void 0 ? axis * DEFAULT_SWIPE_FRACTION : Math.abs(amount);
1023
+ const max = axis * MAX_SWIPE_FRACTION;
1024
+ return Math.max(1, Math.round(Math.min(requested, max)));
1025
+ }
1026
+ function scrollToProbeDistanceFor(direction, size) {
1027
+ const axis = isVertical(direction) ? size.height : size.width;
1028
+ return Math.max(1, Math.round(axis * SCROLL_TO_PROBE_SWIPE_FRACTION));
1029
+ }
1030
+ function swipeEndpoints(direction, size, distance) {
1031
+ const cx = Math.round(size.width / 2);
1032
+ const cy = Math.round(size.height / 2);
1033
+ const half = Math.round(distance / 2);
1034
+ switch (direction) {
1035
+ case "down":
1036
+ return { start: { x: cx, y: cy + half }, end: { x: cx, y: cy - half } };
1037
+ case "up":
1038
+ return { start: { x: cx, y: cy - half }, end: { x: cx, y: cy + half } };
1039
+ case "right":
1040
+ return { start: { x: cx + half, y: cy }, end: { x: cx - half, y: cy } };
1041
+ case "left":
1042
+ return { start: { x: cx - half, y: cy }, end: { x: cx + half, y: cy } };
1043
+ }
1044
+ }
1045
+ function buildSwipeActions(start, end) {
1046
+ return {
1047
+ type: "pointer",
1048
+ id: "finger1",
1049
+ parameters: { pointerType: "touch" },
1050
+ actions: [
1051
+ { type: "pointerMove", duration: 0, x: start.x, y: start.y, origin: "viewport" },
1052
+ { type: "pointerDown", button: 0 },
1053
+ { type: "pause", duration: SWIPE_HOLD_MS },
1054
+ { type: "pointerMove", duration: SWIPE_MOVE_DURATION_MS, x: end.x, y: end.y, origin: "viewport" },
1055
+ { type: "pointerUp", button: 0 }
1056
+ ]
1057
+ };
1058
+ }
1059
+ function buildDirectionalSwipe(direction, size, amount) {
1060
+ const normalizedDirection = amount !== void 0 && amount < 0 ? OPPOSITE_SWIPE_DIRECTIONS[direction] : direction;
1061
+ const distance = swipeDistanceFor(normalizedDirection, size, amount);
1062
+ const { start, end } = swipeEndpoints(normalizedDirection, size, distance);
1063
+ return { actions: buildSwipeActions(start, end), distance, start, end };
1064
+ }
1065
+
1066
+ // src/browser/android-driver.ts
1298
1067
  var ANDROID_CAPABILITIES = /* @__PURE__ */ new Set([
1299
1068
  "query",
1300
1069
  "interact",
@@ -1409,6 +1178,11 @@ function createAndroidDriver(client, options = {}) {
1409
1178
  async function fillSelector(selector, value) {
1410
1179
  await client.setValue(await resolveOne(selector), value);
1411
1180
  }
1181
+ async function swipe(direction, amount, size) {
1182
+ const actualSize = size ?? await client.windowSize();
1183
+ const { actions } = buildDirectionalSwipe(direction, actualSize, amount);
1184
+ await client.performActions(actions);
1185
+ }
1412
1186
  return {
1413
1187
  capabilities: ANDROID_CAPABILITIES,
1414
1188
  // navigation -----------------------------------------------------------
@@ -1446,8 +1220,31 @@ function createAndroidDriver(client, options = {}) {
1446
1220
  hover() {
1447
1221
  return rejectUnsupported("hover");
1448
1222
  },
1449
- scrollIntoView() {
1450
- return rejectUnsupported("scrollTo");
1223
+ // Screen-centred swipe via the W3C actions endpoint (PROWL-080). Direction
1224
+ // semantics match the web step: scrolling "down" reveals lower content, so
1225
+ // the finger drags up. See ./touch-gestures.ts.
1226
+ async scroll(direction, amount) {
1227
+ await swipe(direction, amount);
1228
+ },
1229
+ // Resolve the element, short-circuiting if it is already present in the
1230
+ // hierarchy; otherwise use the shared bounded down/up mobile probe before
1231
+ // failing with a message naming the selector and attempts.
1232
+ async scrollIntoView(selector) {
1233
+ const query = parseAndroidSelector(selector);
1234
+ let probeSize;
1235
+ const found = await probeScrollIntoView({
1236
+ isVisible: async () => (await client.findElements(query)).length > 0,
1237
+ swipe: async (direction) => {
1238
+ probeSize ??= await client.windowSize();
1239
+ await swipe(direction, scrollToProbeDistanceFor(direction, probeSize), probeSize);
1240
+ }
1241
+ });
1242
+ if (found) {
1243
+ return;
1244
+ }
1245
+ throw new Error(
1246
+ `scrollTo: element "${selector}" not visible after ${MAX_SCROLL_TO_SWIPES} scroll attempts on the Android target`
1247
+ );
1451
1248
  },
1452
1249
  setInputFiles() {
1453
1250
  return rejectUnsupported("setInputFiles");
@@ -1919,6 +1716,13 @@ function createUia2AgentClient(transport, sessionId, options = {}) {
1919
1716
  async pressKeyCode(keyCode) {
1920
1717
  await transport.request("POST", `${base}/appium/device/press_keycode`, { keycode: keyCode });
1921
1718
  },
1719
+ async windowSize() {
1720
+ const value = await transport.request("GET", `${base}/window/current/size`);
1721
+ return toScreenSize(value, "uiautomator2 /window/current/size");
1722
+ },
1723
+ async performActions(actions) {
1724
+ await transport.request("POST", `${base}/actions`, { actions: [actions] });
1725
+ },
1922
1726
  async screenshotPng() {
1923
1727
  const value = await transport.request("GET", `${base}/screenshot`);
1924
1728
  if (typeof value !== "string") {
@@ -2066,6 +1870,7 @@ async function launchAndroidSession(options) {
2066
1870
  readyDeadlineMs,
2067
1871
  appPackage: pkg
2068
1872
  });
1873
+ await launchPackage(runner, serial, pkg);
2069
1874
  const driver = createAndroidDriver(client, { appLabel: pkg });
2070
1875
  return { client, driver, package: pkg, serial, teardown };
2071
1876
  } catch (error) {
@@ -2086,6 +1891,7 @@ var IOS_CAPABILITIES = /* @__PURE__ */ new Set([
2086
1891
  ]);
2087
1892
  var WAIT_POLL_INTERVAL_MS2 = 250;
2088
1893
  var DEFAULT_WAIT_TIMEOUT_MS2 = 5e3;
1894
+ var DISPLAYED_CHECK_CONCURRENCY = 4;
2089
1895
  var IOS_PRESS_KEYS = ["backspace", "del", "delete", "enter", "home", "return"];
2090
1896
  function escapePredicateArg(value) {
2091
1897
  return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
@@ -2160,6 +1966,64 @@ function createIosDriver(client, options) {
2160
1966
  async function fillSelector(selector, value) {
2161
1967
  await client.setValue(await resolveOne(selector), value);
2162
1968
  }
1969
+ async function swipe(direction, amount, size) {
1970
+ const actualSize = size ?? await client.windowSize();
1971
+ const { actions } = buildDirectionalSwipe(direction, actualSize, amount);
1972
+ await client.performActions(actions);
1973
+ }
1974
+ async function visibleElementIds(query) {
1975
+ const ids = await client.findElements(query);
1976
+ const visible = Array.from({ length: ids.length }, () => null);
1977
+ let nextIndex = 0;
1978
+ const workerCount = Math.min(DISPLAYED_CHECK_CONCURRENCY, ids.length);
1979
+ await Promise.all(
1980
+ Array.from({ length: workerCount }, async () => {
1981
+ for (; ; ) {
1982
+ const index = nextIndex;
1983
+ nextIndex += 1;
1984
+ if (index >= ids.length) {
1985
+ return;
1986
+ }
1987
+ const id = ids[index];
1988
+ if (await client.isDisplayed(id)) {
1989
+ visible[index] = id;
1990
+ }
1991
+ }
1992
+ })
1993
+ );
1994
+ return visible.filter((id) => id !== null);
1995
+ }
1996
+ async function hasVisibleElement(query) {
1997
+ const ids = await client.findElements(query);
1998
+ if (ids.length === 0) {
1999
+ return false;
2000
+ }
2001
+ if (await client.isDisplayed(ids[0])) {
2002
+ return true;
2003
+ }
2004
+ let found = false;
2005
+ let nextIndex = 1;
2006
+ const workerCount = Math.min(DISPLAYED_CHECK_CONCURRENCY, ids.length - 1);
2007
+ await Promise.all(
2008
+ Array.from({ length: workerCount }, async () => {
2009
+ for (; ; ) {
2010
+ if (found) {
2011
+ return;
2012
+ }
2013
+ const index = nextIndex;
2014
+ nextIndex += 1;
2015
+ if (index >= ids.length) {
2016
+ return;
2017
+ }
2018
+ if (await client.isDisplayed(ids[index])) {
2019
+ found = true;
2020
+ return;
2021
+ }
2022
+ }
2023
+ })
2024
+ );
2025
+ return found;
2026
+ }
2163
2027
  async function pressKey(key) {
2164
2028
  const name = key.trim().toLowerCase();
2165
2029
  if (name === "enter" || name === "return") {
@@ -2191,6 +2055,9 @@ function createIosDriver(client, options) {
2191
2055
  async count(selector) {
2192
2056
  return (await client.findElements(parseIosSelector(selector))).length;
2193
2057
  },
2058
+ async visibleCount(selector) {
2059
+ return (await visibleElementIds(parseIosSelector(selector))).length;
2060
+ },
2194
2061
  async textContent(selector) {
2195
2062
  const id = await client.findElement(parseIosSelector(selector));
2196
2063
  if (id === null) {
@@ -2215,15 +2082,38 @@ function createIosDriver(client, options) {
2215
2082
  hover() {
2216
2083
  return rejectUnsupported("hover");
2217
2084
  },
2218
- scrollIntoView() {
2219
- return rejectUnsupported("scrollTo");
2085
+ // Screen-centred swipe via the W3C actions endpoint (PROWL-080). Direction
2086
+ // semantics match the web step: scrolling "down" reveals lower content, so
2087
+ // the finger drags up. See ./touch-gestures.ts.
2088
+ async scroll(direction, amount) {
2089
+ await swipe(direction, amount);
2090
+ },
2091
+ // Resolve the element, short-circuiting only if WDA reports a matching
2092
+ // element displayed in the viewport; hierarchy-only matches can be offscreen.
2093
+ // Otherwise use the shared bounded down/up mobile probe before failing.
2094
+ async scrollIntoView(selector) {
2095
+ const query = parseIosSelector(selector);
2096
+ let probeSize;
2097
+ const found = await probeScrollIntoView({
2098
+ isVisible: () => hasVisibleElement(query),
2099
+ swipe: async (direction) => {
2100
+ probeSize ??= await client.windowSize();
2101
+ await swipe(direction, scrollToProbeDistanceFor(direction, probeSize), probeSize);
2102
+ }
2103
+ });
2104
+ if (found) {
2105
+ return;
2106
+ }
2107
+ throw new Error(
2108
+ `scrollTo: element "${selector}" not visible after ${MAX_SCROLL_TO_SWIPES} scroll attempts on the iOS target`
2109
+ );
2220
2110
  },
2221
2111
  setInputFiles() {
2222
2112
  return rejectUnsupported("setInputFiles");
2223
2113
  },
2224
2114
  // semantic locators ----------------------------------------------------
2225
2115
  async countByRole(role, name) {
2226
- return (await client.findElements({ by: "role", role, name })).length;
2116
+ return (await visibleElementIds({ by: "role", role, name })).length;
2227
2117
  },
2228
2118
  async clickFirstByRole(role, name) {
2229
2119
  const id = await client.findElement({ by: "role", role, name });
@@ -2233,7 +2123,7 @@ function createIosDriver(client, options) {
2233
2123
  await client.click(id);
2234
2124
  },
2235
2125
  async countByLabel(label) {
2236
- return (await client.findElements({ by: "label", value: label })).length;
2126
+ return (await visibleElementIds({ by: "label", value: label })).length;
2237
2127
  },
2238
2128
  async fillFirstByLabel(label, value) {
2239
2129
  const id = await client.findElement({ by: "label", value: label });
@@ -2251,7 +2141,7 @@ function createIosDriver(client, options) {
2251
2141
  const timeoutMs = waitOptions?.timeout ?? DEFAULT_WAIT_TIMEOUT_MS2;
2252
2142
  const deadline = Date.now() + timeoutMs;
2253
2143
  for (; ; ) {
2254
- if ((await client.findElements(query)).length > 0) {
2144
+ if (await hasVisibleElement(query)) {
2255
2145
  return;
2256
2146
  }
2257
2147
  if (Date.now() >= deadline) {
@@ -2744,12 +2634,29 @@ function createWdaAgentClient(transport, sessionId) {
2744
2634
  const value = await transport.request("GET", `${base}/element/${elementId}/text`);
2745
2635
  return typeof value === "string" ? value : value == null ? null : String(value);
2746
2636
  },
2637
+ async isDisplayed(elementId) {
2638
+ try {
2639
+ return await transport.request("GET", `${base}/element/${elementId}/displayed`) === true;
2640
+ } catch (error) {
2641
+ if (isNoSuchElement2(error)) {
2642
+ return false;
2643
+ }
2644
+ throw error;
2645
+ }
2646
+ },
2747
2647
  async sendKeys(keys) {
2748
2648
  await transport.request("POST", `${base}/wda/keys`, { value: keys });
2749
2649
  },
2750
2650
  async homescreen() {
2751
2651
  await transport.request("POST", "/wda/homescreen", {});
2752
2652
  },
2653
+ async windowSize() {
2654
+ const value = await transport.request("GET", `${base}/window/size`);
2655
+ return toScreenSize(value, "WebDriverAgent /window/size");
2656
+ },
2657
+ async performActions(actions) {
2658
+ await transport.request("POST", `${base}/actions`, { actions: [actions] });
2659
+ },
2753
2660
  async source() {
2754
2661
  const value = await transport.request("GET", "/source");
2755
2662
  if (typeof value !== "string") {
@@ -3404,6 +3311,16 @@ function createPlaywrightDriver(page) {
3404
3311
  async hover(selector) {
3405
3312
  await page.locator(selector).hover();
3406
3313
  },
3314
+ async scroll(direction, amount = 500) {
3315
+ const deltas = {
3316
+ up: [0, -amount],
3317
+ down: [0, amount],
3318
+ left: [-amount, 0],
3319
+ right: [amount, 0]
3320
+ };
3321
+ const [x, y] = deltas[direction];
3322
+ await page.evaluate(([sx, sy]) => window.scrollBy(sx, sy), [x, y]);
3323
+ },
3407
3324
  async scrollIntoView(selector) {
3408
3325
  await page.locator(selector).scrollIntoViewIfNeeded();
3409
3326
  },
@@ -4129,11 +4046,14 @@ function toVisibilitySelector(value) {
4129
4046
  if (looksLikeSelector(value)) return value;
4130
4047
  return textContainsSelector(value);
4131
4048
  }
4049
+ function countVisible(driver, selector) {
4050
+ return driver.visibleCount?.(selector) ?? driver.count(selector);
4051
+ }
4132
4052
  async function runInlineAssert(driver, policy, assertion) {
4133
4053
  if (assertion.visible !== void 0) {
4134
4054
  const selector = toVisibilitySelector(assertion.visible);
4135
4055
  policy.assertAllowedSelector(selector);
4136
- const count = await driver.count(selector);
4056
+ const count = await countVisible(driver, selector);
4137
4057
  if (count === 0) {
4138
4058
  throw new Error(`Expected visible: ${assertion.visible}`);
4139
4059
  }
@@ -4142,7 +4062,7 @@ async function runInlineAssert(driver, policy, assertion) {
4142
4062
  if (assertion.notVisible !== void 0) {
4143
4063
  const selector = toVisibilitySelector(assertion.notVisible);
4144
4064
  policy.assertAllowedSelector(selector);
4145
- const count = await driver.count(selector);
4065
+ const count = await countVisible(driver, selector);
4146
4066
  if (count > 0) {
4147
4067
  throw new Error(`Expected not visible: ${assertion.notVisible}`);
4148
4068
  }
@@ -4532,25 +4452,21 @@ var STEP_HANDLERS = {
4532
4452
  }
4533
4453
  },
4534
4454
  scroll: {
4535
- capabilities: ["evaluate"],
4455
+ // Dispatched through the driver's `scroll` verb (interact), so native mobile
4456
+ // targets synthesize a touch swipe while web keeps its `window.scrollBy`
4457
+ // behavior. The per-target step gate rejects `scroll` on macOS before here.
4458
+ capabilities: ["interact"],
4536
4459
  run: async (h) => {
4537
4460
  if (!("scroll" in h.step)) unknownStep();
4538
- const amount = h.step.scroll.amount ?? 500;
4539
- const scrollMap = {
4540
- up: [0, -amount],
4541
- down: [0, amount],
4542
- left: [-amount, 0],
4543
- right: [amount, 0]
4544
- };
4545
- const [x, y] = scrollMap[h.step.scroll.direction];
4546
- await h.driver.evaluate(([sx, sy]) => window.scrollBy(sx, sy), [x, y]);
4461
+ const { direction, amount } = h.step.scroll;
4462
+ await h.driver.scroll(direction, amount);
4547
4463
  return {
4548
4464
  kind: "result",
4549
4465
  result: {
4550
4466
  type: "scroll",
4551
4467
  status: "pass",
4552
4468
  durationMs: Date.now() - h.stepStart,
4553
- value: `${h.step.scroll.direction} ${amount}px`
4469
+ value: amount === void 0 ? direction : `${direction} ${amount}px`
4554
4470
  }
4555
4471
  };
4556
4472
  }
@@ -4596,7 +4512,7 @@ var STEP_HANDLERS = {
4596
4512
  const condition = h.step.if;
4597
4513
  const selector = condition.visible ?? condition.notVisible;
4598
4514
  h.policy.assertAllowedSelector(selector);
4599
- const count = await h.driver.count(selector);
4515
+ const count = await countVisible(h.driver, selector);
4600
4516
  const conditionMet = condition.visible !== void 0 ? count > 0 : count === 0;
4601
4517
  if (conditionMet) {
4602
4518
  const subResult = await h.executeNested({
@@ -4683,7 +4599,7 @@ var STEP_HANDLERS = {
4683
4599
  const whileSelector = repeat.while.visible ?? repeat.while.notVisible;
4684
4600
  h.policy.assertAllowedSelector(whileSelector);
4685
4601
  for (let i = 0; i < maxIter; i++) {
4686
- const whileCount = await h.driver.count(whileSelector);
4602
+ const whileCount = await countVisible(h.driver, whileSelector);
4687
4603
  const shouldContinue = repeat.while.visible !== void 0 ? whileCount > 0 : whileCount === 0;
4688
4604
  if (!shouldContinue) break;
4689
4605
  totalSubSteps += repeat.steps.length;
@@ -7433,7 +7349,6 @@ function tccGuidance() {
7433
7349
  }
7434
7350
 
7435
7351
  export {
7436
- interpolateHunt,
7437
7352
  WEB_ONLY_STEP_TYPES,
7438
7353
  webOnlyReason,
7439
7354
  assertStepsSupportedByTarget,
@@ -7591,4 +7506,4 @@ export {
7591
7506
  collectMacdriverStatus,
7592
7507
  tccGuidance
7593
7508
  };
7594
- //# sourceMappingURL=chunk-R7NUH44M.js.map
7509
+ //# sourceMappingURL=chunk-2RNOK64Y.js.map