prowl-tools 0.1.7 → 0.1.8

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
  },
@@ -1295,6 +943,103 @@ function parseSnapshot(xml) {
1295
943
 
1296
944
  // src/browser/android-driver.ts
1297
945
  import fs3 from "fs";
946
+
947
+ // src/browser/touch-gestures.ts
948
+ var DEFAULT_SWIPE_FRACTION = 0.75;
949
+ var SCROLL_TO_PROBE_SWIPE_FRACTION = 0.6;
950
+ var MAX_SWIPE_FRACTION = 0.9;
951
+ var SWIPE_HOLD_MS = 100;
952
+ var SWIPE_MOVE_DURATION_MS = 300;
953
+ var SCROLL_TO_SWEEP_DEPTH = 10;
954
+ var SCROLL_TO_PROBE_DIRECTIONS = [
955
+ ...Array.from({ length: SCROLL_TO_SWEEP_DEPTH }, () => "down"),
956
+ ...Array.from({ length: SCROLL_TO_SWEEP_DEPTH * 2 }, () => "up")
957
+ ];
958
+ var MAX_SCROLL_TO_SWIPES = SCROLL_TO_PROBE_DIRECTIONS.length;
959
+ var OPPOSITE_SWIPE_DIRECTIONS = {
960
+ up: "down",
961
+ down: "up",
962
+ left: "right",
963
+ right: "left"
964
+ };
965
+ async function probeScrollIntoView({
966
+ isVisible,
967
+ swipe,
968
+ directions = SCROLL_TO_PROBE_DIRECTIONS
969
+ }) {
970
+ if (await isVisible()) {
971
+ return true;
972
+ }
973
+ for (const direction of directions) {
974
+ await swipe(direction);
975
+ if (await isVisible()) {
976
+ return true;
977
+ }
978
+ }
979
+ return false;
980
+ }
981
+ function toScreenSize(value, source) {
982
+ const record = value ?? {};
983
+ const width = Number(record.width);
984
+ const height = Number(record.height);
985
+ if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) {
986
+ throw new Error(`${source} did not return a usable screen size`);
987
+ }
988
+ return { width, height };
989
+ }
990
+ function isVertical(direction) {
991
+ return direction === "up" || direction === "down";
992
+ }
993
+ function swipeDistanceFor(direction, size, amount) {
994
+ if (amount !== void 0 && !Number.isFinite(amount)) {
995
+ throw new Error("scroll amount must be a finite number");
996
+ }
997
+ const axis = isVertical(direction) ? size.height : size.width;
998
+ const requested = amount === void 0 ? axis * DEFAULT_SWIPE_FRACTION : Math.abs(amount);
999
+ const max = axis * MAX_SWIPE_FRACTION;
1000
+ return Math.max(1, Math.round(Math.min(requested, max)));
1001
+ }
1002
+ function scrollToProbeDistanceFor(direction, size) {
1003
+ const axis = isVertical(direction) ? size.height : size.width;
1004
+ return Math.max(1, Math.round(axis * SCROLL_TO_PROBE_SWIPE_FRACTION));
1005
+ }
1006
+ function swipeEndpoints(direction, size, distance) {
1007
+ const cx = Math.round(size.width / 2);
1008
+ const cy = Math.round(size.height / 2);
1009
+ const half = Math.round(distance / 2);
1010
+ switch (direction) {
1011
+ case "down":
1012
+ return { start: { x: cx, y: cy + half }, end: { x: cx, y: cy - half } };
1013
+ case "up":
1014
+ return { start: { x: cx, y: cy - half }, end: { x: cx, y: cy + half } };
1015
+ case "right":
1016
+ return { start: { x: cx + half, y: cy }, end: { x: cx - half, y: cy } };
1017
+ case "left":
1018
+ return { start: { x: cx - half, y: cy }, end: { x: cx + half, y: cy } };
1019
+ }
1020
+ }
1021
+ function buildSwipeActions(start, end) {
1022
+ return {
1023
+ type: "pointer",
1024
+ id: "finger1",
1025
+ parameters: { pointerType: "touch" },
1026
+ actions: [
1027
+ { type: "pointerMove", duration: 0, x: start.x, y: start.y, origin: "viewport" },
1028
+ { type: "pointerDown", button: 0 },
1029
+ { type: "pause", duration: SWIPE_HOLD_MS },
1030
+ { type: "pointerMove", duration: SWIPE_MOVE_DURATION_MS, x: end.x, y: end.y, origin: "viewport" },
1031
+ { type: "pointerUp", button: 0 }
1032
+ ]
1033
+ };
1034
+ }
1035
+ function buildDirectionalSwipe(direction, size, amount) {
1036
+ const normalizedDirection = amount !== void 0 && amount < 0 ? OPPOSITE_SWIPE_DIRECTIONS[direction] : direction;
1037
+ const distance = swipeDistanceFor(normalizedDirection, size, amount);
1038
+ const { start, end } = swipeEndpoints(normalizedDirection, size, distance);
1039
+ return { actions: buildSwipeActions(start, end), distance, start, end };
1040
+ }
1041
+
1042
+ // src/browser/android-driver.ts
1298
1043
  var ANDROID_CAPABILITIES = /* @__PURE__ */ new Set([
1299
1044
  "query",
1300
1045
  "interact",
@@ -1409,6 +1154,11 @@ function createAndroidDriver(client, options = {}) {
1409
1154
  async function fillSelector(selector, value) {
1410
1155
  await client.setValue(await resolveOne(selector), value);
1411
1156
  }
1157
+ async function swipe(direction, amount, size) {
1158
+ const actualSize = size ?? await client.windowSize();
1159
+ const { actions } = buildDirectionalSwipe(direction, actualSize, amount);
1160
+ await client.performActions(actions);
1161
+ }
1412
1162
  return {
1413
1163
  capabilities: ANDROID_CAPABILITIES,
1414
1164
  // navigation -----------------------------------------------------------
@@ -1446,8 +1196,31 @@ function createAndroidDriver(client, options = {}) {
1446
1196
  hover() {
1447
1197
  return rejectUnsupported("hover");
1448
1198
  },
1449
- scrollIntoView() {
1450
- return rejectUnsupported("scrollTo");
1199
+ // Screen-centred swipe via the W3C actions endpoint (PROWL-080). Direction
1200
+ // semantics match the web step: scrolling "down" reveals lower content, so
1201
+ // the finger drags up. See ./touch-gestures.ts.
1202
+ async scroll(direction, amount) {
1203
+ await swipe(direction, amount);
1204
+ },
1205
+ // Resolve the element, short-circuiting if it is already present in the
1206
+ // hierarchy; otherwise use the shared bounded down/up mobile probe before
1207
+ // failing with a message naming the selector and attempts.
1208
+ async scrollIntoView(selector) {
1209
+ const query = parseAndroidSelector(selector);
1210
+ let probeSize;
1211
+ const found = await probeScrollIntoView({
1212
+ isVisible: async () => (await client.findElements(query)).length > 0,
1213
+ swipe: async (direction) => {
1214
+ probeSize ??= await client.windowSize();
1215
+ await swipe(direction, scrollToProbeDistanceFor(direction, probeSize), probeSize);
1216
+ }
1217
+ });
1218
+ if (found) {
1219
+ return;
1220
+ }
1221
+ throw new Error(
1222
+ `scrollTo: element "${selector}" not visible after ${MAX_SCROLL_TO_SWIPES} scroll attempts on the Android target`
1223
+ );
1451
1224
  },
1452
1225
  setInputFiles() {
1453
1226
  return rejectUnsupported("setInputFiles");
@@ -1919,6 +1692,13 @@ function createUia2AgentClient(transport, sessionId, options = {}) {
1919
1692
  async pressKeyCode(keyCode) {
1920
1693
  await transport.request("POST", `${base}/appium/device/press_keycode`, { keycode: keyCode });
1921
1694
  },
1695
+ async windowSize() {
1696
+ const value = await transport.request("GET", `${base}/window/current/size`);
1697
+ return toScreenSize(value, "uiautomator2 /window/current/size");
1698
+ },
1699
+ async performActions(actions) {
1700
+ await transport.request("POST", `${base}/actions`, { actions: [actions] });
1701
+ },
1922
1702
  async screenshotPng() {
1923
1703
  const value = await transport.request("GET", `${base}/screenshot`);
1924
1704
  if (typeof value !== "string") {
@@ -2066,6 +1846,7 @@ async function launchAndroidSession(options) {
2066
1846
  readyDeadlineMs,
2067
1847
  appPackage: pkg
2068
1848
  });
1849
+ await launchPackage(runner, serial, pkg);
2069
1850
  const driver = createAndroidDriver(client, { appLabel: pkg });
2070
1851
  return { client, driver, package: pkg, serial, teardown };
2071
1852
  } catch (error) {
@@ -2086,6 +1867,7 @@ var IOS_CAPABILITIES = /* @__PURE__ */ new Set([
2086
1867
  ]);
2087
1868
  var WAIT_POLL_INTERVAL_MS2 = 250;
2088
1869
  var DEFAULT_WAIT_TIMEOUT_MS2 = 5e3;
1870
+ var DISPLAYED_CHECK_CONCURRENCY = 4;
2089
1871
  var IOS_PRESS_KEYS = ["backspace", "del", "delete", "enter", "home", "return"];
2090
1872
  function escapePredicateArg(value) {
2091
1873
  return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
@@ -2160,6 +1942,64 @@ function createIosDriver(client, options) {
2160
1942
  async function fillSelector(selector, value) {
2161
1943
  await client.setValue(await resolveOne(selector), value);
2162
1944
  }
1945
+ async function swipe(direction, amount, size) {
1946
+ const actualSize = size ?? await client.windowSize();
1947
+ const { actions } = buildDirectionalSwipe(direction, actualSize, amount);
1948
+ await client.performActions(actions);
1949
+ }
1950
+ async function visibleElementIds(query) {
1951
+ const ids = await client.findElements(query);
1952
+ const visible = Array.from({ length: ids.length }, () => null);
1953
+ let nextIndex = 0;
1954
+ const workerCount = Math.min(DISPLAYED_CHECK_CONCURRENCY, ids.length);
1955
+ await Promise.all(
1956
+ Array.from({ length: workerCount }, async () => {
1957
+ for (; ; ) {
1958
+ const index = nextIndex;
1959
+ nextIndex += 1;
1960
+ if (index >= ids.length) {
1961
+ return;
1962
+ }
1963
+ const id = ids[index];
1964
+ if (await client.isDisplayed(id)) {
1965
+ visible[index] = id;
1966
+ }
1967
+ }
1968
+ })
1969
+ );
1970
+ return visible.filter((id) => id !== null);
1971
+ }
1972
+ async function hasVisibleElement(query) {
1973
+ const ids = await client.findElements(query);
1974
+ if (ids.length === 0) {
1975
+ return false;
1976
+ }
1977
+ if (await client.isDisplayed(ids[0])) {
1978
+ return true;
1979
+ }
1980
+ let found = false;
1981
+ let nextIndex = 1;
1982
+ const workerCount = Math.min(DISPLAYED_CHECK_CONCURRENCY, ids.length - 1);
1983
+ await Promise.all(
1984
+ Array.from({ length: workerCount }, async () => {
1985
+ for (; ; ) {
1986
+ if (found) {
1987
+ return;
1988
+ }
1989
+ const index = nextIndex;
1990
+ nextIndex += 1;
1991
+ if (index >= ids.length) {
1992
+ return;
1993
+ }
1994
+ if (await client.isDisplayed(ids[index])) {
1995
+ found = true;
1996
+ return;
1997
+ }
1998
+ }
1999
+ })
2000
+ );
2001
+ return found;
2002
+ }
2163
2003
  async function pressKey(key) {
2164
2004
  const name = key.trim().toLowerCase();
2165
2005
  if (name === "enter" || name === "return") {
@@ -2191,6 +2031,9 @@ function createIosDriver(client, options) {
2191
2031
  async count(selector) {
2192
2032
  return (await client.findElements(parseIosSelector(selector))).length;
2193
2033
  },
2034
+ async visibleCount(selector) {
2035
+ return (await visibleElementIds(parseIosSelector(selector))).length;
2036
+ },
2194
2037
  async textContent(selector) {
2195
2038
  const id = await client.findElement(parseIosSelector(selector));
2196
2039
  if (id === null) {
@@ -2215,15 +2058,38 @@ function createIosDriver(client, options) {
2215
2058
  hover() {
2216
2059
  return rejectUnsupported("hover");
2217
2060
  },
2218
- scrollIntoView() {
2219
- return rejectUnsupported("scrollTo");
2061
+ // Screen-centred swipe via the W3C actions endpoint (PROWL-080). Direction
2062
+ // semantics match the web step: scrolling "down" reveals lower content, so
2063
+ // the finger drags up. See ./touch-gestures.ts.
2064
+ async scroll(direction, amount) {
2065
+ await swipe(direction, amount);
2066
+ },
2067
+ // Resolve the element, short-circuiting only if WDA reports a matching
2068
+ // element displayed in the viewport; hierarchy-only matches can be offscreen.
2069
+ // Otherwise use the shared bounded down/up mobile probe before failing.
2070
+ async scrollIntoView(selector) {
2071
+ const query = parseIosSelector(selector);
2072
+ let probeSize;
2073
+ const found = await probeScrollIntoView({
2074
+ isVisible: () => hasVisibleElement(query),
2075
+ swipe: async (direction) => {
2076
+ probeSize ??= await client.windowSize();
2077
+ await swipe(direction, scrollToProbeDistanceFor(direction, probeSize), probeSize);
2078
+ }
2079
+ });
2080
+ if (found) {
2081
+ return;
2082
+ }
2083
+ throw new Error(
2084
+ `scrollTo: element "${selector}" not visible after ${MAX_SCROLL_TO_SWIPES} scroll attempts on the iOS target`
2085
+ );
2220
2086
  },
2221
2087
  setInputFiles() {
2222
2088
  return rejectUnsupported("setInputFiles");
2223
2089
  },
2224
2090
  // semantic locators ----------------------------------------------------
2225
2091
  async countByRole(role, name) {
2226
- return (await client.findElements({ by: "role", role, name })).length;
2092
+ return (await visibleElementIds({ by: "role", role, name })).length;
2227
2093
  },
2228
2094
  async clickFirstByRole(role, name) {
2229
2095
  const id = await client.findElement({ by: "role", role, name });
@@ -2233,7 +2099,7 @@ function createIosDriver(client, options) {
2233
2099
  await client.click(id);
2234
2100
  },
2235
2101
  async countByLabel(label) {
2236
- return (await client.findElements({ by: "label", value: label })).length;
2102
+ return (await visibleElementIds({ by: "label", value: label })).length;
2237
2103
  },
2238
2104
  async fillFirstByLabel(label, value) {
2239
2105
  const id = await client.findElement({ by: "label", value: label });
@@ -2251,7 +2117,7 @@ function createIosDriver(client, options) {
2251
2117
  const timeoutMs = waitOptions?.timeout ?? DEFAULT_WAIT_TIMEOUT_MS2;
2252
2118
  const deadline = Date.now() + timeoutMs;
2253
2119
  for (; ; ) {
2254
- if ((await client.findElements(query)).length > 0) {
2120
+ if (await hasVisibleElement(query)) {
2255
2121
  return;
2256
2122
  }
2257
2123
  if (Date.now() >= deadline) {
@@ -2744,12 +2610,29 @@ function createWdaAgentClient(transport, sessionId) {
2744
2610
  const value = await transport.request("GET", `${base}/element/${elementId}/text`);
2745
2611
  return typeof value === "string" ? value : value == null ? null : String(value);
2746
2612
  },
2613
+ async isDisplayed(elementId) {
2614
+ try {
2615
+ return await transport.request("GET", `${base}/element/${elementId}/displayed`) === true;
2616
+ } catch (error) {
2617
+ if (isNoSuchElement2(error)) {
2618
+ return false;
2619
+ }
2620
+ throw error;
2621
+ }
2622
+ },
2747
2623
  async sendKeys(keys) {
2748
2624
  await transport.request("POST", `${base}/wda/keys`, { value: keys });
2749
2625
  },
2750
2626
  async homescreen() {
2751
2627
  await transport.request("POST", "/wda/homescreen", {});
2752
2628
  },
2629
+ async windowSize() {
2630
+ const value = await transport.request("GET", `${base}/window/size`);
2631
+ return toScreenSize(value, "WebDriverAgent /window/size");
2632
+ },
2633
+ async performActions(actions) {
2634
+ await transport.request("POST", `${base}/actions`, { actions: [actions] });
2635
+ },
2753
2636
  async source() {
2754
2637
  const value = await transport.request("GET", "/source");
2755
2638
  if (typeof value !== "string") {
@@ -3404,6 +3287,16 @@ function createPlaywrightDriver(page) {
3404
3287
  async hover(selector) {
3405
3288
  await page.locator(selector).hover();
3406
3289
  },
3290
+ async scroll(direction, amount = 500) {
3291
+ const deltas = {
3292
+ up: [0, -amount],
3293
+ down: [0, amount],
3294
+ left: [-amount, 0],
3295
+ right: [amount, 0]
3296
+ };
3297
+ const [x, y] = deltas[direction];
3298
+ await page.evaluate(([sx, sy]) => window.scrollBy(sx, sy), [x, y]);
3299
+ },
3407
3300
  async scrollIntoView(selector) {
3408
3301
  await page.locator(selector).scrollIntoViewIfNeeded();
3409
3302
  },
@@ -4129,11 +4022,14 @@ function toVisibilitySelector(value) {
4129
4022
  if (looksLikeSelector(value)) return value;
4130
4023
  return textContainsSelector(value);
4131
4024
  }
4025
+ function countVisible(driver, selector) {
4026
+ return driver.visibleCount?.(selector) ?? driver.count(selector);
4027
+ }
4132
4028
  async function runInlineAssert(driver, policy, assertion) {
4133
4029
  if (assertion.visible !== void 0) {
4134
4030
  const selector = toVisibilitySelector(assertion.visible);
4135
4031
  policy.assertAllowedSelector(selector);
4136
- const count = await driver.count(selector);
4032
+ const count = await countVisible(driver, selector);
4137
4033
  if (count === 0) {
4138
4034
  throw new Error(`Expected visible: ${assertion.visible}`);
4139
4035
  }
@@ -4142,7 +4038,7 @@ async function runInlineAssert(driver, policy, assertion) {
4142
4038
  if (assertion.notVisible !== void 0) {
4143
4039
  const selector = toVisibilitySelector(assertion.notVisible);
4144
4040
  policy.assertAllowedSelector(selector);
4145
- const count = await driver.count(selector);
4041
+ const count = await countVisible(driver, selector);
4146
4042
  if (count > 0) {
4147
4043
  throw new Error(`Expected not visible: ${assertion.notVisible}`);
4148
4044
  }
@@ -4532,25 +4428,21 @@ var STEP_HANDLERS = {
4532
4428
  }
4533
4429
  },
4534
4430
  scroll: {
4535
- capabilities: ["evaluate"],
4431
+ // Dispatched through the driver's `scroll` verb (interact), so native mobile
4432
+ // targets synthesize a touch swipe while web keeps its `window.scrollBy`
4433
+ // behavior. The per-target step gate rejects `scroll` on macOS before here.
4434
+ capabilities: ["interact"],
4536
4435
  run: async (h) => {
4537
4436
  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]);
4437
+ const { direction, amount } = h.step.scroll;
4438
+ await h.driver.scroll(direction, amount);
4547
4439
  return {
4548
4440
  kind: "result",
4549
4441
  result: {
4550
4442
  type: "scroll",
4551
4443
  status: "pass",
4552
4444
  durationMs: Date.now() - h.stepStart,
4553
- value: `${h.step.scroll.direction} ${amount}px`
4445
+ value: amount === void 0 ? direction : `${direction} ${amount}px`
4554
4446
  }
4555
4447
  };
4556
4448
  }
@@ -4596,7 +4488,7 @@ var STEP_HANDLERS = {
4596
4488
  const condition = h.step.if;
4597
4489
  const selector = condition.visible ?? condition.notVisible;
4598
4490
  h.policy.assertAllowedSelector(selector);
4599
- const count = await h.driver.count(selector);
4491
+ const count = await countVisible(h.driver, selector);
4600
4492
  const conditionMet = condition.visible !== void 0 ? count > 0 : count === 0;
4601
4493
  if (conditionMet) {
4602
4494
  const subResult = await h.executeNested({
@@ -4683,7 +4575,7 @@ var STEP_HANDLERS = {
4683
4575
  const whileSelector = repeat.while.visible ?? repeat.while.notVisible;
4684
4576
  h.policy.assertAllowedSelector(whileSelector);
4685
4577
  for (let i = 0; i < maxIter; i++) {
4686
- const whileCount = await h.driver.count(whileSelector);
4578
+ const whileCount = await countVisible(h.driver, whileSelector);
4687
4579
  const shouldContinue = repeat.while.visible !== void 0 ? whileCount > 0 : whileCount === 0;
4688
4580
  if (!shouldContinue) break;
4689
4581
  totalSubSteps += repeat.steps.length;
@@ -7433,7 +7325,6 @@ function tccGuidance() {
7433
7325
  }
7434
7326
 
7435
7327
  export {
7436
- interpolateHunt,
7437
7328
  WEB_ONLY_STEP_TYPES,
7438
7329
  webOnlyReason,
7439
7330
  assertStepsSupportedByTarget,
@@ -7591,4 +7482,4 @@ export {
7591
7482
  collectMacdriverStatus,
7592
7483
  tccGuidance
7593
7484
  };
7594
- //# sourceMappingURL=chunk-R7NUH44M.js.map
7485
+ //# sourceMappingURL=chunk-CWLRDV5P.js.map