@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.js CHANGED
@@ -31,7 +31,7 @@ import {
31
31
  // src/session.ts
32
32
  import { remote } from "webdriverio";
33
33
  import { MobileSetupError } from "@vostride/agent-qa-core";
34
- async function createAndroidSession(config, farmSession) {
34
+ async function createAndroidSession(config) {
35
35
  const match = config.device?.match ?? {};
36
36
  const appiumUrl = process.env.AGENT_QA_APPIUM_URL ?? config.appiumUrl ?? "http://localhost:4723";
37
37
  const url = new URL(appiumUrl);
@@ -74,17 +74,6 @@ async function createAndroidSession(config, farmSession) {
74
74
  delete capabilities[key];
75
75
  }
76
76
  }
77
- if (farmSession) {
78
- Object.assign(capabilities, farmSession.capabilities);
79
- return remote({
80
- protocol: "https",
81
- hostname: farmSession.hostname,
82
- port: farmSession.port,
83
- path: farmSession.path,
84
- logLevel: config.verbose ? "info" : "silent",
85
- capabilities
86
- });
87
- }
88
77
  return remote({
89
78
  hostname: url.hostname,
90
79
  port: parseInt(url.port || "4723", 10),
@@ -264,8 +253,7 @@ return (function() {
264
253
  return node ? (node.textContent || '').trim() : '';
265
254
  }
266
255
 
267
- // 437 D-11 (CR-02): a field is named by its label, never by its value, so typed text never
268
- // becomes a name. A wrapping label's text drops the field's own text (a select's options).
256
+ // A field is named by its label, not its value; a wrapping label drops the field's own text.
269
257
  function labelText(el, label) {
270
258
  var text = label.textContent || '';
271
259
  var own = el.textContent || '';
@@ -289,8 +277,7 @@ return (function() {
289
277
  return '';
290
278
  }
291
279
 
292
- // parentIndex is the nearest INCLUDED ancestor (structural/skipped nodes collapse
293
- // away, matching the mobile/web walkers' Pitfall #7 handling).
280
+ // The nearest included ancestor: structural and skipped nodes collapse, as on mobile and web.
294
281
  function nearestAncestorIndex(el) {
295
282
  var p = el.parentElement;
296
283
  while (p) {
@@ -310,8 +297,7 @@ return (function() {
310
297
  if (style.display === 'none' || style.visibility === 'hidden') continue;
311
298
 
312
299
  var tag = el.tagName;
313
- // 2026-10-19 (444.1-05, D-09): only the outermost content-editable element is an editor;
314
- // its descendants are ordinary nodes.
300
+ // Only the outermost content-editable element is an editor; its descendants are ordinary nodes.
315
301
  var host = el.isContentEditable === true
316
302
  && !(el.parentElement && el.parentElement.isContentEditable === true);
317
303
  var ariaRole = el.getAttribute('role');
@@ -349,7 +335,7 @@ return (function() {
349
335
  height: Math.round(rect.height)
350
336
  };
351
337
 
352
- // Real DOM state signals only \u2014 the builder derives every default (D-08).
338
+ // Real DOM state signals only; the builder derives every default.
353
339
  var state = {};
354
340
  var disabled = el.disabled === true
355
341
  || el.hasAttribute('disabled')
@@ -375,7 +361,7 @@ return (function() {
375
361
 
376
362
  var parentIndex = nearestAncestorIndex(el);
377
363
 
378
- // 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.
364
+ // What a field or an editor holds is its value, never its name; a password has no value here.
379
365
  var observedValue = undefined;
380
366
  var textEntry = tag === 'TEXTAREA' || (tag === 'INPUT' && !VALUELESS_TYPES[String(el.type).toLowerCase()]);
381
367
  if (textEntry && el.value) {
@@ -534,14 +520,13 @@ var AndroidPlatformAdapter = class {
534
520
  captureConsole = true;
535
521
  appPackage = null;
536
522
  appPid = null;
537
- // EXEC-03 session-cached app registry (D-10/D-12): enumerated once per session; null = not yet
538
- // enumerated (or enumeration failed and may be retried). Reset in cleanup().
523
+ // Enumerated once per session; `null` is not yet enumerated or retryable; reset in `cleanup()`.
539
524
  appRegistry = null;
540
525
  async setup(config) {
541
526
  this.config = config;
542
527
  this.captureConsole = config.logCapture?.console !== false;
543
528
  this.appPackage = this.config.appPackage ?? config.device?.match?.appPackage ?? null;
544
- this.driver = await createAndroidSession(this.config, config.farmSession);
529
+ this.driver = await createAndroidSession(this.config);
545
530
  const appPackage = this.appPackage;
546
531
  if (appPackage && !this.config.browserName && !this.config.device?.match?.browserName) {
547
532
  let state;
@@ -610,7 +595,7 @@ var AndroidPlatformAdapter = class {
610
595
  // Once true, we know a WebView/browser exists and should retry rather than
611
596
  // falling back to native (which returns an opaque WebView and can crash UiAutomator2).
612
597
  hasSeenWebContext = false;
613
- async observe(_options) {
598
+ async observe() {
614
599
  if (!this.driver) {
615
600
  throw new Error("AndroidPlatformAdapter not initialized \u2014 call setup() first");
616
601
  }
@@ -685,7 +670,7 @@ var AndroidPlatformAdapter = class {
685
670
  const dims = await getImageDimensions(buf);
686
671
  const { width } = await this.driver.getWindowSize();
687
672
  if (dims && Math.abs(dims.width - width) > 1) {
688
- console.warn(`[android-adapter] screenshot width ${dims.width} != window width ${width} (coord-space divergence, handled by scaleFactor per D-19/D-20)`);
673
+ console.warn(`[android-adapter] screenshot width ${dims.width} != window width ${width} (coord-space divergence, handled by scaleFactor)`);
689
674
  }
690
675
  } catch {
691
676
  }
@@ -717,13 +702,7 @@ var AndroidPlatformAdapter = class {
717
702
  if (!level) return newLogs;
718
703
  return newLogs.filter((e) => e.level === level);
719
704
  }
720
- /**
721
- * 428 D-11: the labelled 1s snapshot preview, over `captureScreenshot()` — `takeScreenshot()`
722
- * and nothing else. Never `screenshot()`: that one switches the Appium context, and a tick
723
- * landing between a step's context switch and its command would run the command in the wrong
724
- * context. Declared before the driver exists (a capture then answers `undefined`), so the
725
- * composition's selection never falls through to `screenshot()`.
726
- */
705
+ // `takeScreenshot()` only: `screenshot()` switches the Appium context mid-step.
727
706
  get liveStream() {
728
707
  this.liveStreamCapability ??= createSnapshotLiveStream({ capture: () => this.captureScreenshot() });
729
708
  return this.liveStreamCapability;
@@ -899,9 +878,6 @@ var AndroidPlatformAdapter = class {
899
878
  return void 0;
900
879
  }
901
880
  }
902
- // ---------------------------------------------------------------------------
903
- // Compound behaviors (EXEC-01/02/03)
904
- // ---------------------------------------------------------------------------
905
881
  /** Read the live text of a focused element handle; undefined when nothing is readable. */
906
882
  async readElementText(el) {
907
883
  try {
@@ -918,15 +894,12 @@ var AndroidPlatformAdapter = class {
918
894
  return false;
919
895
  }
920
896
  }
921
- /** An empty field's getText() returns its hint (437 Pitfall 4), so a showing hint reads ''. */
897
+ /** An empty field's getText() returns its hint, so a showing hint reads ''. */
922
898
  async readFocusedValue(el) {
923
899
  if (await this.focusedMatches(FOCUSED_HINT_XPATH)) return "";
924
900
  return await this.readElementText(el);
925
901
  }
926
- /**
927
- * The `mobile: shell` route of keyevent and openQuickSettings (437 D-18). A refusal by secure
928
- * Appium is recorded as unavailable and any other driver error as failed; neither is a pass.
929
- */
902
+ // A secure-Appium refusal is recorded as unavailable and any other driver error as failed.
930
903
  async runShell(verb, args) {
931
904
  try {
932
905
  await this.driver.execute("mobile: shell", args);
@@ -949,7 +922,7 @@ var AndroidPlatformAdapter = class {
949
922
  return { success: false, error: `${verb} failed: ${detail}`, metadata: { shellAction } };
950
923
  }
951
924
  }
952
- /** The one submit mechanism on every Android path (437 D-13); pressed once, never retried. */
925
+ /** The one submit mechanism on every Android path; pressed once, never retried. */
953
926
  async pressEnter() {
954
927
  try {
955
928
  await this.driver.execute("mobile: pressKey", { keycode: ENTER_KEYCODE });
@@ -958,7 +931,7 @@ var AndroidPlatformAdapter = class {
958
931
  return { sent: false, outcome: "failed", detail: firstErrorLine(err) };
959
932
  }
960
933
  }
961
- /** 437 FA-2 (owner, 2026-10-08): line breaks into a single-line field are refused before typing. */
934
+ // Line breaks into a single-line field are refused before typing.
962
935
  refuseLineBreaks(action, box) {
963
936
  const fillRefusal = {
964
937
  verb: "fill",
@@ -972,14 +945,7 @@ var AndroidPlatformAdapter = class {
972
945
  metadata: { fillRefusal, ...action.submit ? { fillSubmit: { ...INPUT_FAILED } } : {} }
973
946
  };
974
947
  }
975
- /**
976
- * Native fill from the focused handle (437 D-08..D-10, D-12, D-13, D-15, D-17). Delivers once
977
- * (addValue for append, setValue for replace), reads back from the same handle and records the
978
- * shared verdict. An unknown append prior or an unreadable read-back is unverified, which never
979
- * stops a requested Enter; a mismatch or a multi-line field holds it. A driver error during
980
- * delivery ends the fill: nothing is typed again and no Enter follows. A secure field yields
981
- * length sentinels only.
982
- */
948
+ // Delivers once and reads back; an unverified result still presses Enter, a mismatch holds it.
983
949
  async fillNativeCompound(el, action, box) {
984
950
  const isAppend = action.clearMode === "append";
985
951
  if (LINE_BREAK_RE.test(action.text) && await this.focusedMatches(FOCUSED_SINGLE_LINE_XPATH)) {
@@ -1015,11 +981,7 @@ var AndroidPlatformAdapter = class {
1015
981
  return void 0;
1016
982
  }
1017
983
  }
1018
- /**
1019
- * 437 D-11 (WR-02): where the focused node is relative to the ref, judged by its read bounds.
1020
- * An unreadable or unparseable value is no evidence, so the native path stays (D-04, D-15).
1021
- * 444.1 D-07: 'elsewhere' needs positive evidence, the bounds of another observed field.
1022
- */
984
+ // `elsewhere` needs positive evidence: the read bounds of another observed field.
1023
985
  async focusIsElsewhere(handle, ref, bounds) {
1024
986
  let raw;
1025
987
  try {
@@ -1036,18 +998,12 @@ var AndroidPlatformAdapter = class {
1036
998
  }
1037
999
  return "moved";
1038
1000
  }
1039
- /** Ctrl+A then DEL as key input; clears only the view holding input focus (437 D-14). */
1001
+ /** Ctrl+A then DEL as key input; clears only the view holding input focus. */
1040
1002
  async clearByKeys() {
1041
1003
  await this.driver.execute("mobile: pressKey", CTRL_A);
1042
1004
  await this.driver.execute("mobile: pressKey", { keycode: DEL_KEYCODE });
1043
1005
  }
1044
- /**
1045
- * The last-resort keyboard path (437 D-14): the tap left no focused handle. Replace clears by
1046
- * keys, the text goes through one `mobile: type` (Appium's Unicode IME, exact for every D-08
1047
- * text), and the value is read back only when a focused handle can be found afterwards. An
1048
- * append's prior value is unknown, so it is never better than unverified. The field's kind comes
1049
- * from the observed input-type (D-15): only positive evidence refuses line breaks or holds Enter.
1050
- */
1006
+ // Last resort when the tap left no handle; an append's prior is unknown, so at best unverified.
1051
1007
  async fillKeyboardPath(action, box, refData) {
1052
1008
  const isAppend = action.clearMode === "append";
1053
1009
  const kind = fieldKindOf(refData);
@@ -1075,11 +1031,7 @@ var AndroidPlatformAdapter = class {
1075
1031
  enter: () => this.pressEnter()
1076
1032
  });
1077
1033
  }
1078
- /**
1079
- * Native and keyboard clearText (437 D-16): clearValue on the focused handle, or Ctrl+A then DEL
1080
- * when the tap left none, then the hint-aware read-back judged against the empty string. The
1081
- * caller has already checked that a focused handle is the referenced field (WR-02).
1082
- */
1034
+ // The caller has already checked that a focused handle is the referenced field.
1083
1035
  async clearNativeOrKeyboard(box, handle) {
1084
1036
  const path = handle ? "native" : "keyboard";
1085
1037
  const before = handle ? await this.isSecure(handle) : false;
@@ -1139,11 +1091,7 @@ var AndroidPlatformAdapter = class {
1139
1091
  metadata: { fillVerdict, ...action.submit ? { fillSubmit: { ...INPUT_FAILED } } : {} }
1140
1092
  };
1141
1093
  }
1142
- /**
1143
- * The shared end of every Android fill (437 D-09, D-10, D-13, D-15, D-17): records the verdict
1144
- * and, when a submit was requested, holds Enter on a mismatch or a multi-line field and otherwise
1145
- * presses it once through `enter`.
1146
- */
1094
+ // Holds Enter on a mismatch or a multi-line field; otherwise presses it once.
1147
1095
  async settleFill(action, box, settle) {
1148
1096
  const { path, verdict, multiline, enter } = settle;
1149
1097
  const fillVerdict = {
@@ -1179,7 +1127,7 @@ var AndroidPlatformAdapter = class {
1179
1127
  }
1180
1128
  return { success: true, ...box, ...notice, metadata };
1181
1129
  }
1182
- /** A real W3C touch tap in the WebView context at the ref's CSS-pixel centre (437 D-11). */
1130
+ /** A real W3C touch tap in the WebView context at the ref's CSS-pixel centre. */
1183
1131
  async trustedWebTap(center) {
1184
1132
  await safePerform(
1185
1133
  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 })
@@ -1232,7 +1180,7 @@ var AndroidPlatformAdapter = class {
1232
1180
  if (this.activeWebContext) await this.ensureContext(this.activeWebContext);
1233
1181
  }
1234
1182
  }
1235
- /** 437 D-11: a tap that left no editable field, or another field, focused writes nothing. */
1183
+ // A tap that leaves no editable field, or another field, focused writes nothing.
1236
1184
  refuseFocus(verb, ref, focus, submit, box) {
1237
1185
  const nothing = verb === "fill" ? "nothing was typed" : "nothing was cleared";
1238
1186
  let fillRefusal;
@@ -1254,10 +1202,7 @@ var AndroidPlatformAdapter = class {
1254
1202
  metadata: { fillRefusal, ...submit ? { fillSubmit: { ...INPUT_FAILED } } : {} }
1255
1203
  };
1256
1204
  }
1257
- /**
1258
- * WebView clearText (437 D-11, D-16): the same tap and focus probe as the fill, W3C Ctrl+A and
1259
- * Backspace, then a read-back judged against the empty string. Never success without that check.
1260
- */
1205
+ // Never success without the read-back judged against the empty string.
1261
1206
  async clearWebViewField(ref, center, box) {
1262
1207
  const driver = this.driver;
1263
1208
  await this.trustedWebTap(center);
@@ -1273,11 +1218,7 @@ var AndroidPlatformAdapter = class {
1273
1218
  }
1274
1219
  return this.settleClear("webview", await this.readWebFieldValue(), focus.secure, box);
1275
1220
  }
1276
- /**
1277
- * WebView fill as trusted input (437 D-11): a W3C tap on the ref, a read-only focus probe,
1278
- * W3C Ctrl+A and Backspace for replace, `mobile: type` from NATIVE_APP, a read-only read-back,
1279
- * and one `mobile: pressKey 66` from NATIVE_APP. No script writes the field or fires an event.
1280
- */
1221
+ // Trusted input only: no page script writes the field or fires an event.
1281
1222
  async fillWebViewCompound(action, box, center) {
1282
1223
  const driver = this.driver;
1283
1224
  const isAppend = action.clearMode === "append";
@@ -1314,15 +1255,7 @@ var AndroidPlatformAdapter = class {
1314
1255
  enter: () => this.withNativeContext(() => this.pressEnter())
1315
1256
  });
1316
1257
  }
1317
- /**
1318
- * EXEC-02 platform primitives injected into the shared runner scroll loops (D-02/D-03; 436-02
1319
- * D-25). A native swipe is `mobile: scrollGesture` over the container (the ref's bounds, else the
1320
- * screen), whose boolean is the driver's own boundary signal; in a WebView the touch swipe is
1321
- * measured over every WebView scroller before and after (436-18), paired by identity (436-23).
1322
- * nativeSearchScroll drives the UiAutomator
1323
- * `UiScrollable.scrollIntoView` rung over an injection-escaped selector; readInterimTree is a
1324
- * light page-source parse — NO refMap mint, NO screenshot, lastRefs untouched.
1325
- */
1258
+ // Native swipes trust `scrollGesture`'s boolean; WebView swipes compare every scroller by id.
1326
1259
  async buildAndroidScrollPrimitives(scrollDirection, maxSwipes, factor = 1) {
1327
1260
  const driver = this.driver;
1328
1261
  const viewport = await driver.getWindowSize();
@@ -1381,13 +1314,7 @@ var AndroidPlatformAdapter = class {
1381
1314
  }
1382
1315
  };
1383
1316
  }
1384
- /**
1385
- * The WebView scrollers a touch swipe may have acted on (436-18 · D-25), read by the shared
1386
- * WEBVIEW_SCROLLERS_SCRIPT. The touch point cannot be mapped into the WebView's CSS coordinates,
1387
- * so the swipe is judged over all of them.
1388
- * 2026-10-08 (436-23): a frame in view reads unknown; members carry an identity and are paired
1389
- * by it; open shadow roots are walked; only on-screen element scrollers are members.
1390
- */
1317
+ // The touch point cannot map into CSS coordinates, so the swipe is judged over every scroller.
1391
1318
  async readWebViewScrollers() {
1392
1319
  try {
1393
1320
  return parseWebViewScrollers(await this.driver.execute(WEBVIEW_SCROLLERS_SCRIPT));
@@ -1395,12 +1322,7 @@ var AndroidPlatformAdapter = class {
1395
1322
  return void 0;
1396
1323
  }
1397
1324
  }
1398
- /**
1399
- * EXEC-03 openApp (D-10/D-11/D-12). Resolve a human name to a package id (or validate an
1400
- * explicit bundleId) over the session-cached registry, then launch via activateApp. On a
1401
- * resolution MISS the registry is re-enumerated exactly once (catches mid-run installs) before
1402
- * erroring with the resolver's candidate ids. Never a best-guess launch (T-359-04).
1403
- */
1325
+ // On a resolution miss the registry is re-enumerated once before erroring; never a best guess.
1404
1326
  async openApp(action) {
1405
1327
  const driver = this.driver;
1406
1328
  let registry = await this.ensureAppRegistry();
@@ -1452,12 +1374,7 @@ var AndroidPlatformAdapter = class {
1452
1374
  if (enumerated !== null) this.appRegistry = enumerated;
1453
1375
  return enumerated;
1454
1376
  }
1455
- /**
1456
- * Enumerate installed apps through the driver only (ownership contract — no adb/child_process).
1457
- * `mobile: listApps` maps package ids → AppRegistryEntry (labels only when the same call carries
1458
- * them, D-10 — no per-package round-trips). On failure, fall back to `mobile: shell` pm-list ONLY
1459
- * when the AGENT_QA_ANDROID_USE_MOBILE_SHELL gate is '1' (Pitfall 4). Null = enumeration unavailable.
1460
- */
1377
+ // Driver only, no adb; the `mobile: shell` fallback needs `AGENT_QA_ANDROID_USE_MOBILE_SHELL=1`.
1461
1378
  async enumerateApps() {
1462
1379
  const driver = this.driver;
1463
1380
  try {
@@ -1893,8 +1810,6 @@ var AndroidPlatformAdapter = class {
1893
1810
  success: false,
1894
1811
  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.'
1895
1812
  };
1896
- case "expr-result":
1897
- return { success: true, data: void 0 };
1898
1813
  }
1899
1814
  }
1900
1815
  case "drag": {
@@ -1948,11 +1863,7 @@ var AndroidPlatformAdapter = class {
1948
1863
  }
1949
1864
  return { success: true, coordinates: { x, y } };
1950
1865
  }
1951
- // Runner-minted / author-channel verbs (D-01/D-15): reportValue, assertAI, extractAI, and
1952
- // setVariable are intercepted at the runner dispatch seam and never reach the adapter. A
1953
- // grouped §7.1 defensive case surfaces a dispatch-routing error if one arrives here;
1954
- // assertNever remains the backstop for a genuinely unknown schema verb. (uniqueValue is
1955
- // web-only and is not part of ActionsFor<'android'>.)
1866
+ // Runner-channel verbs never reach the adapter; one arriving here is a routing error.
1956
1867
  case "reportValue":
1957
1868
  case "assertAI":
1958
1869
  case "extractAI":