prowl-tools 0.1.5 → 0.1.7
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 +578 -185
- package/dist/{chunk-ITOSUJCN.js → chunk-O3OUTZ2P.js} +22 -2
- package/dist/chunk-O3OUTZ2P.js.map +1 -0
- package/dist/{chunk-2KD2XCTH.js → chunk-R7NUH44M.js} +1690 -298
- package/dist/chunk-R7NUH44M.js.map +1 -0
- package/dist/index.cjs +1839 -365
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +277 -19
- package/dist/index.js.map +1 -1
- package/dist/lib.cjs +1864 -403
- package/dist/lib.cjs.map +1 -1
- package/dist/lib.d.cts +702 -58
- package/dist/lib.d.ts +702 -58
- package/dist/lib.js +130 -8
- package/dist/{loader-FCXPARP7.js → loader-X37URHUV.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-2KD2XCTH.js.map +0 -1
- package/dist/chunk-ITOSUJCN.js.map +0 -1
- /package/dist/{loader-FCXPARP7.js.map → loader-X37URHUV.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
|
}
|
|
@@ -86,27 +86,43 @@ var init_visual = __esm({
|
|
|
86
86
|
// src/index.ts
|
|
87
87
|
var src_exports = {};
|
|
88
88
|
__export(src_exports, {
|
|
89
|
+
ANDROID_INTERACTIVE_CLASSES: () => ANDROID_INTERACTIVE_CLASSES,
|
|
89
90
|
ANDROID_KEYCODES: () => ANDROID_KEYCODES,
|
|
91
|
+
ANDROID_MATCH_DIALECT: () => ANDROID_MATCH_DIALECT,
|
|
90
92
|
DEFAULT_AGENT_REQUEST_TIMEOUT_MS: () => DEFAULT_AGENT_REQUEST_TIMEOUT_MS,
|
|
91
93
|
DEFAULT_ANALYZE_TREE_DEPTH: () => DEFAULT_ANALYZE_TREE_DEPTH,
|
|
92
94
|
DEFAULT_FLAKY_THRESHOLD: () => DEFAULT_FLAKY_THRESHOLD,
|
|
93
95
|
DEFAULT_REQUEST_TIMEOUT_MS: () => DEFAULT_REQUEST_TIMEOUT_MS,
|
|
94
96
|
DEFAULT_SIMULATOR_LOCK_ROOT: () => DEFAULT_SIMULATOR_LOCK_ROOT,
|
|
95
97
|
DEFAULT_WDA_REQUEST_TIMEOUT_MS: () => DEFAULT_WDA_REQUEST_TIMEOUT_MS,
|
|
98
|
+
HELPER_BINARY: () => HELPER_BINARY,
|
|
96
99
|
INTERACTIVE_ROLES: () => INTERACTIVE_ROLES,
|
|
100
|
+
IOS_INTERACTIVE_TYPES: () => IOS_INTERACTIVE_TYPES,
|
|
101
|
+
IOS_MATCH_DIALECT: () => IOS_MATCH_DIALECT,
|
|
97
102
|
IOS_PRESS_KEYS: () => IOS_PRESS_KEYS,
|
|
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,
|
|
109
|
+
MACOS_MATCH_DIALECT: () => MACOS_MATCH_DIALECT,
|
|
110
|
+
NATIVE_ATTRIBUTE_MAP: () => NATIVE_ATTRIBUTE_MAP,
|
|
98
111
|
SpawnMacHelperClient: () => SpawnMacHelperClient,
|
|
99
112
|
UIA2_REMOTE_PORT: () => UIA2_REMOTE_PORT,
|
|
100
113
|
Uia2HttpError: () => Uia2HttpError,
|
|
101
114
|
Uia2Transport: () => Uia2Transport,
|
|
102
|
-
WDA_RUNNER_APP_NAME: () => WDA_RUNNER_APP_NAME,
|
|
103
115
|
WDA_RUNNER_BUNDLE_ID: () => WDA_RUNNER_BUNDLE_ID,
|
|
116
|
+
WDA_USE_PORT_ENV: () => WDA_USE_PORT_ENV,
|
|
104
117
|
WEB_ONLY_STEP_TYPES: () => WEB_ONLY_STEP_TYPES,
|
|
105
118
|
WdaHttpError: () => WdaHttpError,
|
|
106
119
|
WdaTransport: () => WdaTransport,
|
|
120
|
+
analyzeAndroidApp: () => analyzeAndroidApp,
|
|
121
|
+
analyzeIosApp: () => analyzeIosApp,
|
|
107
122
|
analyzeMacApp: () => analyzeMacApp,
|
|
108
123
|
analyzePage: () => analyzePage,
|
|
109
124
|
androidAppAllowedIdentities: () => androidAppAllowedIdentities,
|
|
125
|
+
androidNodeToNative: () => androidNodeToNative,
|
|
110
126
|
androidQueryToLocator: () => androidQueryToLocator,
|
|
111
127
|
assertAndroidAppAllowed: () => assertAndroidAppAllowed,
|
|
112
128
|
assertIosAppAllowed: () => assertIosAppAllowed,
|
|
@@ -119,6 +135,8 @@ __export(src_exports, {
|
|
|
119
135
|
closeIosSession: () => closeIosSession,
|
|
120
136
|
closeMacSession: () => closeMacSession,
|
|
121
137
|
clusterFailures: () => clusterFailures,
|
|
138
|
+
codesignVerifier: () => codesignVerifier,
|
|
139
|
+
collectMacdriverStatus: () => collectMacdriverStatus,
|
|
122
140
|
computeFlakeScore: () => computeFlakeScore,
|
|
123
141
|
configSchema: () => configSchema,
|
|
124
142
|
createAndroidDriver: () => createAndroidDriver,
|
|
@@ -128,8 +146,12 @@ __export(src_exports, {
|
|
|
128
146
|
createUia2Session: () => createUia2Session,
|
|
129
147
|
createWdaAgentClient: () => createWdaAgentClient,
|
|
130
148
|
createWdaSession: () => createWdaSession,
|
|
149
|
+
decodeXmlEntities: () => decodeXmlEntities,
|
|
131
150
|
defaultAgentConnector: () => defaultAgentConnector,
|
|
132
151
|
defaultIosAgentConnector: () => defaultIosAgentConnector,
|
|
152
|
+
defaultWdaTestRunPreparer: () => defaultWdaTestRunPreparer,
|
|
153
|
+
dittoExtractor: () => dittoExtractor,
|
|
154
|
+
downloadAndVerify: () => downloadAndVerify,
|
|
133
155
|
escapePredicateArg: () => escapePredicateArg,
|
|
134
156
|
escapeUiSelectorArg: () => escapeUiSelectorArg,
|
|
135
157
|
extractElementId: () => extractElementId,
|
|
@@ -139,9 +161,14 @@ __export(src_exports, {
|
|
|
139
161
|
generateHunt: () => generateHunt,
|
|
140
162
|
healSelector: () => healSelector,
|
|
141
163
|
huntSchema: () => huntSchema,
|
|
164
|
+
injectUsePortIntoXctestrun: () => injectUsePortIntoXctestrun,
|
|
165
|
+
installMacdriver: () => installMacdriver,
|
|
142
166
|
interpolateHunt: () => interpolateHunt,
|
|
143
167
|
iosAppAllowedIdentities: () => iosAppAllowedIdentities,
|
|
168
|
+
iosNodeToNative: () => iosNodeToNative,
|
|
144
169
|
iosQueryToLocator: () => iosQueryToLocator,
|
|
170
|
+
isAndroidInteractive: () => isAndroidInteractive,
|
|
171
|
+
isIosInteractive: () => isIosInteractive,
|
|
145
172
|
launchAndroidSession: () => launchAndroidSession,
|
|
146
173
|
launchIosSession: () => launchIosSession,
|
|
147
174
|
launchMacSession: () => launchMacSession,
|
|
@@ -152,18 +179,41 @@ __export(src_exports, {
|
|
|
152
179
|
loadHunt: () => loadHunt,
|
|
153
180
|
loadHuntMeta: () => loadHuntMeta,
|
|
154
181
|
loadHuntTags: () => loadHuntTags,
|
|
182
|
+
macdriverAssetName: () => macdriverAssetName,
|
|
183
|
+
macdriverAssetUrl: () => macdriverAssetUrl,
|
|
155
184
|
macdriverBuildInstructions: () => macdriverBuildInstructions,
|
|
185
|
+
macdriverChecksumName: () => macdriverChecksumName,
|
|
186
|
+
macdriverInstallRoot: () => macdriverInstallRoot,
|
|
187
|
+
macdriverInstalledBinary: () => macdriverInstalledBinary,
|
|
188
|
+
macdriverReleaseTag: () => macdriverReleaseTag,
|
|
189
|
+
macdriverVersionDir: () => macdriverVersionDir,
|
|
190
|
+
matchAndroidSelector: () => matchAndroidSelector,
|
|
191
|
+
matchIosSelector: () => matchIosSelector,
|
|
192
|
+
matchNativeTree: () => matchNativeTree,
|
|
193
|
+
nodeMatchesSelector: () => nodeMatchesSelector,
|
|
156
194
|
normalizeXcuiClassName: () => normalizeXcuiClassName,
|
|
157
195
|
parseAaptPackage: () => parseAaptPackage,
|
|
158
196
|
parseAdbDevices: () => parseAdbDevices,
|
|
197
|
+
parseAndroidHierarchy: () => parseAndroidHierarchy,
|
|
159
198
|
parseAndroidSelector: () => parseAndroidSelector,
|
|
199
|
+
parseChecksumFile: () => parseChecksumFile,
|
|
200
|
+
parseCodesignDetails: () => parseCodesignDetails,
|
|
160
201
|
parseForwardPort: () => parseForwardPort,
|
|
202
|
+
parseIosHierarchy: () => parseIosHierarchy,
|
|
161
203
|
parseIosSelector: () => parseIosSelector,
|
|
162
204
|
parseMacSelector: () => parseMacSelector,
|
|
205
|
+
parseNativeSelector: () => parseNativeSelector,
|
|
163
206
|
parseSimctlDevices: () => parseSimctlDevices,
|
|
207
|
+
parseSnapshot: () => parseSnapshot,
|
|
164
208
|
parseXcodeVersion: () => parseXcodeVersion,
|
|
209
|
+
parseXml: () => parseXml,
|
|
210
|
+
qualifyResourceId: () => qualifyResourceId,
|
|
211
|
+
quoteSelectorValue: () => quoteSelectorValue,
|
|
212
|
+
rankAndroidSelectors: () => rankAndroidSelectors,
|
|
165
213
|
rankFlaky: () => rankFlaky,
|
|
214
|
+
rankIosSelectors: () => rankIosSelectors,
|
|
166
215
|
rankMacSelectors: () => rankMacSelectors,
|
|
216
|
+
rankNativeSelectors: () => rankNativeSelectors,
|
|
167
217
|
readHistory: () => readHistory,
|
|
168
218
|
readHuntHistory: () => readHuntHistory,
|
|
169
219
|
readIosBundleIdentifier: () => readIosBundleIdentifier,
|
|
@@ -171,19 +221,30 @@ __export(src_exports, {
|
|
|
171
221
|
resolveAgentApks: () => resolveAgentApks,
|
|
172
222
|
resolveHelperBinary: () => resolveHelperBinary,
|
|
173
223
|
resolveWdaProject: () => resolveWdaProject,
|
|
174
|
-
|
|
224
|
+
resolveWdaTestRun: () => resolveWdaTestRun,
|
|
175
225
|
runHunt: () => runHunt,
|
|
176
226
|
runSuite: () => runSuite,
|
|
227
|
+
runVersionProbe: () => runVersionProbe,
|
|
177
228
|
selectDeviceSerial: () => selectDeviceSerial,
|
|
178
229
|
selectSimulatorUdid: () => selectSimulatorUdid,
|
|
230
|
+
sha256Hex: () => sha256Hex,
|
|
231
|
+
shortIosType: () => shortIosType,
|
|
179
232
|
stepSchema: () => stepSchema,
|
|
233
|
+
tccGuidance: () => tccGuidance,
|
|
234
|
+
unquoteSelectorValue: () => unquoteSelectorValue,
|
|
180
235
|
unwrapAndroidTextSelector: () => unwrapAndroidTextSelector,
|
|
181
236
|
unwrapIosTextSelector: () => unwrapIosTextSelector,
|
|
237
|
+
unwrapNativeTextSelector: () => unwrapNativeTextSelector,
|
|
182
238
|
updateBacklogFromSuite: () => updateBacklogFromSuite,
|
|
239
|
+
validateMacdriverArchiveEntries: () => validateMacdriverArchiveEntries,
|
|
240
|
+
validateMacdriverVersion: () => validateMacdriverVersion,
|
|
241
|
+
verifyMacdriverSignature: () => verifyMacdriverSignature,
|
|
183
242
|
waitForAgentReady: () => waitForAgentReady,
|
|
184
243
|
waitForWdaReady: () => waitForWdaReady,
|
|
185
244
|
wdaCacheDir: () => wdaCacheDir,
|
|
186
|
-
|
|
245
|
+
wdaTestRunArgs: () => wdaTestRunArgs,
|
|
246
|
+
webOnlyReason: () => webOnlyReason,
|
|
247
|
+
zipinfoArchiveLister: () => zipinfoArchiveLister
|
|
187
248
|
});
|
|
188
249
|
module.exports = __toCommonJS(src_exports);
|
|
189
250
|
|
|
@@ -366,25 +427,71 @@ function createMacDriver(client, options = {}) {
|
|
|
366
427
|
// src/browser/mac-helper.ts
|
|
367
428
|
var import_node_child_process = require("child_process");
|
|
368
429
|
var import_node_fs = __toESM(require("fs"), 1);
|
|
369
|
-
var
|
|
430
|
+
var import_node_os2 = __toESM(require("os"), 1);
|
|
431
|
+
var import_node_path2 = __toESM(require("path"), 1);
|
|
370
432
|
var import_node_url = require("url");
|
|
371
|
-
|
|
433
|
+
|
|
434
|
+
// src/browser/macdriver-release.ts
|
|
435
|
+
var import_node_os = __toESM(require("os"), 1);
|
|
436
|
+
var import_node_path = __toESM(require("path"), 1);
|
|
372
437
|
var HELPER_BINARY = "prowl-macdriver";
|
|
438
|
+
var MACDRIVER_VERSION = "0.1.0";
|
|
439
|
+
var MACDRIVER_REPO = "prowl-tools/prowl";
|
|
440
|
+
var MACDRIVER_SIGNING_IDENTIFIER = "tools.prowl.macdriver";
|
|
441
|
+
var MACDRIVER_SIGNING_AUTHORITY_PREFIX = "Developer ID Application: Genkei Labs";
|
|
442
|
+
var MACDRIVER_VERSION_PATTERN = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z][0-9A-Za-z.-]*)?(?:\+[0-9A-Za-z][0-9A-Za-z.-]*)?$/;
|
|
443
|
+
function validateMacdriverVersion(version) {
|
|
444
|
+
if (!MACDRIVER_VERSION_PATTERN.test(version)) {
|
|
445
|
+
throw new Error(
|
|
446
|
+
`Invalid prowl-macdriver version "${version}". Expected a release version like 0.1.0.`
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
return version;
|
|
450
|
+
}
|
|
451
|
+
function macdriverReleaseTag(version = MACDRIVER_VERSION) {
|
|
452
|
+
return `macdriver-v${validateMacdriverVersion(version)}`;
|
|
453
|
+
}
|
|
454
|
+
function macdriverAssetName(version = MACDRIVER_VERSION) {
|
|
455
|
+
return `prowl-macdriver-v${validateMacdriverVersion(version)}-universal.zip`;
|
|
456
|
+
}
|
|
457
|
+
function macdriverChecksumName(version = MACDRIVER_VERSION) {
|
|
458
|
+
return `${macdriverAssetName(version)}.sha256`;
|
|
459
|
+
}
|
|
460
|
+
function macdriverAssetUrl(assetName, version = MACDRIVER_VERSION) {
|
|
461
|
+
return `https://github.com/${MACDRIVER_REPO}/releases/download/${macdriverReleaseTag(version)}/${assetName}`;
|
|
462
|
+
}
|
|
463
|
+
function macdriverInstallRoot(homedir = import_node_os.default.homedir()) {
|
|
464
|
+
return import_node_path.default.join(homedir, ".prowl", "macdriver");
|
|
465
|
+
}
|
|
466
|
+
function macdriverVersionDir(version = MACDRIVER_VERSION, homedir = import_node_os.default.homedir()) {
|
|
467
|
+
const root = import_node_path.default.resolve(macdriverInstallRoot(homedir));
|
|
468
|
+
const versionDir = import_node_path.default.resolve(root, validateMacdriverVersion(version));
|
|
469
|
+
if (!versionDir.startsWith(root + import_node_path.default.sep)) {
|
|
470
|
+
throw new Error(`Resolved prowl-macdriver version directory escaped install root: ${versionDir}`);
|
|
471
|
+
}
|
|
472
|
+
return versionDir;
|
|
473
|
+
}
|
|
474
|
+
function macdriverInstalledBinary(version = MACDRIVER_VERSION, homedir = import_node_os.default.homedir()) {
|
|
475
|
+
return import_node_path.default.join(macdriverVersionDir(version, homedir), HELPER_BINARY);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// src/browser/mac-helper.ts
|
|
479
|
+
var import_meta = {};
|
|
373
480
|
function macdriverBuildInstructions() {
|
|
374
|
-
return "The macOS target
|
|
481
|
+
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.";
|
|
375
482
|
}
|
|
376
483
|
function getPackageRoot() {
|
|
377
|
-
let dir =
|
|
378
|
-
const root =
|
|
484
|
+
let dir = import_node_path2.default.dirname((0, import_node_url.fileURLToPath)(import_meta.url));
|
|
485
|
+
const root = import_node_path2.default.parse(dir).root;
|
|
379
486
|
while (dir !== root) {
|
|
380
|
-
if (import_node_fs.default.existsSync(
|
|
487
|
+
if (import_node_fs.default.existsSync(import_node_path2.default.join(dir, "package.json"))) {
|
|
381
488
|
return dir;
|
|
382
489
|
}
|
|
383
|
-
dir =
|
|
490
|
+
dir = import_node_path2.default.dirname(dir);
|
|
384
491
|
}
|
|
385
492
|
return root;
|
|
386
493
|
}
|
|
387
|
-
function resolveHelperBinary(env = process.env) {
|
|
494
|
+
function resolveHelperBinary(env = process.env, options = {}) {
|
|
388
495
|
const override = env.PROWL_MACDRIVER_BIN;
|
|
389
496
|
if (override) {
|
|
390
497
|
if (!import_node_fs.default.existsSync(override)) {
|
|
@@ -395,10 +502,15 @@ ${macdriverBuildInstructions()}`
|
|
|
395
502
|
}
|
|
396
503
|
return override;
|
|
397
504
|
}
|
|
505
|
+
const homedir = options.homedir ?? import_node_os2.default.homedir();
|
|
506
|
+
const userBinary = macdriverInstalledBinary(MACDRIVER_VERSION, homedir);
|
|
507
|
+
if (import_node_fs.default.existsSync(userBinary)) {
|
|
508
|
+
return userBinary;
|
|
509
|
+
}
|
|
398
510
|
const root = getPackageRoot();
|
|
399
511
|
const candidates = [
|
|
400
|
-
|
|
401
|
-
|
|
512
|
+
import_node_path2.default.join(root, "macdriver", ".build", "release", HELPER_BINARY),
|
|
513
|
+
import_node_path2.default.join(root, "macdriver", ".build", "debug", HELPER_BINARY)
|
|
402
514
|
];
|
|
403
515
|
for (const candidate of candidates) {
|
|
404
516
|
if (import_node_fs.default.existsSync(candidate)) {
|
|
@@ -570,8 +682,583 @@ async function closeMacSession(session) {
|
|
|
570
682
|
}
|
|
571
683
|
}
|
|
572
684
|
|
|
573
|
-
// src/browser/
|
|
685
|
+
// src/browser/macdriver-install.ts
|
|
686
|
+
var import_node_child_process2 = require("child_process");
|
|
687
|
+
var import_node_crypto = require("crypto");
|
|
574
688
|
var import_node_fs2 = __toESM(require("fs"), 1);
|
|
689
|
+
var import_node_os3 = __toESM(require("os"), 1);
|
|
690
|
+
var import_node_path3 = __toESM(require("path"), 1);
|
|
691
|
+
var import_node_util = require("util");
|
|
692
|
+
var execFileAsync = (0, import_node_util.promisify)(import_node_child_process2.execFile);
|
|
693
|
+
function parseChecksumFile(text) {
|
|
694
|
+
const token = text.trim().split(/\s+/, 1)[0] ?? "";
|
|
695
|
+
const digest = token.toLowerCase();
|
|
696
|
+
if (!/^[0-9a-f]{64}$/.test(digest)) {
|
|
697
|
+
throw new Error(`Malformed .sha256 checksum file (expected a 64-char hex digest, got: ${text.trim().slice(0, 80)})`);
|
|
698
|
+
}
|
|
699
|
+
return digest;
|
|
700
|
+
}
|
|
701
|
+
function sha256Hex(bytes) {
|
|
702
|
+
return (0, import_node_crypto.createHash)("sha256").update(bytes).digest("hex");
|
|
703
|
+
}
|
|
704
|
+
var commandRunner = async (file, args) => execFileAsync(file, args);
|
|
705
|
+
function commandOutput(result) {
|
|
706
|
+
return [result.stdout, result.stderr].filter((value) => value !== void 0).map((value) => value.toString()).join("\n").trim();
|
|
707
|
+
}
|
|
708
|
+
function commandErrorDetail(error) {
|
|
709
|
+
const err = error;
|
|
710
|
+
return [err.stderr, err.stdout, err.message].filter((value) => value !== void 0 && value !== "").map((value) => value.toString()).join("\n").trim();
|
|
711
|
+
}
|
|
712
|
+
async function runRequiredCommand(run, binaryPath, command, args, label) {
|
|
713
|
+
try {
|
|
714
|
+
return await run(command, args);
|
|
715
|
+
} catch (error) {
|
|
716
|
+
const detail = commandErrorDetail(error);
|
|
717
|
+
throw new Error(`${label} failed for ${binaryPath}${detail ? `: ${detail}` : ""}`);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
var zipinfoArchiveLister = async (zipPath) => {
|
|
721
|
+
try {
|
|
722
|
+
const { stdout } = await execFileAsync("zipinfo", ["-1", zipPath]);
|
|
723
|
+
return stdout.toString().split(/\r?\n/).filter((entry) => entry.length > 0);
|
|
724
|
+
} catch (error) {
|
|
725
|
+
const detail = commandErrorDetail(error);
|
|
726
|
+
throw new Error(`Failed to inspect release archive ${zipPath} with zipinfo${detail ? `: ${detail}` : ""}`);
|
|
727
|
+
}
|
|
728
|
+
};
|
|
729
|
+
function normalizeArchiveEntryName(entry) {
|
|
730
|
+
if (entry.length === 0 || entry !== entry.trim() || entry.includes("\0") || entry.includes("\\")) {
|
|
731
|
+
throw new Error(`Unsafe path in prowl-macdriver release archive: ${JSON.stringify(entry)}`);
|
|
732
|
+
}
|
|
733
|
+
if (entry.endsWith("/")) {
|
|
734
|
+
throw new Error(`Unexpected directory in prowl-macdriver release archive: ${entry}`);
|
|
735
|
+
}
|
|
736
|
+
if (import_node_path3.default.posix.isAbsolute(entry)) {
|
|
737
|
+
throw new Error(`Unsafe absolute path in prowl-macdriver release archive: ${entry}`);
|
|
738
|
+
}
|
|
739
|
+
const parts = entry.split("/");
|
|
740
|
+
if (parts.some((part) => part === "" || part === "." || part === "..")) {
|
|
741
|
+
throw new Error(`Unsafe path in prowl-macdriver release archive: ${entry}`);
|
|
742
|
+
}
|
|
743
|
+
return entry;
|
|
744
|
+
}
|
|
745
|
+
function validateMacdriverArchiveEntries(entries) {
|
|
746
|
+
const normalized = entries.map(normalizeArchiveEntryName);
|
|
747
|
+
if (normalized.length !== 1 || normalized[0] !== HELPER_BINARY) {
|
|
748
|
+
const shown = normalized.length > 0 ? normalized.join(", ") : "(empty archive)";
|
|
749
|
+
throw new Error(
|
|
750
|
+
`Unexpected prowl-macdriver release archive contents: ${shown}. Expected exactly "${HELPER_BINARY}" at the archive root.`
|
|
751
|
+
);
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
var dittoExtractor = async (zipPath, destDir) => {
|
|
755
|
+
await execFileAsync("ditto", ["-x", "-k", zipPath, destDir]);
|
|
756
|
+
};
|
|
757
|
+
function parseCodesignDetails(text) {
|
|
758
|
+
const details = { identifier: null, authorities: [], teamIdentifier: null };
|
|
759
|
+
for (const line of text.split(/\r?\n/)) {
|
|
760
|
+
const trimmed = line.trim();
|
|
761
|
+
if (trimmed.startsWith("Identifier=")) {
|
|
762
|
+
details.identifier = trimmed.slice("Identifier=".length);
|
|
763
|
+
} else if (trimmed.startsWith("Authority=")) {
|
|
764
|
+
details.authorities.push(trimmed.slice("Authority=".length));
|
|
765
|
+
} else if (trimmed.startsWith("TeamIdentifier=")) {
|
|
766
|
+
details.teamIdentifier = trimmed.slice("TeamIdentifier=".length);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
return details;
|
|
770
|
+
}
|
|
771
|
+
function validateCodesignDetails(details, binaryPath) {
|
|
772
|
+
if (details.identifier !== MACDRIVER_SIGNING_IDENTIFIER) {
|
|
773
|
+
throw new Error(
|
|
774
|
+
`codesign verification failed for ${binaryPath}: expected identifier "${MACDRIVER_SIGNING_IDENTIFIER}", got "${details.identifier ?? "missing"}"`
|
|
775
|
+
);
|
|
776
|
+
}
|
|
777
|
+
const developerIdAuthority = details.authorities.find(
|
|
778
|
+
(authority) => authority.startsWith(`${MACDRIVER_SIGNING_AUTHORITY_PREFIX} (`)
|
|
779
|
+
);
|
|
780
|
+
const authorityTeamId = developerIdAuthority?.match(/\(([A-Z0-9]{10})\)$/)?.[1] ?? null;
|
|
781
|
+
if (!developerIdAuthority || !authorityTeamId) {
|
|
782
|
+
const shown = details.authorities.length > 0 ? details.authorities.join(" / ") : "missing";
|
|
783
|
+
throw new Error(
|
|
784
|
+
`codesign verification failed for ${binaryPath}: expected ${MACDRIVER_SIGNING_AUTHORITY_PREFIX} signer, got ${shown}`
|
|
785
|
+
);
|
|
786
|
+
}
|
|
787
|
+
if (!details.teamIdentifier) {
|
|
788
|
+
throw new Error(`codesign verification failed for ${binaryPath}: missing TeamIdentifier`);
|
|
789
|
+
}
|
|
790
|
+
if (details.teamIdentifier !== authorityTeamId) {
|
|
791
|
+
throw new Error(
|
|
792
|
+
`codesign verification failed for ${binaryPath}: TeamIdentifier ${details.teamIdentifier} does not match Developer ID authority team ${authorityTeamId}`
|
|
793
|
+
);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
async function verifyMacdriverSignature(binaryPath, run = commandRunner) {
|
|
797
|
+
await runRequiredCommand(run, binaryPath, "codesign", ["--verify", "--strict", binaryPath], "codesign verification");
|
|
798
|
+
const display = await runRequiredCommand(
|
|
799
|
+
run,
|
|
800
|
+
binaryPath,
|
|
801
|
+
"codesign",
|
|
802
|
+
["--display", "--verbose=4", binaryPath],
|
|
803
|
+
"codesign detail inspection"
|
|
804
|
+
);
|
|
805
|
+
validateCodesignDetails(parseCodesignDetails(commandOutput(display)), binaryPath);
|
|
806
|
+
await runRequiredCommand(
|
|
807
|
+
run,
|
|
808
|
+
binaryPath,
|
|
809
|
+
"spctl",
|
|
810
|
+
["--assess", "--type", "execute", "--verbose=4", binaryPath],
|
|
811
|
+
"spctl assessment"
|
|
812
|
+
);
|
|
813
|
+
}
|
|
814
|
+
var codesignVerifier = async (binaryPath) => {
|
|
815
|
+
await verifyMacdriverSignature(binaryPath);
|
|
816
|
+
};
|
|
817
|
+
async function downloadAndVerify(options = {}) {
|
|
818
|
+
const version = options.version ?? MACDRIVER_VERSION;
|
|
819
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
820
|
+
const assetName = macdriverAssetName(version);
|
|
821
|
+
const zipUrl = macdriverAssetUrl(assetName, version);
|
|
822
|
+
const sumUrl = macdriverAssetUrl(macdriverChecksumName(version), version);
|
|
823
|
+
const [zipRes, sumRes] = await Promise.all([
|
|
824
|
+
fetchImpl(zipUrl, { redirect: "follow" }),
|
|
825
|
+
fetchImpl(sumUrl, { redirect: "follow" })
|
|
826
|
+
]);
|
|
827
|
+
if (zipRes.status === 404 || sumRes.status === 404) {
|
|
828
|
+
throw new Error(
|
|
829
|
+
`No published prowl-macdriver release for ${macdriverReleaseTag(version)} yet.
|
|
830
|
+
The signed binary is cut by the maintainer; until then build from source:
|
|
831
|
+
cd macdriver && swift build -c release`
|
|
832
|
+
);
|
|
833
|
+
}
|
|
834
|
+
if (!zipRes.ok) {
|
|
835
|
+
throw new Error(`Failed to download ${assetName} (HTTP ${zipRes.status}) from ${zipUrl}`);
|
|
836
|
+
}
|
|
837
|
+
if (!sumRes.ok) {
|
|
838
|
+
throw new Error(`Failed to download the checksum (HTTP ${sumRes.status}) from ${sumUrl}`);
|
|
839
|
+
}
|
|
840
|
+
const zipBytes = Buffer.from(await zipRes.arrayBuffer());
|
|
841
|
+
const expected = parseChecksumFile(await sumRes.text());
|
|
842
|
+
const actual = sha256Hex(zipBytes);
|
|
843
|
+
if (actual !== expected) {
|
|
844
|
+
throw new Error(
|
|
845
|
+
`Checksum mismatch for ${assetName}.
|
|
846
|
+
expected: ${expected}
|
|
847
|
+
actual: ${actual}
|
|
848
|
+
The download was rejected and discarded; re-run the install, and report it if it repeats.`
|
|
849
|
+
);
|
|
850
|
+
}
|
|
851
|
+
return zipBytes;
|
|
852
|
+
}
|
|
853
|
+
async function installMacdriver(options = {}) {
|
|
854
|
+
const version = validateMacdriverVersion(options.version ?? MACDRIVER_VERSION);
|
|
855
|
+
const homedir = options.homedir ?? import_node_os3.default.homedir();
|
|
856
|
+
const listArchiveEntries = options.listArchiveEntries ?? zipinfoArchiveLister;
|
|
857
|
+
const extract = options.extract ?? dittoExtractor;
|
|
858
|
+
const verifySignature = options.verifySignature ?? codesignVerifier;
|
|
859
|
+
const installRoot = macdriverInstallRoot(homedir);
|
|
860
|
+
const versionDir = macdriverVersionDir(version, homedir);
|
|
861
|
+
const binaryPath = macdriverInstalledBinary(version, homedir);
|
|
862
|
+
if (!options.force && import_node_fs2.default.existsSync(binaryPath)) {
|
|
863
|
+
return { version, binaryPath, alreadyInstalled: true };
|
|
864
|
+
}
|
|
865
|
+
const zipBytes = await downloadAndVerify({ version, fetchImpl: options.fetchImpl });
|
|
866
|
+
import_node_fs2.default.mkdirSync(installRoot, { recursive: true });
|
|
867
|
+
const stagingDir = import_node_fs2.default.mkdtempSync(import_node_path3.default.join(installRoot, `.tmp-${version}-`));
|
|
868
|
+
const extractDir = import_node_path3.default.join(stagingDir, "extract");
|
|
869
|
+
const zipPath = import_node_path3.default.join(stagingDir, macdriverAssetName(version));
|
|
870
|
+
try {
|
|
871
|
+
import_node_fs2.default.mkdirSync(extractDir);
|
|
872
|
+
import_node_fs2.default.writeFileSync(zipPath, zipBytes);
|
|
873
|
+
validateMacdriverArchiveEntries(await listArchiveEntries(zipPath));
|
|
874
|
+
await extract(zipPath, extractDir);
|
|
875
|
+
const stagedBinaryPath = import_node_path3.default.join(extractDir, HELPER_BINARY);
|
|
876
|
+
assertExtractedHelper(extractDir, stagedBinaryPath);
|
|
877
|
+
import_node_fs2.default.chmodSync(stagedBinaryPath, 493);
|
|
878
|
+
await verifySignature(stagedBinaryPath);
|
|
879
|
+
replaceVersionDir(versionDir, extractDir, installRoot);
|
|
880
|
+
} finally {
|
|
881
|
+
import_node_fs2.default.rmSync(stagingDir, { recursive: true, force: true });
|
|
882
|
+
}
|
|
883
|
+
return { version, binaryPath, alreadyInstalled: false };
|
|
884
|
+
}
|
|
885
|
+
function assertExtractedHelper(extractDir, binaryPath) {
|
|
886
|
+
const entries = import_node_fs2.default.readdirSync(extractDir);
|
|
887
|
+
if (!entries.includes(HELPER_BINARY)) {
|
|
888
|
+
throw new Error(`The release archive did not contain a "${HELPER_BINARY}" binary.`);
|
|
889
|
+
}
|
|
890
|
+
if (entries.length !== 1 || entries[0] !== HELPER_BINARY) {
|
|
891
|
+
const shown = entries.length > 0 ? entries.join(", ") : "(empty directory)";
|
|
892
|
+
throw new Error(
|
|
893
|
+
`Unexpected extracted prowl-macdriver archive contents: ${shown}. Expected exactly "${HELPER_BINARY}".`
|
|
894
|
+
);
|
|
895
|
+
}
|
|
896
|
+
let stat = null;
|
|
897
|
+
try {
|
|
898
|
+
stat = import_node_fs2.default.lstatSync(binaryPath);
|
|
899
|
+
} catch {
|
|
900
|
+
}
|
|
901
|
+
if (!stat?.isFile()) {
|
|
902
|
+
throw new Error(`The release archive "${HELPER_BINARY}" entry is not a regular file.`);
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
function replaceVersionDir(versionDir, stagedVersionDir, installRoot) {
|
|
906
|
+
const backupDir = import_node_path3.default.join(installRoot, `.previous-${import_node_path3.default.basename(versionDir)}-${process.pid}-${Date.now()}`);
|
|
907
|
+
let backedUp = false;
|
|
908
|
+
try {
|
|
909
|
+
if (import_node_fs2.default.existsSync(versionDir)) {
|
|
910
|
+
import_node_fs2.default.renameSync(versionDir, backupDir);
|
|
911
|
+
backedUp = true;
|
|
912
|
+
}
|
|
913
|
+
import_node_fs2.default.renameSync(stagedVersionDir, versionDir);
|
|
914
|
+
if (backedUp) {
|
|
915
|
+
import_node_fs2.default.rmSync(backupDir, { recursive: true, force: true });
|
|
916
|
+
}
|
|
917
|
+
} catch (error) {
|
|
918
|
+
if (backedUp && !import_node_fs2.default.existsSync(versionDir) && import_node_fs2.default.existsSync(backupDir)) {
|
|
919
|
+
import_node_fs2.default.renameSync(backupDir, versionDir);
|
|
920
|
+
}
|
|
921
|
+
throw error;
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
var runVersionProbe = async (binaryPath) => {
|
|
925
|
+
try {
|
|
926
|
+
const { stdout } = await execFileAsync(binaryPath, ["version"], { timeout: 5e3 });
|
|
927
|
+
const match = stdout.match(/prowl-macdriver\s+(\S+)/);
|
|
928
|
+
return match ? match[1] : stdout.trim() || null;
|
|
929
|
+
} catch {
|
|
930
|
+
return null;
|
|
931
|
+
}
|
|
932
|
+
};
|
|
933
|
+
async function collectMacdriverStatus(options = {}) {
|
|
934
|
+
const env = options.env ?? process.env;
|
|
935
|
+
const homedir = options.homedir ?? import_node_os3.default.homedir();
|
|
936
|
+
const probe = options.probe ?? runVersionProbe;
|
|
937
|
+
let resolved = null;
|
|
938
|
+
try {
|
|
939
|
+
const resolvedPath = resolveHelperBinary(env, { homedir });
|
|
940
|
+
resolved = { path: resolvedPath, source: classifyResolvedSource(resolvedPath, env, homedir) };
|
|
941
|
+
} catch {
|
|
942
|
+
resolved = null;
|
|
943
|
+
}
|
|
944
|
+
const installed = [];
|
|
945
|
+
const root = macdriverInstallRoot(homedir);
|
|
946
|
+
if (import_node_fs2.default.existsSync(root)) {
|
|
947
|
+
for (const entry of import_node_fs2.default.readdirSync(root, { withFileTypes: true })) {
|
|
948
|
+
if (!entry.isDirectory()) continue;
|
|
949
|
+
let binaryPath;
|
|
950
|
+
try {
|
|
951
|
+
binaryPath = macdriverInstalledBinary(entry.name, homedir);
|
|
952
|
+
} catch {
|
|
953
|
+
continue;
|
|
954
|
+
}
|
|
955
|
+
if (import_node_fs2.default.existsSync(binaryPath)) {
|
|
956
|
+
installed.push({ version: entry.name, binaryPath });
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
installed.sort((a, b) => a.version.localeCompare(b.version));
|
|
960
|
+
}
|
|
961
|
+
const probedVersion = resolved ? await probe(resolved.path) : null;
|
|
962
|
+
return { resolved, pinnedVersion: MACDRIVER_VERSION, installed, probedVersion };
|
|
963
|
+
}
|
|
964
|
+
function classifyResolvedSource(resolvedPath, env, homedir) {
|
|
965
|
+
if (env.PROWL_MACDRIVER_BIN && resolvedPath === env.PROWL_MACDRIVER_BIN) {
|
|
966
|
+
return "env";
|
|
967
|
+
}
|
|
968
|
+
if (resolvedPath.startsWith(macdriverInstallRoot(homedir) + import_node_path3.default.sep)) {
|
|
969
|
+
return "user-install";
|
|
970
|
+
}
|
|
971
|
+
return "source-build";
|
|
972
|
+
}
|
|
973
|
+
function tccGuidance() {
|
|
974
|
+
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.";
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
// src/browser/android-driver.ts
|
|
978
|
+
var import_node_fs3 = __toESM(require("fs"), 1);
|
|
979
|
+
|
|
980
|
+
// src/analyzer/xml.ts
|
|
981
|
+
var ENTITIES = {
|
|
982
|
+
amp: "&",
|
|
983
|
+
lt: "<",
|
|
984
|
+
gt: ">",
|
|
985
|
+
quot: '"',
|
|
986
|
+
apos: "'"
|
|
987
|
+
};
|
|
988
|
+
function decodeXmlEntities(value) {
|
|
989
|
+
if (!value.includes("&")) {
|
|
990
|
+
return value;
|
|
991
|
+
}
|
|
992
|
+
return value.replace(/&(#(?:[xX][0-9a-fA-F]+|[0-9]+)|[a-zA-Z]+);/g, (match, code) => {
|
|
993
|
+
if (code[0] === "#") {
|
|
994
|
+
const hex = code[1] === "x" || code[1] === "X";
|
|
995
|
+
const num2 = Number.parseInt(code.slice(hex ? 2 : 1), hex ? 16 : 10);
|
|
996
|
+
return Number.isSafeInteger(num2) && num2 <= 1114111 ? String.fromCodePoint(num2) : match;
|
|
997
|
+
}
|
|
998
|
+
const named = ENTITIES[code];
|
|
999
|
+
return named ?? match;
|
|
1000
|
+
});
|
|
1001
|
+
}
|
|
1002
|
+
var ATTR_RE = /([^\s=/]+)\s*=\s*("([^"]*)"|'([^']*)')/g;
|
|
1003
|
+
function parseTagBody(body) {
|
|
1004
|
+
const trimmed = body.trim();
|
|
1005
|
+
const nameMatch = /^([^\s/>]+)/.exec(trimmed);
|
|
1006
|
+
const tag = nameMatch ? nameMatch[1] : "";
|
|
1007
|
+
const attrs = {};
|
|
1008
|
+
const rest = trimmed.slice(tag.length);
|
|
1009
|
+
ATTR_RE.lastIndex = 0;
|
|
1010
|
+
let m;
|
|
1011
|
+
while ((m = ATTR_RE.exec(rest)) !== null) {
|
|
1012
|
+
const rawValue = m[3] !== void 0 ? m[3] : m[4] ?? "";
|
|
1013
|
+
attrs[m[1]] = decodeXmlEntities(rawValue);
|
|
1014
|
+
}
|
|
1015
|
+
return { tag, attrs };
|
|
1016
|
+
}
|
|
1017
|
+
function parseXml(input) {
|
|
1018
|
+
const n = input.length;
|
|
1019
|
+
const stack = [];
|
|
1020
|
+
let root = null;
|
|
1021
|
+
let i = 0;
|
|
1022
|
+
while (i < n) {
|
|
1023
|
+
const lt = input.indexOf("<", i);
|
|
1024
|
+
if (lt < 0) {
|
|
1025
|
+
break;
|
|
1026
|
+
}
|
|
1027
|
+
i = lt + 1;
|
|
1028
|
+
const ch = input[i];
|
|
1029
|
+
if (ch === "?") {
|
|
1030
|
+
const end = input.indexOf("?>", i);
|
|
1031
|
+
i = end < 0 ? n : end + 2;
|
|
1032
|
+
continue;
|
|
1033
|
+
}
|
|
1034
|
+
if (ch === "!") {
|
|
1035
|
+
if (input.startsWith("!--", i)) {
|
|
1036
|
+
const end = input.indexOf("-->", i);
|
|
1037
|
+
i = end < 0 ? n : end + 3;
|
|
1038
|
+
} else {
|
|
1039
|
+
const end = input.indexOf(">", i);
|
|
1040
|
+
i = end < 0 ? n : end + 1;
|
|
1041
|
+
}
|
|
1042
|
+
continue;
|
|
1043
|
+
}
|
|
1044
|
+
if (ch === "/") {
|
|
1045
|
+
const gt = input.indexOf(">", i);
|
|
1046
|
+
i = gt < 0 ? n : gt + 1;
|
|
1047
|
+
stack.pop();
|
|
1048
|
+
continue;
|
|
1049
|
+
}
|
|
1050
|
+
let j = i;
|
|
1051
|
+
let quote2 = null;
|
|
1052
|
+
while (j < n) {
|
|
1053
|
+
const c = input[j];
|
|
1054
|
+
if (quote2 !== null) {
|
|
1055
|
+
if (c === quote2) {
|
|
1056
|
+
quote2 = null;
|
|
1057
|
+
}
|
|
1058
|
+
} else if (c === '"' || c === "'") {
|
|
1059
|
+
quote2 = c;
|
|
1060
|
+
} else if (c === ">") {
|
|
1061
|
+
break;
|
|
1062
|
+
}
|
|
1063
|
+
j += 1;
|
|
1064
|
+
}
|
|
1065
|
+
const inner = input.slice(i, j);
|
|
1066
|
+
i = j + 1;
|
|
1067
|
+
const selfClose = inner.endsWith("/");
|
|
1068
|
+
const body = selfClose ? inner.slice(0, -1) : inner;
|
|
1069
|
+
const { tag, attrs } = parseTagBody(body);
|
|
1070
|
+
if (tag.length === 0) {
|
|
1071
|
+
continue;
|
|
1072
|
+
}
|
|
1073
|
+
const element = { tag, attrs, children: [] };
|
|
1074
|
+
const parent = stack[stack.length - 1];
|
|
1075
|
+
if (parent) {
|
|
1076
|
+
parent.children.push(element);
|
|
1077
|
+
}
|
|
1078
|
+
if (root === null) {
|
|
1079
|
+
root = element;
|
|
1080
|
+
}
|
|
1081
|
+
if (!selfClose) {
|
|
1082
|
+
stack.push(element);
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
return root;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
// src/selector/native.ts
|
|
1089
|
+
function unquoteSelectorValue(value) {
|
|
1090
|
+
const trimmed = value.trim();
|
|
1091
|
+
const first = trimmed[0];
|
|
1092
|
+
if ((first === '"' || first === "'") && trimmed.endsWith(first) && trimmed.length >= 2) {
|
|
1093
|
+
return trimmed.slice(1, -1);
|
|
1094
|
+
}
|
|
1095
|
+
return trimmed;
|
|
1096
|
+
}
|
|
1097
|
+
function quoteSelectorValue(value) {
|
|
1098
|
+
return `"${value}"`;
|
|
1099
|
+
}
|
|
1100
|
+
var ROLE_SELECTOR_RE = /^role=([A-Za-z][\w.$-]*)(?:\[name=(.+)\])?$/s;
|
|
1101
|
+
var NATIVE_SELECTOR_PREFIX_RE = /^(id|label|text|role)=/;
|
|
1102
|
+
function invalidSelectorMessage(selector, reason) {
|
|
1103
|
+
return `Invalid native selector ${JSON.stringify(selector)}: ${reason}.`;
|
|
1104
|
+
}
|
|
1105
|
+
function parseNativeSelector(selector) {
|
|
1106
|
+
const trimmed = selector.trim();
|
|
1107
|
+
if (trimmed.length === 0) {
|
|
1108
|
+
throw new Error(
|
|
1109
|
+
invalidSelectorMessage(
|
|
1110
|
+
selector,
|
|
1111
|
+
"selector is empty; use id=, label=, text=, role=, :focus, or a bare text value"
|
|
1112
|
+
)
|
|
1113
|
+
);
|
|
1114
|
+
}
|
|
1115
|
+
if (trimmed === ":focus") {
|
|
1116
|
+
return { kind: "focused" };
|
|
1117
|
+
}
|
|
1118
|
+
const idMatch = /^id=(.+)$/s.exec(trimmed);
|
|
1119
|
+
if (idMatch) {
|
|
1120
|
+
return { kind: "id", value: unquoteSelectorValue(idMatch[1]) };
|
|
1121
|
+
}
|
|
1122
|
+
const roleMatch = ROLE_SELECTOR_RE.exec(trimmed);
|
|
1123
|
+
if (roleMatch) {
|
|
1124
|
+
const name = roleMatch[2] !== void 0 ? unquoteSelectorValue(roleMatch[2]) : void 0;
|
|
1125
|
+
return name !== void 0 && name.length > 0 ? { kind: "role", role: roleMatch[1], name } : { kind: "role", role: roleMatch[1] };
|
|
1126
|
+
}
|
|
1127
|
+
const labelMatch = /^label=(.+)$/s.exec(trimmed);
|
|
1128
|
+
if (labelMatch) {
|
|
1129
|
+
return { kind: "label", value: unquoteSelectorValue(labelMatch[1]) };
|
|
1130
|
+
}
|
|
1131
|
+
const textMatch = /^text=(.+)$/s.exec(trimmed);
|
|
1132
|
+
if (textMatch) {
|
|
1133
|
+
return { kind: "text", value: unquoteSelectorValue(textMatch[1]) };
|
|
1134
|
+
}
|
|
1135
|
+
const prefix = NATIVE_SELECTOR_PREFIX_RE.exec(trimmed);
|
|
1136
|
+
if (prefix) {
|
|
1137
|
+
throw new Error(
|
|
1138
|
+
invalidSelectorMessage(
|
|
1139
|
+
selector,
|
|
1140
|
+
`malformed ${prefix[1]}= selector; expected id=<value>, label=<value>, text=<value>, or role=<Type>[name=<value>]`
|
|
1141
|
+
)
|
|
1142
|
+
);
|
|
1143
|
+
}
|
|
1144
|
+
return { kind: "text", value: trimmed };
|
|
1145
|
+
}
|
|
1146
|
+
function unwrapNativeTextSelector(selector) {
|
|
1147
|
+
const trimmed = selector.trim();
|
|
1148
|
+
if (!trimmed.startsWith("text=")) {
|
|
1149
|
+
return null;
|
|
1150
|
+
}
|
|
1151
|
+
return unquoteSelectorValue(trimmed.slice("text=".length));
|
|
1152
|
+
}
|
|
1153
|
+
function qualifyResourceId(value, appPackage) {
|
|
1154
|
+
if (value.includes(":") || !appPackage) {
|
|
1155
|
+
return value;
|
|
1156
|
+
}
|
|
1157
|
+
return `${appPackage}:id/${value}`;
|
|
1158
|
+
}
|
|
1159
|
+
function normalizeXcuiClassName(role) {
|
|
1160
|
+
return role.startsWith("XCUIElementType") ? role : `XCUIElementType${role}`;
|
|
1161
|
+
}
|
|
1162
|
+
function shortIosType(type) {
|
|
1163
|
+
return type.startsWith("XCUIElementType") ? type.slice("XCUIElementType".length) : type;
|
|
1164
|
+
}
|
|
1165
|
+
function rankNativeSelectors(fields) {
|
|
1166
|
+
const selectors = [];
|
|
1167
|
+
if (fields.id) {
|
|
1168
|
+
selectors.push(`id=${fields.id}`);
|
|
1169
|
+
}
|
|
1170
|
+
if (fields.label) {
|
|
1171
|
+
selectors.push(`label=${quoteSelectorValue(fields.label)}`);
|
|
1172
|
+
}
|
|
1173
|
+
if (fields.role && fields.name) {
|
|
1174
|
+
selectors.push(`role=${fields.role}[name=${quoteSelectorValue(fields.name)}]`);
|
|
1175
|
+
}
|
|
1176
|
+
if (fields.name) {
|
|
1177
|
+
selectors.push(`text=${quoteSelectorValue(fields.name)}`);
|
|
1178
|
+
}
|
|
1179
|
+
if (selectors.length === 0 && fields.role) {
|
|
1180
|
+
selectors.push(`role=${fields.role}`);
|
|
1181
|
+
}
|
|
1182
|
+
return selectors;
|
|
1183
|
+
}
|
|
1184
|
+
var NATIVE_ATTRIBUTE_MAP = {
|
|
1185
|
+
android: {
|
|
1186
|
+
id: { attribute: "resource-id", match: "exact (package-qualified)" },
|
|
1187
|
+
label: { attribute: "content-desc", match: "exact" },
|
|
1188
|
+
text: { attribute: "text", match: "substring" },
|
|
1189
|
+
role: { attribute: "class", match: "exact" }
|
|
1190
|
+
},
|
|
1191
|
+
ios: {
|
|
1192
|
+
id: { attribute: "accessibility id (name)", match: "exact" },
|
|
1193
|
+
label: { attribute: "label", match: "exact" },
|
|
1194
|
+
text: { attribute: "label | value", match: "substring" },
|
|
1195
|
+
role: { attribute: "type (XCUIElementType\u2026)", match: "exact" }
|
|
1196
|
+
},
|
|
1197
|
+
macos: {
|
|
1198
|
+
id: { attribute: "AXIdentifier", match: "exact" },
|
|
1199
|
+
label: { attribute: "title | description", match: "exact" },
|
|
1200
|
+
text: { attribute: "title | description | value", match: "substring" },
|
|
1201
|
+
role: { attribute: "AXRole", match: "exact" }
|
|
1202
|
+
}
|
|
1203
|
+
};
|
|
1204
|
+
var ANDROID_MATCH_DIALECT = {
|
|
1205
|
+
platform: "android",
|
|
1206
|
+
normalizeRole: (role) => role,
|
|
1207
|
+
normalizeId: (value, options) => qualifyResourceId(value, options.appPackage)
|
|
1208
|
+
};
|
|
1209
|
+
var IOS_MATCH_DIALECT = {
|
|
1210
|
+
platform: "ios",
|
|
1211
|
+
normalizeRole: (role) => normalizeXcuiClassName(role),
|
|
1212
|
+
normalizeId: (value) => value
|
|
1213
|
+
};
|
|
1214
|
+
var MACOS_MATCH_DIALECT = {
|
|
1215
|
+
platform: "macos",
|
|
1216
|
+
normalizeRole: (role) => role,
|
|
1217
|
+
normalizeId: (value) => value
|
|
1218
|
+
};
|
|
1219
|
+
function nodeMatchesSelector(dialect, selector, node, options = {}) {
|
|
1220
|
+
switch (selector.kind) {
|
|
1221
|
+
case "id":
|
|
1222
|
+
return node.id !== void 0 && node.id === dialect.normalizeId(selector.value, options);
|
|
1223
|
+
case "label":
|
|
1224
|
+
return node.label !== void 0 && node.label === selector.value;
|
|
1225
|
+
case "text":
|
|
1226
|
+
return node.textValues.some((t) => t.includes(selector.value));
|
|
1227
|
+
case "focused":
|
|
1228
|
+
return node.focused === true;
|
|
1229
|
+
case "role": {
|
|
1230
|
+
if (node.role === void 0) {
|
|
1231
|
+
return false;
|
|
1232
|
+
}
|
|
1233
|
+
if (dialect.normalizeRole(node.role) !== dialect.normalizeRole(selector.role)) {
|
|
1234
|
+
return false;
|
|
1235
|
+
}
|
|
1236
|
+
if (selector.name === void 0 || selector.name.length === 0) {
|
|
1237
|
+
return true;
|
|
1238
|
+
}
|
|
1239
|
+
const name = selector.name;
|
|
1240
|
+
return node.textValues.some((t) => t.includes(name));
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
function matchNativeTree(dialect, selector, root, project, children, options = {}) {
|
|
1245
|
+
const out = [];
|
|
1246
|
+
const visit = (node) => {
|
|
1247
|
+
if (nodeMatchesSelector(dialect, selector, project(node), options)) {
|
|
1248
|
+
out.push(node);
|
|
1249
|
+
}
|
|
1250
|
+
for (const child of children(node)) {
|
|
1251
|
+
visit(child);
|
|
1252
|
+
}
|
|
1253
|
+
};
|
|
1254
|
+
visit(root);
|
|
1255
|
+
return out;
|
|
1256
|
+
}
|
|
1257
|
+
function parseSnapshot(xml) {
|
|
1258
|
+
return parseXml(xml);
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
// src/browser/android-driver.ts
|
|
575
1262
|
var ANDROID_CAPABILITIES = /* @__PURE__ */ new Set([
|
|
576
1263
|
"query",
|
|
577
1264
|
"interact",
|
|
@@ -605,50 +1292,29 @@ var ANDROID_KEYCODES = {
|
|
|
605
1292
|
pageup: 92,
|
|
606
1293
|
pagedown: 93
|
|
607
1294
|
};
|
|
608
|
-
function unquote2(value) {
|
|
609
|
-
const trimmed = value.trim();
|
|
610
|
-
const first = trimmed[0];
|
|
611
|
-
if ((first === '"' || first === "'") && trimmed.endsWith(first) && trimmed.length >= 2) {
|
|
612
|
-
return trimmed.slice(1, -1);
|
|
613
|
-
}
|
|
614
|
-
return trimmed;
|
|
615
|
-
}
|
|
616
1295
|
function escapeUiSelectorArg(value) {
|
|
617
1296
|
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
618
1297
|
}
|
|
619
|
-
function
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
return name !== void 0 && name.length > 0 ? { by: "role", role: roleMatch[1], name } : { by: "role", role: roleMatch[1] };
|
|
632
|
-
}
|
|
633
|
-
const labelMatch = /^label=(.+)$/s.exec(trimmed);
|
|
634
|
-
if (labelMatch) {
|
|
635
|
-
return { by: "accessibilityId", value: unquote2(labelMatch[1]) };
|
|
636
|
-
}
|
|
637
|
-
const textMatch = /^text=(.+)$/s.exec(trimmed);
|
|
638
|
-
if (textMatch) {
|
|
639
|
-
return { by: "text", value: unquote2(textMatch[1]) };
|
|
1298
|
+
function toAndroidQuery(selector) {
|
|
1299
|
+
switch (selector.kind) {
|
|
1300
|
+
case "focused":
|
|
1301
|
+
return { by: "focused" };
|
|
1302
|
+
case "id":
|
|
1303
|
+
return { by: "id", value: selector.value };
|
|
1304
|
+
case "role":
|
|
1305
|
+
return selector.name !== void 0 ? { by: "role", role: selector.role, name: selector.name } : { by: "role", role: selector.role };
|
|
1306
|
+
case "label":
|
|
1307
|
+
return { by: "accessibilityId", value: selector.value };
|
|
1308
|
+
case "text":
|
|
1309
|
+
return { by: "text", value: selector.value };
|
|
640
1310
|
}
|
|
641
|
-
|
|
1311
|
+
}
|
|
1312
|
+
function parseAndroidSelector(selector) {
|
|
1313
|
+
return toAndroidQuery(parseNativeSelector(selector));
|
|
642
1314
|
}
|
|
643
1315
|
function locator(strategy, selector) {
|
|
644
1316
|
return { strategy, selector, context: "" };
|
|
645
1317
|
}
|
|
646
|
-
function qualifyResourceId(value, appPackage) {
|
|
647
|
-
if (value.includes(":") || !appPackage) {
|
|
648
|
-
return value;
|
|
649
|
-
}
|
|
650
|
-
return `${appPackage}:id/${value}`;
|
|
651
|
-
}
|
|
652
1318
|
function androidQueryToLocator(query, options = {}) {
|
|
653
1319
|
switch (query.by) {
|
|
654
1320
|
case "id":
|
|
@@ -675,11 +1341,7 @@ function androidQueryToLocator(query, options = {}) {
|
|
|
675
1341
|
}
|
|
676
1342
|
}
|
|
677
1343
|
function unwrapAndroidTextSelector(selector) {
|
|
678
|
-
|
|
679
|
-
if (!trimmed.startsWith("text=")) {
|
|
680
|
-
return null;
|
|
681
|
-
}
|
|
682
|
-
return unquote2(trimmed.slice(5));
|
|
1344
|
+
return unwrapNativeTextSelector(selector);
|
|
683
1345
|
}
|
|
684
1346
|
function keyCodeFor(key) {
|
|
685
1347
|
const code = ANDROID_KEYCODES[key.trim().toLowerCase()];
|
|
@@ -805,7 +1467,7 @@ function createAndroidDriver(client, options = {}) {
|
|
|
805
1467
|
},
|
|
806
1468
|
async screenshot(screenshotOptions) {
|
|
807
1469
|
const png = await client.screenshotPng();
|
|
808
|
-
|
|
1470
|
+
import_node_fs3.default.writeFileSync(screenshotOptions.path, png);
|
|
809
1471
|
},
|
|
810
1472
|
// network / dialogs / downloads (all web-only) -------------------------
|
|
811
1473
|
onResponse(_handler) {
|
|
@@ -831,14 +1493,14 @@ function createAndroidDriver(client, options = {}) {
|
|
|
831
1493
|
|
|
832
1494
|
// src/browser/android-helper.ts
|
|
833
1495
|
var import_node_module = require("module");
|
|
834
|
-
var
|
|
835
|
-
var
|
|
836
|
-
var
|
|
1496
|
+
var import_node_child_process5 = require("child_process");
|
|
1497
|
+
var import_node_fs5 = __toESM(require("fs"), 1);
|
|
1498
|
+
var import_node_path5 = __toESM(require("path"), 1);
|
|
837
1499
|
|
|
838
1500
|
// src/browser/android-adb.ts
|
|
839
|
-
var
|
|
1501
|
+
var import_node_child_process3 = require("child_process");
|
|
840
1502
|
var execFileAdbRunner = (args, options) => new Promise((resolve) => {
|
|
841
|
-
(0,
|
|
1503
|
+
(0, import_node_child_process3.execFile)(
|
|
842
1504
|
"adb",
|
|
843
1505
|
args,
|
|
844
1506
|
{ encoding: "utf-8", timeout: options?.timeoutMs, maxBuffer: 16 * 1024 * 1024 },
|
|
@@ -854,7 +1516,7 @@ var execFileAdbRunner = (args, options) => new Promise((resolve) => {
|
|
|
854
1516
|
);
|
|
855
1517
|
});
|
|
856
1518
|
var spawnAdbProcess = (args) => {
|
|
857
|
-
const child = (0,
|
|
1519
|
+
const child = (0, import_node_child_process3.spawn)("adb", args, { stdio: "ignore" });
|
|
858
1520
|
child.on("error", () => {
|
|
859
1521
|
});
|
|
860
1522
|
return { kill: () => child.kill() };
|
|
@@ -960,22 +1622,63 @@ async function installApk(runner, serial, apkPath) {
|
|
|
960
1622
|
);
|
|
961
1623
|
}
|
|
962
1624
|
}
|
|
1625
|
+
function formatUnknownError(error) {
|
|
1626
|
+
return error instanceof Error ? error.message : String(error);
|
|
1627
|
+
}
|
|
1628
|
+
function runnerPhaseError(message, error) {
|
|
1629
|
+
return new Error(`${message}: ${formatUnknownError(error)}`, { cause: error });
|
|
1630
|
+
}
|
|
1631
|
+
function parseResolvedComponent(stdout, pkg) {
|
|
1632
|
+
const lines = stdout.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
|
|
1633
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
1634
|
+
if (/^[\w.]+\/[\w.$]+$/.test(lines[i]) && lines[i].startsWith(`${pkg}/`)) {
|
|
1635
|
+
return lines[i];
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
return null;
|
|
1639
|
+
}
|
|
963
1640
|
async function launchPackage(runner, serial, pkg) {
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
1641
|
+
let resolved;
|
|
1642
|
+
try {
|
|
1643
|
+
resolved = await runner(
|
|
1644
|
+
withSerial(serial, [
|
|
1645
|
+
"shell",
|
|
1646
|
+
"cmd",
|
|
1647
|
+
"package",
|
|
1648
|
+
"resolve-activity",
|
|
1649
|
+
"--brief",
|
|
1650
|
+
"-c",
|
|
1651
|
+
"android.intent.category.LAUNCHER",
|
|
1652
|
+
pkg
|
|
1653
|
+
]),
|
|
1654
|
+
{ timeoutMs: 3e4 }
|
|
1655
|
+
);
|
|
1656
|
+
} catch (error) {
|
|
1657
|
+
throw runnerPhaseError(
|
|
1658
|
+
`Failed to resolve the launcher activity for Android package "${pkg}" via adb`,
|
|
1659
|
+
error
|
|
1660
|
+
);
|
|
1661
|
+
}
|
|
1662
|
+
const component = parseResolvedComponent(resolved.stdout, pkg);
|
|
1663
|
+
if (resolved.code !== 0 || !component) {
|
|
1664
|
+
throw new Error(
|
|
1665
|
+
`Could not resolve a launcher activity for Android package "${pkg}" (exit ${resolved.code})${(resolved.stdout + resolved.stderr).trim() ? `: ${(resolved.stdout + resolved.stderr).trim().slice(0, 300)}` : ""}. Is it installed?`
|
|
1666
|
+
);
|
|
1667
|
+
}
|
|
1668
|
+
let start;
|
|
1669
|
+
try {
|
|
1670
|
+
start = await runner(withSerial(serial, ["shell", "am", "start", "-n", component]), {
|
|
1671
|
+
timeoutMs: 3e4
|
|
1672
|
+
});
|
|
1673
|
+
} catch (error) {
|
|
1674
|
+
throw runnerPhaseError(
|
|
1675
|
+
`Failed to launch Android package "${pkg}" with \`am start\` (${component}) via adb`,
|
|
1676
|
+
error
|
|
1677
|
+
);
|
|
1678
|
+
}
|
|
1679
|
+
if (start.code !== 0 || /error|does not exist|cannot start/i.test(start.stdout + start.stderr)) {
|
|
977
1680
|
throw new Error(
|
|
978
|
-
`Failed to launch Android package "${pkg}" (exit ${
|
|
1681
|
+
`Failed to launch Android package "${pkg}" (${component}, exit ${start.code}): ${(start.stdout + start.stderr).trim().slice(0, 400)}.`
|
|
979
1682
|
);
|
|
980
1683
|
}
|
|
981
1684
|
}
|
|
@@ -1037,12 +1740,12 @@ var Uia2Transport = class {
|
|
|
1037
1740
|
* {@link Uia2HttpError} on a non-2xx response, or a timeout error when the
|
|
1038
1741
|
* per-request deadline elapses.
|
|
1039
1742
|
*/
|
|
1040
|
-
async request(method,
|
|
1743
|
+
async request(method, path20, body, timeoutMs) {
|
|
1041
1744
|
const requestTimeoutMs = timeoutMs ?? this.requestTimeoutMs;
|
|
1042
1745
|
const controller = new AbortController();
|
|
1043
1746
|
const timer = setTimeout(() => controller.abort(), requestTimeoutMs);
|
|
1044
1747
|
timer.unref?.();
|
|
1045
|
-
const url = `${this.baseUrl}${
|
|
1748
|
+
const url = `${this.baseUrl}${path20}`;
|
|
1046
1749
|
let response;
|
|
1047
1750
|
try {
|
|
1048
1751
|
response = await this.fetchImpl(url, {
|
|
@@ -1054,7 +1757,7 @@ var Uia2Transport = class {
|
|
|
1054
1757
|
} catch (error) {
|
|
1055
1758
|
if (controller.signal.aborted) {
|
|
1056
1759
|
const shown = requestTimeoutMs >= 1e3 ? `${Math.round(requestTimeoutMs / 1e3)}s` : `${requestTimeoutMs}ms`;
|
|
1057
|
-
throw new Error(`uiautomator2 request ${method} ${
|
|
1760
|
+
throw new Error(`uiautomator2 request ${method} ${path20} timed out after ${shown}`);
|
|
1058
1761
|
}
|
|
1059
1762
|
throw error instanceof Error ? error : new Error(String(error));
|
|
1060
1763
|
} finally {
|
|
@@ -1065,7 +1768,7 @@ var Uia2Transport = class {
|
|
|
1065
1768
|
if (!response.ok) {
|
|
1066
1769
|
const wdError = extractWebdriverError(parsed);
|
|
1067
1770
|
throw new Uia2HttpError(
|
|
1068
|
-
`uiautomator2 ${method} ${
|
|
1771
|
+
`uiautomator2 ${method} ${path20} failed (${response.status})${wdError ? `: ${wdError}` : ""}`,
|
|
1069
1772
|
response.status,
|
|
1070
1773
|
wdError
|
|
1071
1774
|
);
|
|
@@ -1148,10 +1851,10 @@ function sleep(ms) {
|
|
|
1148
1851
|
}
|
|
1149
1852
|
function createUia2AgentClient(transport, sessionId, options = {}) {
|
|
1150
1853
|
const base = `/session/${sessionId}`;
|
|
1151
|
-
async function locate(query,
|
|
1854
|
+
async function locate(query, path20) {
|
|
1152
1855
|
return transport.request(
|
|
1153
1856
|
"POST",
|
|
1154
|
-
`${base}${
|
|
1857
|
+
`${base}${path20}`,
|
|
1155
1858
|
androidQueryToLocator(query, { appPackage: options.appPackage })
|
|
1156
1859
|
);
|
|
1157
1860
|
}
|
|
@@ -1193,6 +1896,13 @@ function createUia2AgentClient(transport, sessionId, options = {}) {
|
|
|
1193
1896
|
}
|
|
1194
1897
|
return Buffer.from(value, "base64");
|
|
1195
1898
|
},
|
|
1899
|
+
async source() {
|
|
1900
|
+
const value = await transport.request("GET", `${base}/source`);
|
|
1901
|
+
if (typeof value !== "string") {
|
|
1902
|
+
throw new Error("uiautomator2 /source did not return XML text; cannot analyze Android UI hierarchy");
|
|
1903
|
+
}
|
|
1904
|
+
return value;
|
|
1905
|
+
},
|
|
1196
1906
|
async close() {
|
|
1197
1907
|
await transport.request("DELETE", base).catch(() => void 0);
|
|
1198
1908
|
}
|
|
@@ -1200,9 +1910,9 @@ function createUia2AgentClient(transport, sessionId, options = {}) {
|
|
|
1200
1910
|
}
|
|
1201
1911
|
|
|
1202
1912
|
// src/config/target.ts
|
|
1203
|
-
var
|
|
1204
|
-
var
|
|
1205
|
-
var
|
|
1913
|
+
var import_node_child_process4 = require("child_process");
|
|
1914
|
+
var import_node_fs4 = __toESM(require("fs"), 1);
|
|
1915
|
+
var import_node_path4 = __toESM(require("path"), 1);
|
|
1206
1916
|
var WEB_ONLY_STEP_TYPES = /* @__PURE__ */ new Set([
|
|
1207
1917
|
"navigate",
|
|
1208
1918
|
"waitForUrl",
|
|
@@ -1287,7 +1997,7 @@ function looksLikeIosAppPath(app) {
|
|
|
1287
1997
|
}
|
|
1288
1998
|
if (trimmed.toLowerCase().endsWith(".app")) {
|
|
1289
1999
|
try {
|
|
1290
|
-
return
|
|
2000
|
+
return import_node_fs4.default.statSync(normalizeAppPath(app)).isDirectory();
|
|
1291
2001
|
} catch {
|
|
1292
2002
|
return false;
|
|
1293
2003
|
}
|
|
@@ -1295,29 +2005,29 @@ function looksLikeIosAppPath(app) {
|
|
|
1295
2005
|
return false;
|
|
1296
2006
|
}
|
|
1297
2007
|
function normalizeAppPath(app) {
|
|
1298
|
-
return
|
|
2008
|
+
return import_node_path4.default.resolve(trimTrailingPathSeparators(app));
|
|
1299
2009
|
}
|
|
1300
2010
|
function parseBundleIdentifier(plist) {
|
|
1301
2011
|
const match = /<key>\s*CFBundleIdentifier\s*<\/key>\s*<string>\s*([^<]+?)\s*<\/string>/s.exec(plist);
|
|
1302
2012
|
return match?.[1]?.trim() || null;
|
|
1303
2013
|
}
|
|
1304
2014
|
function readBundleIdentifier(appPath, ...plistSubPath) {
|
|
1305
|
-
const infoPlistPath =
|
|
1306
|
-
if (!
|
|
2015
|
+
const infoPlistPath = import_node_path4.default.join(normalizeAppPath(appPath), ...plistSubPath);
|
|
2016
|
+
if (!import_node_fs4.default.existsSync(infoPlistPath)) {
|
|
1307
2017
|
return null;
|
|
1308
2018
|
}
|
|
1309
2019
|
try {
|
|
1310
|
-
const parsed = parseBundleIdentifier(
|
|
2020
|
+
const parsed = parseBundleIdentifier(import_node_fs4.default.readFileSync(infoPlistPath, "utf-8"));
|
|
1311
2021
|
if (parsed) {
|
|
1312
2022
|
return parsed;
|
|
1313
2023
|
}
|
|
1314
2024
|
} catch {
|
|
1315
2025
|
}
|
|
1316
|
-
if (process.platform !== "darwin" || !
|
|
2026
|
+
if (process.platform !== "darwin" || !import_node_fs4.default.existsSync("/usr/bin/plutil")) {
|
|
1317
2027
|
return null;
|
|
1318
2028
|
}
|
|
1319
2029
|
try {
|
|
1320
|
-
const output = (0,
|
|
2030
|
+
const output = (0, import_node_child_process4.execFileSync)(
|
|
1321
2031
|
"/usr/bin/plutil",
|
|
1322
2032
|
["-extract", "CFBundleIdentifier", "raw", "-o", "-", infoPlistPath],
|
|
1323
2033
|
{ encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"], timeout: 1e3 }
|
|
@@ -1339,7 +2049,7 @@ function appBundleAllowedIdentities(app, readBundleId, isAppPath = looksLikeAppB
|
|
|
1339
2049
|
const normalizedPath = normalizeAppPath(app);
|
|
1340
2050
|
identities.add(trimTrailingPathSeparators(app));
|
|
1341
2051
|
identities.add(normalizedPath);
|
|
1342
|
-
const bundleName =
|
|
2052
|
+
const bundleName = import_node_path4.default.basename(normalizedPath).replace(/\.app$/i, "");
|
|
1343
2053
|
if (bundleName) {
|
|
1344
2054
|
identities.add(bundleName);
|
|
1345
2055
|
}
|
|
@@ -1367,9 +2077,9 @@ function looksLikeApkPath(app) {
|
|
|
1367
2077
|
return trimmed.includes("/") || trimmed.includes("\\") || trimmed.toLowerCase().endsWith(".apk");
|
|
1368
2078
|
}
|
|
1369
2079
|
function normalizeAndroidApkPath(app) {
|
|
1370
|
-
const resolved =
|
|
2080
|
+
const resolved = import_node_path4.default.resolve(trimTrailingPathSeparators(app));
|
|
1371
2081
|
try {
|
|
1372
|
-
return
|
|
2082
|
+
return import_node_fs4.default.realpathSync.native(resolved);
|
|
1373
2083
|
} catch {
|
|
1374
2084
|
return resolved;
|
|
1375
2085
|
}
|
|
@@ -1412,12 +2122,12 @@ function resolveAgentApks(requireFn = (0, import_node_module.createRequire)(impo
|
|
|
1412
2122
|
"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"
|
|
1413
2123
|
);
|
|
1414
2124
|
}
|
|
1415
|
-
const pkgDir =
|
|
2125
|
+
const pkgDir = import_node_path5.default.dirname(pkgJsonPath);
|
|
1416
2126
|
const version = requireFn(pkgJsonPath).version;
|
|
1417
|
-
const serverApk =
|
|
1418
|
-
const testApk =
|
|
2127
|
+
const serverApk = import_node_path5.default.join(pkgDir, "apks", `appium-uiautomator2-server-v${version}.apk`);
|
|
2128
|
+
const testApk = import_node_path5.default.join(pkgDir, "apks", "appium-uiautomator2-server-debug-androidTest.apk");
|
|
1419
2129
|
for (const apk of [serverApk, testApk]) {
|
|
1420
|
-
if (!
|
|
2130
|
+
if (!import_node_fs5.default.existsSync(apk)) {
|
|
1421
2131
|
throw new Error(`Expected uiautomator2 agent APK is missing: ${apk}. Reinstall dependencies.`);
|
|
1422
2132
|
}
|
|
1423
2133
|
}
|
|
@@ -1429,7 +2139,7 @@ function looksLikeApk(app) {
|
|
|
1429
2139
|
var execFileAaptResolver = async (apkPath) => {
|
|
1430
2140
|
for (const tool of ["aapt", "aapt2"]) {
|
|
1431
2141
|
const output = await new Promise((resolve) => {
|
|
1432
|
-
(0,
|
|
2142
|
+
(0, import_node_child_process5.execFile)(
|
|
1433
2143
|
tool,
|
|
1434
2144
|
["dump", "badging", apkPath],
|
|
1435
2145
|
{ encoding: "utf-8", timeout: 2e4, maxBuffer: 8 * 1024 * 1024 },
|
|
@@ -1463,8 +2173,8 @@ async function resolvePackage(app, runner, serial, aaptResolver, allowedApps) {
|
|
|
1463
2173
|
assertAndroidAppAllowed(allowedApps, app);
|
|
1464
2174
|
return app;
|
|
1465
2175
|
}
|
|
1466
|
-
const apkPath =
|
|
1467
|
-
if (!
|
|
2176
|
+
const apkPath = import_node_path5.default.resolve(app);
|
|
2177
|
+
if (!import_node_fs5.default.existsSync(apkPath)) {
|
|
1468
2178
|
throw new Error(`APK not found: ${apkPath}`);
|
|
1469
2179
|
}
|
|
1470
2180
|
const pkg = await aaptResolver(apkPath);
|
|
@@ -1545,43 +2255,25 @@ var IOS_CAPABILITIES = /* @__PURE__ */ new Set([
|
|
|
1545
2255
|
var WAIT_POLL_INTERVAL_MS2 = 250;
|
|
1546
2256
|
var DEFAULT_WAIT_TIMEOUT_MS2 = 5e3;
|
|
1547
2257
|
var IOS_PRESS_KEYS = ["backspace", "del", "delete", "enter", "home", "return"];
|
|
1548
|
-
function unquote3(value) {
|
|
1549
|
-
const trimmed = value.trim();
|
|
1550
|
-
const first = trimmed[0];
|
|
1551
|
-
if ((first === '"' || first === "'") && trimmed.endsWith(first) && trimmed.length >= 2) {
|
|
1552
|
-
return trimmed.slice(1, -1);
|
|
1553
|
-
}
|
|
1554
|
-
return trimmed;
|
|
1555
|
-
}
|
|
1556
2258
|
function escapePredicateArg(value) {
|
|
1557
2259
|
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
1558
2260
|
}
|
|
1559
|
-
function
|
|
1560
|
-
|
|
2261
|
+
function toIosQuery(selector) {
|
|
2262
|
+
switch (selector.kind) {
|
|
2263
|
+
case "focused":
|
|
2264
|
+
return { by: "focused" };
|
|
2265
|
+
case "id":
|
|
2266
|
+
return { by: "accessibilityId", value: selector.value };
|
|
2267
|
+
case "role":
|
|
2268
|
+
return selector.name !== void 0 ? { by: "role", role: selector.role, name: selector.name } : { by: "role", role: selector.role };
|
|
2269
|
+
case "label":
|
|
2270
|
+
return { by: "label", value: selector.value };
|
|
2271
|
+
case "text":
|
|
2272
|
+
return { by: "text", value: selector.value };
|
|
2273
|
+
}
|
|
1561
2274
|
}
|
|
1562
2275
|
function parseIosSelector(selector) {
|
|
1563
|
-
|
|
1564
|
-
if (trimmed === ":focus") {
|
|
1565
|
-
return { by: "focused" };
|
|
1566
|
-
}
|
|
1567
|
-
const idMatch = /^id=(.+)$/s.exec(trimmed);
|
|
1568
|
-
if (idMatch) {
|
|
1569
|
-
return { by: "accessibilityId", value: unquote3(idMatch[1]) };
|
|
1570
|
-
}
|
|
1571
|
-
const roleMatch = /^role=([A-Za-z][\w.$-]*)(?:\[name=(.+)\])?$/s.exec(trimmed);
|
|
1572
|
-
if (roleMatch) {
|
|
1573
|
-
const name = roleMatch[2] !== void 0 ? unquote3(roleMatch[2]) : void 0;
|
|
1574
|
-
return name !== void 0 && name.length > 0 ? { by: "role", role: roleMatch[1], name } : { by: "role", role: roleMatch[1] };
|
|
1575
|
-
}
|
|
1576
|
-
const labelMatch = /^label=(.+)$/s.exec(trimmed);
|
|
1577
|
-
if (labelMatch) {
|
|
1578
|
-
return { by: "label", value: unquote3(labelMatch[1]) };
|
|
1579
|
-
}
|
|
1580
|
-
const textMatch = /^text=(.+)$/s.exec(trimmed);
|
|
1581
|
-
if (textMatch) {
|
|
1582
|
-
return { by: "text", value: unquote3(textMatch[1]) };
|
|
1583
|
-
}
|
|
1584
|
-
return { by: "text", value: trimmed };
|
|
2276
|
+
return toIosQuery(parseNativeSelector(selector));
|
|
1585
2277
|
}
|
|
1586
2278
|
function iosQueryToLocator(query) {
|
|
1587
2279
|
switch (query.by) {
|
|
@@ -1613,11 +2305,7 @@ function iosQueryToLocator(query) {
|
|
|
1613
2305
|
}
|
|
1614
2306
|
}
|
|
1615
2307
|
function unwrapIosTextSelector(selector) {
|
|
1616
|
-
|
|
1617
|
-
if (!trimmed.startsWith("text=")) {
|
|
1618
|
-
return null;
|
|
1619
|
-
}
|
|
1620
|
-
return unquote3(trimmed.slice(5));
|
|
2308
|
+
return unwrapNativeTextSelector(selector);
|
|
1621
2309
|
}
|
|
1622
2310
|
function delay2(ms) {
|
|
1623
2311
|
return new Promise((resolve) => {
|
|
@@ -1777,20 +2465,34 @@ function createIosDriver(client, options) {
|
|
|
1777
2465
|
|
|
1778
2466
|
// src/browser/ios-helper.ts
|
|
1779
2467
|
var import_node_module2 = require("module");
|
|
1780
|
-
var
|
|
1781
|
-
var
|
|
1782
|
-
var
|
|
2468
|
+
var import_node_child_process7 = require("child_process");
|
|
2469
|
+
var import_node_fs6 = __toESM(require("fs"), 1);
|
|
2470
|
+
var import_node_os5 = __toESM(require("os"), 1);
|
|
2471
|
+
var import_node_path7 = __toESM(require("path"), 1);
|
|
1783
2472
|
|
|
1784
2473
|
// src/browser/ios-simctl.ts
|
|
1785
|
-
var
|
|
2474
|
+
var import_node_child_process6 = require("child_process");
|
|
1786
2475
|
var import_promises = require("fs/promises");
|
|
1787
2476
|
var import_node_net = __toESM(require("net"), 1);
|
|
1788
|
-
var
|
|
1789
|
-
var
|
|
1790
|
-
var
|
|
2477
|
+
var import_node_os4 = __toESM(require("os"), 1);
|
|
2478
|
+
var import_node_path6 = __toESM(require("path"), 1);
|
|
2479
|
+
var spawnXcrunProcess = (args, options) => {
|
|
2480
|
+
const child = (0, import_node_child_process6.spawn)("xcrun", args, {
|
|
2481
|
+
stdio: "ignore",
|
|
2482
|
+
env: options?.env ? { ...process.env, ...options.env } : process.env
|
|
2483
|
+
});
|
|
2484
|
+
child.on("error", () => {
|
|
2485
|
+
});
|
|
2486
|
+
return {
|
|
2487
|
+
kill: () => {
|
|
2488
|
+
child.kill();
|
|
2489
|
+
}
|
|
2490
|
+
};
|
|
2491
|
+
};
|
|
2492
|
+
var DEFAULT_SIMULATOR_LOCK_ROOT = import_node_path6.default.join(import_node_os4.default.tmpdir(), "prowl-ios-simulator-locks");
|
|
1791
2493
|
var SIMULATOR_LOCK_OWNER_FILE = "owner.json";
|
|
1792
2494
|
var execFileXcrunRunner = (args, options) => new Promise((resolve) => {
|
|
1793
|
-
(0,
|
|
2495
|
+
(0, import_node_child_process6.execFile)(
|
|
1794
2496
|
"xcrun",
|
|
1795
2497
|
args,
|
|
1796
2498
|
{
|
|
@@ -1827,7 +2529,7 @@ function isProcessAlive(pid) {
|
|
|
1827
2529
|
}
|
|
1828
2530
|
async function removeStaleSimulatorLock(lockPath) {
|
|
1829
2531
|
try {
|
|
1830
|
-
const ownerText = await (0, import_promises.readFile)(
|
|
2532
|
+
const ownerText = await (0, import_promises.readFile)(import_node_path6.default.join(lockPath, SIMULATOR_LOCK_OWNER_FILE), "utf8");
|
|
1831
2533
|
const owner = JSON.parse(ownerText);
|
|
1832
2534
|
if (typeof owner.pid === "number" && Number.isInteger(owner.pid) && owner.pid > 0 && !isProcessAlive(owner.pid)) {
|
|
1833
2535
|
await (0, import_promises.rm)(lockPath, { recursive: true, force: true });
|
|
@@ -1845,7 +2547,7 @@ function simulatorReservedError(udid) {
|
|
|
1845
2547
|
}
|
|
1846
2548
|
async function reserveSimulatorUdid(udid, options = {}) {
|
|
1847
2549
|
const lockRoot = options.lockRoot ?? DEFAULT_SIMULATOR_LOCK_ROOT;
|
|
1848
|
-
const lockPath =
|
|
2550
|
+
const lockPath = import_node_path6.default.join(lockRoot, simulatorLockName(udid));
|
|
1849
2551
|
await (0, import_promises.mkdir)(lockRoot, { recursive: true });
|
|
1850
2552
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
1851
2553
|
try {
|
|
@@ -1869,7 +2571,7 @@ async function reserveSimulatorUdid(udid, options = {}) {
|
|
|
1869
2571
|
};
|
|
1870
2572
|
try {
|
|
1871
2573
|
await (0, import_promises.writeFile)(
|
|
1872
|
-
|
|
2574
|
+
import_node_path6.default.join(lockPath, SIMULATOR_LOCK_OWNER_FILE),
|
|
1873
2575
|
`${JSON.stringify({ pid: process.pid, udid, createdAt: (/* @__PURE__ */ new Date()).toISOString() })}
|
|
1874
2576
|
`,
|
|
1875
2577
|
{ flag: "wx" }
|
|
@@ -2061,15 +2763,15 @@ var WdaTransport = class {
|
|
|
2061
2763
|
* {@link WdaHttpError} on a non-2xx response, or a timeout error when the
|
|
2062
2764
|
* per-request deadline elapses.
|
|
2063
2765
|
*/
|
|
2064
|
-
async requestFull(method,
|
|
2766
|
+
async requestFull(method, path20, body, timeoutMs) {
|
|
2065
2767
|
const requestTimeoutMs = timeoutMs ?? this.requestTimeoutMs;
|
|
2066
2768
|
const controller = new AbortController();
|
|
2067
2769
|
const timer = setTimeout(() => controller.abort(), requestTimeoutMs);
|
|
2068
2770
|
timer.unref?.();
|
|
2069
|
-
const url = `${this.baseUrl}${
|
|
2771
|
+
const url = `${this.baseUrl}${path20}`;
|
|
2070
2772
|
const timeoutError = () => {
|
|
2071
2773
|
const shown = requestTimeoutMs >= 1e3 ? `${Math.round(requestTimeoutMs / 1e3)}s` : `${requestTimeoutMs}ms`;
|
|
2072
|
-
return new Error(`WebDriverAgent request ${method} ${
|
|
2774
|
+
return new Error(`WebDriverAgent request ${method} ${path20} timed out after ${shown}`);
|
|
2073
2775
|
};
|
|
2074
2776
|
let response;
|
|
2075
2777
|
try {
|
|
@@ -2101,7 +2803,7 @@ var WdaTransport = class {
|
|
|
2101
2803
|
if (!response.ok) {
|
|
2102
2804
|
const wdError = extractWebdriverError2(parsed);
|
|
2103
2805
|
throw new WdaHttpError(
|
|
2104
|
-
`WebDriverAgent ${method} ${
|
|
2806
|
+
`WebDriverAgent ${method} ${path20} failed (${response.status})${wdError ? `: ${wdError}` : ""}`,
|
|
2105
2807
|
response.status,
|
|
2106
2808
|
wdError
|
|
2107
2809
|
);
|
|
@@ -2109,8 +2811,8 @@ var WdaTransport = class {
|
|
|
2109
2811
|
return parsed;
|
|
2110
2812
|
}
|
|
2111
2813
|
/** Like {@link requestFull} but returns just the `value` field. */
|
|
2112
|
-
async request(method,
|
|
2113
|
-
const parsed = await this.requestFull(method,
|
|
2814
|
+
async request(method, path20, body, timeoutMs) {
|
|
2815
|
+
const parsed = await this.requestFull(method, path20, body, timeoutMs);
|
|
2114
2816
|
return parsed?.value;
|
|
2115
2817
|
}
|
|
2116
2818
|
};
|
|
@@ -2186,8 +2888,8 @@ function sleep2(ms) {
|
|
|
2186
2888
|
}
|
|
2187
2889
|
function createWdaAgentClient(transport, sessionId) {
|
|
2188
2890
|
const base = `/session/${sessionId}`;
|
|
2189
|
-
async function locate(query,
|
|
2190
|
-
return transport.request("POST", `${base}${
|
|
2891
|
+
async function locate(query, path20) {
|
|
2892
|
+
return transport.request("POST", `${base}${path20}`, iosQueryToLocator(query));
|
|
2191
2893
|
}
|
|
2192
2894
|
return {
|
|
2193
2895
|
async findElement(query) {
|
|
@@ -2223,6 +2925,13 @@ function createWdaAgentClient(transport, sessionId) {
|
|
|
2223
2925
|
async homescreen() {
|
|
2224
2926
|
await transport.request("POST", "/wda/homescreen", {});
|
|
2225
2927
|
},
|
|
2928
|
+
async source() {
|
|
2929
|
+
const value = await transport.request("GET", "/source");
|
|
2930
|
+
if (typeof value !== "string") {
|
|
2931
|
+
throw new Error("WebDriverAgent /source did not return XML text; cannot analyze iOS UI hierarchy");
|
|
2932
|
+
}
|
|
2933
|
+
return value;
|
|
2934
|
+
},
|
|
2226
2935
|
async close() {
|
|
2227
2936
|
await transport.request("DELETE", base).catch(() => void 0);
|
|
2228
2937
|
}
|
|
@@ -2232,8 +2941,8 @@ function createWdaAgentClient(transport, sessionId) {
|
|
|
2232
2941
|
// src/browser/ios-helper.ts
|
|
2233
2942
|
var import_meta3 = {};
|
|
2234
2943
|
var WDA_RUNNER_BUNDLE_ID = "com.facebook.WebDriverAgentRunner.xctrunner";
|
|
2235
|
-
var WDA_RUNNER_APP_NAME = "WebDriverAgentRunner-Runner.app";
|
|
2236
2944
|
var WDA_USE_PORT_ENV = "USE_PORT";
|
|
2945
|
+
var PREPARED_XCTESTRUN_PREFIX = "prowl-wda-xctestrun-";
|
|
2237
2946
|
var WDA_STARTUP_ATTEMPTS = 2;
|
|
2238
2947
|
var defaultIosAgentConnector = async ({
|
|
2239
2948
|
host,
|
|
@@ -2256,32 +2965,63 @@ function resolveWdaProject(requireFn = (0, import_node_module2.createRequire)(im
|
|
|
2256
2965
|
"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"
|
|
2257
2966
|
);
|
|
2258
2967
|
}
|
|
2259
|
-
const pkgDir =
|
|
2968
|
+
const pkgDir = import_node_path7.default.dirname(pkgJsonPath);
|
|
2260
2969
|
const version = requireFn(pkgJsonPath).version;
|
|
2261
|
-
const projectPath =
|
|
2262
|
-
if (!
|
|
2970
|
+
const projectPath = import_node_path7.default.join(pkgDir, "WebDriverAgent.xcodeproj");
|
|
2971
|
+
if (!import_node_fs6.default.existsSync(projectPath)) {
|
|
2263
2972
|
throw new Error(`Expected WebDriverAgent project is missing: ${projectPath}. Reinstall dependencies.`);
|
|
2264
2973
|
}
|
|
2265
2974
|
return { projectPath, version };
|
|
2266
2975
|
}
|
|
2267
|
-
function wdaCacheDir(wdaVersion, xcode, homeDir =
|
|
2268
|
-
return
|
|
2976
|
+
function wdaCacheDir(wdaVersion, xcode, homeDir = import_node_os5.default.homedir()) {
|
|
2977
|
+
return import_node_path7.default.join(homeDir, ".prowl", "wda", `${wdaVersion}-xcode${xcode}`);
|
|
2978
|
+
}
|
|
2979
|
+
function productsDir(derivedDataPath) {
|
|
2980
|
+
return import_node_path7.default.join(derivedDataPath, "Build", "Products");
|
|
2981
|
+
}
|
|
2982
|
+
function findXctestrunIn(dir) {
|
|
2983
|
+
let entries;
|
|
2984
|
+
try {
|
|
2985
|
+
entries = import_node_fs6.default.readdirSync(dir);
|
|
2986
|
+
} catch {
|
|
2987
|
+
return null;
|
|
2988
|
+
}
|
|
2989
|
+
const match = entries.filter((name) => name.endsWith(".xctestrun") && !name.startsWith(PREPARED_XCTESTRUN_PREFIX)).sort()[0];
|
|
2990
|
+
return match ? import_node_path7.default.join(dir, match) : null;
|
|
2269
2991
|
}
|
|
2270
|
-
function
|
|
2271
|
-
|
|
2992
|
+
function resolveOverrideXctestrun(override) {
|
|
2993
|
+
if (!import_node_fs6.default.existsSync(override)) {
|
|
2994
|
+
throw new Error(`PROWL_WDA_RUNNER points at a missing path: ${override}`);
|
|
2995
|
+
}
|
|
2996
|
+
if (override.endsWith(".xctestrun")) {
|
|
2997
|
+
return override;
|
|
2998
|
+
}
|
|
2999
|
+
const candidates = [];
|
|
3000
|
+
const stat = import_node_fs6.default.statSync(override);
|
|
3001
|
+
if (override.endsWith(".app")) {
|
|
3002
|
+
candidates.push(import_node_path7.default.dirname(import_node_path7.default.dirname(override)));
|
|
3003
|
+
} else if (stat.isDirectory()) {
|
|
3004
|
+
candidates.push(override, productsDir(override));
|
|
3005
|
+
}
|
|
3006
|
+
for (const dir of candidates) {
|
|
3007
|
+
const found = findXctestrunIn(dir);
|
|
3008
|
+
if (found) {
|
|
3009
|
+
return found;
|
|
3010
|
+
}
|
|
3011
|
+
}
|
|
3012
|
+
throw new Error(
|
|
3013
|
+
`PROWL_WDA_RUNNER (${override}) does not resolve to a WebDriverAgent .xctestrun. Point it at the generated \`*.xctestrun\`, at the \`-derivedDataPath\` directory from \`xcodebuild build-for-testing\`, or at that build's runner \`.app\`.`
|
|
3014
|
+
);
|
|
2272
3015
|
}
|
|
2273
|
-
async function
|
|
3016
|
+
async function resolveWdaTestRun(options = {}) {
|
|
2274
3017
|
const runner = options.runner ?? execFileXcrunRunner;
|
|
2275
3018
|
const env = options.env ?? process.env;
|
|
2276
|
-
const homeDir = options.homeDir ??
|
|
3019
|
+
const homeDir = options.homeDir ?? import_node_os5.default.homedir();
|
|
2277
3020
|
const log = options.logger ?? ((message) => process.stderr.write(`${message}
|
|
2278
3021
|
`));
|
|
2279
3022
|
const override = env.PROWL_WDA_RUNNER;
|
|
2280
3023
|
if (override) {
|
|
2281
|
-
|
|
2282
|
-
throw new Error(`PROWL_WDA_RUNNER points at a missing path: ${override}`);
|
|
2283
|
-
}
|
|
2284
|
-
return override;
|
|
3024
|
+
return resolveOverrideXctestrun(override);
|
|
2285
3025
|
}
|
|
2286
3026
|
const { projectPath, version } = resolveWdaProject(options.requireFn);
|
|
2287
3027
|
const xcode = await xcodeVersion(runner);
|
|
@@ -2291,14 +3031,14 @@ async function resolveWdaRunner(options = {}) {
|
|
|
2291
3031
|
);
|
|
2292
3032
|
}
|
|
2293
3033
|
const cacheDir = wdaCacheDir(version, xcode, homeDir);
|
|
2294
|
-
const
|
|
2295
|
-
if (
|
|
2296
|
-
return
|
|
3034
|
+
const cached = findXctestrunIn(productsDir(cacheDir));
|
|
3035
|
+
if (cached) {
|
|
3036
|
+
return cached;
|
|
2297
3037
|
}
|
|
2298
3038
|
log(
|
|
2299
3039
|
`Prowl: building WebDriverAgent for the iOS target (first run only; this can take a few minutes)\u2026`
|
|
2300
3040
|
);
|
|
2301
|
-
|
|
3041
|
+
import_node_fs6.default.mkdirSync(cacheDir, { recursive: true });
|
|
2302
3042
|
const result = await runner(
|
|
2303
3043
|
[
|
|
2304
3044
|
"xcodebuild",
|
|
@@ -2320,12 +3060,106 @@ async function resolveWdaRunner(options = {}) {
|
|
|
2320
3060
|
`Failed to build WebDriverAgent with \`xcodebuild build-for-testing\`. Ensure a full Xcode (not just the command-line tools) is installed and selected (\`xcode-select -p\`). Details: ${(result.stderr.trim() || result.stdout.trim()).slice(-800)}`
|
|
2321
3061
|
);
|
|
2322
3062
|
}
|
|
2323
|
-
|
|
3063
|
+
const built = findXctestrunIn(productsDir(cacheDir));
|
|
3064
|
+
if (!built) {
|
|
3065
|
+
throw new Error(
|
|
3066
|
+
`WebDriverAgent build succeeded but no .xctestrun was found under ${productsDir(cacheDir)}. This may indicate an Xcode layout change; set PROWL_WDA_RUNNER to a prebuilt runner/xctestrun.`
|
|
3067
|
+
);
|
|
3068
|
+
}
|
|
3069
|
+
return built;
|
|
3070
|
+
}
|
|
3071
|
+
function injectUsePortIntoXctestrun(plist, port) {
|
|
3072
|
+
const clone = structuredClone(plist);
|
|
3073
|
+
let injected = 0;
|
|
3074
|
+
const visit = (node) => {
|
|
3075
|
+
if (Array.isArray(node)) {
|
|
3076
|
+
for (const entry of node) {
|
|
3077
|
+
visit(entry);
|
|
3078
|
+
}
|
|
3079
|
+
return;
|
|
3080
|
+
}
|
|
3081
|
+
if (!node || typeof node !== "object") {
|
|
3082
|
+
return;
|
|
3083
|
+
}
|
|
3084
|
+
const obj = node;
|
|
3085
|
+
const isTarget = typeof obj.TestBundlePath === "string" || typeof obj.TestHostPath === "string";
|
|
3086
|
+
const existingEnv = obj.EnvironmentVariables && typeof obj.EnvironmentVariables === "object" && !Array.isArray(obj.EnvironmentVariables) ? obj.EnvironmentVariables : void 0;
|
|
3087
|
+
if (isTarget || existingEnv) {
|
|
3088
|
+
const env = existingEnv ?? {};
|
|
3089
|
+
env[WDA_USE_PORT_ENV] = String(port);
|
|
3090
|
+
obj.EnvironmentVariables = env;
|
|
3091
|
+
injected += 1;
|
|
3092
|
+
}
|
|
3093
|
+
for (const value of Object.values(obj)) {
|
|
3094
|
+
visit(value);
|
|
3095
|
+
}
|
|
3096
|
+
};
|
|
3097
|
+
visit(clone);
|
|
3098
|
+
if (injected === 0) {
|
|
2324
3099
|
throw new Error(
|
|
2325
|
-
|
|
3100
|
+
"Could not find a test target in the WebDriverAgent .xctestrun to set USE_PORT. The xctestrun format may have changed; set PROWL_WDA_RUNNER to a compatible runner."
|
|
3101
|
+
);
|
|
3102
|
+
}
|
|
3103
|
+
return clone;
|
|
3104
|
+
}
|
|
3105
|
+
function runPlutil(args) {
|
|
3106
|
+
return new Promise((resolve, reject) => {
|
|
3107
|
+
(0, import_node_child_process7.execFile)(
|
|
3108
|
+
"plutil",
|
|
3109
|
+
args,
|
|
3110
|
+
{ encoding: "utf-8", maxBuffer: 32 * 1024 * 1024 },
|
|
3111
|
+
(error, stdout, stderr) => {
|
|
3112
|
+
if (error) {
|
|
3113
|
+
reject(
|
|
3114
|
+
new Error(
|
|
3115
|
+
`\`plutil ${args.join(" ")}\` failed: ${(stderr || error.message).slice(0, 400)}`
|
|
3116
|
+
)
|
|
3117
|
+
);
|
|
3118
|
+
return;
|
|
3119
|
+
}
|
|
3120
|
+
resolve(stdout ?? "");
|
|
3121
|
+
}
|
|
2326
3122
|
);
|
|
3123
|
+
});
|
|
3124
|
+
}
|
|
3125
|
+
var defaultWdaTestRunPreparer = async ({ xctestrunPath, port }) => {
|
|
3126
|
+
const json = await runPlutil(["-convert", "json", "-o", "-", xctestrunPath]);
|
|
3127
|
+
let parsed;
|
|
3128
|
+
try {
|
|
3129
|
+
parsed = JSON.parse(json);
|
|
3130
|
+
} catch {
|
|
3131
|
+
throw new Error(`Could not parse the WebDriverAgent .xctestrun as JSON: ${xctestrunPath}`);
|
|
3132
|
+
}
|
|
3133
|
+
const injected = injectUsePortIntoXctestrun(parsed, port);
|
|
3134
|
+
const stem = `${PREPARED_XCTESTRUN_PREFIX}${port}-${process.pid}`;
|
|
3135
|
+
const outPath = import_node_path7.default.join(import_node_path7.default.dirname(xctestrunPath), `${stem}.xctestrun`);
|
|
3136
|
+
const jsonPath = import_node_path7.default.join(import_node_os5.default.tmpdir(), `${stem}.json`);
|
|
3137
|
+
import_node_fs6.default.writeFileSync(jsonPath, JSON.stringify(injected));
|
|
3138
|
+
try {
|
|
3139
|
+
await runPlutil(["-convert", "xml1", jsonPath, "-o", outPath]);
|
|
3140
|
+
} finally {
|
|
3141
|
+
import_node_fs6.default.rmSync(jsonPath, { force: true });
|
|
3142
|
+
}
|
|
3143
|
+
return outPath;
|
|
3144
|
+
};
|
|
3145
|
+
function cleanupPreparedTestRun(preparedPath) {
|
|
3146
|
+
if (!preparedPath) {
|
|
3147
|
+
return;
|
|
2327
3148
|
}
|
|
2328
|
-
|
|
3149
|
+
if (!import_node_path7.default.basename(preparedPath).startsWith(PREPARED_XCTESTRUN_PREFIX)) {
|
|
3150
|
+
return;
|
|
3151
|
+
}
|
|
3152
|
+
import_node_fs6.default.rmSync(preparedPath, { force: true });
|
|
3153
|
+
}
|
|
3154
|
+
function wdaTestRunArgs(xctestrunPath, udid) {
|
|
3155
|
+
return [
|
|
3156
|
+
"xcodebuild",
|
|
3157
|
+
"test-without-building",
|
|
3158
|
+
"-xctestrun",
|
|
3159
|
+
xctestrunPath,
|
|
3160
|
+
"-destination",
|
|
3161
|
+
`id=${udid}`
|
|
3162
|
+
];
|
|
2329
3163
|
}
|
|
2330
3164
|
async function resolveBundleId(app, runner, udid, coldStart, allowedApps) {
|
|
2331
3165
|
if (!looksLikeIosAppPath(app)) {
|
|
@@ -2337,8 +3171,8 @@ async function resolveBundleId(app, runner, udid, coldStart, allowedApps) {
|
|
|
2337
3171
|
}
|
|
2338
3172
|
return app;
|
|
2339
3173
|
}
|
|
2340
|
-
const appPath =
|
|
2341
|
-
if (!
|
|
3174
|
+
const appPath = import_node_path7.default.resolve(app);
|
|
3175
|
+
if (!import_node_fs6.default.existsSync(appPath)) {
|
|
2342
3176
|
throw new Error(`.app bundle not found: ${appPath}`);
|
|
2343
3177
|
}
|
|
2344
3178
|
const bundleId = readIosBundleIdentifier(appPath);
|
|
@@ -2356,6 +3190,8 @@ async function resolveBundleId(app, runner, udid, coldStart, allowedApps) {
|
|
|
2356
3190
|
}
|
|
2357
3191
|
async function launchIosSession(options) {
|
|
2358
3192
|
const runner = options.runner ?? execFileXcrunRunner;
|
|
3193
|
+
const spawner = options.spawner ?? spawnXcrunProcess;
|
|
3194
|
+
const preparer = options.testRunPreparer ?? defaultWdaTestRunPreparer;
|
|
2359
3195
|
const portAllocator = options.portAllocator ?? findFreePort;
|
|
2360
3196
|
const connector = options.agentConnector ?? defaultIosAgentConnector;
|
|
2361
3197
|
const requestTimeoutMs = Math.max(options.timeoutMs ?? 1e4, DEFAULT_WDA_REQUEST_TIMEOUT_MS) + 5e3;
|
|
@@ -2379,14 +3215,14 @@ async function launchIosSession(options) {
|
|
|
2379
3215
|
options.coldStart ?? false,
|
|
2380
3216
|
options.allowedApps ?? []
|
|
2381
3217
|
);
|
|
2382
|
-
const
|
|
2383
|
-
await installApp(runner, udid, wdaRunnerApp);
|
|
3218
|
+
const baseTestRun = options.wdaTestRun ?? await resolveWdaTestRun({ runner, logger: options.logger });
|
|
2384
3219
|
for (let attempt = 1; attempt <= WDA_STARTUP_ATTEMPTS; attempt += 1) {
|
|
2385
3220
|
let client;
|
|
3221
|
+
let wdaProcess;
|
|
3222
|
+
let preparedTestRun;
|
|
2386
3223
|
let tornDown = false;
|
|
2387
|
-
let terminateWda = false;
|
|
2388
3224
|
let terminateTarget = false;
|
|
2389
|
-
let stage = "
|
|
3225
|
+
let stage = "prepare";
|
|
2390
3226
|
const teardownAttempt = async () => {
|
|
2391
3227
|
if (tornDown) {
|
|
2392
3228
|
return;
|
|
@@ -2395,18 +3231,18 @@ async function launchIosSession(options) {
|
|
|
2395
3231
|
if (client) {
|
|
2396
3232
|
await client.close().catch(() => void 0);
|
|
2397
3233
|
}
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
}
|
|
3234
|
+
wdaProcess?.kill();
|
|
3235
|
+
await terminateApp(runner, udid, WDA_RUNNER_BUNDLE_ID);
|
|
2401
3236
|
if (terminateTarget) {
|
|
2402
3237
|
await terminateApp(runner, udid, bundleId);
|
|
2403
3238
|
}
|
|
3239
|
+
cleanupPreparedTestRun(preparedTestRun);
|
|
2404
3240
|
};
|
|
2405
3241
|
try {
|
|
2406
3242
|
const port = await portAllocator();
|
|
3243
|
+
preparedTestRun = await preparer({ xctestrunPath: baseTestRun, port });
|
|
2407
3244
|
stage = "wda-launch";
|
|
2408
|
-
|
|
2409
|
-
await launchApp(runner, udid, WDA_RUNNER_BUNDLE_ID, { [WDA_USE_PORT_ENV]: String(port) });
|
|
3245
|
+
wdaProcess = spawner(wdaTestRunArgs(preparedTestRun, udid));
|
|
2410
3246
|
stage = "target-launch";
|
|
2411
3247
|
terminateTarget = true;
|
|
2412
3248
|
await launchApp(runner, udid, bundleId);
|
|
@@ -2439,12 +3275,12 @@ async function closeIosSession(session) {
|
|
|
2439
3275
|
}
|
|
2440
3276
|
|
|
2441
3277
|
// src/runner/index.ts
|
|
2442
|
-
var
|
|
2443
|
-
var
|
|
3278
|
+
var import_node_fs15 = __toESM(require("fs"), 1);
|
|
3279
|
+
var import_node_path16 = __toESM(require("path"), 1);
|
|
2444
3280
|
|
|
2445
3281
|
// src/config/loader.ts
|
|
2446
|
-
var
|
|
2447
|
-
var
|
|
3282
|
+
var import_node_fs7 = __toESM(require("fs"), 1);
|
|
3283
|
+
var import_node_path8 = __toESM(require("path"), 1);
|
|
2448
3284
|
var import_yaml = __toESM(require("yaml"), 1);
|
|
2449
3285
|
var import_dotenv = __toESM(require("dotenv"), 1);
|
|
2450
3286
|
|
|
@@ -2462,7 +3298,7 @@ function isValidHuntName(name) {
|
|
|
2462
3298
|
function assertValidHuntName(name) {
|
|
2463
3299
|
if (!isValidHuntName(name)) {
|
|
2464
3300
|
throw new Error(
|
|
2465
|
-
`Invalid hunt name: "${name}".
|
|
3301
|
+
`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).`
|
|
2466
3302
|
);
|
|
2467
3303
|
}
|
|
2468
3304
|
}
|
|
@@ -2713,6 +3549,9 @@ var assertScreenshotStepSchema = import_zod.z.object({
|
|
|
2713
3549
|
threshold: import_zod.z.number().min(0).max(1).optional()
|
|
2714
3550
|
}).strict()
|
|
2715
3551
|
}).strict();
|
|
3552
|
+
var assertWithAiStepSchema = import_zod.z.object({
|
|
3553
|
+
assertWithAI: import_zod.z.string().min(1)
|
|
3554
|
+
}).strict();
|
|
2716
3555
|
var copyTextStepSchema = import_zod.z.object({
|
|
2717
3556
|
copyText: import_zod.z.object({
|
|
2718
3557
|
selector: import_zod.z.string().min(1),
|
|
@@ -2755,6 +3594,7 @@ var stepSchema = import_zod.z.union([
|
|
|
2755
3594
|
evalScriptStepSchema,
|
|
2756
3595
|
runScriptStepSchema,
|
|
2757
3596
|
assertScreenshotStepSchema,
|
|
3597
|
+
assertWithAiStepSchema,
|
|
2758
3598
|
copyTextStepSchema,
|
|
2759
3599
|
waitForDownloadStepSchema
|
|
2760
3600
|
]);
|
|
@@ -2835,12 +3675,12 @@ function findConfigPath(startDir) {
|
|
|
2835
3675
|
let current = startDir;
|
|
2836
3676
|
while (current) {
|
|
2837
3677
|
for (const dir of [CONFIG_DIR, LEGACY_CONFIG_DIR]) {
|
|
2838
|
-
const candidate =
|
|
2839
|
-
if (
|
|
3678
|
+
const candidate = import_node_path8.default.join(current, dir, "config.yml");
|
|
3679
|
+
if (import_node_fs7.default.existsSync(candidate)) {
|
|
2840
3680
|
return candidate;
|
|
2841
3681
|
}
|
|
2842
3682
|
}
|
|
2843
|
-
const parent =
|
|
3683
|
+
const parent = import_node_path8.default.dirname(current);
|
|
2844
3684
|
if (parent === current) {
|
|
2845
3685
|
break;
|
|
2846
3686
|
}
|
|
@@ -2947,19 +3787,19 @@ function ensureAllowedDomain(allowed, urlValue) {
|
|
|
2947
3787
|
return allowed;
|
|
2948
3788
|
}
|
|
2949
3789
|
function loadConfig(configPath) {
|
|
2950
|
-
const resolvedPath = configPath ?
|
|
3790
|
+
const resolvedPath = configPath ? import_node_path8.default.resolve(configPath) : findConfigPath(process.cwd());
|
|
2951
3791
|
if (!resolvedPath) {
|
|
2952
3792
|
throw new Error("Could not find .prowl/config.yml. Run `prowl init` first.");
|
|
2953
3793
|
}
|
|
2954
|
-
if (!
|
|
3794
|
+
if (!import_node_fs7.default.existsSync(resolvedPath)) {
|
|
2955
3795
|
throw new Error(`Config file not found at ${resolvedPath}`);
|
|
2956
3796
|
}
|
|
2957
|
-
const configDir =
|
|
2958
|
-
if (
|
|
3797
|
+
const configDir = import_node_path8.default.dirname(resolvedPath);
|
|
3798
|
+
if (import_node_path8.default.basename(configDir) === LEGACY_CONFIG_DIR) {
|
|
2959
3799
|
warnLegacyConfigDir();
|
|
2960
3800
|
}
|
|
2961
|
-
import_dotenv.default.config({ path:
|
|
2962
|
-
const raw =
|
|
3801
|
+
import_dotenv.default.config({ path: import_node_path8.default.join(configDir, ".env"), override: false });
|
|
3802
|
+
const raw = import_node_fs7.default.readFileSync(resolvedPath, "utf-8");
|
|
2963
3803
|
const parsed = import_yaml.default.parse(raw) ?? {};
|
|
2964
3804
|
const validated = configSchema.parse(parsed);
|
|
2965
3805
|
const config = mergeConfig(validated);
|
|
@@ -2973,32 +3813,32 @@ function loadConfig(configPath) {
|
|
|
2973
3813
|
}
|
|
2974
3814
|
function loadHunt(huntName, configDir) {
|
|
2975
3815
|
assertValidHuntName(huntName);
|
|
2976
|
-
const huntPath =
|
|
2977
|
-
if (!
|
|
3816
|
+
const huntPath = import_node_path8.default.join(configDir, "hunts", `${huntName}.yml`);
|
|
3817
|
+
if (!import_node_fs7.default.existsSync(huntPath)) {
|
|
2978
3818
|
throw new Error(`Hunt file not found: ${huntPath}`);
|
|
2979
3819
|
}
|
|
2980
|
-
const raw =
|
|
3820
|
+
const raw = import_node_fs7.default.readFileSync(huntPath, "utf-8");
|
|
2981
3821
|
const parsed = import_yaml.default.parse(raw) ?? {};
|
|
2982
3822
|
const validated = huntSchema.parse(parsed);
|
|
2983
3823
|
return validated;
|
|
2984
3824
|
}
|
|
2985
3825
|
function loadHuntTags(huntName, configDir) {
|
|
2986
3826
|
assertValidHuntName(huntName);
|
|
2987
|
-
const huntPath =
|
|
2988
|
-
if (!
|
|
3827
|
+
const huntPath = import_node_path8.default.join(configDir, "hunts", `${huntName}.yml`);
|
|
3828
|
+
if (!import_node_fs7.default.existsSync(huntPath)) {
|
|
2989
3829
|
return [];
|
|
2990
3830
|
}
|
|
2991
|
-
const raw =
|
|
3831
|
+
const raw = import_node_fs7.default.readFileSync(huntPath, "utf-8");
|
|
2992
3832
|
const parsed = import_yaml.default.parse(raw) ?? {};
|
|
2993
3833
|
return Array.isArray(parsed.tags) ? parsed.tags : [];
|
|
2994
3834
|
}
|
|
2995
3835
|
function loadHuntMeta(huntName, configDir) {
|
|
2996
3836
|
assertValidHuntName(huntName);
|
|
2997
|
-
const huntPath =
|
|
2998
|
-
if (!
|
|
3837
|
+
const huntPath = import_node_path8.default.join(configDir, "hunts", `${huntName}.yml`);
|
|
3838
|
+
if (!import_node_fs7.default.existsSync(huntPath)) {
|
|
2999
3839
|
return { tags: [] };
|
|
3000
3840
|
}
|
|
3001
|
-
const raw =
|
|
3841
|
+
const raw = import_node_fs7.default.readFileSync(huntPath, "utf-8");
|
|
3002
3842
|
const parsed = import_yaml.default.parse(raw) ?? {};
|
|
3003
3843
|
return {
|
|
3004
3844
|
description: typeof parsed.description === "string" ? parsed.description : void 0,
|
|
@@ -3006,24 +3846,24 @@ function loadHuntMeta(huntName, configDir) {
|
|
|
3006
3846
|
};
|
|
3007
3847
|
}
|
|
3008
3848
|
function listHunts(configDir) {
|
|
3009
|
-
const huntsDir =
|
|
3010
|
-
if (!
|
|
3849
|
+
const huntsDir = import_node_path8.default.join(configDir, "hunts");
|
|
3850
|
+
if (!import_node_fs7.default.existsSync(huntsDir)) {
|
|
3011
3851
|
return [];
|
|
3012
3852
|
}
|
|
3013
|
-
const stats =
|
|
3853
|
+
const stats = import_node_fs7.default.statSync(huntsDir);
|
|
3014
3854
|
if (!stats.isDirectory()) {
|
|
3015
3855
|
throw new Error(`Hunts path is not a directory: ${huntsDir}`);
|
|
3016
3856
|
}
|
|
3017
3857
|
const results = [];
|
|
3018
3858
|
function scanDir(dir) {
|
|
3019
|
-
const entries =
|
|
3859
|
+
const entries = import_node_fs7.default.readdirSync(dir, { withFileTypes: true });
|
|
3020
3860
|
for (const entry of entries) {
|
|
3021
3861
|
if (entry.isFile() && entry.name.endsWith(".yml")) {
|
|
3022
|
-
const fullPath =
|
|
3023
|
-
const relative =
|
|
3862
|
+
const fullPath = import_node_path8.default.join(dir, entry.name);
|
|
3863
|
+
const relative = import_node_path8.default.relative(huntsDir, fullPath);
|
|
3024
3864
|
results.push(relative.replace(/\.yml$/, ""));
|
|
3025
3865
|
} else if (entry.isDirectory()) {
|
|
3026
|
-
scanDir(
|
|
3866
|
+
scanDir(import_node_path8.default.join(dir, entry.name));
|
|
3027
3867
|
}
|
|
3028
3868
|
}
|
|
3029
3869
|
}
|
|
@@ -3032,7 +3872,7 @@ function listHunts(configDir) {
|
|
|
3032
3872
|
}
|
|
3033
3873
|
|
|
3034
3874
|
// src/config/interpolate.ts
|
|
3035
|
-
var
|
|
3875
|
+
var import_node_crypto2 = __toESM(require("crypto"), 1);
|
|
3036
3876
|
var VAR_PATTERN = /\{\{([A-Z0-9_]+)\}\}/g;
|
|
3037
3877
|
function collectInterpolatedValues(input, vars, values) {
|
|
3038
3878
|
if (typeof input === "string") {
|
|
@@ -3071,8 +3911,8 @@ var RANDOM_FIRST_NAMES = ["Alex", "Jordan", "Morgan", "Taylor", "Casey", "Riley"
|
|
|
3071
3911
|
var RANDOM_LAST_NAMES = ["Smith", "Johnson", "Brown", "Davis", "Wilson", "Clark", "Hall", "Young"];
|
|
3072
3912
|
function generateRandomVars(randomSource) {
|
|
3073
3913
|
const random = randomSource?.random ?? Math.random;
|
|
3074
|
-
const randomBytes = randomSource?.randomBytes ??
|
|
3075
|
-
const randomUUID = randomSource?.randomUUID ??
|
|
3914
|
+
const randomBytes = randomSource?.randomBytes ?? import_node_crypto2.default.randomBytes;
|
|
3915
|
+
const randomUUID = randomSource?.randomUUID ?? import_node_crypto2.default.randomUUID;
|
|
3076
3916
|
const hex = randomBytes(4).toString("hex");
|
|
3077
3917
|
const firstIndex = Math.floor(random() * RANDOM_FIRST_NAMES.length);
|
|
3078
3918
|
const lastIndex = Math.floor(random() * RANDOM_LAST_NAMES.length);
|
|
@@ -3325,6 +4165,11 @@ function interpolateStep(step, vars, stepPath2, redacted) {
|
|
|
3325
4165
|
}
|
|
3326
4166
|
};
|
|
3327
4167
|
}
|
|
4168
|
+
if ("assertWithAI" in step) {
|
|
4169
|
+
return {
|
|
4170
|
+
assertWithAI: interpolateString(step.assertWithAI, vars).value
|
|
4171
|
+
};
|
|
4172
|
+
}
|
|
3328
4173
|
if ("copyText" in step) {
|
|
3329
4174
|
return {
|
|
3330
4175
|
copyText: {
|
|
@@ -3398,8 +4243,8 @@ function interpolateHunt(hunt, env, randomVars = generateRandomVars()) {
|
|
|
3398
4243
|
}
|
|
3399
4244
|
|
|
3400
4245
|
// src/browser/playwright-driver.ts
|
|
3401
|
-
var
|
|
3402
|
-
var
|
|
4246
|
+
var import_node_fs8 = __toESM(require("fs"), 1);
|
|
4247
|
+
var import_node_path9 = __toESM(require("path"), 1);
|
|
3403
4248
|
var import_playwright = require("playwright");
|
|
3404
4249
|
var ENGINES = { chromium: import_playwright.chromium, firefox: import_playwright.firefox, webkit: import_playwright.webkit };
|
|
3405
4250
|
async function launchBrowser(options) {
|
|
@@ -3421,14 +4266,14 @@ async function launchBrowser(options) {
|
|
|
3421
4266
|
contextOptions.viewport = options.viewport;
|
|
3422
4267
|
}
|
|
3423
4268
|
if (options.storageStatePath) {
|
|
3424
|
-
if (
|
|
4269
|
+
if (import_node_fs8.default.existsSync(options.storageStatePath)) {
|
|
3425
4270
|
contextOptions.storageState = options.storageStatePath;
|
|
3426
4271
|
} else {
|
|
3427
4272
|
console.warn(`Auth state file not found: ${options.storageStatePath}. Run "prowl login" to create it.`);
|
|
3428
4273
|
}
|
|
3429
4274
|
}
|
|
3430
4275
|
if (options.recordHar) {
|
|
3431
|
-
contextOptions.recordHar = { path:
|
|
4276
|
+
contextOptions.recordHar = { path: import_node_path9.default.join(options.runDir, "network.har") };
|
|
3432
4277
|
}
|
|
3433
4278
|
const context = await browser.newContext(contextOptions);
|
|
3434
4279
|
const page = await context.newPage();
|
|
@@ -3436,7 +4281,7 @@ async function launchBrowser(options) {
|
|
|
3436
4281
|
page.setDefaultNavigationTimeout(options.timeout);
|
|
3437
4282
|
let tracePath;
|
|
3438
4283
|
if (options.trace) {
|
|
3439
|
-
tracePath =
|
|
4284
|
+
tracePath = import_node_path9.default.join(options.runDir, "trace.zip");
|
|
3440
4285
|
await context.tracing.start({ screenshots: true, snapshots: true, sources: true });
|
|
3441
4286
|
}
|
|
3442
4287
|
return { browser, context, page, tracePath };
|
|
@@ -3611,8 +4456,8 @@ function createPlaywrightDriver(page) {
|
|
|
3611
4456
|
}
|
|
3612
4457
|
|
|
3613
4458
|
// src/runner/steps.ts
|
|
3614
|
-
var
|
|
3615
|
-
var
|
|
4459
|
+
var import_node_fs10 = __toESM(require("fs"), 1);
|
|
4460
|
+
var import_node_path11 = __toESM(require("path"), 1);
|
|
3616
4461
|
|
|
3617
4462
|
// src/runner/healing.ts
|
|
3618
4463
|
var INTERACTIVE_TAGS = ["button", "a", "input", "select", "textarea"];
|
|
@@ -3756,14 +4601,254 @@ function createRunPolicy(driver, options) {
|
|
|
3756
4601
|
return { selector: healed.selector, healedFrom: healed.healedFrom };
|
|
3757
4602
|
}
|
|
3758
4603
|
return {
|
|
3759
|
-
assertWithinMaxSteps,
|
|
3760
|
-
ensureUrlAllowed,
|
|
3761
|
-
ensureAppAllowed,
|
|
3762
|
-
ensureLocationAllowed,
|
|
3763
|
-
assertAllowedSelector,
|
|
3764
|
-
resolveActionSelector
|
|
4604
|
+
assertWithinMaxSteps,
|
|
4605
|
+
ensureUrlAllowed,
|
|
4606
|
+
ensureAppAllowed,
|
|
4607
|
+
ensureLocationAllowed,
|
|
4608
|
+
assertAllowedSelector,
|
|
4609
|
+
resolveActionSelector
|
|
4610
|
+
};
|
|
4611
|
+
}
|
|
4612
|
+
|
|
4613
|
+
// src/generator/ai.ts
|
|
4614
|
+
var DEFAULT_BASE_URL = {
|
|
4615
|
+
anthropic: "https://api.anthropic.com",
|
|
4616
|
+
openai: "https://api.openai.com"
|
|
4617
|
+
};
|
|
4618
|
+
var AI_REQUEST_TIMEOUT_MS = 3e4;
|
|
4619
|
+
function apiRoot(config) {
|
|
4620
|
+
return config.baseUrl ?? DEFAULT_BASE_URL[config.provider];
|
|
4621
|
+
}
|
|
4622
|
+
function defaultModelFor(provider) {
|
|
4623
|
+
return provider === "anthropic" ? "claude-sonnet-4-5-20250929" : "gpt-4o";
|
|
4624
|
+
}
|
|
4625
|
+
function resolveAiEnv() {
|
|
4626
|
+
const provider = process.env.PROWL_AI_PROVIDER ?? "anthropic";
|
|
4627
|
+
if (provider !== "anthropic" && provider !== "openai") {
|
|
4628
|
+
throw new Error(`Unsupported AI provider: ${provider}. Use "anthropic" or "openai".`);
|
|
4629
|
+
}
|
|
4630
|
+
const model = process.env.PROWL_AI_MODEL ?? defaultModelFor(provider);
|
|
4631
|
+
const baseUrl = normalizeBaseUrl(process.env.PROWL_AI_BASE_URL) ?? DEFAULT_BASE_URL[provider];
|
|
4632
|
+
return { provider, model, baseUrl, apiKey: process.env.PROWL_AI_KEY };
|
|
4633
|
+
}
|
|
4634
|
+
function normalizeBaseUrl(value) {
|
|
4635
|
+
if (value === void 0) return void 0;
|
|
4636
|
+
const trimmed = value.trim().replace(/\/+$/, "");
|
|
4637
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
4638
|
+
}
|
|
4639
|
+
function resolveAiConfig() {
|
|
4640
|
+
const env = resolveAiEnv();
|
|
4641
|
+
if (!env.apiKey) {
|
|
4642
|
+
throw new Error(
|
|
4643
|
+
"PROWL_AI_KEY environment variable is required. Set it to your Anthropic or OpenAI API key."
|
|
4644
|
+
);
|
|
4645
|
+
}
|
|
4646
|
+
return { provider: env.provider, model: env.model, apiKey: env.apiKey, baseUrl: env.baseUrl };
|
|
4647
|
+
}
|
|
4648
|
+
function tryResolveAiConfig() {
|
|
4649
|
+
const env = resolveAiEnv();
|
|
4650
|
+
if (!env.apiKey) {
|
|
4651
|
+
return null;
|
|
4652
|
+
}
|
|
4653
|
+
return { provider: env.provider, model: env.model, apiKey: env.apiKey, baseUrl: env.baseUrl };
|
|
4654
|
+
}
|
|
4655
|
+
async function generateWithAi(prompt, config) {
|
|
4656
|
+
if (config.provider === "anthropic") {
|
|
4657
|
+
return generateWithAnthropic(prompt, config);
|
|
4658
|
+
}
|
|
4659
|
+
return generateWithOpenAi(prompt, config);
|
|
4660
|
+
}
|
|
4661
|
+
async function generateWithAnthropic(prompt, config) {
|
|
4662
|
+
const data = await postJson(
|
|
4663
|
+
"Anthropic",
|
|
4664
|
+
`${apiRoot(config)}/v1/messages`,
|
|
4665
|
+
anthropicHeaders(config),
|
|
4666
|
+
{
|
|
4667
|
+
model: config.model,
|
|
4668
|
+
max_tokens: 4096,
|
|
4669
|
+
messages: [
|
|
4670
|
+
{ role: "user", content: prompt }
|
|
4671
|
+
]
|
|
4672
|
+
}
|
|
4673
|
+
);
|
|
4674
|
+
return extractAnthropicText(data);
|
|
4675
|
+
}
|
|
4676
|
+
async function generateWithOpenAi(prompt, config) {
|
|
4677
|
+
const data = await postJson(
|
|
4678
|
+
"OpenAI",
|
|
4679
|
+
`${apiRoot(config)}/v1/chat/completions`,
|
|
4680
|
+
openAiHeaders(config),
|
|
4681
|
+
{
|
|
4682
|
+
model: config.model,
|
|
4683
|
+
messages: [
|
|
4684
|
+
{ role: "user", content: prompt }
|
|
4685
|
+
],
|
|
4686
|
+
max_tokens: 4096
|
|
4687
|
+
}
|
|
4688
|
+
);
|
|
4689
|
+
return extractOpenAiText(data);
|
|
4690
|
+
}
|
|
4691
|
+
function buildVisionPrompt(assertion) {
|
|
4692
|
+
return [
|
|
4693
|
+
"You are a meticulous QA reviewer. You are given a screenshot of an application",
|
|
4694
|
+
"and a single assertion describing what should be true about it.",
|
|
4695
|
+
"",
|
|
4696
|
+
"Assertion:",
|
|
4697
|
+
assertion,
|
|
4698
|
+
"",
|
|
4699
|
+
"Decide whether the assertion holds for the screenshot. Judge ONLY what is",
|
|
4700
|
+
"visible; do not assume behavior you cannot see. Be strict: if the assertion",
|
|
4701
|
+
"is not clearly satisfied, it fails.",
|
|
4702
|
+
"",
|
|
4703
|
+
"Reply with ONLY a single JSON object on one line, no markdown, no code fences:",
|
|
4704
|
+
'{"pass": <true|false>, "reason": "<one concise sentence explaining the verdict>"}'
|
|
4705
|
+
].join("\n");
|
|
4706
|
+
}
|
|
4707
|
+
function parseVisionVerdict(raw) {
|
|
4708
|
+
const cleaned = stripCodeFences(raw).trim();
|
|
4709
|
+
const candidate = extractJsonObject(cleaned);
|
|
4710
|
+
if (candidate === null) {
|
|
4711
|
+
throw new Error(
|
|
4712
|
+
`Could not parse a JSON verdict from the AI response: ${truncateForError(raw)}`
|
|
4713
|
+
);
|
|
4714
|
+
}
|
|
4715
|
+
let parsed;
|
|
4716
|
+
try {
|
|
4717
|
+
parsed = JSON.parse(candidate);
|
|
4718
|
+
} catch {
|
|
4719
|
+
throw new Error(
|
|
4720
|
+
`AI verdict was not valid JSON: ${truncateForError(raw)}`
|
|
4721
|
+
);
|
|
4722
|
+
}
|
|
4723
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
4724
|
+
throw new Error(`AI verdict was not a JSON object: ${truncateForError(raw)}`);
|
|
4725
|
+
}
|
|
4726
|
+
const record = parsed;
|
|
4727
|
+
if (typeof record.pass !== "boolean") {
|
|
4728
|
+
throw new Error(
|
|
4729
|
+
`AI verdict is missing a boolean "pass" field: ${truncateForError(raw)}`
|
|
4730
|
+
);
|
|
4731
|
+
}
|
|
4732
|
+
const reason = typeof record.reason === "string" && record.reason.trim().length > 0 ? record.reason.trim() : record.pass ? "Assertion satisfied." : "Assertion not satisfied.";
|
|
4733
|
+
return { pass: record.pass, reason };
|
|
4734
|
+
}
|
|
4735
|
+
function stripCodeFences(text) {
|
|
4736
|
+
const fence = text.match(/```(?:json)?\s*([\s\S]*?)```/i);
|
|
4737
|
+
return fence ? fence[1] : text;
|
|
4738
|
+
}
|
|
4739
|
+
function extractJsonObject(text) {
|
|
4740
|
+
const trimmed = text.trim();
|
|
4741
|
+
if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
|
|
4742
|
+
return trimmed;
|
|
4743
|
+
}
|
|
4744
|
+
const first = trimmed.indexOf("{");
|
|
4745
|
+
const last = trimmed.lastIndexOf("}");
|
|
4746
|
+
if (first === -1 || last === -1 || last <= first) {
|
|
4747
|
+
return null;
|
|
4748
|
+
}
|
|
4749
|
+
return trimmed.slice(first, last + 1);
|
|
4750
|
+
}
|
|
4751
|
+
function truncateForError(text) {
|
|
4752
|
+
const collapsed = text.replace(/\s+/g, " ").trim();
|
|
4753
|
+
return collapsed.length > 200 ? `${collapsed.slice(0, 199)}\u2026` : collapsed;
|
|
4754
|
+
}
|
|
4755
|
+
async function assertWithAiVision(input, config) {
|
|
4756
|
+
const raw = config.provider === "anthropic" ? await visionWithAnthropic(input, config) : await visionWithOpenAi(input, config);
|
|
4757
|
+
return parseVisionVerdict(raw);
|
|
4758
|
+
}
|
|
4759
|
+
async function visionWithAnthropic(input, config) {
|
|
4760
|
+
const data = await postJson(
|
|
4761
|
+
"Anthropic",
|
|
4762
|
+
`${apiRoot(config)}/v1/messages`,
|
|
4763
|
+
anthropicHeaders(config),
|
|
4764
|
+
{
|
|
4765
|
+
model: config.model,
|
|
4766
|
+
max_tokens: 1024,
|
|
4767
|
+
temperature: 0,
|
|
4768
|
+
messages: [
|
|
4769
|
+
{
|
|
4770
|
+
role: "user",
|
|
4771
|
+
content: [
|
|
4772
|
+
{
|
|
4773
|
+
type: "image",
|
|
4774
|
+
source: {
|
|
4775
|
+
type: "base64",
|
|
4776
|
+
media_type: input.mediaType,
|
|
4777
|
+
data: input.imageBase64
|
|
4778
|
+
}
|
|
4779
|
+
},
|
|
4780
|
+
{ type: "text", text: buildVisionPrompt(input.assertion) }
|
|
4781
|
+
]
|
|
4782
|
+
}
|
|
4783
|
+
]
|
|
4784
|
+
}
|
|
4785
|
+
);
|
|
4786
|
+
return extractAnthropicText(data);
|
|
4787
|
+
}
|
|
4788
|
+
async function visionWithOpenAi(input, config) {
|
|
4789
|
+
const data = await postJson(
|
|
4790
|
+
"OpenAI",
|
|
4791
|
+
`${apiRoot(config)}/v1/chat/completions`,
|
|
4792
|
+
openAiHeaders(config),
|
|
4793
|
+
{
|
|
4794
|
+
model: config.model,
|
|
4795
|
+
max_tokens: 1024,
|
|
4796
|
+
temperature: 0,
|
|
4797
|
+
messages: [
|
|
4798
|
+
{
|
|
4799
|
+
role: "user",
|
|
4800
|
+
content: [
|
|
4801
|
+
{ type: "text", text: buildVisionPrompt(input.assertion) },
|
|
4802
|
+
{
|
|
4803
|
+
type: "image_url",
|
|
4804
|
+
image_url: { url: `data:${input.mediaType};base64,${input.imageBase64}` }
|
|
4805
|
+
}
|
|
4806
|
+
]
|
|
4807
|
+
}
|
|
4808
|
+
]
|
|
4809
|
+
}
|
|
4810
|
+
);
|
|
4811
|
+
return extractOpenAiText(data);
|
|
4812
|
+
}
|
|
4813
|
+
async function postJson(provider, url, headers, body) {
|
|
4814
|
+
const response = await fetch(url, {
|
|
4815
|
+
method: "POST",
|
|
4816
|
+
headers,
|
|
4817
|
+
body: JSON.stringify(body),
|
|
4818
|
+
signal: AbortSignal.timeout(AI_REQUEST_TIMEOUT_MS)
|
|
4819
|
+
});
|
|
4820
|
+
if (!response.ok) {
|
|
4821
|
+
const responseBody = await response.text();
|
|
4822
|
+
throw new Error(`${provider} API error (${response.status}): ${responseBody}`);
|
|
4823
|
+
}
|
|
4824
|
+
return await response.json();
|
|
4825
|
+
}
|
|
4826
|
+
function anthropicHeaders(config) {
|
|
4827
|
+
return {
|
|
4828
|
+
"Content-Type": "application/json",
|
|
4829
|
+
"x-api-key": config.apiKey,
|
|
4830
|
+
"anthropic-version": "2023-06-01"
|
|
4831
|
+
};
|
|
4832
|
+
}
|
|
4833
|
+
function openAiHeaders(config) {
|
|
4834
|
+
return {
|
|
4835
|
+
"Content-Type": "application/json",
|
|
4836
|
+
"Authorization": `Bearer ${config.apiKey}`
|
|
3765
4837
|
};
|
|
3766
4838
|
}
|
|
4839
|
+
function extractAnthropicText(data) {
|
|
4840
|
+
const textBlock = data.content?.find((c) => c.type === "text");
|
|
4841
|
+
if (!textBlock?.text) {
|
|
4842
|
+
throw new Error("Anthropic API returned no text content");
|
|
4843
|
+
}
|
|
4844
|
+
return textBlock.text;
|
|
4845
|
+
}
|
|
4846
|
+
function extractOpenAiText(data) {
|
|
4847
|
+
if (!data.choices?.[0]?.message?.content) {
|
|
4848
|
+
throw new Error("OpenAI API returned no content");
|
|
4849
|
+
}
|
|
4850
|
+
return data.choices[0].message.content;
|
|
4851
|
+
}
|
|
3767
4852
|
|
|
3768
4853
|
// src/runner/steps.ts
|
|
3769
4854
|
function getStepType(step) {
|
|
@@ -3793,6 +4878,7 @@ function getStepType(step) {
|
|
|
3793
4878
|
if ("evalScript" in step) return "evalScript";
|
|
3794
4879
|
if ("runScript" in step) return "runScript";
|
|
3795
4880
|
if ("assertScreenshot" in step) return "assertScreenshot";
|
|
4881
|
+
if ("assertWithAI" in step) return "assertWithAI";
|
|
3796
4882
|
if ("copyText" in step) return "copyText";
|
|
3797
4883
|
if ("waitForDownload" in step) return "waitForDownload";
|
|
3798
4884
|
return "step";
|
|
@@ -3852,6 +4938,9 @@ function applyRuntimeVars(step, vars) {
|
|
|
3852
4938
|
}
|
|
3853
4939
|
};
|
|
3854
4940
|
}
|
|
4941
|
+
if ("assertWithAI" in step) {
|
|
4942
|
+
return { assertWithAI: sub(step.assertWithAI) };
|
|
4943
|
+
}
|
|
3855
4944
|
if ("copyText" in step) {
|
|
3856
4945
|
return { copyText: { selector: sub(step.copyText.selector), as: step.copyText.as } };
|
|
3857
4946
|
}
|
|
@@ -4102,7 +5191,7 @@ async function runInlineAssert(driver, policy, assertion) {
|
|
|
4102
5191
|
throw new Error("assert step is missing an assertion type");
|
|
4103
5192
|
}
|
|
4104
5193
|
function screenshotPath(screenshotsDir, fileName) {
|
|
4105
|
-
return
|
|
5194
|
+
return import_node_path11.default.join(screenshotsDir, fileName);
|
|
4106
5195
|
}
|
|
4107
5196
|
function stepPath(prefix, index) {
|
|
4108
5197
|
return prefix ? `${prefix}.${index}` : `${index}`;
|
|
@@ -4119,7 +5208,7 @@ function validateDownloadFilename(suggestedFilename) {
|
|
|
4119
5208
|
const safeFilename = suggestedFilename.trim();
|
|
4120
5209
|
const allowedFilenamePattern = /^[^<>:"/\\|?*]+$/;
|
|
4121
5210
|
const hasControlCharacter = Array.from(safeFilename).some((char) => char.charCodeAt(0) < 32);
|
|
4122
|
-
if (safeFilename.length === 0 || safeFilename !== suggestedFilename || safeFilename !==
|
|
5211
|
+
if (safeFilename.length === 0 || safeFilename !== suggestedFilename || safeFilename !== import_node_path11.default.basename(safeFilename) || safeFilename.includes("..") || /[/\\]/.test(safeFilename) || hasControlCharacter || !allowedFilenamePattern.test(safeFilename)) {
|
|
4123
5212
|
throw new Error(`Invalid download filename: "${suggestedFilename}"`);
|
|
4124
5213
|
}
|
|
4125
5214
|
return safeFilename;
|
|
@@ -4299,7 +5388,7 @@ var STEP_HANDLERS = {
|
|
|
4299
5388
|
if (!("setInputFiles" in h.step)) unknownStep();
|
|
4300
5389
|
const resolvedInput = await h.policy.resolveActionSelector(h.step.setInputFiles.selector);
|
|
4301
5390
|
const rawFiles = h.step.setInputFiles.files;
|
|
4302
|
-
const resolveFile = (f) =>
|
|
5391
|
+
const resolveFile = (f) => import_node_path11.default.isAbsolute(f) ? f : import_node_path11.default.join(h.context.configDir, f);
|
|
4303
5392
|
const resolvedFiles = Array.isArray(rawFiles) ? rawFiles.map(resolveFile) : resolveFile(rawFiles);
|
|
4304
5393
|
await h.driver.setInputFiles(resolvedInput.selector, resolvedFiles);
|
|
4305
5394
|
h.policy.ensureLocationAllowed(h.driver);
|
|
@@ -4661,15 +5750,15 @@ var STEP_HANDLERS = {
|
|
|
4661
5750
|
if (!responseFile) {
|
|
4662
5751
|
throw new Error("mock.response must include either body or file");
|
|
4663
5752
|
}
|
|
4664
|
-
const candidateFilePath =
|
|
4665
|
-
const resolvedConfigDir =
|
|
4666
|
-
const resolvedFilePath =
|
|
4667
|
-
const relativePath =
|
|
4668
|
-
const isWithinConfigDir = relativePath === "" || relativePath !== ".." && !relativePath.startsWith(`..${
|
|
5753
|
+
const candidateFilePath = import_node_path11.default.isAbsolute(responseFile) ? responseFile : import_node_path11.default.join(h.context.configDir, responseFile);
|
|
5754
|
+
const resolvedConfigDir = import_node_path11.default.resolve(h.context.configDir);
|
|
5755
|
+
const resolvedFilePath = import_node_path11.default.resolve(candidateFilePath);
|
|
5756
|
+
const relativePath = import_node_path11.default.relative(resolvedConfigDir, resolvedFilePath);
|
|
5757
|
+
const isWithinConfigDir = relativePath === "" || relativePath !== ".." && !relativePath.startsWith(`..${import_node_path11.default.sep}`) && !import_node_path11.default.isAbsolute(relativePath);
|
|
4669
5758
|
if (!isWithinConfigDir) {
|
|
4670
5759
|
throw new Error("mock.response.file must resolve within config directory");
|
|
4671
5760
|
}
|
|
4672
|
-
responseBody = await
|
|
5761
|
+
responseBody = await import_node_fs10.default.promises.readFile(resolvedFilePath, "utf-8");
|
|
4673
5762
|
}
|
|
4674
5763
|
const contentType = mock.response.contentType ?? "application/json";
|
|
4675
5764
|
const status = mock.response.status;
|
|
@@ -4732,8 +5821,8 @@ var STEP_HANDLERS = {
|
|
|
4732
5821
|
capabilities: ["evaluate"],
|
|
4733
5822
|
run: async (h) => {
|
|
4734
5823
|
if (!("runScript" in h.step)) unknownStep();
|
|
4735
|
-
const filePath =
|
|
4736
|
-
const fileContents =
|
|
5824
|
+
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);
|
|
5825
|
+
const fileContents = import_node_fs10.default.readFileSync(filePath, "utf-8");
|
|
4737
5826
|
await h.driver.evaluate(fileContents);
|
|
4738
5827
|
return {
|
|
4739
5828
|
kind: "result",
|
|
@@ -4754,19 +5843,19 @@ var STEP_HANDLERS = {
|
|
|
4754
5843
|
const name = h.step.assertScreenshot.name;
|
|
4755
5844
|
const threshold = h.step.assertScreenshot.threshold ?? 0.1;
|
|
4756
5845
|
const baselineDir = ensureBaselineDir2(h.context.configDir);
|
|
4757
|
-
const baselinePath =
|
|
4758
|
-
const currentScreenshotPath =
|
|
4759
|
-
|
|
5846
|
+
const baselinePath = import_node_path11.default.join(baselineDir, `${name}.png`);
|
|
5847
|
+
const currentScreenshotPath = import_node_path11.default.join(h.context.runDir, "screenshots", `${name}-current.png`);
|
|
5848
|
+
import_node_fs10.default.mkdirSync(import_node_path11.default.dirname(currentScreenshotPath), { recursive: true });
|
|
4760
5849
|
await h.driver.screenshot({ path: currentScreenshotPath, fullPage: true });
|
|
4761
|
-
h.screenshots.push(
|
|
4762
|
-
if (!
|
|
4763
|
-
|
|
5850
|
+
h.screenshots.push(import_node_path11.default.join("screenshots", `${name}-current.png`));
|
|
5851
|
+
if (!import_node_fs10.default.existsSync(baselinePath)) {
|
|
5852
|
+
import_node_fs10.default.copyFileSync(currentScreenshotPath, baselinePath);
|
|
4764
5853
|
return {
|
|
4765
5854
|
kind: "result",
|
|
4766
5855
|
result: { type: "assertScreenshot", status: "pass", durationMs: Date.now() - h.stepStart, value: "baseline created" }
|
|
4767
5856
|
};
|
|
4768
5857
|
}
|
|
4769
|
-
const diffPath =
|
|
5858
|
+
const diffPath = import_node_path11.default.join(h.context.runDir, "screenshots", `${name}-diff.png`);
|
|
4770
5859
|
const comparison = await compareScreenshots2(baselinePath, currentScreenshotPath, diffPath, threshold);
|
|
4771
5860
|
if (comparison.match) {
|
|
4772
5861
|
return {
|
|
@@ -4779,12 +5868,54 @@ var STEP_HANDLERS = {
|
|
|
4779
5868
|
}
|
|
4780
5869
|
};
|
|
4781
5870
|
}
|
|
4782
|
-
h.screenshots.push(
|
|
5871
|
+
h.screenshots.push(import_node_path11.default.join("screenshots", `${name}-diff.png`));
|
|
4783
5872
|
throw new Error(
|
|
4784
5873
|
`Visual regression: ${(comparison.diffPercentage * 100).toFixed(2)}% diff exceeds threshold ${(threshold * 100).toFixed(0)}%`
|
|
4785
5874
|
);
|
|
4786
5875
|
}
|
|
4787
5876
|
},
|
|
5877
|
+
assertWithAI: {
|
|
5878
|
+
capabilities: ["screenshot"],
|
|
5879
|
+
run: async (h) => {
|
|
5880
|
+
if (!("assertWithAI" in h.step)) unknownStep();
|
|
5881
|
+
const assertion = h.step.assertWithAI;
|
|
5882
|
+
const resolve = h.context.resolveAiConfig ?? tryResolveAiConfig;
|
|
5883
|
+
const aiConfig = resolve();
|
|
5884
|
+
if (!aiConfig) {
|
|
5885
|
+
return {
|
|
5886
|
+
kind: "result",
|
|
5887
|
+
result: {
|
|
5888
|
+
type: "assertWithAI",
|
|
5889
|
+
status: "warn",
|
|
5890
|
+
durationMs: Date.now() - h.stepStart,
|
|
5891
|
+
value: `skipped: no AI provider configured (set PROWL_AI_KEY to enable) \u2014 "${assertion}"`
|
|
5892
|
+
}
|
|
5893
|
+
};
|
|
5894
|
+
}
|
|
5895
|
+
const fileName = `assertWithAI_step_${h.index + 1}.png`;
|
|
5896
|
+
const relative = await h.addScreenshot(fileName);
|
|
5897
|
+
const screenshotFullPath = import_node_path11.default.join(h.context.runDir, relative);
|
|
5898
|
+
const imageBase64 = import_node_fs10.default.readFileSync(screenshotFullPath).toString("base64");
|
|
5899
|
+
const assertVision = h.context.assertVision ?? assertWithAiVision;
|
|
5900
|
+
const verdict = await assertVision(
|
|
5901
|
+
{ imageBase64, mediaType: "image/png", assertion },
|
|
5902
|
+
aiConfig
|
|
5903
|
+
);
|
|
5904
|
+
if (verdict.pass) {
|
|
5905
|
+
return {
|
|
5906
|
+
kind: "result",
|
|
5907
|
+
result: {
|
|
5908
|
+
type: "assertWithAI",
|
|
5909
|
+
status: "pass",
|
|
5910
|
+
durationMs: Date.now() - h.stepStart,
|
|
5911
|
+
value: verdict.reason,
|
|
5912
|
+
screenshot: relative
|
|
5913
|
+
}
|
|
5914
|
+
};
|
|
5915
|
+
}
|
|
5916
|
+
throw new Error(`AI assertion failed: ${verdict.reason}`);
|
|
5917
|
+
}
|
|
5918
|
+
},
|
|
4788
5919
|
copyText: {
|
|
4789
5920
|
capabilities: ["query"],
|
|
4790
5921
|
run: async (h) => {
|
|
@@ -4821,7 +5952,7 @@ var STEP_HANDLERS = {
|
|
|
4821
5952
|
`Download filename mismatch: expected "${opts.filename}", got "${suggestedFilename}"`
|
|
4822
5953
|
);
|
|
4823
5954
|
}
|
|
4824
|
-
const savePath =
|
|
5955
|
+
const savePath = import_node_path11.default.join(h.context.runDir, suggestedFilename);
|
|
4825
5956
|
await download.saveAs(savePath);
|
|
4826
5957
|
return {
|
|
4827
5958
|
kind: "result",
|
|
@@ -4850,8 +5981,8 @@ async function executeSteps(context) {
|
|
|
4850
5981
|
maxSteps: context.maxSteps,
|
|
4851
5982
|
selfHealing: context.selfHealing
|
|
4852
5983
|
});
|
|
4853
|
-
const screenshotsDir =
|
|
4854
|
-
|
|
5984
|
+
const screenshotsDir = import_node_path11.default.join(context.runDir, "screenshots");
|
|
5985
|
+
import_node_fs10.default.mkdirSync(screenshotsDir, { recursive: true });
|
|
4855
5986
|
const currentHuntName = context.huntStack?.[context.huntStack.length - 1];
|
|
4856
5987
|
policy.assertWithinMaxSteps(context.steps.length, currentHuntName);
|
|
4857
5988
|
const results = [];
|
|
@@ -4861,7 +5992,7 @@ async function executeSteps(context) {
|
|
|
4861
5992
|
const addScreenshot = async (fileName) => {
|
|
4862
5993
|
const fullPath = screenshotPath(screenshotsDir, fileName);
|
|
4863
5994
|
await captureScreenshot2(driver, fullPath);
|
|
4864
|
-
const relative =
|
|
5995
|
+
const relative = import_node_path11.default.join("screenshots", fileName);
|
|
4865
5996
|
screenshots.push(relative);
|
|
4866
5997
|
return relative;
|
|
4867
5998
|
};
|
|
@@ -4949,12 +6080,12 @@ async function executeSteps(context) {
|
|
|
4949
6080
|
return { results, screenshots, failed: false };
|
|
4950
6081
|
}
|
|
4951
6082
|
async function captureFinalScreenshot(page, runDir) {
|
|
4952
|
-
const screenshotsDir =
|
|
4953
|
-
|
|
6083
|
+
const screenshotsDir = import_node_path11.default.join(runDir, "screenshots");
|
|
6084
|
+
import_node_fs10.default.mkdirSync(screenshotsDir, { recursive: true });
|
|
4954
6085
|
const fileName = "final.png";
|
|
4955
6086
|
const filePath = screenshotPath(screenshotsDir, fileName);
|
|
4956
6087
|
await captureScreenshot2(page, filePath);
|
|
4957
|
-
return
|
|
6088
|
+
return import_node_path11.default.join("screenshots", fileName);
|
|
4958
6089
|
}
|
|
4959
6090
|
|
|
4960
6091
|
// src/runner/assertions.ts
|
|
@@ -5076,6 +6207,59 @@ async function evaluateAssertions(options) {
|
|
|
5076
6207
|
}
|
|
5077
6208
|
return results;
|
|
5078
6209
|
}
|
|
6210
|
+
async function evaluateNativeAssertions(options) {
|
|
6211
|
+
const assertions = mergeAssertions(options.config, options.huntAssertions);
|
|
6212
|
+
const authoredTypes = new Set(
|
|
6213
|
+
(options.huntAssertions ?? []).map((assertion) => Object.keys(assertion)[0])
|
|
6214
|
+
);
|
|
6215
|
+
const results = [];
|
|
6216
|
+
const warnings = [];
|
|
6217
|
+
for (const assertion of assertions) {
|
|
6218
|
+
const type = Object.keys(assertion)[0] ?? "assertion";
|
|
6219
|
+
try {
|
|
6220
|
+
if ("selectorExists" in assertion) {
|
|
6221
|
+
options.assertAllowedSelector?.(assertion.selectorExists);
|
|
6222
|
+
const count = await options.driver.count(assertion.selectorExists);
|
|
6223
|
+
results.push({
|
|
6224
|
+
type: "selectorExists",
|
|
6225
|
+
value: assertion.selectorExists,
|
|
6226
|
+
status: count > 0 ? "pass" : "fail",
|
|
6227
|
+
error: count > 0 ? void 0 : "Selector not found"
|
|
6228
|
+
});
|
|
6229
|
+
continue;
|
|
6230
|
+
}
|
|
6231
|
+
if ("selectorNotExists" in assertion) {
|
|
6232
|
+
options.assertAllowedSelector?.(assertion.selectorNotExists);
|
|
6233
|
+
const count = await options.driver.count(assertion.selectorNotExists);
|
|
6234
|
+
results.push({
|
|
6235
|
+
type: "selectorNotExists",
|
|
6236
|
+
value: assertion.selectorNotExists,
|
|
6237
|
+
status: count === 0 ? "pass" : "fail",
|
|
6238
|
+
error: count === 0 ? void 0 : "Selector exists"
|
|
6239
|
+
});
|
|
6240
|
+
continue;
|
|
6241
|
+
}
|
|
6242
|
+
const rawValue = assertion[type];
|
|
6243
|
+
results.push({
|
|
6244
|
+
type,
|
|
6245
|
+
value: rawValue,
|
|
6246
|
+
status: "skipped",
|
|
6247
|
+
error: `skipped (web-only): not supported on the ${options.targetLabel} target`
|
|
6248
|
+
});
|
|
6249
|
+
if (authoredTypes.has(type)) {
|
|
6250
|
+
warnings.push(`${type} is web-only; skipped on ${options.targetLabel} target`);
|
|
6251
|
+
}
|
|
6252
|
+
} catch (error) {
|
|
6253
|
+
const detail = error instanceof Error ? error.message : "no error details";
|
|
6254
|
+
results.push({
|
|
6255
|
+
type,
|
|
6256
|
+
status: "fail",
|
|
6257
|
+
error: `Native assertion "${type}" failed: ${detail}`
|
|
6258
|
+
});
|
|
6259
|
+
}
|
|
6260
|
+
}
|
|
6261
|
+
return { results, warnings };
|
|
6262
|
+
}
|
|
5079
6263
|
|
|
5080
6264
|
// src/runner/tracing.ts
|
|
5081
6265
|
var DEFAULT_TRACE_HEADER = "traceparent";
|
|
@@ -5114,18 +6298,18 @@ function captureTraceCorrelation(response, headerName, sink, redactionValues = [
|
|
|
5114
6298
|
}
|
|
5115
6299
|
|
|
5116
6300
|
// src/reporter/result.ts
|
|
5117
|
-
var
|
|
5118
|
-
var
|
|
6301
|
+
var import_node_fs11 = __toESM(require("fs"), 1);
|
|
6302
|
+
var import_node_path12 = __toESM(require("path"), 1);
|
|
5119
6303
|
function writeResult(runDir, result) {
|
|
5120
6304
|
const fileName = "result.json";
|
|
5121
|
-
const fullPath =
|
|
5122
|
-
|
|
6305
|
+
const fullPath = import_node_path12.default.join(runDir, fileName);
|
|
6306
|
+
import_node_fs11.default.writeFileSync(fullPath, JSON.stringify(result, null, 2));
|
|
5123
6307
|
return fileName;
|
|
5124
6308
|
}
|
|
5125
6309
|
|
|
5126
6310
|
// src/reporter/summary.ts
|
|
5127
|
-
var
|
|
5128
|
-
var
|
|
6311
|
+
var import_node_fs12 = __toESM(require("fs"), 1);
|
|
6312
|
+
var import_node_path13 = __toESM(require("path"), 1);
|
|
5129
6313
|
function escapeMd(text) {
|
|
5130
6314
|
return text.replace(/([|`*_{}[\]()#+\-!\\])/g, "\\$1");
|
|
5131
6315
|
}
|
|
@@ -5203,28 +6387,29 @@ function writeSummary(runDir, result) {
|
|
|
5203
6387
|
}
|
|
5204
6388
|
}
|
|
5205
6389
|
const fileName = "summary.md";
|
|
5206
|
-
const fullPath =
|
|
5207
|
-
|
|
6390
|
+
const fullPath = import_node_path13.default.join(runDir, fileName);
|
|
6391
|
+
import_node_fs12.default.writeFileSync(fullPath, `${lines.join("\n")}
|
|
5208
6392
|
`);
|
|
5209
6393
|
return fileName;
|
|
5210
6394
|
}
|
|
5211
6395
|
|
|
5212
6396
|
// src/reporter/junit.ts
|
|
5213
|
-
var
|
|
5214
|
-
var
|
|
6397
|
+
var import_node_fs13 = __toESM(require("fs"), 1);
|
|
6398
|
+
var import_node_path14 = __toESM(require("path"), 1);
|
|
5215
6399
|
function escapeXml(text) {
|
|
5216
6400
|
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
5217
6401
|
}
|
|
5218
6402
|
function writeJunit(runDir, result) {
|
|
5219
6403
|
const totalTests = result.steps.length + result.assertions.length;
|
|
5220
6404
|
const failures = result.steps.filter((s) => s.status === "fail").length + result.assertions.filter((a) => a.status === "fail").length;
|
|
6405
|
+
const skipped = result.assertions.filter((a) => a.status === "skipped").length;
|
|
5221
6406
|
const timeSeconds = (result.durationMs / 1e3).toFixed(3);
|
|
5222
6407
|
const huntName = escapeXml(result.hunt);
|
|
5223
6408
|
const lines = [];
|
|
5224
6409
|
lines.push('<?xml version="1.0" encoding="UTF-8"?>');
|
|
5225
6410
|
lines.push("<testsuites>");
|
|
5226
6411
|
lines.push(
|
|
5227
|
-
` <testsuite name="${huntName}" tests="${totalTests}" failures="${failures}" errors="0" time="${timeSeconds}" timestamp="${escapeXml(result.startedAt)}">`
|
|
6412
|
+
` <testsuite name="${huntName}" tests="${totalTests}" failures="${failures}" errors="0" skipped="${skipped}" time="${timeSeconds}" timestamp="${escapeXml(result.startedAt)}">`
|
|
5228
6413
|
);
|
|
5229
6414
|
for (let i = 0; i < result.steps.length; i++) {
|
|
5230
6415
|
const step = result.steps[i];
|
|
@@ -5248,6 +6433,11 @@ function writeJunit(runDir, result) {
|
|
|
5248
6433
|
lines.push(` <testcase name="${caseName}" classname="${huntName}" time="0">`);
|
|
5249
6434
|
lines.push(` <failure message="${escapedFailureText}" type="assertion">${escapedFailureText}</failure>`);
|
|
5250
6435
|
lines.push(" </testcase>");
|
|
6436
|
+
} else if (assertion.status === "skipped") {
|
|
6437
|
+
const skipText = escapeXml(assertion.error ?? "skipped");
|
|
6438
|
+
lines.push(` <testcase name="${caseName}" classname="${huntName}" time="0">`);
|
|
6439
|
+
lines.push(` <skipped message="${skipText}"/>`);
|
|
6440
|
+
lines.push(" </testcase>");
|
|
5251
6441
|
} else {
|
|
5252
6442
|
lines.push(` <testcase name="${caseName}" classname="${huntName}" time="0"/>`);
|
|
5253
6443
|
}
|
|
@@ -5255,8 +6445,8 @@ function writeJunit(runDir, result) {
|
|
|
5255
6445
|
lines.push(" </testsuite>");
|
|
5256
6446
|
lines.push("</testsuites>");
|
|
5257
6447
|
const fileName = "junit.xml";
|
|
5258
|
-
const fullPath =
|
|
5259
|
-
|
|
6448
|
+
const fullPath = import_node_path14.default.join(runDir, fileName);
|
|
6449
|
+
import_node_fs13.default.writeFileSync(fullPath, `${lines.join("\n")}
|
|
5260
6450
|
`);
|
|
5261
6451
|
return fileName;
|
|
5262
6452
|
}
|
|
@@ -5290,15 +6480,15 @@ function timestamp(prefix) {
|
|
|
5290
6480
|
}
|
|
5291
6481
|
|
|
5292
6482
|
// src/runner/history.ts
|
|
5293
|
-
var
|
|
5294
|
-
var
|
|
6483
|
+
var import_node_fs14 = __toESM(require("fs"), 1);
|
|
6484
|
+
var import_node_path15 = __toESM(require("path"), 1);
|
|
5295
6485
|
var HISTORY_FILE = "history.json";
|
|
5296
6486
|
var LOCK_FILE_SUFFIX = ".lock";
|
|
5297
6487
|
var LOCK_RETRY_MS = 10;
|
|
5298
6488
|
var LOCK_TIMEOUT_MS = 5e3;
|
|
5299
6489
|
var SLEEP_BUFFER = new Int32Array(new SharedArrayBuffer(4));
|
|
5300
6490
|
function historyPath(configDir) {
|
|
5301
|
-
return
|
|
6491
|
+
return import_node_path15.default.join(configDir, HISTORY_FILE);
|
|
5302
6492
|
}
|
|
5303
6493
|
function isHistoryEntry(value) {
|
|
5304
6494
|
if (!value || typeof value !== "object") {
|
|
@@ -5309,11 +6499,11 @@ function isHistoryEntry(value) {
|
|
|
5309
6499
|
}
|
|
5310
6500
|
function readHistory(configDir) {
|
|
5311
6501
|
const filePath = historyPath(configDir);
|
|
5312
|
-
if (!
|
|
6502
|
+
if (!import_node_fs14.default.existsSync(filePath)) {
|
|
5313
6503
|
return { entries: [] };
|
|
5314
6504
|
}
|
|
5315
6505
|
try {
|
|
5316
|
-
const raw =
|
|
6506
|
+
const raw = import_node_fs14.default.readFileSync(filePath, "utf-8");
|
|
5317
6507
|
const parsed = JSON.parse(raw);
|
|
5318
6508
|
if (parsed && typeof parsed === "object" && "entries" in parsed && Array.isArray(parsed.entries)) {
|
|
5319
6509
|
const validatedEntries = parsed.entries.filter(isHistoryEntry);
|
|
@@ -5352,12 +6542,12 @@ function sleepSync(ms) {
|
|
|
5352
6542
|
function withHistoryLock(configDir, fn) {
|
|
5353
6543
|
const filePath = historyPath(configDir);
|
|
5354
6544
|
const lockPath = `${filePath}${LOCK_FILE_SUFFIX}`;
|
|
5355
|
-
|
|
6545
|
+
import_node_fs14.default.mkdirSync(import_node_path15.default.dirname(filePath), { recursive: true });
|
|
5356
6546
|
const startedAt = Date.now();
|
|
5357
6547
|
while (Date.now() - startedAt < LOCK_TIMEOUT_MS) {
|
|
5358
6548
|
let fd;
|
|
5359
6549
|
try {
|
|
5360
|
-
fd =
|
|
6550
|
+
fd = import_node_fs14.default.openSync(lockPath, "wx");
|
|
5361
6551
|
} catch (error) {
|
|
5362
6552
|
if (error.code === "EEXIST") {
|
|
5363
6553
|
sleepSync(LOCK_RETRY_MS);
|
|
@@ -5369,10 +6559,10 @@ function withHistoryLock(configDir, fn) {
|
|
|
5369
6559
|
return fn();
|
|
5370
6560
|
} finally {
|
|
5371
6561
|
try {
|
|
5372
|
-
|
|
6562
|
+
import_node_fs14.default.closeSync(fd);
|
|
5373
6563
|
} catch {
|
|
5374
6564
|
}
|
|
5375
|
-
|
|
6565
|
+
import_node_fs14.default.rmSync(lockPath, { force: true });
|
|
5376
6566
|
}
|
|
5377
6567
|
}
|
|
5378
6568
|
throw new Error(
|
|
@@ -5385,9 +6575,9 @@ function appendEntry(configDir, entry, maxRuns) {
|
|
|
5385
6575
|
const current = readHistory(configDir);
|
|
5386
6576
|
const next = pruneEntries([...current.entries, entry], maxRuns);
|
|
5387
6577
|
const tempPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
5388
|
-
|
|
6578
|
+
import_node_fs14.default.writeFileSync(tempPath, `${JSON.stringify({ entries: next }, null, 2)}
|
|
5389
6579
|
`);
|
|
5390
|
-
|
|
6580
|
+
import_node_fs14.default.renameSync(tempPath, filePath);
|
|
5391
6581
|
});
|
|
5392
6582
|
}
|
|
5393
6583
|
|
|
@@ -5400,11 +6590,11 @@ function parseViewportFlag(value) {
|
|
|
5400
6590
|
return value;
|
|
5401
6591
|
}
|
|
5402
6592
|
function resolvePath(configDir, inputPath) {
|
|
5403
|
-
if (
|
|
6593
|
+
if (import_node_path16.default.isAbsolute(inputPath)) {
|
|
5404
6594
|
return inputPath;
|
|
5405
6595
|
}
|
|
5406
|
-
const projectRoot =
|
|
5407
|
-
return
|
|
6596
|
+
const projectRoot = import_node_path16.default.dirname(configDir);
|
|
6597
|
+
return import_node_path16.default.join(projectRoot, inputPath);
|
|
5408
6598
|
}
|
|
5409
6599
|
function buildRunResult(options) {
|
|
5410
6600
|
return {
|
|
@@ -5423,12 +6613,12 @@ function buildRunResult(options) {
|
|
|
5423
6613
|
}
|
|
5424
6614
|
function writeConsoleLog(runDir, entries) {
|
|
5425
6615
|
const fileName = "console.log";
|
|
5426
|
-
const filePath =
|
|
6616
|
+
const filePath = import_node_path16.default.join(runDir, fileName);
|
|
5427
6617
|
const lines = entries.map((entry) => {
|
|
5428
6618
|
const location = entry.location ? ` (${entry.location})` : "";
|
|
5429
6619
|
return `[${entry.type}] ${entry.text}${location}`;
|
|
5430
6620
|
});
|
|
5431
|
-
|
|
6621
|
+
import_node_fs15.default.writeFileSync(filePath, `${lines.join("\n")}
|
|
5432
6622
|
`);
|
|
5433
6623
|
return fileName;
|
|
5434
6624
|
}
|
|
@@ -5436,8 +6626,8 @@ async function executeHuntAttempt(options, config, configDir, interpolatedHunt,
|
|
|
5436
6626
|
const headless = options.headed ? false : config.browser.headless;
|
|
5437
6627
|
const slowMo = options.slowMo ?? config.browser.slowMo;
|
|
5438
6628
|
const maxSteps = config.guardrails.maxSteps;
|
|
5439
|
-
const runDir =
|
|
5440
|
-
|
|
6629
|
+
const runDir = import_node_path16.default.join(configDir, "runs", timestamp());
|
|
6630
|
+
import_node_fs15.default.mkdirSync(runDir, { recursive: true });
|
|
5441
6631
|
const storageStatePath = config.auth.storageStatePath ? resolvePath(configDir, config.auth.storageStatePath) : void 0;
|
|
5442
6632
|
const engine = options.browser ?? config.browser.engine;
|
|
5443
6633
|
const channel = options.channel ?? config.browser.channel;
|
|
@@ -5620,8 +6810,8 @@ async function runHunt(options) {
|
|
|
5620
6810
|
}
|
|
5621
6811
|
async function executeNativeHuntAttempt(options, config, configDir, interpolatedHunt, redactedFillSteps, randomVars, allowedApps, native) {
|
|
5622
6812
|
const maxSteps = config.guardrails.maxSteps;
|
|
5623
|
-
const runDir =
|
|
5624
|
-
|
|
6813
|
+
const runDir = import_node_path16.default.join(configDir, "runs", timestamp());
|
|
6814
|
+
import_node_fs15.default.mkdirSync(runDir, { recursive: true });
|
|
5625
6815
|
const session = await native.launchSession();
|
|
5626
6816
|
let result;
|
|
5627
6817
|
try {
|
|
@@ -5629,6 +6819,13 @@ async function executeNativeHuntAttempt(options, config, configDir, interpolated
|
|
|
5629
6819
|
const appIdentity = native.sessionAppIdentity(session);
|
|
5630
6820
|
const targetLabel = `${native.targetType}:${appIdentity}`;
|
|
5631
6821
|
const effectiveAllowedApps = [.../* @__PURE__ */ new Set([...allowedApps, native.targetApp, appIdentity])];
|
|
6822
|
+
const assertionPolicy = createRunPolicy(driver, {
|
|
6823
|
+
forbiddenSelectors: config.guardrails.forbiddenSelectors,
|
|
6824
|
+
allowedDomains: [],
|
|
6825
|
+
allowedApps: effectiveAllowedApps,
|
|
6826
|
+
maxSteps,
|
|
6827
|
+
selfHealing: config.guardrails.selfHealing
|
|
6828
|
+
});
|
|
5632
6829
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
5633
6830
|
const startTime = Date.now();
|
|
5634
6831
|
let stepResults = [];
|
|
@@ -5668,8 +6865,19 @@ async function executeNativeHuntAttempt(options, config, configDir, interpolated
|
|
|
5668
6865
|
} catch {
|
|
5669
6866
|
finalScreenshot = void 0;
|
|
5670
6867
|
}
|
|
6868
|
+
const { results: assertionResults, warnings: assertionWarnings } = await evaluateNativeAssertions({
|
|
6869
|
+
driver,
|
|
6870
|
+
config,
|
|
6871
|
+
huntAssertions: interpolatedHunt.assertions,
|
|
6872
|
+
assertAllowedSelector: assertionPolicy.assertAllowedSelector,
|
|
6873
|
+
targetLabel: nativeTargetLabel(native.targetType)
|
|
6874
|
+
});
|
|
6875
|
+
for (const warning of assertionWarnings) {
|
|
6876
|
+
console.warn(warning);
|
|
6877
|
+
}
|
|
5671
6878
|
const durationMs = Date.now() - startTime;
|
|
5672
|
-
const
|
|
6879
|
+
const assertionsFailed = assertionResults.some((assertion) => assertion.status === "fail");
|
|
6880
|
+
const status = stepFailed || assertionsFailed ? "fail" : "pass";
|
|
5673
6881
|
const artifacts = {
|
|
5674
6882
|
screenshots: finalScreenshot ? [...stepScreenshots, finalScreenshot] : stepScreenshots
|
|
5675
6883
|
};
|
|
@@ -5680,7 +6888,7 @@ async function executeNativeHuntAttempt(options, config, configDir, interpolated
|
|
|
5680
6888
|
hunt: options.huntName,
|
|
5681
6889
|
targetUrl: targetLabel,
|
|
5682
6890
|
steps: stepResults,
|
|
5683
|
-
assertions:
|
|
6891
|
+
assertions: assertionResults,
|
|
5684
6892
|
artifacts
|
|
5685
6893
|
});
|
|
5686
6894
|
result = writeReports(runDir, runResult, { junit: options.junit ?? config.artifacts.junit });
|
|
@@ -5793,7 +7001,6 @@ async function runNativeHunt(options, config, configDir, target, native) {
|
|
|
5793
7001
|
const hunt = loadHunt(options.huntName, configDir);
|
|
5794
7002
|
const { hunt: interpolatedHunt, redactedFillSteps, randomVars } = interpolateHunt(hunt, process.env);
|
|
5795
7003
|
assertStepsSupportedByTarget(interpolatedHunt.steps, native.targetType);
|
|
5796
|
-
assertHuntAssertionsSupportedByTarget(interpolatedHunt.assertions, native.targetType);
|
|
5797
7004
|
native.assertAppAllowed(config.guardrails.allowedApps, target);
|
|
5798
7005
|
const maxSteps = config.guardrails.maxSteps;
|
|
5799
7006
|
if (interpolatedHunt.steps.length > maxSteps) {
|
|
@@ -5834,7 +7041,7 @@ async function runNativeHunt(options, config, configDir, target, native) {
|
|
|
5834
7041
|
}
|
|
5835
7042
|
function recordHistory(configDir, outcome, maxRuns) {
|
|
5836
7043
|
try {
|
|
5837
|
-
const relativeRunDir =
|
|
7044
|
+
const relativeRunDir = import_node_path16.default.relative(configDir, outcome.runDir);
|
|
5838
7045
|
appendEntry(
|
|
5839
7046
|
configDir,
|
|
5840
7047
|
{
|
|
@@ -5851,11 +7058,11 @@ function recordHistory(configDir, outcome, maxRuns) {
|
|
|
5851
7058
|
}
|
|
5852
7059
|
|
|
5853
7060
|
// src/runner/suite.ts
|
|
5854
|
-
var
|
|
7061
|
+
var import_node_path19 = __toESM(require("path"), 1);
|
|
5855
7062
|
|
|
5856
7063
|
// src/reporter/ci-summary.ts
|
|
5857
|
-
var
|
|
5858
|
-
var
|
|
7064
|
+
var import_node_fs16 = __toESM(require("fs"), 1);
|
|
7065
|
+
var import_node_path17 = __toESM(require("path"), 1);
|
|
5859
7066
|
var import_chalk = __toESM(require("chalk"), 1);
|
|
5860
7067
|
function countCiResults(results) {
|
|
5861
7068
|
return {
|
|
@@ -5885,9 +7092,9 @@ function writeCiResult(ciRunDir, results, startedAt, totalDurationMs, flaky = []
|
|
|
5885
7092
|
...flaky.length > 0 ? { flaky } : {},
|
|
5886
7093
|
...clusters.length > 0 ? { clusters } : {}
|
|
5887
7094
|
};
|
|
5888
|
-
|
|
5889
|
-
const filePath =
|
|
5890
|
-
|
|
7095
|
+
import_node_fs16.default.mkdirSync(ciRunDir, { recursive: true });
|
|
7096
|
+
const filePath = import_node_path17.default.join(ciRunDir, "ci-result.json");
|
|
7097
|
+
import_node_fs16.default.writeFileSync(filePath, JSON.stringify(ciResult, null, 2) + "\n");
|
|
5891
7098
|
return filePath;
|
|
5892
7099
|
}
|
|
5893
7100
|
|
|
@@ -5954,7 +7161,7 @@ function rankFlaky(configDir, options = {}) {
|
|
|
5954
7161
|
}
|
|
5955
7162
|
|
|
5956
7163
|
// src/backlog/fingerprint.ts
|
|
5957
|
-
var
|
|
7164
|
+
var import_node_crypto3 = require("crypto");
|
|
5958
7165
|
function normalizeError(error) {
|
|
5959
7166
|
return error.toLowerCase().replace(/0x[0-9a-f]+/g, "").replace(/\b\d+(?:\.\d+)?\s*(?:ms|s|px)\b/g, "").replace(/\s+/g, " ").trim();
|
|
5960
7167
|
}
|
|
@@ -5977,7 +7184,7 @@ function computeFingerprint(failure) {
|
|
|
5977
7184
|
failure.selector ?? "",
|
|
5978
7185
|
normalizeError(failure.error)
|
|
5979
7186
|
];
|
|
5980
|
-
return (0,
|
|
7187
|
+
return (0, import_node_crypto3.createHash)("sha1").update(parts.join("|")).digest("hex").slice(0, 8);
|
|
5981
7188
|
}
|
|
5982
7189
|
function buildMarker(failure, hash) {
|
|
5983
7190
|
const hunt = sanitizeMarkerValue(failure.hunt);
|
|
@@ -6021,8 +7228,8 @@ function clusterFailures(failures) {
|
|
|
6021
7228
|
}
|
|
6022
7229
|
|
|
6023
7230
|
// src/backlog/index.ts
|
|
6024
|
-
var
|
|
6025
|
-
var
|
|
7231
|
+
var import_node_fs17 = __toESM(require("fs"), 1);
|
|
7232
|
+
var import_node_path18 = __toESM(require("path"), 1);
|
|
6026
7233
|
|
|
6027
7234
|
// src/backlog/parse.ts
|
|
6028
7235
|
var MARKER_FP = /<!--\s*prowl:fp=([0-9a-f]+)/;
|
|
@@ -6112,7 +7319,7 @@ ${after}`;
|
|
|
6112
7319
|
// src/backlog/index.ts
|
|
6113
7320
|
function readFileOrEmpty(filePath) {
|
|
6114
7321
|
try {
|
|
6115
|
-
return
|
|
7322
|
+
return import_node_fs17.default.readFileSync(filePath, "utf-8");
|
|
6116
7323
|
} catch (error) {
|
|
6117
7324
|
const err = error;
|
|
6118
7325
|
if (err.code === "ENOENT") return "";
|
|
@@ -6128,7 +7335,7 @@ function buildFailure(hunt) {
|
|
|
6128
7335
|
if (!hunt.runDir) return failure;
|
|
6129
7336
|
let run;
|
|
6130
7337
|
try {
|
|
6131
|
-
const resultJson = readFileOrEmpty(
|
|
7338
|
+
const resultJson = readFileOrEmpty(import_node_path18.default.join(hunt.runDir, "result.json"));
|
|
6132
7339
|
if (!resultJson) return failure;
|
|
6133
7340
|
run = JSON.parse(resultJson);
|
|
6134
7341
|
} catch (error) {
|
|
@@ -6157,8 +7364,8 @@ function extractFailures(suiteResult) {
|
|
|
6157
7364
|
}
|
|
6158
7365
|
function updateBacklogFromSuite(suiteResult, options = {}) {
|
|
6159
7366
|
const projectRoot = options.projectRoot ?? process.cwd();
|
|
6160
|
-
const backlogPath = options.backlogPath ??
|
|
6161
|
-
const resolvedPath = options.resolvedPath ??
|
|
7367
|
+
const backlogPath = options.backlogPath ?? import_node_path18.default.join(projectRoot, "docs", "backlog.md");
|
|
7368
|
+
const resolvedPath = options.resolvedPath ?? import_node_path18.default.join(projectRoot, "docs", "resolved.md");
|
|
6162
7369
|
const date = options.date ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
6163
7370
|
const summary = { created: [], regressions: [], skipped: [], backlogPath };
|
|
6164
7371
|
const failures = extractFailures(suiteResult);
|
|
@@ -6190,8 +7397,8 @@ function updateBacklogFromSuite(suiteResult, options = {}) {
|
|
|
6190
7397
|
}
|
|
6191
7398
|
}
|
|
6192
7399
|
if (ticketsToAdd.length > 0) {
|
|
6193
|
-
|
|
6194
|
-
|
|
7400
|
+
import_node_fs17.default.mkdirSync(import_node_path18.default.dirname(backlogPath), { recursive: true });
|
|
7401
|
+
import_node_fs17.default.writeFileSync(backlogPath, insertTickets(backlogContent, ticketsToAdd));
|
|
6195
7402
|
}
|
|
6196
7403
|
return summary;
|
|
6197
7404
|
}
|
|
@@ -6352,7 +7559,7 @@ async function runSuite(options = {}) {
|
|
|
6352
7559
|
const clusters = clusterFailures(
|
|
6353
7560
|
extractFailures({ result: { hunts: results }, resultPath: null })
|
|
6354
7561
|
).filter((cluster) => cluster.count > 1);
|
|
6355
|
-
const ciRunDir =
|
|
7562
|
+
const ciRunDir = import_node_path19.default.join(configDir, "runs", timestamp("ci"));
|
|
6356
7563
|
const resultPath = writeCiResult(ciRunDir, results, startedAt, totalDurationMs, flaky, clusters);
|
|
6357
7564
|
const { passed, failed, skipped } = countCiResults(results);
|
|
6358
7565
|
return {
|
|
@@ -6615,6 +7822,273 @@ async function readStatusMenu(client, menuTimeoutSeconds) {
|
|
|
6615
7822
|
}
|
|
6616
7823
|
}
|
|
6617
7824
|
|
|
7825
|
+
// src/analyzer/android.ts
|
|
7826
|
+
var ANDROID_INTERACTIVE_CLASSES = /* @__PURE__ */ new Set([
|
|
7827
|
+
"android.widget.Button",
|
|
7828
|
+
"android.widget.ImageButton",
|
|
7829
|
+
"android.widget.EditText",
|
|
7830
|
+
"android.widget.CheckBox",
|
|
7831
|
+
"android.widget.RadioButton",
|
|
7832
|
+
"android.widget.Switch",
|
|
7833
|
+
"android.widget.ToggleButton",
|
|
7834
|
+
"android.widget.Spinner",
|
|
7835
|
+
"android.widget.SeekBar",
|
|
7836
|
+
"android.widget.RatingBar",
|
|
7837
|
+
"android.widget.CompoundButton",
|
|
7838
|
+
"android.widget.AutoCompleteTextView",
|
|
7839
|
+
"android.widget.MultiAutoCompleteTextView",
|
|
7840
|
+
"android.widget.CheckedTextView",
|
|
7841
|
+
"androidx.appcompat.widget.SwitchCompat",
|
|
7842
|
+
"androidx.appcompat.widget.AppCompatButton",
|
|
7843
|
+
"androidx.appcompat.widget.AppCompatEditText"
|
|
7844
|
+
]);
|
|
7845
|
+
function str2(value) {
|
|
7846
|
+
if (value === void 0) {
|
|
7847
|
+
return void 0;
|
|
7848
|
+
}
|
|
7849
|
+
const trimmed = value.trim();
|
|
7850
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
7851
|
+
}
|
|
7852
|
+
function bool(value) {
|
|
7853
|
+
if (value === void 0) {
|
|
7854
|
+
return void 0;
|
|
7855
|
+
}
|
|
7856
|
+
return value === "true";
|
|
7857
|
+
}
|
|
7858
|
+
function toAndroidNode(element) {
|
|
7859
|
+
const a = element.attrs;
|
|
7860
|
+
return {
|
|
7861
|
+
className: str2(a["class"]),
|
|
7862
|
+
resourceId: str2(a["resource-id"]),
|
|
7863
|
+
contentDesc: str2(a["content-desc"]),
|
|
7864
|
+
text: str2(a.text),
|
|
7865
|
+
package: str2(a.package),
|
|
7866
|
+
clickable: bool(a.clickable),
|
|
7867
|
+
longClickable: bool(a["long-clickable"]),
|
|
7868
|
+
checkable: bool(a.checkable),
|
|
7869
|
+
checked: bool(a.checked),
|
|
7870
|
+
scrollable: bool(a.scrollable),
|
|
7871
|
+
focusable: bool(a.focusable),
|
|
7872
|
+
focused: bool(a.focused),
|
|
7873
|
+
enabled: bool(a.enabled),
|
|
7874
|
+
children: element.children.map(toAndroidNode)
|
|
7875
|
+
};
|
|
7876
|
+
}
|
|
7877
|
+
function parseAndroidHierarchy(xml) {
|
|
7878
|
+
const root = parseXml(xml);
|
|
7879
|
+
return root ? toAndroidNode(root) : null;
|
|
7880
|
+
}
|
|
7881
|
+
function isAndroidInteractive(node) {
|
|
7882
|
+
if (node.clickable || node.longClickable || node.checkable || node.scrollable) {
|
|
7883
|
+
return true;
|
|
7884
|
+
}
|
|
7885
|
+
return node.className !== void 0 && ANDROID_INTERACTIVE_CLASSES.has(node.className);
|
|
7886
|
+
}
|
|
7887
|
+
function rankAndroidSelectors(node) {
|
|
7888
|
+
return rankNativeSelectors({
|
|
7889
|
+
id: node.resourceId,
|
|
7890
|
+
label: node.contentDesc,
|
|
7891
|
+
role: node.className,
|
|
7892
|
+
name: node.text
|
|
7893
|
+
});
|
|
7894
|
+
}
|
|
7895
|
+
function androidNodeToNative(node) {
|
|
7896
|
+
return {
|
|
7897
|
+
...node.resourceId !== void 0 ? { id: node.resourceId } : {},
|
|
7898
|
+
...node.contentDesc !== void 0 ? { label: node.contentDesc } : {},
|
|
7899
|
+
...node.className !== void 0 ? { role: node.className } : {},
|
|
7900
|
+
textValues: node.text !== void 0 ? [node.text] : [],
|
|
7901
|
+
...node.focused !== void 0 ? { focused: node.focused } : {}
|
|
7902
|
+
};
|
|
7903
|
+
}
|
|
7904
|
+
function matchAndroidSelector(xml, selector, options = {}) {
|
|
7905
|
+
const parsedSelector = parseNativeSelector(selector);
|
|
7906
|
+
const root = parseAndroidHierarchy(xml);
|
|
7907
|
+
if (!root) {
|
|
7908
|
+
return [];
|
|
7909
|
+
}
|
|
7910
|
+
return matchNativeTree(
|
|
7911
|
+
ANDROID_MATCH_DIALECT,
|
|
7912
|
+
parsedSelector,
|
|
7913
|
+
root,
|
|
7914
|
+
androidNodeToNative,
|
|
7915
|
+
(node) => node.children,
|
|
7916
|
+
options
|
|
7917
|
+
);
|
|
7918
|
+
}
|
|
7919
|
+
function toElement2(node) {
|
|
7920
|
+
return {
|
|
7921
|
+
className: node.className ?? "?",
|
|
7922
|
+
...node.resourceId ? { resourceId: node.resourceId } : {},
|
|
7923
|
+
...node.contentDesc ? { contentDesc: node.contentDesc } : {},
|
|
7924
|
+
...node.text ? { text: node.text } : {},
|
|
7925
|
+
...node.clickable !== void 0 ? { clickable: node.clickable } : {},
|
|
7926
|
+
...node.checkable !== void 0 ? { checkable: node.checkable } : {},
|
|
7927
|
+
...node.scrollable !== void 0 ? { scrollable: node.scrollable } : {},
|
|
7928
|
+
...node.enabled !== void 0 ? { enabled: node.enabled } : {},
|
|
7929
|
+
selectors: rankAndroidSelectors(node)
|
|
7930
|
+
};
|
|
7931
|
+
}
|
|
7932
|
+
function collectInteractive2(root) {
|
|
7933
|
+
const out = [];
|
|
7934
|
+
const visit = (node) => {
|
|
7935
|
+
if (isAndroidInteractive(node)) {
|
|
7936
|
+
out.push(toElement2(node));
|
|
7937
|
+
}
|
|
7938
|
+
for (const child of node.children) {
|
|
7939
|
+
visit(child);
|
|
7940
|
+
}
|
|
7941
|
+
};
|
|
7942
|
+
visit(root);
|
|
7943
|
+
return out;
|
|
7944
|
+
}
|
|
7945
|
+
async function analyzeAndroidApp(client, options) {
|
|
7946
|
+
const xml = await client.source();
|
|
7947
|
+
const root = parseAndroidHierarchy(xml);
|
|
7948
|
+
const elements = root ? collectInteractive2(root) : [];
|
|
7949
|
+
return { app: options.app, elements };
|
|
7950
|
+
}
|
|
7951
|
+
|
|
7952
|
+
// src/analyzer/ios.ts
|
|
7953
|
+
var IOS_INTERACTIVE_TYPES = /* @__PURE__ */ new Set([
|
|
7954
|
+
"XCUIElementTypeButton",
|
|
7955
|
+
"XCUIElementTypeCell",
|
|
7956
|
+
"XCUIElementTypeTextField",
|
|
7957
|
+
"XCUIElementTypeSecureTextField",
|
|
7958
|
+
"XCUIElementTypeSearchField",
|
|
7959
|
+
"XCUIElementTypeSwitch",
|
|
7960
|
+
"XCUIElementTypeToggle",
|
|
7961
|
+
"XCUIElementTypeLink",
|
|
7962
|
+
"XCUIElementTypeMenuItem",
|
|
7963
|
+
"XCUIElementTypeSlider",
|
|
7964
|
+
"XCUIElementTypeStepper",
|
|
7965
|
+
"XCUIElementTypeTextView",
|
|
7966
|
+
"XCUIElementTypePickerWheel",
|
|
7967
|
+
"XCUIElementTypeTab",
|
|
7968
|
+
"XCUIElementTypeSegmentedControl",
|
|
7969
|
+
"XCUIElementTypeCheckBox",
|
|
7970
|
+
"XCUIElementTypeRadioButton",
|
|
7971
|
+
"XCUIElementTypeKey"
|
|
7972
|
+
]);
|
|
7973
|
+
var IOS_WINDOW_TYPE = "XCUIElementTypeWindow";
|
|
7974
|
+
function str3(value) {
|
|
7975
|
+
if (value === void 0) {
|
|
7976
|
+
return void 0;
|
|
7977
|
+
}
|
|
7978
|
+
const trimmed = value.trim();
|
|
7979
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
7980
|
+
}
|
|
7981
|
+
function bool2(value) {
|
|
7982
|
+
if (value === void 0) {
|
|
7983
|
+
return void 0;
|
|
7984
|
+
}
|
|
7985
|
+
return value === "true" || value === "1";
|
|
7986
|
+
}
|
|
7987
|
+
function toIosNode(element) {
|
|
7988
|
+
const a = element.attrs;
|
|
7989
|
+
return {
|
|
7990
|
+
type: str3(a.type) ?? str3(element.tag),
|
|
7991
|
+
name: str3(a.name),
|
|
7992
|
+
label: str3(a.label),
|
|
7993
|
+
value: str3(a.value),
|
|
7994
|
+
enabled: bool2(a.enabled),
|
|
7995
|
+
visible: bool2(a.visible),
|
|
7996
|
+
children: element.children.map(toIosNode)
|
|
7997
|
+
};
|
|
7998
|
+
}
|
|
7999
|
+
function parseIosHierarchy(xml) {
|
|
8000
|
+
const root = parseXml(xml);
|
|
8001
|
+
return root ? toIosNode(root) : null;
|
|
8002
|
+
}
|
|
8003
|
+
function hasAccessibilityId(node) {
|
|
8004
|
+
return node.name !== void 0 && node.name !== node.label;
|
|
8005
|
+
}
|
|
8006
|
+
function rankIosSelectors(node) {
|
|
8007
|
+
return rankNativeSelectors({
|
|
8008
|
+
id: hasAccessibilityId(node) ? node.name : void 0,
|
|
8009
|
+
label: node.label,
|
|
8010
|
+
role: node.type ? shortIosType(node.type) : void 0,
|
|
8011
|
+
name: node.label ?? node.value
|
|
8012
|
+
});
|
|
8013
|
+
}
|
|
8014
|
+
function iosNodeToNative(node) {
|
|
8015
|
+
const textValues = [];
|
|
8016
|
+
if (node.label !== void 0) {
|
|
8017
|
+
textValues.push(node.label);
|
|
8018
|
+
}
|
|
8019
|
+
if (node.value !== void 0) {
|
|
8020
|
+
textValues.push(node.value);
|
|
8021
|
+
}
|
|
8022
|
+
return {
|
|
8023
|
+
...node.name !== void 0 ? { id: node.name } : {},
|
|
8024
|
+
...node.label !== void 0 ? { label: node.label } : {},
|
|
8025
|
+
...node.type !== void 0 ? { role: node.type } : {},
|
|
8026
|
+
textValues
|
|
8027
|
+
};
|
|
8028
|
+
}
|
|
8029
|
+
function matchIosSelector(xml, selector) {
|
|
8030
|
+
const parsedSelector = parseNativeSelector(selector);
|
|
8031
|
+
const root = parseIosHierarchy(xml);
|
|
8032
|
+
if (!root) {
|
|
8033
|
+
return [];
|
|
8034
|
+
}
|
|
8035
|
+
return matchNativeTree(
|
|
8036
|
+
IOS_MATCH_DIALECT,
|
|
8037
|
+
parsedSelector,
|
|
8038
|
+
root,
|
|
8039
|
+
iosNodeToNative,
|
|
8040
|
+
(node) => node.children
|
|
8041
|
+
);
|
|
8042
|
+
}
|
|
8043
|
+
function toElement3(node) {
|
|
8044
|
+
return {
|
|
8045
|
+
type: node.type ?? "?",
|
|
8046
|
+
...node.name ? { name: node.name } : {},
|
|
8047
|
+
...node.label ? { label: node.label } : {},
|
|
8048
|
+
...node.value ? { value: node.value } : {},
|
|
8049
|
+
...node.enabled !== void 0 ? { enabled: node.enabled } : {},
|
|
8050
|
+
...node.visible !== void 0 ? { visible: node.visible } : {},
|
|
8051
|
+
selectors: rankIosSelectors(node)
|
|
8052
|
+
};
|
|
8053
|
+
}
|
|
8054
|
+
function toWindow2(node) {
|
|
8055
|
+
const [best] = rankIosSelectors(node);
|
|
8056
|
+
return {
|
|
8057
|
+
...node.name ? { name: node.name } : {},
|
|
8058
|
+
...node.label ? { label: node.label } : {},
|
|
8059
|
+
selector: best ?? `role=${shortIosType(IOS_WINDOW_TYPE)}`
|
|
8060
|
+
};
|
|
8061
|
+
}
|
|
8062
|
+
function isIosInteractive(node) {
|
|
8063
|
+
return node.type !== void 0 && IOS_INTERACTIVE_TYPES.has(node.type);
|
|
8064
|
+
}
|
|
8065
|
+
function collect(root) {
|
|
8066
|
+
const elements = [];
|
|
8067
|
+
const windows = [];
|
|
8068
|
+
const visit = (node) => {
|
|
8069
|
+
if (node.type === IOS_WINDOW_TYPE) {
|
|
8070
|
+
windows.push(toWindow2(node));
|
|
8071
|
+
}
|
|
8072
|
+
if (isIosInteractive(node)) {
|
|
8073
|
+
elements.push(toElement3(node));
|
|
8074
|
+
}
|
|
8075
|
+
for (const child of node.children) {
|
|
8076
|
+
visit(child);
|
|
8077
|
+
}
|
|
8078
|
+
};
|
|
8079
|
+
visit(root);
|
|
8080
|
+
return { elements, windows };
|
|
8081
|
+
}
|
|
8082
|
+
async function analyzeIosApp(client, options) {
|
|
8083
|
+
const xml = await client.source();
|
|
8084
|
+
const root = parseIosHierarchy(xml);
|
|
8085
|
+
if (!root) {
|
|
8086
|
+
return { app: options.app, elements: [], windows: [] };
|
|
8087
|
+
}
|
|
8088
|
+
const { elements, windows } = collect(root);
|
|
8089
|
+
return { app: options.app, elements, windows };
|
|
8090
|
+
}
|
|
8091
|
+
|
|
6618
8092
|
// src/generator/index.ts
|
|
6619
8093
|
var import_yaml2 = __toESM(require("yaml"), 1);
|
|
6620
8094
|
|
|
@@ -6675,6 +8149,7 @@ var STEP_REFERENCE = `
|
|
|
6675
8149
|
|
|
6676
8150
|
### Visual Regression
|
|
6677
8151
|
- assertScreenshot: { name: "baseline-name", threshold: 0.1 }
|
|
8152
|
+
- assertWithAI: "The login form should show email and password fields" \u2014 AI checks the screenshot against the claim
|
|
6678
8153
|
|
|
6679
8154
|
### Control Flow
|
|
6680
8155
|
- if: { visible: ".banner", then: [steps...] }
|
|
@@ -6716,81 +8191,6 @@ function extractYamlFromResponse(response) {
|
|
|
6716
8191
|
return response.trim();
|
|
6717
8192
|
}
|
|
6718
8193
|
|
|
6719
|
-
// src/generator/ai.ts
|
|
6720
|
-
function resolveAiConfig() {
|
|
6721
|
-
const provider = process.env.PROWL_AI_PROVIDER ?? "anthropic";
|
|
6722
|
-
if (provider !== "anthropic" && provider !== "openai") {
|
|
6723
|
-
throw new Error(`Unsupported AI provider: ${provider}. Use "anthropic" or "openai".`);
|
|
6724
|
-
}
|
|
6725
|
-
const apiKey = process.env.PROWL_AI_KEY;
|
|
6726
|
-
if (!apiKey) {
|
|
6727
|
-
throw new Error(
|
|
6728
|
-
"PROWL_AI_KEY environment variable is required. Set it to your Anthropic or OpenAI API key."
|
|
6729
|
-
);
|
|
6730
|
-
}
|
|
6731
|
-
const defaultModel = provider === "anthropic" ? "claude-sonnet-4-5-20250929" : "gpt-4o";
|
|
6732
|
-
const model = process.env.PROWL_AI_MODEL ?? defaultModel;
|
|
6733
|
-
return { provider, model, apiKey };
|
|
6734
|
-
}
|
|
6735
|
-
async function generateWithAi(prompt, config) {
|
|
6736
|
-
if (config.provider === "anthropic") {
|
|
6737
|
-
return generateWithAnthropic(prompt, config);
|
|
6738
|
-
}
|
|
6739
|
-
return generateWithOpenAi(prompt, config);
|
|
6740
|
-
}
|
|
6741
|
-
async function generateWithAnthropic(prompt, config) {
|
|
6742
|
-
const response = await fetch("https://api.anthropic.com/v1/messages", {
|
|
6743
|
-
method: "POST",
|
|
6744
|
-
headers: {
|
|
6745
|
-
"Content-Type": "application/json",
|
|
6746
|
-
"x-api-key": config.apiKey,
|
|
6747
|
-
"anthropic-version": "2023-06-01"
|
|
6748
|
-
},
|
|
6749
|
-
body: JSON.stringify({
|
|
6750
|
-
model: config.model,
|
|
6751
|
-
max_tokens: 4096,
|
|
6752
|
-
messages: [
|
|
6753
|
-
{ role: "user", content: prompt }
|
|
6754
|
-
]
|
|
6755
|
-
})
|
|
6756
|
-
});
|
|
6757
|
-
if (!response.ok) {
|
|
6758
|
-
const body = await response.text();
|
|
6759
|
-
throw new Error(`Anthropic API error (${response.status}): ${body}`);
|
|
6760
|
-
}
|
|
6761
|
-
const data = await response.json();
|
|
6762
|
-
const textBlock = data.content.find((c) => c.type === "text");
|
|
6763
|
-
if (!textBlock?.text) {
|
|
6764
|
-
throw new Error("Anthropic API returned no text content");
|
|
6765
|
-
}
|
|
6766
|
-
return textBlock.text;
|
|
6767
|
-
}
|
|
6768
|
-
async function generateWithOpenAi(prompt, config) {
|
|
6769
|
-
const response = await fetch("https://api.openai.com/v1/chat/completions", {
|
|
6770
|
-
method: "POST",
|
|
6771
|
-
headers: {
|
|
6772
|
-
"Content-Type": "application/json",
|
|
6773
|
-
"Authorization": `Bearer ${config.apiKey}`
|
|
6774
|
-
},
|
|
6775
|
-
body: JSON.stringify({
|
|
6776
|
-
model: config.model,
|
|
6777
|
-
messages: [
|
|
6778
|
-
{ role: "user", content: prompt }
|
|
6779
|
-
],
|
|
6780
|
-
max_tokens: 4096
|
|
6781
|
-
})
|
|
6782
|
-
});
|
|
6783
|
-
if (!response.ok) {
|
|
6784
|
-
const body = await response.text();
|
|
6785
|
-
throw new Error(`OpenAI API error (${response.status}): ${body}`);
|
|
6786
|
-
}
|
|
6787
|
-
const data = await response.json();
|
|
6788
|
-
if (!data.choices?.[0]?.message?.content) {
|
|
6789
|
-
throw new Error("OpenAI API returned no content");
|
|
6790
|
-
}
|
|
6791
|
-
return data.choices[0].message.content;
|
|
6792
|
-
}
|
|
6793
|
-
|
|
6794
8194
|
// src/generator/index.ts
|
|
6795
8195
|
function parseViewportFlag2(value) {
|
|
6796
8196
|
const match = /^(\d+)x(\d+)$/i.exec(value);
|
|
@@ -6835,27 +8235,43 @@ async function generateHunt(options) {
|
|
|
6835
8235
|
}
|
|
6836
8236
|
// Annotate the CommonJS export names for ESM import in node:
|
|
6837
8237
|
0 && (module.exports = {
|
|
8238
|
+
ANDROID_INTERACTIVE_CLASSES,
|
|
6838
8239
|
ANDROID_KEYCODES,
|
|
8240
|
+
ANDROID_MATCH_DIALECT,
|
|
6839
8241
|
DEFAULT_AGENT_REQUEST_TIMEOUT_MS,
|
|
6840
8242
|
DEFAULT_ANALYZE_TREE_DEPTH,
|
|
6841
8243
|
DEFAULT_FLAKY_THRESHOLD,
|
|
6842
8244
|
DEFAULT_REQUEST_TIMEOUT_MS,
|
|
6843
8245
|
DEFAULT_SIMULATOR_LOCK_ROOT,
|
|
6844
8246
|
DEFAULT_WDA_REQUEST_TIMEOUT_MS,
|
|
8247
|
+
HELPER_BINARY,
|
|
6845
8248
|
INTERACTIVE_ROLES,
|
|
8249
|
+
IOS_INTERACTIVE_TYPES,
|
|
8250
|
+
IOS_MATCH_DIALECT,
|
|
6846
8251
|
IOS_PRESS_KEYS,
|
|
8252
|
+
IOS_WINDOW_TYPE,
|
|
8253
|
+
MACDRIVER_REPO,
|
|
8254
|
+
MACDRIVER_SIGNING_AUTHORITY_PREFIX,
|
|
8255
|
+
MACDRIVER_SIGNING_IDENTIFIER,
|
|
8256
|
+
MACDRIVER_VERSION,
|
|
8257
|
+
MACDRIVER_VERSION_PATTERN,
|
|
8258
|
+
MACOS_MATCH_DIALECT,
|
|
8259
|
+
NATIVE_ATTRIBUTE_MAP,
|
|
6847
8260
|
SpawnMacHelperClient,
|
|
6848
8261
|
UIA2_REMOTE_PORT,
|
|
6849
8262
|
Uia2HttpError,
|
|
6850
8263
|
Uia2Transport,
|
|
6851
|
-
WDA_RUNNER_APP_NAME,
|
|
6852
8264
|
WDA_RUNNER_BUNDLE_ID,
|
|
8265
|
+
WDA_USE_PORT_ENV,
|
|
6853
8266
|
WEB_ONLY_STEP_TYPES,
|
|
6854
8267
|
WdaHttpError,
|
|
6855
8268
|
WdaTransport,
|
|
8269
|
+
analyzeAndroidApp,
|
|
8270
|
+
analyzeIosApp,
|
|
6856
8271
|
analyzeMacApp,
|
|
6857
8272
|
analyzePage,
|
|
6858
8273
|
androidAppAllowedIdentities,
|
|
8274
|
+
androidNodeToNative,
|
|
6859
8275
|
androidQueryToLocator,
|
|
6860
8276
|
assertAndroidAppAllowed,
|
|
6861
8277
|
assertIosAppAllowed,
|
|
@@ -6868,6 +8284,8 @@ async function generateHunt(options) {
|
|
|
6868
8284
|
closeIosSession,
|
|
6869
8285
|
closeMacSession,
|
|
6870
8286
|
clusterFailures,
|
|
8287
|
+
codesignVerifier,
|
|
8288
|
+
collectMacdriverStatus,
|
|
6871
8289
|
computeFlakeScore,
|
|
6872
8290
|
configSchema,
|
|
6873
8291
|
createAndroidDriver,
|
|
@@ -6877,8 +8295,12 @@ async function generateHunt(options) {
|
|
|
6877
8295
|
createUia2Session,
|
|
6878
8296
|
createWdaAgentClient,
|
|
6879
8297
|
createWdaSession,
|
|
8298
|
+
decodeXmlEntities,
|
|
6880
8299
|
defaultAgentConnector,
|
|
6881
8300
|
defaultIosAgentConnector,
|
|
8301
|
+
defaultWdaTestRunPreparer,
|
|
8302
|
+
dittoExtractor,
|
|
8303
|
+
downloadAndVerify,
|
|
6882
8304
|
escapePredicateArg,
|
|
6883
8305
|
escapeUiSelectorArg,
|
|
6884
8306
|
extractElementId,
|
|
@@ -6888,9 +8310,14 @@ async function generateHunt(options) {
|
|
|
6888
8310
|
generateHunt,
|
|
6889
8311
|
healSelector,
|
|
6890
8312
|
huntSchema,
|
|
8313
|
+
injectUsePortIntoXctestrun,
|
|
8314
|
+
installMacdriver,
|
|
6891
8315
|
interpolateHunt,
|
|
6892
8316
|
iosAppAllowedIdentities,
|
|
8317
|
+
iosNodeToNative,
|
|
6893
8318
|
iosQueryToLocator,
|
|
8319
|
+
isAndroidInteractive,
|
|
8320
|
+
isIosInteractive,
|
|
6894
8321
|
launchAndroidSession,
|
|
6895
8322
|
launchIosSession,
|
|
6896
8323
|
launchMacSession,
|
|
@@ -6901,18 +8328,41 @@ async function generateHunt(options) {
|
|
|
6901
8328
|
loadHunt,
|
|
6902
8329
|
loadHuntMeta,
|
|
6903
8330
|
loadHuntTags,
|
|
8331
|
+
macdriverAssetName,
|
|
8332
|
+
macdriverAssetUrl,
|
|
6904
8333
|
macdriverBuildInstructions,
|
|
8334
|
+
macdriverChecksumName,
|
|
8335
|
+
macdriverInstallRoot,
|
|
8336
|
+
macdriverInstalledBinary,
|
|
8337
|
+
macdriverReleaseTag,
|
|
8338
|
+
macdriverVersionDir,
|
|
8339
|
+
matchAndroidSelector,
|
|
8340
|
+
matchIosSelector,
|
|
8341
|
+
matchNativeTree,
|
|
8342
|
+
nodeMatchesSelector,
|
|
6905
8343
|
normalizeXcuiClassName,
|
|
6906
8344
|
parseAaptPackage,
|
|
6907
8345
|
parseAdbDevices,
|
|
8346
|
+
parseAndroidHierarchy,
|
|
6908
8347
|
parseAndroidSelector,
|
|
8348
|
+
parseChecksumFile,
|
|
8349
|
+
parseCodesignDetails,
|
|
6909
8350
|
parseForwardPort,
|
|
8351
|
+
parseIosHierarchy,
|
|
6910
8352
|
parseIosSelector,
|
|
6911
8353
|
parseMacSelector,
|
|
8354
|
+
parseNativeSelector,
|
|
6912
8355
|
parseSimctlDevices,
|
|
8356
|
+
parseSnapshot,
|
|
6913
8357
|
parseXcodeVersion,
|
|
8358
|
+
parseXml,
|
|
8359
|
+
qualifyResourceId,
|
|
8360
|
+
quoteSelectorValue,
|
|
8361
|
+
rankAndroidSelectors,
|
|
6914
8362
|
rankFlaky,
|
|
8363
|
+
rankIosSelectors,
|
|
6915
8364
|
rankMacSelectors,
|
|
8365
|
+
rankNativeSelectors,
|
|
6916
8366
|
readHistory,
|
|
6917
8367
|
readHuntHistory,
|
|
6918
8368
|
readIosBundleIdentifier,
|
|
@@ -6920,18 +8370,29 @@ async function generateHunt(options) {
|
|
|
6920
8370
|
resolveAgentApks,
|
|
6921
8371
|
resolveHelperBinary,
|
|
6922
8372
|
resolveWdaProject,
|
|
6923
|
-
|
|
8373
|
+
resolveWdaTestRun,
|
|
6924
8374
|
runHunt,
|
|
6925
8375
|
runSuite,
|
|
8376
|
+
runVersionProbe,
|
|
6926
8377
|
selectDeviceSerial,
|
|
6927
8378
|
selectSimulatorUdid,
|
|
8379
|
+
sha256Hex,
|
|
8380
|
+
shortIosType,
|
|
6928
8381
|
stepSchema,
|
|
8382
|
+
tccGuidance,
|
|
8383
|
+
unquoteSelectorValue,
|
|
6929
8384
|
unwrapAndroidTextSelector,
|
|
6930
8385
|
unwrapIosTextSelector,
|
|
8386
|
+
unwrapNativeTextSelector,
|
|
6931
8387
|
updateBacklogFromSuite,
|
|
8388
|
+
validateMacdriverArchiveEntries,
|
|
8389
|
+
validateMacdriverVersion,
|
|
8390
|
+
verifyMacdriverSignature,
|
|
6932
8391
|
waitForAgentReady,
|
|
6933
8392
|
waitForWdaReady,
|
|
6934
8393
|
wdaCacheDir,
|
|
6935
|
-
|
|
8394
|
+
wdaTestRunArgs,
|
|
8395
|
+
webOnlyReason,
|
|
8396
|
+
zipinfoArchiveLister
|
|
6936
8397
|
});
|
|
6937
8398
|
//# sourceMappingURL=lib.cjs.map
|