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
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
// feature-parity: migration.launch
|
|
1
2
|
import assert from 'node:assert';
|
|
2
|
-
import { mkdtemp, rm } from 'node:fs/promises';
|
|
3
|
+
import { access, mkdtemp, rm } from 'node:fs/promises';
|
|
3
4
|
import os from 'node:os';
|
|
4
5
|
import path from 'node:path';
|
|
6
|
+
import { PassThrough } from 'node:stream';
|
|
5
7
|
import { afterEach, describe, it } from 'node:test';
|
|
6
8
|
|
|
7
9
|
import {
|
|
@@ -10,7 +12,13 @@ import {
|
|
|
10
12
|
launchAndConnectRealBrowser,
|
|
11
13
|
launchAndConnectRealBrowserWithDependencies,
|
|
12
14
|
launchRealBrowser,
|
|
15
|
+
waitForCdpEndpoint,
|
|
13
16
|
} from '../../../src/browser/real-browser.js';
|
|
17
|
+
import { PortRaceError } from '../../../src/browser/debugging-port.js';
|
|
18
|
+
import {
|
|
19
|
+
detectAutomationControlledTriggers,
|
|
20
|
+
disablesAutomationControlled,
|
|
21
|
+
} from '../../../src/fingerprint/automation-parity.js';
|
|
14
22
|
import {
|
|
15
23
|
launchAndConnectRealBrowser as publicHelper,
|
|
16
24
|
launchRealBrowser as publicShortHelper,
|
|
@@ -32,55 +40,145 @@ describe('launchAndConnectRealBrowser', () => {
|
|
|
32
40
|
assert.equal(publicShortHelper, launchRealBrowser);
|
|
33
41
|
});
|
|
34
42
|
|
|
35
|
-
it('builds
|
|
43
|
+
it('builds exactly the command line a person would type', () => {
|
|
44
|
+
// Issue #103: nothing but the dedicated profile and a fixed port.
|
|
45
|
+
assert.deepEqual(
|
|
46
|
+
buildRealBrowserArgs({
|
|
47
|
+
userDataDir: '/tmp/browser-commander-dedicated',
|
|
48
|
+
remoteDebuggingPort: 9333,
|
|
49
|
+
}),
|
|
50
|
+
[
|
|
51
|
+
'--user-data-dir=/tmp/browser-commander-dedicated',
|
|
52
|
+
'--remote-debugging-port=9333',
|
|
53
|
+
'about:blank',
|
|
54
|
+
]
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('opens a blank tab unless the caller passes a start URL', () => {
|
|
59
|
+
// With no URL, Edge opens edge://welcome-new-profile, which closes the
|
|
60
|
+
// window a few seconds later. Both engines open about:blank the same way.
|
|
61
|
+
assert.deepEqual(
|
|
62
|
+
buildRealBrowserArgs({
|
|
63
|
+
userDataDir: '/tmp/browser-commander-dedicated',
|
|
64
|
+
remoteDebuggingPort: 9333,
|
|
65
|
+
args: ['--lang=en-US', 'https://example.com/'],
|
|
66
|
+
}),
|
|
67
|
+
[
|
|
68
|
+
'--user-data-dir=/tmp/browser-commander-dedicated',
|
|
69
|
+
'--remote-debugging-port=9333',
|
|
70
|
+
'--lang=en-US',
|
|
71
|
+
'https://example.com/',
|
|
72
|
+
]
|
|
73
|
+
);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('never turns AutomationControlled on in a headful launch (#101)', () => {
|
|
36
77
|
const args = buildRealBrowserArgs({
|
|
37
78
|
userDataDir: '/tmp/browser-commander-dedicated',
|
|
38
79
|
remoteDebuggingPort: 9333,
|
|
39
|
-
|
|
80
|
+
restrictions: ['legacy-defaults', 'no-extensions', 'no-translate'],
|
|
40
81
|
args: ['--lang=en-US'],
|
|
41
82
|
});
|
|
83
|
+
assert.deepEqual(detectAutomationControlledTriggers(args), []);
|
|
84
|
+
// ...so the switch that shows the unsupported-flag infobar is not needed.
|
|
85
|
+
assert.equal(disablesAutomationControlled(args), false);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('refuses port 0, which makes navigator.webdriver true', () => {
|
|
89
|
+
assert.throws(
|
|
90
|
+
() =>
|
|
91
|
+
buildRealBrowserArgs({
|
|
92
|
+
userDataDir: '/tmp/browser-commander-dedicated',
|
|
93
|
+
remoteDebuggingPort: 0,
|
|
94
|
+
}),
|
|
95
|
+
/AutomationControlled/
|
|
96
|
+
);
|
|
97
|
+
});
|
|
42
98
|
|
|
99
|
+
it('launches headless exactly as a person would, with no off switch', () => {
|
|
100
|
+
// A hand-started headless Chrome reports navigator.webdriver false, so the
|
|
101
|
+
// off switch would only be a command-line difference of its own.
|
|
102
|
+
assert.deepEqual(
|
|
103
|
+
buildRealBrowserArgs({
|
|
104
|
+
userDataDir: '/tmp/browser-commander-dedicated',
|
|
105
|
+
remoteDebuggingPort: 9333,
|
|
106
|
+
headless: true,
|
|
107
|
+
}),
|
|
108
|
+
[
|
|
109
|
+
'--user-data-dir=/tmp/browser-commander-dedicated',
|
|
110
|
+
'--remote-debugging-port=9333',
|
|
111
|
+
'--headless=new',
|
|
112
|
+
'about:blank',
|
|
113
|
+
]
|
|
114
|
+
);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('adds the off switch when a custom argument is a trigger', () => {
|
|
118
|
+
const args = buildRealBrowserArgs({
|
|
119
|
+
userDataDir: '/tmp/browser-commander-dedicated',
|
|
120
|
+
remoteDebuggingPort: 9333,
|
|
121
|
+
args: ['--disable-blink-features=Foo', '--enable-automation'],
|
|
122
|
+
});
|
|
43
123
|
assert.deepEqual(args, [
|
|
44
|
-
'--remote-debugging-address=127.0.0.1',
|
|
45
|
-
'--remote-debugging-port=9333',
|
|
46
124
|
'--user-data-dir=/tmp/browser-commander-dedicated',
|
|
47
|
-
'--
|
|
48
|
-
'--
|
|
49
|
-
'--
|
|
50
|
-
'
|
|
51
|
-
'--no-first-run',
|
|
52
|
-
'--no-default-browser-check',
|
|
53
|
-
'--disable-crash-restore',
|
|
54
|
-
'--headless=new',
|
|
55
|
-
'--lang=en-US',
|
|
125
|
+
'--remote-debugging-port=9333',
|
|
126
|
+
'--disable-blink-features=Foo,AutomationControlled',
|
|
127
|
+
'--enable-automation',
|
|
128
|
+
'about:blank',
|
|
56
129
|
]);
|
|
130
|
+
assert.equal(
|
|
131
|
+
buildRealBrowserArgs({
|
|
132
|
+
userDataDir: '/tmp/browser-commander-dedicated',
|
|
133
|
+
remoteDebuggingPort: 9333,
|
|
134
|
+
args: ['--enable-automation'],
|
|
135
|
+
automationParity: false,
|
|
136
|
+
}).includes('--disable-blink-features=AutomationControlled'),
|
|
137
|
+
false
|
|
138
|
+
);
|
|
57
139
|
});
|
|
58
140
|
|
|
59
|
-
it('
|
|
141
|
+
it('applies opt-in restrictions and merges feature lists', () => {
|
|
60
142
|
const args = buildRealBrowserArgs({
|
|
61
143
|
userDataDir: '/tmp/browser-commander-dedicated',
|
|
62
|
-
|
|
144
|
+
remoteDebuggingPort: 9333,
|
|
145
|
+
restrictions: ['no-sync', 'no-translate'],
|
|
146
|
+
args: ['--legacy-arg', '--disable-features=Foo'],
|
|
63
147
|
extraArgs: ['--lang=en-US'],
|
|
64
|
-
ignoreDefaultArgs: ['--no-first-run'],
|
|
65
148
|
});
|
|
66
149
|
|
|
67
|
-
assert.
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
150
|
+
assert.deepEqual(args, [
|
|
151
|
+
'--user-data-dir=/tmp/browser-commander-dedicated',
|
|
152
|
+
'--remote-debugging-port=9333',
|
|
153
|
+
'--disable-sync',
|
|
154
|
+
'--disable-features=Translate,Foo',
|
|
155
|
+
'--legacy-arg',
|
|
156
|
+
'--lang=en-US',
|
|
157
|
+
'about:blank',
|
|
158
|
+
]);
|
|
159
|
+
assert.throws(
|
|
160
|
+
() =>
|
|
161
|
+
buildRealBrowserArgs({
|
|
162
|
+
userDataDir: '/tmp/x',
|
|
163
|
+
remoteDebuggingPort: 9333,
|
|
164
|
+
restrictions: ['no-such-thing'],
|
|
165
|
+
}),
|
|
166
|
+
/Unknown launch restriction/
|
|
167
|
+
);
|
|
71
168
|
});
|
|
72
169
|
|
|
73
170
|
it('rejects custom arguments that could bypass protected CDP settings', () => {
|
|
74
171
|
for (const argument of [
|
|
75
172
|
'--remote-debugging-address=0.0.0.0',
|
|
76
173
|
'--remote-debugging-port=9222',
|
|
174
|
+
'--remote-debugging-pipe',
|
|
77
175
|
'--user-data-dir=/tmp/other',
|
|
78
176
|
]) {
|
|
79
177
|
assert.throws(
|
|
80
178
|
() =>
|
|
81
179
|
buildRealBrowserArgs({
|
|
82
180
|
userDataDir: '/tmp/browser-commander-dedicated',
|
|
83
|
-
remoteDebuggingPort:
|
|
181
|
+
remoteDebuggingPort: 9333,
|
|
84
182
|
args: [argument],
|
|
85
183
|
}),
|
|
86
184
|
/managed by launchAndConnectRealBrowser/
|
|
@@ -106,11 +204,9 @@ describe('launchAndConnectRealBrowser', () => {
|
|
|
106
204
|
});
|
|
107
205
|
|
|
108
206
|
it('spawns, waits, connects, and returns process metadata', async () => {
|
|
109
|
-
temporaryDirectory = await
|
|
110
|
-
path.join(os.tmpdir(), 'browser-commander-real-browser-test-')
|
|
111
|
-
);
|
|
207
|
+
temporaryDirectory = await makeDedicatedProfile();
|
|
112
208
|
const calls = [];
|
|
113
|
-
const browserProcess =
|
|
209
|
+
const browserProcess = fakeProcess(calls);
|
|
114
210
|
const browser = { id: 'browser' };
|
|
115
211
|
const page = { id: 'page' };
|
|
116
212
|
|
|
@@ -119,13 +215,13 @@ describe('launchAndConnectRealBrowser', () => {
|
|
|
119
215
|
engine: 'puppeteer',
|
|
120
216
|
channel: 'chrome',
|
|
121
217
|
userDataDir: temporaryDirectory,
|
|
122
|
-
remoteDebuggingPort: 0,
|
|
123
218
|
seedCookies: [{ name: 'SID', value: 'saved' }],
|
|
124
219
|
},
|
|
125
220
|
{
|
|
126
221
|
resolveExecutable: async () => '/opt/google/chrome',
|
|
127
|
-
|
|
128
|
-
|
|
222
|
+
reservePort: async () => 9444,
|
|
223
|
+
spawnBrowser: (executablePath, args, options) => {
|
|
224
|
+
calls.push(['spawn', executablePath, args, options]);
|
|
129
225
|
return browserProcess;
|
|
130
226
|
},
|
|
131
227
|
waitForEndpoint: async (options) => {
|
|
@@ -143,9 +239,21 @@ describe('launchAndConnectRealBrowser', () => {
|
|
|
143
239
|
assert.equal(result.page, page);
|
|
144
240
|
assert.equal(result.browserProcess, browserProcess);
|
|
145
241
|
assert.equal(result.cdpEndpoint, 'http://127.0.0.1:9444');
|
|
242
|
+
assert.equal(result.remoteDebuggingPort, 9444);
|
|
146
243
|
assert.equal(result.executablePath, '/opt/google/chrome');
|
|
147
244
|
assert.equal(result.userDataDir, temporaryDirectory);
|
|
148
|
-
assert.
|
|
245
|
+
assert.equal(result.temporaryProfile, false);
|
|
246
|
+
assert.deepEqual(calls[0], [
|
|
247
|
+
'spawn',
|
|
248
|
+
'/opt/google/chrome',
|
|
249
|
+
[
|
|
250
|
+
`--user-data-dir=${temporaryDirectory}`,
|
|
251
|
+
'--remote-debugging-port=9444',
|
|
252
|
+
'about:blank',
|
|
253
|
+
],
|
|
254
|
+
{ env: undefined, verbose: false },
|
|
255
|
+
]);
|
|
256
|
+
assert.deepEqual(calls[1], ['wait', 9444]);
|
|
149
257
|
assert.deepEqual(calls[2], [
|
|
150
258
|
'connect',
|
|
151
259
|
{
|
|
@@ -155,12 +263,213 @@ describe('launchAndConnectRealBrowser', () => {
|
|
|
155
263
|
verbose: false,
|
|
156
264
|
},
|
|
157
265
|
]);
|
|
266
|
+
// First-run UI is suppressed with Chrome's own sentinel, not a switch.
|
|
267
|
+
await access(path.join(temporaryDirectory, 'First Run'));
|
|
158
268
|
});
|
|
159
269
|
|
|
160
|
-
it('
|
|
161
|
-
temporaryDirectory = await
|
|
162
|
-
|
|
270
|
+
it('migrates a profile before launch and seeds the migrated cookies', async () => {
|
|
271
|
+
temporaryDirectory = await makeDedicatedProfile();
|
|
272
|
+
const calls = [];
|
|
273
|
+
const browserProcess = fakeProcess(calls);
|
|
274
|
+
let connectOptions;
|
|
275
|
+
let migrateOptions;
|
|
276
|
+
|
|
277
|
+
const result = await launchAndConnectRealBrowserWithDependencies(
|
|
278
|
+
{
|
|
279
|
+
engine: 'puppeteer',
|
|
280
|
+
channel: 'chrome',
|
|
281
|
+
userDataDir: temporaryDirectory,
|
|
282
|
+
seedCookies: [{ name: 'existing', value: 'keep' }],
|
|
283
|
+
migrateFrom: {
|
|
284
|
+
browser: 'chrome',
|
|
285
|
+
profile: 'Default',
|
|
286
|
+
include: ['cookies', 'bookmarks'],
|
|
287
|
+
domains: ['google.com'],
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
resolveExecutable: async () => '/opt/google/chrome',
|
|
292
|
+
reservePort: async () => 9445,
|
|
293
|
+
spawnBrowser: () => browserProcess,
|
|
294
|
+
waitForEndpoint: async () => 'http://127.0.0.1:9445',
|
|
295
|
+
connect: async (options) => {
|
|
296
|
+
connectOptions = options;
|
|
297
|
+
return { browser: {}, page: {} };
|
|
298
|
+
},
|
|
299
|
+
migrateProfile: async (options) => {
|
|
300
|
+
migrateOptions = options;
|
|
301
|
+
return {
|
|
302
|
+
source: {
|
|
303
|
+
browser: 'chrome',
|
|
304
|
+
profile: 'Default',
|
|
305
|
+
userDataDir: null,
|
|
306
|
+
},
|
|
307
|
+
target: options.to,
|
|
308
|
+
migrated: {
|
|
309
|
+
cookies: 1,
|
|
310
|
+
bookmarks: 2,
|
|
311
|
+
history: 0,
|
|
312
|
+
passwords: 0,
|
|
313
|
+
preferences: 0,
|
|
314
|
+
extensions: 0,
|
|
315
|
+
},
|
|
316
|
+
skipped: [],
|
|
317
|
+
warnings: [],
|
|
318
|
+
cookies: [
|
|
319
|
+
{ name: 'SID', value: 'migrated', domain: '.google.com' },
|
|
320
|
+
],
|
|
321
|
+
};
|
|
322
|
+
},
|
|
323
|
+
}
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
// Migration runs before launch, targeting the Default profile directory,
|
|
327
|
+
// and receives the launching channel as the target browser for key
|
|
328
|
+
// derivation.
|
|
329
|
+
assert.equal(migrateOptions.to, path.join(temporaryDirectory, 'Default'));
|
|
330
|
+
assert.equal(migrateOptions.targetBrowser, 'chrome');
|
|
331
|
+
assert.deepEqual(migrateOptions.include, ['cookies', 'bookmarks']);
|
|
332
|
+
assert.deepEqual(migrateOptions.domains, ['google.com']);
|
|
333
|
+
assert.deepEqual(migrateOptions.from, {
|
|
334
|
+
browser: 'chrome',
|
|
335
|
+
profile: 'Default',
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
// Migrated cookies are appended to any explicit seedCookies.
|
|
339
|
+
assert.deepEqual(connectOptions.seedCookies, [
|
|
340
|
+
{ name: 'existing', value: 'keep' },
|
|
341
|
+
{ name: 'SID', value: 'migrated', domain: '.google.com' },
|
|
342
|
+
]);
|
|
343
|
+
|
|
344
|
+
// The session exposes the report without the bulky raw cookie array.
|
|
345
|
+
assert.equal(result.migration.migrated.bookmarks, 2);
|
|
346
|
+
assert.equal(result.migration.migrated.cookies, 1);
|
|
347
|
+
assert.equal(result.migration.cookies, undefined);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it('retries with a new reserved port after a port race', async () => {
|
|
351
|
+
const calls = [];
|
|
352
|
+
const ports = [40001, 40002];
|
|
353
|
+
const result = await launchAndConnectRealBrowserWithDependencies(
|
|
354
|
+
{ closeTimeout: 50 },
|
|
355
|
+
{
|
|
356
|
+
resolveExecutable: async () => '/opt/google/chrome',
|
|
357
|
+
reservePort: async () => ports.shift(),
|
|
358
|
+
spawnBrowser: () => fakeProcess(calls),
|
|
359
|
+
waitForEndpoint: async ({ remoteDebuggingPort }) => {
|
|
360
|
+
calls.push(['wait', remoteDebuggingPort]);
|
|
361
|
+
if (remoteDebuggingPort === 40001) {
|
|
362
|
+
throw new PortRaceError(40001, 'taken');
|
|
363
|
+
}
|
|
364
|
+
return `http://127.0.0.1:${remoteDebuggingPort}`;
|
|
365
|
+
},
|
|
366
|
+
connect: async () => ({ browser: null, page: null }),
|
|
367
|
+
}
|
|
368
|
+
);
|
|
369
|
+
assert.deepEqual(calls, [['wait', 40001], ['kill'], ['wait', 40002]]);
|
|
370
|
+
assert.equal(result.remoteDebuggingPort, 40002);
|
|
371
|
+
await result.close();
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
it('does not retry a port the caller chose', async () => {
|
|
375
|
+
let attempts = 0;
|
|
376
|
+
await assert.rejects(
|
|
377
|
+
() =>
|
|
378
|
+
launchAndConnectRealBrowserWithDependencies(
|
|
379
|
+
{ remoteDebuggingPort: 9555 },
|
|
380
|
+
{
|
|
381
|
+
resolveExecutable: async () => '/opt/google/chrome',
|
|
382
|
+
spawnBrowser: () => fakeProcess([]),
|
|
383
|
+
waitForEndpoint: async () => {
|
|
384
|
+
attempts++;
|
|
385
|
+
throw new PortRaceError(9555, 'taken');
|
|
386
|
+
},
|
|
387
|
+
}
|
|
388
|
+
),
|
|
389
|
+
PortRaceError
|
|
390
|
+
);
|
|
391
|
+
assert.equal(attempts, 1);
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
it('uses a fresh temporary profile by default and deletes it on close', async () => {
|
|
395
|
+
const calls = [];
|
|
396
|
+
const browserProcess = fakeProcess(calls);
|
|
397
|
+
const result = await launchAndConnectRealBrowserWithDependencies(
|
|
398
|
+
{ closeTimeout: 50 },
|
|
399
|
+
{
|
|
400
|
+
resolveExecutable: async () => '/opt/google/chrome',
|
|
401
|
+
reservePort: async () => 40003,
|
|
402
|
+
spawnBrowser: () => browserProcess,
|
|
403
|
+
waitForEndpoint: async () => 'http://127.0.0.1:40003',
|
|
404
|
+
connect: async () => ({ browser: null, page: null }),
|
|
405
|
+
}
|
|
406
|
+
);
|
|
407
|
+
assert.equal(result.temporaryProfile, true);
|
|
408
|
+
assert.match(
|
|
409
|
+
path.basename(result.userDataDir),
|
|
410
|
+
/^browser-commander-profile-/
|
|
411
|
+
);
|
|
412
|
+
await access(path.join(result.userDataDir, 'First Run'));
|
|
413
|
+
|
|
414
|
+
await result.close();
|
|
415
|
+
assert.deepEqual(calls, [['kill']]);
|
|
416
|
+
await assert.rejects(() => access(result.userDataDir), /ENOENT/);
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
it('closes the browser gracefully through the engine before killing it', async () => {
|
|
420
|
+
const calls = [];
|
|
421
|
+
const browserProcess = fakeProcess(calls);
|
|
422
|
+
const browser = {
|
|
423
|
+
close: async () => {
|
|
424
|
+
calls.push(['browser.close']);
|
|
425
|
+
browserProcess.exit(0);
|
|
426
|
+
},
|
|
427
|
+
};
|
|
428
|
+
const result = await launchAndConnectRealBrowserWithDependencies(
|
|
429
|
+
{ engine: 'puppeteer' },
|
|
430
|
+
{
|
|
431
|
+
resolveExecutable: async () => '/opt/google/chrome',
|
|
432
|
+
reservePort: async () => 40004,
|
|
433
|
+
spawnBrowser: () => browserProcess,
|
|
434
|
+
waitForEndpoint: async () => 'http://127.0.0.1:40004',
|
|
435
|
+
connect: async () => ({ browser, page: null }),
|
|
436
|
+
}
|
|
437
|
+
);
|
|
438
|
+
// browser.close() is the session's close(), so it cleans up too.
|
|
439
|
+
await browser.close();
|
|
440
|
+
assert.deepEqual(calls, [['browser.close']]);
|
|
441
|
+
await assert.rejects(() => access(result.userDataDir), /ENOENT/);
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
it('passes restriction environment to the browser only', async () => {
|
|
445
|
+
const before = process.env.GOOGLE_API_KEY;
|
|
446
|
+
let spawnOptions;
|
|
447
|
+
const result = await launchAndConnectRealBrowserWithDependencies(
|
|
448
|
+
{
|
|
449
|
+
restrictions: ['no-google-services'],
|
|
450
|
+
env: { EXTRA: '1' },
|
|
451
|
+
closeTimeout: 50,
|
|
452
|
+
},
|
|
453
|
+
{
|
|
454
|
+
resolveExecutable: async () => '/opt/google/chrome',
|
|
455
|
+
reservePort: async () => 40005,
|
|
456
|
+
spawnBrowser: (executablePath, args, options) => {
|
|
457
|
+
spawnOptions = options;
|
|
458
|
+
return fakeProcess([]);
|
|
459
|
+
},
|
|
460
|
+
waitForEndpoint: async () => 'http://127.0.0.1:40005',
|
|
461
|
+
connect: async () => ({ browser: null, page: null }),
|
|
462
|
+
}
|
|
163
463
|
);
|
|
464
|
+
assert.equal(spawnOptions.env.GOOGLE_API_KEY, 'no');
|
|
465
|
+
assert.equal(spawnOptions.env.GOOGLE_DEFAULT_CLIENT_ID, 'no');
|
|
466
|
+
assert.equal(spawnOptions.env.EXTRA, '1');
|
|
467
|
+
assert.equal(process.env.GOOGLE_API_KEY, before);
|
|
468
|
+
await result.close();
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
it('terminates the spawned browser when connection fails', async () => {
|
|
472
|
+
temporaryDirectory = await makeDedicatedProfile();
|
|
164
473
|
let killed = false;
|
|
165
474
|
const browserProcess = {
|
|
166
475
|
exitCode: null,
|
|
@@ -175,6 +484,7 @@ describe('launchAndConnectRealBrowser', () => {
|
|
|
175
484
|
{ userDataDir: temporaryDirectory },
|
|
176
485
|
{
|
|
177
486
|
resolveExecutable: async () => '/opt/google/chrome',
|
|
487
|
+
reservePort: async () => 9222,
|
|
178
488
|
spawnBrowser: () => browserProcess,
|
|
179
489
|
waitForEndpoint: async () => 'http://127.0.0.1:9222',
|
|
180
490
|
connect: async () => {
|
|
@@ -188,3 +498,118 @@ describe('launchAndConnectRealBrowser', () => {
|
|
|
188
498
|
assert.equal(killed, true);
|
|
189
499
|
});
|
|
190
500
|
});
|
|
501
|
+
|
|
502
|
+
describe('waitForCdpEndpoint', () => {
|
|
503
|
+
const listening = (port, id = 'abc') =>
|
|
504
|
+
`DevTools listening on ws://127.0.0.1:${port}/devtools/browser/${id}\n`;
|
|
505
|
+
|
|
506
|
+
function stderrWith(text) {
|
|
507
|
+
const stream = new PassThrough();
|
|
508
|
+
stream.write(text);
|
|
509
|
+
return stream;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
it('confirms ownership from the stderr line and the served browser id', async () => {
|
|
513
|
+
const endpoint = await waitForCdpEndpoint({
|
|
514
|
+
remoteDebuggingPort: 9777,
|
|
515
|
+
userDataDir: '/nonexistent',
|
|
516
|
+
browserProcess: { exitCode: null, stderr: stderrWith(listening(9777)) },
|
|
517
|
+
timeout: 2000,
|
|
518
|
+
fetchImplementation: async (url) => {
|
|
519
|
+
assert.equal(url, 'http://127.0.0.1:9777/json/version');
|
|
520
|
+
return {
|
|
521
|
+
ok: true,
|
|
522
|
+
json: async () => ({
|
|
523
|
+
webSocketDebuggerUrl: 'ws://127.0.0.1:9777/devtools/browser/abc',
|
|
524
|
+
}),
|
|
525
|
+
};
|
|
526
|
+
},
|
|
527
|
+
});
|
|
528
|
+
assert.equal(endpoint, 'http://127.0.0.1:9777');
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
const waitWith = (stderr, fetchImplementation, timeout = 2000) =>
|
|
532
|
+
waitForCdpEndpoint({
|
|
533
|
+
remoteDebuggingPort: 9777,
|
|
534
|
+
userDataDir: '/nonexistent',
|
|
535
|
+
browserProcess: { exitCode: null, stderr },
|
|
536
|
+
timeout,
|
|
537
|
+
fetchImplementation,
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
it('reports a race when another browser answers on the port', async () => {
|
|
541
|
+
await assert.rejects(
|
|
542
|
+
() =>
|
|
543
|
+
waitWith(stderrWith(listening(9777, 'ours')), async () => ({
|
|
544
|
+
ok: true,
|
|
545
|
+
json: async () => ({
|
|
546
|
+
webSocketDebuggerUrl:
|
|
547
|
+
'ws://127.0.0.1:9777/devtools/browser/somebody-else',
|
|
548
|
+
}),
|
|
549
|
+
})),
|
|
550
|
+
PortRaceError
|
|
551
|
+
);
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
it('reports a race when Chrome fell back to another address', async () => {
|
|
555
|
+
const stderr = stderrWith(
|
|
556
|
+
'bind() failed: Address already in use (98)\nDevTools listening on ws://[::1]:9777/devtools/browser/x\n'
|
|
557
|
+
);
|
|
558
|
+
await assert.rejects(
|
|
559
|
+
() =>
|
|
560
|
+
waitWith(stderr, async () => {
|
|
561
|
+
throw new Error('must not probe a port we do not own');
|
|
562
|
+
}),
|
|
563
|
+
PortRaceError
|
|
564
|
+
);
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
it('never probes the port before the browser claims it', async () => {
|
|
568
|
+
let probes = 0;
|
|
569
|
+
await assert.rejects(
|
|
570
|
+
() =>
|
|
571
|
+
waitWith(
|
|
572
|
+
new PassThrough(),
|
|
573
|
+
async () => {
|
|
574
|
+
probes++;
|
|
575
|
+
return { ok: false };
|
|
576
|
+
},
|
|
577
|
+
300
|
|
578
|
+
),
|
|
579
|
+
/Timed out/
|
|
580
|
+
);
|
|
581
|
+
assert.equal(probes, 0);
|
|
582
|
+
});
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
// A dedicated user-data directory; the suite's afterEach removes it.
|
|
586
|
+
function makeDedicatedProfile() {
|
|
587
|
+
return mkdtemp(
|
|
588
|
+
path.join(os.tmpdir(), 'browser-commander-real-browser-test-')
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function fakeProcess(calls) {
|
|
593
|
+
const listeners = [];
|
|
594
|
+
const process = {
|
|
595
|
+
exitCode: null,
|
|
596
|
+
kill: () => {
|
|
597
|
+
calls.push(['kill']);
|
|
598
|
+
process.exit(143);
|
|
599
|
+
},
|
|
600
|
+
once: (event, listener) => {
|
|
601
|
+
if (process.exitCode === null) {
|
|
602
|
+
listeners.push(listener);
|
|
603
|
+
} else {
|
|
604
|
+
listener(process.exitCode);
|
|
605
|
+
}
|
|
606
|
+
},
|
|
607
|
+
exit: (code) => {
|
|
608
|
+
process.exitCode = code;
|
|
609
|
+
for (const listener of listeners.splice(0)) {
|
|
610
|
+
listener(code);
|
|
611
|
+
}
|
|
612
|
+
},
|
|
613
|
+
};
|
|
614
|
+
return process;
|
|
615
|
+
}
|