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/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
- resolveWdaRunner: () => resolveWdaRunner,
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 parseAndroidSelector(selector) {
620
- const trimmed = selector.trim();
621
- if (trimmed === ":focus") {
622
- return { by: "focused" };
623
- }
624
- const idMatch = /^id=(.+)$/s.exec(trimmed);
625
- if (idMatch) {
626
- return { by: "id", value: unquote2(idMatch[1]) };
627
- }
628
- const roleMatch = /^role=([A-Za-z][\w.$-]*)(?:\[name=(.+)\])?$/s.exec(trimmed);
629
- if (roleMatch) {
630
- const name = roleMatch[2] !== void 0 ? unquote2(roleMatch[2]) : void 0;
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
- return { by: "text", value: trimmed };
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
- const trimmed = selector.trim();
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
- const result = await runner(
965
- withSerial(serial, [
966
- "shell",
967
- "monkey",
968
- "-p",
969
- pkg,
970
- "-c",
971
- "android.intent.category.LAUNCHER",
972
- "1"
973
- ]),
974
- { timeoutMs: 3e4 }
975
- );
976
- if (result.code !== 0 || /no activities found|aborted|cannot launch/i.test(result.stdout + result.stderr)) {
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
- `Failed to launch Android package "${pkg}" (exit ${result.code}): ${(result.stdout + result.stderr).trim().slice(0, 400)}. Is it installed?`
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 normalizeXcuiClassName(role) {
1560
- return role.startsWith("XCUIElementType") ? role : `XCUIElementType${role}`;
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
- const trimmed = selector.trim();
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
- const trimmed = selector.trim();
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 runnerAppPath(derivedDataPath) {
2271
- return import_node_path5.default.join(derivedDataPath, "Build", "Products", "Debug-iphonesimulator", WDA_RUNNER_APP_NAME);
2609
+ function productsDir(derivedDataPath) {
2610
+ return import_node_path5.default.join(derivedDataPath, "Build", "Products");
2272
2611
  }
2273
- async function resolveWdaRunner(options = {}) {
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
- if (!import_node_fs5.default.existsSync(override)) {
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 cachedRunner = runnerAppPath(cacheDir);
2295
- if (import_node_fs5.default.existsSync(cachedRunner)) {
2296
- return cachedRunner;
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
- if (!import_node_fs5.default.existsSync(cachedRunner)) {
2693
+ const built = findXctestrunIn(productsDir(cacheDir));
2694
+ if (!built) {
2324
2695
  throw new Error(
2325
- `WebDriverAgent build succeeded but the runner app was not found at ${cachedRunner}. This may indicate an Xcode layout change; set PROWL_WDA_RUNNER to a prebuilt runner.`
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 cachedRunner;
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 wdaRunnerApp = options.wdaRunnerApp ?? await resolveWdaRunner({ runner, logger: options.logger });
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 = "port";
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
- if (terminateWda) {
2399
- await terminateApp(runner, udid, WDA_RUNNER_BUNDLE_ID);
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
- terminateWda = true;
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
- function ensureLocationAllowed(activeDriver) {
3727
- if (activeDriver.capabilities.has("navigate")) {
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 healProbe = {
3732
- locator: (selector) => ({ count: () => driver.count(selector) })
3733
- };
3734
- async function resolveActionSelector(selector) {
3735
- assertAllowedSelector(selector);
3736
- if (!selfHealing) {
3737
- return { selector };
3738
- }
3739
- let matched = false;
3740
- try {
3741
- matched = await driver.count(selector) > 0;
3742
- } catch {
3743
- return { selector };
3744
- }
3745
- if (matched) {
3746
- return { selector };
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
- const healed = await healSelector(healProbe, selector, { enabled: true });
3749
- if (!healed) {
3750
- return { selector };
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
- assertAllowedSelector(healed.selector);
3753
- console.warn(
3754
- `Self-healed selector: "${selector}" \u2192 "${healed.selector}" (${healed.strategy}). Update your hunt to use a stable selector.`
3755
- );
3756
- return { selector: healed.selector, healedFrom: healed.healedFrom };
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
- assertWithinMaxSteps,
3760
- ensureUrlAllowed,
3761
- ensureAppAllowed,
3762
- ensureLocationAllowed,
3763
- assertAllowedSelector,
3764
- resolveActionSelector
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
- resolveWdaRunner,
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