@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.js CHANGED
@@ -1,88 +1,13 @@
1
1
  // src/adapter.ts
2
+ import { DEFAULT_NAVIGATION_TIMEOUT_MS } from "@vostride/agent-qa-schema";
2
3
  import {
3
4
  assertNever,
4
- buildScreenState as buildScreenState3,
5
+ buildScreenState as buildScreenState2,
5
6
  createSnapshotLiveStream,
6
7
  LIVE_KEEPALIVE_QUIET_MS
7
8
  } from "@vostride/agent-qa-runner";
8
9
  import { convertKeysForPlatform, isMacPlatform } from "@vostride/agent-qa-runner";
9
10
 
10
- // src/observer.ts
11
- import { buildScreenState } from "@vostride/agent-qa-runner";
12
-
13
- // src/dom-extractor.ts
14
- var SKIP_TAGS = /* @__PURE__ */ new Set([
15
- "SCRIPT",
16
- "STYLE",
17
- "SVG",
18
- "NOSCRIPT",
19
- "LINK",
20
- "META",
21
- "TEMPLATE",
22
- "IFRAME"
23
- ]);
24
- function extractDomTree(opts) {
25
- const { maxDepth, maxTextLen } = opts;
26
- const doc = globalThis.document;
27
- const win = globalThis.window;
28
- const lines = [];
29
- function isHidden(el) {
30
- if (!el.offsetParent && el.tagName !== "BODY" && el.tagName !== "HTML") {
31
- const style = win.getComputedStyle(el);
32
- if (style.display === "none" || style.visibility === "hidden") return true;
33
- }
34
- return false;
35
- }
36
- function walk(node, depth, indent) {
37
- if (depth > maxDepth) {
38
- lines.push(indent + "[... deeper content truncated]");
39
- return;
40
- }
41
- for (let child = node.firstChild; child; child = child.nextSibling) {
42
- if (child.nodeType === 3) {
43
- const text = child.textContent.trim();
44
- if (text) {
45
- const truncated = text.length > maxTextLen ? text.slice(0, maxTextLen) + "..." : text;
46
- lines.push(indent + truncated);
47
- }
48
- continue;
49
- }
50
- if (child.nodeType !== 1) continue;
51
- const tag = child.tagName;
52
- if (SKIP_TAGS.has(tag)) continue;
53
- if (isHidden(child)) continue;
54
- const isCustom = tag.toLowerCase().includes("-");
55
- const annotation = child.getAttribute("aria-label") || child.getAttribute("role") || "";
56
- let label = tag.toLowerCase();
57
- if (isCustom) {
58
- if (!child.shadowRoot && !child.firstChild) {
59
- label += " (custom element, closed shadow root)";
60
- } else {
61
- label += " (custom element)";
62
- }
63
- }
64
- if (annotation) label += ` [${annotation}]`;
65
- lines.push(indent + label);
66
- if (child.shadowRoot) {
67
- lines.push(indent + " #shadow-root");
68
- walk(child.shadowRoot, depth + 1, indent + " ");
69
- }
70
- walk(child, depth + 1, indent + " ");
71
- }
72
- }
73
- walk(doc.body, 0, "");
74
- return lines.join("\n");
75
- }
76
- async function extractDom(page, opts) {
77
- const maxDepth = opts?.maxDepth ?? 20;
78
- const maxTextLen = opts?.maxTextLen ?? 200;
79
- try {
80
- return await page.evaluate(extractDomTree, { maxDepth, maxTextLen });
81
- } catch {
82
- return void 0;
83
- }
84
- }
85
-
86
11
  // src/observer.ts
