@olib-ai/owl-browser-sdk 2.0.5 → 2.0.7

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.
Files changed (111) hide show
  1. package/README.md +107 -0
  2. package/dist/extraction/content-cleaner.d.ts +40 -0
  3. package/dist/extraction/content-cleaner.d.ts.map +1 -0
  4. package/dist/extraction/content-cleaner.js +393 -0
  5. package/dist/extraction/content-cleaner.js.map +1 -0
  6. package/dist/extraction/extractor.d.ts +139 -0
  7. package/dist/extraction/extractor.d.ts.map +1 -0
  8. package/dist/extraction/extractor.js +212 -0
  9. package/dist/extraction/extractor.js.map +1 -0
  10. package/dist/extraction/html-processor.d.ts +75 -0
  11. package/dist/extraction/html-processor.d.ts.map +1 -0
  12. package/dist/extraction/html-processor.js +192 -0
  13. package/dist/extraction/html-processor.js.map +1 -0
  14. package/dist/extraction/index.d.ts +14 -0
  15. package/dist/extraction/index.d.ts.map +1 -0
  16. package/dist/extraction/index.js +19 -0
  17. package/dist/extraction/index.js.map +1 -0
  18. package/dist/extraction/list-extractor.d.ts +24 -0
  19. package/dist/extraction/list-extractor.d.ts.map +1 -0
  20. package/dist/extraction/list-extractor.js +303 -0
  21. package/dist/extraction/list-extractor.js.map +1 -0
  22. package/dist/extraction/meta-extractor.d.ts +40 -0
  23. package/dist/extraction/meta-extractor.d.ts.map +1 -0
  24. package/dist/extraction/meta-extractor.js +216 -0
  25. package/dist/extraction/meta-extractor.js.map +1 -0
  26. package/dist/extraction/pagination.d.ts +29 -0
  27. package/dist/extraction/pagination.d.ts.map +1 -0
  28. package/dist/extraction/pagination.js +323 -0
  29. package/dist/extraction/pagination.js.map +1 -0
  30. package/dist/extraction/pattern-detector.d.ts +16 -0
  31. package/dist/extraction/pattern-detector.d.ts.map +1 -0
  32. package/dist/extraction/pattern-detector.js +390 -0
  33. package/dist/extraction/pattern-detector.js.map +1 -0
  34. package/dist/extraction/scrape-session.d.ts +23 -0
  35. package/dist/extraction/scrape-session.d.ts.map +1 -0
  36. package/dist/extraction/scrape-session.js +192 -0
  37. package/dist/extraction/scrape-session.js.map +1 -0
  38. package/dist/extraction/selector-engine.d.ts +23 -0
  39. package/dist/extraction/selector-engine.d.ts.map +1 -0
  40. package/dist/extraction/selector-engine.js +127 -0
  41. package/dist/extraction/selector-engine.js.map +1 -0
  42. package/dist/extraction/table-extractor.d.ts +29 -0
  43. package/dist/extraction/table-extractor.d.ts.map +1 -0
  44. package/dist/extraction/table-extractor.js +282 -0
  45. package/dist/extraction/table-extractor.js.map +1 -0
  46. package/dist/extraction/transforms.d.ts +47 -0
  47. package/dist/extraction/transforms.d.ts.map +1 -0
  48. package/dist/extraction/transforms.js +277 -0
  49. package/dist/extraction/transforms.js.map +1 -0
  50. package/dist/extraction/types.d.ts +199 -0
  51. package/dist/extraction/types.d.ts.map +1 -0
  52. package/dist/extraction/types.js +5 -0
  53. package/dist/extraction/types.js.map +1 -0
  54. package/dist/index.d.ts +1 -0
  55. package/dist/index.d.ts.map +1 -1
  56. package/dist/index.js +2 -0
  57. package/dist/index.js.map +1 -1
  58. package/dist/playwright/browser-type.d.ts +101 -0
  59. package/dist/playwright/browser-type.d.ts.map +1 -0
  60. package/dist/playwright/browser-type.js +134 -0
  61. package/dist/playwright/browser-type.js.map +1 -0
  62. package/dist/playwright/browser.d.ts +98 -0
  63. package/dist/playwright/browser.d.ts.map +1 -0
  64. package/dist/playwright/browser.js +229 -0
  65. package/dist/playwright/browser.js.map +1 -0
  66. package/dist/playwright/context.d.ts +217 -0
  67. package/dist/playwright/context.d.ts.map +1 -0
  68. package/dist/playwright/context.js +518 -0
  69. package/dist/playwright/context.js.map +1 -0
  70. package/dist/playwright/extractor.d.ts +108 -0
  71. package/dist/playwright/extractor.d.ts.map +1 -0
  72. package/dist/playwright/extractor.js +404 -0
  73. package/dist/playwright/extractor.js.map +1 -0
  74. package/dist/playwright/frame.d.ts +147 -0
  75. package/dist/playwright/frame.d.ts.map +1 -0
  76. package/dist/playwright/frame.js +492 -0
  77. package/dist/playwright/frame.js.map +1 -0
  78. package/dist/playwright/index.d.ts +163 -0
  79. package/dist/playwright/index.d.ts.map +1 -0
  80. package/dist/playwright/index.js +313 -0
  81. package/dist/playwright/index.js.map +1 -0
  82. package/dist/playwright/keyboard.d.ts +74 -0
  83. package/dist/playwright/keyboard.d.ts.map +1 -0
  84. package/dist/playwright/keyboard.js +187 -0
  85. package/dist/playwright/keyboard.js.map +1 -0
  86. package/dist/playwright/locator.d.ts +237 -0
  87. package/dist/playwright/locator.d.ts.map +1 -0
  88. package/dist/playwright/locator.js +667 -0
  89. package/dist/playwright/locator.js.map +1 -0
  90. package/dist/playwright/mouse.d.ts +82 -0
  91. package/dist/playwright/mouse.d.ts.map +1 -0
  92. package/dist/playwright/mouse.js +137 -0
  93. package/dist/playwright/mouse.js.map +1 -0
  94. package/dist/playwright/page-helpers.d.ts +267 -0
  95. package/dist/playwright/page-helpers.d.ts.map +1 -0
  96. package/dist/playwright/page-helpers.js +449 -0
  97. package/dist/playwright/page-helpers.js.map +1 -0
  98. package/dist/playwright/page.d.ts +605 -0
  99. package/dist/playwright/page.d.ts.map +1 -0
  100. package/dist/playwright/page.js +1698 -0
  101. package/dist/playwright/page.js.map +1 -0
  102. package/dist/playwright/response.d.ts +100 -0
  103. package/dist/playwright/response.d.ts.map +1 -0
  104. package/dist/playwright/response.js +194 -0
  105. package/dist/playwright/response.js.map +1 -0
  106. package/dist/playwright/types.d.ts +354 -0
  107. package/dist/playwright/types.d.ts.map +1 -0
  108. package/dist/playwright/types.js +8 -0
  109. package/dist/playwright/types.js.map +1 -0
  110. package/openapi.json +327 -35
  111. package/package.json +10 -1
