browser-commander 0.5.4 → 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 +35 -0
- package/README.md +93 -0
- package/package.json +1 -1
- package/src/README.md +31 -0
- package/src/bindings.js +27 -0
- package/src/browser/launcher.js +27 -2
- package/src/browser/media.js +57 -0
- package/src/core/dialog-manager.js +158 -0
- package/src/core/engine-adapter.js +80 -0
- package/src/exports.js +4 -0
- package/src/factory.js +37 -0
- package/src/interactions/keyboard.js +91 -0
- package/tests/e2e/playwright.e2e.test.js +84 -0
- package/tests/e2e/puppeteer.e2e.test.js +71 -0
- package/tests/helpers/mocks.js +6 -0
- package/tests/unit/bindings.test.js +72 -0
- package/tests/unit/browser/media.test.js +176 -0
- package/tests/unit/core/dialog-manager.test.js +310 -0
- package/tests/unit/factory.test.js +57 -0
- package/tests/unit/interactions/keyboard.test.js +316 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,40 @@
|
|
|
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
|
+
|
|
32
|
+
## 0.6.0
|
|
33
|
+
|
|
34
|
+
### Minor Changes
|
|
35
|
+
|
|
36
|
+
- 7d83530: Document extensibility escape hatch: `commander.page` and `launchBrowser()` return values expose the raw underlying Playwright/Puppeteer page object as an official mechanism for accessing engine-specific APIs not yet supported by browser-commander (e.g. `page.pdf()`, `page.emulateMedia()`, `page.keyboard`, `page.on('dialog', ...)`). Adds tests verifying `commander.page` is the exact raw page object.
|
|
37
|
+
|
|
3
38
|
## 0.5.4
|
|
4
39
|
|
|
5
40
|
### Patch 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
|
|
@@ -318,6 +350,67 @@ action: async (ctx) => {
|
|
|
318
350
|
};
|
|
319
351
|
```
|
|
320
352
|
|
|
353
|
+
## Extensibility / Escape Hatch
|
|
354
|
+
|
|
355
|
+
`browser-commander` cannot anticipate every browser API. When you need an API that is not yet supported, you can access the raw underlying engine objects directly as an **official extensibility escape hatch**.
|
|
356
|
+
|
|
357
|
+
### Using `commander.page` for engine-specific APIs
|
|
358
|
+
|
|
359
|
+
`makeBrowserCommander` exposes `commander.page` — this is the **raw Playwright or Puppeteer page object**, not a wrapper. Use it directly for APIs browser-commander doesn't yet support:
|
|
360
|
+
|
|
361
|
+
```javascript
|
|
362
|
+
const { browser, page } = await launchBrowser({ engine: 'playwright' });
|
|
363
|
+
const commander = makeBrowserCommander({ page });
|
|
364
|
+
|
|
365
|
+
// Access engine-specific API via commander.page
|
|
366
|
+
// Example: PDF generation (issue #35)
|
|
367
|
+
const pdfBuffer = await commander.page.pdf({
|
|
368
|
+
format: 'A4',
|
|
369
|
+
printBackground: true,
|
|
370
|
+
margin: { top: '1cm', right: '1cm', bottom: '1cm', left: '1cm' },
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
// Example: Color scheme emulation (issue #36)
|
|
374
|
+
await commander.page.emulateMedia({ colorScheme: 'dark' });
|
|
375
|
+
|
|
376
|
+
// Example: Keyboard interactions (issue #37)
|
|
377
|
+
await commander.page.keyboard.press('Escape');
|
|
378
|
+
|
|
379
|
+
// Example: Dialog handling (issue #38)
|
|
380
|
+
commander.page.on('dialog', async (dialog) => {
|
|
381
|
+
await dialog.dismiss();
|
|
382
|
+
});
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
### Using `launchBrowser` raw return values
|
|
386
|
+
|
|
387
|
+
`launchBrowser()` returns the raw `{ browser, page }` objects from the underlying engine. You can use these directly:
|
|
388
|
+
|
|
389
|
+
```javascript
|
|
390
|
+
const { browser, page } = await launchBrowser({ engine: 'playwright' });
|
|
391
|
+
|
|
392
|
+
// Use raw page directly for engine-specific APIs
|
|
393
|
+
await page.pdf({ format: 'A4' });
|
|
394
|
+
|
|
395
|
+
// Or create a commander for the unified API
|
|
396
|
+
const commander = makeBrowserCommander({ page });
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
### No more `_page` hacks
|
|
400
|
+
|
|
401
|
+
If you previously used `page._page || page` to access the raw page, replace it with `commander.page`:
|
|
402
|
+
|
|
403
|
+
```javascript
|
|
404
|
+
// BEFORE (fragile hack):
|
|
405
|
+
const rawPage = page._page || page;
|
|
406
|
+
await rawPage.pdf({ format: 'A4' });
|
|
407
|
+
|
|
408
|
+
// AFTER (official API):
|
|
409
|
+
await commander.page.pdf({ format: 'A4' });
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
This is the **official extensibility mechanism** while awaiting browser-commander to add first-class support for these APIs. Please [report missing APIs](https://github.com/link-foundation/browser-commander/issues) so they can be added.
|
|
413
|
+
|
|
321
414
|
## Debugging
|
|
322
415
|
|
|
323
416
|
Enable verbose mode for detailed logs:
|
package/package.json
CHANGED
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
|
}
|
package/src/browser/launcher.js
CHANGED
|
@@ -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
|
-
|
|
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';
|