@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.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
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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 } =
|
|
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
|
-
//
|
|
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
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
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,
|