@@ -0,0 +1,1698 @@
1
+ /**
2
+ * Playwright-compatible Page class for Owl Browser.
3
+ *
4
+ * This is the primary interface for browser interaction. Each Page instance
5
+ * maps to a single browser context + tab in Owl Browser, translating
6
+ * Playwright API calls into the corresponding tool executions.
7
+ *
8
+ * Supports: navigation, interaction, content extraction, screenshots,
9
+ * evaluation, waiting, network interception (route/unroute), dialog
10
+ * handling, console monitoring, downloads, file upload, clipboard,
11
+ * scrolling, zoom, viewport, frames, tabs, video recording, and emulation.
12
+ */
13
+ import { writeFile } from 'node:fs/promises';
14
+ import { queryAll as extractQueryAll, queryFirst as extractQueryFirst, extractTable as extractTableFn, extractMeta as extractMetaFn, extractStructuredData as extractStructuredDataFn, countElements as countElementsFn, } from './extractor.js';
15
+ import { Buffer } from 'node:buffer';
16
+ import { createResponse } from './response.js';
17
+ import { Keyboard } from './keyboard.js';
18
+ import { Mouse } from './mouse.js';
19
+ import { Frame, FrameLocator } from './frame.js';
20
+ import { Locator, NthLocator } from './locator.js';
21
+ import { Route, Dialog, ConsoleMessage, Download, Video, } from './page-helpers.js';
22
+ /**
23
+ * Page provides the main API for interacting with a single browser tab.
24
+ *
25
+ * Mirrors the Playwright Page API surface. All async methods translate
26
+ * to Owl Browser tool executions via the underlying OwlBrowser client.
27
+ *
28
+ * @example
29
+ * ```typescript
30
+ * const page = await context.newPage();
31
+ * await page.goto('https://example.com');
32
+ * await page.click('#submit');
33
+ * const title = await page.title();
34
+ * ```
35
+ */
36
+ export class Page {
37
+ _client;
38
+ _contextId;
39
+ _keyboard;
40
+ _mouse;
41
+ _mainFrame;
42
+ _url = 'about:blank';
43
+ _viewport = null;
44
+ _closed = false;
45
+ _tabId;
46
+ _routes = [];
47
+ _eventHandlers = new Map();
48
+ _video = null;
49
+ _dialogPollTimer = null;
50
+ _consolePollTimer = null;
51
+ _consoleLogOffset = 0;
52
+ _dialogSetupReady = null;
53
+ constructor(client, contextId, tabId) {
54
+ this._client = client;
55
+ this._contextId = contextId;
56
+ this._tabId = tabId ?? null;
57
+ this._keyboard = new Keyboard(client, contextId);
58
+ this._mouse = new Mouse(client, contextId);
59
+ this._mainFrame = new Frame(client, contextId, null, 'main');
60
+ }
61
+ // ==================== Properties ====================
62
+ /** The Keyboard instance for this page. */
63
+ get keyboard() {
64
+ return this._keyboard;
65
+ }
66
+ /** The Mouse instance for this page. */
67
+ get mouse() {
68
+ return this._mouse;
69
+ }
70
+ /** The main Frame of this page. */
71
+ mainFrame() {
72
+ return this._mainFrame;
73
+ }
74
+ /** The current page URL (synchronous getter). */
75
+ get url() {
76
+ return this._url;
77
+ }
78
+ /** Whether the page has been closed. */
79
+ isClosed() {
80
+ return this._closed;
81
+ }
82
+ /** The current viewport size, or null if not set. */
83
+ viewportSize() {
84
+ return this._viewport;
85
+ }
86
+ /** The underlying context ID. */
87
+ get contextId() {
88
+ return this._contextId;
89
+ }
90
+ /**
91
+ * Execute a browser tool with auto-injected context_id.
92
+ *
93
+ * @internal Used by Locator and other classes to execute tools.
94
+ * @param toolName - Browser tool name (e.g., 'browser_click')
95
+ * @param params - Tool parameters (context_id is auto-injected)
96
+ * @returns Tool execution result
97
+ */
98
+ async _execute(toolName, params = {}) {
99
+ return this._client.execute(toolName, {
100
+ context_id: this._contextId,
101
+ ...params,
102
+ });
103
+ }
104
+ // ==================== Navigation ====================
105
+ /**
106
+ * Navigate to a URL.
107
+ *
108
+ * @param url - Target URL (must include protocol)
109
+ * @param options - Navigation options (waitUntil, timeout, referer)
110
+ * @returns Response object or null
111
+ */
112
+ async goto(url, options) {
113
+ const waitUntil = mapWaitUntil(options?.waitUntil);
114
+ const params = {
115
+ context_id: this._contextId,
116
+ url,
117
+ };
118
+ if (waitUntil) {
119
+ params['wait_until'] = waitUntil;
120
+ }
121
+ if (options?.timeout !== undefined) {
122
+ params['timeout'] = String(options.timeout);
123
+ }
124
+ const result = await this._client.execute('browser_navigate', params);
125
+ this._url = url;
126
+ await this._syncUrl();
127
+ return createResponse(result);
128
+ }
129
+ /**
130
+ * Navigate back in history.
131
+ *
132
+ * @param options - Navigation options
133
+ * @returns Response object or null
134
+ */
135
+ async goBack(options) {
136
+ const params = {
137
+ context_id: this._contextId,
138
+ };
139
+ if (options?.waitUntil) {
140
+ params['wait_until'] = mapWaitUntil(options.waitUntil);
141
+ }
142
+ if (options?.timeout !== undefined) {
143
+ params['timeout'] = String(options.timeout);
144
+ }
145
+ const result = await this._client.execute('browser_go_back', params);
146
+ await this._syncUrl();
147
+ return createResponse(result);
148
+ }
149
+ /**
150
+ * Navigate forward in history.
151
+ *
152
+ * @param options - Navigation options
153
+ * @returns Response object or null
154
+ */
155
+ async goForward(options) {
156
+ const params = {
157
+ context_id: this._contextId,
158
+ };
159
+ if (options?.waitUntil) {
160
+ params['wait_until'] = mapWaitUntil(options.waitUntil);
161
+ }
162
+ if (options?.timeout !== undefined) {
163
+ params['timeout'] = String(options.timeout);
164
+ }
165
+ const result = await this._client.execute('browser_go_forward', params);
166
+ await this._syncUrl();
167
+ return createResponse(result);
168
+ }
169
+ /**
170
+ * Reload the page.
171
+ *
172
+ * @param options - Navigation options
173
+ * @returns Response object or null
174
+ */
175
+ async reload(options) {
176
+ const params = {
177
+ context_id: this._contextId,
178
+ };
179
+ if (options?.waitUntil) {
180
+ params['wait_until'] = mapWaitUntil(options.waitUntil);
181
+ }
182
+ if (options?.timeout !== undefined) {
183
+ params['timeout'] = String(options.timeout);
184
+ }
185
+ const result = await this._client.execute('browser_reload', params);
186
+ return createResponse(result);
187
+ }
188
+ // ==================== Content ====================
189
+ /** Get the page title. */
190
+ async title() {
191
+ const result = await this._client.execute('browser_get_page_info', {
192
+ context_id: this._contextId,
193
+ });
194
+ const res = result;
195
+ return String(res['title'] ?? '');
196
+ }
197
+ /** Get the full HTML content of the page. */
198
+ async content() {
199
+ const result = await this._client.execute('browser_get_html', {
200
+ context_id: this._contextId,
201
+ });
202
+ // Result is a raw HTML string
203
+ if (typeof result === 'string')
204
+ return result;
205
+ const res = result;
206
+ return String(res['html'] ?? res['result'] ?? '');
207
+ }
208
+ /**
209
+ * Set the HTML content of the page.
210
+ *
211
+ * @param html - HTML string to load
212
+ * @param options - Set content options
213
+ */
214
+ async setContent(html, options) {
215
+ void options;
216
+ await this._client.execute('browser_set_content', {
217
+ context_id: this._contextId,
218
+ html,
219
+ });
220
+ }
221
+ // ==================== Schema-Driven Extraction ====================
222
+ /**
223
+ * Extract structured data from all elements matching a CSS selector.
224
+ *
225
+ * Fetches the page HTML once and parses it SDK-side using cheerio.
226
+ * Each matching container element has the given fields extracted from it.
227
+ *
228
+ * Field syntax:
229
+ * `"selector"` → textContent of the matched child element
230
+ * `"selector@attr"` → attribute value of the matched child element
231
+ * `"@attr"` → attribute on the container element itself
232
+ *
233
+ * @param selector - CSS selector for repeating container elements
234
+ * @param fields - Mapping of output names to extraction specs
235
+ * @returns Array of objects with extracted values
236
+ *
237
+ * @example
238
+ * ```typescript
239
+ * const products = await page.queryAll('.product-card', {
240
+ * name: 'h2',
241
+ * price: '.price',
242
+ * image: 'img@src',
243
+ * link: 'a@href',
244
+ * });
245
+ * ```
246
+ */
247
+ async queryAll(selector, fields) {
248
+ const html = await this.content();
249
+ return extractQueryAll(html, selector, fields);
250
+ }
251
+ /**
252
+ * Extract structured data from the first element matching a CSS selector.
253
+ *
254
+ * @param selector - CSS selector for the container element
255
+ * @param fields - Mapping of output names to extraction specs
256
+ * @returns Single record or null if no match
257
+ */
258
+ async queryFirst(selector, fields) {
259
+ const html = await this.content();
260
+ return extractQueryFirst(html, selector, fields);
261
+ }
262
+ /**
263
+ * Extract a <table> as an array of records.
264
+ *
265
+ * @param selector - CSS selector for the table (default: 'table')
266
+ * @param options - Optional headers override
267
+ * @returns Array of records with header keys
268
+ */
269
+ async extractTable(selector, options) {
270
+ const html = await this.content();
271
+ return extractTableFn(html, selector ?? 'table', options);
272
+ }
273
+ /**
274
+ * Extract JSON-LD structured data from the page.
275
+ *
276
+ * @returns Array of parsed JSON-LD objects
277
+ */
278
+ async extractStructuredData() {
279
+ const html = await this.content();
280
+ return extractStructuredDataFn(html);
281
+ }
282
+ /**
283
+ * Extract meta tags from the page.
284
+ *
285
+ * @returns MetaData with title, description, canonical, og, twitter, other
286
+ */
287
+ async extractMeta() {
288
+ const html = await this.content();
289
+ return extractMetaFn(html);
290
+ }
291
+ /**
292
+ * Count elements matching a CSS selector.
293
+ *
294
+ * @param selector - CSS selector
295
+ * @returns Number of matching elements
296
+ */
297
+ async count(selector) {
298
+ const html = await this.content();
299
+ return countElementsFn(html, selector);
300
+ }
301
+ /**
302
+ * Extract structured data across multiple pages or scroll loads.
303
+ *
304
+ * Supports two pagination modes:
305
+ * - **Click-next**: clicks a "next" button and waits for page update.
306
+ * - **Infinite scroll**: scrolls to bottom and waits for new content.
307
+ *
308
+ * Automatically detects end of data: next button gone/disabled/hidden,
309
+ * no new items after dedup, or max pages/scrolls reached.
310
+ *
311
+ * @param selector - CSS selector for repeating container elements
312
+ * @param options - Scrape options (fields, next, scroll, limits)
313
+ * @returns Deduplicated array of all extracted records across pages
314
+ */
315
+ async scrape(selector, options) {
316
+ const { fields, next, maxPages = 10, wait = 2000, scroll = false, maxScrolls = 20, scrollWait = 2000, urls, urlPattern, startPage = 1, endPage, pageStep = 1, follow, onPageDone, retries = 0, retryDelay = 1000, } = options;
317
+ const allItems = [];
318
+ const seen = new Set();
319
+ // Helper: extract and dedup items from current page
320
+ const extractPage = async (pageNum) => {
321
+ const html = await this.content();
322
+ const items = extractQueryAll(html, selector, fields);
323
+ let newCount = 0;
324
+ for (const item of items) {
325
+ const key = JSON.stringify(Object.entries(item).sort(([a], [b]) => a.localeCompare(b)));
326
+ if (!seen.has(key)) {
327
+ seen.add(key);
328
+ allItems.push(item);
329
+ newCount++;
330
+ }
331
+ }
332
+ onPageDone?.({ page: pageNum, newItems: newCount, totalItems: allItems.length });
333
+ return newCount;
334
+ };
335
+ // Helper: retry a function
336
+ const withRetry = async (fn) => {
337
+ let lastError;
338
+ for (let attempt = 0; attempt <= retries; attempt++) {
339
+ try {
340
+ return await fn();
341
+ }
342
+ catch (e) {
343
+ lastError = e;
344
+ if (attempt < retries) {
345
+ await this.waitForTimeout(retryDelay);
346
+ }
347
+ }
348
+ }
349
+ throw lastError;
350
+ };
351
+ // Mode 3: URL pagination
352
+ if (urls || urlPattern) {
353
+ const urlList = [];
354
+ if (urls) {
355
+ urlList.push(...urls);
356
+ }
357
+ else if (urlPattern) {
358
+ const end = endPage ?? (startPage + maxPages - 1);
359
+ for (let p = startPage; p <= end; p += pageStep) {
360
+ const offset = (p - 1) * pageStep;
361
+ urlList.push(urlPattern
362
+ .replace(/\{page\}/g, String(p))
363
+ .replace(/\{offset\}/g, String(offset)));
364
+ }
365
+ }
366
+ for (let i = 0; i < urlList.length; i++) {
367
+ await withRetry(async () => {
368
+ await this.goto(urlList[i]);
369
+ await this.waitForLoadState();
370
+ if (wait > 0)
371
+ await this.waitForTimeout(wait);
372
+ });
373
+ const newCount = await extractPage(i + 1);
374
+ if (newCount === 0)
375
+ break;
376
+ }
377
+ }
378
+ // Mode 1 & 2: Click-next or Infinite scroll
379
+ else {
380
+ const limit = scroll ? maxScrolls : maxPages;
381
+ for (let i = 0; i < limit; i++) {
382
+ const newCount = await extractPage(i + 1);
383
+ if (newCount === 0)
384
+ break;
385
+ if (scroll) {
386
+ await this.scrollToBottom();
387
+ await this.waitForTimeout(scrollWait);
388
+ }
389
+ else if (next) {
390
+ const loc = this.locator(next);
391
+ try {
392
+ const count = await loc.count();
393
+ if (count === 0)
394
+ break;
395
+ }
396
+ catch {
397
+ break;
398
+ }
399
+ if (!(await loc.isVisible()) || !(await loc.isEnabled()))
400
+ break;
401
+ await loc.click();
402
+ await this.waitForLoadState();
403
+ await this.waitForTimeout(wait);
404
+ }
405
+ else {
406
+ break;
407
+ }
408
+ }
409
+ }
410
+ // Detail page following
411
+ if (follow) {
412
+ const followWait = follow.wait ?? 1000;
413
+ for (const item of allItems) {
414
+ // Extract the URL from the item using the urlSelector as a string spec
415
+ const detailUrl = item[follow.urlSelector]
416
+ ?? extractQueryFirst(await this.content(), selector, { _url: follow.urlSelector })?.['_url'];
417
+ if (!detailUrl || typeof detailUrl !== 'string')
418
+ continue;
419
+ // Resolve relative URLs
420
+ let fullUrl = detailUrl;
421
+ if (!detailUrl.startsWith('http://') && !detailUrl.startsWith('https://')) {
422
+ try {
423
+ fullUrl = new URL(detailUrl, this.url).href;
424
+ }
425
+ catch {
426
+ continue;
427
+ }
428
+ }
429
+ const prevUrl = this.url;
430
+ try {
431
+ await withRetry(async () => {
432
+ await this.goto(fullUrl);
433
+ await this.waitForLoadState();
434
+ if (followWait > 0)
435
+ await this.waitForTimeout(followWait);
436
+ });
437
+ const detailHtml = await this.content();
438
+ const detailData = extractQueryFirst(detailHtml, 'body', follow.fields)
439
+ ?? extractQueryFirst(detailHtml, selector, follow.fields);
440
+ if (detailData) {
441
+ Object.assign(item, detailData);
442
+ }
443
+ }
444
+ catch {
445
+ // Skip failed detail pages
446
+ }
447
+ // Navigate back
448
+ try {
449
+ await this.goto(prevUrl);
450
+ await this.waitForLoadState();
451
+ }
452
+ catch {
453
+ // Best effort
454
+ }
455
+ }
456
+ }
457
+ return allItems;
458
+ }
459
+ /**
460
+ * Get the page content as Markdown.
461
+ *
462
+ * @returns Markdown representation of the page
463
+ */
464
+ async markdown() {
465
+ const result = await this._client.execute('browser_get_markdown', {
466
+ context_id: this._contextId,
467
+ });
468
+ // Result is a raw markdown string
469
+ if (typeof result === 'string')
470
+ return result;
471
+ const res = result;
472
+ return String(res['markdown'] ?? res['result'] ?? '');
473
+ }
474
+ // ==================== Interaction ====================
475
+ /**
476
+ * Click an element.
477
+ *
478
+ * @param selector - CSS selector, XY coordinates, or natural language description
479
+ * @param options - Click options (button, clickCount, etc.)
480
+ */
481
+ async click(selector, options) {
482
+ // Ensure dialog setup is complete before interacting (prevents race condition)
483
+ if (this._dialogSetupReady) {
484
+ await this._dialogSetupReady;
485
+ this._dialogSetupReady = null;
486
+ }
487
+ if (options?.button === 'right') {
488
+ await this._client.execute('browser_right_click', {
489
+ context_id: this._contextId,
490
+ selector,
491
+ });
492
+ return;
493
+ }
494
+ if (options?.clickCount === 2) {
495
+ await this._client.execute('browser_double_click', {
496
+ context_id: this._contextId,
497
+ selector,
498
+ });
499
+ return;
500
+ }
501
+ await this._client.execute('browser_click', {
502
+ context_id: this._contextId,
503
+ selector,
504
+ });
505
+ }
506
+ /** Double-click an element. */
507
+ async dblclick(selector, options) {
508
+ void options;
509
+ await this._client.execute('browser_double_click', {
510
+ context_id: this._contextId,
511
+ selector,
512
+ });
513
+ }
514
+ /**
515
+ * Fill an input element (clears existing content first).
516
+ *
517
+ * @param selector - Target input element
518
+ * @param value - Text to fill
519
+ * @param options - Fill options
520
+ */
521
+ async fill(selector, value, options) {
522
+ void options;
523
+ await this._client.execute('browser_clear_input', {
524
+ context_id: this._contextId,
525
+ selector,
526
+ });
527
+ await this._client.execute('browser_type', {
528
+ context_id: this._contextId,
529
+ selector,
530
+ text: value,
531
+ });
532
+ }
533
+ /**
534
+ * Type text into an element (does NOT clear existing content).
535
+ *
536
+ * @param selector - Target input element
537
+ * @param text - Text to type
538
+ * @param options - Type options
539
+ */
540
+ async type(selector, text, options) {
541
+ void options;
542
+ await this._client.execute('browser_type', {
543
+ context_id: this._contextId,
544
+ selector,
545
+ text,
546
+ });
547
+ }
548
+ /**
549
+ * Focus an element and press a key.
550
+ *
551
+ * For special keys (Enter, Tab, Escape, etc.) uses browser_press_key.
552
+ * For single character keys (a-z, 0-9, etc.) uses browser_type to simulate typing.
553
+ */
554
+ async press(selector, key, options) {
555
+ void options;
556
+ await this._client.execute('browser_focus', {
557
+ context_id: this._contextId,
558
+ selector,
559
+ });
560
+ // Delegate to keyboard which handles the special-key vs character distinction
561
+ await this._keyboard.press(key);
562
+ }
563
+ /** Hover over an element. */
564
+ async hover(selector, options) {
565
+ void options;
566
+ await this._client.execute('browser_hover', {
567
+ context_id: this._contextId,
568
+ selector,
569
+ });
570
+ }
571
+ /** Focus an element. */
572
+ async focus(selector, options) {
573
+ void options;
574
+ await this._client.execute('browser_focus', {
575
+ context_id: this._contextId,
576
+ selector,
577
+ });
578
+ }
579
+ /** Blur the currently focused element. */
580
+ async blur(selector) {
581
+ await this._client.execute('browser_blur', {
582
+ context_id: this._contextId,
583
+ selector,
584
+ });
585
+ }
586
+ /** Select option(s) from a dropdown. Calls browser_pick once per value. */
587
+ async selectOption(selector, values, options) {
588
+ void options;
589
+ const valueList = extractSelectValues(values);
590
+ for (const value of valueList) {
591
+ await this._client.execute('browser_pick', {
592
+ context_id: this._contextId,
593
+ selector,
594
+ value,
595
+ });
596
+ }
597
+ return valueList;
598
+ }
599
+ /** Check a checkbox (no-op if already checked). */
600
+ async check(selector, options) {
601
+ void options;
602
+ const checked = await this.isChecked(selector);
603
+ if (!checked) {
604
+ await this.click(selector);
605
+ }
606
+ }
607
+ /** Uncheck a checkbox (no-op if already unchecked). */
608
+ async uncheck(selector, options) {
609
+ void options;
610
+ const checked = await this.isChecked(selector);
611
+ if (checked) {
612
+ await this.click(selector);
613
+ }
614
+ }
615
+ /** Drag an element and drop it onto another element. */
616
+ async dragAndDrop(source, target, options) {
617
+ void options;
618
+ // Use browser_html5_drag_drop which accepts CSS selectors.
619
+ // browser_drag_drop requires pixel coordinates and is for non-HTML5 drag.
620
+ await this._client.execute('browser_html5_drag_drop', {
621
+ context_id: this._contextId,
622
+ source_selector: source,
623
+ target_selector: target,
624
+ });
625
+ }
626
+ /**
627
+ * Upload files to a file input element.
628
+ *
629
+ * @param selector - CSS selector for the file input element
630
+ * @param files - File path(s) to upload
631
+ * @param options - Upload options
632
+ */
633
+ async setInputFiles(selector, files, options) {
634
+ void options;
635
+ const filePaths = Array.isArray(files) ? files : [files];
636
+ await this._client.execute('browser_upload_file', {
637
+ context_id: this._contextId,
638
+ selector,
639
+ file_paths: JSON.stringify(filePaths),
640
+ });
641
+ }
642
+ /** Submit a form. */
643
+ async submitForm(selector) {
644
+ await this._client.execute('browser_submit_form', {
645
+ context_id: this._contextId,
646
+ selector,
647
+ });
648
+ }
649
+ /** Select all text in the focused element. */
650
+ async selectAll() {
651
+ await this._client.execute('browser_select_all', {
652
+ context_id: this._contextId,
653
+ });
654
+ }
655
+ // ==================== Queries ====================
656
+ /** Get the text content of an element. */
657
+ async textContent(selector) {
658
+ const result = await this._client.execute('browser_extract_text', {
659
+ context_id: this._contextId,
660
+ selector,
661
+ });
662
+ // Result is a raw string (the text content)
663
+ if (result === null || result === undefined)
664
+ return null;
665
+ if (typeof result === 'string')
666
+ return result;
667
+ const res = result;
668
+ return res['text'] ?? String(res['result'] ?? '');
669
+ }
670
+ /** Get the inner HTML of an element. */
671
+ async innerHTML(selector) {
672
+ const result = await this._client.execute('browser_get_html', {
673
+ context_id: this._contextId,
674
+ selector,
675
+ });
676
+ if (typeof result === 'string')
677
+ return result;
678
+ const res = result;
679
+ return String(res['html'] ?? res['result'] ?? '');
680
+ }
681
+ /** Get the inner text of an element (visible text only). */
682
+ async innerText(selector) {
683
+ const text = await this.textContent(selector);
684
+ return text ?? '';
685
+ }
686
+ /** Get an attribute value from an element. */
687
+ async getAttribute(selector, name) {
688
+ const result = await this._client.execute('browser_get_attribute', {
689
+ context_id: this._contextId,
690
+ selector,
691
+ attribute: name,
692
+ });
693
+ if (result === null || result === undefined)
694
+ return null;
695
+ if (typeof result === 'string')
696
+ return result.length > 0 ? result : null;
697
+ const res = result;
698
+ const val = res['value'] ?? res['result'];
699
+ if (val === undefined || val === null)
700
+ return null;
701
+ return String(val);
702
+ }
703
+ /** Get the bounding box of an element. */
704
+ async boundingBox(selector) {
705
+ try {
706
+ const result = await this._client.execute('browser_get_bounding_box', {
707
+ context_id: this._contextId,
708
+ selector,
709
+ });
710
+ const res = result;
711
+ return {
712
+ x: Number(res['x'] ?? 0),
713
+ y: Number(res['y'] ?? 0),
714
+ width: Number(res['width'] ?? 0),
715
+ height: Number(res['height'] ?? 0),
716
+ };
717
+ }
718
+ catch {
719
+ return null;
720
+ }
721
+ }
722
+ // ==================== Element State ====================
723
+ /** Check if an element is visible. */
724
+ async isVisible(selector) {
725
+ const result = await this._client.execute('browser_is_visible', {
726
+ context_id: this._contextId,
727
+ selector,
728
+ });
729
+ const res = result;
730
+ if (res['error_code'] === 'visible' || res['visible'] === true)
731
+ return true;
732
+ if (res['error_code'] === 'hidden')
733
+ return false;
734
+ return false;
735
+ }
736
+ /** Check if an element is enabled. */
737
+ async isEnabled(selector) {
738
+ const result = await this._client.execute('browser_is_enabled', {
739
+ context_id: this._contextId,
740
+ selector,
741
+ });
742
+ const res = result;
743
+ if (res['error_code'] === 'enabled' || res['enabled'] === true)
744
+ return true;
745
+ if (res['error_code'] === 'disabled')
746
+ return false;
747
+ return false;
748
+ }
749
+ /** Check if a checkbox/radio is checked. */
750
+ async isChecked(selector) {
751
+ const result = await this._client.execute('browser_is_checked', {
752
+ context_id: this._contextId,
753
+ selector,
754
+ });
755
+ const res = result;
756
+ if (res['error_code'] === 'checked' || res['checked'] === true)
757
+ return true;
758
+ if (res['error_code'] === 'unchecked')
759
+ return false;
760
+ return false;
761
+ }
762
+ // ==================== Screenshots & PDF ====================
763
+ /**
764
+ * Take a screenshot of the page.
765
+ *
766
+ * @param options - Screenshot options (path, type, fullPage, etc.)
767
+ * @returns PNG image data as Buffer
768
+ */
769
+ async screenshot(options) {
770
+ const params = {
771
+ context_id: this._contextId,
772
+ };
773
+ if (options?.fullPage) {
774
+ params['full_page'] = true;
775
+ }
776
+ const result = await this._client.execute('browser_screenshot', params);
777
+ // Result is a raw base64 string (not an object)
778
+ const base64Data = typeof result === 'string'
779
+ ? result
780
+ : String(result['data'] ?? result['screenshot'] ?? result['image'] ?? '');
781
+ const buffer = Buffer.from(base64Data, 'base64');
782
+ if (options?.path) {
783
+ await writeFile(options.path, buffer);
784
+ }
785
+ return buffer;
786
+ }
787
+ /**
788
+ * Generate a PDF of the page.
789
+ *
790
+ * Stub: Owl Browser does not directly support PDF generation.
791
+ * Returns an empty buffer.
792
+ *
793
+ * @param options - PDF options
794
+ * @returns Empty Buffer
795
+ */
796
+ async pdf(options) {
797
+ void options;
798
+ return Buffer.alloc(0);
799
+ }
800
+ // ==================== Evaluate ====================
801
+ /**
802
+ * Evaluate JavaScript in the page context.
803
+ *
804
+ * @param pageFunction - Function or expression string to evaluate
805
+ * @param arg - Optional argument to pass to the function
806
+ * @returns The result of the evaluation
807
+ */
808
+ async evaluate(pageFunction, arg) {
809
+ // Ensure dialog setup is complete before evaluating (prevents race condition)
810
+ if (this._dialogSetupReady) {
811
+ await this._dialogSetupReady;
812
+ this._dialogSetupReady = null;
813
+ }
814
+ const expression = typeof pageFunction === 'function'
815
+ ? `(${pageFunction.toString()})(${arg !== undefined ? JSON.stringify(arg) : ''})`
816
+ : pageFunction;
817
+ const result = await this._client.execute('browser_evaluate', {
818
+ context_id: this._contextId,
819
+ expression,
820
+ });
821
+ return result;
822
+ }
823
+ /**
824
+ * Evaluate JavaScript and return a handle (simplified).
825
+ *
826
+ * @param pageFunction - Function or expression string
827
+ * @param arg - Optional argument
828
+ * @returns Evaluation result
829
+ */
830
+ async evaluateHandle(pageFunction, arg) {
831
+ return this.evaluate(pageFunction, arg);
832
+ }
833
+ /**
834
+ * Add a script to evaluate when a new document is created.
835
+ *
836
+ * @param script - JavaScript code or path to script file
837
+ * @param arg - Optional argument
838
+ */
839
+ async addInitScript(script, arg) {
840
+ void arg;
841
+ const code = typeof script === 'string' ? script : '';
842
+ if (code) {
843
+ await this._client.execute('browser_evaluate', {
844
+ context_id: this._contextId,
845
+ script: code,
846
+ return_value: false,
847
+ });
848
+ }
849
+ }
850
+ /**
851
+ * Expose a function in the page's global scope.
852
+ *
853
+ * Stub: Creates a global function that logs calls via console.
854
+ *
855
+ * @param name - Function name to expose
856
+ * @param callback - Function implementation
857
+ */
858
+ async exposeFunction(name, callback) {
859
+ void callback;
860
+ await this._client.execute('browser_evaluate', {
861
+ context_id: this._contextId,
862
+ script: `window['${name}'] = function() { console.log('${name} called', ...arguments); }`,
863
+ return_value: false,
864
+ });
865
+ }
866
+ // ==================== Waiting ====================
867
+ /** Wait for a selector to appear in the DOM. */
868
+ async waitForSelector(selector, options) {
869
+ const params = {
870
+ context_id: this._contextId,
871
+ selector,
872
+ };
873
+ if (options?.timeout !== undefined) {
874
+ params['timeout'] = options.timeout;
875
+ }
876
+ await this._client.execute('browser_wait_for_selector', params);
877
+ }
878
+ /** Wait for a fixed amount of time. */
879
+ async waitForTimeout(timeout) {
880
+ await this._client.execute('browser_wait', {
881
+ context_id: this._contextId,
882
+ timeout,
883
+ });
884
+ }
885
+ /** Wait for the page URL to match. */
886
+ async waitForURL(url, options) {
887
+ const isRegex = url instanceof RegExp;
888
+ const urlString = typeof url === 'string'
889
+ ? url
890
+ : url instanceof RegExp
891
+ ? url.source
892
+ : '*';
893
+ const params = {
894
+ context_id: this._contextId,
895
+ url_pattern: urlString,
896
+ };
897
+ if (isRegex) {
898
+ params['is_regex'] = true;
899
+ }
900
+ if (options?.timeout !== undefined) {
901
+ params['timeout'] = options.timeout;
902
+ }
903
+ await this._client.execute('browser_wait_for_url', params);
904
+ await this._syncUrl();
905
+ }
906
+ /** Wait for a JavaScript function to return truthy. */
907
+ async waitForFunction(pageFunction, arg, options) {
908
+ const fn = typeof pageFunction === 'function'
909
+ ? `(${pageFunction.toString()})(${arg !== undefined ? JSON.stringify(arg) : ''})`
910
+ : pageFunction;
911
+ const params = {
912
+ context_id: this._contextId,
913
+ js_function: fn,
914
+ };
915
+ if (options?.timeout !== undefined) {
916
+ params['timeout'] = String(options.timeout);
917
+ }
918
+ if (options?.polling !== undefined && options.polling !== 'raf') {
919
+ params['polling'] = String(options.polling);
920
+ }
921
+ await this._client.execute('browser_wait_for_function', params);
922
+ }
923
+ /** Wait for the page to reach a specific load state. */
924
+ async waitForLoadState(state, options) {
925
+ const params = {
926
+ context_id: this._contextId,
927
+ };
928
+ if (options?.timeout !== undefined) {
929
+ params['timeout'] = options.timeout;
930
+ }
931
+ if (state === 'domcontentloaded') {
932
+ await this._client.execute('browser_wait_for_selector', {
933
+ ...params,
934
+ selector: 'body',
935
+ });
936
+ }
937
+ else if (state === 'load') {
938
+ await this._client.execute('browser_wait_for_function', {
939
+ ...params,
940
+ js_function: 'document.readyState === "complete"',
941
+ });
942
+ }
943
+ else {
944
+ // 'networkidle' or undefined — default behavior
945
+ await this._client.execute('browser_wait_for_network_idle', params);
946
+ }
947
+ }
948
+ /** Wait for a specific event to fire. */
949
+ async waitForEvent(event, optionsOrPredicate) {
950
+ const options = typeof optionsOrPredicate === 'function'
951
+ ? { predicate: optionsOrPredicate }
952
+ : optionsOrPredicate;
953
+ const timeout = options?.timeout ?? 30000;
954
+ if (event === 'download') {
955
+ return this._waitForDownload(timeout);
956
+ }
957
+ if (event === 'dialog') {
958
+ return this._waitForDialog(timeout);
959
+ }
960
+ if (event === 'console') {
961
+ return this._waitForConsole(timeout);
962
+ }
963
+ // Generic wait fallback
964
+ await this.waitForTimeout(Math.min(timeout, 1000));
965
+ return null;
966
+ }
967
+ /**
968
+ * Wait for a network response matching the given URL/predicate.
969
+ *
970
+ * Enables network logging and polls browser_get_network_log for matches.
971
+ *
972
+ * @param urlOrPredicate - URL string, RegExp, or predicate function
973
+ * @param options - Wait options (timeout)
974
+ * @returns A Response object for the matching network entry
975
+ */
976
+ async waitForResponse(urlOrPredicate, options) {
977
+ const timeout = options?.timeout ?? 30000;
978
+ await this._client.execute('browser_enable_network_logging', {
979
+ context_id: this._contextId,
980
+ enable: true,
981
+ });
982
+ const start = Date.now();
983
+ while (Date.now() - start < timeout) {
984
+ const result = await this._client.execute('browser_get_network_log', {
985
+ context_id: this._contextId,
986
+ });
987
+ const res = result;
988
+ // browser_get_network_log returns { requests: [...], responses: [...] }
989
+ const entries = (res['responses'] ?? []);
990
+ if (Array.isArray(entries)) {
991
+ for (const entry of entries) {
992
+ const entryUrl = String(entry['url'] ?? '');
993
+ const status = Number(entry['status'] ?? entry['statusCode'] ?? 0);
994
+ if (typeof urlOrPredicate === 'string') {
995
+ if (entryUrl.includes(urlOrPredicate) && status > 0) {
996
+ return createResponse({ url: entryUrl, status });
997
+ }
998
+ }
999
+ else if (urlOrPredicate instanceof RegExp) {
1000
+ if (urlOrPredicate.test(entryUrl) && status > 0) {
1001
+ return createResponse({ url: entryUrl, status });
1002
+ }
1003
+ }
1004
+ // Predicate support would require constructing a full Response first
1005
+ }
1006
+ }
1007
+ await this.waitForTimeout(200);
1008
+ }
1009
+ throw new Error(`Timeout waiting for response (${timeout}ms)`);
1010
+ }
1011
+ /**
1012
+ * Wait for a network request matching the given URL/predicate.
1013
+ *
1014
+ * @param urlOrPredicate - URL string or RegExp to match
1015
+ * @param options - Wait options (timeout)
1016
+ */
1017
+ async waitForRequest(urlOrPredicate, options) {
1018
+ const timeout = options?.timeout ?? 30000;
1019
+ await this._client.execute('browser_enable_network_logging', {
1020
+ context_id: this._contextId,
1021
+ enable: true,
1022
+ });
1023
+ const start = Date.now();
1024
+ while (Date.now() - start < timeout) {
1025
+ const result = await this._client.execute('browser_get_network_log', {
1026
+ context_id: this._contextId,
1027
+ });
1028
+ const res = result;
1029
+ // browser_get_network_log returns { requests: [...], responses: [...] }
1030
+ const entries = (res['requests'] ?? []);
1031
+ if (Array.isArray(entries)) {
1032
+ for (const entry of entries) {
1033
+ const entryUrl = String(entry['url'] ?? '');
1034
+ const method = String(entry['method'] ?? 'GET');
1035
+ if (typeof urlOrPredicate === 'string') {
1036
+ if (entryUrl.includes(urlOrPredicate)) {
1037
+ return { url: () => entryUrl, method: () => method };
1038
+ }
1039
+ }
1040
+ else if (urlOrPredicate instanceof RegExp) {
1041
+ if (urlOrPredicate.test(entryUrl)) {
1042
+ return { url: () => entryUrl, method: () => method };
1043
+ }
1044
+ }
1045
+ }
1046
+ }
1047
+ await this.waitForTimeout(200);
1048
+ }
1049
+ throw new Error(`Timeout waiting for request (${timeout}ms)`);
1050
+ }
1051
+ // ==================== Network Interception ====================
1052
+ /**
1053
+ * Intercept network requests matching a URL pattern.
1054
+ *
1055
+ * @param url - URL pattern (glob) or RegExp to match
1056
+ * @param handler - Handler function to process intercepted requests
1057
+ * @param options - Route options (e.g., times)
1058
+ */
1059
+ async route(url, handler, options) {
1060
+ const urlPattern = url instanceof RegExp ? url.source : url;
1061
+ // Enable network interception
1062
+ await this._client.execute('browser_enable_network_interception', {
1063
+ context_id: this._contextId,
1064
+ enable: true,
1065
+ });
1066
+ // Add a block rule first (handler will override with fulfill/abort/continue)
1067
+ const result = await this._client.execute('browser_add_network_rule', {
1068
+ context_id: this._contextId,
1069
+ url_pattern: urlPattern,
1070
+ action: 'block',
1071
+ is_regex: url instanceof RegExp,
1072
+ });
1073
+ const res = result;
1074
+ const ruleId = String(res['rule_id'] ?? '');
1075
+ this._routes.push({
1076
+ urlPattern,
1077
+ ruleId,
1078
+ handler,
1079
+ remaining: options?.times ?? null,
1080
+ });
1081
+ // Invoke handler with a Route instance
1082
+ const route = new Route(this._client, this._contextId, ruleId, urlPattern);
1083
+ await handler(route);
1084
+ }
1085
+ /**
1086
+ * Remove a previously registered route handler.
1087
+ *
1088
+ * @param url - URL pattern that was registered
1089
+ * @param handler - Optional specific handler to remove
1090
+ */
1091
+ async unroute(url, handler) {
1092
+ const urlPattern = url instanceof RegExp ? url.source : url;
1093
+ for (let i = this._routes.length - 1; i >= 0; i--) {
1094
+ const entry = this._routes[i];
1095
+ if (entry && entry.urlPattern === urlPattern) {
1096
+ if (handler === undefined || entry.handler === handler) {
1097
+ try {
1098
+ await this._client.execute('browser_remove_network_rule', {
1099
+ context_id: this._contextId,
1100
+ rule_id: entry.ruleId,
1101
+ });
1102
+ }
1103
+ catch {
1104
+ // Rule may already be removed
1105
+ }
1106
+ this._routes.splice(i, 1);
1107
+ }
1108
+ }
1109
+ }
1110
+ }
1111
+ /** Remove all route handlers. */
1112
+ async unrouteAll() {
1113
+ for (const entry of this._routes) {
1114
+ try {
1115
+ await this._client.execute('browser_remove_network_rule', {
1116
+ context_id: this._contextId,
1117
+ rule_id: entry.ruleId,
1118
+ });
1119
+ }
1120
+ catch {
1121
+ // Ignore
1122
+ }
1123
+ }
1124
+ this._routes.length = 0;
1125
+ }
1126
+ // ==================== Events ====================
1127
+ /**
1128
+ * Register an event handler.
1129
+ *
1130
+ * @param event - Event name ('dialog', 'console', 'download', etc.)
1131
+ * @param handler - Event handler function
1132
+ */
1133
+ on(event, handler) {
1134
+ const handlers = this._eventHandlers.get(event) ?? [];
1135
+ handlers.push(handler);
1136
+ this._eventHandlers.set(event, handlers);
1137
+ // Start background polling when relevant event listeners are registered
1138
+ if (event === 'dialog' && !this._dialogPollTimer) {
1139
+ this._startDialogPolling();
1140
+ }
1141
+ if (event === 'console' && !this._consolePollTimer) {
1142
+ this._startConsolePolling();
1143
+ }
1144
+ return this;
1145
+ }
1146
+ /**
1147
+ * Register a one-time event handler.
1148
+ *
1149
+ * @param event - Event name
1150
+ * @param handler - Event handler function (called once then removed)
1151
+ */
1152
+ once(event, handler) {
1153
+ const wrapper = (...args) => {
1154
+ this.off(event, wrapper);
1155
+ return handler(...args);
1156
+ };
1157
+ return this.on(event, wrapper);
1158
+ }
1159
+ /**
1160
+ * Remove an event handler.
1161
+ *
1162
+ * @param event - Event name
1163
+ * @param handler - Handler to remove
1164
+ */
1165
+ off(event, handler) {
1166
+ const handlers = this._eventHandlers.get(event);
1167
+ if (handlers) {
1168
+ const idx = handlers.indexOf(handler);
1169
+ if (idx >= 0) {
1170
+ handlers.splice(idx, 1);
1171
+ }
1172
+ }
1173
+ return this;
1174
+ }
1175
+ /**
1176
+ * Remove all event handlers for an event (or all events).
1177
+ *
1178
+ * @param event - Optional event name
1179
+ */
1180
+ removeAllListeners(event) {
1181
+ if (event) {
1182
+ this._eventHandlers.delete(event);
1183
+ }
1184
+ else {
1185
+ this._eventHandlers.clear();
1186
+ }
1187
+ return this;
1188
+ }
1189
+ // ==================== Viewport & Emulation ====================
1190
+ /** Set the viewport size. */
1191
+ async setViewportSize(size) {
1192
+ await this._client.execute('browser_set_viewport', {
1193
+ context_id: this._contextId,
1194
+ width: size.width,
1195
+ height: size.height,
1196
+ });
1197
+ this._viewport = { ...size };
1198
+ }
1199
+ /**
1200
+ * Emulate media features.
1201
+ *
1202
+ * @param options - Media emulation options (colorScheme, reducedMotion)
1203
+ */
1204
+ async emulateMedia(options) {
1205
+ if (options?.colorScheme) {
1206
+ await this.evaluate(`document.documentElement.style.colorScheme = '${options.colorScheme}'`);
1207
+ }
1208
+ // Media type and other options require CDP which Owl wraps differently
1209
+ }
1210
+ /**
1211
+ * Set extra HTTP headers for all requests.
1212
+ *
1213
+ * No-op: Owl Browser does not support runtime header injection.
1214
+ * Custom headers should be configured at context creation via VM profiles.
1215
+ */
1216
+ async setExtraHTTPHeaders(headers) {
1217
+ void headers;
1218
+ // Not supported — requires VM profile configuration at context creation
1219
+ }
1220
+ // ==================== Scrolling & Zoom ====================
1221
+ /** Scroll by pixel delta. */
1222
+ async scrollBy(deltaX, deltaY) {
1223
+ const params = {
1224
+ context_id: this._contextId,
1225
+ y: Math.round(deltaY),
1226
+ };
1227
+ if (deltaX !== 0) {
1228
+ params['x'] = Math.round(deltaX);
1229
+ }
1230
+ await this._client.execute('browser_scroll_by', params);
1231
+ }
1232
+ /** Scroll an element into view. */
1233
+ async scrollToElement(selector) {
1234
+ await this._client.execute('browser_scroll_to_element', {
1235
+ context_id: this._contextId,
1236
+ selector,
1237
+ });
1238
+ }
1239
+ /** Scroll to the top of the page. */
1240
+ async scrollToTop() {
1241
+ await this._client.execute('browser_scroll_to_top', {
1242
+ context_id: this._contextId,
1243
+ });
1244
+ }
1245
+ /** Scroll to the bottom of the page. */
1246
+ async scrollToBottom() {
1247
+ await this._client.execute('browser_scroll_to_bottom', {
1248
+ context_id: this._contextId,
1249
+ });
1250
+ }
1251
+ /** Zoom in the page. */
1252
+ async zoomIn() {
1253
+ await this._client.execute('browser_zoom_in', {
1254
+ context_id: this._contextId,
1255
+ });
1256
+ }
1257
+ /** Zoom out the page. */
1258
+ async zoomOut() {
1259
+ await this._client.execute('browser_zoom_out', {
1260
+ context_id: this._contextId,
1261
+ });
1262
+ }
1263
+ /** Reset zoom to 100%. */
1264
+ async zoomReset() {
1265
+ await this._client.execute('browser_zoom_reset', {
1266
+ context_id: this._contextId,
1267
+ });
1268
+ }
1269
+ // ==================== Clipboard ====================
1270
+ /** Read text from the clipboard. */
1271
+ async clipboardRead() {
1272
+ const result = await this._client.execute('browser_clipboard_read', {
1273
+ context_id: this._contextId,
1274
+ });
1275
+ const res = result;
1276
+ return String(res['text'] ?? res['content'] ?? '');
1277
+ }
1278
+ /** Write text to the clipboard. */
1279
+ async clipboardWrite(text) {
1280
+ await this._client.execute('browser_clipboard_write', {
1281
+ context_id: this._contextId,
1282
+ text,
1283
+ });
1284
+ }
1285
+ /** Clear the clipboard. */
1286
+ async clipboardClear() {
1287
+ await this._client.execute('browser_clipboard_clear', {
1288
+ context_id: this._contextId,
1289
+ });
1290
+ }
1291
+ // ==================== Console ====================
1292
+ /**
1293
+ * Get console log entries.
1294
+ *
1295
+ * @param options - Filter options (level, filter text, limit)
1296
+ * @returns Array of ConsoleMessage objects
1297
+ */
1298
+ async getConsoleMessages(options) {
1299
+ const params = {
1300
+ context_id: this._contextId,
1301
+ };
1302
+ if (options?.level)
1303
+ params['level'] = options.level;
1304
+ if (options?.filter)
1305
+ params['filter'] = options.filter;
1306
+ if (options?.limit !== undefined)
1307
+ params['limit'] = String(options.limit);
1308
+ const result = await this._client.execute('browser_get_console_log', params);
1309
+ const res = result;
1310
+ const logs = res['logs'] ?? res['entries'] ?? [];
1311
+ if (!Array.isArray(logs))
1312
+ return [];
1313
+ return logs.map((entry) => {
1314
+ const e = entry;
1315
+ return new ConsoleMessage(String(e['level'] ?? 'log'), String(e['message'] ?? e['text'] ?? ''), e['url'], e['line'], e['column']);
1316
+ });
1317
+ }
1318
+ /** Clear console logs. */
1319
+ async clearConsole() {
1320
+ await this._client.execute('browser_clear_console_log', {
1321
+ context_id: this._contextId,
1322
+ });
1323
+ }
1324
+ // ==================== Video ====================
1325
+ /**
1326
+ * Get the Video instance for this page (if recording is active).
1327
+ *
1328
+ * @returns Video instance or null
1329
+ */
1330
+ video() {
1331
+ return this._video;
1332
+ }
1333
+ /**
1334
+ * Start video recording for this page.
1335
+ *
1336
+ * @param options - Recording options (fps, codec)
1337
+ */
1338
+ async startVideoRecording(options) {
1339
+ const params = {
1340
+ context_id: this._contextId,
1341
+ };
1342
+ if (options?.fps !== undefined)
1343
+ params['fps'] = String(options.fps);
1344
+ if (options?.codec)
1345
+ params['codec'] = options.codec;
1346
+ await this._client.execute('browser_start_video_recording', params);
1347
+ this._video = new Video(this._client, this._contextId);
1348
+ return this._video;
1349
+ }
1350
+ /** Stop video recording. */
1351
+ async stopVideoRecording() {
1352
+ await this._client.execute('browser_stop_video_recording', {
1353
+ context_id: this._contextId,
1354
+ });
1355
+ }
1356
+ // ==================== Locators ====================
1357
+ /** Create a Locator for the given selector. */
1358
+ locator(selector) {
1359
+ return new Locator(this, selector);
1360
+ }
1361
+ /** Query a single element (alias for locator). */
1362
+ $(selector) {
1363
+ return new Locator(this, selector);
1364
+ }
1365
+ /** Query all matching elements (returns locators for each match). */
1366
+ async $$(selector) {
1367
+ const loc = new Locator(this, selector);
1368
+ const n = await loc.count();
1369
+ const locators = [];
1370
+ for (let i = 0; i < n; i++) {
1371
+ locators.push(new NthLocator(this, loc._selector, i));
1372
+ }
1373
+ return locators;
1374
+ }
1375
+ /** Get a locator by text content. */
1376
+ getByText(text, options) {
1377
+ void options;
1378
+ const textStr = text instanceof RegExp ? text.source : text;
1379
+ return new Locator(this, `text=${textStr}`);
1380
+ }
1381
+ /** Get a locator by role. */
1382
+ getByRole(role, options) {
1383
+ if (options?.name) {
1384
+ const name = options.name instanceof RegExp ? options.name.source : options.name;
1385
+ return new Locator(this, `role=${role}[name="${name}"]`);
1386
+ }
1387
+ return new Locator(this, `role=${role}`);
1388
+ }
1389
+ /** Get a locator by placeholder text. */
1390
+ getByPlaceholder(text, options) {
1391
+ void options;
1392
+ const textStr = text instanceof RegExp ? text.source : text;
1393
+ return new Locator(this, `placeholder=${textStr}`);
1394
+ }
1395
+ /** Get a locator by label text. */
1396
+ getByLabel(text, options) {
1397
+ void options;
1398
+ const textStr = text instanceof RegExp ? text.source : text;
1399
+ return new Locator(this, `label=${textStr}`);
1400
+ }
1401
+ /** Get a locator by alt text. */
1402
+ getByAltText(text, options) {
1403
+ void options;
1404
+ const textStr = text instanceof RegExp ? text.source : text;
1405
+ return new Locator(this, `alt=${textStr}`);
1406
+ }
1407
+ /** Get a locator by title attribute. */
1408
+ getByTitle(text, options) {
1409
+ void options;
1410
+ const textStr = text instanceof RegExp ? text.source : text;
1411
+ return new Locator(this, `title=${textStr}`);
1412
+ }
1413
+ /** Get a locator by data-testid attribute. */
1414
+ getByTestId(testId) {
1415
+ const idStr = testId instanceof RegExp ? testId.source : testId;
1416
+ return new Locator(this, `data-testid=${idStr}`);
1417
+ }
1418
+ // ==================== Frames ====================
1419
+ /** Get all frames on the page (cached, returns main frame). */
1420
+ frames() {
1421
+ return [this._mainFrame];
1422
+ }
1423
+ /** Get all frames by querying the browser. */
1424
+ async framesAsync() {
1425
+ try {
1426
+ const result = await this._client.execute('browser_list_frames', {
1427
+ context_id: this._contextId,
1428
+ });
1429
+ // browser_list_frames returns a JSON array directly
1430
+ const frames = Array.isArray(result) ? result : [];
1431
+ if (frames.length === 0)
1432
+ return [this._mainFrame];
1433
+ return frames.map((f) => {
1434
+ const entry = f;
1435
+ const id = String(entry['frame_id'] ?? entry['id'] ?? '');
1436
+ const name = String(entry['name'] ?? '');
1437
+ return new Frame(this._client, this._contextId, id, name || id);
1438
+ });
1439
+ }
1440
+ catch {
1441
+ return [this._mainFrame];
1442
+ }
1443
+ }
1444
+ /** Get a frame by name or URL. */
1445
+ frame(nameOrUrl) {
1446
+ if (typeof nameOrUrl === 'string') {
1447
+ return new Frame(this._client, this._contextId, nameOrUrl, nameOrUrl);
1448
+ }
1449
+ const selector = nameOrUrl.name ?? '';
1450
+ return new Frame(this._client, this._contextId, selector, selector);
1451
+ }
1452
+ /**
1453
+ * Get a FrameLocator for interacting with iframe content.
1454
+ *
1455
+ * @param selector - CSS selector for the iframe element
1456
+ * @returns A FrameLocator scoped to the iframe
1457
+ */
1458
+ frameLocator(selector) {
1459
+ return new FrameLocator(this._client, this._contextId, selector, this);
1460
+ }
1461
+ // ==================== Tab Management ====================
1462
+ /** Close the page/tab. */
1463
+ async close() {
1464
+ this._closed = true;
1465
+ if (this._dialogPollTimer) {
1466
+ clearTimeout(this._dialogPollTimer);
1467
+ this._dialogPollTimer = null;
1468
+ }
1469
+ if (this._consolePollTimer) {
1470
+ clearTimeout(this._consolePollTimer);
1471
+ this._consolePollTimer = null;
1472
+ }
1473
+ if (this._tabId) {
1474
+ try {
1475
+ await this._client.execute('browser_close_tab', {
1476
+ context_id: this._contextId,
1477
+ tab_id: this._tabId,
1478
+ });
1479
+ }
1480
+ catch {
1481
+ // Tab may already be closed
1482
+ }
1483
+ }
1484
+ }
1485
+ /** Bring the page tab to front. */
1486
+ async bringToFront() {
1487
+ if (this._tabId) {
1488
+ await this._client.execute('browser_switch_tab', {
1489
+ context_id: this._contextId,
1490
+ tab_id: this._tabId,
1491
+ });
1492
+ }
1493
+ }
1494
+ // ==================== Downloads ====================
1495
+ /**
1496
+ * Set the download directory path.
1497
+ *
1498
+ * @param path - Absolute directory path for downloads
1499
+ */
1500
+ async setDownloadPath(path) {
1501
+ await this._client.execute('browser_set_download_path', {
1502
+ context_id: this._contextId,
1503
+ path,
1504
+ });
1505
+ }
1506
+ /**
1507
+ * Get all downloads for this context.
1508
+ *
1509
+ * @returns Array of Download objects
1510
+ */
1511
+ async getDownloads() {
1512
+ const result = await this._client.execute('browser_get_downloads', {
1513
+ context_id: this._contextId,
1514
+ });
1515
+ const res = result;
1516
+ const downloads = res['downloads'] ?? [];
1517
+ if (!Array.isArray(downloads))
1518
+ return [];
1519
+ return downloads.map((d) => {
1520
+ const entry = d;
1521
+ return new Download(this._client, this._contextId, String(entry['id'] ?? entry['download_id'] ?? ''), String(entry['url'] ?? ''), String(entry['filename'] ?? entry['suggested_filename'] ?? ''));
1522
+ });
1523
+ }
1524
+ // ==================== Dialog Handling ====================
1525
+ /**
1526
+ * Configure automatic dialog handling.
1527
+ *
1528
+ * @param dialogType - Dialog type ('alert', 'confirm', 'prompt', 'beforeunload')
1529
+ * @param action - Action ('accept', 'dismiss', 'accept_with_text')
1530
+ * @param promptText - Text for prompt dialogs
1531
+ */
1532
+ async setDialogAction(dialogType, action, promptText) {
1533
+ const params = {
1534
+ context_id: this._contextId,
1535
+ dialog_type: dialogType,
1536
+ action,
1537
+ };
1538
+ if (promptText !== undefined) {
1539
+ params['prompt_text'] = promptText;
1540
+ }
1541
+ await this._client.execute('browser_set_dialog_action', params);
1542
+ }
1543
+ // ==================== Internal Helpers ====================
1544
+ /** Sync the internal URL cache with the actual page URL. */
1545
+ async _syncUrl() {
1546
+ try {
1547
+ const result = await this._client.execute('browser_get_page_info', {
1548
+ context_id: this._contextId,
1549
+ });
1550
+ const res = result;
1551
+ if (typeof res['url'] === 'string') {
1552
+ this._url = res['url'];
1553
+ }
1554
+ }
1555
+ catch {
1556
+ // Non-critical; URL cache may be stale
1557
+ }
1558
+ }
1559
+ /** Start polling for dialogs and dispatching to event handlers. */
1560
+ _startDialogPolling() {
1561
+ // Set all dialog types to "wait" mode so dialogs stay pending for our poll
1562
+ this._dialogSetupReady = Promise.allSettled(['alert', 'confirm', 'prompt', 'beforeunload'].map(dt => this._client.execute('browser_set_dialog_action', {
1563
+ context_id: this._contextId,
1564
+ dialog_type: dt,
1565
+ action: 'wait',
1566
+ }))).then(() => { });
1567
+ const poll = async () => {
1568
+ if (this._closed)
1569
+ return;
1570
+ try {
1571
+ const result = await this._client.execute('browser_get_pending_dialog', {
1572
+ context_id: this._contextId,
1573
+ });
1574
+ const res = result;
1575
+ const dialogType = res['dialog_type'] ?? res['type'];
1576
+ if (dialogType) {
1577
+ const dialog = new Dialog(this._client, String(dialogType), String(res['message'] ?? ''), String(res['default_value'] ?? ''), String(res['dialog_id'] ?? res['id'] ?? ''));
1578
+ const handlers = this._eventHandlers.get('dialog') ?? [];
1579
+ for (const handler of handlers) {
1580
+ try {
1581
+ await handler(dialog);
1582
+ }
1583
+ catch {
1584
+ // Handler error; ignore
1585
+ }
1586
+ }
1587
+ }
1588
+ }
1589
+ catch {
1590
+ // No dialog pending or error; ignore
1591
+ }
1592
+ if (!this._closed) {
1593
+ this._dialogPollTimer = setTimeout(poll, 250);
1594
+ }
1595
+ };
1596
+ this._dialogPollTimer = setTimeout(poll, 250);
1597
+ }
1598
+ /** Start polling for console messages and dispatching to event handlers. */
1599
+ _startConsolePolling() {
1600
+ const poll = async () => {
1601
+ if (this._closed)
1602
+ return;
1603
+ try {
1604
+ const messages = await this.getConsoleMessages();
1605
+ const newMessages = messages.slice(this._consoleLogOffset);
1606
+ this._consoleLogOffset = messages.length;
1607
+ const handlers = this._eventHandlers.get('console') ?? [];
1608
+ for (const msg of newMessages) {
1609
+ for (const handler of handlers) {
1610
+ try {
1611
+ await handler(msg);
1612
+ }
1613
+ catch {
1614
+ // Handler error; ignore
1615
+ }
1616
+ }
1617
+ }
1618
+ }
1619
+ catch {
1620
+ // Error polling console; ignore
1621
+ }
1622
+ if (!this._closed) {
1623
+ this._consolePollTimer = setTimeout(poll, 500);
1624
+ }
1625
+ };
1626
+ this._consolePollTimer = setTimeout(poll, 500);
1627
+ }
1628
+ /** Wait for a download event. */
1629
+ async _waitForDownload(timeout) {
1630
+ const start = Date.now();
1631
+ while (Date.now() - start < timeout) {
1632
+ const result = await this._client.execute('browser_get_downloads', {
1633
+ context_id: this._contextId,
1634
+ });
1635
+ const res = result;
1636
+ const downloads = res['downloads'];
1637
+ if (Array.isArray(downloads) && downloads.length > 0) {
1638
+ const latest = downloads[downloads.length - 1];
1639
+ return new Download(this._client, this._contextId, String(latest['id'] ?? latest['download_id'] ?? ''), String(latest['url'] ?? ''), String(latest['filename'] ?? latest['suggested_filename'] ?? ''));
1640
+ }
1641
+ await this.waitForTimeout(200);
1642
+ }
1643
+ throw new Error(`Timeout waiting for download (${timeout}ms)`);
1644
+ }
1645
+ /** Wait for a dialog event. */
1646
+ async _waitForDialog(timeout) {
1647
+ const params = {
1648
+ context_id: this._contextId,
1649
+ };
1650
+ if (timeout) {
1651
+ params['timeout'] = String(timeout);
1652
+ }
1653
+ const result = await this._client.execute('browser_wait_for_dialog', params);
1654
+ const res = result;
1655
+ return new Dialog(this._client, String(res['dialog_type'] ?? res['type'] ?? 'alert'), String(res['message'] ?? ''), String(res['default_value'] ?? ''), String(res['dialog_id'] ?? res['id'] ?? ''));
1656
+ }
1657
+ /** Wait for a console message event. */
1658
+ async _waitForConsole(timeout) {
1659
+ const start = Date.now();
1660
+ while (Date.now() - start < timeout) {
1661
+ const messages = await this.getConsoleMessages({ limit: 1 });
1662
+ if (messages.length > 0 && messages[0]) {
1663
+ return messages[0];
1664
+ }
1665
+ await this.waitForTimeout(200);
1666
+ }
1667
+ throw new Error(`Timeout waiting for console message (${timeout}ms)`);
1668
+ }
1669
+ }
1670
+ /** Map Playwright waitUntil values to Owl Browser equivalents. */
1671
+ function mapWaitUntil(value) {
1672
+ switch (value) {
1673
+ case 'load':
1674
+ return 'load';
1675
+ case 'domcontentloaded':
1676
+ return 'domcontentloaded';
1677
+ case 'networkidle':
1678
+ return 'networkidle';
1679
+ case 'commit':
1680
+ return '';
1681
+ default:
1682
+ return 'load';
1683
+ }
1684
+ }
1685
+ /** Extract all option values from SelectOptionValue as an array. */
1686
+ function extractSelectValues(values) {
1687
+ if (typeof values === 'string')
1688
+ return [values];
1689
+ if (Array.isArray(values)) {
1690
+ return values.map((v) => {
1691
+ if (typeof v === 'string')
1692
+ return v;
1693
+ return v.value ?? v.label ?? '';
1694
+ }).filter(Boolean);
1695
+ }
1696
+ return [values.value ?? values.label ?? ''].filter(Boolean);
1697
+ }
1698
+ //# sourceMappingURL=page.js.map