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.
Files changed (34) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +52 -5
  3. package/package.json +1 -1
  4. package/src/browser/browser-handoff.js +4 -0
  5. package/src/browser/chrome-for-testing-cache.js +198 -0
  6. package/src/browser/launch-executable.js +203 -0
  7. package/src/browser/launch-restrictions.json +7 -1
  8. package/src/browser/launcher.js +78 -65
  9. package/src/browser/migration/firefox.js +1 -1
  10. package/src/browser/migration/index.js +10 -0
  11. package/src/browser/migration/os-crypt-keys.js +36 -6
  12. package/src/browser/migration/passwords.js +1 -1
  13. package/src/browser/parity.js +6 -1
  14. package/src/browser/real-browser.js +53 -14
  15. package/src/browser/webdriver.js +63 -8
  16. package/src/capture/screenshot.js +16 -1
  17. package/src/capture/viewport.js +2 -1
  18. package/src/exports.js +5 -0
  19. package/src/traces/recorder.js +18 -2
  20. package/tests/e2e/chrome-for-testing.e2e.test.js +105 -0
  21. package/tests/e2e/startup-readiness.e2e.test.js +2 -0
  22. package/tests/unit/browser/browser-handoff.test.js +13 -0
  23. package/tests/unit/browser/chrome-for-testing-cache.test.js +139 -0
  24. package/tests/unit/browser/launch-executable.test.js +94 -0
  25. package/tests/unit/browser/launcher.test.js +235 -31
  26. package/tests/unit/browser/migration/os-crypt-keys.test.js +57 -0
  27. package/tests/unit/browser/migration/safari-data.test.js +43 -19
  28. package/tests/unit/browser/parity.test.js +19 -0
  29. package/tests/unit/browser/real-browser.test.js +125 -0
  30. package/tests/unit/browser/webdriver.test.js +95 -14
  31. package/tests/unit/scripts/check-web-archive.test.js +77 -2
  32. package/tests/unit/traces/issue-162-defaults.test.js +1 -1
  33. package/tests/unit/traces/issue-166-policy.test.js +1 -1
  34. package/tests/unit/traces/issue-180-screenshots.test.js +173 -0
@@ -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 the installed Chrome
289
- * itself, exactly like a person who wants to attach a debugger would:
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
- if (colorScheme !== undefined || fingerprint !== undefined || downloads) {
392
- throw new Error(
393
- 'selenium does not support CDP emulation, fingerprint overrides or managed downloads (webdriver-no-cdp-emulation)'
394
- );
395
- }
396
- if (launch === 'real' && rest.browser === 'firefox') {
397
- throw new Error("selenium Firefox requires launch: 'engine'");
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 === 'win32' ? 'v10' : 'v11',
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 === 'darwin' || platform === 'linux') {
93
- const password = await readSafeStorage({ browser, platform, environment });
94
- return { key: deriveChromiumCookieKey(password, platform), prefix: 'v11' };
115
+ if (platform !== 'darwin' && platform !== 'linux') {
116
+ throw new Error(
117
+ `resolveTargetKey does not derive a Windows key; use createWindowsProfileKey`
118
+ );
95
119
  }
96
- throw new Error(
97
- `resolveTargetKey does not derive a Windows key; use createWindowsProfileKey`
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 === 'win32' ? 'v10' : 'v11',
111
+ targetPrefix = platform === 'linux' ? 'v11' : 'v10',
112
112
  domains,
113
113
  }) {
114
114
  const sourcePath = path.join(sourceProfileDir, 'Login Data');
@@ -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
- if ((options.restrictions ?? []).length > 0) {
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 driverInterruption(engine, interruption) {
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 genuine installed Chrome-family browser with a dedicated profile,
379
- * wait for its loopback CDP endpoint, and attach through {@link connectBrowser}.
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='chrome'] - Installed Chrome-family 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 ?? resolveSystemBrowserExecutable;
705
- const launchChannel = attach?.browser ?? channel;
731
+ dependencies.resolveExecutable ??
732
+ ((request) => resolveLaunchExecutable({ engine, ...request }));
706
733
  const executablePath = await Promise.resolve()
707
734
  .then(() =>
708
735
  resolveExecutable({
709
- channel: launchChannel,
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
- ...driverInterruption(engine, interruption),
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
  },