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.
@@ -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
  // ============================================================================
@@ -197,6 +237,15 @@ export class EngineAdapter {
197
237
  getMainFrame() {
198
238
  throw new Error('getMainFrame() must be implemented by subclass');
199
239
  }
240
+
241
+ /**
242
+ * Generate a PDF of the current page
243
+ * @param {Object} options - PDF options (format, printBackground, margin, etc.)
244
+ * @returns {Promise<Buffer>} - PDF buffer
245
+ */
246
+ async pdf(options = {}) {
247
+ throw new Error('pdf() must be implemented by subclass');
248
+ }
200
249
  }
201
250
 
202
251
  /**
@@ -298,6 +347,26 @@ export class PlaywrightAdapter extends EngineAdapter {
298
347
  await locatorOrElement.focus();
299
348
  }
300
349
 
350
+ // ============================================================================
351
+ // Page-level Keyboard Operations
352
+ // ============================================================================
353
+
354
+ async keyboardPress(key) {
355
+ await this.page.keyboard.press(key);
356
+ }
357
+
358
+ async keyboardType(text) {
359
+ await this.page.keyboard.type(text);
360
+ }
361
+
362
+ async keyboardDown(key) {
363
+ await this.page.keyboard.down(key);
364
+ }
365
+
366
+ async keyboardUp(key) {
367
+ await this.page.keyboard.up(key);
368
+ }
369
+
301
370
  // ============================================================================
302
371
  // Page-level Operations
303
372
  // ============================================================================
@@ -329,6 +398,10 @@ export class PlaywrightAdapter extends EngineAdapter {
329
398
  getMainFrame() {
330
399
  return this.page.mainFrame();
331
400
  }
401
+
402
+ async pdf(options = {}) {
403
+ return await this.page.pdf(options);
404
+ }
332
405
  }
333
406
 
334
407
  /**
@@ -433,6 +506,26 @@ export class PuppeteerAdapter extends EngineAdapter {
433
506
  await locatorOrElement.focus();
434
507
  }
435
508
 
509
+ // ============================================================================
510
+ // Page-level Keyboard Operations
511
+ // ============================================================================
512
+
513
+ async keyboardPress(key) {
514
+ await this.page.keyboard.press(key);
515
+ }
516
+
517
+ async keyboardType(text) {
518
+ await this.page.keyboard.type(text);
519
+ }
520
+
521
+ async keyboardDown(key) {
522
+ await this.page.keyboard.down(key);
523
+ }
524
+
525
+ async keyboardUp(key) {
526
+ await this.page.keyboard.up(key);
527
+ }
528
+
436
529
  // ============================================================================
437
530
  // Page-level Operations
438
531
  // ============================================================================
@@ -445,6 +538,10 @@ export class PuppeteerAdapter extends EngineAdapter {
445
538
  getMainFrame() {
446
539
  return this.page.mainFrame();
447
540
  }
541
+
542
+ async pdf(options = {}) {
543
+ return await this.page.pdf(options);
544
+ }
448
545
  }
449
546
 
450
547
  /**
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,
@@ -53,6 +55,8 @@ export {
53
55
  verifyNavigation,
54
56
  } from './browser/navigation.js';
55
57
 
58
+ export { pdf } from './browser/pdf.js';
59
+
56
60
  // Re-export element operations
57
61
  export {
58
62
  createPlaywrightLocator,
@@ -109,6 +113,8 @@ export {
109
113
  verifyFill,
110
114
  } from './interactions/fill.js';
111
115
 
116
+ export { pressKey, typeText, keyDown, keyUp } from './interactions/keyboard.js';
117
+
112
118
  // Re-export utilities
113
119
  export { wait, evaluate, safeEvaluate } from './utilities/wait.js';
114
120
  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,
@@ -0,0 +1,91 @@
1
+ import { createEngineAdapter } from '../core/engine-adapter.js';
2
+
3
+ /**
4
+ * Press a key at the page level (e.g. 'Escape', 'Enter', 'Tab').
5
+ *
6
+ * Supported key names follow the Playwright/Puppeteer convention:
7
+ * https://playwright.dev/docs/api/class-keyboard#keyboard-press
8
+ *
9
+ * @param {Object} options - Configuration options
10
+ * @param {Object} options.page - Browser page object
11
+ * @param {string} options.engine - Engine type ('playwright' or 'puppeteer')
12
+ * @param {string} options.key - Key to press (e.g. 'Escape', 'Enter', 'Tab', 'ArrowDown')
13
+ * @param {Object} [options.adapter] - Engine adapter (optional, created if not provided)
14
+ * @returns {Promise<void>}
15
+ */
16
+ export async function pressKey(options = {}) {
17
+ const { page, engine, key, adapter: providedAdapter } = options;
18
+
19
+ if (!key) {
20
+ throw new Error('pressKey: key is required in options');
21
+ }
22
+
23
+ const adapter = providedAdapter || createEngineAdapter(page, engine);
24
+ await adapter.keyboardPress(key);
25
+ }
26
+
27
+ /**
28
+ * Type text at the page level (dispatches key events for each character).
29
+ * Unlike element-level fill/type, this sends keyboard events to whatever
30
+ * element is currently focused on the page.
31
+ *
32
+ * @param {Object} options - Configuration options
33
+ * @param {Object} options.page - Browser page object
34
+ * @param {string} options.engine - Engine type ('playwright' or 'puppeteer')
35
+ * @param {string} options.text - Text to type
36
+ * @param {Object} [options.adapter] - Engine adapter (optional, created if not provided)
37
+ * @returns {Promise<void>}
38
+ */
39
+ export async function typeText(options = {}) {
40
+ const { page, engine, text, adapter: providedAdapter } = options;
41
+
42
+ if (!text) {
43
+ throw new Error('typeText: text is required in options');
44
+ }
45
+
46
+ const adapter = providedAdapter || createEngineAdapter(page, engine);
47
+ await adapter.keyboardType(text);
48
+ }
49
+
50
+ /**
51
+ * Hold a key down at the page level.
52
+ * Must be paired with keyUp() to release the key.
53
+ *
54
+ * @param {Object} options - Configuration options
55
+ * @param {Object} options.page - Browser page object
56
+ * @param {string} options.engine - Engine type ('playwright' or 'puppeteer')
57
+ * @param {string} options.key - Key to hold down
58
+ * @param {Object} [options.adapter] - Engine adapter (optional, created if not provided)
59
+ * @returns {Promise<void>}
60
+ */
61
+ export async function keyDown(options = {}) {
62
+ const { page, engine, key, adapter: providedAdapter } = options;
63
+
64
+ if (!key) {
65
+ throw new Error('keyDown: key is required in options');
66
+ }
67
+
68
+ const adapter = providedAdapter || createEngineAdapter(page, engine);
69
+ await adapter.keyboardDown(key);
70
+ }
71
+
72
+ /**
73
+ * Release a held key at the page level.
74
+ *
75
+ * @param {Object} options - Configuration options
76
+ * @param {Object} options.page - Browser page object
77
+ * @param {string} options.engine - Engine type ('playwright' or 'puppeteer')
78
+ * @param {string} options.key - Key to release
79
+ * @param {Object} [options.adapter] - Engine adapter (optional, created if not provided)
80
+ * @returns {Promise<void>}
81
+ */
82
+ export async function keyUp(options = {}) {
83
+ const { page, engine, key, adapter: providedAdapter } = options;
84
+
85
+ if (!key) {
86
+ throw new Error('keyUp: key is required in options');
87
+ }
88
+
89
+ const adapter = providedAdapter || createEngineAdapter(page, engine);
90
+ await adapter.keyboardUp(key);
91
+ }
@@ -438,5 +438,89 @@ describe(
438
438
  assert.ok(elements.length >= 4);
439
439
  });
