@vostride/agent-qa-web 0.1.20 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +2725 -772
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +139 -20
- package/dist/index.d.ts +139 -20
- package/dist/index.js +2731 -766
- package/dist/index.js.map +1 -1
- package/package.json +6 -3
package/dist/index.cjs
CHANGED
|
@@ -44,7 +44,11 @@ __export(index_exports, {
|
|
|
44
44
|
module.exports = __toCommonJS(index_exports);
|
|
45
45
|
|
|
46
46
|
// src/adapter.ts
|
|
47
|
-
var
|
|
47
|
+
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");
|
|
48
52
|
|
|
49
53
|
// src/dom-extractor.ts
|
|
50
54
|
var SKIP_TAGS = /* @__PURE__ */ new Set([
|
|
@@ -157,59 +161,87 @@ var CONTENT_ROLES = /* @__PURE__ */ new Set([
|
|
|
157
161
|
"region",
|
|
158
162
|
"navigation",
|
|
159
163
|
"progressbar",
|
|
160
|
-
"tabpanel"
|
|
164
|
+
"tabpanel",
|
|
165
|
+
"row",
|
|
166
|
+
"dialog",
|
|
167
|
+
"alertdialog"
|
|
161
168
|
]);
|
|
169
|
+
var LIMIT_KEYS = ["valuenow", "valuemin", "valuemax"];
|
|
162
170
|
var LINE_PATTERN = /^(\s*-\s*)(\w+)(?:\s+"([^"]*)")?(.*)$/;
|
|
163
171
|
var QUOTED_LINE_PATTERN = /^(\s*-\s*)'(\w+)(?:\s+"([^"]*)")?(.*)'\s*$/;
|
|
172
|
+
var REF_ANNOTATION = /\[ref=([A-Za-z0-9_-]+)\]/;
|
|
173
|
+
var BRACKET_ATTR = /\[(\w+)(?:=([^\]]*))?\]/g;
|
|
174
|
+
var INLINE_TEXT = /^(?:\s*\[[^\]]*\])*\s*:\s*(.*)$/;
|
|
175
|
+
function inlineText(suffix) {
|
|
176
|
+
const raw = suffix?.match(INLINE_TEXT)?.[1]?.trim();
|
|
177
|
+
if (!raw) return void 0;
|
|
178
|
+
if (raw.length >= 2 && raw.startsWith('"') && raw.endsWith('"')) {
|
|
179
|
+
try {
|
|
180
|
+
const parsed = JSON.parse(raw);
|
|
181
|
+
if (typeof parsed === "string") return parsed;
|
|
182
|
+
} catch {
|
|
183
|
+
return raw.slice(1, -1);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return raw;
|
|
187
|
+
}
|
|
188
|
+
function appendText(node, text) {
|
|
189
|
+
const tidy2 = text?.replace(/\s+/g, " ").trim();
|
|
190
|
+
if (!tidy2) return;
|
|
191
|
+
node.text = node.text === void 0 ? tidy2 : `${node.text} ${tidy2}`;
|
|
192
|
+
}
|
|
164
193
|
function parseAriaSnapshot(ariaTree) {
|
|
165
194
|
const lines = ariaTree.split("\n");
|
|
166
|
-
const
|
|
167
|
-
const
|
|
168
|
-
const
|
|
169
|
-
let refCounter = 0;
|
|
170
|
-
const roleNameCounts = /* @__PURE__ */ new Map();
|
|
171
|
-
const refToKey = /* @__PURE__ */ new Map();
|
|
195
|
+
const nodes = [];
|
|
196
|
+
const nativeRefs = [];
|
|
197
|
+
const ancestors = [];
|
|
172
198
|
for (const line of lines) {
|
|
173
199
|
const match = line.match(LINE_PATTERN) || line.match(QUOTED_LINE_PATTERN);
|
|
174
|
-
if (!match)
|
|
175
|
-
|
|
176
|
-
continue;
|
|
177
|
-
}
|
|
178
|
-
const [, prefix, role, name, suffix] = match;
|
|
200
|
+
if (!match) continue;
|
|
201
|
+
const [, , role, name, suffix] = match;
|
|
179
202
|
const roleLower = role.toLowerCase();
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
203
|
+
const leadingSpaces = line.length - line.trimStart().length;
|
|
204
|
+
const depth = Math.floor(leadingSpaces / 2);
|
|
205
|
+
while (ancestors.length > 0 && ancestors[ancestors.length - 1].depth >= depth) {
|
|
206
|
+
ancestors.pop();
|
|
207
|
+
}
|
|
208
|
+
let keptAncestor;
|
|
209
|
+
for (let k = ancestors.length - 1; k >= 0; k--) {
|
|
210
|
+
if (ancestors[k].index !== void 0) {
|
|
211
|
+
keptAncestor = ancestors[k].index;
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (!INTERACTIVE_ROLES.has(roleLower) && !CONTENT_ROLES.has(roleLower)) {
|
|
216
|
+
if (keptAncestor !== void 0) {
|
|
217
|
+
appendText(nodes[keptAncestor], name);
|
|
218
|
+
appendText(nodes[keptAncestor], inlineText(suffix));
|
|
219
|
+
}
|
|
220
|
+
ancestors.push({ depth });
|
|
183
221
|
continue;
|
|
184
222
|
}
|
|
185
|
-
|
|
186
|
-
const
|
|
187
|
-
const
|
|
188
|
-
const currentCount = roleNameCounts.get(key) ?? 0;
|
|
189
|
-
roleNameCounts.set(key, currentCount + 1);
|
|
190
|
-
refToKey.set(ref, key);
|
|
191
|
-
refs[ref] = { role: roleLower, name, nth: currentCount };
|
|
223
|
+
const parentIndex = keptAncestor;
|
|
224
|
+
const refMatch = suffix?.match(REF_ANNOTATION);
|
|
225
|
+
const nativeRef = refMatch ? refMatch[1] : void 0;
|
|
192
226
|
const attributes = {};
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
227
|
+
if (suffix) {
|
|
228
|
+
for (const am of suffix.matchAll(BRACKET_ATTR)) {
|
|
229
|
+
if (am[1] === "ref") continue;
|
|
196
230
|
attributes[am[1]] = am[2] ?? "true";
|
|
197
231
|
}
|
|
198
232
|
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
if (
|
|
202
|
-
|
|
203
|
-
if (
|
|
204
|
-
|
|
233
|
+
const node = { role: roleLower };
|
|
234
|
+
if (name) node.name = name;
|
|
235
|
+
if (parentIndex !== void 0) node.parentIndex = parentIndex;
|
|
236
|
+
if (Object.keys(attributes).length > 0) node.attributes = attributes;
|
|
237
|
+
if ("disabled" in attributes) node.state = { enabled: false };
|
|
238
|
+
if (CONTENT_ROLES.has(roleLower)) appendText(node, inlineText(suffix));
|
|
239
|
+
const index = nodes.length;
|
|
240
|
+
nodes.push(node);
|
|
241
|
+
nativeRefs.push(nativeRef);
|
|
242
|
+
ancestors.push({ depth, index });
|
|
205
243
|
}
|
|
206
|
-
|
|
207
|
-
const key = refToKey.get(ref);
|
|
208
|
-
if ((roleNameCounts.get(key) ?? 0) <= 1) {
|
|
209
|
-
delete refs[ref].nth;
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
return { tree: resultLines.join("\n"), elements, refs };
|
|
244
|
+
return { nodes, nativeRefs };
|
|
213
245
|
}
|
|
214
246
|
function hideAgentQaElements() {
|
|
215
247
|
const doc = globalThis.document;
|
|
@@ -225,8 +257,133 @@ function restoreAgentQaElements() {
|
|
|
225
257
|
el.removeAttribute("aria-hidden");
|
|
226
258
|
});
|
|
227
259
|
}
|
|
260
|
+
async function captureWebScroll(page) {
|
|
261
|
+
const fallbackViewport = page.viewportSize() ?? { width: 0, height: 0 };
|
|
262
|
+
try {
|
|
263
|
+
const raw = await page.evaluate(() => {
|
|
264
|
+
const w = globalThis;
|
|
265
|
+
const docEl = w.document.documentElement;
|
|
266
|
+
return {
|
|
267
|
+
width: w.innerWidth,
|
|
268
|
+
height: w.innerHeight,
|
|
269
|
+
offsetX: w.scrollX,
|
|
270
|
+
offsetY: w.scrollY,
|
|
271
|
+
contentWidth: docEl.scrollWidth,
|
|
272
|
+
contentHeight: docEl.scrollHeight
|
|
273
|
+
};
|
|
274
|
+
});
|
|
275
|
+
if (raw && typeof raw === "object") {
|
|
276
|
+
return {
|
|
277
|
+
viewport: {
|
|
278
|
+
width: numberOr(raw.width, fallbackViewport.width),
|
|
279
|
+
height: numberOr(raw.height, fallbackViewport.height)
|
|
280
|
+
},
|
|
281
|
+
scroll: {
|
|
282
|
+
offsetX: numberOr(raw.offsetX, 0),
|
|
283
|
+
offsetY: numberOr(raw.offsetY, 0),
|
|
284
|
+
contentWidth: numberOr(raw.contentWidth, 0),
|
|
285
|
+
contentHeight: numberOr(raw.contentHeight, 0)
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
} catch {
|
|
290
|
+
}
|
|
291
|
+
return {
|
|
292
|
+
viewport: fallbackViewport,
|
|
293
|
+
scroll: { offsetX: 0, offsetY: 0, contentWidth: 0, contentHeight: 0 }
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
async function enrichWebNodes(page, nodes, nativeRefs) {
|
|
297
|
+
try {
|
|
298
|
+
const handles = [];
|
|
299
|
+
const targetIndices = [];
|
|
300
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
301
|
+
const ref = nativeRefs[i];
|
|
302
|
+
if (!ref) continue;
|
|
303
|
+
try {
|
|
304
|
+
const handle = await page.locator("aria-ref=" + ref).elementHandle();
|
|
305
|
+
if (handle) {
|
|
306
|
+
handles.push(handle);
|
|
307
|
+
targetIndices.push(i);
|
|
308
|
+
}
|
|
309
|
+
} catch {
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
if (handles.length === 0) return;
|
|
313
|
+
const raw = await page.evaluate((els) => {
|
|
314
|
+
const w = globalThis;
|
|
315
|
+
const active = w.document.activeElement;
|
|
316
|
+
return els.map((el) => {
|
|
317
|
+
if (!el) return null;
|
|
318
|
+
const rect = typeof el.getBoundingClientRect === "function" ? el.getBoundingClientRect() : null;
|
|
319
|
+
const style = typeof w.getComputedStyle === "function" ? w.getComputedStyle(el) : null;
|
|
320
|
+
const hidden = style ? style.visibility === "hidden" || style.display === "none" : false;
|
|
321
|
+
const hasBox = !!rect && rect.width > 0 && rect.height > 0;
|
|
322
|
+
const disabled = el.disabled === true || typeof el.hasAttribute === "function" && el.hasAttribute("disabled") || typeof el.getAttribute === "function" && el.getAttribute("aria-disabled") === "true";
|
|
323
|
+
const attr = (n) => typeof el.getAttribute === "function" ? el.getAttribute(n) : null;
|
|
324
|
+
const inputType = el.tagName === "INPUT" && typeof el.type === "string" ? el.type : "";
|
|
325
|
+
const isRange = inputType === "range";
|
|
326
|
+
const isNumber = inputType === "number";
|
|
327
|
+
return {
|
|
328
|
+
bounds: rect ? { x: rect.x, y: rect.y, width: rect.width, height: rect.height } : null,
|
|
329
|
+
enabled: !disabled,
|
|
330
|
+
focused: el === active,
|
|
331
|
+
visible: hasBox && !hidden,
|
|
332
|
+
testId: attr("data-testid"),
|
|
333
|
+
ariaLabel: attr("aria-label"),
|
|
334
|
+
placeholder: attr("placeholder"),
|
|
335
|
+
title: attr("title"),
|
|
336
|
+
valuenow: attr("aria-valuenow") ?? (isRange || isNumber ? String(el.value) : null),
|
|
337
|
+
valuemin: attr("aria-valuemin") ?? (isRange ? el.min || "0" : isNumber ? el.min || null : null),
|
|
338
|
+
valuemax: attr("aria-valuemax") ?? (isRange ? el.max || "100" : isNumber ? el.max || null : null)
|
|
339
|
+
};
|
|
340
|
+
});
|
|
341
|
+
}, handles);
|
|
342
|
+
if (!Array.isArray(raw)) return;
|
|
343
|
+
for (let k = 0; k < targetIndices.length; k++) {
|
|
344
|
+
const info = raw[k];
|
|
345
|
+
if (!info || typeof info !== "object") continue;
|
|
346
|
+
const node = nodes[targetIndices[k]];
|
|
347
|
+
const bounds = coerceBounds(info.bounds);
|
|
348
|
+
if (bounds) node.bounds = bounds;
|
|
349
|
+
const state = { ...node.state ?? {} };
|
|
350
|
+
if (typeof info.enabled === "boolean") state.enabled = info.enabled;
|
|
351
|
+
if (typeof info.focused === "boolean") state.focused = info.focused;
|
|
352
|
+
if (typeof info.visible === "boolean") state.visible = info.visible;
|
|
353
|
+
if (Object.keys(state).length > 0) node.state = state;
|
|
354
|
+
const labels = { ...node.labels ?? {} };
|
|
355
|
+
const testId = coerceString(info.testId);
|
|
356
|
+
if (testId) labels.testId = testId;
|
|
357
|
+
const ariaLabel = coerceString(info.ariaLabel);
|
|
358
|
+
if (ariaLabel) labels.ariaLabel = ariaLabel;
|
|
359
|
+
const placeholder = coerceString(info.placeholder);
|
|
360
|
+
if (placeholder) labels.placeholder = placeholder;
|
|
361
|
+
const title = coerceString(info.title);
|
|
362
|
+
if (title) labels.title = title;
|
|
363
|
+
if (Object.keys(labels).length > 0) node.labels = labels;
|
|
364
|
+
const limits = {};
|
|
365
|
+
for (const key of LIMIT_KEYS) {
|
|
366
|
+
const value = coerceString(info[key]);
|
|
367
|
+
if (value) limits[key] = value;
|
|
368
|
+
}
|
|
369
|
+
if (Object.keys(limits).length > 0) node.attributes = { ...node.attributes ?? {}, ...limits };
|
|
370
|
+
}
|
|
371
|
+
} catch {
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
function coerceBounds(value) {
|
|
375
|
+
if (!value || typeof value !== "object") return void 0;
|
|
376
|
+
const v = value;
|
|
377
|
+
const x = v.x, y = v.y, width = v.width, height = v.height;
|
|
378
|
+
if (typeof x === "number" && Number.isFinite(x) && typeof y === "number" && Number.isFinite(y) && typeof width === "number" && Number.isFinite(width) && typeof height === "number" && Number.isFinite(height)) {
|
|
379
|
+
return { x, y, width, height };
|
|
380
|
+
}
|
|
381
|
+
return void 0;
|
|
382
|
+
}
|
|
383
|
+
function coerceString(value) {
|
|
384
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
385
|
+
}
|
|
228
386
|
async function observePage(page, options) {
|
|
229
|
-
const viewport = page.viewportSize();
|
|
230
387
|
const locator = options?.filterSelector ? page.locator(options.filterSelector) : page.locator(":root");
|
|
231
388
|
await page.evaluate(hideAgentQaElements);
|
|
232
389
|
let ariaTree;
|
|
@@ -235,141 +392,9 @@ async function observePage(page, options) {
|
|
|
235
392
|
} finally {
|
|
236
393
|
await page.evaluate(restoreAgentQaElements);
|
|
237
394
|
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
elements: [],
|
|
242
|
-
url: page.url(),
|
|
243
|
-
timestamp: Date.now(),
|
|
244
|
-
metadata: {
|
|
245
|
-
coordSpace: "viewport",
|
|
246
|
-
viewportWidth: viewport?.width ?? 0,
|
|
247
|
-
viewportHeight: viewport?.height ?? 0,
|
|
248
|
-
refMap: {}
|
|
249
|
-
}
|
|
250
|
-
};
|
|
251
|
-
}
|
|
252
|
-
const { tree, elements, refs } = parseAriaSnapshot(ariaTree);
|
|
253
|
-
try {
|
|
254
|
-
const refEntries = Object.entries(refs);
|
|
255
|
-
if (refEntries.length > 0) {
|
|
256
|
-
const refArgs = refEntries.map(
|
|
257
|
-
([ref, data]) => [ref, data.role, data.name, data.nth]
|
|
258
|
-
);
|
|
259
|
-
const result = await Promise.race([
|
|
260
|
-
page.evaluate((args) => {
|
|
261
|
-
const doc = globalThis.document;
|
|
262
|
-
const IMPLICIT_ROLES = {
|
|
263
|
-
button: "button",
|
|
264
|
-
a: "link",
|
|
265
|
-
input: "textbox",
|
|
266
|
-
select: "combobox",
|
|
267
|
-
textarea: "textbox",
|
|
268
|
-
h1: "heading",
|
|
269
|
-
h2: "heading",
|
|
270
|
-
h3: "heading",
|
|
271
|
-
h4: "heading",
|
|
272
|
-
h5: "heading",
|
|
273
|
-
h6: "heading",
|
|
274
|
-
nav: "navigation",
|
|
275
|
-
main: "main",
|
|
276
|
-
aside: "complementary",
|
|
277
|
-
footer: "contentinfo",
|
|
278
|
-
header: "banner",
|
|
279
|
-
form: "form",
|
|
280
|
-
table: "table",
|
|
281
|
-
img: "img",
|
|
282
|
-
ul: "list",
|
|
283
|
-
ol: "list",
|
|
284
|
-
li: "listitem",
|
|
285
|
-
dialog: "dialog",
|
|
286
|
-
details: "group",
|
|
287
|
-
summary: "button",
|
|
288
|
-
progress: "progressbar",
|
|
289
|
-
meter: "meter",
|
|
290
|
-
output: "status",
|
|
291
|
-
article: "article",
|
|
292
|
-
section: "region",
|
|
293
|
-
fieldset: "group",
|
|
294
|
-
legend: "legend"
|
|
295
|
-
};
|
|
296
|
-
const INPUT_TYPE_ROLES = {
|
|
297
|
-
checkbox: "checkbox",
|
|
298
|
-
radio: "radio",
|
|
299
|
-
range: "slider",
|
|
300
|
-
number: "spinbutton",
|
|
301
|
-
search: "searchbox",
|
|
302
|
-
email: "textbox",
|
|
303
|
-
tel: "textbox",
|
|
304
|
-
url: "textbox",
|
|
305
|
-
password: "textbox"
|
|
306
|
-
};
|
|
307
|
-
function getAccessibleName(el) {
|
|
308
|
-
const label = el.getAttribute("aria-label");
|
|
309
|
-
if (label) return label.trim();
|
|
310
|
-
const labelledBy = el.getAttribute("aria-labelledby");
|
|
311
|
-
if (labelledBy) {
|
|
312
|
-
const parts = labelledBy.split(/\s+/).map((id) => {
|
|
313
|
-
const ref = doc.getElementById(id);
|
|
314
|
-
return ref ? (ref.textContent || "").trim() : "";
|
|
315
|
-
}).filter(Boolean);
|
|
316
|
-
if (parts.length) return parts.join(" ");
|
|
317
|
-
}
|
|
318
|
-
const title = el.getAttribute("title");
|
|
319
|
-
if (title) return title.trim();
|
|
320
|
-
const alt = el.getAttribute("alt");
|
|
321
|
-
if (alt) return alt.trim();
|
|
322
|
-
return (el.textContent || "").trim();
|
|
323
|
-
}
|
|
324
|
-
function matchesName(elName, targetName) {
|
|
325
|
-
return elName.toLowerCase().includes(targetName.toLowerCase());
|
|
326
|
-
}
|
|
327
|
-
function getImplicitRole(el) {
|
|
328
|
-
const tag = el.tagName.toLowerCase();
|
|
329
|
-
if (tag === "input") {
|
|
330
|
-
const type = (el.getAttribute("type") || "text").toLowerCase();
|
|
331
|
-
return INPUT_TYPE_ROLES[type] || "textbox";
|
|
332
|
-
}
|
|
333
|
-
if (tag === "a" && el.hasAttribute("href")) return "link";
|
|
334
|
-
return IMPLICIT_ROLES[tag] || null;
|
|
335
|
-
}
|
|
336
|
-
const results = [];
|
|
337
|
-
for (const [ref, role, name, nth] of args) {
|
|
338
|
-
const candidates = [];
|
|
339
|
-
doc.querySelectorAll(`[role="${role}"]`).forEach((el) => candidates.push(el));
|
|
340
|
-
doc.querySelectorAll("*").forEach((el) => {
|
|
341
|
-
if (el.getAttribute("role")) return;
|
|
342
|
-
if (getImplicitRole(el) === role) candidates.push(el);
|
|
343
|
-
});
|
|
344
|
-
let matched = candidates;
|
|
345
|
-
if (name) {
|
|
346
|
-
matched = candidates.filter((el) => matchesName(getAccessibleName(el), name));
|
|
347
|
-
}
|
|
348
|
-
const target = matched[nth ?? 0];
|
|
349
|
-
if (target) {
|
|
350
|
-
const rect = target.getBoundingClientRect();
|
|
351
|
-
results.push([ref, {
|
|
352
|
-
x: Math.round(rect.x),
|
|
353
|
-
y: Math.round(rect.y),
|
|
354
|
-
width: Math.round(rect.width),
|
|
355
|
-
height: Math.round(rect.height)
|
|
356
|
-
}]);
|
|
357
|
-
} else {
|
|
358
|
-
results.push([ref, null]);
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
return results;
|
|
362
|
-
}, refArgs),
|
|
363
|
-
new Promise((resolve) => setTimeout(() => resolve(null), 3e3))
|
|
364
|
-
]);
|
|
365
|
-
if (result) {
|
|
366
|
-
for (const [ref, box] of result) {
|
|
367
|
-
if (box) refs[ref].bounds = box;
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
} catch {
|
|
372
|
-
}
|
|
395
|
+
const timestamp = Date.now();
|
|
396
|
+
const url = page.url();
|
|
397
|
+
const { viewport: capturedViewport, scroll } = await captureWebScroll(page);
|
|
373
398
|
let domContext;
|
|
374
399
|
if (options?.extractDom) {
|
|
375
400
|
try {
|
|
@@ -377,23 +402,44 @@ async function observePage(page, options) {
|
|
|
377
402
|
} catch {
|
|
378
403
|
}
|
|
379
404
|
}
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
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
|
+
function numberOr(value, fallback) {
|
|
422
|
+
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
393
423
|
}
|
|
394
424
|
|
|
395
425
|
// src/element-resolver.ts
|
|
396
|
-
var
|
|
426
|
+
var import_agent_qa_runner2 = require("@vostride/agent-qa-runner");
|
|
427
|
+
var ACTIONS_WITHOUT_REF = /* @__PURE__ */ new Set(["navigate", "waitFor", "delay", "waitForUrl", "assertValue"]);
|
|
428
|
+
function buildRefMap(binding, nodes, nativeRefs, elements) {
|
|
429
|
+
const refs = {};
|
|
430
|
+
for (const [ref, bind] of Object.entries(binding)) {
|
|
431
|
+
const node = nodes[bind.index];
|
|
432
|
+
const nativeRef = nativeRefs[bind.index];
|
|
433
|
+
const element = elements[bind.index];
|
|
434
|
+
if (!node || nativeRef === void 0 || !element) continue;
|
|
435
|
+
const entry = { nativeRef, role: node.role, name: node.name ?? "" };
|
|
436
|
+
const rowText = (0, import_agent_qa_runner2.rowTextOf)(element, elements);
|
|
437
|
+
if (rowText !== void 0) entry.rowText = rowText;
|
|
438
|
+
if (node.bounds !== void 0) entry.bounds = node.bounds;
|
|
439
|
+
refs[ref] = entry;
|
|
440
|
+
}
|
|
441
|
+
return refs;
|
|
442
|
+
}
|
|
397
443
|
var ElementResolver = class {
|
|
398
444
|
constructor(page, refs) {
|
|
399
445
|
this.page = page;
|
|
@@ -404,16 +450,7 @@ var ElementResolver = class {
|
|
|
404
450
|
if (!data) {
|
|
405
451
|
throw new Error(`Unknown ref "${ref}". Available refs: ${Object.keys(this.refs).join(", ") || "none"}`);
|
|
406
452
|
}
|
|
407
|
-
|
|
408
|
-
if (data.name) {
|
|
409
|
-
locator = this.page.getByRole(data.role, { name: data.name, exact: true });
|
|
410
|
-
} else {
|
|
411
|
-
locator = this.page.getByRole(data.role);
|
|
412
|
-
}
|
|
413
|
-
if (data.nth !== void 0) {
|
|
414
|
-
locator = locator.nth(data.nth);
|
|
415
|
-
}
|
|
416
|
-
return locator;
|
|
453
|
+
return this.page.locator("aria-ref=" + data.nativeRef);
|
|
417
454
|
}
|
|
418
455
|
resolveAction(action) {
|
|
419
456
|
if (ACTIONS_WITHOUT_REF.has(action.type)) {
|
|
@@ -434,6 +471,120 @@ var ElementResolver = class {
|
|
|
434
471
|
}
|
|
435
472
|
};
|
|
436
473
|
|
|
474
|
+
// src/binding.ts
|
|
475
|
+
var import_agent_qa_runner3 = require("@vostride/agent-qa-runner");
|
|
476
|
+
var BINDING_LANES = ["ref", "fromRef", "toRef"];
|
|
477
|
+
var FRAME_REF = /^f\d+e/;
|
|
478
|
+
var INVALID_FRAME = /invalid frame/i;
|
|
479
|
+
var WHAT_CHANGED = {
|
|
480
|
+
"stale-observation": "the page holds a new document since the observation",
|
|
481
|
+
"frame-detached": "the frame that held the observed element is gone",
|
|
482
|
+
"binding-lost": "the observed element is no longer on the page",
|
|
483
|
+
"binding-changed": "the observed element now has a different role, name or row"
|
|
484
|
+
};
|
|
485
|
+
function hideAgentQaElements2() {
|
|
486
|
+
const doc = globalThis.document;
|
|
487
|
+
doc.querySelectorAll("[data-agent-qa-internal]").forEach((el) => {
|
|
488
|
+
el.style.display = "none";
|
|
489
|
+
el.setAttribute("aria-hidden", "true");
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
function restoreAgentQaElements2() {
|
|
493
|
+
const doc = globalThis.document;
|
|
494
|
+
doc.querySelectorAll("[data-agent-qa-internal]").forEach((el) => {
|
|
495
|
+
el.style.display = "";
|
|
496
|
+
el.removeAttribute("aria-hidden");
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
async function snapshotRoot(page, options = {}) {
|
|
500
|
+
const { timeoutMs, retryTimeoutMs } = options;
|
|
501
|
+
await page.evaluate(hideAgentQaElements2);
|
|
502
|
+
try {
|
|
503
|
+
if (timeoutMs === void 0) return await page.locator(":root").ariaSnapshot({ mode: "ai" });
|
|
504
|
+
return await page.locator(":root").ariaSnapshot({ mode: "ai", timeout: timeoutMs }).catch((error) => {
|
|
505
|
+
if (!(error instanceof Error && error.name === "TimeoutError")) throw error;
|
|
506
|
+
return page.locator(":root").ariaSnapshot({
|
|
507
|
+
mode: "ai",
|
|
508
|
+
...retryTimeoutMs !== void 0 ? { timeout: retryTimeoutMs } : {}
|
|
509
|
+
});
|
|
510
|
+
});
|
|
511
|
+
} finally {
|
|
512
|
+
await page.evaluate(restoreAgentQaElements2);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
async function captureEpoch(page) {
|
|
516
|
+
const document = await page.evaluateHandle(() => globalThis.document);
|
|
517
|
+
return { page, document };
|
|
518
|
+
}
|
|
519
|
+
async function disposeEpoch(epoch) {
|
|
520
|
+
try {
|
|
521
|
+
await epoch?.document.dispose();
|
|
522
|
+
} catch {
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
function isChildFrameRef(nativeRef) {
|
|
526
|
+
return FRAME_REF.test(nativeRef);
|
|
527
|
+
}
|
|
528
|
+
async function checkDocument(page, epoch) {
|
|
529
|
+
if (epoch === void 0 || epoch.page !== page) return { outcome: "stale-observation" };
|
|
530
|
+
try {
|
|
531
|
+
const same = await epoch.document.evaluate((d) => d === globalThis.document);
|
|
532
|
+
return same === true ? { outcome: "ok" } : { outcome: "stale-observation" };
|
|
533
|
+
} catch {
|
|
534
|
+
return { outcome: "stale-observation" };
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
async function frameDetached(page, nativeRef) {
|
|
538
|
+
if (!isChildFrameRef(nativeRef)) return false;
|
|
539
|
+
try {
|
|
540
|
+
await page.locator("aria-ref=" + nativeRef).count();
|
|
541
|
+
return false;
|
|
542
|
+
} catch (error) {
|
|
543
|
+
return error instanceof Error && INVALID_FRAME.test(error.message);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
function tidy(text) {
|
|
547
|
+
return text?.replace(/\s+/g, " ").trim();
|
|
548
|
+
}
|
|
549
|
+
async function checkBinding(page, epoch, entry, snapshot) {
|
|
550
|
+
const document = await checkDocument(page, epoch);
|
|
551
|
+
if (document.outcome !== "ok") return document;
|
|
552
|
+
const { nodes, nativeRefs } = parseAriaSnapshot(await snapshotRoot(page, snapshot));
|
|
553
|
+
const index = nativeRefs.indexOf(entry.nativeRef);
|
|
554
|
+
if (index < 0) {
|
|
555
|
+
const detached = await frameDetached(page, entry.nativeRef);
|
|
556
|
+
return { outcome: detached ? "frame-detached" : "binding-lost" };
|
|
557
|
+
}
|
|
558
|
+
const { state } = (0, import_agent_qa_runner3.buildScreenState)({
|
|
559
|
+
platform: "web",
|
|
560
|
+
nodes,
|
|
561
|
+
viewport: { width: 0, height: 0 },
|
|
562
|
+
scroll: { offsetX: 0, offsetY: 0, contentWidth: 0, contentHeight: 0 },
|
|
563
|
+
coordSpace: "viewport",
|
|
564
|
+
timestamp: 0
|
|
565
|
+
});
|
|
566
|
+
const current = state.elements[index];
|
|
567
|
+
if (current === void 0 || current.role !== entry.role || tidy(current.name) !== tidy(entry.name) || tidy((0, import_agent_qa_runner3.rowTextOf)(current, state.elements)) !== tidy(entry.rowText)) {
|
|
568
|
+
return { outcome: "binding-changed" };
|
|
569
|
+
}
|
|
570
|
+
return { outcome: "ok" };
|
|
571
|
+
}
|
|
572
|
+
function laneRefs(action) {
|
|
573
|
+
const refs = [];
|
|
574
|
+
for (const lane of BINDING_LANES) {
|
|
575
|
+
const ref = Reflect.get(action, lane);
|
|
576
|
+
if (typeof ref === "string") refs.push(ref);
|
|
577
|
+
}
|
|
578
|
+
return refs;
|
|
579
|
+
}
|
|
580
|
+
function bindingRefusal(outcome, ref) {
|
|
581
|
+
return {
|
|
582
|
+
success: false,
|
|
583
|
+
error: `${outcome}: ref ${ref} \u2014 ${WHAT_CHANGED[outcome]}; nothing was sent`,
|
|
584
|
+
metadata: { binding: { outcome, ref, inputSent: false } }
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
|
|
437
588
|
// src/action-validator.ts
|
|
438
589
|
var ActionValidationError = class extends Error {
|
|
439
590
|
suggestion;
|
|
@@ -445,8 +596,8 @@ var ActionValidationError = class extends Error {
|
|
|
445
596
|
};
|
|
446
597
|
var FILL_ROLES = /* @__PURE__ */ new Set(["textbox", "searchbox", "spinbutton", "combobox"]);
|
|
447
598
|
var SELECT_ROLES = /* @__PURE__ */ new Set(["listbox", "combobox"]);
|
|
448
|
-
var NO_REF_ACTIONS = /* @__PURE__ */ new Set(["scroll", "navigate", "waitFor", "delay", "waitForUrl", "
|
|
449
|
-
var ANY_ROLE_ACTIONS = /* @__PURE__ */ new Set(["click", "hover", "keypress", "paste", "doubleClick", "rightClick"]);
|
|
599
|
+
var NO_REF_ACTIONS = /* @__PURE__ */ new Set(["scroll", "navigate", "waitFor", "delay", "waitForUrl", "tab", "inspect", "route", "unroute", "networkState", "storageState", "handleDialog", "uniqueValue", "assertValue", "assertAI", "extractAI", "reportValue", "setCookies", "setLocalStorage", "executeScript", "setVariable"]);
|
|
600
|
+
var ANY_ROLE_ACTIONS = /* @__PURE__ */ new Set(["click", "hover", "keypress", "paste", "doubleClick", "rightClick", "focus"]);
|
|
450
601
|
function validateAction(action, elementRole) {
|
|
451
602
|
if (NO_REF_ACTIONS.has(action.type)) {
|
|
452
603
|
return { valid: true };
|
|
@@ -570,150 +721,534 @@ async function runReadinessChecks(page) {
|
|
|
570
721
|
}
|
|
571
722
|
}
|
|
572
723
|
|
|
724
|
+
// src/compound-web.ts
|
|
725
|
+
var import_agent_qa_runner4 = require("@vostride/agent-qa-runner");
|
|
726
|
+
|
|
573
727
|
// src/scroll-helper.ts
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
password: "textbox"
|
|
624
|
-
};
|
|
625
|
-
function getAccessibleName(el) {
|
|
626
|
-
const label = el.getAttribute("aria-label");
|
|
627
|
-
if (label) return label.trim();
|
|
628
|
-
const labelledBy = el.getAttribute("aria-labelledby");
|
|
629
|
-
if (labelledBy) {
|
|
630
|
-
const parts = labelledBy.split(/\s+/).map((id) => {
|
|
631
|
-
const ref = doc.getElementById(id);
|
|
632
|
-
return ref ? (ref.textContent || "").trim() : "";
|
|
633
|
-
}).filter(Boolean);
|
|
634
|
-
if (parts.length) return parts.join(" ");
|
|
728
|
+
var NOT_FOUND = { bounds: null, scrolledContainer: "notFound", scrolled: false };
|
|
729
|
+
var EVALUATE_TIMEOUT_MS = 1e3;
|
|
730
|
+
async function scrollWithRef(page, lastRefs, ref, deltaX, deltaY, scrollAxis) {
|
|
731
|
+
const nativeRef = lastRefs[ref]?.nativeRef;
|
|
732
|
+
if (nativeRef !== void 0 && isChildFrameRef(nativeRef)) return NOT_FOUND;
|
|
733
|
+
try {
|
|
734
|
+
const locator = new ElementResolver(page, lastRefs).resolve(ref);
|
|
735
|
+
if (await locator.count() === 0) return NOT_FOUND;
|
|
736
|
+
const scrolled = await locator.evaluate(
|
|
737
|
+
(target, args) => {
|
|
738
|
+
const [dx, dy, axis] = args;
|
|
739
|
+
const win = globalThis;
|
|
740
|
+
const doc = win.document;
|
|
741
|
+
const el = target;
|
|
742
|
+
const prop = axis === "horizontal" ? "scrollLeft" : "scrollTop";
|
|
743
|
+
const maxOf = (node) => axis === "horizontal" ? node.scrollWidth - node.clientWidth : node.scrollHeight - node.clientHeight;
|
|
744
|
+
const boundsOf = () => {
|
|
745
|
+
const rect = el.getBoundingClientRect();
|
|
746
|
+
return { x: Math.round(rect.x), y: Math.round(rect.y), width: Math.round(rect.width), height: Math.round(rect.height) };
|
|
747
|
+
};
|
|
748
|
+
const scrollable = (node) => {
|
|
749
|
+
const style = win.getComputedStyle(node);
|
|
750
|
+
const overflow = axis === "horizontal" ? style.overflowX : style.overflowY;
|
|
751
|
+
return (overflow === "auto" || overflow === "scroll" || overflow === "hidden") && maxOf(node) > 0;
|
|
752
|
+
};
|
|
753
|
+
const scroller = doc.scrollingElement;
|
|
754
|
+
const rootStyle = win.getComputedStyle(doc.documentElement);
|
|
755
|
+
const bodyPropagates = rootStyle.overflowX === "visible" && rootStyle.overflowY === "visible";
|
|
756
|
+
const isDocumentScroller = (node) => node === doc.documentElement || node === scroller || bodyPropagates && node === doc.body;
|
|
757
|
+
const parentOf = (node) => {
|
|
758
|
+
if (node.assignedSlot) return node.assignedSlot;
|
|
759
|
+
if (node.parentElement) return node.parentElement;
|
|
760
|
+
const parent = node.parentNode;
|
|
761
|
+
return parent && parent.host ? parent.host : null;
|
|
762
|
+
};
|
|
763
|
+
if (!isDocumentScroller(el)) {
|
|
764
|
+
if (scrollable(el)) {
|
|
765
|
+
const before2 = el[prop];
|
|
766
|
+
el.scrollBy(dx, dy);
|
|
767
|
+
const after2 = el[prop];
|
|
768
|
+
return { bounds: boundsOf(), scrolledContainer: "element", scrolled: before2 !== after2, offsets: { before: before2, after: after2, max: maxOf(el) } };
|
|
769
|
+
}
|
|
770
|
+
for (let parent = parentOf(el); parent && !isDocumentScroller(parent); parent = parentOf(parent)) {
|
|
771
|
+
if (!scrollable(parent)) continue;
|
|
772
|
+
const before2 = parent[prop];
|
|
773
|
+
parent.scrollBy(dx, dy);
|
|
774
|
+
const after2 = parent[prop];
|
|
775
|
+
return { bounds: boundsOf(), scrolledContainer: "ancestor", scrolled: before2 !== after2, offsets: { before: before2, after: after2, max: maxOf(parent) } };
|
|
776
|
+
}
|
|
635
777
|
}
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
}
|
|
642
|
-
function getImplicitRole(el) {
|
|
643
|
-
const tag = el.tagName.toLowerCase();
|
|
644
|
-
if (tag === "input") {
|
|
645
|
-
const type = (el.getAttribute("type") || "text").toLowerCase();
|
|
646
|
-
return INPUT_TYPE_ROLES[type] || "textbox";
|
|
778
|
+
if (!scroller) {
|
|
779
|
+
const x = win.scrollX;
|
|
780
|
+
const y = win.scrollY;
|
|
781
|
+
win.scrollBy(dx, dy);
|
|
782
|
+
return { bounds: boundsOf(), scrolledContainer: "documentElement", scrolled: x !== win.scrollX || y !== win.scrollY };
|
|
647
783
|
}
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
doc.querySelectorAll(`[role="${role}"]`).forEach((el) => candidates.push(el));
|
|
653
|
-
doc.querySelectorAll("*").forEach((el) => {
|
|
654
|
-
if (el.getAttribute("role")) return;
|
|
655
|
-
if (getImplicitRole(el) === role) candidates.push(el);
|
|
656
|
-
});
|
|
657
|
-
let matched = candidates;
|
|
658
|
-
if (name) {
|
|
659
|
-
matched = candidates.filter(
|
|
660
|
-
(el) => getAccessibleName(el).toLowerCase().includes(name.toLowerCase())
|
|
661
|
-
);
|
|
662
|
-
}
|
|
663
|
-
const target = matched[nth];
|
|
664
|
-
if (!target) {
|
|
665
|
-
win.scrollBy(dx, dy);
|
|
666
|
-
return { bounds: null, scrolledContainer: "notFound", scrolled: true };
|
|
667
|
-
}
|
|
668
|
-
const targetStyle = win.getComputedStyle(target);
|
|
669
|
-
const targetOverflow = axis === "horizontal" ? targetStyle.overflowX : targetStyle.overflowY;
|
|
670
|
-
const targetScrollable = targetOverflow === "auto" || targetOverflow === "scroll" || targetOverflow === "hidden";
|
|
671
|
-
const targetHasContent = axis === "horizontal" ? target.scrollWidth > target.clientWidth : target.scrollHeight > target.clientHeight;
|
|
672
|
-
if (targetScrollable && targetHasContent) {
|
|
673
|
-
const prop2 = axis === "horizontal" ? "scrollLeft" : "scrollTop";
|
|
674
|
-
const before2 = target[prop2];
|
|
675
|
-
target.scrollBy(dx, dy);
|
|
676
|
-
const after2 = target[prop2];
|
|
677
|
-
const rect2 = target.getBoundingClientRect();
|
|
784
|
+
const before = scroller[prop];
|
|
785
|
+
scroller.scrollBy(dx, dy);
|
|
786
|
+
const after = scroller[prop];
|
|
787
|
+
const max = maxOf(scroller);
|
|
678
788
|
return {
|
|
679
|
-
bounds:
|
|
680
|
-
scrolledContainer: "
|
|
681
|
-
scrolled:
|
|
789
|
+
bounds: boundsOf(),
|
|
790
|
+
scrolledContainer: "documentElement",
|
|
791
|
+
scrolled: before !== after,
|
|
792
|
+
...max > 0 ? { offsets: { before, after, max } } : {}
|
|
682
793
|
};
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
794
|
+
},
|
|
795
|
+
[deltaX, deltaY, scrollAxis],
|
|
796
|
+
{ timeout: EVALUATE_TIMEOUT_MS }
|
|
797
|
+
);
|
|
798
|
+
return scrolled && typeof scrolled === "object" ? scrolled : NOT_FOUND;
|
|
799
|
+
} catch {
|
|
800
|
+
return NOT_FOUND;
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
// src/compound-web.ts
|
|
805
|
+
async function safeInputValue(locator) {
|
|
806
|
+
try {
|
|
807
|
+
return await locator.inputValue();
|
|
808
|
+
} catch {
|
|
809
|
+
try {
|
|
810
|
+
const value = await locator.evaluate((el) => {
|
|
811
|
+
const node = el;
|
|
812
|
+
return node.value ?? node.textContent;
|
|
813
|
+
});
|
|
814
|
+
return typeof value === "string" ? value : void 0;
|
|
815
|
+
} catch {
|
|
816
|
+
return void 0;
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
async function runWebFillCompound(locator, boundingBox, action) {
|
|
821
|
+
const isAppend = action.clearMode === "append";
|
|
822
|
+
const prior = isAppend ? await safeInputValue(locator) ?? "" : "";
|
|
823
|
+
const secure = await locator.getAttribute("type") === "password";
|
|
824
|
+
if (isAppend) {
|
|
825
|
+
await locator.click();
|
|
826
|
+
await locator.press("End");
|
|
827
|
+
await locator.pressSequentially(action.text);
|
|
828
|
+
} else {
|
|
829
|
+
await locator.fill(action.text);
|
|
830
|
+
}
|
|
831
|
+
const expected = isAppend ? prior + action.text : action.text;
|
|
832
|
+
const actual = await safeInputValue(locator);
|
|
833
|
+
const box = boundingBox ? { boundingBox } : {};
|
|
834
|
+
if (actual === void 0) {
|
|
835
|
+
return { success: true, ...box, metadata: { fillVerdict: { verb: "fill", verdict: "unverified" } } };
|
|
836
|
+
}
|
|
837
|
+
const verdict = (0, import_agent_qa_runner4.compareFillReread)({ expected, actual, secure });
|
|
838
|
+
if (verdict.ok && action.submit) {
|
|
839
|
+
await locator.press("Enter");
|
|
840
|
+
}
|
|
841
|
+
const metadata = {
|
|
842
|
+
...verdict.meta ? { fillReread: verdict.meta } : {},
|
|
843
|
+
fillVerdict: {
|
|
844
|
+
verb: "fill",
|
|
845
|
+
verdict: verdict.verdict,
|
|
846
|
+
...verdict.secure ? { secure: true } : {}
|
|
847
|
+
}
|
|
848
|
+
};
|
|
849
|
+
return verdict.ok ? { success: true, ...box, metadata } : { success: false, error: verdict.selfCorrectingError, ...box, metadata };
|
|
850
|
+
}
|
|
851
|
+
function deltaFor(direction, magnitude) {
|
|
852
|
+
switch (direction) {
|
|
853
|
+
case "down":
|
|
854
|
+
return { dx: 0, dy: magnitude, axis: "vertical" };
|
|
855
|
+
case "up":
|
|
856
|
+
return { dx: 0, dy: -magnitude, axis: "vertical" };
|
|
857
|
+
case "right":
|
|
858
|
+
return { dx: magnitude, dy: 0, axis: "horizontal" };
|
|
859
|
+
case "left":
|
|
860
|
+
return { dx: -magnitude, dy: 0, axis: "horizontal" };
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
async function settle(page) {
|
|
864
|
+
await page.evaluate(`new Promise(resolve => {
|
|
865
|
+
const t = setTimeout(() => resolve('timeout'), 500);
|
|
866
|
+
requestAnimationFrame(() => requestAnimationFrame(() => {
|
|
867
|
+
clearTimeout(t);
|
|
868
|
+
resolve('raf');
|
|
869
|
+
}));
|
|
870
|
+
})`);
|
|
871
|
+
}
|
|
872
|
+
function snapshotScrollers(axis) {
|
|
873
|
+
const g = globalThis;
|
|
874
|
+
const doc = g.document;
|
|
875
|
+
const root = doc.scrollingElement ?? doc.documentElement;
|
|
876
|
+
const read = (el) => ({
|
|
877
|
+
x: el.scrollLeft,
|
|
878
|
+
y: el.scrollTop,
|
|
879
|
+
maxX: el.scrollWidth - el.clientWidth,
|
|
880
|
+
maxY: el.scrollHeight - el.clientHeight
|
|
881
|
+
});
|
|
882
|
+
const scrolls = (value) => value === "auto" || value === "scroll" || value === "overlay";
|
|
883
|
+
const rootStyle = g.getComputedStyle(doc.documentElement);
|
|
884
|
+
const bodyPropagates = rootStyle.overflowX === "visible" && rootStyle.overflowY === "visible";
|
|
885
|
+
const offsets = /* @__PURE__ */ new Map();
|
|
886
|
+
offsets.set(root, read(root));
|
|
887
|
+
const all = doc.querySelectorAll("*");
|
|
888
|
+
for (let i = 0; i < all.length; i += 1) {
|
|
889
|
+
const el = all[i];
|
|
890
|
+
if (el === root || el === doc.documentElement) continue;
|
|
891
|
+
if (el === doc.body && bodyPropagates) continue;
|
|
892
|
+
const o = read(el);
|
|
893
|
+
if (!((axis === "vertical" ? o.maxY : o.maxX) > 0)) continue;
|
|
894
|
+
const style = g.getComputedStyle(el);
|
|
895
|
+
if (!scrolls(axis === "vertical" ? style.overflowY : style.overflowX)) continue;
|
|
896
|
+
offsets.set(el, o);
|
|
897
|
+
}
|
|
898
|
+
const state = {
|
|
899
|
+
offsets,
|
|
900
|
+
target: null,
|
|
901
|
+
listener: () => void 0
|
|
902
|
+
};
|
|
903
|
+
state.listener = (event) => {
|
|
904
|
+
state.target = event.target;
|
|
905
|
+
};
|
|
906
|
+
g.addEventListener("wheel", state.listener, { capture: true, once: true, passive: true });
|
|
907
|
+
return state;
|
|
908
|
+
}
|
|
909
|
+
function readPointerChain(state) {
|
|
910
|
+
const g = globalThis;
|
|
911
|
+
const s = state;
|
|
912
|
+
if (!s.target) {
|
|
913
|
+
g.removeEventListener("wheel", s.listener, { capture: true });
|
|
914
|
+
return void 0;
|
|
915
|
+
}
|
|
916
|
+
const root = g.document.scrollingElement ?? g.document.documentElement;
|
|
917
|
+
const chain = [];
|
|
918
|
+
for (let el = s.target; el; el = el.parentElement) {
|
|
919
|
+
if (s.offsets.has(el) && !chain.includes(el)) chain.push(el);
|
|
920
|
+
}
|
|
921
|
+
if (!chain.includes(root)) chain.push(root);
|
|
922
|
+
return {
|
|
923
|
+
before: chain.map((el) => s.offsets.get(el)),
|
|
924
|
+
after: chain.map((el) => ({
|
|
925
|
+
x: el.scrollLeft,
|
|
926
|
+
y: el.scrollTop,
|
|
927
|
+
maxX: el.scrollWidth - el.clientWidth,
|
|
928
|
+
maxY: el.scrollHeight - el.clientHeight
|
|
929
|
+
}))
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
async function wheelPointerChain(page, dx, dy, axis) {
|
|
933
|
+
let handle;
|
|
934
|
+
try {
|
|
935
|
+
handle = await page.evaluateHandle(snapshotScrollers, axis);
|
|
936
|
+
} catch {
|
|
937
|
+
handle = void 0;
|
|
938
|
+
}
|
|
939
|
+
await page.mouse.wheel(dx, dy);
|
|
940
|
+
await settle(page);
|
|
941
|
+
if (!handle) return void 0;
|
|
942
|
+
try {
|
|
943
|
+
const chain = await handle.evaluate(readPointerChain);
|
|
944
|
+
if (!chain || typeof chain !== "object" || !Array.isArray(chain.before) || !Array.isArray(chain.after)) {
|
|
945
|
+
return void 0;
|
|
946
|
+
}
|
|
947
|
+
return { before: chain.before, after: chain.after };
|
|
948
|
+
} catch {
|
|
949
|
+
return void 0;
|
|
950
|
+
} finally {
|
|
951
|
+
await handle.dispose().catch(() => void 0);
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
var FRAME_PROBE_TIMEOUT_MS = 1e3;
|
|
955
|
+
function judgeCandidates(elements) {
|
|
956
|
+
const g = globalThis;
|
|
957
|
+
const doc = g.document;
|
|
958
|
+
const root = doc.documentElement;
|
|
959
|
+
const rootStyle = g.getComputedStyle(root);
|
|
960
|
+
const bodyPropagates = rootStyle.overflowX === "visible" && rootStyle.overflowY === "visible";
|
|
961
|
+
const parentOf = (node) => node.assignedSlot ?? node.parentElement ?? node.parentNode?.host ?? null;
|
|
962
|
+
const clips = (node) => {
|
|
963
|
+
const style = g.getComputedStyle(node);
|
|
964
|
+
return style.overflowX !== "visible" || style.overflowY !== "visible";
|
|
965
|
+
};
|
|
966
|
+
const escapes = (node) => {
|
|
967
|
+
const position = g.getComputedStyle(node).position;
|
|
968
|
+
return position === "fixed" || position === "absolute";
|
|
969
|
+
};
|
|
970
|
+
const contentRects = (node) => {
|
|
971
|
+
const range = doc.createRange();
|
|
972
|
+
range.selectNodeContents(node);
|
|
973
|
+
return Array.from(range.getClientRects());
|
|
974
|
+
};
|
|
975
|
+
const replaced = /^(IMG|VIDEO|CANVAS|IFRAME|EMBED|OBJECT|SVG|INPUT|TEXTAREA|SELECT)$/;
|
|
976
|
+
const unpainted = (s) => s.backgroundImage === "none" && /^(transparent|rgba\(.*,\s*0\))$/.test(s.backgroundColor);
|
|
977
|
+
const emptyPseudo = (p) => p.content === "none" || p.content === "normal" || p.content === '""' && unpainted(p);
|
|
978
|
+
const transparent = (node, x, y) => {
|
|
979
|
+
const s = g.getComputedStyle(node);
|
|
980
|
+
if (s.opacity === "0") return true;
|
|
981
|
+
if (node.namespaceURI !== "http://www.w3.org/1999/xhtml") return false;
|
|
982
|
+
if (replaced.test(node.tagName)) return false;
|
|
983
|
+
if (node.shadowRoot || node.tagName.includes("-")) return false;
|
|
984
|
+
if (!unpainted(s) || s.backdropFilter !== "none") return false;
|
|
985
|
+
if (!emptyPseudo(g.getComputedStyle(node, "::before"))) return false;
|
|
986
|
+
if (!emptyPseudo(g.getComputedStyle(node, "::after"))) return false;
|
|
987
|
+
return !contentRects(node).some(
|
|
988
|
+
(t) => t.left <= x && x <= t.right && t.top <= y && y <= t.bottom
|
|
989
|
+
);
|
|
990
|
+
};
|
|
991
|
+
const judge = (el) => {
|
|
992
|
+
const style = g.getComputedStyle(el);
|
|
993
|
+
if (style.visibility !== "visible") return { state: "hidden" };
|
|
994
|
+
for (let n = el; n; n = parentOf(n)) {
|
|
995
|
+
if (g.getComputedStyle(n).opacity === "0") return { state: "hidden" };
|
|
996
|
+
}
|
|
997
|
+
let r = el.getBoundingClientRect();
|
|
998
|
+
const byText = style.display === "contents" || r.width === 0 || r.height === 0;
|
|
999
|
+
if (byText) {
|
|
1000
|
+
const clipped = clips(el) || style.clip !== "auto" || style.clipPath !== "none";
|
|
1001
|
+
if (style.display !== "contents" && clipped) return { state: "unknown" };
|
|
1002
|
+
const rects = contentRects(el).filter((t) => t.width > 0 && t.height > 0);
|
|
1003
|
+
if (rects.length === 0) return { state: "unknown" };
|
|
1004
|
+
r = {
|
|
1005
|
+
left: Math.min(...rects.map((t) => t.left)),
|
|
1006
|
+
top: Math.min(...rects.map((t) => t.top)),
|
|
1007
|
+
right: Math.max(...rects.map((t) => t.right)),
|
|
1008
|
+
bottom: Math.max(...rects.map((t) => t.bottom))
|
|
1009
|
+
};
|
|
1010
|
+
} else if (r.width < 2 || r.height < 2) {
|
|
1011
|
+
return { state: "hidden" };
|
|
1012
|
+
}
|
|
1013
|
+
let left = Math.max(r.left, 0);
|
|
1014
|
+
let top = Math.max(r.top, 0);
|
|
1015
|
+
let right = Math.min(r.right, g.innerWidth);
|
|
1016
|
+
let bottom = Math.min(r.bottom, g.innerHeight);
|
|
1017
|
+
if (!(right > left && bottom > top)) return { state: "hidden" };
|
|
1018
|
+
let positioned = escapes(el);
|
|
1019
|
+
for (let a = parentOf(el); a && a !== root; a = parentOf(a)) {
|
|
1020
|
+
if (!(a === doc.body && bodyPropagates) && !positioned && clips(a)) {
|
|
1021
|
+
const b = a.getBoundingClientRect();
|
|
1022
|
+
const cl = b.left + a.clientLeft;
|
|
1023
|
+
const ct = b.top + a.clientTop;
|
|
1024
|
+
left = Math.max(left, cl);
|
|
1025
|
+
top = Math.max(top, ct);
|
|
1026
|
+
right = Math.min(right, cl + a.clientWidth);
|
|
1027
|
+
bottom = Math.min(bottom, ct + a.clientHeight);
|
|
1028
|
+
if (!(right > left && bottom > top)) return { state: "unknown" };
|
|
1029
|
+
}
|
|
1030
|
+
if (escapes(a)) positioned = true;
|
|
1031
|
+
}
|
|
1032
|
+
if (right - left < 2 || bottom - top < 2) return { state: "unknown" };
|
|
1033
|
+
const x = (left + right) / 2;
|
|
1034
|
+
const y = (top + bottom) / 2;
|
|
1035
|
+
const seen = { state: "visible", rect: { left, top, right, bottom } };
|
|
1036
|
+
for (const hit of el.getRootNode().elementsFromPoint(x, y)) {
|
|
1037
|
+
if (hit === el || el.contains(hit)) return seen;
|
|
1038
|
+
if (style.pointerEvents === "none" || byText) {
|
|
1039
|
+
for (let a = parentOf(el); a; a = parentOf(a)) {
|
|
1040
|
+
if (a === hit) return seen;
|
|
701
1041
|
}
|
|
702
|
-
parent = parent.parentElement;
|
|
703
1042
|
}
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
1043
|
+
if (!transparent(hit, x, y)) return { state: "unknown" };
|
|
1044
|
+
}
|
|
1045
|
+
return { state: "unknown" };
|
|
1046
|
+
};
|
|
1047
|
+
return elements.map(judge);
|
|
1048
|
+
}
|
|
1049
|
+
function projectThroughFrame(frameElement, box) {
|
|
1050
|
+
const g = globalThis;
|
|
1051
|
+
const doc = g.document;
|
|
1052
|
+
const root = doc.documentElement;
|
|
1053
|
+
const el = frameElement;
|
|
1054
|
+
const rootStyle = g.getComputedStyle(root);
|
|
1055
|
+
const bodyPropagates = rootStyle.overflowX === "visible" && rootStyle.overflowY === "visible";
|
|
1056
|
+
const parentOf = (node) => node.assignedSlot ?? node.parentElement ?? node.parentNode?.host ?? null;
|
|
1057
|
+
const clips = (node) => {
|
|
1058
|
+
const style2 = g.getComputedStyle(node);
|
|
1059
|
+
return style2.overflowX !== "visible" || style2.overflowY !== "visible";
|
|
1060
|
+
};
|
|
1061
|
+
if (g.getComputedStyle(el).visibility !== "visible") return null;
|
|
1062
|
+
for (let n = el; n; n = parentOf(n)) {
|
|
1063
|
+
if (g.getComputedStyle(n).opacity === "0") return null;
|
|
1064
|
+
}
|
|
1065
|
+
for (let n = el; n; n = parentOf(n)) {
|
|
1066
|
+
const style2 = g.getComputedStyle(n);
|
|
1067
|
+
if (style2.transform !== "none") return null;
|
|
1068
|
+
const zoom = Number.parseFloat(style2.zoom);
|
|
1069
|
+
if (Number.isFinite(zoom) && zoom !== 1) return null;
|
|
1070
|
+
}
|
|
1071
|
+
const style = g.getComputedStyle(el);
|
|
1072
|
+
const padLeft = Number.parseFloat(style.paddingLeft) || 0;
|
|
1073
|
+
const padTop = Number.parseFloat(style.paddingTop) || 0;
|
|
1074
|
+
const padRight = Number.parseFloat(style.paddingRight) || 0;
|
|
1075
|
+
const padBottom = Number.parseFloat(style.paddingBottom) || 0;
|
|
1076
|
+
const r = el.getBoundingClientRect();
|
|
1077
|
+
const ox = r.left + el.clientLeft + padLeft;
|
|
1078
|
+
const oy = r.top + el.clientTop + padTop;
|
|
1079
|
+
let left = Math.max(box.left + ox, ox, 0);
|
|
1080
|
+
let top = Math.max(box.top + oy, oy, 0);
|
|
1081
|
+
let right = Math.min(box.right + ox, ox + el.clientWidth - padLeft - padRight, g.innerWidth);
|
|
1082
|
+
let bottom = Math.min(box.bottom + oy, oy + el.clientHeight - padTop - padBottom, g.innerHeight);
|
|
1083
|
+
if (!(right > left && bottom > top)) return null;
|
|
1084
|
+
for (let a = parentOf(el); a && a !== root; a = parentOf(a)) {
|
|
1085
|
+
if (a === doc.body && bodyPropagates) continue;
|
|
1086
|
+
if (!clips(a)) continue;
|
|
1087
|
+
const b = a.getBoundingClientRect();
|
|
1088
|
+
const cl = b.left + a.clientLeft;
|
|
1089
|
+
const ct = b.top + a.clientTop;
|
|
1090
|
+
left = Math.max(left, cl);
|
|
1091
|
+
top = Math.max(top, ct);
|
|
1092
|
+
right = Math.min(right, cl + a.clientWidth);
|
|
1093
|
+
bottom = Math.min(bottom, ct + a.clientHeight);
|
|
1094
|
+
if (!(right > left && bottom > top)) return null;
|
|
1095
|
+
}
|
|
1096
|
+
const hit = el.getRootNode().elementFromPoint((left + right) / 2, (top + bottom) / 2);
|
|
1097
|
+
if (hit !== el) return null;
|
|
1098
|
+
return { left, top, right, bottom };
|
|
1099
|
+
}
|
|
1100
|
+
function candidatesIn(scope, target) {
|
|
1101
|
+
return scope.getByText(target).or(scope.getByLabel(target)).or(scope.getByAltText(target)).or(scope.getByTitle(target));
|
|
1102
|
+
}
|
|
1103
|
+
async function judgeIn(scope, target) {
|
|
1104
|
+
const judged = await candidatesIn(scope, target).evaluateAll(judgeCandidates);
|
|
1105
|
+
if (!Array.isArray(judged)) throw new Error("probe answered no list");
|
|
1106
|
+
return judged.map((candidate) => {
|
|
1107
|
+
const state = candidate?.state;
|
|
1108
|
+
if (state === "visible" && candidate?.rect) return { state, rect: candidate.rect };
|
|
1109
|
+
return { state: state === "hidden" ? "hidden" : "unknown" };
|
|
1110
|
+
});
|
|
1111
|
+
}
|
|
1112
|
+
function combine(states) {
|
|
1113
|
+
if (states.includes("visible")) return "visible";
|
|
1114
|
+
if (states.includes("unknown")) return "unknown";
|
|
1115
|
+
return "hidden";
|
|
1116
|
+
}
|
|
1117
|
+
async function projectToMainFrame(frame, box) {
|
|
1118
|
+
let current = frame;
|
|
1119
|
+
let projected = box;
|
|
1120
|
+
for (let parent = current.parentFrame(); parent; parent = current.parentFrame()) {
|
|
1121
|
+
const handle = await current.frameElement();
|
|
1122
|
+
try {
|
|
1123
|
+
const next = await handle.evaluate(projectThroughFrame, projected);
|
|
1124
|
+
if (!next || typeof next !== "object") return false;
|
|
1125
|
+
projected = next;
|
|
1126
|
+
} finally {
|
|
1127
|
+
await handle.dispose().catch(() => void 0);
|
|
1128
|
+
}
|
|
1129
|
+
current = parent;
|
|
1130
|
+
}
|
|
1131
|
+
return true;
|
|
1132
|
+
}
|
|
1133
|
+
async function probeChildFrame(frame, target) {
|
|
1134
|
+
try {
|
|
1135
|
+
const states = [];
|
|
1136
|
+
for (const candidate of await judgeIn(frame, target)) {
|
|
1137
|
+
if (candidate.state === "visible" && candidate.rect && await projectToMainFrame(frame, candidate.rect)) {
|
|
1138
|
+
return "visible";
|
|
1139
|
+
}
|
|
1140
|
+
states.push("unknown");
|
|
1141
|
+
}
|
|
1142
|
+
return combine(states);
|
|
1143
|
+
} catch {
|
|
1144
|
+
return "unknown";
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
async function probeChildFrameBounded(frame, target, pending) {
|
|
1148
|
+
if (frame.url() === "" || pending.has(frame)) return "unknown";
|
|
1149
|
+
const liveness = frame.evaluate("1");
|
|
1150
|
+
let timer;
|
|
1151
|
+
const bound = new Promise((resolve) => {
|
|
1152
|
+
timer = setTimeout(() => {
|
|
1153
|
+
resolve("timeout");
|
|
1154
|
+
}, FRAME_PROBE_TIMEOUT_MS);
|
|
1155
|
+
});
|
|
1156
|
+
let answer;
|
|
1157
|
+
try {
|
|
1158
|
+
answer = await Promise.race([liveness.then(() => "alive"), bound]);
|
|
1159
|
+
} catch {
|
|
1160
|
+
return "unknown";
|
|
1161
|
+
} finally {
|
|
1162
|
+
clearTimeout(timer);
|
|
1163
|
+
}
|
|
1164
|
+
if (answer === "alive") return await probeChildFrame(frame, target);
|
|
1165
|
+
pending.set(frame, liveness);
|
|
1166
|
+
const release = () => {
|
|
1167
|
+
if (pending.get(frame) === liveness) pending.delete(frame);
|
|
1168
|
+
};
|
|
1169
|
+
liveness.then(release, release);
|
|
1170
|
+
return "unknown";
|
|
1171
|
+
}
|
|
1172
|
+
async function probeOnScreen(page, target, pending) {
|
|
1173
|
+
let states;
|
|
1174
|
+
try {
|
|
1175
|
+
states = (await judgeIn(page, target)).map((candidate) => candidate.state);
|
|
1176
|
+
} catch {
|
|
1177
|
+
return "unknown";
|
|
1178
|
+
}
|
|
1179
|
+
if (combine(states) === "visible") return "visible";
|
|
1180
|
+
try {
|
|
1181
|
+
const main = page.mainFrame();
|
|
1182
|
+
const children = page.frames().filter((frame) => frame !== main && !frame.isDetached());
|
|
1183
|
+
states.push(...await Promise.all(children.map((frame) => probeChildFrameBounded(frame, target, pending))));
|
|
1184
|
+
} catch {
|
|
1185
|
+
states.push("unknown");
|
|
1186
|
+
}
|
|
1187
|
+
return combine(states);
|
|
1188
|
+
}
|
|
1189
|
+
function buildScrollPrimitives(page, lastRefs, magnitude) {
|
|
1190
|
+
const pending = /* @__PURE__ */ new WeakMap();
|
|
1191
|
+
return {
|
|
1192
|
+
async swipeOnce(direction, ref) {
|
|
1193
|
+
const { dx, dy, axis } = deltaFor(direction, magnitude);
|
|
1194
|
+
if (ref) {
|
|
1195
|
+
const scrolled = await scrollWithRef(page, lastRefs, ref, dx, dy, axis);
|
|
1196
|
+
if (scrolled.scrolledContainer !== "notFound") await settle(page);
|
|
1197
|
+
return (0, import_agent_qa_runner4.swipeSignalFromOffsets)(direction, scrolled.offsets);
|
|
1198
|
+
}
|
|
1199
|
+
const chain = await wheelPointerChain(page, dx, dy, axis);
|
|
1200
|
+
return (0, import_agent_qa_runner4.scrollerSetSwipeSignal)(direction, chain?.before, chain?.after);
|
|
1201
|
+
},
|
|
1202
|
+
async nativeSearchScroll(target) {
|
|
1203
|
+
try {
|
|
1204
|
+
await page.getByText(target).first().scrollIntoViewIfNeeded();
|
|
1205
|
+
return true;
|
|
1206
|
+
} catch {
|
|
1207
|
+
return false;
|
|
1208
|
+
}
|
|
1209
|
+
},
|
|
1210
|
+
async readInterimTree() {
|
|
1211
|
+
const tree = await page.locator(":root").ariaSnapshot({ mode: "ai" });
|
|
1212
|
+
return parseAriaSnapshot(tree).nodes;
|
|
714
1213
|
},
|
|
715
|
-
|
|
716
|
-
|
|
1214
|
+
async probeTargetVisible(target) {
|
|
1215
|
+
return probeOnScreen(page, target, pending);
|
|
1216
|
+
}
|
|
1217
|
+
};
|
|
1218
|
+
}
|
|
1219
|
+
async function runWebScrollCompound(page, lastRefs, action) {
|
|
1220
|
+
const viewport = page.viewportSize() ?? { width: 1280, height: 720 };
|
|
1221
|
+
const horizontal = action.direction === "left" || action.direction === "right";
|
|
1222
|
+
if (action.mode === "untilVisible") {
|
|
1223
|
+
const magnitude2 = horizontal ? viewport.width : viewport.height;
|
|
1224
|
+
const primitives2 = buildScrollPrimitives(page, lastRefs, magnitude2);
|
|
1225
|
+
const { meta: meta2 } = await (0, import_agent_qa_runner4.runUntilVisibleLoop)(primitives2, {
|
|
1226
|
+
target: action.text,
|
|
1227
|
+
direction: action.direction,
|
|
1228
|
+
maxSwipes: action.maxSwipes,
|
|
1229
|
+
retryOpposite: action.retryOpposite,
|
|
1230
|
+
...action.ref ? { ref: action.ref } : {}
|
|
1231
|
+
});
|
|
1232
|
+
return { success: true, metadata: { scrollUntilVisible: meta2 } };
|
|
1233
|
+
}
|
|
1234
|
+
const factor = action.amount === "halfPage" ? 0.5 : 1;
|
|
1235
|
+
const magnitude = Math.round((horizontal ? viewport.width : viewport.height) * factor);
|
|
1236
|
+
const primitives = buildScrollPrimitives(page, lastRefs, magnitude);
|
|
1237
|
+
if (action.amount === "toEnd") {
|
|
1238
|
+
const { meta: meta2 } = await (0, import_agent_qa_runner4.runToEndScroll)(primitives, action.direction, action.ref);
|
|
1239
|
+
return { success: true, metadata: { scrollUntilVisible: meta2 } };
|
|
1240
|
+
}
|
|
1241
|
+
const { meta } = await (0, import_agent_qa_runner4.runDirectionScroll)(primitives, {
|
|
1242
|
+
direction: action.direction,
|
|
1243
|
+
...action.ref ? { ref: action.ref } : {}
|
|
1244
|
+
});
|
|
1245
|
+
return { success: true, metadata: { scrollUntilVisible: meta } };
|
|
1246
|
+
}
|
|
1247
|
+
function webOpenAppError() {
|
|
1248
|
+
return {
|
|
1249
|
+
success: false,
|
|
1250
|
+
error: "openApp is not executable on web \u2014 use navigate(url) with the target app's web address instead."
|
|
1251
|
+
};
|
|
717
1252
|
}
|
|
718
1253
|
|
|
719
1254
|
// src/browser-install-error.ts
|
|
@@ -738,29 +1273,856 @@ function formatMissingBrowserError(browserName) {
|
|
|
738
1273
|
].join("\n");
|
|
739
1274
|
}
|
|
740
1275
|
|
|
741
|
-
// src/
|
|
742
|
-
|
|
743
|
-
|
|
1276
|
+
// src/recording.ts
|
|
1277
|
+
var import_node_crypto = require("crypto");
|
|
1278
|
+
var import_promises = require("fs/promises");
|
|
1279
|
+
var import_node_path = require("path");
|
|
1280
|
+
var RECORDING_DEFAULT_SIZE = { width: 1280, height: 720 };
|
|
1281
|
+
function jpegSize(bytes) {
|
|
1282
|
+
if (bytes[0] !== 255 || bytes[1] !== 216) return void 0;
|
|
1283
|
+
for (let i = 2; i + 8 < bytes.length; ) {
|
|
1284
|
+
if (bytes[i++] !== 255) return void 0;
|
|
1285
|
+
const marker = bytes[i++];
|
|
1286
|
+
const length = bytes.readUInt16BE(i);
|
|
1287
|
+
if (length < 2 || i + length > bytes.length) return void 0;
|
|
1288
|
+
if ([192, 193, 194].includes(marker)) {
|
|
1289
|
+
return { height: bytes.readUInt16BE(i + 3), width: bytes.readUInt16BE(i + 5) };
|
|
1290
|
+
}
|
|
1291
|
+
i += length;
|
|
1292
|
+
}
|
|
1293
|
+
return void 0;
|
|
744
1294
|
}
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
1295
|
+
function createScreencastRecording(getPage, config, getPageRevision) {
|
|
1296
|
+
let active;
|
|
1297
|
+
let recovery;
|
|
1298
|
+
let busy = false;
|
|
1299
|
+
let firstFrameEpoch;
|
|
1300
|
+
let scene;
|
|
1301
|
+
let sceneRevision = 0;
|
|
1302
|
+
let bracket;
|
|
1303
|
+
async function recover() {
|
|
1304
|
+
if (!recovery) return;
|
|
1305
|
+
await recovery.page.screencast.stop();
|
|
1306
|
+
await (0, import_promises.rm)(recovery.stagingPath, { force: true });
|
|
1307
|
+
recovery = void 0;
|
|
1308
|
+
}
|
|
1309
|
+
return {
|
|
1310
|
+
async start() {
|
|
1311
|
+
if (busy || active) throw new Error("Recording already started");
|
|
1312
|
+
busy = true;
|
|
1313
|
+
try {
|
|
1314
|
+
await recover();
|
|
1315
|
+
const page = getPage();
|
|
1316
|
+
if (!page) throw new Error("Recording requires an initialized page");
|
|
1317
|
+
const stagingPath = (0, import_node_path.join)(config.videoDir, `${(0, import_node_crypto.randomUUID)()}.webm`);
|
|
1318
|
+
const size = config.videoSize ?? page.viewportSize() ?? RECORDING_DEFAULT_SIZE;
|
|
1319
|
+
firstFrameEpoch = void 0;
|
|
1320
|
+
scene = void 0;
|
|
1321
|
+
sceneRevision++;
|
|
1322
|
+
recovery = { page, stagingPath };
|
|
1323
|
+
try {
|
|
1324
|
+
await (0, import_promises.mkdir)(config.videoDir, { recursive: true });
|
|
1325
|
+
await page.screencast.start({
|
|
1326
|
+
path: stagingPath,
|
|
1327
|
+
size,
|
|
1328
|
+
onFrame: async (frame) => {
|
|
1329
|
+
firstFrameEpoch ??= frame.timestamp;
|
|
1330
|
+
const pixels = jpegSize(frame.data);
|
|
1331
|
+
const next = pixels?.width === size.width && pixels.height === size.height && frame.viewportWidth === size.width && frame.viewportHeight === size.height ? { width: size.width, height: size.height } : void 0;
|
|
1332
|
+
if (next?.width !== scene?.width || next?.height !== scene?.height) sceneRevision++;
|
|
1333
|
+
scene = next;
|
|
1334
|
+
}
|
|
1335
|
+
});
|
|
1336
|
+
} catch (error) {
|
|
1337
|
+
try {
|
|
1338
|
+
await recover();
|
|
1339
|
+
} catch {
|
|
1340
|
+
}
|
|
1341
|
+
throw error;
|
|
1342
|
+
}
|
|
1343
|
+
active = recovery;
|
|
1344
|
+
recovery = void 0;
|
|
1345
|
+
return { zeroMs: performance.now() };
|
|
1346
|
+
} finally {
|
|
1347
|
+
busy = false;
|
|
1348
|
+
}
|
|
1349
|
+
},
|
|
1350
|
+
beginAnnotation() {
|
|
1351
|
+
const recording = active;
|
|
1352
|
+
const before = scene;
|
|
1353
|
+
const epoch = firstFrameEpoch;
|
|
1354
|
+
const revision = sceneRevision;
|
|
1355
|
+
if (!getPageRevision) return void 0;
|
|
1356
|
+
const pageRevision = getPageRevision();
|
|
1357
|
+
if (!recording || !before || epoch === void 0 || getPage() !== recording.page) return void 0;
|
|
1358
|
+
const viewport = recording.page.viewportSize();
|
|
1359
|
+
if (viewport?.width !== before.width || viewport.height !== before.height) return void 0;
|
|
1360
|
+
const startMs = performance.timeOrigin + performance.now() - epoch;
|
|
1361
|
+
const settle2 = () => {
|
|
1362
|
+
const after = recording.page.viewportSize();
|
|
1363
|
+
const endMs = performance.timeOrigin + performance.now() - epoch;
|
|
1364
|
+
if (active !== recording || getPage() !== recording.page || revision !== sceneRevision || pageRevision !== getPageRevision() || after?.width !== before.width || after.height !== before.height || !Number.isFinite(startMs) || !Number.isFinite(endMs) || startMs < 0 || endMs <= startMs) return void 0;
|
|
1365
|
+
return { startMs, endMs, scene: "recorded-page", transform: "identity", ...before };
|
|
1366
|
+
};
|
|
1367
|
+
let ended;
|
|
1368
|
+
bracket = { end() {
|
|
1369
|
+
ended = { witness: settle2() };
|
|
1370
|
+
} };
|
|
1371
|
+
return () => ended ? ended.witness : settle2();
|
|
1372
|
+
},
|
|
1373
|
+
endAction() {
|
|
1374
|
+
bracket?.end();
|
|
1375
|
+
},
|
|
1376
|
+
async stop() {
|
|
1377
|
+
if (busy) throw new Error("Recording operation already in progress");
|
|
1378
|
+
busy = true;
|
|
1379
|
+
try {
|
|
1380
|
+
const recording = active;
|
|
1381
|
+
if (!recording) {
|
|
1382
|
+
await recover();
|
|
1383
|
+
return void 0;
|
|
1384
|
+
}
|
|
1385
|
+
active = void 0;
|
|
1386
|
+
try {
|
|
1387
|
+
await recording.page.screencast.stop();
|
|
1388
|
+
} catch (error) {
|
|
1389
|
+
recovery = recording;
|
|
1390
|
+
try {
|
|
1391
|
+
await recover();
|
|
1392
|
+
} catch {
|
|
1393
|
+
}
|
|
1394
|
+
throw error;
|
|
1395
|
+
}
|
|
1396
|
+
return { stagingPath: recording.stagingPath, contentType: "video/webm" };
|
|
1397
|
+
} finally {
|
|
1398
|
+
busy = false;
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
};
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
// src/operation-capture.ts
|
|
1405
|
+
var import_node_crypto2 = require("crypto");
|
|
1406
|
+
var import_node_path2 = require("path");
|
|
1407
|
+
var import_agent_qa_runner5 = require("@vostride/agent-qa-runner");
|
|
1408
|
+
var DOWNLOAD_START_GRACE_MS = 500;
|
|
1409
|
+
var NAVIGATION_SETTLE_MS = 250;
|
|
1410
|
+
var DOWNLOAD_COMPLETION_CAP_MS = 12e4;
|
|
1411
|
+
var NAVIGATION_WINDOW_CAP_MS = 5e3;
|
|
1412
|
+
var DOWNLOAD_HOLD_FILE_CAP = 1024 * 1024;
|
|
1413
|
+
var DOWNLOAD_HOLD_TOTAL_CAP = 8 * 1024 * 1024;
|
|
1414
|
+
var DOWNLOAD_ACTIVATION_ACTIONS = /* @__PURE__ */ new Set([
|
|
1415
|
+
"click",
|
|
1416
|
+
"doubleClick",
|
|
1417
|
+
"rightClick",
|
|
1418
|
+
"keypress",
|
|
1419
|
+
"select",
|
|
1420
|
+
"navigate",
|
|
1421
|
+
"executeScript",
|
|
1422
|
+
"handleDialog",
|
|
1423
|
+
"fileUpload",
|
|
1424
|
+
"tab"
|
|
1425
|
+
]);
|
|
1426
|
+
function isDownloadActivation(action) {
|
|
1427
|
+
if (action.type === "fill") return action.submit === true;
|
|
1428
|
+
if (action.type === "tab") return action.action === "new";
|
|
1429
|
+
return DOWNLOAD_ACTIVATION_ACTIONS.has(action.type);
|
|
1430
|
+
}
|
|
1431
|
+
var UNFINISHED = /* @__PURE__ */ Symbol("unfinished");
|
|
1432
|
+
var CLIPBOARD_BINDING = "__agentQaAppClipboardWrite";
|
|
1433
|
+
var CLIPBOARD_EXCLUDED_ACTIONS = /* @__PURE__ */ new Set(["copy", "paste"]);
|
|
1434
|
+
var CLIPBOARD_SOURCES = /* @__PURE__ */ new Set(["writeText", "write", "copy-event"]);
|
|
1435
|
+
var READ_BACK_TIMEOUT_MS = 2e3;
|
|
1436
|
+
var CLIPBOARD_CLAIMS_PER_ACTION = 8;
|
|
1437
|
+
var CLIPBOARD_READBACK_ORIGIN = "https://agent-qa-readback.invalid";
|
|
1438
|
+
var PROBE_MATCHED = 1;
|
|
1439
|
+
var PROBE_DIFFERENT = 0;
|
|
1440
|
+
function installProbeReadback() {
|
|
1441
|
+
const scope = globalThis;
|
|
1442
|
+
let preImage;
|
|
1443
|
+
let status = "none";
|
|
1444
|
+
const read = async () => {
|
|
1445
|
+
try {
|
|
1446
|
+
const text = await scope.navigator.clipboard.readText();
|
|
1447
|
+
return typeof text === "string" ? text : void 0;
|
|
1448
|
+
} catch {
|
|
1449
|
+
return void 0;
|
|
1450
|
+
}
|
|
1451
|
+
};
|
|
1452
|
+
Object.defineProperty(globalThis, "__agentQaReadback", {
|
|
1453
|
+
value: Object.freeze({
|
|
1454
|
+
get status() {
|
|
1455
|
+
return status;
|
|
1456
|
+
},
|
|
1457
|
+
async snapshot() {
|
|
1458
|
+
preImage = void 0;
|
|
1459
|
+
status = "none";
|
|
1460
|
+
const text = await read();
|
|
1461
|
+
if (text === void 0) {
|
|
1462
|
+
status = "unreadable";
|
|
1463
|
+
} else {
|
|
1464
|
+
preImage = text;
|
|
1465
|
+
status = "read";
|
|
1466
|
+
}
|
|
1467
|
+
return status;
|
|
1468
|
+
},
|
|
1469
|
+
async compare(finalText) {
|
|
1470
|
+
if (status !== "read") return -2;
|
|
1471
|
+
const now = await read();
|
|
1472
|
+
if (now === void 0) return -2;
|
|
1473
|
+
if (now === preImage) return -3;
|
|
1474
|
+
return now === finalText ? 1 : 0;
|
|
1475
|
+
}
|
|
1476
|
+
})
|
|
1477
|
+
});
|
|
1478
|
+
}
|
|
1479
|
+
var CLIPBOARD_HOOK = `(() => {
|
|
1480
|
+
const binding = ${JSON.stringify(CLIPBOARD_BINDING)};
|
|
1481
|
+
const report = (via, text) => {
|
|
1482
|
+
const send = window[binding];
|
|
1483
|
+
if (typeof send !== 'function' || typeof text !== 'string' || text.length === 0) return;
|
|
1484
|
+
try { Promise.resolve(send({ via, text })).catch(() => {}); } catch {}
|
|
1485
|
+
};
|
|
1486
|
+
// 2026-10-08 (436-14): a write is reported only once the browser accepted it; the app still
|
|
1487
|
+
// gets the original promise, rejections included.
|
|
1488
|
+
const proto = typeof Clipboard === 'function' ? Clipboard.prototype : undefined;
|
|
1489
|
+
if (proto && typeof proto.writeText === 'function') {
|
|
1490
|
+
const writeText = proto.writeText;
|
|
1491
|
+
proto.writeText = function (data) {
|
|
1492
|
+
let text;
|
|
1493
|
+
try { text = String(data); } catch {}
|
|
1494
|
+
const result = writeText.apply(this, arguments);
|
|
1495
|
+
try { Promise.resolve(result).then(() => report('writeText', text), () => {}); } catch {}
|
|
1496
|
+
return result;
|
|
1497
|
+
};
|
|
1498
|
+
}
|
|
1499
|
+
if (proto && typeof proto.write === 'function') {
|
|
1500
|
+
const write = proto.write;
|
|
1501
|
+
proto.write = function (items) {
|
|
1502
|
+
const texts = [];
|
|
1503
|
+
try {
|
|
1504
|
+
for (const item of Array.from(items || [])) {
|
|
1505
|
+
if (!item || !Array.from(item.types || []).includes('text/plain')) continue;
|
|
1506
|
+
texts.push(item.getType('text/plain').then((blob) => blob.text()).catch(() => undefined));
|
|
1507
|
+
}
|
|
1508
|
+
} catch {}
|
|
1509
|
+
const result = write.apply(this, arguments);
|
|
1510
|
+
try {
|
|
1511
|
+
Promise.resolve(result).then(
|
|
1512
|
+
() => Promise.all(texts).then((all) => { for (const text of all) report('write', text); }),
|
|
1513
|
+
() => {},
|
|
1514
|
+
);
|
|
1515
|
+
} catch {}
|
|
1516
|
+
return result;
|
|
1517
|
+
};
|
|
1518
|
+
}
|
|
1519
|
+
// A copy event is tracked from window's capture phase, which runs before any app handler can
|
|
1520
|
+
// stop propagation, and settled once dispatch is over: a prevented copy writes what the app
|
|
1521
|
+
// set (seen through setData), an unprevented one writes the selection taken at dispatch.
|
|
1522
|
+
let pending = null;
|
|
1523
|
+
const selected = (event) => {
|
|
1524
|
+
for (const el of [event.target, document.activeElement]) {
|
|
1525
|
+
if (!el || (el.tagName !== 'TEXTAREA' && el.tagName !== 'INPUT')) continue;
|
|
1526
|
+
try {
|
|
1527
|
+
const start = el.selectionStart, end = el.selectionEnd;
|
|
1528
|
+
if (typeof start === 'number' && typeof end === 'number' && end > start) return el.value.slice(start, end);
|
|
1529
|
+
} catch {}
|
|
1530
|
+
}
|
|
1531
|
+
const selection = document.getSelection();
|
|
1532
|
+
return selection ? selection.toString() : '';
|
|
1533
|
+
};
|
|
1534
|
+
const settle = () => {
|
|
1535
|
+
const copy = pending;
|
|
1536
|
+
pending = null;
|
|
1537
|
+
if (!copy) return;
|
|
1538
|
+
if (!copy.event.defaultPrevented) return report('copy-event', copy.selection);
|
|
1539
|
+
if (copy.set !== undefined) return report('copy-event', copy.set);
|
|
1540
|
+
try { report('copy-event', copy.event.clipboardData ? copy.event.clipboardData.getData('text/plain') : ''); } catch {}
|
|
1541
|
+
};
|
|
1542
|
+
if (typeof DataTransfer === 'function' && typeof DataTransfer.prototype.setData === 'function') {
|
|
1543
|
+
const setData = DataTransfer.prototype.setData;
|
|
1544
|
+
DataTransfer.prototype.setData = function (format, data) {
|
|
1545
|
+
if (pending && this === pending.event.clipboardData && /^text(\\/plain)?$/i.test(String(format))) pending.set = String(data);
|
|
1546
|
+
return setData.apply(this, arguments);
|
|
1547
|
+
};
|
|
1548
|
+
}
|
|
1549
|
+
window.addEventListener('copy', (event) => {
|
|
1550
|
+
settle();
|
|
1551
|
+
pending = { event, selection: selected(event), set: undefined };
|
|
1552
|
+
setTimeout(settle, 0);
|
|
1553
|
+
}, true);
|
|
1554
|
+
})();`;
|
|
1555
|
+
var PLAYWRIGHT_DEBUG_ENV = ["PWDEBUG", "npm_config_pwdebug", "npm_package_config_pwdebug"];
|
|
1556
|
+
var DEBUG_OFF = /* @__PURE__ */ new Set(["", "0", "false"]);
|
|
1557
|
+
function isHeadlessLaunch(launch, env) {
|
|
1558
|
+
if (launch.headless !== true || launch.channel !== void 0) return false;
|
|
1559
|
+
return PLAYWRIGHT_DEBUG_ENV.every((name) => env[name] === void 0 || DEBUG_OFF.has(env[name]));
|
|
1560
|
+
}
|
|
1561
|
+
var OperationCapture = class {
|
|
1562
|
+
completionCapMs;
|
|
1563
|
+
startGraceMs;
|
|
1564
|
+
attachedPages = /* @__PURE__ */ new WeakSet();
|
|
1565
|
+
downloads = [];
|
|
1566
|
+
/** Downloads that started while no start window was open, kept for the next action. */
|
|
1567
|
+
held = [];
|
|
1568
|
+
/** Completed downloads' bytes by sha256, oldest first; memory only (439 D-04). */
|
|
1569
|
+
heldBytes = /* @__PURE__ */ new Map();
|
|
1570
|
+
heldByteTotal = 0;
|
|
1571
|
+
pendingNavigations = /* @__PURE__ */ new Map();
|
|
1572
|
+
clipboardWrites = [];
|
|
1573
|
+
armed = false;
|
|
1574
|
+
startWindow = false;
|
|
1575
|
+
navigationWindow = false;
|
|
1576
|
+
navigationSettledAt = 0;
|
|
1577
|
+
navigationSettledMain = false;
|
|
1578
|
+
actingPage;
|
|
1579
|
+
clipboardArmed = false;
|
|
1580
|
+
activation = false;
|
|
1581
|
+
boundAt = 0;
|
|
1582
|
+
abortSignal;
|
|
1583
|
+
notify = null;
|
|
1584
|
+
appContext = null;
|
|
1585
|
+
probe = null;
|
|
1586
|
+
probeContext = null;
|
|
1587
|
+
probeGeneration = 0;
|
|
1588
|
+
readsClipboard;
|
|
1589
|
+
/** The probe generation holding this action's pre-image; -1 when none was read. */
|
|
1590
|
+
preImageGeneration = -1;
|
|
1591
|
+
/** A probe generation whose read did not settle; skipped until resetProbe() replaces it. */
|
|
1592
|
+
unreadableGeneration = -1;
|
|
1593
|
+
constructor(options = {}) {
|
|
1594
|
+
this.completionCapMs = options.completionCapMs ?? DOWNLOAD_COMPLETION_CAP_MS;
|
|
1595
|
+
this.startGraceMs = options.startGraceMs ?? DOWNLOAD_START_GRACE_MS;
|
|
1596
|
+
this.readsClipboard = options.headless === true;
|
|
1597
|
+
}
|
|
1598
|
+
async attach(context) {
|
|
1599
|
+
this.appContext = context;
|
|
1600
|
+
for (const page of context.pages()) this.attachPage(page);
|
|
1601
|
+
context.on("page", (page) => this.attachPage(page));
|
|
1602
|
+
context.on("request", (request) => this.onRequest(request));
|
|
1603
|
+
context.on("response", (response) => {
|
|
1604
|
+
this.settleNavigation(response.request());
|
|
1605
|
+
});
|
|
1606
|
+
context.on("requestfailed", (request) => {
|
|
1607
|
+
this.settleNavigation(request);
|
|
1608
|
+
});
|
|
1609
|
+
await context.exposeBinding(CLIPBOARD_BINDING, (source, payload) => {
|
|
1610
|
+
this.onClipboardWrite(source.frame, payload);
|
|
1611
|
+
});
|
|
1612
|
+
await context.addInitScript(CLIPBOARD_HOOK);
|
|
1613
|
+
}
|
|
1614
|
+
async arm(action, options = {}, actingPage) {
|
|
1615
|
+
const carried = [...this.downloads.map((entry) => entry.download), ...this.held];
|
|
1616
|
+
this.held = [];
|
|
1617
|
+
this.downloads = carried.map((download) => ({ download, late: true }));
|
|
1618
|
+
this.clipboardWrites = [];
|
|
1619
|
+
this.armed = true;
|
|
1620
|
+
this.startWindow = true;
|
|
1621
|
+
this.clipboardArmed = false;
|
|
1622
|
+
this.activation = isDownloadActivation(action);
|
|
1623
|
+
this.navigationWindow = this.activation;
|
|
1624
|
+
this.navigationSettledAt = 0;
|
|
1625
|
+
this.navigationSettledMain = false;
|
|
1626
|
+
this.actingPage = actingPage;
|
|
1627
|
+
this.pendingNavigations.clear();
|
|
1628
|
+
const remaining = options.remainingStepMs;
|
|
1629
|
+
this.boundAt = Date.now() + (typeof remaining === "number" && Number.isFinite(remaining) ? Math.max(0, remaining) : this.completionCapMs);
|
|
1630
|
+
this.abortSignal = options.abortSignal;
|
|
1631
|
+
this.preImageGeneration = -1;
|
|
1632
|
+
const clipboardAction = !CLIPBOARD_EXCLUDED_ACTIONS.has(action.type);
|
|
1633
|
+
if (this.readsClipboard && clipboardAction && this.timeLeft() > 0) await this.takePreImage();
|
|
1634
|
+
this.clipboardArmed = this.armed && clipboardAction;
|
|
1635
|
+
}
|
|
1636
|
+
timeLeft() {
|
|
1637
|
+
return this.abortSignal?.aborted ? 0 : this.boundAt - Date.now();
|
|
1638
|
+
}
|
|
1639
|
+
// Calls `fire` once when the action's abort signal fires; the returned function removes it.
|
|
1640
|
+
onAbort(fire) {
|
|
1641
|
+
const signal = this.abortSignal;
|
|
1642
|
+
if (!signal) return () => void 0;
|
|
1643
|
+
if (signal.aborted) {
|
|
1644
|
+
queueMicrotask(fire);
|
|
1645
|
+
return () => void 0;
|
|
1646
|
+
}
|
|
1647
|
+
signal.addEventListener("abort", fire, { once: true });
|
|
1648
|
+
return () => {
|
|
1649
|
+
signal.removeEventListener("abort", fire);
|
|
1650
|
+
};
|
|
1651
|
+
}
|
|
1652
|
+
async collect() {
|
|
1653
|
+
if (!this.armed) return void 0;
|
|
1654
|
+
let next = 0;
|
|
1655
|
+
try {
|
|
1656
|
+
await this.waitForStartWindow();
|
|
1657
|
+
this.closeStartWindow();
|
|
1658
|
+
const writes = this.clipboardWrites;
|
|
1659
|
+
this.clipboardArmed = false;
|
|
1660
|
+
this.clipboardWrites = [];
|
|
1661
|
+
const clipboard = !this.readsClipboard ? writes.map(({ source, text }) => ({ source, text, readBack: "unavailable" })) : writes.length > 0 ? await this.readBack(writes) : [];
|
|
1662
|
+
const evidence = [];
|
|
1663
|
+
while (next < this.downloads.length) {
|
|
1664
|
+
const { download, late } = this.downloads[next++];
|
|
1665
|
+
const finished = await this.finish(download);
|
|
1666
|
+
evidence.push(late ? { ...finished, late: true } : finished);
|
|
1667
|
+
}
|
|
1668
|
+
if (evidence.length === 0 && clipboard.length === 0) return void 0;
|
|
1669
|
+
return {
|
|
1670
|
+
...evidence.length > 0 ? { downloads: evidence } : {},
|
|
1671
|
+
...clipboard.length > 0 ? { clipboard } : {}
|
|
1672
|
+
};
|
|
1673
|
+
} finally {
|
|
1674
|
+
this.held.push(...this.downloads.slice(next).map((entry) => entry.download));
|
|
1675
|
+
this.closeStartWindow();
|
|
1676
|
+
this.armed = false;
|
|
1677
|
+
this.clipboardArmed = false;
|
|
1678
|
+
this.downloads = [];
|
|
1679
|
+
this.clipboardWrites = [];
|
|
1680
|
+
this.abortSignal = void 0;
|
|
1681
|
+
this.actingPage = void 0;
|
|
1682
|
+
this.preImageGeneration = -1;
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
// The grace after an activation action, then for as long as a navigation it started has neither
|
|
1686
|
+
// a response nor a failure, plus NAVIGATION_SETTLE_MS after the last answer; all bounded by the
|
|
1687
|
+
// action's time left. Fixed when the wait begins: the acting page's main-frame navigation ends at
|
|
1688
|
+
// mainEnd (half the time left), any other at capEnd (also NAVIGATION_WINDOW_CAP_MS).
|
|
1689
|
+
async waitForStartWindow() {
|
|
1690
|
+
if (!this.activation) return;
|
|
1691
|
+
const start = Date.now();
|
|
1692
|
+
const graceEnd = start + Math.min(this.startGraceMs, Math.max(0, this.timeLeft()));
|
|
1693
|
+
const reserve = Math.max(0, this.timeLeft()) / 2;
|
|
1694
|
+
const capEnd = start + Math.min(NAVIGATION_WINDOW_CAP_MS, reserve);
|
|
1695
|
+
const mainEnd = start + reserve;
|
|
1696
|
+
for (; ; ) {
|
|
1697
|
+
const left = this.timeLeft();
|
|
1698
|
+
if (left <= 0) return;
|
|
1699
|
+
const now = Date.now();
|
|
1700
|
+
const settleEnd = Math.min(
|
|
1701
|
+
this.navigationSettledAt + NAVIGATION_SETTLE_MS,
|
|
1702
|
+
this.navigationSettledMain ? mainEnd : capEnd
|
|
1703
|
+
);
|
|
1704
|
+
const openFor = Math.max(graceEnd, settleEnd) - now;
|
|
1705
|
+
const pendingEnd = this.pendingNavigationEnd(mainEnd, capEnd);
|
|
1706
|
+
if (openFor > 0) await this.waitForSignal(Math.min(openFor, left));
|
|
1707
|
+
else if (now < pendingEnd) await this.waitForSignal(Math.min(pendingEnd - now, left));
|
|
1708
|
+
else return;
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
// mainEnd while a main-frame navigation of the acting page is pending, else capEnd while any
|
|
1712
|
+
// other is, else 0.
|
|
1713
|
+
pendingNavigationEnd(mainEnd, capEnd) {
|
|
1714
|
+
let end = 0;
|
|
1715
|
+
for (const { main } of this.pendingNavigations.values()) {
|
|
1716
|
+
if (main) return mainEnd;
|
|
1717
|
+
end = capEnd;
|
|
1718
|
+
}
|
|
1719
|
+
return end;
|
|
1720
|
+
}
|
|
1721
|
+
closeStartWindow() {
|
|
1722
|
+
this.startWindow = false;
|
|
1723
|
+
this.navigationWindow = false;
|
|
1724
|
+
this.pendingNavigations.clear();
|
|
1725
|
+
}
|
|
1726
|
+
onRequest(request) {
|
|
1727
|
+
if (!this.navigationWindow || !request.isNavigationRequest()) return;
|
|
1728
|
+
let page;
|
|
1729
|
+
let main = false;
|
|
1730
|
+
try {
|
|
1731
|
+
const frame = request.frame();
|
|
1732
|
+
page = frame.page();
|
|
1733
|
+
main = this.actingPage !== void 0 && frame === this.actingPage.mainFrame();
|
|
1734
|
+
} catch {
|
|
1735
|
+
}
|
|
1736
|
+
this.pendingNavigations.set(request, { page, main });
|
|
1737
|
+
}
|
|
1738
|
+
settleNavigation(request) {
|
|
1739
|
+
const pending = this.pendingNavigations.get(request);
|
|
1740
|
+
if (!pending) return;
|
|
1741
|
+
this.pendingNavigations.delete(request);
|
|
1742
|
+
this.navigationSettledAt = Date.now();
|
|
1743
|
+
this.navigationSettledMain = pending.main;
|
|
1744
|
+
this.notify?.();
|
|
1745
|
+
}
|
|
1746
|
+
onClipboardWrite(frame, payload) {
|
|
1747
|
+
if (!this.armed || !this.clipboardArmed) return;
|
|
1748
|
+
if (!payload || typeof payload !== "object") return;
|
|
1749
|
+
const { via, text } = payload;
|
|
1750
|
+
if (typeof via !== "string" || !CLIPBOARD_SOURCES.has(via) || typeof text !== "string") return;
|
|
1751
|
+
this.clipboardWrites.push({ source: via, text, frame });
|
|
1752
|
+
if (this.clipboardWrites.length > CLIPBOARD_CLAIMS_PER_ACTION) this.clipboardWrites.shift();
|
|
1753
|
+
}
|
|
1754
|
+
// Takes the probe page's pre-image of the clipboard; a closed or crashed probe is replaced once.
|
|
1755
|
+
async takePreImage() {
|
|
1756
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
1757
|
+
const probe = await this.openProbe();
|
|
1758
|
+
if (!probe) return;
|
|
1759
|
+
if (probe.isClosed()) {
|
|
1760
|
+
await this.resetProbe();
|
|
1761
|
+
continue;
|
|
1762
|
+
}
|
|
1763
|
+
const generation = this.probeGeneration;
|
|
1764
|
+
const bound = Math.min(READ_BACK_TIMEOUT_MS, this.timeLeft());
|
|
1765
|
+
if (generation === this.unreadableGeneration || bound <= 0) return;
|
|
1766
|
+
try {
|
|
1767
|
+
const status = await this.bounded(
|
|
1768
|
+
probe.evaluate(() => globalThis.__agentQaReadback.snapshot()),
|
|
1769
|
+
bound
|
|
1770
|
+
);
|
|
1771
|
+
if (status === void 0) this.markUnreadable(generation, bound);
|
|
1772
|
+
else if (status === "read" && generation === this.probeGeneration) this.preImageGeneration = generation;
|
|
1773
|
+
return;
|
|
1774
|
+
} catch {
|
|
1775
|
+
await this.resetProbe();
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
// Only the final claim is compared, against a clipboard that changed during the action; an
|
|
1780
|
+
// earlier claim is matched only when its text equals a matched final claim.
|
|
1781
|
+
async readBack(writes) {
|
|
1782
|
+
const last = writes.length - 1;
|
|
1783
|
+
const final = writes[last];
|
|
1784
|
+
const answer = await this.readClipboard(final.frame, final.text);
|
|
1785
|
+
return writes.map(({ source, text }, position) => ({
|
|
1786
|
+
source,
|
|
1787
|
+
text,
|
|
1788
|
+
readBack: answer === PROBE_MATCHED && text === final.text ? "matched" : answer === PROBE_DIFFERENT && position === last ? "mismatched" : "unavailable"
|
|
1789
|
+
}));
|
|
1790
|
+
}
|
|
1791
|
+
// PROBE_MATCHED or PROBE_DIFFERENT when the clipboard changed during the action, else undefined.
|
|
1792
|
+
// Only the final claim's text enters the probe page and only a number comes back.
|
|
1793
|
+
async readClipboard(frame, finalText) {
|
|
1794
|
+
const generation = this.preImageGeneration;
|
|
1795
|
+
if (generation < 0 || generation !== this.probeGeneration || generation === this.unreadableGeneration) {
|
|
1796
|
+
return void 0;
|
|
1797
|
+
}
|
|
1798
|
+
if (frame.isDetached()) return void 0;
|
|
1799
|
+
try {
|
|
1800
|
+
if (new URL(frame.url()).origin === "null") return void 0;
|
|
1801
|
+
} catch {
|
|
1802
|
+
return void 0;
|
|
1803
|
+
}
|
|
1804
|
+
const frameBound = Math.min(READ_BACK_TIMEOUT_MS, this.timeLeft());
|
|
1805
|
+
if (frameBound <= 0) return void 0;
|
|
1806
|
+
const secure = await this.bounded(
|
|
1807
|
+
frame.evaluate("window.isSecureContext && !!(navigator.clipboard && navigator.clipboard.readText)"),
|
|
1808
|
+
frameBound
|
|
1809
|
+
).catch(() => false);
|
|
1810
|
+
if (secure !== true) return void 0;
|
|
1811
|
+
const probe = await this.openProbe();
|
|
1812
|
+
const bound = Math.min(READ_BACK_TIMEOUT_MS, this.timeLeft());
|
|
1813
|
+
if (!probe || generation !== this.probeGeneration || bound <= 0) return void 0;
|
|
1814
|
+
let answer;
|
|
1815
|
+
try {
|
|
1816
|
+
if (probe.isClosed()) throw new Error("probe closed");
|
|
1817
|
+
answer = await this.bounded(
|
|
1818
|
+
probe.evaluate((text) => globalThis.__agentQaReadback.compare(text), finalText),
|
|
1819
|
+
bound
|
|
1820
|
+
);
|
|
1821
|
+
} catch {
|
|
1822
|
+
await this.resetProbe();
|
|
1823
|
+
return void 0;
|
|
1824
|
+
}
|
|
1825
|
+
if (answer === void 0) this.markUnreadable(generation, bound);
|
|
1826
|
+
return answer === PROBE_MATCHED || answer === PROBE_DIFFERENT ? answer : void 0;
|
|
1827
|
+
}
|
|
1828
|
+
// A read cut short by the step's time or its abort says nothing about the probe.
|
|
1829
|
+
markUnreadable(generation, bound) {
|
|
1830
|
+
if (bound >= READ_BACK_TIMEOUT_MS && !this.abortSignal?.aborted) this.unreadableGeneration = generation;
|
|
1831
|
+
}
|
|
1832
|
+
// Resolves undefined after `ms` or on the action's abort; rejects when `promise` rejects.
|
|
1833
|
+
async bounded(promise, ms) {
|
|
1834
|
+
let off = () => void 0;
|
|
1835
|
+
try {
|
|
1836
|
+
return await Promise.race([
|
|
1837
|
+
withTimeout(promise, ms),
|
|
1838
|
+
new Promise((resolve) => {
|
|
1839
|
+
off = this.onAbort(() => {
|
|
1840
|
+
resolve(void 0);
|
|
1841
|
+
});
|
|
1842
|
+
})
|
|
1843
|
+
]);
|
|
1844
|
+
} finally {
|
|
1845
|
+
off();
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
openProbe() {
|
|
1849
|
+
if (!this.readsClipboard) return Promise.resolve(void 0);
|
|
1850
|
+
this.probe ??= this.createProbe(this.probeGeneration);
|
|
1851
|
+
return this.probe;
|
|
1852
|
+
}
|
|
1853
|
+
// A separate context of the app's browser: only the reserved origin loads (a blank document, no
|
|
1854
|
+
// init script), every other request is aborted, and only that origin may read the clipboard.
|
|
1855
|
+
async createProbe(generation) {
|
|
1856
|
+
let context;
|
|
1857
|
+
try {
|
|
1858
|
+
const browser = this.appContext?.browser();
|
|
1859
|
+
if (!browser) return void 0;
|
|
1860
|
+
context = await browser.newContext();
|
|
1861
|
+
if (generation !== this.probeGeneration) {
|
|
1862
|
+
await context.close().catch(() => void 0);
|
|
1863
|
+
return void 0;
|
|
1864
|
+
}
|
|
1865
|
+
this.probeContext = context;
|
|
1866
|
+
await context.route("**/*", (route) => {
|
|
1867
|
+
let origin = "";
|
|
1868
|
+
try {
|
|
1869
|
+
origin = new URL(route.request().url()).origin;
|
|
1870
|
+
} catch {
|
|
1871
|
+
}
|
|
1872
|
+
return origin === CLIPBOARD_READBACK_ORIGIN ? route.fulfill({ contentType: "text/html", body: "<!doctype html>" }) : route.abort();
|
|
1873
|
+
});
|
|
1874
|
+
await context.grantPermissions(["clipboard-read"], { origin: CLIPBOARD_READBACK_ORIGIN });
|
|
1875
|
+
const page = await context.newPage();
|
|
1876
|
+
page.once("crash", () => {
|
|
1877
|
+
if (generation === this.probeGeneration) void this.resetProbe();
|
|
1878
|
+
});
|
|
1879
|
+
await page.goto(`${CLIPBOARD_READBACK_ORIGIN}/`);
|
|
1880
|
+
await page.evaluate(installProbeReadback);
|
|
1881
|
+
return page;
|
|
1882
|
+
} catch {
|
|
1883
|
+
if (generation === this.probeGeneration) {
|
|
1884
|
+
this.probe = null;
|
|
1885
|
+
this.probeContext = null;
|
|
1886
|
+
}
|
|
1887
|
+
await context?.close().catch(() => void 0);
|
|
1888
|
+
return void 0;
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
/** The bytes of a completed download with this sha256 while the hold keeps them. */
|
|
1892
|
+
readDownloadBytes(sha256) {
|
|
1893
|
+
return this.heldBytes.get(sha256);
|
|
1894
|
+
}
|
|
1895
|
+
holdBytes(sha256, bytes) {
|
|
1896
|
+
if (bytes.length > DOWNLOAD_HOLD_FILE_CAP) return;
|
|
1897
|
+
const previous = this.heldBytes.get(sha256);
|
|
1898
|
+
if (previous !== void 0) {
|
|
1899
|
+
this.heldBytes.delete(sha256);
|
|
1900
|
+
this.heldByteTotal -= previous.length;
|
|
1901
|
+
}
|
|
1902
|
+
this.heldBytes.set(sha256, bytes);
|
|
1903
|
+
this.heldByteTotal += bytes.length;
|
|
1904
|
+
for (const [key, value] of this.heldBytes) {
|
|
1905
|
+
if (this.heldByteTotal <= DOWNLOAD_HOLD_TOTAL_CAP) break;
|
|
1906
|
+
this.heldBytes.delete(key);
|
|
1907
|
+
this.heldByteTotal -= value.length;
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
/** Cancels and deletes every held download, clears the held bytes, then closes the read-back probe context. */
|
|
1911
|
+
async dispose() {
|
|
1912
|
+
this.heldBytes.clear();
|
|
1913
|
+
this.heldByteTotal = 0;
|
|
1914
|
+
const held = this.held;
|
|
1915
|
+
this.held = [];
|
|
1916
|
+
for (const download of held) {
|
|
1917
|
+
await download.cancel().catch(() => void 0);
|
|
1918
|
+
await download.delete().catch(() => void 0);
|
|
1919
|
+
}
|
|
1920
|
+
await this.resetProbe();
|
|
1921
|
+
}
|
|
1922
|
+
// Closes only the probe context; held downloads are never touched here.
|
|
1923
|
+
async resetProbe() {
|
|
1924
|
+
const context = this.probeContext;
|
|
1925
|
+
this.probeGeneration += 1;
|
|
1926
|
+
this.unreadableGeneration = -1;
|
|
1927
|
+
this.probe = null;
|
|
1928
|
+
this.probeContext = null;
|
|
1929
|
+
await context?.close().catch(() => void 0);
|
|
1930
|
+
}
|
|
1931
|
+
attachPage(page) {
|
|
1932
|
+
if (this.attachedPages.has(page)) return;
|
|
1933
|
+
this.attachedPages.add(page);
|
|
1934
|
+
page.on("download", (download) => {
|
|
1935
|
+
if (!this.startWindow) {
|
|
1936
|
+
this.held.push(download);
|
|
1937
|
+
return;
|
|
1938
|
+
}
|
|
1939
|
+
this.downloads.push({ download, late: false });
|
|
1940
|
+
this.notify?.();
|
|
1941
|
+
});
|
|
1942
|
+
page.on("close", () => {
|
|
1943
|
+
for (const [request, pending] of this.pendingNavigations) if (pending.page === page) this.settleNavigation(request);
|
|
1944
|
+
});
|
|
1945
|
+
}
|
|
1946
|
+
// Resolves on a download start, a settled navigation, the abort, or after `ms`.
|
|
1947
|
+
async waitForSignal(ms) {
|
|
1948
|
+
let off = () => void 0;
|
|
1949
|
+
await new Promise((resolve) => {
|
|
1950
|
+
const timer = setTimeout(done, ms);
|
|
1951
|
+
function done() {
|
|
1952
|
+
clearTimeout(timer);
|
|
1953
|
+
resolve();
|
|
1954
|
+
}
|
|
1955
|
+
this.notify = done;
|
|
1956
|
+
off = this.onAbort(done);
|
|
1957
|
+
});
|
|
1958
|
+
off();
|
|
1959
|
+
this.notify = null;
|
|
1960
|
+
}
|
|
1961
|
+
async finish(download) {
|
|
1962
|
+
const suggestedFilename = download.suggestedFilename();
|
|
1963
|
+
const base = {
|
|
1964
|
+
suggestedFilename,
|
|
1965
|
+
extension: (0, import_node_path2.extname)(suggestedFilename)
|
|
1966
|
+
};
|
|
1967
|
+
let timer;
|
|
1968
|
+
let off = () => void 0;
|
|
1969
|
+
const outcome = await Promise.race([
|
|
1970
|
+
download.failure().catch((error) => error instanceof Error ? error.message : String(error)),
|
|
1971
|
+
new Promise((resolve) => {
|
|
1972
|
+
const end = () => {
|
|
1973
|
+
resolve(UNFINISHED);
|
|
1974
|
+
};
|
|
1975
|
+
timer = setTimeout(end, Math.max(0, this.timeLeft()));
|
|
1976
|
+
off = this.onAbort(end);
|
|
1977
|
+
})
|
|
1978
|
+
]);
|
|
1979
|
+
clearTimeout(timer);
|
|
1980
|
+
off();
|
|
1981
|
+
if (outcome === UNFINISHED) {
|
|
1982
|
+
await download.cancel().catch(() => void 0);
|
|
1983
|
+
await download.delete().catch(() => void 0);
|
|
1984
|
+
return { ...base, state: "unfinished" };
|
|
1985
|
+
}
|
|
1986
|
+
if (outcome !== null) {
|
|
1987
|
+
await download.delete().catch(() => void 0);
|
|
1988
|
+
return { ...base, state: outcome === "canceled" ? "canceled" : "failed", failure: outcome };
|
|
1989
|
+
}
|
|
1990
|
+
try {
|
|
1991
|
+
const { size, sha256, bytes } = await digest(download);
|
|
1992
|
+
if (bytes !== void 0) this.holdBytes(sha256, bytes);
|
|
1993
|
+
return { ...base, size, sha256, state: "completed" };
|
|
1994
|
+
} catch (error) {
|
|
1995
|
+
return { ...base, state: "failed", failure: error instanceof Error ? error.message : String(error) };
|
|
1996
|
+
} finally {
|
|
1997
|
+
await download.delete().catch(() => void 0);
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
};
|
|
2001
|
+
async function withTimeout(promise, ms) {
|
|
2002
|
+
let timer;
|
|
2003
|
+
try {
|
|
2004
|
+
return await Promise.race([
|
|
2005
|
+
promise,
|
|
2006
|
+
new Promise((resolve) => {
|
|
2007
|
+
timer = setTimeout(() => resolve(void 0), ms);
|
|
2008
|
+
})
|
|
2009
|
+
]);
|
|
2010
|
+
} finally {
|
|
2011
|
+
clearTimeout(timer);
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
async function digest(download) {
|
|
2015
|
+
const stream = await download.createReadStream();
|
|
2016
|
+
const hash = (0, import_node_crypto2.createHash)("sha256");
|
|
2017
|
+
let size = 0;
|
|
2018
|
+
let chunks = [];
|
|
2019
|
+
for await (const chunk of stream) {
|
|
2020
|
+
const bytes = chunk;
|
|
2021
|
+
size += bytes.length;
|
|
2022
|
+
hash.update(bytes);
|
|
2023
|
+
if (size > DOWNLOAD_HOLD_FILE_CAP) chunks = void 0;
|
|
2024
|
+
else chunks?.push(bytes);
|
|
2025
|
+
}
|
|
2026
|
+
const sha256 = hash.digest("hex");
|
|
2027
|
+
return chunks === void 0 ? { size, sha256 } : { size, sha256, bytes: new Uint8Array(Buffer.concat(chunks)) };
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2030
|
+
// src/adapter.ts
|
|
2031
|
+
var OBSERVE_SNAPSHOT_TIMEOUT_MS = 5e3;
|
|
2032
|
+
var OBSERVE_SNAPSHOT_RETRY_TIMEOUT_MS = 3e4;
|
|
2033
|
+
var OBSERVE_SNAPSHOT = {
|
|
2034
|
+
timeoutMs: OBSERVE_SNAPSHOT_TIMEOUT_MS,
|
|
2035
|
+
retryTimeoutMs: OBSERVE_SNAPSHOT_RETRY_TIMEOUT_MS
|
|
2036
|
+
};
|
|
2037
|
+
var SCREENSHOT_TIMEOUT_MS = 5e3;
|
|
2038
|
+
function withOperationMetadata(result, captured) {
|
|
2039
|
+
const download = captured?.downloads?.length ? captured.downloads : void 0;
|
|
2040
|
+
const clipboard = captured?.clipboard?.length ? captured.clipboard : void 0;
|
|
2041
|
+
if (!download && !clipboard) return result;
|
|
2042
|
+
return {
|
|
2043
|
+
...result,
|
|
2044
|
+
metadata: { ...result.metadata, ...download ? { download } : {}, ...clipboard ? { clipboard } : {} }
|
|
2045
|
+
};
|
|
2046
|
+
}
|
|
2047
|
+
function clickDelayOption(clickDelay) {
|
|
2048
|
+
return clickDelay !== void 0 && clickDelay > 0 ? { delay: clickDelay } : {};
|
|
2049
|
+
}
|
|
2050
|
+
function readSeamStorageState(action) {
|
|
2051
|
+
return action.state;
|
|
2052
|
+
}
|
|
2053
|
+
var SCREENCAST_PARAMS = {
|
|
2054
|
+
format: "jpeg",
|
|
2055
|
+
quality: 60,
|
|
2056
|
+
maxWidth: 1280,
|
|
2057
|
+
maxHeight: 720,
|
|
2058
|
+
everyNthFrame: 1
|
|
2059
|
+
};
|
|
2060
|
+
var SCREENCAST_ACK_WINDOW = 64;
|
|
2061
|
+
var KEEPALIVE_ECHO_WINDOW_MS = 250;
|
|
2062
|
+
var SCREENCAST_RECOVERY_MS = 1e4;
|
|
2063
|
+
function decodeBase64(data) {
|
|
2064
|
+
const binary = atob(data);
|
|
2065
|
+
const bytes = new Uint8Array(binary.length);
|
|
2066
|
+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
2067
|
+
return bytes;
|
|
2068
|
+
}
|
|
2069
|
+
var WebPlatformAdapter = class {
|
|
2070
|
+
constructor(options = {}) {
|
|
2071
|
+
this.options = options;
|
|
2072
|
+
}
|
|
2073
|
+
platform = "web";
|
|
2074
|
+
browser = null;
|
|
2075
|
+
context = null;
|
|
749
2076
|
page = null;
|
|
2077
|
+
// The native binding of the last observation and its document (438 D-06). Neither crosses the
|
|
2078
|
+
// port (observe returns only the ScreenState).
|
|
750
2079
|
lastRefs = {};
|
|
2080
|
+
epoch;
|
|
751
2081
|
pageLogBuffers = /* @__PURE__ */ new Map();
|
|
752
2082
|
logCaptureConfig = { console: true, network: true };
|
|
753
2083
|
disconnected = false;
|
|
754
2084
|
intentionalClose = false;
|
|
2085
|
+
// Persistent route interception + one-shot dialog arms live until an explicit unroute or
|
|
2086
|
+
// test teardown (D-12/D-18); cleanup() drains both so nothing bleeds across tests (Pitfall 7).
|
|
2087
|
+
armedRoutes = /* @__PURE__ */ new Set();
|
|
2088
|
+
pendingDialogArm = null;
|
|
2089
|
+
// 403 D-05: the live screencast's CDP session, owned here and never handed out.
|
|
2090
|
+
browserName = "chromium";
|
|
2091
|
+
screencast = null;
|
|
2092
|
+
screencastCapability = null;
|
|
2093
|
+
recordingCapability;
|
|
2094
|
+
recordingPageRevision = 0;
|
|
2095
|
+
recordingConfig;
|
|
2096
|
+
// 403-02: the PRODUCT's frame identity, and the CDP ack token it maps to. See `liveStream`.
|
|
2097
|
+
screencastFrameSeq = 0;
|
|
2098
|
+
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
|
+
screencastOnFrame = null;
|
|
2102
|
+
keepaliveTimer = null;
|
|
2103
|
+
lastScreencastSize = null;
|
|
2104
|
+
keepaliveEchoDeadline = null;
|
|
2105
|
+
screencastChain = Promise.resolve();
|
|
2106
|
+
// 428 D-12: the labelled snapshot preview standing in for a screencast withdrawn mid-stream.
|
|
2107
|
+
withdrawnSnapshot = null;
|
|
2108
|
+
screencastRecoveryTimer = null;
|
|
2109
|
+
// 436 D-19, D-21: download and app-clipboard capture, Chromium only; armed around each execute().
|
|
2110
|
+
operationCapture = null;
|
|
2111
|
+
// 436 D-23: Firefox and WebKit run no capture and name themselves as the reason.
|
|
2112
|
+
operationCaptureSupport;
|
|
755
2113
|
async setup(config) {
|
|
2114
|
+
this.recordingConfig = config.recording;
|
|
756
2115
|
const { chromium, firefox, webkit } = await import("playwright-core");
|
|
757
2116
|
const launchers = { chromium, firefox, webkit };
|
|
758
2117
|
const browserName = config.browser?.name ?? "chromium";
|
|
2118
|
+
this.browserName = browserName;
|
|
759
2119
|
const launcher = launchers[browserName];
|
|
2120
|
+
const headless = config.browser?.headless ?? true;
|
|
760
2121
|
try {
|
|
761
2122
|
this.browser = await launcher.launch({
|
|
762
|
-
headless
|
|
763
|
-
...browserName === "chromium" ? { args: ["--disable-blink-features=AutomationControlled"] } : {}
|
|
2123
|
+
headless,
|
|
2124
|
+
...browserName === "chromium" ? { args: ["--disable-blink-features=AutomationControlled"] } : {},
|
|
2125
|
+
...this.options.hostOwnsSignals === true ? { handleSIGINT: false, handleSIGTERM: false, handleSIGHUP: false } : {}
|
|
764
2126
|
});
|
|
765
2127
|
} catch (error) {
|
|
766
2128
|
if (isPlaywrightMissingBrowserError(error)) {
|
|
@@ -778,12 +2140,6 @@ var WebPlatformAdapter = class {
|
|
|
778
2140
|
const contextOptions = {
|
|
779
2141
|
viewport: config.browser?.viewport ?? { width: 1280, height: 720 }
|
|
780
2142
|
};
|
|
781
|
-
if (config.recording?.enabled) {
|
|
782
|
-
contextOptions.recordVideo = {
|
|
783
|
-
dir: config.recording.videoDir,
|
|
784
|
-
size: config.recording.videoSize ?? config.browser?.viewport ?? { width: 1280, height: 720 }
|
|
785
|
-
};
|
|
786
|
-
}
|
|
787
2143
|
if (config.authState?.storageStatePath) {
|
|
788
2144
|
contextOptions.storageState = config.authState.storageStatePath;
|
|
789
2145
|
}
|
|
@@ -797,19 +2153,367 @@ var WebPlatformAdapter = class {
|
|
|
797
2153
|
});
|
|
798
2154
|
this.page = await this.context.newPage();
|
|
799
2155
|
this.attachLogListeners(this.page);
|
|
2156
|
+
if (browserName === "chromium") {
|
|
2157
|
+
this.operationCapture = new OperationCapture({ headless: isHeadlessLaunch({ headless }, process.env) });
|
|
2158
|
+
await this.operationCapture.attach(this.context);
|
|
2159
|
+
this.operationCaptureSupport = { download: true, clipboard: true };
|
|
2160
|
+
} else {
|
|
2161
|
+
this.operationCaptureSupport = { download: false, clipboard: false, reason: browserName };
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
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
|
+
*/
|
|
2180
|
+
get liveStream() {
|
|
2181
|
+
if (this.browserName !== "chromium") return void 0;
|
|
2182
|
+
if (!this.page || !this.context) return void 0;
|
|
2183
|
+
if (this.screencastCapability) return this.screencastCapability;
|
|
2184
|
+
const capability = {
|
|
2185
|
+
kind: "cdp-screencast",
|
|
2186
|
+
start: (onFrame) => this.onScreencastChain(async () => {
|
|
2187
|
+
if (this.screencastOnFrame !== null) return;
|
|
2188
|
+
const page = this.page;
|
|
2189
|
+
const context = this.context;
|
|
2190
|
+
if (!page || !context) return;
|
|
2191
|
+
const cdp = await context.newCDPSession(page);
|
|
2192
|
+
this.screencast = cdp;
|
|
2193
|
+
this.screencastOnFrame = onFrame;
|
|
2194
|
+
this.screencastAckTokens.clear();
|
|
2195
|
+
this.attachScreencast(cdp, onFrame);
|
|
2196
|
+
try {
|
|
2197
|
+
await cdp.send("Page.startScreencast", { ...SCREENCAST_PARAMS });
|
|
2198
|
+
} catch (error) {
|
|
2199
|
+
this.screencast = null;
|
|
2200
|
+
this.screencastOnFrame = null;
|
|
2201
|
+
try {
|
|
2202
|
+
await cdp.detach();
|
|
2203
|
+
} catch {
|
|
2204
|
+
}
|
|
2205
|
+
throw error;
|
|
2206
|
+
}
|
|
2207
|
+
if (this.screencast === cdp) this.armKeepalive(cdp);
|
|
2208
|
+
}),
|
|
2209
|
+
ack: async (frameId) => {
|
|
2210
|
+
const cdp = this.screencast;
|
|
2211
|
+
if (!cdp) return;
|
|
2212
|
+
const sessionId = this.screencastAckTokens.get(frameId);
|
|
2213
|
+
this.screencastAckTokens.delete(frameId);
|
|
2214
|
+
if (sessionId === void 0) return;
|
|
2215
|
+
try {
|
|
2216
|
+
await cdp.send("Page.screencastFrameAck", { sessionId });
|
|
2217
|
+
} catch {
|
|
2218
|
+
}
|
|
2219
|
+
},
|
|
2220
|
+
stop: () => this.onScreencastChain(async () => {
|
|
2221
|
+
this.screencastOnFrame = null;
|
|
2222
|
+
await this.stopWithdrawnSnapshot();
|
|
2223
|
+
await this.detachScreencast();
|
|
2224
|
+
})
|
|
2225
|
+
};
|
|
2226
|
+
this.screencastCapability = capability;
|
|
2227
|
+
return capability;
|
|
2228
|
+
}
|
|
2229
|
+
/** Run `step` after every earlier start, stop and re-bind has settled (428 D-18). */
|
|
2230
|
+
onScreencastChain(step) {
|
|
2231
|
+
const run = this.screencastChain.then(step);
|
|
2232
|
+
this.screencastChain = run.catch(() => void 0);
|
|
2233
|
+
return run;
|
|
2234
|
+
}
|
|
2235
|
+
/** Stop and detach the current CDP session, best-effort, and forget its tokens and keepalive. */
|
|
2236
|
+
async detachScreencast() {
|
|
2237
|
+
const cdp = this.screencast;
|
|
2238
|
+
this.screencast = null;
|
|
2239
|
+
this.clearKeepalive();
|
|
2240
|
+
this.keepaliveEchoDeadline = null;
|
|
2241
|
+
this.screencastAckTokens.clear();
|
|
2242
|
+
if (!cdp) return;
|
|
2243
|
+
try {
|
|
2244
|
+
await cdp.send("Page.stopScreencast");
|
|
2245
|
+
} catch {
|
|
2246
|
+
}
|
|
2247
|
+
try {
|
|
2248
|
+
await cdp.detach();
|
|
2249
|
+
} catch {
|
|
2250
|
+
}
|
|
2251
|
+
}
|
|
2252
|
+
/** Wire `cdp`'s screencast frames to `onFrame`; shared by `start` and `rebindScreencast`. */
|
|
2253
|
+
attachScreencast(cdp, onFrame) {
|
|
2254
|
+
cdp.on("Page.screencastFrame", (event) => {
|
|
2255
|
+
if (this.screencast !== cdp) return;
|
|
2256
|
+
const metadata = event.metadata;
|
|
2257
|
+
this.lastScreencastSize = { width: metadata.deviceWidth, height: metadata.deviceHeight };
|
|
2258
|
+
this.armKeepalive(cdp);
|
|
2259
|
+
const echoDeadline = this.keepaliveEchoDeadline;
|
|
2260
|
+
this.keepaliveEchoDeadline = null;
|
|
2261
|
+
if (echoDeadline !== null && Date.now() <= echoDeadline) {
|
|
2262
|
+
void cdp.send("Page.screencastFrameAck", { sessionId: event.sessionId }).catch(() => void 0);
|
|
2263
|
+
return;
|
|
2264
|
+
}
|
|
2265
|
+
const id = ++this.screencastFrameSeq;
|
|
2266
|
+
this.screencastAckTokens.set(id, event.sessionId);
|
|
2267
|
+
while (this.screencastAckTokens.size > SCREENCAST_ACK_WINDOW) {
|
|
2268
|
+
const oldest = this.screencastAckTokens.keys().next();
|
|
2269
|
+
if (oldest.done === true) break;
|
|
2270
|
+
const token = this.screencastAckTokens.get(oldest.value);
|
|
2271
|
+
this.screencastAckTokens.delete(oldest.value);
|
|
2272
|
+
if (token !== void 0) {
|
|
2273
|
+
void cdp.send("Page.screencastFrameAck", { sessionId: token }).catch(() => void 0);
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
onFrame({
|
|
2277
|
+
id,
|
|
2278
|
+
bytes: decodeBase64(event.data),
|
|
2279
|
+
metadata: {
|
|
2280
|
+
width: metadata.deviceWidth,
|
|
2281
|
+
height: metadata.deviceHeight,
|
|
2282
|
+
// CDP's `TimeSinceEpoch` is SECONDS; every other layer reads epoch ms (428 D-15).
|
|
2283
|
+
...metadata.timestamp === void 0 ? {} : { timestamp: Math.round(metadata.timestamp * 1e3) },
|
|
2284
|
+
scrollOffsetX: metadata.scrollOffsetX,
|
|
2285
|
+
scrollOffsetY: metadata.scrollOffsetY
|
|
2286
|
+
}
|
|
2287
|
+
});
|
|
2288
|
+
});
|
|
2289
|
+
}
|
|
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
|
+
*/
|
|
2295
|
+
setActivePage(page) {
|
|
2296
|
+
this.page = page;
|
|
2297
|
+
this.recordingPageRevision++;
|
|
2298
|
+
if (this.screencastOnFrame !== null) {
|
|
2299
|
+
void this.onScreencastChain(() => this.rebindScreencast());
|
|
2300
|
+
}
|
|
2301
|
+
}
|
|
2302
|
+
/**
|
|
2303
|
+
* Move the running screencast onto `this.page` without its subscribers noticing: the old session
|
|
2304
|
+
* is stopped and detached, a new one is started with the stored `onFrame`, and the id sequence
|
|
2305
|
+
* continues. Ids still pending in the hub settle through its ack bound and are dropped here as
|
|
2306
|
+
* unknown. Best-effort throughout; it never throws into a step.
|
|
2307
|
+
*/
|
|
2308
|
+
async rebindScreencast(options = {}) {
|
|
2309
|
+
const onFrame = this.screencastOnFrame;
|
|
2310
|
+
const page = this.page;
|
|
2311
|
+
const context = this.context;
|
|
2312
|
+
if (onFrame === null || !page || !context) return;
|
|
2313
|
+
await this.detachScreencast();
|
|
2314
|
+
this.lastScreencastSize = null;
|
|
2315
|
+
if (options.focus !== false) {
|
|
2316
|
+
try {
|
|
2317
|
+
await page.bringToFront();
|
|
2318
|
+
} catch {
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2321
|
+
let cdp;
|
|
2322
|
+
try {
|
|
2323
|
+
cdp = await context.newCDPSession(page);
|
|
2324
|
+
} catch {
|
|
2325
|
+
this.handleScreencastWithdrawn(null);
|
|
2326
|
+
return;
|
|
2327
|
+
}
|
|
2328
|
+
this.screencast = cdp;
|
|
2329
|
+
this.attachScreencast(cdp, onFrame);
|
|
2330
|
+
try {
|
|
2331
|
+
await cdp.send("Page.startScreencast", { ...SCREENCAST_PARAMS });
|
|
2332
|
+
} catch {
|
|
2333
|
+
this.handleScreencastWithdrawn(cdp);
|
|
2334
|
+
return;
|
|
2335
|
+
}
|
|
2336
|
+
if (this.screencast !== cdp) return;
|
|
2337
|
+
this.armKeepalive(cdp);
|
|
2338
|
+
await this.stopWithdrawnSnapshot();
|
|
2339
|
+
}
|
|
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
|
+
*/
|
|
2348
|
+
handleScreencastWithdrawn(cdp) {
|
|
2349
|
+
void this.onScreencastChain(async () => {
|
|
2350
|
+
if (this.screencast !== cdp) return;
|
|
2351
|
+
const onFrame = this.screencastOnFrame;
|
|
2352
|
+
if (onFrame === null || this.withdrawnSnapshot !== null || !this.browserIsLive()) return;
|
|
2353
|
+
await this.detachScreencast();
|
|
2354
|
+
const snapshot = (0, import_agent_qa_runner6.createSnapshotLiveStream)({ capture: () => this.screenshot() });
|
|
2355
|
+
this.withdrawnSnapshot = snapshot;
|
|
2356
|
+
await snapshot.start((frame) => {
|
|
2357
|
+
if (this.withdrawnSnapshot !== snapshot) return;
|
|
2358
|
+
onFrame({ ...frame, id: ++this.screencastFrameSeq });
|
|
2359
|
+
});
|
|
2360
|
+
this.armScreencastRecovery();
|
|
2361
|
+
});
|
|
2362
|
+
}
|
|
2363
|
+
/** While the snapshot fallback runs, try the screencast again every SCREENCAST_RECOVERY_MS (428 WR-08). */
|
|
2364
|
+
armScreencastRecovery() {
|
|
2365
|
+
this.clearScreencastRecovery();
|
|
2366
|
+
const timer = setTimeout(() => {
|
|
2367
|
+
this.screencastRecoveryTimer = null;
|
|
2368
|
+
void this.onScreencastChain(async () => {
|
|
2369
|
+
if (this.withdrawnSnapshot === null || this.screencastOnFrame === null || !this.browserIsLive()) return;
|
|
2370
|
+
await this.rebindScreencast({ focus: false });
|
|
2371
|
+
if (this.withdrawnSnapshot !== null) this.armScreencastRecovery();
|
|
2372
|
+
});
|
|
2373
|
+
}, SCREENCAST_RECOVERY_MS);
|
|
2374
|
+
timer.unref?.();
|
|
2375
|
+
this.screencastRecoveryTimer = timer;
|
|
2376
|
+
}
|
|
2377
|
+
clearScreencastRecovery() {
|
|
2378
|
+
if (this.screencastRecoveryTimer !== null) clearTimeout(this.screencastRecoveryTimer);
|
|
2379
|
+
this.screencastRecoveryTimer = null;
|
|
2380
|
+
}
|
|
2381
|
+
browserIsLive() {
|
|
2382
|
+
return !this.intentionalClose && !this.disconnected && this.browser?.isConnected() === true;
|
|
2383
|
+
}
|
|
2384
|
+
async stopWithdrawnSnapshot() {
|
|
2385
|
+
this.clearScreencastRecovery();
|
|
2386
|
+
const snapshot = this.withdrawnSnapshot;
|
|
2387
|
+
this.withdrawnSnapshot = null;
|
|
2388
|
+
try {
|
|
2389
|
+
await snapshot?.stop();
|
|
2390
|
+
} catch {
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2393
|
+
clearKeepalive() {
|
|
2394
|
+
if (this.keepaliveTimer !== null) clearTimeout(this.keepaliveTimer);
|
|
2395
|
+
this.keepaliveTimer = null;
|
|
2396
|
+
}
|
|
2397
|
+
/** (Re-)arm the quiet-page keepalive for `cdp`; every screencast frame pushes it back. */
|
|
2398
|
+
armKeepalive(cdp) {
|
|
2399
|
+
this.clearKeepalive();
|
|
2400
|
+
const timer = setTimeout(() => {
|
|
2401
|
+
void this.fireKeepalive(cdp);
|
|
2402
|
+
}, import_agent_qa_runner6.LIVE_KEEPALIVE_QUIET_MS);
|
|
2403
|
+
timer.unref?.();
|
|
2404
|
+
this.keepaliveTimer = timer;
|
|
2405
|
+
}
|
|
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
|
+
*/
|
|
2414
|
+
async fireKeepalive(cdp) {
|
|
2415
|
+
this.keepaliveTimer = null;
|
|
2416
|
+
if (this.screencast !== cdp) return;
|
|
2417
|
+
const framesBefore = this.screencastFrameSeq;
|
|
2418
|
+
let data;
|
|
2419
|
+
try {
|
|
2420
|
+
const result = await cdp.send("Page.captureScreenshot", { format: "jpeg", quality: 60 });
|
|
2421
|
+
data = result.data;
|
|
2422
|
+
} catch {
|
|
2423
|
+
if (this.screencast !== cdp) return;
|
|
2424
|
+
const withdrawn = this.screencastOnFrame !== null && this.browserIsLive() && !await this.sessionAnswers(cdp);
|
|
2425
|
+
if (this.screencast !== cdp) return;
|
|
2426
|
+
if (withdrawn) this.handleScreencastWithdrawn(cdp);
|
|
2427
|
+
else this.armKeepalive(cdp);
|
|
2428
|
+
return;
|
|
2429
|
+
}
|
|
2430
|
+
if (this.screencast !== cdp) return;
|
|
2431
|
+
const onFrame = this.screencastOnFrame;
|
|
2432
|
+
if (onFrame === null) return;
|
|
2433
|
+
if (this.screencastFrameSeq !== framesBefore) return;
|
|
2434
|
+
const size = this.lastScreencastSize ?? this.page?.viewportSize() ?? { width: 0, height: 0 };
|
|
2435
|
+
onFrame({
|
|
2436
|
+
id: ++this.screencastFrameSeq,
|
|
2437
|
+
bytes: decodeBase64(data),
|
|
2438
|
+
metadata: { width: size.width, height: size.height, timestamp: Date.now() }
|
|
2439
|
+
});
|
|
2440
|
+
this.keepaliveEchoDeadline = Date.now() + KEEPALIVE_ECHO_WINDOW_MS;
|
|
2441
|
+
this.armKeepalive(cdp);
|
|
2442
|
+
}
|
|
2443
|
+
/** Whether `cdp` still answers a command that cannot fail on a usable session. */
|
|
2444
|
+
async sessionAnswers(cdp) {
|
|
2445
|
+
try {
|
|
2446
|
+
await cdp.send("Runtime.evaluate", { expression: "1" });
|
|
2447
|
+
return true;
|
|
2448
|
+
} catch {
|
|
2449
|
+
return false;
|
|
2450
|
+
}
|
|
2451
|
+
}
|
|
2452
|
+
get recording() {
|
|
2453
|
+
if (!this.page || !this.context || !this.recordingConfig) return void 0;
|
|
2454
|
+
return this.recordingCapability ??= createScreencastRecording(
|
|
2455
|
+
() => this.page,
|
|
2456
|
+
this.recordingConfig,
|
|
2457
|
+
() => this.recordingPageRevision
|
|
2458
|
+
);
|
|
800
2459
|
}
|
|
801
2460
|
async cleanup() {
|
|
802
2461
|
this.intentionalClose = true;
|
|
2462
|
+
try {
|
|
2463
|
+
await this.recordingCapability?.stop();
|
|
2464
|
+
} catch {
|
|
2465
|
+
}
|
|
2466
|
+
this.recordingCapability = void 0;
|
|
2467
|
+
this.recordingConfig = void 0;
|
|
2468
|
+
this.screencastOnFrame = null;
|
|
2469
|
+
await this.stopWithdrawnSnapshot();
|
|
2470
|
+
try {
|
|
2471
|
+
await this.screencastCapability?.stop();
|
|
2472
|
+
} catch {
|
|
2473
|
+
}
|
|
2474
|
+
this.screencastCapability = null;
|
|
2475
|
+
try {
|
|
2476
|
+
await this.operationCapture?.dispose();
|
|
2477
|
+
} catch {
|
|
2478
|
+
}
|
|
2479
|
+
if (this.context) {
|
|
2480
|
+
try {
|
|
2481
|
+
await this.context.unrouteAll({ behavior: "ignoreErrors" });
|
|
2482
|
+
} catch {
|
|
2483
|
+
}
|
|
2484
|
+
try {
|
|
2485
|
+
await this.context.setOffline(false);
|
|
2486
|
+
} catch {
|
|
2487
|
+
}
|
|
2488
|
+
}
|
|
2489
|
+
if (this.page && this.pendingDialogArm) {
|
|
2490
|
+
try {
|
|
2491
|
+
this.page.off("dialog", this.pendingDialogArm);
|
|
2492
|
+
} catch {
|
|
2493
|
+
}
|
|
2494
|
+
}
|
|
2495
|
+
this.armedRoutes.clear();
|
|
2496
|
+
this.pendingDialogArm = null;
|
|
2497
|
+
await disposeEpoch(this.epoch);
|
|
2498
|
+
this.epoch = void 0;
|
|
803
2499
|
if (this.browser) {
|
|
804
2500
|
await this.browser.close();
|
|
805
2501
|
}
|
|
806
2502
|
this.browser = null;
|
|
807
2503
|
this.context = null;
|
|
808
2504
|
this.page = null;
|
|
2505
|
+
this.operationCapture = null;
|
|
2506
|
+
this.operationCaptureSupport = void 0;
|
|
809
2507
|
this.lastRefs = {};
|
|
810
2508
|
this.pageLogBuffers.clear();
|
|
811
2509
|
this.disconnected = false;
|
|
812
2510
|
this.intentionalClose = false;
|
|
2511
|
+
this.screencast = null;
|
|
2512
|
+
this.screencastOnFrame = null;
|
|
2513
|
+
this.clearKeepalive();
|
|
2514
|
+
this.lastScreencastSize = null;
|
|
2515
|
+
this.screencastAckTokens.clear();
|
|
2516
|
+
this.screencastChain = Promise.resolve();
|
|
813
2517
|
}
|
|
814
2518
|
get isBrowserDisconnected() {
|
|
815
2519
|
return this.disconnected;
|
|
@@ -818,14 +2522,63 @@ var WebPlatformAdapter = class {
|
|
|
818
2522
|
if (!this.page) {
|
|
819
2523
|
throw new Error("WebPlatformAdapter not initialized \u2014 call setup() first");
|
|
820
2524
|
}
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
2525
|
+
const page = this.page;
|
|
2526
|
+
await waitForPageReady(page);
|
|
2527
|
+
const epoch = await captureEpoch(page);
|
|
2528
|
+
try {
|
|
2529
|
+
const state = await this.observeDocument(page, options);
|
|
2530
|
+
await disposeEpoch(this.epoch);
|
|
2531
|
+
this.epoch = epoch;
|
|
2532
|
+
return state;
|
|
2533
|
+
} catch (error) {
|
|
2534
|
+
await disposeEpoch(epoch);
|
|
2535
|
+
throw error;
|
|
2536
|
+
}
|
|
2537
|
+
}
|
|
2538
|
+
async observeDocument(page, options) {
|
|
2539
|
+
const ariaTree = await snapshotRoot(page, OBSERVE_SNAPSHOT);
|
|
2540
|
+
const { nodes, nativeRefs } = parseAriaSnapshot(ariaTree);
|
|
2541
|
+
if (nodes.length > 0) {
|
|
2542
|
+
await enrichWebNodes(page, nodes, nativeRefs);
|
|
2543
|
+
}
|
|
2544
|
+
const { viewport, scroll } = await captureWebScroll(page);
|
|
2545
|
+
let domContext;
|
|
2546
|
+
if (options?.extractDom) {
|
|
2547
|
+
try {
|
|
2548
|
+
domContext = await extractDom(page);
|
|
2549
|
+
} catch {
|
|
2550
|
+
}
|
|
2551
|
+
}
|
|
2552
|
+
const { state, binding } = (0, import_agent_qa_runner6.buildScreenState)({
|
|
2553
|
+
platform: "web",
|
|
2554
|
+
nodes,
|
|
2555
|
+
viewport,
|
|
2556
|
+
scroll,
|
|
2557
|
+
coordSpace: "viewport",
|
|
2558
|
+
timestamp: Date.now(),
|
|
2559
|
+
url: page.url(),
|
|
2560
|
+
...domContext !== void 0 ? { domContext } : {}
|
|
824
2561
|
});
|
|
825
|
-
this.lastRefs = state.
|
|
2562
|
+
this.lastRefs = buildRefMap(binding, nodes, nativeRefs, state.elements);
|
|
826
2563
|
return state;
|
|
827
2564
|
}
|
|
828
|
-
async execute(action) {
|
|
2565
|
+
async execute(action, options) {
|
|
2566
|
+
if (!this.page) {
|
|
2567
|
+
throw new Error("WebPlatformAdapter not initialized \u2014 call setup() first");
|
|
2568
|
+
}
|
|
2569
|
+
const capture = this.operationCapture;
|
|
2570
|
+
await capture?.arm(action, options, this.page);
|
|
2571
|
+
const result = await this.executeWithErrorResult(action);
|
|
2572
|
+
this.recordingCapability?.endAction();
|
|
2573
|
+
let captured;
|
|
2574
|
+
try {
|
|
2575
|
+
captured = await capture?.collect();
|
|
2576
|
+
} catch {
|
|
2577
|
+
captured = void 0;
|
|
2578
|
+
}
|
|
2579
|
+
return withOperationMetadata(result, captured);
|
|
2580
|
+
}
|
|
2581
|
+
async executeWithErrorResult(action) {
|
|
829
2582
|
if (!this.page) {
|
|
830
2583
|
throw new Error("WebPlatformAdapter not initialized \u2014 call setup() first");
|
|
831
2584
|
}
|
|
@@ -857,7 +2610,7 @@ var WebPlatformAdapter = class {
|
|
|
857
2610
|
async screenshot() {
|
|
858
2611
|
try {
|
|
859
2612
|
if (this.page) {
|
|
860
|
-
return await this.page.screenshot({ scale: "css" });
|
|
2613
|
+
return await this.page.screenshot({ scale: "css", timeout: SCREENSHOT_TIMEOUT_MS });
|
|
861
2614
|
}
|
|
862
2615
|
return void 0;
|
|
863
2616
|
} catch {
|
|
@@ -867,15 +2620,6 @@ var WebPlatformAdapter = class {
|
|
|
867
2620
|
getPage() {
|
|
868
2621
|
return this.page;
|
|
869
2622
|
}
|
|
870
|
-
async getVideoPath() {
|
|
871
|
-
try {
|
|
872
|
-
const video = this.page?.video();
|
|
873
|
-
if (!video) return null;
|
|
874
|
-
return await video.path();
|
|
875
|
-
} catch {
|
|
876
|
-
return null;
|
|
877
|
-
}
|
|
878
|
-
}
|
|
879
2623
|
drainConsoleLogs(level) {
|
|
880
2624
|
const all = [];
|
|
881
2625
|
for (const buffers of this.pageLogBuffers.values()) {
|
|
@@ -896,6 +2640,18 @@ var WebPlatformAdapter = class {
|
|
|
896
2640
|
if (!urlPattern) return all;
|
|
897
2641
|
return all.filter((e) => e.url.includes(urlPattern));
|
|
898
2642
|
}
|
|
2643
|
+
/** 439 D-04: a completed download's bytes held in memory, by its sha256. */
|
|
2644
|
+
readDownloadBytes(sha256) {
|
|
2645
|
+
return this.operationCapture?.readDownloadBytes(sha256);
|
|
2646
|
+
}
|
|
2647
|
+
peekNetworkLogs(urlPattern) {
|
|
2648
|
+
const all = [];
|
|
2649
|
+
for (const buffers of this.pageLogBuffers.values()) {
|
|
2650
|
+
all.push(...buffers.networkLogs.slice(buffers.networkDrainIndex));
|
|
2651
|
+
}
|
|
2652
|
+
if (!urlPattern) return all;
|
|
2653
|
+
return all.filter((e) => e.url.includes(urlPattern));
|
|
2654
|
+
}
|
|
899
2655
|
attachLogListeners(page) {
|
|
900
2656
|
const buffers = {
|
|
901
2657
|
consoleLogs: [],
|
|
@@ -983,6 +2739,12 @@ var WebPlatformAdapter = class {
|
|
|
983
2739
|
async executeAction(action) {
|
|
984
2740
|
const page = this.page;
|
|
985
2741
|
const resolver = new ElementResolver(page, this.lastRefs);
|
|
2742
|
+
for (const ref of laneRefs(action)) {
|
|
2743
|
+
const entry = this.lastRefs[ref];
|
|
2744
|
+
if (entry === void 0) continue;
|
|
2745
|
+
const check = action.type === "scroll" ? await checkDocument(page, this.epoch) : await checkBinding(page, this.epoch, entry, OBSERVE_SNAPSHOT);
|
|
2746
|
+
if (check.outcome !== "ok") return bindingRefusal(check.outcome, ref);
|
|
2747
|
+
}
|
|
986
2748
|
if ("ref" in action && typeof action.ref === "string") {
|
|
987
2749
|
const refData = this.lastRefs[action.ref];
|
|
988
2750
|
const role = refData?.role;
|
|
@@ -997,15 +2759,18 @@ var WebPlatformAdapter = class {
|
|
|
997
2759
|
const box = await resolver.getBoundingBox(action.ref);
|
|
998
2760
|
const coordinates = box ? { x: box.x + box.width / 2, y: box.y + box.height / 2 } : void 0;
|
|
999
2761
|
await locator.click({
|
|
1000
|
-
...clickDelayOption(action.clickDelay)
|
|
2762
|
+
...clickDelayOption(action.clickDelay),
|
|
2763
|
+
...this.operationCapture ? { noWaitAfter: true } : {}
|
|
1001
2764
|
});
|
|
1002
2765
|
return { success: true, coordinates, boundingBox: box ?? void 0 };
|
|
1003
2766
|
}
|
|
1004
2767
|
case "fill": {
|
|
1005
2768
|
const locator = resolver.resolve(action.ref);
|
|
1006
2769
|
const box = await resolver.getBoundingBox(action.ref);
|
|
1007
|
-
|
|
1008
|
-
|
|
2770
|
+
return runWebFillCompound(locator, box ?? void 0, action);
|
|
2771
|
+
}
|
|
2772
|
+
case "openApp": {
|
|
2773
|
+
return webOpenAppError();
|
|
1009
2774
|
}
|
|
1010
2775
|
case "select": {
|
|
1011
2776
|
const locator = resolver.resolve(action.ref);
|
|
@@ -1014,53 +2779,44 @@ var WebPlatformAdapter = class {
|
|
|
1014
2779
|
return { success: true, boundingBox: box ?? void 0 };
|
|
1015
2780
|
}
|
|
1016
2781
|
case "navigate": {
|
|
1017
|
-
|
|
2782
|
+
switch (action.to) {
|
|
2783
|
+
case "url":
|
|
2784
|
+
await page.goto(action.url, { waitUntil: "domcontentloaded" });
|
|
2785
|
+
break;
|
|
2786
|
+
case "back":
|
|
2787
|
+
await page.goBack({ waitUntil: "domcontentloaded" });
|
|
2788
|
+
break;
|
|
2789
|
+
case "forward":
|
|
2790
|
+
await page.goForward({ waitUntil: "domcontentloaded" });
|
|
2791
|
+
break;
|
|
2792
|
+
case "reload":
|
|
2793
|
+
await page.reload({ waitUntil: "domcontentloaded" });
|
|
2794
|
+
break;
|
|
2795
|
+
}
|
|
1018
2796
|
await waitForPageReady(page);
|
|
1019
2797
|
return { success: true };
|
|
1020
2798
|
}
|
|
1021
2799
|
case "scroll": {
|
|
1022
|
-
|
|
1023
|
-
const deltaY = action.scrollType === "vertical" ? action.value : 0;
|
|
1024
|
-
let coordinates;
|
|
1025
|
-
let boundingBox;
|
|
1026
|
-
let scrollData;
|
|
1027
|
-
if (action.ref) {
|
|
1028
|
-
const refData = this.lastRefs[action.ref];
|
|
1029
|
-
if (refData) {
|
|
1030
|
-
const scrollAxis = action.scrollType === "horizontal" ? "horizontal" : "vertical";
|
|
1031
|
-
const result = await scrollWithRef(page, refData, deltaX, deltaY, scrollAxis);
|
|
1032
|
-
if (result.bounds) {
|
|
1033
|
-
coordinates = { x: result.bounds.x + result.bounds.width / 2, y: result.bounds.y + result.bounds.height / 2 };
|
|
1034
|
-
boundingBox = result.bounds;
|
|
1035
|
-
}
|
|
1036
|
-
scrollData = { scrolled: result.scrolled, scrolledContainer: result.scrolledContainer };
|
|
1037
|
-
} else {
|
|
1038
|
-
await page.mouse.wheel(deltaX, deltaY);
|
|
1039
|
-
}
|
|
1040
|
-
} else {
|
|
1041
|
-
await page.mouse.wheel(deltaX, deltaY);
|
|
1042
|
-
}
|
|
1043
|
-
await page.evaluate(`new Promise(resolve => {
|
|
1044
|
-
const t = setTimeout(() => resolve('timeout'), 500);
|
|
1045
|
-
requestAnimationFrame(() => requestAnimationFrame(() => {
|
|
1046
|
-
clearTimeout(t);
|
|
1047
|
-
resolve('raf');
|
|
1048
|
-
}));
|
|
1049
|
-
})`);
|
|
1050
|
-
return { success: true, coordinates, boundingBox, data: scrollData };
|
|
2800
|
+
return runWebScrollCompound(page, this.lastRefs, action);
|
|
1051
2801
|
}
|
|
1052
2802
|
case "waitFor": {
|
|
1053
|
-
const timeout = action.
|
|
2803
|
+
const timeout = action.timeoutMs;
|
|
1054
2804
|
try {
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
2805
|
+
switch (action.on) {
|
|
2806
|
+
case "text":
|
|
2807
|
+
await page.getByText(action.text).first().waitFor({ state: "visible", timeout });
|
|
2808
|
+
break;
|
|
2809
|
+
case "gone":
|
|
2810
|
+
await page.getByText(action.text).first().waitFor({ state: "hidden", timeout });
|
|
2811
|
+
break;
|
|
2812
|
+
case "selector":
|
|
2813
|
+
await page.waitForSelector(action.selector, { timeout });
|
|
2814
|
+
break;
|
|
1063
2815
|
}
|
|
2816
|
+
} catch {
|
|
2817
|
+
const what = action.on === "selector" ? `selector "${action.selector}"` : `text "${action.text}"`;
|
|
2818
|
+
const how = action.on === "gone" ? "was still present after" : "did not appear within";
|
|
2819
|
+
return { success: false, error: `waitFor: ${what} ${how} ${timeout}ms.` };
|
|
1064
2820
|
}
|
|
1065
2821
|
return { success: true };
|
|
1066
2822
|
}
|
|
@@ -1080,12 +2836,18 @@ var WebPlatformAdapter = class {
|
|
|
1080
2836
|
}
|
|
1081
2837
|
}
|
|
1082
2838
|
case "keypress": {
|
|
1083
|
-
const effectiveKeys = (0,
|
|
2839
|
+
const effectiveKeys = (0, import_agent_qa_runner7.convertKeysForPlatform)(action.keys, {
|
|
1084
2840
|
enabled: action.convertPlatformKeys !== false,
|
|
1085
|
-
isMac: (0,
|
|
2841
|
+
isMac: (0, import_agent_qa_runner7.isMacPlatform)()
|
|
1086
2842
|
});
|
|
1087
2843
|
for (const key of effectiveKeys) {
|
|
1088
|
-
|
|
2844
|
+
if (action.mode === "down") {
|
|
2845
|
+
await page.keyboard.down(key);
|
|
2846
|
+
} else if (action.mode === "up") {
|
|
2847
|
+
await page.keyboard.up(key);
|
|
2848
|
+
} else {
|
|
2849
|
+
await page.keyboard.press(key);
|
|
2850
|
+
}
|
|
1089
2851
|
}
|
|
1090
2852
|
return { success: true };
|
|
1091
2853
|
}
|
|
@@ -1131,75 +2893,110 @@ var WebPlatformAdapter = class {
|
|
|
1131
2893
|
await page.evaluate(`navigator.clipboard.writeText(${JSON.stringify(text)})`);
|
|
1132
2894
|
return { success: true, boundingBox: box ?? void 0, data: { copied: text } };
|
|
1133
2895
|
}
|
|
1134
|
-
case "
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
} else {
|
|
1151
|
-
|
|
2896
|
+
case "tab": {
|
|
2897
|
+
if (action.action === "new") {
|
|
2898
|
+
const newPage = await this.context.newPage();
|
|
2899
|
+
this.attachLogListeners(newPage);
|
|
2900
|
+
await newPage.goto(action.url, { waitUntil: "domcontentloaded" });
|
|
2901
|
+
await waitForPageReady(newPage);
|
|
2902
|
+
this.setActivePage(newPage);
|
|
2903
|
+
return { success: true };
|
|
2904
|
+
}
|
|
2905
|
+
const pages = this.context.pages();
|
|
2906
|
+
let target;
|
|
2907
|
+
if (action.index !== void 0) {
|
|
2908
|
+
if (action.index < 0 || action.index >= pages.length) {
|
|
2909
|
+
return { success: false, error: `Tab index ${action.index} out of range (${pages.length} tabs open)` };
|
|
2910
|
+
}
|
|
2911
|
+
target = pages[action.index];
|
|
2912
|
+
} else if (action.title !== void 0) {
|
|
2913
|
+
for (const p of pages) {
|
|
2914
|
+
const t = await p.title();
|
|
2915
|
+
if (t.includes(action.title)) {
|
|
2916
|
+
target = p;
|
|
2917
|
+
break;
|
|
2918
|
+
}
|
|
2919
|
+
}
|
|
2920
|
+
} else if (action.url !== void 0) {
|
|
2921
|
+
target = pages.find((p) => p.url().includes(action.url));
|
|
1152
2922
|
}
|
|
1153
|
-
|
|
2923
|
+
if (!target) {
|
|
2924
|
+
return { success: false, error: `No tab found matching ${JSON.stringify({ index: action.index, title: action.title, url: action.url })}` };
|
|
2925
|
+
}
|
|
2926
|
+
this.setActivePage(target);
|
|
1154
2927
|
return { success: true };
|
|
1155
2928
|
}
|
|
1156
|
-
case "
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
2929
|
+
case "inspect": {
|
|
2930
|
+
switch (action.what) {
|
|
2931
|
+
case "console": {
|
|
2932
|
+
let entries;
|
|
2933
|
+
if (action.tab) {
|
|
2934
|
+
const targetPage = await this.resolveTabTargetAsync(action.tab);
|
|
2935
|
+
if (!targetPage) {
|
|
2936
|
+
return { success: false, error: `No tab found matching ${JSON.stringify(action.tab)}` };
|
|
2937
|
+
}
|
|
2938
|
+
const buffers = this.pageLogBuffers.get(targetPage);
|
|
2939
|
+
entries = buffers ? [...buffers.consoleLogs] : [];
|
|
2940
|
+
} else {
|
|
2941
|
+
entries = [];
|
|
2942
|
+
for (const buffers of this.pageLogBuffers.values()) {
|
|
2943
|
+
entries.push(...buffers.consoleLogs);
|
|
2944
|
+
}
|
|
2945
|
+
}
|
|
2946
|
+
if (action.level) {
|
|
2947
|
+
entries = entries.filter((e) => e.level === action.level);
|
|
2948
|
+
}
|
|
2949
|
+
return { success: true, data: entries };
|
|
1162
2950
|
}
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
2951
|
+
case "network": {
|
|
2952
|
+
let entries;
|
|
2953
|
+
if (action.tab) {
|
|
2954
|
+
const targetPage = await this.resolveTabTargetAsync(action.tab);
|
|
2955
|
+
if (!targetPage) {
|
|
2956
|
+
return { success: false, error: `No tab found matching ${JSON.stringify(action.tab)}` };
|
|
2957
|
+
}
|
|
2958
|
+
const buffers = this.pageLogBuffers.get(targetPage);
|
|
2959
|
+
entries = buffers ? [...buffers.networkLogs] : [];
|
|
2960
|
+
} else {
|
|
2961
|
+
entries = [];
|
|
2962
|
+
for (const buffers of this.pageLogBuffers.values()) {
|
|
2963
|
+
entries.push(...buffers.networkLogs);
|
|
2964
|
+
}
|
|
2965
|
+
}
|
|
2966
|
+
if (action.urlPattern) {
|
|
2967
|
+
entries = entries.filter((e) => e.url.includes(action.urlPattern));
|
|
2968
|
+
}
|
|
2969
|
+
return { success: true, data: entries };
|
|
1169
2970
|
}
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
let entries;
|
|
1178
|
-
if (action.tab) {
|
|
1179
|
-
const targetPage = await this.resolveTabTargetAsync(action.tab);
|
|
1180
|
-
if (!targetPage) {
|
|
1181
|
-
return { success: false, error: `No tab found matching ${JSON.stringify(action.tab)}` };
|
|
2971
|
+
case "cookies": {
|
|
2972
|
+
const cookies = await this.context.cookies();
|
|
2973
|
+
if (action.name) {
|
|
2974
|
+
const cookie = cookies.find((c) => c.name === action.name) ?? null;
|
|
2975
|
+
return { success: true, data: cookie };
|
|
2976
|
+
}
|
|
2977
|
+
return { success: true, data: cookies };
|
|
1182
2978
|
}
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
2979
|
+
case "localStorage": {
|
|
2980
|
+
const pageUrl = page.url();
|
|
2981
|
+
if (pageUrl === "about:blank") {
|
|
2982
|
+
return { success: true, data: action.key ? null : {} };
|
|
2983
|
+
}
|
|
2984
|
+
if (action.key) {
|
|
2985
|
+
const value = await page.evaluate(
|
|
2986
|
+
(k) => localStorage.getItem(k),
|
|
2987
|
+
action.key
|
|
2988
|
+
);
|
|
2989
|
+
return { success: true, data: value };
|
|
2990
|
+
}
|
|
2991
|
+
const all = await page.evaluate(
|
|
2992
|
+
() => Object.fromEntries(
|
|
2993
|
+
Object.keys(localStorage).map((k) => [k, localStorage.getItem(k)])
|
|
2994
|
+
)
|
|
2995
|
+
);
|
|
2996
|
+
return { success: true, data: all };
|
|
1189
2997
|
}
|
|
1190
2998
|
}
|
|
1191
|
-
|
|
1192
|
-
entries = entries.filter((e) => e.url.includes(action.urlPattern));
|
|
1193
|
-
}
|
|
1194
|
-
return { success: true, data: entries };
|
|
1195
|
-
}
|
|
1196
|
-
case "readCookies": {
|
|
1197
|
-
const cookies = await this.context.cookies();
|
|
1198
|
-
if (action.name) {
|
|
1199
|
-
const cookie = cookies.find((c) => c.name === action.name) ?? null;
|
|
1200
|
-
return { success: true, data: cookie };
|
|
1201
|
-
}
|
|
1202
|
-
return { success: true, data: cookies };
|
|
2999
|
+
return { success: true };
|
|
1203
3000
|
}
|
|
1204
3001
|
case "setCookies": {
|
|
1205
3002
|
const cookiesWithDefaults = action.cookies.map((c) => ({
|
|
@@ -1209,25 +3006,6 @@ var WebPlatformAdapter = class {
|
|
|
1209
3006
|
await this.context.addCookies(cookiesWithDefaults);
|
|
1210
3007
|
return { success: true };
|
|
1211
3008
|
}
|
|
1212
|
-
case "readLocalStorage": {
|
|
1213
|
-
const pageUrl = page.url();
|
|
1214
|
-
if (pageUrl === "about:blank") {
|
|
1215
|
-
return { success: true, data: action.key ? null : {} };
|
|
1216
|
-
}
|
|
1217
|
-
if (action.key) {
|
|
1218
|
-
const value = await page.evaluate(
|
|
1219
|
-
(k) => localStorage.getItem(k),
|
|
1220
|
-
action.key
|
|
1221
|
-
);
|
|
1222
|
-
return { success: true, data: value };
|
|
1223
|
-
}
|
|
1224
|
-
const all = await page.evaluate(
|
|
1225
|
-
() => Object.fromEntries(
|
|
1226
|
-
Object.keys(localStorage).map((k) => [k, localStorage.getItem(k)])
|
|
1227
|
-
)
|
|
1228
|
-
);
|
|
1229
|
-
return { success: true, data: all };
|
|
1230
|
-
}
|
|
1231
3009
|
case "setLocalStorage": {
|
|
1232
3010
|
await page.evaluate((entries) => {
|
|
1233
3011
|
for (const { key, value } of entries) {
|
|
@@ -1236,8 +3014,37 @@ var WebPlatformAdapter = class {
|
|
|
1236
3014
|
}, action.entries);
|
|
1237
3015
|
return { success: true };
|
|
1238
3016
|
}
|
|
1239
|
-
case "
|
|
1240
|
-
|
|
3017
|
+
case "focus": {
|
|
3018
|
+
const locator = resolver.resolve(action.ref);
|
|
3019
|
+
const box = await resolver.getBoundingBox(action.ref);
|
|
3020
|
+
await locator.focus();
|
|
3021
|
+
return { success: true, boundingBox: box ?? void 0 };
|
|
3022
|
+
}
|
|
3023
|
+
case "assertValue": {
|
|
3024
|
+
let value;
|
|
3025
|
+
if ("expr" in action) {
|
|
3026
|
+
value = await page.evaluate(action.expr);
|
|
3027
|
+
} else {
|
|
3028
|
+
switch (action.source) {
|
|
3029
|
+
case "url":
|
|
3030
|
+
value = page.url();
|
|
3031
|
+
break;
|
|
3032
|
+
case "storage":
|
|
3033
|
+
value = page.url() === "about:blank" ? {} : await page.evaluate(
|
|
3034
|
+
() => Object.fromEntries(
|
|
3035
|
+
Object.keys(localStorage).map((k) => [k, localStorage.getItem(k)])
|
|
3036
|
+
)
|
|
3037
|
+
);
|
|
3038
|
+
break;
|
|
3039
|
+
case "network":
|
|
3040
|
+
value = this.peekNetworkLogs();
|
|
3041
|
+
break;
|
|
3042
|
+
case "expr-result":
|
|
3043
|
+
value = void 0;
|
|
3044
|
+
break;
|
|
3045
|
+
}
|
|
3046
|
+
}
|
|
3047
|
+
return { success: true, data: value };
|
|
1241
3048
|
}
|
|
1242
3049
|
case "clearText": {
|
|
1243
3050
|
const loc = resolver.resolve(action.ref);
|
|
@@ -1245,18 +3052,6 @@ var WebPlatformAdapter = class {
|
|
|
1245
3052
|
await loc.clear();
|
|
1246
3053
|
return { success: true, boundingBox: clearBox ?? void 0 };
|
|
1247
3054
|
}
|
|
1248
|
-
case "drag": {
|
|
1249
|
-
const fromLoc = resolver.resolve(action.fromRef);
|
|
1250
|
-
const toLoc = resolver.resolve(action.toRef);
|
|
1251
|
-
await fromLoc.dragTo(toLoc);
|
|
1252
|
-
return { success: true };
|
|
1253
|
-
}
|
|
1254
|
-
case "doubleTap": {
|
|
1255
|
-
const dblLoc = resolver.resolve(action.ref);
|
|
1256
|
-
const dblBox = await resolver.getBoundingBox(action.ref);
|
|
1257
|
-
await dblLoc.dblclick();
|
|
1258
|
-
return { success: true, boundingBox: dblBox ?? void 0 };
|
|
1259
|
-
}
|
|
1260
3055
|
case "doubleClick": {
|
|
1261
3056
|
const locator = resolver.resolve(action.ref);
|
|
1262
3057
|
const box = await resolver.getBoundingBox(action.ref);
|
|
@@ -1278,90 +3073,116 @@ var WebPlatformAdapter = class {
|
|
|
1278
3073
|
});
|
|
1279
3074
|
return { success: true, coordinates, boundingBox: box ?? void 0 };
|
|
1280
3075
|
}
|
|
1281
|
-
case "
|
|
1282
|
-
await
|
|
1283
|
-
|
|
3076
|
+
case "route": {
|
|
3077
|
+
await this.context.route(action.urlPattern, async (route) => {
|
|
3078
|
+
await route.fulfill({
|
|
3079
|
+
status: 200,
|
|
3080
|
+
...action.contentType !== void 0 ? { contentType: action.contentType } : {},
|
|
3081
|
+
...action.headers !== void 0 ? { headers: action.headers } : {},
|
|
3082
|
+
...action.body !== void 0 ? { body: action.body } : {}
|
|
3083
|
+
});
|
|
3084
|
+
});
|
|
3085
|
+
this.armedRoutes.add(action.urlPattern);
|
|
1284
3086
|
return { success: true };
|
|
1285
3087
|
}
|
|
1286
|
-
case "
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
3088
|
+
case "unroute": {
|
|
3089
|
+
if (action.urlPattern !== void 0) {
|
|
3090
|
+
await this.context.unroute(action.urlPattern);
|
|
3091
|
+
this.armedRoutes.delete(action.urlPattern);
|
|
3092
|
+
} else {
|
|
3093
|
+
await this.context.unrouteAll({ behavior: "ignoreErrors" });
|
|
3094
|
+
this.armedRoutes.clear();
|
|
3095
|
+
}
|
|
1292
3096
|
return { success: true };
|
|
1293
3097
|
}
|
|
1294
|
-
case "
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
if (action.index < 0 || action.index >= pages.length) {
|
|
1299
|
-
return { success: false, error: `Tab index ${action.index} out of range (${pages.length} tabs open)` };
|
|
1300
|
-
}
|
|
1301
|
-
target = pages[action.index];
|
|
1302
|
-
} else if (action.title) {
|
|
1303
|
-
for (const p of pages) {
|
|
1304
|
-
const t = await p.title();
|
|
1305
|
-
if (t.includes(action.title)) {
|
|
1306
|
-
target = p;
|
|
1307
|
-
break;
|
|
1308
|
-
}
|
|
1309
|
-
}
|
|
1310
|
-
} else if (action.url) {
|
|
1311
|
-
target = pages.find((p) => p.url().includes(action.url));
|
|
1312
|
-
} else {
|
|
1313
|
-
return { success: false, error: "switchTab requires at least one of: index, title, or url" };
|
|
3098
|
+
case "networkState": {
|
|
3099
|
+
if (action.mode === "offline") {
|
|
3100
|
+
await this.context.setOffline(true);
|
|
3101
|
+
return { success: true };
|
|
1314
3102
|
}
|
|
1315
|
-
|
|
1316
|
-
|
|
3103
|
+
try {
|
|
3104
|
+
const cdp = await this.context.newCDPSession(page);
|
|
3105
|
+
await cdp.send("Network.emulateNetworkConditions", {
|
|
3106
|
+
offline: false,
|
|
3107
|
+
latency: action.latencyMs,
|
|
3108
|
+
downloadThroughput: action.downloadKbps * 1e3 / 8,
|
|
3109
|
+
uploadThroughput: action.uploadKbps * 1e3 / 8
|
|
3110
|
+
});
|
|
3111
|
+
return { success: true };
|
|
3112
|
+
} catch (err) {
|
|
3113
|
+
return {
|
|
3114
|
+
success: false,
|
|
3115
|
+
error: `Network throttling is only available on Chromium (via CDP); this context does not support it. ${err instanceof Error ? err.message : String(err)}`
|
|
3116
|
+
};
|
|
1317
3117
|
}
|
|
1318
|
-
|
|
3118
|
+
}
|
|
3119
|
+
case "storageState": {
|
|
3120
|
+
if (action.op === "save") {
|
|
3121
|
+
const state = await this.context.storageState();
|
|
3122
|
+
return { success: true, data: { name: action.name, state } };
|
|
3123
|
+
}
|
|
3124
|
+
await this.context.setStorageState(readSeamStorageState(action));
|
|
1319
3125
|
return { success: true };
|
|
1320
3126
|
}
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
case "pinch":
|
|
1326
|
-
case "multiTap": {
|
|
1327
|
-
return { success: false, error: `${action.type} is a mobile-only gesture` };
|
|
1328
|
-
}
|
|
1329
|
-
case "tapCoordinate": {
|
|
1330
|
-
const vp = page.viewportSize();
|
|
1331
|
-
if (vp) {
|
|
1332
|
-
(0, import_agent_qa_core.warnIfOutOfBounds)({ x: action.x, y: action.y }, { width: vp.width, height: vp.height }, "tapCoordinate");
|
|
3127
|
+
case "handleDialog": {
|
|
3128
|
+
if (this.pendingDialogArm) {
|
|
3129
|
+
page.off("dialog", this.pendingDialogArm);
|
|
3130
|
+
this.pendingDialogArm = null;
|
|
1333
3131
|
}
|
|
1334
|
-
|
|
1335
|
-
|
|
3132
|
+
const accept = action.accept;
|
|
3133
|
+
const promptText = action.promptText;
|
|
3134
|
+
const handler = (dialog) => {
|
|
3135
|
+
this.pendingDialogArm = null;
|
|
3136
|
+
if (accept) {
|
|
3137
|
+
void dialog.accept(promptText);
|
|
3138
|
+
} else {
|
|
3139
|
+
void dialog.dismiss();
|
|
3140
|
+
}
|
|
3141
|
+
};
|
|
3142
|
+
this.pendingDialogArm = handler;
|
|
3143
|
+
page.once("dialog", handler);
|
|
3144
|
+
return { success: true };
|
|
1336
3145
|
}
|
|
1337
3146
|
case "executeScript": {
|
|
1338
3147
|
return { success: false, error: "executeScript is a mobile-only action" };
|
|
1339
3148
|
}
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
case
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
3149
|
+
// Runner-minted / author-channel verbs (D-01/D-15/D-16): reportValue, uniqueValue, assertAI,
|
|
3150
|
+
// extractAI, and setVariable are intercepted at the dispatch seam and never reach the adapter.
|
|
3151
|
+
// A grouped §7.1 defensive case surfaces a dispatch-routing error if one arrives here;
|
|
3152
|
+
// assertNever remains the backstop for a genuinely unknown schema verb.
|
|
3153
|
+
case "reportValue":
|
|
3154
|
+
case "uniqueValue":
|
|
3155
|
+
case "assertAI":
|
|
3156
|
+
case "extractAI":
|
|
3157
|
+
case "setVariable":
|
|
3158
|
+
return {
|
|
3159
|
+
success: false,
|
|
3160
|
+
error: `Action "${action.type}" is handled by the runner dispatch seam and must not reach the web adapter (runner-minted/author-channel verb). This indicates a dispatch routing error.`
|
|
3161
|
+
};
|
|
3162
|
+
default:
|
|
3163
|
+
return (0, import_agent_qa_runner6.assertNever)(action);
|
|
1349
3164
|
}
|
|
1350
3165
|
}
|
|
1351
3166
|
};
|
|
1352
3167
|
|
|
1353
3168
|
// src/farm-adapter.ts
|
|
1354
|
-
var
|
|
1355
|
-
var
|
|
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");
|
|
1356
3172
|
function clickDelayOption2(clickDelay) {
|
|
1357
3173
|
return clickDelay !== void 0 && clickDelay > 0 ? { delay: clickDelay } : {};
|
|
1358
3174
|
}
|
|
3175
|
+
function readSeamStorageState2(action) {
|
|
3176
|
+
return action.state;
|
|
3177
|
+
}
|
|
1359
3178
|
var FarmWebAdapter = class {
|
|
1360
3179
|
platform = "web";
|
|
1361
3180
|
browser = null;
|
|
1362
3181
|
context = null;
|
|
1363
3182
|
page = null;
|
|
3183
|
+
// The native binding of the last observation and its document (438 D-06).
|
|
1364
3184
|
lastRefs = {};
|
|
3185
|
+
epoch;
|
|
1365
3186
|
sessionUrl;
|
|
1366
3187
|
sessionId;
|
|
1367
3188
|
testFailed = false;
|
|
@@ -1369,12 +3190,19 @@ var FarmWebAdapter = class {
|
|
|
1369
3190
|
credentials = null;
|
|
1370
3191
|
pageLogBuffers = /* @__PURE__ */ new Map();
|
|
1371
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;
|
|
1372
3199
|
async setup(config) {
|
|
3200
|
+
this.recordingConfig = config.recording;
|
|
1373
3201
|
if (!config.farmSession) {
|
|
1374
3202
|
throw new Error("FarmWebAdapter requires config.farmSession to be set");
|
|
1375
3203
|
}
|
|
1376
|
-
(0,
|
|
1377
|
-
const provider = (0,
|
|
3204
|
+
(0, import_agent_qa_core.registerAllProviders)();
|
|
3205
|
+
const provider = (0, import_agent_qa_core.getProvider)("browserstack");
|
|
1378
3206
|
if (!provider) {
|
|
1379
3207
|
throw new Error("Unknown farm provider: browserstack");
|
|
1380
3208
|
}
|
|
@@ -1394,12 +3222,6 @@ var FarmWebAdapter = class {
|
|
|
1394
3222
|
const contextOptions = {
|
|
1395
3223
|
viewport: config.browser?.viewport ?? { width: 1280, height: 720 }
|
|
1396
3224
|
};
|
|
1397
|
-
if (config.recording?.enabled) {
|
|
1398
|
-
contextOptions.recordVideo = {
|
|
1399
|
-
dir: config.recording.videoDir,
|
|
1400
|
-
size: config.recording.videoSize ?? config.browser?.viewport ?? { width: 1280, height: 720 }
|
|
1401
|
-
};
|
|
1402
|
-
}
|
|
1403
3225
|
if (config.authState?.storageStatePath) {
|
|
1404
3226
|
contextOptions.storageState = config.authState.storageStatePath;
|
|
1405
3227
|
}
|
|
@@ -1442,7 +3264,40 @@ var FarmWebAdapter = class {
|
|
|
1442
3264
|
this.logCaptureConfig = { console: config.logCapture?.console !== false, network: config.logCapture?.network !== false };
|
|
1443
3265
|
this.attachLogListeners(this.page);
|
|
1444
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
|
+
}
|
|
1445
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;
|
|
1446
3301
|
if (this.browser) {
|
|
1447
3302
|
await this.browser.close();
|
|
1448
3303
|
}
|
|
@@ -1473,11 +3328,44 @@ var FarmWebAdapter = class {
|
|
|
1473
3328
|
if (!this.page) {
|
|
1474
3329
|
throw new Error("FarmWebAdapter not initialized \u2014 call setup() first");
|
|
1475
3330
|
}
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
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 } : {}
|
|
1479
3367
|
});
|
|
1480
|
-
this.lastRefs = state.
|
|
3368
|
+
this.lastRefs = buildRefMap(binding, nodes, nativeRefs, state.elements);
|
|
1481
3369
|
return state;
|
|
1482
3370
|
}
|
|
1483
3371
|
async execute(action) {
|
|
@@ -1522,15 +3410,6 @@ var FarmWebAdapter = class {
|
|
|
1522
3410
|
getPage() {
|
|
1523
3411
|
return this.page;
|
|
1524
3412
|
}
|
|
1525
|
-
async getVideoPath() {
|
|
1526
|
-
try {
|
|
1527
|
-
const video = this.page?.video();
|
|
1528
|
-
if (!video) return null;
|
|
1529
|
-
return await video.path();
|
|
1530
|
-
} catch {
|
|
1531
|
-
return null;
|
|
1532
|
-
}
|
|
1533
|
-
}
|
|
1534
3413
|
markFailed() {
|
|
1535
3414
|
this.testFailed = true;
|
|
1536
3415
|
}
|
|
@@ -1560,6 +3439,14 @@ var FarmWebAdapter = class {
|
|
|
1560
3439
|
if (!urlPattern) return all;
|
|
1561
3440
|
return all.filter((e) => e.url.includes(urlPattern));
|
|
1562
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
|
+
}
|
|
1563
3450
|
attachLogListeners(page) {
|
|
1564
3451
|
const buffers = {
|
|
1565
3452
|
consoleLogs: [],
|
|
@@ -1647,6 +3534,12 @@ var FarmWebAdapter = class {
|
|
|
1647
3534
|
async executeAction(action) {
|
|
1648
3535
|
const page = this.page;
|
|
1649
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
|
+
}
|
|
1650
3543
|
if ("ref" in action && typeof action.ref === "string") {
|
|
1651
3544
|
const refData = this.lastRefs[action.ref];
|
|
1652
3545
|
const role = refData?.role;
|
|
@@ -1668,8 +3561,10 @@ var FarmWebAdapter = class {
|
|
|
1668
3561
|
case "fill": {
|
|
1669
3562
|
const locator = resolver.resolve(action.ref);
|
|
1670
3563
|
const box = await resolver.getBoundingBox(action.ref);
|
|
1671
|
-
|
|
1672
|
-
|
|
3564
|
+
return runWebFillCompound(locator, box ?? void 0, action);
|
|
3565
|
+
}
|
|
3566
|
+
case "openApp": {
|
|
3567
|
+
return webOpenAppError();
|
|
1673
3568
|
}
|
|
1674
3569
|
case "select": {
|
|
1675
3570
|
const locator = resolver.resolve(action.ref);
|
|
@@ -1678,53 +3573,44 @@ var FarmWebAdapter = class {
|
|
|
1678
3573
|
return { success: true, boundingBox: box ?? void 0 };
|
|
1679
3574
|
}
|
|
1680
3575
|
case "navigate": {
|
|
1681
|
-
|
|
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
|
+
}
|
|
1682
3590
|
await waitForPageReady(page);
|
|
1683
3591
|
return { success: true };
|
|
1684
3592
|
}
|
|
1685
3593
|
case "scroll": {
|
|
1686
|
-
|
|
1687
|
-
const deltaY = action.scrollType === "vertical" ? action.value : 0;
|
|
1688
|
-
let coordinates;
|
|
1689
|
-
let boundingBox;
|
|
1690
|
-
let scrollData;
|
|
1691
|
-
if (action.ref) {
|
|
1692
|
-
const refData = this.lastRefs[action.ref];
|
|
1693
|
-
if (refData) {
|
|
1694
|
-
const scrollAxis = action.scrollType === "horizontal" ? "horizontal" : "vertical";
|
|
1695
|
-
const result = await scrollWithRef(page, refData, deltaX, deltaY, scrollAxis);
|
|
1696
|
-
if (result.bounds) {
|
|
1697
|
-
coordinates = { x: result.bounds.x + result.bounds.width / 2, y: result.bounds.y + result.bounds.height / 2 };
|
|
1698
|
-
boundingBox = result.bounds;
|
|
1699
|
-
}
|
|
1700
|
-
scrollData = { scrolled: result.scrolled, scrolledContainer: result.scrolledContainer };
|
|
1701
|
-
} else {
|
|
1702
|
-
await page.mouse.wheel(deltaX, deltaY);
|
|
1703
|
-
}
|
|
1704
|
-
} else {
|
|
1705
|
-
await page.mouse.wheel(deltaX, deltaY);
|
|
1706
|
-
}
|
|
1707
|
-
await page.evaluate(`new Promise(resolve => {
|
|
1708
|
-
const t = setTimeout(() => resolve('timeout'), 500);
|
|
1709
|
-
requestAnimationFrame(() => requestAnimationFrame(() => {
|
|
1710
|
-
clearTimeout(t);
|
|
1711
|
-
resolve('raf');
|
|
1712
|
-
}));
|
|
1713
|
-
})`);
|
|
1714
|
-
return { success: true, coordinates, boundingBox, data: scrollData };
|
|
3594
|
+
return runWebScrollCompound(page, this.lastRefs, action);
|
|
1715
3595
|
}
|
|
1716
3596
|
case "waitFor": {
|
|
1717
|
-
const timeout = action.
|
|
3597
|
+
const timeout = action.timeoutMs;
|
|
1718
3598
|
try {
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
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;
|
|
1727
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.` };
|
|
1728
3614
|
}
|
|
1729
3615
|
return { success: true };
|
|
1730
3616
|
}
|
|
@@ -1744,12 +3630,18 @@ var FarmWebAdapter = class {
|
|
|
1744
3630
|
}
|
|
1745
3631
|
}
|
|
1746
3632
|
case "keypress": {
|
|
1747
|
-
const effectiveKeys = (0,
|
|
3633
|
+
const effectiveKeys = (0, import_agent_qa_runner9.convertKeysForPlatform)(action.keys, {
|
|
1748
3634
|
enabled: action.convertPlatformKeys !== false,
|
|
1749
|
-
isMac: (0,
|
|
3635
|
+
isMac: (0, import_agent_qa_runner9.isMacPlatform)()
|
|
1750
3636
|
});
|
|
1751
3637
|
for (const key of effectiveKeys) {
|
|
1752
|
-
|
|
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
|
+
}
|
|
1753
3645
|
}
|
|
1754
3646
|
return { success: true };
|
|
1755
3647
|
}
|
|
@@ -1795,75 +3687,110 @@ var FarmWebAdapter = class {
|
|
|
1795
3687
|
await page.evaluate(`navigator.clipboard.writeText(${JSON.stringify(text)})`);
|
|
1796
3688
|
return { success: true, boundingBox: box ?? void 0, data: { copied: text } };
|
|
1797
3689
|
}
|
|
1798
|
-
case "
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
case "refresh": {
|
|
1807
|
-
await page.reload({ waitUntil: "domcontentloaded" });
|
|
1808
|
-
await waitForPageReady(page);
|
|
1809
|
-
return { success: true };
|
|
1810
|
-
}
|
|
1811
|
-
case "navigateHistory": {
|
|
1812
|
-
if (action.direction === "back") {
|
|
1813
|
-
await page.goBack({ waitUntil: "domcontentloaded" });
|
|
1814
|
-
} else {
|
|
1815
|
-
await page.goForward({ waitUntil: "domcontentloaded" });
|
|
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 };
|
|
1816
3698
|
}
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
if (action.tab) {
|
|
1823
|
-
const targetPage = await this.resolveTabTargetAsync(action.tab);
|
|
1824
|
-
if (!targetPage) {
|
|
1825
|
-
return { success: false, error: `No tab found matching ${JSON.stringify(action.tab)}` };
|
|
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)` };
|
|
1826
3704
|
}
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
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
|
+
}
|
|
1833
3713
|
}
|
|
3714
|
+
} else if (action.url !== void 0) {
|
|
3715
|
+
target = pages.find((p) => p.url().includes(action.url));
|
|
1834
3716
|
}
|
|
1835
|
-
if (
|
|
1836
|
-
|
|
3717
|
+
if (!target) {
|
|
3718
|
+
return { success: false, error: `No tab found matching ${JSON.stringify({ index: action.index, title: action.title, url: action.url })}` };
|
|
1837
3719
|
}
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
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 };
|
|
1846
3744
|
}
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
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 };
|
|
1853
3791
|
}
|
|
1854
3792
|
}
|
|
1855
|
-
|
|
1856
|
-
entries = entries.filter((e) => e.url.includes(action.urlPattern));
|
|
1857
|
-
}
|
|
1858
|
-
return { success: true, data: entries };
|
|
1859
|
-
}
|
|
1860
|
-
case "readCookies": {
|
|
1861
|
-
const cookies = await this.context.cookies();
|
|
1862
|
-
if (action.name) {
|
|
1863
|
-
const cookie = cookies.find((c) => c.name === action.name) ?? null;
|
|
1864
|
-
return { success: true, data: cookie };
|
|
1865
|
-
}
|
|
1866
|
-
return { success: true, data: cookies };
|
|
3793
|
+
return { success: true };
|
|
1867
3794
|
}
|
|
1868
3795
|
case "setCookies": {
|
|
1869
3796
|
const cookiesWithDefaults = action.cookies.map((c) => ({
|
|
@@ -1873,25 +3800,6 @@ var FarmWebAdapter = class {
|
|
|
1873
3800
|
await this.context.addCookies(cookiesWithDefaults);
|
|
1874
3801
|
return { success: true };
|
|
1875
3802
|
}
|
|
1876
|
-
case "readLocalStorage": {
|
|
1877
|
-
const pageUrl = page.url();
|
|
1878
|
-
if (pageUrl === "about:blank") {
|
|
1879
|
-
return { success: true, data: action.key ? null : {} };
|
|
1880
|
-
}
|
|
1881
|
-
if (action.key) {
|
|
1882
|
-
const value = await page.evaluate(
|
|
1883
|
-
(k) => localStorage.getItem(k),
|
|
1884
|
-
action.key
|
|
1885
|
-
);
|
|
1886
|
-
return { success: true, data: value };
|
|
1887
|
-
}
|
|
1888
|
-
const all = await page.evaluate(
|
|
1889
|
-
() => Object.fromEntries(
|
|
1890
|
-
Object.keys(localStorage).map((k) => [k, localStorage.getItem(k)])
|
|
1891
|
-
)
|
|
1892
|
-
);
|
|
1893
|
-
return { success: true, data: all };
|
|
1894
|
-
}
|
|
1895
3803
|
case "setLocalStorage": {
|
|
1896
3804
|
await page.evaluate((entries) => {
|
|
1897
3805
|
for (const { key, value } of entries) {
|
|
@@ -1900,8 +3808,37 @@ var FarmWebAdapter = class {
|
|
|
1900
3808
|
}, action.entries);
|
|
1901
3809
|
return { success: true };
|
|
1902
3810
|
}
|
|
1903
|
-
case "
|
|
1904
|
-
|
|
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 };
|
|
1905
3842
|
}
|
|
1906
3843
|
case "clearText": {
|
|
1907
3844
|
const loc = resolver.resolve(action.ref);
|
|
@@ -1909,18 +3846,6 @@ var FarmWebAdapter = class {
|
|
|
1909
3846
|
await loc.clear();
|
|
1910
3847
|
return { success: true, boundingBox: clearBox ?? void 0 };
|
|
1911
3848
|
}
|
|
1912
|
-
case "drag": {
|
|
1913
|
-
const fromLoc = resolver.resolve(action.fromRef);
|
|
1914
|
-
const toLoc = resolver.resolve(action.toRef);
|
|
1915
|
-
await fromLoc.dragTo(toLoc);
|
|
1916
|
-
return { success: true };
|
|
1917
|
-
}
|
|
1918
|
-
case "doubleTap": {
|
|
1919
|
-
const dblLoc = resolver.resolve(action.ref);
|
|
1920
|
-
const dblBox = await resolver.getBoundingBox(action.ref);
|
|
1921
|
-
await dblLoc.dblclick();
|
|
1922
|
-
return { success: true, boundingBox: dblBox ?? void 0 };
|
|
1923
|
-
}
|
|
1924
3849
|
case "doubleClick": {
|
|
1925
3850
|
const locator = resolver.resolve(action.ref);
|
|
1926
3851
|
const box = await resolver.getBoundingBox(action.ref);
|
|
@@ -1942,66 +3867,94 @@ var FarmWebAdapter = class {
|
|
|
1942
3867
|
});
|
|
1943
3868
|
return { success: true, coordinates, boundingBox: box ?? void 0 };
|
|
1944
3869
|
}
|
|
1945
|
-
case "
|
|
1946
|
-
await
|
|
1947
|
-
|
|
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);
|
|
1948
3880
|
return { success: true };
|
|
1949
3881
|
}
|
|
1950
|
-
case "
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
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
|
+
}
|
|
1956
3890
|
return { success: true };
|
|
1957
3891
|
}
|
|
1958
|
-
case "
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
if (action.index < 0 || action.index >= pages.length) {
|
|
1963
|
-
return { success: false, error: `Tab index ${action.index} out of range (${pages.length} tabs open)` };
|
|
1964
|
-
}
|
|
1965
|
-
target = pages[action.index];
|
|
1966
|
-
} else if (action.title) {
|
|
1967
|
-
for (const p of pages) {
|
|
1968
|
-
const t = await p.title();
|
|
1969
|
-
if (t.includes(action.title)) {
|
|
1970
|
-
target = p;
|
|
1971
|
-
break;
|
|
1972
|
-
}
|
|
1973
|
-
}
|
|
1974
|
-
} else if (action.url) {
|
|
1975
|
-
target = pages.find((p) => p.url().includes(action.url));
|
|
1976
|
-
} else {
|
|
1977
|
-
return { success: false, error: "switchTab requires at least one of: index, title, or url" };
|
|
3892
|
+
case "networkState": {
|
|
3893
|
+
if (action.mode === "offline") {
|
|
3894
|
+
await this.context.setOffline(true);
|
|
3895
|
+
return { success: true };
|
|
1978
3896
|
}
|
|
1979
|
-
|
|
1980
|
-
|
|
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
|
+
};
|
|
1981
3911
|
}
|
|
1982
|
-
this.page = target;
|
|
1983
|
-
return { success: true };
|
|
1984
3912
|
}
|
|
1985
|
-
case "
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
}
|
|
1992
|
-
case "tapCoordinate": {
|
|
1993
|
-
await page.mouse.click(action.x, action.y);
|
|
1994
|
-
return { success: true, coordinates: { x: action.x, y: action.y } };
|
|
1995
|
-
}
|
|
1996
|
-
case "hideKeyboard":
|
|
1997
|
-
case "launchApp":
|
|
1998
|
-
case "stopApp":
|
|
1999
|
-
case "setOrientation": {
|
|
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));
|
|
2000
3919
|
return { success: true };
|
|
2001
3920
|
}
|
|
2002
|
-
|
|
2003
|
-
|
|
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 };
|
|
2004
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.
|
|
3947
|
+
case "reportValue":
|
|
3948
|
+
case "uniqueValue":
|
|
3949
|
+
case "assertAI":
|
|
3950
|
+
case "extractAI":
|
|
3951
|
+
case "setVariable":
|
|
3952
|
+
return {
|
|
3953
|
+
success: false,
|
|
3954
|
+
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
|
+
};
|
|
3956
|
+
default:
|
|
3957
|
+
return (0, import_agent_qa_runner8.assertNever)(action);
|
|
2005
3958
|
}
|
|
2006
3959
|
}
|
|
2007
3960
|
};
|