@vostride/agent-qa-android 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
@@ -45,7 +45,7 @@ var import_agent_qa_core2 = require("@vostride/agent-qa-core");
45
45
  // src/session.ts
46
46
  var import_webdriverio = require("webdriverio");
47
47
  var import_agent_qa_core = require("@vostride/agent-qa-core");
48
- async function createAndroidSession(config, farmSession) {
48
+ async function createAndroidSession(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);
@@ -88,17 +88,6 @@ async function createAndroidSession(config, farmSession) {
88
88
  delete capabilities[key];
89
89
  }
90
90
  }
91
- if (farmSession) {
92
- Object.assign(capabilities, farmSession.capabilities);
93
- return (0, import_webdriverio.remote)({
94
- protocol: "https",
95
- hostname: farmSession.hostname,
96
- port: farmSession.port,
97
- path: farmSession.path,
98
- logLevel: config.verbose ? "info" : "silent",
99
- capabilities
100
- });
101
- }
102
91
  return (0, import_webdriverio.remote)({
103
92
  hostname: url.hostname,
104
93
  port: parseInt(url.port || "4723", 10),
@@ -278,8 +267,7 @@ return (function() {
278
267
  return node ? (node.textContent || '').trim() : '';
279
268
  }
280
269
 
281
- // 437 D-11 (CR-02): a field is named by its label, never by its value, so typed text never
282
- // becomes a name. A wrapping label's text drops the field's own text (a select's options).
270
+ // A field is named by its label, not its value; a wrapping label drops the field's own text.
283
271
  function labelText(el, label) {
284
272
  var text = label.textContent || '';
285
273
  var own = el.textContent || '';
@@ -303,8 +291,7 @@ return (function() {
303
291
  return '';
304
292
  }
305
293
 
306
- // parentIndex is the nearest INCLUDED ancestor (structural/skipped nodes collapse
307
- // away, matching the mobile/web walkers' Pitfall #7 handling).
294
+ // The nearest included ancestor: structural and skipped nodes collapse, as on mobile and web.
308
295
  function nearestAncestorIndex(el) {
309
296
  var p = el.parentElement;
310
297
  while (p) {
@@ -324,8 +311,7 @@ return (function() {
324
311
  if (style.display === 'none' || style.visibility === 'hidden') continue;
325
312
 
326
313
  var tag = el.tagName;
327
- // 2026-10-19 (444.1-05, D-09): only the outermost content-editable element is an editor;
328
- // its descendants are ordinary nodes.
314
+ // Only the outermost content-editable element is an editor; its descendants are ordinary nodes.
329
315
  var host = el.isContentEditable === true
330
316
  && !(el.parentElement && el.parentElement.isContentEditable === true);
331
317
  var ariaRole = el.getAttribute('role');
@@ -363,7 +349,7 @@ return (function() {
363
349
  height: Math.round(rect.height)
364
350
  };
365
351
 
366
- // Real DOM state signals only \u2014 the builder derives every default (D-08).
352
+ // Real DOM state signals only; the builder derives every default.
367
353
  var state = {};
368
354
  var disabled = el.disabled === true
369
355
  || el.hasAttribute('disabled')
@@ -389,7 +375,7 @@ return (function() {
389
375
 
390
376
  var parentIndex = nearestAncestorIndex(el);
391
377
 
392
- // 2026-10-19 (444.1-05 D-08, 444.1-22 D-33): what a field or an editor holds is its value, never its name; a password's never.
378
+ // What a field or an editor holds is its value, never its name; a password has no value here.
393
379
  var observedValue = undefined;
394
380
  var textEntry = tag === 'TEXTAREA' || (tag === 'INPUT' && !VALUELESS_TYPES[String(el.type).toLowerCase()]);
395
381
  if (textEntry && el.value) {
@@ -548,14 +534,13 @@ var AndroidPlatformAdapter = class {
548
534
  captureConsole = true;
549
535
  appPackage = null;
550
536
  appPid = null;
551
- // EXEC-03 session-cached app registry (D-10/D-12): enumerated once per session; null = not yet
552
- // enumerated (or enumeration failed and may be retried). Reset in cleanup().
537
+ // Enumerated once per session; `null` is not yet enumerated or retryable; reset in `cleanup()`.
553
538
  appRegistry = null;
554
539
  async setup(config) {
555
540
  this.config = config;
556
541
  this.captureConsole = config.logCapture?.console !== false;
557
542
  this.appPackage = this.config.appPackage ?? config.device?.match?.appPackage ?? null;
558
- this.driver = await createAndroidSession(this.config, config.farmSession);
543
+ this.driver = await createAndroidSession(this.config);
559
544
  const appPackage = this.appPackage;
560
545
  if (appPackage && !this.config.browserName && !this.config.device?.match?.browserName) {
561
546
  let state;
@@ -624,7 +609,7 @@ var AndroidPlatformAdapter = class {
624
609
  // Once true, we know a WebView/browser exists and should retry rather than
625
610
  // falling back to native (which returns an opaque WebView and can crash UiAutomator2).
626
611
  hasSeenWebContext = false;
627
- async observe(_options) {
612
+ async observe() {
628
613
  if (!this.driver) {
629
614
  throw new Error("AndroidPlatformAdapter not initialized \u2014 call setup() first");
630
615
  }
@@ -699,7 +684,7 @@ var AndroidPlatformAdapter = class {
699
684
  const dims = await getImageDimensions(buf);
700
685
  const { width } = await this.driver.getWindowSize();
701
686
  if (dims && Math.abs(dims.width - width) > 1) {
702
- console.warn(`[android-adapter] screenshot width ${dims.width} != window width ${width} (coord-space divergence, handled by scaleFactor per D-19/D-20)`);
687
+ console.warn(`[android-adapter] screenshot width ${dims.width} != window width ${width} (coord-space divergence, handled by scaleFactor)`);
703
688
  }
704
689
  } catch {
705
690
  }
@@ -731,13 +716,7 @@ var AndroidPlatformAdapter = class {
731
716
  if (!level) return newLogs;
732
717
  return newLogs.filter((e) => e.level === level);
733
718
  }
734
- /**
735
- * 428 D-11: the labelled 1s snapshot preview, over `captureScreenshot()` — `takeScreenshot()`
736
- * and nothing else. Never `screenshot()`: that one switches the Appium context, and a tick
737
- * landing between a step's context switch and its command would run the command in the wrong
738
- * context. Declared before the driver exists (a capture then answers `undefined`), so the
739
- * composition's selection never falls through to `screenshot()`.
740
- */
719
+ // `takeScreenshot()` only: `screenshot()` switches the Appium context mid-step.
741
720
  get liveStream() {
742
721
  this.liveStreamCapability ??= (0, import_agent_qa_runner.createSnapshotLiveStream)({ capture: () => this.captureScreenshot() });
743
722
  return this.liveStreamCapability;
@@ -913,9 +892,6 @@ var AndroidPlatformAdapter = class {
913
892
  return void 0;
914
893
  }
915
894
  }
916
- // ---------------------------------------------------------------------------
917
- // Compound behaviors (EXEC-01/02/03)
918
- // ---------------------------------------------------------------------------
919
895
  /** Read the live text of a focused element handle; undefined when nothing is readable. */
920
896
  async readElementText(el) {
921
897
  try {
@@ -932,15 +908,12 @@ var AndroidPlatformAdapter = class {
932
908
  return false;
933
909
  }
934
910
  }
935
- /** An empty field's getText() returns its hint (437 Pitfall 4), so a showing hint reads ''. */
911
+ /** An empty field's getText() returns its hint, so a showing hint reads ''. */
936
912
  async readFocusedValue(el) {
937
913
  if (await this.focusedMatches(FOCUSED_HINT_XPATH)) return "";
938
914
  return await this.readElementText(el);
939
915
  }
940
- /**
941
- * The `mobile: shell` route of keyevent and openQuickSettings (437 D-18). A refusal by secure
942
- * Appium is recorded as unavailable and any other driver error as failed; neither is a pass.
943
- */
916
+ // A secure-Appium refusal is recorded as unavailable and any other driver error as failed.
944
917
  async runShell(verb, args) {
945
918
  try {
946
919
  await this.driver.execute("mobile: shell", args);
@@ -963,7 +936,7 @@ var AndroidPlatformAdapter = class {
963
936
  return { success: false, error: `${verb} failed: ${detail}`, metadata: { shellAction } };
964
937
  }
965
938
  }
966
- /** The one submit mechanism on every Android path (437 D-13); pressed once, never retried. */
939
+ /** The one submit mechanism on every Android path; pressed once, never retried. */
967
940
  async pressEnter() {
968
941
  try {
969
942
  await this.driver.execute("mobile: pressKey", { keycode: ENTER_KEYCODE });
@@ -972,7 +945,7 @@ var AndroidPlatformAdapter = class {
972
945
  return { sent: false, outcome: "failed", detail: firstErrorLine(err) };
973
946
  }
974
947
  }
975
- /** 437 FA-2 (owner, 2026-10-08): line breaks into a single-line field are refused before typing. */
948
+ // Line breaks into a single-line field are refused before typing.
976
949
  refuseLineBreaks(action, box) {
977
950
  const fillRefusal = {
978
951
  verb: "fill",
@@ -986,14 +959,7 @@ var AndroidPlatformAdapter = class {
986
959
  metadata: { fillRefusal, ...action.submit ? { fillSubmit: { ...INPUT_FAILED } } : {} }
987
960
  };
988
961
  }
989
- /**
990
- * Native fill from the focused handle (437 D-08..D-10, D-12, D-13, D-15, D-17). Delivers once
991
- * (addValue for append, setValue for replace), reads back from the same handle and records the
992
- * shared verdict. An unknown append prior or an unreadable read-back is unverified, which never
993
- * stops a requested Enter; a mismatch or a multi-line field holds it. A driver error during
994
- * delivery ends the fill: nothing is typed again and no Enter follows. A secure field yields
995
- * length sentinels only.
996
- */
962
+ // Delivers once and reads back; an unverified result still presses Enter, a mismatch holds it.
997
963
  async fillNativeCompound(el, action, box) {
998
964
  const isAppend = action.clearMode === "append";
999
965
  if (LINE_BREAK_RE.test(action.text) && await this.focusedMatches(FOCUSED_SINGLE_LINE_XPATH)) {
@@ -1029,11 +995,7 @@ var AndroidPlatformAdapter = class {
1029
995
  return void 0;
1030
996
  }
1031
997
  }
1032
- /**
1033
- * 437 D-11 (WR-02): where the focused node is relative to the ref, judged by its read bounds.
1034
- * An unreadable or unparseable value is no evidence, so the native path stays (D-04, D-15).
1035
- * 444.1 D-07: 'elsewhere' needs positive evidence, the bounds of another observed field.
1036
- */
998
+ // `elsewhere` needs positive evidence: the read bounds of another observed field.
1037
999
  async focusIsElsewhere(handle, ref, bounds) {
1038
1000
  let raw;
1039
1001
  try {
@@ -1050,18 +1012,12 @@ var AndroidPlatformAdapter = class {
1050
1012
  }
1051
1013
  return "moved";
1052
1014
  }
1053
- /** Ctrl+A then DEL as key input; clears only the view holding input focus (437 D-14). */
1015
+ /** Ctrl+A then DEL as key input; clears only the view holding input focus. */
1054
1016
  async clearByKeys() {
1055
1017
  await this.driver.execute("mobile: pressKey", CTRL_A);
1056
1018
  await this.driver.execute("mobile: pressKey", { keycode: DEL_KEYCODE });
1057
1019
  }
1058
- /**
1059
- * The last-resort keyboard path (437 D-14): the tap left no focused handle. Replace clears by
1060
- * keys, the text goes through one `mobile: type` (Appium's Unicode IME, exact for every D-08
1061
- * text), and the value is read back only when a focused handle can be found afterwards. An
1062
- * append's prior value is unknown, so it is never better than unverified. The field's kind comes
1063
- * from the observed input-type (D-15): only positive evidence refuses line breaks or holds Enter.
1064
- */
1020
+ // Last resort when the tap left no handle; an append's prior is unknown, so at best unverified.
1065
1021
  async fillKeyboardPath(action, box, refData) {
1066
1022
  const isAppend = action.clearMode === "append";
1067
1023
  const kind = fieldKindOf(refData);
@@ -1089,11 +1045,7 @@ var AndroidPlatformAdapter = class {
1089
1045
  enter: () => this.pressEnter()
1090
1046
  });
1091
1047
  }
1092
- /**
1093
- * Native and keyboard clearText (437 D-16): clearValue on the focused handle, or Ctrl+A then DEL
1094
- * when the tap left none, then the hint-aware read-back judged against the empty string. The
1095
- * caller has already checked that a focused handle is the referenced field (WR-02).
1096
- */
1048
+ // The caller has already checked that a focused handle is the referenced field.
1097
1049
  async clearNativeOrKeyboard(box, handle) {
1098
1050
  const path = handle ? "native" : "keyboard";
1099
1051
  const before = handle ? await this.isSecure(handle) : false;
@@ -1153,11 +1105,7 @@ var AndroidPlatformAdapter = class {
1153
1105
  metadata: { fillVerdict, ...action.submit ? { fillSubmit: { ...INPUT_FAILED } } : {} }
1154
1106
  };
1155
1107
  }
1156
- /**
1157
- * The shared end of every Android fill (437 D-09, D-10, D-13, D-15, D-17): records the verdict
1158
- * and, when a submit was requested, holds Enter on a mismatch or a multi-line field and otherwise
1159
- * presses it once through `enter`.
1160
- */
1108
+ // Holds Enter on a mismatch or a multi-line field; otherwise presses it once.
1161
1109
  async settleFill(action, box, settle) {
1162
1110
  const { path, verdict, multiline, enter } = settle;
1163
1111
  const fillVerdict = {
@@ -1193,7 +1141,7 @@ var AndroidPlatformAdapter = class {
1193
1141
  }
1194
1142
  return { success: true, ...box, ...notice, metadata };
1195
1143
  }
1196
- /** A real W3C touch tap in the WebView context at the ref's CSS-pixel centre (437 D-11). */
1144
+ /** A real W3C touch tap in the WebView context at the ref's CSS-pixel centre. */
1197
1145
  async trustedWebTap(center) {
1198
1146
  await safePerform(
1199
1147
  this.driver.action("pointer", { parameters: { pointerType: "touch" } }).move({ x: Math.round(center.x), y: Math.round(center.y), origin: "viewport" }).down({ button: 0 }).up({ button: 0 })
@@ -1246,7 +1194,7 @@ var AndroidPlatformAdapter = class {
1246
1194
  if (this.activeWebContext) await this.ensureContext(this.activeWebContext);
1247
1195
  }
1248
1196
  }
1249
- /** 437 D-11: a tap that left no editable field, or another field, focused writes nothing. */
1197
+ // A tap that leaves no editable field, or another field, focused writes nothing.
1250
1198
  refuseFocus(verb, ref, focus, submit, box) {
1251
1199
  const nothing = verb === "fill" ? "nothing was typed" : "nothing was cleared";
1252
1200
  let fillRefusal;
@@ -1268,10 +1216,7 @@ var AndroidPlatformAdapter = class {
1268
1216
  metadata: { fillRefusal, ...submit ? { fillSubmit: { ...INPUT_FAILED } } : {} }
1269
1217
  };
1270
1218
  }
1271
- /**
1272
- * WebView clearText (437 D-11, D-16): the same tap and focus probe as the fill, W3C Ctrl+A and
1273
- * Backspace, then a read-back judged against the empty string. Never success without that check.
1274
- */
1219
+ // Never success without the read-back judged against the empty string.
1275
1220
  async clearWebViewField(ref, center, box) {
1276
1221
  const driver = this.driver;
1277
1222
  await this.trustedWebTap(center);
@@ -1287,11 +1232,7 @@ var AndroidPlatformAdapter = class {
1287
1232
  }
1288
1233
  return this.settleClear("webview", await this.readWebFieldValue(), focus.secure, box);
1289
1234
  }
1290
- /**
1291
- * WebView fill as trusted input (437 D-11): a W3C tap on the ref, a read-only focus probe,
1292
- * W3C Ctrl+A and Backspace for replace, `mobile: type` from NATIVE_APP, a read-only read-back,
1293
- * and one `mobile: pressKey 66` from NATIVE_APP. No script writes the field or fires an event.
1294
- */
1235
+ // Trusted input only: no page script writes the field or fires an event.
1295
1236
  async fillWebViewCompound(action, box, center) {
1296
1237
  const driver = this.driver;
1297
1238
  const isAppend = action.clearMode === "append";
@@ -1328,15 +1269,7 @@ var AndroidPlatformAdapter = class {
1328
1269
  enter: () => this.withNativeContext(() => this.pressEnter())
1329
1270
  });
1330
1271
  }
1331
- /**
1332
- * EXEC-02 platform primitives injected into the shared runner scroll loops (D-02/D-03; 436-02
1333
- * D-25). A native swipe is `mobile: scrollGesture` over the container (the ref's bounds, else the
1334
- * screen), whose boolean is the driver's own boundary signal; in a WebView the touch swipe is
1335
- * measured over every WebView scroller before and after (436-18), paired by identity (436-23).
1336
- * nativeSearchScroll drives the UiAutomator
1337
- * `UiScrollable.scrollIntoView` rung over an injection-escaped selector; readInterimTree is a
1338
- * light page-source parse — NO refMap mint, NO screenshot, lastRefs untouched.
1339
- */
1272
+ // Native swipes trust `scrollGesture`'s boolean; WebView swipes compare every scroller by id.
1340
1273
  async buildAndroidScrollPrimitives(scrollDirection, maxSwipes, factor = 1) {
1341
1274
  const driver = this.driver;
1342
1275
  const viewport = await driver.getWindowSize();
@@ -1395,13 +1328,7 @@ var AndroidPlatformAdapter = class {
1395
1328
  }
1396
1329
  };
1397
1330
  }
1398
- /**
1399
- * The WebView scrollers a touch swipe may have acted on (436-18 · D-25), read by the shared
1400
- * WEBVIEW_SCROLLERS_SCRIPT. The touch point cannot be mapped into the WebView's CSS coordinates,
1401
- * so the swipe is judged over all of them.
1402
- * 2026-10-08 (436-23): a frame in view reads unknown; members carry an identity and are paired
1403
- * by it; open shadow roots are walked; only on-screen element scrollers are members.
1404
- */
1331
+ // The touch point cannot map into CSS coordinates, so the swipe is judged over every scroller.
1405
1332
  async readWebViewScrollers() {
1406
1333
  try {
1407
1334
  return (0, import_agent_qa_core2.parseWebViewScrollers)(await this.driver.execute(import_agent_qa_core2.WEBVIEW_SCROLLERS_SCRIPT));
@@ -1409,12 +1336,7 @@ var AndroidPlatformAdapter = class {
1409
1336
  return void 0;
1410
1337
  }
1411
1338
  }
1412
- /**
1413
- * EXEC-03 openApp (D-10/D-11/D-12). Resolve a human name to a package id (or validate an
1414
- * explicit bundleId) over the session-cached registry, then launch via activateApp. On a
1415
- * resolution MISS the registry is re-enumerated exactly once (catches mid-run installs) before
1416
- * erroring with the resolver's candidate ids. Never a best-guess launch (T-359-04).
1417
- */
1339
+ // On a resolution miss the registry is re-enumerated once before erroring; never a best guess.
1418
1340
  async openApp(action) {
1419
1341
  const driver = this.driver;
1420
1342
  let registry = await this.ensureAppRegistry();
@@ -1466,12 +1388,7 @@ var AndroidPlatformAdapter = class {
1466
1388
  if (enumerated !== null) this.appRegistry = enumerated;
1467
1389
  return enumerated;
1468
1390
  }
1469
- /**
1470
- * Enumerate installed apps through the driver only (ownership contract — no adb/child_process).
1471
- * `mobile: listApps` maps package ids → AppRegistryEntry (labels only when the same call carries
1472
- * them, D-10 — no per-package round-trips). On failure, fall back to `mobile: shell` pm-list ONLY
1473
- * when the AGENT_QA_ANDROID_USE_MOBILE_SHELL gate is '1' (Pitfall 4). Null = enumeration unavailable.
1474
- */
1391
+ // Driver only, no adb; the `mobile: shell` fallback needs `AGENT_QA_ANDROID_USE_MOBILE_SHELL=1`.
1475
1392
  async enumerateApps() {
1476
1393
  const driver = this.driver;
1477
1394
  try {
@@ -1907,8 +1824,6 @@ var AndroidPlatformAdapter = class {
1907
1824
  success: false,
1908
1825
  error: 'assertValue {source:"network"} (HTTP log inspection) is not available on the Android adapter. Use a web target for network assertions, or assert on visible UI text.'
1909
1826
  };
1910
- case "expr-result":
1911
- return { success: true, data: void 0 };
1912
1827
  }
1913
1828
  }
1914
1829
  case "drag": {
@@ -1962,11 +1877,7 @@ var AndroidPlatformAdapter = class {
1962
1877
  }
1963
1878
  return { success: true, coordinates: { x, y } };
1964
1879
  }
1965
- // Runner-minted / author-channel verbs (D-01/D-15): reportValue, assertAI, extractAI, and
1966
- // setVariable are intercepted at the runner dispatch seam and never reach the adapter. A
1967
- // grouped §7.1 defensive case surfaces a dispatch-routing error if one arrives here;
1968
- // assertNever remains the backstop for a genuinely unknown schema verb. (uniqueValue is
1969
- // web-only and is not part of ActionsFor<'android'>.)
1880
+ // Runner-channel verbs never reach the adapter; one arriving here is a routing error.
1970
1881
  case "reportValue":
1971
1882
  case "assertAI":
1972
1883
  case "extractAI":