@vostride/agent-qa-web 0.2.2 → 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
@@ -32,10 +32,7 @@ var index_exports = {};
32
32
  __export(index_exports, {
33
33
  ActionValidationError: () => ActionValidationError,
34
34
  ElementResolver: () => ElementResolver,
35
- FarmWebAdapter: () => FarmWebAdapter,
36
35
  WebPlatformAdapter: () => WebPlatformAdapter,
37
- extractDom: () => extractDom,
38
- observePage: () => observePage,
39
36
  parseAriaSnapshot: () => parseAriaSnapshot,
40
37
  runAccessibilityCheck: () => runAccessibilityCheck,
41
38
  validateAction: () => validateAction,
@@ -44,84 +41,9 @@ __export(index_exports, {
44
41
  module.exports = __toCommonJS(index_exports);
45
42
 
46
43
  // src/adapter.ts
44
+ var import_agent_qa_schema = require("@vostride/agent-qa-schema");
45
+ var import_agent_qa_runner5 = require("@vostride/agent-qa-runner");
47
46
  var import_agent_qa_runner6 = require("@vostride/agent-qa-runner");
48
- var import_agent_qa_runner7 = require("@vostride/agent-qa-runner");
49
-
50
- // src/observer.ts
51
- var import_agent_qa_runner = require("@vostride/agent-qa-runner");
52
-
53
- // src/dom-extractor.ts
54
- var SKIP_TAGS = /* @__PURE__ */ new Set([
55
- "SCRIPT",
56
- "STYLE",
57
- "SVG",
58
- "NOSCRIPT",
59
- "LINK",
60
- "META",
61
- "TEMPLATE",
62
- "IFRAME"
63
- ]);
64
- function extractDomTree(opts) {
65
- const { maxDepth, maxTextLen } = opts;
66
- const doc = globalThis.document;
67
- const win = globalThis.window;
68
- const lines = [];
69
- function isHidden(el) {
70
- if (!el.offsetParent && el.tagName !== "BODY" && el.tagName !== "HTML") {
71
- const style = win.getComputedStyle(el);
72
- if (style.display === "none" || style.visibility === "hidden") return true;
73
- }
74
- return false;
75
- }
76
- function walk(node, depth, indent) {
77
- if (depth > maxDepth) {
78
- lines.push(indent + "[... deeper content truncated]");
79
- return;
80
- }
81
- for (let child = node.firstChild; child; child = child.nextSibling) {
82
- if (child.nodeType === 3) {
83
- const text = child.textContent.trim();
84
- if (text) {
85
- const truncated = text.length > maxTextLen ? text.slice(0, maxTextLen) + "..." : text;
86
- lines.push(indent + truncated);
87
- }
88
- continue;
89
- }
90
- if (child.nodeType !== 1) continue;
91
- const tag = child.tagName;
92
- if (SKIP_TAGS.has(tag)) continue;
93
- if (isHidden(child)) continue;
94
- const isCustom = tag.toLowerCase().includes("-");
95
- const annotation = child.getAttribute("aria-label") || child.getAttribute("role") || "";
96
- let label = tag.toLowerCase();
97
- if (isCustom) {
98
- if (!child.shadowRoot && !child.firstChild) {
99
- label += " (custom element, closed shadow root)";
100
- } else {
101
- label += " (custom element)";
102
- }
103
- }
104
- if (annotation) label += ` [${annotation}]`;
105
- lines.push(indent + label);
106
- if (child.shadowRoot) {
107
- lines.push(indent + " #shadow-root");
108
- walk(child.shadowRoot, depth + 1, indent + " ");
109
- }
110
- walk(child, depth + 1, indent + " ");
111
- }
112
- }
113
- walk(doc.body, 0, "");
114
- return lines.join("\n");
115
- }
116
- async function extractDom(page, opts) {
117
- const maxDepth = opts?.maxDepth ?? 20;
118
- const maxTextLen = opts?.maxTextLen ?? 200;
119
- try {
120
- return await page.evaluate(extractDomTree, { maxDepth, maxTextLen });
121
- } catch {
122
- return void 0;
123
- }
124
- }
125
47
 
126
48
  // src/observer.ts
127
49
  var INTERACTIVE_ROLES = /* @__PURE__ */ new Set([
@@ -243,20 +165,6 @@ function parseAriaSnapshot(ariaTree) {
243
165
  }
244
166
  return { nodes, nativeRefs };
245
167
  }
246
- function hideAgentQaElements() {
247
- const doc = globalThis.document;
248
- doc.querySelectorAll("[data-agent-qa-internal]").forEach((el) => {
249
- el.style.display = "none";
250
- el.setAttribute("aria-hidden", "true");
251
- });
252
- }
253
- function restoreAgentQaElements() {
254
- const doc = globalThis.document;
255
- doc.querySelectorAll("[data-agent-qa-internal]").forEach((el) => {
256
- el.style.display = "";
257
- el.removeAttribute("aria-hidden");
258
- });
259
- }
260
168
  async function captureWebScroll(page) {
261
169
  const fallbackViewport = page.viewportSize() ?? { width: 0, height: 0 };
262
170
  try {
@@ -383,47 +291,12 @@ function coerceBounds(value) {
383
291
  function coerceString(value) {
384
292
  return typeof value === "string" && value.length > 0 ? value : void 0;
385
293
  }
386
- async function observePage(page, options) {
387
- const locator = options?.filterSelector ? page.locator(options.filterSelector) : page.locator(":root");
388
- await page.evaluate(hideAgentQaElements);
389
- let ariaTree;
390
- try {
391
- ariaTree = await locator.ariaSnapshot();
392
- } finally {
393
- await page.evaluate(restoreAgentQaElements);
394
- }
395
- const timestamp = Date.now();
396
- const url = page.url();
397
- const { viewport: capturedViewport, scroll } = await captureWebScroll(page);
398
- let domContext;
399
- if (options?.extractDom) {
400
- try {
401
- domContext = await extractDom(page);
402
- } catch {
403
- }
404
- }
405
- const { nodes, nativeRefs } = ariaTree ? parseAriaSnapshot(ariaTree) : { nodes: [], nativeRefs: [] };
406
- if (nodes.length > 0) {
407
- await enrichWebNodes(page, nodes, nativeRefs);
408
- }
409
- const { state } = (0, import_agent_qa_runner.buildScreenState)({
410
- platform: "web",
411
- nodes,
412
- viewport: capturedViewport,
413
- scroll,
414
- coordSpace: "viewport",
415
- timestamp,
416
- url,
417
- ...domContext ? { domContext } : {}
418
- });
419
- return state;
420
- }
421
294
  function numberOr(value, fallback) {
422
295
  return typeof value === "number" && Number.isFinite(value) ? value : fallback;
423
296
  }
424
297
 
425
298
  // src/element-resolver.ts
426
- var import_agent_qa_runner2 = require("@vostride/agent-qa-runner");
299
+ var import_agent_qa_runner = require("@vostride/agent-qa-runner");
427
300
  var ACTIONS_WITHOUT_REF = /* @__PURE__ */ new Set(["navigate", "waitFor", "delay", "waitForUrl", "assertValue"]);
428
301
  function buildRefMap(binding, nodes, nativeRefs, elements) {
429
302
  const refs = {};
@@ -433,7 +306,7 @@ function buildRefMap(binding, nodes, nativeRefs, elements) {
433
306
  const element = elements[bind.index];
434
307
  if (!node || nativeRef === void 0 || !element) continue;
435
308
  const entry = { nativeRef, role: node.role, name: node.name ?? "" };
436
- const rowText = (0, import_agent_qa_runner2.rowTextOf)(element, elements);
309
+ const rowText = (0, import_agent_qa_runner.rowTextOf)(element, elements);
437
310
  if (rowText !== void 0) entry.rowText = rowText;
438
311
  if (node.bounds !== void 0) entry.bounds = node.bounds;
439
312
  refs[ref] = entry;
@@ -472,7 +345,7 @@ var ElementResolver = class {
472
345
  };
473
346
 
474
347
  // src/binding.ts
475
- var import_agent_qa_runner3 = require("@vostride/agent-qa-runner");
348
+ var import_agent_qa_runner2 = require("@vostride/agent-qa-runner");
476
349
  var BINDING_LANES = ["ref", "fromRef", "toRef"];
477
350
  var FRAME_REF = /^f\d+e/;
478
351
  var INVALID_FRAME = /invalid frame/i;
@@ -482,14 +355,14 @@ var WHAT_CHANGED = {
482
355
  "binding-lost": "the observed element is no longer on the page",
483
356
  "binding-changed": "the observed element now has a different role, name or row"
484
357
  };
485
- function hideAgentQaElements2() {
358
+ function hideAgentQaElements() {
486
359
  const doc = globalThis.document;
487
360
  doc.querySelectorAll("[data-agent-qa-internal]").forEach((el) => {
488
361
  el.style.display = "none";
489
362
  el.setAttribute("aria-hidden", "true");
490
363
  });
491
364
  }
492
- function restoreAgentQaElements2() {
365
+ function restoreAgentQaElements() {
493
366
  const doc = globalThis.document;
494
367
  doc.querySelectorAll("[data-agent-qa-internal]").forEach((el) => {
495
368
  el.style.display = "";
@@ -498,7 +371,7 @@ function restoreAgentQaElements2() {
498
371
  }
499
372
  async function snapshotRoot(page, options = {}) {
500
373
  const { timeoutMs, retryTimeoutMs } = options;
501
- await page.evaluate(hideAgentQaElements2);
374
+ await page.evaluate(hideAgentQaElements);
502
375
  try {
503
376
  if (timeoutMs === void 0) return await page.locator(":root").ariaSnapshot({ mode: "ai" });
504
377
  return await page.locator(":root").ariaSnapshot({ mode: "ai", timeout: timeoutMs }).catch((error) => {
@@ -509,7 +382,7 @@ async function snapshotRoot(page, options = {}) {
509
382
  });
510
383
  });
511
384
  } finally {
512
- await page.evaluate(restoreAgentQaElements2);
385
+ await page.evaluate(restoreAgentQaElements);
513
386
  }
514
387
  }
515
388
  async function captureEpoch(page) {
@@ -555,7 +428,7 @@ async function checkBinding(page, epoch, entry, snapshot) {
555
428
  const detached = await frameDetached(page, entry.nativeRef);
556
429
  return { outcome: detached ? "frame-detached" : "binding-lost" };
557
430
  }
558
- const { state } = (0, import_agent_qa_runner3.buildScreenState)({
431
+ const { state } = (0, import_agent_qa_runner2.buildScreenState)({
559
432
  platform: "web",
560
433
  nodes,
561
434
  viewport: { width: 0, height: 0 },
@@ -564,7 +437,7 @@ async function checkBinding(page, epoch, entry, snapshot) {
564
437
  timestamp: 0
565
438
  });
566
439
  const current = state.elements[index];
567
- if (current === void 0 || current.role !== entry.role || tidy(current.name) !== tidy(entry.name) || tidy((0, import_agent_qa_runner3.rowTextOf)(current, state.elements)) !== tidy(entry.rowText)) {
440
+ if (current === void 0 || current.role !== entry.role || tidy(current.name) !== tidy(entry.name) || tidy((0, import_agent_qa_runner2.rowTextOf)(current, state.elements)) !== tidy(entry.rowText)) {
568
441
  return { outcome: "binding-changed" };
569
442
  }
570
443
  return { outcome: "ok" };
@@ -722,7 +595,7 @@ async function runReadinessChecks(page) {
722
595
  }
723
596
 
724
597
  // src/compound-web.ts
725
- var import_agent_qa_runner4 = require("@vostride/agent-qa-runner");
598
+ var import_agent_qa_runner3 = require("@vostride/agent-qa-runner");
726
599
 
727
600
  // src/scroll-helper.ts
728
601
  var NOT_FOUND = { bounds: null, scrolledContainer: "notFound", scrolled: false };
@@ -834,7 +707,7 @@ async function runWebFillCompound(locator, boundingBox, action) {
834
707
  if (actual === void 0) {
835
708
  return { success: true, ...box, metadata: { fillVerdict: { verb: "fill", verdict: "unverified" } } };
836
709
  }
837
- const verdict = (0, import_agent_qa_runner4.compareFillReread)({ expected, actual, secure });
710
+ const verdict = (0, import_agent_qa_runner3.compareFillReread)({ expected, actual, secure });
838
711
  if (verdict.ok && action.submit) {
839
712
  await locator.press("Enter");
840
713
  }
@@ -1194,10 +1067,10 @@ function buildScrollPrimitives(page, lastRefs, magnitude) {
1194
1067
  if (ref) {
1195
1068
  const scrolled = await scrollWithRef(page, lastRefs, ref, dx, dy, axis);
1196
1069
  if (scrolled.scrolledContainer !== "notFound") await settle(page);
1197
- return (0, import_agent_qa_runner4.swipeSignalFromOffsets)(direction, scrolled.offsets);
1070
+ return (0, import_agent_qa_runner3.swipeSignalFromOffsets)(direction, scrolled.offsets);
1198
1071
  }
1199
1072
  const chain = await wheelPointerChain(page, dx, dy, axis);
1200
- return (0, import_agent_qa_runner4.scrollerSetSwipeSignal)(direction, chain?.before, chain?.after);
1073
+ return (0, import_agent_qa_runner3.scrollerSetSwipeSignal)(direction, chain?.before, chain?.after);
1201
1074
  },
1202
1075
  async nativeSearchScroll(target) {
1203
1076
  try {
@@ -1222,7 +1095,7 @@ async function runWebScrollCompound(page, lastRefs, action) {
1222
1095
  if (action.mode === "untilVisible") {
1223
1096
  const magnitude2 = horizontal ? viewport.width : viewport.height;
1224
1097
  const primitives2 = buildScrollPrimitives(page, lastRefs, magnitude2);
1225
- const { meta: meta2 } = await (0, import_agent_qa_runner4.runUntilVisibleLoop)(primitives2, {
1098
+ const { meta: meta2 } = await (0, import_agent_qa_runner3.runUntilVisibleLoop)(primitives2, {
1226
1099
  target: action.text,
1227
1100
  direction: action.direction,
1228
1101
  maxSwipes: action.maxSwipes,
@@ -1235,10 +1108,10 @@ async function runWebScrollCompound(page, lastRefs, action) {
1235
1108
  const magnitude = Math.round((horizontal ? viewport.width : viewport.height) * factor);
1236
1109
  const primitives = buildScrollPrimitives(page, lastRefs, magnitude);
1237
1110
  if (action.amount === "toEnd") {
1238
- const { meta: meta2 } = await (0, import_agent_qa_runner4.runToEndScroll)(primitives, action.direction, action.ref);
1111
+ const { meta: meta2 } = await (0, import_agent_qa_runner3.runToEndScroll)(primitives, action.direction, action.ref);
1239
1112
  return { success: true, metadata: { scrollUntilVisible: meta2 } };
1240
1113
  }
1241
- const { meta } = await (0, import_agent_qa_runner4.runDirectionScroll)(primitives, {
1114
+ const { meta } = await (0, import_agent_qa_runner3.runDirectionScroll)(primitives, {
1242
1115
  direction: action.direction,
1243
1116
  ...action.ref ? { ref: action.ref } : {}
1244
1117
  });
@@ -1352,7 +1225,6 @@ function createScreencastRecording(getPage, config, getPageRevision) {
1352
1225
  const before = scene;
1353
1226
  const epoch = firstFrameEpoch;
1354
1227
  const revision = sceneRevision;
1355
- if (!getPageRevision) return void 0;
1356
1228
  const pageRevision = getPageRevision();
1357
1229
  if (!recording || !before || epoch === void 0 || getPage() !== recording.page) return void 0;
1358
1230
  const viewport = recording.page.viewportSize();
@@ -1404,7 +1276,7 @@ function createScreencastRecording(getPage, config, getPageRevision) {
1404
1276
  // src/operation-capture.ts
1405
1277
  var import_node_crypto2 = require("crypto");
1406
1278
  var import_node_path2 = require("path");
1407
- var import_agent_qa_runner5 = require("@vostride/agent-qa-runner");
1279
+ var import_agent_qa_runner4 = require("@vostride/agent-qa-runner");
1408
1280
  var DOWNLOAD_START_GRACE_MS = 500;
1409
1281
  var NAVIGATION_SETTLE_MS = 250;
1410
1282
  var DOWNLOAD_COMPLETION_CAP_MS = 12e4;
@@ -1483,8 +1355,7 @@ var CLIPBOARD_HOOK = `(() => {
1483
1355
  if (typeof send !== 'function' || typeof text !== 'string' || text.length === 0) return;
1484
1356
  try { Promise.resolve(send({ via, text })).catch(() => {}); } catch {}
1485
1357
  };
1486
- // 2026-10-08 (436-14): a write is reported only once the browser accepted it; the app still
1487
- // gets the original promise, rejections included.
1358
+ // A write is reported only once the browser accepted it; the app gets the original promise.
1488
1359
  const proto = typeof Clipboard === 'function' ? Clipboard.prototype : undefined;
1489
1360
  if (proto && typeof proto.writeText === 'function') {
1490
1361
  const writeText = proto.writeText;
@@ -1565,7 +1436,7 @@ var OperationCapture = class {
1565
1436
  downloads = [];
1566
1437
  /** Downloads that started while no start window was open, kept for the next action. */
1567
1438
  held = [];
1568
- /** Completed downloads' bytes by sha256, oldest first; memory only (439 D-04). */
1439
+ /** Completed downloads' bytes by sha256, oldest first; memory only. */
1569
1440
  heldBytes = /* @__PURE__ */ new Map();
1570
1441
  heldByteTotal = 0;
1571
1442
  pendingNavigations = /* @__PURE__ */ new Map();
@@ -2074,41 +1945,34 @@ var WebPlatformAdapter = class {
2074
1945
  browser = null;
2075
1946
  context = null;
2076
1947
  page = null;
2077
- // The native binding of the last observation and its document (438 D-06). Neither crosses the
2078
- // port (observe returns only the ScreenState).
1948
+ // Adapter-private: observe returns only the `ScreenState`, never the native binding.
2079
1949
  lastRefs = {};
2080
1950
  epoch;
2081
1951
  pageLogBuffers = /* @__PURE__ */ new Map();
2082
1952
  logCaptureConfig = { console: true, network: true };
1953
+ navigationTimeoutMs = import_agent_qa_schema.DEFAULT_NAVIGATION_TIMEOUT_MS;
2083
1954
  disconnected = false;
2084
1955
  intentionalClose = false;
2085
- // Persistent route interception + one-shot dialog arms live until an explicit unroute or
2086
- // test teardown (D-12/D-18); cleanup() drains both so nothing bleeds across tests (Pitfall 7).
1956
+ // Routes persist until unroute and a dialog arm until its dialog; `cleanup()` drains both.
2087
1957
  armedRoutes = /* @__PURE__ */ new Set();
2088
1958
  pendingDialogArm = null;
2089
- // 403 D-05: the live screencast's CDP session, owned here and never handed out.
2090
1959
  browserName = "chromium";
2091
1960
  screencast = null;
2092
1961
  screencastCapability = null;
2093
1962
  recordingCapability;
2094
1963
  recordingPageRevision = 0;
2095
1964
  recordingConfig;
2096
- // 403-02: the PRODUCT's frame identity, and the CDP ack token it maps to. See `liveStream`.
2097
1965
  screencastFrameSeq = 0;
2098
1966
  screencastAckTokens = /* @__PURE__ */ new Map();
2099
- // 428 D-15: the running screencast's consumer, its quiet-page keepalive, and the size a keepalive
2100
- // frame reports (the last screencast frame's device size).
2101
1967
  screencastOnFrame = null;
2102
1968
  keepaliveTimer = null;
2103
1969
  lastScreencastSize = null;
2104
1970
  keepaliveEchoDeadline = null;
2105
1971
  screencastChain = Promise.resolve();
2106
- // 428 D-12: the labelled snapshot preview standing in for a screencast withdrawn mid-stream.
2107
1972
  withdrawnSnapshot = null;
2108
1973
  screencastRecoveryTimer = null;
2109
- // 436 D-19, D-21: download and app-clipboard capture, Chromium only; armed around each execute().
2110
1974
  operationCapture = null;
2111
- // 436 D-23: Firefox and WebKit run no capture and name themselves as the reason.
1975
+ // Firefox and WebKit run no capture and name themselves as the reason.
2112
1976
  operationCaptureSupport;
2113
1977
  async setup(config) {
2114
1978
  this.recordingConfig = config.recording;
@@ -2144,6 +2008,8 @@ var WebPlatformAdapter = class {
2144
2008
  contextOptions.storageState = config.authState.storageStatePath;
2145
2009
  }
2146
2010
  this.context = await this.browser.newContext(contextOptions);
2011
+ this.navigationTimeoutMs = config.timeouts?.navigation ?? import_agent_qa_schema.DEFAULT_NAVIGATION_TIMEOUT_MS;
2012
+ this.context.setDefaultNavigationTimeout(this.navigationTimeoutMs);
2147
2013
  if (browserName === "chromium") {
2148
2014
  await this.context.grantPermissions(["clipboard-write"]);
2149
2015
  }
@@ -2161,22 +2027,7 @@ var WebPlatformAdapter = class {
2161
2027
  this.operationCaptureSupport = { download: false, clipboard: false, reason: browserName };
2162
2028
  }
2163
2029
  }
2164
- /**
2165
- * 403 D-05/LIVE-01: the Chromium screencast producer, or ABSENT when this adapter cannot stream.
2166
- *
2167
- * Absence means "no continuous stream", not "no preview": since 428-05 the composition's
2168
- * `liveStreamFor` wraps this adapter's `screenshot()` in the labelled 1 s snapshot producer, both
2169
- * as the fallback behind a screencast that fails to start and as the whole preview on a
2170
- * non-Chromium target. The two gates here (Chromium only, and a page to stream) stay exactly as
2171
- * they are, so the absence stays honest.
2172
- *
2173
- * SCOPE (428 D-18, closes WIN #273): the screencast shows the page the test is acting on. Both
2174
- * page-switch sites (`tab new`, `tab switch`) go through `setActivePage`, which re-binds the
2175
- * running screencast to the new page INSIDE this same capability object: no stop, no re-register,
2176
- * no stream end, so a subscriber only sees the picture change. Start, stop and re-bind run on one
2177
- * serialised chain. A popup or a closing tab never changes `this.page` in this adapter, so the
2178
- * preview already shows the acting page in those cases.
2179
- */
2030
+ // Absence means no stream, not no preview: the runner's `liveStreamFor` wraps `screenshot()`.
2180
2031
  get liveStream() {
2181
2032
  if (this.browserName !== "chromium") return void 0;
2182
2033
  if (!this.page || !this.context) return void 0;
@@ -2226,7 +2077,7 @@ var WebPlatformAdapter = class {
2226
2077
  this.screencastCapability = capability;
2227
2078
  return capability;
2228
2079
  }
2229
- /** Run `step` after every earlier start, stop and re-bind has settled (428 D-18). */
2080
+ /** Run `step` after every earlier start, stop and re-bind has settled. */
2230
2081
  onScreencastChain(step) {
2231
2082
  const run = this.screencastChain.then(step);
2232
2083
  this.screencastChain = run.catch(() => void 0);
@@ -2279,7 +2130,7 @@ var WebPlatformAdapter = class {
2279
2130
  metadata: {
2280
2131
  width: metadata.deviceWidth,
2281
2132
  height: metadata.deviceHeight,
2282
- // CDP's `TimeSinceEpoch` is SECONDS; every other layer reads epoch ms (428 D-15).
2133
+ // CDP's `TimeSinceEpoch` is in seconds; every other layer uses epoch ms.
2283
2134
  ...metadata.timestamp === void 0 ? {} : { timestamp: Math.round(metadata.timestamp * 1e3) },
2284
2135
  scrollOffsetX: metadata.scrollOffsetX,
2285
2136
  scrollOffsetY: metadata.scrollOffsetY
@@ -2287,11 +2138,7 @@ var WebPlatformAdapter = class {
2287
2138
  });
2288
2139
  });
2289
2140
  }
2290
- /**
2291
- * The one place the page the test acts on changes (428 D-18). Recording keeps following its
2292
- * starting page through the revision counter (424 reconciliation 25(d)); the live screencast,
2293
- * when running, re-binds to the new page on the serialised chain.
2294
- */
2141
+ // Every page switch goes through here so a running screencast re-binds on its chain.
2295
2142
  setActivePage(page) {
2296
2143
  this.page = page;
2297
2144
  this.recordingPageRevision++;
@@ -2337,21 +2184,14 @@ var WebPlatformAdapter = class {
2337
2184
  this.armKeepalive(cdp);
2338
2185
  await this.stopWithdrawnSnapshot();
2339
2186
  }
2340
- /**
2341
- * 428 D-12's second half: the screencast was withdrawn while the browser is still up (its CDP
2342
- * session detached or unusable), which only this capability can see. Detected when a keepalive
2343
- * capture or a re-bind's start rejects on the active session; the capability then serves the
2344
- * labelled 1 s snapshot preview of the current page through the same `onFrame`, until `stop()`,
2345
- * `cleanup()` or a successful re-bind. The declared kind stays `cdp-screencast`; the frames carry
2346
- * `kind: 'snapshot'`, which is what labels the preview. Runs on the serialised chain.
2347
- */
2187
+ // Frames carry `kind: 'snapshot'` until a re-bind; the declared kind stays `cdp-screencast`.
2348
2188
  handleScreencastWithdrawn(cdp) {
2349
2189
  void this.onScreencastChain(async () => {
2350
2190
  if (this.screencast !== cdp) return;
2351
2191
  const onFrame = this.screencastOnFrame;
2352
2192
  if (onFrame === null || this.withdrawnSnapshot !== null || !this.browserIsLive()) return;
2353
2193
  await this.detachScreencast();
2354
- const snapshot = (0, import_agent_qa_runner6.createSnapshotLiveStream)({ capture: () => this.screenshot() });
2194
+ const snapshot = (0, import_agent_qa_runner5.createSnapshotLiveStream)({ capture: () => this.screenshot() });
2355
2195
  this.withdrawnSnapshot = snapshot;
2356
2196
  await snapshot.start((frame) => {
2357
2197
  if (this.withdrawnSnapshot !== snapshot) return;
@@ -2360,7 +2200,6 @@ var WebPlatformAdapter = class {
2360
2200
  this.armScreencastRecovery();
2361
2201
  });
2362
2202
  }
2363
- /** While the snapshot fallback runs, try the screencast again every SCREENCAST_RECOVERY_MS (428 WR-08). */
2364
2203
  armScreencastRecovery() {
2365
2204
  this.clearScreencastRecovery();
2366
2205
  const timer = setTimeout(() => {
@@ -2399,18 +2238,11 @@ var WebPlatformAdapter = class {
2399
2238
  this.clearKeepalive();
2400
2239
  const timer = setTimeout(() => {
2401
2240
  void this.fireKeepalive(cdp);
2402
- }, import_agent_qa_runner6.LIVE_KEEPALIVE_QUIET_MS);
2241
+ }, import_agent_qa_runner5.LIVE_KEEPALIVE_QUIET_MS);
2403
2242
  timer.unref?.();
2404
2243
  this.keepaliveTimer = timer;
2405
2244
  }
2406
- /**
2407
- * 428 D-15: a fresh capture on the same CDP session, emitted as a frame with no ack token (the ack
2408
- * path drops ids it never minted a token for). A capture that rejects emits nothing. While the
2409
- * capability is started under a live browser, a session that then fails a trivial probe too is a
2410
- * withdrawn screencast (D-12, see `handleScreencastWithdrawn`); one that still answers only had a
2411
- * bad moment, such as a cross-process navigation, and re-arms (428 WR-08). One that never settles
2412
- * emits nothing and arms nothing, which is the honest amber.
2413
- */
2245
+ // A rejected capture means a withdrawn screencast only if a trivial probe fails too.
2414
2246
  async fireKeepalive(cdp) {
2415
2247
  this.keepaliveTimer = null;
2416
2248
  if (this.screencast !== cdp) return;
@@ -2518,7 +2350,7 @@ var WebPlatformAdapter = class {
2518
2350
  get isBrowserDisconnected() {
2519
2351
  return this.disconnected;
2520
2352
  }
2521
- async observe(options) {
2353
+ async observe() {
2522
2354
  if (!this.page) {
2523
2355
  throw new Error("WebPlatformAdapter not initialized \u2014 call setup() first");
2524
2356
  }
@@ -2526,7 +2358,7 @@ var WebPlatformAdapter = class {
2526
2358
  await waitForPageReady(page);
2527
2359
  const epoch = await captureEpoch(page);
2528
2360
  try {
2529
- const state = await this.observeDocument(page, options);
2361
+ const state = await this.observeDocument(page);
2530
2362
  await disposeEpoch(this.epoch);
2531
2363
  this.epoch = epoch;
2532
2364
  return state;
@@ -2535,29 +2367,21 @@ var WebPlatformAdapter = class {
2535
2367
  throw error;
2536
2368
  }
2537
2369
  }
2538
- async observeDocument(page, options) {
2370
+ async observeDocument(page) {
2539
2371
  const ariaTree = await snapshotRoot(page, OBSERVE_SNAPSHOT);
2540
2372
  const { nodes, nativeRefs } = parseAriaSnapshot(ariaTree);
2541
2373
  if (nodes.length > 0) {
2542
2374
  await enrichWebNodes(page, nodes, nativeRefs);
2543
2375
  }
2544
2376
  const { viewport, scroll } = await captureWebScroll(page);
2545
- let domContext;
2546
- if (options?.extractDom) {
2547
- try {
2548
- domContext = await extractDom(page);
2549
- } catch {
2550
- }
2551
- }
2552
- const { state, binding } = (0, import_agent_qa_runner6.buildScreenState)({
2377
+ const { state, binding } = (0, import_agent_qa_runner5.buildScreenState)({
2553
2378
  platform: "web",
2554
2379
  nodes,
2555
2380
  viewport,
2556
2381
  scroll,
2557
2382
  coordSpace: "viewport",
2558
2383
  timestamp: Date.now(),
2559
- url: page.url(),
2560
- ...domContext !== void 0 ? { domContext } : {}
2384
+ url: page.url()
2561
2385
  });
2562
2386
  this.lastRefs = buildRefMap(binding, nodes, nativeRefs, state.elements);
2563
2387
  return state;
@@ -2578,6 +2402,15 @@ var WebPlatformAdapter = class {
2578
2402
  }
2579
2403
  return withOperationMetadata(result, captured);
2580
2404
  }
2405
+ // A navigation that timed out stays pending and blocks every later call on its page until stopped.
2406
+ async stopLoading(page) {
2407
+ try {
2408
+ const cdp = await this.context.newCDPSession(page);
2409
+ await cdp.send("Page.stopLoading");
2410
+ await cdp.detach();
2411
+ } catch {
2412
+ }
2413
+ }
2581
2414
  async executeWithErrorResult(action) {
2582
2415
  if (!this.page) {
2583
2416
  throw new Error("WebPlatformAdapter not initialized \u2014 call setup() first");
@@ -2640,7 +2473,6 @@ var WebPlatformAdapter = class {
2640
2473
  if (!urlPattern) return all;
2641
2474
  return all.filter((e) => e.url.includes(urlPattern));
2642
2475
  }
2643
- /** 439 D-04: a completed download's bytes held in memory, by its sha256. */
2644
2476
  readDownloadBytes(sha256) {
2645
2477
  return this.operationCapture?.readDownloadBytes(sha256);
2646
2478
  }
@@ -2663,8 +2495,9 @@ var WebPlatformAdapter = class {
2663
2495
  this.pageLogBuffers.set(page, buffers);
2664
2496
  if (this.logCaptureConfig.console) {
2665
2497
  page.on("console", (msg) => {
2498
+ const type = msg.type();
2666
2499
  buffers.consoleLogs.push({
2667
- level: msg.type(),
2500
+ level: type === "warning" ? "warn" : type,
2668
2501
  text: msg.text(),
2669
2502
  location: msg.location(),
2670
2503
  timestamp: Date.now()
@@ -2779,19 +2612,24 @@ var WebPlatformAdapter = class {
2779
2612
  return { success: true, boundingBox: box ?? void 0 };
2780
2613
  }
2781
2614
  case "navigate": {
2782
- switch (action.to) {
2783
- case "url":
2784
- await page.goto(action.url, { waitUntil: "domcontentloaded" });
2785
- break;
2786
- case "back":
2787
- await page.goBack({ waitUntil: "domcontentloaded" });
2788
- break;
2789
- case "forward":
2790
- await page.goForward({ waitUntil: "domcontentloaded" });
2791
- break;
2792
- case "reload":
2793
- await page.reload({ waitUntil: "domcontentloaded" });
2794
- break;
2615
+ try {
2616
+ switch (action.to) {
2617
+ case "url":
2618
+ await page.goto(action.url, { waitUntil: "domcontentloaded" });
2619
+ break;
2620
+ case "back":
2621
+ await page.goBack({ waitUntil: "domcontentloaded" });
2622
+ break;
2623
+ case "forward":
2624
+ await page.goForward({ waitUntil: "domcontentloaded" });
2625
+ break;
2626
+ case "reload":
2627
+ await page.reload({ waitUntil: "domcontentloaded" });
2628
+ break;
2629
+ }
2630
+ } catch (err) {
2631
+ await this.stopLoading(page);
2632
+ throw err;
2795
2633
  }
2796
2634
  await waitForPageReady(page);
2797
2635
  return { success: true };
@@ -2826,19 +2664,19 @@ var WebPlatformAdapter = class {
2826
2664
  }
2827
2665
  case "waitForUrl": {
2828
2666
  try {
2829
- await page.waitForURL(action.pattern, { timeout: 3e4 });
2667
+ await page.waitForURL(action.pattern, { timeout: this.navigationTimeoutMs });
2830
2668
  return { success: true };
2831
2669
  } catch (err) {
2832
2670
  return {
2833
2671
  success: false,
2834
- error: `URL pattern "${action.pattern}" did not match within 30s. Hint: use "**" wildcards for substring matching (e.g., "**/dashboard**"). ${err instanceof Error ? err.message : String(err)}`
2672
+ error: `URL pattern "${action.pattern}" did not match within ${this.navigationTimeoutMs}ms. Hint: use "**" wildcards for substring matching (e.g., "**/dashboard**"). ${err instanceof Error ? err.message : String(err)}`
2835
2673
  };
2836
2674
  }
2837
2675
  }
2838
2676
  case "keypress": {
2839
- const effectiveKeys = (0, import_agent_qa_runner7.convertKeysForPlatform)(action.keys, {
2677
+ const effectiveKeys = (0, import_agent_qa_runner6.convertKeysForPlatform)(action.keys, {
2840
2678
  enabled: action.convertPlatformKeys !== false,
2841
- isMac: (0, import_agent_qa_runner7.isMacPlatform)()
2679
+ isMac: (0, import_agent_qa_runner6.isMacPlatform)()
2842
2680
  });
2843
2681
  for (const key of effectiveKeys) {
2844
2682
  if (action.mode === "down") {
@@ -2897,7 +2735,12 @@ var WebPlatformAdapter = class {
2897
2735
  if (action.action === "new") {
2898
2736
  const newPage = await this.context.newPage();
2899
2737
  this.attachLogListeners(newPage);
2900
- await newPage.goto(action.url, { waitUntil: "domcontentloaded" });
2738
+ try {
2739
+ await newPage.goto(action.url, { waitUntil: "domcontentloaded" });
2740
+ } catch (err) {
2741
+ await this.stopLoading(newPage);
2742
+ throw err;
2743
+ }
2901
2744
  await waitForPageReady(newPage);
2902
2745
  this.setActivePage(newPage);
2903
2746
  return { success: true };
@@ -3039,9 +2882,6 @@ var WebPlatformAdapter = class {
3039
2882
  case "network":
3040
2883
  value = this.peekNetworkLogs();
3041
2884
  break;
3042
- case "expr-result":
3043
- value = void 0;
3044
- break;
3045
2885
  }
3046
2886
  }
3047
2887
  return { success: true, data: value };
@@ -3146,804 +2986,7 @@ var WebPlatformAdapter = class {
3146
2986
  case "executeScript": {
3147
2987
  return { success: false, error: "executeScript is a mobile-only action" };
3148
2988
  }
3149
- // Runner-minted / author-channel verbs (D-01/D-15/D-16): reportValue, uniqueValue, assertAI,
3150
- // extractAI, and setVariable are intercepted at the dispatch seam and never reach the adapter.
3151
- // A grouped §7.1 defensive case surfaces a dispatch-routing error if one arrives here;
3152
- // assertNever remains the backstop for a genuinely unknown schema verb.
3153
- case "reportValue":
3154
- case "uniqueValue":
3155
- case "assertAI":
3156
- case "extractAI":
3157
- case "setVariable":
3158
- return {
3159
- success: false,
3160
- error: `Action "${action.type}" is handled by the runner dispatch seam and must not reach the web adapter (runner-minted/author-channel verb). This indicates a dispatch routing error.`
3161
- };
3162
- default:
3163
- return (0, import_agent_qa_runner6.assertNever)(action);
3164
- }
3165
- }
3166
- };
3167
-
3168
- // src/farm-adapter.ts
3169
- var import_agent_qa_runner8 = require("@vostride/agent-qa-runner");
3170
- var import_agent_qa_core = require("@vostride/agent-qa-core");
3171
- var import_agent_qa_runner9 = require("@vostride/agent-qa-runner");
3172
- function clickDelayOption2(clickDelay) {
3173
- return clickDelay !== void 0 && clickDelay > 0 ? { delay: clickDelay } : {};
3174
- }
3175
- function readSeamStorageState2(action) {
3176
- return action.state;
3177
- }
3178
- var FarmWebAdapter = class {
3179
- platform = "web";
3180
- browser = null;
3181
- context = null;
3182
- page = null;
3183
- // The native binding of the last observation and its document (438 D-06).
3184
- lastRefs = {};
3185
- epoch;
3186
- sessionUrl;
3187
- sessionId;
3188
- testFailed = false;
3189
- exitHandler = null;
3190
- credentials = null;
3191
- pageLogBuffers = /* @__PURE__ */ new Map();
3192
- logCaptureConfig = { console: true, network: true };
3193
- // Persistent route interception + one-shot dialog arms live until an explicit unroute or
3194
- // test teardown (D-12/D-18); cleanup() drains both so nothing bleeds across tests (Pitfall 7).
3195
- armedRoutes = /* @__PURE__ */ new Set();
3196
- pendingDialogArm = null;
3197
- recordingCapability;
3198
- recordingConfig;
3199
- async setup(config) {
3200
- this.recordingConfig = config.recording;
3201
- if (!config.farmSession) {
3202
- throw new Error("FarmWebAdapter requires config.farmSession to be set");
3203
- }
3204
- (0, import_agent_qa_core.registerAllProviders)();
3205
- const provider = (0, import_agent_qa_core.getProvider)("browserstack");
3206
- if (!provider) {
3207
- throw new Error("Unknown farm provider: browserstack");
3208
- }
3209
- this.credentials = provider.resolveCredentials(config.farmSession.capabilities);
3210
- if (!config.browser?.name) {
3211
- throw new Error("FarmWebAdapter requires config.browser.name to be set");
3212
- }
3213
- const credentials = this.credentials;
3214
- const farmWebConfig = {
3215
- browser: config.browser,
3216
- credentials,
3217
- testName: config.farmSession.capabilities?.testName
3218
- };
3219
- const { wsEndpoint } = await provider.resolveWebEndpoint(farmWebConfig);
3220
- const { chromium } = await import("playwright-core");
3221
- this.browser = await chromium.connect({ wsEndpoint });
3222
- const contextOptions = {
3223
- viewport: config.browser?.viewport ?? { width: 1280, height: 720 }
3224
- };
3225
- if (config.authState?.storageStatePath) {
3226
- contextOptions.storageState = config.authState.storageStatePath;
3227
- }
3228
- this.context = await this.browser.newContext(contextOptions);
3229
- await this.context.grantPermissions(["clipboard-write"]);
3230
- this.page = await this.context.newPage();
3231
- try {
3232
- const cdpSession = await this.page.context().newCDPSession(this.page);
3233
- const result = await cdpSession.send("Browser.getVersion");
3234
- await cdpSession.detach();
3235
- const auth = Buffer.from(`${credentials.username}:${credentials.accessKey}`).toString("base64");
3236
- const resp = await fetch("https://api.browserstack.com/automate/builds.json?limit=1&status=running", {
3237
- headers: { Authorization: `Basic ${auth}` }
3238
- });
3239
- if (resp.ok) {
3240
- const builds = await resp.json();
3241
- if (builds.length > 0) {
3242
- const buildId = builds[0].automation_build.hashed_id;
3243
- const sessResp = await fetch(`https://api.browserstack.com/automate/builds/${buildId}/sessions.json?limit=1`, {
3244
- headers: { Authorization: `Basic ${auth}` }
3245
- });
3246
- if (sessResp.ok) {
3247
- const sessions = await sessResp.json();
3248
- if (sessions.length > 0) {
3249
- this.sessionId = sessions[0].automation_session.hashed_id;
3250
- this.sessionUrl = sessions[0].automation_session.browser_url;
3251
- }
3252
- }
3253
- }
3254
- }
3255
- } catch {
3256
- }
3257
- this.exitHandler = () => {
3258
- try {
3259
- this.browser?.close();
3260
- } catch {
3261
- }
3262
- };
3263
- process.on("exit", this.exitHandler);
3264
- this.logCaptureConfig = { console: config.logCapture?.console !== false, network: config.logCapture?.network !== false };
3265
- this.attachLogListeners(this.page);
3266
- }
3267
- get recording() {
3268
- if (!this.page || !this.context || !this.recordingConfig) return void 0;
3269
- return this.recordingCapability ??= createScreencastRecording(
3270
- () => this.page,
3271
- this.recordingConfig
3272
- );
3273
- }
3274
- async cleanup() {
3275
- try {
3276
- await this.recordingCapability?.stop();
3277
- } catch {
3278
- }
3279
- this.recordingCapability = void 0;
3280
- this.recordingConfig = void 0;
3281
- if (this.context) {
3282
- try {
3283
- await this.context.unrouteAll({ behavior: "ignoreErrors" });
3284
- } catch {
3285
- }
3286
- try {
3287
- await this.context.setOffline(false);
3288
- } catch {
3289
- }
3290
- }
3291
- if (this.page && this.pendingDialogArm) {
3292
- try {
3293
- this.page.off("dialog", this.pendingDialogArm);
3294
- } catch {
3295
- }
3296
- }
3297
- this.armedRoutes.clear();
3298
- this.pendingDialogArm = null;
3299
- await disposeEpoch(this.epoch);
3300
- this.epoch = void 0;
3301
- if (this.browser) {
3302
- await this.browser.close();
3303
- }
3304
- if (this.sessionId && this.credentials) {
3305
- try {
3306
- const status = this.testFailed ? "failed" : "passed";
3307
- const reason = this.testFailed ? "Test failed" : "Test passed";
3308
- const auth = Buffer.from(`${this.credentials.username}:${this.credentials.accessKey}`).toString("base64");
3309
- await fetch(`https://api.browserstack.com/automate/sessions/${this.sessionId}.json`, {
3310
- method: "PUT",
3311
- headers: { Authorization: `Basic ${auth}`, "Content-Type": "application/json" },
3312
- body: JSON.stringify({ status, reason })
3313
- });
3314
- } catch {
3315
- }
3316
- }
3317
- if (this.exitHandler) {
3318
- process.removeListener("exit", this.exitHandler);
3319
- this.exitHandler = null;
3320
- }
3321
- this.browser = null;
3322
- this.context = null;
3323
- this.page = null;
3324
- this.lastRefs = {};
3325
- this.pageLogBuffers.clear();
3326
- }
3327
- async observe(options) {
3328
- if (!this.page) {
3329
- throw new Error("FarmWebAdapter not initialized \u2014 call setup() first");
3330
- }
3331
- const page = this.page;
3332
- await waitForPageReady(page);
3333
- const epoch = await captureEpoch(page);
3334
- try {
3335
- const state = await this.observeDocument(page, options);
3336
- await disposeEpoch(this.epoch);
3337
- this.epoch = epoch;
3338
- return state;
3339
- } catch (error) {
3340
- await disposeEpoch(epoch);
3341
- throw error;
3342
- }
3343
- }
3344
- async observeDocument(page, options) {
3345
- const ariaTree = await snapshotRoot(page);
3346
- const { nodes, nativeRefs } = parseAriaSnapshot(ariaTree);
3347
- if (nodes.length > 0) {
3348
- await enrichWebNodes(page, nodes, nativeRefs);
3349
- }
3350
- const { viewport, scroll } = await captureWebScroll(page);
3351
- let domContext;
3352
- if (options?.extractDom) {
3353
- try {
3354
- domContext = await extractDom(page);
3355
- } catch {
3356
- }
3357
- }
3358
- const { state, binding } = (0, import_agent_qa_runner8.buildScreenState)({
3359
- platform: "web",
3360
- nodes,
3361
- viewport,
3362
- scroll,
3363
- coordSpace: "viewport",
3364
- timestamp: Date.now(),
3365
- url: page.url(),
3366
- ...domContext !== void 0 ? { domContext } : {}
3367
- });
3368
- this.lastRefs = buildRefMap(binding, nodes, nativeRefs, state.elements);
3369
- return state;
3370
- }
3371
- async execute(action) {
3372
- if (!this.page) {
3373
- throw new Error("FarmWebAdapter not initialized \u2014 call setup() first");
3374
- }
3375
- try {
3376
- return await this.executeAction(action);
3377
- } catch (err) {
3378
- let screenshot;
3379
- let boundingBox;
3380
- try {
3381
- screenshot = await this.page.screenshot();
3382
- } catch {
3383
- }
3384
- try {
3385
- if ("ref" in action && typeof action.ref === "string") {
3386
- const resolver = new ElementResolver(this.page, this.lastRefs);
3387
- const box = await resolver.getBoundingBox(action.ref);
3388
- if (box) boundingBox = box;
3389
- }
3390
- } catch {
3391
- }
3392
- return {
3393
- success: false,
3394
- error: err instanceof Error ? err.message : String(err),
3395
- screenshot,
3396
- boundingBox
3397
- };
3398
- }
3399
- }
3400
- async screenshot() {
3401
- try {
3402
- if (this.page) {
3403
- return await this.page.screenshot();
3404
- }
3405
- return void 0;
3406
- } catch {
3407
- return void 0;
3408
- }
3409
- }
3410
- getPage() {
3411
- return this.page;
3412
- }
3413
- markFailed() {
3414
- this.testFailed = true;
3415
- }
3416
- getSessionUrl() {
3417
- return this.sessionUrl;
3418
- }
3419
- getSessionId() {
3420
- return this.sessionId;
3421
- }
3422
- drainConsoleLogs(level) {
3423
- const all = [];
3424
- for (const buffers of this.pageLogBuffers.values()) {
3425
- const newLogs = buffers.consoleLogs.slice(buffers.consoleDrainIndex);
3426
- buffers.consoleDrainIndex = buffers.consoleLogs.length;
3427
- all.push(...newLogs);
3428
- }
3429
- if (!level) return all;
3430
- return all.filter((e) => e.level === level);
3431
- }
3432
- drainNetworkLogs(urlPattern) {
3433
- const all = [];
3434
- for (const buffers of this.pageLogBuffers.values()) {
3435
- const newLogs = buffers.networkLogs.slice(buffers.networkDrainIndex);
3436
- buffers.networkDrainIndex = buffers.networkLogs.length;
3437
- all.push(...newLogs);
3438
- }
3439
- if (!urlPattern) return all;
3440
- return all.filter((e) => e.url.includes(urlPattern));
3441
- }
3442
- peekNetworkLogs(urlPattern) {
3443
- const all = [];
3444
- for (const buffers of this.pageLogBuffers.values()) {
3445
- all.push(...buffers.networkLogs.slice(buffers.networkDrainIndex));
3446
- }
3447
- if (!urlPattern) return all;
3448
- return all.filter((e) => e.url.includes(urlPattern));
3449
- }
3450
- attachLogListeners(page) {
3451
- const buffers = {
3452
- consoleLogs: [],
3453
- networkLogs: [],
3454
- consoleDrainIndex: 0,
3455
- networkDrainIndex: 0,
3456
- pendingRequests: /* @__PURE__ */ new Map()
3457
- };
3458
- this.pageLogBuffers.set(page, buffers);
3459
- if (this.logCaptureConfig.console) {
3460
- page.on("console", (msg) => {
3461
- buffers.consoleLogs.push({
3462
- level: msg.type(),
3463
- text: msg.text(),
3464
- location: msg.location(),
3465
- timestamp: Date.now()
3466
- });
3467
- if (buffers.consoleLogs.length > 1e3) {
3468
- buffers.consoleLogs = buffers.consoleLogs.slice(-500);
3469
- }
3470
- });
3471
- }
3472
- if (this.logCaptureConfig.network) {
3473
- page.on("request", (request) => {
3474
- buffers.pendingRequests.set(request.url() + request.method(), {
3475
- url: request.url(),
3476
- method: request.method(),
3477
- headers: request.headers(),
3478
- startTime: Date.now(),
3479
- postData: request.postData() ?? void 0
3480
- });
3481
- });
3482
- page.on("response", async (response) => {
3483
- const key = response.url() + response.request().method();
3484
- const pending = buffers.pendingRequests.get(key);
3485
- buffers.pendingRequests.delete(key);
3486
- let body;
3487
- try {
3488
- const buf = await response.body();
3489
- const ct = response.headers()["content-type"] ?? "";
3490
- if (ct.includes("image/") && !ct.includes("svg")) {
3491
- body = buf.length > 32768 ? void 0 : `data:${ct.split(";")[0]};base64,${buf.toString("base64")}`;
3492
- } else {
3493
- const text = buf.toString("utf-8");
3494
- body = text.length > 32768 ? text.slice(0, 32768) + "...[truncated]" : text;
3495
- }
3496
- } catch {
3497
- body = void 0;
3498
- }
3499
- buffers.networkLogs.push({
3500
- url: response.url(),
3501
- method: response.request().method(),
3502
- status: response.status(),
3503
- requestHeaders: pending?.headers ?? response.request().headers(),
3504
- responseHeaders: response.headers(),
3505
- body,
3506
- requestBody: pending?.postData,
3507
- startTime: pending?.startTime ?? Date.now(),
3508
- endTime: Date.now(),
3509
- timing: response.request().timing()
3510
- });
3511
- if (buffers.networkLogs.length > 500) {
3512
- buffers.networkLogs = buffers.networkLogs.slice(-250);
3513
- }
3514
- });
3515
- }
3516
- }
3517
- async resolveTabTargetAsync(tab) {
3518
- const pages = this.context.pages();
3519
- if (tab.index !== void 0) {
3520
- return tab.index >= 0 && tab.index < pages.length ? pages[tab.index] : void 0;
3521
- }
3522
- if (tab.title) {
3523
- for (const p of pages) {
3524
- const t = await p.title();
3525
- if (t.includes(tab.title)) return p;
3526
- }
3527
- return void 0;
3528
- }
3529
- if (tab.url) {
3530
- return pages.find((p) => p.url().includes(tab.url));
3531
- }
3532
- return void 0;
3533
- }
3534
- async executeAction(action) {
3535
- const page = this.page;
3536
- const resolver = new ElementResolver(page, this.lastRefs);
3537
- for (const ref of laneRefs(action)) {
3538
- const entry = this.lastRefs[ref];
3539
- if (entry === void 0) continue;
3540
- const check = action.type === "scroll" ? await checkDocument(page, this.epoch) : await checkBinding(page, this.epoch, entry);
3541
- if (check.outcome !== "ok") return bindingRefusal(check.outcome, ref);
3542
- }
3543
- if ("ref" in action && typeof action.ref === "string") {
3544
- const refData = this.lastRefs[action.ref];
3545
- const role = refData?.role;
3546
- const validation = validateAction(action, role);
3547
- if (!validation.valid) {
3548
- return { success: false, error: validation.error };
3549
- }
3550
- }
3551
- switch (action.type) {
3552
- case "click": {
3553
- const locator = resolver.resolve(action.ref);
3554
- const box = await resolver.getBoundingBox(action.ref);
3555
- const coordinates = box ? { x: box.x + box.width / 2, y: box.y + box.height / 2 } : void 0;
3556
- await locator.click({
3557
- ...clickDelayOption2(action.clickDelay)
3558
- });
3559
- return { success: true, coordinates, boundingBox: box ?? void 0 };
3560
- }
3561
- case "fill": {
3562
- const locator = resolver.resolve(action.ref);
3563
- const box = await resolver.getBoundingBox(action.ref);
3564
- return runWebFillCompound(locator, box ?? void 0, action);
3565
- }
3566
- case "openApp": {
3567
- return webOpenAppError();
3568
- }
3569
- case "select": {
3570
- const locator = resolver.resolve(action.ref);
3571
- const box = await resolver.getBoundingBox(action.ref);
3572
- await locator.selectOption(action.value);
3573
- return { success: true, boundingBox: box ?? void 0 };
3574
- }
3575
- case "navigate": {
3576
- switch (action.to) {
3577
- case "url":
3578
- await page.goto(action.url, { waitUntil: "domcontentloaded" });
3579
- break;
3580
- case "back":
3581
- await page.goBack({ waitUntil: "domcontentloaded" });
3582
- break;
3583
- case "forward":
3584
- await page.goForward({ waitUntil: "domcontentloaded" });
3585
- break;
3586
- case "reload":
3587
- await page.reload({ waitUntil: "domcontentloaded" });
3588
- break;
3589
- }
3590
- await waitForPageReady(page);
3591
- return { success: true };
3592
- }
3593
- case "scroll": {
3594
- return runWebScrollCompound(page, this.lastRefs, action);
3595
- }
3596
- case "waitFor": {
3597
- const timeout = action.timeoutMs;
3598
- try {
3599
- switch (action.on) {
3600
- case "text":
3601
- await page.getByText(action.text).first().waitFor({ state: "visible", timeout });
3602
- break;
3603
- case "gone":
3604
- await page.getByText(action.text).first().waitFor({ state: "hidden", timeout });
3605
- break;
3606
- case "selector":
3607
- await page.waitForSelector(action.selector, { timeout });
3608
- break;
3609
- }
3610
- } catch {
3611
- const what = action.on === "selector" ? `selector "${action.selector}"` : `text "${action.text}"`;
3612
- const how = action.on === "gone" ? "was still present after" : "did not appear within";
3613
- return { success: false, error: `waitFor: ${what} ${how} ${timeout}ms.` };
3614
- }
3615
- return { success: true };
3616
- }
3617
- case "delay": {
3618
- await page.waitForTimeout(action.ms);
3619
- return { success: true };
3620
- }
3621
- case "waitForUrl": {
3622
- try {
3623
- await page.waitForURL(action.pattern, { timeout: 3e4 });
3624
- return { success: true };
3625
- } catch (err) {
3626
- return {
3627
- success: false,
3628
- error: `URL pattern "${action.pattern}" did not match within 30s. Hint: use "**" wildcards for substring matching (e.g., "**/dashboard**"). ${err instanceof Error ? err.message : String(err)}`
3629
- };
3630
- }
3631
- }
3632
- case "keypress": {
3633
- const effectiveKeys = (0, import_agent_qa_runner9.convertKeysForPlatform)(action.keys, {
3634
- enabled: action.convertPlatformKeys !== false,
3635
- isMac: (0, import_agent_qa_runner9.isMacPlatform)()
3636
- });
3637
- for (const key of effectiveKeys) {
3638
- if (action.mode === "down") {
3639
- await page.keyboard.down(key);
3640
- } else if (action.mode === "up") {
3641
- await page.keyboard.up(key);
3642
- } else {
3643
- await page.keyboard.press(key);
3644
- }
3645
- }
3646
- return { success: true };
3647
- }
3648
- case "hover": {
3649
- const locator = resolver.resolve(action.ref);
3650
- const box = await resolver.getBoundingBox(action.ref);
3651
- await locator.hover();
3652
- return { success: true, boundingBox: box ?? void 0 };
3653
- }
3654
- case "paste": {
3655
- const locator = resolver.resolve(action.ref);
3656
- const box = await resolver.getBoundingBox(action.ref);
3657
- await locator.focus();
3658
- await page.evaluate(`(() => {
3659
- const el = document.activeElement;
3660
- if (!el) return;
3661
- const dt = new DataTransfer();
3662
- dt.setData('text/plain', ${JSON.stringify(action.value)});
3663
- el.dispatchEvent(new ClipboardEvent('paste', {
3664
- clipboardData: dt,
3665
- bubbles: true,
3666
- cancelable: true,
3667
- }));
3668
- })()`);
3669
- return { success: true, boundingBox: box ?? void 0 };
3670
- }
3671
- case "fileUpload": {
3672
- const { existsSync } = await import("fs");
3673
- for (const filePath of action.files) {
3674
- if (!existsSync(filePath)) {
3675
- return { success: false, error: `File not found: ${filePath}` };
3676
- }
3677
- }
3678
- const locator = resolver.resolve(action.ref);
3679
- const box = await resolver.getBoundingBox(action.ref);
3680
- await locator.setInputFiles(action.files);
3681
- return { success: true, boundingBox: box ?? void 0 };
3682
- }
3683
- case "copy": {
3684
- const locator = resolver.resolve(action.ref);
3685
- const box = await resolver.getBoundingBox(action.ref);
3686
- const text = await locator.textContent() ?? "";
3687
- await page.evaluate(`navigator.clipboard.writeText(${JSON.stringify(text)})`);
3688
- return { success: true, boundingBox: box ?? void 0, data: { copied: text } };
3689
- }
3690
- case "tab": {
3691
- if (action.action === "new") {
3692
- const newPage = await this.context.newPage();
3693
- this.attachLogListeners(newPage);
3694
- await newPage.goto(action.url, { waitUntil: "domcontentloaded" });
3695
- await waitForPageReady(newPage);
3696
- this.page = newPage;
3697
- return { success: true };
3698
- }
3699
- const pages = this.context.pages();
3700
- let target;
3701
- if (action.index !== void 0) {
3702
- if (action.index < 0 || action.index >= pages.length) {
3703
- return { success: false, error: `Tab index ${action.index} out of range (${pages.length} tabs open)` };
3704
- }
3705
- target = pages[action.index];
3706
- } else if (action.title !== void 0) {
3707
- for (const p of pages) {
3708
- const t = await p.title();
3709
- if (t.includes(action.title)) {
3710
- target = p;
3711
- break;
3712
- }
3713
- }
3714
- } else if (action.url !== void 0) {
3715
- target = pages.find((p) => p.url().includes(action.url));
3716
- }
3717
- if (!target) {
3718
- return { success: false, error: `No tab found matching ${JSON.stringify({ index: action.index, title: action.title, url: action.url })}` };
3719
- }
3720
- this.page = target;
3721
- return { success: true };
3722
- }
3723
- case "inspect": {
3724
- switch (action.what) {
3725
- case "console": {
3726
- let entries;
3727
- if (action.tab) {
3728
- const targetPage = await this.resolveTabTargetAsync(action.tab);
3729
- if (!targetPage) {
3730
- return { success: false, error: `No tab found matching ${JSON.stringify(action.tab)}` };
3731
- }
3732
- const buffers = this.pageLogBuffers.get(targetPage);
3733
- entries = buffers ? [...buffers.consoleLogs] : [];
3734
- } else {
3735
- entries = [];
3736
- for (const buffers of this.pageLogBuffers.values()) {
3737
- entries.push(...buffers.consoleLogs);
3738
- }
3739
- }
3740
- if (action.level) {
3741
- entries = entries.filter((e) => e.level === action.level);
3742
- }
3743
- return { success: true, data: entries };
3744
- }
3745
- case "network": {
3746
- let entries;
3747
- if (action.tab) {
3748
- const targetPage = await this.resolveTabTargetAsync(action.tab);
3749
- if (!targetPage) {
3750
- return { success: false, error: `No tab found matching ${JSON.stringify(action.tab)}` };
3751
- }
3752
- const buffers = this.pageLogBuffers.get(targetPage);
3753
- entries = buffers ? [...buffers.networkLogs] : [];
3754
- } else {
3755
- entries = [];
3756
- for (const buffers of this.pageLogBuffers.values()) {
3757
- entries.push(...buffers.networkLogs);
3758
- }
3759
- }
3760
- if (action.urlPattern) {
3761
- entries = entries.filter((e) => e.url.includes(action.urlPattern));
3762
- }
3763
- return { success: true, data: entries };
3764
- }
3765
- case "cookies": {
3766
- const cookies = await this.context.cookies();
3767
- if (action.name) {
3768
- const cookie = cookies.find((c) => c.name === action.name) ?? null;
3769
- return { success: true, data: cookie };
3770
- }
3771
- return { success: true, data: cookies };
3772
- }
3773
- case "localStorage": {
3774
- const pageUrl = page.url();
3775
- if (pageUrl === "about:blank") {
3776
- return { success: true, data: action.key ? null : {} };
3777
- }
3778
- if (action.key) {
3779
- const value = await page.evaluate(
3780
- (k) => localStorage.getItem(k),
3781
- action.key
3782
- );
3783
- return { success: true, data: value };
3784
- }
3785
- const all = await page.evaluate(
3786
- () => Object.fromEntries(
3787
- Object.keys(localStorage).map((k) => [k, localStorage.getItem(k)])
3788
- )
3789
- );
3790
- return { success: true, data: all };
3791
- }
3792
- }
3793
- return { success: true };
3794
- }
3795
- case "setCookies": {
3796
- const cookiesWithDefaults = action.cookies.map((c) => ({
3797
- ...c,
3798
- url: !c.domain ? page.url() : void 0
3799
- }));
3800
- await this.context.addCookies(cookiesWithDefaults);
3801
- return { success: true };
3802
- }
3803
- case "setLocalStorage": {
3804
- await page.evaluate((entries) => {
3805
- for (const { key, value } of entries) {
3806
- localStorage.setItem(key, value);
3807
- }
3808
- }, action.entries);
3809
- return { success: true };
3810
- }
3811
- case "focus": {
3812
- const locator = resolver.resolve(action.ref);
3813
- const box = await resolver.getBoundingBox(action.ref);
3814
- await locator.focus();
3815
- return { success: true, boundingBox: box ?? void 0 };
3816
- }
3817
- case "assertValue": {
3818
- let value;
3819
- if ("expr" in action) {
3820
- value = await page.evaluate(action.expr);
3821
- } else {
3822
- switch (action.source) {
3823
- case "url":
3824
- value = page.url();
3825
- break;
3826
- case "storage":
3827
- value = page.url() === "about:blank" ? {} : await page.evaluate(
3828
- () => Object.fromEntries(
3829
- Object.keys(localStorage).map((k) => [k, localStorage.getItem(k)])
3830
- )
3831
- );
3832
- break;
3833
- case "network":
3834
- value = this.peekNetworkLogs();
3835
- break;
3836
- case "expr-result":
3837
- value = void 0;
3838
- break;
3839
- }
3840
- }
3841
- return { success: true, data: value };
3842
- }
3843
- case "clearText": {
3844
- const loc = resolver.resolve(action.ref);
3845
- const clearBox = await resolver.getBoundingBox(action.ref);
3846
- await loc.clear();
3847
- return { success: true, boundingBox: clearBox ?? void 0 };
3848
- }
3849
- case "doubleClick": {
3850
- const locator = resolver.resolve(action.ref);
3851
- const box = await resolver.getBoundingBox(action.ref);
3852
- const coordinates = box ? action.relativePosition ? { x: box.x + action.relativePosition.x, y: box.y + action.relativePosition.y } : { x: box.x + box.width / 2, y: box.y + box.height / 2 } : void 0;
3853
- await locator.dblclick({
3854
- ...action.relativePosition && { position: action.relativePosition },
3855
- ...clickDelayOption2(action.clickDelay)
3856
- });
3857
- return { success: true, coordinates, boundingBox: box ?? void 0 };
3858
- }
3859
- case "rightClick": {
3860
- const locator = resolver.resolve(action.ref);
3861
- const box = await resolver.getBoundingBox(action.ref);
3862
- const coordinates = box ? action.relativePosition ? { x: box.x + action.relativePosition.x, y: box.y + action.relativePosition.y } : { x: box.x + box.width / 2, y: box.y + box.height / 2 } : void 0;
3863
- await locator.click({
3864
- button: "right",
3865
- ...action.relativePosition && { position: action.relativePosition },
3866
- ...clickDelayOption2(action.clickDelay)
3867
- });
3868
- return { success: true, coordinates, boundingBox: box ?? void 0 };
3869
- }
3870
- case "route": {
3871
- await this.context.route(action.urlPattern, async (route) => {
3872
- await route.fulfill({
3873
- status: 200,
3874
- ...action.contentType !== void 0 ? { contentType: action.contentType } : {},
3875
- ...action.headers !== void 0 ? { headers: action.headers } : {},
3876
- ...action.body !== void 0 ? { body: action.body } : {}
3877
- });
3878
- });
3879
- this.armedRoutes.add(action.urlPattern);
3880
- return { success: true };
3881
- }
3882
- case "unroute": {
3883
- if (action.urlPattern !== void 0) {
3884
- await this.context.unroute(action.urlPattern);
3885
- this.armedRoutes.delete(action.urlPattern);
3886
- } else {
3887
- await this.context.unrouteAll({ behavior: "ignoreErrors" });
3888
- this.armedRoutes.clear();
3889
- }
3890
- return { success: true };
3891
- }
3892
- case "networkState": {
3893
- if (action.mode === "offline") {
3894
- await this.context.setOffline(true);
3895
- return { success: true };
3896
- }
3897
- try {
3898
- const cdp = await this.context.newCDPSession(page);
3899
- await cdp.send("Network.emulateNetworkConditions", {
3900
- offline: false,
3901
- latency: action.latencyMs,
3902
- downloadThroughput: action.downloadKbps * 1e3 / 8,
3903
- uploadThroughput: action.uploadKbps * 1e3 / 8
3904
- });
3905
- return { success: true };
3906
- } catch (err) {
3907
- return {
3908
- success: false,
3909
- error: `Network throttling is only available on Chromium (via CDP); this context does not support it. ${err instanceof Error ? err.message : String(err)}`
3910
- };
3911
- }
3912
- }
3913
- case "storageState": {
3914
- if (action.op === "save") {
3915
- const state = await this.context.storageState();
3916
- return { success: true, data: { name: action.name, state } };
3917
- }
3918
- await this.context.setStorageState(readSeamStorageState2(action));
3919
- return { success: true };
3920
- }
3921
- case "handleDialog": {
3922
- if (this.pendingDialogArm) {
3923
- page.off("dialog", this.pendingDialogArm);
3924
- this.pendingDialogArm = null;
3925
- }
3926
- const accept = action.accept;
3927
- const promptText = action.promptText;
3928
- const handler = (dialog) => {
3929
- this.pendingDialogArm = null;
3930
- if (accept) {
3931
- void dialog.accept(promptText);
3932
- } else {
3933
- void dialog.dismiss();
3934
- }
3935
- };
3936
- this.pendingDialogArm = handler;
3937
- page.once("dialog", handler);
3938
- return { success: true };
3939
- }
3940
- case "executeScript": {
3941
- return { success: false, error: "executeScript not supported on farm adapter" };
3942
- }
3943
- // Runner-minted / author-channel verbs (D-01/D-15/D-16): reportValue, uniqueValue, assertAI,
3944
- // extractAI, and setVariable are intercepted at the dispatch seam and never reach the adapter.
3945
- // A grouped §7.1 defensive case surfaces a dispatch-routing error if one arrives here;
3946
- // assertNever remains the backstop for a genuinely unknown schema verb.
2989
+ // The dispatch seam intercepts these verbs; reaching the adapter is a routing error.
3947
2990
  case "reportValue":
3948
2991
  case "uniqueValue":
3949
2992
  case "assertAI":
@@ -3954,7 +2997,7 @@ var FarmWebAdapter = class {
3954
2997
  error: `Action "${action.type}" is handled by the runner dispatch seam and must not reach the web adapter (runner-minted/author-channel verb). This indicates a dispatch routing error.`
3955
2998
  };
3956
2999
  default:
3957
- return (0, import_agent_qa_runner8.assertNever)(action);
3000
+ return (0, import_agent_qa_runner5.assertNever)(action);
3958
3001
  }
3959
3002
  }
3960
3003
  };
@@ -3996,10 +3039,7 @@ async function runAccessibilityCheck(page, options) {
3996
3039
  0 && (module.exports = {
3997
3040
  ActionValidationError,
3998
3041
  ElementResolver,
3999
- FarmWebAdapter,
4000
3042
  WebPlatformAdapter,
4001
- extractDom,
4002
- observePage,
4003
3043
  parseAriaSnapshot,
4004
3044
  runAccessibilityCheck,
4005
3045
  validateAction,