@vostride/agent-qa-web 0.0.1 → 0.1.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/LICENSE.md +105 -0
- package/NOTICE.md +11 -0
- package/dist/index.cjs +2049 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +136 -0
- package/dist/index.d.ts +136 -0
- package/dist/index.js +2003 -0
- package/dist/index.js.map +1 -0
- package/package.json +59 -3
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,2049 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
ActionValidationError: () => ActionValidationError,
|
|
34
|
+
ElementResolver: () => ElementResolver,
|
|
35
|
+
FarmWebAdapter: () => FarmWebAdapter,
|
|
36
|
+
WebPlatformAdapter: () => WebPlatformAdapter,
|
|
37
|
+
extractDom: () => extractDom,
|
|
38
|
+
observePage: () => observePage,
|
|
39
|
+
parseAriaSnapshot: () => parseAriaSnapshot,
|
|
40
|
+
runAccessibilityCheck: () => runAccessibilityCheck,
|
|
41
|
+
validateAction: () => validateAction,
|
|
42
|
+
waitForPageReady: () => waitForPageReady
|
|
43
|
+
});
|
|
44
|
+
module.exports = __toCommonJS(index_exports);
|
|
45
|
+
|
|
46
|
+
// src/adapter.ts
|
|
47
|
+
var import_agent_qa_core = require("@vostride/agent-qa-core");
|
|
48
|
+
|
|
49
|
+
// src/dom-extractor.ts
|
|
50
|
+
var SKIP_TAGS = /* @__PURE__ */ new Set([
|
|
51
|
+
"SCRIPT",
|
|
52
|
+
"STYLE",
|
|
53
|
+
"SVG",
|
|
54
|
+
"NOSCRIPT",
|
|
55
|
+
"LINK",
|
|
56
|
+
"META",
|
|
57
|
+
"TEMPLATE",
|
|
58
|
+
"IFRAME"
|
|
59
|
+
]);
|
|
60
|
+
function extractDomTree(opts) {
|
|
61
|
+
const { maxDepth, maxTextLen } = opts;
|
|
62
|
+
const doc = globalThis.document;
|
|
63
|
+
const win = globalThis.window;
|
|
64
|
+
const lines = [];
|
|
65
|
+
function isHidden(el) {
|
|
66
|
+
if (!el.offsetParent && el.tagName !== "BODY" && el.tagName !== "HTML") {
|
|
67
|
+
const style = win.getComputedStyle(el);
|
|
68
|
+
if (style.display === "none" || style.visibility === "hidden") return true;
|
|
69
|
+
}
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
function walk(node, depth, indent) {
|
|
73
|
+
if (depth > maxDepth) {
|
|
74
|
+
lines.push(indent + "[... deeper content truncated]");
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
for (let child = node.firstChild; child; child = child.nextSibling) {
|
|
78
|
+
if (child.nodeType === 3) {
|
|
79
|
+
const text = child.textContent.trim();
|
|
80
|
+
if (text) {
|
|
81
|
+
const truncated = text.length > maxTextLen ? text.slice(0, maxTextLen) + "..." : text;
|
|
82
|
+
lines.push(indent + truncated);
|
|
83
|
+
}
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (child.nodeType !== 1) continue;
|
|
87
|
+
const tag = child.tagName;
|
|
88
|
+
if (SKIP_TAGS.has(tag)) continue;
|
|
89
|
+
if (isHidden(child)) continue;
|
|
90
|
+
const isCustom = tag.toLowerCase().includes("-");
|
|
91
|
+
const annotation = child.getAttribute("aria-label") || child.getAttribute("role") || "";
|
|
92
|
+
let label = tag.toLowerCase();
|
|
93
|
+
if (isCustom) {
|
|
94
|
+
if (!child.shadowRoot && !child.firstChild) {
|
|
95
|
+
label += " (custom element, closed shadow root)";
|
|
96
|
+
} else {
|
|
97
|
+
label += " (custom element)";
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (annotation) label += ` [${annotation}]`;
|
|
101
|
+
lines.push(indent + label);
|
|
102
|
+
if (child.shadowRoot) {
|
|
103
|
+
lines.push(indent + " #shadow-root");
|
|
104
|
+
walk(child.shadowRoot, depth + 1, indent + " ");
|
|
105
|
+
}
|
|
106
|
+
walk(child, depth + 1, indent + " ");
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
walk(doc.body, 0, "");
|
|
110
|
+
return lines.join("\n");
|
|
111
|
+
}
|
|
112
|
+
async function extractDom(page, opts) {
|
|
113
|
+
const maxDepth = opts?.maxDepth ?? 20;
|
|
114
|
+
const maxTextLen = opts?.maxTextLen ?? 200;
|
|
115
|
+
try {
|
|
116
|
+
return await page.evaluate(extractDomTree, { maxDepth, maxTextLen });
|
|
117
|
+
} catch {
|
|
118
|
+
return void 0;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// src/observer.ts
|
|
123
|
+
var INTERACTIVE_ROLES = /* @__PURE__ */ new Set([
|
|
124
|
+
"button",
|
|
125
|
+
"link",
|
|
126
|
+
"textbox",
|
|
127
|
+
"checkbox",
|
|
128
|
+
"radio",
|
|
129
|
+
"combobox",
|
|
130
|
+
"listbox",
|
|
131
|
+
"menuitem",
|
|
132
|
+
"menuitemradio",
|
|
133
|
+
"menuitemcheckbox",
|
|
134
|
+
"option",
|
|
135
|
+
"searchbox",
|
|
136
|
+
"slider",
|
|
137
|
+
"spinbutton",
|
|
138
|
+
"switch",
|
|
139
|
+
"tab",
|
|
140
|
+
"treeitem",
|
|
141
|
+
// WAI-ARIA 1.2 standalone widget roles
|
|
142
|
+
"gridcell",
|
|
143
|
+
"scrollbar",
|
|
144
|
+
"separator",
|
|
145
|
+
// WAI-ARIA 1.2 composite widget roles (interactive containers)
|
|
146
|
+
"grid",
|
|
147
|
+
"menu",
|
|
148
|
+
"menubar",
|
|
149
|
+
"tree",
|
|
150
|
+
"treegrid"
|
|
151
|
+
]);
|
|
152
|
+
var CONTENT_ROLES = /* @__PURE__ */ new Set([
|
|
153
|
+
"heading",
|
|
154
|
+
"cell",
|
|
155
|
+
"listitem",
|
|
156
|
+
"article",
|
|
157
|
+
"region",
|
|
158
|
+
"navigation",
|
|
159
|
+
"progressbar",
|
|
160
|
+
"tabpanel"
|
|
161
|
+
]);
|
|
162
|
+
var LINE_PATTERN = /^(\s*-\s*)(\w+)(?:\s+"([^"]*)")?(.*)$/;
|
|
163
|
+
var QUOTED_LINE_PATTERN = /^(\s*-\s*)'(\w+)(?:\s+"([^"]*)")?(.*)'\s*$/;
|
|
164
|
+
function parseAriaSnapshot(ariaTree) {
|
|
165
|
+
const lines = ariaTree.split("\n");
|
|
166
|
+
const elements = [];
|
|
167
|
+
const refs = {};
|
|
168
|
+
const resultLines = [];
|
|
169
|
+
let refCounter = 0;
|
|
170
|
+
const roleNameCounts = /* @__PURE__ */ new Map();
|
|
171
|
+
const refToKey = /* @__PURE__ */ new Map();
|
|
172
|
+
for (const line of lines) {
|
|
173
|
+
const match = line.match(LINE_PATTERN) || line.match(QUOTED_LINE_PATTERN);
|
|
174
|
+
if (!match) {
|
|
175
|
+
resultLines.push(line);
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
const [, prefix, role, name, suffix] = match;
|
|
179
|
+
const roleLower = role.toLowerCase();
|
|
180
|
+
const shouldGetRef = INTERACTIVE_ROLES.has(roleLower) || CONTENT_ROLES.has(roleLower);
|
|
181
|
+
if (!shouldGetRef) {
|
|
182
|
+
resultLines.push(line);
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
refCounter++;
|
|
186
|
+
const ref = `e${refCounter}`;
|
|
187
|
+
const key = `${roleLower}:${name ?? ""}`;
|
|
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 };
|
|
192
|
+
const attributes = {};
|
|
193
|
+
const attrMatches = suffix?.matchAll(/\[(\w+)(?:=([^\]]*))?\]/g);
|
|
194
|
+
if (attrMatches) {
|
|
195
|
+
for (const am of attrMatches) {
|
|
196
|
+
attributes[am[1]] = am[2] ?? "true";
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
elements.push({ ref, role: roleLower, name: name ?? "", attributes });
|
|
200
|
+
let enhanced = `${prefix}${role}`;
|
|
201
|
+
if (name) enhanced += ` "${name}"`;
|
|
202
|
+
enhanced += ` [ref=${ref}]`;
|
|
203
|
+
if (suffix?.trim()) enhanced += suffix;
|
|
204
|
+
resultLines.push(enhanced);
|
|
205
|
+
}
|
|
206
|
+
for (const [ref, data] of Object.entries(refs)) {
|
|
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 };
|
|
213
|
+
}
|
|
214
|
+
function hideAgentQaElements() {
|
|
215
|
+
const doc = globalThis.document;
|
|
216
|
+
doc.querySelectorAll("[data-agent-qa-internal]").forEach((el) => {
|
|
217
|
+
el.style.display = "none";
|
|
218
|
+
el.setAttribute("aria-hidden", "true");
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
function restoreAgentQaElements() {
|
|
222
|
+
const doc = globalThis.document;
|
|
223
|
+
doc.querySelectorAll("[data-agent-qa-internal]").forEach((el) => {
|
|
224
|
+
el.style.display = "";
|
|
225
|
+
el.removeAttribute("aria-hidden");
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
async function observePage(page, options) {
|
|
229
|
+
const viewport = page.viewportSize();
|
|
230
|
+
const locator = options?.filterSelector ? page.locator(options.filterSelector) : page.locator(":root");
|
|
231
|
+
await page.evaluate(hideAgentQaElements);
|
|
232
|
+
let ariaTree;
|
|
233
|
+
try {
|
|
234
|
+
ariaTree = await locator.ariaSnapshot();
|
|
235
|
+
} finally {
|
|
236
|
+
await page.evaluate(restoreAgentQaElements);
|
|
237
|
+
}
|
|
238
|
+
if (!ariaTree) {
|
|
239
|
+
return {
|
|
240
|
+
tree: "(empty)",
|
|
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
|
+
}
|
|
373
|
+
let domContext;
|
|
374
|
+
if (options?.extractDom) {
|
|
375
|
+
try {
|
|
376
|
+
domContext = await extractDom(page);
|
|
377
|
+
} catch {
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return {
|
|
381
|
+
tree,
|
|
382
|
+
elements,
|
|
383
|
+
url: page.url(),
|
|
384
|
+
timestamp: Date.now(),
|
|
385
|
+
metadata: {
|
|
386
|
+
coordSpace: "viewport",
|
|
387
|
+
viewportWidth: viewport?.width ?? 0,
|
|
388
|
+
viewportHeight: viewport?.height ?? 0,
|
|
389
|
+
refMap: refs,
|
|
390
|
+
...domContext ? { domContext } : {}
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// src/element-resolver.ts
|
|
396
|
+
var ACTIONS_WITHOUT_REF = /* @__PURE__ */ new Set(["navigate", "waitFor", "delay", "waitForUrl", "assert"]);
|
|
397
|
+
var ElementResolver = class {
|
|
398
|
+
constructor(page, refs) {
|
|
399
|
+
this.page = page;
|
|
400
|
+
this.refs = refs;
|
|
401
|
+
}
|
|
402
|
+
resolve(ref) {
|
|
403
|
+
const data = this.refs[ref];
|
|
404
|
+
if (!data) {
|
|
405
|
+
throw new Error(`Unknown ref "${ref}". Available refs: ${Object.keys(this.refs).join(", ") || "none"}`);
|
|
406
|
+
}
|
|
407
|
+
let locator;
|
|
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;
|
|
417
|
+
}
|
|
418
|
+
resolveAction(action) {
|
|
419
|
+
if (ACTIONS_WITHOUT_REF.has(action.type)) {
|
|
420
|
+
return null;
|
|
421
|
+
}
|
|
422
|
+
if ("ref" in action && typeof action.ref === "string") {
|
|
423
|
+
return this.resolve(action.ref);
|
|
424
|
+
}
|
|
425
|
+
return null;
|
|
426
|
+
}
|
|
427
|
+
async getBoundingBox(ref) {
|
|
428
|
+
try {
|
|
429
|
+
const locator = this.resolve(ref);
|
|
430
|
+
return await locator.boundingBox();
|
|
431
|
+
} catch {
|
|
432
|
+
return null;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
// src/action-validator.ts
|
|
438
|
+
var ActionValidationError = class extends Error {
|
|
439
|
+
suggestion;
|
|
440
|
+
constructor(message, suggestion) {
|
|
441
|
+
super(message);
|
|
442
|
+
this.name = "ActionValidationError";
|
|
443
|
+
this.suggestion = suggestion;
|
|
444
|
+
}
|
|
445
|
+
};
|
|
446
|
+
var FILL_ROLES = /* @__PURE__ */ new Set(["textbox", "searchbox", "spinbutton", "combobox"]);
|
|
447
|
+
var SELECT_ROLES = /* @__PURE__ */ new Set(["listbox", "combobox"]);
|
|
448
|
+
var NO_REF_ACTIONS = /* @__PURE__ */ new Set(["scroll", "navigate", "waitFor", "delay", "waitForUrl", "assert", "pinch", "multiTap", "keyDown", "keyUp", "refresh", "navigateHistory", "readConsoleLogs", "readNetworkLogs", "readCookies", "setCookies", "readLocalStorage", "setLocalStorage", "executeScript"]);
|
|
449
|
+
var ANY_ROLE_ACTIONS = /* @__PURE__ */ new Set(["click", "hover", "keypress", "paste", "doubleClick", "rightClick"]);
|
|
450
|
+
function validateAction(action, elementRole) {
|
|
451
|
+
if (NO_REF_ACTIONS.has(action.type)) {
|
|
452
|
+
return { valid: true };
|
|
453
|
+
}
|
|
454
|
+
if (ANY_ROLE_ACTIONS.has(action.type)) {
|
|
455
|
+
return { valid: true };
|
|
456
|
+
}
|
|
457
|
+
if (!elementRole) {
|
|
458
|
+
return { valid: true };
|
|
459
|
+
}
|
|
460
|
+
const role = elementRole.toLowerCase();
|
|
461
|
+
if (action.type === "fill") {
|
|
462
|
+
if (FILL_ROLES.has(role)) {
|
|
463
|
+
return { valid: true };
|
|
464
|
+
}
|
|
465
|
+
if (SELECT_ROLES.has(role)) {
|
|
466
|
+
return {
|
|
467
|
+
valid: false,
|
|
468
|
+
error: `Cannot fill() on a '${role}' element \u2014 fill is for text inputs. Try 'select' to choose an option.`,
|
|
469
|
+
suggestion: "select"
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
return {
|
|
473
|
+
valid: false,
|
|
474
|
+
error: `Element ${action.ref} is not fillable (role: ${role}). Use a textbox or input element instead.`,
|
|
475
|
+
suggestion: "click"
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
if (action.type === "clearText") {
|
|
479
|
+
if (FILL_ROLES.has(role)) {
|
|
480
|
+
return { valid: true };
|
|
481
|
+
}
|
|
482
|
+
return {
|
|
483
|
+
valid: false,
|
|
484
|
+
error: `Element ${action.ref} is not fillable (role: ${role}). clearText only works on text inputs. Use a textbox or input element instead.`,
|
|
485
|
+
suggestion: "click"
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
if (action.type === "select") {
|
|
489
|
+
if (SELECT_ROLES.has(role)) {
|
|
490
|
+
return { valid: true };
|
|
491
|
+
}
|
|
492
|
+
if (FILL_ROLES.has(role)) {
|
|
493
|
+
return {
|
|
494
|
+
valid: false,
|
|
495
|
+
error: `Cannot select() on a '${role}' element \u2014 select is for lists. Try 'fill' to type a value instead.`,
|
|
496
|
+
suggestion: "fill"
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
return {
|
|
500
|
+
valid: false,
|
|
501
|
+
error: `Cannot select() on a '${role}' element \u2014 select is for list/combo elements.`,
|
|
502
|
+
suggestion: "click"
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
return { valid: true };
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// src/smart-wait.ts
|
|
509
|
+
var LOADING_SELECTORS = [
|
|
510
|
+
'[aria-busy="true"]',
|
|
511
|
+
'[aria-label*="loading" i]',
|
|
512
|
+
".loading",
|
|
513
|
+
".spinner",
|
|
514
|
+
".skeleton",
|
|
515
|
+
"[data-loading]",
|
|
516
|
+
'[data-testid="loading"]',
|
|
517
|
+
"progress:not([value])"
|
|
518
|
+
];
|
|
519
|
+
async function waitForPageReady(page, options) {
|
|
520
|
+
const timeout = options?.timeout ?? 1e4;
|
|
521
|
+
await Promise.race([
|
|
522
|
+
runReadinessChecks(page),
|
|
523
|
+
new Promise((resolve) => setTimeout(resolve, timeout))
|
|
524
|
+
]);
|
|
525
|
+
}
|
|
526
|
+
function checkLoadingIndicators(selectors) {
|
|
527
|
+
const doc = globalThis.document;
|
|
528
|
+
return selectors.some((sel) => doc.querySelector(sel) !== null);
|
|
529
|
+
}
|
|
530
|
+
function checkLoadingGone(selectors) {
|
|
531
|
+
const doc = globalThis.document;
|
|
532
|
+
return !selectors.some((sel) => doc.querySelector(sel));
|
|
533
|
+
}
|
|
534
|
+
function checkAnimationsRunning() {
|
|
535
|
+
const doc = globalThis.document;
|
|
536
|
+
return doc.getAnimations().filter((a) => a.playState === "running").length > 0;
|
|
537
|
+
}
|
|
538
|
+
function checkAnimationsSettled() {
|
|
539
|
+
const doc = globalThis.document;
|
|
540
|
+
return doc.getAnimations().filter((a) => a.playState === "running").length === 0;
|
|
541
|
+
}
|
|
542
|
+
async function runReadinessChecks(page) {
|
|
543
|
+
try {
|
|
544
|
+
await page.waitForLoadState("domcontentloaded", { timeout: 5e3 });
|
|
545
|
+
} catch {
|
|
546
|
+
}
|
|
547
|
+
try {
|
|
548
|
+
await page.waitForLoadState("networkidle", { timeout: 5e3 });
|
|
549
|
+
} catch {
|
|
550
|
+
}
|
|
551
|
+
try {
|
|
552
|
+
const hasIndicators = await page.evaluate(checkLoadingIndicators, LOADING_SELECTORS);
|
|
553
|
+
if (hasIndicators) {
|
|
554
|
+
try {
|
|
555
|
+
await page.waitForFunction(checkLoadingGone, LOADING_SELECTORS, { timeout: 3e3 });
|
|
556
|
+
} catch {
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
} catch {
|
|
560
|
+
}
|
|
561
|
+
try {
|
|
562
|
+
const hasRunning = await page.evaluate(checkAnimationsRunning);
|
|
563
|
+
if (hasRunning) {
|
|
564
|
+
try {
|
|
565
|
+
await page.waitForFunction(checkAnimationsSettled, void 0, { timeout: 2e3 });
|
|
566
|
+
} catch {
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
} catch {
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// src/scroll-helper.ts
|
|
574
|
+
async function scrollWithRef(page, refData, deltaX, deltaY, scrollAxis) {
|
|
575
|
+
return page.evaluate(
|
|
576
|
+
(args) => {
|
|
577
|
+
const [role, name, nth, dx, dy, axis] = args;
|
|
578
|
+
const doc = globalThis.document;
|
|
579
|
+
const win = globalThis.window;
|
|
580
|
+
const IMPLICIT_ROLES = {
|
|
581
|
+
button: "button",
|
|
582
|
+
a: "link",
|
|
583
|
+
input: "textbox",
|
|
584
|
+
select: "combobox",
|
|
585
|
+
textarea: "textbox",
|
|
586
|
+
h1: "heading",
|
|
587
|
+
h2: "heading",
|
|
588
|
+
h3: "heading",
|
|
589
|
+
h4: "heading",
|
|
590
|
+
h5: "heading",
|
|
591
|
+
h6: "heading",
|
|
592
|
+
nav: "navigation",
|
|
593
|
+
main: "main",
|
|
594
|
+
aside: "complementary",
|
|
595
|
+
footer: "contentinfo",
|
|
596
|
+
header: "banner",
|
|
597
|
+
form: "form",
|
|
598
|
+
table: "table",
|
|
599
|
+
img: "img",
|
|
600
|
+
ul: "list",
|
|
601
|
+
ol: "list",
|
|
602
|
+
li: "listitem",
|
|
603
|
+
dialog: "dialog",
|
|
604
|
+
details: "group",
|
|
605
|
+
summary: "button",
|
|
606
|
+
progress: "progressbar",
|
|
607
|
+
meter: "meter",
|
|
608
|
+
output: "status",
|
|
609
|
+
article: "article",
|
|
610
|
+
section: "region",
|
|
611
|
+
fieldset: "group",
|
|
612
|
+
legend: "legend"
|
|
613
|
+
};
|
|
614
|
+
const INPUT_TYPE_ROLES = {
|
|
615
|
+
checkbox: "checkbox",
|
|
616
|
+
radio: "radio",
|
|
617
|
+
range: "slider",
|
|
618
|
+
number: "spinbutton",
|
|
619
|
+
search: "searchbox",
|
|
620
|
+
email: "textbox",
|
|
621
|
+
tel: "textbox",
|
|
622
|
+
url: "textbox",
|
|
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(" ");
|
|
635
|
+
}
|
|
636
|
+
const title = el.getAttribute("title");
|
|
637
|
+
if (title) return title.trim();
|
|
638
|
+
const alt = el.getAttribute("alt");
|
|
639
|
+
if (alt) return alt.trim();
|
|
640
|
+
return (el.textContent || "").trim();
|
|
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";
|
|
647
|
+
}
|
|
648
|
+
if (tag === "a" && el.hasAttribute("href")) return "link";
|
|
649
|
+
return IMPLICIT_ROLES[tag] || null;
|
|
650
|
+
}
|
|
651
|
+
const candidates = [];
|
|
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();
|
|
678
|
+
return {
|
|
679
|
+
bounds: { x: Math.round(rect2.x), y: Math.round(rect2.y), width: Math.round(rect2.width), height: Math.round(rect2.height) },
|
|
680
|
+
scrolledContainer: "element",
|
|
681
|
+
scrolled: before2 !== after2
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
let parent = target.parentElement;
|
|
685
|
+
while (parent && parent !== doc.documentElement) {
|
|
686
|
+
const style = win.getComputedStyle(parent);
|
|
687
|
+
const overflow = axis === "horizontal" ? style.overflowX : style.overflowY;
|
|
688
|
+
const isScrollable = overflow === "auto" || overflow === "scroll" || overflow === "hidden";
|
|
689
|
+
const hasContent = axis === "horizontal" ? parent.scrollWidth > parent.clientWidth : parent.scrollHeight > parent.clientHeight;
|
|
690
|
+
if (isScrollable && hasContent) {
|
|
691
|
+
const prop2 = axis === "horizontal" ? "scrollLeft" : "scrollTop";
|
|
692
|
+
const before2 = parent[prop2];
|
|
693
|
+
parent.scrollBy(dx, dy);
|
|
694
|
+
const after2 = parent[prop2];
|
|
695
|
+
const rect2 = target.getBoundingClientRect();
|
|
696
|
+
return {
|
|
697
|
+
bounds: { x: Math.round(rect2.x), y: Math.round(rect2.y), width: Math.round(rect2.width), height: Math.round(rect2.height) },
|
|
698
|
+
scrolledContainer: "ancestor",
|
|
699
|
+
scrolled: before2 !== after2
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
parent = parent.parentElement;
|
|
703
|
+
}
|
|
704
|
+
const prop = axis === "horizontal" ? "scrollLeft" : "scrollTop";
|
|
705
|
+
const before = doc.documentElement[prop];
|
|
706
|
+
doc.documentElement.scrollBy(dx, dy);
|
|
707
|
+
const after = doc.documentElement[prop];
|
|
708
|
+
const rect = target.getBoundingClientRect();
|
|
709
|
+
return {
|
|
710
|
+
bounds: { x: Math.round(rect.x), y: Math.round(rect.y), width: Math.round(rect.width), height: Math.round(rect.height) },
|
|
711
|
+
scrolledContainer: "documentElement",
|
|
712
|
+
scrolled: before !== after
|
|
713
|
+
};
|
|
714
|
+
},
|
|
715
|
+
[refData.role, refData.name, refData.nth ?? 0, deltaX, deltaY, scrollAxis]
|
|
716
|
+
);
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// src/browser-install-error.ts
|
|
720
|
+
function displayBrowserName(browserName) {
|
|
721
|
+
if (browserName === "chromium") return "Chromium";
|
|
722
|
+
if (browserName === "firefox") return "Firefox";
|
|
723
|
+
return "WebKit";
|
|
724
|
+
}
|
|
725
|
+
function isPlaywrightMissingBrowserError(error) {
|
|
726
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
727
|
+
const lower = message.toLowerCase();
|
|
728
|
+
return lower.includes("executable doesn't exist") && lower.includes("playwright") && lower.includes("install");
|
|
729
|
+
}
|
|
730
|
+
function formatMissingBrowserError(browserName) {
|
|
731
|
+
return [
|
|
732
|
+
`agent-qa browser support is not installed for ${displayBrowserName(browserName)}.`,
|
|
733
|
+
"",
|
|
734
|
+
"Install the browser managed by agent-qa, then rerun the test:",
|
|
735
|
+
` agent-qa install-browsers --${browserName}`,
|
|
736
|
+
"",
|
|
737
|
+
"This can happen after upgrading agent-qa or Playwright because browser binaries live outside the package install."
|
|
738
|
+
].join("\n");
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// src/adapter.ts
|
|
742
|
+
function clickDelayOption(clickDelay) {
|
|
743
|
+
return clickDelay !== void 0 && clickDelay > 0 ? { delay: clickDelay } : {};
|
|
744
|
+
}
|
|
745
|
+
var WebPlatformAdapter = class {
|
|
746
|
+
platform = "web";
|
|
747
|
+
browser = null;
|
|
748
|
+
context = null;
|
|
749
|
+
page = null;
|
|
750
|
+
lastRefs = {};
|
|
751
|
+
pageLogBuffers = /* @__PURE__ */ new Map();
|
|
752
|
+
logCaptureConfig = { console: true, network: true };
|
|
753
|
+
disconnected = false;
|
|
754
|
+
intentionalClose = false;
|
|
755
|
+
async setup(config) {
|
|
756
|
+
const { chromium, firefox, webkit } = await import("playwright-core");
|
|
757
|
+
const launchers = { chromium, firefox, webkit };
|
|
758
|
+
const browserName = config.browser?.name ?? "chromium";
|
|
759
|
+
const launcher = launchers[browserName];
|
|
760
|
+
try {
|
|
761
|
+
this.browser = await launcher.launch({
|
|
762
|
+
headless: config.browser?.headless ?? true,
|
|
763
|
+
...browserName === "chromium" ? { args: ["--disable-blink-features=AutomationControlled"] } : {}
|
|
764
|
+
});
|
|
765
|
+
} catch (error) {
|
|
766
|
+
if (isPlaywrightMissingBrowserError(error)) {
|
|
767
|
+
throw new Error(formatMissingBrowserError(browserName));
|
|
768
|
+
}
|
|
769
|
+
throw error;
|
|
770
|
+
}
|
|
771
|
+
this.disconnected = false;
|
|
772
|
+
this.intentionalClose = false;
|
|
773
|
+
this.browser.on("disconnected", () => {
|
|
774
|
+
if (!this.intentionalClose) {
|
|
775
|
+
this.disconnected = true;
|
|
776
|
+
}
|
|
777
|
+
});
|
|
778
|
+
const contextOptions = {
|
|
779
|
+
viewport: config.browser?.viewport ?? { width: 1280, height: 720 }
|
|
780
|
+
};
|
|
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
|
+
this.context = await this.browser.newContext(contextOptions);
|
|
788
|
+
if (browserName === "chromium") {
|
|
789
|
+
await this.context.grantPermissions(["clipboard-write"]);
|
|
790
|
+
}
|
|
791
|
+
this.logCaptureConfig = { console: config.logCapture?.console !== false, network: config.logCapture?.network !== false };
|
|
792
|
+
await this.context.addInitScript(() => {
|
|
793
|
+
Object.defineProperty(navigator, "webdriver", { get: () => false });
|
|
794
|
+
});
|
|
795
|
+
this.page = await this.context.newPage();
|
|
796
|
+
this.attachLogListeners(this.page);
|
|
797
|
+
}
|
|
798
|
+
async cleanup() {
|
|
799
|
+
this.intentionalClose = true;
|
|
800
|
+
if (this.browser) {
|
|
801
|
+
await this.browser.close();
|
|
802
|
+
}
|
|
803
|
+
this.browser = null;
|
|
804
|
+
this.context = null;
|
|
805
|
+
this.page = null;
|
|
806
|
+
this.lastRefs = {};
|
|
807
|
+
this.pageLogBuffers.clear();
|
|
808
|
+
this.disconnected = false;
|
|
809
|
+
this.intentionalClose = false;
|
|
810
|
+
}
|
|
811
|
+
get isBrowserDisconnected() {
|
|
812
|
+
return this.disconnected;
|
|
813
|
+
}
|
|
814
|
+
async observe(options) {
|
|
815
|
+
if (!this.page) {
|
|
816
|
+
throw new Error("WebPlatformAdapter not initialized \u2014 call setup() first");
|
|
817
|
+
}
|
|
818
|
+
await waitForPageReady(this.page);
|
|
819
|
+
const state = await observePage(this.page, {
|
|
820
|
+
extractDom: options?.extractDom
|
|
821
|
+
});
|
|
822
|
+
this.lastRefs = state.metadata.refMap ?? {};
|
|
823
|
+
return state;
|
|
824
|
+
}
|
|
825
|
+
async execute(action) {
|
|
826
|
+
if (!this.page) {
|
|
827
|
+
throw new Error("WebPlatformAdapter not initialized \u2014 call setup() first");
|
|
828
|
+
}
|
|
829
|
+
try {
|
|
830
|
+
return await this.executeAction(action);
|
|
831
|
+
} catch (err) {
|
|
832
|
+
let screenshot;
|
|
833
|
+
let boundingBox;
|
|
834
|
+
try {
|
|
835
|
+
screenshot = await this.page.screenshot({ scale: "css" });
|
|
836
|
+
} catch {
|
|
837
|
+
}
|
|
838
|
+
try {
|
|
839
|
+
if ("ref" in action && typeof action.ref === "string") {
|
|
840
|
+
const resolver = new ElementResolver(this.page, this.lastRefs);
|
|
841
|
+
const box = await resolver.getBoundingBox(action.ref);
|
|
842
|
+
if (box) boundingBox = box;
|
|
843
|
+
}
|
|
844
|
+
} catch {
|
|
845
|
+
}
|
|
846
|
+
return {
|
|
847
|
+
success: false,
|
|
848
|
+
error: err instanceof Error ? err.message : String(err),
|
|
849
|
+
screenshot,
|
|
850
|
+
boundingBox
|
|
851
|
+
};
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
async screenshot() {
|
|
855
|
+
try {
|
|
856
|
+
if (this.page) {
|
|
857
|
+
return await this.page.screenshot({ scale: "css" });
|
|
858
|
+
}
|
|
859
|
+
return void 0;
|
|
860
|
+
} catch {
|
|
861
|
+
return void 0;
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
getPage() {
|
|
865
|
+
return this.page;
|
|
866
|
+
}
|
|
867
|
+
async getVideoPath() {
|
|
868
|
+
try {
|
|
869
|
+
const video = this.page?.video();
|
|
870
|
+
if (!video) return null;
|
|
871
|
+
return await video.path();
|
|
872
|
+
} catch {
|
|
873
|
+
return null;
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
drainConsoleLogs(level) {
|
|
877
|
+
const all = [];
|
|
878
|
+
for (const buffers of this.pageLogBuffers.values()) {
|
|
879
|
+
const newLogs = buffers.consoleLogs.slice(buffers.consoleDrainIndex);
|
|
880
|
+
buffers.consoleDrainIndex = buffers.consoleLogs.length;
|
|
881
|
+
all.push(...newLogs);
|
|
882
|
+
}
|
|
883
|
+
if (!level) return all;
|
|
884
|
+
return all.filter((e) => e.level === level);
|
|
885
|
+
}
|
|
886
|
+
drainNetworkLogs(urlPattern) {
|
|
887
|
+
const all = [];
|
|
888
|
+
for (const buffers of this.pageLogBuffers.values()) {
|
|
889
|
+
const newLogs = buffers.networkLogs.slice(buffers.networkDrainIndex);
|
|
890
|
+
buffers.networkDrainIndex = buffers.networkLogs.length;
|
|
891
|
+
all.push(...newLogs);
|
|
892
|
+
}
|
|
893
|
+
if (!urlPattern) return all;
|
|
894
|
+
return all.filter((e) => e.url.includes(urlPattern));
|
|
895
|
+
}
|
|
896
|
+
attachLogListeners(page) {
|
|
897
|
+
const buffers = {
|
|
898
|
+
consoleLogs: [],
|
|
899
|
+
networkLogs: [],
|
|
900
|
+
consoleDrainIndex: 0,
|
|
901
|
+
networkDrainIndex: 0,
|
|
902
|
+
pendingRequests: /* @__PURE__ */ new Map()
|
|
903
|
+
};
|
|
904
|
+
this.pageLogBuffers.set(page, buffers);
|
|
905
|
+
if (this.logCaptureConfig.console) {
|
|
906
|
+
page.on("console", (msg) => {
|
|
907
|
+
buffers.consoleLogs.push({
|
|
908
|
+
level: msg.type(),
|
|
909
|
+
text: msg.text(),
|
|
910
|
+
location: msg.location(),
|
|
911
|
+
timestamp: Date.now()
|
|
912
|
+
});
|
|
913
|
+
if (buffers.consoleLogs.length > 1e3) {
|
|
914
|
+
buffers.consoleLogs = buffers.consoleLogs.slice(-500);
|
|
915
|
+
}
|
|
916
|
+
});
|
|
917
|
+
}
|
|
918
|
+
if (this.logCaptureConfig.network) {
|
|
919
|
+
page.on("request", (request) => {
|
|
920
|
+
buffers.pendingRequests.set(request.url() + request.method(), {
|
|
921
|
+
url: request.url(),
|
|
922
|
+
method: request.method(),
|
|
923
|
+
headers: request.headers(),
|
|
924
|
+
startTime: Date.now(),
|
|
925
|
+
postData: request.postData() ?? void 0
|
|
926
|
+
});
|
|
927
|
+
});
|
|
928
|
+
page.on("response", async (response) => {
|
|
929
|
+
const key = response.url() + response.request().method();
|
|
930
|
+
const pending = buffers.pendingRequests.get(key);
|
|
931
|
+
buffers.pendingRequests.delete(key);
|
|
932
|
+
let body;
|
|
933
|
+
try {
|
|
934
|
+
const buf = await response.body();
|
|
935
|
+
const ct = response.headers()["content-type"] ?? "";
|
|
936
|
+
if (ct.includes("image/") && !ct.includes("svg")) {
|
|
937
|
+
body = buf.length > 32768 ? void 0 : `data:${ct.split(";")[0]};base64,${buf.toString("base64")}`;
|
|
938
|
+
} else {
|
|
939
|
+
const text = buf.toString("utf-8");
|
|
940
|
+
body = text.length > 32768 ? text.slice(0, 32768) + "...[truncated]" : text;
|
|
941
|
+
}
|
|
942
|
+
} catch {
|
|
943
|
+
body = void 0;
|
|
944
|
+
}
|
|
945
|
+
buffers.networkLogs.push({
|
|
946
|
+
url: response.url(),
|
|
947
|
+
method: response.request().method(),
|
|
948
|
+
status: response.status(),
|
|
949
|
+
requestHeaders: pending?.headers ?? response.request().headers(),
|
|
950
|
+
responseHeaders: response.headers(),
|
|
951
|
+
body,
|
|
952
|
+
requestBody: pending?.postData,
|
|
953
|
+
startTime: pending?.startTime ?? Date.now(),
|
|
954
|
+
endTime: Date.now(),
|
|
955
|
+
timing: response.request().timing()
|
|
956
|
+
});
|
|
957
|
+
if (buffers.networkLogs.length > 500) {
|
|
958
|
+
buffers.networkLogs = buffers.networkLogs.slice(-250);
|
|
959
|
+
}
|
|
960
|
+
});
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
async resolveTabTargetAsync(tab) {
|
|
964
|
+
const pages = this.context.pages();
|
|
965
|
+
if (tab.index !== void 0) {
|
|
966
|
+
return tab.index >= 0 && tab.index < pages.length ? pages[tab.index] : void 0;
|
|
967
|
+
}
|
|
968
|
+
if (tab.title) {
|
|
969
|
+
for (const p of pages) {
|
|
970
|
+
const t = await p.title();
|
|
971
|
+
if (t.includes(tab.title)) return p;
|
|
972
|
+
}
|
|
973
|
+
return void 0;
|
|
974
|
+
}
|
|
975
|
+
if (tab.url) {
|
|
976
|
+
return pages.find((p) => p.url().includes(tab.url));
|
|
977
|
+
}
|
|
978
|
+
return void 0;
|
|
979
|
+
}
|
|
980
|
+
async executeAction(action) {
|
|
981
|
+
const page = this.page;
|
|
982
|
+
const resolver = new ElementResolver(page, this.lastRefs);
|
|
983
|
+
if ("ref" in action && typeof action.ref === "string") {
|
|
984
|
+
const refData = this.lastRefs[action.ref];
|
|
985
|
+
const role = refData?.role;
|
|
986
|
+
const validation = validateAction(action, role);
|
|
987
|
+
if (!validation.valid) {
|
|
988
|
+
return { success: false, error: validation.error };
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
switch (action.type) {
|
|
992
|
+
case "click": {
|
|
993
|
+
const locator = resolver.resolve(action.ref);
|
|
994
|
+
const box = await resolver.getBoundingBox(action.ref);
|
|
995
|
+
const coordinates = box ? { x: box.x + box.width / 2, y: box.y + box.height / 2 } : void 0;
|
|
996
|
+
await locator.click({
|
|
997
|
+
...clickDelayOption(action.clickDelay)
|
|
998
|
+
});
|
|
999
|
+
return { success: true, coordinates, boundingBox: box ?? void 0 };
|
|
1000
|
+
}
|
|
1001
|
+
case "fill": {
|
|
1002
|
+
const locator = resolver.resolve(action.ref);
|
|
1003
|
+
const box = await resolver.getBoundingBox(action.ref);
|
|
1004
|
+
await locator.fill(action.value);
|
|
1005
|
+
return { success: true, boundingBox: box ?? void 0 };
|
|
1006
|
+
}
|
|
1007
|
+
case "select": {
|
|
1008
|
+
const locator = resolver.resolve(action.ref);
|
|
1009
|
+
const box = await resolver.getBoundingBox(action.ref);
|
|
1010
|
+
await locator.selectOption(action.value);
|
|
1011
|
+
return { success: true, boundingBox: box ?? void 0 };
|
|
1012
|
+
}
|
|
1013
|
+
case "navigate": {
|
|
1014
|
+
await page.goto(action.url, { waitUntil: "domcontentloaded" });
|
|
1015
|
+
await waitForPageReady(page);
|
|
1016
|
+
return { success: true };
|
|
1017
|
+
}
|
|
1018
|
+
case "scroll": {
|
|
1019
|
+
const deltaX = action.scrollType === "horizontal" ? action.value : 0;
|
|
1020
|
+
const deltaY = action.scrollType === "vertical" ? action.value : 0;
|
|
1021
|
+
let coordinates;
|
|
1022
|
+
let boundingBox;
|
|
1023
|
+
let scrollData;
|
|
1024
|
+
if (action.ref) {
|
|
1025
|
+
const refData = this.lastRefs[action.ref];
|
|
1026
|
+
if (refData) {
|
|
1027
|
+
const scrollAxis = action.scrollType === "horizontal" ? "horizontal" : "vertical";
|
|
1028
|
+
const result = await scrollWithRef(page, refData, deltaX, deltaY, scrollAxis);
|
|
1029
|
+
if (result.bounds) {
|
|
1030
|
+
coordinates = { x: result.bounds.x + result.bounds.width / 2, y: result.bounds.y + result.bounds.height / 2 };
|
|
1031
|
+
boundingBox = result.bounds;
|
|
1032
|
+
}
|
|
1033
|
+
scrollData = { scrolled: result.scrolled, scrolledContainer: result.scrolledContainer };
|
|
1034
|
+
} else {
|
|
1035
|
+
await page.mouse.wheel(deltaX, deltaY);
|
|
1036
|
+
}
|
|
1037
|
+
} else {
|
|
1038
|
+
await page.mouse.wheel(deltaX, deltaY);
|
|
1039
|
+
}
|
|
1040
|
+
await page.evaluate(`new Promise(resolve => {
|
|
1041
|
+
const t = setTimeout(() => resolve('timeout'), 500);
|
|
1042
|
+
requestAnimationFrame(() => requestAnimationFrame(() => {
|
|
1043
|
+
clearTimeout(t);
|
|
1044
|
+
resolve('raf');
|
|
1045
|
+
}));
|
|
1046
|
+
})`);
|
|
1047
|
+
return { success: true, coordinates, boundingBox, data: scrollData };
|
|
1048
|
+
}
|
|
1049
|
+
case "waitFor": {
|
|
1050
|
+
const timeout = action.timeout ?? 5e3;
|
|
1051
|
+
try {
|
|
1052
|
+
await page.waitForFunction(
|
|
1053
|
+
`document.body && document.body.innerText.includes(${JSON.stringify(action.condition)})`,
|
|
1054
|
+
{ timeout }
|
|
1055
|
+
);
|
|
1056
|
+
} catch {
|
|
1057
|
+
try {
|
|
1058
|
+
await page.waitForSelector(action.condition, { timeout });
|
|
1059
|
+
} catch {
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
return { success: true };
|
|
1063
|
+
}
|
|
1064
|
+
case "delay": {
|
|
1065
|
+
await page.waitForTimeout(action.ms);
|
|
1066
|
+
return { success: true };
|
|
1067
|
+
}
|
|
1068
|
+
case "waitForUrl": {
|
|
1069
|
+
try {
|
|
1070
|
+
await page.waitForURL(action.pattern, { timeout: 3e4 });
|
|
1071
|
+
return { success: true };
|
|
1072
|
+
} catch (err) {
|
|
1073
|
+
return {
|
|
1074
|
+
success: false,
|
|
1075
|
+
error: `URL pattern "${action.pattern}" did not match within 30s. Hint: use "**" wildcards for substring matching (e.g., "**/dashboard**"). ${err instanceof Error ? err.message : String(err)}`
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
case "keypress": {
|
|
1080
|
+
const effectiveKeys = (0, import_agent_qa_core.convertKeysForPlatform)(action.keys, {
|
|
1081
|
+
enabled: action.convertPlatformKeys !== false,
|
|
1082
|
+
isMac: (0, import_agent_qa_core.isMacPlatform)()
|
|
1083
|
+
});
|
|
1084
|
+
for (const key of effectiveKeys) {
|
|
1085
|
+
await page.keyboard.press(key);
|
|
1086
|
+
}
|
|
1087
|
+
return { success: true };
|
|
1088
|
+
}
|
|
1089
|
+
case "hover": {
|
|
1090
|
+
const locator = resolver.resolve(action.ref);
|
|
1091
|
+
const box = await resolver.getBoundingBox(action.ref);
|
|
1092
|
+
await locator.hover();
|
|
1093
|
+
return { success: true, boundingBox: box ?? void 0 };
|
|
1094
|
+
}
|
|
1095
|
+
case "paste": {
|
|
1096
|
+
const locator = resolver.resolve(action.ref);
|
|
1097
|
+
const box = await resolver.getBoundingBox(action.ref);
|
|
1098
|
+
await locator.focus();
|
|
1099
|
+
await page.evaluate(`(() => {
|
|
1100
|
+
const el = document.activeElement;
|
|
1101
|
+
if (!el) return;
|
|
1102
|
+
const dt = new DataTransfer();
|
|
1103
|
+
dt.setData('text/plain', ${JSON.stringify(action.value)});
|
|
1104
|
+
el.dispatchEvent(new ClipboardEvent('paste', {
|
|
1105
|
+
clipboardData: dt,
|
|
1106
|
+
bubbles: true,
|
|
1107
|
+
cancelable: true,
|
|
1108
|
+
}));
|
|
1109
|
+
})()`);
|
|
1110
|
+
return { success: true, boundingBox: box ?? void 0 };
|
|
1111
|
+
}
|
|
1112
|
+
case "fileUpload": {
|
|
1113
|
+
const { existsSync } = await import("fs");
|
|
1114
|
+
for (const filePath of action.files) {
|
|
1115
|
+
if (!existsSync(filePath)) {
|
|
1116
|
+
return { success: false, error: `File not found: ${filePath}` };
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
const locator = resolver.resolve(action.ref);
|
|
1120
|
+
const box = await resolver.getBoundingBox(action.ref);
|
|
1121
|
+
await locator.setInputFiles(action.files);
|
|
1122
|
+
return { success: true, boundingBox: box ?? void 0 };
|
|
1123
|
+
}
|
|
1124
|
+
case "copy": {
|
|
1125
|
+
const locator = resolver.resolve(action.ref);
|
|
1126
|
+
const box = await resolver.getBoundingBox(action.ref);
|
|
1127
|
+
const text = await locator.textContent() ?? "";
|
|
1128
|
+
await page.evaluate(`navigator.clipboard.writeText(${JSON.stringify(text)})`);
|
|
1129
|
+
return { success: true, boundingBox: box ?? void 0, data: { copied: text } };
|
|
1130
|
+
}
|
|
1131
|
+
case "keyDown": {
|
|
1132
|
+
await page.keyboard.down(action.key);
|
|
1133
|
+
return { success: true };
|
|
1134
|
+
}
|
|
1135
|
+
case "keyUp": {
|
|
1136
|
+
await page.keyboard.up(action.key);
|
|
1137
|
+
return { success: true };
|
|
1138
|
+
}
|
|
1139
|
+
case "refresh": {
|
|
1140
|
+
await page.reload({ waitUntil: "domcontentloaded" });
|
|
1141
|
+
await waitForPageReady(page);
|
|
1142
|
+
return { success: true };
|
|
1143
|
+
}
|
|
1144
|
+
case "navigateHistory": {
|
|
1145
|
+
if (action.direction === "back") {
|
|
1146
|
+
await page.goBack({ waitUntil: "domcontentloaded" });
|
|
1147
|
+
} else {
|
|
1148
|
+
await page.goForward({ waitUntil: "domcontentloaded" });
|
|
1149
|
+
}
|
|
1150
|
+
await waitForPageReady(page);
|
|
1151
|
+
return { success: true };
|
|
1152
|
+
}
|
|
1153
|
+
case "readConsoleLogs": {
|
|
1154
|
+
let entries;
|
|
1155
|
+
if (action.tab) {
|
|
1156
|
+
const targetPage = await this.resolveTabTargetAsync(action.tab);
|
|
1157
|
+
if (!targetPage) {
|
|
1158
|
+
return { success: false, error: `No tab found matching ${JSON.stringify(action.tab)}` };
|
|
1159
|
+
}
|
|
1160
|
+
const buffers = this.pageLogBuffers.get(targetPage);
|
|
1161
|
+
entries = buffers ? [...buffers.consoleLogs] : [];
|
|
1162
|
+
} else {
|
|
1163
|
+
entries = [];
|
|
1164
|
+
for (const buffers of this.pageLogBuffers.values()) {
|
|
1165
|
+
entries.push(...buffers.consoleLogs);
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
if (action.level) {
|
|
1169
|
+
entries = entries.filter((e) => e.level === action.level);
|
|
1170
|
+
}
|
|
1171
|
+
return { success: true, data: entries };
|
|
1172
|
+
}
|
|
1173
|
+
case "readNetworkLogs": {
|
|
1174
|
+
let entries;
|
|
1175
|
+
if (action.tab) {
|
|
1176
|
+
const targetPage = await this.resolveTabTargetAsync(action.tab);
|
|
1177
|
+
if (!targetPage) {
|
|
1178
|
+
return { success: false, error: `No tab found matching ${JSON.stringify(action.tab)}` };
|
|
1179
|
+
}
|
|
1180
|
+
const buffers = this.pageLogBuffers.get(targetPage);
|
|
1181
|
+
entries = buffers ? [...buffers.networkLogs] : [];
|
|
1182
|
+
} else {
|
|
1183
|
+
entries = [];
|
|
1184
|
+
for (const buffers of this.pageLogBuffers.values()) {
|
|
1185
|
+
entries.push(...buffers.networkLogs);
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
if (action.urlPattern) {
|
|
1189
|
+
entries = entries.filter((e) => e.url.includes(action.urlPattern));
|
|
1190
|
+
}
|
|
1191
|
+
return { success: true, data: entries };
|
|
1192
|
+
}
|
|
1193
|
+
case "readCookies": {
|
|
1194
|
+
const cookies = await this.context.cookies();
|
|
1195
|
+
if (action.name) {
|
|
1196
|
+
const cookie = cookies.find((c) => c.name === action.name) ?? null;
|
|
1197
|
+
return { success: true, data: cookie };
|
|
1198
|
+
}
|
|
1199
|
+
return { success: true, data: cookies };
|
|
1200
|
+
}
|
|
1201
|
+
case "setCookies": {
|
|
1202
|
+
const cookiesWithDefaults = action.cookies.map((c) => ({
|
|
1203
|
+
...c,
|
|
1204
|
+
url: !c.domain ? page.url() : void 0
|
|
1205
|
+
}));
|
|
1206
|
+
await this.context.addCookies(cookiesWithDefaults);
|
|
1207
|
+
return { success: true };
|
|
1208
|
+
}
|
|
1209
|
+
case "readLocalStorage": {
|
|
1210
|
+
const pageUrl = page.url();
|
|
1211
|
+
if (pageUrl === "about:blank") {
|
|
1212
|
+
return { success: true, data: action.key ? null : {} };
|
|
1213
|
+
}
|
|
1214
|
+
if (action.key) {
|
|
1215
|
+
const value = await page.evaluate(
|
|
1216
|
+
(k) => localStorage.getItem(k),
|
|
1217
|
+
action.key
|
|
1218
|
+
);
|
|
1219
|
+
return { success: true, data: value };
|
|
1220
|
+
}
|
|
1221
|
+
const all = await page.evaluate(
|
|
1222
|
+
() => Object.fromEntries(
|
|
1223
|
+
Object.keys(localStorage).map((k) => [k, localStorage.getItem(k)])
|
|
1224
|
+
)
|
|
1225
|
+
);
|
|
1226
|
+
return { success: true, data: all };
|
|
1227
|
+
}
|
|
1228
|
+
case "setLocalStorage": {
|
|
1229
|
+
await page.evaluate((entries) => {
|
|
1230
|
+
for (const { key, value } of entries) {
|
|
1231
|
+
localStorage.setItem(key, value);
|
|
1232
|
+
}
|
|
1233
|
+
}, action.entries);
|
|
1234
|
+
return { success: true };
|
|
1235
|
+
}
|
|
1236
|
+
case "assert": {
|
|
1237
|
+
return { success: true };
|
|
1238
|
+
}
|
|
1239
|
+
case "clearText": {
|
|
1240
|
+
const loc = resolver.resolve(action.ref);
|
|
1241
|
+
const clearBox = await resolver.getBoundingBox(action.ref);
|
|
1242
|
+
await loc.clear();
|
|
1243
|
+
return { success: true, boundingBox: clearBox ?? void 0 };
|
|
1244
|
+
}
|
|
1245
|
+
case "drag": {
|
|
1246
|
+
const fromLoc = resolver.resolve(action.fromRef);
|
|
1247
|
+
const toLoc = resolver.resolve(action.toRef);
|
|
1248
|
+
await fromLoc.dragTo(toLoc);
|
|
1249
|
+
return { success: true };
|
|
1250
|
+
}
|
|
1251
|
+
case "doubleTap": {
|
|
1252
|
+
const dblLoc = resolver.resolve(action.ref);
|
|
1253
|
+
const dblBox = await resolver.getBoundingBox(action.ref);
|
|
1254
|
+
await dblLoc.dblclick();
|
|
1255
|
+
return { success: true, boundingBox: dblBox ?? void 0 };
|
|
1256
|
+
}
|
|
1257
|
+
case "doubleClick": {
|
|
1258
|
+
const locator = resolver.resolve(action.ref);
|
|
1259
|
+
const box = await resolver.getBoundingBox(action.ref);
|
|
1260
|
+
const coordinates = box ? action.relativePosition ? { x: box.x + action.relativePosition.x, y: box.y + action.relativePosition.y } : { x: box.x + box.width / 2, y: box.y + box.height / 2 } : void 0;
|
|
1261
|
+
await locator.dblclick({
|
|
1262
|
+
...action.relativePosition && { position: action.relativePosition },
|
|
1263
|
+
...clickDelayOption(action.clickDelay)
|
|
1264
|
+
});
|
|
1265
|
+
return { success: true, coordinates, boundingBox: box ?? void 0 };
|
|
1266
|
+
}
|
|
1267
|
+
case "rightClick": {
|
|
1268
|
+
const locator = resolver.resolve(action.ref);
|
|
1269
|
+
const box = await resolver.getBoundingBox(action.ref);
|
|
1270
|
+
const coordinates = box ? action.relativePosition ? { x: box.x + action.relativePosition.x, y: box.y + action.relativePosition.y } : { x: box.x + box.width / 2, y: box.y + box.height / 2 } : void 0;
|
|
1271
|
+
await locator.click({
|
|
1272
|
+
button: "right",
|
|
1273
|
+
...action.relativePosition && { position: action.relativePosition },
|
|
1274
|
+
...clickDelayOption(action.clickDelay)
|
|
1275
|
+
});
|
|
1276
|
+
return { success: true, coordinates, boundingBox: box ?? void 0 };
|
|
1277
|
+
}
|
|
1278
|
+
case "openLink": {
|
|
1279
|
+
await page.goto(action.url, { waitUntil: "domcontentloaded" });
|
|
1280
|
+
await waitForPageReady(page);
|
|
1281
|
+
return { success: true };
|
|
1282
|
+
}
|
|
1283
|
+
case "newTab": {
|
|
1284
|
+
const newPage = await this.context.newPage();
|
|
1285
|
+
this.attachLogListeners(newPage);
|
|
1286
|
+
await newPage.goto(action.url, { waitUntil: "domcontentloaded" });
|
|
1287
|
+
await waitForPageReady(newPage);
|
|
1288
|
+
this.page = newPage;
|
|
1289
|
+
return { success: true };
|
|
1290
|
+
}
|
|
1291
|
+
case "switchTab": {
|
|
1292
|
+
const pages = this.context.pages();
|
|
1293
|
+
let target;
|
|
1294
|
+
if (action.index !== void 0) {
|
|
1295
|
+
if (action.index < 0 || action.index >= pages.length) {
|
|
1296
|
+
return { success: false, error: `Tab index ${action.index} out of range (${pages.length} tabs open)` };
|
|
1297
|
+
}
|
|
1298
|
+
target = pages[action.index];
|
|
1299
|
+
} else if (action.title) {
|
|
1300
|
+
for (const p of pages) {
|
|
1301
|
+
const t = await p.title();
|
|
1302
|
+
if (t.includes(action.title)) {
|
|
1303
|
+
target = p;
|
|
1304
|
+
break;
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
} else if (action.url) {
|
|
1308
|
+
target = pages.find((p) => p.url().includes(action.url));
|
|
1309
|
+
} else {
|
|
1310
|
+
return { success: false, error: "switchTab requires at least one of: index, title, or url" };
|
|
1311
|
+
}
|
|
1312
|
+
if (!target) {
|
|
1313
|
+
return { success: false, error: `No tab found matching ${JSON.stringify({ index: action.index, title: action.title, url: action.url })}` };
|
|
1314
|
+
}
|
|
1315
|
+
this.page = target;
|
|
1316
|
+
return { success: true };
|
|
1317
|
+
}
|
|
1318
|
+
// Mobile-only actions — graceful unsupported messages
|
|
1319
|
+
case "tap":
|
|
1320
|
+
case "swipe":
|
|
1321
|
+
case "longpress":
|
|
1322
|
+
case "pinch":
|
|
1323
|
+
case "multiTap": {
|
|
1324
|
+
return { success: false, error: `${action.type} is a mobile-only gesture` };
|
|
1325
|
+
}
|
|
1326
|
+
case "tapCoordinate": {
|
|
1327
|
+
const vp = page.viewportSize();
|
|
1328
|
+
if (vp) {
|
|
1329
|
+
(0, import_agent_qa_core.warnIfOutOfBounds)({ x: action.x, y: action.y }, { width: vp.width, height: vp.height }, "tapCoordinate");
|
|
1330
|
+
}
|
|
1331
|
+
await page.mouse.click(action.x, action.y);
|
|
1332
|
+
return { success: true, coordinates: { x: action.x, y: action.y } };
|
|
1333
|
+
}
|
|
1334
|
+
case "executeScript": {
|
|
1335
|
+
return { success: false, error: "executeScript is a mobile-only action" };
|
|
1336
|
+
}
|
|
1337
|
+
case "hideKeyboard":
|
|
1338
|
+
case "launchApp":
|
|
1339
|
+
case "stopApp":
|
|
1340
|
+
case "setOrientation": {
|
|
1341
|
+
return { success: true };
|
|
1342
|
+
}
|
|
1343
|
+
default: {
|
|
1344
|
+
return { success: false, error: `Unknown action type: ${action.type}` };
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
};
|
|
1349
|
+
|
|
1350
|
+
// src/farm-adapter.ts
|
|
1351
|
+
var import_agent_qa_core2 = require("@vostride/agent-qa-core");
|
|
1352
|
+
var import_agent_qa_core3 = require("@vostride/agent-qa-core");
|
|
1353
|
+
function clickDelayOption2(clickDelay) {
|
|
1354
|
+
return clickDelay !== void 0 && clickDelay > 0 ? { delay: clickDelay } : {};
|
|
1355
|
+
}
|
|
1356
|
+
var FarmWebAdapter = class {
|
|
1357
|
+
platform = "web";
|
|
1358
|
+
browser = null;
|
|
1359
|
+
context = null;
|
|
1360
|
+
page = null;
|
|
1361
|
+
lastRefs = {};
|
|
1362
|
+
sessionUrl;
|
|
1363
|
+
sessionId;
|
|
1364
|
+
testFailed = false;
|
|
1365
|
+
exitHandler = null;
|
|
1366
|
+
credentials = null;
|
|
1367
|
+
pageLogBuffers = /* @__PURE__ */ new Map();
|
|
1368
|
+
logCaptureConfig = { console: true, network: true };
|
|
1369
|
+
async setup(config) {
|
|
1370
|
+
if (!config.farmSession) {
|
|
1371
|
+
throw new Error("FarmWebAdapter requires config.farmSession to be set");
|
|
1372
|
+
}
|
|
1373
|
+
(0, import_agent_qa_core2.registerAllProviders)();
|
|
1374
|
+
const provider = (0, import_agent_qa_core2.getProvider)("browserstack");
|
|
1375
|
+
if (!provider) {
|
|
1376
|
+
throw new Error("Unknown farm provider: browserstack");
|
|
1377
|
+
}
|
|
1378
|
+
this.credentials = provider.resolveCredentials(config.farmSession.capabilities);
|
|
1379
|
+
if (!config.browser?.name) {
|
|
1380
|
+
throw new Error("FarmWebAdapter requires config.browser.name to be set");
|
|
1381
|
+
}
|
|
1382
|
+
const credentials = this.credentials;
|
|
1383
|
+
const farmWebConfig = {
|
|
1384
|
+
browser: config.browser,
|
|
1385
|
+
credentials,
|
|
1386
|
+
testName: config.farmSession.capabilities?.testName
|
|
1387
|
+
};
|
|
1388
|
+
const { wsEndpoint } = await provider.resolveWebEndpoint(farmWebConfig);
|
|
1389
|
+
const { chromium } = await import("playwright-core");
|
|
1390
|
+
this.browser = await chromium.connect({ wsEndpoint });
|
|
1391
|
+
const contextOptions = {
|
|
1392
|
+
viewport: config.browser?.viewport ?? { width: 1280, height: 720 }
|
|
1393
|
+
};
|
|
1394
|
+
if (config.recording?.enabled) {
|
|
1395
|
+
contextOptions.recordVideo = {
|
|
1396
|
+
dir: config.recording.videoDir,
|
|
1397
|
+
size: config.recording.videoSize ?? config.browser?.viewport ?? { width: 1280, height: 720 }
|
|
1398
|
+
};
|
|
1399
|
+
}
|
|
1400
|
+
this.context = await this.browser.newContext(contextOptions);
|
|
1401
|
+
await this.context.grantPermissions(["clipboard-write"]);
|
|
1402
|
+
this.page = await this.context.newPage();
|
|
1403
|
+
try {
|
|
1404
|
+
const cdpSession = await this.page.context().newCDPSession(this.page);
|
|
1405
|
+
const result = await cdpSession.send("Browser.getVersion");
|
|
1406
|
+
await cdpSession.detach();
|
|
1407
|
+
const auth = Buffer.from(`${credentials.username}:${credentials.accessKey}`).toString("base64");
|
|
1408
|
+
const resp = await fetch("https://api.browserstack.com/automate/builds.json?limit=1&status=running", {
|
|
1409
|
+
headers: { Authorization: `Basic ${auth}` }
|
|
1410
|
+
});
|
|
1411
|
+
if (resp.ok) {
|
|
1412
|
+
const builds = await resp.json();
|
|
1413
|
+
if (builds.length > 0) {
|
|
1414
|
+
const buildId = builds[0].automation_build.hashed_id;
|
|
1415
|
+
const sessResp = await fetch(`https://api.browserstack.com/automate/builds/${buildId}/sessions.json?limit=1`, {
|
|
1416
|
+
headers: { Authorization: `Basic ${auth}` }
|
|
1417
|
+
});
|
|
1418
|
+
if (sessResp.ok) {
|
|
1419
|
+
const sessions = await sessResp.json();
|
|
1420
|
+
if (sessions.length > 0) {
|
|
1421
|
+
this.sessionId = sessions[0].automation_session.hashed_id;
|
|
1422
|
+
this.sessionUrl = sessions[0].automation_session.browser_url;
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
} catch {
|
|
1428
|
+
}
|
|
1429
|
+
this.exitHandler = () => {
|
|
1430
|
+
try {
|
|
1431
|
+
this.browser?.close();
|
|
1432
|
+
} catch {
|
|
1433
|
+
}
|
|
1434
|
+
};
|
|
1435
|
+
process.on("exit", this.exitHandler);
|
|
1436
|
+
this.logCaptureConfig = { console: config.logCapture?.console !== false, network: config.logCapture?.network !== false };
|
|
1437
|
+
this.attachLogListeners(this.page);
|
|
1438
|
+
}
|
|
1439
|
+
async cleanup() {
|
|
1440
|
+
if (this.browser) {
|
|
1441
|
+
await this.browser.close();
|
|
1442
|
+
}
|
|
1443
|
+
if (this.sessionId && this.credentials) {
|
|
1444
|
+
try {
|
|
1445
|
+
const status = this.testFailed ? "failed" : "passed";
|
|
1446
|
+
const reason = this.testFailed ? "Test failed" : "Test passed";
|
|
1447
|
+
const auth = Buffer.from(`${this.credentials.username}:${this.credentials.accessKey}`).toString("base64");
|
|
1448
|
+
await fetch(`https://api.browserstack.com/automate/sessions/${this.sessionId}.json`, {
|
|
1449
|
+
method: "PUT",
|
|
1450
|
+
headers: { Authorization: `Basic ${auth}`, "Content-Type": "application/json" },
|
|
1451
|
+
body: JSON.stringify({ status, reason })
|
|
1452
|
+
});
|
|
1453
|
+
} catch {
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
if (this.exitHandler) {
|
|
1457
|
+
process.removeListener("exit", this.exitHandler);
|
|
1458
|
+
this.exitHandler = null;
|
|
1459
|
+
}
|
|
1460
|
+
this.browser = null;
|
|
1461
|
+
this.context = null;
|
|
1462
|
+
this.page = null;
|
|
1463
|
+
this.lastRefs = {};
|
|
1464
|
+
this.pageLogBuffers.clear();
|
|
1465
|
+
}
|
|
1466
|
+
async observe(options) {
|
|
1467
|
+
if (!this.page) {
|
|
1468
|
+
throw new Error("FarmWebAdapter not initialized \u2014 call setup() first");
|
|
1469
|
+
}
|
|
1470
|
+
await waitForPageReady(this.page);
|
|
1471
|
+
const state = await observePage(this.page, {
|
|
1472
|
+
extractDom: options?.extractDom
|
|
1473
|
+
});
|
|
1474
|
+
this.lastRefs = state.metadata.refMap ?? {};
|
|
1475
|
+
return state;
|
|
1476
|
+
}
|
|
1477
|
+
async execute(action) {
|
|
1478
|
+
if (!this.page) {
|
|
1479
|
+
throw new Error("FarmWebAdapter not initialized \u2014 call setup() first");
|
|
1480
|
+
}
|
|
1481
|
+
try {
|
|
1482
|
+
return await this.executeAction(action);
|
|
1483
|
+
} catch (err) {
|
|
1484
|
+
let screenshot;
|
|
1485
|
+
let boundingBox;
|
|
1486
|
+
try {
|
|
1487
|
+
screenshot = await this.page.screenshot();
|
|
1488
|
+
} catch {
|
|
1489
|
+
}
|
|
1490
|
+
try {
|
|
1491
|
+
if ("ref" in action && typeof action.ref === "string") {
|
|
1492
|
+
const resolver = new ElementResolver(this.page, this.lastRefs);
|
|
1493
|
+
const box = await resolver.getBoundingBox(action.ref);
|
|
1494
|
+
if (box) boundingBox = box;
|
|
1495
|
+
}
|
|
1496
|
+
} catch {
|
|
1497
|
+
}
|
|
1498
|
+
return {
|
|
1499
|
+
success: false,
|
|
1500
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1501
|
+
screenshot,
|
|
1502
|
+
boundingBox
|
|
1503
|
+
};
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
async screenshot() {
|
|
1507
|
+
try {
|
|
1508
|
+
if (this.page) {
|
|
1509
|
+
return await this.page.screenshot();
|
|
1510
|
+
}
|
|
1511
|
+
return void 0;
|
|
1512
|
+
} catch {
|
|
1513
|
+
return void 0;
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
getPage() {
|
|
1517
|
+
return this.page;
|
|
1518
|
+
}
|
|
1519
|
+
async getVideoPath() {
|
|
1520
|
+
try {
|
|
1521
|
+
const video = this.page?.video();
|
|
1522
|
+
if (!video) return null;
|
|
1523
|
+
return await video.path();
|
|
1524
|
+
} catch {
|
|
1525
|
+
return null;
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
markFailed() {
|
|
1529
|
+
this.testFailed = true;
|
|
1530
|
+
}
|
|
1531
|
+
getSessionUrl() {
|
|
1532
|
+
return this.sessionUrl;
|
|
1533
|
+
}
|
|
1534
|
+
getSessionId() {
|
|
1535
|
+
return this.sessionId;
|
|
1536
|
+
}
|
|
1537
|
+
drainConsoleLogs(level) {
|
|
1538
|
+
const all = [];
|
|
1539
|
+
for (const buffers of this.pageLogBuffers.values()) {
|
|
1540
|
+
const newLogs = buffers.consoleLogs.slice(buffers.consoleDrainIndex);
|
|
1541
|
+
buffers.consoleDrainIndex = buffers.consoleLogs.length;
|
|
1542
|
+
all.push(...newLogs);
|
|
1543
|
+
}
|
|
1544
|
+
if (!level) return all;
|
|
1545
|
+
return all.filter((e) => e.level === level);
|
|
1546
|
+
}
|
|
1547
|
+
drainNetworkLogs(urlPattern) {
|
|
1548
|
+
const all = [];
|
|
1549
|
+
for (const buffers of this.pageLogBuffers.values()) {
|
|
1550
|
+
const newLogs = buffers.networkLogs.slice(buffers.networkDrainIndex);
|
|
1551
|
+
buffers.networkDrainIndex = buffers.networkLogs.length;
|
|
1552
|
+
all.push(...newLogs);
|
|
1553
|
+
}
|
|
1554
|
+
if (!urlPattern) return all;
|
|
1555
|
+
return all.filter((e) => e.url.includes(urlPattern));
|
|
1556
|
+
}
|
|
1557
|
+
attachLogListeners(page) {
|
|
1558
|
+
const buffers = {
|
|
1559
|
+
consoleLogs: [],
|
|
1560
|
+
networkLogs: [],
|
|
1561
|
+
consoleDrainIndex: 0,
|
|
1562
|
+
networkDrainIndex: 0,
|
|
1563
|
+
pendingRequests: /* @__PURE__ */ new Map()
|
|
1564
|
+
};
|
|
1565
|
+
this.pageLogBuffers.set(page, buffers);
|
|
1566
|
+
if (this.logCaptureConfig.console) {
|
|
1567
|
+
page.on("console", (msg) => {
|
|
1568
|
+
buffers.consoleLogs.push({
|
|
1569
|
+
level: msg.type(),
|
|
1570
|
+
text: msg.text(),
|
|
1571
|
+
location: msg.location(),
|
|
1572
|
+
timestamp: Date.now()
|
|
1573
|
+
});
|
|
1574
|
+
if (buffers.consoleLogs.length > 1e3) {
|
|
1575
|
+
buffers.consoleLogs = buffers.consoleLogs.slice(-500);
|
|
1576
|
+
}
|
|
1577
|
+
});
|
|
1578
|
+
}
|
|
1579
|
+
if (this.logCaptureConfig.network) {
|
|
1580
|
+
page.on("request", (request) => {
|
|
1581
|
+
buffers.pendingRequests.set(request.url() + request.method(), {
|
|
1582
|
+
url: request.url(),
|
|
1583
|
+
method: request.method(),
|
|
1584
|
+
headers: request.headers(),
|
|
1585
|
+
startTime: Date.now(),
|
|
1586
|
+
postData: request.postData() ?? void 0
|
|
1587
|
+
});
|
|
1588
|
+
});
|
|
1589
|
+
page.on("response", async (response) => {
|
|
1590
|
+
const key = response.url() + response.request().method();
|
|
1591
|
+
const pending = buffers.pendingRequests.get(key);
|
|
1592
|
+
buffers.pendingRequests.delete(key);
|
|
1593
|
+
let body;
|
|
1594
|
+
try {
|
|
1595
|
+
const buf = await response.body();
|
|
1596
|
+
const ct = response.headers()["content-type"] ?? "";
|
|
1597
|
+
if (ct.includes("image/") && !ct.includes("svg")) {
|
|
1598
|
+
body = buf.length > 32768 ? void 0 : `data:${ct.split(";")[0]};base64,${buf.toString("base64")}`;
|
|
1599
|
+
} else {
|
|
1600
|
+
const text = buf.toString("utf-8");
|
|
1601
|
+
body = text.length > 32768 ? text.slice(0, 32768) + "...[truncated]" : text;
|
|
1602
|
+
}
|
|
1603
|
+
} catch {
|
|
1604
|
+
body = void 0;
|
|
1605
|
+
}
|
|
1606
|
+
buffers.networkLogs.push({
|
|
1607
|
+
url: response.url(),
|
|
1608
|
+
method: response.request().method(),
|
|
1609
|
+
status: response.status(),
|
|
1610
|
+
requestHeaders: pending?.headers ?? response.request().headers(),
|
|
1611
|
+
responseHeaders: response.headers(),
|
|
1612
|
+
body,
|
|
1613
|
+
requestBody: pending?.postData,
|
|
1614
|
+
startTime: pending?.startTime ?? Date.now(),
|
|
1615
|
+
endTime: Date.now(),
|
|
1616
|
+
timing: response.request().timing()
|
|
1617
|
+
});
|
|
1618
|
+
if (buffers.networkLogs.length > 500) {
|
|
1619
|
+
buffers.networkLogs = buffers.networkLogs.slice(-250);
|
|
1620
|
+
}
|
|
1621
|
+
});
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
async resolveTabTargetAsync(tab) {
|
|
1625
|
+
const pages = this.context.pages();
|
|
1626
|
+
if (tab.index !== void 0) {
|
|
1627
|
+
return tab.index >= 0 && tab.index < pages.length ? pages[tab.index] : void 0;
|
|
1628
|
+
}
|
|
1629
|
+
if (tab.title) {
|
|
1630
|
+
for (const p of pages) {
|
|
1631
|
+
const t = await p.title();
|
|
1632
|
+
if (t.includes(tab.title)) return p;
|
|
1633
|
+
}
|
|
1634
|
+
return void 0;
|
|
1635
|
+
}
|
|
1636
|
+
if (tab.url) {
|
|
1637
|
+
return pages.find((p) => p.url().includes(tab.url));
|
|
1638
|
+
}
|
|
1639
|
+
return void 0;
|
|
1640
|
+
}
|
|
1641
|
+
async executeAction(action) {
|
|
1642
|
+
const page = this.page;
|
|
1643
|
+
const resolver = new ElementResolver(page, this.lastRefs);
|
|
1644
|
+
if ("ref" in action && typeof action.ref === "string") {
|
|
1645
|
+
const refData = this.lastRefs[action.ref];
|
|
1646
|
+
const role = refData?.role;
|
|
1647
|
+
const validation = validateAction(action, role);
|
|
1648
|
+
if (!validation.valid) {
|
|
1649
|
+
return { success: false, error: validation.error };
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
switch (action.type) {
|
|
1653
|
+
case "click": {
|
|
1654
|
+
const locator = resolver.resolve(action.ref);
|
|
1655
|
+
const box = await resolver.getBoundingBox(action.ref);
|
|
1656
|
+
const coordinates = box ? { x: box.x + box.width / 2, y: box.y + box.height / 2 } : void 0;
|
|
1657
|
+
await locator.click({
|
|
1658
|
+
...clickDelayOption2(action.clickDelay)
|
|
1659
|
+
});
|
|
1660
|
+
return { success: true, coordinates, boundingBox: box ?? void 0 };
|
|
1661
|
+
}
|
|
1662
|
+
case "fill": {
|
|
1663
|
+
const locator = resolver.resolve(action.ref);
|
|
1664
|
+
const box = await resolver.getBoundingBox(action.ref);
|
|
1665
|
+
await locator.fill(action.value);
|
|
1666
|
+
return { success: true, boundingBox: box ?? void 0 };
|
|
1667
|
+
}
|
|
1668
|
+
case "select": {
|
|
1669
|
+
const locator = resolver.resolve(action.ref);
|
|
1670
|
+
const box = await resolver.getBoundingBox(action.ref);
|
|
1671
|
+
await locator.selectOption(action.value);
|
|
1672
|
+
return { success: true, boundingBox: box ?? void 0 };
|
|
1673
|
+
}
|
|
1674
|
+
case "navigate": {
|
|
1675
|
+
await page.goto(action.url, { waitUntil: "domcontentloaded" });
|
|
1676
|
+
await waitForPageReady(page);
|
|
1677
|
+
return { success: true };
|
|
1678
|
+
}
|
|
1679
|
+
case "scroll": {
|
|
1680
|
+
const deltaX = action.scrollType === "horizontal" ? action.value : 0;
|
|
1681
|
+
const deltaY = action.scrollType === "vertical" ? action.value : 0;
|
|
1682
|
+
let coordinates;
|
|
1683
|
+
let boundingBox;
|
|
1684
|
+
let scrollData;
|
|
1685
|
+
if (action.ref) {
|
|
1686
|
+
const refData = this.lastRefs[action.ref];
|
|
1687
|
+
if (refData) {
|
|
1688
|
+
const scrollAxis = action.scrollType === "horizontal" ? "horizontal" : "vertical";
|
|
1689
|
+
const result = await scrollWithRef(page, refData, deltaX, deltaY, scrollAxis);
|
|
1690
|
+
if (result.bounds) {
|
|
1691
|
+
coordinates = { x: result.bounds.x + result.bounds.width / 2, y: result.bounds.y + result.bounds.height / 2 };
|
|
1692
|
+
boundingBox = result.bounds;
|
|
1693
|
+
}
|
|
1694
|
+
scrollData = { scrolled: result.scrolled, scrolledContainer: result.scrolledContainer };
|
|
1695
|
+
} else {
|
|
1696
|
+
await page.mouse.wheel(deltaX, deltaY);
|
|
1697
|
+
}
|
|
1698
|
+
} else {
|
|
1699
|
+
await page.mouse.wheel(deltaX, deltaY);
|
|
1700
|
+
}
|
|
1701
|
+
await page.evaluate(`new Promise(resolve => {
|
|
1702
|
+
const t = setTimeout(() => resolve('timeout'), 500);
|
|
1703
|
+
requestAnimationFrame(() => requestAnimationFrame(() => {
|
|
1704
|
+
clearTimeout(t);
|
|
1705
|
+
resolve('raf');
|
|
1706
|
+
}));
|
|
1707
|
+
})`);
|
|
1708
|
+
return { success: true, coordinates, boundingBox, data: scrollData };
|
|
1709
|
+
}
|
|
1710
|
+
case "waitFor": {
|
|
1711
|
+
const timeout = action.timeout ?? 5e3;
|
|
1712
|
+
try {
|
|
1713
|
+
await page.waitForFunction(
|
|
1714
|
+
`document.body && document.body.innerText.includes(${JSON.stringify(action.condition)})`,
|
|
1715
|
+
{ timeout }
|
|
1716
|
+
);
|
|
1717
|
+
} catch {
|
|
1718
|
+
try {
|
|
1719
|
+
await page.waitForSelector(action.condition, { timeout });
|
|
1720
|
+
} catch {
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
return { success: true };
|
|
1724
|
+
}
|
|
1725
|
+
case "delay": {
|
|
1726
|
+
await page.waitForTimeout(action.ms);
|
|
1727
|
+
return { success: true };
|
|
1728
|
+
}
|
|
1729
|
+
case "waitForUrl": {
|
|
1730
|
+
try {
|
|
1731
|
+
await page.waitForURL(action.pattern, { timeout: 3e4 });
|
|
1732
|
+
return { success: true };
|
|
1733
|
+
} catch (err) {
|
|
1734
|
+
return {
|
|
1735
|
+
success: false,
|
|
1736
|
+
error: `URL pattern "${action.pattern}" did not match within 30s. Hint: use "**" wildcards for substring matching (e.g., "**/dashboard**"). ${err instanceof Error ? err.message : String(err)}`
|
|
1737
|
+
};
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
case "keypress": {
|
|
1741
|
+
const effectiveKeys = (0, import_agent_qa_core3.convertKeysForPlatform)(action.keys, {
|
|
1742
|
+
enabled: action.convertPlatformKeys !== false,
|
|
1743
|
+
isMac: (0, import_agent_qa_core3.isMacPlatform)()
|
|
1744
|
+
});
|
|
1745
|
+
for (const key of effectiveKeys) {
|
|
1746
|
+
await page.keyboard.press(key);
|
|
1747
|
+
}
|
|
1748
|
+
return { success: true };
|
|
1749
|
+
}
|
|
1750
|
+
case "hover": {
|
|
1751
|
+
const locator = resolver.resolve(action.ref);
|
|
1752
|
+
const box = await resolver.getBoundingBox(action.ref);
|
|
1753
|
+
await locator.hover();
|
|
1754
|
+
return { success: true, boundingBox: box ?? void 0 };
|
|
1755
|
+
}
|
|
1756
|
+
case "paste": {
|
|
1757
|
+
const locator = resolver.resolve(action.ref);
|
|
1758
|
+
const box = await resolver.getBoundingBox(action.ref);
|
|
1759
|
+
await locator.focus();
|
|
1760
|
+
await page.evaluate(`(() => {
|
|
1761
|
+
const el = document.activeElement;
|
|
1762
|
+
if (!el) return;
|
|
1763
|
+
const dt = new DataTransfer();
|
|
1764
|
+
dt.setData('text/plain', ${JSON.stringify(action.value)});
|
|
1765
|
+
el.dispatchEvent(new ClipboardEvent('paste', {
|
|
1766
|
+
clipboardData: dt,
|
|
1767
|
+
bubbles: true,
|
|
1768
|
+
cancelable: true,
|
|
1769
|
+
}));
|
|
1770
|
+
})()`);
|
|
1771
|
+
return { success: true, boundingBox: box ?? void 0 };
|
|
1772
|
+
}
|
|
1773
|
+
case "fileUpload": {
|
|
1774
|
+
const { existsSync } = await import("fs");
|
|
1775
|
+
for (const filePath of action.files) {
|
|
1776
|
+
if (!existsSync(filePath)) {
|
|
1777
|
+
return { success: false, error: `File not found: ${filePath}` };
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
const locator = resolver.resolve(action.ref);
|
|
1781
|
+
const box = await resolver.getBoundingBox(action.ref);
|
|
1782
|
+
await locator.setInputFiles(action.files);
|
|
1783
|
+
return { success: true, boundingBox: box ?? void 0 };
|
|
1784
|
+
}
|
|
1785
|
+
case "copy": {
|
|
1786
|
+
const locator = resolver.resolve(action.ref);
|
|
1787
|
+
const box = await resolver.getBoundingBox(action.ref);
|
|
1788
|
+
const text = await locator.textContent() ?? "";
|
|
1789
|
+
await page.evaluate(`navigator.clipboard.writeText(${JSON.stringify(text)})`);
|
|
1790
|
+
return { success: true, boundingBox: box ?? void 0, data: { copied: text } };
|
|
1791
|
+
}
|
|
1792
|
+
case "keyDown": {
|
|
1793
|
+
await page.keyboard.down(action.key);
|
|
1794
|
+
return { success: true };
|
|
1795
|
+
}
|
|
1796
|
+
case "keyUp": {
|
|
1797
|
+
await page.keyboard.up(action.key);
|
|
1798
|
+
return { success: true };
|
|
1799
|
+
}
|
|
1800
|
+
case "refresh": {
|
|
1801
|
+
await page.reload({ waitUntil: "domcontentloaded" });
|
|
1802
|
+
await waitForPageReady(page);
|
|
1803
|
+
return { success: true };
|
|
1804
|
+
}
|
|
1805
|
+
case "navigateHistory": {
|
|
1806
|
+
if (action.direction === "back") {
|
|
1807
|
+
await page.goBack({ waitUntil: "domcontentloaded" });
|
|
1808
|
+
} else {
|
|
1809
|
+
await page.goForward({ waitUntil: "domcontentloaded" });
|
|
1810
|
+
}
|
|
1811
|
+
await waitForPageReady(page);
|
|
1812
|
+
return { success: true };
|
|
1813
|
+
}
|
|
1814
|
+
case "readConsoleLogs": {
|
|
1815
|
+
let entries;
|
|
1816
|
+
if (action.tab) {
|
|
1817
|
+
const targetPage = await this.resolveTabTargetAsync(action.tab);
|
|
1818
|
+
if (!targetPage) {
|
|
1819
|
+
return { success: false, error: `No tab found matching ${JSON.stringify(action.tab)}` };
|
|
1820
|
+
}
|
|
1821
|
+
const buffers = this.pageLogBuffers.get(targetPage);
|
|
1822
|
+
entries = buffers ? [...buffers.consoleLogs] : [];
|
|
1823
|
+
} else {
|
|
1824
|
+
entries = [];
|
|
1825
|
+
for (const buffers of this.pageLogBuffers.values()) {
|
|
1826
|
+
entries.push(...buffers.consoleLogs);
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
if (action.level) {
|
|
1830
|
+
entries = entries.filter((e) => e.level === action.level);
|
|
1831
|
+
}
|
|
1832
|
+
return { success: true, data: entries };
|
|
1833
|
+
}
|
|
1834
|
+
case "readNetworkLogs": {
|
|
1835
|
+
let entries;
|
|
1836
|
+
if (action.tab) {
|
|
1837
|
+
const targetPage = await this.resolveTabTargetAsync(action.tab);
|
|
1838
|
+
if (!targetPage) {
|
|
1839
|
+
return { success: false, error: `No tab found matching ${JSON.stringify(action.tab)}` };
|
|
1840
|
+
}
|
|
1841
|
+
const buffers = this.pageLogBuffers.get(targetPage);
|
|
1842
|
+
entries = buffers ? [...buffers.networkLogs] : [];
|
|
1843
|
+
} else {
|
|
1844
|
+
entries = [];
|
|
1845
|
+
for (const buffers of this.pageLogBuffers.values()) {
|
|
1846
|
+
entries.push(...buffers.networkLogs);
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
if (action.urlPattern) {
|
|
1850
|
+
entries = entries.filter((e) => e.url.includes(action.urlPattern));
|
|
1851
|
+
}
|
|
1852
|
+
return { success: true, data: entries };
|
|
1853
|
+
}
|
|
1854
|
+
case "readCookies": {
|
|
1855
|
+
const cookies = await this.context.cookies();
|
|
1856
|
+
if (action.name) {
|
|
1857
|
+
const cookie = cookies.find((c) => c.name === action.name) ?? null;
|
|
1858
|
+
return { success: true, data: cookie };
|
|
1859
|
+
}
|
|
1860
|
+
return { success: true, data: cookies };
|
|
1861
|
+
}
|
|
1862
|
+
case "setCookies": {
|
|
1863
|
+
const cookiesWithDefaults = action.cookies.map((c) => ({
|
|
1864
|
+
...c,
|
|
1865
|
+
url: !c.domain ? page.url() : void 0
|
|
1866
|
+
}));
|
|
1867
|
+
await this.context.addCookies(cookiesWithDefaults);
|
|
1868
|
+
return { success: true };
|
|
1869
|
+
}
|
|
1870
|
+
case "readLocalStorage": {
|
|
1871
|
+
const pageUrl = page.url();
|
|
1872
|
+
if (pageUrl === "about:blank") {
|
|
1873
|
+
return { success: true, data: action.key ? null : {} };
|
|
1874
|
+
}
|
|
1875
|
+
if (action.key) {
|
|
1876
|
+
const value = await page.evaluate(
|
|
1877
|
+
(k) => localStorage.getItem(k),
|
|
1878
|
+
action.key
|
|
1879
|
+
);
|
|
1880
|
+
return { success: true, data: value };
|
|
1881
|
+
}
|
|
1882
|
+
const all = await page.evaluate(
|
|
1883
|
+
() => Object.fromEntries(
|
|
1884
|
+
Object.keys(localStorage).map((k) => [k, localStorage.getItem(k)])
|
|
1885
|
+
)
|
|
1886
|
+
);
|
|
1887
|
+
return { success: true, data: all };
|
|
1888
|
+
}
|
|
1889
|
+
case "setLocalStorage": {
|
|
1890
|
+
await page.evaluate((entries) => {
|
|
1891
|
+
for (const { key, value } of entries) {
|
|
1892
|
+
localStorage.setItem(key, value);
|
|
1893
|
+
}
|
|
1894
|
+
}, action.entries);
|
|
1895
|
+
return { success: true };
|
|
1896
|
+
}
|
|
1897
|
+
case "assert": {
|
|
1898
|
+
return { success: true };
|
|
1899
|
+
}
|
|
1900
|
+
case "clearText": {
|
|
1901
|
+
const loc = resolver.resolve(action.ref);
|
|
1902
|
+
const clearBox = await resolver.getBoundingBox(action.ref);
|
|
1903
|
+
await loc.clear();
|
|
1904
|
+
return { success: true, boundingBox: clearBox ?? void 0 };
|
|
1905
|
+
}
|
|
1906
|
+
case "drag": {
|
|
1907
|
+
const fromLoc = resolver.resolve(action.fromRef);
|
|
1908
|
+
const toLoc = resolver.resolve(action.toRef);
|
|
1909
|
+
await fromLoc.dragTo(toLoc);
|
|
1910
|
+
return { success: true };
|
|
1911
|
+
}
|
|
1912
|
+
case "doubleTap": {
|
|
1913
|
+
const dblLoc = resolver.resolve(action.ref);
|
|
1914
|
+
const dblBox = await resolver.getBoundingBox(action.ref);
|
|
1915
|
+
await dblLoc.dblclick();
|
|
1916
|
+
return { success: true, boundingBox: dblBox ?? void 0 };
|
|
1917
|
+
}
|
|
1918
|
+
case "doubleClick": {
|
|
1919
|
+
const locator = resolver.resolve(action.ref);
|
|
1920
|
+
const box = await resolver.getBoundingBox(action.ref);
|
|
1921
|
+
const coordinates = box ? action.relativePosition ? { x: box.x + action.relativePosition.x, y: box.y + action.relativePosition.y } : { x: box.x + box.width / 2, y: box.y + box.height / 2 } : void 0;
|
|
1922
|
+
await locator.dblclick({
|
|
1923
|
+
...action.relativePosition && { position: action.relativePosition },
|
|
1924
|
+
...clickDelayOption2(action.clickDelay)
|
|
1925
|
+
});
|
|
1926
|
+
return { success: true, coordinates, boundingBox: box ?? void 0 };
|
|
1927
|
+
}
|
|
1928
|
+
case "rightClick": {
|
|
1929
|
+
const locator = resolver.resolve(action.ref);
|
|
1930
|
+
const box = await resolver.getBoundingBox(action.ref);
|
|
1931
|
+
const coordinates = box ? action.relativePosition ? { x: box.x + action.relativePosition.x, y: box.y + action.relativePosition.y } : { x: box.x + box.width / 2, y: box.y + box.height / 2 } : void 0;
|
|
1932
|
+
await locator.click({
|
|
1933
|
+
button: "right",
|
|
1934
|
+
...action.relativePosition && { position: action.relativePosition },
|
|
1935
|
+
...clickDelayOption2(action.clickDelay)
|
|
1936
|
+
});
|
|
1937
|
+
return { success: true, coordinates, boundingBox: box ?? void 0 };
|
|
1938
|
+
}
|
|
1939
|
+
case "openLink": {
|
|
1940
|
+
await page.goto(action.url, { waitUntil: "domcontentloaded" });
|
|
1941
|
+
await waitForPageReady(page);
|
|
1942
|
+
return { success: true };
|
|
1943
|
+
}
|
|
1944
|
+
case "newTab": {
|
|
1945
|
+
const newPage = await this.context.newPage();
|
|
1946
|
+
this.attachLogListeners(newPage);
|
|
1947
|
+
await newPage.goto(action.url, { waitUntil: "domcontentloaded" });
|
|
1948
|
+
await waitForPageReady(newPage);
|
|
1949
|
+
this.page = newPage;
|
|
1950
|
+
return { success: true };
|
|
1951
|
+
}
|
|
1952
|
+
case "switchTab": {
|
|
1953
|
+
const pages = this.context.pages();
|
|
1954
|
+
let target;
|
|
1955
|
+
if (action.index !== void 0) {
|
|
1956
|
+
if (action.index < 0 || action.index >= pages.length) {
|
|
1957
|
+
return { success: false, error: `Tab index ${action.index} out of range (${pages.length} tabs open)` };
|
|
1958
|
+
}
|
|
1959
|
+
target = pages[action.index];
|
|
1960
|
+
} else if (action.title) {
|
|
1961
|
+
for (const p of pages) {
|
|
1962
|
+
const t = await p.title();
|
|
1963
|
+
if (t.includes(action.title)) {
|
|
1964
|
+
target = p;
|
|
1965
|
+
break;
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
} else if (action.url) {
|
|
1969
|
+
target = pages.find((p) => p.url().includes(action.url));
|
|
1970
|
+
} else {
|
|
1971
|
+
return { success: false, error: "switchTab requires at least one of: index, title, or url" };
|
|
1972
|
+
}
|
|
1973
|
+
if (!target) {
|
|
1974
|
+
return { success: false, error: `No tab found matching ${JSON.stringify({ index: action.index, title: action.title, url: action.url })}` };
|
|
1975
|
+
}
|
|
1976
|
+
this.page = target;
|
|
1977
|
+
return { success: true };
|
|
1978
|
+
}
|
|
1979
|
+
case "tap":
|
|
1980
|
+
case "swipe":
|
|
1981
|
+
case "longpress":
|
|
1982
|
+
case "pinch":
|
|
1983
|
+
case "multiTap": {
|
|
1984
|
+
return { success: false, error: `${action.type} is a mobile-only gesture` };
|
|
1985
|
+
}
|
|
1986
|
+
case "tapCoordinate": {
|
|
1987
|
+
await page.mouse.click(action.x, action.y);
|
|
1988
|
+
return { success: true, coordinates: { x: action.x, y: action.y } };
|
|
1989
|
+
}
|
|
1990
|
+
case "hideKeyboard":
|
|
1991
|
+
case "launchApp":
|
|
1992
|
+
case "stopApp":
|
|
1993
|
+
case "setOrientation": {
|
|
1994
|
+
return { success: true };
|
|
1995
|
+
}
|
|
1996
|
+
default: {
|
|
1997
|
+
return { success: false, error: `Unknown action type: ${action.type}` };
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
}
|
|
2001
|
+
};
|
|
2002
|
+
|
|
2003
|
+
// src/accessibility.ts
|
|
2004
|
+
var import_playwright = require("@axe-core/playwright");
|
|
2005
|
+
async function runAccessibilityCheck(page, options) {
|
|
2006
|
+
let builder = new import_playwright.AxeBuilder({ page });
|
|
2007
|
+
if (options?.standard) {
|
|
2008
|
+
const tagMap = {
|
|
2009
|
+
"wcag2a": ["wcag2a"],
|
|
2010
|
+
"wcag2aa": ["wcag2a", "wcag2aa"],
|
|
2011
|
+
"wcag2aaa": ["wcag2a", "wcag2aa", "wcag2aaa"]
|
|
2012
|
+
};
|
|
2013
|
+
builder = builder.withTags(tagMap[options.standard] ?? ["wcag2a", "wcag2aa"]);
|
|
2014
|
+
}
|
|
2015
|
+
if (options?.disableRules?.length) {
|
|
2016
|
+
builder = builder.disableRules(options.disableRules);
|
|
2017
|
+
}
|
|
2018
|
+
if (options?.exclude?.length) {
|
|
2019
|
+
for (const selector of options.exclude) {
|
|
2020
|
+
builder = builder.exclude(selector);
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
const results = await builder.analyze();
|
|
2024
|
+
return results.violations.map((v) => ({
|
|
2025
|
+
ruleId: v.id,
|
|
2026
|
+
impact: v.impact,
|
|
2027
|
+
description: v.description,
|
|
2028
|
+
help: v.help,
|
|
2029
|
+
helpUrl: v.helpUrl,
|
|
2030
|
+
nodes: v.nodes.slice(0, 10).map((n) => ({
|
|
2031
|
+
html: n.html,
|
|
2032
|
+
target: n.target
|
|
2033
|
+
}))
|
|
2034
|
+
}));
|
|
2035
|
+
}
|
|
2036
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
2037
|
+
0 && (module.exports = {
|
|
2038
|
+
ActionValidationError,
|
|
2039
|
+
ElementResolver,
|
|
2040
|
+
FarmWebAdapter,
|
|
2041
|
+
WebPlatformAdapter,
|
|
2042
|
+
extractDom,
|
|
2043
|
+
observePage,
|
|
2044
|
+
parseAriaSnapshot,
|
|
2045
|
+
runAccessibilityCheck,
|
|
2046
|
+
validateAction,
|
|
2047
|
+
waitForPageReady
|
|
2048
|
+
});
|
|
2049
|
+
//# sourceMappingURL=index.cjs.map
|