browser-commander 0.6.0 → 0.8.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,40 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.8.0
4
+
5
+ ### Minor Changes
6
+
7
+ - a681a87: Add unified `page.pdf(options)` method to `EngineAdapter`, `PlaywrightAdapter`, and `PuppeteerAdapter`, eliminating the need for users to access raw page objects via the `page._page || page` workaround. The `pdf()` method is also exposed on the `BrowserCommander` facade via `commander.pdf({ pdfOptions })`.
8
+
9
+ ## 0.7.0
10
+
11
+ ### Minor Changes
12
+
13
+ - Add emulateMedia API for unified color scheme emulation across all engines
14
+
15
+ 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`.
16
+
17
+ Fixes #36
18
+
19
+ - 785eb13: Add unified dialog event handling API (`page.on('dialog', handler)`)
20
+ - New `DialogManager` (`core/dialog-manager.js`) that registers `page.on('dialog')` for both Playwright and Puppeteer
21
+ - `commander.onDialog(handler)` — register a handler for browser dialogs (alert, confirm, prompt, beforeunload)
22
+ - `commander.offDialog(handler)` — remove a previously registered handler
23
+ - `commander.clearDialogHandlers()` — remove all dialog handlers
24
+ - Auto-dismiss behavior when no handlers are registered (prevents page from freezing)
25
+ - `enableDialogManager` option (default: `true`) to opt out if needed
26
+ - Exports `createDialogManager` for low-level usage
27
+ - 19 new unit tests covering all dialog handling scenarios
28
+
29
+ - 80ec5f7: Add page-level keyboard interaction support (issue #37)
30
+
31
+ Expose keyboard input methods on the commander object, enabling users to press
32
+ keys, type text, and hold modifier keys without accessing the raw page object
33
+ directly. New API: `commander.keyboard.press()`, `commander.keyboard.type()`,
34
+ `commander.keyboard.down()`, `commander.keyboard.up()`, and flat aliases
35
+ `commander.pressKey()`, `commander.typeText()`, `commander.keyDown()`,
36
+ `commander.keyUp()`.
37
+
3
38
  ## 0.6.0
4
39
 
5
40
  ### 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
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Example: PDF generation via browser-commander
3
+ *
4
+ * This example shows how to use the unified page.pdf() method
5
+ * instead of the old workaround: `const rawPage = page._page || page`.
6
+ *
7
+ * Supports both Playwright and Puppeteer engines.
8
+ *
9
+ * Usage (Playwright):
10
+ * node examples/pdf-generation.js playwright
11
+ *
12
+ * Usage (Puppeteer):
13
+ * node examples/pdf-generation.js puppeteer
14
+ */
15
+
16
+ import fs from 'node:fs';
17
+ import path from 'node:path';
18
+ import { fileURLToPath } from 'node:url';
19
+
20
+ import { makeBrowserCommander } from '../src/factory.js';
21
+
22
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
23
+ const engine = process.argv[2] || 'playwright';
24
+
25
+ async function generatePdf() {
26
+ let browser;
27
+ let page;
28
+
29
+ if (engine === 'playwright') {
30
+ const { chromium } = await import('playwright');
31
+ browser = await chromium.launch({ headless: true });
32
+ const context = await browser.newContext();
33
+ page = await context.newPage();
34
+ } else if (engine === 'puppeteer') {
35
+ const puppeteer = await import('puppeteer');
36
+ browser = await puppeteer.launch({ headless: true });
37
+ page = await browser.newPage();
38
+ } else {
39
+ console.error(
40
+ `Unknown engine: ${engine}. Use 'playwright' or 'puppeteer'.`
41
+ );
42
+ process.exit(1);
43
+ }
44
+
45
+ try {
46
+ const commander = makeBrowserCommander({ page, verbose: false });
47
+
48
+ // Navigate to a page
49
+ await page.goto('https://example.com');
50
+
51
+ // Generate PDF using the unified API (no workarounds needed!)
52
+ const pdfBuffer = await commander.pdf({
53
+ pdfOptions: {
54
+ format: 'A4',
55
+ printBackground: true,
56
+ margin: { top: '1cm', right: '1cm', bottom: '1cm', left: '1cm' },
57
+ },
58
+ });
59
+
60
+ const outputPath = path.join(__dirname, 'output.pdf');
61
+ fs.writeFileSync(outputPath, pdfBuffer);
62
+
63
+ console.log(
64
+ `PDF generated successfully: ${outputPath} (${pdfBuffer.length} bytes)`
65
+ );
66
+
67
+ await commander.destroy();
68
+ } finally {
69
+ await browser.close();
70
+ }
71
+ }
72
+
73
+ generatePdf().catch((error) => {
74
+ console.error('Error:', error.message);
75
+ process.exit(1);
76
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "browser-commander",
3
- "version": "0.6.0",
3
+ "version": "0.8.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,8 @@ import {
52
53
  checkAndClearFlag,
53
54
  findToggleButton,
54
55
  } from './high-level/universal-logic.js';
56
+ import { pdf } from './browser/pdf.js';
57
+ import { pressKey, typeText, keyDown, keyUp } from './interactions/keyboard.js';
55
58
 
56
59
  /**
57
60
  * Create bound functions for a browser commander instance
@@ -191,6 +194,9 @@ export function createBoundFunctions(options = {}) {
191
194
  const fillTextAreaBound = (opts) =>
192
195
  fillTextArea({ ...opts, page, engine, wait: waitBound, log });
193
196
 
197
+ // Bound media emulation
198
+ const emulateMediaBound = (opts) => emulateMedia({ ...opts, page, engine });
199
+
194
200
  // Bound high-level
195
201
  const waitForUrlConditionBound = (opts) =>
196
202
  waitForUrlCondition({
@@ -210,6 +216,15 @@ export function createBoundFunctions(options = {}) {
210
216
  findByText: findByTextBound,
211
217
  });
212
218
 
219
+ // Bound pdf generation
220
+ const pdfBound = (opts = {}) => pdf({ ...opts, page, engine });
221
+
222
+ // Bound keyboard
223
+ const pressKeyBound = (opts) => pressKey({ ...opts, page, engine });
224
+ const typeTextBound = (opts) => typeText({ ...opts, page, engine });
225
+ const keyDownBound = (opts) => keyDown({ ...opts, page, engine });
226
+ const keyUpBound = (opts) => keyUp({ ...opts, page, engine });
227
+
213
228
  // Wrap functions with text selector support
214
229
  const fillTextAreaWrapped = withTextSelectorSupport(
215
230
  fillTextAreaBound,
@@ -267,6 +282,7 @@ export function createBoundFunctions(options = {}) {
267
282
 
268
283
  // Main API functions
269
284
  wait: waitBound,
285
+ emulateMedia: emulateMediaBound,
270
286
  fillTextArea: fillTextAreaWrapped,
271
287
  clickButton: clickButtonWrapped,
272
288
  evaluate: evaluateBound,
@@ -294,5 +310,23 @@ export function createBoundFunctions(options = {}) {
294
310
  installClickListener: installClickListenerBound,
295
311
  checkAndClearFlag: checkAndClearFlagBound,
296
312
  findToggleButton: findToggleButtonBound,
313
+
314
+ // PDF generation
315
+ pdf: pdfBound,
316
+
317
+ // Page-level keyboard interaction
318
+ // Usage: await commander.keyboard.press('Escape')
319
+ keyboard: {
320
+ press: (key) => pressKeyBound({ key }),
321
+ type: (text) => typeTextBound({ text }),
322
+ down: (key) => keyDownBound({ key }),
323
+ up: (key) => keyUpBound({ key }),
324
+ },
325
+
326
+ // Also expose as individual flat functions for functional-style usage
327
+ pressKey: pressKeyBound,
328
+ typeText: typeTextBound,
329
+ keyDown: keyDownBound,
330
+ keyUp: keyUpBound,
297
331
  };
298
332
  }
@@ -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,31 @@
1
+ /**
2
+ * PDF generation support
3
+ *
4
+ * Wraps Playwright/Puppeteer's page.pdf() method behind a unified interface.
5
+ * Both engines expose identical options: format, printBackground, margin, etc.
6
+ *
7
+ * Note: PDF generation only works in Chromium headless mode.
8
+ * Playwright Firefox and WebKit do not support page.pdf().
9
+ */
10
+
11
+ import { createEngineAdapter } from '../core/engine-adapter.js';
12
+
13
+ /**
14
+ * Generate a PDF of the current page
15
+ *
16
+ * @param {Object} options - Configuration options
17
+ * @param {Object} options.page - Playwright or Puppeteer page object
18
+ * @param {string} options.engine - Engine type ('playwright' or 'puppeteer')
19
+ * @param {Object} options.pdfOptions - PDF generation options passed to the engine
20
+ * @param {string} [options.pdfOptions.format] - Paper format (e.g. 'A4', 'Letter')
21
+ * @param {boolean} [options.pdfOptions.printBackground] - Print background graphics
22
+ * @param {Object} [options.pdfOptions.margin] - Page margins { top, right, bottom, left }
23
+ * @param {string} [options.pdfOptions.path] - File path to save the PDF (optional)
24
+ * @param {string} [options.pdfOptions.scale] - Scale of the webpage rendering (0.1–2)
25
+ * @returns {Promise<Buffer>} - PDF as a Buffer
26
+ */
27
+ export async function pdf(options = {}) {
28
+ const { page, engine, pdfOptions = {} } = options;
29
+ const adapter = createEngineAdapter(page, engine);
30
+ return await adapter.pdf(pdfOptions);
31
+ }
@@ -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
+ }