@vostride/agent-qa-ios 0.2.3 → 0.2.4

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/index.cjs CHANGED
@@ -42,10 +42,10 @@ function positiveInteger(value) {
42
42
  const parsed = Number(value.trim());
43
43
  return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : void 0;
44
44
  }
45
- function resolveIOSSnapshotDepth(farmCapabilities) {
46
- return positiveInteger(farmCapabilities?.[IOS_SNAPSHOT_DEPTH_CAPABILITY]) ?? positiveInteger(process.env[IOS_SNAPSHOT_DEPTH_ENV]) ?? IOS_SNAPSHOT_MAX_DEPTH;
45
+ function resolveIOSSnapshotDepth() {
46
+ return positiveInteger(process.env[IOS_SNAPSHOT_DEPTH_ENV]) ?? IOS_SNAPSHOT_MAX_DEPTH;
47
47
  }
48
- async function createIOSSession(config, farmSession) {
48
+ async function createIOSSession(config) {
49
49
  const match = config.device?.match ?? {};
50
50
  const appiumUrl = process.env.AGENT_QA_APPIUM_URL ?? config.appiumUrl ?? "http://localhost:4723";
51
51
  const url = new URL(appiumUrl);
@@ -71,17 +71,6 @@ async function createIOSSession(config, farmSession) {
71
71
  delete capabilities[key];
72
72
  }
73
73
  }
74
- if (farmSession) {
75
- Object.assign(capabilities, farmSession.capabilities);
76
- return (0, import_webdriverio.remote)({
77
- protocol: "https",
78
- hostname: farmSession.hostname,
79
- port: farmSession.port,
80
- path: farmSession.path,
81
- logLevel: config.verbose ? "info" : "silent",
82
- capabilities
83
- });
84
- }
85
74
  return (0, import_webdriverio.remote)({
86
75
  hostname: url.hostname,
87
76
  port: parseInt(url.port || "4723", 10),
@@ -130,15 +119,6 @@ function firstErrorLine(err) {
130
119
  return (err instanceof Error ? err.message : String(err)).split("\n")[0];
131
120
  }
132
121
  var IOS_TAP_HOLD_MS = 100;
133
- async function safePerform(chain) {
134
- try {
135
- await chain.perform();
136
- } catch (err) {
137
- const msg = err instanceof Error ? err.message : String(err);
138
- if (msg.includes("actions") && msg.includes("DELETE")) return;
139
- throw err;
140
- }
141
- }
142
122
  var IOS_SYSLOG_REGEX = /^.*?\s+(\S+)\[(\d+)\].*?:\s+(.*)/;
143
123
  var IOS_APPIUM_LOG_REGEX = /^\S+\s+\S+\s+\[(\w+)\]\s+\[(\S+)\]\s+(.*)/;
144
124
  var IOS_LEVEL_MAP = {
@@ -284,8 +264,7 @@ var IOSPlatformAdapter = class {
284
264
  consoleDrainIndex = 0;
285
265
  captureConsole = true;
286
266
  bundleId = null;
287
- // EXEC-03 session-cached app registry (D-10/D-12): enumerated once per session. null = not yet
288
- // enumerated OR enumeration unavailable (iOS simulator — mobile: listApps throws). Reset in cleanup().
267
+ // `null` means not yet enumerated, or unavailable because simulators throw on `listApps`.
289
268
  appRegistry = null;
290
269
  sessionSnapshotDepth = IOS_SNAPSHOT_MAX_DEPTH;
291
270
  snapshotDepth = IOS_SNAPSHOT_MAX_DEPTH;
@@ -295,11 +274,11 @@ var IOSPlatformAdapter = class {
295
274
  this.config = config;
296
275
  this.captureConsole = config.logCapture?.console !== false;
297
276
  this.bundleId = this.config.bundleId ?? config.device?.match?.bundleId ?? null;
298
- this.sessionSnapshotDepth = resolveIOSSnapshotDepth(config.farmSession?.capabilities);
277
+ this.sessionSnapshotDepth = resolveIOSSnapshotDepth();
299
278
  this.snapshotDepth = this.sessionSnapshotDepth;
300
279
  this.readsAtLoweredDepth = 0;
301
280
  this.snapshotDepthWarned = false;
302
- this.driver = await createIOSSession(this.config, config.farmSession);
281
+ this.driver = await createIOSSession(this.config);
303
282
  const bundleId = this.bundleId;
304
283
  if (bundleId) {
305
284
  try {
@@ -342,18 +321,12 @@ var IOSPlatformAdapter = class {
342
321
  this.readsAtLoweredDepth = 0;
343
322
  this.snapshotDepthWarned = false;
344
323
  }
345
- /**
346
- * 428 D-11: the labelled 1s snapshot preview, over `captureScreenshot()` — `takeScreenshot()`
347
- * and nothing else. Never `screenshot()`: that one switches the Appium context, and a tick
348
- * landing between a step's context switch and its command would run the command in the wrong
349
- * context. Declared before the driver exists (a capture then answers `undefined`), so the
350
- * composition's selection never falls through to `screenshot()`.
351
- */
324
+ // `takeScreenshot()` only: `screenshot()` switches the Appium context mid-step.
352
325
  get liveStream() {
353
326
  this.liveStreamCapability ??= (0, import_agent_qa_runner.createSnapshotLiveStream)({ capture: () => this.captureScreenshot() });
354
327
  return this.liveStreamCapability;
355
328
  }
356
- async observe(_options) {
329
+ async observe() {
357
330
  if (!this.driver) {
358
331
  throw new Error("IOSPlatformAdapter not initialized \u2014 call setup() first");
359
332
  }
@@ -517,9 +490,6 @@ var IOSPlatformAdapter = class {
517
490
  return void 0;
518
491
  }
519
492
  }
520
- // ---------------------------------------------------------------------------
521
- // Compound behaviors (EXEC-01/02/03)
522
- // ---------------------------------------------------------------------------
523
493
  async readIOSPageSource() {
524
494
  const driver = this.driver;
525
495
  if (this.snapshotDepth < this.sessionSnapshotDepth && this.readsAtLoweredDepth >= IOS_SNAPSHOT_PROBE_INTERVAL) {
@@ -547,8 +517,9 @@ var IOSPlatformAdapter = class {
547
517
  if (!this.snapshotDepthWarned) {
548
518
  this.snapshotDepthWarned = true;
549
519
  const detail = (readErr instanceof Error ? readErr.message : String(readErr)).split("\n")[0].slice(0, 160);
550
- console.warn(
551
- `[ios-adapter] page source failed at snapshotMaxDepth ${failedDepth}; later reads stay at ${IOS_SNAPSHOT_FALLBACK_DEPTH} and retry ${this.sessionSnapshotDepth} every ${IOS_SNAPSHOT_PROBE_INTERVAL} reads (${detail})`
520
+ (0, import_agent_qa_core.printRunText)(
521
+ `[ios-adapter] page source failed at snapshotMaxDepth ${failedDepth}; later reads stay at ${IOS_SNAPSHOT_FALLBACK_DEPTH} and retry ${this.sessionSnapshotDepth} every ${IOS_SNAPSHOT_PROBE_INTERVAL} reads (${detail})`,
522
+ (text) => console.warn(text)
552
523
  );
553
524
  }
554
525
  }
@@ -560,9 +531,7 @@ var IOSPlatformAdapter = class {
560
531
  return xml;
561
532
  }
562
533
  async tapAt(point) {
563
- await safePerform(
564
- this.driver.action("pointer", { parameters: { pointerType: "touch" } }).move({ x: Math.round(point.x), y: Math.round(point.y) }).down({ button: 0 }).pause(IOS_TAP_HOLD_MS).up({ button: 0 })
565
- );
534
+ await this.driver.action("pointer", { parameters: { pointerType: "touch" } }).move({ x: Math.round(point.x), y: Math.round(point.y) }).down({ button: 0 }).pause(IOS_TAP_HOLD_MS).up({ button: 0 }).perform();
566
535
  }
567
536
  async readRect(elementId) {
568
537
  try {
@@ -694,27 +663,7 @@ var IOSPlatformAdapter = class {
694
663
  return void 0;
695
664
  }
696
665
  }
697
- /**
698
- * EXEC-01 iOS fill compound (D-05..D-08). From the SAME focused-element handle: (append) capture
699
- * the prior value FIRST via getAttribute('value') then addValue, or (replace) setValue; RE-READ
700
- * via getAttribute('value') on that same handle; verdict via compareFillReread. Secure fields
701
- * (nativeType XCUIElementTypeSecureTextField) take the non-empty-only lane — the comparator yields
702
- * length sentinels so no raw value enters the result (T-359-02); bullet masking is tolerated
703
- * (A3/Pitfall 7 — never a length compare). On a passing re-read AND submit, a return/newline is
704
- * delivered AFTER the re-read (D-07). Single typing attempt; a mismatch is success:false + §7.1.
705
- *
706
- * Append (437 FA-1, owner 2026-10-08): the XCUITest `value` attribute often lags after addValue,
707
- * so a re-read equal to the prior cannot show the appended text. That fill is recorded unverified:
708
- * never exact, and never a mismatch. Every fill records the shared verdict as `fillVerdict`.
709
- * An empty field reads as its placeholder, so an append prior equal to it counts as empty.
710
- *
711
- * Return in a multi-line field adds a new line instead of submitting, so a multi-line submit is
712
- * never sent and is recorded as such.
713
- *
714
- * An unreadable re-read is unverified and does not stop a requested Return, as on Android (D-09,
715
- * D-10; owner 2026-10-09), but focus is confirmed once first and a Return is held as focus-lost
716
- * when the field no longer has it (444.1 D-10).
717
- */
666
+ // XCUITest `value` lags after `addValue`, so an append read back as the prior is unverified.
718
667
  async fillIOSCompound(el, action, box, secure, multiline, target) {
719
668
  const driver = this.driver;
720
669
  const isAppend = action.clearMode === "append";
@@ -752,13 +701,7 @@ var IOSPlatformAdapter = class {
752
701
  };
753
702
  return verdict.ok ? { success: true, ...box, ...notice, metadata } : { success: false, error: verdict.selfCorrectingError, ...box, ...notice, metadata };
754
703
  }
755
- /**
756
- * EXEC-02 platform primitives injected into the shared runner scroll loops (D-02/D-03). The swipe
757
- * is a page-sized directional gesture (inverted to the finger direction) that reports unknown
758
- * movement; nativeSearchScroll drives
759
- * the XCUITest `mobile: scroll` predicateString rung over an injection-escaped target; readInterimTree
760
- * is a light page-source parse — NO refMap mint, NO screenshot, lastRefs untouched.
761
- */
704
+ // iOS swipes report unknown movement; `readInterimTree` mints no refs and takes no screenshot.
762
705
  async buildIOSScrollPrimitives(scrollDirection, maxSwipes, factor = 1) {
763
706
  const driver = this.driver;
764
707
  const viewport = await driver.getWindowSize();
@@ -774,9 +717,7 @@ var IOSPlatformAdapter = class {
774
717
  sy = Math.round(center.y);
775
718
  }
776
719
  const pts = (0, import_agent_qa_core.computeSwipe)(sx, sy, fingerDirectionFor(direction), distance);
777
- await safePerform(
778
- driver.action("pointer", { parameters: { pointerType: "touch" } }).move({ x: pts.startX, y: pts.startY }).down({ button: 0 }).move({ x: pts.endX, y: pts.endY, duration: 300 }).up({ button: 0 })
779
- );
720
+ await driver.action("pointer", { parameters: { pointerType: "touch" } }).move({ x: pts.startX, y: pts.startY }).down({ button: 0 }).move({ x: pts.endX, y: pts.endY, duration: 300 }).up({ button: 0 }).perform();
780
721
  return "unknown";
781
722
  },
782
723
  nativeSearchScroll: async (target) => {
@@ -795,14 +736,7 @@ var IOSPlatformAdapter = class {
795
736
  }
796
737
  };
797
738
  }
798
- /**
799
- * EXEC-03 openApp (D-10/D-12, Pitfall 5). Resolve a human name to a bundle id (or validate an
800
- * explicit bundleId) over the session-cached registry, then launch via activateApp. On real devices
801
- * `mobile: listApps` enumerates the registry; on simulators it throws, leaving the registry
802
- * unavailable — the appName lane then mints a structured §7.1 error directing to an explicit
803
- * bundleId, and the bundleId lane validates without the registry (isAppInstalled / activateApp
804
- * error conversion, A2). Never a best-guess launch (T-359-04); an invalid bundle never succeeds.
805
- */
739
+ // Simulators throw on `listApps`, so `appName` needs a real device; `bundleId` works without it.
806
740
  async openApp(action) {
807
741
  const driver = this.driver;
808
742
  const registry = await this.ensureAppRegistry();
@@ -841,12 +775,7 @@ var IOSPlatformAdapter = class {
841
775
  }
842
776
  }
843
777
  }
844
- /**
845
- * Explicit `{bundleId}` launch (D-12, A2). With a registry, validate membership; without one
846
- * (simulator) validate via `driver.isAppInstalled` when the driver exposes it, otherwise attempt
847
- * `activateApp` and convert a driver rejection into the structured §7.1 error. An invalid bundle
848
- * NEVER returns success:true.
849
- */
778
+ // Without a registry, validate via `isAppInstalled` or convert an `activateApp` rejection.
850
779
  async openByBundleId(bundleId, registry) {
851
780
  const driver = this.driver;
852
781
  if (registry !== null) {
@@ -893,12 +822,7 @@ var IOSPlatformAdapter = class {
893
822
  if (enumerated !== null) this.appRegistry = enumerated;
894
823
  return enumerated;
895
824
  }
896
- /**
897
- * Enumerate installed apps through the driver only (ownership contract — webdriverio only, no
898
- * child_process/simctl). `mobile: listApps` maps bundle ids → AppRegistryEntry with CFBundleName
899
- * labels when present (D-10 — no per-app round-trips). It throws on simulators (Pitfall 5) ⇒ null,
900
- * leaving enumeration unavailable WITHOUT crashing.
901
- */
825
+ // Driver only, no simctl; `listApps` throws on simulators, which leaves enumeration unavailable.
902
826
  async enumerateApps() {
903
827
  const driver = this.driver;
904
828
  try {
@@ -1075,9 +999,7 @@ var IOSPlatformAdapter = class {
1075
999
  (0, import_agent_qa_core.warnIfOutOfBounds)({ x: pts.startX, y: pts.startY }, { width, height }, "swipe");
1076
1000
  (0, import_agent_qa_core.warnIfOutOfBounds)({ x: pts.endX, y: pts.endY }, { width, height }, "swipe");
1077
1001
  const swipeDuration = action.durationMs;
1078
- await safePerform(
1079
- driver.action("pointer", { parameters: { pointerType: "touch" } }).move({ x: pts.startX, y: pts.startY }).down({ button: 0 }).move({ x: pts.endX, y: pts.endY, duration: swipeDuration }).up({ button: 0 })
1080
- );
1002
+ await driver.action("pointer", { parameters: { pointerType: "touch" } }).move({ x: pts.startX, y: pts.startY }).down({ button: 0 }).move({ x: pts.endX, y: pts.endY, duration: swipeDuration }).up({ button: 0 }).perform();
1081
1003
  return {
1082
1004
  success: true,
1083
1005
  startCoordinates: { x: pts.startX, y: pts.startY },
@@ -1128,9 +1050,7 @@ var IOSPlatformAdapter = class {
1128
1050
  }
1129
1051
  case "longpress": {
1130
1052
  const { center } = resolver.resolve(action.ref);
1131
- await safePerform(
1132
- driver.action("pointer", { parameters: { pointerType: "touch" } }).move({ x: Math.round(center.x), y: Math.round(center.y) }).down({ button: 0 }).pause(action.duration ?? 1e3).up({ button: 0 })
1133
- );
1053
+ await driver.action("pointer", { parameters: { pointerType: "touch" } }).move({ x: Math.round(center.x), y: Math.round(center.y) }).down({ button: 0 }).pause(action.duration ?? 1e3).up({ button: 0 }).perform();
1134
1054
  return { success: true };
1135
1055
  }
1136
1056
  case "waitFor": {
@@ -1318,25 +1238,19 @@ var IOSPlatformAdapter = class {
1318
1238
  success: false,
1319
1239
  error: 'assertValue {source:"network"} (HTTP log inspection) is not available on the iOS adapter. Use a web target for network assertions, or assert on visible UI text.'
1320
1240
  };
1321
- case "expr-result":
1322
- return { success: true, data: void 0 };
1323
1241
  }
1324
1242
  }
1325
1243
  case "drag": {
1326
1244
  const from = resolver.resolve(action.fromRef);
1327
1245
  const to = resolver.resolve(action.toRef);
1328
- await safePerform(
1329
- driver.action("pointer", { parameters: { pointerType: "touch" } }).move({ x: Math.round(from.center.x), y: Math.round(from.center.y) }).down({ button: 0 }).pause(300).move({ x: Math.round(to.center.x), y: Math.round(to.center.y), duration: 500 }).up({ button: 0 })
1330
- );
1246
+ await driver.action("pointer", { parameters: { pointerType: "touch" } }).move({ x: Math.round(from.center.x), y: Math.round(from.center.y) }).down({ button: 0 }).pause(300).move({ x: Math.round(to.center.x), y: Math.round(to.center.y), duration: 500 }).up({ button: 0 }).perform();
1331
1247
  return { success: true };
1332
1248
  }
1333
1249
  case "doubleTap": {
1334
1250
  const { center, bounds } = resolver.resolve(action.ref);
1335
1251
  const x = Math.round(center.x);
1336
1252
  const y = Math.round(center.y);
1337
- await safePerform(
1338
- driver.action("pointer", { parameters: { pointerType: "touch" } }).move({ x, y }).down({ button: 0 }).up({ button: 0 }).pause(50).down({ button: 0 }).up({ button: 0 })
1339
- );
1253
+ await driver.action("pointer", { parameters: { pointerType: "touch" } }).move({ x, y }).down({ button: 0 }).up({ button: 0 }).pause(50).down({ button: 0 }).up({ button: 0 }).perform();
1340
1254
  return { success: true, coordinates: center, boundingBox: bounds };
1341
1255
  }
1342
1256
  case "openApp":
@@ -1429,16 +1343,10 @@ var IOSPlatformAdapter = class {
1429
1343
  end = { x: midX, y: Math.round(height * 0.4) };
1430
1344
  break;
1431
1345
  }
1432
- await safePerform(
1433
- driver.action("pointer", { parameters: { pointerType: "touch" } }).move({ x: start.x, y: start.y }).down({ button: 0 }).move({ x: end.x, y: end.y, duration: 300 }).up({ button: 0 })
1434
- );
1346
+ await driver.action("pointer", { parameters: { pointerType: "touch" } }).move({ x: start.x, y: start.y }).down({ button: 0 }).move({ x: end.x, y: end.y, duration: 300 }).up({ button: 0 }).perform();
1435
1347
  return { success: true, startCoordinates: start, endCoordinates: end };
1436
1348
  }
1437
- // Runner-minted / author-channel verbs (D-01/D-15): reportValue, assertAI, extractAI, and
1438
- // setVariable are intercepted at the runner dispatch seam and never reach the adapter. A
1439
- // grouped §7.1 defensive case surfaces a dispatch-routing error if one arrives here;
1440
- // assertNever remains the backstop for a genuinely unknown schema verb. (uniqueValue is
1441
- // web-only and is not part of ActionsFor<'ios'>.)
1349
+ // Runner-channel verbs never reach the adapter; one arriving here is a routing error.
1442
1350
  case "reportValue":
1443
1351
  case "assertAI":
1444
1352
  case "extractAI":