browser-commander 0.32.1 → 0.33.1
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 +16 -0
- package/README.md +52 -5
- package/package.json +1 -1
- package/src/browser/browser-handoff.js +4 -0
- package/src/browser/chrome-for-testing-cache.js +198 -0
- package/src/browser/launch-executable.js +203 -0
- package/src/browser/launch-restrictions.json +7 -1
- package/src/browser/launcher.js +78 -65
- package/src/browser/migration/firefox.js +1 -1
- package/src/browser/migration/index.js +10 -0
- package/src/browser/migration/os-crypt-keys.js +36 -6
- package/src/browser/migration/passwords.js +1 -1
- package/src/browser/parity.js +6 -1
- package/src/browser/real-browser.js +53 -14
- package/src/browser/webdriver.js +63 -8
- package/src/capture/screenshot.js +16 -1
- package/src/capture/viewport.js +2 -1
- package/src/exports.js +5 -0
- package/src/traces/recorder.js +18 -2
- package/tests/e2e/chrome-for-testing.e2e.test.js +105 -0
- package/tests/e2e/startup-readiness.e2e.test.js +2 -0
- package/tests/unit/browser/browser-handoff.test.js +13 -0
- package/tests/unit/browser/chrome-for-testing-cache.test.js +139 -0
- package/tests/unit/browser/launch-executable.test.js +94 -0
- package/tests/unit/browser/launcher.test.js +235 -31
- package/tests/unit/browser/migration/os-crypt-keys.test.js +57 -0
- package/tests/unit/browser/migration/safari-data.test.js +43 -19
- package/tests/unit/browser/parity.test.js +19 -0
- package/tests/unit/browser/real-browser.test.js +125 -0
- package/tests/unit/browser/webdriver.test.js +95 -14
- package/tests/unit/scripts/check-web-archive.test.js +77 -2
- package/tests/unit/traces/issue-162-defaults.test.js +1 -1
- package/tests/unit/traces/issue-166-policy.test.js +1 -1
- package/tests/unit/traces/issue-180-screenshots.test.js +173 -0
package/src/browser/launcher.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { access } from 'node:fs/promises';
|
|
2
1
|
import { launchFailure } from './launch-diagnostics.js';
|
|
3
2
|
import {
|
|
4
3
|
installSessionPersistence,
|
|
@@ -20,9 +19,13 @@ import {
|
|
|
20
19
|
configureUserDataDir,
|
|
21
20
|
removeUserDataDir,
|
|
22
21
|
} from './profile-directory.js';
|
|
22
|
+
import {
|
|
23
|
+
ENGINE_BROWSERS,
|
|
24
|
+
defaultRestrictionsFor,
|
|
25
|
+
resolveLaunchExecutable,
|
|
26
|
+
} from './launch-executable.js';
|
|
23
27
|
import { launchRealBrowser } from './real-browser.js';
|
|
24
28
|
import { resolveRestrictions } from './restrictions.js';
|
|
25
|
-
import { resolveSystemBrowserExecutable } from './system-browser.js';
|
|
26
29
|
import { applyFingerprint } from '../fingerprint/apply.js';
|
|
27
30
|
import { attachDownloads } from '../downloads/attach.js';
|
|
28
31
|
import { launchWebDriver } from './webdriver.js';
|
|
@@ -36,14 +39,11 @@ import {
|
|
|
36
39
|
} from './storage-state.js';
|
|
37
40
|
import { interruptionMode } from '../utilities/subprocess.js';
|
|
38
41
|
|
|
42
|
+
export { resolveLaunchExecutable };
|
|
43
|
+
|
|
39
44
|
/** How `launchBrowser` starts the browser (issue #103). */
|
|
40
45
|
export const LAUNCH_MODES = Object.freeze(['real', 'engine']);
|
|
41
46
|
|
|
42
|
-
const loadEngine = {
|
|
43
|
-
playwright: async () => (await import('playwright')).chromium,
|
|
44
|
-
puppeteer: async () => (await import('puppeteer')).default,
|
|
45
|
-
};
|
|
46
|
-
|
|
47
47
|
function validateLaunchMode({ engine, launch, attach }) {
|
|
48
48
|
assertSupportedEngine(engine);
|
|
49
49
|
if (!LAUNCH_MODES.includes(launch)) {
|
|
@@ -58,48 +58,6 @@ function validateLaunchMode({ engine, launch, attach }) {
|
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
async function isExecutable(file) {
|
|
62
|
-
try {
|
|
63
|
-
await access(file);
|
|
64
|
-
return true;
|
|
65
|
-
} catch {
|
|
66
|
-
return false;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Pick the browser binary for a real launch.
|
|
72
|
-
*
|
|
73
|
-
* An explicit `executablePath` or `channel` is honoured as given. Without
|
|
74
|
-
* either, the installed Google Chrome is preferred - it is the browser a
|
|
75
|
-
* person would start - and the engine's own downloaded Chromium is the
|
|
76
|
-
* fallback, so a machine with only `npx playwright install` still works. The
|
|
77
|
-
* binary is spawned by Browser Commander either way, with the same clean
|
|
78
|
-
* command line.
|
|
79
|
-
*/
|
|
80
|
-
export async function resolveLaunchExecutable(
|
|
81
|
-
{ engine, channel, executablePath },
|
|
82
|
-
{ resolveSystem = resolveSystemBrowserExecutable, loadEngineModule } = {}
|
|
83
|
-
) {
|
|
84
|
-
if (executablePath !== undefined || channel !== undefined) {
|
|
85
|
-
return await resolveSystem({
|
|
86
|
-
channel: channel ?? 'chrome',
|
|
87
|
-
executablePath,
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
try {
|
|
91
|
-
return await resolveSystem({ channel: 'chrome' });
|
|
92
|
-
} catch (error) {
|
|
93
|
-
const bundled = await (loadEngineModule ?? loadEngine[engine])()
|
|
94
|
-
.then((module) => module.executablePath())
|
|
95
|
-
.catch(() => undefined);
|
|
96
|
-
if (bundled && (await isExecutable(bundled))) {
|
|
97
|
-
return bundled;
|
|
98
|
-
}
|
|
99
|
-
throw error;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
61
|
async function launchReal(options, dependencies) {
|
|
104
62
|
const {
|
|
105
63
|
engine,
|
|
@@ -144,14 +102,47 @@ async function launchReal(options, dependencies) {
|
|
|
144
102
|
return { browser, page, close, connectedBrowser, ...meta };
|
|
145
103
|
}
|
|
146
104
|
|
|
105
|
+
// The Chrome for Testing an engine launch starts on its own, or the one a
|
|
106
|
+
// Selenium launch is pointed at; undefined for any installed browser.
|
|
107
|
+
async function engineExecutable(
|
|
108
|
+
{ engine, browser = 'chrome', channel, executablePath },
|
|
109
|
+
engineModule,
|
|
110
|
+
dependencies
|
|
111
|
+
) {
|
|
112
|
+
if (executablePath !== undefined || channel !== undefined) {
|
|
113
|
+
return executablePath;
|
|
114
|
+
}
|
|
115
|
+
if (engine === 'selenium') {
|
|
116
|
+
// Without Chrome for Testing this fails rather than let the driver pick
|
|
117
|
+
// the installed Google Chrome (issue #179).
|
|
118
|
+
return browser === 'chrome'
|
|
119
|
+
? await resolveLaunchExecutable({ engine }, dependencies)
|
|
120
|
+
: undefined;
|
|
121
|
+
}
|
|
122
|
+
return await Promise.resolve()
|
|
123
|
+
.then(() => engineModule.executablePath?.())
|
|
124
|
+
.catch(() => undefined);
|
|
125
|
+
}
|
|
126
|
+
|
|
147
127
|
async function launchWithEngine(options, dependencies) {
|
|
148
128
|
if (options.engine === 'selenium') {
|
|
129
|
+
// launchWebDriver adds the defaults of the browser it resolves.
|
|
130
|
+
const executablePath = await engineExecutable(options, null, dependencies);
|
|
149
131
|
const launched = await (dependencies.launchWebDriver ?? launchWebDriver)({
|
|
150
132
|
...options,
|
|
133
|
+
...(executablePath ? { executablePath } : {}),
|
|
151
134
|
args: [...options.args, ...options.extraArgs],
|
|
152
135
|
});
|
|
153
136
|
return { ...launched, browser: launched.driver };
|
|
154
137
|
}
|
|
138
|
+
const engineModule = await (
|
|
139
|
+
dependencies.loadEngineModule ?? ENGINE_BROWSERS[options.engine]
|
|
140
|
+
)();
|
|
141
|
+
const defaultRestrictions = defaultRestrictionsFor(
|
|
142
|
+
await engineExecutable(options, engineModule, dependencies),
|
|
143
|
+
options.defaultRestrictions
|
|
144
|
+
);
|
|
145
|
+
const restrictions = [...defaultRestrictions, ...options.restrictions];
|
|
155
146
|
const {
|
|
156
147
|
engine,
|
|
157
148
|
userDataDir: requestedUserDataDir,
|
|
@@ -160,7 +151,6 @@ async function launchWithEngine(options, dependencies) {
|
|
|
160
151
|
args,
|
|
161
152
|
extraArgs,
|
|
162
153
|
ignoreDefaultArgs,
|
|
163
|
-
restrictions,
|
|
164
154
|
env,
|
|
165
155
|
colorScheme,
|
|
166
156
|
channel,
|
|
@@ -195,9 +185,6 @@ async function launchWithEngine(options, dependencies) {
|
|
|
195
185
|
},
|
|
196
186
|
localState: options.localState,
|
|
197
187
|
});
|
|
198
|
-
const engineModule = await (
|
|
199
|
-
dependencies.loadEngineModule ?? loadEngine[engine]
|
|
200
|
-
)();
|
|
201
188
|
const selection = { channel, executablePath };
|
|
202
189
|
|
|
203
190
|
let browser;
|
|
@@ -263,6 +250,7 @@ async function launchWithEngine(options, dependencies) {
|
|
|
263
250
|
userDataDir,
|
|
264
251
|
temporaryProfile,
|
|
265
252
|
args: chrome.args,
|
|
253
|
+
defaultRestrictions,
|
|
266
254
|
};
|
|
267
255
|
}
|
|
268
256
|
|
|
@@ -285,13 +273,20 @@ async function unfocusAddressBar(page, verbose, settleMs = 500) {
|
|
|
285
273
|
/**
|
|
286
274
|
* Launch a browser.
|
|
287
275
|
*
|
|
288
|
-
* By default (`launch: 'real'`) Browser Commander starts
|
|
289
|
-
*
|
|
276
|
+
* By default (`launch: 'real'`) Browser Commander starts Chrome itself,
|
|
277
|
+
* exactly like a person who wants to attach a debugger would:
|
|
290
278
|
* `--user-data-dir=<fresh temporary profile> --remote-debugging-port=<reserved
|
|
291
279
|
* port>` and nothing else, then attaches the engine over CDP (issues #101 and
|
|
292
280
|
* #103). The profile is deleted on close. Every restriction the library used
|
|
293
281
|
* to add silently is an explicit opt-in through `restrictions`.
|
|
294
282
|
*
|
|
283
|
+
* The browser is the engine's Chrome for Testing, an application of its own,
|
|
284
|
+
* so the user's Google Chrome is never taken over; `channel: 'chrome'` asks
|
|
285
|
+
* for the installed Google Chrome. Chrome for Testing gets the
|
|
286
|
+
* `chrome-for-testing` restrictions (`--test-type=gpu`, the only switch that
|
|
287
|
+
* hides its "only for automated testing" bar, and `--use-mock-keychain`)
|
|
288
|
+
* unless `defaultRestrictions` says otherwise (issue #179).
|
|
289
|
+
*
|
|
295
290
|
* `launch: 'engine'` keeps the Playwright/Puppeteer-launched browser for CI
|
|
296
291
|
* and headless use; the switches those engines add are listed in
|
|
297
292
|
* limitations.json (`engine-launch-switches`).
|
|
@@ -304,12 +299,13 @@ async function unfocusAddressBar(page, verbose, settleMs = 500) {
|
|
|
304
299
|
* @param {number} [options.slowMo=0] - Slow down operations by ms
|
|
305
300
|
* @param {boolean} [options.verbose=false] - Enable verbose logging
|
|
306
301
|
* @param {string[]} [options.restrictions] - Opt-in restrictions from launch-restrictions.json, such as 'no-extensions' or the 'legacy-defaults' preset
|
|
302
|
+
* @param {string[]} [options.defaultRestrictions] - Restrictions that replace the executable's defaults (the 'chrome-for-testing' preset for Chrome for Testing, none otherwise); [] turns them off
|
|
307
303
|
* @param {string[]} [options.args] - Additional Chrome arguments
|
|
308
304
|
* @param {string[]} [options.extraArgs] - Additional Chrome arguments appended after args
|
|
309
305
|
* @param {boolean|string[]} [options.ignoreDefaultArgs] - Engine default switches to omit, or true for all ('engine' launch only)
|
|
310
306
|
* @param {Object<string,string>} [options.env] - Extra environment for the browser process only
|
|
311
307
|
* @param {string|null} [options.colorScheme] - Emulate color scheme: 'light', 'dark', 'no-preference', or null to reset
|
|
312
|
-
* @param {string} [options.channel] - Installed browser channel, such as 'chrome', 'chrome-beta', 'msedge', 'brave' or 'chromium'
|
|
308
|
+
* @param {string} [options.channel] - Installed browser channel, such as 'chrome', 'chrome-beta', 'msedge', 'brave' or 'chromium'; without it (and without executablePath) the engine's Chrome for Testing is started
|
|
313
309
|
* @param {string} [options.executablePath] - Explicit path to a Chrome or Chromium executable
|
|
314
310
|
* @param {number} [options.remoteDebuggingPort] - Fixed CDP port for the real launch; a free one is reserved when omitted
|
|
315
311
|
* @param {string|Object} [options.storageState] - Playwright-compatible storage state path or object
|
|
@@ -317,7 +313,7 @@ async function unfocusAddressBar(page, verbose, settleMs = 500) {
|
|
|
317
313
|
* @param {boolean} [options.automationParity=true] - Keep navigator.webdriver false where a launch switch would turn it on (headless or engine launches)
|
|
318
314
|
* @param {boolean|Object} [options.downloads] - Manage downloads: true for defaults, or {directory, persist, conflict}. The directory may be an absolute path, 'user-downloads' or 'temporary'.
|
|
319
315
|
* @param {boolean|Object} [options.debug=false] - Opt-in recording: true, {output}, or {directory, retain, maxDurationMs, limits} for managed storage
|
|
320
|
-
* @returns {Promise<{browser: Object, page: Object, downloads: (Object|null), close: function(): Promise<void>, launch: string, userDataDir: string, temporaryProfile: boolean, args: Array<string>}>} Browser (a BrowserContext for Playwright), page, download manager and launch metadata
|
|
316
|
+
* @returns {Promise<{browser: Object, page: Object, downloads: (Object|null), close: function(): Promise<void>, launch: string, userDataDir: string, temporaryProfile: boolean, args: Array<string>, defaultRestrictions: Array<string>}>} Browser (a BrowserContext for Playwright), page, download manager and launch metadata
|
|
321
317
|
*/
|
|
322
318
|
export async function launchBrowser(options = {}) {
|
|
323
319
|
return await launchBrowserWithDependencies(options);
|
|
@@ -368,6 +364,23 @@ export async function launchBrowserWithDependencies(
|
|
|
368
364
|
}
|
|
369
365
|
}
|
|
370
366
|
|
|
367
|
+
function validateSeleniumLaunch({
|
|
368
|
+
launch,
|
|
369
|
+
browser,
|
|
370
|
+
colorScheme,
|
|
371
|
+
fingerprint,
|
|
372
|
+
downloads,
|
|
373
|
+
}) {
|
|
374
|
+
if (colorScheme !== undefined || fingerprint !== undefined || downloads) {
|
|
375
|
+
throw new Error(
|
|
376
|
+
'selenium does not support CDP emulation, fingerprint overrides or managed downloads (webdriver-no-cdp-emulation)'
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
if (launch === 'real' && browser === 'firefox') {
|
|
380
|
+
throw new Error("selenium Firefox requires launch: 'engine'");
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
371
384
|
async function launchNonSafariBrowser(options, dependencies) {
|
|
372
385
|
const {
|
|
373
386
|
engine = 'playwright',
|
|
@@ -388,16 +401,16 @@ async function launchNonSafariBrowser(options, dependencies) {
|
|
|
388
401
|
} = options;
|
|
389
402
|
validateLaunchMode({ engine, launch, attach: rest.attach });
|
|
390
403
|
if (engine === 'selenium') {
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
}
|
|
404
|
+
validateSeleniumLaunch({
|
|
405
|
+
launch,
|
|
406
|
+
colorScheme,
|
|
407
|
+
fingerprint,
|
|
408
|
+
downloads,
|
|
409
|
+
...rest,
|
|
410
|
+
});
|
|
399
411
|
}
|
|
400
412
|
// Validate arguments before anything is started or written to disk.
|
|
413
|
+
resolveRestrictions(rest.defaultRestrictions ?? []);
|
|
401
414
|
resolveChromeArgs({
|
|
402
415
|
args,
|
|
403
416
|
extraArgs,
|
|
@@ -218,7 +218,7 @@ export async function migrateFirefoxPasswords({
|
|
|
218
218
|
targetProfileDir,
|
|
219
219
|
platform,
|
|
220
220
|
targetKey,
|
|
221
|
-
targetPrefix = platform === '
|
|
221
|
+
targetPrefix = platform === 'linux' ? 'v11' : 'v10',
|
|
222
222
|
primaryPassword = Buffer.alloc(0),
|
|
223
223
|
domains,
|
|
224
224
|
}) {
|
|
@@ -122,6 +122,7 @@ async function resolvePasswordKeys({
|
|
|
122
122
|
keys,
|
|
123
123
|
browser,
|
|
124
124
|
targetBrowser,
|
|
125
|
+
targetKeystore,
|
|
125
126
|
platform,
|
|
126
127
|
sourceProfileDir,
|
|
127
128
|
environment,
|
|
@@ -135,6 +136,7 @@ async function resolvePasswordKeys({
|
|
|
135
136
|
const { key: targetKey, prefix: targetPrefix } = await resolveTargetKey({
|
|
136
137
|
browser: targetBrowser,
|
|
137
138
|
platform,
|
|
139
|
+
keystore: targetKeystore,
|
|
138
140
|
environment,
|
|
139
141
|
});
|
|
140
142
|
const resolveSourceKey = isChromiumBrowser(browser)
|
|
@@ -159,6 +161,7 @@ async function resolvePasswordKeys({
|
|
|
159
161
|
* @param {boolean} [options.includePaymentCards=false] - Separate explicit payment-card consent
|
|
160
162
|
* @param {string} [options.platform=process.platform]
|
|
161
163
|
* @param {string} [options.targetBrowser] - Launching browser channel (key derivation)
|
|
164
|
+
* @param {'mock'|'os'} [options.targetKeystore='os'] - 'mock' when the launch bypasses the OS keystore (`targetKeystoreFor`)
|
|
162
165
|
* @param {Object} [options.keys] - Injected password keys {resolveSourceKey, targetKey, targetPrefix, primaryPassword}
|
|
163
166
|
* @param {string} [options.homeDir]
|
|
164
167
|
* @param {Object} [options.environment]
|
|
@@ -171,6 +174,7 @@ export async function migrateProfile({
|
|
|
171
174
|
domains,
|
|
172
175
|
platform = process.platform,
|
|
173
176
|
targetBrowser,
|
|
177
|
+
targetKeystore,
|
|
174
178
|
keys,
|
|
175
179
|
homeDir = os.homedir(),
|
|
176
180
|
environment = process.env,
|
|
@@ -272,6 +276,7 @@ export async function migrateProfile({
|
|
|
272
276
|
keys,
|
|
273
277
|
browser,
|
|
274
278
|
targetBrowser,
|
|
279
|
+
targetKeystore,
|
|
275
280
|
platform,
|
|
276
281
|
sourceProfileDir,
|
|
277
282
|
environment,
|
|
@@ -352,6 +357,7 @@ export async function migrateProfile({
|
|
|
352
357
|
keys,
|
|
353
358
|
browser,
|
|
354
359
|
targetBrowser,
|
|
360
|
+
targetKeystore,
|
|
355
361
|
platform,
|
|
356
362
|
sourceProfileDir,
|
|
357
363
|
environment,
|
|
@@ -377,6 +383,7 @@ async function migrateSafariProfile({
|
|
|
377
383
|
selected,
|
|
378
384
|
sourceProfileDir,
|
|
379
385
|
targetBrowser,
|
|
386
|
+
targetKeystore,
|
|
380
387
|
to,
|
|
381
388
|
}) {
|
|
382
389
|
const passwordKeys =
|
|
@@ -385,6 +392,7 @@ async function migrateSafariProfile({
|
|
|
385
392
|
keys,
|
|
386
393
|
browser,
|
|
387
394
|
targetBrowser: targetBrowser ?? 'chrome',
|
|
395
|
+
targetKeystore,
|
|
388
396
|
platform,
|
|
389
397
|
sourceProfileDir,
|
|
390
398
|
environment,
|
|
@@ -442,6 +450,7 @@ async function migratePasswordClass(
|
|
|
442
450
|
platform,
|
|
443
451
|
sourceProfileDir,
|
|
444
452
|
targetBrowser,
|
|
453
|
+
targetKeystore,
|
|
445
454
|
to,
|
|
446
455
|
}
|
|
447
456
|
) {
|
|
@@ -464,6 +473,7 @@ async function migratePasswordClass(
|
|
|
464
473
|
keys,
|
|
465
474
|
browser,
|
|
466
475
|
targetBrowser: targetBrowser ?? (isFirefox ? 'chrome' : browser),
|
|
476
|
+
targetKeystore,
|
|
467
477
|
platform,
|
|
468
478
|
sourceProfileDir,
|
|
469
479
|
environment,
|
|
@@ -73,12 +73,34 @@ export function createSourceKeyResolver({
|
|
|
73
73
|
};
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
/**
|
|
77
|
+
* Which keystore a launch encrypts its new profile with: `'mock'` when the
|
|
78
|
+
* launch switches bypass the OS keystore (`--use-mock-keychain` on macOS, as
|
|
79
|
+
* Chrome for Testing gets by default, or `--password-store=basic` on Linux),
|
|
80
|
+
* `'os'` otherwise.
|
|
81
|
+
*
|
|
82
|
+
* @param {string[]} args - The browser command-line switches
|
|
83
|
+
* @param {string} platform
|
|
84
|
+
* @returns {'mock'|'os'}
|
|
85
|
+
*/
|
|
86
|
+
export function targetKeystoreFor(args, platform) {
|
|
87
|
+
const bypass =
|
|
88
|
+
platform === 'darwin' ? '--use-mock-keychain' : '--password-store=basic';
|
|
89
|
+
return platform !== 'win32' && args.includes(bypass) ? 'mock' : 'os';
|
|
90
|
+
}
|
|
91
|
+
|
|
76
92
|
/**
|
|
77
93
|
* Resolve the target (launching) profile's encryption key on macOS/Linux.
|
|
78
94
|
*
|
|
95
|
+
* macOS Chromium only knows the `v10` tag; Linux tags keyring keys `v11` and
|
|
96
|
+
* its fixed basic-store key `v10`. A `'mock'` keystore uses Chromium's fixed
|
|
97
|
+
* mock passwords: `mock_password` for the mock keychain, `peanuts` for the
|
|
98
|
+
* basic store.
|
|
99
|
+
*
|
|
79
100
|
* @param {Object} options
|
|
80
101
|
* @param {string} options.browser - Launching browser channel
|
|
81
102
|
* @param {string} options.platform
|
|
103
|
+
* @param {'mock'|'os'} [options.keystore='os'] - See `targetKeystoreFor`
|
|
82
104
|
* @param {Object} [options.environment]
|
|
83
105
|
* @param {Function} [options.readSafeStoragePassword]
|
|
84
106
|
* @returns {Promise<{key: Buffer, prefix: string}>}
|
|
@@ -86,16 +108,24 @@ export function createSourceKeyResolver({
|
|
|
86
108
|
export async function resolveTargetKey({
|
|
87
109
|
browser,
|
|
88
110
|
platform,
|
|
111
|
+
keystore = 'os',
|
|
89
112
|
environment = process.env,
|
|
90
113
|
readSafeStoragePassword: readSafeStorage = readSafeStoragePassword,
|
|
91
114
|
}) {
|
|
92
|
-
if (platform
|
|
93
|
-
|
|
94
|
-
|
|
115
|
+
if (platform !== 'darwin' && platform !== 'linux') {
|
|
116
|
+
throw new Error(
|
|
117
|
+
`resolveTargetKey does not derive a Windows key; use createWindowsProfileKey`
|
|
118
|
+
);
|
|
95
119
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
120
|
+
if (keystore === 'mock') {
|
|
121
|
+
const password = platform === 'darwin' ? 'mock_password' : 'peanuts';
|
|
122
|
+
return { key: deriveChromiumCookieKey(password, platform), prefix: 'v10' };
|
|
123
|
+
}
|
|
124
|
+
const password = await readSafeStorage({ browser, platform, environment });
|
|
125
|
+
return {
|
|
126
|
+
key: deriveChromiumCookieKey(password, platform),
|
|
127
|
+
prefix: platform === 'darwin' ? 'v10' : 'v11',
|
|
128
|
+
};
|
|
99
129
|
}
|
|
100
130
|
|
|
101
131
|
/** Convenience: the Local State path next to a profile directory. */
|
|
@@ -108,7 +108,7 @@ export async function migratePasswords({
|
|
|
108
108
|
platform = process.platform,
|
|
109
109
|
resolveSourceKey,
|
|
110
110
|
targetKey,
|
|
111
|
-
targetPrefix = platform === '
|
|
111
|
+
targetPrefix = platform === 'linux' ? 'v11' : 'v10',
|
|
112
112
|
domains,
|
|
113
113
|
}) {
|
|
114
114
|
const sourcePath = path.join(sourceProfileDir, 'Login Data');
|
package/src/browser/parity.js
CHANGED
|
@@ -317,7 +317,12 @@ function normalizeWhitespace(text) {
|
|
|
317
317
|
function requestedArgsFor(session, options) {
|
|
318
318
|
const requested = [...(options.args ?? []), ...(options.extraArgs ?? [])];
|
|
319
319
|
const launchedArgs = session.args ?? [];
|
|
320
|
-
|
|
320
|
+
// Chrome for Testing's default restrictions are requested too (issue #179).
|
|
321
|
+
const restrictions = [
|
|
322
|
+
...(session.defaultRestrictions ?? []),
|
|
323
|
+
...(options.restrictions ?? []),
|
|
324
|
+
];
|
|
325
|
+
if (restrictions.length > 0) {
|
|
321
326
|
requested.push(
|
|
322
327
|
...launchedArgs.filter(
|
|
323
328
|
(arg) =>
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
} from './attach/snapshot-launch.js';
|
|
24
24
|
import { connectBrowser } from './connector.js';
|
|
25
25
|
import { migrateProfile } from './migration/index.js';
|
|
26
|
+
import { targetKeystoreFor } from './migration/os-crypt-keys.js';
|
|
26
27
|
import {
|
|
27
28
|
assertFixedDebuggingPort,
|
|
28
29
|
classifyDevToolsOwnership,
|
|
@@ -48,6 +49,11 @@ import {
|
|
|
48
49
|
knownDefaultUserDataDirs,
|
|
49
50
|
resolveSystemBrowserExecutable,
|
|
50
51
|
} from './system-browser.js';
|
|
52
|
+
import {
|
|
53
|
+
defaultRestrictionsFor,
|
|
54
|
+
isChromeForTesting,
|
|
55
|
+
resolveLaunchExecutable,
|
|
56
|
+
} from './launch-executable.js';
|
|
51
57
|
|
|
52
58
|
/**
|
|
53
59
|
* Switches the launcher owns. Letting a caller pass them would break the
|
|
@@ -368,29 +374,34 @@ function createCloser({
|
|
|
368
374
|
|
|
369
375
|
/**
|
|
370
376
|
* Selenium attaches through a chromedriver the launch starts; the driver
|
|
371
|
-
* follows the browser's interruption ownership (issue #174)
|
|
377
|
+
* follows the browser's interruption ownership (issue #174) and must match
|
|
378
|
+
* the browser just started, not the installed Chrome Selenium Manager would
|
|
379
|
+
* otherwise pick (issue #179).
|
|
372
380
|
*/
|
|
373
|
-
function
|
|
374
|
-
return engine === 'selenium' ? { interruption } : {};
|
|
381
|
+
function driverOptions(engine, { interruption, executablePath }) {
|
|
382
|
+
return engine === 'selenium' ? { interruption, executablePath } : {};
|
|
375
383
|
}
|
|
376
384
|
|
|
377
385
|
/**
|
|
378
|
-
* Spawn a
|
|
379
|
-
*
|
|
386
|
+
* Spawn a Chrome-family browser with a dedicated profile, wait for its
|
|
387
|
+
* loopback CDP endpoint, and attach through {@link connectBrowser}.
|
|
380
388
|
*
|
|
381
389
|
* The command line is `--user-data-dir=<dir> --remote-debugging-port=<port>`
|
|
382
390
|
* and nothing else unless asked for, so the session is as close to a
|
|
383
391
|
* hand-started browser as CDP allows and `navigator.webdriver` is false.
|
|
392
|
+
* Without `channel`, `executablePath` or `attach` the engine's Chrome for
|
|
393
|
+
* Testing is started, with the `chrome-for-testing` restrictions (issue #179).
|
|
384
394
|
*
|
|
385
395
|
* @param {Object} options - Launch and connection options
|
|
386
396
|
* @param {'playwright'|'puppeteer'} [options.engine='playwright'] - Automation engine
|
|
387
|
-
* @param {string} [options.channel
|
|
397
|
+
* @param {string} [options.channel] - Installed Chrome-family channel, such as 'chrome' for the installed Google Chrome; it also names the browser a snapshot or migration belongs to (default 'chrome')
|
|
388
398
|
* @param {string} [options.executablePath] - Explicit installed browser executable
|
|
389
399
|
* @param {string} [options.userDataDir] - Persistent dedicated profile. When omitted a fresh temporary profile is created and deleted on close.
|
|
390
400
|
* @param {number} [options.remoteDebuggingPort] - Fixed loopback CDP port. When omitted a free port is reserved (and re-reserved on a port race). Zero is refused because it sets navigator.webdriver.
|
|
391
401
|
* @param {number} [options.portAttempts=3] - Launch attempts when a reserved port is lost to a race
|
|
392
402
|
* @param {boolean} [options.headless=false] - Run the installed browser headlessly
|
|
393
403
|
* @param {string[]} [options.restrictions] - Opt-in restrictions from launch-restrictions.json, such as 'no-extensions'
|
|
404
|
+
* @param {string[]} [options.defaultRestrictions] - Restrictions that replace the executable's defaults: the 'chrome-for-testing' preset for Chrome for Testing, none for any other browser. Pass [] to start Chrome for Testing without them.
|
|
394
405
|
* @param {string[]} [options.args] - Additional browser arguments
|
|
395
406
|
* @param {string[]} [options.extraArgs] - Additional browser arguments appended after args
|
|
396
407
|
* @param {boolean} [options.defaultBrowserCheck=false] - Allow the disposable browser to ask to become the default
|
|
@@ -404,7 +415,7 @@ function driverInterruption(engine, interruption) {
|
|
|
404
415
|
* @param {{browser: string, profile: (string|undefined), userDataDir: (string|undefined), include: (Array<string>|undefined), domains: (Array<string>|undefined)}} [options.migrateFrom] - Migrate a real browser profile into the dedicated profile before launch; on-disk data is written before the browser starts and cookies are seeded over CDP after connecting
|
|
405
416
|
* @param {{mode: 'snapshot', browser: (string|undefined), profile: (string|undefined), userDataDir: (string|undefined)}} [options.attach] - Launch on a read-only snapshot of a real profile (issue #102, addendum): `snapshotUserDataDir()` copies it into a temporary user data directory, which is deleted on close. `browser` defaults to the channel's browser and `profile` to Default. Mutually exclusive with `migrateFrom` and `userDataDir`.
|
|
406
417
|
* @param {boolean} [options.verbose=false] - Show browser and connection logs
|
|
407
|
-
* @returns {Promise<{browser: Object, page: Object, downloads: (Object|null), close: function(): Promise<void>, browserProcess: Object, cdpEndpoint: string, remoteDebuggingPort: number, executablePath: string, userDataDir: string, temporaryProfile: boolean, args: Array<string>, migration: (Object|undefined), attach: ({mode: 'snapshot', snapshot: Object, differences: Array<{aspect: string, description: string}>}|undefined)}>} Connected handles and process metadata (with a `migration` report when `migrateFrom` was given and an `attach` entry when `attach` was given)
|
|
418
|
+
* @returns {Promise<{browser: Object, page: Object, downloads: (Object|null), close: function(): Promise<void>, browserProcess: Object, cdpEndpoint: string, remoteDebuggingPort: number, executablePath: string, userDataDir: string, temporaryProfile: boolean, args: Array<string>, defaultRestrictions: Array<string>, migration: (Object|undefined), attach: ({mode: 'snapshot', snapshot: Object, differences: Array<{aspect: string, description: string}>}|undefined)}>} Connected handles and process metadata (with a `migration` report when `migrateFrom` was given and an `attach` entry when `attach` was given)
|
|
408
419
|
*/
|
|
409
420
|
export async function launchAndConnectRealBrowser(options = {}) {
|
|
410
421
|
return await launchAndConnectRealBrowserWithDependencies(options);
|
|
@@ -595,6 +606,9 @@ async function runPreLaunchMigration({
|
|
|
595
606
|
migrateFrom,
|
|
596
607
|
userDataDir,
|
|
597
608
|
channel,
|
|
609
|
+
executablePath,
|
|
610
|
+
argOptions,
|
|
611
|
+
platform,
|
|
598
612
|
temporaryProfile,
|
|
599
613
|
migrate,
|
|
600
614
|
}) {
|
|
@@ -603,6 +617,14 @@ async function runPreLaunchMigration({
|
|
|
603
617
|
}
|
|
604
618
|
const { include, domains, passwordCsv, includePaymentCards, ...from } =
|
|
605
619
|
migrateFrom;
|
|
620
|
+
// Passwords are re-encrypted with the key of the browser that starts:
|
|
621
|
+
// Chrome for Testing keeps "Chromium Safe Storage", and its default mock
|
|
622
|
+
// keychain uses Chromium's fixed mock password (issue #179).
|
|
623
|
+
const launchArgs = [
|
|
624
|
+
...resolveRestrictions(argOptions.restrictions).args,
|
|
625
|
+
...argOptions.args,
|
|
626
|
+
...argOptions.extraArgs,
|
|
627
|
+
];
|
|
606
628
|
try {
|
|
607
629
|
const report = await migrate({
|
|
608
630
|
from,
|
|
@@ -611,7 +633,8 @@ async function runPreLaunchMigration({
|
|
|
611
633
|
domains,
|
|
612
634
|
passwordCsv,
|
|
613
635
|
includePaymentCards,
|
|
614
|
-
targetBrowser: channel,
|
|
636
|
+
targetBrowser: isChromeForTesting(executablePath) ? 'chromium' : channel,
|
|
637
|
+
targetKeystore: targetKeystoreFor(launchArgs, platform),
|
|
615
638
|
});
|
|
616
639
|
const { cookies, ...migration } = report;
|
|
617
640
|
return { migration, migratedCookies: cookies ?? [] };
|
|
@@ -662,6 +685,7 @@ export async function launchAndConnectRealBrowserWithDependencies(
|
|
|
662
685
|
portAttempts = 3,
|
|
663
686
|
headless = false,
|
|
664
687
|
restrictions = [],
|
|
688
|
+
defaultRestrictions: requestedDefaultRestrictions,
|
|
665
689
|
args = [],
|
|
666
690
|
extraArgs = [],
|
|
667
691
|
ignoreDefaultArgs: _ignoredLegacyDefaults,
|
|
@@ -699,14 +723,17 @@ export async function launchAndConnectRealBrowserWithDependencies(
|
|
|
699
723
|
attach,
|
|
700
724
|
migrateFrom,
|
|
701
725
|
});
|
|
726
|
+
resolveRestrictions(requestedDefaultRestrictions ?? []);
|
|
702
727
|
|
|
728
|
+
// Without a channel, executablePath or attach the engine's Chrome for
|
|
729
|
+
// Testing is started, never the user's Google Chrome (issue #179).
|
|
703
730
|
const resolveExecutable =
|
|
704
|
-
dependencies.resolveExecutable ??
|
|
705
|
-
|
|
731
|
+
dependencies.resolveExecutable ??
|
|
732
|
+
((request) => resolveLaunchExecutable({ engine, ...request }));
|
|
706
733
|
const executablePath = await Promise.resolve()
|
|
707
734
|
.then(() =>
|
|
708
735
|
resolveExecutable({
|
|
709
|
-
channel:
|
|
736
|
+
channel: attach?.browser ?? options.channel,
|
|
710
737
|
executablePath: requestedExecutablePath,
|
|
711
738
|
})
|
|
712
739
|
)
|
|
@@ -718,6 +745,14 @@ export async function launchAndConnectRealBrowserWithDependencies(
|
|
|
718
745
|
executablePath: requestedExecutablePath,
|
|
719
746
|
});
|
|
720
747
|
});
|
|
748
|
+
// Chrome for Testing gets its infobar hidden and a mock keychain unless the
|
|
749
|
+
// caller sets defaultRestrictions; the snapshot keystore warning, the
|
|
750
|
+
// environment and the profile preferences all see them.
|
|
751
|
+
const defaultRestrictions = defaultRestrictionsFor(
|
|
752
|
+
executablePath,
|
|
753
|
+
requestedDefaultRestrictions
|
|
754
|
+
);
|
|
755
|
+
argOptions.restrictions = [...defaultRestrictions, ...restrictions];
|
|
721
756
|
|
|
722
757
|
const temporaryProfile = !requestedUserDataDir;
|
|
723
758
|
const { userDataDir, snapshotLaunch } = await prepareProfile({
|
|
@@ -728,12 +763,15 @@ export async function launchAndConnectRealBrowserWithDependencies(
|
|
|
728
763
|
snapshot: dependencies.snapshotUserDataDir,
|
|
729
764
|
firstRun,
|
|
730
765
|
});
|
|
731
|
-
const childEnv = browserEnvironment(restrictions, env);
|
|
766
|
+
const childEnv = browserEnvironment(argOptions.restrictions, env);
|
|
732
767
|
|
|
733
768
|
const { migration, migratedCookies } = await runPreLaunchMigration({
|
|
734
769
|
migrateFrom,
|
|
735
770
|
userDataDir,
|
|
736
771
|
channel,
|
|
772
|
+
executablePath,
|
|
773
|
+
argOptions,
|
|
774
|
+
platform: dependencies.platform ?? process.platform,
|
|
737
775
|
temporaryProfile,
|
|
738
776
|
migrate: dependencies.migrateProfile ?? migrateProfile,
|
|
739
777
|
});
|
|
@@ -745,7 +783,7 @@ export async function launchAndConnectRealBrowserWithDependencies(
|
|
|
745
783
|
configureUserDataDir(userDataDir, {
|
|
746
784
|
defaultBrowserCheck,
|
|
747
785
|
preferences: {
|
|
748
|
-
...resolveRestrictions(restrictions).preferences,
|
|
786
|
+
...resolveRestrictions(argOptions.restrictions).preferences,
|
|
749
787
|
...preferences,
|
|
750
788
|
},
|
|
751
789
|
localState,
|
|
@@ -787,7 +825,7 @@ export async function launchAndConnectRealBrowserWithDependencies(
|
|
|
787
825
|
engine,
|
|
788
826
|
cdpEndpoint: resolvedCdpEndpoint,
|
|
789
827
|
...connectionOptions,
|
|
790
|
-
...
|
|
828
|
+
...driverOptions(engine, { interruption, executablePath }),
|
|
791
829
|
seedCookies,
|
|
792
830
|
verbose,
|
|
793
831
|
});
|
|
@@ -814,6 +852,7 @@ export async function launchAndConnectRealBrowserWithDependencies(
|
|
|
814
852
|
userDataDir,
|
|
815
853
|
temporaryProfile,
|
|
816
854
|
args: launched.browserArgs,
|
|
855
|
+
defaultRestrictions,
|
|
817
856
|
...(migration ? { migration } : {}),
|
|
818
857
|
...(snapshotLaunch ? { attach: snapshotLaunch.attach } : {}),
|
|
819
858
|
},
|