@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/README.md +78 -0
- package/dist/index.cjs +81 -1041
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +7 -117
- package/dist/index.d.ts +7 -117
- package/dist/index.js +65 -1022
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
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
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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,
|
|
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,
|
|
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
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
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
|
-
//
|
|
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
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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,
|
|
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
|
-
},
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
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:
|
|
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
|
|
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,
|
|
2677
|
+
const effectiveKeys = (0, import_agent_qa_runner6.convertKeysForPlatform)(action.keys, {
|
|
2840
2678
|
enabled: action.convertPlatformKeys !== false,
|
|
2841
|
-
isMac: (0,
|
|
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
|
-
|
|
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
|
-
//
|
|
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,
|
|
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,
|