browser-commander 0.6.0 → 0.7.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/CHANGELOG.md CHANGED
@@ -1,5 +1,34 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.7.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Add emulateMedia API for unified color scheme emulation across all engines
8
+
9
+ Implements `emulateMedia({ colorScheme })` as a unified API for color scheme emulation (prefers-color-scheme) across Playwright and Puppeteer engines. Also adds `colorScheme` as a launch option to `launchBrowser`.
10
+
11
+ Fixes #36
12
+
13
+ - 785eb13: Add unified dialog event handling API (`page.on('dialog', handler)`)
14
+ - New `DialogManager` (`core/dialog-manager.js`) that registers `page.on('dialog')` for both Playwright and Puppeteer
15
+ - `commander.onDialog(handler)` — register a handler for browser dialogs (alert, confirm, prompt, beforeunload)
16
+ - `commander.offDialog(handler)` — remove a previously registered handler
17
+ - `commander.clearDialogHandlers()` — remove all dialog handlers
18
+ - Auto-dismiss behavior when no handlers are registered (prevents page from freezing)
19
+ - `enableDialogManager` option (default: `true`) to opt out if needed
20
+ - Exports `createDialogManager` for low-level usage
21
+ - 19 new unit tests covering all dialog handling scenarios
22
+
23
+ - 80ec5f7: Add page-level keyboard interaction support (issue #37)
24
+
25
+ Expose keyboard input methods on the commander object, enabling users to press
26
+ keys, type text, and hold modifier keys without accessing the raw page object
27
+ directly. New API: `commander.keyboard.press()`, `commander.keyboard.type()`,
28
+ `commander.keyboard.down()`, `commander.keyboard.up()`, and flat aliases
29
+ `commander.pressKey()`, `commander.typeText()`, `commander.keyDown()`,
30
+ `commander.keyUp()`.
31
+
3
32
  ## 0.6.0
4
33
 
5
34
  ### Minor Changes
package/README.md CHANGED
@@ -206,6 +206,38 @@ const { browser, page } = await launchBrowser({
206
206
 
207
207
  The `args` option allows passing custom Chrome arguments, which is useful for headless server environments (Docker, CI/CD) that require flags like `--no-sandbox`.
208
208
 
209
+ The `colorScheme` option allows setting the initial color scheme (`'light'`, `'dark'`, or `'no-preference'`) at launch time for screenshot services and testing tools:
210
+
211
+ ```javascript
212
+ const { browser, page } = await launchBrowser({
213
+ engine: 'playwright',
214
+ colorScheme: 'dark', // 'light', 'dark', or 'no-preference'
215
+ });
216
+ ```
217
+
218
+ ### commander.emulateMedia(options)
219
+
220
+ Emulate media features (e.g. `prefers-color-scheme`) for the current page:
221
+
222
+ ```javascript
223
+ // Set dark mode
224
+ await commander.emulateMedia({ colorScheme: 'dark' });
225
+
226
+ // Set light mode
227
+ await commander.emulateMedia({ colorScheme: 'light' });
228
+
229
+ // Reset to system default
230
+ await commander.emulateMedia({ colorScheme: null });
231
+ ```
232
+
233
+ Works with both Playwright (`page.emulateMedia`) and Puppeteer (`page.emulateMediaFeatures`). Can also be used as a standalone function:
234
+
235
+ ```javascript
236
+ import { emulateMedia } from 'browser-commander';
237
+
238
+ await emulateMedia({ page, engine: 'playwright', colorScheme: 'dark' });
239
+ ```
240
+
209
241
  ### makeBrowserCommander(options)
210
242
 
211
243
  ```javascript
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "browser-commander",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Universal browser automation library that supports both Playwright and Puppeteer with a unified API",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
package/src/README.md CHANGED
@@ -341,6 +341,7 @@ browser-commander/
341
341
  ├── interactions/
342
342
  │ ├── click.js # clickButton, clickElement
343
343
  │ ├── fill.js # fillTextArea
344
+ │ ├── keyboard.js # pressKey, typeText, keyDown, keyUp
344
345
  │ └── scroll.js # scrollIntoView
345
346
  ├── utilities/
346
347
  │ ├── wait.js # wait(), evaluate()
@@ -436,6 +437,36 @@ await commander.fillTextArea({
436
437
  });
437
438
  ```
438
439
 
440
+ ### commander.keyboard
441
+
442
+ Page-level keyboard input, independent of any specific element. Useful for
443
+ dismissing dialogs, submitting forms via Enter, tab navigation, etc.
444
+
445
+ ```javascript
446
+ // Press a key (e.g. dismiss a modal)
447
+ await commander.keyboard.press('Escape');
448
+
449
+ // Type text at the page level (sent to the currently focused element)
450
+ await commander.keyboard.type('Hello World');
451
+
452
+ // Modifier key combinations
453
+ await commander.keyboard.down('Control');
454
+ await commander.keyboard.press('a'); // Select All
455
+ await commander.keyboard.up('Control');
456
+ ```
457
+
458
+ Also available as flat functions:
459
+
460
+ ```javascript
461
+ await commander.pressKey({ key: 'Enter' });
462
+ await commander.typeText({ text: 'some text' });
463
+ await commander.keyDown({ key: 'Shift' });
464
+ await commander.keyUp({ key: 'Shift' });
465
+ ```
466
+
467
+ Key names follow the [Playwright keyboard convention](https://playwright.dev/docs/api/class-keyboard#keyboard-press):
468
+ `'Escape'`, `'Enter'`, `'Tab'`, `'ArrowUp'`, `'ArrowDown'`, `'Control'`, `'Shift'`, `'Alt'`, etc.
469
+
439
470
  ### commander.destroy()
440
471
 
441
472
  ```javascript
package/src/bindings.js CHANGED
@@ -12,6 +12,7 @@ import {
12
12
  waitForPageReady,
13
13
  waitAfterAction,
14
14
  } from './browser/navigation.js';
15
+ import { emulateMedia } from './browser/media.js';
15
16
  import {
16
17
  createPlaywrightLocator,
17
18
  getLocatorOrElement,
@@ -52,6 +53,7 @@ import {
52
53
  checkAndClearFlag,
53
54
  findToggleButton,
54
55
  } from './high-level/universal-logic.js';
56
+ import { pressKey, typeText, keyDown, keyUp } from './interactions/keyboard.js';
55
57
 
56
58
  /**
57
59
  * Create bound functions for a browser commander instance
@@ -191,6 +193,9 @@ export function createBoundFunctions(options = {}) {
191
193
  const fillTextAreaBound = (opts) =>
192
194
  fillTextArea({ ...opts, page, engine, wait: waitBound, log });
193
195
 
196
+ // Bound media emulation
197
+ const emulateMediaBound = (opts) => emulateMedia({ ...opts, page, engine });
198
+
194
199
  // Bound high-level
195
200
  const waitForUrlConditionBound = (opts) =>
196
201
  waitForUrlCondition({
@@ -210,6 +215,12 @@ export function createBoundFunctions(options = {}) {
210
215
  findByText: findByTextBound,
211
216
  });
212
217
 
218
+ // Bound keyboard
219
+ const pressKeyBound = (opts) => pressKey({ ...opts, page, engine });
220
+ const typeTextBound = (opts) => typeText({ ...opts, page, engine });
221
+ const keyDownBound = (opts) => keyDown({ ...opts, page, engine });
222
+ const keyUpBound = (opts) => keyUp({ ...opts, page, engine });
223
+
213
224
  // Wrap functions with text selector support
214
225
  const fillTextAreaWrapped = withTextSelectorSupport(
215
226
  fillTextAreaBound,
@@ -267,6 +278,7 @@ export function createBoundFunctions(options = {}) {
267
278
 
268
279
  // Main API functions
269
280
  wait: waitBound,
281
+ emulateMedia: emulateMediaBound,
270
282
  fillTextArea: fillTextAreaWrapped,
271
283
  clickButton: clickButtonWrapped,
272
284
  evaluate: evaluateBound,
@@ -294,5 +306,20 @@ export function createBoundFunctions(options = {}) {
294
306
  installClickListener: installClickListenerBound,
295
307
  checkAndClearFlag: checkAndClearFlagBound,
296
308
  findToggleButton: findToggleButtonBound,
309
+
310
+ // Page-level keyboard interaction
311
+ // Usage: await commander.keyboard.press('Escape')
312
+ keyboard: {
313
+ press: (key) => pressKeyBound({ key }),
314
+ type: (text) => typeTextBound({ text }),
315
+ down: (key) => keyDownBound({ key }),
316
+ up: (key) => keyUpBound({ key }),
317
+ },
318
+
319
+ // Also expose as individual flat functions for functional-style usage
320
+ pressKey: pressKeyBound,
321
+ typeText: typeTextBound,
322
+ keyDown: keyDownBound,
323
+ keyUp: keyUpBound,
297
324
  };
298
325
  }
@@ -2,6 +2,7 @@ import path from 'path';
2
2
  import os from 'os';
3
3
  import { CHROME_ARGS } from '../core/constants.js';
4
4
  import { disableTranslateInPreferences } from '../core/preferences.js';
5
+ import { emulateMedia } from './media.js';
5
6
 
6
7
  /**
7
8
  * Launch browser with default configuration
@@ -12,6 +13,7 @@ import { disableTranslateInPreferences } from '../core/preferences.js';
12
13
  * @param {number} options.slowMo - Slow down operations by ms (default: 150 for Playwright, 0 for Puppeteer)
13
14
  * @param {boolean} options.verbose - Enable verbose logging (default: false)
14
15
  * @param {string[]} options.args - Custom Chrome arguments to append to the default CHROME_ARGS
16
+ * @param {string|null} [options.colorScheme] - Emulate color scheme: 'light', 'dark', 'no-preference', or null to reset
15
17
  * @returns {Promise<Object>} - Object with browser and page
16
18
  */
17
19
  export async function launchBrowser(options = {}) {
@@ -22,6 +24,7 @@ export async function launchBrowser(options = {}) {
22
24
  slowMo = engine === 'playwright' ? 150 : 0,
23
25
  verbose = false,
24
26
  args = [],
27
+ colorScheme,
25
28
  } = options;
26
29
 
27
30
  // Combine default CHROME_ARGS with custom args
@@ -50,14 +53,22 @@ export async function launchBrowser(options = {}) {
50
53
 
51
54
  if (engine === 'playwright') {
52
55
  const { chromium } = await import('playwright');
53
- browser = await chromium.launchPersistentContext(userDataDir, {
56
+ const contextOptions = {
54
57
  headless,
55
58
  slowMo,
56
59
  chromiumSandbox: true,
57
60
  viewport: null,
58
61
  args: chromeArgs,
59
62
  ignoreDefaultArgs: ['--enable-automation'],
60
- });
63
+ };
64
+ // Playwright supports colorScheme as a context-level launch option
65
+ if (colorScheme !== undefined) {
66
+ contextOptions.colorScheme = colorScheme;
67
+ }
68
+ browser = await chromium.launchPersistentContext(
69
+ userDataDir,
70
+ contextOptions
71
+ );
61
72
  page = browser.pages()[0];
62
73
  } else {
63
74
  const puppeteer = await import('puppeteer');
@@ -75,6 +86,20 @@ export async function launchBrowser(options = {}) {
75
86
  console.log(`✅ Browser launched with ${engine} engine`);
76
87
  }
77
88
 
89
+ // Apply color scheme emulation if requested (Puppeteer needs page-level emulation)
90
+ if (colorScheme !== undefined && engine === 'puppeteer') {
91
+ try {
92
+ await emulateMedia({ page, engine, colorScheme });
93
+ if (verbose) {
94
+ console.log(`✅ Color scheme set to "${colorScheme}"`);
95
+ }
96
+ } catch (error) {
97
+ if (verbose) {
98
+ console.log(`⚠️ Could not set color scheme: ${error.message}`);
99
+ }
100
+ }
101
+ }
102
+
78
103
  // Unfocus address bar automatically after browser launch
79
104
  // Using page.bringToFront() - confirmed working solution
80
105
  try {
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Browser Commander - Media Emulation
3
+ * Provides unified color scheme emulation across Playwright and Puppeteer.
4
+ */
5
+
6
+ const VALID_COLOR_SCHEMES = ['light', 'dark', 'no-preference'];
7
+
8
+ /**
9
+ * Emulate media features (e.g. prefers-color-scheme) for the page.
10
+ *
11
+ * @param {Object} options
12
+ * @param {Object} options.page - Playwright or Puppeteer page object
13
+ * @param {string} options.engine - Engine type: 'playwright' or 'puppeteer'
14
+ * @param {string|null} [options.colorScheme] - Color scheme: 'light', 'dark', 'no-preference', or null to reset
15
+ * @returns {Promise<void>}
16
+ */
17
+ export async function emulateMedia({ page, engine, colorScheme } = {}) {
18
+ if (!page) {
19
+ throw new Error('page is required in emulateMedia');
20
+ }
21
+ if (!engine) {
22
+ throw new Error('engine is required in emulateMedia');
23
+ }
24
+
25
+ if (colorScheme !== null && colorScheme !== undefined) {
26
+ if (!VALID_COLOR_SCHEMES.includes(colorScheme)) {
27
+ throw new Error(
28
+ `Invalid colorScheme: "${colorScheme}". Expected one of: ${VALID_COLOR_SCHEMES.join(', ')}, or null`
29
+ );
30
+ }
31
+ }
32
+
33
+ if (engine === 'playwright') {
34
+ // Playwright supports emulateMedia natively
35
+ const mediaOptions = {};
36
+ if (colorScheme !== undefined) {
37
+ mediaOptions.colorScheme = colorScheme;
38
+ }
39
+ await page.emulateMedia(mediaOptions);
40
+ } else if (engine === 'puppeteer') {
41
+ // Puppeteer supports emulateMediaFeatures since v5.4.0
42
+ if (colorScheme === null || colorScheme === undefined) {
43
+ // Reset to default
44
+ await page.emulateMediaFeatures([
45
+ { name: 'prefers-color-scheme', value: '' },
46
+ ]);
47
+ } else {
48
+ await page.emulateMediaFeatures([
49
+ { name: 'prefers-color-scheme', value: colorScheme },
50
+ ]);
51
+ }
52
+ } else {
53
+ throw new Error(
54
+ `Unsupported engine: ${engine}. Expected 'playwright' or 'puppeteer'`
55
+ );
56
+ }
57
+ }
@@ -0,0 +1,158 @@
1
+ /**
2
+ * DialogManager - Centralized dialog/alert event handling
3
+ *
4
+ * This module provides:
5
+ * - Unified dialog event handling for Playwright and Puppeteer
6
+ * - Session-aware handler registration (auto-cleanup on navigation)
7
+ * - Support for alert, confirm, prompt, and beforeunload dialogs
8
+ *
9
+ * Both Playwright and Puppeteer expose page.on('dialog', handler)
10
+ * with a Dialog object that has accept(), dismiss(), message(), and type().
11
+ */
12
+
13
+ /**
14
+ * Create a DialogManager instance for a page
15
+ * @param {Object} options - Configuration options
16
+ * @param {Object} options.page - Playwright or Puppeteer page object
17
+ * @param {string} options.engine - 'playwright' or 'puppeteer'
18
+ * @param {Function} options.log - Logger instance
19
+ * @returns {Object} - DialogManager API
20
+ */
21
+ export function createDialogManager(options = {}) {
22
+ const { page, engine, log } = options;
23
+
24
+ if (!page) {
25
+ throw new Error('page is required in options');
26
+ }
27
+
28
+ // User-registered dialog handlers
29
+ const handlers = [];
30
+
31
+ // Whether we are currently listening to the page's dialog event
32
+ let isListening = false;
33
+
34
+ /**
35
+ * Internal handler that is registered on the page.
36
+ * It calls all user-registered handlers in order.
37
+ * If no handler has accepted/dismissed the dialog after all handlers run,
38
+ * we auto-dismiss to prevent the page from freezing.
39
+ */
40
+ async function handleDialog(dialog) {
41
+ const type =
42
+ typeof dialog.type === 'function' ? dialog.type() : dialog.type;
43
+ const message =
44
+ typeof dialog.message === 'function' ? dialog.message() : dialog.message;
45
+
46
+ log.debug(() => `💬 Dialog event: type="${type}", message="${message}"`);
47
+
48
+ if (handlers.length === 0) {
49
+ log.debug(
50
+ () =>
51
+ `⚠️ No dialog handlers registered — auto-dismissing "${type}" dialog`
52
+ );
53
+ try {
54
+ await dialog.dismiss();
55
+ } catch (e) {
56
+ log.debug(() => `⚠️ Failed to auto-dismiss dialog: ${e.message}`);
57
+ }
58
+ return;
59
+ }
60
+
61
+ let handled = false;
62
+
63
+ for (const fn of handlers) {
64
+ try {
65
+ await fn(dialog);
66
+ handled = true;
67
+ } catch (e) {
68
+ log.debug(() => `⚠️ Error in dialog handler: ${e.message}`);
69
+ }
70
+ }
71
+
72
+ // Safety net: if no handler successfully ran, auto-dismiss
73
+ if (!handled) {
74
+ log.debug(
75
+ () =>
76
+ `⚠️ All dialog handlers failed — auto-dismissing "${type}" dialog`
77
+ );
78
+ try {
79
+ await dialog.dismiss();
80
+ } catch (e) {
81
+ log.debug(() => `⚠️ Failed to auto-dismiss dialog: ${e.message}`);
82
+ }
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Add a dialog event handler
88
+ * @param {Function} handler - Async function receiving (dialog) object
89
+ * dialog.type() → 'alert' | 'confirm' | 'prompt' | 'beforeunload'
90
+ * dialog.message() → The dialog message text
91
+ * dialog.accept(text?) → Accept / confirm (optional text for prompts)
92
+ * dialog.dismiss() → Dismiss / cancel the dialog
93
+ */
94
+ function onDialog(handler) {
95
+ if (typeof handler !== 'function') {
96
+ throw new Error('Dialog handler must be a function');
97
+ }
98
+ handlers.push(handler);
99
+ log.debug(() => `🔌 Dialog handler registered (total: ${handlers.length})`);
100
+ }
101
+
102
+ /**
103
+ * Remove a dialog event handler
104
+ * @param {Function} handler - The handler function to remove
105
+ */
106
+ function offDialog(handler) {
107
+ const index = handlers.indexOf(handler);
108
+ if (index !== -1) {
109
+ handlers.splice(index, 1);
110
+ log.debug(
111
+ () => `🔌 Dialog handler removed (remaining: ${handlers.length})`
112
+ );
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Remove all dialog event handlers
118
+ */
119
+ function clearDialogHandlers() {
120
+ handlers.length = 0;
121
+ log.debug(() => '🔌 All dialog handlers cleared');
122
+ }
123
+
124
+ /**
125
+ * Start listening for dialog events on the page
126
+ */
127
+ function startListening() {
128
+ if (isListening) {
129
+ return;
130
+ }
131
+ page.on('dialog', handleDialog);
132
+ isListening = true;
133
+ log.debug(() => '🔌 Dialog manager started');
134
+ }
135
+
136
+ /**
137
+ * Stop listening for dialog events on the page
138
+ */
139
+ function stopListening() {
140
+ if (!isListening) {
141
+ return;
142
+ }
143
+ page.off('dialog', handleDialog);
144
+ isListening = false;
145
+ log.debug(() => '🔌 Dialog manager stopped');
146
+ }
147
+
148
+ return {
149
+ // Handler registration
150
+ onDialog,
151
+ offDialog,
152
+ clearDialogHandlers,
153
+
154
+ // Lifecycle
155
+ startListening,
156
+ stopListening,
157
+ };
158
+ }
@@ -176,6 +176,46 @@ export class EngineAdapter {
176
176
  throw new Error('focus() must be implemented by subclass');
177
177
  }
178
178
 
179
+ // ============================================================================
180
+ // Page-level Keyboard Operations
181
+ // ============================================================================
182
+
183
+ /**
184
+ * Press a key at the page level (e.g. 'Escape', 'Enter', 'Tab')
185
+ * @param {string} key - Key name (Playwright/Puppeteer key format)
186
+ * @returns {Promise<void>}
187
+ */
188
+ async keyboardPress(key) {
189
+ throw new Error('keyboardPress() must be implemented by subclass');
190
+ }
191
+
192
+ /**
193
+ * Type text at the page level (dispatches key events for each character)
194
+ * @param {string} text - Text to type
195
+ * @returns {Promise<void>}
196
+ */
197
+ async keyboardType(text) {
198
+ throw new Error('keyboardType() must be implemented by subclass');
199
+ }
200
+
201
+ /**
202
+ * Hold a key down at the page level
203
+ * @param {string} key - Key name
204
+ * @returns {Promise<void>}
205
+ */
206
+ async keyboardDown(key) {
207
+ throw new Error('keyboardDown() must be implemented by subclass');
208
+ }
209
+
210
+ /**
211
+ * Release a held key at the page level
212
+ * @param {string} key - Key name
213
+ * @returns {Promise<void>}
214
+ */
215
+ async keyboardUp(key) {
216
+ throw new Error('keyboardUp() must be implemented by subclass');
217
+ }
218
+
179
219
  // ============================================================================
180
220
  // Page-level Operations
181
221
  // ============================================================================
@@ -298,6 +338,26 @@ export class PlaywrightAdapter extends EngineAdapter {
298
338
  await locatorOrElement.focus();
299
339
  }
300
340
 
341
+ // ============================================================================
342
+ // Page-level Keyboard Operations
343
+ // ============================================================================
344
+
345
+ async keyboardPress(key) {
346
+ await this.page.keyboard.press(key);
347
+ }
348
+
349
+ async keyboardType(text) {
350
+ await this.page.keyboard.type(text);
351
+ }
352
+
353
+ async keyboardDown(key) {
354
+ await this.page.keyboard.down(key);
355
+ }
356
+
357
+ async keyboardUp(key) {
358
+ await this.page.keyboard.up(key);
359
+ }
360
+
301
361
  // ============================================================================
302
362
  // Page-level Operations
303
363
  // ============================================================================
@@ -433,6 +493,26 @@ export class PuppeteerAdapter extends EngineAdapter {
433
493
  await locatorOrElement.focus();
434
494
  }
435
495
 
496
+ // ============================================================================
497
+ // Page-level Keyboard Operations
498
+ // ============================================================================
499
+
500
+ async keyboardPress(key) {
501
+ await this.page.keyboard.press(key);
502
+ }
503
+
504
+ async keyboardType(text) {
505
+ await this.page.keyboard.type(text);
506
+ }
507
+
508
+ async keyboardDown(key) {
509
+ await this.page.keyboard.down(key);
510
+ }
511
+
512
+ async keyboardUp(key) {
513
+ await this.page.keyboard.up(key);
514
+ }
515
+
436
516
  // ============================================================================
437
517
  // Page-level Operations
438
518
  // ============================================================================
package/src/exports.js CHANGED
@@ -20,6 +20,7 @@ export {
20
20
  export { createNetworkTracker } from './core/network-tracker.js';
21
21
  export { createNavigationManager } from './core/navigation-manager.js';
22
22
  export { createPageSessionFactory } from './core/page-session.js';
23
+ export { createDialogManager } from './core/dialog-manager.js';
23
24
 
24
25
  // Re-export engine adapter
25
26
  export {
@@ -42,6 +43,7 @@ export {
42
43
 
43
44
  // Re-export browser management
44
45
  export { launchBrowser } from './browser/launcher.js';
46
+ export { emulateMedia } from './browser/media.js';
45
47
  export {
46
48
  waitForUrlStabilization,
47
49
  goto,
@@ -109,6 +111,8 @@ export {
109
111
  verifyFill,
110
112
  } from './interactions/fill.js';
111
113
 
114
+ export { pressKey, typeText, keyDown, keyUp } from './interactions/keyboard.js';
115
+
112
116
  // Re-export utilities
113
117
  export { wait, evaluate, safeEvaluate } from './utilities/wait.js';
114
118
  export { getUrl, unfocusAddressBar } from './utilities/url.js';
package/src/factory.js CHANGED
@@ -9,6 +9,7 @@ import { detectEngine } from './core/engine-detection.js';
9
9
  import { createNetworkTracker } from './core/network-tracker.js';
10
10
  import { createNavigationManager } from './core/navigation-manager.js';
11
11
  import { createPageSessionFactory } from './core/page-session.js';
12
+ import { createDialogManager } from './core/dialog-manager.js';
12
13
  import {
13
14
  createPageTriggerManager,
14
15
  ActionStoppedError,
@@ -35,6 +36,7 @@ export function makeBrowserCommander(options = {}) {
35
36
  verbose = false,
36
37
  enableNetworkTracking = true,
37
38
  enableNavigationManager = true,
39
+ enableDialogManager = true,
38
40
  } = options;
39
41
 
40
42
  if (!page) {
@@ -61,6 +63,13 @@ export function makeBrowserCommander(options = {}) {
61
63
  let navigationManager = null;
62
64
  let sessionFactory = null;
63
65
 
66
+ // Create DialogManager if enabled
67
+ let dialogManager = null;
68
+ if (enableDialogManager) {
69
+ dialogManager = createDialogManager({ page, engine, log });
70
+ dialogManager.startListening();
71
+ }
72
+
64
73
  // PageTriggerManager (will be initialized after commander is created)
65
74
  let pageTriggerManager = null;
66
75
 
@@ -111,6 +120,9 @@ export function makeBrowserCommander(options = {}) {
111
120
  if (sessionFactory) {
112
121
  await sessionFactory.endAllSessions();
113
122
  }
123
+ if (dialogManager) {
124
+ dialogManager.stopListening();
125
+ }
114
126
  };
115
127
 
116
128
  // Build commander object
@@ -125,6 +137,7 @@ export function makeBrowserCommander(options = {}) {
125
137
  navigationManager,
126
138
  sessionFactory,
127
139
  pageTriggerManager,
140
+ dialogManager,
128
141
 
129
142
  // All bound functions
130
143
  ...boundFunctions,
@@ -172,6 +185,30 @@ export function makeBrowserCommander(options = {}) {
172
185
  throw new Error('pageTrigger requires enableNavigationManager: true');
173
186
  },
174
187
 
188
+ // Dialog event handling API
189
+ // Register a handler: commander.onDialog(async (dialog) => { await dialog.dismiss(); })
190
+ // dialog.type() → 'alert' | 'confirm' | 'prompt' | 'beforeunload'
191
+ // dialog.message() → The dialog message text
192
+ // dialog.accept(text?) → Accept/confirm (optional text for prompts)
193
+ // dialog.dismiss() → Dismiss/cancel the dialog
194
+ onDialog: dialogManager
195
+ ? (fn) => dialogManager.onDialog(fn)
196
+ : () => {
197
+ throw new Error('onDialog requires enableDialogManager: true');
198
+ },
199
+ offDialog: dialogManager
200
+ ? (fn) => dialogManager.offDialog(fn)
201
+ : () => {
202
+ throw new Error('offDialog requires enableDialogManager: true');
203
+ },
204
+ clearDialogHandlers: dialogManager
205
+ ? () => dialogManager.clearDialogHandlers()
206
+ : () => {
207
+ throw new Error(
208
+ 'clearDialogHandlers requires enableDialogManager: true'
209
+ );
210
+ },
211
+
175
212
  // URL condition helpers
176
213
  makeUrlCondition,
177
214
  allConditions,