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.
- package/README.md +56 -7
- package/dist/{chunk-R7NUH44M.js → chunk-2RNOK64Y.js} +314 -399
- package/dist/chunk-2RNOK64Y.js.map +1 -0
- package/dist/{chunk-O3OUTZ2P.js → chunk-JFJQNJSJ.js} +394 -2
- package/dist/chunk-JFJQNJSJ.js.map +1 -0
- package/dist/index.cjs +1564 -692
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +597 -40
- package/dist/index.js.map +1 -1
- package/dist/lib.cjs +650 -346
- package/dist/lib.cjs.map +1 -1
- package/dist/lib.d.cts +95 -17
- package/dist/lib.d.ts +95 -17
- package/dist/lib.js +3 -3
- package/dist/{loader-X37URHUV.js → loader-JTHA4BYG.js} +2 -2
- package/examples/hunts/form.yml +53 -0
- package/examples/hunts/macos-hello.yml +49 -0
- package/package.json +1 -1
- package/dist/chunk-O3OUTZ2P.js.map +0 -1
- package/dist/chunk-R7NUH44M.js.map +0 -1
- /package/dist/{loader-X37URHUV.js.map → loader-JTHA4BYG.js.map} +0 -0
|
@@ -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-
|
|
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
|
-
|
|
1450
|
-
|
|
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
|
-
|
|
2219
|
-
|
|
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
|
|
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
|
|
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 (
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
4539
|
-
|
|
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: `${
|
|
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
|
|
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
|
|
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-
|
|
7509
|
+
//# sourceMappingURL=chunk-2RNOK64Y.js.map
|