browser-commander 0.20.0 → 0.21.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 +145 -0
- package/README.md +216 -32
- package/bin/browser-commander.js +16 -0
- package/eslint.config.js +10 -0
- package/examples/react-test-app/src/App.jsx +24 -23
- package/extension/background.js +14 -0
- package/extension/manifest.json +12 -0
- package/extension/relay-handler.js +353 -0
- package/package.json +19 -4
- package/src/browser/attach/differences.js +131 -0
- package/src/browser/attach/extension-relay.js +402 -0
- package/src/browser/attach/index.js +81 -0
- package/src/browser/attach/snapshot-launch.js +146 -0
- package/src/browser/attach/snapshot.js +374 -0
- package/src/browser/attach/websocket.js +615 -0
- package/src/browser/browser-cookie-cache.js +12 -17
- package/src/browser/browser-cookie-credentials.js +6 -9
- package/src/browser/browser-cookies.js +5 -2
- package/src/browser/cdp-session.js +94 -0
- package/src/browser/connector.js +42 -4
- package/src/browser/debugging-port.js +155 -0
- package/src/browser/launch-options.js +63 -52
- package/src/browser/launch-restrictions.json +147 -0
- package/src/browser/launcher.js +329 -133
- package/src/browser/media.js +5 -0
- package/src/browser/migration/bookmarks.js +73 -0
- package/src/browser/migration/chromium-crypto.js +89 -0
- package/src/browser/migration/cookies.js +155 -0
- package/src/browser/migration/extensions.js +187 -0
- package/src/browser/migration/firefox-bookmarks.js +144 -0
- package/src/browser/migration/firefox-der.js +84 -0
- package/src/browser/migration/firefox-nss.js +281 -0
- package/src/browser/migration/firefox.js +334 -0
- package/src/browser/migration/fs-utils.js +51 -0
- package/src/browser/migration/history.js +103 -0
- package/src/browser/migration/index.js +307 -0
- package/src/browser/migration/os-crypt-keys.js +106 -0
- package/src/browser/migration/passwords.js +171 -0
- package/src/browser/migration/preferences.js +132 -0
- package/src/browser/migration/sqlite-snapshot.js +138 -0
- package/src/browser/open-in-user-browser.js +112 -0
- package/src/browser/parity.js +439 -0
- package/src/browser/profile-directory.js +102 -0
- package/src/browser/real-browser.js +513 -306
- package/src/browser/restrictions.js +148 -0
- package/src/browser/system-browser.js +232 -0
- package/src/browser/webdriver.js +572 -0
- package/src/cli/args.js +253 -0
- package/src/cli/commands.js +332 -0
- package/src/cli/dispatcher.js +99 -0
- package/src/cli/handle-methods.js +136 -0
- package/src/cli/handles.js +273 -0
- package/src/cli/high-level-methods.js +310 -0
- package/src/cli/main.js +67 -0
- package/src/cli/modules.js +100 -0
- package/src/cli/rpc-error.js +77 -0
- package/src/cli/script.js +77 -0
- package/src/cli/serve.js +144 -0
- package/src/cli/sessions.js +195 -0
- package/src/core/constants.js +7 -1
- package/src/core/engine-adapter.js +107 -1
- package/src/core/engine-detection.js +18 -3
- package/src/core/navigation-safety.js +4 -0
- package/src/core/webdriver-conversions.js +347 -0
- package/src/core/webdriver-events.js +229 -0
- package/src/core/webdriver-page.js +716 -0
- package/src/downloads/attach.js +6 -0
- package/src/elements/selectors.js +3 -2
- package/src/elements/visibility.js +4 -0
- package/src/exports.js +61 -2
- package/src/factory.js +19 -4
- package/src/fingerprint/apply.js +6 -0
- package/src/fingerprint/automation-parity.js +7 -8
- package/src/fingerprint/limitations.js +1 -1
- package/src/fingerprint/limitations.json +45 -2
- package/src/parity/harness.js +303 -0
- package/src/parity/probe.js +838 -0
- package/src/parity/version-page.js +144 -0
- package/src/utilities/subprocess.js +199 -0
- package/tests/e2e/api-coverage.e2e.test.js +174 -0
- package/tests/e2e/cli.e2e.test.js +160 -0
- package/tests/e2e/fingerprint-parity.e2e.test.js +104 -46
- package/tests/e2e/google-signin-probe.e2e.test.js +112 -0
- package/tests/e2e/measure-parity.e2e.test.js +118 -0
- package/tests/e2e/playwright.e2e.test.js +25 -17
- package/tests/e2e/puppeteer.e2e.test.js +17 -23
- package/tests/e2e/webdriver.e2e.test.js +239 -0
- package/tests/fixtures/firefox-nss-fixtures.mjs +211 -0
- package/tests/helpers/cli-fakes.js +214 -0
- package/tests/helpers/declared-api.js +80 -0
- package/tests/helpers/e2e-browser.js +40 -14
- package/tests/helpers/fake-chrome.js +124 -0
- package/tests/helpers/migration-fixtures.js +242 -0
- package/tests/helpers/temp-directory.js +24 -0
- package/tests/helpers/webdriver-mocks.js +183 -0
- package/tests/unit/browser/attach/extension-relay.test.js +360 -0
- package/tests/unit/browser/attach/snapshot-launch.test.js +312 -0
- package/tests/unit/browser/attach/snapshot.test.js +390 -0
- package/tests/unit/browser/attach/websocket.test.js +240 -0
- package/tests/unit/browser/browser-cookies.test.js +2 -32
- package/tests/unit/browser/cdp-session.test.js +162 -0
- package/tests/unit/browser/connector.test.js +23 -0
- package/tests/unit/browser/launch-building-blocks.test.js +238 -0
- package/tests/unit/browser/launch-options.test.js +41 -22
- package/tests/unit/browser/launcher.test.js +281 -102
- package/tests/unit/browser/media.test.js +13 -1
- package/tests/unit/browser/migration/bookmarks.test.js +69 -0
- package/tests/unit/browser/migration/chromium-crypto.test.js +94 -0
- package/tests/unit/browser/migration/cookies.test.js +94 -0
- package/tests/unit/browser/migration/extensions.test.js +104 -0
- package/tests/unit/browser/migration/firefox-nss.test.js +113 -0
- package/tests/unit/browser/migration/firefox.test.js +139 -0
- package/tests/unit/browser/migration/history.test.js +48 -0
- package/tests/unit/browser/migration/index.test.js +117 -0
- package/tests/unit/browser/migration/os-crypt-keys.test.js +95 -0
- package/tests/unit/browser/migration/passwords.test.js +144 -0
- package/tests/unit/browser/migration/preferences.test.js +77 -0
- package/tests/unit/browser/migration/sqlite-snapshot.test.js +75 -0
- package/tests/unit/browser/open-in-user-browser.test.js +123 -0
- package/tests/unit/browser/parity.test.js +281 -0
- package/tests/unit/browser/real-browser.test.js +458 -33
- package/tests/unit/browser/webdriver.test.js +473 -0
- package/tests/unit/cli/bin.test.js +69 -0
- package/tests/unit/cli/declared-api.test.js +49 -0
- package/tests/unit/cli/dispatcher.test.js +429 -0
- package/tests/unit/cli/handles.test.js +170 -0
- package/tests/unit/cli/main.test.js +409 -0
- package/tests/unit/cli/serve.test.js +167 -0
- package/tests/unit/core/webdriver-conversions.test.js +235 -0
- package/tests/unit/core/webdriver-page.test.js +534 -0
- package/tests/unit/fingerprint/automation-parity.test.js +17 -4
- package/tests/unit/fingerprint/limitations.test.js +1 -1
- package/tests/unit/parity/harness.test.js +138 -0
- package/tests/unit/scripts/ci-timeout-budgets.test.js +4 -0
- package/tests/unit/scripts/feature-parity.test.js +221 -0
- package/tests/unit/traces/reader.test.js +1 -0
- package/tests/unit/traces/recorder.test.js +1 -0
- package/tests/unit/utilities/command-stream-only.test.js +28 -0
- package/tests/unit/utilities/subprocess.test.js +80 -0
|
@@ -606,9 +606,115 @@ export function createEngineAdapter(page, engine) {
|
|
|
606
606
|
return new PlaywrightAdapter(page);
|
|
607
607
|
} else if (engine === 'puppeteer') {
|
|
608
608
|
return new PuppeteerAdapter(page);
|
|
609
|
+
} else if (engine === 'selenium') {
|
|
610
|
+
return new SeleniumAdapter(page);
|
|
609
611
|
} else {
|
|
610
612
|
throw new Error(
|
|
611
|
-
`Unsupported engine: ${engine}. Expected 'playwright' or '
|
|
613
|
+
`Unsupported engine: ${engine}. Expected 'playwright', 'puppeteer' or 'selenium'`
|
|
612
614
|
);
|
|
613
615
|
}
|
|
614
616
|
}
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Selenium (WebDriver) adapter implementation (issue #104).
|
|
620
|
+
*
|
|
621
|
+
* The page is a `WebDriverPage` facade, which already speaks the Puppeteer
|
|
622
|
+
* page API, so everything not listed here is the Puppeteer implementation.
|
|
623
|
+
* The overrides use WebDriver's native element commands where they exist:
|
|
624
|
+
* Element Click, Element Send Keys and Element Clear run the browser's own
|
|
625
|
+
* interactability checks and produce trusted input events.
|
|
626
|
+
*/
|
|
627
|
+
export class SeleniumAdapter extends PuppeteerAdapter {
|
|
628
|
+
getEngineName() {
|
|
629
|
+
return 'selenium';
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
async click(locatorOrElement, options = {}) {
|
|
633
|
+
if (options.force) {
|
|
634
|
+
// Element Click refuses an obscured element; force means "click where
|
|
635
|
+
// it is anyway", which is a pointer action at the element's centre.
|
|
636
|
+
await this.page.evaluate(
|
|
637
|
+
(el) => el.scrollIntoView({ block: 'center', inline: 'center' }),
|
|
638
|
+
locatorOrElement
|
|
639
|
+
);
|
|
640
|
+
await this.page.driver
|
|
641
|
+
.actions({ async: true })
|
|
642
|
+
.move({ origin: locatorOrElement })
|
|
643
|
+
.click()
|
|
644
|
+
.perform();
|
|
645
|
+
} else {
|
|
646
|
+
await locatorOrElement.click();
|
|
647
|
+
}
|
|
648
|
+
// A click may navigate; page.url() should say where it went.
|
|
649
|
+
await this.page.syncUrl();
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
async type(locatorOrElement, text) {
|
|
653
|
+
await locatorOrElement.sendKeys(text);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
async fill(locatorOrElement, text) {
|
|
657
|
+
await locatorOrElement.clear();
|
|
658
|
+
await locatorOrElement.sendKeys(text);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
async focus(locatorOrElement) {
|
|
662
|
+
await this.page.evaluate((el) => el.focus(), locatorOrElement);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// ============================================================================
|
|
666
|
+
// WebDriver and BiDi access
|
|
667
|
+
// ============================================================================
|
|
668
|
+
|
|
669
|
+
/** @returns {Object} The selenium-webdriver WebDriver */
|
|
670
|
+
getDriver() {
|
|
671
|
+
return this.page.driver;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
/** @returns {Promise<Object|null>} The BiDi connection, null for classic */
|
|
675
|
+
getBidi() {
|
|
676
|
+
return this.page.bidi();
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/**
|
|
680
|
+
* Subscribe to a raw BiDi event, e.g. 'log.entryAdded'.
|
|
681
|
+
* @returns {Promise<{unsubscribe: Function}>}
|
|
682
|
+
*/
|
|
683
|
+
onBidiEvent(method, handler) {
|
|
684
|
+
return this.page.subscribeBidi(method, handler);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/**
|
|
688
|
+
* Receive console messages (BiDi `log.entryAdded`).
|
|
689
|
+
* @param {Function} handler - Receives a Puppeteer-shaped ConsoleMessage
|
|
690
|
+
* @returns {Promise<Function>} Resolves once subscribed, to an unsubscribe
|
|
691
|
+
*/
|
|
692
|
+
onConsoleMessage(handler) {
|
|
693
|
+
return this._listen('console', handler);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
/**
|
|
697
|
+
* Receive top-level navigations (BiDi browsingContext events).
|
|
698
|
+
* @param {Function} handler - Receives the new URL
|
|
699
|
+
* @returns {Promise<Function>} Resolves once subscribed, to an unsubscribe
|
|
700
|
+
*/
|
|
701
|
+
onNavigation(handler) {
|
|
702
|
+
return this._listen('framenavigated', (frame) => handler(frame.url()));
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
async _listen(eventName, handler) {
|
|
706
|
+
await this.page.requireBidi(`page "${eventName}" events`);
|
|
707
|
+
this.page.on(eventName, handler);
|
|
708
|
+
await this.page.eventsReady();
|
|
709
|
+
return () => this.page.off(eventName, handler);
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/**
|
|
713
|
+
* Navigate with BiDi `browsingContext.navigate`.
|
|
714
|
+
* @param {string} url
|
|
715
|
+
* @param {Object} [options] - {wait: 'none'|'interactive'|'complete'}
|
|
716
|
+
*/
|
|
717
|
+
navigate(url, options = {}) {
|
|
718
|
+
return this.page.navigateBidi(url, options);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
@@ -2,10 +2,25 @@ import { isVerboseEnabled } from './logger.js';
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Detect which browser automation engine is being used
|
|
5
|
-
* @param {Object} pageOrContext - Page or context object from Playwright or
|
|
6
|
-
*
|
|
5
|
+
* @param {Object} pageOrContext - Page or context object from Playwright or
|
|
6
|
+
* Puppeteer, a selenium-webdriver WebDriver, or a WebDriverPage over one
|
|
7
|
+
* @returns {string} - 'playwright', 'puppeteer' or 'selenium'
|
|
7
8
|
*/
|
|
8
9
|
export function detectEngine(pageOrContext) {
|
|
10
|
+
// Selenium first: the WebDriverPage facade has $eval/$$eval like Puppeteer,
|
|
11
|
+
// and a bare WebDriver has none of the page methods the checks below use.
|
|
12
|
+
if (
|
|
13
|
+
pageOrContext.isWebDriverPage === true ||
|
|
14
|
+
(typeof pageOrContext.findElements === 'function' &&
|
|
15
|
+
typeof pageOrContext.executeScript === 'function' &&
|
|
16
|
+
typeof pageOrContext.getCurrentUrl === 'function')
|
|
17
|
+
) {
|
|
18
|
+
if (isVerboseEnabled()) {
|
|
19
|
+
console.log('🔍 [ENGINE DETECTION] Detected: selenium');
|
|
20
|
+
}
|
|
21
|
+
return 'selenium';
|
|
22
|
+
}
|
|
23
|
+
|
|
9
24
|
const hasEval = !!pageOrContext.$eval;
|
|
10
25
|
const hasEvalAll = !!pageOrContext.$$eval;
|
|
11
26
|
const locatorType = typeof pageOrContext.locator;
|
|
@@ -44,6 +59,6 @@ export function detectEngine(pageOrContext) {
|
|
|
44
59
|
console.log('🔍 [ENGINE DETECTION] Could not detect engine!');
|
|
45
60
|
}
|
|
46
61
|
throw new Error(
|
|
47
|
-
'Unknown browser automation engine. Expected Playwright or Puppeteer page
|
|
62
|
+
'Unknown browser automation engine. Expected a Playwright or Puppeteer page, or a selenium-webdriver WebDriver.'
|
|
48
63
|
);
|
|
49
64
|
}
|
|
@@ -27,6 +27,10 @@ export function isNavigationError(error) {
|
|
|
27
27
|
'Frame was detached',
|
|
28
28
|
'context was destroyed',
|
|
29
29
|
'Page crashed',
|
|
30
|
+
// WebDriver (selenium engine): an element or window from the previous
|
|
31
|
+
// document after a navigation.
|
|
32
|
+
'stale element reference',
|
|
33
|
+
'no such window',
|
|
30
34
|
];
|
|
31
35
|
|
|
32
36
|
return navigationErrorPatterns.some((pattern) =>
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Translations between the Puppeteer-shaped page API the rest of Browser
|
|
3
|
+
* Commander speaks and the WebDriver wire protocol (issue #104).
|
|
4
|
+
*
|
|
5
|
+
* Everything here is a pure function of its arguments, so the facade in
|
|
6
|
+
* `webdriver-page.js` stays about sequencing commands and these stay testable
|
|
7
|
+
* without a browser.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* W3C WebDriver key codes for the named keys the Playwright and Puppeteer
|
|
12
|
+
* keyboards accept (WebDriver specification, "Keyboard actions", table of
|
|
13
|
+
* normalized key values). A single character is sent as itself.
|
|
14
|
+
*/
|
|
15
|
+
export const WEBDRIVER_KEYS = Object.freeze({
|
|
16
|
+
Cancel: '\uE001',
|
|
17
|
+
Help: '\uE002',
|
|
18
|
+
Backspace: '\uE003',
|
|
19
|
+
Tab: '\uE004',
|
|
20
|
+
Clear: '\uE005',
|
|
21
|
+
Enter: '\uE007',
|
|
22
|
+
Shift: '\uE008',
|
|
23
|
+
ShiftLeft: '\uE008',
|
|
24
|
+
Control: '\uE009',
|
|
25
|
+
ControlLeft: '\uE009',
|
|
26
|
+
Alt: '\uE00A',
|
|
27
|
+
AltLeft: '\uE00A',
|
|
28
|
+
Pause: '\uE00B',
|
|
29
|
+
Escape: '\uE00C',
|
|
30
|
+
Space: '\uE00D',
|
|
31
|
+
PageUp: '\uE00E',
|
|
32
|
+
PageDown: '\uE00F',
|
|
33
|
+
End: '\uE010',
|
|
34
|
+
Home: '\uE011',
|
|
35
|
+
ArrowLeft: '\uE012',
|
|
36
|
+
ArrowUp: '\uE013',
|
|
37
|
+
ArrowRight: '\uE014',
|
|
38
|
+
ArrowDown: '\uE015',
|
|
39
|
+
Insert: '\uE016',
|
|
40
|
+
Delete: '\uE017',
|
|
41
|
+
F1: '\uE031',
|
|
42
|
+
F2: '\uE032',
|
|
43
|
+
F3: '\uE033',
|
|
44
|
+
F4: '\uE034',
|
|
45
|
+
F5: '\uE035',
|
|
46
|
+
F6: '\uE036',
|
|
47
|
+
F7: '\uE037',
|
|
48
|
+
F8: '\uE038',
|
|
49
|
+
F9: '\uE039',
|
|
50
|
+
F10: '\uE03A',
|
|
51
|
+
F11: '\uE03B',
|
|
52
|
+
F12: '\uE03C',
|
|
53
|
+
Meta: '\uE03D',
|
|
54
|
+
MetaLeft: '\uE03D',
|
|
55
|
+
Command: '\uE03D',
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Translate one key name into the value a WebDriver key action carries.
|
|
60
|
+
*
|
|
61
|
+
* @param {string} key - A single character or a name such as 'Enter'
|
|
62
|
+
* @returns {string} The WebDriver key value
|
|
63
|
+
*/
|
|
64
|
+
export function toWebDriverKey(key) {
|
|
65
|
+
if (typeof key !== 'string' || key.length === 0) {
|
|
66
|
+
throw new TypeError(`A key must be a non-empty string, got ${key}`);
|
|
67
|
+
}
|
|
68
|
+
if ([...key].length === 1) {
|
|
69
|
+
return key;
|
|
70
|
+
}
|
|
71
|
+
const value = WEBDRIVER_KEYS[key];
|
|
72
|
+
if (value === undefined) {
|
|
73
|
+
throw new Error(
|
|
74
|
+
`Unknown key "${key}" for WebDriver. Use a single character or one of: ${Object.keys(WEBDRIVER_KEYS).join(', ')}`
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
return value;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Split a Playwright-style chord such as 'Control+A' into the keys held
|
|
82
|
+
* together. A lone '+' is the plus key, not a separator.
|
|
83
|
+
*
|
|
84
|
+
* @param {string} key
|
|
85
|
+
* @returns {string[]} WebDriver key values, modifiers first
|
|
86
|
+
*/
|
|
87
|
+
export function toWebDriverChord(key) {
|
|
88
|
+
const parts =
|
|
89
|
+
key.length > 1 && key.includes('+')
|
|
90
|
+
? key.split('+').map((part, index, all) =>
|
|
91
|
+
// 'Control++' holds Control and presses '+'.
|
|
92
|
+
part === '' && index === all.length - 1 ? '+' : part
|
|
93
|
+
)
|
|
94
|
+
: [key];
|
|
95
|
+
return parts.filter((part) => part !== '').map(toWebDriverKey);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Paper sizes Puppeteer and Playwright accept, in inches (width, height). */
|
|
99
|
+
const PAPER_FORMATS_IN = Object.freeze({
|
|
100
|
+
letter: [8.5, 11],
|
|
101
|
+
legal: [8.5, 14],
|
|
102
|
+
tabloid: [11, 17],
|
|
103
|
+
ledger: [17, 11],
|
|
104
|
+
a0: [33.1102, 46.811],
|
|
105
|
+
a1: [23.3858, 33.1102],
|
|
106
|
+
a2: [16.5354, 23.3858],
|
|
107
|
+
a3: [11.6929, 16.5354],
|
|
108
|
+
a4: [8.2677, 11.6929],
|
|
109
|
+
a5: [5.8268, 8.2677],
|
|
110
|
+
a6: [4.1339, 5.8268],
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const CM_PER_UNIT = Object.freeze({
|
|
114
|
+
px: 2.54 / 96,
|
|
115
|
+
in: 2.54,
|
|
116
|
+
cm: 1,
|
|
117
|
+
mm: 0.1,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Convert a Puppeteer length (a number of CSS pixels, or a string with a
|
|
122
|
+
* px/in/cm/mm unit) into the centimetres WebDriver's Print Page expects.
|
|
123
|
+
*
|
|
124
|
+
* @param {number|string} value
|
|
125
|
+
* @returns {number}
|
|
126
|
+
*/
|
|
127
|
+
export function toCentimetres(value) {
|
|
128
|
+
if (typeof value === 'number') {
|
|
129
|
+
return value * CM_PER_UNIT.px;
|
|
130
|
+
}
|
|
131
|
+
const match = /^(-?\d+(?:\.\d+)?)\s*(px|in|cm|mm)?$/i.exec(
|
|
132
|
+
String(value).trim()
|
|
133
|
+
);
|
|
134
|
+
if (!match) {
|
|
135
|
+
throw new Error(`Cannot convert "${value}" to a print length`);
|
|
136
|
+
}
|
|
137
|
+
const unit = (match[2] ?? 'px').toLowerCase();
|
|
138
|
+
return Number(match[1]) * CM_PER_UNIT[unit];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* PDF options that have no WebDriver Print Page equivalent. They are refused
|
|
143
|
+
* rather than dropped, because a PDF without the header a caller asked for is
|
|
144
|
+
* a silently wrong result (limitation `webdriver-print-options`).
|
|
145
|
+
*/
|
|
146
|
+
export const UNSUPPORTED_PRINT_OPTIONS = Object.freeze([
|
|
147
|
+
'displayHeaderFooter',
|
|
148
|
+
'headerTemplate',
|
|
149
|
+
'footerTemplate',
|
|
150
|
+
'preferCSSPageSize',
|
|
151
|
+
'tagged',
|
|
152
|
+
'outline',
|
|
153
|
+
'omitBackground',
|
|
154
|
+
]);
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Translate Puppeteer/Playwright `page.pdf()` options into the parameters of
|
|
158
|
+
* selenium-webdriver's `driver.printPage()`.
|
|
159
|
+
*
|
|
160
|
+
* @param {Object} [options]
|
|
161
|
+
* @returns {Object} printPage options (lengths in centimetres)
|
|
162
|
+
*/
|
|
163
|
+
export function toPrintOptions(options = {}) {
|
|
164
|
+
const unsupported = UNSUPPORTED_PRINT_OPTIONS.filter((name) => options[name]);
|
|
165
|
+
if (unsupported.length > 0) {
|
|
166
|
+
throw new Error(
|
|
167
|
+
`WebDriver Print Page does not support ${unsupported.join(', ')} ` +
|
|
168
|
+
'(see the webdriver-print-options limitation)'
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const print = {};
|
|
173
|
+
if (options.landscape) {
|
|
174
|
+
print.orientation = 'landscape';
|
|
175
|
+
}
|
|
176
|
+
if (options.printBackground !== undefined) {
|
|
177
|
+
print.background = Boolean(options.printBackground);
|
|
178
|
+
}
|
|
179
|
+
if (options.scale !== undefined) {
|
|
180
|
+
print.scale = options.scale;
|
|
181
|
+
}
|
|
182
|
+
if (options.pageRanges) {
|
|
183
|
+
print.pageRanges = String(options.pageRanges)
|
|
184
|
+
.split(',')
|
|
185
|
+
.map((range) => range.trim())
|
|
186
|
+
.filter(Boolean);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (options.format) {
|
|
190
|
+
const size = PAPER_FORMATS_IN[String(options.format).toLowerCase()];
|
|
191
|
+
if (!size) {
|
|
192
|
+
throw new Error(`Unknown paper format: ${options.format}`);
|
|
193
|
+
}
|
|
194
|
+
print.width = size[0] * CM_PER_UNIT.in;
|
|
195
|
+
print.height = size[1] * CM_PER_UNIT.in;
|
|
196
|
+
}
|
|
197
|
+
if (options.width !== undefined) {
|
|
198
|
+
print.width = toCentimetres(options.width);
|
|
199
|
+
}
|
|
200
|
+
if (options.height !== undefined) {
|
|
201
|
+
print.height = toCentimetres(options.height);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
for (const side of ['top', 'right', 'bottom', 'left']) {
|
|
205
|
+
if (options.margin?.[side] !== undefined) {
|
|
206
|
+
print[side] = toCentimetres(options.margin[side]);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return print;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* A WebDriver cookie in the shape `page.cookies()` returns in Puppeteer.
|
|
214
|
+
*
|
|
215
|
+
* @param {Object} cookie - WebDriver cookie
|
|
216
|
+
* @returns {Object}
|
|
217
|
+
*/
|
|
218
|
+
export function fromWebDriverCookie(cookie) {
|
|
219
|
+
const session = cookie.expiry === undefined || cookie.expiry === null;
|
|
220
|
+
return {
|
|
221
|
+
name: cookie.name,
|
|
222
|
+
value: cookie.value,
|
|
223
|
+
domain: cookie.domain,
|
|
224
|
+
path: cookie.path ?? '/',
|
|
225
|
+
expires: session ? -1 : Number(cookie.expiry),
|
|
226
|
+
size: `${cookie.name}${cookie.value}`.length,
|
|
227
|
+
httpOnly: Boolean(cookie.httpOnly),
|
|
228
|
+
secure: Boolean(cookie.secure),
|
|
229
|
+
session,
|
|
230
|
+
...(cookie.sameSite ? { sameSite: cookie.sameSite } : {}),
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* A Puppeteer `setCookie()` argument as a WebDriver cookie.
|
|
236
|
+
*
|
|
237
|
+
* WebDriver can only add a cookie for the document that is loaded, so a
|
|
238
|
+
* cookie's `url` is not a way to target another site here; the domain of the
|
|
239
|
+
* current document applies unless `domain` names a parent of it.
|
|
240
|
+
*
|
|
241
|
+
* @param {Object} cookie - Puppeteer cookie parameter
|
|
242
|
+
* @returns {Object}
|
|
243
|
+
*/
|
|
244
|
+
export function toWebDriverCookie(cookie) {
|
|
245
|
+
const result = { name: cookie.name, value: cookie.value };
|
|
246
|
+
for (const field of ['path', 'domain', 'secure', 'httpOnly', 'sameSite']) {
|
|
247
|
+
if (cookie[field] !== undefined) {
|
|
248
|
+
result[field] = cookie[field];
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
if (typeof cookie.expires === 'number' && cookie.expires > 0) {
|
|
252
|
+
result.expiry = Math.floor(cookie.expires);
|
|
253
|
+
}
|
|
254
|
+
return result;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* The page-side script that runs a function (or evaluates an expression) and
|
|
259
|
+
* reports the document URL in the same round trip.
|
|
260
|
+
*
|
|
261
|
+
* The function source is part of the script body instead of being passed as an
|
|
262
|
+
* argument and compiled in the page: WebDriver compiles the body outside the
|
|
263
|
+
* page's Content-Security-Policy, whereas `new Function()` in the page is
|
|
264
|
+
* refused on any site that forbids `unsafe-eval`.
|
|
265
|
+
*
|
|
266
|
+
* @param {Function|string} pageFunction
|
|
267
|
+
* @returns {string} Script for `driver.executeScript`
|
|
268
|
+
*/
|
|
269
|
+
export function buildEvaluateScript(pageFunction) {
|
|
270
|
+
const call =
|
|
271
|
+
typeof pageFunction === 'function'
|
|
272
|
+
? `(${pageFunction.toString()}).apply(null, args)`
|
|
273
|
+
: `(${pageFunction})`;
|
|
274
|
+
return [
|
|
275
|
+
'const args = Array.prototype.slice.call(arguments);',
|
|
276
|
+
`return (async () => [await ${call}, location.href])();`,
|
|
277
|
+
].join('\n');
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const SCRIPT_UNSAFE_CHARACTERS = {
|
|
281
|
+
'<': '\\u003C',
|
|
282
|
+
'>': '\\u003E',
|
|
283
|
+
'/': '\\u002F',
|
|
284
|
+
'\u2028': '\\u2028',
|
|
285
|
+
'\u2029': '\\u2029',
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* A JSON value as a JavaScript literal that is safe to splice into script
|
|
290
|
+
* source: `JSON.stringify` alone leaves `</script>`, U+2028 and U+2029, which
|
|
291
|
+
* end or break the surrounding code.
|
|
292
|
+
*
|
|
293
|
+
* @param {*} value - JSON-serialisable value
|
|
294
|
+
* @returns {string}
|
|
295
|
+
*/
|
|
296
|
+
export function toScriptLiteral(value) {
|
|
297
|
+
return JSON.stringify(value).replace(
|
|
298
|
+
/[<>/\u2028\u2029]/gu,
|
|
299
|
+
(character) => SCRIPT_UNSAFE_CHARACTERS[character]
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* A BiDi `log.entryAdded` entry as a Puppeteer ConsoleMessage.
|
|
305
|
+
*
|
|
306
|
+
* @param {Object} params
|
|
307
|
+
* @returns {Object}
|
|
308
|
+
*/
|
|
309
|
+
export function toConsoleMessage(params) {
|
|
310
|
+
const type = params.method ?? params.level ?? 'log';
|
|
311
|
+
return {
|
|
312
|
+
type: () => (type === 'warn' ? 'warning' : type),
|
|
313
|
+
text: () => params.text ?? '',
|
|
314
|
+
args: () => params.args ?? [],
|
|
315
|
+
location: () => ({
|
|
316
|
+
url: params.source?.url ?? params.stackTrace?.callFrames?.[0]?.url,
|
|
317
|
+
}),
|
|
318
|
+
level: params.level,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* A BiDi network event as a Puppeteer HTTPRequest.
|
|
324
|
+
*
|
|
325
|
+
* @param {Object} params - BiDi network event parameters
|
|
326
|
+
* @returns {Object}
|
|
327
|
+
*/
|
|
328
|
+
export function toNetworkRequest(params) {
|
|
329
|
+
const request = params.request ?? {};
|
|
330
|
+
const headers = Object.fromEntries(
|
|
331
|
+
(request.headers ?? []).map(({ name, value }) => [
|
|
332
|
+
name.toLowerCase(),
|
|
333
|
+
value?.value ?? value,
|
|
334
|
+
])
|
|
335
|
+
);
|
|
336
|
+
return {
|
|
337
|
+
id: request.request,
|
|
338
|
+
url: () => request.url,
|
|
339
|
+
method: () => request.method,
|
|
340
|
+
headers: () => headers,
|
|
341
|
+
isNavigationRequest: () => Boolean(params.navigation),
|
|
342
|
+
failure: () =>
|
|
343
|
+
params.errorText ? { errorText: params.errorText } : undefined,
|
|
344
|
+
response: () =>
|
|
345
|
+
params.response ? { status: () => params.response.status } : null,
|
|
346
|
+
};
|
|
347
|
+
}
|