@browserflow-ai/exploration 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/claude-cli.d.ts +57 -0
- package/dist/adapters/claude-cli.d.ts.map +1 -0
- package/dist/adapters/claude-cli.js +195 -0
- package/dist/adapters/claude-cli.js.map +1 -0
- package/dist/adapters/claude.d.ts +54 -0
- package/dist/adapters/claude.d.ts.map +1 -0
- package/dist/adapters/claude.js +160 -0
- package/dist/adapters/claude.js.map +1 -0
- package/dist/adapters/index.d.ts +6 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +4 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/types.d.ts +196 -0
- package/dist/adapters/types.d.ts.map +1 -0
- package/dist/adapters/types.js +3 -0
- package/dist/adapters/types.js.map +1 -0
- package/dist/agent-browser-session.d.ts +62 -0
- package/dist/agent-browser-session.d.ts.map +1 -0
- package/dist/agent-browser-session.js +272 -0
- package/dist/agent-browser-session.js.map +1 -0
- package/dist/evidence.d.ts +111 -0
- package/dist/evidence.d.ts.map +1 -0
- package/dist/evidence.js +144 -0
- package/dist/evidence.js.map +1 -0
- package/dist/explorer.d.ts +180 -0
- package/dist/explorer.d.ts.map +1 -0
- package/dist/explorer.js +393 -0
- package/dist/explorer.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/locator-candidates.d.ts +127 -0
- package/dist/locator-candidates.d.ts.map +1 -0
- package/dist/locator-candidates.js +358 -0
- package/dist/locator-candidates.js.map +1 -0
- package/dist/step-executor.d.ts +99 -0
- package/dist/step-executor.d.ts.map +1 -0
- package/dist/step-executor.js +646 -0
- package/dist/step-executor.js.map +1 -0
- package/package.json +34 -0
- package/src/adapters/claude-cli.test.ts +134 -0
- package/src/adapters/claude-cli.ts +240 -0
- package/src/adapters/claude.test.ts +195 -0
- package/src/adapters/claude.ts +190 -0
- package/src/adapters/index.ts +21 -0
- package/src/adapters/types.ts +207 -0
- package/src/agent-browser-session.test.ts +369 -0
- package/src/agent-browser-session.ts +349 -0
- package/src/evidence.test.ts +239 -0
- package/src/evidence.ts +203 -0
- package/src/explorer.test.ts +321 -0
- package/src/explorer.ts +565 -0
- package/src/index.ts +51 -0
- package/src/locator-candidates.test.ts +602 -0
- package/src/locator-candidates.ts +441 -0
- package/src/step-executor.test.ts +696 -0
- package/src/step-executor.ts +783 -0
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for AgentBrowserSession adapter
|
|
3
|
+
*
|
|
4
|
+
* These tests verify that the BrowserSession adapter correctly wraps
|
|
5
|
+
* the agent-browser BrowserManager API.
|
|
6
|
+
*
|
|
7
|
+
* NOTE: These are integration tests requiring real browser binaries.
|
|
8
|
+
* Skipped by default - run with BROWSER_TESTS=1 to enable.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
|
|
12
|
+
import { AgentBrowserSession } from './agent-browser-session';
|
|
13
|
+
import type { BrowserSession } from './explorer';
|
|
14
|
+
|
|
15
|
+
// Skip browser integration tests unless explicitly enabled
|
|
16
|
+
const describeBrowser = process.env.BROWSER_TESTS ? describe : describe.skip;
|
|
17
|
+
|
|
18
|
+
describeBrowser('AgentBrowserSession', () => {
|
|
19
|
+
let session: BrowserSession;
|
|
20
|
+
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
session = new AgentBrowserSession();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
afterEach(async () => {
|
|
26
|
+
if (session.isLaunched()) {
|
|
27
|
+
await session.close();
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('Lifecycle', () => {
|
|
32
|
+
test('isLaunched returns false before launch', () => {
|
|
33
|
+
expect(session.isLaunched()).toBe(false);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('launch initializes browser with default options', async () => {
|
|
37
|
+
await session.launch();
|
|
38
|
+
expect(session.isLaunched()).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('launch accepts custom viewport', async () => {
|
|
42
|
+
await session.launch({
|
|
43
|
+
viewport: { width: 1920, height: 1080 },
|
|
44
|
+
});
|
|
45
|
+
expect(session.isLaunched()).toBe(true);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('launch accepts headless option', async () => {
|
|
49
|
+
await session.launch({
|
|
50
|
+
headless: true,
|
|
51
|
+
});
|
|
52
|
+
expect(session.isLaunched()).toBe(true);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('close terminates browser session', async () => {
|
|
56
|
+
await session.launch();
|
|
57
|
+
await session.close();
|
|
58
|
+
expect(session.isLaunched()).toBe(false);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('close is idempotent', async () => {
|
|
62
|
+
await session.launch();
|
|
63
|
+
await session.close();
|
|
64
|
+
await session.close(); // Should not throw
|
|
65
|
+
expect(session.isLaunched()).toBe(false);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe('Navigation', () => {
|
|
70
|
+
beforeEach(async () => {
|
|
71
|
+
await session.launch();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('navigate loads a URL', async () => {
|
|
75
|
+
await session.navigate('https://example.com');
|
|
76
|
+
const url = session.getCurrentURL?.();
|
|
77
|
+
expect(url).toBe('https://example.com/');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('back navigates to previous page', async () => {
|
|
81
|
+
if (!session.back) {
|
|
82
|
+
throw new Error('back method not implemented');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
await session.navigate('https://example.com');
|
|
86
|
+
await session.navigate('https://example.com/page2');
|
|
87
|
+
await session.back();
|
|
88
|
+
|
|
89
|
+
const url = session.getCurrentURL?.();
|
|
90
|
+
expect(url).toBe('https://example.com/');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('forward navigates to next page', async () => {
|
|
94
|
+
if (!session.forward) {
|
|
95
|
+
throw new Error('forward method not implemented');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
await session.navigate('https://example.com');
|
|
99
|
+
await session.navigate('https://example.com/page2');
|
|
100
|
+
await session.back?.();
|
|
101
|
+
await session.forward();
|
|
102
|
+
|
|
103
|
+
const url = session.getCurrentURL?.();
|
|
104
|
+
expect(url).toContain('page2');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('refresh reloads current page', async () => {
|
|
108
|
+
if (!session.refresh) {
|
|
109
|
+
throw new Error('refresh method not implemented');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
await session.navigate('https://example.com');
|
|
113
|
+
await session.refresh();
|
|
114
|
+
|
|
115
|
+
const url = session.getCurrentURL?.();
|
|
116
|
+
expect(url).toBe('https://example.com/');
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe('Screenshots', () => {
|
|
121
|
+
beforeEach(async () => {
|
|
122
|
+
await session.launch();
|
|
123
|
+
await session.navigate('https://example.com');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('screenshot returns buffer', async () => {
|
|
127
|
+
const buffer = await session.screenshot();
|
|
128
|
+
expect(buffer).toBeInstanceOf(Buffer);
|
|
129
|
+
expect(buffer.length).toBeGreaterThan(0);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('screenshot captures valid PNG', async () => {
|
|
133
|
+
const buffer = await session.screenshot();
|
|
134
|
+
// PNG signature: 89 50 4E 47
|
|
135
|
+
expect(buffer[0]).toBe(0x89);
|
|
136
|
+
expect(buffer[1]).toBe(0x50);
|
|
137
|
+
expect(buffer[2]).toBe(0x4e);
|
|
138
|
+
expect(buffer[3]).toBe(0x47);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe('Snapshots', () => {
|
|
143
|
+
beforeEach(async () => {
|
|
144
|
+
await session.launch();
|
|
145
|
+
await session.navigate('https://example.com');
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test('getSnapshot returns enhanced snapshot', async () => {
|
|
149
|
+
const snapshot = await session.getSnapshot();
|
|
150
|
+
expect(snapshot).toHaveProperty('tree');
|
|
151
|
+
expect(snapshot).toHaveProperty('refs');
|
|
152
|
+
expect(typeof snapshot.tree).toBe('string');
|
|
153
|
+
expect(typeof snapshot.refs).toBe('object');
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test('getSnapshot with interactive=true includes refs', async () => {
|
|
157
|
+
const snapshot = await session.getSnapshot({ interactive: true });
|
|
158
|
+
expect(snapshot.refs).toBeDefined();
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test('getSnapshot with selector filters elements', async () => {
|
|
162
|
+
const snapshot = await session.getSnapshot({ selector: 'h1' });
|
|
163
|
+
expect(snapshot.tree).toContain('heading');
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
describe('Interaction Methods', () => {
|
|
168
|
+
beforeEach(async () => {
|
|
169
|
+
await session.launch();
|
|
170
|
+
// Navigate to a page with form elements for testing
|
|
171
|
+
await session.navigate('data:text/html,<html><body><button id="btn">Click</button><input id="inp" /></body></html>');
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test('click performs click action', async () => {
|
|
175
|
+
if (!session.click) {
|
|
176
|
+
throw new Error('click method not implemented');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// This should not throw
|
|
180
|
+
await session.click('#btn');
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test('fill populates input field', async () => {
|
|
184
|
+
if (!session.fill) {
|
|
185
|
+
throw new Error('fill method not implemented');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
await session.fill('#inp', 'test value');
|
|
189
|
+
// If we had a way to verify the value, we would check it here
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test('type enters text character by character', async () => {
|
|
193
|
+
if (!session.type) {
|
|
194
|
+
throw new Error('type method not implemented');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
await session.type('#inp', 'typing test');
|
|
198
|
+
// Type should work like fill but character-by-character
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test('press sends keyboard event', async () => {
|
|
202
|
+
if (!session.press) {
|
|
203
|
+
throw new Error('press method not implemented');
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
await session.press('Enter');
|
|
207
|
+
// Should not throw
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test('check toggles checkbox', async () => {
|
|
211
|
+
if (!session.check) {
|
|
212
|
+
throw new Error('check method not implemented');
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Navigate to page with checkbox
|
|
216
|
+
await session.navigate('data:text/html,<html><body><input type="checkbox" id="chk" /></body></html>');
|
|
217
|
+
await session.check('#chk', true);
|
|
218
|
+
// Should not throw
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test('select chooses option from dropdown', async () => {
|
|
222
|
+
if (!session.select) {
|
|
223
|
+
throw new Error('select method not implemented');
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Navigate to page with select
|
|
227
|
+
await session.navigate('data:text/html,<html><body><select id="sel"><option value="1">One</option></select></body></html>');
|
|
228
|
+
await session.select('#sel', '1');
|
|
229
|
+
// Should not throw
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
describe('Wait Methods', () => {
|
|
234
|
+
beforeEach(async () => {
|
|
235
|
+
await session.launch();
|
|
236
|
+
await session.navigate('https://example.com');
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
test('waitForSelector waits for element', async () => {
|
|
240
|
+
if (!session.waitForSelector) {
|
|
241
|
+
throw new Error('waitForSelector method not implemented');
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
await session.waitForSelector('body', 5000);
|
|
245
|
+
// Should not throw or timeout
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test('waitForURL waits for URL pattern', async () => {
|
|
249
|
+
if (!session.waitForURL) {
|
|
250
|
+
throw new Error('waitForURL method not implemented');
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
await session.waitForURL('**/example.com/**', 5000);
|
|
254
|
+
// Should not throw
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test('waitForText waits for text content', async () => {
|
|
258
|
+
if (!session.waitForText) {
|
|
259
|
+
throw new Error('waitForText method not implemented');
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
await session.waitForText('Example Domain', 5000);
|
|
263
|
+
// Should not throw
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test('waitForLoadState waits for page load', async () => {
|
|
267
|
+
if (!session.waitForLoadState) {
|
|
268
|
+
throw new Error('waitForLoadState method not implemented');
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
await session.waitForLoadState('load');
|
|
272
|
+
// Should not throw
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test('waitForTimeout delays execution', async () => {
|
|
276
|
+
if (!session.waitForTimeout) {
|
|
277
|
+
throw new Error('waitForTimeout method not implemented');
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const start = Date.now();
|
|
281
|
+
await session.waitForTimeout(100);
|
|
282
|
+
const elapsed = Date.now() - start;
|
|
283
|
+
expect(elapsed).toBeGreaterThanOrEqual(100);
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
describe('Scroll Methods', () => {
|
|
288
|
+
beforeEach(async () => {
|
|
289
|
+
await session.launch();
|
|
290
|
+
// Navigate to a tall page for scrolling
|
|
291
|
+
await session.navigate('data:text/html,<html><body style="height:3000px"><div id="bottom" style="position:absolute;top:2500px">Bottom</div></body></html>');
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
test('scrollIntoView scrolls element into viewport', async () => {
|
|
295
|
+
if (!session.scrollIntoView) {
|
|
296
|
+
throw new Error('scrollIntoView method not implemented');
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
await session.scrollIntoView('#bottom');
|
|
300
|
+
// Should not throw
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
test('scroll moves viewport by coordinates', async () => {
|
|
304
|
+
if (!session.scroll) {
|
|
305
|
+
throw new Error('scroll method not implemented');
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
await session.scroll(0, 1000);
|
|
309
|
+
// Should not throw
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
describe('Error Handling', () => {
|
|
314
|
+
test('navigate throws on invalid URL', async () => {
|
|
315
|
+
await session.launch();
|
|
316
|
+
expect(async () => {
|
|
317
|
+
await session.navigate('not-a-valid-url');
|
|
318
|
+
}).toThrow();
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
test('methods throw when browser not launched', async () => {
|
|
322
|
+
expect(async () => {
|
|
323
|
+
await session.navigate('https://example.com');
|
|
324
|
+
}).toThrow();
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
test('screenshot throws when browser not launched', async () => {
|
|
328
|
+
expect(async () => {
|
|
329
|
+
await session.screenshot();
|
|
330
|
+
}).toThrow();
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
describe('Ref Handling', () => {
|
|
335
|
+
beforeEach(async () => {
|
|
336
|
+
await session.launch();
|
|
337
|
+
await session.navigate('https://example.com');
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
test('click accepts @ref format', async () => {
|
|
341
|
+
if (!session.click) {
|
|
342
|
+
throw new Error('click method not implemented');
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// The adapter should handle @e1 format refs
|
|
346
|
+
// This may fail if no element with that ref exists, but should not throw parse errors
|
|
347
|
+
try {
|
|
348
|
+
await session.click('@e1');
|
|
349
|
+
} catch (e) {
|
|
350
|
+
// Expected to potentially fail if ref doesn't exist
|
|
351
|
+
expect(e).toBeDefined();
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
test('fill accepts ref without @ prefix', async () => {
|
|
356
|
+
if (!session.fill) {
|
|
357
|
+
throw new Error('fill method not implemented');
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Should handle refs with or without @ prefix
|
|
361
|
+
try {
|
|
362
|
+
await session.fill('e1', 'value');
|
|
363
|
+
} catch (e) {
|
|
364
|
+
// Expected to potentially fail if ref doesn't exist
|
|
365
|
+
expect(e).toBeDefined();
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
});
|
|
369
|
+
});
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BrowserSession adapter using agent-browser's BrowserManager
|
|
3
|
+
*
|
|
4
|
+
* This adapter wraps agent-browser to provide a clean BrowserSession interface
|
|
5
|
+
* for the exploration engine.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { BrowserManager } from 'agent-browser/dist/browser.js';
|
|
9
|
+
import type { BrowserSession, BrowserLaunchOptions } from './explorer';
|
|
10
|
+
import type { EnhancedSnapshot } from './adapters/types';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* AgentBrowserSession implements BrowserSession using agent-browser
|
|
14
|
+
*/
|
|
15
|
+
export class AgentBrowserSession implements BrowserSession {
|
|
16
|
+
private browser: BrowserManager;
|
|
17
|
+
|
|
18
|
+
constructor() {
|
|
19
|
+
this.browser = new BrowserManager();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
isLaunched(): boolean {
|
|
23
|
+
return this.browser.isLaunched();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async launch(options?: BrowserLaunchOptions): Promise<void> {
|
|
27
|
+
const launchOptions = {
|
|
28
|
+
id: 'launch',
|
|
29
|
+
action: 'launch' as const,
|
|
30
|
+
headless: options?.headless ?? true,
|
|
31
|
+
viewport: options?.viewport ?? { width: 1280, height: 720 },
|
|
32
|
+
browser: 'chromium' as const,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
await this.browser.launch(launchOptions);
|
|
37
|
+
} catch (error) {
|
|
38
|
+
throw new Error(
|
|
39
|
+
`Failed to launch browser: ${error instanceof Error ? error.message : String(error)}`
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async navigate(url: string): Promise<void> {
|
|
45
|
+
this.ensureLaunched();
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const page = this.browser.getPage();
|
|
49
|
+
await page.goto(url, { waitUntil: 'load' });
|
|
50
|
+
} catch (error) {
|
|
51
|
+
throw new Error(
|
|
52
|
+
`Failed to navigate to ${url}: ${error instanceof Error ? error.message : String(error)}`
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async screenshot(options?: {
|
|
58
|
+
fullPage?: boolean;
|
|
59
|
+
clip?: { x: number; y: number; width: number; height: number };
|
|
60
|
+
mask?: string[];
|
|
61
|
+
quality?: number;
|
|
62
|
+
}): Promise<Buffer> {
|
|
63
|
+
this.ensureLaunched();
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const page = this.browser.getPage();
|
|
67
|
+
const { mask, ...restOptions } = options ?? {};
|
|
68
|
+
const buffer = await page.screenshot({
|
|
69
|
+
type: 'png',
|
|
70
|
+
...restOptions,
|
|
71
|
+
...(mask && { mask: mask.map((selector) => page.locator(selector)) }),
|
|
72
|
+
});
|
|
73
|
+
return Buffer.from(buffer);
|
|
74
|
+
} catch (error) {
|
|
75
|
+
throw new Error(
|
|
76
|
+
`Failed to capture screenshot: ${error instanceof Error ? error.message : String(error)}`
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async getSnapshot(options?: {
|
|
82
|
+
interactive?: boolean;
|
|
83
|
+
maxDepth?: number;
|
|
84
|
+
compact?: boolean;
|
|
85
|
+
selector?: string;
|
|
86
|
+
}): Promise<EnhancedSnapshot> {
|
|
87
|
+
this.ensureLaunched();
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
const snapshot = await this.browser.getSnapshot(options);
|
|
91
|
+
return {
|
|
92
|
+
tree: snapshot.tree,
|
|
93
|
+
refs: snapshot.refs,
|
|
94
|
+
};
|
|
95
|
+
} catch (error) {
|
|
96
|
+
throw new Error(
|
|
97
|
+
`Failed to get snapshot: ${error instanceof Error ? error.message : String(error)}`
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async close(): Promise<void> {
|
|
103
|
+
if (this.browser.isLaunched()) {
|
|
104
|
+
try {
|
|
105
|
+
await this.browser.close();
|
|
106
|
+
} catch (error) {
|
|
107
|
+
// Ignore errors on close - browser might already be closed
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Interaction methods
|
|
113
|
+
async click(ref: string): Promise<void> {
|
|
114
|
+
this.ensureLaunched();
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
const locator = this.browser.getLocator(ref);
|
|
118
|
+
await locator.click();
|
|
119
|
+
} catch (error) {
|
|
120
|
+
throw new Error(
|
|
121
|
+
`Failed to click ${ref}: ${error instanceof Error ? error.message : String(error)}`
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async fill(ref: string, value: string): Promise<void> {
|
|
127
|
+
this.ensureLaunched();
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
const locator = this.browser.getLocator(ref);
|
|
131
|
+
await locator.fill(value);
|
|
132
|
+
} catch (error) {
|
|
133
|
+
throw new Error(
|
|
134
|
+
`Failed to fill ${ref}: ${error instanceof Error ? error.message : String(error)}`
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async type(ref: string, text: string): Promise<void> {
|
|
140
|
+
this.ensureLaunched();
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
const locator = this.browser.getLocator(ref);
|
|
144
|
+
await locator.pressSequentially(text);
|
|
145
|
+
} catch (error) {
|
|
146
|
+
throw new Error(
|
|
147
|
+
`Failed to type into ${ref}: ${error instanceof Error ? error.message : String(error)}`
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async select(ref: string, option: string): Promise<void> {
|
|
153
|
+
this.ensureLaunched();
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
const locator = this.browser.getLocator(ref);
|
|
157
|
+
await locator.selectOption(option);
|
|
158
|
+
} catch (error) {
|
|
159
|
+
throw new Error(
|
|
160
|
+
`Failed to select option in ${ref}: ${error instanceof Error ? error.message : String(error)}`
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async check(ref: string, checked: boolean): Promise<void> {
|
|
166
|
+
this.ensureLaunched();
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
const locator = this.browser.getLocator(ref);
|
|
170
|
+
if (checked) {
|
|
171
|
+
await locator.check();
|
|
172
|
+
} else {
|
|
173
|
+
await locator.uncheck();
|
|
174
|
+
}
|
|
175
|
+
} catch (error) {
|
|
176
|
+
throw new Error(
|
|
177
|
+
`Failed to ${checked ? 'check' : 'uncheck'} ${ref}: ${error instanceof Error ? error.message : String(error)}`
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async press(key: string): Promise<void> {
|
|
183
|
+
this.ensureLaunched();
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
const page = this.browser.getPage();
|
|
187
|
+
await page.keyboard.press(key);
|
|
188
|
+
} catch (error) {
|
|
189
|
+
throw new Error(
|
|
190
|
+
`Failed to press key ${key}: ${error instanceof Error ? error.message : String(error)}`
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Navigation methods
|
|
196
|
+
async back(): Promise<void> {
|
|
197
|
+
this.ensureLaunched();
|
|
198
|
+
|
|
199
|
+
try {
|
|
200
|
+
const page = this.browser.getPage();
|
|
201
|
+
await page.goBack();
|
|
202
|
+
} catch (error) {
|
|
203
|
+
throw new Error(
|
|
204
|
+
`Failed to go back: ${error instanceof Error ? error.message : String(error)}`
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async forward(): Promise<void> {
|
|
210
|
+
this.ensureLaunched();
|
|
211
|
+
|
|
212
|
+
try {
|
|
213
|
+
const page = this.browser.getPage();
|
|
214
|
+
await page.goForward();
|
|
215
|
+
} catch (error) {
|
|
216
|
+
throw new Error(
|
|
217
|
+
`Failed to go forward: ${error instanceof Error ? error.message : String(error)}`
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async refresh(): Promise<void> {
|
|
223
|
+
this.ensureLaunched();
|
|
224
|
+
|
|
225
|
+
try {
|
|
226
|
+
const page = this.browser.getPage();
|
|
227
|
+
await page.reload();
|
|
228
|
+
} catch (error) {
|
|
229
|
+
throw new Error(
|
|
230
|
+
`Failed to refresh page: ${error instanceof Error ? error.message : String(error)}`
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Wait methods
|
|
236
|
+
async waitForSelector(selector: string, timeout: number): Promise<void> {
|
|
237
|
+
this.ensureLaunched();
|
|
238
|
+
|
|
239
|
+
try {
|
|
240
|
+
const page = this.browser.getPage();
|
|
241
|
+
await page.waitForSelector(selector, { timeout });
|
|
242
|
+
} catch (error) {
|
|
243
|
+
throw new Error(
|
|
244
|
+
`Failed to wait for selector ${selector}: ${error instanceof Error ? error.message : String(error)}`
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async waitForURL(urlPattern: string, timeout: number): Promise<void> {
|
|
250
|
+
this.ensureLaunched();
|
|
251
|
+
|
|
252
|
+
try {
|
|
253
|
+
const page = this.browser.getPage();
|
|
254
|
+
await page.waitForURL(urlPattern, { timeout });
|
|
255
|
+
} catch (error) {
|
|
256
|
+
throw new Error(
|
|
257
|
+
`Failed to wait for URL ${urlPattern}: ${error instanceof Error ? error.message : String(error)}`
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
async waitForText(text: string, timeout: number): Promise<void> {
|
|
263
|
+
this.ensureLaunched();
|
|
264
|
+
|
|
265
|
+
try {
|
|
266
|
+
const page = this.browser.getPage();
|
|
267
|
+
await page.locator(`text=${text}`).waitFor({ timeout });
|
|
268
|
+
} catch (error) {
|
|
269
|
+
throw new Error(
|
|
270
|
+
`Failed to wait for text "${text}": ${error instanceof Error ? error.message : String(error)}`
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
async waitForLoadState(state: 'load' | 'domcontentloaded' | 'networkidle'): Promise<void> {
|
|
276
|
+
this.ensureLaunched();
|
|
277
|
+
|
|
278
|
+
try {
|
|
279
|
+
const page = this.browser.getPage();
|
|
280
|
+
await page.waitForLoadState(state);
|
|
281
|
+
} catch (error) {
|
|
282
|
+
throw new Error(
|
|
283
|
+
`Failed to wait for load state ${state}: ${error instanceof Error ? error.message : String(error)}`
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
async waitForTimeout(ms: number): Promise<void> {
|
|
289
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Scroll methods
|
|
293
|
+
async scrollIntoView(ref: string): Promise<void> {
|
|
294
|
+
this.ensureLaunched();
|
|
295
|
+
|
|
296
|
+
try {
|
|
297
|
+
const locator = this.browser.getLocator(ref);
|
|
298
|
+
await locator.scrollIntoViewIfNeeded();
|
|
299
|
+
} catch (error) {
|
|
300
|
+
throw new Error(
|
|
301
|
+
`Failed to scroll ${ref} into view: ${error instanceof Error ? error.message : String(error)}`
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
async scroll(x: number, y: number): Promise<void> {
|
|
307
|
+
this.ensureLaunched();
|
|
308
|
+
|
|
309
|
+
try {
|
|
310
|
+
const page = this.browser.getPage();
|
|
311
|
+
// Use mouse wheel for scrolling (more reliable than evaluate)
|
|
312
|
+
await page.mouse.wheel(x, y);
|
|
313
|
+
} catch (error) {
|
|
314
|
+
throw new Error(
|
|
315
|
+
`Failed to scroll by (${x}, ${y}): ${error instanceof Error ? error.message : String(error)}`
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// State methods
|
|
321
|
+
getCurrentURL(): string {
|
|
322
|
+
this.ensureLaunched();
|
|
323
|
+
|
|
324
|
+
try {
|
|
325
|
+
const page = this.browser.getPage();
|
|
326
|
+
return page.url();
|
|
327
|
+
} catch (error) {
|
|
328
|
+
throw new Error(
|
|
329
|
+
`Failed to get current URL: ${error instanceof Error ? error.message : String(error)}`
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Ensures browser is launched before performing operations
|
|
336
|
+
*/
|
|
337
|
+
private ensureLaunched(): void {
|
|
338
|
+
if (!this.browser.isLaunched()) {
|
|
339
|
+
throw new Error('Browser is not launched. Call launch() first.');
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Factory function to create a new browser session
|
|
346
|
+
*/
|
|
347
|
+
export function createBrowserSession(): BrowserSession {
|
|
348
|
+
return new AgentBrowserSession();
|
|
349
|
+
}
|