440
440
  });
441
+
442
+ describe('Keyboard Interactions', () => {
443
+ it('should press Escape key to close modal', async () => {
444
+ if (!commander) {
445
+ return;
446
+ }
447
+
448
+ // Open modal
449
+ await commander.click({ selector: '[data-testid="btn-open-modal"]' });
450
+ await commander.wait({ ms: 100 });
451
+
452
+ // Verify modal is open
453
+ const modalOpen = await commander.isVisible({
454
+ selector: '[data-testid="modal"]',
455
+ });
456
+ assert.strictEqual(modalOpen, true);
457
+
458
+ // Close modal with Escape key
459
+ await commander.keyboard.press('Escape');
460
+ await commander.wait({ ms: 150 });
461
+
462
+ // Modal should be closed
463
+ const modalClosed = await commander.isVisible({
464
+ selector: '[data-testid="modal"]',
465
+ });
466
+ assert.strictEqual(modalClosed, false);
467
+ });
468
+
469
+ it('should type text at page level via keyboard.type', async () => {
470
+ if (!commander) {
471
+ return;
472
+ }
473
+
474
+ // Focus the name input
475
+ await commander.click({ selector: '[data-testid="input-name"]' });
476
+ await commander.wait({ ms: 50 });
477
+
478
+ // Clear and type via page-level keyboard
479
+ await page.fill('[data-testid="input-name"]', '');
480
+ await commander.keyboard.type('Keyboard Test');
481
+ await commander.wait({ ms: 50 });
482
+
483
+ const value = await commander.inputValue({
484
+ selector: '[data-testid="input-name"]',
485
+ });
486
+ assert.ok(value.includes('Keyboard Test'));
487
+ });
488
+
489
+ it('should support keyboard.press via pressKey flat function', async () => {
490
+ if (!commander) {
491
+ return;
492
+ }
493
+
494
+ // Open modal
495
+ await commander.click({ selector: '[data-testid="btn-open-modal"]' });
496
+ await commander.wait({ ms: 100 });
497
+
498
+ const modalOpen = await commander.isVisible({
499
+ selector: '[data-testid="modal"]',
500
+ });
501
+ assert.strictEqual(modalOpen, true);
502
+
503
+ // Close via flat pressKey
504
+ await commander.pressKey({ key: 'Escape' });
505
+ await commander.wait({ ms: 150 });
506
+
507
+ const modalClosed = await commander.isVisible({
508
+ selector: '[data-testid="modal"]',
509
+ });
510
+ assert.strictEqual(modalClosed, false);
511
+ });
512
+
513
+ it('should submit form via Enter key', async () => {
514
+ if (!commander) {
515
+ return;
516
+ }
517
+
518
+ // Focus submit button and press Enter
519
+ await page.focus('[data-testid="btn-increment"]');
520
+ await commander.keyboard.press('Enter');
521
+ await commander.wait({ ms: 100 });
522
+ // Test passes if no error is thrown
523
+ });
524
+ });
441
525
  }
