prowl-tools 0.1.5 → 0.1.6
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 +251 -9
- package/dist/{chunk-2KD2XCTH.js → chunk-5KQR3IR3.js} +1182 -234
- package/dist/chunk-5KQR3IR3.js.map +1 -0
- package/dist/{chunk-ITOSUJCN.js → chunk-WHAMB4TY.js} +5 -1
- package/dist/chunk-WHAMB4TY.js.map +1 -0
- package/dist/index.cjs +1152 -204
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +173 -10
- package/dist/index.js.map +1 -1
- package/dist/lib.cjs +1220 -232
- package/dist/lib.cjs.map +1 -1
- package/dist/lib.d.cts +540 -53
- package/dist/lib.d.ts +540 -53
- package/dist/lib.js +74 -6
- package/dist/{loader-FCXPARP7.js → loader-PBBYV3U7.js} +2 -2
- package/package.json +1 -1
- package/dist/chunk-2KD2XCTH.js.map +0 -1
- package/dist/chunk-ITOSUJCN.js.map +0 -1
- /package/dist/{loader-FCXPARP7.js.map → loader-PBBYV3U7.js.map} +0 -0
package/dist/lib.cjs
CHANGED
|
@@ -86,7 +86,9 @@ 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,
|
|
@@ -94,19 +96,27 @@ __export(src_exports, {
|
|
|
94
96
|
DEFAULT_SIMULATOR_LOCK_ROOT: () => DEFAULT_SIMULATOR_LOCK_ROOT,
|
|
95
97
|
DEFAULT_WDA_REQUEST_TIMEOUT_MS: () => DEFAULT_WDA_REQUEST_TIMEOUT_MS,
|
|
96
98
|
INTERACTIVE_ROLES: () => INTERACTIVE_ROLES,
|
|
99
|
+
IOS_INTERACTIVE_TYPES: () => IOS_INTERACTIVE_TYPES,
|
|
100
|
+
IOS_MATCH_DIALECT: () => IOS_MATCH_DIALECT,
|
|
97
101
|
IOS_PRESS_KEYS: () => IOS_PRESS_KEYS,
|
|
102
|
+
IOS_WINDOW_TYPE: () => IOS_WINDOW_TYPE,
|
|
103
|
+
MACOS_MATCH_DIALECT: () => MACOS_MATCH_DIALECT,
|
|
104
|
+
NATIVE_ATTRIBUTE_MAP: () => NATIVE_ATTRIBUTE_MAP,
|
|
98
105
|
SpawnMacHelperClient: () => SpawnMacHelperClient,
|
|
99
106
|
UIA2_REMOTE_PORT: () => UIA2_REMOTE_PORT,
|
|
100
107
|
Uia2HttpError: () => Uia2HttpError,
|
|
101
108
|
Uia2Transport: () => Uia2Transport,
|
|
102
|
-
WDA_RUNNER_APP_NAME: () => WDA_RUNNER_APP_NAME,
|
|
103
109
|
WDA_RUNNER_BUNDLE_ID: () => WDA_RUNNER_BUNDLE_ID,
|
|
110
|
+
WDA_USE_PORT_ENV: () => WDA_USE_PORT_ENV,
|
|
104
111
|
WEB_ONLY_STEP_TYPES: () => WEB_ONLY_STEP_TYPES,
|
|
105
112
|
WdaHttpError: () => WdaHttpError,
|
|
106
113
|
WdaTransport: () => WdaTransport,
|
|
114
|
+
analyzeAndroidApp: () => analyzeAndroidApp,
|
|
115
|
+
analyzeIosApp: () => analyzeIosApp,
|
|
107
116
|
analyzeMacApp: () => analyzeMacApp,
|
|
108
117
|
analyzePage: () => analyzePage,
|
|
109
118
|
androidAppAllowedIdentities: () => androidAppAllowedIdentities,
|
|
119
|
+
androidNodeToNative: () => androidNodeToNative,
|
|
110
120
|
androidQueryToLocator: () => androidQueryToLocator,
|
|
111
121
|
assertAndroidAppAllowed: () => assertAndroidAppAllowed,
|
|
112
122
|
assertIosAppAllowed: () => assertIosAppAllowed,
|
|
@@ -128,8 +138,10 @@ __export(src_exports, {
|
|
|
128
138
|
createUia2Session: () => createUia2Session,
|
|
129
139
|
createWdaAgentClient: () => createWdaAgentClient,
|
|
130
140
|
createWdaSession: () => createWdaSession,
|
|
141
|
+
decodeXmlEntities: () => decodeXmlEntities,
|
|
131
142
|
defaultAgentConnector: () => defaultAgentConnector,
|
|
132
143
|
defaultIosAgentConnector: () => defaultIosAgentConnector,
|
|
144
|
+
defaultWdaTestRunPreparer: () => defaultWdaTestRunPreparer,
|
|
133
145
|
escapePredicateArg: () => escapePredicateArg,
|
|
134
146
|
escapeUiSelectorArg: () => escapeUiSelectorArg,
|
|
135
147
|
extractElementId: () => extractElementId,
|
|
@@ -139,9 +151,13 @@ __export(src_exports, {
|
|
|
139
151
|
generateHunt: () => generateHunt,
|
|
140
152
|
healSelector: () => healSelector,
|
|
141
153
|
huntSchema: () => huntSchema,
|
|
154
|
+
injectUsePortIntoXctestrun: () => injectUsePortIntoXctestrun,
|
|
142
155
|
interpolateHunt: () => interpolateHunt,
|
|
143
156
|
iosAppAllowedIdentities: () => iosAppAllowedIdentities,
|
|
157
|
+
iosNodeToNative: () => iosNodeToNative,
|
|
144
158
|
iosQueryToLocator: () => iosQueryToLocator,
|
|
159
|
+
isAndroidInteractive: () => isAndroidInteractive,
|
|
160
|
+
isIosInteractive: () => isIosInteractive,
|
|
145
161
|
launchAndroidSession: () => launchAndroidSession,
|
|
146
162
|
launchIosSession: () => launchIosSession,
|
|
147
163
|
launchMacSession: () => launchMacSession,
|
|
@@ -153,17 +169,31 @@ __export(src_exports, {
|
|
|
153
169
|
loadHuntMeta: () => loadHuntMeta,
|
|
154
170
|
loadHuntTags: () => loadHuntTags,
|
|
155
171
|
macdriverBuildInstructions: () => macdriverBuildInstructions,
|
|
172
|
+
matchAndroidSelector: () => matchAndroidSelector,
|
|
173
|
+
matchIosSelector: () => matchIosSelector,
|
|
174
|
+
matchNativeTree: () => matchNativeTree,
|
|
175
|
+
nodeMatchesSelector: () => nodeMatchesSelector,
|
|
156
176
|
normalizeXcuiClassName: () => normalizeXcuiClassName,
|
|
157
177
|
parseAaptPackage: () => parseAaptPackage,
|
|
158
178
|
parseAdbDevices: () => parseAdbDevices,
|
|
179
|
+
parseAndroidHierarchy: () => parseAndroidHierarchy,
|
|
159
180
|
parseAndroidSelector: () => parseAndroidSelector,
|
|
160
181
|
parseForwardPort: () => parseForwardPort,
|
|
182
|
+
parseIosHierarchy: () => parseIosHierarchy,
|
|
161
183
|
parseIosSelector: () => parseIosSelector,
|
|
162
184
|
parseMacSelector: () => parseMacSelector,
|
|
185
|
+
parseNativeSelector: () => parseNativeSelector,
|
|
163
186
|
parseSimctlDevices: () => parseSimctlDevices,
|
|
187
|
+
parseSnapshot: () => parseSnapshot,
|
|
164
188
|
parseXcodeVersion: () => parseXcodeVersion,
|
|
189
|
+
parseXml: () => parseXml,
|
|
190
|
+
qualifyResourceId: () => qualifyResourceId,
|
|
191
|
+
quoteSelectorValue: () => quoteSelectorValue,
|
|
192
|
+
rankAndroidSelectors: () => rankAndroidSelectors,
|
|
165
193
|
rankFlaky: () => rankFlaky,
|
|
194
|
+
rankIosSelectors: () => rankIosSelectors,
|
|
166
195
|
rankMacSelectors: () => rankMacSelectors,
|
|
196
|
+
rankNativeSelectors: () => rankNativeSelectors,
|
|
167
197
|
readHistory: () => readHistory,
|
|
168
198
|
readHuntHistory: () => readHuntHistory,
|
|
169
199
|
readIosBundleIdentifier: () => readIosBundleIdentifier,
|
|
@@ -171,18 +201,22 @@ __export(src_exports, {
|
|
|
171
201
|
resolveAgentApks: () => resolveAgentApks,
|
|
172
202
|
resolveHelperBinary: () => resolveHelperBinary,
|
|
173
203
|
resolveWdaProject: () => resolveWdaProject,
|
|
174
|
-
|
|
204
|
+
resolveWdaTestRun: () => resolveWdaTestRun,
|
|
175
205
|
runHunt: () => runHunt,
|
|
176
206
|
runSuite: () => runSuite,
|
|
177
207
|
selectDeviceSerial: () => selectDeviceSerial,
|
|
178
208
|
selectSimulatorUdid: () => selectSimulatorUdid,
|
|
209
|
+
shortIosType: () => shortIosType,
|
|
179
210
|
stepSchema: () => stepSchema,
|
|
211
|
+
unquoteSelectorValue: () => unquoteSelectorValue,
|
|
180
212
|
unwrapAndroidTextSelector: () => unwrapAndroidTextSelector,
|
|
181
213
|
unwrapIosTextSelector: () => unwrapIosTextSelector,
|
|
214
|
+
unwrapNativeTextSelector: () => unwrapNativeTextSelector,
|
|
182
215
|
updateBacklogFromSuite: () => updateBacklogFromSuite,
|
|
183
216
|
waitForAgentReady: () => waitForAgentReady,
|
|
184
217
|
waitForWdaReady: () => waitForWdaReady,
|
|
185
218
|
wdaCacheDir: () => wdaCacheDir,
|
|
219
|
+
wdaTestRunArgs: () => wdaTestRunArgs,
|
|
186
220
|
webOnlyReason: () => webOnlyReason
|
|
187
221
|
});
|
|
188
222
|
module.exports = __toCommonJS(src_exports);
|
|
@@ -572,6 +606,289 @@ async function closeMacSession(session) {
|
|
|
572
606
|
|
|
573
607
|
// src/browser/android-driver.ts
|
|
574
608
|
var import_node_fs2 = __toESM(require("fs"), 1);
|
|
609
|
+
|
|
610
|
+
// src/analyzer/xml.ts
|
|
611
|
+
var ENTITIES = {
|
|
612
|
+
amp: "&",
|
|
613
|
+
lt: "<",
|
|
614
|
+
gt: ">",
|
|
615
|
+
quot: '"',
|
|
616
|
+
apos: "'"
|
|
617
|
+
};
|
|
618
|
+
function decodeXmlEntities(value) {
|
|
619
|
+
if (!value.includes("&")) {
|
|
620
|
+
return value;
|
|
621
|
+
}
|
|
622
|
+
return value.replace(/&(#(?:[xX][0-9a-fA-F]+|[0-9]+)|[a-zA-Z]+);/g, (match, code) => {
|
|
623
|
+
if (code[0] === "#") {
|
|
624
|
+
const hex = code[1] === "x" || code[1] === "X";
|
|
625
|
+
const num2 = Number.parseInt(code.slice(hex ? 2 : 1), hex ? 16 : 10);
|
|
626
|
+
return Number.isSafeInteger(num2) && num2 <= 1114111 ? String.fromCodePoint(num2) : match;
|
|
627
|
+
}
|
|
628
|
+
const named = ENTITIES[code];
|
|
629
|
+
return named ?? match;
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
var ATTR_RE = /([^\s=/]+)\s*=\s*("([^"]*)"|'([^']*)')/g;
|
|
633
|
+
function parseTagBody(body) {
|
|
634
|
+
const trimmed = body.trim();
|
|
635
|
+
const nameMatch = /^([^\s/>]+)/.exec(trimmed);
|
|
636
|
+
const tag = nameMatch ? nameMatch[1] : "";
|
|
637
|
+
const attrs = {};
|
|
638
|
+
const rest = trimmed.slice(tag.length);
|
|
639
|
+
ATTR_RE.lastIndex = 0;
|
|
640
|
+
let m;
|
|
641
|
+
while ((m = ATTR_RE.exec(rest)) !== null) {
|
|
642
|
+
const rawValue = m[3] !== void 0 ? m[3] : m[4] ?? "";
|
|
643
|
+
attrs[m[1]] = decodeXmlEntities(rawValue);
|
|
644
|
+
}
|
|
645
|
+
return { tag, attrs };
|
|
646
|
+
}
|
|
647
|
+
function parseXml(input) {
|
|
648
|
+
const n = input.length;
|
|
649
|
+
const stack = [];
|
|
650
|
+
let root = null;
|
|
651
|
+
let i = 0;
|
|
652
|
+
while (i < n) {
|
|
653
|
+
const lt = input.indexOf("<", i);
|
|
654
|
+
if (lt < 0) {
|
|
655
|
+
break;
|
|
656
|
+
}
|
|
657
|
+
i = lt + 1;
|
|
658
|
+
const ch = input[i];
|
|
659
|
+
if (ch === "?") {
|
|
660
|
+
const end = input.indexOf("?>", i);
|
|
661
|
+
i = end < 0 ? n : end + 2;
|
|
662
|
+
continue;
|
|
663
|
+
}
|
|
664
|
+
if (ch === "!") {
|
|
665
|
+
if (input.startsWith("!--", i)) {
|
|
666
|
+
const end = input.indexOf("-->", i);
|
|
667
|
+
i = end < 0 ? n : end + 3;
|
|
668
|
+
} else {
|
|
669
|
+
const end = input.indexOf(">", i);
|
|
670
|
+
i = end < 0 ? n : end + 1;
|
|
671
|
+
}
|
|
672
|
+
continue;
|
|
673
|
+
}
|
|
674
|
+
if (ch === "/") {
|
|
675
|
+
const gt = input.indexOf(">", i);
|
|
676
|
+
i = gt < 0 ? n : gt + 1;
|
|
677
|
+
stack.pop();
|
|
678
|
+
continue;
|
|
679
|
+
}
|
|
680
|
+
let j = i;
|
|
681
|
+
let quote2 = null;
|
|
682
|
+
while (j < n) {
|
|
683
|
+
const c = input[j];
|
|
684
|
+
if (quote2 !== null) {
|
|
685
|
+
if (c === quote2) {
|
|
686
|
+
quote2 = null;
|
|
687
|
+
}
|
|
688
|
+
} else if (c === '"' || c === "'") {
|
|
689
|
+
quote2 = c;
|
|
690
|
+
} else if (c === ">") {
|
|
691
|
+
break;
|
|
692
|
+
}
|
|
693
|
+
j += 1;
|
|
694
|
+
}
|
|
695
|
+
const inner = input.slice(i, j);
|
|
696
|
+
i = j + 1;
|
|
697
|
+
const selfClose = inner.endsWith("/");
|
|
698
|
+
const body = selfClose ? inner.slice(0, -1) : inner;
|
|
699
|
+
const { tag, attrs } = parseTagBody(body);
|
|
700
|
+
if (tag.length === 0) {
|
|
701
|
+
continue;
|
|
702
|
+
}
|
|
703
|
+
const element = { tag, attrs, children: [] };
|
|
704
|
+
const parent = stack[stack.length - 1];
|
|
705
|
+
if (parent) {
|
|
706
|
+
parent.children.push(element);
|
|
707
|
+
}
|
|
708
|
+
if (root === null) {
|
|
709
|
+
root = element;
|
|
710
|
+
}
|
|
711
|
+
if (!selfClose) {
|
|
712
|
+
stack.push(element);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
return root;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// src/selector/native.ts
|
|
719
|
+
function unquoteSelectorValue(value) {
|
|
720
|
+
const trimmed = value.trim();
|
|
721
|
+
const first = trimmed[0];
|
|
722
|
+
if ((first === '"' || first === "'") && trimmed.endsWith(first) && trimmed.length >= 2) {
|
|
723
|
+
return trimmed.slice(1, -1);
|
|
724
|
+
}
|
|
725
|
+
return trimmed;
|
|
726
|
+
}
|
|
727
|
+
function quoteSelectorValue(value) {
|
|
728
|
+
return `"${value}"`;
|
|
729
|
+
}
|
|
730
|
+
var ROLE_SELECTOR_RE = /^role=([A-Za-z][\w.$-]*)(?:\[name=(.+)\])?$/s;
|
|
731
|
+
var NATIVE_SELECTOR_PREFIX_RE = /^(id|label|text|role)=/;
|
|
732
|
+
function invalidSelectorMessage(selector, reason) {
|
|
733
|
+
return `Invalid native selector ${JSON.stringify(selector)}: ${reason}.`;
|
|
734
|
+
}
|
|
735
|
+
function parseNativeSelector(selector) {
|
|
736
|
+
const trimmed = selector.trim();
|
|
737
|
+
if (trimmed.length === 0) {
|
|
738
|
+
throw new Error(
|
|
739
|
+
invalidSelectorMessage(
|
|
740
|
+
selector,
|
|
741
|
+
"selector is empty; use id=, label=, text=, role=, :focus, or a bare text value"
|
|
742
|
+
)
|
|
743
|
+
);
|
|
744
|
+
}
|
|
745
|
+
if (trimmed === ":focus") {
|
|
746
|
+
return { kind: "focused" };
|
|
747
|
+
}
|
|
748
|
+
const idMatch = /^id=(.+)$/s.exec(trimmed);
|
|
749
|
+
if (idMatch) {
|
|
750
|
+
return { kind: "id", value: unquoteSelectorValue(idMatch[1]) };
|
|
751
|
+
}
|
|
752
|
+
const roleMatch = ROLE_SELECTOR_RE.exec(trimmed);
|
|
753
|
+
if (roleMatch) {
|
|
754
|
+
const name = roleMatch[2] !== void 0 ? unquoteSelectorValue(roleMatch[2]) : void 0;
|
|
755
|
+
return name !== void 0 && name.length > 0 ? { kind: "role", role: roleMatch[1], name } : { kind: "role", role: roleMatch[1] };
|
|
756
|
+
}
|
|
757
|
+
const labelMatch = /^label=(.+)$/s.exec(trimmed);
|
|
758
|
+
if (labelMatch) {
|
|
759
|
+
return { kind: "label", value: unquoteSelectorValue(labelMatch[1]) };
|
|
760
|
+
}
|
|
761
|
+
const textMatch = /^text=(.+)$/s.exec(trimmed);
|
|
762
|
+
if (textMatch) {
|
|
763
|
+
return { kind: "text", value: unquoteSelectorValue(textMatch[1]) };
|
|
764
|
+
}
|
|
765
|
+
const prefix = NATIVE_SELECTOR_PREFIX_RE.exec(trimmed);
|
|
766
|
+
if (prefix) {
|
|
767
|
+
throw new Error(
|
|
768
|
+
invalidSelectorMessage(
|
|
769
|
+
selector,
|
|
770
|
+
`malformed ${prefix[1]}= selector; expected id=<value>, label=<value>, text=<value>, or role=<Type>[name=<value>]`
|
|
771
|
+
)
|
|
772
|
+
);
|
|
773
|
+
}
|
|
774
|
+
return { kind: "text", value: trimmed };
|
|
775
|
+
}
|
|
776
|
+
function unwrapNativeTextSelector(selector) {
|
|
777
|
+
const trimmed = selector.trim();
|
|
778
|
+
if (!trimmed.startsWith("text=")) {
|
|
779
|
+
return null;
|
|
780
|
+
}
|
|
781
|
+
return unquoteSelectorValue(trimmed.slice("text=".length));
|
|
782
|
+
}
|
|
783
|
+
function qualifyResourceId(value, appPackage) {
|
|
784
|
+
if (value.includes(":") || !appPackage) {
|
|
785
|
+
return value;
|
|
786
|
+
}
|
|
787
|
+
return `${appPackage}:id/${value}`;
|
|
788
|
+
}
|
|
789
|
+
function normalizeXcuiClassName(role) {
|
|
790
|
+
return role.startsWith("XCUIElementType") ? role : `XCUIElementType${role}`;
|
|
791
|
+
}
|
|
792
|
+
function shortIosType(type) {
|
|
793
|
+
return type.startsWith("XCUIElementType") ? type.slice("XCUIElementType".length) : type;
|
|
794
|
+
}
|
|
795
|
+
function rankNativeSelectors(fields) {
|
|
796
|
+
const selectors = [];
|
|
797
|
+
if (fields.id) {
|
|
798
|
+
selectors.push(`id=${fields.id}`);
|
|
799
|
+
}
|
|
800
|
+
if (fields.label) {
|
|
801
|
+
selectors.push(`label=${quoteSelectorValue(fields.label)}`);
|
|
802
|
+
}
|
|
803
|
+
if (fields.role && fields.name) {
|
|
804
|
+
selectors.push(`role=${fields.role}[name=${quoteSelectorValue(fields.name)}]`);
|
|
805
|
+
}
|
|
806
|
+
if (fields.name) {
|
|
807
|
+
selectors.push(`text=${quoteSelectorValue(fields.name)}`);
|
|
808
|
+
}
|
|
809
|
+
if (selectors.length === 0 && fields.role) {
|
|
810
|
+
selectors.push(`role=${fields.role}`);
|
|
811
|
+
}
|
|
812
|
+
return selectors;
|
|
813
|
+
}
|
|
814
|
+
var NATIVE_ATTRIBUTE_MAP = {
|
|
815
|
+
android: {
|
|
816
|
+
id: { attribute: "resource-id", match: "exact (package-qualified)" },
|
|
817
|
+
label: { attribute: "content-desc", match: "exact" },
|
|
818
|
+
text: { attribute: "text", match: "substring" },
|
|
819
|
+
role: { attribute: "class", match: "exact" }
|
|
820
|
+
},
|
|
821
|
+
ios: {
|
|
822
|
+
id: { attribute: "accessibility id (name)", match: "exact" },
|
|
823
|
+
label: { attribute: "label", match: "exact" },
|
|
824
|
+
text: { attribute: "label | value", match: "substring" },
|
|
825
|
+
role: { attribute: "type (XCUIElementType\u2026)", match: "exact" }
|
|
826
|
+
},
|
|
827
|
+
macos: {
|
|
828
|
+
id: { attribute: "AXIdentifier", match: "exact" },
|
|
829
|
+
label: { attribute: "title | description", match: "exact" },
|
|
830
|
+
text: { attribute: "title | description | value", match: "substring" },
|
|
831
|
+
role: { attribute: "AXRole", match: "exact" }
|
|
832
|
+
}
|
|
833
|
+
};
|
|
834
|
+
var ANDROID_MATCH_DIALECT = {
|
|
835
|
+
platform: "android",
|
|
836
|
+
normalizeRole: (role) => role,
|
|
837
|
+
normalizeId: (value, options) => qualifyResourceId(value, options.appPackage)
|
|
838
|
+
};
|
|
839
|
+
var IOS_MATCH_DIALECT = {
|
|
840
|
+
platform: "ios",
|
|
841
|
+
normalizeRole: (role) => normalizeXcuiClassName(role),
|
|
842
|
+
normalizeId: (value) => value
|
|
843
|
+
};
|
|
844
|
+
var MACOS_MATCH_DIALECT = {
|
|
845
|
+
platform: "macos",
|
|
846
|
+
normalizeRole: (role) => role,
|
|
847
|
+
normalizeId: (value) => value
|
|
848
|
+
};
|
|
849
|
+
function nodeMatchesSelector(dialect, selector, node, options = {}) {
|
|
850
|
+
switch (selector.kind) {
|
|
851
|
+
case "id":
|
|
852
|
+
return node.id !== void 0 && node.id === dialect.normalizeId(selector.value, options);
|
|
853
|
+
case "label":
|
|
854
|
+
return node.label !== void 0 && node.label === selector.value;
|
|
855
|
+
case "text":
|
|
856
|
+
return node.textValues.some((t) => t.includes(selector.value));
|
|
857
|
+
case "focused":
|
|
858
|
+
return node.focused === true;
|
|
859
|
+
case "role": {
|
|
860
|
+
if (node.role === void 0) {
|
|
861
|
+
return false;
|
|
862
|
+
}
|
|
863
|
+
if (dialect.normalizeRole(node.role) !== dialect.normalizeRole(selector.role)) {
|
|
864
|
+
return false;
|
|
865
|
+
}
|
|
866
|
+
if (selector.name === void 0 || selector.name.length === 0) {
|
|
867
|
+
return true;
|
|
868
|
+
}
|
|
869
|
+
const name = selector.name;
|
|
870
|
+
return node.textValues.some((t) => t.includes(name));
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
function matchNativeTree(dialect, selector, root, project, children, options = {}) {
|
|
875
|
+
const out = [];
|
|
876
|
+
const visit = (node) => {
|
|
877
|
+
if (nodeMatchesSelector(dialect, selector, project(node), options)) {
|
|
878
|
+
out.push(node);
|
|
879
|
+
}
|
|
880
|
+
for (const child of children(node)) {
|
|
881
|
+
visit(child);
|
|
882
|
+
}
|
|
883
|
+
};
|
|
884
|
+
visit(root);
|
|
885
|
+
return out;
|
|
886
|
+
}
|
|
887
|
+
function parseSnapshot(xml) {
|
|
888
|
+
return parseXml(xml);
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
// src/browser/android-driver.ts
|
|
575
892
|
var ANDROID_CAPABILITIES = /* @__PURE__ */ new Set([
|
|
576
893
|
"query",
|
|
577
894
|
"interact",
|
|
@@ -605,50 +922,29 @@ var ANDROID_KEYCODES = {
|
|
|
605
922
|
pageup: 92,
|
|
606
923
|
pagedown: 93
|
|
607
924
|
};
|
|
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
925
|
function escapeUiSelectorArg(value) {
|
|
617
926
|
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
618
927
|
}
|
|
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]) };
|
|
928
|
+
function toAndroidQuery(selector) {
|
|
929
|
+
switch (selector.kind) {
|
|
930
|
+
case "focused":
|
|
931
|
+
return { by: "focused" };
|
|
932
|
+
case "id":
|
|
933
|
+
return { by: "id", value: selector.value };
|
|
934
|
+
case "role":
|
|
935
|
+
return selector.name !== void 0 ? { by: "role", role: selector.role, name: selector.name } : { by: "role", role: selector.role };
|
|
936
|
+
case "label":
|
|
937
|
+
return { by: "accessibilityId", value: selector.value };
|
|
938
|
+
case "text":
|
|
939
|
+
return { by: "text", value: selector.value };
|
|
640
940
|
}
|
|
641
|
-
|
|
941
|
+
}
|
|
942
|
+
function parseAndroidSelector(selector) {
|
|
943
|
+
return toAndroidQuery(parseNativeSelector(selector));
|
|
642
944
|
}
|
|
643
945
|
function locator(strategy, selector) {
|
|
644
946
|
return { strategy, selector, context: "" };
|
|
645
947
|
}
|
|
646
|
-
function qualifyResourceId(value, appPackage) {
|
|
647
|
-
if (value.includes(":") || !appPackage) {
|
|
648
|
-
return value;
|
|
649
|
-
}
|
|
650
|
-
return `${appPackage}:id/${value}`;
|
|
651
|
-
}
|
|
652
948
|
function androidQueryToLocator(query, options = {}) {
|
|
653
949
|
switch (query.by) {
|
|
654
950
|
case "id":
|
|
@@ -675,11 +971,7 @@ function androidQueryToLocator(query, options = {}) {
|
|
|
675
971
|
}
|
|
676
972
|
}
|
|
677
973
|
function unwrapAndroidTextSelector(selector) {
|
|
678
|
-
|
|
679
|
-
if (!trimmed.startsWith("text=")) {
|
|
680
|
-
return null;
|
|
681
|
-
}
|
|
682
|
-
return unquote2(trimmed.slice(5));
|
|
974
|
+
return unwrapNativeTextSelector(selector);
|
|
683
975
|
}
|
|
684
976
|
function keyCodeFor(key) {
|
|
685
977
|
const code = ANDROID_KEYCODES[key.trim().toLowerCase()];
|
|
@@ -960,22 +1252,63 @@ async function installApk(runner, serial, apkPath) {
|
|
|
960
1252
|
);
|
|
961
1253
|
}
|
|
962
1254
|
}
|
|
1255
|
+
function formatUnknownError(error) {
|
|
1256
|
+
return error instanceof Error ? error.message : String(error);
|
|
1257
|
+
}
|
|
1258
|
+
function runnerPhaseError(message, error) {
|
|
1259
|
+
return new Error(`${message}: ${formatUnknownError(error)}`, { cause: error });
|
|
1260
|
+
}
|
|
1261
|
+
function parseResolvedComponent(stdout, pkg) {
|
|
1262
|
+
const lines = stdout.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
|
|
1263
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
1264
|
+
if (/^[\w.]+\/[\w.$]+$/.test(lines[i]) && lines[i].startsWith(`${pkg}/`)) {
|
|
1265
|
+
return lines[i];
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
return null;
|
|
1269
|
+
}
|
|
963
1270
|
async function launchPackage(runner, serial, pkg) {
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
1271
|
+
let resolved;
|
|
1272
|
+
try {
|
|
1273
|
+
resolved = await runner(
|
|
1274
|
+
withSerial(serial, [
|
|
1275
|
+
"shell",
|
|
1276
|
+
"cmd",
|
|
1277
|
+
"package",
|
|
1278
|
+
"resolve-activity",
|
|
1279
|
+
"--brief",
|
|
1280
|
+
"-c",
|
|
1281
|
+
"android.intent.category.LAUNCHER",
|
|
1282
|
+
pkg
|
|
1283
|
+
]),
|
|
1284
|
+
{ timeoutMs: 3e4 }
|
|
1285
|
+
);
|
|
1286
|
+
} catch (error) {
|
|
1287
|
+
throw runnerPhaseError(
|
|
1288
|
+
`Failed to resolve the launcher activity for Android package "${pkg}" via adb`,
|
|
1289
|
+
error
|
|
1290
|
+
);
|
|
1291
|
+
}
|
|
1292
|
+
const component = parseResolvedComponent(resolved.stdout, pkg);
|
|
1293
|
+
if (resolved.code !== 0 || !component) {
|
|
977
1294
|
throw new Error(
|
|
978
|
-
`
|
|
1295
|
+
`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?`
|
|
1296
|
+
);
|
|
1297
|
+
}
|
|
1298
|
+
let start;
|
|
1299
|
+
try {
|
|
1300
|
+
start = await runner(withSerial(serial, ["shell", "am", "start", "-n", component]), {
|
|
1301
|
+
timeoutMs: 3e4
|
|
1302
|
+
});
|
|
1303
|
+
} catch (error) {
|
|
1304
|
+
throw runnerPhaseError(
|
|
1305
|
+
`Failed to launch Android package "${pkg}" with \`am start\` (${component}) via adb`,
|
|
1306
|
+
error
|
|
1307
|
+
);
|
|
1308
|
+
}
|
|
1309
|
+
if (start.code !== 0 || /error|does not exist|cannot start/i.test(start.stdout + start.stderr)) {
|
|
1310
|
+
throw new Error(
|
|
1311
|
+
`Failed to launch Android package "${pkg}" (${component}, exit ${start.code}): ${(start.stdout + start.stderr).trim().slice(0, 400)}.`
|
|
979
1312
|
);
|
|
980
1313
|
}
|
|
981
1314
|
}
|
|
@@ -1193,6 +1526,13 @@ function createUia2AgentClient(transport, sessionId, options = {}) {
|
|
|
1193
1526
|
}
|
|
1194
1527
|
return Buffer.from(value, "base64");
|
|
1195
1528
|
},
|
|
1529
|
+
async source() {
|
|
1530
|
+
const value = await transport.request("GET", `${base}/source`);
|
|
1531
|
+
if (typeof value !== "string") {
|
|
1532
|
+
throw new Error("uiautomator2 /source did not return XML text; cannot analyze Android UI hierarchy");
|
|
1533
|
+
}
|
|
1534
|
+
return value;
|
|
1535
|
+
},
|
|
1196
1536
|
async close() {
|
|
1197
1537
|
await transport.request("DELETE", base).catch(() => void 0);
|
|
1198
1538
|
}
|
|
@@ -1545,43 +1885,25 @@ var IOS_CAPABILITIES = /* @__PURE__ */ new Set([
|
|
|
1545
1885
|
var WAIT_POLL_INTERVAL_MS2 = 250;
|
|
1546
1886
|
var DEFAULT_WAIT_TIMEOUT_MS2 = 5e3;
|
|
1547
1887
|
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
1888
|
function escapePredicateArg(value) {
|
|
1557
1889
|
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
1558
1890
|
}
|
|
1559
|
-
function
|
|
1560
|
-
|
|
1891
|
+
function toIosQuery(selector) {
|
|
1892
|
+
switch (selector.kind) {
|
|
1893
|
+
case "focused":
|
|
1894
|
+
return { by: "focused" };
|
|
1895
|
+
case "id":
|
|
1896
|
+
return { by: "accessibilityId", value: selector.value };
|
|
1897
|
+
case "role":
|
|
1898
|
+
return selector.name !== void 0 ? { by: "role", role: selector.role, name: selector.name } : { by: "role", role: selector.role };
|
|
1899
|
+
case "label":
|
|
1900
|
+
return { by: "label", value: selector.value };
|
|
1901
|
+
case "text":
|
|
1902
|
+
return { by: "text", value: selector.value };
|
|
1903
|
+
}
|
|
1561
1904
|
}
|
|
1562
1905
|
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 };
|
|
1906
|
+
return toIosQuery(parseNativeSelector(selector));
|
|
1585
1907
|
}
|
|
1586
1908
|
function iosQueryToLocator(query) {
|
|
1587
1909
|
switch (query.by) {
|
|
@@ -1613,11 +1935,7 @@ function iosQueryToLocator(query) {
|
|
|
1613
1935
|
}
|
|
1614
1936
|
}
|
|
1615
1937
|
function unwrapIosTextSelector(selector) {
|
|
1616
|
-
|
|
1617
|
-
if (!trimmed.startsWith("text=")) {
|
|
1618
|
-
return null;
|
|
1619
|
-
}
|
|
1620
|
-
return unquote3(trimmed.slice(5));
|
|
1938
|
+
return unwrapNativeTextSelector(selector);
|
|
1621
1939
|
}
|
|
1622
1940
|
function delay2(ms) {
|
|
1623
1941
|
return new Promise((resolve) => {
|
|
@@ -1777,6 +2095,7 @@ function createIosDriver(client, options) {
|
|
|
1777
2095
|
|
|
1778
2096
|
// src/browser/ios-helper.ts
|
|
1779
2097
|
var import_node_module2 = require("module");
|
|
2098
|
+
var import_node_child_process6 = require("child_process");
|
|
1780
2099
|
var import_node_fs5 = __toESM(require("fs"), 1);
|
|
1781
2100
|
var import_node_os2 = __toESM(require("os"), 1);
|
|
1782
2101
|
var import_node_path5 = __toESM(require("path"), 1);
|
|
@@ -1787,6 +2106,19 @@ var import_promises = require("fs/promises");
|
|
|
1787
2106
|
var import_node_net = __toESM(require("net"), 1);
|
|
1788
2107
|
var import_node_os = __toESM(require("os"), 1);
|
|
1789
2108
|
var import_node_path4 = __toESM(require("path"), 1);
|
|
2109
|
+
var spawnXcrunProcess = (args, options) => {
|
|
2110
|
+
const child = (0, import_node_child_process5.spawn)("xcrun", args, {
|
|
2111
|
+
stdio: "ignore",
|
|
2112
|
+
env: options?.env ? { ...process.env, ...options.env } : process.env
|
|
2113
|
+
});
|
|
2114
|
+
child.on("error", () => {
|
|
2115
|
+
});
|
|
2116
|
+
return {
|
|
2117
|
+
kill: () => {
|
|
2118
|
+
child.kill();
|
|
2119
|
+
}
|
|
2120
|
+
};
|
|
2121
|
+
};
|
|
1790
2122
|
var DEFAULT_SIMULATOR_LOCK_ROOT = import_node_path4.default.join(import_node_os.default.tmpdir(), "prowl-ios-simulator-locks");
|
|
1791
2123
|
var SIMULATOR_LOCK_OWNER_FILE = "owner.json";
|
|
1792
2124
|
var execFileXcrunRunner = (args, options) => new Promise((resolve) => {
|
|
@@ -2223,6 +2555,13 @@ function createWdaAgentClient(transport, sessionId) {
|
|
|
2223
2555
|
async homescreen() {
|
|
2224
2556
|
await transport.request("POST", "/wda/homescreen", {});
|
|
2225
2557
|
},
|
|
2558
|
+
async source() {
|
|
2559
|
+
const value = await transport.request("GET", "/source");
|
|
2560
|
+
if (typeof value !== "string") {
|
|
2561
|
+
throw new Error("WebDriverAgent /source did not return XML text; cannot analyze iOS UI hierarchy");
|
|
2562
|
+
}
|
|
2563
|
+
return value;
|
|
2564
|
+
},
|
|
2226
2565
|
async close() {
|
|
2227
2566
|
await transport.request("DELETE", base).catch(() => void 0);
|
|
2228
2567
|
}
|
|
@@ -2232,8 +2571,8 @@ function createWdaAgentClient(transport, sessionId) {
|
|
|
2232
2571
|
// src/browser/ios-helper.ts
|
|
2233
2572
|
var import_meta3 = {};
|
|
2234
2573
|
var WDA_RUNNER_BUNDLE_ID = "com.facebook.WebDriverAgentRunner.xctrunner";
|
|
2235
|
-
var WDA_RUNNER_APP_NAME = "WebDriverAgentRunner-Runner.app";
|
|
2236
2574
|
var WDA_USE_PORT_ENV = "USE_PORT";
|
|
2575
|
+
var PREPARED_XCTESTRUN_PREFIX = "prowl-wda-xctestrun-";
|
|
2237
2576
|
var WDA_STARTUP_ATTEMPTS = 2;
|
|
2238
2577
|
var defaultIosAgentConnector = async ({
|
|
2239
2578
|
host,
|
|
@@ -2267,10 +2606,44 @@ function resolveWdaProject(requireFn = (0, import_node_module2.createRequire)(im
|
|
|
2267
2606
|
function wdaCacheDir(wdaVersion, xcode, homeDir = import_node_os2.default.homedir()) {
|
|
2268
2607
|
return import_node_path5.default.join(homeDir, ".prowl", "wda", `${wdaVersion}-xcode${xcode}`);
|
|
2269
2608
|
}
|
|
2270
|
-
function
|
|
2271
|
-
return import_node_path5.default.join(derivedDataPath, "Build", "Products"
|
|
2609
|
+
function productsDir(derivedDataPath) {
|
|
2610
|
+
return import_node_path5.default.join(derivedDataPath, "Build", "Products");
|
|
2272
2611
|
}
|
|
2273
|
-
|
|
2612
|
+
function findXctestrunIn(dir) {
|
|
2613
|
+
let entries;
|
|
2614
|
+
try {
|
|
2615
|
+
entries = import_node_fs5.default.readdirSync(dir);
|
|
2616
|
+
} catch {
|
|
2617
|
+
return null;
|
|
2618
|
+
}
|
|
2619
|
+
const match = entries.filter((name) => name.endsWith(".xctestrun") && !name.startsWith(PREPARED_XCTESTRUN_PREFIX)).sort()[0];
|
|
2620
|
+
return match ? import_node_path5.default.join(dir, match) : null;
|
|
2621
|
+
}
|
|
2622
|
+
function resolveOverrideXctestrun(override) {
|
|
2623
|
+
if (!import_node_fs5.default.existsSync(override)) {
|
|
2624
|
+
throw new Error(`PROWL_WDA_RUNNER points at a missing path: ${override}`);
|
|
2625
|
+
}
|
|
2626
|
+
if (override.endsWith(".xctestrun")) {
|
|
2627
|
+
return override;
|
|
2628
|
+
}
|
|
2629
|
+
const candidates = [];
|
|
2630
|
+
const stat = import_node_fs5.default.statSync(override);
|
|
2631
|
+
if (override.endsWith(".app")) {
|
|
2632
|
+
candidates.push(import_node_path5.default.dirname(import_node_path5.default.dirname(override)));
|
|
2633
|
+
} else if (stat.isDirectory()) {
|
|
2634
|
+
candidates.push(override, productsDir(override));
|
|
2635
|
+
}
|
|
2636
|
+
for (const dir of candidates) {
|
|
2637
|
+
const found = findXctestrunIn(dir);
|
|
2638
|
+
if (found) {
|
|
2639
|
+
return found;
|
|
2640
|
+
}
|
|
2641
|
+
}
|
|
2642
|
+
throw new Error(
|
|
2643
|
+
`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\`.`
|
|
2644
|
+
);
|
|
2645
|
+
}
|
|
2646
|
+
async function resolveWdaTestRun(options = {}) {
|
|
2274
2647
|
const runner = options.runner ?? execFileXcrunRunner;
|
|
2275
2648
|
const env = options.env ?? process.env;
|
|
2276
2649
|
const homeDir = options.homeDir ?? import_node_os2.default.homedir();
|
|
@@ -2278,10 +2651,7 @@ async function resolveWdaRunner(options = {}) {
|
|
|
2278
2651
|
`));
|
|
2279
2652
|
const override = env.PROWL_WDA_RUNNER;
|
|
2280
2653
|
if (override) {
|
|
2281
|
-
|
|
2282
|
-
throw new Error(`PROWL_WDA_RUNNER points at a missing path: ${override}`);
|
|
2283
|
-
}
|
|
2284
|
-
return override;
|
|
2654
|
+
return resolveOverrideXctestrun(override);
|
|
2285
2655
|
}
|
|
2286
2656
|
const { projectPath, version } = resolveWdaProject(options.requireFn);
|
|
2287
2657
|
const xcode = await xcodeVersion(runner);
|
|
@@ -2291,9 +2661,9 @@ async function resolveWdaRunner(options = {}) {
|
|
|
2291
2661
|
);
|
|
2292
2662
|
}
|
|
2293
2663
|
const cacheDir = wdaCacheDir(version, xcode, homeDir);
|
|
2294
|
-
const
|
|
2295
|
-
if (
|
|
2296
|
-
return
|
|
2664
|
+
const cached = findXctestrunIn(productsDir(cacheDir));
|
|
2665
|
+
if (cached) {
|
|
2666
|
+
return cached;
|
|
2297
2667
|
}
|
|
2298
2668
|
log(
|
|
2299
2669
|
`Prowl: building WebDriverAgent for the iOS target (first run only; this can take a few minutes)\u2026`
|
|
@@ -2320,12 +2690,106 @@ async function resolveWdaRunner(options = {}) {
|
|
|
2320
2690
|
`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
2691
|
);
|
|
2322
2692
|
}
|
|
2323
|
-
|
|
2693
|
+
const built = findXctestrunIn(productsDir(cacheDir));
|
|
2694
|
+
if (!built) {
|
|
2324
2695
|
throw new Error(
|
|
2325
|
-
`WebDriverAgent build succeeded but
|
|
2696
|
+
`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.`
|
|
2697
|
+
);
|
|
2698
|
+
}
|
|
2699
|
+
return built;
|
|
2700
|
+
}
|
|
2701
|
+
function injectUsePortIntoXctestrun(plist, port) {
|
|
2702
|
+
const clone = structuredClone(plist);
|
|
2703
|
+
let injected = 0;
|
|
2704
|
+
const visit = (node) => {
|
|
2705
|
+
if (Array.isArray(node)) {
|
|
2706
|
+
for (const entry of node) {
|
|
2707
|
+
visit(entry);
|
|
2708
|
+
}
|
|
2709
|
+
return;
|
|
2710
|
+
}
|
|
2711
|
+
if (!node || typeof node !== "object") {
|
|
2712
|
+
return;
|
|
2713
|
+
}
|
|
2714
|
+
const obj = node;
|
|
2715
|
+
const isTarget = typeof obj.TestBundlePath === "string" || typeof obj.TestHostPath === "string";
|
|
2716
|
+
const existingEnv = obj.EnvironmentVariables && typeof obj.EnvironmentVariables === "object" && !Array.isArray(obj.EnvironmentVariables) ? obj.EnvironmentVariables : void 0;
|
|
2717
|
+
if (isTarget || existingEnv) {
|
|
2718
|
+
const env = existingEnv ?? {};
|
|
2719
|
+
env[WDA_USE_PORT_ENV] = String(port);
|
|
2720
|
+
obj.EnvironmentVariables = env;
|
|
2721
|
+
injected += 1;
|
|
2722
|
+
}
|
|
2723
|
+
for (const value of Object.values(obj)) {
|
|
2724
|
+
visit(value);
|
|
2725
|
+
}
|
|
2726
|
+
};
|
|
2727
|
+
visit(clone);
|
|
2728
|
+
if (injected === 0) {
|
|
2729
|
+
throw new Error(
|
|
2730
|
+
"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."
|
|
2731
|
+
);
|
|
2732
|
+
}
|
|
2733
|
+
return clone;
|
|
2734
|
+
}
|
|
2735
|
+
function runPlutil(args) {
|
|
2736
|
+
return new Promise((resolve, reject) => {
|
|
2737
|
+
(0, import_node_child_process6.execFile)(
|
|
2738
|
+
"plutil",
|
|
2739
|
+
args,
|
|
2740
|
+
{ encoding: "utf-8", maxBuffer: 32 * 1024 * 1024 },
|
|
2741
|
+
(error, stdout, stderr) => {
|
|
2742
|
+
if (error) {
|
|
2743
|
+
reject(
|
|
2744
|
+
new Error(
|
|
2745
|
+
`\`plutil ${args.join(" ")}\` failed: ${(stderr || error.message).slice(0, 400)}`
|
|
2746
|
+
)
|
|
2747
|
+
);
|
|
2748
|
+
return;
|
|
2749
|
+
}
|
|
2750
|
+
resolve(stdout ?? "");
|
|
2751
|
+
}
|
|
2326
2752
|
);
|
|
2753
|
+
});
|
|
2754
|
+
}
|
|
2755
|
+
var defaultWdaTestRunPreparer = async ({ xctestrunPath, port }) => {
|
|
2756
|
+
const json = await runPlutil(["-convert", "json", "-o", "-", xctestrunPath]);
|
|
2757
|
+
let parsed;
|
|
2758
|
+
try {
|
|
2759
|
+
parsed = JSON.parse(json);
|
|
2760
|
+
} catch {
|
|
2761
|
+
throw new Error(`Could not parse the WebDriverAgent .xctestrun as JSON: ${xctestrunPath}`);
|
|
2762
|
+
}
|
|
2763
|
+
const injected = injectUsePortIntoXctestrun(parsed, port);
|
|
2764
|
+
const stem = `${PREPARED_XCTESTRUN_PREFIX}${port}-${process.pid}`;
|
|
2765
|
+
const outPath = import_node_path5.default.join(import_node_path5.default.dirname(xctestrunPath), `${stem}.xctestrun`);
|
|
2766
|
+
const jsonPath = import_node_path5.default.join(import_node_os2.default.tmpdir(), `${stem}.json`);
|
|
2767
|
+
import_node_fs5.default.writeFileSync(jsonPath, JSON.stringify(injected));
|
|
2768
|
+
try {
|
|
2769
|
+
await runPlutil(["-convert", "xml1", jsonPath, "-o", outPath]);
|
|
2770
|
+
} finally {
|
|
2771
|
+
import_node_fs5.default.rmSync(jsonPath, { force: true });
|
|
2327
2772
|
}
|
|
2328
|
-
return
|
|
2773
|
+
return outPath;
|
|
2774
|
+
};
|
|
2775
|
+
function cleanupPreparedTestRun(preparedPath) {
|
|
2776
|
+
if (!preparedPath) {
|
|
2777
|
+
return;
|
|
2778
|
+
}
|
|
2779
|
+
if (!import_node_path5.default.basename(preparedPath).startsWith(PREPARED_XCTESTRUN_PREFIX)) {
|
|
2780
|
+
return;
|
|
2781
|
+
}
|
|
2782
|
+
import_node_fs5.default.rmSync(preparedPath, { force: true });
|
|
2783
|
+
}
|
|
2784
|
+
function wdaTestRunArgs(xctestrunPath, udid) {
|
|
2785
|
+
return [
|
|
2786
|
+
"xcodebuild",
|
|
2787
|
+
"test-without-building",
|
|
2788
|
+
"-xctestrun",
|
|
2789
|
+
xctestrunPath,
|
|
2790
|
+
"-destination",
|
|
2791
|
+
`id=${udid}`
|
|
2792
|
+
];
|
|
2329
2793
|
}
|
|
2330
2794
|
async function resolveBundleId(app, runner, udid, coldStart, allowedApps) {
|
|
2331
2795
|
if (!looksLikeIosAppPath(app)) {
|
|
@@ -2356,6 +2820,8 @@ async function resolveBundleId(app, runner, udid, coldStart, allowedApps) {
|
|
|
2356
2820
|
}
|
|
2357
2821
|
async function launchIosSession(options) {
|
|
2358
2822
|
const runner = options.runner ?? execFileXcrunRunner;
|
|
2823
|
+
const spawner = options.spawner ?? spawnXcrunProcess;
|
|
2824
|
+
const preparer = options.testRunPreparer ?? defaultWdaTestRunPreparer;
|
|
2359
2825
|
const portAllocator = options.portAllocator ?? findFreePort;
|
|
2360
2826
|
const connector = options.agentConnector ?? defaultIosAgentConnector;
|
|
2361
2827
|
const requestTimeoutMs = Math.max(options.timeoutMs ?? 1e4, DEFAULT_WDA_REQUEST_TIMEOUT_MS) + 5e3;
|
|
@@ -2379,14 +2845,14 @@ async function launchIosSession(options) {
|
|
|
2379
2845
|
options.coldStart ?? false,
|
|
2380
2846
|
options.allowedApps ?? []
|
|
2381
2847
|
);
|
|
2382
|
-
const
|
|
2383
|
-
await installApp(runner, udid, wdaRunnerApp);
|
|
2848
|
+
const baseTestRun = options.wdaTestRun ?? await resolveWdaTestRun({ runner, logger: options.logger });
|
|
2384
2849
|
for (let attempt = 1; attempt <= WDA_STARTUP_ATTEMPTS; attempt += 1) {
|
|
2385
2850
|
let client;
|
|
2851
|
+
let wdaProcess;
|
|
2852
|
+
let preparedTestRun;
|
|
2386
2853
|
let tornDown = false;
|
|
2387
|
-
let terminateWda = false;
|
|
2388
2854
|
let terminateTarget = false;
|
|
2389
|
-
let stage = "
|
|
2855
|
+
let stage = "prepare";
|
|
2390
2856
|
const teardownAttempt = async () => {
|
|
2391
2857
|
if (tornDown) {
|
|
2392
2858
|
return;
|
|
@@ -2395,18 +2861,18 @@ async function launchIosSession(options) {
|
|
|
2395
2861
|
if (client) {
|
|
2396
2862
|
await client.close().catch(() => void 0);
|
|
2397
2863
|
}
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
}
|
|
2864
|
+
wdaProcess?.kill();
|
|
2865
|
+
await terminateApp(runner, udid, WDA_RUNNER_BUNDLE_ID);
|
|
2401
2866
|
if (terminateTarget) {
|
|
2402
2867
|
await terminateApp(runner, udid, bundleId);
|
|
2403
2868
|
}
|
|
2869
|
+
cleanupPreparedTestRun(preparedTestRun);
|
|
2404
2870
|
};
|
|
2405
2871
|
try {
|
|
2406
2872
|
const port = await portAllocator();
|
|
2873
|
+
preparedTestRun = await preparer({ xctestrunPath: baseTestRun, port });
|
|
2407
2874
|
stage = "wda-launch";
|
|
2408
|
-
|
|
2409
|
-
await launchApp(runner, udid, WDA_RUNNER_BUNDLE_ID, { [WDA_USE_PORT_ENV]: String(port) });
|
|
2875
|
+
wdaProcess = spawner(wdaTestRunArgs(preparedTestRun, udid));
|
|
2410
2876
|
stage = "target-launch";
|
|
2411
2877
|
terminateTarget = true;
|
|
2412
2878
|
await launchApp(runner, udid, bundleId);
|
|
@@ -2713,6 +3179,9 @@ var assertScreenshotStepSchema = import_zod.z.object({
|
|
|
2713
3179
|
threshold: import_zod.z.number().min(0).max(1).optional()
|
|
2714
3180
|
}).strict()
|
|
2715
3181
|
}).strict();
|
|
3182
|
+
var assertWithAiStepSchema = import_zod.z.object({
|
|
3183
|
+
assertWithAI: import_zod.z.string().min(1)
|
|
3184
|
+
}).strict();
|
|
2716
3185
|
var copyTextStepSchema = import_zod.z.object({
|
|
2717
3186
|
copyText: import_zod.z.object({
|
|
2718
3187
|
selector: import_zod.z.string().min(1),
|
|
@@ -2755,6 +3224,7 @@ var stepSchema = import_zod.z.union([
|
|
|
2755
3224
|
evalScriptStepSchema,
|
|
2756
3225
|
runScriptStepSchema,
|
|
2757
3226
|
assertScreenshotStepSchema,
|
|
3227
|
+
assertWithAiStepSchema,
|
|
2758
3228
|
copyTextStepSchema,
|
|
2759
3229
|
waitForDownloadStepSchema
|
|
2760
3230
|
]);
|
|
@@ -3325,6 +3795,11 @@ function interpolateStep(step, vars, stepPath2, redacted) {
|
|
|
3325
3795
|
}
|
|
3326
3796
|
};
|
|
3327
3797
|
}
|
|
3798
|
+
if ("assertWithAI" in step) {
|
|
3799
|
+
return {
|
|
3800
|
+
assertWithAI: interpolateString(step.assertWithAI, vars).value
|
|
3801
|
+
};
|
|
3802
|
+
}
|
|
3328
3803
|
if ("copyText" in step) {
|
|
3329
3804
|
return {
|
|
3330
3805
|
copyText: {
|
|
@@ -3718,52 +4193,292 @@ function createRunPolicy(driver, options) {
|
|
|
3718
4193
|
throw new Error(`Navigation to disallowed domain: ${url.hostname}`);
|
|
3719
4194
|
}
|
|
3720
4195
|
}
|
|
3721
|
-
function ensureAppAllowed(app) {
|
|
3722
|
-
if (!allowedApps.includes(app)) {
|
|
3723
|
-
throw new Error(`Interaction with disallowed app: ${app}`);
|
|
3724
|
-
}
|
|
4196
|
+
function ensureAppAllowed(app) {
|
|
4197
|
+
if (!allowedApps.includes(app)) {
|
|
4198
|
+
throw new Error(`Interaction with disallowed app: ${app}`);
|
|
4199
|
+
}
|
|
4200
|
+
}
|
|
4201
|
+
function ensureLocationAllowed(activeDriver) {
|
|
4202
|
+
if (activeDriver.capabilities.has("navigate")) {
|
|
4203
|
+
ensureUrlAllowed(activeDriver.currentUrl());
|
|
4204
|
+
}
|
|
4205
|
+
}
|
|
4206
|
+
const healProbe = {
|
|
4207
|
+
locator: (selector) => ({ count: () => driver.count(selector) })
|
|
4208
|
+
};
|
|
4209
|
+
async function resolveActionSelector(selector) {
|
|
4210
|
+
assertAllowedSelector(selector);
|
|
4211
|
+
if (!selfHealing) {
|
|
4212
|
+
return { selector };
|
|
4213
|
+
}
|
|
4214
|
+
let matched = false;
|
|
4215
|
+
try {
|
|
4216
|
+
matched = await driver.count(selector) > 0;
|
|
4217
|
+
} catch {
|
|
4218
|
+
return { selector };
|
|
4219
|
+
}
|
|
4220
|
+
if (matched) {
|
|
4221
|
+
return { selector };
|
|
4222
|
+
}
|
|
4223
|
+
const healed = await healSelector(healProbe, selector, { enabled: true });
|
|
4224
|
+
if (!healed) {
|
|
4225
|
+
return { selector };
|
|
4226
|
+
}
|
|
4227
|
+
assertAllowedSelector(healed.selector);
|
|
4228
|
+
console.warn(
|
|
4229
|
+
`Self-healed selector: "${selector}" \u2192 "${healed.selector}" (${healed.strategy}). Update your hunt to use a stable selector.`
|
|
4230
|
+
);
|
|
4231
|
+
return { selector: healed.selector, healedFrom: healed.healedFrom };
|
|
4232
|
+
}
|
|
4233
|
+
return {
|
|
4234
|
+
assertWithinMaxSteps,
|
|
4235
|
+
ensureUrlAllowed,
|
|
4236
|
+
ensureAppAllowed,
|
|
4237
|
+
ensureLocationAllowed,
|
|
4238
|
+
assertAllowedSelector,
|
|
4239
|
+
resolveActionSelector
|
|
4240
|
+
};
|
|
4241
|
+
}
|
|
4242
|
+
|
|
4243
|
+
// src/generator/ai.ts
|
|
4244
|
+
var DEFAULT_BASE_URL = {
|
|
4245
|
+
anthropic: "https://api.anthropic.com",
|
|
4246
|
+
openai: "https://api.openai.com"
|
|
4247
|
+
};
|
|
4248
|
+
var AI_REQUEST_TIMEOUT_MS = 3e4;
|
|
4249
|
+
function apiRoot(config) {
|
|
4250
|
+
return config.baseUrl ?? DEFAULT_BASE_URL[config.provider];
|
|
4251
|
+
}
|
|
4252
|
+
function defaultModelFor(provider) {
|
|
4253
|
+
return provider === "anthropic" ? "claude-sonnet-4-5-20250929" : "gpt-4o";
|
|
4254
|
+
}
|
|
4255
|
+
function resolveAiEnv() {
|
|
4256
|
+
const provider = process.env.PROWL_AI_PROVIDER ?? "anthropic";
|
|
4257
|
+
if (provider !== "anthropic" && provider !== "openai") {
|
|
4258
|
+
throw new Error(`Unsupported AI provider: ${provider}. Use "anthropic" or "openai".`);
|
|
4259
|
+
}
|
|
4260
|
+
const model = process.env.PROWL_AI_MODEL ?? defaultModelFor(provider);
|
|
4261
|
+
const baseUrl = normalizeBaseUrl(process.env.PROWL_AI_BASE_URL) ?? DEFAULT_BASE_URL[provider];
|
|
4262
|
+
return { provider, model, baseUrl, apiKey: process.env.PROWL_AI_KEY };
|
|
4263
|
+
}
|
|
4264
|
+
function normalizeBaseUrl(value) {
|
|
4265
|
+
if (value === void 0) return void 0;
|
|
4266
|
+
const trimmed = value.trim().replace(/\/+$/, "");
|
|
4267
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
4268
|
+
}
|
|
4269
|
+
function resolveAiConfig() {
|
|
4270
|
+
const env = resolveAiEnv();
|
|
4271
|
+
if (!env.apiKey) {
|
|
4272
|
+
throw new Error(
|
|
4273
|
+
"PROWL_AI_KEY environment variable is required. Set it to your Anthropic or OpenAI API key."
|
|
4274
|
+
);
|
|
4275
|
+
}
|
|
4276
|
+
return { provider: env.provider, model: env.model, apiKey: env.apiKey, baseUrl: env.baseUrl };
|
|
4277
|
+
}
|
|
4278
|
+
function tryResolveAiConfig() {
|
|
4279
|
+
const env = resolveAiEnv();
|
|
4280
|
+
if (!env.apiKey) {
|
|
4281
|
+
return null;
|
|
4282
|
+
}
|
|
4283
|
+
return { provider: env.provider, model: env.model, apiKey: env.apiKey, baseUrl: env.baseUrl };
|
|
4284
|
+
}
|
|
4285
|
+
async function generateWithAi(prompt, config) {
|
|
4286
|
+
if (config.provider === "anthropic") {
|
|
4287
|
+
return generateWithAnthropic(prompt, config);
|
|
4288
|
+
}
|
|
4289
|
+
return generateWithOpenAi(prompt, config);
|
|
4290
|
+
}
|
|
4291
|
+
async function generateWithAnthropic(prompt, config) {
|
|
4292
|
+
const data = await postJson(
|
|
4293
|
+
"Anthropic",
|
|
4294
|
+
`${apiRoot(config)}/v1/messages`,
|
|
4295
|
+
anthropicHeaders(config),
|
|
4296
|
+
{
|
|
4297
|
+
model: config.model,
|
|
4298
|
+
max_tokens: 4096,
|
|
4299
|
+
messages: [
|
|
4300
|
+
{ role: "user", content: prompt }
|
|
4301
|
+
]
|
|
4302
|
+
}
|
|
4303
|
+
);
|
|
4304
|
+
return extractAnthropicText(data);
|
|
4305
|
+
}
|
|
4306
|
+
async function generateWithOpenAi(prompt, config) {
|
|
4307
|
+
const data = await postJson(
|
|
4308
|
+
"OpenAI",
|
|
4309
|
+
`${apiRoot(config)}/v1/chat/completions`,
|
|
4310
|
+
openAiHeaders(config),
|
|
4311
|
+
{
|
|
4312
|
+
model: config.model,
|
|
4313
|
+
messages: [
|
|
4314
|
+
{ role: "user", content: prompt }
|
|
4315
|
+
],
|
|
4316
|
+
max_tokens: 4096
|
|
4317
|
+
}
|
|
4318
|
+
);
|
|
4319
|
+
return extractOpenAiText(data);
|
|
4320
|
+
}
|
|
4321
|
+
function buildVisionPrompt(assertion) {
|
|
4322
|
+
return [
|
|
4323
|
+
"You are a meticulous QA reviewer. You are given a screenshot of an application",
|
|
4324
|
+
"and a single assertion describing what should be true about it.",
|
|
4325
|
+
"",
|
|
4326
|
+
"Assertion:",
|
|
4327
|
+
assertion,
|
|
4328
|
+
"",
|
|
4329
|
+
"Decide whether the assertion holds for the screenshot. Judge ONLY what is",
|
|
4330
|
+
"visible; do not assume behavior you cannot see. Be strict: if the assertion",
|
|
4331
|
+
"is not clearly satisfied, it fails.",
|
|
4332
|
+
"",
|
|
4333
|
+
"Reply with ONLY a single JSON object on one line, no markdown, no code fences:",
|
|
4334
|
+
'{"pass": <true|false>, "reason": "<one concise sentence explaining the verdict>"}'
|
|
4335
|
+
].join("\n");
|
|
4336
|
+
}
|
|
4337
|
+
function parseVisionVerdict(raw) {
|
|
4338
|
+
const cleaned = stripCodeFences(raw).trim();
|
|
4339
|
+
const candidate = extractJsonObject(cleaned);
|
|
4340
|
+
if (candidate === null) {
|
|
4341
|
+
throw new Error(
|
|
4342
|
+
`Could not parse a JSON verdict from the AI response: ${truncateForError(raw)}`
|
|
4343
|
+
);
|
|
4344
|
+
}
|
|
4345
|
+
let parsed;
|
|
4346
|
+
try {
|
|
4347
|
+
parsed = JSON.parse(candidate);
|
|
4348
|
+
} catch {
|
|
4349
|
+
throw new Error(
|
|
4350
|
+
`AI verdict was not valid JSON: ${truncateForError(raw)}`
|
|
4351
|
+
);
|
|
3725
4352
|
}
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
ensureUrlAllowed(activeDriver.currentUrl());
|
|
3729
|
-
}
|
|
4353
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
4354
|
+
throw new Error(`AI verdict was not a JSON object: ${truncateForError(raw)}`);
|
|
3730
4355
|
}
|
|
3731
|
-
const
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
|
|
4356
|
+
const record = parsed;
|
|
4357
|
+
if (typeof record.pass !== "boolean") {
|
|
4358
|
+
throw new Error(
|
|
4359
|
+
`AI verdict is missing a boolean "pass" field: ${truncateForError(raw)}`
|
|
4360
|
+
);
|
|
4361
|
+
}
|
|
4362
|
+
const reason = typeof record.reason === "string" && record.reason.trim().length > 0 ? record.reason.trim() : record.pass ? "Assertion satisfied." : "Assertion not satisfied.";
|
|
4363
|
+
return { pass: record.pass, reason };
|
|
4364
|
+
}
|
|
4365
|
+
function stripCodeFences(text) {
|
|
4366
|
+
const fence = text.match(/```(?:json)?\s*([\s\S]*?)```/i);
|
|
4367
|
+
return fence ? fence[1] : text;
|
|
4368
|
+
}
|
|
4369
|
+
function extractJsonObject(text) {
|
|
4370
|
+
const trimmed = text.trim();
|
|
4371
|
+
if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
|
|
4372
|
+
return trimmed;
|
|
4373
|
+
}
|
|
4374
|
+
const first = trimmed.indexOf("{");
|
|
4375
|
+
const last = trimmed.lastIndexOf("}");
|
|
4376
|
+
if (first === -1 || last === -1 || last <= first) {
|
|
4377
|
+
return null;
|
|
4378
|
+
}
|
|
4379
|
+
return trimmed.slice(first, last + 1);
|
|
4380
|
+
}
|
|
4381
|
+
function truncateForError(text) {
|
|
4382
|
+
const collapsed = text.replace(/\s+/g, " ").trim();
|
|
4383
|
+
return collapsed.length > 200 ? `${collapsed.slice(0, 199)}\u2026` : collapsed;
|
|
4384
|
+
}
|
|
4385
|
+
async function assertWithAiVision(input, config) {
|
|
4386
|
+
const raw = config.provider === "anthropic" ? await visionWithAnthropic(input, config) : await visionWithOpenAi(input, config);
|
|
4387
|
+
return parseVisionVerdict(raw);
|
|
4388
|
+
}
|
|
4389
|
+
async function visionWithAnthropic(input, config) {
|
|
4390
|
+
const data = await postJson(
|
|
4391
|
+
"Anthropic",
|
|
4392
|
+
`${apiRoot(config)}/v1/messages`,
|
|
4393
|
+
anthropicHeaders(config),
|
|
4394
|
+
{
|
|
4395
|
+
model: config.model,
|
|
4396
|
+
max_tokens: 1024,
|
|
4397
|
+
temperature: 0,
|
|
4398
|
+
messages: [
|
|
4399
|
+
{
|
|
4400
|
+
role: "user",
|
|
4401
|
+
content: [
|
|
4402
|
+
{
|
|
4403
|
+
type: "image",
|
|
4404
|
+
source: {
|
|
4405
|
+
type: "base64",
|
|
4406
|
+
media_type: input.mediaType,
|
|
4407
|
+
data: input.imageBase64
|
|
4408
|
+
}
|
|
4409
|
+
},
|
|
4410
|
+
{ type: "text", text: buildVisionPrompt(input.assertion) }
|
|
4411
|
+
]
|
|
4412
|
+
}
|
|
4413
|
+
]
|
|
3747
4414
|
}
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
4415
|
+
);
|
|
4416
|
+
return extractAnthropicText(data);
|
|
4417
|
+
}
|
|
4418
|
+
async function visionWithOpenAi(input, config) {
|
|
4419
|
+
const data = await postJson(
|
|
4420
|
+
"OpenAI",
|
|
4421
|
+
`${apiRoot(config)}/v1/chat/completions`,
|
|
4422
|
+
openAiHeaders(config),
|
|
4423
|
+
{
|
|
4424
|
+
model: config.model,
|
|
4425
|
+
max_tokens: 1024,
|
|
4426
|
+
temperature: 0,
|
|
4427
|
+
messages: [
|
|
4428
|
+
{
|
|
4429
|
+
role: "user",
|
|
4430
|
+
content: [
|
|
4431
|
+
{ type: "text", text: buildVisionPrompt(input.assertion) },
|
|
4432
|
+
{
|
|
4433
|
+
type: "image_url",
|
|
4434
|
+
image_url: { url: `data:${input.mediaType};base64,${input.imageBase64}` }
|
|
4435
|
+
}
|
|
4436
|
+
]
|
|
4437
|
+
}
|
|
4438
|
+
]
|
|
3751
4439
|
}
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
4440
|
+
);
|
|
4441
|
+
return extractOpenAiText(data);
|
|
4442
|
+
}
|
|
4443
|
+
async function postJson(provider, url, headers, body) {
|
|
4444
|
+
const response = await fetch(url, {
|
|
4445
|
+
method: "POST",
|
|
4446
|
+
headers,
|
|
4447
|
+
body: JSON.stringify(body),
|
|
4448
|
+
signal: AbortSignal.timeout(AI_REQUEST_TIMEOUT_MS)
|
|
4449
|
+
});
|
|
4450
|
+
if (!response.ok) {
|
|
4451
|
+
const responseBody = await response.text();
|
|
4452
|
+
throw new Error(`${provider} API error (${response.status}): ${responseBody}`);
|
|
3757
4453
|
}
|
|
4454
|
+
return await response.json();
|
|
4455
|
+
}
|
|
4456
|
+
function anthropicHeaders(config) {
|
|
3758
4457
|
return {
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
4458
|
+
"Content-Type": "application/json",
|
|
4459
|
+
"x-api-key": config.apiKey,
|
|
4460
|
+
"anthropic-version": "2023-06-01"
|
|
4461
|
+
};
|
|
4462
|
+
}
|
|
4463
|
+
function openAiHeaders(config) {
|
|
4464
|
+
return {
|
|
4465
|
+
"Content-Type": "application/json",
|
|
4466
|
+
"Authorization": `Bearer ${config.apiKey}`
|
|
3765
4467
|
};
|
|
3766
4468
|
}
|
|
4469
|
+
function extractAnthropicText(data) {
|
|
4470
|
+
const textBlock = data.content?.find((c) => c.type === "text");
|
|
4471
|
+
if (!textBlock?.text) {
|
|
4472
|
+
throw new Error("Anthropic API returned no text content");
|
|
4473
|
+
}
|
|
4474
|
+
return textBlock.text;
|
|
4475
|
+
}
|
|
4476
|
+
function extractOpenAiText(data) {
|
|
4477
|
+
if (!data.choices?.[0]?.message?.content) {
|
|
4478
|
+
throw new Error("OpenAI API returned no content");
|
|
4479
|
+
}
|
|
4480
|
+
return data.choices[0].message.content;
|
|
4481
|
+
}
|
|
3767
4482
|
|
|
3768
4483
|
// src/runner/steps.ts
|
|
3769
4484
|
function getStepType(step) {
|
|
@@ -3793,6 +4508,7 @@ function getStepType(step) {
|
|
|
3793
4508
|
if ("evalScript" in step) return "evalScript";
|
|
3794
4509
|
if ("runScript" in step) return "runScript";
|
|
3795
4510
|
if ("assertScreenshot" in step) return "assertScreenshot";
|
|
4511
|
+
if ("assertWithAI" in step) return "assertWithAI";
|
|
3796
4512
|
if ("copyText" in step) return "copyText";
|
|
3797
4513
|
if ("waitForDownload" in step) return "waitForDownload";
|
|
3798
4514
|
return "step";
|
|
@@ -3852,6 +4568,9 @@ function applyRuntimeVars(step, vars) {
|
|
|
3852
4568
|
}
|
|
3853
4569
|
};
|
|
3854
4570
|
}
|
|
4571
|
+
if ("assertWithAI" in step) {
|
|
4572
|
+
return { assertWithAI: sub(step.assertWithAI) };
|
|
4573
|
+
}
|
|
3855
4574
|
if ("copyText" in step) {
|
|
3856
4575
|
return { copyText: { selector: sub(step.copyText.selector), as: step.copyText.as } };
|
|
3857
4576
|
}
|
|
@@ -4785,6 +5504,48 @@ var STEP_HANDLERS = {
|
|
|
4785
5504
|
);
|
|
4786
5505
|
}
|
|
4787
5506
|
},
|
|
5507
|
+
assertWithAI: {
|
|
5508
|
+
capabilities: ["screenshot"],
|
|
5509
|
+
run: async (h) => {
|
|
5510
|
+
if (!("assertWithAI" in h.step)) unknownStep();
|
|
5511
|
+
const assertion = h.step.assertWithAI;
|
|
5512
|
+
const resolve = h.context.resolveAiConfig ?? tryResolveAiConfig;
|
|
5513
|
+
const aiConfig = resolve();
|
|
5514
|
+
if (!aiConfig) {
|
|
5515
|
+
return {
|
|
5516
|
+
kind: "result",
|
|
5517
|
+
result: {
|
|
5518
|
+
type: "assertWithAI",
|
|
5519
|
+
status: "warn",
|
|
5520
|
+
durationMs: Date.now() - h.stepStart,
|
|
5521
|
+
value: `skipped: no AI provider configured (set PROWL_AI_KEY to enable) \u2014 "${assertion}"`
|
|
5522
|
+
}
|
|
5523
|
+
};
|
|
5524
|
+
}
|
|
5525
|
+
const fileName = `assertWithAI_step_${h.index + 1}.png`;
|
|
5526
|
+
const relative = await h.addScreenshot(fileName);
|
|
5527
|
+
const screenshotFullPath = import_node_path9.default.join(h.context.runDir, relative);
|
|
5528
|
+
const imageBase64 = import_node_fs9.default.readFileSync(screenshotFullPath).toString("base64");
|
|
5529
|
+
const assertVision = h.context.assertVision ?? assertWithAiVision;
|
|
5530
|
+
const verdict = await assertVision(
|
|
5531
|
+
{ imageBase64, mediaType: "image/png", assertion },
|
|
5532
|
+
aiConfig
|
|
5533
|
+
);
|
|
5534
|
+
if (verdict.pass) {
|
|
5535
|
+
return {
|
|
5536
|
+
kind: "result",
|
|
5537
|
+
result: {
|
|
5538
|
+
type: "assertWithAI",
|
|
5539
|
+
status: "pass",
|
|
5540
|
+
durationMs: Date.now() - h.stepStart,
|
|
5541
|
+
value: verdict.reason,
|
|
5542
|
+
screenshot: relative
|
|
5543
|
+
}
|
|
5544
|
+
};
|
|
5545
|
+
}
|
|
5546
|
+
throw new Error(`AI assertion failed: ${verdict.reason}`);
|
|
5547
|
+
}
|
|
5548
|
+
},
|
|
4788
5549
|
copyText: {
|
|
4789
5550
|
capabilities: ["query"],
|
|
4790
5551
|
run: async (h) => {
|
|
@@ -6615,6 +7376,273 @@ async function readStatusMenu(client, menuTimeoutSeconds) {
|
|
|
6615
7376
|
}
|
|
6616
7377
|
}
|
|
6617
7378
|
|
|
7379
|
+
// src/analyzer/android.ts
|
|
7380
|
+
var ANDROID_INTERACTIVE_CLASSES = /* @__PURE__ */ new Set([
|
|
7381
|
+
"android.widget.Button",
|
|
7382
|
+
"android.widget.ImageButton",
|
|
7383
|
+
"android.widget.EditText",
|
|
7384
|
+
"android.widget.CheckBox",
|
|
7385
|
+
"android.widget.RadioButton",
|
|
7386
|
+
"android.widget.Switch",
|
|
7387
|
+
"android.widget.ToggleButton",
|
|
7388
|
+
"android.widget.Spinner",
|
|
7389
|
+
"android.widget.SeekBar",
|
|
7390
|
+
"android.widget.RatingBar",
|
|
7391
|
+
"android.widget.CompoundButton",
|
|
7392
|
+
"android.widget.AutoCompleteTextView",
|
|
7393
|
+
"android.widget.MultiAutoCompleteTextView",
|
|
7394
|
+
"android.widget.CheckedTextView",
|
|
7395
|
+
"androidx.appcompat.widget.SwitchCompat",
|
|
7396
|
+
"androidx.appcompat.widget.AppCompatButton",
|
|
7397
|
+
"androidx.appcompat.widget.AppCompatEditText"
|
|
7398
|
+
]);
|
|
7399
|
+
function str2(value) {
|
|
7400
|
+
if (value === void 0) {
|
|
7401
|
+
return void 0;
|
|
7402
|
+
}
|
|
7403
|
+
const trimmed = value.trim();
|
|
7404
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
7405
|
+
}
|
|
7406
|
+
function bool(value) {
|
|
7407
|
+
if (value === void 0) {
|
|
7408
|
+
return void 0;
|
|
7409
|
+
}
|
|
7410
|
+
return value === "true";
|
|
7411
|
+
}
|
|
7412
|
+
function toAndroidNode(element) {
|
|
7413
|
+
const a = element.attrs;
|
|
7414
|
+
return {
|
|
7415
|
+
className: str2(a["class"]),
|
|
7416
|
+
resourceId: str2(a["resource-id"]),
|
|
7417
|
+
contentDesc: str2(a["content-desc"]),
|
|
7418
|
+
text: str2(a.text),
|
|
7419
|
+
package: str2(a.package),
|
|
7420
|
+
clickable: bool(a.clickable),
|
|
7421
|
+
longClickable: bool(a["long-clickable"]),
|
|
7422
|
+
checkable: bool(a.checkable),
|
|
7423
|
+
checked: bool(a.checked),
|
|
7424
|
+
scrollable: bool(a.scrollable),
|
|
7425
|
+
focusable: bool(a.focusable),
|
|
7426
|
+
focused: bool(a.focused),
|
|
7427
|
+
enabled: bool(a.enabled),
|
|
7428
|
+
children: element.children.map(toAndroidNode)
|
|
7429
|
+
};
|
|
7430
|
+
}
|
|
7431
|
+
function parseAndroidHierarchy(xml) {
|
|
7432
|
+
const root = parseXml(xml);
|
|
7433
|
+
return root ? toAndroidNode(root) : null;
|
|
7434
|
+
}
|
|
7435
|
+
function isAndroidInteractive(node) {
|
|
7436
|
+
if (node.clickable || node.longClickable || node.checkable || node.scrollable) {
|
|
7437
|
+
return true;
|
|
7438
|
+
}
|
|
7439
|
+
return node.className !== void 0 && ANDROID_INTERACTIVE_CLASSES.has(node.className);
|
|
7440
|
+
}
|
|
7441
|
+
function rankAndroidSelectors(node) {
|
|
7442
|
+
return rankNativeSelectors({
|
|
7443
|
+
id: node.resourceId,
|
|
7444
|
+
label: node.contentDesc,
|
|
7445
|
+
role: node.className,
|
|
7446
|
+
name: node.text
|
|
7447
|
+
});
|
|
7448
|
+
}
|
|
7449
|
+
function androidNodeToNative(node) {
|
|
7450
|
+
return {
|
|
7451
|
+
...node.resourceId !== void 0 ? { id: node.resourceId } : {},
|
|
7452
|
+
...node.contentDesc !== void 0 ? { label: node.contentDesc } : {},
|
|
7453
|
+
...node.className !== void 0 ? { role: node.className } : {},
|
|
7454
|
+
textValues: node.text !== void 0 ? [node.text] : [],
|
|
7455
|
+
...node.focused !== void 0 ? { focused: node.focused } : {}
|
|
7456
|
+
};
|
|
7457
|
+
}
|
|
7458
|
+
function matchAndroidSelector(xml, selector, options = {}) {
|
|
7459
|
+
const parsedSelector = parseNativeSelector(selector);
|
|
7460
|
+
const root = parseAndroidHierarchy(xml);
|
|
7461
|
+
if (!root) {
|
|
7462
|
+
return [];
|
|
7463
|
+
}
|
|
7464
|
+
return matchNativeTree(
|
|
7465
|
+
ANDROID_MATCH_DIALECT,
|
|
7466
|
+
parsedSelector,
|
|
7467
|
+
root,
|
|
7468
|
+
androidNodeToNative,
|
|
7469
|
+
(node) => node.children,
|
|
7470
|
+
options
|
|
7471
|
+
);
|
|
7472
|
+
}
|
|
7473
|
+
function toElement2(node) {
|
|
7474
|
+
return {
|
|
7475
|
+
className: node.className ?? "?",
|
|
7476
|
+
...node.resourceId ? { resourceId: node.resourceId } : {},
|
|
7477
|
+
...node.contentDesc ? { contentDesc: node.contentDesc } : {},
|
|
7478
|
+
...node.text ? { text: node.text } : {},
|
|
7479
|
+
...node.clickable !== void 0 ? { clickable: node.clickable } : {},
|
|
7480
|
+
...node.checkable !== void 0 ? { checkable: node.checkable } : {},
|
|
7481
|
+
...node.scrollable !== void 0 ? { scrollable: node.scrollable } : {},
|
|
7482
|
+
...node.enabled !== void 0 ? { enabled: node.enabled } : {},
|
|
7483
|
+
selectors: rankAndroidSelectors(node)
|
|
7484
|
+
};
|
|
7485
|
+
}
|
|
7486
|
+
function collectInteractive2(root) {
|
|
7487
|
+
const out = [];
|
|
7488
|
+
const visit = (node) => {
|
|
7489
|
+
if (isAndroidInteractive(node)) {
|
|
7490
|
+
out.push(toElement2(node));
|
|
7491
|
+
}
|
|
7492
|
+
for (const child of node.children) {
|
|
7493
|
+
visit(child);
|
|
7494
|
+
}
|
|
7495
|
+
};
|
|
7496
|
+
visit(root);
|
|
7497
|
+
return out;
|
|
7498
|
+
}
|
|
7499
|
+
async function analyzeAndroidApp(client, options) {
|
|
7500
|
+
const xml = await client.source();
|
|
7501
|
+
const root = parseAndroidHierarchy(xml);
|
|
7502
|
+
const elements = root ? collectInteractive2(root) : [];
|
|
7503
|
+
return { app: options.app, elements };
|
|
7504
|
+
}
|
|
7505
|
+
|
|
7506
|
+
// src/analyzer/ios.ts
|
|
7507
|
+
var IOS_INTERACTIVE_TYPES = /* @__PURE__ */ new Set([
|
|
7508
|
+
"XCUIElementTypeButton",
|
|
7509
|
+
"XCUIElementTypeCell",
|
|
7510
|
+
"XCUIElementTypeTextField",
|
|
7511
|
+
"XCUIElementTypeSecureTextField",
|
|
7512
|
+
"XCUIElementTypeSearchField",
|
|
7513
|
+
"XCUIElementTypeSwitch",
|
|
7514
|
+
"XCUIElementTypeToggle",
|
|
7515
|
+
"XCUIElementTypeLink",
|
|
7516
|
+
"XCUIElementTypeMenuItem",
|
|
7517
|
+
"XCUIElementTypeSlider",
|
|
7518
|
+
"XCUIElementTypeStepper",
|
|
7519
|
+
"XCUIElementTypeTextView",
|
|
7520
|
+
"XCUIElementTypePickerWheel",
|
|
7521
|
+
"XCUIElementTypeTab",
|
|
7522
|
+
"XCUIElementTypeSegmentedControl",
|
|
7523
|
+
"XCUIElementTypeCheckBox",
|
|
7524
|
+
"XCUIElementTypeRadioButton",
|
|
7525
|
+
"XCUIElementTypeKey"
|
|
7526
|
+
]);
|
|
7527
|
+
var IOS_WINDOW_TYPE = "XCUIElementTypeWindow";
|
|
7528
|
+
function str3(value) {
|
|
7529
|
+
if (value === void 0) {
|
|
7530
|
+
return void 0;
|
|
7531
|
+
}
|
|
7532
|
+
const trimmed = value.trim();
|
|
7533
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
7534
|
+
}
|
|
7535
|
+
function bool2(value) {
|
|
7536
|
+
if (value === void 0) {
|
|
7537
|
+
return void 0;
|
|
7538
|
+
}
|
|
7539
|
+
return value === "true" || value === "1";
|
|
7540
|
+
}
|
|
7541
|
+
function toIosNode(element) {
|
|
7542
|
+
const a = element.attrs;
|
|
7543
|
+
return {
|
|
7544
|
+
type: str3(a.type) ?? str3(element.tag),
|
|
7545
|
+
name: str3(a.name),
|
|
7546
|
+
label: str3(a.label),
|
|
7547
|
+
value: str3(a.value),
|
|
7548
|
+
enabled: bool2(a.enabled),
|
|
7549
|
+
visible: bool2(a.visible),
|
|
7550
|
+
children: element.children.map(toIosNode)
|
|
7551
|
+
};
|
|
7552
|
+
}
|
|
7553
|
+
function parseIosHierarchy(xml) {
|
|
7554
|
+
const root = parseXml(xml);
|
|
7555
|
+
return root ? toIosNode(root) : null;
|
|
7556
|
+
}
|
|
7557
|
+
function hasAccessibilityId(node) {
|
|
7558
|
+
return node.name !== void 0 && node.name !== node.label;
|
|
7559
|
+
}
|
|
7560
|
+
function rankIosSelectors(node) {
|
|
7561
|
+
return rankNativeSelectors({
|
|
7562
|
+
id: hasAccessibilityId(node) ? node.name : void 0,
|
|
7563
|
+
label: node.label,
|
|
7564
|
+
role: node.type ? shortIosType(node.type) : void 0,
|
|
7565
|
+
name: node.label ?? node.value
|
|
7566
|
+
});
|
|
7567
|
+
}
|
|
7568
|
+
function iosNodeToNative(node) {
|
|
7569
|
+
const textValues = [];
|
|
7570
|
+
if (node.label !== void 0) {
|
|
7571
|
+
textValues.push(node.label);
|
|
7572
|
+
}
|
|
7573
|
+
if (node.value !== void 0) {
|
|
7574
|
+
textValues.push(node.value);
|
|
7575
|
+
}
|
|
7576
|
+
return {
|
|
7577
|
+
...node.name !== void 0 ? { id: node.name } : {},
|
|
7578
|
+
...node.label !== void 0 ? { label: node.label } : {},
|
|
7579
|
+
...node.type !== void 0 ? { role: node.type } : {},
|
|
7580
|
+
textValues
|
|
7581
|
+
};
|
|
7582
|
+
}
|
|
7583
|
+
function matchIosSelector(xml, selector) {
|
|
7584
|
+
const parsedSelector = parseNativeSelector(selector);
|
|
7585
|
+
const root = parseIosHierarchy(xml);
|
|
7586
|
+
if (!root) {
|
|
7587
|
+
return [];
|
|
7588
|
+
}
|
|
7589
|
+
return matchNativeTree(
|
|
7590
|
+
IOS_MATCH_DIALECT,
|
|
7591
|
+
parsedSelector,
|
|
7592
|
+
root,
|
|
7593
|
+
iosNodeToNative,
|
|
7594
|
+
(node) => node.children
|
|
7595
|
+
);
|
|
7596
|
+
}
|
|
7597
|
+
function toElement3(node) {
|
|
7598
|
+
return {
|
|
7599
|
+
type: node.type ?? "?",
|
|
7600
|
+
...node.name ? { name: node.name } : {},
|
|
7601
|
+
...node.label ? { label: node.label } : {},
|
|
7602
|
+
...node.value ? { value: node.value } : {},
|
|
7603
|
+
...node.enabled !== void 0 ? { enabled: node.enabled } : {},
|
|
7604
|
+
...node.visible !== void 0 ? { visible: node.visible } : {},
|
|
7605
|
+
selectors: rankIosSelectors(node)
|
|
7606
|
+
};
|
|
7607
|
+
}
|
|
7608
|
+
function toWindow2(node) {
|
|
7609
|
+
const [best] = rankIosSelectors(node);
|
|
7610
|
+
return {
|
|
7611
|
+
...node.name ? { name: node.name } : {},
|
|
7612
|
+
...node.label ? { label: node.label } : {},
|
|
7613
|
+
selector: best ?? `role=${shortIosType(IOS_WINDOW_TYPE)}`
|
|
7614
|
+
};
|
|
7615
|
+
}
|
|
7616
|
+
function isIosInteractive(node) {
|
|
7617
|
+
return node.type !== void 0 && IOS_INTERACTIVE_TYPES.has(node.type);
|
|
7618
|
+
}
|
|
7619
|
+
function collect(root) {
|
|
7620
|
+
const elements = [];
|
|
7621
|
+
const windows = [];
|
|
7622
|
+
const visit = (node) => {
|
|
7623
|
+
if (node.type === IOS_WINDOW_TYPE) {
|
|
7624
|
+
windows.push(toWindow2(node));
|
|
7625
|
+
}
|
|
7626
|
+
if (isIosInteractive(node)) {
|
|
7627
|
+
elements.push(toElement3(node));
|
|
7628
|
+
}
|
|
7629
|
+
for (const child of node.children) {
|
|
7630
|
+
visit(child);
|
|
7631
|
+
}
|
|
7632
|
+
};
|
|
7633
|
+
visit(root);
|
|
7634
|
+
return { elements, windows };
|
|
7635
|
+
}
|
|
7636
|
+
async function analyzeIosApp(client, options) {
|
|
7637
|
+
const xml = await client.source();
|
|
7638
|
+
const root = parseIosHierarchy(xml);
|
|
7639
|
+
if (!root) {
|
|
7640
|
+
return { app: options.app, elements: [], windows: [] };
|
|
7641
|
+
}
|
|
7642
|
+
const { elements, windows } = collect(root);
|
|
7643
|
+
return { app: options.app, elements, windows };
|
|
7644
|
+
}
|
|
7645
|
+
|
|
6618
7646
|
// src/generator/index.ts
|
|
6619
7647
|
var import_yaml2 = __toESM(require("yaml"), 1);
|
|
6620
7648
|
|
|
@@ -6675,6 +7703,7 @@ var STEP_REFERENCE = `
|
|
|
6675
7703
|
|
|
6676
7704
|
### Visual Regression
|
|
6677
7705
|
- assertScreenshot: { name: "baseline-name", threshold: 0.1 }
|
|
7706
|
+
- assertWithAI: "The login form should show email and password fields" \u2014 AI checks the screenshot against the claim
|
|
6678
7707
|
|
|
6679
7708
|
### Control Flow
|
|
6680
7709
|
- if: { visible: ".banner", then: [steps...] }
|
|
@@ -6716,81 +7745,6 @@ function extractYamlFromResponse(response) {
|
|
|
6716
7745
|
return response.trim();
|
|
6717
7746
|
}
|
|
6718
7747
|
|
|
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
7748
|
// src/generator/index.ts
|
|
6795
7749
|
function parseViewportFlag2(value) {
|
|
6796
7750
|
const match = /^(\d+)x(\d+)$/i.exec(value);
|
|
@@ -6835,7 +7789,9 @@ async function generateHunt(options) {
|
|
|
6835
7789
|
}
|
|
6836
7790
|
// Annotate the CommonJS export names for ESM import in node:
|
|
6837
7791
|
0 && (module.exports = {
|
|
7792
|
+
ANDROID_INTERACTIVE_CLASSES,
|
|
6838
7793
|
ANDROID_KEYCODES,
|
|
7794
|
+
ANDROID_MATCH_DIALECT,
|
|
6839
7795
|
DEFAULT_AGENT_REQUEST_TIMEOUT_MS,
|
|
6840
7796
|
DEFAULT_ANALYZE_TREE_DEPTH,
|
|
6841
7797
|
DEFAULT_FLAKY_THRESHOLD,
|
|
@@ -6843,19 +7799,27 @@ async function generateHunt(options) {
|
|
|
6843
7799
|
DEFAULT_SIMULATOR_LOCK_ROOT,
|
|
6844
7800
|
DEFAULT_WDA_REQUEST_TIMEOUT_MS,
|
|
6845
7801
|
INTERACTIVE_ROLES,
|
|
7802
|
+
IOS_INTERACTIVE_TYPES,
|
|
7803
|
+
IOS_MATCH_DIALECT,
|
|
6846
7804
|
IOS_PRESS_KEYS,
|
|
7805
|
+
IOS_WINDOW_TYPE,
|
|
7806
|
+
MACOS_MATCH_DIALECT,
|
|
7807
|
+
NATIVE_ATTRIBUTE_MAP,
|
|
6847
7808
|
SpawnMacHelperClient,
|
|
6848
7809
|
UIA2_REMOTE_PORT,
|
|
6849
7810
|
Uia2HttpError,
|
|
6850
7811
|
Uia2Transport,
|
|
6851
|
-
WDA_RUNNER_APP_NAME,
|
|
6852
7812
|
WDA_RUNNER_BUNDLE_ID,
|
|
7813
|
+
WDA_USE_PORT_ENV,
|
|
6853
7814
|
WEB_ONLY_STEP_TYPES,
|
|
6854
7815
|
WdaHttpError,
|
|
6855
7816
|
WdaTransport,
|
|
7817
|
+
analyzeAndroidApp,
|
|
7818
|
+
analyzeIosApp,
|
|
6856
7819
|
analyzeMacApp,
|
|
6857
7820
|
analyzePage,
|
|
6858
7821
|
androidAppAllowedIdentities,
|
|
7822
|
+
androidNodeToNative,
|
|
6859
7823
|
androidQueryToLocator,
|
|
6860
7824
|
assertAndroidAppAllowed,
|
|
6861
7825
|
assertIosAppAllowed,
|
|
@@ -6877,8 +7841,10 @@ async function generateHunt(options) {
|
|
|
6877
7841
|
createUia2Session,
|
|
6878
7842
|
createWdaAgentClient,
|
|
6879
7843
|
createWdaSession,
|
|
7844
|
+
decodeXmlEntities,
|
|
6880
7845
|
defaultAgentConnector,
|
|
6881
7846
|
defaultIosAgentConnector,
|
|
7847
|
+
defaultWdaTestRunPreparer,
|
|
6882
7848
|
escapePredicateArg,
|
|
6883
7849
|
escapeUiSelectorArg,
|
|
6884
7850
|
extractElementId,
|
|
@@ -6888,9 +7854,13 @@ async function generateHunt(options) {
|
|
|
6888
7854
|
generateHunt,
|
|
6889
7855
|
healSelector,
|
|
6890
7856
|
huntSchema,
|
|
7857
|
+
injectUsePortIntoXctestrun,
|
|
6891
7858
|
interpolateHunt,
|
|
6892
7859
|
iosAppAllowedIdentities,
|
|
7860
|
+
iosNodeToNative,
|
|
6893
7861
|
iosQueryToLocator,
|
|
7862
|
+
isAndroidInteractive,
|
|
7863
|
+
isIosInteractive,
|
|
6894
7864
|
launchAndroidSession,
|
|
6895
7865
|
launchIosSession,
|
|
6896
7866
|
launchMacSession,
|
|
@@ -6902,17 +7872,31 @@ async function generateHunt(options) {
|
|
|
6902
7872
|
loadHuntMeta,
|
|
6903
7873
|
loadHuntTags,
|
|
6904
7874
|
macdriverBuildInstructions,
|
|
7875
|
+
matchAndroidSelector,
|
|
7876
|
+
matchIosSelector,
|
|
7877
|
+
matchNativeTree,
|
|
7878
|
+
nodeMatchesSelector,
|
|
6905
7879
|
normalizeXcuiClassName,
|
|
6906
7880
|
parseAaptPackage,
|
|
6907
7881
|
parseAdbDevices,
|
|
7882
|
+
parseAndroidHierarchy,
|
|
6908
7883
|
parseAndroidSelector,
|
|
6909
7884
|
parseForwardPort,
|
|
7885
|
+
parseIosHierarchy,
|
|
6910
7886
|
parseIosSelector,
|
|
6911
7887
|
parseMacSelector,
|
|
7888
|
+
parseNativeSelector,
|
|
6912
7889
|
parseSimctlDevices,
|
|
7890
|
+
parseSnapshot,
|
|
6913
7891
|
parseXcodeVersion,
|
|
7892
|
+
parseXml,
|
|
7893
|
+
qualifyResourceId,
|
|
7894
|
+
quoteSelectorValue,
|
|
7895
|
+
rankAndroidSelectors,
|
|
6914
7896
|
rankFlaky,
|
|
7897
|
+
rankIosSelectors,
|
|
6915
7898
|
rankMacSelectors,
|
|
7899
|
+
rankNativeSelectors,
|
|
6916
7900
|
readHistory,
|
|
6917
7901
|
readHuntHistory,
|
|
6918
7902
|
readIosBundleIdentifier,
|
|
@@ -6920,18 +7904,22 @@ async function generateHunt(options) {
|
|
|
6920
7904
|
resolveAgentApks,
|
|
6921
7905
|
resolveHelperBinary,
|
|
6922
7906
|
resolveWdaProject,
|
|
6923
|
-
|
|
7907
|
+
resolveWdaTestRun,
|
|
6924
7908
|
runHunt,
|
|
6925
7909
|
runSuite,
|
|
6926
7910
|
selectDeviceSerial,
|
|
6927
7911
|
selectSimulatorUdid,
|
|
7912
|
+
shortIosType,
|
|
6928
7913
|
stepSchema,
|
|
7914
|
+
unquoteSelectorValue,
|
|
6929
7915
|
unwrapAndroidTextSelector,
|
|
6930
7916
|
unwrapIosTextSelector,
|
|
7917
|
+
unwrapNativeTextSelector,
|
|
6931
7918
|
updateBacklogFromSuite,
|
|
6932
7919
|
waitForAgentReady,
|
|
6933
7920
|
waitForWdaReady,
|
|
6934
7921
|
wdaCacheDir,
|
|
7922
|
+
wdaTestRunArgs,
|
|
6935
7923
|
webOnlyReason
|
|
6936
7924
|
});
|
|
6937
7925
|
//# sourceMappingURL=lib.cjs.map
|