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
package/dist/lib.cjs
CHANGED
|
@@ -37,12 +37,12 @@ __export(visual_exports, {
|
|
|
37
37
|
ensureBaselineDir: () => ensureBaselineDir
|
|
38
38
|
});
|
|
39
39
|
async function compareScreenshots(baselinePath, currentPath, diffPath, threshold) {
|
|
40
|
-
const baselineData = import_pngjs.PNG.sync.read(
|
|
41
|
-
const currentData = import_pngjs.PNG.sync.read(
|
|
40
|
+
const baselineData = import_pngjs.PNG.sync.read(import_node_fs9.default.readFileSync(baselinePath));
|
|
41
|
+
const currentData = import_pngjs.PNG.sync.read(import_node_fs9.default.readFileSync(currentPath));
|
|
42
42
|
const { width, height } = baselineData;
|
|
43
43
|
if (currentData.width !== width || currentData.height !== height) {
|
|
44
44
|
const diff2 = new import_pngjs.PNG({ width, height });
|
|
45
|
-
|
|
45
|
+
import_node_fs9.default.writeFileSync(diffPath, import_pngjs.PNG.sync.write(diff2));
|
|
46
46
|
return {
|
|
47
47
|
match: false,
|
|
48
48
|
diffPercentage: 1,
|
|
@@ -60,7 +60,7 @@ async function compareScreenshots(baselinePath, currentPath, diffPath, threshold
|
|
|
60
60
|
);
|
|
61
61
|
const totalPixels = width * height;
|
|
62
62
|
const diffPercentage = diffPixels / totalPixels;
|
|
63
|
-
|
|
63
|
+
import_node_fs9.default.writeFileSync(diffPath, import_pngjs.PNG.sync.write(diff));
|
|
64
64
|
return {
|
|
65
65
|
match: diffPercentage <= threshold,
|
|
66
66
|
diffPercentage,
|
|
@@ -68,16 +68,16 @@ async function compareScreenshots(baselinePath, currentPath, diffPath, threshold
|
|
|
68
68
|
};
|
|
69
69
|
}
|
|
70
70
|
function ensureBaselineDir(configDir) {
|
|
71
|
-
const baselineDir =
|
|
72
|
-
|
|
71
|
+
const baselineDir = import_node_path10.default.join(configDir, "baselines");
|
|
72
|
+
import_node_fs9.default.mkdirSync(baselineDir, { recursive: true });
|
|
73
73
|
return baselineDir;
|
|
74
74
|
}
|
|
75
|
-
var
|
|
75
|
+
var import_node_fs9, import_node_path10, import_pngjs, import_pixelmatch;
|
|
76
76
|
var init_visual = __esm({
|
|
77
77
|
"src/runner/visual.ts"() {
|
|
78
78
|
"use strict";
|
|
79
|
-
|
|
80
|
-
|
|
79
|
+
import_node_fs9 = __toESM(require("fs"), 1);
|
|
80
|
+
import_node_path10 = __toESM(require("path"), 1);
|
|
81
81
|
import_pngjs = require("pngjs");
|
|
82
82
|
import_pixelmatch = __toESM(require("pixelmatch"), 1);
|
|
83
83
|
}
|
|
@@ -95,11 +95,17 @@ __export(src_exports, {
|
|
|
95
95
|
DEFAULT_REQUEST_TIMEOUT_MS: () => DEFAULT_REQUEST_TIMEOUT_MS,
|
|
96
96
|
DEFAULT_SIMULATOR_LOCK_ROOT: () => DEFAULT_SIMULATOR_LOCK_ROOT,
|
|
97
97
|
DEFAULT_WDA_REQUEST_TIMEOUT_MS: () => DEFAULT_WDA_REQUEST_TIMEOUT_MS,
|
|
98
|
+
HELPER_BINARY: () => HELPER_BINARY,
|
|
98
99
|
INTERACTIVE_ROLES: () => INTERACTIVE_ROLES,
|
|
99
100
|
IOS_INTERACTIVE_TYPES: () => IOS_INTERACTIVE_TYPES,
|
|
100
101
|
IOS_MATCH_DIALECT: () => IOS_MATCH_DIALECT,
|
|
101
102
|
IOS_PRESS_KEYS: () => IOS_PRESS_KEYS,
|
|
102
103
|
IOS_WINDOW_TYPE: () => IOS_WINDOW_TYPE,
|
|
104
|
+
MACDRIVER_REPO: () => MACDRIVER_REPO,
|
|
105
|
+
MACDRIVER_SIGNING_AUTHORITY_PREFIX: () => MACDRIVER_SIGNING_AUTHORITY_PREFIX,
|
|
106
|
+
MACDRIVER_SIGNING_IDENTIFIER: () => MACDRIVER_SIGNING_IDENTIFIER,
|
|
107
|
+
MACDRIVER_VERSION: () => MACDRIVER_VERSION,
|
|
108
|
+
MACDRIVER_VERSION_PATTERN: () => MACDRIVER_VERSION_PATTERN,
|
|
103
109
|
MACOS_MATCH_DIALECT: () => MACOS_MATCH_DIALECT,
|
|
104
110
|
NATIVE_ATTRIBUTE_MAP: () => NATIVE_ATTRIBUTE_MAP,
|
|
105
111
|
SpawnMacHelperClient: () => SpawnMacHelperClient,
|
|
@@ -129,6 +135,8 @@ __export(src_exports, {
|
|
|
129
135
|
closeIosSession: () => closeIosSession,
|
|
130
136
|
closeMacSession: () => closeMacSession,
|
|
131
137
|
clusterFailures: () => clusterFailures,
|
|
138
|
+
codesignVerifier: () => codesignVerifier,
|
|
139
|
+
collectMacdriverStatus: () => collectMacdriverStatus,
|
|
132
140
|
computeFlakeScore: () => computeFlakeScore,
|
|
133
141
|
configSchema: () => configSchema,
|
|
134
142
|
createAndroidDriver: () => createAndroidDriver,
|
|
@@ -142,6 +150,8 @@ __export(src_exports, {
|
|
|
142
150
|
defaultAgentConnector: () => defaultAgentConnector,
|
|
143
151
|
defaultIosAgentConnector: () => defaultIosAgentConnector,
|
|
144
152
|
defaultWdaTestRunPreparer: () => defaultWdaTestRunPreparer,
|
|
153
|
+
dittoExtractor: () => dittoExtractor,
|
|
154
|
+
downloadAndVerify: () => downloadAndVerify,
|
|
145
155
|
escapePredicateArg: () => escapePredicateArg,
|
|
146
156
|
escapeUiSelectorArg: () => escapeUiSelectorArg,
|
|
147
157
|
extractElementId: () => extractElementId,
|
|
@@ -152,6 +162,7 @@ __export(src_exports, {
|
|
|
152
162
|
healSelector: () => healSelector,
|
|
153
163
|
huntSchema: () => huntSchema,
|
|
154
164
|
injectUsePortIntoXctestrun: () => injectUsePortIntoXctestrun,
|
|
165
|
+
installMacdriver: () => installMacdriver,
|
|
155
166
|
interpolateHunt: () => interpolateHunt,
|
|
156
167
|
iosAppAllowedIdentities: () => iosAppAllowedIdentities,
|
|
157
168
|
iosNodeToNative: () => iosNodeToNative,
|
|
@@ -168,7 +179,14 @@ __export(src_exports, {
|
|
|
168
179
|
loadHunt: () => loadHunt,
|
|
169
180
|
loadHuntMeta: () => loadHuntMeta,
|
|
170
181
|
loadHuntTags: () => loadHuntTags,
|
|
182
|
+
macdriverAssetName: () => macdriverAssetName,
|
|
183
|
+
macdriverAssetUrl: () => macdriverAssetUrl,
|
|
171
184
|
macdriverBuildInstructions: () => macdriverBuildInstructions,
|
|
185
|
+
macdriverChecksumName: () => macdriverChecksumName,
|
|
186
|
+
macdriverInstallRoot: () => macdriverInstallRoot,
|
|
187
|
+
macdriverInstalledBinary: () => macdriverInstalledBinary,
|
|
188
|
+
macdriverReleaseTag: () => macdriverReleaseTag,
|
|
189
|
+
macdriverVersionDir: () => macdriverVersionDir,
|
|
172
190
|
matchAndroidSelector: () => matchAndroidSelector,
|
|
173
191
|
matchIosSelector: () => matchIosSelector,
|
|
174
192
|
matchNativeTree: () => matchNativeTree,
|
|
@@ -178,6 +196,8 @@ __export(src_exports, {
|
|
|
178
196
|
parseAdbDevices: () => parseAdbDevices,
|
|
179
197
|
parseAndroidHierarchy: () => parseAndroidHierarchy,
|
|
180
198
|
parseAndroidSelector: () => parseAndroidSelector,
|
|
199
|
+
parseChecksumFile: () => parseChecksumFile,
|
|
200
|
+
parseCodesignDetails: () => parseCodesignDetails,
|
|
181
201
|
parseForwardPort: () => parseForwardPort,
|
|
182
202
|
parseIosHierarchy: () => parseIosHierarchy,
|
|
183
203
|
parseIosSelector: () => parseIosSelector,
|
|
@@ -204,20 +224,27 @@ __export(src_exports, {
|
|
|
204
224
|
resolveWdaTestRun: () => resolveWdaTestRun,
|
|
205
225
|
runHunt: () => runHunt,
|
|
206
226
|
runSuite: () => runSuite,
|
|
227
|
+
runVersionProbe: () => runVersionProbe,
|
|
207
228
|
selectDeviceSerial: () => selectDeviceSerial,
|
|
208
229
|
selectSimulatorUdid: () => selectSimulatorUdid,
|
|
230
|
+
sha256Hex: () => sha256Hex,
|
|
209
231
|
shortIosType: () => shortIosType,
|
|
210
232
|
stepSchema: () => stepSchema,
|
|
233
|
+
tccGuidance: () => tccGuidance,
|
|
211
234
|
unquoteSelectorValue: () => unquoteSelectorValue,
|
|
212
235
|
unwrapAndroidTextSelector: () => unwrapAndroidTextSelector,
|
|
213
236
|
unwrapIosTextSelector: () => unwrapIosTextSelector,
|
|
214
237
|
unwrapNativeTextSelector: () => unwrapNativeTextSelector,
|
|
215
238
|
updateBacklogFromSuite: () => updateBacklogFromSuite,
|
|
239
|
+
validateMacdriverArchiveEntries: () => validateMacdriverArchiveEntries,
|
|
240
|
+
validateMacdriverVersion: () => validateMacdriverVersion,
|
|
241
|
+
verifyMacdriverSignature: () => verifyMacdriverSignature,
|
|
216
242
|
waitForAgentReady: () => waitForAgentReady,
|
|
217
243
|
waitForWdaReady: () => waitForWdaReady,
|
|
218
244
|
wdaCacheDir: () => wdaCacheDir,
|
|
219
245
|
wdaTestRunArgs: () => wdaTestRunArgs,
|
|
220
|
-
webOnlyReason: () => webOnlyReason
|
|
246
|
+
webOnlyReason: () => webOnlyReason,
|
|
247
|
+
zipinfoArchiveLister: () => zipinfoArchiveLister
|
|
221
248
|
});
|
|
222
249
|
module.exports = __toCommonJS(src_exports);
|
|
223
250
|
|
|
@@ -329,6 +356,9 @@ function createMacDriver(client, options = {}) {
|
|
|
329
356
|
async hover(selector) {
|
|
330
357
|
await query("hover", selector);
|
|
331
358
|
},
|
|
359
|
+
scroll() {
|
|
360
|
+
return rejectUnsupported("scroll");
|
|
361
|
+
},
|
|
332
362
|
async scrollIntoView(selector) {
|
|
333
363
|
await query("scrollTo", selector);
|
|
334
364
|
},
|
|
@@ -400,25 +430,71 @@ function createMacDriver(client, options = {}) {
|
|
|
400
430
|
// src/browser/mac-helper.ts
|
|
401
431
|
var import_node_child_process = require("child_process");
|
|
402
432
|
var import_node_fs = __toESM(require("fs"), 1);
|
|
403
|
-
var
|
|
433
|
+
var import_node_os2 = __toESM(require("os"), 1);
|
|
434
|
+
var import_node_path2 = __toESM(require("path"), 1);
|
|
404
435
|
var import_node_url = require("url");
|
|
405
|
-
|
|
436
|
+
|
|
437
|
+
// src/browser/macdriver-release.ts
|
|
438
|
+
var import_node_os = __toESM(require("os"), 1);
|
|
439
|
+
var import_node_path = __toESM(require("path"), 1);
|
|
406
440
|
var HELPER_BINARY = "prowl-macdriver";
|
|
441
|
+
var MACDRIVER_VERSION = "0.1.0";
|
|
442
|
+
var MACDRIVER_REPO = "prowl-tools/prowl";
|
|
443
|
+
var MACDRIVER_SIGNING_IDENTIFIER = "tools.prowl.macdriver";
|
|
444
|
+
var MACDRIVER_SIGNING_AUTHORITY_PREFIX = "Developer ID Application: Genkei Labs";
|
|
445
|
+
var MACDRIVER_VERSION_PATTERN = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z][0-9A-Za-z.-]*)?(?:\+[0-9A-Za-z][0-9A-Za-z.-]*)?$/;
|
|
446
|
+
function validateMacdriverVersion(version) {
|
|
447
|
+
if (!MACDRIVER_VERSION_PATTERN.test(version)) {
|
|
448
|
+
throw new Error(
|
|
449
|
+
`Invalid prowl-macdriver version "${version}". Expected a release version like 0.1.0.`
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
return version;
|
|
453
|
+
}
|
|
454
|
+
function macdriverReleaseTag(version = MACDRIVER_VERSION) {
|
|
455
|
+
return `macdriver-v${validateMacdriverVersion(version)}`;
|
|
456
|
+
}
|
|
457
|
+
function macdriverAssetName(version = MACDRIVER_VERSION) {
|
|
458
|
+
return `prowl-macdriver-v${validateMacdriverVersion(version)}-universal.zip`;
|
|
459
|
+
}
|
|
460
|
+
function macdriverChecksumName(version = MACDRIVER_VERSION) {
|
|
461
|
+
return `${macdriverAssetName(version)}.sha256`;
|
|
462
|
+
}
|
|
463
|
+
function macdriverAssetUrl(assetName, version = MACDRIVER_VERSION) {
|
|
464
|
+
return `https://github.com/${MACDRIVER_REPO}/releases/download/${macdriverReleaseTag(version)}/${assetName}`;
|
|
465
|
+
}
|
|
466
|
+
function macdriverInstallRoot(homedir = import_node_os.default.homedir()) {
|
|
467
|
+
return import_node_path.default.join(homedir, ".prowl", "macdriver");
|
|
468
|
+
}
|
|
469
|
+
function macdriverVersionDir(version = MACDRIVER_VERSION, homedir = import_node_os.default.homedir()) {
|
|
470
|
+
const root = import_node_path.default.resolve(macdriverInstallRoot(homedir));
|
|
471
|
+
const versionDir = import_node_path.default.resolve(root, validateMacdriverVersion(version));
|
|
472
|
+
if (!versionDir.startsWith(root + import_node_path.default.sep)) {
|
|
473
|
+
throw new Error(`Resolved prowl-macdriver version directory escaped install root: ${versionDir}`);
|
|
474
|
+
}
|
|
475
|
+
return versionDir;
|
|
476
|
+
}
|
|
477
|
+
function macdriverInstalledBinary(version = MACDRIVER_VERSION, homedir = import_node_os.default.homedir()) {
|
|
478
|
+
return import_node_path.default.join(macdriverVersionDir(version, homedir), HELPER_BINARY);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// src/browser/mac-helper.ts
|
|
482
|
+
var import_meta = {};
|
|
407
483
|
function macdriverBuildInstructions() {
|
|
408
|
-
return "The macOS target
|
|
484
|
+
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.";
|
|
409
485
|
}
|
|
410
486
|
function getPackageRoot() {
|
|
411
|
-
let dir =
|
|
412
|
-
const root =
|
|
487
|
+
let dir = import_node_path2.default.dirname((0, import_node_url.fileURLToPath)(import_meta.url));
|
|
488
|
+
const root = import_node_path2.default.parse(dir).root;
|
|
413
489
|
while (dir !== root) {
|
|
414
|
-
if (import_node_fs.default.existsSync(
|
|
490
|
+
if (import_node_fs.default.existsSync(import_node_path2.default.join(dir, "package.json"))) {
|
|
415
491
|
return dir;
|
|
416
492
|
}
|
|
417
|
-
dir =
|
|
493
|
+
dir = import_node_path2.default.dirname(dir);
|
|
418
494
|
}
|
|
419
495
|
return root;
|
|
420
496
|
}
|
|
421
|
-
function resolveHelperBinary(env = process.env) {
|
|
497
|
+
function resolveHelperBinary(env = process.env, options = {}) {
|
|
422
498
|
const override = env.PROWL_MACDRIVER_BIN;
|
|
423
499
|
if (override) {
|
|
424
500
|
if (!import_node_fs.default.existsSync(override)) {
|
|
@@ -429,10 +505,15 @@ ${macdriverBuildInstructions()}`
|
|
|
429
505
|
}
|
|
430
506
|
return override;
|
|
431
507
|
}
|
|
508
|
+
const homedir = options.homedir ?? import_node_os2.default.homedir();
|
|
509
|
+
const userBinary = macdriverInstalledBinary(MACDRIVER_VERSION, homedir);
|
|
510
|
+
if (import_node_fs.default.existsSync(userBinary)) {
|
|
511
|
+
return userBinary;
|
|
512
|
+
}
|
|
432
513
|
const root = getPackageRoot();
|
|
433
514
|
const candidates = [
|
|
434
|
-
|
|
435
|
-
|
|
515
|
+
import_node_path2.default.join(root, "macdriver", ".build", "release", HELPER_BINARY),
|
|
516
|
+
import_node_path2.default.join(root, "macdriver", ".build", "debug", HELPER_BINARY)
|
|
436
517
|
];
|
|
437
518
|
for (const candidate of candidates) {
|
|
438
519
|
if (import_node_fs.default.existsSync(candidate)) {
|
|
@@ -604,8 +685,300 @@ async function closeMacSession(session) {
|
|
|
604
685
|
}
|
|
605
686
|
}
|
|
606
687
|
|
|
607
|
-
// src/browser/
|
|
688
|
+
// src/browser/macdriver-install.ts
|
|
689
|
+
var import_node_child_process2 = require("child_process");
|
|
690
|
+
var import_node_crypto = require("crypto");
|
|
608
691
|
var import_node_fs2 = __toESM(require("fs"), 1);
|
|
692
|
+
var import_node_os3 = __toESM(require("os"), 1);
|
|
693
|
+
var import_node_path3 = __toESM(require("path"), 1);
|
|
694
|
+
var import_node_util = require("util");
|
|
695
|
+
var execFileAsync = (0, import_node_util.promisify)(import_node_child_process2.execFile);
|
|
696
|
+
function parseChecksumFile(text) {
|
|
697
|
+
const token = text.trim().split(/\s+/, 1)[0] ?? "";
|
|
698
|
+
const digest = token.toLowerCase();
|
|
699
|
+
if (!/^[0-9a-f]{64}$/.test(digest)) {
|
|
700
|
+
throw new Error(`Malformed .sha256 checksum file (expected a 64-char hex digest, got: ${text.trim().slice(0, 80)})`);
|
|
701
|
+
}
|
|
702
|
+
return digest;
|
|
703
|
+
}
|
|
704
|
+
function sha256Hex(bytes) {
|
|
705
|
+
return (0, import_node_crypto.createHash)("sha256").update(bytes).digest("hex");
|
|
706
|
+
}
|
|
707
|
+
var commandRunner = async (file, args) => execFileAsync(file, args);
|
|
708
|
+
function commandOutput(result) {
|
|
709
|
+
return [result.stdout, result.stderr].filter((value) => value !== void 0).map((value) => value.toString()).join("\n").trim();
|
|
710
|
+
}
|
|
711
|
+
function commandErrorDetail(error) {
|
|
712
|
+
const err = error;
|
|
713
|
+
return [err.stderr, err.stdout, err.message].filter((value) => value !== void 0 && value !== "").map((value) => value.toString()).join("\n").trim();
|
|
714
|
+
}
|
|
715
|
+
async function runRequiredCommand(run, binaryPath, command, args, label) {
|
|
716
|
+
try {
|
|
717
|
+
return await run(command, args);
|
|
718
|
+
} catch (error) {
|
|
719
|
+
const detail = commandErrorDetail(error);
|
|
720
|
+
throw new Error(`${label} failed for ${binaryPath}${detail ? `: ${detail}` : ""}`);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
var zipinfoArchiveLister = async (zipPath) => {
|
|
724
|
+
try {
|
|
725
|
+
const { stdout } = await execFileAsync("zipinfo", ["-1", zipPath]);
|
|
726
|
+
return stdout.toString().split(/\r?\n/).filter((entry) => entry.length > 0);
|
|
727
|
+
} catch (error) {
|
|
728
|
+
const detail = commandErrorDetail(error);
|
|
729
|
+
throw new Error(`Failed to inspect release archive ${zipPath} with zipinfo${detail ? `: ${detail}` : ""}`);
|
|
730
|
+
}
|
|
731
|
+
};
|
|
732
|
+
function normalizeArchiveEntryName(entry) {
|
|
733
|
+
if (entry.length === 0 || entry !== entry.trim() || entry.includes("\0") || entry.includes("\\")) {
|
|
734
|
+
throw new Error(`Unsafe path in prowl-macdriver release archive: ${JSON.stringify(entry)}`);
|
|
735
|
+
}
|
|
736
|
+
if (entry.endsWith("/")) {
|
|
737
|
+
throw new Error(`Unexpected directory in prowl-macdriver release archive: ${entry}`);
|
|
738
|
+
}
|
|
739
|
+
if (import_node_path3.default.posix.isAbsolute(entry)) {
|
|
740
|
+
throw new Error(`Unsafe absolute path in prowl-macdriver release archive: ${entry}`);
|
|
741
|
+
}
|
|
742
|
+
const parts = entry.split("/");
|
|
743
|
+
if (parts.some((part) => part === "" || part === "." || part === "..")) {
|
|
744
|
+
throw new Error(`Unsafe path in prowl-macdriver release archive: ${entry}`);
|
|
745
|
+
}
|
|
746
|
+
return entry;
|
|
747
|
+
}
|
|
748
|
+
function validateMacdriverArchiveEntries(entries) {
|
|
749
|
+
const normalized = entries.map(normalizeArchiveEntryName);
|
|
750
|
+
if (normalized.length !== 1 || normalized[0] !== HELPER_BINARY) {
|
|
751
|
+
const shown = normalized.length > 0 ? normalized.join(", ") : "(empty archive)";
|
|
752
|
+
throw new Error(
|
|
753
|
+
`Unexpected prowl-macdriver release archive contents: ${shown}. Expected exactly "${HELPER_BINARY}" at the archive root.`
|
|
754
|
+
);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
var dittoExtractor = async (zipPath, destDir) => {
|
|
758
|
+
await execFileAsync("ditto", ["-x", "-k", zipPath, destDir]);
|
|
759
|
+
};
|
|
760
|
+
function parseCodesignDetails(text) {
|
|
761
|
+
const details = { identifier: null, authorities: [], teamIdentifier: null };
|
|
762
|
+
for (const line of text.split(/\r?\n/)) {
|
|
763
|
+
const trimmed = line.trim();
|
|
764
|
+
if (trimmed.startsWith("Identifier=")) {
|
|
765
|
+
details.identifier = trimmed.slice("Identifier=".length);
|
|
766
|
+
} else if (trimmed.startsWith("Authority=")) {
|
|
767
|
+
details.authorities.push(trimmed.slice("Authority=".length));
|
|
768
|
+
} else if (trimmed.startsWith("TeamIdentifier=")) {
|
|
769
|
+
details.teamIdentifier = trimmed.slice("TeamIdentifier=".length);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
return details;
|
|
773
|
+
}
|
|
774
|
+
function validateCodesignDetails(details, binaryPath) {
|
|
775
|
+
if (details.identifier !== MACDRIVER_SIGNING_IDENTIFIER) {
|
|
776
|
+
throw new Error(
|
|
777
|
+
`codesign verification failed for ${binaryPath}: expected identifier "${MACDRIVER_SIGNING_IDENTIFIER}", got "${details.identifier ?? "missing"}"`
|
|
778
|
+
);
|
|
779
|
+
}
|
|
780
|
+
const developerIdAuthority = details.authorities.find(
|
|
781
|
+
(authority) => authority.startsWith(`${MACDRIVER_SIGNING_AUTHORITY_PREFIX} (`)
|
|
782
|
+
);
|
|
783
|
+
const authorityTeamId = developerIdAuthority?.match(/\(([A-Z0-9]{10})\)$/)?.[1] ?? null;
|
|
784
|
+
if (!developerIdAuthority || !authorityTeamId) {
|
|
785
|
+
const shown = details.authorities.length > 0 ? details.authorities.join(" / ") : "missing";
|
|
786
|
+
throw new Error(
|
|
787
|
+
`codesign verification failed for ${binaryPath}: expected ${MACDRIVER_SIGNING_AUTHORITY_PREFIX} signer, got ${shown}`
|
|
788
|
+
);
|
|
789
|
+
}
|
|
790
|
+
if (!details.teamIdentifier) {
|
|
791
|
+
throw new Error(`codesign verification failed for ${binaryPath}: missing TeamIdentifier`);
|
|
792
|
+
}
|
|
793
|
+
if (details.teamIdentifier !== authorityTeamId) {
|
|
794
|
+
throw new Error(
|
|
795
|
+
`codesign verification failed for ${binaryPath}: TeamIdentifier ${details.teamIdentifier} does not match Developer ID authority team ${authorityTeamId}`
|
|
796
|
+
);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
async function verifyMacdriverSignature(binaryPath, run = commandRunner) {
|
|
800
|
+
await runRequiredCommand(run, binaryPath, "codesign", ["--verify", "--strict", binaryPath], "codesign verification");
|
|
801
|
+
const display = await runRequiredCommand(
|
|
802
|
+
run,
|
|
803
|
+
binaryPath,
|
|
804
|
+
"codesign",
|
|
805
|
+
["--display", "--verbose=4", binaryPath],
|
|
806
|
+
"codesign detail inspection"
|
|
807
|
+
);
|
|
808
|
+
validateCodesignDetails(parseCodesignDetails(commandOutput(display)), binaryPath);
|
|
809
|
+
await runRequiredCommand(
|
|
810
|
+
run,
|
|
811
|
+
binaryPath,
|
|
812
|
+
"spctl",
|
|
813
|
+
["--assess", "--type", "execute", "--verbose=4", binaryPath],
|
|
814
|
+
"spctl assessment"
|
|
815
|
+
);
|
|
816
|
+
}
|
|
817
|
+
var codesignVerifier = async (binaryPath) => {
|
|
818
|
+
await verifyMacdriverSignature(binaryPath);
|
|
819
|
+
};
|
|
820
|
+
async function downloadAndVerify(options = {}) {
|
|
821
|
+
const version = options.version ?? MACDRIVER_VERSION;
|
|
822
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
823
|
+
const assetName = macdriverAssetName(version);
|
|
824
|
+
const zipUrl = macdriverAssetUrl(assetName, version);
|
|
825
|
+
const sumUrl = macdriverAssetUrl(macdriverChecksumName(version), version);
|
|
826
|
+
const [zipRes, sumRes] = await Promise.all([
|
|
827
|
+
fetchImpl(zipUrl, { redirect: "follow" }),
|
|
828
|
+
fetchImpl(sumUrl, { redirect: "follow" })
|
|
829
|
+
]);
|
|
830
|
+
if (zipRes.status === 404 || sumRes.status === 404) {
|
|
831
|
+
throw new Error(
|
|
832
|
+
`No published prowl-macdriver release for ${macdriverReleaseTag(version)} yet.
|
|
833
|
+
The signed binary is cut by the maintainer; until then build from source:
|
|
834
|
+
cd macdriver && swift build -c release`
|
|
835
|
+
);
|
|
836
|
+
}
|
|
837
|
+
if (!zipRes.ok) {
|
|
838
|
+
throw new Error(`Failed to download ${assetName} (HTTP ${zipRes.status}) from ${zipUrl}`);
|
|
839
|
+
}
|
|
840
|
+
if (!sumRes.ok) {
|
|
841
|
+
throw new Error(`Failed to download the checksum (HTTP ${sumRes.status}) from ${sumUrl}`);
|
|
842
|
+
}
|
|
843
|
+
const zipBytes = Buffer.from(await zipRes.arrayBuffer());
|
|
844
|
+
const expected = parseChecksumFile(await sumRes.text());
|
|
845
|
+
const actual = sha256Hex(zipBytes);
|
|
846
|
+
if (actual !== expected) {
|
|
847
|
+
throw new Error(
|
|
848
|
+
`Checksum mismatch for ${assetName}.
|
|
849
|
+
expected: ${expected}
|
|
850
|
+
actual: ${actual}
|
|
851
|
+
The download was rejected and discarded; re-run the install, and report it if it repeats.`
|
|
852
|
+
);
|
|
853
|
+
}
|
|
854
|
+
return zipBytes;
|
|
855
|
+
}
|
|
856
|
+
async function installMacdriver(options = {}) {
|
|
857
|
+
const version = validateMacdriverVersion(options.version ?? MACDRIVER_VERSION);
|
|
858
|
+
const homedir = options.homedir ?? import_node_os3.default.homedir();
|
|
859
|
+
const listArchiveEntries = options.listArchiveEntries ?? zipinfoArchiveLister;
|
|
860
|
+
const extract = options.extract ?? dittoExtractor;
|
|
861
|
+
const verifySignature = options.verifySignature ?? codesignVerifier;
|
|
862
|
+
const installRoot = macdriverInstallRoot(homedir);
|
|
863
|
+
const versionDir = macdriverVersionDir(version, homedir);
|
|
864
|
+
const binaryPath = macdriverInstalledBinary(version, homedir);
|
|
865
|
+
if (!options.force && import_node_fs2.default.existsSync(binaryPath)) {
|
|
866
|
+
return { version, binaryPath, alreadyInstalled: true };
|
|
867
|
+
}
|
|
868
|
+
const zipBytes = await downloadAndVerify({ version, fetchImpl: options.fetchImpl });
|
|
869
|
+
import_node_fs2.default.mkdirSync(installRoot, { recursive: true });
|
|
870
|
+
const stagingDir = import_node_fs2.default.mkdtempSync(import_node_path3.default.join(installRoot, `.tmp-${version}-`));
|
|
871
|
+
const extractDir = import_node_path3.default.join(stagingDir, "extract");
|
|
872
|
+
const zipPath = import_node_path3.default.join(stagingDir, macdriverAssetName(version));
|
|
873
|
+
try {
|
|
874
|
+
import_node_fs2.default.mkdirSync(extractDir);
|
|
875
|
+
import_node_fs2.default.writeFileSync(zipPath, zipBytes);
|
|
876
|
+
validateMacdriverArchiveEntries(await listArchiveEntries(zipPath));
|
|
877
|
+
await extract(zipPath, extractDir);
|
|
878
|
+
const stagedBinaryPath = import_node_path3.default.join(extractDir, HELPER_BINARY);
|
|
879
|
+
assertExtractedHelper(extractDir, stagedBinaryPath);
|
|
880
|
+
import_node_fs2.default.chmodSync(stagedBinaryPath, 493);
|
|
881
|
+
await verifySignature(stagedBinaryPath);
|
|
882
|
+
replaceVersionDir(versionDir, extractDir, installRoot);
|
|
883
|
+
} finally {
|
|
884
|
+
import_node_fs2.default.rmSync(stagingDir, { recursive: true, force: true });
|
|
885
|
+
}
|
|
886
|
+
return { version, binaryPath, alreadyInstalled: false };
|
|
887
|
+
}
|
|
888
|
+
function assertExtractedHelper(extractDir, binaryPath) {
|
|
889
|
+
const entries = import_node_fs2.default.readdirSync(extractDir);
|
|
890
|
+
if (!entries.includes(HELPER_BINARY)) {
|
|
891
|
+
throw new Error(`The release archive did not contain a "${HELPER_BINARY}" binary.`);
|
|
892
|
+
}
|
|
893
|
+
if (entries.length !== 1 || entries[0] !== HELPER_BINARY) {
|
|
894
|
+
const shown = entries.length > 0 ? entries.join(", ") : "(empty directory)";
|
|
895
|
+
throw new Error(
|
|
896
|
+
`Unexpected extracted prowl-macdriver archive contents: ${shown}. Expected exactly "${HELPER_BINARY}".`
|
|
897
|
+
);
|
|
898
|
+
}
|
|
899
|
+
let stat = null;
|
|
900
|
+
try {
|
|
901
|
+
stat = import_node_fs2.default.lstatSync(binaryPath);
|
|
902
|
+
} catch {
|
|
903
|
+
}
|
|
904
|
+
if (!stat?.isFile()) {
|
|
905
|
+
throw new Error(`The release archive "${HELPER_BINARY}" entry is not a regular file.`);
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
function replaceVersionDir(versionDir, stagedVersionDir, installRoot) {
|
|
909
|
+
const backupDir = import_node_path3.default.join(installRoot, `.previous-${import_node_path3.default.basename(versionDir)}-${process.pid}-${Date.now()}`);
|
|
910
|
+
let backedUp = false;
|
|
911
|
+
try {
|
|
912
|
+
if (import_node_fs2.default.existsSync(versionDir)) {
|
|
913
|
+
import_node_fs2.default.renameSync(versionDir, backupDir);
|
|
914
|
+
backedUp = true;
|
|
915
|
+
}
|
|
916
|
+
import_node_fs2.default.renameSync(stagedVersionDir, versionDir);
|
|
917
|
+
if (backedUp) {
|
|
918
|
+
import_node_fs2.default.rmSync(backupDir, { recursive: true, force: true });
|
|
919
|
+
}
|
|
920
|
+
} catch (error) {
|
|
921
|
+
if (backedUp && !import_node_fs2.default.existsSync(versionDir) && import_node_fs2.default.existsSync(backupDir)) {
|
|
922
|
+
import_node_fs2.default.renameSync(backupDir, versionDir);
|
|
923
|
+
}
|
|
924
|
+
throw error;
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
var runVersionProbe = async (binaryPath) => {
|
|
928
|
+
try {
|
|
929
|
+
const { stdout } = await execFileAsync(binaryPath, ["version"], { timeout: 5e3 });
|
|
930
|
+
const match = stdout.match(/prowl-macdriver\s+(\S+)/);
|
|
931
|
+
return match ? match[1] : stdout.trim() || null;
|
|
932
|
+
} catch {
|
|
933
|
+
return null;
|
|
934
|
+
}
|
|
935
|
+
};
|
|
936
|
+
async function collectMacdriverStatus(options = {}) {
|
|
937
|
+
const env = options.env ?? process.env;
|
|
938
|
+
const homedir = options.homedir ?? import_node_os3.default.homedir();
|
|
939
|
+
const probe = options.probe ?? runVersionProbe;
|
|
940
|
+
let resolved = null;
|
|
941
|
+
try {
|
|
942
|
+
const resolvedPath = resolveHelperBinary(env, { homedir });
|
|
943
|
+
resolved = { path: resolvedPath, source: classifyResolvedSource(resolvedPath, env, homedir) };
|
|
944
|
+
} catch {
|
|
945
|
+
resolved = null;
|
|
946
|
+
}
|
|
947
|
+
const installed = [];
|
|
948
|
+
const root = macdriverInstallRoot(homedir);
|
|
949
|
+
if (import_node_fs2.default.existsSync(root)) {
|
|
950
|
+
for (const entry of import_node_fs2.default.readdirSync(root, { withFileTypes: true })) {
|
|
951
|
+
if (!entry.isDirectory()) continue;
|
|
952
|
+
let binaryPath;
|
|
953
|
+
try {
|
|
954
|
+
binaryPath = macdriverInstalledBinary(entry.name, homedir);
|
|
955
|
+
} catch {
|
|
956
|
+
continue;
|
|
957
|
+
}
|
|
958
|
+
if (import_node_fs2.default.existsSync(binaryPath)) {
|
|
959
|
+
installed.push({ version: entry.name, binaryPath });
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
installed.sort((a, b) => a.version.localeCompare(b.version));
|
|
963
|
+
}
|
|
964
|
+
const probedVersion = resolved ? await probe(resolved.path) : null;
|
|
965
|
+
return { resolved, pinnedVersion: MACDRIVER_VERSION, installed, probedVersion };
|
|
966
|
+
}
|
|
967
|
+
function classifyResolvedSource(resolvedPath, env, homedir) {
|
|
968
|
+
if (env.PROWL_MACDRIVER_BIN && resolvedPath === env.PROWL_MACDRIVER_BIN) {
|
|
969
|
+
return "env";
|
|
970
|
+
}
|
|
971
|
+
if (resolvedPath.startsWith(macdriverInstallRoot(homedir) + import_node_path3.default.sep)) {
|
|
972
|
+
return "user-install";
|
|
973
|
+
}
|
|
974
|
+
return "source-build";
|
|
975
|
+
}
|
|
976
|
+
function tccGuidance() {
|
|
977
|
+
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.";
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
// src/browser/android-driver.ts
|
|
981
|
+
var import_node_fs3 = __toESM(require("fs"), 1);
|
|
609
982
|
|
|
610
983
|
// src/analyzer/xml.ts
|
|
611
984
|
var ENTITIES = {
|
|
@@ -888,6 +1261,101 @@ function parseSnapshot(xml) {
|
|
|
888
1261
|
return parseXml(xml);
|
|
889
1262
|
}
|
|
890
1263
|
|
|
1264
|
+
// src/browser/touch-gestures.ts
|
|
1265
|
+
var DEFAULT_SWIPE_FRACTION = 0.75;
|
|
1266
|
+
var SCROLL_TO_PROBE_SWIPE_FRACTION = 0.6;
|
|
1267
|
+
var MAX_SWIPE_FRACTION = 0.9;
|
|
1268
|
+
var SWIPE_HOLD_MS = 100;
|
|
1269
|
+
var SWIPE_MOVE_DURATION_MS = 300;
|
|
1270
|
+
var SCROLL_TO_SWEEP_DEPTH = 10;
|
|
1271
|
+
var SCROLL_TO_PROBE_DIRECTIONS = [
|
|
1272
|
+
...Array.from({ length: SCROLL_TO_SWEEP_DEPTH }, () => "down"),
|
|
1273
|
+
...Array.from({ length: SCROLL_TO_SWEEP_DEPTH * 2 }, () => "up")
|
|
1274
|
+
];
|
|
1275
|
+
var MAX_SCROLL_TO_SWIPES = SCROLL_TO_PROBE_DIRECTIONS.length;
|
|
1276
|
+
var OPPOSITE_SWIPE_DIRECTIONS = {
|
|
1277
|
+
up: "down",
|
|
1278
|
+
down: "up",
|
|
1279
|
+
left: "right",
|
|
1280
|
+
right: "left"
|
|
1281
|
+
};
|
|
1282
|
+
async function probeScrollIntoView({
|
|
1283
|
+
isVisible,
|
|
1284
|
+
swipe,
|
|
1285
|
+
directions = SCROLL_TO_PROBE_DIRECTIONS
|
|
1286
|
+
}) {
|
|
1287
|
+
if (await isVisible()) {
|
|
1288
|
+
return true;
|
|
1289
|
+
}
|
|
1290
|
+
for (const direction of directions) {
|
|
1291
|
+
await swipe(direction);
|
|
1292
|
+
if (await isVisible()) {
|
|
1293
|
+
return true;
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
return false;
|
|
1297
|
+
}
|
|
1298
|
+
function toScreenSize(value, source) {
|
|
1299
|
+
const record = value ?? {};
|
|
1300
|
+
const width = Number(record.width);
|
|
1301
|
+
const height = Number(record.height);
|
|
1302
|
+
if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) {
|
|
1303
|
+
throw new Error(`${source} did not return a usable screen size`);
|
|
1304
|
+
}
|
|
1305
|
+
return { width, height };
|
|
1306
|
+
}
|
|
1307
|
+
function isVertical(direction) {
|
|
1308
|
+
return direction === "up" || direction === "down";
|
|
1309
|
+
}
|
|
1310
|
+
function swipeDistanceFor(direction, size, amount) {
|
|
1311
|
+
if (amount !== void 0 && !Number.isFinite(amount)) {
|
|
1312
|
+
throw new Error("scroll amount must be a finite number");
|
|
1313
|
+
}
|
|
1314
|
+
const axis = isVertical(direction) ? size.height : size.width;
|
|
1315
|
+
const requested = amount === void 0 ? axis * DEFAULT_SWIPE_FRACTION : Math.abs(amount);
|
|
1316
|
+
const max = axis * MAX_SWIPE_FRACTION;
|
|
1317
|
+
return Math.max(1, Math.round(Math.min(requested, max)));
|
|
1318
|
+
}
|
|
1319
|
+
function scrollToProbeDistanceFor(direction, size) {
|
|
1320
|
+
const axis = isVertical(direction) ? size.height : size.width;
|
|
1321
|
+
return Math.max(1, Math.round(axis * SCROLL_TO_PROBE_SWIPE_FRACTION));
|
|
1322
|
+
}
|
|
1323
|
+
function swipeEndpoints(direction, size, distance) {
|
|
1324
|
+
const cx = Math.round(size.width / 2);
|
|
1325
|
+
const cy = Math.round(size.height / 2);
|
|
1326
|
+
const half = Math.round(distance / 2);
|
|
1327
|
+
switch (direction) {
|
|
1328
|
+
case "down":
|
|
1329
|
+
return { start: { x: cx, y: cy + half }, end: { x: cx, y: cy - half } };
|
|
1330
|
+
case "up":
|
|
1331
|
+
return { start: { x: cx, y: cy - half }, end: { x: cx, y: cy + half } };
|
|
1332
|
+
case "right":
|
|
1333
|
+
return { start: { x: cx + half, y: cy }, end: { x: cx - half, y: cy } };
|
|
1334
|
+
case "left":
|
|
1335
|
+
return { start: { x: cx - half, y: cy }, end: { x: cx + half, y: cy } };
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
function buildSwipeActions(start, end) {
|
|
1339
|
+
return {
|
|
1340
|
+
type: "pointer",
|
|
1341
|
+
id: "finger1",
|
|
1342
|
+
parameters: { pointerType: "touch" },
|
|
1343
|
+
actions: [
|
|
1344
|
+
{ type: "pointerMove", duration: 0, x: start.x, y: start.y, origin: "viewport" },
|
|
1345
|
+
{ type: "pointerDown", button: 0 },
|
|
1346
|
+
{ type: "pause", duration: SWIPE_HOLD_MS },
|
|
1347
|
+
{ type: "pointerMove", duration: SWIPE_MOVE_DURATION_MS, x: end.x, y: end.y, origin: "viewport" },
|
|
1348
|
+
{ type: "pointerUp", button: 0 }
|
|
1349
|
+
]
|
|
1350
|
+
};
|
|
1351
|
+
}
|
|
1352
|
+
function buildDirectionalSwipe(direction, size, amount) {
|
|
1353
|
+
const normalizedDirection = amount !== void 0 && amount < 0 ? OPPOSITE_SWIPE_DIRECTIONS[direction] : direction;
|
|
1354
|
+
const distance = swipeDistanceFor(normalizedDirection, size, amount);
|
|
1355
|
+
const { start, end } = swipeEndpoints(normalizedDirection, size, distance);
|
|
1356
|
+
return { actions: buildSwipeActions(start, end), distance, start, end };
|
|
1357
|
+
}
|
|
1358
|
+
|
|
891
1359
|
// src/browser/android-driver.ts
|
|
892
1360
|
var ANDROID_CAPABILITIES = /* @__PURE__ */ new Set([
|
|
893
1361
|
"query",
|
|
@@ -1003,6 +1471,11 @@ function createAndroidDriver(client, options = {}) {
|
|
|
1003
1471
|
async function fillSelector(selector, value) {
|
|
1004
1472
|
await client.setValue(await resolveOne(selector), value);
|
|
1005
1473
|
}
|
|
1474
|
+
async function swipe(direction, amount, size) {
|
|
1475
|
+
const actualSize = size ?? await client.windowSize();
|
|
1476
|
+
const { actions } = buildDirectionalSwipe(direction, actualSize, amount);
|
|
1477
|
+
await client.performActions(actions);
|
|
1478
|
+
}
|
|
1006
1479
|
return {
|
|
1007
1480
|
capabilities: ANDROID_CAPABILITIES,
|
|
1008
1481
|
// navigation -----------------------------------------------------------
|
|
@@ -1040,8 +1513,31 @@ function createAndroidDriver(client, options = {}) {
|
|
|
1040
1513
|
hover() {
|
|
1041
1514
|
return rejectUnsupported("hover");
|
|
1042
1515
|
},
|
|
1043
|
-
|
|
1044
|
-
|
|
1516
|
+
// Screen-centred swipe via the W3C actions endpoint (PROWL-080). Direction
|
|
1517
|
+
// semantics match the web step: scrolling "down" reveals lower content, so
|
|
1518
|
+
// the finger drags up. See ./touch-gestures.ts.
|
|
1519
|
+
async scroll(direction, amount) {
|
|
1520
|
+
await swipe(direction, amount);
|
|
1521
|
+
},
|
|
1522
|
+
// Resolve the element, short-circuiting if it is already present in the
|
|
1523
|
+
// hierarchy; otherwise use the shared bounded down/up mobile probe before
|
|
1524
|
+
// failing with a message naming the selector and attempts.
|
|
1525
|
+
async scrollIntoView(selector) {
|
|
1526
|
+
const query = parseAndroidSelector(selector);
|
|
1527
|
+
let probeSize;
|
|
1528
|
+
const found = await probeScrollIntoView({
|
|
1529
|
+
isVisible: async () => (await client.findElements(query)).length > 0,
|
|
1530
|
+
swipe: async (direction) => {
|
|
1531
|
+
probeSize ??= await client.windowSize();
|
|
1532
|
+
await swipe(direction, scrollToProbeDistanceFor(direction, probeSize), probeSize);
|
|
1533
|
+
}
|
|
1534
|
+
});
|
|
1535
|
+
if (found) {
|
|
1536
|
+
return;
|
|
1537
|
+
}
|
|
1538
|
+
throw new Error(
|
|
1539
|
+
`scrollTo: element "${selector}" not visible after ${MAX_SCROLL_TO_SWIPES} scroll attempts on the Android target`
|
|
1540
|
+
);
|
|
1045
1541
|
},
|
|
1046
1542
|
setInputFiles() {
|
|
1047
1543
|
return rejectUnsupported("setInputFiles");
|
|
@@ -1097,7 +1593,7 @@ function createAndroidDriver(client, options = {}) {
|
|
|
1097
1593
|
},
|
|
1098
1594
|
async screenshot(screenshotOptions) {
|
|
1099
1595
|
const png = await client.screenshotPng();
|
|
1100
|
-
|
|
1596
|
+
import_node_fs3.default.writeFileSync(screenshotOptions.path, png);
|
|
1101
1597
|
},
|
|
1102
1598
|
// network / dialogs / downloads (all web-only) -------------------------
|
|
1103
1599
|
onResponse(_handler) {
|
|
@@ -1123,14 +1619,14 @@ function createAndroidDriver(client, options = {}) {
|
|
|
1123
1619
|
|
|
1124
1620
|
// src/browser/android-helper.ts
|
|
1125
1621
|
var import_node_module = require("module");
|
|
1126
|
-
var
|
|
1127
|
-
var
|
|
1128
|
-
var
|
|
1622
|
+
var import_node_child_process5 = require("child_process");
|
|
1623
|
+
var import_node_fs5 = __toESM(require("fs"), 1);
|
|
1624
|
+
var import_node_path5 = __toESM(require("path"), 1);
|
|
1129
1625
|
|
|
1130
1626
|
// src/browser/android-adb.ts
|
|
1131
|
-
var
|
|
1627
|
+
var import_node_child_process3 = require("child_process");
|
|
1132
1628
|
var execFileAdbRunner = (args, options) => new Promise((resolve) => {
|
|
1133
|
-
(0,
|
|
1629
|
+
(0, import_node_child_process3.execFile)(
|
|
1134
1630
|
"adb",
|
|
1135
1631
|
args,
|
|
1136
1632
|
{ encoding: "utf-8", timeout: options?.timeoutMs, maxBuffer: 16 * 1024 * 1024 },
|
|
@@ -1146,7 +1642,7 @@ var execFileAdbRunner = (args, options) => new Promise((resolve) => {
|
|
|
1146
1642
|
);
|
|
1147
1643
|
});
|
|
1148
1644
|
var spawnAdbProcess = (args) => {
|
|
1149
|
-
const child = (0,
|
|
1645
|
+
const child = (0, import_node_child_process3.spawn)("adb", args, { stdio: "ignore" });
|
|
1150
1646
|
child.on("error", () => {
|
|
1151
1647
|
});
|
|
1152
1648
|
return { kill: () => child.kill() };
|
|
@@ -1370,12 +1866,12 @@ var Uia2Transport = class {
|
|
|
1370
1866
|
* {@link Uia2HttpError} on a non-2xx response, or a timeout error when the
|
|
1371
1867
|
* per-request deadline elapses.
|
|
1372
1868
|
*/
|
|
1373
|
-
async request(method,
|
|
1869
|
+
async request(method, path20, body, timeoutMs) {
|
|
1374
1870
|
const requestTimeoutMs = timeoutMs ?? this.requestTimeoutMs;
|
|
1375
1871
|
const controller = new AbortController();
|
|
1376
1872
|
const timer = setTimeout(() => controller.abort(), requestTimeoutMs);
|
|
1377
1873
|
timer.unref?.();
|
|
1378
|
-
const url = `${this.baseUrl}${
|
|
1874
|
+
const url = `${this.baseUrl}${path20}`;
|
|
1379
1875
|
let response;
|
|
1380
1876
|
try {
|
|
1381
1877
|
response = await this.fetchImpl(url, {
|
|
@@ -1387,7 +1883,7 @@ var Uia2Transport = class {
|
|
|
1387
1883
|
} catch (error) {
|
|
1388
1884
|
if (controller.signal.aborted) {
|
|
1389
1885
|
const shown = requestTimeoutMs >= 1e3 ? `${Math.round(requestTimeoutMs / 1e3)}s` : `${requestTimeoutMs}ms`;
|
|
1390
|
-
throw new Error(`uiautomator2 request ${method} ${
|
|
1886
|
+
throw new Error(`uiautomator2 request ${method} ${path20} timed out after ${shown}`);
|
|
1391
1887
|
}
|
|
1392
1888
|
throw error instanceof Error ? error : new Error(String(error));
|
|
1393
1889
|
} finally {
|
|
@@ -1398,7 +1894,7 @@ var Uia2Transport = class {
|
|
|
1398
1894
|
if (!response.ok) {
|
|
1399
1895
|
const wdError = extractWebdriverError(parsed);
|
|
1400
1896
|
throw new Uia2HttpError(
|
|
1401
|
-
`uiautomator2 ${method} ${
|
|
1897
|
+
`uiautomator2 ${method} ${path20} failed (${response.status})${wdError ? `: ${wdError}` : ""}`,
|
|
1402
1898
|
response.status,
|
|
1403
1899
|
wdError
|
|
1404
1900
|
);
|
|
@@ -1481,10 +1977,10 @@ function sleep(ms) {
|
|
|
1481
1977
|
}
|
|
1482
1978
|
function createUia2AgentClient(transport, sessionId, options = {}) {
|
|
1483
1979
|
const base = `/session/${sessionId}`;
|
|
1484
|
-
async function locate(query,
|
|
1980
|
+
async function locate(query, path20) {
|
|
1485
1981
|
return transport.request(
|
|
1486
1982
|
"POST",
|
|
1487
|
-
`${base}${
|
|
1983
|
+
`${base}${path20}`,
|
|
1488
1984
|
androidQueryToLocator(query, { appPackage: options.appPackage })
|
|
1489
1985
|
);
|
|
1490
1986
|
}
|
|
@@ -1519,6 +2015,13 @@ function createUia2AgentClient(transport, sessionId, options = {}) {
|
|
|
1519
2015
|
async pressKeyCode(keyCode) {
|
|
1520
2016
|
await transport.request("POST", `${base}/appium/device/press_keycode`, { keycode: keyCode });
|
|
1521
2017
|
},
|
|
2018
|
+
async windowSize() {
|
|
2019
|
+
const value = await transport.request("GET", `${base}/window/current/size`);
|
|
2020
|
+
return toScreenSize(value, "uiautomator2 /window/current/size");
|
|
2021
|
+
},
|
|
2022
|
+
async performActions(actions) {
|
|
2023
|
+
await transport.request("POST", `${base}/actions`, { actions: [actions] });
|
|
2024
|
+
},
|
|
1522
2025
|
async screenshotPng() {
|
|
1523
2026
|
const value = await transport.request("GET", `${base}/screenshot`);
|
|
1524
2027
|
if (typeof value !== "string") {
|
|
@@ -1540,9 +2043,9 @@ function createUia2AgentClient(transport, sessionId, options = {}) {
|
|
|
1540
2043
|
}
|
|
1541
2044
|
|
|
1542
2045
|
// src/config/target.ts
|
|
1543
|
-
var
|
|
1544
|
-
var
|
|
1545
|
-
var
|
|
2046
|
+
var import_node_child_process4 = require("child_process");
|
|
2047
|
+
var import_node_fs4 = __toESM(require("fs"), 1);
|
|
2048
|
+
var import_node_path4 = __toESM(require("path"), 1);
|
|
1546
2049
|
var WEB_ONLY_STEP_TYPES = /* @__PURE__ */ new Set([
|
|
1547
2050
|
"navigate",
|
|
1548
2051
|
"waitForUrl",
|
|
@@ -1555,10 +2058,17 @@ var WEB_ONLY_STEP_TYPES = /* @__PURE__ */ new Set([
|
|
|
1555
2058
|
"select",
|
|
1556
2059
|
"selectOption",
|
|
1557
2060
|
"setInputFiles",
|
|
1558
|
-
"waitForDownload"
|
|
1559
|
-
"scroll"
|
|
1560
|
-
// directional scroll runs window.scrollBy (evaluate) — use scrollTo instead
|
|
2061
|
+
"waitForDownload"
|
|
1561
2062
|
]);
|
|
2063
|
+
var MACOS_UNSUPPORTED_STEP_TYPES = /* @__PURE__ */ new Set(["scroll"]);
|
|
2064
|
+
function macosUnsupportedReason(step) {
|
|
2065
|
+
for (const type of MACOS_UNSUPPORTED_STEP_TYPES) {
|
|
2066
|
+
if (type in step) {
|
|
2067
|
+
return type;
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
return null;
|
|
2071
|
+
}
|
|
1562
2072
|
function webOnlyReason(step) {
|
|
1563
2073
|
for (const type of WEB_ONLY_STEP_TYPES) {
|
|
1564
2074
|
if (type in step) {
|
|
@@ -1594,6 +2104,14 @@ function assertStepsSupportedByTarget(steps, target) {
|
|
|
1594
2104
|
`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.).`
|
|
1595
2105
|
);
|
|
1596
2106
|
}
|
|
2107
|
+
if (target === "macos") {
|
|
2108
|
+
const macReason = macosUnsupportedReason(step);
|
|
2109
|
+
if (macReason) {
|
|
2110
|
+
throw new Error(
|
|
2111
|
+
`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).`
|
|
2112
|
+
);
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
1597
2115
|
if ("if" in step) {
|
|
1598
2116
|
assertStepsSupportedByTarget(step.if.then, target);
|
|
1599
2117
|
if (step.if.else) {
|
|
@@ -1627,7 +2145,7 @@ function looksLikeIosAppPath(app) {
|
|
|
1627
2145
|
}
|
|
1628
2146
|
if (trimmed.toLowerCase().endsWith(".app")) {
|
|
1629
2147
|
try {
|
|
1630
|
-
return
|
|
2148
|
+
return import_node_fs4.default.statSync(normalizeAppPath(app)).isDirectory();
|
|
1631
2149
|
} catch {
|
|
1632
2150
|
return false;
|
|
1633
2151
|
}
|
|
@@ -1635,29 +2153,29 @@ function looksLikeIosAppPath(app) {
|
|
|
1635
2153
|
return false;
|
|
1636
2154
|
}
|
|
1637
2155
|
function normalizeAppPath(app) {
|
|
1638
|
-
return
|
|
2156
|
+
return import_node_path4.default.resolve(trimTrailingPathSeparators(app));
|
|
1639
2157
|
}
|
|
1640
2158
|
function parseBundleIdentifier(plist) {
|
|
1641
2159
|
const match = /<key>\s*CFBundleIdentifier\s*<\/key>\s*<string>\s*([^<]+?)\s*<\/string>/s.exec(plist);
|
|
1642
2160
|
return match?.[1]?.trim() || null;
|
|
1643
2161
|
}
|
|
1644
2162
|
function readBundleIdentifier(appPath, ...plistSubPath) {
|
|
1645
|
-
const infoPlistPath =
|
|
1646
|
-
if (!
|
|
2163
|
+
const infoPlistPath = import_node_path4.default.join(normalizeAppPath(appPath), ...plistSubPath);
|
|
2164
|
+
if (!import_node_fs4.default.existsSync(infoPlistPath)) {
|
|
1647
2165
|
return null;
|
|
1648
2166
|
}
|
|
1649
2167
|
try {
|
|
1650
|
-
const parsed = parseBundleIdentifier(
|
|
2168
|
+
const parsed = parseBundleIdentifier(import_node_fs4.default.readFileSync(infoPlistPath, "utf-8"));
|
|
1651
2169
|
if (parsed) {
|
|
1652
2170
|
return parsed;
|
|
1653
2171
|
}
|
|
1654
2172
|
} catch {
|
|
1655
2173
|
}
|
|
1656
|
-
if (process.platform !== "darwin" || !
|
|
2174
|
+
if (process.platform !== "darwin" || !import_node_fs4.default.existsSync("/usr/bin/plutil")) {
|
|
1657
2175
|
return null;
|
|
1658
2176
|
}
|
|
1659
2177
|
try {
|
|
1660
|
-
const output = (0,
|
|
2178
|
+
const output = (0, import_node_child_process4.execFileSync)(
|
|
1661
2179
|
"/usr/bin/plutil",
|
|
1662
2180
|
["-extract", "CFBundleIdentifier", "raw", "-o", "-", infoPlistPath],
|
|
1663
2181
|
{ encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"], timeout: 1e3 }
|
|
@@ -1679,7 +2197,7 @@ function appBundleAllowedIdentities(app, readBundleId, isAppPath = looksLikeAppB
|
|
|
1679
2197
|
const normalizedPath = normalizeAppPath(app);
|
|
1680
2198
|
identities.add(trimTrailingPathSeparators(app));
|
|
1681
2199
|
identities.add(normalizedPath);
|
|
1682
|
-
const bundleName =
|
|
2200
|
+
const bundleName = import_node_path4.default.basename(normalizedPath).replace(/\.app$/i, "");
|
|
1683
2201
|
if (bundleName) {
|
|
1684
2202
|
identities.add(bundleName);
|
|
1685
2203
|
}
|
|
@@ -1707,9 +2225,9 @@ function looksLikeApkPath(app) {
|
|
|
1707
2225
|
return trimmed.includes("/") || trimmed.includes("\\") || trimmed.toLowerCase().endsWith(".apk");
|
|
1708
2226
|
}
|
|
1709
2227
|
function normalizeAndroidApkPath(app) {
|
|
1710
|
-
const resolved =
|
|
2228
|
+
const resolved = import_node_path4.default.resolve(trimTrailingPathSeparators(app));
|
|
1711
2229
|
try {
|
|
1712
|
-
return
|
|
2230
|
+
return import_node_fs4.default.realpathSync.native(resolved);
|
|
1713
2231
|
} catch {
|
|
1714
2232
|
return resolved;
|
|
1715
2233
|
}
|
|
@@ -1752,12 +2270,12 @@ function resolveAgentApks(requireFn = (0, import_node_module.createRequire)(impo
|
|
|
1752
2270
|
"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"
|
|
1753
2271
|
);
|
|
1754
2272
|
}
|
|
1755
|
-
const pkgDir =
|
|
2273
|
+
const pkgDir = import_node_path5.default.dirname(pkgJsonPath);
|
|
1756
2274
|
const version = requireFn(pkgJsonPath).version;
|
|
1757
|
-
const serverApk =
|
|
1758
|
-
const testApk =
|
|
2275
|
+
const serverApk = import_node_path5.default.join(pkgDir, "apks", `appium-uiautomator2-server-v${version}.apk`);
|
|
2276
|
+
const testApk = import_node_path5.default.join(pkgDir, "apks", "appium-uiautomator2-server-debug-androidTest.apk");
|
|
1759
2277
|
for (const apk of [serverApk, testApk]) {
|
|
1760
|
-
if (!
|
|
2278
|
+
if (!import_node_fs5.default.existsSync(apk)) {
|
|
1761
2279
|
throw new Error(`Expected uiautomator2 agent APK is missing: ${apk}. Reinstall dependencies.`);
|
|
1762
2280
|
}
|
|
1763
2281
|
}
|
|
@@ -1769,7 +2287,7 @@ function looksLikeApk(app) {
|
|
|
1769
2287
|
var execFileAaptResolver = async (apkPath) => {
|
|
1770
2288
|
for (const tool of ["aapt", "aapt2"]) {
|
|
1771
2289
|
const output = await new Promise((resolve) => {
|
|
1772
|
-
(0,
|
|
2290
|
+
(0, import_node_child_process5.execFile)(
|
|
1773
2291
|
tool,
|
|
1774
2292
|
["dump", "badging", apkPath],
|
|
1775
2293
|
{ encoding: "utf-8", timeout: 2e4, maxBuffer: 8 * 1024 * 1024 },
|
|
@@ -1803,8 +2321,8 @@ async function resolvePackage(app, runner, serial, aaptResolver, allowedApps) {
|
|
|
1803
2321
|
assertAndroidAppAllowed(allowedApps, app);
|
|
1804
2322
|
return app;
|
|
1805
2323
|
}
|
|
1806
|
-
const apkPath =
|
|
1807
|
-
if (!
|
|
2324
|
+
const apkPath = import_node_path5.default.resolve(app);
|
|
2325
|
+
if (!import_node_fs5.default.existsSync(apkPath)) {
|
|
1808
2326
|
throw new Error(`APK not found: ${apkPath}`);
|
|
1809
2327
|
}
|
|
1810
2328
|
const pkg = await aaptResolver(apkPath);
|
|
@@ -1864,6 +2382,7 @@ async function launchAndroidSession(options) {
|
|
|
1864
2382
|
readyDeadlineMs,
|
|
1865
2383
|
appPackage: pkg
|
|
1866
2384
|
});
|
|
2385
|
+
await launchPackage(runner, serial, pkg);
|
|
1867
2386
|
const driver = createAndroidDriver(client, { appLabel: pkg });
|
|
1868
2387
|
return { client, driver, package: pkg, serial, teardown };
|
|
1869
2388
|
} catch (error) {
|
|
@@ -1884,6 +2403,7 @@ var IOS_CAPABILITIES = /* @__PURE__ */ new Set([
|
|
|
1884
2403
|
]);
|
|
1885
2404
|
var WAIT_POLL_INTERVAL_MS2 = 250;
|
|
1886
2405
|
var DEFAULT_WAIT_TIMEOUT_MS2 = 5e3;
|
|
2406
|
+
var DISPLAYED_CHECK_CONCURRENCY = 4;
|
|
1887
2407
|
var IOS_PRESS_KEYS = ["backspace", "del", "delete", "enter", "home", "return"];
|
|
1888
2408
|
function escapePredicateArg(value) {
|
|
1889
2409
|
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
@@ -1958,6 +2478,64 @@ function createIosDriver(client, options) {
|
|
|
1958
2478
|
async function fillSelector(selector, value) {
|
|
1959
2479
|
await client.setValue(await resolveOne(selector), value);
|
|
1960
2480
|
}
|
|
2481
|
+
async function swipe(direction, amount, size) {
|
|
2482
|
+
const actualSize = size ?? await client.windowSize();
|
|
2483
|
+
const { actions } = buildDirectionalSwipe(direction, actualSize, amount);
|
|
2484
|
+
await client.performActions(actions);
|
|
2485
|
+
}
|
|
2486
|
+
async function visibleElementIds(query) {
|
|
2487
|
+
const ids = await client.findElements(query);
|
|
2488
|
+
const visible = Array.from({ length: ids.length }, () => null);
|
|
2489
|
+
let nextIndex = 0;
|
|
2490
|
+
const workerCount = Math.min(DISPLAYED_CHECK_CONCURRENCY, ids.length);
|
|
2491
|
+
await Promise.all(
|
|
2492
|
+
Array.from({ length: workerCount }, async () => {
|
|
2493
|
+
for (; ; ) {
|
|
2494
|
+
const index = nextIndex;
|
|
2495
|
+
nextIndex += 1;
|
|
2496
|
+
if (index >= ids.length) {
|
|
2497
|
+
return;
|
|
2498
|
+
}
|
|
2499
|
+
const id = ids[index];
|
|
2500
|
+
if (await client.isDisplayed(id)) {
|
|
2501
|
+
visible[index] = id;
|
|
2502
|
+
}
|
|
2503
|
+
}
|
|
2504
|
+
})
|
|
2505
|
+
);
|
|
2506
|
+
return visible.filter((id) => id !== null);
|
|
2507
|
+
}
|
|
2508
|
+
async function hasVisibleElement(query) {
|
|
2509
|
+
const ids = await client.findElements(query);
|
|
2510
|
+
if (ids.length === 0) {
|
|
2511
|
+
return false;
|
|
2512
|
+
}
|
|
2513
|
+
if (await client.isDisplayed(ids[0])) {
|
|
2514
|
+
return true;
|
|
2515
|
+
}
|
|
2516
|
+
let found = false;
|
|
2517
|
+
let nextIndex = 1;
|
|
2518
|
+
const workerCount = Math.min(DISPLAYED_CHECK_CONCURRENCY, ids.length - 1);
|
|
2519
|
+
await Promise.all(
|
|
2520
|
+
Array.from({ length: workerCount }, async () => {
|
|
2521
|
+
for (; ; ) {
|
|
2522
|
+
if (found) {
|
|
2523
|
+
return;
|
|
2524
|
+
}
|
|
2525
|
+
const index = nextIndex;
|
|
2526
|
+
nextIndex += 1;
|
|
2527
|
+
if (index >= ids.length) {
|
|
2528
|
+
return;
|
|
2529
|
+
}
|
|
2530
|
+
if (await client.isDisplayed(ids[index])) {
|
|
2531
|
+
found = true;
|
|
2532
|
+
return;
|
|
2533
|
+
}
|
|
2534
|
+
}
|
|
2535
|
+
})
|
|
2536
|
+
);
|
|
2537
|
+
return found;
|
|
2538
|
+
}
|
|
1961
2539
|
async function pressKey(key) {
|
|
1962
2540
|
const name = key.trim().toLowerCase();
|
|
1963
2541
|
if (name === "enter" || name === "return") {
|
|
@@ -1989,6 +2567,9 @@ function createIosDriver(client, options) {
|
|
|
1989
2567
|
async count(selector) {
|
|
1990
2568
|
return (await client.findElements(parseIosSelector(selector))).length;
|
|
1991
2569
|
},
|
|
2570
|
+
async visibleCount(selector) {
|
|
2571
|
+
return (await visibleElementIds(parseIosSelector(selector))).length;
|
|
2572
|
+
},
|
|
1992
2573
|
async textContent(selector) {
|
|
1993
2574
|
const id = await client.findElement(parseIosSelector(selector));
|
|
1994
2575
|
if (id === null) {
|
|
@@ -2013,15 +2594,38 @@ function createIosDriver(client, options) {
|
|
|
2013
2594
|
hover() {
|
|
2014
2595
|
return rejectUnsupported("hover");
|
|
2015
2596
|
},
|
|
2016
|
-
|
|
2017
|
-
|
|
2597
|
+
// Screen-centred swipe via the W3C actions endpoint (PROWL-080). Direction
|
|
2598
|
+
// semantics match the web step: scrolling "down" reveals lower content, so
|
|
2599
|
+
// the finger drags up. See ./touch-gestures.ts.
|
|
2600
|
+
async scroll(direction, amount) {
|
|
2601
|
+
await swipe(direction, amount);
|
|
2602
|
+
},
|
|
2603
|
+
// Resolve the element, short-circuiting only if WDA reports a matching
|
|
2604
|
+
// element displayed in the viewport; hierarchy-only matches can be offscreen.
|
|
2605
|
+
// Otherwise use the shared bounded down/up mobile probe before failing.
|
|
2606
|
+
async scrollIntoView(selector) {
|
|
2607
|
+
const query = parseIosSelector(selector);
|
|
2608
|
+
let probeSize;
|
|
2609
|
+
const found = await probeScrollIntoView({
|
|
2610
|
+
isVisible: () => hasVisibleElement(query),
|
|
2611
|
+
swipe: async (direction) => {
|
|
2612
|
+
probeSize ??= await client.windowSize();
|
|
2613
|
+
await swipe(direction, scrollToProbeDistanceFor(direction, probeSize), probeSize);
|
|
2614
|
+
}
|
|
2615
|
+
});
|
|
2616
|
+
if (found) {
|
|
2617
|
+
return;
|
|
2618
|
+
}
|
|
2619
|
+
throw new Error(
|
|
2620
|
+
`scrollTo: element "${selector}" not visible after ${MAX_SCROLL_TO_SWIPES} scroll attempts on the iOS target`
|
|
2621
|
+
);
|
|
2018
2622
|
},
|
|
2019
2623
|
setInputFiles() {
|
|
2020
2624
|
return rejectUnsupported("setInputFiles");
|
|
2021
2625
|
},
|
|
2022
2626
|
// semantic locators ----------------------------------------------------
|
|
2023
2627
|
async countByRole(role, name) {
|
|
2024
|
-
return (await
|
|
2628
|
+
return (await visibleElementIds({ by: "role", role, name })).length;
|
|
2025
2629
|
},
|
|
2026
2630
|
async clickFirstByRole(role, name) {
|
|
2027
2631
|
const id = await client.findElement({ by: "role", role, name });
|
|
@@ -2031,7 +2635,7 @@ function createIosDriver(client, options) {
|
|
|
2031
2635
|
await client.click(id);
|
|
2032
2636
|
},
|
|
2033
2637
|
async countByLabel(label) {
|
|
2034
|
-
return (await
|
|
2638
|
+
return (await visibleElementIds({ by: "label", value: label })).length;
|
|
2035
2639
|
},
|
|
2036
2640
|
async fillFirstByLabel(label, value) {
|
|
2037
2641
|
const id = await client.findElement({ by: "label", value: label });
|
|
@@ -2049,7 +2653,7 @@ function createIosDriver(client, options) {
|
|
|
2049
2653
|
const timeoutMs = waitOptions?.timeout ?? DEFAULT_WAIT_TIMEOUT_MS2;
|
|
2050
2654
|
const deadline = Date.now() + timeoutMs;
|
|
2051
2655
|
for (; ; ) {
|
|
2052
|
-
if (
|
|
2656
|
+
if (await hasVisibleElement(query)) {
|
|
2053
2657
|
return;
|
|
2054
2658
|
}
|
|
2055
2659
|
if (Date.now() >= deadline) {
|
|
@@ -2095,19 +2699,19 @@ function createIosDriver(client, options) {
|
|
|
2095
2699
|
|
|
2096
2700
|
// src/browser/ios-helper.ts
|
|
2097
2701
|
var import_node_module2 = require("module");
|
|
2098
|
-
var
|
|
2099
|
-
var
|
|
2100
|
-
var
|
|
2101
|
-
var
|
|
2702
|
+
var import_node_child_process7 = require("child_process");
|
|
2703
|
+
var import_node_fs6 = __toESM(require("fs"), 1);
|
|
2704
|
+
var import_node_os5 = __toESM(require("os"), 1);
|
|
2705
|
+
var import_node_path7 = __toESM(require("path"), 1);
|
|
2102
2706
|
|
|
2103
2707
|
// src/browser/ios-simctl.ts
|
|
2104
|
-
var
|
|
2708
|
+
var import_node_child_process6 = require("child_process");
|
|
2105
2709
|
var import_promises = require("fs/promises");
|
|
2106
2710
|
var import_node_net = __toESM(require("net"), 1);
|
|
2107
|
-
var
|
|
2108
|
-
var
|
|
2711
|
+
var import_node_os4 = __toESM(require("os"), 1);
|
|
2712
|
+
var import_node_path6 = __toESM(require("path"), 1);
|
|
2109
2713
|
var spawnXcrunProcess = (args, options) => {
|
|
2110
|
-
const child = (0,
|
|
2714
|
+
const child = (0, import_node_child_process6.spawn)("xcrun", args, {
|
|
2111
2715
|
stdio: "ignore",
|
|
2112
2716
|
env: options?.env ? { ...process.env, ...options.env } : process.env
|
|
2113
2717
|
});
|
|
@@ -2119,10 +2723,10 @@ var spawnXcrunProcess = (args, options) => {
|
|
|
2119
2723
|
}
|
|
2120
2724
|
};
|
|
2121
2725
|
};
|
|
2122
|
-
var DEFAULT_SIMULATOR_LOCK_ROOT =
|
|
2726
|
+
var DEFAULT_SIMULATOR_LOCK_ROOT = import_node_path6.default.join(import_node_os4.default.tmpdir(), "prowl-ios-simulator-locks");
|
|
2123
2727
|
var SIMULATOR_LOCK_OWNER_FILE = "owner.json";
|
|
2124
2728
|
var execFileXcrunRunner = (args, options) => new Promise((resolve) => {
|
|
2125
|
-
(0,
|
|
2729
|
+
(0, import_node_child_process6.execFile)(
|
|
2126
2730
|
"xcrun",
|
|
2127
2731
|
args,
|
|
2128
2732
|
{
|
|
@@ -2159,7 +2763,7 @@ function isProcessAlive(pid) {
|
|
|
2159
2763
|
}
|
|
2160
2764
|
async function removeStaleSimulatorLock(lockPath) {
|
|
2161
2765
|
try {
|
|
2162
|
-
const ownerText = await (0, import_promises.readFile)(
|
|
2766
|
+
const ownerText = await (0, import_promises.readFile)(import_node_path6.default.join(lockPath, SIMULATOR_LOCK_OWNER_FILE), "utf8");
|
|
2163
2767
|
const owner = JSON.parse(ownerText);
|
|
2164
2768
|
if (typeof owner.pid === "number" && Number.isInteger(owner.pid) && owner.pid > 0 && !isProcessAlive(owner.pid)) {
|
|
2165
2769
|
await (0, import_promises.rm)(lockPath, { recursive: true, force: true });
|
|
@@ -2177,7 +2781,7 @@ function simulatorReservedError(udid) {
|
|
|
2177
2781
|
}
|
|
2178
2782
|
async function reserveSimulatorUdid(udid, options = {}) {
|
|
2179
2783
|
const lockRoot = options.lockRoot ?? DEFAULT_SIMULATOR_LOCK_ROOT;
|
|
2180
|
-
const lockPath =
|
|
2784
|
+
const lockPath = import_node_path6.default.join(lockRoot, simulatorLockName(udid));
|
|
2181
2785
|
await (0, import_promises.mkdir)(lockRoot, { recursive: true });
|
|
2182
2786
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
2183
2787
|
try {
|
|
@@ -2201,7 +2805,7 @@ async function reserveSimulatorUdid(udid, options = {}) {
|
|
|
2201
2805
|
};
|
|
2202
2806
|
try {
|
|
2203
2807
|
await (0, import_promises.writeFile)(
|
|
2204
|
-
|
|
2808
|
+
import_node_path6.default.join(lockPath, SIMULATOR_LOCK_OWNER_FILE),
|
|
2205
2809
|
`${JSON.stringify({ pid: process.pid, udid, createdAt: (/* @__PURE__ */ new Date()).toISOString() })}
|
|
2206
2810
|
`,
|
|
2207
2811
|
{ flag: "wx" }
|
|
@@ -2393,15 +2997,15 @@ var WdaTransport = class {
|
|
|
2393
2997
|
* {@link WdaHttpError} on a non-2xx response, or a timeout error when the
|
|
2394
2998
|
* per-request deadline elapses.
|
|
2395
2999
|
*/
|
|
2396
|
-
async requestFull(method,
|
|
3000
|
+
async requestFull(method, path20, body, timeoutMs) {
|
|
2397
3001
|
const requestTimeoutMs = timeoutMs ?? this.requestTimeoutMs;
|
|
2398
3002
|
const controller = new AbortController();
|
|
2399
3003
|
const timer = setTimeout(() => controller.abort(), requestTimeoutMs);
|
|
2400
3004
|
timer.unref?.();
|
|
2401
|
-
const url = `${this.baseUrl}${
|
|
3005
|
+
const url = `${this.baseUrl}${path20}`;
|
|
2402
3006
|
const timeoutError = () => {
|
|
2403
3007
|
const shown = requestTimeoutMs >= 1e3 ? `${Math.round(requestTimeoutMs / 1e3)}s` : `${requestTimeoutMs}ms`;
|
|
2404
|
-
return new Error(`WebDriverAgent request ${method} ${
|
|
3008
|
+
return new Error(`WebDriverAgent request ${method} ${path20} timed out after ${shown}`);
|
|
2405
3009
|
};
|
|
2406
3010
|
let response;
|
|
2407
3011
|
try {
|
|
@@ -2433,7 +3037,7 @@ var WdaTransport = class {
|
|
|
2433
3037
|
if (!response.ok) {
|
|
2434
3038
|
const wdError = extractWebdriverError2(parsed);
|
|
2435
3039
|
throw new WdaHttpError(
|
|
2436
|
-
`WebDriverAgent ${method} ${
|
|
3040
|
+
`WebDriverAgent ${method} ${path20} failed (${response.status})${wdError ? `: ${wdError}` : ""}`,
|
|
2437
3041
|
response.status,
|
|
2438
3042
|
wdError
|
|
2439
3043
|
);
|
|
@@ -2441,8 +3045,8 @@ var WdaTransport = class {
|
|
|
2441
3045
|
return parsed;
|
|
2442
3046
|
}
|
|
2443
3047
|
/** Like {@link requestFull} but returns just the `value` field. */
|
|
2444
|
-
async request(method,
|
|
2445
|
-
const parsed = await this.requestFull(method,
|
|
3048
|
+
async request(method, path20, body, timeoutMs) {
|
|
3049
|
+
const parsed = await this.requestFull(method, path20, body, timeoutMs);
|
|
2446
3050
|
return parsed?.value;
|
|
2447
3051
|
}
|
|
2448
3052
|
};
|
|
@@ -2518,8 +3122,8 @@ function sleep2(ms) {
|
|
|
2518
3122
|
}
|
|
2519
3123
|
function createWdaAgentClient(transport, sessionId) {
|
|
2520
3124
|
const base = `/session/${sessionId}`;
|
|
2521
|
-
async function locate(query,
|
|
2522
|
-
return transport.request("POST", `${base}${
|
|
3125
|
+
async function locate(query, path20) {
|
|
3126
|
+
return transport.request("POST", `${base}${path20}`, iosQueryToLocator(query));
|
|
2523
3127
|
}
|
|
2524
3128
|
return {
|
|
2525
3129
|
async findElement(query) {
|
|
@@ -2549,12 +3153,29 @@ function createWdaAgentClient(transport, sessionId) {
|
|
|
2549
3153
|
const value = await transport.request("GET", `${base}/element/${elementId}/text`);
|
|
2550
3154
|
return typeof value === "string" ? value : value == null ? null : String(value);
|
|
2551
3155
|
},
|
|
3156
|
+
async isDisplayed(elementId) {
|
|
3157
|
+
try {
|
|
3158
|
+
return await transport.request("GET", `${base}/element/${elementId}/displayed`) === true;
|
|
3159
|
+
} catch (error) {
|
|
3160
|
+
if (isNoSuchElement2(error)) {
|
|
3161
|
+
return false;
|
|
3162
|
+
}
|
|
3163
|
+
throw error;
|
|
3164
|
+
}
|
|
3165
|
+
},
|
|
2552
3166
|
async sendKeys(keys) {
|
|
2553
3167
|
await transport.request("POST", `${base}/wda/keys`, { value: keys });
|
|
2554
3168
|
},
|
|
2555
3169
|
async homescreen() {
|
|
2556
3170
|
await transport.request("POST", "/wda/homescreen", {});
|
|
2557
3171
|
},
|
|
3172
|
+
async windowSize() {
|
|
3173
|
+
const value = await transport.request("GET", `${base}/window/size`);
|
|
3174
|
+
return toScreenSize(value, "WebDriverAgent /window/size");
|
|
3175
|
+
},
|
|
3176
|
+
async performActions(actions) {
|
|
3177
|
+
await transport.request("POST", `${base}/actions`, { actions: [actions] });
|
|
3178
|
+
},
|
|
2558
3179
|
async source() {
|
|
2559
3180
|
const value = await transport.request("GET", "/source");
|
|
2560
3181
|
if (typeof value !== "string") {
|
|
@@ -2595,41 +3216,41 @@ function resolveWdaProject(requireFn = (0, import_node_module2.createRequire)(im
|
|
|
2595
3216
|
"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"
|
|
2596
3217
|
);
|
|
2597
3218
|
}
|
|
2598
|
-
const pkgDir =
|
|
3219
|
+
const pkgDir = import_node_path7.default.dirname(pkgJsonPath);
|
|
2599
3220
|
const version = requireFn(pkgJsonPath).version;
|
|
2600
|
-
const projectPath =
|
|
2601
|
-
if (!
|
|
3221
|
+
const projectPath = import_node_path7.default.join(pkgDir, "WebDriverAgent.xcodeproj");
|
|
3222
|
+
if (!import_node_fs6.default.existsSync(projectPath)) {
|
|
2602
3223
|
throw new Error(`Expected WebDriverAgent project is missing: ${projectPath}. Reinstall dependencies.`);
|
|
2603
3224
|
}
|
|
2604
3225
|
return { projectPath, version };
|
|
2605
3226
|
}
|
|
2606
|
-
function wdaCacheDir(wdaVersion, xcode, homeDir =
|
|
2607
|
-
return
|
|
3227
|
+
function wdaCacheDir(wdaVersion, xcode, homeDir = import_node_os5.default.homedir()) {
|
|
3228
|
+
return import_node_path7.default.join(homeDir, ".prowl", "wda", `${wdaVersion}-xcode${xcode}`);
|
|
2608
3229
|
}
|
|
2609
3230
|
function productsDir(derivedDataPath) {
|
|
2610
|
-
return
|
|
3231
|
+
return import_node_path7.default.join(derivedDataPath, "Build", "Products");
|
|
2611
3232
|
}
|
|
2612
3233
|
function findXctestrunIn(dir) {
|
|
2613
3234
|
let entries;
|
|
2614
3235
|
try {
|
|
2615
|
-
entries =
|
|
3236
|
+
entries = import_node_fs6.default.readdirSync(dir);
|
|
2616
3237
|
} catch {
|
|
2617
3238
|
return null;
|
|
2618
3239
|
}
|
|
2619
3240
|
const match = entries.filter((name) => name.endsWith(".xctestrun") && !name.startsWith(PREPARED_XCTESTRUN_PREFIX)).sort()[0];
|
|
2620
|
-
return match ?
|
|
3241
|
+
return match ? import_node_path7.default.join(dir, match) : null;
|
|
2621
3242
|
}
|
|
2622
3243
|
function resolveOverrideXctestrun(override) {
|
|
2623
|
-
if (!
|
|
3244
|
+
if (!import_node_fs6.default.existsSync(override)) {
|
|
2624
3245
|
throw new Error(`PROWL_WDA_RUNNER points at a missing path: ${override}`);
|
|
2625
3246
|
}
|
|
2626
3247
|
if (override.endsWith(".xctestrun")) {
|
|
2627
3248
|
return override;
|
|
2628
3249
|
}
|
|
2629
3250
|
const candidates = [];
|
|
2630
|
-
const stat =
|
|
3251
|
+
const stat = import_node_fs6.default.statSync(override);
|
|
2631
3252
|
if (override.endsWith(".app")) {
|
|
2632
|
-
candidates.push(
|
|
3253
|
+
candidates.push(import_node_path7.default.dirname(import_node_path7.default.dirname(override)));
|
|
2633
3254
|
} else if (stat.isDirectory()) {
|
|
2634
3255
|
candidates.push(override, productsDir(override));
|
|
2635
3256
|
}
|
|
@@ -2646,7 +3267,7 @@ function resolveOverrideXctestrun(override) {
|
|
|
2646
3267
|
async function resolveWdaTestRun(options = {}) {
|
|
2647
3268
|
const runner = options.runner ?? execFileXcrunRunner;
|
|
2648
3269
|
const env = options.env ?? process.env;
|
|
2649
|
-
const homeDir = options.homeDir ??
|
|
3270
|
+
const homeDir = options.homeDir ?? import_node_os5.default.homedir();
|
|
2650
3271
|
const log = options.logger ?? ((message) => process.stderr.write(`${message}
|
|
2651
3272
|
`));
|
|
2652
3273
|
const override = env.PROWL_WDA_RUNNER;
|
|
@@ -2668,7 +3289,7 @@ async function resolveWdaTestRun(options = {}) {
|
|
|
2668
3289
|
log(
|
|
2669
3290
|
`Prowl: building WebDriverAgent for the iOS target (first run only; this can take a few minutes)\u2026`
|
|
2670
3291
|
);
|
|
2671
|
-
|
|
3292
|
+
import_node_fs6.default.mkdirSync(cacheDir, { recursive: true });
|
|
2672
3293
|
const result = await runner(
|
|
2673
3294
|
[
|
|
2674
3295
|
"xcodebuild",
|
|
@@ -2734,7 +3355,7 @@ function injectUsePortIntoXctestrun(plist, port) {
|
|
|
2734
3355
|
}
|
|
2735
3356
|
function runPlutil(args) {
|
|
2736
3357
|
return new Promise((resolve, reject) => {
|
|
2737
|
-
(0,
|
|
3358
|
+
(0, import_node_child_process7.execFile)(
|
|
2738
3359
|
"plutil",
|
|
2739
3360
|
args,
|
|
2740
3361
|
{ encoding: "utf-8", maxBuffer: 32 * 1024 * 1024 },
|
|
@@ -2762,13 +3383,13 @@ var defaultWdaTestRunPreparer = async ({ xctestrunPath, port }) => {
|
|
|
2762
3383
|
}
|
|
2763
3384
|
const injected = injectUsePortIntoXctestrun(parsed, port);
|
|
2764
3385
|
const stem = `${PREPARED_XCTESTRUN_PREFIX}${port}-${process.pid}`;
|
|
2765
|
-
const outPath =
|
|
2766
|
-
const jsonPath =
|
|
2767
|
-
|
|
3386
|
+
const outPath = import_node_path7.default.join(import_node_path7.default.dirname(xctestrunPath), `${stem}.xctestrun`);
|
|
3387
|
+
const jsonPath = import_node_path7.default.join(import_node_os5.default.tmpdir(), `${stem}.json`);
|
|
3388
|
+
import_node_fs6.default.writeFileSync(jsonPath, JSON.stringify(injected));
|
|
2768
3389
|
try {
|
|
2769
3390
|
await runPlutil(["-convert", "xml1", jsonPath, "-o", outPath]);
|
|
2770
3391
|
} finally {
|
|
2771
|
-
|
|
3392
|
+
import_node_fs6.default.rmSync(jsonPath, { force: true });
|
|
2772
3393
|
}
|
|
2773
3394
|
return outPath;
|
|
2774
3395
|
};
|
|
@@ -2776,10 +3397,10 @@ function cleanupPreparedTestRun(preparedPath) {
|
|
|
2776
3397
|
if (!preparedPath) {
|
|
2777
3398
|
return;
|
|
2778
3399
|
}
|
|
2779
|
-
if (!
|
|
3400
|
+
if (!import_node_path7.default.basename(preparedPath).startsWith(PREPARED_XCTESTRUN_PREFIX)) {
|
|
2780
3401
|
return;
|
|
2781
3402
|
}
|
|
2782
|
-
|
|
3403
|
+
import_node_fs6.default.rmSync(preparedPath, { force: true });
|
|
2783
3404
|
}
|
|
2784
3405
|
function wdaTestRunArgs(xctestrunPath, udid) {
|
|
2785
3406
|
return [
|
|
@@ -2801,8 +3422,8 @@ async function resolveBundleId(app, runner, udid, coldStart, allowedApps) {
|
|
|
2801
3422
|
}
|
|
2802
3423
|
return app;
|
|
2803
3424
|
}
|
|
2804
|
-
const appPath =
|
|
2805
|
-
if (!
|
|
3425
|
+
const appPath = import_node_path7.default.resolve(app);
|
|
3426
|
+
if (!import_node_fs6.default.existsSync(appPath)) {
|
|
2806
3427
|
throw new Error(`.app bundle not found: ${appPath}`);
|
|
2807
3428
|
}
|
|
2808
3429
|
const bundleId = readIosBundleIdentifier(appPath);
|
|
@@ -2905,12 +3526,12 @@ async function closeIosSession(session) {
|
|
|
2905
3526
|
}
|
|
2906
3527
|
|
|
2907
3528
|
// src/runner/index.ts
|
|
2908
|
-
var
|
|
2909
|
-
var
|
|
3529
|
+
var import_node_fs15 = __toESM(require("fs"), 1);
|
|
3530
|
+
var import_node_path16 = __toESM(require("path"), 1);
|
|
2910
3531
|
|
|
2911
3532
|
// src/config/loader.ts
|
|
2912
|
-
var
|
|
2913
|
-
var
|
|
3533
|
+
var import_node_fs7 = __toESM(require("fs"), 1);
|
|
3534
|
+
var import_node_path8 = __toESM(require("path"), 1);
|
|
2914
3535
|
var import_yaml = __toESM(require("yaml"), 1);
|
|
2915
3536
|
var import_dotenv = __toESM(require("dotenv"), 1);
|
|
2916
3537
|
|
|
@@ -2928,7 +3549,7 @@ function isValidHuntName(name) {
|
|
|
2928
3549
|
function assertValidHuntName(name) {
|
|
2929
3550
|
if (!isValidHuntName(name)) {
|
|
2930
3551
|
throw new Error(
|
|
2931
|
-
`Invalid hunt name: "${name}".
|
|
3552
|
+
`Invalid hunt name: "${name}". A hunt name is its file name under .prowl/hunts/ (letters, numbers, hyphens, underscores, and forward slashes for nested hunts). Accepted forms: a bare name (homepage), a nested name (admin/users), or a path (.prowl/hunts/homepage.yml or hunts/homepage.yml).`
|
|
2932
3553
|
);
|
|
2933
3554
|
}
|
|
2934
3555
|
}
|
|
@@ -3249,260 +3870,8 @@ var huntSchema = import_zod.z.object({
|
|
|
3249
3870
|
}).strict().optional()
|
|
3250
3871
|
}).strict();
|
|
3251
3872
|
|
|
3252
|
-
// src/config/loader.ts
|
|
3253
|
-
var DEFAULT_WEB_URL = "http://localhost:3000";
|
|
3254
|
-
var DEFAULT_CONFIG = {
|
|
3255
|
-
target: {
|
|
3256
|
-
type: "web",
|
|
3257
|
-
url: DEFAULT_WEB_URL
|
|
3258
|
-
},
|
|
3259
|
-
browser: {
|
|
3260
|
-
headless: true,
|
|
3261
|
-
slowMo: 0,
|
|
3262
|
-
timeout: 3e4,
|
|
3263
|
-
engine: "chromium",
|
|
3264
|
-
viewport: { width: 1280, height: 720 }
|
|
3265
|
-
},
|
|
3266
|
-
artifacts: {
|
|
3267
|
-
screenshots: "on-failure",
|
|
3268
|
-
networkHar: false,
|
|
3269
|
-
console: true,
|
|
3270
|
-
junit: false
|
|
3271
|
-
},
|
|
3272
|
-
assertions: {
|
|
3273
|
-
noConsoleErrors: true,
|
|
3274
|
-
noNetworkErrors: true,
|
|
3275
|
-
maxTotalTimeMs: 3e4,
|
|
3276
|
-
networkIgnorePatterns: []
|
|
3277
|
-
},
|
|
3278
|
-
guardrails: {
|
|
3279
|
-
maxSteps: 50,
|
|
3280
|
-
allowedDomains: ["localhost", "127.0.0.1", "0.0.0.0"],
|
|
3281
|
-
allowedApps: [],
|
|
3282
|
-
forbiddenSelectors: ["[data-danger]", ".delete-btn"],
|
|
3283
|
-
selfHealing: false
|
|
3284
|
-
},
|
|
3285
|
-
auth: {
|
|
3286
|
-
storageStatePath: ".prowl/auth-state.json"
|
|
3287
|
-
},
|
|
3288
|
-
history: {
|
|
3289
|
-
maxRuns: 100
|
|
3290
|
-
}
|
|
3291
|
-
};
|
|
3292
|
-
var CONFIG_DIR = ".prowl";
|
|
3293
|
-
var LEGACY_CONFIG_DIR = ".prowlqa";
|
|
3294
|
-
var legacyDirWarned = false;
|
|
3295
|
-
function warnLegacyConfigDir() {
|
|
3296
|
-
if (legacyDirWarned) {
|
|
3297
|
-
return;
|
|
3298
|
-
}
|
|
3299
|
-
legacyDirWarned = true;
|
|
3300
|
-
console.warn(
|
|
3301
|
-
'Warning: the ".prowlqa/" config directory is deprecated; rename it to ".prowl/". Support for ".prowlqa/" will be removed in a future release.'
|
|
3302
|
-
);
|
|
3303
|
-
}
|
|
3304
|
-
function findConfigPath(startDir) {
|
|
3305
|
-
let current = startDir;
|
|
3306
|
-
while (current) {
|
|
3307
|
-
for (const dir of [CONFIG_DIR, LEGACY_CONFIG_DIR]) {
|
|
3308
|
-
const candidate = import_node_path6.default.join(current, dir, "config.yml");
|
|
3309
|
-
if (import_node_fs6.default.existsSync(candidate)) {
|
|
3310
|
-
return candidate;
|
|
3311
|
-
}
|
|
3312
|
-
}
|
|
3313
|
-
const parent = import_node_path6.default.dirname(current);
|
|
3314
|
-
if (parent === current) {
|
|
3315
|
-
break;
|
|
3316
|
-
}
|
|
3317
|
-
current = parent;
|
|
3318
|
-
}
|
|
3319
|
-
return null;
|
|
3320
|
-
}
|
|
3321
|
-
var VIEWPORT_PRESETS = {
|
|
3322
|
-
mobile: { width: 375, height: 812 },
|
|
3323
|
-
tablet: { width: 768, height: 1024 },
|
|
3324
|
-
desktop: { width: 1280, height: 720 }
|
|
3325
|
-
};
|
|
3326
|
-
function resolveViewport(value) {
|
|
3327
|
-
if (value === void 0) {
|
|
3328
|
-
return DEFAULT_CONFIG.browser.viewport;
|
|
3329
|
-
}
|
|
3330
|
-
if (typeof value === "string") {
|
|
3331
|
-
const preset = VIEWPORT_PRESETS[value];
|
|
3332
|
-
if (!preset) {
|
|
3333
|
-
throw new Error(`Unknown viewport preset: "${value}". Use mobile, tablet, or desktop.`);
|
|
3334
|
-
}
|
|
3335
|
-
return preset;
|
|
3336
|
-
}
|
|
3337
|
-
return value;
|
|
3338
|
-
}
|
|
3339
|
-
function resolveTarget(target) {
|
|
3340
|
-
const type = target?.type;
|
|
3341
|
-
if (type === "macos") {
|
|
3342
|
-
return { type: "macos", app: target.app };
|
|
3343
|
-
}
|
|
3344
|
-
if (type === "android") {
|
|
3345
|
-
const androidTarget = target;
|
|
3346
|
-
return {
|
|
3347
|
-
type: "android",
|
|
3348
|
-
app: androidTarget.app,
|
|
3349
|
-
...androidTarget.deviceSerial !== void 0 ? { deviceSerial: androidTarget.deviceSerial } : {},
|
|
3350
|
-
...androidTarget.coldStart !== void 0 ? { coldStart: androidTarget.coldStart } : {}
|
|
3351
|
-
};
|
|
3352
|
-
}
|
|
3353
|
-
if (type === "ios") {
|
|
3354
|
-
const iosTarget = target;
|
|
3355
|
-
return {
|
|
3356
|
-
type: "ios",
|
|
3357
|
-
app: iosTarget.app,
|
|
3358
|
-
...iosTarget.udid !== void 0 ? { udid: iosTarget.udid } : {},
|
|
3359
|
-
...iosTarget.coldStart !== void 0 ? { coldStart: iosTarget.coldStart } : {}
|
|
3360
|
-
};
|
|
3361
|
-
}
|
|
3362
|
-
return {
|
|
3363
|
-
type: "web",
|
|
3364
|
-
url: target?.url ?? DEFAULT_WEB_URL
|
|
3365
|
-
};
|
|
3366
|
-
}
|
|
3367
|
-
function mergeConfig(partial) {
|
|
3368
|
-
return {
|
|
3369
|
-
target: resolveTarget(partial.target),
|
|
3370
|
-
browser: {
|
|
3371
|
-
headless: partial.browser?.headless ?? DEFAULT_CONFIG.browser.headless,
|
|
3372
|
-
slowMo: partial.browser?.slowMo ?? DEFAULT_CONFIG.browser.slowMo,
|
|
3373
|
-
timeout: partial.browser?.timeout ?? DEFAULT_CONFIG.browser.timeout,
|
|
3374
|
-
engine: partial.browser?.engine ?? DEFAULT_CONFIG.browser.engine,
|
|
3375
|
-
channel: partial.browser?.channel,
|
|
3376
|
-
viewport: resolveViewport(partial.browser?.viewport)
|
|
3377
|
-
},
|
|
3378
|
-
artifacts: {
|
|
3379
|
-
screenshots: partial.artifacts?.screenshots ?? DEFAULT_CONFIG.artifacts.screenshots,
|
|
3380
|
-
networkHar: partial.artifacts?.networkHar ?? DEFAULT_CONFIG.artifacts.networkHar,
|
|
3381
|
-
console: partial.artifacts?.console ?? DEFAULT_CONFIG.artifacts.console,
|
|
3382
|
-
junit: partial.artifacts?.junit ?? DEFAULT_CONFIG.artifacts.junit
|
|
3383
|
-
},
|
|
3384
|
-
assertions: {
|
|
3385
|
-
noConsoleErrors: partial.assertions?.noConsoleErrors ?? DEFAULT_CONFIG.assertions.noConsoleErrors,
|
|
3386
|
-
noNetworkErrors: partial.assertions?.noNetworkErrors ?? DEFAULT_CONFIG.assertions.noNetworkErrors,
|
|
3387
|
-
maxTotalTimeMs: partial.assertions?.maxTotalTimeMs ?? DEFAULT_CONFIG.assertions.maxTotalTimeMs,
|
|
3388
|
-
networkIgnorePatterns: partial.assertions?.networkIgnorePatterns ?? DEFAULT_CONFIG.assertions.networkIgnorePatterns
|
|
3389
|
-
},
|
|
3390
|
-
guardrails: {
|
|
3391
|
-
maxSteps: partial.guardrails?.maxSteps ?? DEFAULT_CONFIG.guardrails.maxSteps,
|
|
3392
|
-
allowedDomains: partial.guardrails?.allowedDomains ?? DEFAULT_CONFIG.guardrails.allowedDomains,
|
|
3393
|
-
allowedApps: partial.guardrails?.allowedApps ?? DEFAULT_CONFIG.guardrails.allowedApps,
|
|
3394
|
-
forbiddenSelectors: partial.guardrails?.forbiddenSelectors ?? DEFAULT_CONFIG.guardrails.forbiddenSelectors,
|
|
3395
|
-
selfHealing: partial.guardrails?.selfHealing ?? DEFAULT_CONFIG.guardrails.selfHealing
|
|
3396
|
-
},
|
|
3397
|
-
auth: {
|
|
3398
|
-
storageStatePath: partial.auth?.storageStatePath ?? (partial.auth !== void 0 ? DEFAULT_CONFIG.auth.storageStatePath : void 0)
|
|
3399
|
-
},
|
|
3400
|
-
history: {
|
|
3401
|
-
maxRuns: partial.history?.maxRuns ?? DEFAULT_CONFIG.history.maxRuns
|
|
3402
|
-
},
|
|
3403
|
-
bugLog: partial.bugLog,
|
|
3404
|
-
tracing: partial.tracing,
|
|
3405
|
-
reliability: partial.reliability
|
|
3406
|
-
};
|
|
3407
|
-
}
|
|
3408
|
-
function ensureAllowedDomain(allowed, urlValue) {
|
|
3409
|
-
try {
|
|
3410
|
-
const host = new URL(urlValue).hostname;
|
|
3411
|
-
if (!allowed.includes(host)) {
|
|
3412
|
-
return [...allowed, host];
|
|
3413
|
-
}
|
|
3414
|
-
} catch {
|
|
3415
|
-
return allowed;
|
|
3416
|
-
}
|
|
3417
|
-
return allowed;
|
|
3418
|
-
}
|
|
3419
|
-
function loadConfig(configPath) {
|
|
3420
|
-
const resolvedPath = configPath ? import_node_path6.default.resolve(configPath) : findConfigPath(process.cwd());
|
|
3421
|
-
if (!resolvedPath) {
|
|
3422
|
-
throw new Error("Could not find .prowl/config.yml. Run `prowl init` first.");
|
|
3423
|
-
}
|
|
3424
|
-
if (!import_node_fs6.default.existsSync(resolvedPath)) {
|
|
3425
|
-
throw new Error(`Config file not found at ${resolvedPath}`);
|
|
3426
|
-
}
|
|
3427
|
-
const configDir = import_node_path6.default.dirname(resolvedPath);
|
|
3428
|
-
if (import_node_path6.default.basename(configDir) === LEGACY_CONFIG_DIR) {
|
|
3429
|
-
warnLegacyConfigDir();
|
|
3430
|
-
}
|
|
3431
|
-
import_dotenv.default.config({ path: import_node_path6.default.join(configDir, ".env"), override: false });
|
|
3432
|
-
const raw = import_node_fs6.default.readFileSync(resolvedPath, "utf-8");
|
|
3433
|
-
const parsed = import_yaml.default.parse(raw) ?? {};
|
|
3434
|
-
const validated = configSchema.parse(parsed);
|
|
3435
|
-
const config = mergeConfig(validated);
|
|
3436
|
-
if (config.target.type === "web") {
|
|
3437
|
-
config.guardrails.allowedDomains = ensureAllowedDomain(
|
|
3438
|
-
config.guardrails.allowedDomains,
|
|
3439
|
-
config.target.url
|
|
3440
|
-
);
|
|
3441
|
-
}
|
|
3442
|
-
return { config, configPath: resolvedPath, configDir };
|
|
3443
|
-
}
|
|
3444
|
-
function loadHunt(huntName, configDir) {
|
|
3445
|
-
assertValidHuntName(huntName);
|
|
3446
|
-
const huntPath = import_node_path6.default.join(configDir, "hunts", `${huntName}.yml`);
|
|
3447
|
-
if (!import_node_fs6.default.existsSync(huntPath)) {
|
|
3448
|
-
throw new Error(`Hunt file not found: ${huntPath}`);
|
|
3449
|
-
}
|
|
3450
|
-
const raw = import_node_fs6.default.readFileSync(huntPath, "utf-8");
|
|
3451
|
-
const parsed = import_yaml.default.parse(raw) ?? {};
|
|
3452
|
-
const validated = huntSchema.parse(parsed);
|
|
3453
|
-
return validated;
|
|
3454
|
-
}
|
|
3455
|
-
function loadHuntTags(huntName, configDir) {
|
|
3456
|
-
assertValidHuntName(huntName);
|
|
3457
|
-
const huntPath = import_node_path6.default.join(configDir, "hunts", `${huntName}.yml`);
|
|
3458
|
-
if (!import_node_fs6.default.existsSync(huntPath)) {
|
|
3459
|
-
return [];
|
|
3460
|
-
}
|
|
3461
|
-
const raw = import_node_fs6.default.readFileSync(huntPath, "utf-8");
|
|
3462
|
-
const parsed = import_yaml.default.parse(raw) ?? {};
|
|
3463
|
-
return Array.isArray(parsed.tags) ? parsed.tags : [];
|
|
3464
|
-
}
|
|
3465
|
-
function loadHuntMeta(huntName, configDir) {
|
|
3466
|
-
assertValidHuntName(huntName);
|
|
3467
|
-
const huntPath = import_node_path6.default.join(configDir, "hunts", `${huntName}.yml`);
|
|
3468
|
-
if (!import_node_fs6.default.existsSync(huntPath)) {
|
|
3469
|
-
return { tags: [] };
|
|
3470
|
-
}
|
|
3471
|
-
const raw = import_node_fs6.default.readFileSync(huntPath, "utf-8");
|
|
3472
|
-
const parsed = import_yaml.default.parse(raw) ?? {};
|
|
3473
|
-
return {
|
|
3474
|
-
description: typeof parsed.description === "string" ? parsed.description : void 0,
|
|
3475
|
-
tags: Array.isArray(parsed.tags) ? parsed.tags : []
|
|
3476
|
-
};
|
|
3477
|
-
}
|
|
3478
|
-
function listHunts(configDir) {
|
|
3479
|
-
const huntsDir = import_node_path6.default.join(configDir, "hunts");
|
|
3480
|
-
if (!import_node_fs6.default.existsSync(huntsDir)) {
|
|
3481
|
-
return [];
|
|
3482
|
-
}
|
|
3483
|
-
const stats = import_node_fs6.default.statSync(huntsDir);
|
|
3484
|
-
if (!stats.isDirectory()) {
|
|
3485
|
-
throw new Error(`Hunts path is not a directory: ${huntsDir}`);
|
|
3486
|
-
}
|
|
3487
|
-
const results = [];
|
|
3488
|
-
function scanDir(dir) {
|
|
3489
|
-
const entries = import_node_fs6.default.readdirSync(dir, { withFileTypes: true });
|
|
3490
|
-
for (const entry of entries) {
|
|
3491
|
-
if (entry.isFile() && entry.name.endsWith(".yml")) {
|
|
3492
|
-
const fullPath = import_node_path6.default.join(dir, entry.name);
|
|
3493
|
-
const relative = import_node_path6.default.relative(huntsDir, fullPath);
|
|
3494
|
-
results.push(relative.replace(/\.yml$/, ""));
|
|
3495
|
-
} else if (entry.isDirectory()) {
|
|
3496
|
-
scanDir(import_node_path6.default.join(dir, entry.name));
|
|
3497
|
-
}
|
|
3498
|
-
}
|
|
3499
|
-
}
|
|
3500
|
-
scanDir(huntsDir);
|
|
3501
|
-
return results.sort((a, b) => a.localeCompare(b));
|
|
3502
|
-
}
|
|
3503
|
-
|
|
3504
3873
|
// src/config/interpolate.ts
|
|
3505
|
-
var
|
|
3874
|
+
var import_node_crypto2 = __toESM(require("crypto"), 1);
|
|
3506
3875
|
var VAR_PATTERN = /\{\{([A-Z0-9_]+)\}\}/g;
|
|
3507
3876
|
function collectInterpolatedValues(input, vars, values) {
|
|
3508
3877
|
if (typeof input === "string") {
|
|
@@ -3541,8 +3910,8 @@ var RANDOM_FIRST_NAMES = ["Alex", "Jordan", "Morgan", "Taylor", "Casey", "Riley"
|
|
|
3541
3910
|
var RANDOM_LAST_NAMES = ["Smith", "Johnson", "Brown", "Davis", "Wilson", "Clark", "Hall", "Young"];
|
|
3542
3911
|
function generateRandomVars(randomSource) {
|
|
3543
3912
|
const random = randomSource?.random ?? Math.random;
|
|
3544
|
-
const randomBytes = randomSource?.randomBytes ??
|
|
3545
|
-
const randomUUID = randomSource?.randomUUID ??
|
|
3913
|
+
const randomBytes = randomSource?.randomBytes ?? import_node_crypto2.default.randomBytes;
|
|
3914
|
+
const randomUUID = randomSource?.randomUUID ?? import_node_crypto2.default.randomUUID;
|
|
3546
3915
|
const hex = randomBytes(4).toString("hex");
|
|
3547
3916
|
const firstIndex = Math.floor(random() * RANDOM_FIRST_NAMES.length);
|
|
3548
3917
|
const lastIndex = Math.floor(random() * RANDOM_LAST_NAMES.length);
|
|
@@ -3795,86 +4164,358 @@ function interpolateStep(step, vars, stepPath2, redacted) {
|
|
|
3795
4164
|
}
|
|
3796
4165
|
};
|
|
3797
4166
|
}
|
|
3798
|
-
if ("assertWithAI" in step) {
|
|
3799
|
-
return {
|
|
3800
|
-
assertWithAI: interpolateString(step.assertWithAI, vars).value
|
|
3801
|
-
};
|
|
4167
|
+
if ("assertWithAI" in step) {
|
|
4168
|
+
return {
|
|
4169
|
+
assertWithAI: interpolateString(step.assertWithAI, vars).value
|
|
4170
|
+
};
|
|
4171
|
+
}
|
|
4172
|
+
if ("copyText" in step) {
|
|
4173
|
+
return {
|
|
4174
|
+
copyText: {
|
|
4175
|
+
selector: interpolateString(step.copyText.selector, vars).value,
|
|
4176
|
+
as: step.copyText.as
|
|
4177
|
+
}
|
|
4178
|
+
};
|
|
4179
|
+
}
|
|
4180
|
+
if ("waitForDownload" in step) {
|
|
4181
|
+
if (step.waitForDownload === null) {
|
|
4182
|
+
return { waitForDownload: null };
|
|
4183
|
+
}
|
|
4184
|
+
return {
|
|
4185
|
+
waitForDownload: {
|
|
4186
|
+
...step.waitForDownload.filename !== void 0 ? { filename: interpolateString(step.waitForDownload.filename, vars).value } : {},
|
|
4187
|
+
...step.waitForDownload.timeout !== void 0 ? { timeout: step.waitForDownload.timeout } : {}
|
|
4188
|
+
}
|
|
4189
|
+
};
|
|
4190
|
+
}
|
|
4191
|
+
return step;
|
|
4192
|
+
}
|
|
4193
|
+
function interpolateAssertion(assertion, vars) {
|
|
4194
|
+
if ("selectorExists" in assertion) {
|
|
4195
|
+
return { selectorExists: interpolateString(assertion.selectorExists, vars).value };
|
|
4196
|
+
}
|
|
4197
|
+
if ("selectorNotExists" in assertion) {
|
|
4198
|
+
return { selectorNotExists: interpolateString(assertion.selectorNotExists, vars).value };
|
|
4199
|
+
}
|
|
4200
|
+
if ("urlIncludes" in assertion) {
|
|
4201
|
+
return { urlIncludes: interpolateString(assertion.urlIncludes, vars).value };
|
|
4202
|
+
}
|
|
4203
|
+
if ("urlEquals" in assertion) {
|
|
4204
|
+
return { urlEquals: interpolateString(assertion.urlEquals, vars).value };
|
|
4205
|
+
}
|
|
4206
|
+
if ("noConsoleErrors" in assertion) {
|
|
4207
|
+
return { noConsoleErrors: assertion.noConsoleErrors };
|
|
4208
|
+
}
|
|
4209
|
+
if ("noNetworkErrors" in assertion) {
|
|
4210
|
+
return { noNetworkErrors: assertion.noNetworkErrors };
|
|
4211
|
+
}
|
|
4212
|
+
return assertion;
|
|
4213
|
+
}
|
|
4214
|
+
function interpolateHunt(hunt, env, randomVars = generateRandomVars()) {
|
|
4215
|
+
const redactedFillSteps = /* @__PURE__ */ new Set();
|
|
4216
|
+
const envVars = Object.fromEntries(
|
|
4217
|
+
Object.entries(env).filter(([, value]) => value !== void 0)
|
|
4218
|
+
);
|
|
4219
|
+
const baseVars = { ...randomVars, ...envVars };
|
|
4220
|
+
const resolvedHuntVars = {};
|
|
4221
|
+
for (const [key, value] of Object.entries(hunt.vars ?? {})) {
|
|
4222
|
+
resolvedHuntVars[key] = interpolateString(value, baseVars).value;
|
|
4223
|
+
}
|
|
4224
|
+
const vars = { ...baseVars, ...resolvedHuntVars };
|
|
4225
|
+
const redactionValues = /* @__PURE__ */ new Set();
|
|
4226
|
+
collectInterpolatedValues(hunt.steps, vars, redactionValues);
|
|
4227
|
+
collectInterpolatedValues(hunt.assertions, vars, redactionValues);
|
|
4228
|
+
const steps = hunt.steps.map(
|
|
4229
|
+
(step, index) => interpolateStep(step, vars, `${index}`, redactedFillSteps)
|
|
4230
|
+
);
|
|
4231
|
+
const assertions = hunt.assertions?.map((assertion) => interpolateAssertion(assertion, vars));
|
|
4232
|
+
return {
|
|
4233
|
+
hunt: {
|
|
4234
|
+
...hunt,
|
|
4235
|
+
steps,
|
|
4236
|
+
assertions
|
|
4237
|
+
},
|
|
4238
|
+
redactedFillSteps,
|
|
4239
|
+
randomVars,
|
|
4240
|
+
redactionValues: [...redactionValues]
|
|
4241
|
+
};
|
|
4242
|
+
}
|
|
4243
|
+
|
|
4244
|
+
// src/config/loader.ts
|
|
4245
|
+
var DEFAULT_WEB_URL = "http://localhost:3000";
|
|
4246
|
+
var DEFAULT_CONFIG = {
|
|
4247
|
+
target: {
|
|
4248
|
+
type: "web",
|
|
4249
|
+
url: DEFAULT_WEB_URL
|
|
4250
|
+
},
|
|
4251
|
+
browser: {
|
|
4252
|
+
headless: true,
|
|
4253
|
+
slowMo: 0,
|
|
4254
|
+
timeout: 3e4,
|
|
4255
|
+
engine: "chromium",
|
|
4256
|
+
viewport: { width: 1280, height: 720 }
|
|
4257
|
+
},
|
|
4258
|
+
artifacts: {
|
|
4259
|
+
screenshots: "on-failure",
|
|
4260
|
+
networkHar: false,
|
|
4261
|
+
console: true,
|
|
4262
|
+
junit: false
|
|
4263
|
+
},
|
|
4264
|
+
assertions: {
|
|
4265
|
+
noConsoleErrors: true,
|
|
4266
|
+
noNetworkErrors: true,
|
|
4267
|
+
maxTotalTimeMs: 3e4,
|
|
4268
|
+
networkIgnorePatterns: []
|
|
4269
|
+
},
|
|
4270
|
+
guardrails: {
|
|
4271
|
+
maxSteps: 50,
|
|
4272
|
+
allowedDomains: ["localhost", "127.0.0.1", "0.0.0.0"],
|
|
4273
|
+
allowedApps: [],
|
|
4274
|
+
forbiddenSelectors: ["[data-danger]", ".delete-btn"],
|
|
4275
|
+
selfHealing: false
|
|
4276
|
+
},
|
|
4277
|
+
auth: {
|
|
4278
|
+
storageStatePath: ".prowl/auth-state.json"
|
|
4279
|
+
},
|
|
4280
|
+
history: {
|
|
4281
|
+
maxRuns: 100
|
|
4282
|
+
}
|
|
4283
|
+
};
|
|
4284
|
+
var CONFIG_DIR = ".prowl";
|
|
4285
|
+
var LEGACY_CONFIG_DIR = ".prowlqa";
|
|
4286
|
+
var legacyDirWarned = false;
|
|
4287
|
+
function warnLegacyConfigDir() {
|
|
4288
|
+
if (legacyDirWarned) {
|
|
4289
|
+
return;
|
|
4290
|
+
}
|
|
4291
|
+
legacyDirWarned = true;
|
|
4292
|
+
console.warn(
|
|
4293
|
+
'Warning: the ".prowlqa/" config directory is deprecated; rename it to ".prowl/". Support for ".prowlqa/" will be removed in a future release.'
|
|
4294
|
+
);
|
|
4295
|
+
}
|
|
4296
|
+
function findConfigPath(startDir) {
|
|
4297
|
+
let current = startDir;
|
|
4298
|
+
while (current) {
|
|
4299
|
+
for (const dir of [CONFIG_DIR, LEGACY_CONFIG_DIR]) {
|
|
4300
|
+
const candidate = import_node_path8.default.join(current, dir, "config.yml");
|
|
4301
|
+
if (import_node_fs7.default.existsSync(candidate)) {
|
|
4302
|
+
return candidate;
|
|
4303
|
+
}
|
|
4304
|
+
}
|
|
4305
|
+
const parent = import_node_path8.default.dirname(current);
|
|
4306
|
+
if (parent === current) {
|
|
4307
|
+
break;
|
|
4308
|
+
}
|
|
4309
|
+
current = parent;
|
|
4310
|
+
}
|
|
4311
|
+
return null;
|
|
4312
|
+
}
|
|
4313
|
+
var VIEWPORT_PRESETS = {
|
|
4314
|
+
mobile: { width: 375, height: 812 },
|
|
4315
|
+
tablet: { width: 768, height: 1024 },
|
|
4316
|
+
desktop: { width: 1280, height: 720 }
|
|
4317
|
+
};
|
|
4318
|
+
function resolveViewport(value) {
|
|
4319
|
+
if (value === void 0) {
|
|
4320
|
+
return DEFAULT_CONFIG.browser.viewport;
|
|
4321
|
+
}
|
|
4322
|
+
if (typeof value === "string") {
|
|
4323
|
+
const preset = VIEWPORT_PRESETS[value];
|
|
4324
|
+
if (!preset) {
|
|
4325
|
+
throw new Error(`Unknown viewport preset: "${value}". Use mobile, tablet, or desktop.`);
|
|
4326
|
+
}
|
|
4327
|
+
return preset;
|
|
4328
|
+
}
|
|
4329
|
+
return value;
|
|
4330
|
+
}
|
|
4331
|
+
function resolveTarget(target) {
|
|
4332
|
+
const type = target?.type;
|
|
4333
|
+
if (type === "macos") {
|
|
4334
|
+
return { type: "macos", app: target.app };
|
|
4335
|
+
}
|
|
4336
|
+
if (type === "android") {
|
|
4337
|
+
const androidTarget = target;
|
|
4338
|
+
return {
|
|
4339
|
+
type: "android",
|
|
4340
|
+
app: androidTarget.app,
|
|
4341
|
+
...androidTarget.deviceSerial !== void 0 ? { deviceSerial: androidTarget.deviceSerial } : {},
|
|
4342
|
+
...androidTarget.coldStart !== void 0 ? { coldStart: androidTarget.coldStart } : {}
|
|
4343
|
+
};
|
|
4344
|
+
}
|
|
4345
|
+
if (type === "ios") {
|
|
4346
|
+
const iosTarget = target;
|
|
4347
|
+
return {
|
|
4348
|
+
type: "ios",
|
|
4349
|
+
app: iosTarget.app,
|
|
4350
|
+
...iosTarget.udid !== void 0 ? { udid: iosTarget.udid } : {},
|
|
4351
|
+
...iosTarget.coldStart !== void 0 ? { coldStart: iosTarget.coldStart } : {}
|
|
4352
|
+
};
|
|
4353
|
+
}
|
|
4354
|
+
return {
|
|
4355
|
+
type: "web",
|
|
4356
|
+
url: target?.url ?? DEFAULT_WEB_URL
|
|
4357
|
+
};
|
|
4358
|
+
}
|
|
4359
|
+
function mergeConfig(partial) {
|
|
4360
|
+
return {
|
|
4361
|
+
target: resolveTarget(partial.target),
|
|
4362
|
+
browser: {
|
|
4363
|
+
headless: partial.browser?.headless ?? DEFAULT_CONFIG.browser.headless,
|
|
4364
|
+
slowMo: partial.browser?.slowMo ?? DEFAULT_CONFIG.browser.slowMo,
|
|
4365
|
+
timeout: partial.browser?.timeout ?? DEFAULT_CONFIG.browser.timeout,
|
|
4366
|
+
engine: partial.browser?.engine ?? DEFAULT_CONFIG.browser.engine,
|
|
4367
|
+
channel: partial.browser?.channel,
|
|
4368
|
+
viewport: resolveViewport(partial.browser?.viewport)
|
|
4369
|
+
},
|
|
4370
|
+
artifacts: {
|
|
4371
|
+
screenshots: partial.artifacts?.screenshots ?? DEFAULT_CONFIG.artifacts.screenshots,
|
|
4372
|
+
networkHar: partial.artifacts?.networkHar ?? DEFAULT_CONFIG.artifacts.networkHar,
|
|
4373
|
+
console: partial.artifacts?.console ?? DEFAULT_CONFIG.artifacts.console,
|
|
4374
|
+
junit: partial.artifacts?.junit ?? DEFAULT_CONFIG.artifacts.junit
|
|
4375
|
+
},
|
|
4376
|
+
assertions: {
|
|
4377
|
+
noConsoleErrors: partial.assertions?.noConsoleErrors ?? DEFAULT_CONFIG.assertions.noConsoleErrors,
|
|
4378
|
+
noNetworkErrors: partial.assertions?.noNetworkErrors ?? DEFAULT_CONFIG.assertions.noNetworkErrors,
|
|
4379
|
+
maxTotalTimeMs: partial.assertions?.maxTotalTimeMs ?? DEFAULT_CONFIG.assertions.maxTotalTimeMs,
|
|
4380
|
+
networkIgnorePatterns: partial.assertions?.networkIgnorePatterns ?? DEFAULT_CONFIG.assertions.networkIgnorePatterns
|
|
4381
|
+
},
|
|
4382
|
+
guardrails: {
|
|
4383
|
+
maxSteps: partial.guardrails?.maxSteps ?? DEFAULT_CONFIG.guardrails.maxSteps,
|
|
4384
|
+
allowedDomains: partial.guardrails?.allowedDomains ?? DEFAULT_CONFIG.guardrails.allowedDomains,
|
|
4385
|
+
allowedApps: partial.guardrails?.allowedApps ?? DEFAULT_CONFIG.guardrails.allowedApps,
|
|
4386
|
+
forbiddenSelectors: partial.guardrails?.forbiddenSelectors ?? DEFAULT_CONFIG.guardrails.forbiddenSelectors,
|
|
4387
|
+
selfHealing: partial.guardrails?.selfHealing ?? DEFAULT_CONFIG.guardrails.selfHealing
|
|
4388
|
+
},
|
|
4389
|
+
auth: {
|
|
4390
|
+
storageStatePath: partial.auth?.storageStatePath ?? (partial.auth !== void 0 ? DEFAULT_CONFIG.auth.storageStatePath : void 0)
|
|
4391
|
+
},
|
|
4392
|
+
history: {
|
|
4393
|
+
maxRuns: partial.history?.maxRuns ?? DEFAULT_CONFIG.history.maxRuns
|
|
4394
|
+
},
|
|
4395
|
+
bugLog: partial.bugLog,
|
|
4396
|
+
tracing: partial.tracing,
|
|
4397
|
+
reliability: partial.reliability
|
|
4398
|
+
};
|
|
4399
|
+
}
|
|
4400
|
+
function envStringVars(env) {
|
|
4401
|
+
return Object.fromEntries(
|
|
4402
|
+
Object.entries(env).filter(([, value]) => value !== void 0)
|
|
4403
|
+
);
|
|
4404
|
+
}
|
|
4405
|
+
function interpolateConfigStrings(value, vars) {
|
|
4406
|
+
if (typeof value === "string") {
|
|
4407
|
+
return interpolateString(value, vars).value;
|
|
4408
|
+
}
|
|
4409
|
+
if (Array.isArray(value)) {
|
|
4410
|
+
return value.map((item) => interpolateConfigStrings(item, vars));
|
|
3802
4411
|
}
|
|
3803
|
-
if ("
|
|
3804
|
-
return
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
as: step.copyText.as
|
|
3808
|
-
}
|
|
3809
|
-
};
|
|
4412
|
+
if (value && typeof value === "object") {
|
|
4413
|
+
return Object.fromEntries(
|
|
4414
|
+
Object.entries(value).map(([key, item]) => [key, interpolateConfigStrings(item, vars)])
|
|
4415
|
+
);
|
|
3810
4416
|
}
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
|
|
4417
|
+
return value;
|
|
4418
|
+
}
|
|
4419
|
+
function ensureAllowedDomain(allowed, urlValue) {
|
|
4420
|
+
try {
|
|
4421
|
+
const host = new URL(urlValue).hostname;
|
|
4422
|
+
if (!allowed.includes(host)) {
|
|
4423
|
+
return [...allowed, host];
|
|
3814
4424
|
}
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
...step.waitForDownload.filename !== void 0 ? { filename: interpolateString(step.waitForDownload.filename, vars).value } : {},
|
|
3818
|
-
...step.waitForDownload.timeout !== void 0 ? { timeout: step.waitForDownload.timeout } : {}
|
|
3819
|
-
}
|
|
3820
|
-
};
|
|
4425
|
+
} catch {
|
|
4426
|
+
return allowed;
|
|
3821
4427
|
}
|
|
3822
|
-
return
|
|
4428
|
+
return allowed;
|
|
3823
4429
|
}
|
|
3824
|
-
function
|
|
3825
|
-
|
|
3826
|
-
|
|
4430
|
+
function loadConfig(configPath) {
|
|
4431
|
+
const resolvedPath = configPath ? import_node_path8.default.resolve(configPath) : findConfigPath(process.cwd());
|
|
4432
|
+
if (!resolvedPath) {
|
|
4433
|
+
throw new Error("Could not find .prowl/config.yml. Run `prowl init` first.");
|
|
3827
4434
|
}
|
|
3828
|
-
if (
|
|
3829
|
-
|
|
4435
|
+
if (!import_node_fs7.default.existsSync(resolvedPath)) {
|
|
4436
|
+
throw new Error(`Config file not found at ${resolvedPath}`);
|
|
3830
4437
|
}
|
|
3831
|
-
|
|
3832
|
-
|
|
4438
|
+
const configDir = import_node_path8.default.dirname(resolvedPath);
|
|
4439
|
+
if (import_node_path8.default.basename(configDir) === LEGACY_CONFIG_DIR) {
|
|
4440
|
+
warnLegacyConfigDir();
|
|
3833
4441
|
}
|
|
3834
|
-
|
|
3835
|
-
|
|
4442
|
+
import_dotenv.default.config({ path: import_node_path8.default.join(configDir, ".env"), override: false });
|
|
4443
|
+
const raw = import_node_fs7.default.readFileSync(resolvedPath, "utf-8");
|
|
4444
|
+
const parsed = import_yaml.default.parse(raw) ?? {};
|
|
4445
|
+
const interpolated = interpolateConfigStrings(parsed, envStringVars(process.env));
|
|
4446
|
+
const validated = configSchema.parse(interpolated);
|
|
4447
|
+
const config = mergeConfig(validated);
|
|
4448
|
+
if (config.target.type === "web") {
|
|
4449
|
+
config.guardrails.allowedDomains = ensureAllowedDomain(
|
|
4450
|
+
config.guardrails.allowedDomains,
|
|
4451
|
+
config.target.url
|
|
4452
|
+
);
|
|
3836
4453
|
}
|
|
3837
|
-
|
|
3838
|
-
|
|
4454
|
+
return { config, configPath: resolvedPath, configDir };
|
|
4455
|
+
}
|
|
4456
|
+
function loadHunt(huntName, configDir) {
|
|
4457
|
+
assertValidHuntName(huntName);
|
|
4458
|
+
const huntPath = import_node_path8.default.join(configDir, "hunts", `${huntName}.yml`);
|
|
4459
|
+
if (!import_node_fs7.default.existsSync(huntPath)) {
|
|
4460
|
+
throw new Error(`Hunt file not found: ${huntPath}`);
|
|
3839
4461
|
}
|
|
3840
|
-
|
|
3841
|
-
|
|
4462
|
+
const raw = import_node_fs7.default.readFileSync(huntPath, "utf-8");
|
|
4463
|
+
const parsed = import_yaml.default.parse(raw) ?? {};
|
|
4464
|
+
const validated = huntSchema.parse(parsed);
|
|
4465
|
+
return validated;
|
|
4466
|
+
}
|
|
4467
|
+
function loadHuntTags(huntName, configDir) {
|
|
4468
|
+
assertValidHuntName(huntName);
|
|
4469
|
+
const huntPath = import_node_path8.default.join(configDir, "hunts", `${huntName}.yml`);
|
|
4470
|
+
if (!import_node_fs7.default.existsSync(huntPath)) {
|
|
4471
|
+
return [];
|
|
3842
4472
|
}
|
|
3843
|
-
|
|
4473
|
+
const raw = import_node_fs7.default.readFileSync(huntPath, "utf-8");
|
|
4474
|
+
const parsed = import_yaml.default.parse(raw) ?? {};
|
|
4475
|
+
return Array.isArray(parsed.tags) ? parsed.tags : [];
|
|
3844
4476
|
}
|
|
3845
|
-
function
|
|
3846
|
-
|
|
3847
|
-
const
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
const baseVars = { ...randomVars, ...envVars };
|
|
3851
|
-
const resolvedHuntVars = {};
|
|
3852
|
-
for (const [key, value] of Object.entries(hunt.vars ?? {})) {
|
|
3853
|
-
resolvedHuntVars[key] = interpolateString(value, baseVars).value;
|
|
4477
|
+
function loadHuntMeta(huntName, configDir) {
|
|
4478
|
+
assertValidHuntName(huntName);
|
|
4479
|
+
const huntPath = import_node_path8.default.join(configDir, "hunts", `${huntName}.yml`);
|
|
4480
|
+
if (!import_node_fs7.default.existsSync(huntPath)) {
|
|
4481
|
+
return { tags: [] };
|
|
3854
4482
|
}
|
|
3855
|
-
const
|
|
3856
|
-
const
|
|
3857
|
-
collectInterpolatedValues(hunt.steps, vars, redactionValues);
|
|
3858
|
-
collectInterpolatedValues(hunt.assertions, vars, redactionValues);
|
|
3859
|
-
const steps = hunt.steps.map(
|
|
3860
|
-
(step, index) => interpolateStep(step, vars, `${index}`, redactedFillSteps)
|
|
3861
|
-
);
|
|
3862
|
-
const assertions = hunt.assertions?.map((assertion) => interpolateAssertion(assertion, vars));
|
|
4483
|
+
const raw = import_node_fs7.default.readFileSync(huntPath, "utf-8");
|
|
4484
|
+
const parsed = import_yaml.default.parse(raw) ?? {};
|
|
3863
4485
|
return {
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
steps,
|
|
3867
|
-
assertions
|
|
3868
|
-
},
|
|
3869
|
-
redactedFillSteps,
|
|
3870
|
-
randomVars,
|
|
3871
|
-
redactionValues: [...redactionValues]
|
|
4486
|
+
description: typeof parsed.description === "string" ? parsed.description : void 0,
|
|
4487
|
+
tags: Array.isArray(parsed.tags) ? parsed.tags : []
|
|
3872
4488
|
};
|
|
3873
4489
|
}
|
|
4490
|
+
function listHunts(configDir) {
|
|
4491
|
+
const huntsDir = import_node_path8.default.join(configDir, "hunts");
|
|
4492
|
+
if (!import_node_fs7.default.existsSync(huntsDir)) {
|
|
4493
|
+
return [];
|
|
4494
|
+
}
|
|
4495
|
+
const stats = import_node_fs7.default.statSync(huntsDir);
|
|
4496
|
+
if (!stats.isDirectory()) {
|
|
4497
|
+
throw new Error(`Hunts path is not a directory: ${huntsDir}`);
|
|
4498
|
+
}
|
|
4499
|
+
const results = [];
|
|
4500
|
+
function scanDir(dir) {
|
|
4501
|
+
const entries = import_node_fs7.default.readdirSync(dir, { withFileTypes: true });
|
|
4502
|
+
for (const entry of entries) {
|
|
4503
|
+
if (entry.isFile() && entry.name.endsWith(".yml")) {
|
|
4504
|
+
const fullPath = import_node_path8.default.join(dir, entry.name);
|
|
4505
|
+
const relative = import_node_path8.default.relative(huntsDir, fullPath);
|
|
4506
|
+
results.push(relative.replace(/\.yml$/, ""));
|
|
4507
|
+
} else if (entry.isDirectory()) {
|
|
4508
|
+
scanDir(import_node_path8.default.join(dir, entry.name));
|
|
4509
|
+
}
|
|
4510
|
+
}
|
|
4511
|
+
}
|
|
4512
|
+
scanDir(huntsDir);
|
|
4513
|
+
return results.sort((a, b) => a.localeCompare(b));
|
|
4514
|
+
}
|
|
3874
4515
|
|
|
3875
4516
|
// src/browser/playwright-driver.ts
|
|
3876
|
-
var
|
|
3877
|
-
var
|
|
4517
|
+
var import_node_fs8 = __toESM(require("fs"), 1);
|
|
4518
|
+
var import_node_path9 = __toESM(require("path"), 1);
|
|
3878
4519
|
var import_playwright = require("playwright");
|
|
3879
4520
|
var ENGINES = { chromium: import_playwright.chromium, firefox: import_playwright.firefox, webkit: import_playwright.webkit };
|
|
3880
4521
|
async function launchBrowser(options) {
|
|
@@ -3896,14 +4537,14 @@ async function launchBrowser(options) {
|
|
|
3896
4537
|
contextOptions.viewport = options.viewport;
|
|
3897
4538
|
}
|
|
3898
4539
|
if (options.storageStatePath) {
|
|
3899
|
-
if (
|
|
4540
|
+
if (import_node_fs8.default.existsSync(options.storageStatePath)) {
|
|
3900
4541
|
contextOptions.storageState = options.storageStatePath;
|
|
3901
4542
|
} else {
|
|
3902
4543
|
console.warn(`Auth state file not found: ${options.storageStatePath}. Run "prowl login" to create it.`);
|
|
3903
4544
|
}
|
|
3904
4545
|
}
|
|
3905
4546
|
if (options.recordHar) {
|
|
3906
|
-
contextOptions.recordHar = { path:
|
|
4547
|
+
contextOptions.recordHar = { path: import_node_path9.default.join(options.runDir, "network.har") };
|
|
3907
4548
|
}
|
|
3908
4549
|
const context = await browser.newContext(contextOptions);
|
|
3909
4550
|
const page = await context.newPage();
|
|
@@ -3911,7 +4552,7 @@ async function launchBrowser(options) {
|
|
|
3911
4552
|
page.setDefaultNavigationTimeout(options.timeout);
|
|
3912
4553
|
let tracePath;
|
|
3913
4554
|
if (options.trace) {
|
|
3914
|
-
tracePath =
|
|
4555
|
+
tracePath = import_node_path9.default.join(options.runDir, "trace.zip");
|
|
3915
4556
|
await context.tracing.start({ screenshots: true, snapshots: true, sources: true });
|
|
3916
4557
|
}
|
|
3917
4558
|
return { browser, context, page, tracePath };
|
|
@@ -4006,6 +4647,16 @@ function createPlaywrightDriver(page) {
|
|
|
4006
4647
|
async hover(selector) {
|
|
4007
4648
|
await page.locator(selector).hover();
|
|
4008
4649
|
},
|
|
4650
|
+
async scroll(direction, amount = 500) {
|
|
4651
|
+
const deltas = {
|
|
4652
|
+
up: [0, -amount],
|
|
4653
|
+
down: [0, amount],
|
|
4654
|
+
left: [-amount, 0],
|
|
4655
|
+
right: [amount, 0]
|
|
4656
|
+
};
|
|
4657
|
+
const [x, y] = deltas[direction];
|
|
4658
|
+
await page.evaluate(([sx, sy]) => window.scrollBy(sx, sy), [x, y]);
|
|
4659
|
+
},
|
|
4009
4660
|
async scrollIntoView(selector) {
|
|
4010
4661
|
await page.locator(selector).scrollIntoViewIfNeeded();
|
|
4011
4662
|
},
|
|
@@ -4086,8 +4737,8 @@ function createPlaywrightDriver(page) {
|
|
|
4086
4737
|
}
|
|
4087
4738
|
|
|
4088
4739
|
// src/runner/steps.ts
|
|
4089
|
-
var
|
|
4090
|
-
var
|
|
4740
|
+
var import_node_fs10 = __toESM(require("fs"), 1);
|
|
4741
|
+
var import_node_path11 = __toESM(require("path"), 1);
|
|
4091
4742
|
|
|
4092
4743
|
// src/runner/healing.ts
|
|
4093
4744
|
var INTERACTIVE_TAGS = ["button", "a", "input", "select", "textarea"];
|
|
@@ -4785,11 +5436,14 @@ function toVisibilitySelector(value) {
|
|
|
4785
5436
|
if (looksLikeSelector(value)) return value;
|
|
4786
5437
|
return textContainsSelector(value);
|
|
4787
5438
|
}
|
|
5439
|
+
function countVisible(driver, selector) {
|
|
5440
|
+
return driver.visibleCount?.(selector) ?? driver.count(selector);
|
|
5441
|
+
}
|
|
4788
5442
|
async function runInlineAssert(driver, policy, assertion) {
|
|
4789
5443
|
if (assertion.visible !== void 0) {
|
|
4790
5444
|
const selector = toVisibilitySelector(assertion.visible);
|
|
4791
5445
|
policy.assertAllowedSelector(selector);
|
|
4792
|
-
const count = await driver
|
|
5446
|
+
const count = await countVisible(driver, selector);
|
|
4793
5447
|
if (count === 0) {
|
|
4794
5448
|
throw new Error(`Expected visible: ${assertion.visible}`);
|
|
4795
5449
|
}
|
|
@@ -4798,7 +5452,7 @@ async function runInlineAssert(driver, policy, assertion) {
|
|
|
4798
5452
|
if (assertion.notVisible !== void 0) {
|
|
4799
5453
|
const selector = toVisibilitySelector(assertion.notVisible);
|
|
4800
5454
|
policy.assertAllowedSelector(selector);
|
|
4801
|
-
const count = await driver
|
|
5455
|
+
const count = await countVisible(driver, selector);
|
|
4802
5456
|
if (count > 0) {
|
|
4803
5457
|
throw new Error(`Expected not visible: ${assertion.notVisible}`);
|
|
4804
5458
|
}
|
|
@@ -4821,7 +5475,7 @@ async function runInlineAssert(driver, policy, assertion) {
|
|
|
4821
5475
|
throw new Error("assert step is missing an assertion type");
|
|
4822
5476
|
}
|
|
4823
5477
|
function screenshotPath(screenshotsDir, fileName) {
|
|
4824
|
-
return
|
|
5478
|
+
return import_node_path11.default.join(screenshotsDir, fileName);
|
|
4825
5479
|
}
|
|
4826
5480
|
function stepPath(prefix, index) {
|
|
4827
5481
|
return prefix ? `${prefix}.${index}` : `${index}`;
|
|
@@ -4838,7 +5492,7 @@ function validateDownloadFilename(suggestedFilename) {
|
|
|
4838
5492
|
const safeFilename = suggestedFilename.trim();
|
|
4839
5493
|
const allowedFilenamePattern = /^[^<>:"/\\|?*]+$/;
|
|
4840
5494
|
const hasControlCharacter = Array.from(safeFilename).some((char) => char.charCodeAt(0) < 32);
|
|
4841
|
-
if (safeFilename.length === 0 || safeFilename !== suggestedFilename || safeFilename !==
|
|
5495
|
+
if (safeFilename.length === 0 || safeFilename !== suggestedFilename || safeFilename !== import_node_path11.default.basename(safeFilename) || safeFilename.includes("..") || /[/\\]/.test(safeFilename) || hasControlCharacter || !allowedFilenamePattern.test(safeFilename)) {
|
|
4842
5496
|
throw new Error(`Invalid download filename: "${suggestedFilename}"`);
|
|
4843
5497
|
}
|
|
4844
5498
|
return safeFilename;
|
|
@@ -5018,7 +5672,7 @@ var STEP_HANDLERS = {
|
|
|
5018
5672
|
if (!("setInputFiles" in h.step)) unknownStep();
|
|
5019
5673
|
const resolvedInput = await h.policy.resolveActionSelector(h.step.setInputFiles.selector);
|
|
5020
5674
|
const rawFiles = h.step.setInputFiles.files;
|
|
5021
|
-
const resolveFile = (f) =>
|
|
5675
|
+
const resolveFile = (f) => import_node_path11.default.isAbsolute(f) ? f : import_node_path11.default.join(h.context.configDir, f);
|
|
5022
5676
|
const resolvedFiles = Array.isArray(rawFiles) ? rawFiles.map(resolveFile) : resolveFile(rawFiles);
|
|
5023
5677
|
await h.driver.setInputFiles(resolvedInput.selector, resolvedFiles);
|
|
5024
5678
|
h.policy.ensureLocationAllowed(h.driver);
|
|
@@ -5188,25 +5842,21 @@ var STEP_HANDLERS = {
|
|
|
5188
5842
|
}
|
|
5189
5843
|
},
|
|
5190
5844
|
scroll: {
|
|
5191
|
-
|
|
5845
|
+
// Dispatched through the driver's `scroll` verb (interact), so native mobile
|
|
5846
|
+
// targets synthesize a touch swipe while web keeps its `window.scrollBy`
|
|
5847
|
+
// behavior. The per-target step gate rejects `scroll` on macOS before here.
|
|
5848
|
+
capabilities: ["interact"],
|
|
5192
5849
|
run: async (h) => {
|
|
5193
5850
|
if (!("scroll" in h.step)) unknownStep();
|
|
5194
|
-
const amount = h.step.scroll
|
|
5195
|
-
|
|
5196
|
-
up: [0, -amount],
|
|
5197
|
-
down: [0, amount],
|
|
5198
|
-
left: [-amount, 0],
|
|
5199
|
-
right: [amount, 0]
|
|
5200
|
-
};
|
|
5201
|
-
const [x, y] = scrollMap[h.step.scroll.direction];
|
|
5202
|
-
await h.driver.evaluate(([sx, sy]) => window.scrollBy(sx, sy), [x, y]);
|
|
5851
|
+
const { direction, amount } = h.step.scroll;
|
|
5852
|
+
await h.driver.scroll(direction, amount);
|
|
5203
5853
|
return {
|
|
5204
5854
|
kind: "result",
|
|
5205
5855
|
result: {
|
|
5206
5856
|
type: "scroll",
|
|
5207
5857
|
status: "pass",
|
|
5208
5858
|
durationMs: Date.now() - h.stepStart,
|
|
5209
|
-
value: `${
|
|
5859
|
+
value: amount === void 0 ? direction : `${direction} ${amount}px`
|
|
5210
5860
|
}
|
|
5211
5861
|
};
|
|
5212
5862
|
}
|
|
@@ -5252,7 +5902,7 @@ var STEP_HANDLERS = {
|
|
|
5252
5902
|
const condition = h.step.if;
|
|
5253
5903
|
const selector = condition.visible ?? condition.notVisible;
|
|
5254
5904
|
h.policy.assertAllowedSelector(selector);
|
|
5255
|
-
const count = await h.driver
|
|
5905
|
+
const count = await countVisible(h.driver, selector);
|
|
5256
5906
|
const conditionMet = condition.visible !== void 0 ? count > 0 : count === 0;
|
|
5257
5907
|
if (conditionMet) {
|
|
5258
5908
|
const subResult = await h.executeNested({
|
|
@@ -5339,7 +5989,7 @@ var STEP_HANDLERS = {
|
|
|
5339
5989
|
const whileSelector = repeat.while.visible ?? repeat.while.notVisible;
|
|
5340
5990
|
h.policy.assertAllowedSelector(whileSelector);
|
|
5341
5991
|
for (let i = 0; i < maxIter; i++) {
|
|
5342
|
-
const whileCount = await h.driver
|
|
5992
|
+
const whileCount = await countVisible(h.driver, whileSelector);
|
|
5343
5993
|
const shouldContinue = repeat.while.visible !== void 0 ? whileCount > 0 : whileCount === 0;
|
|
5344
5994
|
if (!shouldContinue) break;
|
|
5345
5995
|
totalSubSteps += repeat.steps.length;
|
|
@@ -5380,15 +6030,15 @@ var STEP_HANDLERS = {
|
|
|
5380
6030
|
if (!responseFile) {
|
|
5381
6031
|
throw new Error("mock.response must include either body or file");
|
|
5382
6032
|
}
|
|
5383
|
-
const candidateFilePath =
|
|
5384
|
-
const resolvedConfigDir =
|
|
5385
|
-
const resolvedFilePath =
|
|
5386
|
-
const relativePath =
|
|
5387
|
-
const isWithinConfigDir = relativePath === "" || relativePath !== ".." && !relativePath.startsWith(`..${
|
|
6033
|
+
const candidateFilePath = import_node_path11.default.isAbsolute(responseFile) ? responseFile : import_node_path11.default.join(h.context.configDir, responseFile);
|
|
6034
|
+
const resolvedConfigDir = import_node_path11.default.resolve(h.context.configDir);
|
|
6035
|
+
const resolvedFilePath = import_node_path11.default.resolve(candidateFilePath);
|
|
6036
|
+
const relativePath = import_node_path11.default.relative(resolvedConfigDir, resolvedFilePath);
|
|
6037
|
+
const isWithinConfigDir = relativePath === "" || relativePath !== ".." && !relativePath.startsWith(`..${import_node_path11.default.sep}`) && !import_node_path11.default.isAbsolute(relativePath);
|
|
5388
6038
|
if (!isWithinConfigDir) {
|
|
5389
6039
|
throw new Error("mock.response.file must resolve within config directory");
|
|
5390
6040
|
}
|
|
5391
|
-
responseBody = await
|
|
6041
|
+
responseBody = await import_node_fs10.default.promises.readFile(resolvedFilePath, "utf-8");
|
|
5392
6042
|
}
|
|
5393
6043
|
const contentType = mock.response.contentType ?? "application/json";
|
|
5394
6044
|
const status = mock.response.status;
|
|
@@ -5451,8 +6101,8 @@ var STEP_HANDLERS = {
|
|
|
5451
6101
|
capabilities: ["evaluate"],
|
|
5452
6102
|
run: async (h) => {
|
|
5453
6103
|
if (!("runScript" in h.step)) unknownStep();
|
|
5454
|
-
const filePath =
|
|
5455
|
-
const fileContents =
|
|
6104
|
+
const filePath = import_node_path11.default.isAbsolute(h.step.runScript.file) ? h.step.runScript.file : import_node_path11.default.join(h.context.configDir, h.step.runScript.file);
|
|
6105
|
+
const fileContents = import_node_fs10.default.readFileSync(filePath, "utf-8");
|
|
5456
6106
|
await h.driver.evaluate(fileContents);
|
|
5457
6107
|
return {
|
|
5458
6108
|
kind: "result",
|
|
@@ -5473,19 +6123,19 @@ var STEP_HANDLERS = {
|
|
|
5473
6123
|
const name = h.step.assertScreenshot.name;
|
|
5474
6124
|
const threshold = h.step.assertScreenshot.threshold ?? 0.1;
|
|
5475
6125
|
const baselineDir = ensureBaselineDir2(h.context.configDir);
|
|
5476
|
-
const baselinePath =
|
|
5477
|
-
const currentScreenshotPath =
|
|
5478
|
-
|
|
6126
|
+
const baselinePath = import_node_path11.default.join(baselineDir, `${name}.png`);
|
|
6127
|
+
const currentScreenshotPath = import_node_path11.default.join(h.context.runDir, "screenshots", `${name}-current.png`);
|
|
6128
|
+
import_node_fs10.default.mkdirSync(import_node_path11.default.dirname(currentScreenshotPath), { recursive: true });
|
|
5479
6129
|
await h.driver.screenshot({ path: currentScreenshotPath, fullPage: true });
|
|
5480
|
-
h.screenshots.push(
|
|
5481
|
-
if (!
|
|
5482
|
-
|
|
6130
|
+
h.screenshots.push(import_node_path11.default.join("screenshots", `${name}-current.png`));
|
|
6131
|
+
if (!import_node_fs10.default.existsSync(baselinePath)) {
|
|
6132
|
+
import_node_fs10.default.copyFileSync(currentScreenshotPath, baselinePath);
|
|
5483
6133
|
return {
|
|
5484
6134
|
kind: "result",
|
|
5485
6135
|
result: { type: "assertScreenshot", status: "pass", durationMs: Date.now() - h.stepStart, value: "baseline created" }
|
|
5486
6136
|
};
|
|
5487
6137
|
}
|
|
5488
|
-
const diffPath =
|
|
6138
|
+
const diffPath = import_node_path11.default.join(h.context.runDir, "screenshots", `${name}-diff.png`);
|
|
5489
6139
|
const comparison = await compareScreenshots2(baselinePath, currentScreenshotPath, diffPath, threshold);
|
|
5490
6140
|
if (comparison.match) {
|
|
5491
6141
|
return {
|
|
@@ -5498,7 +6148,7 @@ var STEP_HANDLERS = {
|
|
|
5498
6148
|
}
|
|
5499
6149
|
};
|
|
5500
6150
|
}
|
|
5501
|
-
h.screenshots.push(
|
|
6151
|
+
h.screenshots.push(import_node_path11.default.join("screenshots", `${name}-diff.png`));
|
|
5502
6152
|
throw new Error(
|
|
5503
6153
|
`Visual regression: ${(comparison.diffPercentage * 100).toFixed(2)}% diff exceeds threshold ${(threshold * 100).toFixed(0)}%`
|
|
5504
6154
|
);
|
|
@@ -5524,8 +6174,8 @@ var STEP_HANDLERS = {
|
|
|
5524
6174
|
}
|
|
5525
6175
|
const fileName = `assertWithAI_step_${h.index + 1}.png`;
|
|
5526
6176
|
const relative = await h.addScreenshot(fileName);
|
|
5527
|
-
const screenshotFullPath =
|
|
5528
|
-
const imageBase64 =
|
|
6177
|
+
const screenshotFullPath = import_node_path11.default.join(h.context.runDir, relative);
|
|
6178
|
+
const imageBase64 = import_node_fs10.default.readFileSync(screenshotFullPath).toString("base64");
|
|
5529
6179
|
const assertVision = h.context.assertVision ?? assertWithAiVision;
|
|
5530
6180
|
const verdict = await assertVision(
|
|
5531
6181
|
{ imageBase64, mediaType: "image/png", assertion },
|
|
@@ -5582,7 +6232,7 @@ var STEP_HANDLERS = {
|
|
|
5582
6232
|
`Download filename mismatch: expected "${opts.filename}", got "${suggestedFilename}"`
|
|
5583
6233
|
);
|
|
5584
6234
|
}
|
|
5585
|
-
const savePath =
|
|
6235
|
+
const savePath = import_node_path11.default.join(h.context.runDir, suggestedFilename);
|
|
5586
6236
|
await download.saveAs(savePath);
|
|
5587
6237
|
return {
|
|
5588
6238
|
kind: "result",
|
|
@@ -5611,8 +6261,8 @@ async function executeSteps(context) {
|
|
|
5611
6261
|
maxSteps: context.maxSteps,
|
|
5612
6262
|
selfHealing: context.selfHealing
|
|
5613
6263
|
});
|
|
5614
|
-
const screenshotsDir =
|
|
5615
|
-
|
|
6264
|
+
const screenshotsDir = import_node_path11.default.join(context.runDir, "screenshots");
|
|
6265
|
+
import_node_fs10.default.mkdirSync(screenshotsDir, { recursive: true });
|
|
5616
6266
|
const currentHuntName = context.huntStack?.[context.huntStack.length - 1];
|
|
5617
6267
|
policy.assertWithinMaxSteps(context.steps.length, currentHuntName);
|
|
5618
6268
|
const results = [];
|
|
@@ -5622,7 +6272,7 @@ async function executeSteps(context) {
|
|
|
5622
6272
|
const addScreenshot = async (fileName) => {
|
|
5623
6273
|
const fullPath = screenshotPath(screenshotsDir, fileName);
|
|
5624
6274
|
await captureScreenshot2(driver, fullPath);
|
|
5625
|
-
const relative =
|
|
6275
|
+
const relative = import_node_path11.default.join("screenshots", fileName);
|
|
5626
6276
|
screenshots.push(relative);
|
|
5627
6277
|
return relative;
|
|
5628
6278
|
};
|
|
@@ -5710,12 +6360,12 @@ async function executeSteps(context) {
|
|
|
5710
6360
|
return { results, screenshots, failed: false };
|
|
5711
6361
|
}
|
|
5712
6362
|
async function captureFinalScreenshot(page, runDir) {
|
|
5713
|
-
const screenshotsDir =
|
|
5714
|
-
|
|
6363
|
+
const screenshotsDir = import_node_path11.default.join(runDir, "screenshots");
|
|
6364
|
+
import_node_fs10.default.mkdirSync(screenshotsDir, { recursive: true });
|
|
5715
6365
|
const fileName = "final.png";
|
|
5716
6366
|
const filePath = screenshotPath(screenshotsDir, fileName);
|
|
5717
6367
|
await captureScreenshot2(page, filePath);
|
|
5718
|
-
return
|
|
6368
|
+
return import_node_path11.default.join("screenshots", fileName);
|
|
5719
6369
|
}
|
|
5720
6370
|
|
|
5721
6371
|
// src/runner/assertions.ts
|
|
@@ -5837,6 +6487,59 @@ async function evaluateAssertions(options) {
|
|
|
5837
6487
|
}
|
|
5838
6488
|
return results;
|
|
5839
6489
|
}
|
|
6490
|
+
async function evaluateNativeAssertions(options) {
|
|
6491
|
+
const assertions = mergeAssertions(options.config, options.huntAssertions);
|
|
6492
|
+
const authoredTypes = new Set(
|
|
6493
|
+
(options.huntAssertions ?? []).map((assertion) => Object.keys(assertion)[0])
|
|
6494
|
+
);
|
|
6495
|
+
const results = [];
|
|
6496
|
+
const warnings = [];
|
|
6497
|
+
for (const assertion of assertions) {
|
|
6498
|
+
const type = Object.keys(assertion)[0] ?? "assertion";
|
|
6499
|
+
try {
|
|
6500
|
+
if ("selectorExists" in assertion) {
|
|
6501
|
+
options.assertAllowedSelector?.(assertion.selectorExists);
|
|
6502
|
+
const count = await options.driver.count(assertion.selectorExists);
|
|
6503
|
+
results.push({
|
|
6504
|
+
type: "selectorExists",
|
|
6505
|
+
value: assertion.selectorExists,
|
|
6506
|
+
status: count > 0 ? "pass" : "fail",
|
|
6507
|
+
error: count > 0 ? void 0 : "Selector not found"
|
|
6508
|
+
});
|
|
6509
|
+
continue;
|
|
6510
|
+
}
|
|
6511
|
+
if ("selectorNotExists" in assertion) {
|
|
6512
|
+
options.assertAllowedSelector?.(assertion.selectorNotExists);
|
|
6513
|
+
const count = await options.driver.count(assertion.selectorNotExists);
|
|
6514
|
+
results.push({
|
|
6515
|
+
type: "selectorNotExists",
|
|
6516
|
+
value: assertion.selectorNotExists,
|
|
6517
|
+
status: count === 0 ? "pass" : "fail",
|
|
6518
|
+
error: count === 0 ? void 0 : "Selector exists"
|
|
6519
|
+
});
|
|
6520
|
+
continue;
|
|
6521
|
+
}
|
|
6522
|
+
const rawValue = assertion[type];
|
|
6523
|
+
results.push({
|
|
6524
|
+
type,
|
|
6525
|
+
value: rawValue,
|
|
6526
|
+
status: "skipped",
|
|
6527
|
+
error: `skipped (web-only): not supported on the ${options.targetLabel} target`
|
|
6528
|
+
});
|
|
6529
|
+
if (authoredTypes.has(type)) {
|
|
6530
|
+
warnings.push(`${type} is web-only; skipped on ${options.targetLabel} target`);
|
|
6531
|
+
}
|
|
6532
|
+
} catch (error) {
|
|
6533
|
+
const detail = error instanceof Error ? error.message : "no error details";
|
|
6534
|
+
results.push({
|
|
6535
|
+
type,
|
|
6536
|
+
status: "fail",
|
|
6537
|
+
error: `Native assertion "${type}" failed: ${detail}`
|
|
6538
|
+
});
|
|
6539
|
+
}
|
|
6540
|
+
}
|
|
6541
|
+
return { results, warnings };
|
|
6542
|
+
}
|
|
5840
6543
|
|
|
5841
6544
|
// src/runner/tracing.ts
|
|
5842
6545
|
var DEFAULT_TRACE_HEADER = "traceparent";
|
|
@@ -5875,18 +6578,18 @@ function captureTraceCorrelation(response, headerName, sink, redactionValues = [
|
|
|
5875
6578
|
}
|
|
5876
6579
|
|
|
5877
6580
|
// src/reporter/result.ts
|
|
5878
|
-
var
|
|
5879
|
-
var
|
|
6581
|
+
var import_node_fs11 = __toESM(require("fs"), 1);
|
|
6582
|
+
var import_node_path12 = __toESM(require("path"), 1);
|
|
5880
6583
|
function writeResult(runDir, result) {
|
|
5881
6584
|
const fileName = "result.json";
|
|
5882
|
-
const fullPath =
|
|
5883
|
-
|
|
6585
|
+
const fullPath = import_node_path12.default.join(runDir, fileName);
|
|
6586
|
+
import_node_fs11.default.writeFileSync(fullPath, JSON.stringify(result, null, 2));
|
|
5884
6587
|
return fileName;
|
|
5885
6588
|
}
|
|
5886
6589
|
|
|
5887
6590
|
// src/reporter/summary.ts
|
|
5888
|
-
var
|
|
5889
|
-
var
|
|
6591
|
+
var import_node_fs12 = __toESM(require("fs"), 1);
|
|
6592
|
+
var import_node_path13 = __toESM(require("path"), 1);
|
|
5890
6593
|
function escapeMd(text) {
|
|
5891
6594
|
return text.replace(/([|`*_{}[\]()#+\-!\\])/g, "\\$1");
|
|
5892
6595
|
}
|
|
@@ -5964,28 +6667,29 @@ function writeSummary(runDir, result) {
|
|
|
5964
6667
|
}
|
|
5965
6668
|
}
|
|
5966
6669
|
const fileName = "summary.md";
|
|
5967
|
-
const fullPath =
|
|
5968
|
-
|
|
6670
|
+
const fullPath = import_node_path13.default.join(runDir, fileName);
|
|
6671
|
+
import_node_fs12.default.writeFileSync(fullPath, `${lines.join("\n")}
|
|
5969
6672
|
`);
|
|
5970
6673
|
return fileName;
|
|
5971
6674
|
}
|
|
5972
6675
|
|
|
5973
6676
|
// src/reporter/junit.ts
|
|
5974
|
-
var
|
|
5975
|
-
var
|
|
6677
|
+
var import_node_fs13 = __toESM(require("fs"), 1);
|
|
6678
|
+
var import_node_path14 = __toESM(require("path"), 1);
|
|
5976
6679
|
function escapeXml(text) {
|
|
5977
6680
|
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
5978
6681
|
}
|
|
5979
6682
|
function writeJunit(runDir, result) {
|
|
5980
6683
|
const totalTests = result.steps.length + result.assertions.length;
|
|
5981
6684
|
const failures = result.steps.filter((s) => s.status === "fail").length + result.assertions.filter((a) => a.status === "fail").length;
|
|
6685
|
+
const skipped = result.assertions.filter((a) => a.status === "skipped").length;
|
|
5982
6686
|
const timeSeconds = (result.durationMs / 1e3).toFixed(3);
|
|
5983
6687
|
const huntName = escapeXml(result.hunt);
|
|
5984
6688
|
const lines = [];
|
|
5985
6689
|
lines.push('<?xml version="1.0" encoding="UTF-8"?>');
|
|
5986
6690
|
lines.push("<testsuites>");
|
|
5987
6691
|
lines.push(
|
|
5988
|
-
` <testsuite name="${huntName}" tests="${totalTests}" failures="${failures}" errors="0" time="${timeSeconds}" timestamp="${escapeXml(result.startedAt)}">`
|
|
6692
|
+
` <testsuite name="${huntName}" tests="${totalTests}" failures="${failures}" errors="0" skipped="${skipped}" time="${timeSeconds}" timestamp="${escapeXml(result.startedAt)}">`
|
|
5989
6693
|
);
|
|
5990
6694
|
for (let i = 0; i < result.steps.length; i++) {
|
|
5991
6695
|
const step = result.steps[i];
|
|
@@ -6009,6 +6713,11 @@ function writeJunit(runDir, result) {
|
|
|
6009
6713
|
lines.push(` <testcase name="${caseName}" classname="${huntName}" time="0">`);
|
|
6010
6714
|
lines.push(` <failure message="${escapedFailureText}" type="assertion">${escapedFailureText}</failure>`);
|
|
6011
6715
|
lines.push(" </testcase>");
|
|
6716
|
+
} else if (assertion.status === "skipped") {
|
|
6717
|
+
const skipText = escapeXml(assertion.error ?? "skipped");
|
|
6718
|
+
lines.push(` <testcase name="${caseName}" classname="${huntName}" time="0">`);
|
|
6719
|
+
lines.push(` <skipped message="${skipText}"/>`);
|
|
6720
|
+
lines.push(" </testcase>");
|
|
6012
6721
|
} else {
|
|
6013
6722
|
lines.push(` <testcase name="${caseName}" classname="${huntName}" time="0"/>`);
|
|
6014
6723
|
}
|
|
@@ -6016,8 +6725,8 @@ function writeJunit(runDir, result) {
|
|
|
6016
6725
|
lines.push(" </testsuite>");
|
|
6017
6726
|
lines.push("</testsuites>");
|
|
6018
6727
|
const fileName = "junit.xml";
|
|
6019
|
-
const fullPath =
|
|
6020
|
-
|
|
6728
|
+
const fullPath = import_node_path14.default.join(runDir, fileName);
|
|
6729
|
+
import_node_fs13.default.writeFileSync(fullPath, `${lines.join("\n")}
|
|
6021
6730
|
`);
|
|
6022
6731
|
return fileName;
|
|
6023
6732
|
}
|
|
@@ -6051,15 +6760,15 @@ function timestamp(prefix) {
|
|
|
6051
6760
|
}
|
|
6052
6761
|
|
|
6053
6762
|
// src/runner/history.ts
|
|
6054
|
-
var
|
|
6055
|
-
var
|
|
6763
|
+
var import_node_fs14 = __toESM(require("fs"), 1);
|
|
6764
|
+
var import_node_path15 = __toESM(require("path"), 1);
|
|
6056
6765
|
var HISTORY_FILE = "history.json";
|
|
6057
6766
|
var LOCK_FILE_SUFFIX = ".lock";
|
|
6058
6767
|
var LOCK_RETRY_MS = 10;
|
|
6059
6768
|
var LOCK_TIMEOUT_MS = 5e3;
|
|
6060
6769
|
var SLEEP_BUFFER = new Int32Array(new SharedArrayBuffer(4));
|
|
6061
6770
|
function historyPath(configDir) {
|
|
6062
|
-
return
|
|
6771
|
+
return import_node_path15.default.join(configDir, HISTORY_FILE);
|
|
6063
6772
|
}
|
|
6064
6773
|
function isHistoryEntry(value) {
|
|
6065
6774
|
if (!value || typeof value !== "object") {
|
|
@@ -6070,11 +6779,11 @@ function isHistoryEntry(value) {
|
|
|
6070
6779
|
}
|
|
6071
6780
|
function readHistory(configDir) {
|
|
6072
6781
|
const filePath = historyPath(configDir);
|
|
6073
|
-
if (!
|
|
6782
|
+
if (!import_node_fs14.default.existsSync(filePath)) {
|
|
6074
6783
|
return { entries: [] };
|
|
6075
6784
|
}
|
|
6076
6785
|
try {
|
|
6077
|
-
const raw =
|
|
6786
|
+
const raw = import_node_fs14.default.readFileSync(filePath, "utf-8");
|
|
6078
6787
|
const parsed = JSON.parse(raw);
|
|
6079
6788
|
if (parsed && typeof parsed === "object" && "entries" in parsed && Array.isArray(parsed.entries)) {
|
|
6080
6789
|
const validatedEntries = parsed.entries.filter(isHistoryEntry);
|
|
@@ -6113,12 +6822,12 @@ function sleepSync(ms) {
|
|
|
6113
6822
|
function withHistoryLock(configDir, fn) {
|
|
6114
6823
|
const filePath = historyPath(configDir);
|
|
6115
6824
|
const lockPath = `${filePath}${LOCK_FILE_SUFFIX}`;
|
|
6116
|
-
|
|
6825
|
+
import_node_fs14.default.mkdirSync(import_node_path15.default.dirname(filePath), { recursive: true });
|
|
6117
6826
|
const startedAt = Date.now();
|
|
6118
6827
|
while (Date.now() - startedAt < LOCK_TIMEOUT_MS) {
|
|
6119
6828
|
let fd;
|
|
6120
6829
|
try {
|
|
6121
|
-
fd =
|
|
6830
|
+
fd = import_node_fs14.default.openSync(lockPath, "wx");
|
|
6122
6831
|
} catch (error) {
|
|
6123
6832
|
if (error.code === "EEXIST") {
|
|
6124
6833
|
sleepSync(LOCK_RETRY_MS);
|
|
@@ -6130,10 +6839,10 @@ function withHistoryLock(configDir, fn) {
|
|
|
6130
6839
|
return fn();
|
|
6131
6840
|
} finally {
|
|
6132
6841
|
try {
|
|
6133
|
-
|
|
6842
|
+
import_node_fs14.default.closeSync(fd);
|
|
6134
6843
|
} catch {
|
|
6135
6844
|
}
|
|
6136
|
-
|
|
6845
|
+
import_node_fs14.default.rmSync(lockPath, { force: true });
|
|
6137
6846
|
}
|
|
6138
6847
|
}
|
|
6139
6848
|
throw new Error(
|
|
@@ -6146,9 +6855,9 @@ function appendEntry(configDir, entry, maxRuns) {
|
|
|
6146
6855
|
const current = readHistory(configDir);
|
|
6147
6856
|
const next = pruneEntries([...current.entries, entry], maxRuns);
|
|
6148
6857
|
const tempPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
6149
|
-
|
|
6858
|
+
import_node_fs14.default.writeFileSync(tempPath, `${JSON.stringify({ entries: next }, null, 2)}
|
|
6150
6859
|
`);
|
|
6151
|
-
|
|
6860
|
+
import_node_fs14.default.renameSync(tempPath, filePath);
|
|
6152
6861
|
});
|
|
6153
6862
|
}
|
|
6154
6863
|
|
|
@@ -6161,11 +6870,11 @@ function parseViewportFlag(value) {
|
|
|
6161
6870
|
return value;
|
|
6162
6871
|
}
|
|
6163
6872
|
function resolvePath(configDir, inputPath) {
|
|
6164
|
-
if (
|
|
6873
|
+
if (import_node_path16.default.isAbsolute(inputPath)) {
|
|
6165
6874
|
return inputPath;
|
|
6166
6875
|
}
|
|
6167
|
-
const projectRoot =
|
|
6168
|
-
return
|
|
6876
|
+
const projectRoot = import_node_path16.default.dirname(configDir);
|
|
6877
|
+
return import_node_path16.default.join(projectRoot, inputPath);
|
|
6169
6878
|
}
|
|
6170
6879
|
function buildRunResult(options) {
|
|
6171
6880
|
return {
|
|
@@ -6184,12 +6893,12 @@ function buildRunResult(options) {
|
|
|
6184
6893
|
}
|
|
6185
6894
|
function writeConsoleLog(runDir, entries) {
|
|
6186
6895
|
const fileName = "console.log";
|
|
6187
|
-
const filePath =
|
|
6896
|
+
const filePath = import_node_path16.default.join(runDir, fileName);
|
|
6188
6897
|
const lines = entries.map((entry) => {
|
|
6189
6898
|
const location = entry.location ? ` (${entry.location})` : "";
|
|
6190
6899
|
return `[${entry.type}] ${entry.text}${location}`;
|
|
6191
6900
|
});
|
|
6192
|
-
|
|
6901
|
+
import_node_fs15.default.writeFileSync(filePath, `${lines.join("\n")}
|
|
6193
6902
|
`);
|
|
6194
6903
|
return fileName;
|
|
6195
6904
|
}
|
|
@@ -6197,8 +6906,8 @@ async function executeHuntAttempt(options, config, configDir, interpolatedHunt,
|
|
|
6197
6906
|
const headless = options.headed ? false : config.browser.headless;
|
|
6198
6907
|
const slowMo = options.slowMo ?? config.browser.slowMo;
|
|
6199
6908
|
const maxSteps = config.guardrails.maxSteps;
|
|
6200
|
-
const runDir =
|
|
6201
|
-
|
|
6909
|
+
const runDir = import_node_path16.default.join(configDir, "runs", timestamp());
|
|
6910
|
+
import_node_fs15.default.mkdirSync(runDir, { recursive: true });
|
|
6202
6911
|
const storageStatePath = config.auth.storageStatePath ? resolvePath(configDir, config.auth.storageStatePath) : void 0;
|
|
6203
6912
|
const engine = options.browser ?? config.browser.engine;
|
|
6204
6913
|
const channel = options.channel ?? config.browser.channel;
|
|
@@ -6381,8 +7090,8 @@ async function runHunt(options) {
|
|
|
6381
7090
|
}
|
|
6382
7091
|
async function executeNativeHuntAttempt(options, config, configDir, interpolatedHunt, redactedFillSteps, randomVars, allowedApps, native) {
|
|
6383
7092
|
const maxSteps = config.guardrails.maxSteps;
|
|
6384
|
-
const runDir =
|
|
6385
|
-
|
|
7093
|
+
const runDir = import_node_path16.default.join(configDir, "runs", timestamp());
|
|
7094
|
+
import_node_fs15.default.mkdirSync(runDir, { recursive: true });
|
|
6386
7095
|
const session = await native.launchSession();
|
|
6387
7096
|
let result;
|
|
6388
7097
|
try {
|
|
@@ -6390,6 +7099,13 @@ async function executeNativeHuntAttempt(options, config, configDir, interpolated
|
|
|
6390
7099
|
const appIdentity = native.sessionAppIdentity(session);
|
|
6391
7100
|
const targetLabel = `${native.targetType}:${appIdentity}`;
|
|
6392
7101
|
const effectiveAllowedApps = [.../* @__PURE__ */ new Set([...allowedApps, native.targetApp, appIdentity])];
|
|
7102
|
+
const assertionPolicy = createRunPolicy(driver, {
|
|
7103
|
+
forbiddenSelectors: config.guardrails.forbiddenSelectors,
|
|
7104
|
+
allowedDomains: [],
|
|
7105
|
+
allowedApps: effectiveAllowedApps,
|
|
7106
|
+
maxSteps,
|
|
7107
|
+
selfHealing: config.guardrails.selfHealing
|
|
7108
|
+
});
|
|
6393
7109
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
6394
7110
|
const startTime = Date.now();
|
|
6395
7111
|
let stepResults = [];
|
|
@@ -6429,8 +7145,19 @@ async function executeNativeHuntAttempt(options, config, configDir, interpolated
|
|
|
6429
7145
|
} catch {
|
|
6430
7146
|
finalScreenshot = void 0;
|
|
6431
7147
|
}
|
|
7148
|
+
const { results: assertionResults, warnings: assertionWarnings } = await evaluateNativeAssertions({
|
|
7149
|
+
driver,
|
|
7150
|
+
config,
|
|
7151
|
+
huntAssertions: interpolatedHunt.assertions,
|
|
7152
|
+
assertAllowedSelector: assertionPolicy.assertAllowedSelector,
|
|
7153
|
+
targetLabel: nativeTargetLabel(native.targetType)
|
|
7154
|
+
});
|
|
7155
|
+
for (const warning of assertionWarnings) {
|
|
7156
|
+
console.warn(warning);
|
|
7157
|
+
}
|
|
6432
7158
|
const durationMs = Date.now() - startTime;
|
|
6433
|
-
const
|
|
7159
|
+
const assertionsFailed = assertionResults.some((assertion) => assertion.status === "fail");
|
|
7160
|
+
const status = stepFailed || assertionsFailed ? "fail" : "pass";
|
|
6434
7161
|
const artifacts = {
|
|
6435
7162
|
screenshots: finalScreenshot ? [...stepScreenshots, finalScreenshot] : stepScreenshots
|
|
6436
7163
|
};
|
|
@@ -6441,7 +7168,7 @@ async function executeNativeHuntAttempt(options, config, configDir, interpolated
|
|
|
6441
7168
|
hunt: options.huntName,
|
|
6442
7169
|
targetUrl: targetLabel,
|
|
6443
7170
|
steps: stepResults,
|
|
6444
|
-
assertions:
|
|
7171
|
+
assertions: assertionResults,
|
|
6445
7172
|
artifacts
|
|
6446
7173
|
});
|
|
6447
7174
|
result = writeReports(runDir, runResult, { junit: options.junit ?? config.artifacts.junit });
|
|
@@ -6554,7 +7281,6 @@ async function runNativeHunt(options, config, configDir, target, native) {
|
|
|
6554
7281
|
const hunt = loadHunt(options.huntName, configDir);
|
|
6555
7282
|
const { hunt: interpolatedHunt, redactedFillSteps, randomVars } = interpolateHunt(hunt, process.env);
|
|
6556
7283
|
assertStepsSupportedByTarget(interpolatedHunt.steps, native.targetType);
|
|
6557
|
-
assertHuntAssertionsSupportedByTarget(interpolatedHunt.assertions, native.targetType);
|
|
6558
7284
|
native.assertAppAllowed(config.guardrails.allowedApps, target);
|
|
6559
7285
|
const maxSteps = config.guardrails.maxSteps;
|
|
6560
7286
|
if (interpolatedHunt.steps.length > maxSteps) {
|
|
@@ -6595,7 +7321,7 @@ async function runNativeHunt(options, config, configDir, target, native) {
|
|
|
6595
7321
|
}
|
|
6596
7322
|
function recordHistory(configDir, outcome, maxRuns) {
|
|
6597
7323
|
try {
|
|
6598
|
-
const relativeRunDir =
|
|
7324
|
+
const relativeRunDir = import_node_path16.default.relative(configDir, outcome.runDir);
|
|
6599
7325
|
appendEntry(
|
|
6600
7326
|
configDir,
|
|
6601
7327
|
{
|
|
@@ -6612,11 +7338,11 @@ function recordHistory(configDir, outcome, maxRuns) {
|
|
|
6612
7338
|
}
|
|
6613
7339
|
|
|
6614
7340
|
// src/runner/suite.ts
|
|
6615
|
-
var
|
|
7341
|
+
var import_node_path19 = __toESM(require("path"), 1);
|
|
6616
7342
|
|
|
6617
7343
|
// src/reporter/ci-summary.ts
|
|
6618
|
-
var
|
|
6619
|
-
var
|
|
7344
|
+
var import_node_fs16 = __toESM(require("fs"), 1);
|
|
7345
|
+
var import_node_path17 = __toESM(require("path"), 1);
|
|
6620
7346
|
var import_chalk = __toESM(require("chalk"), 1);
|
|
6621
7347
|
function countCiResults(results) {
|
|
6622
7348
|
return {
|
|
@@ -6646,9 +7372,9 @@ function writeCiResult(ciRunDir, results, startedAt, totalDurationMs, flaky = []
|
|
|
6646
7372
|
...flaky.length > 0 ? { flaky } : {},
|
|
6647
7373
|
...clusters.length > 0 ? { clusters } : {}
|
|
6648
7374
|
};
|
|
6649
|
-
|
|
6650
|
-
const filePath =
|
|
6651
|
-
|
|
7375
|
+
import_node_fs16.default.mkdirSync(ciRunDir, { recursive: true });
|
|
7376
|
+
const filePath = import_node_path17.default.join(ciRunDir, "ci-result.json");
|
|
7377
|
+
import_node_fs16.default.writeFileSync(filePath, JSON.stringify(ciResult, null, 2) + "\n");
|
|
6652
7378
|
return filePath;
|
|
6653
7379
|
}
|
|
6654
7380
|
|
|
@@ -6715,7 +7441,7 @@ function rankFlaky(configDir, options = {}) {
|
|
|
6715
7441
|
}
|
|
6716
7442
|
|
|
6717
7443
|
// src/backlog/fingerprint.ts
|
|
6718
|
-
var
|
|
7444
|
+
var import_node_crypto3 = require("crypto");
|
|
6719
7445
|
function normalizeError(error) {
|
|
6720
7446
|
return error.toLowerCase().replace(/0x[0-9a-f]+/g, "").replace(/\b\d+(?:\.\d+)?\s*(?:ms|s|px)\b/g, "").replace(/\s+/g, " ").trim();
|
|
6721
7447
|
}
|
|
@@ -6738,7 +7464,7 @@ function computeFingerprint(failure) {
|
|
|
6738
7464
|
failure.selector ?? "",
|
|
6739
7465
|
normalizeError(failure.error)
|
|
6740
7466
|
];
|
|
6741
|
-
return (0,
|
|
7467
|
+
return (0, import_node_crypto3.createHash)("sha1").update(parts.join("|")).digest("hex").slice(0, 8);
|
|
6742
7468
|
}
|
|
6743
7469
|
function buildMarker(failure, hash) {
|
|
6744
7470
|
const hunt = sanitizeMarkerValue(failure.hunt);
|
|
@@ -6782,8 +7508,8 @@ function clusterFailures(failures) {
|
|
|
6782
7508
|
}
|
|
6783
7509
|
|
|
6784
7510
|
// src/backlog/index.ts
|
|
6785
|
-
var
|
|
6786
|
-
var
|
|
7511
|
+
var import_node_fs17 = __toESM(require("fs"), 1);
|
|
7512
|
+
var import_node_path18 = __toESM(require("path"), 1);
|
|
6787
7513
|
|
|
6788
7514
|
// src/backlog/parse.ts
|
|
6789
7515
|
var MARKER_FP = /<!--\s*prowl:fp=([0-9a-f]+)/;
|
|
@@ -6873,7 +7599,7 @@ ${after}`;
|
|
|
6873
7599
|
// src/backlog/index.ts
|
|
6874
7600
|
function readFileOrEmpty(filePath) {
|
|
6875
7601
|
try {
|
|
6876
|
-
return
|
|
7602
|
+
return import_node_fs17.default.readFileSync(filePath, "utf-8");
|
|
6877
7603
|
} catch (error) {
|
|
6878
7604
|
const err = error;
|
|
6879
7605
|
if (err.code === "ENOENT") return "";
|
|
@@ -6889,7 +7615,7 @@ function buildFailure(hunt) {
|
|
|
6889
7615
|
if (!hunt.runDir) return failure;
|
|
6890
7616
|
let run;
|
|
6891
7617
|
try {
|
|
6892
|
-
const resultJson = readFileOrEmpty(
|
|
7618
|
+
const resultJson = readFileOrEmpty(import_node_path18.default.join(hunt.runDir, "result.json"));
|
|
6893
7619
|
if (!resultJson) return failure;
|
|
6894
7620
|
run = JSON.parse(resultJson);
|
|
6895
7621
|
} catch (error) {
|
|
@@ -6918,8 +7644,8 @@ function extractFailures(suiteResult) {
|
|
|
6918
7644
|
}
|
|
6919
7645
|
function updateBacklogFromSuite(suiteResult, options = {}) {
|
|
6920
7646
|
const projectRoot = options.projectRoot ?? process.cwd();
|
|
6921
|
-
const backlogPath = options.backlogPath ??
|
|
6922
|
-
const resolvedPath = options.resolvedPath ??
|
|
7647
|
+
const backlogPath = options.backlogPath ?? import_node_path18.default.join(projectRoot, "docs", "backlog.md");
|
|
7648
|
+
const resolvedPath = options.resolvedPath ?? import_node_path18.default.join(projectRoot, "docs", "resolved.md");
|
|
6923
7649
|
const date = options.date ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
6924
7650
|
const summary = { created: [], regressions: [], skipped: [], backlogPath };
|
|
6925
7651
|
const failures = extractFailures(suiteResult);
|
|
@@ -6951,8 +7677,8 @@ function updateBacklogFromSuite(suiteResult, options = {}) {
|
|
|
6951
7677
|
}
|
|
6952
7678
|
}
|
|
6953
7679
|
if (ticketsToAdd.length > 0) {
|
|
6954
|
-
|
|
6955
|
-
|
|
7680
|
+
import_node_fs17.default.mkdirSync(import_node_path18.default.dirname(backlogPath), { recursive: true });
|
|
7681
|
+
import_node_fs17.default.writeFileSync(backlogPath, insertTickets(backlogContent, ticketsToAdd));
|
|
6956
7682
|
}
|
|
6957
7683
|
return summary;
|
|
6958
7684
|
}
|
|
@@ -7113,7 +7839,7 @@ async function runSuite(options = {}) {
|
|
|
7113
7839
|
const clusters = clusterFailures(
|
|
7114
7840
|
extractFailures({ result: { hunts: results }, resultPath: null })
|
|
7115
7841
|
).filter((cluster) => cluster.count > 1);
|
|
7116
|
-
const ciRunDir =
|
|
7842
|
+
const ciRunDir = import_node_path19.default.join(configDir, "runs", timestamp("ci"));
|
|
7117
7843
|
const resultPath = writeCiResult(ciRunDir, results, startedAt, totalDurationMs, flaky, clusters);
|
|
7118
7844
|
const { passed, failed, skipped } = countCiResults(results);
|
|
7119
7845
|
return {
|
|
@@ -7798,11 +8524,17 @@ async function generateHunt(options) {
|
|
|
7798
8524
|
DEFAULT_REQUEST_TIMEOUT_MS,
|
|
7799
8525
|
DEFAULT_SIMULATOR_LOCK_ROOT,
|
|
7800
8526
|
DEFAULT_WDA_REQUEST_TIMEOUT_MS,
|
|
8527
|
+
HELPER_BINARY,
|
|
7801
8528
|
INTERACTIVE_ROLES,
|
|
7802
8529
|
IOS_INTERACTIVE_TYPES,
|
|
7803
8530
|
IOS_MATCH_DIALECT,
|
|
7804
8531
|
IOS_PRESS_KEYS,
|
|
7805
8532
|
IOS_WINDOW_TYPE,
|
|
8533
|
+
MACDRIVER_REPO,
|
|
8534
|
+
MACDRIVER_SIGNING_AUTHORITY_PREFIX,
|
|
8535
|
+
MACDRIVER_SIGNING_IDENTIFIER,
|
|
8536
|
+
MACDRIVER_VERSION,
|
|
8537
|
+
MACDRIVER_VERSION_PATTERN,
|
|
7806
8538
|
MACOS_MATCH_DIALECT,
|
|
7807
8539
|
NATIVE_ATTRIBUTE_MAP,
|
|
7808
8540
|
SpawnMacHelperClient,
|
|
@@ -7832,6 +8564,8 @@ async function generateHunt(options) {
|
|
|
7832
8564
|
closeIosSession,
|
|
7833
8565
|
closeMacSession,
|
|
7834
8566
|
clusterFailures,
|
|
8567
|
+
codesignVerifier,
|
|
8568
|
+
collectMacdriverStatus,
|
|
7835
8569
|
computeFlakeScore,
|
|
7836
8570
|
configSchema,
|
|
7837
8571
|
createAndroidDriver,
|
|
@@ -7845,6 +8579,8 @@ async function generateHunt(options) {
|
|
|
7845
8579
|
defaultAgentConnector,
|
|
7846
8580
|
defaultIosAgentConnector,
|
|
7847
8581
|
defaultWdaTestRunPreparer,
|
|
8582
|
+
dittoExtractor,
|
|
8583
|
+
downloadAndVerify,
|
|
7848
8584
|
escapePredicateArg,
|
|
7849
8585
|
escapeUiSelectorArg,
|
|
7850
8586
|
extractElementId,
|
|
@@ -7855,6 +8591,7 @@ async function generateHunt(options) {
|
|
|
7855
8591
|
healSelector,
|
|
7856
8592
|
huntSchema,
|
|
7857
8593
|
injectUsePortIntoXctestrun,
|
|
8594
|
+
installMacdriver,
|
|
7858
8595
|
interpolateHunt,
|
|
7859
8596
|
iosAppAllowedIdentities,
|
|
7860
8597
|
iosNodeToNative,
|
|
@@ -7871,7 +8608,14 @@ async function generateHunt(options) {
|
|
|
7871
8608
|
loadHunt,
|
|
7872
8609
|
loadHuntMeta,
|
|
7873
8610
|
loadHuntTags,
|
|
8611
|
+
macdriverAssetName,
|
|
8612
|
+
macdriverAssetUrl,
|
|
7874
8613
|
macdriverBuildInstructions,
|
|
8614
|
+
macdriverChecksumName,
|
|
8615
|
+
macdriverInstallRoot,
|
|
8616
|
+
macdriverInstalledBinary,
|
|
8617
|
+
macdriverReleaseTag,
|
|
8618
|
+
macdriverVersionDir,
|
|
7875
8619
|
matchAndroidSelector,
|
|
7876
8620
|
matchIosSelector,
|
|
7877
8621
|
matchNativeTree,
|
|
@@ -7881,6 +8625,8 @@ async function generateHunt(options) {
|
|
|
7881
8625
|
parseAdbDevices,
|
|
7882
8626
|
parseAndroidHierarchy,
|
|
7883
8627
|
parseAndroidSelector,
|
|
8628
|
+
parseChecksumFile,
|
|
8629
|
+
parseCodesignDetails,
|
|
7884
8630
|
parseForwardPort,
|
|
7885
8631
|
parseIosHierarchy,
|
|
7886
8632
|
parseIosSelector,
|
|
@@ -7907,19 +8653,26 @@ async function generateHunt(options) {
|
|
|
7907
8653
|
resolveWdaTestRun,
|
|
7908
8654
|
runHunt,
|
|
7909
8655
|
runSuite,
|
|
8656
|
+
runVersionProbe,
|
|
7910
8657
|
selectDeviceSerial,
|
|
7911
8658
|
selectSimulatorUdid,
|
|
8659
|
+
sha256Hex,
|
|
7912
8660
|
shortIosType,
|
|
7913
8661
|
stepSchema,
|
|
8662
|
+
tccGuidance,
|
|
7914
8663
|
unquoteSelectorValue,
|
|
7915
8664
|
unwrapAndroidTextSelector,
|
|
7916
8665
|
unwrapIosTextSelector,
|
|
7917
8666
|
unwrapNativeTextSelector,
|
|
7918
8667
|
updateBacklogFromSuite,
|
|
8668
|
+
validateMacdriverArchiveEntries,
|
|
8669
|
+
validateMacdriverVersion,
|
|
8670
|
+
verifyMacdriverSignature,
|
|
7919
8671
|
waitForAgentReady,
|
|
7920
8672
|
waitForWdaReady,
|
|
7921
8673
|
wdaCacheDir,
|
|
7922
8674
|
wdaTestRunArgs,
|
|
7923
|
-
webOnlyReason
|
|
8675
|
+
webOnlyReason,
|
|
8676
|
+
zipinfoArchiveLister
|
|
7924
8677
|
});
|
|
7925
8678
|
//# sourceMappingURL=lib.cjs.map
|