87
12
  var INTERACTIVE_ROLES = /* @__PURE__ */ new Set([
88
13
  "button",
@@ -203,20 +128,6 @@ function parseAriaSnapshot(ariaTree) {
203
128
  }
204
129
  return { nodes, nativeRefs };
205
130
  }
206
- function hideAgentQaElements() {
207
- const doc = globalThis.document;
208
- doc.querySelectorAll("[data-agent-qa-internal]").forEach((el) => {
209
- el.style.display = "none";
210
- el.setAttribute("aria-hidden", "true");
211
- });
212
- }
213
- function restoreAgentQaElements() {
214
- const doc = globalThis.document;
215
- doc.querySelectorAll("[data-agent-qa-internal]").forEach((el) => {
216
- el.style.display = "";
217
- el.removeAttribute("aria-hidden");
218
- });
219
- }
220
131
  async function captureWebScroll(page) {
221
132
  const fallbackViewport = page.viewportSize() ?? { width: 0, height: 0 };
222
133
  try {
@@ -343,41 +254,6 @@ function coerceBounds(value) {
343
254
  function coerceString(value) {
344
255
  return typeof value === "string" && value.length > 0 ? value : void 0;
345
256
  }
346
- async function observePage(page, options) {
347
- const locator = options?.filterSelector ? page.locator(options.filterSelector) : page.locator(":root");
348
- await page.evaluate(hideAgentQaElements);
349
- let ariaTree;
350
- try {
351
- ariaTree = await locator.ariaSnapshot();
352
- } finally {
353
- await page.evaluate(restoreAgentQaElements);
354
- }
355
- const timestamp = Date.now();
356
- const url = page.url();
357
- const { viewport: capturedViewport, scroll } = await captureWebScroll(page);
358
- let domContext;
359
- if (options?.extractDom) {
360
- try {
361
- domContext = await extractDom(page);
362
- } catch {
363
- }
364
- }
365
- const { nodes, nativeRefs } = ariaTree ? parseAriaSnapshot(ariaTree) : { nodes: [], nativeRefs: [] };
366
- if (nodes.length > 0) {
367
- await enrichWebNodes(page, nodes, nativeRefs);
368
- }
369
- const { state } = buildScreenState({
370
- platform: "web",
371
- nodes,
372
- viewport: capturedViewport,
373
- scroll,
374
- coordSpace: "viewport",
375
- timestamp,
376
- url,
377
- ...domContext ? { domContext } : {}
378
- });
379
- return state;
380
- }
381
257
  function numberOr(value, fallback) {
382
258
  return typeof value === "number" && Number.isFinite(value) ? value : fallback;
383
259
  }
@@ -432,7 +308,7 @@ var ElementResolver = class {
432
308
  };
433
309
 
434
310
  // src/binding.ts
435
- import { buildScreenState as buildScreenState2, rowTextOf as rowTextOf2 } from "@vostride/agent-qa-runner";
311
+ import { buildScreenState, rowTextOf as rowTextOf2 } from "@vostride/agent-qa-runner";
436
312
  var BINDING_LANES = ["ref", "fromRef", "toRef"];
437
313
  var FRAME_REF = /^f\d+e/;
438
314
  var INVALID_FRAME = /invalid frame/i;
@@ -442,14 +318,14 @@ var WHAT_CHANGED = {
442
318
  "binding-lost": "the observed element is no longer on the page",
443
319
  "binding-changed": "the observed element now has a different role, name or row"
444
320
  };
445
- function hideAgentQaElements2() {
321
+ function hideAgentQaElements() {
446
322
  const doc = globalThis.document;
447
323
  doc.querySelectorAll("[data-agent-qa-internal]").forEach((el) => {
448
324
  el.style.display = "none";
449
325
  el.setAttribute("aria-hidden", "true");
450
326
  });
451
327
  }
452
- function restoreAgentQaElements2() {
328
+ function restoreAgentQaElements() {
453
329
  const doc = globalThis.document;
454
330
  doc.querySelectorAll("[data-agent-qa-internal]").forEach((el) => {
455
331
  el.style.display = "";
@@ -458,7 +334,7 @@ function restoreAgentQaElements2() {
458
334
  }
459
335
  async function snapshotRoot(page, options = {}) {
460
336
  const { timeoutMs, retryTimeoutMs } = options;
461
- await page.evaluate(hideAgentQaElements2);
337
+ await page.evaluate(hideAgentQaElements);
462
338
  try {
463
339
  if (timeoutMs === void 0) return await page.locator(":root").ariaSnapshot({ mode: "ai" });
464
340
  return await page.locator(":root").ariaSnapshot({ mode: "ai", timeout: timeoutMs }).catch((error) => {
@@ -469,7 +345,7 @@ async function snapshotRoot(page, options = {}) {
469
345
  });
470
346
  });
471
347
  } finally {
472
- await page.evaluate(restoreAgentQaElements2);
348
+ await page.evaluate(restoreAgentQaElements);
473
349
  }
474
350
  }
475
351
  async function captureEpoch(page) {
@@ -515,7 +391,7 @@ async function checkBinding(page, epoch, entry, snapshot) {
515
391
  const detached = await frameDetached(page, entry.nativeRef);
516
392
  return { outcome: detached ? "frame-detached" : "binding-lost" };
517
393
  }
518
- const { state } = buildScreenState2({
394
+ const { state } = buildScreenState({
519
395
  platform: "web",
520
396
  nodes,
521
397
  viewport: { width: 0, height: 0 },
@@ -1319,7 +1195,6 @@ function createScreencastRecording(getPage, config, getPageRevision) {
1319
1195
  const before = scene;
1320
1196
  const epoch = firstFrameEpoch;
1321
1197
  const revision = sceneRevision;
1322
- if (!getPageRevision) return void 0;
1323
1198
  const pageRevision = getPageRevision();
1324
1199
  if (!recording || !before || epoch === void 0 || getPage() !== recording.page) return void 0;
1325
1200
  const viewport = recording.page.viewportSize();
@@ -1450,8 +1325,7 @@ var CLIPBOARD_HOOK = `(() => {
1450
1325
  if (typeof send !== 'function' || typeof text !== 'string' || text.length === 0) return;
1451
1326
  try { Promise.resolve(send({ via, text })).catch(() => {}); } catch {}
1452
1327
  };
1453
- // 2026-10-08 (436-14): a write is reported only once the browser accepted it; the app still
1454
- // gets the original promise, rejections included.
1328
+ // A write is reported only once the browser accepted it; the app gets the original promise.
1455
1329
  const proto = typeof Clipboard === 'function' ? Clipboard.prototype : undefined;
1456
1330
  if (proto && typeof proto.writeText === 'function') {
1457
1331
  const writeText = proto.writeText;
@@ -1532,7 +1406,7 @@ var OperationCapture = class {
1532
1406
  downloads = [];
1533
1407
  /** Downloads that started while no start window was open, kept for the next action. */
1534
1408
  held = [];
1535
- /** Completed downloads' bytes by sha256, oldest first; memory only (439 D-04). */
1409
+ /** Completed downloads' bytes by sha256, oldest first; memory only. */
1536
1410
  heldBytes = /* @__PURE__ */ new Map();
1537
1411
  heldByteTotal = 0;
1538
1412
  pendingNavigations = /* @__PURE__ */ new Map();
@@ -2041,41 +1915,34 @@ var WebPlatformAdapter = class {
2041
1915
  browser = null;
2042
1916
  context = null;
2043
1917
  page = null;
2044
- // The native binding of the last observation and its document (438 D-06). Neither crosses the
2045
- // port (observe returns only the ScreenState).
1918
+ // Adapter-private: observe returns only the `ScreenState`, never the native binding.
2046
1919
  lastRefs = {};
2047
1920
  epoch;
2048
1921
  pageLogBuffers = /* @__PURE__ */ new Map();
2049
1922
  logCaptureConfig = { console: true, network: true };
1923
+ navigationTimeoutMs = DEFAULT_NAVIGATION_TIMEOUT_MS;
2050
1924
  disconnected = false;
2051
1925
  intentionalClose = false;
2052
- // Persistent route interception + one-shot dialog arms live until an explicit unroute or
2053
- // test teardown (D-12/D-18); cleanup() drains both so nothing bleeds across tests (Pitfall 7).
1926
+ // Routes persist until unroute and a dialog arm until its dialog; `cleanup()` drains both.
2054
1927
  armedRoutes = /* @__PURE__ */ new Set();
2055
1928
  pendingDialogArm = null;
2056
- // 403 D-05: the live screencast's CDP session, owned here and never handed out.
2057
1929
  browserName = "chromium";
2058
1930
  screencast = null;
2059
1931
  screencastCapability = null;
2060
1932
  recordingCapability;
2061
1933
  recordingPageRevision = 0;
2062
1934
  recordingConfig;
2063
- // 403-02: the PRODUCT's frame identity, and the CDP ack token it maps to. See `liveStream`.
2064
1935
  screencastFrameSeq = 0;
2065
1936
  screencastAckTokens = /* @__PURE__ */ new Map();
2066
- // 428 D-15: the running screencast's consumer, its quiet-page keepalive, and the size a keepalive
2067
- // frame reports (the last screencast frame's device size).
2068
1937
  screencastOnFrame = null;
2069
1938
  keepaliveTimer = null;
2070
1939
  lastScreencastSize = null;
2071
1940
  keepaliveEchoDeadline = null;
2072
1941
  screencastChain = Promise.resolve();
2073
- // 428 D-12: the labelled snapshot preview standing in for a screencast withdrawn mid-stream.
2074
1942
  withdrawnSnapshot = null;
2075
1943
  screencastRecoveryTimer = null;
2076
- // 436 D-19, D-21: download and app-clipboard capture, Chromium only; armed around each execute().
2077
1944
  operationCapture = null;
2078
- // 436 D-23: Firefox and WebKit run no capture and name themselves as the reason.
1945
+ // Firefox and WebKit run no capture and name themselves as the reason.
2079
1946
  operationCaptureSupport;
2080
1947
  async setup(config) {
2081
1948
  this.recordingConfig = config.recording;
@@ -2111,6 +1978,8 @@ var WebPlatformAdapter = class {
2111
1978
  contextOptions.storageState = config.authState.storageStatePath;
2112
1979
  }
2113
1980
  this.context = await this.browser.newContext(contextOptions);
1981
+ this.navigationTimeoutMs = config.timeouts?.navigation ?? DEFAULT_NAVIGATION_TIMEOUT_MS;
1982
+ this.context.setDefaultNavigationTimeout(this.navigationTimeoutMs);
2114
1983
  if (browserName === "chromium") {
2115
1984
  await this.context.grantPermissions(["clipboard-write"]);
2116
1985
  }
@@ -2128,22 +1997,7 @@ var WebPlatformAdapter = class {
2128
1997
  this.operationCaptureSupport = { download: false, clipboard: false, reason: browserName };
2129
1998
  }
2130
1999
  }
2131
- /**
2132
- * 403 D-05/LIVE-01: the Chromium screencast producer, or ABSENT when this adapter cannot stream.
2133
- *
2134
- * Absence means "no continuous stream", not "no preview": since 428-05 the composition's
2135
- * `liveStreamFor` wraps this adapter's `screenshot()` in the labelled 1 s snapshot producer, both
2136
- * as the fallback behind a screencast that fails to start and as the whole preview on a
2137
- * non-Chromium target. The two gates here (Chromium only, and a page to stream) stay exactly as
2138
- * they are, so the absence stays honest.
2139
- *
2140
- * SCOPE (428 D-18, closes WIN #273): the screencast shows the page the test is acting on. Both
2141
- * page-switch sites (`tab new`, `tab switch`) go through `setActivePage`, which re-binds the
2142
- * running screencast to the new page INSIDE this same capability object: no stop, no re-register,
2143
- * no stream end, so a subscriber only sees the picture change. Start, stop and re-bind run on one
2144
- * serialised chain. A popup or a closing tab never changes `this.page` in this adapter, so the
2145
- * preview already shows the acting page in those cases.
2146
- */
2000
+ // Absence means no stream, not no preview: the runner's `liveStreamFor` wraps `screenshot()`.
2147
2001
  get liveStream() {
2148
2002
  if (this.browserName !== "chromium") return void 0;
2149
2003
  if (!this.page || !this.context) return void 0;
@@ -2193,7 +2047,7 @@ var WebPlatformAdapter = class {
2193
2047
  this.screencastCapability = capability;
2194
2048
  return capability;
2195
2049
  }
2196
- /** Run `step` after every earlier start, stop and re-bind has settled (428 D-18). */
2050
+ /** Run `step` after every earlier start, stop and re-bind has settled. */
2197
2051
  onScreencastChain(step) {
2198
2052
  const run = this.screencastChain.then(step);
2199
2053
  this.screencastChain = run.catch(() => void 0);
@@ -2246,7 +2100,7 @@ var WebPlatformAdapter = class {
2246
2100
  metadata: {
2247
2101
  width: metadata.deviceWidth,
2248
2102
  height: metadata.deviceHeight,
2249
- // CDP's `TimeSinceEpoch` is SECONDS; every other layer reads epoch ms (428 D-15).
2103
+ // CDP's `TimeSinceEpoch` is in seconds; every other layer uses epoch ms.
2250
2104
  ...metadata.timestamp === void 0 ? {} : { timestamp: Math.round(metadata.timestamp * 1e3) },
2251
2105
  scrollOffsetX: metadata.scrollOffsetX,
2252
2106
  scrollOffsetY: metadata.scrollOffsetY
@@ -2254,11 +2108,7 @@ var WebPlatformAdapter = class {
2254
2108
  });
2255
2109
  });
2256
2110
  }
2257
- /**
2258
- * The one place the page the test acts on changes (428 D-18). Recording keeps following its
2259
- * starting page through the revision counter (424 reconciliation 25(d)); the live screencast,
2260
- * when running, re-binds to the new page on the serialised chain.
2261
- */
2111
+ // Every page switch goes through here so a running screencast re-binds on its chain.
2262
2112
  setActivePage(page) {
2263
2113
  this.page = page;
2264
2114
  this.recordingPageRevision++;
@@ -2304,14 +2154,7 @@ var WebPlatformAdapter = class {
2304
2154
  this.armKeepalive(cdp);
2305
2155
  await this.stopWithdrawnSnapshot();
2306
2156
  }
2307
- /**
2308
- * 428 D-12's second half: the screencast was withdrawn while the browser is still up (its CDP
2309
- * session detached or unusable), which only this capability can see. Detected when a keepalive
2310
- * capture or a re-bind's start rejects on the active session; the capability then serves the
2311
- * labelled 1 s snapshot preview of the current page through the same `onFrame`, until `stop()`,
2312
- * `cleanup()` or a successful re-bind. The declared kind stays `cdp-screencast`; the frames carry
2313
- * `kind: 'snapshot'`, which is what labels the preview. Runs on the serialised chain.
2314
- */
2157
+ // Frames carry `kind: 'snapshot'` until a re-bind; the declared kind stays `cdp-screencast`.
2315
2158
  handleScreencastWithdrawn(cdp) {
2316
2159
  void this.onScreencastChain(async () => {
2317
2160
  if (this.screencast !== cdp) return;
@@ -2327,7 +2170,6 @@ var WebPlatformAdapter = class {
2327
2170
  this.armScreencastRecovery();
2328
2171
  });
2329
2172
  }
2330
- /** While the snapshot fallback runs, try the screencast again every SCREENCAST_RECOVERY_MS (428 WR-08). */
2331
2173
  armScreencastRecovery() {
2332
2174
  this.clearScreencastRecovery();
2333
2175
  const timer = setTimeout(() => {
@@ -2370,14 +2212,7 @@ var WebPlatformAdapter = class {
2370
2212
  timer.unref?.();
2371
2213
  this.keepaliveTimer = timer;
2372
2214
  }
2373
- /**
2374
- * 428 D-15: a fresh capture on the same CDP session, emitted as a frame with no ack token (the ack
2375
- * path drops ids it never minted a token for). A capture that rejects emits nothing. While the
2376
- * capability is started under a live browser, a session that then fails a trivial probe too is a
2377
- * withdrawn screencast (D-12, see `handleScreencastWithdrawn`); one that still answers only had a
2378
- * bad moment, such as a cross-process navigation, and re-arms (428 WR-08). One that never settles
2379
- * emits nothing and arms nothing, which is the honest amber.
2380
- */
2215
+ // A rejected capture means a withdrawn screencast only if a trivial probe fails too.
2381
2216
  async fireKeepalive(cdp) {
2382
2217
  this.keepaliveTimer = null;
2383
2218
  if (this.screencast !== cdp) return;
@@ -2485,7 +2320,7 @@ var WebPlatformAdapter = class {
2485
2320
  get isBrowserDisconnected() {
2486
2321
  return this.disconnected;
2487
2322
  }
2488
- async observe(options) {
2323
+ async observe() {
2489
2324
  if (!this.page) {
2490
2325
  throw new Error("WebPlatformAdapter not initialized \u2014 call setup() first");
2491
2326
  }
@@ -2493,7 +2328,7 @@ var WebPlatformAdapter = class {
2493
2328
  await waitForPageReady(page);
2494
2329
  const epoch = await captureEpoch(page);
2495
2330
  try {
2496
- const state = await this.observeDocument(page, options);
2331
+ const state = await this.observeDocument(page);
2497
2332
  await disposeEpoch(this.epoch);
2498
2333
  this.epoch = epoch;
2499
2334
  return state;
@@ -2502,29 +2337,21 @@ var WebPlatformAdapter = class {
2502
2337
  throw error;
2503
2338
  }
2504
2339
  }
2505
- async observeDocument(page, options) {
2340
+ async observeDocument(page) {
2506
2341
  const ariaTree = await snapshotRoot(page, OBSERVE_SNAPSHOT);
2507
2342
  const { nodes, nativeRefs } = parseAriaSnapshot(ariaTree);
2508
2343
  if (nodes.length > 0) {
2509
2344
  await enrichWebNodes(page, nodes, nativeRefs);
2510
2345
  }
2511
2346
  const { viewport, scroll } = await captureWebScroll(page);
2512
- let domContext;
2513
- if (options?.extractDom) {
2514
- try {
2515
- domContext = await extractDom(page);
2516
- } catch {
2517
- }
2518
- }
2519
- const { state, binding } = buildScreenState3({
2347
+ const { state, binding } = buildScreenState2({
2520
2348
  platform: "web",
2521
2349
  nodes,
2522
2350
  viewport,
2523
2351
  scroll,
2524
2352
  coordSpace: "viewport",
2525
2353
  timestamp: Date.now(),
2526
- url: page.url(),
2527
- ...domContext !== void 0 ? { domContext } : {}
2354
+ url: page.url()
2528
2355
  });
2529
2356
  this.lastRefs = buildRefMap(binding, nodes, nativeRefs, state.elements);
2530
2357
  return state;
@@ -2545,6 +2372,15 @@ var WebPlatformAdapter = class {
2545
2372
  }
2546
2373
  return withOperationMetadata(result, captured);
2547
2374
  }
2375
+ // A navigation that timed out stays pending and blocks every later call on its page until stopped.
2376
+ async stopLoading(page) {
2377
+ try {
2378
+ const cdp = await this.context.newCDPSession(page);
2379
+ await cdp.send("Page.stopLoading");
2380
+ await cdp.detach();
2381
+ } catch {
2382
+ }
2383
+ }
2548
2384
  async executeWithErrorResult(action) {
2549
2385
  if (!this.page) {
2550
2386
  throw new Error("WebPlatformAdapter not initialized \u2014 call setup() first");
@@ -2607,7 +2443,6 @@ var WebPlatformAdapter = class {
2607
2443
  if (!urlPattern) return all;
2608
2444
  return all.filter((e) => e.url.includes(urlPattern));
2609
2445
  }
2610
- /** 439 D-04: a completed download's bytes held in memory, by its sha256. */
2611
2446
  readDownloadBytes(sha256) {
2612
2447
  return this.operationCapture?.readDownloadBytes(sha256);
2613
2448
  }
@@ -2630,8 +2465,9 @@ var WebPlatformAdapter = class {
2630
2465
  this.pageLogBuffers.set(page, buffers);
2631
2466
  if (this.logCaptureConfig.console) {
2632
2467
  page.on("console", (msg) => {
2468
+ const type = msg.type();
2633
2469
  buffers.consoleLogs.push({
2634
- level: msg.type(),
2470
+ level: type === "warning" ? "warn" : type,
2635
2471
  text: msg.text(),
2636
2472
  location: msg.location(),
2637
2473
  timestamp: Date.now()
@@ -2746,19 +2582,24 @@ var WebPlatformAdapter = class {
2746
2582
  return { success: true, boundingBox: box ?? void 0 };
2747
2583
  }
2748
2584
  case "navigate": {
2749
- switch (action.to) {
2750
- case "url":
2751
- await page.goto(action.url, { waitUntil: "domcontentloaded" });
2752
- break;
2753
- case "back":
2754
- await page.goBack({ waitUntil: "domcontentloaded" });
2755
- break;
2756
- case "forward":
2757
- await page.goForward({ waitUntil: "domcontentloaded" });
2758
- break;
2759
- case "reload":
2760
- await page.reload({ waitUntil: "domcontentloaded" });
2761
- break;
2585
+ try {
2586
+ switch (action.to) {
2587
+ case "url":
2588
+ await page.goto(action.url, { waitUntil: "domcontentloaded" });
2589
+ break;
2590
+ case "back":
2591
+ await page.goBack({ waitUntil: "domcontentloaded" });
2592
+ break;
2593
+ case "forward":
2594
+ await page.goForward({ waitUntil: "domcontentloaded" });
2595
+ break;
2596
+ case "reload":
2597
+ await page.reload({ waitUntil: "domcontentloaded" });
2598
+ break;
2599
+ }
2600
+ } catch (err) {
2601
+ await this.stopLoading(page);
2602
+ throw err;
2762
2603
  }
2763
2604
  await waitForPageReady(page);
2764
2605
  return { success: true };
@@ -2793,12 +2634,12 @@ var WebPlatformAdapter = class {
2793
2634
  }
2794
2635
  case "waitForUrl": {
2795
2636
  try {
2796
- await page.waitForURL(action.pattern, { timeout: 3e4 });
2637
+ await page.waitForURL(action.pattern, { timeout: this.navigationTimeoutMs });
2797
2638
  return { success: true };
2798
2639
  } catch (err) {
2799
2640
  return {
2800
2641
  success: false,
2801
- 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)}`
2642
+ 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)}`
2802
2643
  };
2803
2644
  }
2804
2645
  }
@@ -2864,7 +2705,12 @@ var WebPlatformAdapter = class {
2864
2705
  if (action.action === "new") {
2865
2706
  const newPage = await this.context.newPage();
2866
2707
  this.attachLogListeners(newPage);
2867
- await newPage.goto(action.url, { waitUntil: "domcontentloaded" });
2708
+ try {
2709
+ await newPage.goto(action.url, { waitUntil: "domcontentloaded" });
2710
+ } catch (err) {
2711
+ await this.stopLoading(newPage);
2712
+ throw err;
2713
+ }
2868
2714
  await waitForPageReady(newPage);
2869
2715
  this.setActivePage(newPage);
2870
2716
  return { success: true };
@@ -3006,9 +2852,6 @@ var WebPlatformAdapter = class {
3006
2852
  case "network":
3007
2853
  value = this.peekNetworkLogs();
3008
2854
  break;
3009
- case "expr-result":
3010
- value = void 0;
3011
- break;
3012
2855
  }
3013
2856
  }
3014
2857
  return { success: true, data: value };
@@ -3113,10 +2956,7 @@ var WebPlatformAdapter = class {
3113
2956
  case "executeScript": {
3114
2957
  return { success: false, error: "executeScript is a mobile-only action" };
3115
2958
  }
3116
- // Runner-minted / author-channel verbs (D-01/D-15/D-16): reportValue, uniqueValue, assertAI,
3117
- // extractAI, and setVariable are intercepted at the dispatch seam and never reach the adapter.
3118
- // A grouped §7.1 defensive case surfaces a dispatch-routing error if one arrives here;
3119
- // assertNever remains the backstop for a genuinely unknown schema verb.
2959
+ // The dispatch seam intercepts these verbs; reaching the adapter is a routing error.
3120
2960
  case "reportValue":
3121
2961
  case "uniqueValue":
3122
2962
  case "assertAI":
@@ -3132,800 +2972,6 @@ var WebPlatformAdapter = class {
3132
2972
  }
3133
2973
  };
3134
2974
 
3135
- // src/farm-adapter.ts
3136
- import { assertNever as assertNever2, buildScreenState as buildScreenState4 } from "@vostride/agent-qa-runner";
3137
- import { getProvider, registerAllProviders } from "@vostride/agent-qa-core";
3138
- import { convertKeysForPlatform as convertKeysForPlatform2, isMacPlatform as isMacPlatform2 } from "@vostride/agent-qa-runner";
3139
- function clickDelayOption2(clickDelay) {
3140
- return clickDelay !== void 0 && clickDelay > 0 ? { delay: clickDelay } : {};
3141
- }
3142
- function readSeamStorageState2(action) {
3143
- return action.state;
3144
- }
3145
- var FarmWebAdapter = class {
3146
- platform = "web";
3147
- browser = null;
3148
- context = null;
3149
- page = null;
3150
- // The native binding of the last observation and its document (438 D-06).
3151
- lastRefs = {};
3152
- epoch;
3153
- sessionUrl;
3154
- sessionId;
3155
- testFailed = false;
3156
- exitHandler = null;
3157
- credentials = null;
3158
- pageLogBuffers = /* @__PURE__ */ new Map();
3159
- logCaptureConfig = { console: true, network: true };
3160
- // Persistent route interception + one-shot dialog arms live until an explicit unroute or
3161
- // test teardown (D-12/D-18); cleanup() drains both so nothing bleeds across tests (Pitfall 7).
3162
- armedRoutes = /* @__PURE__ */ new Set();
3163
- pendingDialogArm = null;
3164
- recordingCapability;
3165
- recordingConfig;
3166
- async setup(config) {
3167
- this.recordingConfig = config.recording;
3168
- if (!config.farmSession) {
3169
- throw new Error("FarmWebAdapter requires config.farmSession to be set");
3170
- }
3171
- registerAllProviders();
3172
- const provider = getProvider("browserstack");
3173
- if (!provider) {
3174
- throw new Error("Unknown farm provider: browserstack");
3175
- }
3176
- this.credentials = provider.resolveCredentials(config.farmSession.capabilities);
3177
- if (!config.browser?.name) {
3178
- throw new Error("FarmWebAdapter requires config.browser.name to be set");
3179
- }
3180
- const credentials = this.credentials;
3181
- const farmWebConfig = {
3182
- browser: config.browser,
3183
- credentials,
3184
- testName: config.farmSession.capabilities?.testName
3185
- };
3186
- const { wsEndpoint } = await provider.resolveWebEndpoint(farmWebConfig);
3187
- const { chromium } = await import("playwright-core");
3188
- this.browser = await chromium.connect({ wsEndpoint });
3189
- const contextOptions = {
3190
- viewport: config.browser?.viewport ?? { width: 1280, height: 720 }
3191
- };
3192
- if (config.authState?.storageStatePath) {
3193
- contextOptions.storageState = config.authState.storageStatePath;
3194
- }
3195
- this.context = await this.browser.newContext(contextOptions);
3196
- await this.context.grantPermissions(["clipboard-write"]);
3197
- this.page = await this.context.newPage();
3198
- try {
3199
- const cdpSession = await this.page.context().newCDPSession(this.page);
3200
- const result = await cdpSession.send("Browser.getVersion");
3201
- await cdpSession.detach();
3202
- const auth = Buffer.from(`${credentials.username}:${credentials.accessKey}`).toString("base64");
3203
- const resp = await fetch("https://api.browserstack.com/automate/builds.json?limit=1&status=running", {
3204
- headers: { Authorization: `Basic ${auth}` }
3205
- });
3206
- if (resp.ok) {
3207
- const builds = await resp.json();
3208
- if (builds.length > 0) {
3209
- const buildId = builds[0].automation_build.hashed_id;
3210
- const sessResp = await fetch(`https://api.browserstack.com/automate/builds/${buildId}/sessions.json?limit=1`, {
3211
- headers: { Authorization: `Basic ${auth}` }
3212
- });
3213
- if (sessResp.ok) {
3214
- const sessions = await sessResp.json();
3215
- if (sessions.length > 0) {
3216
- this.sessionId = sessions[0].automation_session.hashed_id;
3217
- this.sessionUrl = sessions[0].automation_session.browser_url;
3218
- }
3219
- }
3220
- }
3221
- }
3222
- } catch {
3223
- }
3224
- this.exitHandler = () => {
3225
- try {
3226
- this.browser?.close();
3227
- } catch {
3228
- }
3229
- };
3230
- process.on("exit", this.exitHandler);
3231
- this.logCaptureConfig = { console: config.logCapture?.console !== false, network: config.logCapture?.network !== false };
3232
- this.attachLogListeners(this.page);
3233
- }
3234
- get recording() {
3235
- if (!this.page || !this.context || !this.recordingConfig) return void 0;
3236
- return this.recordingCapability ??= createScreencastRecording(
3237
- () => this.page,
3238
- this.recordingConfig
3239
- );
3240
- }
3241
- async cleanup() {
3242
- try {
3243
- await this.recordingCapability?.stop();
3244
- } catch {
3245
- }
3246
- this.recordingCapability = void 0;
3247
- this.recordingConfig = void 0;
3248
- if (this.context) {
3249
- try {
3250
- await this.context.unrouteAll({ behavior: "ignoreErrors" });
3251
- } catch {
3252
- }
3253
- try {
3254
- await this.context.setOffline(false);
3255
- } catch {
3256
- }
3257
- }
3258
- if (this.page && this.pendingDialogArm) {
3259
- try {
3260
- this.page.off("dialog", this.pendingDialogArm);
3261
- } catch {
3262
- }
3263
- }
3264
- this.armedRoutes.clear();
3265
- this.pendingDialogArm = null;
3266
- await disposeEpoch(this.epoch);
3267
- this.epoch = void 0;
3268
- if (this.browser) {
3269
- await this.browser.close();
3270
- }
3271
- if (this.sessionId && this.credentials) {
3272
- try {
3273
- const status = this.testFailed ? "failed" : "passed";
3274
- const reason = this.testFailed ? "Test failed" : "Test passed";
3275
- const auth = Buffer.from(`${this.credentials.username}:${this.credentials.accessKey}`).toString("base64");
3276
- await fetch(`https://api.browserstack.com/automate/sessions/${this.sessionId}.json`, {
3277
- method: "PUT",
3278
- headers: { Authorization: `Basic ${auth}`, "Content-Type": "application/json" },
3279
- body: JSON.stringify({ status, reason })
3280
- });
3281
- } catch {
3282
- }
3283
- }
3284
- if (this.exitHandler) {
3285
- process.removeListener("exit", this.exitHandler);
3286
- this.exitHandler = null;
3287
- }
3288
- this.browser = null;
3289
- this.context = null;
3290
- this.page = null;
3291
- this.lastRefs = {};
3292
- this.pageLogBuffers.clear();
3293
- }
3294
- async observe(options) {
3295
- if (!this.page) {
3296
- throw new Error("FarmWebAdapter not initialized \u2014 call setup() first");
3297
- }
3298
- const page = this.page;
3299
- await waitForPageReady(page);
3300
- const epoch = await captureEpoch(page);
3301
- try {
3302
- const state = await this.observeDocument(page, options);
3303
- await disposeEpoch(this.epoch);
3304
- this.epoch = epoch;
3305
- return state;
3306
- } catch (error) {
3307
- await disposeEpoch(epoch);
3308
- throw error;
3309
- }
3310
- }
3311
- async observeDocument(page, options) {
3312
- const ariaTree = await snapshotRoot(page);
3313
- const { nodes, nativeRefs } = parseAriaSnapshot(ariaTree);
3314
- if (nodes.length > 0) {
3315
- await enrichWebNodes(page, nodes, nativeRefs);
3316
- }
3317
- const { viewport, scroll } = await captureWebScroll(page);
3318
- let domContext;
3319
- if (options?.extractDom) {
3320
- try {
3321
- domContext = await extractDom(page);
3322
- } catch {
3323
- }
3324
- }
3325
- const { state, binding } = buildScreenState4({
3326
- platform: "web",
3327
- nodes,
3328
- viewport,
3329
- scroll,
3330
- coordSpace: "viewport",
3331
- timestamp: Date.now(),
3332
- url: page.url(),
3333
- ...domContext !== void 0 ? { domContext } : {}
3334
- });
3335
- this.lastRefs = buildRefMap(binding, nodes, nativeRefs, state.elements);
3336
- return state;
3337
- }
3338
- async execute(action) {
3339
- if (!this.page) {
3340
- throw new Error("FarmWebAdapter not initialized \u2014 call setup() first");
3341
- }
3342
- try {
3343
- return await this.executeAction(action);
3344
- } catch (err) {
3345
- let screenshot;
3346
- let boundingBox;
3347
- try {
3348
- screenshot = await this.page.screenshot();
3349
- } catch {
3350
- }
3351
- try {
3352
- if ("ref" in action && typeof action.ref === "string") {
3353
- const resolver = new ElementResolver(this.page, this.lastRefs);
3354
- const box = await resolver.getBoundingBox(action.ref);
3355
- if (box) boundingBox = box;
3356
- }
3357
- } catch {
3358
- }
3359
- return {
3360
- success: false,
3361
- error: err instanceof Error ? err.message : String(err),
3362
- screenshot,
3363
- boundingBox
3364
- };
3365
- }
3366
- }
3367
- async screenshot() {
3368
- try {
3369
- if (this.page) {
3370
- return await this.page.screenshot();
3371
- }
3372
- return void 0;
3373
- } catch {
3374
- return void 0;
3375
- }
3376
- }
3377
- getPage() {
3378
- return this.page;
3379
- }
3380
- markFailed() {
3381
- this.testFailed = true;
3382
- }
3383
- getSessionUrl() {
3384
- return this.sessionUrl;
3385
- }
3386
- getSessionId() {
3387
- return this.sessionId;
3388
- }
3389
- drainConsoleLogs(level) {
3390
- const all = [];
3391
- for (const buffers of this.pageLogBuffers.values()) {
3392
- const newLogs = buffers.consoleLogs.slice(buffers.consoleDrainIndex);
3393
- buffers.consoleDrainIndex = buffers.consoleLogs.length;
3394
- all.push(...newLogs);
3395
- }
3396
- if (!level) return all;
3397
- return all.filter((e) => e.level === level);
3398
- }
3399
- drainNetworkLogs(urlPattern) {
3400
- const all = [];
3401
- for (const buffers of this.pageLogBuffers.values()) {
3402
- const newLogs = buffers.networkLogs.slice(buffers.networkDrainIndex);
3403
- buffers.networkDrainIndex = buffers.networkLogs.length;
3404
- all.push(...newLogs);
3405
- }
3406
- if (!urlPattern) return all;
3407
- return all.filter((e) => e.url.includes(urlPattern));
3408
- }
3409
- peekNetworkLogs(urlPattern) {
3410
- const all = [];
3411
- for (const buffers of this.pageLogBuffers.values()) {
3412
- all.push(...buffers.networkLogs.slice(buffers.networkDrainIndex));
3413
- }
3414
- if (!urlPattern) return all;
3415
- return all.filter((e) => e.url.includes(urlPattern));
3416
- }
3417
- attachLogListeners(page) {
3418
- const buffers = {
3419
- consoleLogs: [],
3420
- networkLogs: [],
3421
- consoleDrainIndex: 0,
3422
- networkDrainIndex: 0,
3423
- pendingRequests: /* @__PURE__ */ new Map()
3424
- };
3425
- this.pageLogBuffers.set(page, buffers);
3426
- if (this.logCaptureConfig.console) {
3427
- page.on("console", (msg) => {
3428
- buffers.consoleLogs.push({
3429
- level: msg.type(),
3430
- text: msg.text(),
3431
- location: msg.location(),
3432
- timestamp: Date.now()
3433
- });
3434
- if (buffers.consoleLogs.length > 1e3) {
3435
- buffers.consoleLogs = buffers.consoleLogs.slice(-500);
3436
- }
3437
- });
3438
- }
3439
- if (this.logCaptureConfig.network) {
3440
- page.on("request", (request) => {
3441
- buffers.pendingRequests.set(request.url() + request.method(), {
3442
- url: request.url(),
3443
- method: request.method(),
3444
- headers: request.headers(),
3445
- startTime: Date.now(),
3446
- postData: request.postData() ?? void 0
3447
- });
3448
- });
3449
- page.on("response", async (response) => {
3450
- const key = response.url() + response.request().method();
3451
- const pending = buffers.pendingRequests.get(key);
3452
- buffers.pendingRequests.delete(key);
3453
- let body;
3454
- try {
3455
- const buf = await response.body();
3456
- const ct = response.headers()["content-type"] ?? "";
3457
- if (ct.includes("image/") && !ct.includes("svg")) {
3458
- body = buf.length > 32768 ? void 0 : `data:${ct.split(";")[0]};base64,${buf.toString("base64")}`;
3459
- } else {
3460
- const text = buf.toString("utf-8");
3461
- body = text.length > 32768 ? text.slice(0, 32768) + "...[truncated]" : text;
3462
- }
3463
- } catch {
3464
- body = void 0;
3465
- }
3466
- buffers.networkLogs.push({
3467
- url: response.url(),
3468
- method: response.request().method(),
3469
- status: response.status(),
3470
- requestHeaders: pending?.headers ?? response.request().headers(),
3471
- responseHeaders: response.headers(),
3472
- body,
3473
- requestBody: pending?.postData,
3474
- startTime: pending?.startTime ?? Date.now(),
3475
- endTime: Date.now(),
3476
- timing: response.request().timing()
3477
- });
3478
- if (buffers.networkLogs.length > 500) {
3479
- buffers.networkLogs = buffers.networkLogs.slice(-250);
3480
- }
3481
- });
3482
- }
3483
- }
3484
- async resolveTabTargetAsync(tab) {
3485
- const pages = this.context.pages();
3486
- if (tab.index !== void 0) {
3487
- return tab.index >= 0 && tab.index < pages.length ? pages[tab.index] : void 0;
3488
- }
3489
- if (tab.title) {
3490
- for (const p of pages) {
3491
- const t = await p.title();
3492
- if (t.includes(tab.title)) return p;
3493
- }
3494
- return void 0;
3495
- }
3496
- if (tab.url) {
3497
- return pages.find((p) => p.url().includes(tab.url));
3498
- }
3499
- return void 0;
3500
- }
3501
- async executeAction(action) {
3502
- const page = this.page;
3503
- const resolver = new ElementResolver(page, this.lastRefs);
3504
- for (const ref of laneRefs(action)) {
3505
- const entry = this.lastRefs[ref];
3506
- if (entry === void 0) continue;
3507
- const check = action.type === "scroll" ? await checkDocument(page, this.epoch) : await checkBinding(page, this.epoch, entry);
3508
- if (check.outcome !== "ok") return bindingRefusal(check.outcome, ref);
3509
- }
3510
- if ("ref" in action && typeof action.ref === "string") {
3511
- const refData = this.lastRefs[action.ref];
3512
- const role = refData?.role;
3513
- const validation = validateAction(action, role);
3514
- if (!validation.valid) {
3515
- return { success: false, error: validation.error };
3516
- }
3517
- }
3518
- switch (action.type) {
3519
- case "click": {
3520
- const locator = resolver.resolve(action.ref);
3521
- const box = await resolver.getBoundingBox(action.ref);
3522
- const coordinates = box ? { x: box.x + box.width / 2, y: box.y + box.height / 2 } : void 0;
3523
- await locator.click({
3524
- ...clickDelayOption2(action.clickDelay)
3525
- });
3526
- return { success: true, coordinates, boundingBox: box ?? void 0 };
3527
- }
3528
- case "fill": {
3529
- const locator = resolver.resolve(action.ref);
3530
- const box = await resolver.getBoundingBox(action.ref);
3531
- return runWebFillCompound(locator, box ?? void 0, action);
3532
- }
3533
- case "openApp": {
3534
- return webOpenAppError();
3535
- }
3536
- case "select": {
3537
- const locator = resolver.resolve(action.ref);
3538
- const box = await resolver.getBoundingBox(action.ref);
3539
- await locator.selectOption(action.value);
3540
- return { success: true, boundingBox: box ?? void 0 };
3541
- }
3542
- case "navigate": {
3543
- switch (action.to) {
3544
- case "url":
3545
- await page.goto(action.url, { waitUntil: "domcontentloaded" });
3546
- break;
3547
- case "back":
3548
- await page.goBack({ waitUntil: "domcontentloaded" });
3549
- break;
3550
- case "forward":
3551
- await page.goForward({ waitUntil: "domcontentloaded" });
3552
- break;
3553
- case "reload":
3554
- await page.reload({ waitUntil: "domcontentloaded" });
3555
- break;
3556
- }
3557
- await waitForPageReady(page);
3558
- return { success: true };
3559
- }
3560
- case "scroll": {
3561
- return runWebScrollCompound(page, this.lastRefs, action);
3562
- }
3563
- case "waitFor": {
3564
- const timeout = action.timeoutMs;
3565
- try {
3566
- switch (action.on) {
3567
- case "text":
3568
- await page.getByText(action.text).first().waitFor({ state: "visible", timeout });
3569
- break;
3570
- case "gone":
3571
- await page.getByText(action.text).first().waitFor({ state: "hidden", timeout });
3572
- break;
3573
- case "selector":
3574
- await page.waitForSelector(action.selector, { timeout });
3575
- break;
3576
- }
3577
- } catch {
3578
- const what = action.on === "selector" ? `selector "${action.selector}"` : `text "${action.text}"`;
3579
- const how = action.on === "gone" ? "was still present after" : "did not appear within";
3580
- return { success: false, error: `waitFor: ${what} ${how} ${timeout}ms.` };
3581
- }
3582
- return { success: true };
3583
- }
3584
- case "delay": {
3585
- await page.waitForTimeout(action.ms);
3586
- return { success: true };
3587
- }
3588
- case "waitForUrl": {
3589
- try {
3590
- await page.waitForURL(action.pattern, { timeout: 3e4 });
3591
- return { success: true };
3592
- } catch (err) {
3593
- return {
3594
- success: false,
3595
- 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)}`
3596
- };
3597
- }
3598
- }
3599
- case "keypress": {
3600
- const effectiveKeys = convertKeysForPlatform2(action.keys, {
3601
- enabled: action.convertPlatformKeys !== false,
3602
- isMac: isMacPlatform2()
3603
- });
3604
- for (const key of effectiveKeys) {
3605
- if (action.mode === "down") {
3606
- await page.keyboard.down(key);
3607
- } else if (action.mode === "up") {
3608
- await page.keyboard.up(key);
3609
- } else {
3610
- await page.keyboard.press(key);
3611
- }
3612
- }
3613
- return { success: true };
3614
- }
3615
- case "hover": {
3616
- const locator = resolver.resolve(action.ref);
3617
- const box = await resolver.getBoundingBox(action.ref);
3618
- await locator.hover();
3619
- return { success: true, boundingBox: box ?? void 0 };
3620
- }
3621
- case "paste": {
3622
- const locator = resolver.resolve(action.ref);
3623
- const box = await resolver.getBoundingBox(action.ref);
3624
- await locator.focus();
3625
- await page.evaluate(`(() => {
3626
- const el = document.activeElement;
3627
- if (!el) return;
3628
- const dt = new DataTransfer();
3629
- dt.setData('text/plain', ${JSON.stringify(action.value)});
3630
- el.dispatchEvent(new ClipboardEvent('paste', {
3631
- clipboardData: dt,
3632
- bubbles: true,
3633
- cancelable: true,
3634
- }));
3635
- })()`);
3636
- return { success: true, boundingBox: box ?? void 0 };
3637
- }
3638
- case "fileUpload": {
3639
- const { existsSync } = await import("fs");
3640
- for (const filePath of action.files) {
3641
- if (!existsSync(filePath)) {
3642
- return { success: false, error: `File not found: ${filePath}` };
3643
- }
3644
- }
3645
- const locator = resolver.resolve(action.ref);
3646
- const box = await resolver.getBoundingBox(action.ref);
3647
- await locator.setInputFiles(action.files);
3648
- return { success: true, boundingBox: box ?? void 0 };
3649
- }
3650
- case "copy": {
3651
- const locator = resolver.resolve(action.ref);
3652
- const box = await resolver.getBoundingBox(action.ref);
3653
- const text = await locator.textContent() ?? "";
3654
- await page.evaluate(`navigator.clipboard.writeText(${JSON.stringify(text)})`);
3655
- return { success: true, boundingBox: box ?? void 0, data: { copied: text } };
3656
- }
3657
- case "tab": {
3658
- if (action.action === "new") {
3659
- const newPage = await this.context.newPage();
3660
- this.attachLogListeners(newPage);
3661
- await newPage.goto(action.url, { waitUntil: "domcontentloaded" });
3662
- await waitForPageReady(newPage);
3663
- this.page = newPage;
3664
- return { success: true };
3665
- }
3666
- const pages = this.context.pages();
3667
- let target;
3668
- if (action.index !== void 0) {
3669
- if (action.index < 0 || action.index >= pages.length) {
3670
- return { success: false, error: `Tab index ${action.index} out of range (${pages.length} tabs open)` };
3671
- }
3672
- target = pages[action.index];
3673
- } else if (action.title !== void 0) {
3674
- for (const p of pages) {
3675
- const t = await p.title();
3676
- if (t.includes(action.title)) {
3677
- target = p;
3678
- break;
3679
- }
3680
- }
3681
- } else if (action.url !== void 0) {
3682
- target = pages.find((p) => p.url().includes(action.url));
3683
- }
3684
- if (!target) {
3685
- return { success: false, error: `No tab found matching ${JSON.stringify({ index: action.index, title: action.title, url: action.url })}` };
3686
- }
3687
- this.page = target;
3688
- return { success: true };
3689
- }
3690
- case "inspect": {
3691
- switch (action.what) {
3692
- case "console": {
3693
- let entries;
3694
- if (action.tab) {
3695
- const targetPage = await this.resolveTabTargetAsync(action.tab);
3696
- if (!targetPage) {
3697
- return { success: false, error: `No tab found matching ${JSON.stringify(action.tab)}` };
3698
- }
3699
- const buffers = this.pageLogBuffers.get(targetPage);
3700
- entries = buffers ? [...buffers.consoleLogs] : [];
3701
- } else {
3702
- entries = [];
3703
- for (const buffers of this.pageLogBuffers.values()) {
3704
- entries.push(...buffers.consoleLogs);
3705
- }
3706
- }
3707
- if (action.level) {
3708
- entries = entries.filter((e) => e.level === action.level);
3709
- }
3710
- return { success: true, data: entries };
3711
- }
3712
- case "network": {
3713
- let entries;
3714
- if (action.tab) {
3715
- const targetPage = await this.resolveTabTargetAsync(action.tab);
3716
- if (!targetPage) {
3717
- return { success: false, error: `No tab found matching ${JSON.stringify(action.tab)}` };
3718
- }
3719
- const buffers = this.pageLogBuffers.get(targetPage);
3720
- entries = buffers ? [...buffers.networkLogs] : [];
3721
- } else {
3722
- entries = [];
3723
- for (const buffers of this.pageLogBuffers.values()) {
3724
- entries.push(...buffers.networkLogs);
3725
- }
3726
- }
3727
- if (action.urlPattern) {
3728
- entries = entries.filter((e) => e.url.includes(action.urlPattern));
3729
- }
3730
- return { success: true, data: entries };
3731
- }
3732
- case "cookies": {
3733
- const cookies = await this.context.cookies();
3734
- if (action.name) {
3735
- const cookie = cookies.find((c) => c.name === action.name) ?? null;
3736
- return { success: true, data: cookie };
3737
- }
3738
- return { success: true, data: cookies };
3739
- }
3740
- case "localStorage": {
3741
- const pageUrl = page.url();
3742
- if (pageUrl === "about:blank") {
3743
- return { success: true, data: action.key ? null : {} };
3744
- }
3745
- if (action.key) {
3746
- const value = await page.evaluate(
3747
- (k) => localStorage.getItem(k),
3748
- action.key
3749
- );
3750
- return { success: true, data: value };
3751
- }
3752
- const all = await page.evaluate(
3753
- () => Object.fromEntries(
3754
- Object.keys(localStorage).map((k) => [k, localStorage.getItem(k)])
3755
- )
3756
- );
3757
- return { success: true, data: all };
3758
- }
3759
- }
3760
- return { success: true };
3761
- }
3762
- case "setCookies": {
3763
- const cookiesWithDefaults = action.cookies.map((c) => ({
3764
- ...c,
3765
- url: !c.domain ? page.url() : void 0
3766
- }));
3767
- await this.context.addCookies(cookiesWithDefaults);
3768
- return { success: true };
3769
- }
3770
- case "setLocalStorage": {
3771
- await page.evaluate((entries) => {
3772
- for (const { key, value } of entries) {
3773
- localStorage.setItem(key, value);
3774
- }
3775
- }, action.entries);
3776
- return { success: true };
3777
- }
3778
- case "focus": {
3779
- const locator = resolver.resolve(action.ref);
3780
- const box = await resolver.getBoundingBox(action.ref);
3781
- await locator.focus();
3782
- return { success: true, boundingBox: box ?? void 0 };
3783
- }
3784
- case "assertValue": {
3785
- let value;
3786
- if ("expr" in action) {
3787
- value = await page.evaluate(action.expr);
3788
- } else {
3789
- switch (action.source) {
3790
- case "url":
3791
- value = page.url();
3792
- break;
3793
- case "storage":
3794
- value = page.url() === "about:blank" ? {} : await page.evaluate(
3795
- () => Object.fromEntries(
3796
- Object.keys(localStorage).map((k) => [k, localStorage.getItem(k)])
3797
- )
3798
- );
3799
- break;
3800
- case "network":
3801
- value = this.peekNetworkLogs();
3802
- break;
3803
- case "expr-result":
3804
- value = void 0;
3805
- break;
3806
- }
3807
- }
3808
- return { success: true, data: value };
3809
- }
3810
- case "clearText": {
3811
- const loc = resolver.resolve(action.ref);
3812
- const clearBox = await resolver.getBoundingBox(action.ref);
3813
- await loc.clear();
3814
- return { success: true, boundingBox: clearBox ?? void 0 };
3815
- }
3816
- case "doubleClick": {
3817
- const locator = resolver.resolve(action.ref);
3818
- const box = await resolver.getBoundingBox(action.ref);
3819
- 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;
3820
- await locator.dblclick({
3821
- ...action.relativePosition && { position: action.relativePosition },
3822
- ...clickDelayOption2(action.clickDelay)
3823
- });
3824
- return { success: true, coordinates, boundingBox: box ?? void 0 };
3825
- }
3826
- case "rightClick": {
3827
- const locator = resolver.resolve(action.ref);
3828
- const box = await resolver.getBoundingBox(action.ref);
3829
- 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;
3830
- await locator.click({
3831
- button: "right",
3832
- ...action.relativePosition && { position: action.relativePosition },
3833
- ...clickDelayOption2(action.clickDelay)
3834
- });
3835
- return { success: true, coordinates, boundingBox: box ?? void 0 };
3836
- }
3837
- case "route": {
3838
- await this.context.route(action.urlPattern, async (route) => {
3839
- await route.fulfill({
3840
- status: 200,
3841
- ...action.contentType !== void 0 ? { contentType: action.contentType } : {},
3842
- ...action.headers !== void 0 ? { headers: action.headers } : {},
3843
- ...action.body !== void 0 ? { body: action.body } : {}
3844
- });
3845
- });
3846
- this.armedRoutes.add(action.urlPattern);
3847
- return { success: true };
3848
- }
3849
- case "unroute": {
3850
- if (action.urlPattern !== void 0) {
3851
- await this.context.unroute(action.urlPattern);
3852
- this.armedRoutes.delete(action.urlPattern);
3853
- } else {
3854
- await this.context.unrouteAll({ behavior: "ignoreErrors" });
3855
- this.armedRoutes.clear();
3856
- }
3857
- return { success: true };
3858
- }
3859
- case "networkState": {
3860
- if (action.mode === "offline") {
3861
- await this.context.setOffline(true);
3862
- return { success: true };
3863
- }
3864
- try {
3865
- const cdp = await this.context.newCDPSession(page);
3866
- await cdp.send("Network.emulateNetworkConditions", {
3867
- offline: false,
3868
- latency: action.latencyMs,
3869
- downloadThroughput: action.downloadKbps * 1e3 / 8,
3870
- uploadThroughput: action.uploadKbps * 1e3 / 8
3871
- });
3872
- return { success: true };
3873
- } catch (err) {
3874
- return {
3875
- success: false,
3876
- error: `Network throttling is only available on Chromium (via CDP); this context does not support it. ${err instanceof Error ? err.message : String(err)}`
3877
- };
3878
- }
3879
- }
3880
- case "storageState": {
3881
- if (action.op === "save") {
3882
- const state = await this.context.storageState();
3883
- return { success: true, data: { name: action.name, state } };
3884
- }
3885
- await this.context.setStorageState(readSeamStorageState2(action));
3886
- return { success: true };
3887
- }
3888
- case "handleDialog": {
3889
- if (this.pendingDialogArm) {
3890
- page.off("dialog", this.pendingDialogArm);
3891
- this.pendingDialogArm = null;
3892
- }
3893
- const accept = action.accept;
3894
- const promptText = action.promptText;
3895
- const handler = (dialog) => {
3896
- this.pendingDialogArm = null;
3897
- if (accept) {
3898
- void dialog.accept(promptText);
3899
- } else {
3900
- void dialog.dismiss();
3901
- }
3902
- };
3903
- this.pendingDialogArm = handler;
3904
- page.once("dialog", handler);
3905
- return { success: true };
3906
- }
3907
- case "executeScript": {
3908
- return { success: false, error: "executeScript not supported on farm adapter" };
3909
- }
3910
- // Runner-minted / author-channel verbs (D-01/D-15/D-16): reportValue, uniqueValue, assertAI,
3911
- // extractAI, and setVariable are intercepted at the dispatch seam and never reach the adapter.
3912
- // A grouped §7.1 defensive case surfaces a dispatch-routing error if one arrives here;
3913
- // assertNever remains the backstop for a genuinely unknown schema verb.
3914
- case "reportValue":
3915
- case "uniqueValue":
3916
- case "assertAI":
3917
- case "extractAI":
3918
- case "setVariable":
3919
- return {
3920
- success: false,
3921
- 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.`
3922
- };
3923
- default:
3924
- return assertNever2(action);
3925
- }
3926
- }
3927
- };
3928
-
3929
2975
  // src/accessibility.ts
3930
2976
  import { AxeBuilder } from "@axe-core/playwright";
3931
2977
  async function runAccessibilityCheck(page, options) {
@@ -3962,10 +3008,7 @@ async function runAccessibilityCheck(page, options) {
3962
3008
  export {
3963
3009
  ActionValidationError,
3964
3010
  ElementResolver,
3965
- FarmWebAdapter,
3966
3011
  WebPlatformAdapter,
3967
- extractDom,
3968
- observePage,
3969
3012
  parseAriaSnapshot,
3970
3013
  runAccessibilityCheck,
3971
3014
  validateAction,