442
526
  );
@@ -405,4 +405,75 @@ describe('E2E Tests - Puppeteer Engine', { skip: !process.env.RUN_E2E }, () => {
405
405
  assert.ok(elapsed >= 90, `Expected at least 90ms, got ${elapsed}`);
406
406
  });
407
407
  });
408
+
409
+ describe('Keyboard Interactions', () => {
410
+ it('should press Escape key to close modal', async () => {
411
+ if (!commander) {
412
+ return;
413
+ }
414
+
415
+ // Open modal
416
+ await commander.click({ selector: '[data-testid="btn-open-modal"]' });
417
+ await commander.wait({ ms: 100 });
418
+
419
+ // Verify modal is open
420
+ const modalOpen = await commander.isVisible({
421
+ selector: '[data-testid="modal"]',
422
+ });
423
+ assert.strictEqual(modalOpen, true);
424
+
425
+ // Close modal with Escape key
426
+ await commander.keyboard.press('Escape');
427
+ await commander.wait({ ms: 150 });
428
+
429
+ // Modal should be closed
430
+ const modalClosed = await commander.isVisible({
431
+ selector: '[data-testid="modal"]',
432
+ });
433
+ assert.strictEqual(modalClosed, false);
434
+ });
435
+
436
+ it('should type text at page level via keyboard.type', async () => {
437
+ if (!commander) {
438
+ return;
439
+ }
440
+
441
+ // Focus the name input
442
+ await commander.click({ selector: '[data-testid="input-name"]' });
443
+ await commander.wait({ ms: 50 });
444
+
445
+ // Type via page-level keyboard
446
+ await commander.keyboard.type('Keyboard Test');
447
+ await commander.wait({ ms: 50 });
448
+
449
+ const value = await commander.inputValue({
450
+ selector: '[data-testid="input-name"]',
451
+ });
452
+ assert.ok(value.includes('Keyboard Test'));
453
+ });
454
+
455
+ it('should support pressKey flat function', async () => {
456
+ if (!commander) {
457
+ return;
458
+ }
459
+
460
+ // Open modal
461
+ await commander.click({ selector: '[data-testid="btn-open-modal"]' });
462
+ await commander.wait({ ms: 100 });
463
+
464
+ const modalOpen = await commander.isVisible({
465
+ selector: '[data-testid="modal"]',
466
+ });
467
+ assert.strictEqual(modalOpen, true);
468
+
469
+ // Close via flat pressKey
470
+ await commander.pressKey({ key: 'Escape' });
471
+ await commander.wait({ ms: 150 });
472
+
473
+ const modalClosed = await commander.isVisible({
474
+ selector: '[data-testid="modal"]',
475
+ });
476
+ assert.strictEqual(modalClosed, false);
477
+ });
478
+ });
408
479
  });
