prowl-tools 0.1.6 → 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.
- package/README.md +347 -183
- package/dist/{chunk-5KQR3IR3.js → chunk-CWLRDV5P.js} +839 -504
- package/dist/chunk-CWLRDV5P.js.map +1 -0
- package/dist/{chunk-WHAMB4TY.js → chunk-JFJQNJSJ.js} +411 -3
- package/dist/chunk-JFJQNJSJ.js.map +1 -0
- package/dist/index.cjs +1707 -892
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +108 -13
- package/dist/index.js.map +1 -1
- package/dist/lib.cjs +1285 -532
- package/dist/lib.cjs.map +1 -1
- package/dist/lib.d.cts +235 -24
- package/dist/lib.d.ts +235 -24
- package/dist/lib.js +59 -5
- package/dist/{loader-PBBYV3U7.js → loader-JTHA4BYG.js} +2 -2
- package/examples/hunts/hello.yml +1 -1
- package/examples/hunts/login-flow.yml +58 -0
- package/package.json +6 -3
- package/dist/chunk-5KQR3IR3.js.map +0 -1
- package/dist/chunk-WHAMB4TY.js.map +0 -1
- /package/dist/{loader-PBBYV3U7.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
|
},
|
|
@@ -757,27 +405,71 @@ function createMacDriver(client, options = {}) {
|
|
|
757
405
|
};
|
|
758
406
|
}
|
|
759
407
|
|
|
408
|
+
// src/browser/macdriver-release.ts
|
|
409
|
+
import os from "os";
|
|
410
|
+
import path2 from "path";
|
|
411
|
+
var HELPER_BINARY = "prowl-macdriver";
|
|
412
|
+
var MACDRIVER_VERSION = "0.1.0";
|
|
413
|
+
var MACDRIVER_REPO = "prowl-tools/prowl";
|
|
414
|
+
var MACDRIVER_SIGNING_IDENTIFIER = "tools.prowl.macdriver";
|
|
415
|
+
var MACDRIVER_SIGNING_AUTHORITY_PREFIX = "Developer ID Application: Genkei Labs";
|
|
416
|
+
var MACDRIVER_VERSION_PATTERN = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z][0-9A-Za-z.-]*)?(?:\+[0-9A-Za-z][0-9A-Za-z.-]*)?$/;
|
|
417
|
+
function validateMacdriverVersion(version) {
|
|
418
|
+
if (!MACDRIVER_VERSION_PATTERN.test(version)) {
|
|
419
|
+
throw new Error(
|
|
420
|
+
`Invalid prowl-macdriver version "${version}". Expected a release version like 0.1.0.`
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
return version;
|
|
424
|
+
}
|
|
425
|
+
function macdriverReleaseTag(version = MACDRIVER_VERSION) {
|
|
426
|
+
return `macdriver-v${validateMacdriverVersion(version)}`;
|
|
427
|
+
}
|
|
428
|
+
function macdriverAssetName(version = MACDRIVER_VERSION) {
|
|
429
|
+
return `prowl-macdriver-v${validateMacdriverVersion(version)}-universal.zip`;
|
|
430
|
+
}
|
|
431
|
+
function macdriverChecksumName(version = MACDRIVER_VERSION) {
|
|
432
|
+
return `${macdriverAssetName(version)}.sha256`;
|
|
433
|
+
}
|
|
434
|
+
function macdriverAssetUrl(assetName, version = MACDRIVER_VERSION) {
|
|
435
|
+
return `https://github.com/${MACDRIVER_REPO}/releases/download/${macdriverReleaseTag(version)}/${assetName}`;
|
|
436
|
+
}
|
|
437
|
+
function macdriverInstallRoot(homedir = os.homedir()) {
|
|
438
|
+
return path2.join(homedir, ".prowl", "macdriver");
|
|
439
|
+
}
|
|
440
|
+
function macdriverVersionDir(version = MACDRIVER_VERSION, homedir = os.homedir()) {
|
|
441
|
+
const root = path2.resolve(macdriverInstallRoot(homedir));
|
|
442
|
+
const versionDir = path2.resolve(root, validateMacdriverVersion(version));
|
|
443
|
+
if (!versionDir.startsWith(root + path2.sep)) {
|
|
444
|
+
throw new Error(`Resolved prowl-macdriver version directory escaped install root: ${versionDir}`);
|
|
445
|
+
}
|
|
446
|
+
return versionDir;
|
|
447
|
+
}
|
|
448
|
+
function macdriverInstalledBinary(version = MACDRIVER_VERSION, homedir = os.homedir()) {
|
|
449
|
+
return path2.join(macdriverVersionDir(version, homedir), HELPER_BINARY);
|
|
450
|
+
}
|
|
451
|
+
|
|
760
452
|
// src/browser/mac-helper.ts
|
|
761
453
|
import { spawn } from "child_process";
|
|
762
454
|
import fs2 from "fs";
|
|
763
|
-
import
|
|
455
|
+
import os2 from "os";
|
|
456
|
+
import path3 from "path";
|
|
764
457
|
import { fileURLToPath } from "url";
|
|
765
|
-
var HELPER_BINARY = "prowl-macdriver";
|
|
766
458
|
function macdriverBuildInstructions() {
|
|
767
|
-
return "The macOS target
|
|
459
|
+
return "The macOS target needs the `prowl-macdriver` helper. Install the prebuilt, signed binary (recommended):\n prowl macdriver install\nContributors building from source can instead run:\n cd macdriver && swift build -c release\nor point Prowl at a prebuilt binary via the PROWL_MACDRIVER_BIN environment variable.";
|
|
768
460
|
}
|
|
769
461
|
function getPackageRoot() {
|
|
770
|
-
let dir =
|
|
771
|
-
const root =
|
|
462
|
+
let dir = path3.dirname(fileURLToPath(import.meta.url));
|
|
463
|
+
const root = path3.parse(dir).root;
|
|
772
464
|
while (dir !== root) {
|
|
773
|
-
if (fs2.existsSync(
|
|
465
|
+
if (fs2.existsSync(path3.join(dir, "package.json"))) {
|
|
774
466
|
return dir;
|
|
775
467
|
}
|
|
776
|
-
dir =
|
|
468
|
+
dir = path3.dirname(dir);
|
|
777
469
|
}
|
|
778
470
|
return root;
|
|
779
471
|
}
|
|
780
|
-
function resolveHelperBinary(env = process.env) {
|
|
472
|
+
function resolveHelperBinary(env = process.env, options = {}) {
|
|
781
473
|
const override = env.PROWL_MACDRIVER_BIN;
|
|
782
474
|
if (override) {
|
|
783
475
|
if (!fs2.existsSync(override)) {
|
|
@@ -788,10 +480,15 @@ ${macdriverBuildInstructions()}`
|
|
|
788
480
|
}
|
|
789
481
|
return override;
|
|
790
482
|
}
|
|
483
|
+
const homedir = options.homedir ?? os2.homedir();
|
|
484
|
+
const userBinary = macdriverInstalledBinary(MACDRIVER_VERSION, homedir);
|
|
485
|
+
if (fs2.existsSync(userBinary)) {
|
|
486
|
+
return userBinary;
|
|
487
|
+
}
|
|
791
488
|
const root = getPackageRoot();
|
|
792
489
|
const candidates = [
|
|
793
|
-
|
|
794
|
-
|
|
490
|
+
path3.join(root, "macdriver", ".build", "release", HELPER_BINARY),
|
|
491
|
+
path3.join(root, "macdriver", ".build", "debug", HELPER_BINARY)
|
|
795
492
|
];
|
|
796
493
|
for (const candidate of candidates) {
|
|
797
494
|
if (fs2.existsSync(candidate)) {
|
|
@@ -1246,6 +943,103 @@ function parseSnapshot(xml) {
|
|
|
1246
943
|
|
|
1247
944
|
// src/browser/android-driver.ts
|
|
1248
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
|
|
1249
1043
|
var ANDROID_CAPABILITIES = /* @__PURE__ */ new Set([
|
|
1250
1044
|
"query",
|
|
1251
1045
|
"interact",
|
|
@@ -1360,6 +1154,11 @@ function createAndroidDriver(client, options = {}) {
|
|
|
1360
1154
|
async function fillSelector(selector, value) {
|
|
1361
1155
|
await client.setValue(await resolveOne(selector), value);
|
|
1362
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
|
+
}
|
|
1363
1162
|
return {
|
|
1364
1163
|
capabilities: ANDROID_CAPABILITIES,
|
|
1365
1164
|
// navigation -----------------------------------------------------------
|
|
@@ -1397,8 +1196,31 @@ function createAndroidDriver(client, options = {}) {
|
|
|
1397
1196
|
hover() {
|
|
1398
1197
|
return rejectUnsupported("hover");
|
|
1399
1198
|
},
|
|
1400
|
-
|
|
1401
|
-
|
|
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
|
+
);
|
|
1402
1224
|
},
|
|
1403
1225
|
setInputFiles() {
|
|
1404
1226
|
return rejectUnsupported("setInputFiles");
|
|
@@ -1721,12 +1543,12 @@ var Uia2Transport = class {
|
|
|
1721
1543
|
* {@link Uia2HttpError} on a non-2xx response, or a timeout error when the
|
|
1722
1544
|
* per-request deadline elapses.
|
|
1723
1545
|
*/
|
|
1724
|
-
async request(method,
|
|
1546
|
+
async request(method, path18, body, timeoutMs) {
|
|
1725
1547
|
const requestTimeoutMs = timeoutMs ?? this.requestTimeoutMs;
|
|
1726
1548
|
const controller = new AbortController();
|
|
1727
1549
|
const timer = setTimeout(() => controller.abort(), requestTimeoutMs);
|
|
1728
1550
|
timer.unref?.();
|
|
1729
|
-
const url = `${this.baseUrl}${
|
|
1551
|
+
const url = `${this.baseUrl}${path18}`;
|
|
1730
1552
|
let response;
|
|
1731
1553
|
try {
|
|
1732
1554
|
response = await this.fetchImpl(url, {
|
|
@@ -1738,7 +1560,7 @@ var Uia2Transport = class {
|
|
|
1738
1560
|
} catch (error) {
|
|
1739
1561
|
if (controller.signal.aborted) {
|
|
1740
1562
|
const shown = requestTimeoutMs >= 1e3 ? `${Math.round(requestTimeoutMs / 1e3)}s` : `${requestTimeoutMs}ms`;
|
|
1741
|
-
throw new Error(`uiautomator2 request ${method} ${
|
|
1563
|
+
throw new Error(`uiautomator2 request ${method} ${path18} timed out after ${shown}`);
|
|
1742
1564
|
}
|
|
1743
1565
|
throw error instanceof Error ? error : new Error(String(error));
|
|
1744
1566
|
} finally {
|
|
@@ -1749,7 +1571,7 @@ var Uia2Transport = class {
|
|
|
1749
1571
|
if (!response.ok) {
|
|
1750
1572
|
const wdError = extractWebdriverError(parsed);
|
|
1751
1573
|
throw new Uia2HttpError(
|
|
1752
|
-
`uiautomator2 ${method} ${
|
|
1574
|
+
`uiautomator2 ${method} ${path18} failed (${response.status})${wdError ? `: ${wdError}` : ""}`,
|
|
1753
1575
|
response.status,
|
|
1754
1576
|
wdError
|
|
1755
1577
|
);
|
|
@@ -1832,10 +1654,10 @@ function sleep(ms) {
|
|
|
1832
1654
|
}
|
|
1833
1655
|
function createUia2AgentClient(transport, sessionId, options = {}) {
|
|
1834
1656
|
const base = `/session/${sessionId}`;
|
|
1835
|
-
async function locate(query,
|
|
1657
|
+
async function locate(query, path18) {
|
|
1836
1658
|
return transport.request(
|
|
1837
1659
|
"POST",
|
|
1838
|
-
`${base}${
|
|
1660
|
+
`${base}${path18}`,
|
|
1839
1661
|
androidQueryToLocator(query, { appPackage: options.appPackage })
|
|
1840
1662
|
);
|
|
1841
1663
|
}
|
|
@@ -1870,6 +1692,13 @@ function createUia2AgentClient(transport, sessionId, options = {}) {
|
|
|
1870
1692
|
async pressKeyCode(keyCode) {
|
|
1871
1693
|
await transport.request("POST", `${base}/appium/device/press_keycode`, { keycode: keyCode });
|
|
1872
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
|
+
},
|
|
1873
1702
|
async screenshotPng() {
|
|
1874
1703
|
const value = await transport.request("GET", `${base}/screenshot`);
|
|
1875
1704
|
if (typeof value !== "string") {
|
|
@@ -1894,7 +1723,7 @@ function createUia2AgentClient(transport, sessionId, options = {}) {
|
|
|
1894
1723
|
import { createRequire } from "module";
|
|
1895
1724
|
import { execFile as execFile2 } from "child_process";
|
|
1896
1725
|
import fs4 from "fs";
|
|
1897
|
-
import
|
|
1726
|
+
import path4 from "path";
|
|
1898
1727
|
var UIA2_REMOTE_PORT = 6790;
|
|
1899
1728
|
function resolveAgentApks(requireFn = createRequire(import.meta.url)) {
|
|
1900
1729
|
let pkgJsonPath;
|
|
@@ -1905,10 +1734,10 @@ function resolveAgentApks(requireFn = createRequire(import.meta.url)) {
|
|
|
1905
1734
|
"The Android target requires the `appium-uiautomator2-server` package (its prebuilt APKs). It is an optional dependency of prowl-tools; it may have been skipped (--omit=optional) or failed to install. Restore it for a global Prowl install with: npm install -g appium-uiautomator2-server@10.6.2. If Prowl is installed locally in a project, run: npm install appium-uiautomator2-server@10.6.2"
|
|
1906
1735
|
);
|
|
1907
1736
|
}
|
|
1908
|
-
const pkgDir =
|
|
1737
|
+
const pkgDir = path4.dirname(pkgJsonPath);
|
|
1909
1738
|
const version = requireFn(pkgJsonPath).version;
|
|
1910
|
-
const serverApk =
|
|
1911
|
-
const testApk =
|
|
1739
|
+
const serverApk = path4.join(pkgDir, "apks", `appium-uiautomator2-server-v${version}.apk`);
|
|
1740
|
+
const testApk = path4.join(pkgDir, "apks", "appium-uiautomator2-server-debug-androidTest.apk");
|
|
1912
1741
|
for (const apk of [serverApk, testApk]) {
|
|
1913
1742
|
if (!fs4.existsSync(apk)) {
|
|
1914
1743
|
throw new Error(`Expected uiautomator2 agent APK is missing: ${apk}. Reinstall dependencies.`);
|
|
@@ -1956,7 +1785,7 @@ async function resolvePackage(app, runner, serial, aaptResolver, allowedApps) {
|
|
|
1956
1785
|
assertAndroidAppAllowed(allowedApps, app);
|
|
1957
1786
|
return app;
|
|
1958
1787
|
}
|
|
1959
|
-
const apkPath =
|
|
1788
|
+
const apkPath = path4.resolve(app);
|
|
1960
1789
|
if (!fs4.existsSync(apkPath)) {
|
|
1961
1790
|
throw new Error(`APK not found: ${apkPath}`);
|
|
1962
1791
|
}
|
|
@@ -2017,6 +1846,7 @@ async function launchAndroidSession(options) {
|
|
|
2017
1846
|
readyDeadlineMs,
|
|
2018
1847
|
appPackage: pkg
|
|
2019
1848
|
});
|
|
1849
|
+
await launchPackage(runner, serial, pkg);
|
|
2020
1850
|
const driver = createAndroidDriver(client, { appLabel: pkg });
|
|
2021
1851
|
return { client, driver, package: pkg, serial, teardown };
|
|
2022
1852
|
} catch (error) {
|
|
@@ -2037,6 +1867,7 @@ var IOS_CAPABILITIES = /* @__PURE__ */ new Set([
|
|
|
2037
1867
|
]);
|
|
2038
1868
|
var WAIT_POLL_INTERVAL_MS2 = 250;
|
|
2039
1869
|
var DEFAULT_WAIT_TIMEOUT_MS2 = 5e3;
|
|
1870
|
+
var DISPLAYED_CHECK_CONCURRENCY = 4;
|
|
2040
1871
|
var IOS_PRESS_KEYS = ["backspace", "del", "delete", "enter", "home", "return"];
|
|
2041
1872
|
function escapePredicateArg(value) {
|
|
2042
1873
|
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
@@ -2111,6 +1942,64 @@ function createIosDriver(client, options) {
|
|
|
2111
1942
|
async function fillSelector(selector, value) {
|
|
2112
1943
|
await client.setValue(await resolveOne(selector), value);
|
|
2113
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
|
+
}
|
|
2114
2003
|
async function pressKey(key) {
|
|
2115
2004
|
const name = key.trim().toLowerCase();
|
|
2116
2005
|
if (name === "enter" || name === "return") {
|
|
@@ -2142,6 +2031,9 @@ function createIosDriver(client, options) {
|
|
|
2142
2031
|
async count(selector) {
|
|
2143
2032
|
return (await client.findElements(parseIosSelector(selector))).length;
|
|
2144
2033
|
},
|
|
2034
|
+
async visibleCount(selector) {
|
|
2035
|
+
return (await visibleElementIds(parseIosSelector(selector))).length;
|
|
2036
|
+
},
|
|
2145
2037
|
async textContent(selector) {
|
|
2146
2038
|
const id = await client.findElement(parseIosSelector(selector));
|
|
2147
2039
|
if (id === null) {
|
|
@@ -2166,15 +2058,38 @@ function createIosDriver(client, options) {
|
|
|
2166
2058
|
hover() {
|
|
2167
2059
|
return rejectUnsupported("hover");
|
|
2168
2060
|
},
|
|
2169
|
-
|
|
2170
|
-
|
|
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
|
+
);
|
|
2171
2086
|
},
|
|
2172
2087
|
setInputFiles() {
|
|
2173
2088
|
return rejectUnsupported("setInputFiles");
|
|
2174
2089
|
},
|
|
2175
2090
|
// semantic locators ----------------------------------------------------
|
|
2176
2091
|
async countByRole(role, name) {
|
|
2177
|
-
return (await
|
|
2092
|
+
return (await visibleElementIds({ by: "role", role, name })).length;
|
|
2178
2093
|
},
|
|
2179
2094
|
async clickFirstByRole(role, name) {
|
|
2180
2095
|
const id = await client.findElement({ by: "role", role, name });
|
|
@@ -2184,7 +2099,7 @@ function createIosDriver(client, options) {
|
|
|
2184
2099
|
await client.click(id);
|
|
2185
2100
|
},
|
|
2186
2101
|
async countByLabel(label) {
|
|
2187
|
-
return (await
|
|
2102
|
+
return (await visibleElementIds({ by: "label", value: label })).length;
|
|
2188
2103
|
},
|
|
2189
2104
|
async fillFirstByLabel(label, value) {
|
|
2190
2105
|
const id = await client.findElement({ by: "label", value: label });
|
|
@@ -2202,7 +2117,7 @@ function createIosDriver(client, options) {
|
|
|
2202
2117
|
const timeoutMs = waitOptions?.timeout ?? DEFAULT_WAIT_TIMEOUT_MS2;
|
|
2203
2118
|
const deadline = Date.now() + timeoutMs;
|
|
2204
2119
|
for (; ; ) {
|
|
2205
|
-
if (
|
|
2120
|
+
if (await hasVisibleElement(query)) {
|
|
2206
2121
|
return;
|
|
2207
2122
|
}
|
|
2208
2123
|
if (Date.now() >= deadline) {
|
|
@@ -2250,8 +2165,8 @@ function createIosDriver(client, options) {
|
|
|
2250
2165
|
import { execFile as execFile3, spawn as spawn3 } from "child_process";
|
|
2251
2166
|
import { mkdir, readFile, rm, writeFile } from "fs/promises";
|
|
2252
2167
|
import net from "net";
|
|
2253
|
-
import
|
|
2254
|
-
import
|
|
2168
|
+
import os3 from "os";
|
|
2169
|
+
import path5 from "path";
|
|
2255
2170
|
var spawnXcrunProcess = (args, options) => {
|
|
2256
2171
|
const child = spawn3("xcrun", args, {
|
|
2257
2172
|
stdio: "ignore",
|
|
@@ -2265,7 +2180,7 @@ var spawnXcrunProcess = (args, options) => {
|
|
|
2265
2180
|
}
|
|
2266
2181
|
};
|
|
2267
2182
|
};
|
|
2268
|
-
var DEFAULT_SIMULATOR_LOCK_ROOT =
|
|
2183
|
+
var DEFAULT_SIMULATOR_LOCK_ROOT = path5.join(os3.tmpdir(), "prowl-ios-simulator-locks");
|
|
2269
2184
|
var SIMULATOR_LOCK_OWNER_FILE = "owner.json";
|
|
2270
2185
|
var execFileXcrunRunner = (args, options) => new Promise((resolve) => {
|
|
2271
2186
|
execFile3(
|
|
@@ -2305,7 +2220,7 @@ function isProcessAlive(pid) {
|
|
|
2305
2220
|
}
|
|
2306
2221
|
async function removeStaleSimulatorLock(lockPath) {
|
|
2307
2222
|
try {
|
|
2308
|
-
const ownerText = await readFile(
|
|
2223
|
+
const ownerText = await readFile(path5.join(lockPath, SIMULATOR_LOCK_OWNER_FILE), "utf8");
|
|
2309
2224
|
const owner = JSON.parse(ownerText);
|
|
2310
2225
|
if (typeof owner.pid === "number" && Number.isInteger(owner.pid) && owner.pid > 0 && !isProcessAlive(owner.pid)) {
|
|
2311
2226
|
await rm(lockPath, { recursive: true, force: true });
|
|
@@ -2323,7 +2238,7 @@ function simulatorReservedError(udid) {
|
|
|
2323
2238
|
}
|
|
2324
2239
|
async function reserveSimulatorUdid(udid, options = {}) {
|
|
2325
2240
|
const lockRoot = options.lockRoot ?? DEFAULT_SIMULATOR_LOCK_ROOT;
|
|
2326
|
-
const lockPath =
|
|
2241
|
+
const lockPath = path5.join(lockRoot, simulatorLockName(udid));
|
|
2327
2242
|
await mkdir(lockRoot, { recursive: true });
|
|
2328
2243
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
2329
2244
|
try {
|
|
@@ -2347,7 +2262,7 @@ async function reserveSimulatorUdid(udid, options = {}) {
|
|
|
2347
2262
|
};
|
|
2348
2263
|
try {
|
|
2349
2264
|
await writeFile(
|
|
2350
|
-
|
|
2265
|
+
path5.join(lockPath, SIMULATOR_LOCK_OWNER_FILE),
|
|
2351
2266
|
`${JSON.stringify({ pid: process.pid, udid, createdAt: (/* @__PURE__ */ new Date()).toISOString() })}
|
|
2352
2267
|
`,
|
|
2353
2268
|
{ flag: "wx" }
|
|
@@ -2539,15 +2454,15 @@ var WdaTransport = class {
|
|
|
2539
2454
|
* {@link WdaHttpError} on a non-2xx response, or a timeout error when the
|
|
2540
2455
|
* per-request deadline elapses.
|
|
2541
2456
|
*/
|
|
2542
|
-
async requestFull(method,
|
|
2457
|
+
async requestFull(method, path18, body, timeoutMs) {
|
|
2543
2458
|
const requestTimeoutMs = timeoutMs ?? this.requestTimeoutMs;
|
|
2544
2459
|
const controller = new AbortController();
|
|
2545
2460
|
const timer = setTimeout(() => controller.abort(), requestTimeoutMs);
|
|
2546
2461
|
timer.unref?.();
|
|
2547
|
-
const url = `${this.baseUrl}${
|
|
2462
|
+
const url = `${this.baseUrl}${path18}`;
|
|
2548
2463
|
const timeoutError = () => {
|
|
2549
2464
|
const shown = requestTimeoutMs >= 1e3 ? `${Math.round(requestTimeoutMs / 1e3)}s` : `${requestTimeoutMs}ms`;
|
|
2550
|
-
return new Error(`WebDriverAgent request ${method} ${
|
|
2465
|
+
return new Error(`WebDriverAgent request ${method} ${path18} timed out after ${shown}`);
|
|
2551
2466
|
};
|
|
2552
2467
|
let response;
|
|
2553
2468
|
try {
|
|
@@ -2579,7 +2494,7 @@ var WdaTransport = class {
|
|
|
2579
2494
|
if (!response.ok) {
|
|
2580
2495
|
const wdError = extractWebdriverError2(parsed);
|
|
2581
2496
|
throw new WdaHttpError(
|
|
2582
|
-
`WebDriverAgent ${method} ${
|
|
2497
|
+
`WebDriverAgent ${method} ${path18} failed (${response.status})${wdError ? `: ${wdError}` : ""}`,
|
|
2583
2498
|
response.status,
|
|
2584
2499
|
wdError
|
|
2585
2500
|
);
|
|
@@ -2587,8 +2502,8 @@ var WdaTransport = class {
|
|
|
2587
2502
|
return parsed;
|
|
2588
2503
|
}
|
|
2589
2504
|
/** Like {@link requestFull} but returns just the `value` field. */
|
|
2590
|
-
async request(method,
|
|
2591
|
-
const parsed = await this.requestFull(method,
|
|
2505
|
+
async request(method, path18, body, timeoutMs) {
|
|
2506
|
+
const parsed = await this.requestFull(method, path18, body, timeoutMs);
|
|
2592
2507
|
return parsed?.value;
|
|
2593
2508
|
}
|
|
2594
2509
|
};
|
|
@@ -2664,8 +2579,8 @@ function sleep2(ms) {
|
|
|
2664
2579
|
}
|
|
2665
2580
|
function createWdaAgentClient(transport, sessionId) {
|
|
2666
2581
|
const base = `/session/${sessionId}`;
|
|
2667
|
-
async function locate(query,
|
|
2668
|
-
return transport.request("POST", `${base}${
|
|
2582
|
+
async function locate(query, path18) {
|
|
2583
|
+
return transport.request("POST", `${base}${path18}`, iosQueryToLocator(query));
|
|
2669
2584
|
}
|
|
2670
2585
|
return {
|
|
2671
2586
|
async findElement(query) {
|
|
@@ -2695,12 +2610,29 @@ function createWdaAgentClient(transport, sessionId) {
|
|
|
2695
2610
|
const value = await transport.request("GET", `${base}/element/${elementId}/text`);
|
|
2696
2611
|
return typeof value === "string" ? value : value == null ? null : String(value);
|
|
2697
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
|
+
},
|
|
2698
2623
|
async sendKeys(keys) {
|
|
2699
2624
|
await transport.request("POST", `${base}/wda/keys`, { value: keys });
|
|
2700
2625
|
},
|
|
2701
2626
|
async homescreen() {
|
|
2702
2627
|
await transport.request("POST", "/wda/homescreen", {});
|
|
2703
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
|
+
},
|
|
2704
2636
|
async source() {
|
|
2705
2637
|
const value = await transport.request("GET", "/source");
|
|
2706
2638
|
if (typeof value !== "string") {
|
|
@@ -2718,8 +2650,8 @@ function createWdaAgentClient(transport, sessionId) {
|
|
|
2718
2650
|
import { createRequire as createRequire2 } from "module";
|
|
2719
2651
|
import { execFile as execFile4 } from "child_process";
|
|
2720
2652
|
import fs5 from "fs";
|
|
2721
|
-
import
|
|
2722
|
-
import
|
|
2653
|
+
import os4 from "os";
|
|
2654
|
+
import path6 from "path";
|
|
2723
2655
|
var WDA_RUNNER_BUNDLE_ID = "com.facebook.WebDriverAgentRunner.xctrunner";
|
|
2724
2656
|
var WDA_USE_PORT_ENV = "USE_PORT";
|
|
2725
2657
|
var PREPARED_XCTESTRUN_PREFIX = "prowl-wda-xctestrun-";
|
|
@@ -2745,19 +2677,19 @@ function resolveWdaProject(requireFn = createRequire2(import.meta.url)) {
|
|
|
2745
2677
|
"The iOS target requires the `appium-webdriveragent` package (its WDA Xcode project). It is an optional dependency of prowl-tools; it may have been skipped (--omit=optional) or failed to install. Restore it for a global Prowl install with: npm install -g appium-webdriveragent@16.4.0. If Prowl is installed locally in a project, run: npm install appium-webdriveragent@16.4.0"
|
|
2746
2678
|
);
|
|
2747
2679
|
}
|
|
2748
|
-
const pkgDir =
|
|
2680
|
+
const pkgDir = path6.dirname(pkgJsonPath);
|
|
2749
2681
|
const version = requireFn(pkgJsonPath).version;
|
|
2750
|
-
const projectPath =
|
|
2682
|
+
const projectPath = path6.join(pkgDir, "WebDriverAgent.xcodeproj");
|
|
2751
2683
|
if (!fs5.existsSync(projectPath)) {
|
|
2752
2684
|
throw new Error(`Expected WebDriverAgent project is missing: ${projectPath}. Reinstall dependencies.`);
|
|
2753
2685
|
}
|
|
2754
2686
|
return { projectPath, version };
|
|
2755
2687
|
}
|
|
2756
|
-
function wdaCacheDir(wdaVersion, xcode, homeDir =
|
|
2757
|
-
return
|
|
2688
|
+
function wdaCacheDir(wdaVersion, xcode, homeDir = os4.homedir()) {
|
|
2689
|
+
return path6.join(homeDir, ".prowl", "wda", `${wdaVersion}-xcode${xcode}`);
|
|
2758
2690
|
}
|
|
2759
2691
|
function productsDir(derivedDataPath) {
|
|
2760
|
-
return
|
|
2692
|
+
return path6.join(derivedDataPath, "Build", "Products");
|
|
2761
2693
|
}
|
|
2762
2694
|
function findXctestrunIn(dir) {
|
|
2763
2695
|
let entries;
|
|
@@ -2767,7 +2699,7 @@ function findXctestrunIn(dir) {
|
|
|
2767
2699
|
return null;
|
|
2768
2700
|
}
|
|
2769
2701
|
const match = entries.filter((name) => name.endsWith(".xctestrun") && !name.startsWith(PREPARED_XCTESTRUN_PREFIX)).sort()[0];
|
|
2770
|
-
return match ?
|
|
2702
|
+
return match ? path6.join(dir, match) : null;
|
|
2771
2703
|
}
|
|
2772
2704
|
function resolveOverrideXctestrun(override) {
|
|
2773
2705
|
if (!fs5.existsSync(override)) {
|
|
@@ -2779,7 +2711,7 @@ function resolveOverrideXctestrun(override) {
|
|
|
2779
2711
|
const candidates = [];
|
|
2780
2712
|
const stat = fs5.statSync(override);
|
|
2781
2713
|
if (override.endsWith(".app")) {
|
|
2782
|
-
candidates.push(
|
|
2714
|
+
candidates.push(path6.dirname(path6.dirname(override)));
|
|
2783
2715
|
} else if (stat.isDirectory()) {
|
|
2784
2716
|
candidates.push(override, productsDir(override));
|
|
2785
2717
|
}
|
|
@@ -2796,7 +2728,7 @@ function resolveOverrideXctestrun(override) {
|
|
|
2796
2728
|
async function resolveWdaTestRun(options = {}) {
|
|
2797
2729
|
const runner = options.runner ?? execFileXcrunRunner;
|
|
2798
2730
|
const env = options.env ?? process.env;
|
|
2799
|
-
const homeDir = options.homeDir ??
|
|
2731
|
+
const homeDir = options.homeDir ?? os4.homedir();
|
|
2800
2732
|
const log = options.logger ?? ((message) => process.stderr.write(`${message}
|
|
2801
2733
|
`));
|
|
2802
2734
|
const override = env.PROWL_WDA_RUNNER;
|
|
@@ -2912,8 +2844,8 @@ var defaultWdaTestRunPreparer = async ({ xctestrunPath, port }) => {
|
|
|
2912
2844
|
}
|
|
2913
2845
|
const injected = injectUsePortIntoXctestrun(parsed, port);
|
|
2914
2846
|
const stem = `${PREPARED_XCTESTRUN_PREFIX}${port}-${process.pid}`;
|
|
2915
|
-
const outPath =
|
|
2916
|
-
const jsonPath =
|
|
2847
|
+
const outPath = path6.join(path6.dirname(xctestrunPath), `${stem}.xctestrun`);
|
|
2848
|
+
const jsonPath = path6.join(os4.tmpdir(), `${stem}.json`);
|
|
2917
2849
|
fs5.writeFileSync(jsonPath, JSON.stringify(injected));
|
|
2918
2850
|
try {
|
|
2919
2851
|
await runPlutil(["-convert", "xml1", jsonPath, "-o", outPath]);
|
|
@@ -2926,7 +2858,7 @@ function cleanupPreparedTestRun(preparedPath) {
|
|
|
2926
2858
|
if (!preparedPath) {
|
|
2927
2859
|
return;
|
|
2928
2860
|
}
|
|
2929
|
-
if (!
|
|
2861
|
+
if (!path6.basename(preparedPath).startsWith(PREPARED_XCTESTRUN_PREFIX)) {
|
|
2930
2862
|
return;
|
|
2931
2863
|
}
|
|
2932
2864
|
fs5.rmSync(preparedPath, { force: true });
|
|
@@ -2951,7 +2883,7 @@ async function resolveBundleId(app, runner, udid, coldStart, allowedApps) {
|
|
|
2951
2883
|
}
|
|
2952
2884
|
return app;
|
|
2953
2885
|
}
|
|
2954
|
-
const appPath =
|
|
2886
|
+
const appPath = path6.resolve(app);
|
|
2955
2887
|
if (!fs5.existsSync(appPath)) {
|
|
2956
2888
|
throw new Error(`.app bundle not found: ${appPath}`);
|
|
2957
2889
|
}
|
|
@@ -3110,14 +3042,14 @@ async function healSelector(probe, selector, options) {
|
|
|
3110
3042
|
|
|
3111
3043
|
// src/runner/history.ts
|
|
3112
3044
|
import fs6 from "fs";
|
|
3113
|
-
import
|
|
3045
|
+
import path7 from "path";
|
|
3114
3046
|
var HISTORY_FILE = "history.json";
|
|
3115
3047
|
var LOCK_FILE_SUFFIX = ".lock";
|
|
3116
3048
|
var LOCK_RETRY_MS = 10;
|
|
3117
3049
|
var LOCK_TIMEOUT_MS = 5e3;
|
|
3118
3050
|
var SLEEP_BUFFER = new Int32Array(new SharedArrayBuffer(4));
|
|
3119
3051
|
function historyPath(configDir) {
|
|
3120
|
-
return
|
|
3052
|
+
return path7.join(configDir, HISTORY_FILE);
|
|
3121
3053
|
}
|
|
3122
3054
|
function isHistoryEntry(value) {
|
|
3123
3055
|
if (!value || typeof value !== "object") {
|
|
@@ -3171,7 +3103,7 @@ function sleepSync(ms) {
|
|
|
3171
3103
|
function withHistoryLock(configDir, fn) {
|
|
3172
3104
|
const filePath = historyPath(configDir);
|
|
3173
3105
|
const lockPath = `${filePath}${LOCK_FILE_SUFFIX}`;
|
|
3174
|
-
fs6.mkdirSync(
|
|
3106
|
+
fs6.mkdirSync(path7.dirname(filePath), { recursive: true });
|
|
3175
3107
|
const startedAt = Date.now();
|
|
3176
3108
|
while (Date.now() - startedAt < LOCK_TIMEOUT_MS) {
|
|
3177
3109
|
let fd;
|
|
@@ -3212,11 +3144,11 @@ function appendEntry(configDir, entry, maxRuns) {
|
|
|
3212
3144
|
|
|
3213
3145
|
// src/runner/index.ts
|
|
3214
3146
|
import fs12 from "fs";
|
|
3215
|
-
import
|
|
3147
|
+
import path13 from "path";
|
|
3216
3148
|
|
|
3217
3149
|
// src/browser/playwright-driver.ts
|
|
3218
3150
|
import fs7 from "fs";
|
|
3219
|
-
import
|
|
3151
|
+
import path8 from "path";
|
|
3220
3152
|
import {
|
|
3221
3153
|
chromium,
|
|
3222
3154
|
firefox,
|
|
@@ -3249,7 +3181,7 @@ async function launchBrowser(options) {
|
|
|
3249
3181
|
}
|
|
3250
3182
|
}
|
|
3251
3183
|
if (options.recordHar) {
|
|
3252
|
-
contextOptions.recordHar = { path:
|
|
3184
|
+
contextOptions.recordHar = { path: path8.join(options.runDir, "network.har") };
|
|
3253
3185
|
}
|
|
3254
3186
|
const context = await browser.newContext(contextOptions);
|
|
3255
3187
|
const page = await context.newPage();
|
|
@@ -3257,7 +3189,7 @@ async function launchBrowser(options) {
|
|
|
3257
3189
|
page.setDefaultNavigationTimeout(options.timeout);
|
|
3258
3190
|
let tracePath;
|
|
3259
3191
|
if (options.trace) {
|
|
3260
|
-
tracePath =
|
|
3192
|
+
tracePath = path8.join(options.runDir, "trace.zip");
|
|
3261
3193
|
await context.tracing.start({ screenshots: true, snapshots: true, sources: true });
|
|
3262
3194
|
}
|
|
3263
3195
|
return { browser, context, page, tracePath };
|
|
@@ -3355,6 +3287,16 @@ function createPlaywrightDriver(page) {
|
|
|
3355
3287
|
async hover(selector) {
|
|
3356
3288
|
await page.locator(selector).hover();
|
|
3357
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
|
+
},
|
|
3358
3300
|
async scrollIntoView(selector) {
|
|
3359
3301
|
await page.locator(selector).scrollIntoViewIfNeeded();
|
|
3360
3302
|
},
|
|
@@ -3436,7 +3378,7 @@ function createPlaywrightDriver(page) {
|
|
|
3436
3378
|
|
|
3437
3379
|
// src/runner/steps.ts
|
|
3438
3380
|
import fs8 from "fs";
|
|
3439
|
-
import
|
|
3381
|
+
import path9 from "path";
|
|
3440
3382
|
|
|
3441
3383
|
// src/runner/policy.ts
|
|
3442
3384
|
var ALWAYS_ALLOWED_PROTOCOLS = ["about:", "data:"];
|
|
@@ -4080,11 +4022,14 @@ function toVisibilitySelector(value) {
|
|
|
4080
4022
|
if (looksLikeSelector(value)) return value;
|
|
4081
4023
|
return textContainsSelector(value);
|
|
4082
4024
|
}
|
|
4025
|
+
function countVisible(driver, selector) {
|
|
4026
|
+
return driver.visibleCount?.(selector) ?? driver.count(selector);
|
|
4027
|
+
}
|
|
4083
4028
|
async function runInlineAssert(driver, policy, assertion) {
|
|
4084
4029
|
if (assertion.visible !== void 0) {
|
|
4085
4030
|
const selector = toVisibilitySelector(assertion.visible);
|
|
4086
4031
|
policy.assertAllowedSelector(selector);
|
|
4087
|
-
const count = await driver
|
|
4032
|
+
const count = await countVisible(driver, selector);
|
|
4088
4033
|
if (count === 0) {
|
|
4089
4034
|
throw new Error(`Expected visible: ${assertion.visible}`);
|
|
4090
4035
|
}
|
|
@@ -4093,7 +4038,7 @@ async function runInlineAssert(driver, policy, assertion) {
|
|
|
4093
4038
|
if (assertion.notVisible !== void 0) {
|
|
4094
4039
|
const selector = toVisibilitySelector(assertion.notVisible);
|
|
4095
4040
|
policy.assertAllowedSelector(selector);
|
|
4096
|
-
const count = await driver
|
|
4041
|
+
const count = await countVisible(driver, selector);
|
|
4097
4042
|
if (count > 0) {
|
|
4098
4043
|
throw new Error(`Expected not visible: ${assertion.notVisible}`);
|
|
4099
4044
|
}
|
|
@@ -4116,7 +4061,7 @@ async function runInlineAssert(driver, policy, assertion) {
|
|
|
4116
4061
|
throw new Error("assert step is missing an assertion type");
|
|
4117
4062
|
}
|
|
4118
4063
|
function screenshotPath(screenshotsDir, fileName) {
|
|
4119
|
-
return
|
|
4064
|
+
return path9.join(screenshotsDir, fileName);
|
|
4120
4065
|
}
|
|
4121
4066
|
function stepPath(prefix, index) {
|
|
4122
4067
|
return prefix ? `${prefix}.${index}` : `${index}`;
|
|
@@ -4133,7 +4078,7 @@ function validateDownloadFilename(suggestedFilename) {
|
|
|
4133
4078
|
const safeFilename = suggestedFilename.trim();
|
|
4134
4079
|
const allowedFilenamePattern = /^[^<>:"/\\|?*]+$/;
|
|
4135
4080
|
const hasControlCharacter = Array.from(safeFilename).some((char) => char.charCodeAt(0) < 32);
|
|
4136
|
-
if (safeFilename.length === 0 || safeFilename !== suggestedFilename || safeFilename !==
|
|
4081
|
+
if (safeFilename.length === 0 || safeFilename !== suggestedFilename || safeFilename !== path9.basename(safeFilename) || safeFilename.includes("..") || /[/\\]/.test(safeFilename) || hasControlCharacter || !allowedFilenamePattern.test(safeFilename)) {
|
|
4137
4082
|
throw new Error(`Invalid download filename: "${suggestedFilename}"`);
|
|
4138
4083
|
}
|
|
4139
4084
|
return safeFilename;
|
|
@@ -4313,7 +4258,7 @@ var STEP_HANDLERS = {
|
|
|
4313
4258
|
if (!("setInputFiles" in h.step)) unknownStep();
|
|
4314
4259
|
const resolvedInput = await h.policy.resolveActionSelector(h.step.setInputFiles.selector);
|
|
4315
4260
|
const rawFiles = h.step.setInputFiles.files;
|
|
4316
|
-
const resolveFile = (f) =>
|
|
4261
|
+
const resolveFile = (f) => path9.isAbsolute(f) ? f : path9.join(h.context.configDir, f);
|
|
4317
4262
|
const resolvedFiles = Array.isArray(rawFiles) ? rawFiles.map(resolveFile) : resolveFile(rawFiles);
|
|
4318
4263
|
await h.driver.setInputFiles(resolvedInput.selector, resolvedFiles);
|
|
4319
4264
|
h.policy.ensureLocationAllowed(h.driver);
|
|
@@ -4483,25 +4428,21 @@ var STEP_HANDLERS = {
|
|
|
4483
4428
|
}
|
|
4484
4429
|
},
|
|
4485
4430
|
scroll: {
|
|
4486
|
-
|
|
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"],
|
|
4487
4435
|
run: async (h) => {
|
|
4488
4436
|
if (!("scroll" in h.step)) unknownStep();
|
|
4489
|
-
const amount = h.step.scroll
|
|
4490
|
-
|
|
4491
|
-
up: [0, -amount],
|
|
4492
|
-
down: [0, amount],
|
|
4493
|
-
left: [-amount, 0],
|
|
4494
|
-
right: [amount, 0]
|
|
4495
|
-
};
|
|
4496
|
-
const [x, y] = scrollMap[h.step.scroll.direction];
|
|
4497
|
-
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);
|
|
4498
4439
|
return {
|
|
4499
4440
|
kind: "result",
|
|
4500
4441
|
result: {
|
|
4501
4442
|
type: "scroll",
|
|
4502
4443
|
status: "pass",
|
|
4503
4444
|
durationMs: Date.now() - h.stepStart,
|
|
4504
|
-
value: `${
|
|
4445
|
+
value: amount === void 0 ? direction : `${direction} ${amount}px`
|
|
4505
4446
|
}
|
|
4506
4447
|
};
|
|
4507
4448
|
}
|
|
@@ -4547,7 +4488,7 @@ var STEP_HANDLERS = {
|
|
|
4547
4488
|
const condition = h.step.if;
|
|
4548
4489
|
const selector = condition.visible ?? condition.notVisible;
|
|
4549
4490
|
h.policy.assertAllowedSelector(selector);
|
|
4550
|
-
const count = await h.driver
|
|
4491
|
+
const count = await countVisible(h.driver, selector);
|
|
4551
4492
|
const conditionMet = condition.visible !== void 0 ? count > 0 : count === 0;
|
|
4552
4493
|
if (conditionMet) {
|
|
4553
4494
|
const subResult = await h.executeNested({
|
|
@@ -4634,7 +4575,7 @@ var STEP_HANDLERS = {
|
|
|
4634
4575
|
const whileSelector = repeat.while.visible ?? repeat.while.notVisible;
|
|
4635
4576
|
h.policy.assertAllowedSelector(whileSelector);
|
|
4636
4577
|
for (let i = 0; i < maxIter; i++) {
|
|
4637
|
-
const whileCount = await h.driver
|
|
4578
|
+
const whileCount = await countVisible(h.driver, whileSelector);
|
|
4638
4579
|
const shouldContinue = repeat.while.visible !== void 0 ? whileCount > 0 : whileCount === 0;
|
|
4639
4580
|
if (!shouldContinue) break;
|
|
4640
4581
|
totalSubSteps += repeat.steps.length;
|
|
@@ -4675,11 +4616,11 @@ var STEP_HANDLERS = {
|
|
|
4675
4616
|
if (!responseFile) {
|
|
4676
4617
|
throw new Error("mock.response must include either body or file");
|
|
4677
4618
|
}
|
|
4678
|
-
const candidateFilePath =
|
|
4679
|
-
const resolvedConfigDir =
|
|
4680
|
-
const resolvedFilePath =
|
|
4681
|
-
const relativePath =
|
|
4682
|
-
const isWithinConfigDir = relativePath === "" || relativePath !== ".." && !relativePath.startsWith(`..${
|
|
4619
|
+
const candidateFilePath = path9.isAbsolute(responseFile) ? responseFile : path9.join(h.context.configDir, responseFile);
|
|
4620
|
+
const resolvedConfigDir = path9.resolve(h.context.configDir);
|
|
4621
|
+
const resolvedFilePath = path9.resolve(candidateFilePath);
|
|
4622
|
+
const relativePath = path9.relative(resolvedConfigDir, resolvedFilePath);
|
|
4623
|
+
const isWithinConfigDir = relativePath === "" || relativePath !== ".." && !relativePath.startsWith(`..${path9.sep}`) && !path9.isAbsolute(relativePath);
|
|
4683
4624
|
if (!isWithinConfigDir) {
|
|
4684
4625
|
throw new Error("mock.response.file must resolve within config directory");
|
|
4685
4626
|
}
|
|
@@ -4746,7 +4687,7 @@ var STEP_HANDLERS = {
|
|
|
4746
4687
|
capabilities: ["evaluate"],
|
|
4747
4688
|
run: async (h) => {
|
|
4748
4689
|
if (!("runScript" in h.step)) unknownStep();
|
|
4749
|
-
const filePath =
|
|
4690
|
+
const filePath = path9.isAbsolute(h.step.runScript.file) ? h.step.runScript.file : path9.join(h.context.configDir, h.step.runScript.file);
|
|
4750
4691
|
const fileContents = fs8.readFileSync(filePath, "utf-8");
|
|
4751
4692
|
await h.driver.evaluate(fileContents);
|
|
4752
4693
|
return {
|
|
@@ -4768,11 +4709,11 @@ var STEP_HANDLERS = {
|
|
|
4768
4709
|
const name = h.step.assertScreenshot.name;
|
|
4769
4710
|
const threshold = h.step.assertScreenshot.threshold ?? 0.1;
|
|
4770
4711
|
const baselineDir = ensureBaselineDir(h.context.configDir);
|
|
4771
|
-
const baselinePath =
|
|
4772
|
-
const currentScreenshotPath =
|
|
4773
|
-
fs8.mkdirSync(
|
|
4712
|
+
const baselinePath = path9.join(baselineDir, `${name}.png`);
|
|
4713
|
+
const currentScreenshotPath = path9.join(h.context.runDir, "screenshots", `${name}-current.png`);
|
|
4714
|
+
fs8.mkdirSync(path9.dirname(currentScreenshotPath), { recursive: true });
|
|
4774
4715
|
await h.driver.screenshot({ path: currentScreenshotPath, fullPage: true });
|
|
4775
|
-
h.screenshots.push(
|
|
4716
|
+
h.screenshots.push(path9.join("screenshots", `${name}-current.png`));
|
|
4776
4717
|
if (!fs8.existsSync(baselinePath)) {
|
|
4777
4718
|
fs8.copyFileSync(currentScreenshotPath, baselinePath);
|
|
4778
4719
|
return {
|
|
@@ -4780,7 +4721,7 @@ var STEP_HANDLERS = {
|
|
|
4780
4721
|
result: { type: "assertScreenshot", status: "pass", durationMs: Date.now() - h.stepStart, value: "baseline created" }
|
|
4781
4722
|
};
|
|
4782
4723
|
}
|
|
4783
|
-
const diffPath =
|
|
4724
|
+
const diffPath = path9.join(h.context.runDir, "screenshots", `${name}-diff.png`);
|
|
4784
4725
|
const comparison = await compareScreenshots(baselinePath, currentScreenshotPath, diffPath, threshold);
|
|
4785
4726
|
if (comparison.match) {
|
|
4786
4727
|
return {
|
|
@@ -4793,7 +4734,7 @@ var STEP_HANDLERS = {
|
|
|
4793
4734
|
}
|
|
4794
4735
|
};
|
|
4795
4736
|
}
|
|
4796
|
-
h.screenshots.push(
|
|
4737
|
+
h.screenshots.push(path9.join("screenshots", `${name}-diff.png`));
|
|
4797
4738
|
throw new Error(
|
|
4798
4739
|
`Visual regression: ${(comparison.diffPercentage * 100).toFixed(2)}% diff exceeds threshold ${(threshold * 100).toFixed(0)}%`
|
|
4799
4740
|
);
|
|
@@ -4819,7 +4760,7 @@ var STEP_HANDLERS = {
|
|
|
4819
4760
|
}
|
|
4820
4761
|
const fileName = `assertWithAI_step_${h.index + 1}.png`;
|
|
4821
4762
|
const relative = await h.addScreenshot(fileName);
|
|
4822
|
-
const screenshotFullPath =
|
|
4763
|
+
const screenshotFullPath = path9.join(h.context.runDir, relative);
|
|
4823
4764
|
const imageBase64 = fs8.readFileSync(screenshotFullPath).toString("base64");
|
|
4824
4765
|
const assertVision = h.context.assertVision ?? assertWithAiVision;
|
|
4825
4766
|
const verdict = await assertVision(
|
|
@@ -4877,7 +4818,7 @@ var STEP_HANDLERS = {
|
|
|
4877
4818
|
`Download filename mismatch: expected "${opts.filename}", got "${suggestedFilename}"`
|
|
4878
4819
|
);
|
|
4879
4820
|
}
|
|
4880
|
-
const savePath =
|
|
4821
|
+
const savePath = path9.join(h.context.runDir, suggestedFilename);
|
|
4881
4822
|
await download.saveAs(savePath);
|
|
4882
4823
|
return {
|
|
4883
4824
|
kind: "result",
|
|
@@ -4906,7 +4847,7 @@ async function executeSteps(context) {
|
|
|
4906
4847
|
maxSteps: context.maxSteps,
|
|
4907
4848
|
selfHealing: context.selfHealing
|
|
4908
4849
|
});
|
|
4909
|
-
const screenshotsDir =
|
|
4850
|
+
const screenshotsDir = path9.join(context.runDir, "screenshots");
|
|
4910
4851
|
fs8.mkdirSync(screenshotsDir, { recursive: true });
|
|
4911
4852
|
const currentHuntName = context.huntStack?.[context.huntStack.length - 1];
|
|
4912
4853
|
policy.assertWithinMaxSteps(context.steps.length, currentHuntName);
|
|
@@ -4917,7 +4858,7 @@ async function executeSteps(context) {
|
|
|
4917
4858
|
const addScreenshot = async (fileName) => {
|
|
4918
4859
|
const fullPath = screenshotPath(screenshotsDir, fileName);
|
|
4919
4860
|
await captureScreenshot2(driver, fullPath);
|
|
4920
|
-
const relative =
|
|
4861
|
+
const relative = path9.join("screenshots", fileName);
|
|
4921
4862
|
screenshots.push(relative);
|
|
4922
4863
|
return relative;
|
|
4923
4864
|
};
|
|
@@ -5005,12 +4946,12 @@ async function executeSteps(context) {
|
|
|
5005
4946
|
return { results, screenshots, failed: false };
|
|
5006
4947
|
}
|
|
5007
4948
|
async function captureFinalScreenshot(page, runDir) {
|
|
5008
|
-
const screenshotsDir =
|
|
4949
|
+
const screenshotsDir = path9.join(runDir, "screenshots");
|
|
5009
4950
|
fs8.mkdirSync(screenshotsDir, { recursive: true });
|
|
5010
4951
|
const fileName = "final.png";
|
|
5011
4952
|
const filePath = screenshotPath(screenshotsDir, fileName);
|
|
5012
4953
|
await captureScreenshot2(page, filePath);
|
|
5013
|
-
return
|
|
4954
|
+
return path9.join("screenshots", fileName);
|
|
5014
4955
|
}
|
|
5015
4956
|
|
|
5016
4957
|
// src/runner/assertions.ts
|
|
@@ -5132,6 +5073,59 @@ async function evaluateAssertions(options) {
|
|
|
5132
5073
|
}
|
|
5133
5074
|
return results;
|
|
5134
5075
|
}
|
|
5076
|
+
async function evaluateNativeAssertions(options) {
|
|
5077
|
+
const assertions = mergeAssertions(options.config, options.huntAssertions);
|
|
5078
|
+
const authoredTypes = new Set(
|
|
5079
|
+
(options.huntAssertions ?? []).map((assertion) => Object.keys(assertion)[0])
|
|
5080
|
+
);
|
|
5081
|
+
const results = [];
|
|
5082
|
+
const warnings = [];
|
|
5083
|
+
for (const assertion of assertions) {
|
|
5084
|
+
const type = Object.keys(assertion)[0] ?? "assertion";
|
|
5085
|
+
try {
|
|
5086
|
+
if ("selectorExists" in assertion) {
|
|
5087
|
+
options.assertAllowedSelector?.(assertion.selectorExists);
|
|
5088
|
+
const count = await options.driver.count(assertion.selectorExists);
|
|
5089
|
+
results.push({
|
|
5090
|
+
type: "selectorExists",
|
|
5091
|
+
value: assertion.selectorExists,
|
|
5092
|
+
status: count > 0 ? "pass" : "fail",
|
|
5093
|
+
error: count > 0 ? void 0 : "Selector not found"
|
|
5094
|
+
});
|
|
5095
|
+
continue;
|
|
5096
|
+
}
|
|
5097
|
+
if ("selectorNotExists" in assertion) {
|
|
5098
|
+
options.assertAllowedSelector?.(assertion.selectorNotExists);
|
|
5099
|
+
const count = await options.driver.count(assertion.selectorNotExists);
|
|
5100
|
+
results.push({
|
|
5101
|
+
type: "selectorNotExists",
|
|
5102
|
+
value: assertion.selectorNotExists,
|
|
5103
|
+
status: count === 0 ? "pass" : "fail",
|
|
5104
|
+
error: count === 0 ? void 0 : "Selector exists"
|
|
5105
|
+
});
|
|
5106
|
+
continue;
|
|
5107
|
+
}
|
|
5108
|
+
const rawValue = assertion[type];
|
|
5109
|
+
results.push({
|
|
5110
|
+
type,
|
|
5111
|
+
value: rawValue,
|
|
5112
|
+
status: "skipped",
|
|
5113
|
+
error: `skipped (web-only): not supported on the ${options.targetLabel} target`
|
|
5114
|
+
});
|
|
5115
|
+
if (authoredTypes.has(type)) {
|
|
5116
|
+
warnings.push(`${type} is web-only; skipped on ${options.targetLabel} target`);
|
|
5117
|
+
}
|
|
5118
|
+
} catch (error) {
|
|
5119
|
+
const detail = error instanceof Error ? error.message : "no error details";
|
|
5120
|
+
results.push({
|
|
5121
|
+
type,
|
|
5122
|
+
status: "fail",
|
|
5123
|
+
error: `Native assertion "${type}" failed: ${detail}`
|
|
5124
|
+
});
|
|
5125
|
+
}
|
|
5126
|
+
}
|
|
5127
|
+
return { results, warnings };
|
|
5128
|
+
}
|
|
5135
5129
|
|
|
5136
5130
|
// src/runner/tracing.ts
|
|
5137
5131
|
var DEFAULT_TRACE_HEADER = "traceparent";
|
|
@@ -5171,17 +5165,17 @@ function captureTraceCorrelation(response, headerName, sink, redactionValues = [
|
|
|
5171
5165
|
|
|
5172
5166
|
// src/reporter/result.ts
|
|
5173
5167
|
import fs9 from "fs";
|
|
5174
|
-
import
|
|
5168
|
+
import path10 from "path";
|
|
5175
5169
|
function writeResult(runDir, result) {
|
|
5176
5170
|
const fileName = "result.json";
|
|
5177
|
-
const fullPath =
|
|
5171
|
+
const fullPath = path10.join(runDir, fileName);
|
|
5178
5172
|
fs9.writeFileSync(fullPath, JSON.stringify(result, null, 2));
|
|
5179
5173
|
return fileName;
|
|
5180
5174
|
}
|
|
5181
5175
|
|
|
5182
5176
|
// src/reporter/summary.ts
|
|
5183
5177
|
import fs10 from "fs";
|
|
5184
|
-
import
|
|
5178
|
+
import path11 from "path";
|
|
5185
5179
|
function escapeMd(text) {
|
|
5186
5180
|
return text.replace(/([|`*_{}[\]()#+\-!\\])/g, "\\$1");
|
|
5187
5181
|
}
|
|
@@ -5259,7 +5253,7 @@ function writeSummary(runDir, result) {
|
|
|
5259
5253
|
}
|
|
5260
5254
|
}
|
|
5261
5255
|
const fileName = "summary.md";
|
|
5262
|
-
const fullPath =
|
|
5256
|
+
const fullPath = path11.join(runDir, fileName);
|
|
5263
5257
|
fs10.writeFileSync(fullPath, `${lines.join("\n")}
|
|
5264
5258
|
`);
|
|
5265
5259
|
return fileName;
|
|
@@ -5267,20 +5261,21 @@ function writeSummary(runDir, result) {
|
|
|
5267
5261
|
|
|
5268
5262
|
// src/reporter/junit.ts
|
|
5269
5263
|
import fs11 from "fs";
|
|
5270
|
-
import
|
|
5264
|
+
import path12 from "path";
|
|
5271
5265
|
function escapeXml(text) {
|
|
5272
5266
|
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
5273
5267
|
}
|
|
5274
5268
|
function writeJunit(runDir, result) {
|
|
5275
5269
|
const totalTests = result.steps.length + result.assertions.length;
|
|
5276
5270
|
const failures = result.steps.filter((s) => s.status === "fail").length + result.assertions.filter((a) => a.status === "fail").length;
|
|
5271
|
+
const skipped = result.assertions.filter((a) => a.status === "skipped").length;
|
|
5277
5272
|
const timeSeconds = (result.durationMs / 1e3).toFixed(3);
|
|
5278
5273
|
const huntName = escapeXml(result.hunt);
|
|
5279
5274
|
const lines = [];
|
|
5280
5275
|
lines.push('<?xml version="1.0" encoding="UTF-8"?>');
|
|
5281
5276
|
lines.push("<testsuites>");
|
|
5282
5277
|
lines.push(
|
|
5283
|
-
` <testsuite name="${huntName}" tests="${totalTests}" failures="${failures}" errors="0" time="${timeSeconds}" timestamp="${escapeXml(result.startedAt)}">`
|
|
5278
|
+
` <testsuite name="${huntName}" tests="${totalTests}" failures="${failures}" errors="0" skipped="${skipped}" time="${timeSeconds}" timestamp="${escapeXml(result.startedAt)}">`
|
|
5284
5279
|
);
|
|
5285
5280
|
for (let i = 0; i < result.steps.length; i++) {
|
|
5286
5281
|
const step = result.steps[i];
|
|
@@ -5304,6 +5299,11 @@ function writeJunit(runDir, result) {
|
|
|
5304
5299
|
lines.push(` <testcase name="${caseName}" classname="${huntName}" time="0">`);
|
|
5305
5300
|
lines.push(` <failure message="${escapedFailureText}" type="assertion">${escapedFailureText}</failure>`);
|
|
5306
5301
|
lines.push(" </testcase>");
|
|
5302
|
+
} else if (assertion.status === "skipped") {
|
|
5303
|
+
const skipText = escapeXml(assertion.error ?? "skipped");
|
|
5304
|
+
lines.push(` <testcase name="${caseName}" classname="${huntName}" time="0">`);
|
|
5305
|
+
lines.push(` <skipped message="${skipText}"/>`);
|
|
5306
|
+
lines.push(" </testcase>");
|
|
5307
5307
|
} else {
|
|
5308
5308
|
lines.push(` <testcase name="${caseName}" classname="${huntName}" time="0"/>`);
|
|
5309
5309
|
}
|
|
@@ -5311,7 +5311,7 @@ function writeJunit(runDir, result) {
|
|
|
5311
5311
|
lines.push(" </testsuite>");
|
|
5312
5312
|
lines.push("</testsuites>");
|
|
5313
5313
|
const fileName = "junit.xml";
|
|
5314
|
-
const fullPath =
|
|
5314
|
+
const fullPath = path12.join(runDir, fileName);
|
|
5315
5315
|
fs11.writeFileSync(fullPath, `${lines.join("\n")}
|
|
5316
5316
|
`);
|
|
5317
5317
|
return fileName;
|
|
@@ -5354,11 +5354,11 @@ function parseViewportFlag(value) {
|
|
|
5354
5354
|
return value;
|
|
5355
5355
|
}
|
|
5356
5356
|
function resolvePath(configDir, inputPath) {
|
|
5357
|
-
if (
|
|
5357
|
+
if (path13.isAbsolute(inputPath)) {
|
|
5358
5358
|
return inputPath;
|
|
5359
5359
|
}
|
|
5360
|
-
const projectRoot =
|
|
5361
|
-
return
|
|
5360
|
+
const projectRoot = path13.dirname(configDir);
|
|
5361
|
+
return path13.join(projectRoot, inputPath);
|
|
5362
5362
|
}
|
|
5363
5363
|
function buildRunResult(options) {
|
|
5364
5364
|
return {
|
|
@@ -5377,7 +5377,7 @@ function buildRunResult(options) {
|
|
|
5377
5377
|
}
|
|
5378
5378
|
function writeConsoleLog(runDir, entries) {
|
|
5379
5379
|
const fileName = "console.log";
|
|
5380
|
-
const filePath =
|
|
5380
|
+
const filePath = path13.join(runDir, fileName);
|
|
5381
5381
|
const lines = entries.map((entry) => {
|
|
5382
5382
|
const location = entry.location ? ` (${entry.location})` : "";
|
|
5383
5383
|
return `[${entry.type}] ${entry.text}${location}`;
|
|
@@ -5390,7 +5390,7 @@ async function executeHuntAttempt(options, config, configDir, interpolatedHunt,
|
|
|
5390
5390
|
const headless = options.headed ? false : config.browser.headless;
|
|
5391
5391
|
const slowMo = options.slowMo ?? config.browser.slowMo;
|
|
5392
5392
|
const maxSteps = config.guardrails.maxSteps;
|
|
5393
|
-
const runDir =
|
|
5393
|
+
const runDir = path13.join(configDir, "runs", timestamp());
|
|
5394
5394
|
fs12.mkdirSync(runDir, { recursive: true });
|
|
5395
5395
|
const storageStatePath = config.auth.storageStatePath ? resolvePath(configDir, config.auth.storageStatePath) : void 0;
|
|
5396
5396
|
const engine = options.browser ?? config.browser.engine;
|
|
@@ -5574,7 +5574,7 @@ async function runHunt(options) {
|
|
|
5574
5574
|
}
|
|
5575
5575
|
async function executeNativeHuntAttempt(options, config, configDir, interpolatedHunt, redactedFillSteps, randomVars, allowedApps, native) {
|
|
5576
5576
|
const maxSteps = config.guardrails.maxSteps;
|
|
5577
|
-
const runDir =
|
|
5577
|
+
const runDir = path13.join(configDir, "runs", timestamp());
|
|
5578
5578
|
fs12.mkdirSync(runDir, { recursive: true });
|
|
5579
5579
|
const session = await native.launchSession();
|
|
5580
5580
|
let result;
|
|
@@ -5583,6 +5583,13 @@ async function executeNativeHuntAttempt(options, config, configDir, interpolated
|
|
|
5583
5583
|
const appIdentity = native.sessionAppIdentity(session);
|
|
5584
5584
|
const targetLabel = `${native.targetType}:${appIdentity}`;
|
|
5585
5585
|
const effectiveAllowedApps = [.../* @__PURE__ */ new Set([...allowedApps, native.targetApp, appIdentity])];
|
|
5586
|
+
const assertionPolicy = createRunPolicy(driver, {
|
|
5587
|
+
forbiddenSelectors: config.guardrails.forbiddenSelectors,
|
|
5588
|
+
allowedDomains: [],
|
|
5589
|
+
allowedApps: effectiveAllowedApps,
|
|
5590
|
+
maxSteps,
|
|
5591
|
+
selfHealing: config.guardrails.selfHealing
|
|
5592
|
+
});
|
|
5586
5593
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
5587
5594
|
const startTime = Date.now();
|
|
5588
5595
|
let stepResults = [];
|
|
@@ -5622,8 +5629,19 @@ async function executeNativeHuntAttempt(options, config, configDir, interpolated
|
|
|
5622
5629
|
} catch {
|
|
5623
5630
|
finalScreenshot = void 0;
|
|
5624
5631
|
}
|
|
5632
|
+
const { results: assertionResults, warnings: assertionWarnings } = await evaluateNativeAssertions({
|
|
5633
|
+
driver,
|
|
5634
|
+
config,
|
|
5635
|
+
huntAssertions: interpolatedHunt.assertions,
|
|
5636
|
+
assertAllowedSelector: assertionPolicy.assertAllowedSelector,
|
|
5637
|
+
targetLabel: nativeTargetLabel(native.targetType)
|
|
5638
|
+
});
|
|
5639
|
+
for (const warning of assertionWarnings) {
|
|
5640
|
+
console.warn(warning);
|
|
5641
|
+
}
|
|
5625
5642
|
const durationMs = Date.now() - startTime;
|
|
5626
|
-
const
|
|
5643
|
+
const assertionsFailed = assertionResults.some((assertion) => assertion.status === "fail");
|
|
5644
|
+
const status = stepFailed || assertionsFailed ? "fail" : "pass";
|
|
5627
5645
|
const artifacts = {
|
|
5628
5646
|
screenshots: finalScreenshot ? [...stepScreenshots, finalScreenshot] : stepScreenshots
|
|
5629
5647
|
};
|
|
@@ -5634,7 +5652,7 @@ async function executeNativeHuntAttempt(options, config, configDir, interpolated
|
|
|
5634
5652
|
hunt: options.huntName,
|
|
5635
5653
|
targetUrl: targetLabel,
|
|
5636
5654
|
steps: stepResults,
|
|
5637
|
-
assertions:
|
|
5655
|
+
assertions: assertionResults,
|
|
5638
5656
|
artifacts
|
|
5639
5657
|
});
|
|
5640
5658
|
result = writeReports(runDir, runResult, { junit: options.junit ?? config.artifacts.junit });
|
|
@@ -5747,7 +5765,6 @@ async function runNativeHunt(options, config, configDir, target, native) {
|
|
|
5747
5765
|
const hunt = loadHunt(options.huntName, configDir);
|
|
5748
5766
|
const { hunt: interpolatedHunt, redactedFillSteps, randomVars } = interpolateHunt(hunt, process.env);
|
|
5749
5767
|
assertStepsSupportedByTarget(interpolatedHunt.steps, native.targetType);
|
|
5750
|
-
assertHuntAssertionsSupportedByTarget(interpolatedHunt.assertions, native.targetType);
|
|
5751
5768
|
native.assertAppAllowed(config.guardrails.allowedApps, target);
|
|
5752
5769
|
const maxSteps = config.guardrails.maxSteps;
|
|
5753
5770
|
if (interpolatedHunt.steps.length > maxSteps) {
|
|
@@ -5788,7 +5805,7 @@ async function runNativeHunt(options, config, configDir, target, native) {
|
|
|
5788
5805
|
}
|
|
5789
5806
|
function recordHistory(configDir, outcome, maxRuns) {
|
|
5790
5807
|
try {
|
|
5791
|
-
const relativeRunDir =
|
|
5808
|
+
const relativeRunDir = path13.relative(configDir, outcome.runDir);
|
|
5792
5809
|
appendEntry(
|
|
5793
5810
|
configDir,
|
|
5794
5811
|
{
|
|
@@ -5910,7 +5927,7 @@ function clusterFailures(failures) {
|
|
|
5910
5927
|
|
|
5911
5928
|
// src/backlog/index.ts
|
|
5912
5929
|
import fs13 from "fs";
|
|
5913
|
-
import
|
|
5930
|
+
import path14 from "path";
|
|
5914
5931
|
|
|
5915
5932
|
// src/backlog/parse.ts
|
|
5916
5933
|
var MARKER_FP = /<!--\s*prowl:fp=([0-9a-f]+)/;
|
|
@@ -6016,7 +6033,7 @@ function buildFailure(hunt) {
|
|
|
6016
6033
|
if (!hunt.runDir) return failure;
|
|
6017
6034
|
let run;
|
|
6018
6035
|
try {
|
|
6019
|
-
const resultJson = readFileOrEmpty(
|
|
6036
|
+
const resultJson = readFileOrEmpty(path14.join(hunt.runDir, "result.json"));
|
|
6020
6037
|
if (!resultJson) return failure;
|
|
6021
6038
|
run = JSON.parse(resultJson);
|
|
6022
6039
|
} catch (error) {
|
|
@@ -6045,8 +6062,8 @@ function extractFailures(suiteResult) {
|
|
|
6045
6062
|
}
|
|
6046
6063
|
function updateBacklogFromSuite(suiteResult, options = {}) {
|
|
6047
6064
|
const projectRoot = options.projectRoot ?? process.cwd();
|
|
6048
|
-
const backlogPath = options.backlogPath ??
|
|
6049
|
-
const resolvedPath = options.resolvedPath ??
|
|
6065
|
+
const backlogPath = options.backlogPath ?? path14.join(projectRoot, "docs", "backlog.md");
|
|
6066
|
+
const resolvedPath = options.resolvedPath ?? path14.join(projectRoot, "docs", "resolved.md");
|
|
6050
6067
|
const date = options.date ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
6051
6068
|
const summary = { created: [], regressions: [], skipped: [], backlogPath };
|
|
6052
6069
|
const failures = extractFailures(suiteResult);
|
|
@@ -6078,18 +6095,18 @@ function updateBacklogFromSuite(suiteResult, options = {}) {
|
|
|
6078
6095
|
}
|
|
6079
6096
|
}
|
|
6080
6097
|
if (ticketsToAdd.length > 0) {
|
|
6081
|
-
fs13.mkdirSync(
|
|
6098
|
+
fs13.mkdirSync(path14.dirname(backlogPath), { recursive: true });
|
|
6082
6099
|
fs13.writeFileSync(backlogPath, insertTickets(backlogContent, ticketsToAdd));
|
|
6083
6100
|
}
|
|
6084
6101
|
return summary;
|
|
6085
6102
|
}
|
|
6086
6103
|
|
|
6087
6104
|
// src/runner/suite.ts
|
|
6088
|
-
import
|
|
6105
|
+
import path16 from "path";
|
|
6089
6106
|
|
|
6090
6107
|
// src/reporter/ci-summary.ts
|
|
6091
6108
|
import fs14 from "fs";
|
|
6092
|
-
import
|
|
6109
|
+
import path15 from "path";
|
|
6093
6110
|
import chalk from "chalk";
|
|
6094
6111
|
function countCiResults(results) {
|
|
6095
6112
|
return {
|
|
@@ -6154,7 +6171,7 @@ function writeCiResult(ciRunDir, results, startedAt, totalDurationMs, flaky = []
|
|
|
6154
6171
|
...clusters.length > 0 ? { clusters } : {}
|
|
6155
6172
|
};
|
|
6156
6173
|
fs14.mkdirSync(ciRunDir, { recursive: true });
|
|
6157
|
-
const filePath =
|
|
6174
|
+
const filePath = path15.join(ciRunDir, "ci-result.json");
|
|
6158
6175
|
fs14.writeFileSync(filePath, JSON.stringify(ciResult, null, 2) + "\n");
|
|
6159
6176
|
return filePath;
|
|
6160
6177
|
}
|
|
@@ -6340,7 +6357,7 @@ async function runSuite(options = {}) {
|
|
|
6340
6357
|
const clusters = clusterFailures(
|
|
6341
6358
|
extractFailures({ result: { hunts: results }, resultPath: null })
|
|
6342
6359
|
).filter((cluster) => cluster.count > 1);
|
|
6343
|
-
const ciRunDir =
|
|
6360
|
+
const ciRunDir = path16.join(configDir, "runs", timestamp("ci"));
|
|
6344
6361
|
const resultPath = writeCiResult(ciRunDir, results, startedAt, totalDurationMs, flaky, clusters);
|
|
6345
6362
|
const { passed, failed, skipped } = countCiResults(results);
|
|
6346
6363
|
return {
|
|
@@ -7015,8 +7032,299 @@ async function generateHunt(options) {
|
|
|
7015
7032
|
return yamlStr;
|
|
7016
7033
|
}
|
|
7017
7034
|
|
|
7035
|
+
// src/browser/macdriver-install.ts
|
|
7036
|
+
import { execFile as execFile5 } from "child_process";
|
|
7037
|
+
import { createHash as createHash2 } from "crypto";
|
|
7038
|
+
import fs15 from "fs";
|
|
7039
|
+
import os5 from "os";
|
|
7040
|
+
import path17 from "path";
|
|
7041
|
+
import { promisify } from "util";
|
|
7042
|
+
var execFileAsync = promisify(execFile5);
|
|
7043
|
+
function parseChecksumFile(text) {
|
|
7044
|
+
const token = text.trim().split(/\s+/, 1)[0] ?? "";
|
|
7045
|
+
const digest = token.toLowerCase();
|
|
7046
|
+
if (!/^[0-9a-f]{64}$/.test(digest)) {
|
|
7047
|
+
throw new Error(`Malformed .sha256 checksum file (expected a 64-char hex digest, got: ${text.trim().slice(0, 80)})`);
|
|
7048
|
+
}
|
|
7049
|
+
return digest;
|
|
7050
|
+
}
|
|
7051
|
+
function sha256Hex(bytes) {
|
|
7052
|
+
return createHash2("sha256").update(bytes).digest("hex");
|
|
7053
|
+
}
|
|
7054
|
+
var commandRunner = async (file, args) => execFileAsync(file, args);
|
|
7055
|
+
function commandOutput(result) {
|
|
7056
|
+
return [result.stdout, result.stderr].filter((value) => value !== void 0).map((value) => value.toString()).join("\n").trim();
|
|
7057
|
+
}
|
|
7058
|
+
function commandErrorDetail(error) {
|
|
7059
|
+
const err = error;
|
|
7060
|
+
return [err.stderr, err.stdout, err.message].filter((value) => value !== void 0 && value !== "").map((value) => value.toString()).join("\n").trim();
|
|
7061
|
+
}
|
|
7062
|
+
async function runRequiredCommand(run, binaryPath, command, args, label) {
|
|
7063
|
+
try {
|
|
7064
|
+
return await run(command, args);
|
|
7065
|
+
} catch (error) {
|
|
7066
|
+
const detail = commandErrorDetail(error);
|
|
7067
|
+
throw new Error(`${label} failed for ${binaryPath}${detail ? `: ${detail}` : ""}`);
|
|
7068
|
+
}
|
|
7069
|
+
}
|
|
7070
|
+
var zipinfoArchiveLister = async (zipPath) => {
|
|
7071
|
+
try {
|
|
7072
|
+
const { stdout } = await execFileAsync("zipinfo", ["-1", zipPath]);
|
|
7073
|
+
return stdout.toString().split(/\r?\n/).filter((entry) => entry.length > 0);
|
|
7074
|
+
} catch (error) {
|
|
7075
|
+
const detail = commandErrorDetail(error);
|
|
7076
|
+
throw new Error(`Failed to inspect release archive ${zipPath} with zipinfo${detail ? `: ${detail}` : ""}`);
|
|
7077
|
+
}
|
|
7078
|
+
};
|
|
7079
|
+
function normalizeArchiveEntryName(entry) {
|
|
7080
|
+
if (entry.length === 0 || entry !== entry.trim() || entry.includes("\0") || entry.includes("\\")) {
|
|
7081
|
+
throw new Error(`Unsafe path in prowl-macdriver release archive: ${JSON.stringify(entry)}`);
|
|
7082
|
+
}
|
|
7083
|
+
if (entry.endsWith("/")) {
|
|
7084
|
+
throw new Error(`Unexpected directory in prowl-macdriver release archive: ${entry}`);
|
|
7085
|
+
}
|
|
7086
|
+
if (path17.posix.isAbsolute(entry)) {
|
|
7087
|
+
throw new Error(`Unsafe absolute path in prowl-macdriver release archive: ${entry}`);
|
|
7088
|
+
}
|
|
7089
|
+
const parts = entry.split("/");
|
|
7090
|
+
if (parts.some((part) => part === "" || part === "." || part === "..")) {
|
|
7091
|
+
throw new Error(`Unsafe path in prowl-macdriver release archive: ${entry}`);
|
|
7092
|
+
}
|
|
7093
|
+
return entry;
|
|
7094
|
+
}
|
|
7095
|
+
function validateMacdriverArchiveEntries(entries) {
|
|
7096
|
+
const normalized = entries.map(normalizeArchiveEntryName);
|
|
7097
|
+
if (normalized.length !== 1 || normalized[0] !== HELPER_BINARY) {
|
|
7098
|
+
const shown = normalized.length > 0 ? normalized.join(", ") : "(empty archive)";
|
|
7099
|
+
throw new Error(
|
|
7100
|
+
`Unexpected prowl-macdriver release archive contents: ${shown}. Expected exactly "${HELPER_BINARY}" at the archive root.`
|
|
7101
|
+
);
|
|
7102
|
+
}
|
|
7103
|
+
}
|
|
7104
|
+
var dittoExtractor = async (zipPath, destDir) => {
|
|
7105
|
+
await execFileAsync("ditto", ["-x", "-k", zipPath, destDir]);
|
|
7106
|
+
};
|
|
7107
|
+
function parseCodesignDetails(text) {
|
|
7108
|
+
const details = { identifier: null, authorities: [], teamIdentifier: null };
|
|
7109
|
+
for (const line of text.split(/\r?\n/)) {
|
|
7110
|
+
const trimmed = line.trim();
|
|
7111
|
+
if (trimmed.startsWith("Identifier=")) {
|
|
7112
|
+
details.identifier = trimmed.slice("Identifier=".length);
|
|
7113
|
+
} else if (trimmed.startsWith("Authority=")) {
|
|
7114
|
+
details.authorities.push(trimmed.slice("Authority=".length));
|
|
7115
|
+
} else if (trimmed.startsWith("TeamIdentifier=")) {
|
|
7116
|
+
details.teamIdentifier = trimmed.slice("TeamIdentifier=".length);
|
|
7117
|
+
}
|
|
7118
|
+
}
|
|
7119
|
+
return details;
|
|
7120
|
+
}
|
|
7121
|
+
function validateCodesignDetails(details, binaryPath) {
|
|
7122
|
+
if (details.identifier !== MACDRIVER_SIGNING_IDENTIFIER) {
|
|
7123
|
+
throw new Error(
|
|
7124
|
+
`codesign verification failed for ${binaryPath}: expected identifier "${MACDRIVER_SIGNING_IDENTIFIER}", got "${details.identifier ?? "missing"}"`
|
|
7125
|
+
);
|
|
7126
|
+
}
|
|
7127
|
+
const developerIdAuthority = details.authorities.find(
|
|
7128
|
+
(authority) => authority.startsWith(`${MACDRIVER_SIGNING_AUTHORITY_PREFIX} (`)
|
|
7129
|
+
);
|
|
7130
|
+
const authorityTeamId = developerIdAuthority?.match(/\(([A-Z0-9]{10})\)$/)?.[1] ?? null;
|
|
7131
|
+
if (!developerIdAuthority || !authorityTeamId) {
|
|
7132
|
+
const shown = details.authorities.length > 0 ? details.authorities.join(" / ") : "missing";
|
|
7133
|
+
throw new Error(
|
|
7134
|
+
`codesign verification failed for ${binaryPath}: expected ${MACDRIVER_SIGNING_AUTHORITY_PREFIX} signer, got ${shown}`
|
|
7135
|
+
);
|
|
7136
|
+
}
|
|
7137
|
+
if (!details.teamIdentifier) {
|
|
7138
|
+
throw new Error(`codesign verification failed for ${binaryPath}: missing TeamIdentifier`);
|
|
7139
|
+
}
|
|
7140
|
+
if (details.teamIdentifier !== authorityTeamId) {
|
|
7141
|
+
throw new Error(
|
|
7142
|
+
`codesign verification failed for ${binaryPath}: TeamIdentifier ${details.teamIdentifier} does not match Developer ID authority team ${authorityTeamId}`
|
|
7143
|
+
);
|
|
7144
|
+
}
|
|
7145
|
+
}
|
|
7146
|
+
async function verifyMacdriverSignature(binaryPath, run = commandRunner) {
|
|
7147
|
+
await runRequiredCommand(run, binaryPath, "codesign", ["--verify", "--strict", binaryPath], "codesign verification");
|
|
7148
|
+
const display = await runRequiredCommand(
|
|
7149
|
+
run,
|
|
7150
|
+
binaryPath,
|
|
7151
|
+
"codesign",
|
|
7152
|
+
["--display", "--verbose=4", binaryPath],
|
|
7153
|
+
"codesign detail inspection"
|
|
7154
|
+
);
|
|
7155
|
+
validateCodesignDetails(parseCodesignDetails(commandOutput(display)), binaryPath);
|
|
7156
|
+
await runRequiredCommand(
|
|
7157
|
+
run,
|
|
7158
|
+
binaryPath,
|
|
7159
|
+
"spctl",
|
|
7160
|
+
["--assess", "--type", "execute", "--verbose=4", binaryPath],
|
|
7161
|
+
"spctl assessment"
|
|
7162
|
+
);
|
|
7163
|
+
}
|
|
7164
|
+
var codesignVerifier = async (binaryPath) => {
|
|
7165
|
+
await verifyMacdriverSignature(binaryPath);
|
|
7166
|
+
};
|
|
7167
|
+
async function downloadAndVerify(options = {}) {
|
|
7168
|
+
const version = options.version ?? MACDRIVER_VERSION;
|
|
7169
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
7170
|
+
const assetName = macdriverAssetName(version);
|
|
7171
|
+
const zipUrl = macdriverAssetUrl(assetName, version);
|
|
7172
|
+
const sumUrl = macdriverAssetUrl(macdriverChecksumName(version), version);
|
|
7173
|
+
const [zipRes, sumRes] = await Promise.all([
|
|
7174
|
+
fetchImpl(zipUrl, { redirect: "follow" }),
|
|
7175
|
+
fetchImpl(sumUrl, { redirect: "follow" })
|
|
7176
|
+
]);
|
|
7177
|
+
if (zipRes.status === 404 || sumRes.status === 404) {
|
|
7178
|
+
throw new Error(
|
|
7179
|
+
`No published prowl-macdriver release for ${macdriverReleaseTag(version)} yet.
|
|
7180
|
+
The signed binary is cut by the maintainer; until then build from source:
|
|
7181
|
+
cd macdriver && swift build -c release`
|
|
7182
|
+
);
|
|
7183
|
+
}
|
|
7184
|
+
if (!zipRes.ok) {
|
|
7185
|
+
throw new Error(`Failed to download ${assetName} (HTTP ${zipRes.status}) from ${zipUrl}`);
|
|
7186
|
+
}
|
|
7187
|
+
if (!sumRes.ok) {
|
|
7188
|
+
throw new Error(`Failed to download the checksum (HTTP ${sumRes.status}) from ${sumUrl}`);
|
|
7189
|
+
}
|
|
7190
|
+
const zipBytes = Buffer.from(await zipRes.arrayBuffer());
|
|
7191
|
+
const expected = parseChecksumFile(await sumRes.text());
|
|
7192
|
+
const actual = sha256Hex(zipBytes);
|
|
7193
|
+
if (actual !== expected) {
|
|
7194
|
+
throw new Error(
|
|
7195
|
+
`Checksum mismatch for ${assetName}.
|
|
7196
|
+
expected: ${expected}
|
|
7197
|
+
actual: ${actual}
|
|
7198
|
+
The download was rejected and discarded; re-run the install, and report it if it repeats.`
|
|
7199
|
+
);
|
|
7200
|
+
}
|
|
7201
|
+
return zipBytes;
|
|
7202
|
+
}
|
|
7203
|
+
async function installMacdriver(options = {}) {
|
|
7204
|
+
const version = validateMacdriverVersion(options.version ?? MACDRIVER_VERSION);
|
|
7205
|
+
const homedir = options.homedir ?? os5.homedir();
|
|
7206
|
+
const listArchiveEntries = options.listArchiveEntries ?? zipinfoArchiveLister;
|
|
7207
|
+
const extract = options.extract ?? dittoExtractor;
|
|
7208
|
+
const verifySignature = options.verifySignature ?? codesignVerifier;
|
|
7209
|
+
const installRoot = macdriverInstallRoot(homedir);
|
|
7210
|
+
const versionDir = macdriverVersionDir(version, homedir);
|
|
7211
|
+
const binaryPath = macdriverInstalledBinary(version, homedir);
|
|
7212
|
+
if (!options.force && fs15.existsSync(binaryPath)) {
|
|
7213
|
+
return { version, binaryPath, alreadyInstalled: true };
|
|
7214
|
+
}
|
|
7215
|
+
const zipBytes = await downloadAndVerify({ version, fetchImpl: options.fetchImpl });
|
|
7216
|
+
fs15.mkdirSync(installRoot, { recursive: true });
|
|
7217
|
+
const stagingDir = fs15.mkdtempSync(path17.join(installRoot, `.tmp-${version}-`));
|
|
7218
|
+
const extractDir = path17.join(stagingDir, "extract");
|
|
7219
|
+
const zipPath = path17.join(stagingDir, macdriverAssetName(version));
|
|
7220
|
+
try {
|
|
7221
|
+
fs15.mkdirSync(extractDir);
|
|
7222
|
+
fs15.writeFileSync(zipPath, zipBytes);
|
|
7223
|
+
validateMacdriverArchiveEntries(await listArchiveEntries(zipPath));
|
|
7224
|
+
await extract(zipPath, extractDir);
|
|
7225
|
+
const stagedBinaryPath = path17.join(extractDir, HELPER_BINARY);
|
|
7226
|
+
assertExtractedHelper(extractDir, stagedBinaryPath);
|
|
7227
|
+
fs15.chmodSync(stagedBinaryPath, 493);
|
|
7228
|
+
await verifySignature(stagedBinaryPath);
|
|
7229
|
+
replaceVersionDir(versionDir, extractDir, installRoot);
|
|
7230
|
+
} finally {
|
|
7231
|
+
fs15.rmSync(stagingDir, { recursive: true, force: true });
|
|
7232
|
+
}
|
|
7233
|
+
return { version, binaryPath, alreadyInstalled: false };
|
|
7234
|
+
}
|
|
7235
|
+
function assertExtractedHelper(extractDir, binaryPath) {
|
|
7236
|
+
const entries = fs15.readdirSync(extractDir);
|
|
7237
|
+
if (!entries.includes(HELPER_BINARY)) {
|
|
7238
|
+
throw new Error(`The release archive did not contain a "${HELPER_BINARY}" binary.`);
|
|
7239
|
+
}
|
|
7240
|
+
if (entries.length !== 1 || entries[0] !== HELPER_BINARY) {
|
|
7241
|
+
const shown = entries.length > 0 ? entries.join(", ") : "(empty directory)";
|
|
7242
|
+
throw new Error(
|
|
7243
|
+
`Unexpected extracted prowl-macdriver archive contents: ${shown}. Expected exactly "${HELPER_BINARY}".`
|
|
7244
|
+
);
|
|
7245
|
+
}
|
|
7246
|
+
let stat = null;
|
|
7247
|
+
try {
|
|
7248
|
+
stat = fs15.lstatSync(binaryPath);
|
|
7249
|
+
} catch {
|
|
7250
|
+
}
|
|
7251
|
+
if (!stat?.isFile()) {
|
|
7252
|
+
throw new Error(`The release archive "${HELPER_BINARY}" entry is not a regular file.`);
|
|
7253
|
+
}
|
|
7254
|
+
}
|
|
7255
|
+
function replaceVersionDir(versionDir, stagedVersionDir, installRoot) {
|
|
7256
|
+
const backupDir = path17.join(installRoot, `.previous-${path17.basename(versionDir)}-${process.pid}-${Date.now()}`);
|
|
7257
|
+
let backedUp = false;
|
|
7258
|
+
try {
|
|
7259
|
+
if (fs15.existsSync(versionDir)) {
|
|
7260
|
+
fs15.renameSync(versionDir, backupDir);
|
|
7261
|
+
backedUp = true;
|
|
7262
|
+
}
|
|
7263
|
+
fs15.renameSync(stagedVersionDir, versionDir);
|
|
7264
|
+
if (backedUp) {
|
|
7265
|
+
fs15.rmSync(backupDir, { recursive: true, force: true });
|
|
7266
|
+
}
|
|
7267
|
+
} catch (error) {
|
|
7268
|
+
if (backedUp && !fs15.existsSync(versionDir) && fs15.existsSync(backupDir)) {
|
|
7269
|
+
fs15.renameSync(backupDir, versionDir);
|
|
7270
|
+
}
|
|
7271
|
+
throw error;
|
|
7272
|
+
}
|
|
7273
|
+
}
|
|
7274
|
+
var runVersionProbe = async (binaryPath) => {
|
|
7275
|
+
try {
|
|
7276
|
+
const { stdout } = await execFileAsync(binaryPath, ["version"], { timeout: 5e3 });
|
|
7277
|
+
const match = stdout.match(/prowl-macdriver\s+(\S+)/);
|
|
7278
|
+
return match ? match[1] : stdout.trim() || null;
|
|
7279
|
+
} catch {
|
|
7280
|
+
return null;
|
|
7281
|
+
}
|
|
7282
|
+
};
|
|
7283
|
+
async function collectMacdriverStatus(options = {}) {
|
|
7284
|
+
const env = options.env ?? process.env;
|
|
7285
|
+
const homedir = options.homedir ?? os5.homedir();
|
|
7286
|
+
const probe = options.probe ?? runVersionProbe;
|
|
7287
|
+
let resolved = null;
|
|
7288
|
+
try {
|
|
7289
|
+
const resolvedPath = resolveHelperBinary(env, { homedir });
|
|
7290
|
+
resolved = { path: resolvedPath, source: classifyResolvedSource(resolvedPath, env, homedir) };
|
|
7291
|
+
} catch {
|
|
7292
|
+
resolved = null;
|
|
7293
|
+
}
|
|
7294
|
+
const installed = [];
|
|
7295
|
+
const root = macdriverInstallRoot(homedir);
|
|
7296
|
+
if (fs15.existsSync(root)) {
|
|
7297
|
+
for (const entry of fs15.readdirSync(root, { withFileTypes: true })) {
|
|
7298
|
+
if (!entry.isDirectory()) continue;
|
|
7299
|
+
let binaryPath;
|
|
7300
|
+
try {
|
|
7301
|
+
binaryPath = macdriverInstalledBinary(entry.name, homedir);
|
|
7302
|
+
} catch {
|
|
7303
|
+
continue;
|
|
7304
|
+
}
|
|
7305
|
+
if (fs15.existsSync(binaryPath)) {
|
|
7306
|
+
installed.push({ version: entry.name, binaryPath });
|
|
7307
|
+
}
|
|
7308
|
+
}
|
|
7309
|
+
installed.sort((a, b) => a.version.localeCompare(b.version));
|
|
7310
|
+
}
|
|
7311
|
+
const probedVersion = resolved ? await probe(resolved.path) : null;
|
|
7312
|
+
return { resolved, pinnedVersion: MACDRIVER_VERSION, installed, probedVersion };
|
|
7313
|
+
}
|
|
7314
|
+
function classifyResolvedSource(resolvedPath, env, homedir) {
|
|
7315
|
+
if (env.PROWL_MACDRIVER_BIN && resolvedPath === env.PROWL_MACDRIVER_BIN) {
|
|
7316
|
+
return "env";
|
|
7317
|
+
}
|
|
7318
|
+
if (resolvedPath.startsWith(macdriverInstallRoot(homedir) + path17.sep)) {
|
|
7319
|
+
return "user-install";
|
|
7320
|
+
}
|
|
7321
|
+
return "source-build";
|
|
7322
|
+
}
|
|
7323
|
+
function tccGuidance() {
|
|
7324
|
+
return "macOS permissions: the app that hosts Prowl (your terminal \u2014 Terminal, iTerm, VS Code, \u2026)\nmust be granted, in System Settings \u2192 Privacy & Security:\n \u2022 Accessibility \u2014 required to drive the target app\n \u2022 Screen Recording \u2014 required for screenshots / visual baselines\nGrant both to the terminal app, not to prowl-macdriver itself, then re-run your hunt.";
|
|
7325
|
+
}
|
|
7326
|
+
|
|
7018
7327
|
export {
|
|
7019
|
-
interpolateHunt,
|
|
7020
7328
|
WEB_ONLY_STEP_TYPES,
|
|
7021
7329
|
webOnlyReason,
|
|
7022
7330
|
assertStepsSupportedByTarget,
|
|
@@ -7032,6 +7340,20 @@ export {
|
|
|
7032
7340
|
createPlaywrightDriver,
|
|
7033
7341
|
parseMacSelector,
|
|
7034
7342
|
createMacDriver,
|
|
7343
|
+
HELPER_BINARY,
|
|
7344
|
+
MACDRIVER_VERSION,
|
|
7345
|
+
MACDRIVER_REPO,
|
|
7346
|
+
MACDRIVER_SIGNING_IDENTIFIER,
|
|
7347
|
+
MACDRIVER_SIGNING_AUTHORITY_PREFIX,
|
|
7348
|
+
MACDRIVER_VERSION_PATTERN,
|
|
7349
|
+
validateMacdriverVersion,
|
|
7350
|
+
macdriverReleaseTag,
|
|
7351
|
+
macdriverAssetName,
|
|
7352
|
+
macdriverChecksumName,
|
|
7353
|
+
macdriverAssetUrl,
|
|
7354
|
+
macdriverInstallRoot,
|
|
7355
|
+
macdriverVersionDir,
|
|
7356
|
+
macdriverInstalledBinary,
|
|
7035
7357
|
macdriverBuildInstructions,
|
|
7036
7358
|
resolveHelperBinary,
|
|
7037
7359
|
DEFAULT_REQUEST_TIMEOUT_MS,
|
|
@@ -7145,6 +7467,19 @@ export {
|
|
|
7145
7467
|
matchIosSelector,
|
|
7146
7468
|
isIosInteractive,
|
|
7147
7469
|
analyzeIosApp,
|
|
7148
|
-
generateHunt
|
|
7470
|
+
generateHunt,
|
|
7471
|
+
parseChecksumFile,
|
|
7472
|
+
sha256Hex,
|
|
7473
|
+
zipinfoArchiveLister,
|
|
7474
|
+
validateMacdriverArchiveEntries,
|
|
7475
|
+
dittoExtractor,
|
|
7476
|
+
parseCodesignDetails,
|
|
7477
|
+
verifyMacdriverSignature,
|
|
7478
|
+
codesignVerifier,
|
|
7479
|
+
downloadAndVerify,
|
|
7480
|
+
installMacdriver,
|
|
7481
|
+
runVersionProbe,
|
|
7482
|
+
collectMacdriverStatus,
|
|
7483
|
+
tccGuidance
|
|
7149
7484
|
};
|
|
7150
|
-
//# sourceMappingURL=chunk-
|
|
7485
|
+
//# sourceMappingURL=chunk-CWLRDV5P.js.map
|