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/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,7 +147,10 @@ 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
|
},
|
|
152
155
|
};
|
|
153
156
|
}
|
|
@@ -304,7 +307,10 @@ export function createMockPuppeteerPage(options = {}) {
|
|
|
304
307
|
click: async (sel, opts = {}) => {},
|
|
305
308
|
type: async (sel, text, opts = {}) => {},
|
|
306
309
|
keyboard: {
|
|
310
|
+
press: async (key) => {},
|
|
307
311
|
type: async (text) => {},
|
|
312
|
+
down: async (key) => {},
|
|
313
|
+
up: async (key) => {},
|
|
308
314
|
},
|
|
309
315
|
};
|
|
310
316
|
|
|
@@ -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();
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { describe, it, beforeEach } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import { emulateMedia } from '../../../src/browser/media.js';
|
|
4
|
+
|
|
5
|
+
describe('emulateMedia', () => {
|
|
6
|
+
describe('input validation', () => {
|
|
7
|
+
it('should throw if page is not provided', async () => {
|
|
8
|
+
await assert.rejects(
|
|
9
|
+
() => emulateMedia({ engine: 'playwright', colorScheme: 'dark' }),
|
|
10
|
+
/page is required/
|
|
11
|
+
);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('should throw if engine is not provided', async () => {
|
|
15
|
+
const mockPage = {};
|
|
16
|
+
await assert.rejects(
|
|
17
|
+
() => emulateMedia({ page: mockPage, colorScheme: 'dark' }),
|
|
18
|
+
/engine is required/
|
|
19
|
+
);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should throw for invalid colorScheme', async () => {
|
|
23
|
+
const mockPage = {};
|
|
24
|
+
await assert.rejects(
|
|
25
|
+
() =>
|
|
26
|
+
emulateMedia({
|
|
27
|
+
page: mockPage,
|
|
28
|
+
engine: 'playwright',
|
|
29
|
+
colorScheme: 'invalid',
|
|
30
|
+
}),
|
|
31
|
+
/Invalid colorScheme/
|
|
32
|
+
);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should throw for unsupported engine', async () => {
|
|
36
|
+
const mockPage = {};
|
|
37
|
+
await assert.rejects(
|
|
38
|
+
() =>
|
|
39
|
+
emulateMedia({
|
|
40
|
+
page: mockPage,
|
|
41
|
+
engine: 'selenium',
|
|
42
|
+
colorScheme: 'dark',
|
|
43
|
+
}),
|
|
44
|
+
/Unsupported engine/
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe('Playwright engine', () => {
|
|
50
|
+
let capturedMediaOptions;
|
|
51
|
+
let mockPage;
|
|
52
|
+
|
|
53
|
+
beforeEach(() => {
|
|
54
|
+
capturedMediaOptions = null;
|
|
55
|
+
mockPage = {
|
|
56
|
+
emulateMedia: async (options) => {
|
|
57
|
+
capturedMediaOptions = options;
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should call page.emulateMedia with colorScheme: dark', async () => {
|
|
63
|
+
await emulateMedia({
|
|
64
|
+
page: mockPage,
|
|
65
|
+
engine: 'playwright',
|
|
66
|
+
colorScheme: 'dark',
|
|
67
|
+
});
|
|
68
|
+
assert.deepStrictEqual(capturedMediaOptions, { colorScheme: 'dark' });
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should call page.emulateMedia with colorScheme: light', async () => {
|
|
72
|
+
await emulateMedia({
|
|
73
|
+
page: mockPage,
|
|
74
|
+
engine: 'playwright',
|
|
75
|
+
colorScheme: 'light',
|
|
76
|
+
});
|
|
77
|
+
assert.deepStrictEqual(capturedMediaOptions, { colorScheme: 'light' });
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should call page.emulateMedia with colorScheme: no-preference', async () => {
|
|
81
|
+
await emulateMedia({
|
|
82
|
+
page: mockPage,
|
|
83
|
+
engine: 'playwright',
|
|
84
|
+
colorScheme: 'no-preference',
|
|
85
|
+
});
|
|
86
|
+
assert.deepStrictEqual(capturedMediaOptions, {
|
|
87
|
+
colorScheme: 'no-preference',
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('should call page.emulateMedia with colorScheme: null to reset', async () => {
|
|
92
|
+
await emulateMedia({
|
|
93
|
+
page: mockPage,
|
|
94
|
+
engine: 'playwright',
|
|
95
|
+
colorScheme: null,
|
|
96
|
+
});
|
|
97
|
+
assert.deepStrictEqual(capturedMediaOptions, { colorScheme: null });
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('should call page.emulateMedia with empty options when no colorScheme given', async () => {
|
|
101
|
+
await emulateMedia({
|
|
102
|
+
page: mockPage,
|
|
103
|
+
engine: 'playwright',
|
|
104
|
+
});
|
|
105
|
+
assert.deepStrictEqual(capturedMediaOptions, {});
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe('Puppeteer engine', () => {
|
|
110
|
+
let capturedFeatures;
|
|
111
|
+
let mockPage;
|
|
112
|
+
|
|
113
|
+
beforeEach(() => {
|
|
114
|
+
capturedFeatures = null;
|
|
115
|
+
mockPage = {
|
|
116
|
+
emulateMediaFeatures: async (features) => {
|
|
117
|
+
capturedFeatures = features;
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('should call page.emulateMediaFeatures with dark colorScheme', async () => {
|
|
123
|
+
await emulateMedia({
|
|
124
|
+
page: mockPage,
|
|
125
|
+
engine: 'puppeteer',
|
|
126
|
+
colorScheme: 'dark',
|
|
127
|
+
});
|
|
128
|
+
assert.deepStrictEqual(capturedFeatures, [
|
|
129
|
+
{ name: 'prefers-color-scheme', value: 'dark' },
|
|
130
|
+
]);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('should call page.emulateMediaFeatures with light colorScheme', async () => {
|
|
134
|
+
await emulateMedia({
|
|
135
|
+
page: mockPage,
|
|
136
|
+
engine: 'puppeteer',
|
|
137
|
+
colorScheme: 'light',
|
|
138
|
+
});
|
|
139
|
+
assert.deepStrictEqual(capturedFeatures, [
|
|
140
|
+
{ name: 'prefers-color-scheme', value: 'light' },
|
|
141
|
+
]);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('should call page.emulateMediaFeatures with no-preference colorScheme', async () => {
|
|
145
|
+
await emulateMedia({
|
|
146
|
+
page: mockPage,
|
|
147
|
+
engine: 'puppeteer',
|
|
148
|
+
colorScheme: 'no-preference',
|
|
149
|
+
});
|
|
150
|
+
assert.deepStrictEqual(capturedFeatures, [
|
|
151
|
+
{ name: 'prefers-color-scheme', value: 'no-preference' },
|
|
152
|
+
]);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('should reset by passing empty string when colorScheme is null', async () => {
|
|
156
|
+
await emulateMedia({
|
|
157
|
+
page: mockPage,
|
|
158
|
+
engine: 'puppeteer',
|
|
159
|
+
colorScheme: null,
|
|
160
|
+
});
|
|
161
|
+
assert.deepStrictEqual(capturedFeatures, [
|
|
162
|
+
{ name: 'prefers-color-scheme', value: '' },
|
|
163
|
+
]);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('should reset by passing empty string when colorScheme is undefined', async () => {
|
|
167
|
+
await emulateMedia({
|
|
168
|
+
page: mockPage,
|
|
169
|
+
engine: 'puppeteer',
|
|
170
|
+
});
|
|
171
|
+
assert.deepStrictEqual(capturedFeatures, [
|
|
172
|
+
{ name: 'prefers-color-scheme', value: '' },
|
|
173
|
+
]);
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
});
|