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 +35 -0
- package/README.md +32 -0
- package/examples/pdf-generation.js +76 -0
- package/package.json +1 -1
- package/src/README.md +31 -0
- package/src/bindings.js +34 -0
- package/src/browser/launcher.js +27 -2
- package/src/browser/media.js +57 -0
- package/src/browser/pdf.js +31 -0
- package/src/core/dialog-manager.js +158 -0
- package/src/core/engine-adapter.js +97 -0
- package/src/exports.js +6 -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 +8 -0
- package/tests/unit/bindings.test.js +72 -0
- package/tests/unit/browser/media.test.js +176 -0
- package/tests/unit/browser/pdf.test.js +80 -0
- package/tests/unit/core/dialog-manager.test.js +310 -0
- package/tests/unit/core/engine-adapter.test.js +21 -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.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
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
|
}
|
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,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
|
+
}
|