@@ -147,8 +147,12 @@ export function createMockPlaywrightPage(options = {}) {
147
147
  click: async (sel, opts = {}) => {},
148
148
  type: async (sel, text, opts = {}) => {},
149
149
  keyboard: {
150
+ press: async (key) => {},
150
151
  type: async (text) => {},
152
+ down: async (key) => {},
153
+ up: async (key) => {},
151
154
  },
155
+ pdf: async (opts = {}) => Buffer.from('%PDF-1.4 mock playwright'),
152
156
  };
153
157
  }
154
158
 
@@ -304,8 +308,12 @@ export function createMockPuppeteerPage(options = {}) {
304
308
  click: async (sel, opts = {}) => {},
305
309
  type: async (sel, text, opts = {}) => {},
306
310
  keyboard: {
311
+ press: async (key) => {},
307
312
  type: async (text) => {},
313
+ down: async (key) => {},
314
+ up: async (key) => {},
308
315
  },
316
+ pdf: async (opts = {}) => Buffer.from('%PDF-1.4 mock puppeteer'),
309
317
  };
310
318
 
311
319
  return page;
@@ -139,6 +139,46 @@ describe('bindings', () => {
139
139
  typeof bindings.evaluate === 'function',
140
140
  'evaluate should be a function'
141
141
  );
142
+
143
+ // Keyboard - flat functions
144
+ assert.ok(
145
+ typeof bindings.pressKey === 'function',
146
+ 'pressKey should be a function'
147
+ );
148
+ assert.ok(
149
+ typeof bindings.typeText === 'function',
150
+ 'typeText should be a function'
151
+ );
152
+ assert.ok(
153
+ typeof bindings.keyDown === 'function',
154
+ 'keyDown should be a function'
155
+ );
156
+ assert.ok(
157
+ typeof bindings.keyUp === 'function',
158
+ 'keyUp should be a function'
159
+ );
160
+
161
+ // Keyboard - object API
162
+ assert.ok(
163
+ bindings.keyboard && typeof bindings.keyboard === 'object',
164
+ 'keyboard should be an object'
165
+ );
166
+ assert.ok(
167
+ typeof bindings.keyboard.press === 'function',
168
+ 'keyboard.press should be a function'
169
+ );
170
+ assert.ok(
171
+ typeof bindings.keyboard.type === 'function',
172
+ 'keyboard.type should be a function'
173
+ );
174
+ assert.ok(
175
+ typeof bindings.keyboard.down === 'function',
176
+ 'keyboard.down should be a function'
177
+ );
178
+ assert.ok(
179
+ typeof bindings.keyboard.up === 'function',
180
+ 'keyboard.up should be a function'
181
+ );
142
182
  });
143
183
 
144
184
  it('should pre-bind page and engine to functions', async () => {
@@ -200,6 +240,38 @@ describe('bindings', () => {
200
240
  assert.ok(bindings);
201
241
  });
202
242
 
243
+ it('should invoke keyboard.press via bound keyboard object', async () => {
244
+ const pressedKeys = [];
245
+ const page = createMockPlaywrightPage();
246
+ page.keyboard.press = async (key) => pressedKeys.push(key);
247
+ const log = createMockLogger();
248
+
249
+ const bindings = createBoundFunctions({
250
+ page,
251
+ engine: 'playwright',
252
+ log,
253
+ });
254
+
255
+ await bindings.keyboard.press('Escape');
256
+ assert.deepStrictEqual(pressedKeys, ['Escape']);
257
+ });
258
+
259
+ it('should invoke keyboard.type via bound keyboard object', async () => {
260
+ const typedTexts = [];
261
+ const page = createMockPlaywrightPage();
262
+ page.keyboard.type = async (text) => typedTexts.push(text);
263
+ const log = createMockLogger();
264
+
265
+ const bindings = createBoundFunctions({
266
+ page,
267
+ engine: 'playwright',
268
+ log,
269
+ });
270
+
271
+ await bindings.keyboard.type('hello world');
272
+ assert.deepStrictEqual(typedTexts, ['hello world']);
273
+ });
274
+
203
275
  it('should integrate with networkTracker when provided', () => {
204
276
  const page = createMockPlaywrightPage();
205
277
  const log = createMockLogger();