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
|
@@ -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
|
});
|
package/tests/helpers/mocks.js
CHANGED
|
@@ -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();
|