browser-commander 0.19.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 +156 -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 +25 -6
- package/scripts/run-tests.mjs +4 -2
- 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/downloads/manager.js +9 -0
- package/src/downloads/sources.js +71 -0
- package/src/downloads/staging.js +21 -9
- 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/traces/bundle.js +10 -2
- package/src/traces/recorder.js +16 -3
- 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/downloads.e2e.test.js +79 -2
- 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/trace-fixtures.js +18 -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/downloads/manager.test.js +82 -9
- package/tests/unit/downloads/staging.test.js +24 -2
- 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/ci-workflow-policy.test.js +8 -0
- package/tests/unit/scripts/feature-parity.test.js +221 -0
- package/tests/unit/scripts/npm-release-helpers.test.js +3 -2
- package/tests/unit/scripts/pre-commit-config.test.js +2 -1
- package/tests/unit/traces/bundle.test.js +24 -3
- package/tests/unit/traces/links.test.js +7 -0
- package/tests/unit/traces/reader.test.js +11 -4
- package/tests/unit/traces/recorder.test.js +4 -0
- package/tests/unit/traces/viewer.test.js +4 -0
- package/tests/unit/utilities/command-stream-only.test.js +28 -0
- package/tests/unit/utilities/subprocess.test.js +80 -0
|
@@ -0,0 +1,534 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import { mkdtemp, readFile, rm } from 'node:fs/promises';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
WebDriverPage,
|
|
9
|
+
createWebDriverPage,
|
|
10
|
+
isWebDriver,
|
|
11
|
+
} from '../../../src/core/webdriver-page.js';
|
|
12
|
+
import {
|
|
13
|
+
SeleniumAdapter,
|
|
14
|
+
createEngineAdapter,
|
|
15
|
+
} from '../../../src/core/engine-adapter.js';
|
|
16
|
+
import { detectEngine } from '../../../src/core/engine-detection.js';
|
|
17
|
+
import { isNavigationError } from '../../../src/core/navigation-safety.js';
|
|
18
|
+
import { WEBDRIVER_KEYS } from '../../../src/core/webdriver-conversions.js';
|
|
19
|
+
import { makeBrowserCommander } from '../../../src/factory.js';
|
|
20
|
+
import {
|
|
21
|
+
createMockBidi,
|
|
22
|
+
createMockDriver,
|
|
23
|
+
createMockWebElement,
|
|
24
|
+
} from '../../helpers/webdriver-mocks.js';
|
|
25
|
+
|
|
26
|
+
const BIDI_CAPABILITIES = { webSocketUrl: 'ws://127.0.0.1:1/session/x' };
|
|
27
|
+
|
|
28
|
+
function bidiPage({ responses, unsupported, driverOptions } = {}) {
|
|
29
|
+
const bidi = createMockBidi({ responses, unsupported });
|
|
30
|
+
const driver = createMockDriver({
|
|
31
|
+
capabilities: BIDI_CAPABILITIES,
|
|
32
|
+
...driverOptions,
|
|
33
|
+
});
|
|
34
|
+
const page = new WebDriverPage(driver, { connectBidi: async () => bidi });
|
|
35
|
+
return { page, driver, bidi };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const lastActions = (driver) =>
|
|
39
|
+
driver.calls.filter(([name]) => name === 'actions').at(-1)[1];
|
|
40
|
+
|
|
41
|
+
describe('WebDriverPage', () => {
|
|
42
|
+
it('only wraps something shaped like a WebDriver', () => {
|
|
43
|
+
assert.ok(isWebDriver(createMockDriver()));
|
|
44
|
+
assert.ok(!isWebDriver({ $: () => {} }));
|
|
45
|
+
assert.throws(() => new WebDriverPage({}), /needs a selenium-webdriver/);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('knows its URL and context from the start', async () => {
|
|
49
|
+
const driver = createMockDriver({ url: 'https://example.com/' });
|
|
50
|
+
const page = await createWebDriverPage(driver);
|
|
51
|
+
assert.equal(page.url(), 'https://example.com/');
|
|
52
|
+
assert.equal(page.contextIdSync(), 'context-1');
|
|
53
|
+
assert.equal(page.engine, 'selenium');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('navigates with Get and applies a page-load timeout', async () => {
|
|
57
|
+
const driver = createMockDriver();
|
|
58
|
+
const page = new WebDriverPage(driver);
|
|
59
|
+
assert.equal(
|
|
60
|
+
await page.goto('https://example.com/a', { timeout: 5000 }),
|
|
61
|
+
null
|
|
62
|
+
);
|
|
63
|
+
assert.deepEqual(driver.calls.slice(0, 2), [
|
|
64
|
+
['setTimeouts', { pageLoad: 5000 }],
|
|
65
|
+
['get', 'https://example.com/a'],
|
|
66
|
+
]);
|
|
67
|
+
assert.equal(page.url(), 'https://example.com/a');
|
|
68
|
+
await page.goBack();
|
|
69
|
+
await page.goForward();
|
|
70
|
+
await page.reload();
|
|
71
|
+
assert.deepEqual(
|
|
72
|
+
driver.calls.slice(2).map(([name]) => name),
|
|
73
|
+
['back', 'forward', 'refresh']
|
|
74
|
+
);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('evaluates functions with arguments and learns the URL', async () => {
|
|
78
|
+
const driver = createMockDriver({
|
|
79
|
+
url: 'https://example.com/b',
|
|
80
|
+
script: (source, args) => args[0] * 2,
|
|
81
|
+
});
|
|
82
|
+
const page = new WebDriverPage(driver);
|
|
83
|
+
assert.equal(await page.evaluate((x) => x * 2, 21), 42);
|
|
84
|
+
const [, source, args] = driver.calls.at(-1);
|
|
85
|
+
assert.match(source, /\(x\) => x \* 2/);
|
|
86
|
+
assert.deepEqual(args, [21]);
|
|
87
|
+
assert.equal(page.url(), 'https://example.com/b');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('finds elements by CSS and evaluates on them', async () => {
|
|
91
|
+
const element = createMockWebElement();
|
|
92
|
+
const driver = createMockDriver({
|
|
93
|
+
elements: { '#a': [element] },
|
|
94
|
+
script: (source, args) => (args[0] === element ? 'text' : null),
|
|
95
|
+
});
|
|
96
|
+
const page = new WebDriverPage(driver);
|
|
97
|
+
assert.equal(await page.$('#a'), element);
|
|
98
|
+
assert.equal(await page.$('#missing'), null);
|
|
99
|
+
assert.deepEqual(await page.$$('#a'), [element]);
|
|
100
|
+
assert.equal(await page.$eval('#a', (el) => el.textContent), 'text');
|
|
101
|
+
await assert.rejects(
|
|
102
|
+
() => page.$eval('#missing', (el) => el),
|
|
103
|
+
/failed to find element matching selector "#missing"/
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('waits for a selector to become visible, or times out like Puppeteer', async () => {
|
|
108
|
+
const element = createMockWebElement({ displayed: false });
|
|
109
|
+
const driver = createMockDriver({ elements: { '#a': [element] } });
|
|
110
|
+
const page = new WebDriverPage(driver);
|
|
111
|
+
setTimeout(() => {
|
|
112
|
+
element.displayed = true;
|
|
113
|
+
}, 150);
|
|
114
|
+
assert.equal(
|
|
115
|
+
await page.waitForSelector('#a', { visible: true, timeout: 2000 }),
|
|
116
|
+
element
|
|
117
|
+
);
|
|
118
|
+
assert.equal(await page.waitForSelector('#gone', { hidden: true }), null);
|
|
119
|
+
await assert.rejects(
|
|
120
|
+
() => page.waitForSelector('#never', { timeout: 150 }),
|
|
121
|
+
(error) =>
|
|
122
|
+
error.name === 'TimeoutError' && /150ms exceeded/.test(error.message)
|
|
123
|
+
);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('clicks with Element Click, and uses actions for other buttons and counts', async () => {
|
|
127
|
+
const elements = {};
|
|
128
|
+
const driver = createMockDriver({ elements });
|
|
129
|
+
const element = createMockWebElement({}, driver.calls);
|
|
130
|
+
elements['#b'] = [element];
|
|
131
|
+
const page = new WebDriverPage(driver);
|
|
132
|
+
await page.click('#b');
|
|
133
|
+
assert.ok(driver.calls.some(([name]) => name === 'element.click'));
|
|
134
|
+
await page.click('#b', { button: 'right', clickCount: 2 });
|
|
135
|
+
assert.deepEqual(lastActions(driver), [
|
|
136
|
+
['move', { origin: element }],
|
|
137
|
+
['press', 2],
|
|
138
|
+
['release', 2],
|
|
139
|
+
['press', 2],
|
|
140
|
+
['release', 2],
|
|
141
|
+
]);
|
|
142
|
+
await assert.rejects(() => page.click('#none'), /No element found/);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('presses chords and types through input actions', async () => {
|
|
146
|
+
const driver = createMockDriver();
|
|
147
|
+
const page = new WebDriverPage(driver);
|
|
148
|
+
await page.keyboard.press('Control+A');
|
|
149
|
+
assert.deepEqual(lastActions(driver), [
|
|
150
|
+
['keyDown', WEBDRIVER_KEYS.Control],
|
|
151
|
+
['keyDown', 'A'],
|
|
152
|
+
['keyUp', 'A'],
|
|
153
|
+
['keyUp', WEBDRIVER_KEYS.Control],
|
|
154
|
+
]);
|
|
155
|
+
await page.keyboard.type('hi');
|
|
156
|
+
assert.deepEqual(lastActions(driver), [['sendKeys', 'hi']]);
|
|
157
|
+
await page.keyboard.down('Shift');
|
|
158
|
+
assert.deepEqual(lastActions(driver), [['keyDown', WEBDRIVER_KEYS.Shift]]);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('moves, clicks and scrolls the mouse in viewport coordinates', async () => {
|
|
162
|
+
const driver = createMockDriver();
|
|
163
|
+
const page = new WebDriverPage(driver);
|
|
164
|
+
await page.mouse.click(10.4, 20.6);
|
|
165
|
+
assert.deepEqual(lastActions(driver), [
|
|
166
|
+
['move', { x: 10, y: 21, origin: 'viewport' }],
|
|
167
|
+
['press', 0],
|
|
168
|
+
['release', 0],
|
|
169
|
+
]);
|
|
170
|
+
await page.mouse.wheel({ deltaY: 300 });
|
|
171
|
+
assert.deepEqual(lastActions(driver), [
|
|
172
|
+
['scroll', 10, 21, 0, 300, 'viewport'],
|
|
173
|
+
]);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('prints to PDF with Print Page and writes the file', async () => {
|
|
177
|
+
const directory = await mkdtemp(path.join(tmpdir(), 'wd-pdf-'));
|
|
178
|
+
try {
|
|
179
|
+
const driver = createMockDriver();
|
|
180
|
+
const page = new WebDriverPage(driver);
|
|
181
|
+
const file = path.join(directory, 'out.pdf');
|
|
182
|
+
const buffer = await page.pdf({ path: file, landscape: true });
|
|
183
|
+
assert.equal(buffer.toString(), '%PDF-mock');
|
|
184
|
+
assert.equal((await readFile(file)).toString(), '%PDF-mock');
|
|
185
|
+
assert.deepEqual(driver.calls.at(-1), [
|
|
186
|
+
'printPage',
|
|
187
|
+
{ orientation: 'landscape' },
|
|
188
|
+
]);
|
|
189
|
+
await assert.rejects(
|
|
190
|
+
() => page.pdf({ headerTemplate: '<b>x</b>' }),
|
|
191
|
+
/webdriver-print-options/
|
|
192
|
+
);
|
|
193
|
+
} finally {
|
|
194
|
+
await rm(directory, { recursive: true, force: true });
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('takes viewport screenshots classically and full-page ones over BiDi', async () => {
|
|
199
|
+
const { page, bidi } = bidiPage({
|
|
200
|
+
responses: {
|
|
201
|
+
'browsingContext.captureScreenshot': {
|
|
202
|
+
data: Buffer.from('full').toString('base64'),
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
});
|
|
206
|
+
assert.equal((await page.screenshot()).toString(), 'png');
|
|
207
|
+
assert.equal(
|
|
208
|
+
await page.screenshot({ encoding: 'base64' }),
|
|
209
|
+
Buffer.from('png').toString('base64')
|
|
210
|
+
);
|
|
211
|
+
const full = await page.screenshot({
|
|
212
|
+
fullPage: true,
|
|
213
|
+
type: 'jpeg',
|
|
214
|
+
quality: 80,
|
|
215
|
+
});
|
|
216
|
+
assert.equal(full.toString(), 'full');
|
|
217
|
+
assert.deepEqual(bidi.sent.at(-1).params, {
|
|
218
|
+
context: 'context-1',
|
|
219
|
+
origin: 'document',
|
|
220
|
+
format: { type: 'image/jpeg', quality: 0.8 },
|
|
221
|
+
});
|
|
222
|
+
await page.screenshot({ clip: { x: 1, y: 2, width: 3, height: 4 } });
|
|
223
|
+
assert.deepEqual(bidi.sent.at(-1).params.clip, {
|
|
224
|
+
type: 'box',
|
|
225
|
+
x: 1,
|
|
226
|
+
y: 2,
|
|
227
|
+
width: 3,
|
|
228
|
+
height: 4,
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('explains that full-page screenshots need BiDi on a classic session', async () => {
|
|
233
|
+
const page = new WebDriverPage(createMockDriver());
|
|
234
|
+
assert.equal(await page.bidi(), null);
|
|
235
|
+
await assert.rejects(
|
|
236
|
+
() => page.screenshot({ fullPage: true }),
|
|
237
|
+
/needs WebDriver BiDi; launch with bidi: true.*webdriver-classic-has-no-events/
|
|
238
|
+
);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('reads, adds and deletes cookies in the Puppeteer shape', async () => {
|
|
242
|
+
const driver = createMockDriver({
|
|
243
|
+
cookies: [{ name: 'a', value: '1', domain: 'example.com' }],
|
|
244
|
+
});
|
|
245
|
+
const page = new WebDriverPage(driver);
|
|
246
|
+
assert.equal((await page.cookies())[0].expires, -1);
|
|
247
|
+
await page.setCookie({ name: 'b', value: '2', expires: 1900000000 });
|
|
248
|
+
assert.deepEqual(driver.calls.at(-1), [
|
|
249
|
+
'addCookie',
|
|
250
|
+
{ name: 'b', value: '2', expiry: 1900000000 },
|
|
251
|
+
]);
|
|
252
|
+
await page.deleteCookie({ name: 'a' });
|
|
253
|
+
assert.deepEqual(
|
|
254
|
+
(await page.cookies()).map((cookie) => cookie.name),
|
|
255
|
+
['b']
|
|
256
|
+
);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it('adds and removes preload scripts over BiDi', async () => {
|
|
260
|
+
const { page, bidi } = bidiPage({
|
|
261
|
+
responses: { 'script.addPreloadScript': { script: 'preload-1' } },
|
|
262
|
+
});
|
|
263
|
+
const { identifier } = await page.evaluateOnNewDocument((value) => {
|
|
264
|
+
window.flag = value;
|
|
265
|
+
}, 'on');
|
|
266
|
+
assert.equal(identifier, 'preload-1');
|
|
267
|
+
const { params } = bidi.sent.at(-1);
|
|
268
|
+
assert.deepEqual(params.contexts, ['context-1']);
|
|
269
|
+
assert.match(params.functionDeclaration, /\(\.\.\.\["on"\]\)/);
|
|
270
|
+
await page.removeScriptToEvaluateOnNewDocument(identifier);
|
|
271
|
+
assert.deepEqual(bidi.sent.at(-1), {
|
|
272
|
+
method: 'script.removePreloadScript',
|
|
273
|
+
params: { script: 'preload-1' },
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('turns BiDi error responses into errors', async () => {
|
|
278
|
+
const { page } = bidiPage({
|
|
279
|
+
responses: {
|
|
280
|
+
'browsingContext.navigate': {
|
|
281
|
+
error: 'unknown error',
|
|
282
|
+
message: 'net::ERR_NAME_NOT_RESOLVED',
|
|
283
|
+
},
|
|
284
|
+
},
|
|
285
|
+
});
|
|
286
|
+
await assert.rejects(
|
|
287
|
+
() => page.navigateBidi('https://nowhere.invalid/'),
|
|
288
|
+
/browsingContext.navigate: unknown error: net::ERR_NAME_NOT_RESOLVED/
|
|
289
|
+
);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it('closes its window and reports itself closed', async () => {
|
|
293
|
+
const driver = createMockDriver();
|
|
294
|
+
const page = new WebDriverPage(driver);
|
|
295
|
+
let closed = false;
|
|
296
|
+
page.on('close', () => {
|
|
297
|
+
closed = true;
|
|
298
|
+
});
|
|
299
|
+
await page.close();
|
|
300
|
+
assert.ok(page.isClosed());
|
|
301
|
+
assert.ok(closed);
|
|
302
|
+
assert.ok(page.mainFrame().isDetached());
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
describe('WebDriver BiDi event bridge', () => {
|
|
307
|
+
it('re-emits console entries of its own context as console messages', async () => {
|
|
308
|
+
const { page, bidi } = bidiPage();
|
|
309
|
+
const messages = [];
|
|
310
|
+
page.on('console', (message) => messages.push(message.text()));
|
|
311
|
+
await page.eventsReady();
|
|
312
|
+
bidi.fire('log.entryAdded', {
|
|
313
|
+
type: 'console',
|
|
314
|
+
method: 'log',
|
|
315
|
+
text: 'hello',
|
|
316
|
+
source: { context: 'context-1' },
|
|
317
|
+
});
|
|
318
|
+
bidi.fire('log.entryAdded', {
|
|
319
|
+
type: 'console',
|
|
320
|
+
text: 'other tab',
|
|
321
|
+
source: { context: 'context-2' },
|
|
322
|
+
});
|
|
323
|
+
assert.deepEqual(messages, ['hello']);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it('re-emits javascript log entries as page errors', async () => {
|
|
327
|
+
const { page, bidi } = bidiPage();
|
|
328
|
+
const errors = [];
|
|
329
|
+
page.on('pageerror', (error) => errors.push(error.message));
|
|
330
|
+
await page.eventsReady();
|
|
331
|
+
bidi.fire('log.entryAdded', {
|
|
332
|
+
type: 'javascript',
|
|
333
|
+
text: 'Error: boom',
|
|
334
|
+
source: { context: 'context-1' },
|
|
335
|
+
});
|
|
336
|
+
assert.deepEqual(errors, ['Error: boom']);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it('tracks navigations and falls back to domContentLoaded on older drivers', async () => {
|
|
340
|
+
const { page, bidi } = bidiPage({
|
|
341
|
+
unsupported: ['browsingContext.navigationCommitted'],
|
|
342
|
+
});
|
|
343
|
+
const urls = [];
|
|
344
|
+
page.once('framenavigated', (frame) => urls.push(frame.url()));
|
|
345
|
+
await page.eventsReady();
|
|
346
|
+
assert.ok(bidi.callbacks.has('browsingContext.domContentLoaded'));
|
|
347
|
+
assert.ok(bidi.callbacks.has('browsingContext.fragmentNavigated'));
|
|
348
|
+
bidi.fire('browsingContext.domContentLoaded', {
|
|
349
|
+
context: 'context-1',
|
|
350
|
+
url: 'https://example.com/next',
|
|
351
|
+
});
|
|
352
|
+
assert.deepEqual(urls, ['https://example.com/next']);
|
|
353
|
+
assert.equal(page.url(), 'https://example.com/next');
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it('turns user prompts into dialogs that answer over BiDi', async () => {
|
|
357
|
+
const { page, bidi } = bidiPage();
|
|
358
|
+
page.on('dialog', (dialog) => dialog.accept('yes'));
|
|
359
|
+
await page.eventsReady();
|
|
360
|
+
bidi.fire('browsingContext.userPromptOpened', {
|
|
361
|
+
context: 'context-1',
|
|
362
|
+
type: 'prompt',
|
|
363
|
+
message: 'Sure?',
|
|
364
|
+
});
|
|
365
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
366
|
+
assert.deepEqual(bidi.sent.at(-1), {
|
|
367
|
+
method: 'browsingContext.handleUserPrompt',
|
|
368
|
+
params: { context: 'context-1', accept: true, userText: 'yes' },
|
|
369
|
+
});
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
it('re-emits network events as requests and responses', async () => {
|
|
373
|
+
const { page, bidi } = bidiPage();
|
|
374
|
+
const seen = [];
|
|
375
|
+
page.on('request', (request) => seen.push(['request', request.url()]));
|
|
376
|
+
page.on('response', (response) =>
|
|
377
|
+
seen.push(['response', response.status()])
|
|
378
|
+
);
|
|
379
|
+
await page.eventsReady();
|
|
380
|
+
const request = {
|
|
381
|
+
request: 'r',
|
|
382
|
+
url: 'https://example.com/',
|
|
383
|
+
method: 'GET',
|
|
384
|
+
};
|
|
385
|
+
bidi.fire('network.beforeRequestSent', { request });
|
|
386
|
+
bidi.fire('network.responseCompleted', {
|
|
387
|
+
request,
|
|
388
|
+
response: { status: 200 },
|
|
389
|
+
});
|
|
390
|
+
assert.deepEqual(seen, [
|
|
391
|
+
['request', 'https://example.com/'],
|
|
392
|
+
['response', 200],
|
|
393
|
+
]);
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
it('stays silent on a classic session and unsubscribes on dispose', async () => {
|
|
397
|
+
const classic = new WebDriverPage(createMockDriver());
|
|
398
|
+
classic.on('console', () => {});
|
|
399
|
+
await classic.eventsReady();
|
|
400
|
+
|
|
401
|
+
const { page, bidi } = bidiPage();
|
|
402
|
+
page.on('console', () => {});
|
|
403
|
+
page.on('pageerror', () => {});
|
|
404
|
+
await page.eventsReady();
|
|
405
|
+
assert.equal(bidi.callbacks.size, 1);
|
|
406
|
+
await page.dispose();
|
|
407
|
+
assert.deepEqual(bidi.unsubscribed, ['log.entryAdded']);
|
|
408
|
+
});
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
describe('SeleniumAdapter', () => {
|
|
412
|
+
it('is chosen for the selenium engine', () => {
|
|
413
|
+
const page = new WebDriverPage(createMockDriver());
|
|
414
|
+
const adapter = createEngineAdapter(page, 'selenium');
|
|
415
|
+
assert.ok(adapter instanceof SeleniumAdapter);
|
|
416
|
+
assert.equal(adapter.getEngineName(), 'selenium');
|
|
417
|
+
assert.equal(adapter.getDriver(), page.driver);
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
it('clicks, types and fills with native element commands', async () => {
|
|
421
|
+
const driver = createMockDriver();
|
|
422
|
+
const adapter = new SeleniumAdapter(new WebDriverPage(driver));
|
|
423
|
+
const element = createMockWebElement({}, driver.calls);
|
|
424
|
+
await adapter.click(element);
|
|
425
|
+
await adapter.type(element, 'abc');
|
|
426
|
+
await adapter.fill(element, 'xyz');
|
|
427
|
+
assert.deepEqual(
|
|
428
|
+
driver.calls.filter(([name]) => name.startsWith('element.')),
|
|
429
|
+
[
|
|
430
|
+
['element.click', 'el'],
|
|
431
|
+
['element.sendKeys', 'el', 'abc'],
|
|
432
|
+
['element.clear', 'el'],
|
|
433
|
+
['element.sendKeys', 'el', 'xyz'],
|
|
434
|
+
]
|
|
435
|
+
);
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
it('force-clicks with a pointer action at the element', async () => {
|
|
439
|
+
const driver = createMockDriver();
|
|
440
|
+
const adapter = new SeleniumAdapter(new WebDriverPage(driver));
|
|
441
|
+
const element = createMockWebElement({}, driver.calls);
|
|
442
|
+
await adapter.click(element, { force: true });
|
|
443
|
+
assert.deepEqual(lastActions(driver), [
|
|
444
|
+
['move', { origin: element }],
|
|
445
|
+
['click'],
|
|
446
|
+
]);
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
it('delivers console messages and navigations through BiDi helpers', async () => {
|
|
450
|
+
const { page, bidi } = bidiPage({
|
|
451
|
+
responses: {
|
|
452
|
+
'browsingContext.navigate': {
|
|
453
|
+
navigation: 'n1',
|
|
454
|
+
url: 'https://a.test/',
|
|
455
|
+
},
|
|
456
|
+
},
|
|
457
|
+
});
|
|
458
|
+
const adapter = new SeleniumAdapter(page);
|
|
459
|
+
const texts = [];
|
|
460
|
+
const urls = [];
|
|
461
|
+
const stopConsole = await adapter.onConsoleMessage((message) =>
|
|
462
|
+
texts.push(message.text())
|
|
463
|
+
);
|
|
464
|
+
await adapter.onNavigation((url) => urls.push(url));
|
|
465
|
+
bidi.fire('log.entryAdded', { type: 'console', text: 'one' });
|
|
466
|
+
stopConsole();
|
|
467
|
+
bidi.fire('log.entryAdded', { type: 'console', text: 'two' });
|
|
468
|
+
bidi.fire('browsingContext.navigationCommitted', {
|
|
469
|
+
context: 'context-1',
|
|
470
|
+
url: 'https://a.test/',
|
|
471
|
+
});
|
|
472
|
+
assert.deepEqual(texts, ['one']);
|
|
473
|
+
assert.deepEqual(urls, ['https://a.test/']);
|
|
474
|
+
|
|
475
|
+
const result = await adapter.navigate('https://a.test/', { wait: 'none' });
|
|
476
|
+
assert.equal(result.navigation, 'n1');
|
|
477
|
+
assert.deepEqual(bidi.sent.at(-1).params, {
|
|
478
|
+
context: 'context-1',
|
|
479
|
+
url: 'https://a.test/',
|
|
480
|
+
wait: 'none',
|
|
481
|
+
});
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
it('refuses BiDi helpers on a classic session with a clear error', async () => {
|
|
485
|
+
const adapter = new SeleniumAdapter(new WebDriverPage(createMockDriver()));
|
|
486
|
+
await assert.rejects(
|
|
487
|
+
() => adapter.onConsoleMessage(() => {}),
|
|
488
|
+
/needs WebDriver BiDi/
|
|
489
|
+
);
|
|
490
|
+
});
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
describe('selenium engine detection and the commander', () => {
|
|
494
|
+
it('detects a raw WebDriver and the facade as selenium', () => {
|
|
495
|
+
const driver = createMockDriver();
|
|
496
|
+
assert.equal(detectEngine(driver), 'selenium');
|
|
497
|
+
assert.equal(detectEngine(new WebDriverPage(driver)), 'selenium');
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
it('wraps a raw WebDriver in the facade', () => {
|
|
501
|
+
const driver = createMockDriver();
|
|
502
|
+
const commander = makeBrowserCommander({
|
|
503
|
+
page: driver,
|
|
504
|
+
enableNetworkTracking: false,
|
|
505
|
+
enableNavigationManager: false,
|
|
506
|
+
});
|
|
507
|
+
assert.equal(commander.engine, 'selenium');
|
|
508
|
+
assert.ok(commander.page instanceof WebDriverPage);
|
|
509
|
+
assert.equal(commander.page.driver, driver);
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
it('keeps a facade it was given', () => {
|
|
513
|
+
const page = new WebDriverPage(createMockDriver());
|
|
514
|
+
const commander = makeBrowserCommander({
|
|
515
|
+
page,
|
|
516
|
+
enableNetworkTracking: false,
|
|
517
|
+
enableNavigationManager: false,
|
|
518
|
+
});
|
|
519
|
+
assert.equal(commander.page, page);
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
it('treats stale elements and closed windows as navigation errors', () => {
|
|
523
|
+
assert.ok(
|
|
524
|
+
isNavigationError(
|
|
525
|
+
new Error('stale element reference: stale element not found')
|
|
526
|
+
)
|
|
527
|
+
);
|
|
528
|
+
assert.ok(
|
|
529
|
+
isNavigationError(
|
|
530
|
+
new Error('no such window: target window already closed')
|
|
531
|
+
)
|
|
532
|
+
);
|
|
533
|
+
});
|
|
534
|
+
});
|
|
@@ -271,22 +271,95 @@ describe('download manager (issue #88)', () => {
|
|
|
271
271
|
},
|
|
272
272
|
});
|
|
273
273
|
|
|
274
|
+
/**
|
|
275
|
+
* Describe the CDP command that sends downloads to this test's staging path.
|
|
276
|
+
*
|
|
277
|
+
* @param {string} [browserContextId] - Context to configure
|
|
278
|
+
* @returns {Object} Expected CDP command
|
|
279
|
+
*/
|
|
280
|
+
const downloadBehaviorCommand = (browserContextId) => ({
|
|
281
|
+
method: 'Browser.setDownloadBehavior',
|
|
282
|
+
params: {
|
|
283
|
+
behavior: 'allowAndName',
|
|
284
|
+
downloadPath: path.join(root, STAGING_DIRECTORY),
|
|
285
|
+
eventsEnabled: true,
|
|
286
|
+
...(browserContextId ? { browserContextId } : {}),
|
|
287
|
+
},
|
|
288
|
+
});
|
|
289
|
+
|
|
274
290
|
it('should ask Chromium to report downloads it did not automate', async () => {
|
|
275
291
|
const session = createFakeCdpSession();
|
|
276
292
|
manager = await cdpManagerOver(session);
|
|
277
293
|
|
|
278
|
-
assert.deepStrictEqual(session.sent, [
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
294
|
+
assert.deepStrictEqual(session.sent, [downloadBehaviorCommand()]);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('should scope download behavior to the page browser context', async () => {
|
|
298
|
+
// Issue #97: browser.newPage() and browser.newContext() create a
|
|
299
|
+
// non-default Chromium context. Browser download events are global, but
|
|
300
|
+
// the path configured without this id belongs to the default context.
|
|
301
|
+
const browserSession = createFakeCdpSession();
|
|
302
|
+
browserSession.send = async (method, params) => {
|
|
303
|
+
browserSession.sent.push({ method, params });
|
|
304
|
+
if (method === 'Target.getBrowserContexts') {
|
|
305
|
+
return { browserContextIds: ['context-from-target'] };
|
|
306
|
+
}
|
|
307
|
+
return undefined;
|
|
308
|
+
};
|
|
309
|
+
const pageSession = createFakeCdpSession();
|
|
310
|
+
pageSession.send = async (method, params) => {
|
|
311
|
+
pageSession.sent.push({ method, params });
|
|
312
|
+
return {
|
|
313
|
+
targetInfo: { browserContextId: 'context-from-target' },
|
|
314
|
+
};
|
|
315
|
+
};
|
|
316
|
+
let pageSessionDetached = false;
|
|
317
|
+
pageSession.detach = async () => {
|
|
318
|
+
pageSessionDetached = true;
|
|
319
|
+
};
|
|
320
|
+
const browser = {
|
|
321
|
+
newBrowserCDPSession: async () => browserSession,
|
|
322
|
+
};
|
|
323
|
+
const browserContext = {
|
|
324
|
+
browser: () => browser,
|
|
325
|
+
newCDPSession: async () => pageSession,
|
|
326
|
+
};
|
|
327
|
+
const page = { context: () => browserContext };
|
|
328
|
+
|
|
329
|
+
manager = await createDownloadManager({
|
|
330
|
+
engine: 'playwright',
|
|
331
|
+
browser: browserContext,
|
|
332
|
+
page,
|
|
333
|
+
directory: root,
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
assert.deepStrictEqual(pageSession.sent, [
|
|
337
|
+
{ method: 'Target.getTargetInfo', params: undefined },
|
|
338
|
+
]);
|
|
339
|
+
assert.strictEqual(pageSessionDetached, true);
|
|
340
|
+
assert.deepStrictEqual(browserSession.sent, [
|
|
341
|
+
{ method: 'Target.getBrowserContexts', params: undefined },
|
|
342
|
+
downloadBehaviorCommand('context-from-target'),
|
|
287
343
|
]);
|
|
288
344
|
});
|
|
289
345
|
|
|
346
|
+
it('should keep a persistent context on default download behavior', async () => {
|
|
347
|
+
const session = createFakeCdpSession();
|
|
348
|
+
const persistentContext = {
|
|
349
|
+
browser: () => null,
|
|
350
|
+
newCDPSession: async () => session,
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
manager = await createDownloadManager({
|
|
354
|
+
engine: 'playwright',
|
|
355
|
+
browser: persistentContext,
|
|
356
|
+
page: { context: () => persistentContext },
|
|
357
|
+
directory: root,
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
assert.deepStrictEqual(session.sent, [downloadBehaviorCommand()]);
|
|
361
|
+
});
|
|
362
|
+
|
|
290
363
|
it('should place a manually started download like an automated one', async () => {
|
|
291
364
|
const session = createFakeCdpSession();
|
|
292
365
|
manager = await cdpManagerOver(session);
|
|
@@ -5,6 +5,7 @@ import path from 'node:path';
|
|
|
5
5
|
|
|
6
6
|
import {
|
|
7
7
|
DEFAULT_STAGING_POLL_INTERVAL,
|
|
8
|
+
DEFAULT_STAGING_STABILITY,
|
|
8
9
|
DEFAULT_STAGING_TIMEOUT,
|
|
9
10
|
describeStagingTimeout,
|
|
10
11
|
STAGING_IN_PROGRESS_SUFFIXES,
|
|
@@ -61,11 +62,31 @@ describe('waiting for staged bytes (issue #92)', () => {
|
|
|
61
62
|
clearInterval(grow);
|
|
62
63
|
|
|
63
64
|
assert.strictEqual(settled.ready, true, settled.reason ?? '');
|
|
64
|
-
//
|
|
65
|
-
//
|
|
65
|
+
// The quiet period ends the wait, so the file had stopped growing before
|
|
66
|
+
// it was declared ready.
|
|
66
67
|
assert.strictEqual(settled.bytes, (await fs.stat(staged)).size);
|
|
67
68
|
});
|
|
68
69
|
|
|
70
|
+
it('should survive a pause between writes that is longer than one poll', async () => {
|
|
71
|
+
const staged = path.join(directory.path, 'guid-paused');
|
|
72
|
+
await fs.writeFile(staged, 'first');
|
|
73
|
+
const resume = setTimeout(() => {
|
|
74
|
+
fs.appendFile(staged, '-second').catch(() => {});
|
|
75
|
+
}, 25);
|
|
76
|
+
|
|
77
|
+
const settled = await waitForStagedFile({
|
|
78
|
+
path: staged,
|
|
79
|
+
timeout: 1000,
|
|
80
|
+
interval: 5,
|
|
81
|
+
stability: 60,
|
|
82
|
+
});
|
|
83
|
+
clearTimeout(resume);
|
|
84
|
+
|
|
85
|
+
assert.strictEqual(settled.ready, true, settled.reason ?? '');
|
|
86
|
+
assert.strictEqual(settled.bytes, 'first-second'.length);
|
|
87
|
+
assert.ok(settled.waitedMs >= 60);
|
|
88
|
+
});
|
|
89
|
+
|
|
69
90
|
it('should not claim a file whose partial sibling is still being written', async () => {
|
|
70
91
|
const staged = path.join(directory.path, 'guid-partial');
|
|
71
92
|
await fs.writeFile(staged, 'looks done');
|
|
@@ -111,6 +132,7 @@ describe('waiting for staged bytes (issue #92)', () => {
|
|
|
111
132
|
it('should ship defaults a download can actually finish within', () => {
|
|
112
133
|
assert.ok(DEFAULT_STAGING_TIMEOUT >= 1000);
|
|
113
134
|
assert.ok(DEFAULT_STAGING_POLL_INTERVAL > 0);
|
|
135
|
+
assert.ok(DEFAULT_STAGING_STABILITY > DEFAULT_STAGING_POLL_INTERVAL);
|
|
114
136
|
assert.ok(DEFAULT_STAGING_POLL_INTERVAL < DEFAULT_STAGING_TIMEOUT);
|
|
115
137
|
assert.ok(STAGING_IN_PROGRESS_SUFFIXES.includes('.crdownload'));
|
|
116
138
|
});
|