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,473 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
CHROMEDRIVER_DEFAULT_SWITCHES,
|
|
7
|
+
buildDriverServerArgs,
|
|
8
|
+
connectWebDriver,
|
|
9
|
+
launchWebDriver,
|
|
10
|
+
resolveWebDriverExecutable,
|
|
11
|
+
seleniumManagerPath,
|
|
12
|
+
waitForDriverReady,
|
|
13
|
+
} from '../../../src/browser/webdriver.js';
|
|
14
|
+
import { WebDriverPage } from '../../../src/core/webdriver-page.js';
|
|
15
|
+
import { createMockDriver } from '../../helpers/webdriver-mocks.js';
|
|
16
|
+
|
|
17
|
+
const managerOutput = (result, code = 0) => ({
|
|
18
|
+
stdout: JSON.stringify({ logs: [], result: { code, ...result } }),
|
|
19
|
+
stderr: '',
|
|
20
|
+
code,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const accessOnly =
|
|
24
|
+
(...allowed) =>
|
|
25
|
+
async (file) => {
|
|
26
|
+
if (!allowed.includes(file)) {
|
|
27
|
+
throw Object.assign(new Error(`ENOENT ${file}`), { code: 'ENOENT' });
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/** selenium-webdriver modules that record what the launcher configured. */
|
|
32
|
+
function createMockSelenium(driver = createMockDriver()) {
|
|
33
|
+
const record = { options: [], builders: [] };
|
|
34
|
+
class Options {
|
|
35
|
+
constructor(kind) {
|
|
36
|
+
this.kind = kind;
|
|
37
|
+
this.args = [];
|
|
38
|
+
this.excluded = [];
|
|
39
|
+
this.capabilities = {};
|
|
40
|
+
record.options.push(this);
|
|
41
|
+
}
|
|
42
|
+
addArguments(...args) {
|
|
43
|
+
this.args.push(...args);
|
|
44
|
+
return this;
|
|
45
|
+
}
|
|
46
|
+
excludeSwitches(...switches) {
|
|
47
|
+
this.excluded.push(...switches);
|
|
48
|
+
return this;
|
|
49
|
+
}
|
|
50
|
+
setChromeBinaryPath(binary) {
|
|
51
|
+
this.binary = binary;
|
|
52
|
+
return this;
|
|
53
|
+
}
|
|
54
|
+
setBinary(binary) {
|
|
55
|
+
this.binary = binary;
|
|
56
|
+
return this;
|
|
57
|
+
}
|
|
58
|
+
set(name, value) {
|
|
59
|
+
this.capabilities[name] = value;
|
|
60
|
+
return this;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
class Builder {
|
|
64
|
+
constructor() {
|
|
65
|
+
record.builders.push(this);
|
|
66
|
+
}
|
|
67
|
+
usingServer(url) {
|
|
68
|
+
this.serverUrl = url;
|
|
69
|
+
return this;
|
|
70
|
+
}
|
|
71
|
+
forBrowser(name) {
|
|
72
|
+
this.browser = name;
|
|
73
|
+
return this;
|
|
74
|
+
}
|
|
75
|
+
withCapabilities(capabilities) {
|
|
76
|
+
this.capabilities = capabilities;
|
|
77
|
+
return this;
|
|
78
|
+
}
|
|
79
|
+
setChromeOptions(options) {
|
|
80
|
+
this.options = options;
|
|
81
|
+
return this;
|
|
82
|
+
}
|
|
83
|
+
setFirefoxOptions(options) {
|
|
84
|
+
this.options = options;
|
|
85
|
+
return this;
|
|
86
|
+
}
|
|
87
|
+
build() {
|
|
88
|
+
return driver;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
record,
|
|
93
|
+
driver,
|
|
94
|
+
selenium: {
|
|
95
|
+
webdriver: { Builder },
|
|
96
|
+
chrome: {
|
|
97
|
+
Options: class extends Options {
|
|
98
|
+
constructor() {
|
|
99
|
+
super('chrome');
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
firefox: {
|
|
104
|
+
Options: class extends Options {
|
|
105
|
+
constructor() {
|
|
106
|
+
super('firefox');
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function createMockProcess() {
|
|
115
|
+
let resolveExit;
|
|
116
|
+
const process = {
|
|
117
|
+
exitCode: null,
|
|
118
|
+
killed: false,
|
|
119
|
+
stderr: { on: () => {} },
|
|
120
|
+
exited: new Promise((resolve) => {
|
|
121
|
+
resolveExit = resolve;
|
|
122
|
+
}),
|
|
123
|
+
kill() {
|
|
124
|
+
this.killed = true;
|
|
125
|
+
this.exitCode = 0;
|
|
126
|
+
resolveExit(0);
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
return process;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function launchDependencies(overrides = {}) {
|
|
133
|
+
const mock = createMockSelenium();
|
|
134
|
+
const started = [];
|
|
135
|
+
const ports = [41001, 41002];
|
|
136
|
+
const fetched = [];
|
|
137
|
+
return {
|
|
138
|
+
mock,
|
|
139
|
+
started,
|
|
140
|
+
fetched,
|
|
141
|
+
dependencies: {
|
|
142
|
+
selenium: mock.selenium,
|
|
143
|
+
environment: { PATH: '/usr/bin' },
|
|
144
|
+
platform: 'linux',
|
|
145
|
+
checkAccess: accessOnly('/opt/drivers/chromedriver'),
|
|
146
|
+
runCommand: async () =>
|
|
147
|
+
managerOutput({
|
|
148
|
+
driver_path: '/cache/chromedriver',
|
|
149
|
+
browser_path: '/opt/google/chrome/chrome',
|
|
150
|
+
}),
|
|
151
|
+
reservePort: async () => ports.shift(),
|
|
152
|
+
startProcess: (file, args, options) => {
|
|
153
|
+
const child = createMockProcess();
|
|
154
|
+
started.push({ file, args, options, child });
|
|
155
|
+
return child;
|
|
156
|
+
},
|
|
157
|
+
fetch: async (url) => {
|
|
158
|
+
fetched.push(url);
|
|
159
|
+
if (fetched.length === 1) {
|
|
160
|
+
throw new Error('ECONNREFUSED');
|
|
161
|
+
}
|
|
162
|
+
return { ok: true, json: async () => ({ value: { ready: true } }) };
|
|
163
|
+
},
|
|
164
|
+
...overrides,
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
describe('resolveWebDriverExecutable', () => {
|
|
170
|
+
it('uses an explicit driverPath first', async () => {
|
|
171
|
+
const resolved = await resolveWebDriverExecutable({
|
|
172
|
+
driverPath: '/opt/drivers/chromedriver',
|
|
173
|
+
checkAccess: accessOnly('/opt/drivers/chromedriver'),
|
|
174
|
+
runCommand: () => assert.fail('Selenium Manager must not run'),
|
|
175
|
+
});
|
|
176
|
+
assert.deepEqual(resolved, {
|
|
177
|
+
driverPath: '/opt/drivers/chromedriver',
|
|
178
|
+
browserPath: null,
|
|
179
|
+
source: 'driverPath',
|
|
180
|
+
});
|
|
181
|
+
await assert.rejects(
|
|
182
|
+
() =>
|
|
183
|
+
resolveWebDriverExecutable({
|
|
184
|
+
driverPath: '/missing',
|
|
185
|
+
checkAccess: accessOnly(),
|
|
186
|
+
}),
|
|
187
|
+
/not accessible: \/missing/
|
|
188
|
+
);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('then looks on PATH', async () => {
|
|
192
|
+
const resolved = await resolveWebDriverExecutable({
|
|
193
|
+
browser: 'firefox',
|
|
194
|
+
environment: { PATH: '/a:/b' },
|
|
195
|
+
platform: 'linux',
|
|
196
|
+
checkAccess: accessOnly('/b/geckodriver'),
|
|
197
|
+
runCommand: () => assert.fail('Selenium Manager must not run'),
|
|
198
|
+
});
|
|
199
|
+
assert.equal(resolved.driverPath, '/b/geckodriver');
|
|
200
|
+
assert.equal(resolved.source, 'PATH');
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('uses the target platform path rules on PATH', async () => {
|
|
204
|
+
const resolved = await resolveWebDriverExecutable({
|
|
205
|
+
environment: { PATH: 'C:\\a;C:\\b' },
|
|
206
|
+
platform: 'win32',
|
|
207
|
+
checkAccess: accessOnly('C:\\b\\chromedriver.exe'),
|
|
208
|
+
runCommand: () => assert.fail('Selenium Manager must not run'),
|
|
209
|
+
});
|
|
210
|
+
assert.equal(resolved.driverPath, 'C:\\b\\chromedriver.exe');
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('then asks Selenium Manager for a matching driver', async () => {
|
|
214
|
+
const commands = [];
|
|
215
|
+
const resolved = await resolveWebDriverExecutable({
|
|
216
|
+
executablePath: '/usr/bin/google-chrome',
|
|
217
|
+
environment: { PATH: '' },
|
|
218
|
+
managerPath: '/sm',
|
|
219
|
+
checkAccess: accessOnly(),
|
|
220
|
+
runCommand: async (file, args) => {
|
|
221
|
+
commands.push([file, ...args]);
|
|
222
|
+
return managerOutput({
|
|
223
|
+
driver_path: '/cache/chromedriver',
|
|
224
|
+
browser_path: '/usr/bin/google-chrome',
|
|
225
|
+
});
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
assert.deepEqual(commands, [
|
|
229
|
+
[
|
|
230
|
+
'/sm',
|
|
231
|
+
'--browser',
|
|
232
|
+
'chrome',
|
|
233
|
+
'--output',
|
|
234
|
+
'json',
|
|
235
|
+
'--browser-path',
|
|
236
|
+
'/usr/bin/google-chrome',
|
|
237
|
+
],
|
|
238
|
+
]);
|
|
239
|
+
assert.deepEqual(resolved, {
|
|
240
|
+
driverPath: '/cache/chromedriver',
|
|
241
|
+
browserPath: '/usr/bin/google-chrome',
|
|
242
|
+
source: 'selenium-manager',
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('reports a Selenium Manager failure with its message', async () => {
|
|
247
|
+
const withManagerOutput = (output) =>
|
|
248
|
+
resolveWebDriverExecutable({
|
|
249
|
+
environment: {},
|
|
250
|
+
managerPath: '/sm',
|
|
251
|
+
checkAccess: accessOnly(),
|
|
252
|
+
runCommand: async () => output,
|
|
253
|
+
});
|
|
254
|
+
await assert.rejects(
|
|
255
|
+
withManagerOutput(
|
|
256
|
+
managerOutput({ message: 'Unable to discover proper chromedriver' }, 65)
|
|
257
|
+
),
|
|
258
|
+
/Could not find chromedriver.*Unable to discover proper chromedriver.*Pass driverPath/
|
|
259
|
+
);
|
|
260
|
+
await assert.rejects(
|
|
261
|
+
withManagerOutput({ stdout: 'panic', code: 101 }),
|
|
262
|
+
/did not return JSON/
|
|
263
|
+
);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it('rejects browsers it has no driver for', async () => {
|
|
267
|
+
await assert.rejects(
|
|
268
|
+
() => resolveWebDriverExecutable({ browser: 'safari' }),
|
|
269
|
+
/Unsupported WebDriver browser: safari/
|
|
270
|
+
);
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
describe('WebDriver server helpers', () => {
|
|
275
|
+
it('finds the Selenium Manager binary shipped with selenium-webdriver', () => {
|
|
276
|
+
assert.equal(
|
|
277
|
+
seleniumManagerPath({ environment: { SE_MANAGER_PATH: '/x/sm' } }),
|
|
278
|
+
'/x/sm'
|
|
279
|
+
);
|
|
280
|
+
const linux = seleniumManagerPath({
|
|
281
|
+
platform: 'linux',
|
|
282
|
+
arch: 'x64',
|
|
283
|
+
environment: {},
|
|
284
|
+
});
|
|
285
|
+
assert.match(
|
|
286
|
+
linux,
|
|
287
|
+
/selenium-webdriver[\\/]bin[\\/]linux-x86_64[\\/]selenium-manager$/
|
|
288
|
+
);
|
|
289
|
+
assert.match(
|
|
290
|
+
seleniumManagerPath({ platform: 'win32', environment: {} }),
|
|
291
|
+
/windows[\\/]selenium-manager\.exe$/
|
|
292
|
+
);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('builds the server arguments for each driver', () => {
|
|
296
|
+
assert.deepEqual(buildDriverServerArgs('chrome', 9515), ['--port=9515']);
|
|
297
|
+
assert.deepEqual(buildDriverServerArgs('firefox', 4444), [
|
|
298
|
+
'--port',
|
|
299
|
+
'4444',
|
|
300
|
+
'--host',
|
|
301
|
+
'127.0.0.1',
|
|
302
|
+
]);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it('fails fast when the driver exits before it is ready', async () => {
|
|
306
|
+
await assert.rejects(
|
|
307
|
+
() =>
|
|
308
|
+
waitForDriverReady('http://127.0.0.1:1', {
|
|
309
|
+
driverProcess: { exitCode: 1 },
|
|
310
|
+
output: () => 'bind() failed\n',
|
|
311
|
+
fetchImplementation: async () => assert.fail('not reached'),
|
|
312
|
+
}),
|
|
313
|
+
/exited with code 1 before it was ready: bind\(\) failed/
|
|
314
|
+
);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it('times out when /status never reports ready', async () => {
|
|
318
|
+
await assert.rejects(
|
|
319
|
+
() =>
|
|
320
|
+
waitForDriverReady('http://127.0.0.1:1', {
|
|
321
|
+
timeout: 150,
|
|
322
|
+
fetchImplementation: async () => ({
|
|
323
|
+
ok: true,
|
|
324
|
+
json: async () => ({ value: { ready: false } }),
|
|
325
|
+
}),
|
|
326
|
+
}),
|
|
327
|
+
/was not ready within 150ms/
|
|
328
|
+
);
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it('lists every switch chromedriver adds besides the debugging port', () => {
|
|
332
|
+
assert.equal(CHROMEDRIVER_DEFAULT_SWITCHES.length, 19);
|
|
333
|
+
assert.ok(CHROMEDRIVER_DEFAULT_SWITCHES.includes('enable-automation'));
|
|
334
|
+
assert.ok(!CHROMEDRIVER_DEFAULT_SWITCHES.includes('remote-debugging-port'));
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
describe('launchWebDriver', () => {
|
|
339
|
+
it('starts chromedriver on a reserved port and gives Chrome a clean command line', async () => {
|
|
340
|
+
const { mock, started, fetched, dependencies } = launchDependencies();
|
|
341
|
+
const session = await launchWebDriver(
|
|
342
|
+
{ headless: true, bidi: true },
|
|
343
|
+
dependencies
|
|
344
|
+
);
|
|
345
|
+
try {
|
|
346
|
+
assert.equal(session.driverPath, '/cache/chromedriver');
|
|
347
|
+
assert.equal(session.driverSource, 'selenium-manager');
|
|
348
|
+
assert.equal(session.serverUrl, 'http://127.0.0.1:41001');
|
|
349
|
+
assert.deepEqual(started[0].args, ['--port=41001']);
|
|
350
|
+
assert.deepEqual(fetched, [
|
|
351
|
+
'http://127.0.0.1:41001/status',
|
|
352
|
+
'http://127.0.0.1:41001/status',
|
|
353
|
+
]);
|
|
354
|
+
|
|
355
|
+
const [builder] = mock.record.builders;
|
|
356
|
+
assert.equal(builder.serverUrl, 'http://127.0.0.1:41001');
|
|
357
|
+
assert.equal(builder.browser, 'chrome');
|
|
358
|
+
const options = builder.options;
|
|
359
|
+
assert.equal(options.binary, '/opt/google/chrome/chrome');
|
|
360
|
+
assert.deepEqual(options.excluded, [...CHROMEDRIVER_DEFAULT_SWITCHES]);
|
|
361
|
+
assert.ok(options.args.includes('--remote-debugging-port=41002'));
|
|
362
|
+
assert.ok(options.args.includes('--headless=new'));
|
|
363
|
+
assert.ok(
|
|
364
|
+
options.args.includes(`--user-data-dir=${session.userDataDir}`)
|
|
365
|
+
);
|
|
366
|
+
assert.ok(!options.args.includes('--enable-automation'));
|
|
367
|
+
assert.deepEqual(options.capabilities, {
|
|
368
|
+
webSocketUrl: true,
|
|
369
|
+
unhandledPromptBehavior: 'ignore',
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
assert.ok(session.page instanceof WebDriverPage);
|
|
373
|
+
assert.equal(session.temporaryProfile, true);
|
|
374
|
+
assert.ok(existsSync(session.userDataDir));
|
|
375
|
+
} finally {
|
|
376
|
+
await session.close();
|
|
377
|
+
}
|
|
378
|
+
assert.ok(started[0].child.killed);
|
|
379
|
+
assert.ok(mock.driver.calls.some(([name]) => name === 'quit'));
|
|
380
|
+
assert.ok(!existsSync(session.userDataDir));
|
|
381
|
+
await session.close(); // idempotent
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
it('starts geckodriver for Firefox with a -profile argument', async () => {
|
|
385
|
+
const { mock, started, dependencies } = launchDependencies({
|
|
386
|
+
environment: { PATH: '/usr/bin' },
|
|
387
|
+
checkAccess: accessOnly('/usr/bin/geckodriver'),
|
|
388
|
+
});
|
|
389
|
+
const session = await launchWebDriver(
|
|
390
|
+
{
|
|
391
|
+
browser: 'firefox',
|
|
392
|
+
headless: true,
|
|
393
|
+
executablePath: '/opt/firefox/firefox',
|
|
394
|
+
},
|
|
395
|
+
dependencies
|
|
396
|
+
);
|
|
397
|
+
try {
|
|
398
|
+
assert.equal(started[0].file, '/usr/bin/geckodriver');
|
|
399
|
+
assert.deepEqual(started[0].args, [
|
|
400
|
+
'--port',
|
|
401
|
+
'41001',
|
|
402
|
+
'--host',
|
|
403
|
+
'127.0.0.1',
|
|
404
|
+
]);
|
|
405
|
+
const { options, browser } = mock.record.builders[0];
|
|
406
|
+
assert.equal(browser, 'firefox');
|
|
407
|
+
assert.equal(options.binary, '/opt/firefox/firefox');
|
|
408
|
+
assert.deepEqual(options.args, [
|
|
409
|
+
'-profile',
|
|
410
|
+
session.userDataDir,
|
|
411
|
+
'-headless',
|
|
412
|
+
]);
|
|
413
|
+
assert.deepEqual(options.capabilities, {});
|
|
414
|
+
} finally {
|
|
415
|
+
await session.close();
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
it('refuses Chrome launch restrictions for Firefox', async () => {
|
|
420
|
+
await assert.rejects(
|
|
421
|
+
() =>
|
|
422
|
+
launchWebDriver(
|
|
423
|
+
{ browser: 'firefox', restrictions: ['no-extensions'] },
|
|
424
|
+
launchDependencies().dependencies
|
|
425
|
+
),
|
|
426
|
+
/Launch restrictions are Chrome switches/
|
|
427
|
+
);
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
it('stops the driver and removes the profile when the session cannot start', async () => {
|
|
431
|
+
const { started, dependencies } = launchDependencies();
|
|
432
|
+
const failing = createMockSelenium();
|
|
433
|
+
failing.selenium.webdriver.Builder.prototype.build = () => {
|
|
434
|
+
throw new Error('session not created: Chrome failed to start');
|
|
435
|
+
};
|
|
436
|
+
await assert.rejects(
|
|
437
|
+
() =>
|
|
438
|
+
launchWebDriver({}, { ...dependencies, selenium: failing.selenium }),
|
|
439
|
+
/session not created/
|
|
440
|
+
);
|
|
441
|
+
const profile = failing.record.options[0].args
|
|
442
|
+
.find((arg) => arg.startsWith('--user-data-dir='))
|
|
443
|
+
.slice('--user-data-dir='.length);
|
|
444
|
+
assert.ok(started[0].child.killed);
|
|
445
|
+
assert.ok(!existsSync(profile));
|
|
446
|
+
});
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
describe('connectWebDriver', () => {
|
|
450
|
+
it('opens a session on a running server with the given capabilities', async () => {
|
|
451
|
+
const mock = createMockSelenium();
|
|
452
|
+
const session = await connectWebDriver(
|
|
453
|
+
{
|
|
454
|
+
serverUrl: 'http://grid:4444',
|
|
455
|
+
capabilities: { webSocketUrl: true },
|
|
456
|
+
},
|
|
457
|
+
{ selenium: mock.selenium }
|
|
458
|
+
);
|
|
459
|
+
const [builder] = mock.record.builders;
|
|
460
|
+
assert.equal(builder.serverUrl, 'http://grid:4444');
|
|
461
|
+
assert.deepEqual(builder.capabilities, {
|
|
462
|
+
browserName: 'chrome',
|
|
463
|
+
webSocketUrl: true,
|
|
464
|
+
});
|
|
465
|
+
assert.ok(session.page instanceof WebDriverPage);
|
|
466
|
+
await session.close();
|
|
467
|
+
assert.ok(mock.driver.calls.some(([name]) => name === 'quit'));
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
it('requires a server URL', async () => {
|
|
471
|
+
await assert.rejects(() => connectWebDriver({}), /serverUrl is required/);
|
|
472
|
+
});
|
|
473
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drives `bin/browser-commander.js` as a real process through command-stream
|
|
3
|
+
* (issue #104). Only commands that need no browser run here; the browser
|
|
4
|
+
* flows are in tests/e2e/cli.e2e.test.js.
|
|
5
|
+
*/
|
|
6
|
+
import { describe, it } from 'node:test';
|
|
7
|
+
import assert from 'node:assert';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
|
|
10
|
+
import { runCommand } from '../../../src/utilities/subprocess.js';
|
|
11
|
+
|
|
12
|
+
const BIN = fileURLToPath(
|
|
13
|
+
new URL('../../../bin/browser-commander.js', import.meta.url)
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
function browserCommander(args, options = {}) {
|
|
17
|
+
return runCommand(process.execPath, [BIN, ...args], {
|
|
18
|
+
check: false,
|
|
19
|
+
...options,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe('bin/browser-commander.js', () => {
|
|
24
|
+
it('prints the package version as one JSON document', async () => {
|
|
25
|
+
const { code, stdout } = await browserCommander(['version']);
|
|
26
|
+
|
|
27
|
+
assert.equal(code, 0);
|
|
28
|
+
const document = JSON.parse(stdout);
|
|
29
|
+
assert.equal(document.name, 'browser-commander');
|
|
30
|
+
assert.equal(document.language, 'js');
|
|
31
|
+
assert.match(document.version, /^\d+\.\d+\.\d+/u);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('exits 64 with a JSON usage error', async () => {
|
|
35
|
+
const { code, stdout } = await browserCommander(['bogus']);
|
|
36
|
+
|
|
37
|
+
assert.equal(code, 64);
|
|
38
|
+
assert.deepEqual(JSON.parse(stdout), {
|
|
39
|
+
error: { name: 'UsageError', message: 'Unknown command "bogus"' },
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('serves JSON-RPC on stdin until it closes', async () => {
|
|
44
|
+
const input = [
|
|
45
|
+
{ jsonrpc: '2.0', id: 1, method: 'version' },
|
|
46
|
+
{ jsonrpc: '2.0', id: 2, method: 'no.such.method' },
|
|
47
|
+
]
|
|
48
|
+
.map((request) => `${JSON.stringify(request)}\n`)
|
|
49
|
+
.join('');
|
|
50
|
+
|
|
51
|
+
const { code, stdout } = await browserCommander(['serve', '--stdio'], {
|
|
52
|
+
input,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
assert.equal(code, 0);
|
|
56
|
+
const responses = stdout
|
|
57
|
+
.trim()
|
|
58
|
+
.split('\n')
|
|
59
|
+
.map((line) => JSON.parse(line))
|
|
60
|
+
.sort((a, b) => a.id - b.id);
|
|
61
|
+
assert.equal(responses.length, 2);
|
|
62
|
+
assert.equal(responses[0].result.name, 'browser-commander');
|
|
63
|
+
assert.deepEqual(responses[1], {
|
|
64
|
+
jsonrpc: '2.0',
|
|
65
|
+
id: 2,
|
|
66
|
+
error: { code: -32601, message: 'Unknown method: no.such.method' },
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
parseDeclaredMethods,
|
|
6
|
+
playwrightDeclarations,
|
|
7
|
+
puppeteerDeclarations,
|
|
8
|
+
} from '../../helpers/declared-api.js';
|
|
9
|
+
|
|
10
|
+
const SOURCE = `
|
|
11
|
+
export declare abstract class Page extends EventEmitter<PageEvents> {
|
|
12
|
+
#private;
|
|
13
|
+
protected constructor();
|
|
14
|
+
private _internal(): void;
|
|
15
|
+
static create(): Page;
|
|
16
|
+
get keyboard(): Keyboard;
|
|
17
|
+
readonly url: string;
|
|
18
|
+
abstract goto(url: string): Promise<void>;
|
|
19
|
+
$eval<
|
|
20
|
+
Selector extends string,
|
|
21
|
+
>(selector: Selector): Promise<unknown>;
|
|
22
|
+
waitFor?(): void;
|
|
23
|
+
/**
|
|
24
|
+
* docs(): not a member
|
|
25
|
+
*/
|
|
26
|
+
}
|
|
27
|
+
export interface Locator {
|
|
28
|
+
click(options?: {
|
|
29
|
+
force?: boolean;
|
|
30
|
+
trial(): void;
|
|
31
|
+
}): Promise<void>;
|
|
32
|
+
on(event: 'x', listener: () => void): this;
|
|
33
|
+
on(event: 'y', listener: () => void): this;
|
|
34
|
+
}
|
|
35
|
+
`;
|
|
36
|
+
|
|
37
|
+
describe('parseDeclaredMethods', () => {
|
|
38
|
+
it('keeps public methods and drops accessors, statics and privates', () => {
|
|
39
|
+
const types = parseDeclaredMethods(SOURCE);
|
|
40
|
+
|
|
41
|
+
assert.deepEqual([...types.get('Page')], ['goto', '$eval', 'waitFor']);
|
|
42
|
+
assert.deepEqual([...types.get('Locator')], ['click', 'on']);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('reads the installed Playwright and Puppeteer declarations', () => {
|
|
46
|
+
assert.ok(playwrightDeclarations().get('Page').has('goto'));
|
|
47
|
+
assert.ok(puppeteerDeclarations().get('Page').has('createCDPSession'));
|
|
48
|
+
});
|
|
49
|
+
});
|