agent-browser 0.1.2 → 0.2.0
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/AGENTS.md +26 -0
- package/README.md +68 -11
- package/benchmark/benchmark.ts +521 -0
- package/benchmark/run.ts +322 -0
- package/bin/agent-browser +0 -0
- package/dist/actions.d.ts.map +1 -1
- package/dist/actions.js +46 -35
- package/dist/actions.js.map +1 -1
- package/dist/browser.d.ts +31 -1
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +62 -2
- package/dist/browser.js.map +1 -1
- package/dist/cli-light.d.ts +11 -0
- package/dist/cli-light.d.ts.map +1 -0
- package/dist/cli-light.js +409 -0
- package/dist/cli-light.js.map +1 -0
- package/dist/index.js +13 -8
- package/dist/index.js.map +1 -1
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +4 -0
- package/dist/protocol.js.map +1 -1
- package/dist/snapshot.d.ts +63 -0
- package/dist/snapshot.d.ts.map +1 -0
- package/dist/snapshot.js +298 -0
- package/dist/snapshot.js.map +1 -0
- package/package.json +5 -4
- package/src/actions.ts +50 -36
- package/src/browser.ts +73 -2
- package/src/cli-light.ts +457 -0
- package/src/index.ts +13 -8
- package/src/protocol.ts +4 -0
- package/src/snapshot.ts +380 -0
- package/tsconfig.json +12 -3
package/dist/snapshot.js
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enhanced snapshot with element refs for deterministic element selection.
|
|
3
|
+
*
|
|
4
|
+
* This module generates accessibility snapshots with embedded refs that can be
|
|
5
|
+
* used to click/fill/interact with elements without re-querying the DOM.
|
|
6
|
+
*
|
|
7
|
+
* Example output:
|
|
8
|
+
* - heading "Example Domain" [ref=e1] [level=1]
|
|
9
|
+
* - paragraph: Some text content
|
|
10
|
+
* - button "Submit" [ref=e2]
|
|
11
|
+
* - textbox "Email" [ref=e3]
|
|
12
|
+
*
|
|
13
|
+
* Usage:
|
|
14
|
+
* agent-browser snapshot # Full snapshot
|
|
15
|
+
* agent-browser snapshot -i # Interactive elements only
|
|
16
|
+
* agent-browser snapshot --depth 3 # Limit depth
|
|
17
|
+
* agent-browser click @e2 # Click element by ref
|
|
18
|
+
*/
|
|
19
|
+
// Counter for generating refs
|
|
20
|
+
let refCounter = 0;
|
|
21
|
+
/**
|
|
22
|
+
* Reset ref counter (call at start of each snapshot)
|
|
23
|
+
*/
|
|
24
|
+
export function resetRefs() {
|
|
25
|
+
refCounter = 0;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Generate next ref ID
|
|
29
|
+
*/
|
|
30
|
+
function nextRef() {
|
|
31
|
+
return `e${++refCounter}`;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Roles that are interactive and should get refs
|
|
35
|
+
*/
|
|
36
|
+
const INTERACTIVE_ROLES = new Set([
|
|
37
|
+
'button',
|
|
38
|
+
'link',
|
|
39
|
+
'textbox',
|
|
40
|
+
'checkbox',
|
|
41
|
+
'radio',
|
|
42
|
+
'combobox',
|
|
43
|
+
'listbox',
|
|
44
|
+
'menuitem',
|
|
45
|
+
'menuitemcheckbox',
|
|
46
|
+
'menuitemradio',
|
|
47
|
+
'option',
|
|
48
|
+
'searchbox',
|
|
49
|
+
'slider',
|
|
50
|
+
'spinbutton',
|
|
51
|
+
'switch',
|
|
52
|
+
'tab',
|
|
53
|
+
'treeitem',
|
|
54
|
+
]);
|
|
55
|
+
/**
|
|
56
|
+
* Roles that provide structure/context (get refs for text extraction)
|
|
57
|
+
*/
|
|
58
|
+
const CONTENT_ROLES = new Set([
|
|
59
|
+
'heading',
|
|
60
|
+
'cell',
|
|
61
|
+
'gridcell',
|
|
62
|
+
'columnheader',
|
|
63
|
+
'rowheader',
|
|
64
|
+
'listitem',
|
|
65
|
+
'article',
|
|
66
|
+
'region',
|
|
67
|
+
'main',
|
|
68
|
+
'navigation',
|
|
69
|
+
]);
|
|
70
|
+
/**
|
|
71
|
+
* Roles that are purely structural (can be filtered in compact mode)
|
|
72
|
+
*/
|
|
73
|
+
const STRUCTURAL_ROLES = new Set([
|
|
74
|
+
'generic',
|
|
75
|
+
'group',
|
|
76
|
+
'list',
|
|
77
|
+
'table',
|
|
78
|
+
'row',
|
|
79
|
+
'rowgroup',
|
|
80
|
+
'grid',
|
|
81
|
+
'treegrid',
|
|
82
|
+
'menu',
|
|
83
|
+
'menubar',
|
|
84
|
+
'toolbar',
|
|
85
|
+
'tablist',
|
|
86
|
+
'tree',
|
|
87
|
+
'directory',
|
|
88
|
+
'document',
|
|
89
|
+
'application',
|
|
90
|
+
'presentation',
|
|
91
|
+
'none',
|
|
92
|
+
]);
|
|
93
|
+
/**
|
|
94
|
+
* Build a selector string for storing in ref map
|
|
95
|
+
*/
|
|
96
|
+
function buildSelector(role, name) {
|
|
97
|
+
if (name) {
|
|
98
|
+
const escapedName = name.replace(/"/g, '\\"');
|
|
99
|
+
return `getByRole('${role}', { name: "${escapedName}" })`;
|
|
100
|
+
}
|
|
101
|
+
return `getByRole('${role}')`;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Get enhanced snapshot with refs and optional filtering
|
|
105
|
+
*/
|
|
106
|
+
export async function getEnhancedSnapshot(page, options = {}) {
|
|
107
|
+
resetRefs();
|
|
108
|
+
const refs = {};
|
|
109
|
+
// Get ARIA snapshot from Playwright
|
|
110
|
+
const locator = options.selector ? page.locator(options.selector) : page.locator(':root');
|
|
111
|
+
const ariaTree = await locator.ariaSnapshot();
|
|
112
|
+
if (!ariaTree) {
|
|
113
|
+
return {
|
|
114
|
+
tree: '(empty)',
|
|
115
|
+
refs: {},
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
// Parse and enhance the ARIA tree
|
|
119
|
+
const enhancedTree = processAriaTree(ariaTree, refs, options);
|
|
120
|
+
return { tree: enhancedTree, refs };
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Process ARIA snapshot: add refs and apply filters
|
|
124
|
+
*/
|
|
125
|
+
function processAriaTree(ariaTree, refs, options) {
|
|
126
|
+
const lines = ariaTree.split('\n');
|
|
127
|
+
const result = [];
|
|
128
|
+
// For interactive-only mode, we collect just interactive elements
|
|
129
|
+
if (options.interactive) {
|
|
130
|
+
for (const line of lines) {
|
|
131
|
+
const match = line.match(/^(\s*-\s*)(\w+)(?:\s+"([^"]*)")?(.*)$/);
|
|
132
|
+
if (!match)
|
|
133
|
+
continue;
|
|
134
|
+
const [, , role, name, suffix] = match;
|
|
135
|
+
const roleLower = role.toLowerCase();
|
|
136
|
+
if (INTERACTIVE_ROLES.has(roleLower)) {
|
|
137
|
+
const ref = nextRef();
|
|
138
|
+
refs[ref] = {
|
|
139
|
+
selector: buildSelector(roleLower, name),
|
|
140
|
+
role: roleLower,
|
|
141
|
+
name,
|
|
142
|
+
};
|
|
143
|
+
let enhanced = `- ${role}`;
|
|
144
|
+
if (name)
|
|
145
|
+
enhanced += ` "${name}"`;
|
|
146
|
+
enhanced += ` [ref=${ref}]`;
|
|
147
|
+
if (suffix && suffix.includes('['))
|
|
148
|
+
enhanced += suffix;
|
|
149
|
+
result.push(enhanced);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return result.join('\n') || '(no interactive elements)';
|
|
153
|
+
}
|
|
154
|
+
// Normal processing with depth/compact filters
|
|
155
|
+
for (const line of lines) {
|
|
156
|
+
const processed = processLine(line, refs, options);
|
|
157
|
+
if (processed !== null) {
|
|
158
|
+
result.push(processed);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
// If compact mode, remove empty structural elements
|
|
162
|
+
if (options.compact) {
|
|
163
|
+
return compactTree(result.join('\n'));
|
|
164
|
+
}
|
|
165
|
+
return result.join('\n');
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Get indentation level (number of spaces / 2)
|
|
169
|
+
*/
|
|
170
|
+
function getIndentLevel(line) {
|
|
171
|
+
const match = line.match(/^(\s*)/);
|
|
172
|
+
return match ? Math.floor(match[1].length / 2) : 0;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Process a single line: add ref if needed, filter if requested
|
|
176
|
+
*/
|
|
177
|
+
function processLine(line, refs, options) {
|
|
178
|
+
const depth = getIndentLevel(line);
|
|
179
|
+
// Check max depth
|
|
180
|
+
if (options.maxDepth !== undefined && depth > options.maxDepth) {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
// Match lines like:
|
|
184
|
+
// - button "Submit"
|
|
185
|
+
// - heading "Title" [level=1]
|
|
186
|
+
// - link "Click me":
|
|
187
|
+
const match = line.match(/^(\s*-\s*)(\w+)(?:\s+"([^"]*)")?(.*)$/);
|
|
188
|
+
if (!match) {
|
|
189
|
+
// Metadata lines (like /url:) or text content
|
|
190
|
+
if (options.interactive) {
|
|
191
|
+
// In interactive mode, only keep metadata under interactive elements
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
return line;
|
|
195
|
+
}
|
|
196
|
+
const [, prefix, role, name, suffix] = match;
|
|
197
|
+
const roleLower = role.toLowerCase();
|
|
198
|
+
// Skip metadata lines (like /url:)
|
|
199
|
+
if (role.startsWith('/')) {
|
|
200
|
+
return line;
|
|
201
|
+
}
|
|
202
|
+
const isInteractive = INTERACTIVE_ROLES.has(roleLower);
|
|
203
|
+
const isContent = CONTENT_ROLES.has(roleLower);
|
|
204
|
+
const isStructural = STRUCTURAL_ROLES.has(roleLower);
|
|
205
|
+
// In interactive-only mode, filter non-interactive elements
|
|
206
|
+
if (options.interactive && !isInteractive) {
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
// In compact mode, skip unnamed structural elements
|
|
210
|
+
if (options.compact && isStructural && !name) {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
// Add ref for interactive or named content elements
|
|
214
|
+
const shouldHaveRef = isInteractive || (isContent && name);
|
|
215
|
+
if (shouldHaveRef) {
|
|
216
|
+
const ref = nextRef();
|
|
217
|
+
refs[ref] = {
|
|
218
|
+
selector: buildSelector(roleLower, name),
|
|
219
|
+
role: roleLower,
|
|
220
|
+
name,
|
|
221
|
+
};
|
|
222
|
+
// Build enhanced line with ref
|
|
223
|
+
let enhanced = `${prefix}${role}`;
|
|
224
|
+
if (name)
|
|
225
|
+
enhanced += ` "${name}"`;
|
|
226
|
+
enhanced += ` [ref=${ref}]`;
|
|
227
|
+
if (suffix)
|
|
228
|
+
enhanced += suffix;
|
|
229
|
+
return enhanced;
|
|
230
|
+
}
|
|
231
|
+
return line;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Remove empty structural branches in compact mode
|
|
235
|
+
*/
|
|
236
|
+
function compactTree(tree) {
|
|
237
|
+
const lines = tree.split('\n');
|
|
238
|
+
const result = [];
|
|
239
|
+
// Simple pass: keep lines that have content or refs
|
|
240
|
+
for (let i = 0; i < lines.length; i++) {
|
|
241
|
+
const line = lines[i];
|
|
242
|
+
// Always keep lines with refs
|
|
243
|
+
if (line.includes('[ref=')) {
|
|
244
|
+
result.push(line);
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
// Keep lines with text content (after :)
|
|
248
|
+
if (line.includes(':') && !line.endsWith(':')) {
|
|
249
|
+
result.push(line);
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
// Check if this structural element has children with refs
|
|
253
|
+
const currentIndent = getIndentLevel(line);
|
|
254
|
+
let hasRelevantChildren = false;
|
|
255
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
256
|
+
const childIndent = getIndentLevel(lines[j]);
|
|
257
|
+
if (childIndent <= currentIndent)
|
|
258
|
+
break;
|
|
259
|
+
if (lines[j].includes('[ref=')) {
|
|
260
|
+
hasRelevantChildren = true;
|
|
261
|
+
break;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
if (hasRelevantChildren) {
|
|
265
|
+
result.push(line);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return result.join('\n');
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Parse a ref from command argument (e.g., "@e1" -> "e1")
|
|
272
|
+
*/
|
|
273
|
+
export function parseRef(arg) {
|
|
274
|
+
if (arg.startsWith('@')) {
|
|
275
|
+
return arg.slice(1);
|
|
276
|
+
}
|
|
277
|
+
if (arg.startsWith('ref=')) {
|
|
278
|
+
return arg.slice(4);
|
|
279
|
+
}
|
|
280
|
+
if (/^e\d+$/.test(arg)) {
|
|
281
|
+
return arg;
|
|
282
|
+
}
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Get snapshot statistics
|
|
287
|
+
*/
|
|
288
|
+
export function getSnapshotStats(tree, refs) {
|
|
289
|
+
const interactive = Object.values(refs).filter(r => INTERACTIVE_ROLES.has(r.role)).length;
|
|
290
|
+
return {
|
|
291
|
+
lines: tree.split('\n').length,
|
|
292
|
+
chars: tree.length,
|
|
293
|
+
tokens: Math.ceil(tree.length / 4),
|
|
294
|
+
refs: Object.keys(refs).length,
|
|
295
|
+
interactive,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
//# sourceMappingURL=snapshot.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"snapshot.js","sourceRoot":"","sources":["../src/snapshot.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-browser",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Headless browser automation CLI for AI agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
"agent-browser": "./bin/agent-browser"
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
|
-
"build": "tsc",
|
|
11
|
+
"build": "tsc && bun build ./src/cli-light.ts --compile --outfile ./bin/agent-browser",
|
|
12
|
+
"build:node": "tsc",
|
|
12
13
|
"start": "node dist/index.js",
|
|
13
14
|
"dev": "tsx src/index.ts",
|
|
14
15
|
"typecheck": "tsc --noEmit",
|
|
@@ -29,12 +30,12 @@
|
|
|
29
30
|
"author": "",
|
|
30
31
|
"license": "Apache-2.0",
|
|
31
32
|
"dependencies": {
|
|
32
|
-
"playwright-core": "^1.
|
|
33
|
+
"playwright-core": "^1.57.0",
|
|
33
34
|
"zod": "^3.22.4"
|
|
34
35
|
},
|
|
35
36
|
"devDependencies": {
|
|
36
37
|
"@types/node": "^20.10.0",
|
|
37
|
-
"playwright": "^1.
|
|
38
|
+
"playwright": "^1.57.0",
|
|
38
39
|
"prettier": "^3.7.4",
|
|
39
40
|
"tsx": "^4.6.0",
|
|
40
41
|
"typescript": "^5.3.0",
|
package/src/actions.ts
CHANGED
|
@@ -108,6 +108,7 @@ import { successResponse, errorResponse } from './protocol.js';
|
|
|
108
108
|
// Snapshot response type
|
|
109
109
|
interface SnapshotData {
|
|
110
110
|
snapshot: string;
|
|
111
|
+
refs?: Record<string, { role: string; name?: string }>;
|
|
111
112
|
}
|
|
112
113
|
|
|
113
114
|
/**
|
|
@@ -380,8 +381,10 @@ async function handleNavigate(
|
|
|
380
381
|
}
|
|
381
382
|
|
|
382
383
|
async function handleClick(command: ClickCommand, browser: BrowserManager): Promise<Response> {
|
|
383
|
-
|
|
384
|
-
|
|
384
|
+
// Support both refs (@e1) and regular selectors
|
|
385
|
+
const locator = browser.getLocator(command.selector);
|
|
386
|
+
|
|
387
|
+
await locator.click({
|
|
385
388
|
button: command.button,
|
|
386
389
|
clickCount: command.clickCount,
|
|
387
390
|
delay: command.delay,
|
|
@@ -391,13 +394,13 @@ async function handleClick(command: ClickCommand, browser: BrowserManager): Prom
|
|
|
391
394
|
}
|
|
392
395
|
|
|
393
396
|
async function handleType(command: TypeCommand, browser: BrowserManager): Promise<Response> {
|
|
394
|
-
const
|
|
397
|
+
const locator = browser.getLocator(command.selector);
|
|
395
398
|
|
|
396
399
|
if (command.clear) {
|
|
397
|
-
await
|
|
400
|
+
await locator.fill('');
|
|
398
401
|
}
|
|
399
402
|
|
|
400
|
-
await
|
|
403
|
+
await locator.pressSequentially(command.text, {
|
|
401
404
|
delay: command.delay,
|
|
402
405
|
});
|
|
403
406
|
|
|
@@ -446,15 +449,26 @@ async function handleScreenshot(
|
|
|
446
449
|
}
|
|
447
450
|
|
|
448
451
|
async function handleSnapshot(
|
|
449
|
-
command: Command & { action: 'snapshot' },
|
|
452
|
+
command: Command & { action: 'snapshot'; interactive?: boolean; maxDepth?: number; compact?: boolean; selector?: string },
|
|
450
453
|
browser: BrowserManager
|
|
451
454
|
): Promise<Response<SnapshotData>> {
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
+
// Use enhanced snapshot with refs and optional filtering
|
|
456
|
+
const { tree, refs } = await browser.getSnapshot({
|
|
457
|
+
interactive: command.interactive,
|
|
458
|
+
maxDepth: command.maxDepth,
|
|
459
|
+
compact: command.compact,
|
|
460
|
+
selector: command.selector,
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
// Simplify refs for output (just role and name)
|
|
464
|
+
const simpleRefs: Record<string, { role: string; name?: string }> = {};
|
|
465
|
+
for (const [ref, data] of Object.entries(refs)) {
|
|
466
|
+
simpleRefs[ref] = { role: data.role, name: data.name };
|
|
467
|
+
}
|
|
455
468
|
|
|
456
469
|
return successResponse(command.id, {
|
|
457
|
-
snapshot:
|
|
470
|
+
snapshot: tree || 'Empty page',
|
|
471
|
+
refs: Object.keys(simpleRefs).length > 0 ? simpleRefs : undefined,
|
|
458
472
|
});
|
|
459
473
|
}
|
|
460
474
|
|
|
@@ -533,17 +547,17 @@ async function handleScroll(command: ScrollCommand, browser: BrowserManager): Pr
|
|
|
533
547
|
}
|
|
534
548
|
|
|
535
549
|
async function handleSelect(command: SelectCommand, browser: BrowserManager): Promise<Response> {
|
|
536
|
-
const
|
|
550
|
+
const locator = browser.getLocator(command.selector);
|
|
537
551
|
const values = Array.isArray(command.values) ? command.values : [command.values];
|
|
538
552
|
|
|
539
|
-
await
|
|
553
|
+
await locator.selectOption(values);
|
|
540
554
|
|
|
541
555
|
return successResponse(command.id, { selected: values });
|
|
542
556
|
}
|
|
543
557
|
|
|
544
558
|
async function handleHover(command: HoverCommand, browser: BrowserManager): Promise<Response> {
|
|
545
|
-
const
|
|
546
|
-
await
|
|
559
|
+
const locator = browser.getLocator(command.selector);
|
|
560
|
+
await locator.hover();
|
|
547
561
|
|
|
548
562
|
return successResponse(command.id, { hovered: true });
|
|
549
563
|
}
|
|
@@ -622,27 +636,27 @@ async function handleWindowNew(
|
|
|
622
636
|
// New handlers for enhanced Playwright parity
|
|
623
637
|
|
|
624
638
|
async function handleFill(command: FillCommand, browser: BrowserManager): Promise<Response> {
|
|
625
|
-
const
|
|
626
|
-
await
|
|
639
|
+
const locator = browser.getLocator(command.selector);
|
|
640
|
+
await locator.fill(command.value);
|
|
627
641
|
return successResponse(command.id, { filled: true });
|
|
628
642
|
}
|
|
629
643
|
|
|
630
644
|
async function handleCheck(command: CheckCommand, browser: BrowserManager): Promise<Response> {
|
|
631
|
-
const
|
|
632
|
-
await
|
|
645
|
+
const locator = browser.getLocator(command.selector);
|
|
646
|
+
await locator.check();
|
|
633
647
|
return successResponse(command.id, { checked: true });
|
|
634
648
|
}
|
|
635
649
|
|
|
636
650
|
async function handleUncheck(command: UncheckCommand, browser: BrowserManager): Promise<Response> {
|
|
637
|
-
const
|
|
638
|
-
await
|
|
651
|
+
const locator = browser.getLocator(command.selector);
|
|
652
|
+
await locator.uncheck();
|
|
639
653
|
return successResponse(command.id, { unchecked: true });
|
|
640
654
|
}
|
|
641
655
|
|
|
642
656
|
async function handleUpload(command: UploadCommand, browser: BrowserManager): Promise<Response> {
|
|
643
|
-
const
|
|
657
|
+
const locator = browser.getLocator(command.selector);
|
|
644
658
|
const files = Array.isArray(command.files) ? command.files : [command.files];
|
|
645
|
-
await
|
|
659
|
+
await locator.setInputFiles(files);
|
|
646
660
|
return successResponse(command.id, { uploaded: files });
|
|
647
661
|
}
|
|
648
662
|
|
|
@@ -650,14 +664,14 @@ async function handleDoubleClick(
|
|
|
650
664
|
command: DoubleClickCommand,
|
|
651
665
|
browser: BrowserManager
|
|
652
666
|
): Promise<Response> {
|
|
653
|
-
const
|
|
654
|
-
await
|
|
667
|
+
const locator = browser.getLocator(command.selector);
|
|
668
|
+
await locator.dblclick();
|
|
655
669
|
return successResponse(command.id, { clicked: true });
|
|
656
670
|
}
|
|
657
671
|
|
|
658
672
|
async function handleFocus(command: FocusCommand, browser: BrowserManager): Promise<Response> {
|
|
659
|
-
const
|
|
660
|
-
await
|
|
673
|
+
const locator = browser.getLocator(command.selector);
|
|
674
|
+
await locator.focus();
|
|
661
675
|
return successResponse(command.id, { focused: true });
|
|
662
676
|
}
|
|
663
677
|
|
|
@@ -1017,14 +1031,14 @@ async function handleGetAttribute(
|
|
|
1017
1031
|
command: GetAttributeCommand,
|
|
1018
1032
|
browser: BrowserManager
|
|
1019
1033
|
): Promise<Response> {
|
|
1020
|
-
const
|
|
1021
|
-
const value = await
|
|
1034
|
+
const locator = browser.getLocator(command.selector);
|
|
1035
|
+
const value = await locator.getAttribute(command.attribute);
|
|
1022
1036
|
return successResponse(command.id, { attribute: command.attribute, value });
|
|
1023
1037
|
}
|
|
1024
1038
|
|
|
1025
1039
|
async function handleGetText(command: GetTextCommand, browser: BrowserManager): Promise<Response> {
|
|
1026
|
-
const
|
|
1027
|
-
const text = await
|
|
1040
|
+
const locator = browser.getLocator(command.selector);
|
|
1041
|
+
const text = await locator.textContent();
|
|
1028
1042
|
return successResponse(command.id, { text });
|
|
1029
1043
|
}
|
|
1030
1044
|
|
|
@@ -1032,8 +1046,8 @@ async function handleIsVisible(
|
|
|
1032
1046
|
command: IsVisibleCommand,
|
|
1033
1047
|
browser: BrowserManager
|
|
1034
1048
|
): Promise<Response> {
|
|
1035
|
-
const
|
|
1036
|
-
const visible = await
|
|
1049
|
+
const locator = browser.getLocator(command.selector);
|
|
1050
|
+
const visible = await locator.isVisible();
|
|
1037
1051
|
return successResponse(command.id, { visible });
|
|
1038
1052
|
}
|
|
1039
1053
|
|
|
@@ -1041,8 +1055,8 @@ async function handleIsEnabled(
|
|
|
1041
1055
|
command: IsEnabledCommand,
|
|
1042
1056
|
browser: BrowserManager
|
|
1043
1057
|
): Promise<Response> {
|
|
1044
|
-
const
|
|
1045
|
-
const enabled = await
|
|
1058
|
+
const locator = browser.getLocator(command.selector);
|
|
1059
|
+
const enabled = await locator.isEnabled();
|
|
1046
1060
|
return successResponse(command.id, { enabled });
|
|
1047
1061
|
}
|
|
1048
1062
|
|
|
@@ -1050,8 +1064,8 @@ async function handleIsChecked(
|
|
|
1050
1064
|
command: IsCheckedCommand,
|
|
1051
1065
|
browser: BrowserManager
|
|
1052
1066
|
): Promise<Response> {
|
|
1053
|
-
const
|
|
1054
|
-
const checked = await
|
|
1067
|
+
const locator = browser.getLocator(command.selector);
|
|
1068
|
+
const checked = await locator.isChecked();
|
|
1055
1069
|
return successResponse(command.id, { checked });
|
|
1056
1070
|
}
|
|
1057
1071
|
|
package/src/browser.ts
CHANGED
|
@@ -10,8 +10,10 @@ import {
|
|
|
10
10
|
type Dialog,
|
|
11
11
|
type Request,
|
|
12
12
|
type Route,
|
|
13
|
+
type Locator,
|
|
13
14
|
} from 'playwright-core';
|
|
14
15
|
import type { LaunchCommand } from './types.js';
|
|
16
|
+
import { type RefMap, type EnhancedSnapshot, getEnhancedSnapshot, parseRef } from './snapshot.js';
|
|
15
17
|
|
|
16
18
|
interface TrackedRequest {
|
|
17
19
|
url: string;
|
|
@@ -47,6 +49,8 @@ export class BrowserManager {
|
|
|
47
49
|
private consoleMessages: ConsoleMessage[] = [];
|
|
48
50
|
private pageErrors: PageError[] = [];
|
|
49
51
|
private isRecordingHar: boolean = false;
|
|
52
|
+
private refMap: RefMap = {};
|
|
53
|
+
private lastSnapshot: string = '';
|
|
50
54
|
|
|
51
55
|
/**
|
|
52
56
|
* Check if browser is launched
|
|
@@ -55,6 +59,70 @@ export class BrowserManager {
|
|
|
55
59
|
return this.browser !== null;
|
|
56
60
|
}
|
|
57
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Get enhanced snapshot with refs and cache the ref map
|
|
64
|
+
*/
|
|
65
|
+
async getSnapshot(options?: {
|
|
66
|
+
interactive?: boolean;
|
|
67
|
+
maxDepth?: number;
|
|
68
|
+
compact?: boolean;
|
|
69
|
+
selector?: string;
|
|
70
|
+
}): Promise<EnhancedSnapshot> {
|
|
71
|
+
const page = this.getPage();
|
|
72
|
+
const snapshot = await getEnhancedSnapshot(page, options);
|
|
73
|
+
this.refMap = snapshot.refs;
|
|
74
|
+
this.lastSnapshot = snapshot.tree;
|
|
75
|
+
return snapshot;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Get the cached ref map from last snapshot
|
|
80
|
+
*/
|
|
81
|
+
getRefMap(): RefMap {
|
|
82
|
+
return this.refMap;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Get a locator from a ref (e.g., "e1", "@e1", "ref=e1")
|
|
87
|
+
* Returns null if ref doesn't exist or is invalid
|
|
88
|
+
*/
|
|
89
|
+
getLocatorFromRef(refArg: string): Locator | null {
|
|
90
|
+
const ref = parseRef(refArg);
|
|
91
|
+
if (!ref) return null;
|
|
92
|
+
|
|
93
|
+
const refData = this.refMap[ref];
|
|
94
|
+
if (!refData) return null;
|
|
95
|
+
|
|
96
|
+
const page = this.getPage();
|
|
97
|
+
|
|
98
|
+
// Parse the selector and create locator
|
|
99
|
+
if (refData.name) {
|
|
100
|
+
return page.getByRole(refData.role as any, { name: refData.name });
|
|
101
|
+
} else {
|
|
102
|
+
return page.getByRole(refData.role as any);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Check if a selector looks like a ref
|
|
108
|
+
*/
|
|
109
|
+
isRef(selector: string): boolean {
|
|
110
|
+
return parseRef(selector) !== null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Get locator - supports both refs and regular selectors
|
|
115
|
+
*/
|
|
116
|
+
getLocator(selectorOrRef: string): Locator {
|
|
117
|
+
// Check if it's a ref first
|
|
118
|
+
const locator = this.getLocatorFromRef(selectorOrRef);
|
|
119
|
+
if (locator) return locator;
|
|
120
|
+
|
|
121
|
+
// Otherwise treat as regular selector
|
|
122
|
+
const page = this.getPage();
|
|
123
|
+
return page.locator(selectorOrRef);
|
|
124
|
+
}
|
|
125
|
+
|
|
58
126
|
/**
|
|
59
127
|
* Get the current active page, throws if not launched
|
|
60
128
|
*/
|
|
@@ -428,11 +496,12 @@ export class BrowserManager {
|
|
|
428
496
|
|
|
429
497
|
/**
|
|
430
498
|
* Launch the browser with the specified options
|
|
499
|
+
* If already launched, this is a no-op (browser stays open)
|
|
431
500
|
*/
|
|
432
501
|
async launch(options: LaunchCommand): Promise<void> {
|
|
433
|
-
//
|
|
502
|
+
// If already launched, don't relaunch
|
|
434
503
|
if (this.browser) {
|
|
435
|
-
|
|
504
|
+
return;
|
|
436
505
|
}
|
|
437
506
|
|
|
438
507
|
// Select browser type
|
|
@@ -611,5 +680,7 @@ export class BrowserManager {
|
|
|
611
680
|
}
|
|
612
681
|
|
|
613
682
|
this.activePageIndex = 0;
|
|
683
|
+
this.refMap = {};
|
|
684
|
+
this.lastSnapshot = '';
|
|
614
685
|
}
|
|
615
686
|
}
|