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,6 +1,8 @@
1
- import { describe, it } from 'node:test';
1
+ import { after, describe, it } from 'node:test';
2
2
  import assert from 'node:assert';
3
- import { access } from 'node:fs/promises';
3
+ import { access, chmod, mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
4
+ import { tmpdir } from 'node:os';
5
+ import path from 'node:path';
4
6
 
5
7
  import {
6
8
  launchBrowser,
@@ -52,21 +54,59 @@ function fakeRealLauncher(calls, { engine = 'playwright' } = {}) {
52
54
 
53
55
  const systemChrome = async () => '/usr/bin/google-chrome';
54
56
 
57
+ // An executable laid out like the Chrome for Testing Playwright downloads.
58
+ const cftRoot = await mkdtemp(path.join(tmpdir(), 'browser-commander-cft-'));
59
+ const cftChrome = path.join(
60
+ cftRoot,
61
+ 'chromium-1248',
62
+ 'chrome-linux64',
63
+ 'chrome'
64
+ );
65
+ await mkdir(path.dirname(cftChrome), { recursive: true });
66
+ await writeFile(cftChrome, '#!/bin/sh\n');
67
+ await chmod(cftChrome, 0o755);
68
+ after(() => rm(cftRoot, { recursive: true, force: true }));
69
+ const cftEngine = async () => ({ executablePath: () => cftChrome });
70
+ const missingEngine = async () => ({ executablePath: () => '/nonexistent' });
71
+ const noCachedBrowsers = () => [];
72
+ const noSystemChrome = async () => {
73
+ throw new Error('the installed Google Chrome must not be resolved');
74
+ };
75
+
76
+ // A real launch with recorded calls: the result and launchRealBrowser's options.
77
+ async function realLaunch(options, dependencies, calls = []) {
78
+ const result = await launchBrowserWithDependencies(options, {
79
+ launchRealBrowser: fakeRealLauncher(calls),
80
+ settleMs: 0,
81
+ ...dependencies,
82
+ });
83
+ const [, launched] = calls.find(([name]) => name === 'launchRealBrowser');
84
+ return [result, launched];
85
+ }
86
+
55
87
  describe('launchBrowser', () => {
56
- it('starts the real browser by default, with nothing added (issue #103)', async () => {
88
+ it('starts Chrome for Testing by default, never the installed Google Chrome (issue #179)', async () => {
89
+ const [, options] = await realLaunch(
90
+ {},
91
+ { resolveSystem: noSystemChrome, loadEngineModule: cftEngine }
92
+ );
93
+ assert.equal(options.executablePath, cftChrome);
94
+ // No channel is invented: the launch is not Google Chrome's.
95
+ assert.equal('channel' in options, false);
96
+ // The Chrome for Testing defaults are added by launchRealBrowser.
97
+ assert.deepEqual(options.restrictions, []);
98
+ assert.equal('defaultRestrictions' in options, false);
99
+ });
100
+
101
+ it('starts the requested Google Chrome with nothing added (issue #103)', async () => {
57
102
  const calls = [];
58
103
  const googleBefore = process.env.GOOGLE_API_KEY;
59
104
 
60
- const result = await launchBrowserWithDependencies(
61
- {},
62
- {
63
- launchRealBrowser: fakeRealLauncher(calls),
64
- resolveSystem: systemChrome,
65
- settleMs: 0,
66
- }
105
+ const [result, options] = await realLaunch(
106
+ { channel: 'chrome' },
107
+ { resolveSystem: systemChrome },
108
+ calls
67
109
  );
68
-
69
- const [, options] = calls.find(([name]) => name === 'launchRealBrowser');
70
110
  assert.equal(options.engine, 'playwright');
71
111
  assert.equal(options.executablePath, '/usr/bin/google-chrome');
72
112
  assert.equal('slowMo' in options, false);
@@ -91,7 +131,7 @@ describe('launchBrowser', () => {
91
131
  { engine: 'puppeteer', restrictions: ['no-sync'], slowMo: 20 },
92
132
  {
93
133
  launchRealBrowser: fakeRealLauncher(calls, { engine: 'puppeteer' }),
94
- resolveSystem: systemChrome,
134
+ loadEngineModule: cftEngine,
95
135
  settleMs: 0,
96
136
  }
97
137
  );
@@ -109,7 +149,7 @@ describe('launchBrowser', () => {
109
149
  { colorScheme: 'dark' },
110
150
  {
111
151
  launchRealBrowser: fakeRealLauncher(calls),
112
- resolveSystem: systemChrome,
152
+ loadEngineModule: cftEngine,
113
153
  settleMs: 0,
114
154
  }
115
155
  );
@@ -128,7 +168,7 @@ describe('launchBrowser', () => {
128
168
  { fingerprint: { timezone: 'Not/AZone' } },
129
169
  {
130
170
  launchRealBrowser: fakeRealLauncher(calls),
131
- resolveSystem: systemChrome,
171
+ loadEngineModule: cftEngine,
132
172
  settleMs: 0,
133
173
  }
134
174
  ),
@@ -150,6 +190,10 @@ describe('launchBrowser', () => {
150
190
  () => launchBrowser({ restrictions: ['no-such-restriction'] }),
151
191
  /Unknown launch restriction "no-such-restriction"/
152
192
  );
193
+ await assert.rejects(
194
+ () => launchBrowser({ defaultRestrictions: ['no-such-default'] }),
195
+ /Unknown launch restriction "no-such-default"/
196
+ );
153
197
  });
154
198
 
155
199
  describe("launch: 'engine'", () => {
@@ -233,6 +277,100 @@ describe('launchBrowser', () => {
233
277
  assert.deepEqual(calls.at(-1), ['browser.close']);
234
278
  });
235
279
 
280
+ it('adds the Chrome for Testing defaults to its engine launch (issue #179)', async () => {
281
+ const launchWith = async (options, executablePath) => {
282
+ const calls = [];
283
+ const engine = {
284
+ ...fakePlaywright(calls),
285
+ executablePath: () => executablePath,
286
+ };
287
+ const result = await launchBrowserWithDependencies(
288
+ { launch: 'engine', ...options },
289
+ { loadEngineModule: async () => engine, settleMs: 0 }
290
+ );
291
+ await result.close();
292
+ return { args: calls[0][2].args, defaults: result.defaultRestrictions };
293
+ };
294
+
295
+ const cft = await launchWith({}, cftChrome);
296
+ assert.deepEqual(cft.defaults, [
297
+ 'no-chrome-for-testing-infobar',
298
+ 'mock-keychain',
299
+ ]);
300
+ assert.ok(cft.args.includes('--test-type=gpu'));
301
+ assert.ok(cft.args.includes('--use-mock-keychain'));
302
+
303
+ for (const [options, executablePath] of [
304
+ [{}, '/home/u/.cache/ms-playwright/chromium-1100/chrome-linux/chrome'],
305
+ [{ channel: 'chrome' }, cftChrome],
306
+ [{ defaultRestrictions: [] }, cftChrome],
307
+ ]) {
308
+ const other = await launchWith(options, executablePath);
309
+ assert.deepEqual(other.defaults, [], JSON.stringify(options));
310
+ assert.equal(other.args.includes('--test-type=gpu'), false);
311
+ assert.equal(other.args.includes('--use-mock-keychain'), false);
312
+ }
313
+ });
314
+
315
+ it("points a Selenium launch at the engines' Chrome for Testing (issue #179)", async () => {
316
+ const launches = [];
317
+ const launchWebDriver = async (options) => {
318
+ launches.push(options);
319
+ return { driver: {}, page: fakePage(), close: async () => {} };
320
+ };
321
+ await launchBrowserWithDependencies(
322
+ {
323
+ engine: 'selenium',
324
+ launch: 'engine',
325
+ restrictions: ['no-sync'],
326
+ defaultRestrictions: ['mock-keychain'],
327
+ },
328
+ { launchWebDriver, loadEngineModule: cftEngine, settleMs: 0 }
329
+ );
330
+ assert.equal(launches[0].executablePath, cftChrome);
331
+ // launchWebDriver adds the defaults for the browser it resolves.
332
+ assert.deepEqual(launches[0].restrictions, ['no-sync']);
333
+ assert.deepEqual(launches[0].defaultRestrictions, ['mock-keychain']);
334
+
335
+ // An installed browser asked for by channel is left to Selenium.
336
+ await launchBrowserWithDependencies(
337
+ { engine: 'selenium', launch: 'engine', channel: 'chrome' },
338
+ { launchWebDriver, loadEngineModule: cftEngine, settleMs: 0 }
339
+ );
340
+ assert.equal(launches[1].executablePath, undefined);
341
+
342
+ // Selenium Manager's Chrome for Testing comes last...
343
+ const seleniumCft = process.execPath;
344
+ await launchBrowserWithDependencies(
345
+ { engine: 'selenium', launch: 'engine' },
346
+ {
347
+ launchWebDriver,
348
+ loadEngineModule: missingEngine,
349
+ cachedBrowsers: noCachedBrowsers,
350
+ seleniumManagerBrowser: async () => seleniumCft,
351
+ settleMs: 0,
352
+ }
353
+ );
354
+ assert.equal(launches[2].executablePath, seleniumCft);
355
+
356
+ // ...and without any the launch fails instead of letting the driver
357
+ // pick the installed Google Chrome.
358
+ await assert.rejects(
359
+ launchBrowserWithDependencies(
360
+ { engine: 'selenium', launch: 'engine' },
361
+ {
362
+ launchWebDriver,
363
+ loadEngineModule: missingEngine,
364
+ cachedBrowsers: noCachedBrowsers,
365
+ seleniumManagerBrowser: async () => undefined,
366
+ settleMs: 0,
367
+ }
368
+ ),
369
+ /Chrome for Testing is not installed for selenium/
370
+ );
371
+ assert.equal(launches.length, 3);
372
+ });
373
+
236
374
  it("leaves Ctrl+C to the caller with interruption: 'caller' (#174)", async () => {
237
375
  for (const [interruption, expected] of [
238
376
  [undefined, undefined],
@@ -279,34 +417,100 @@ describe('resolveLaunchExecutable', () => {
279
417
  ]);
280
418
  });
281
419
 
282
- it("falls back to the engine's downloaded Chromium without an installed Chrome", async () => {
283
- const notInstalled = async () => {
284
- throw new Error('Chrome is not installed');
285
- };
420
+ it("starts the engine's Chrome for Testing, never the installed Google Chrome", async () => {
286
421
  const executable = await resolveLaunchExecutable(
287
422
  { engine: 'playwright' },
288
- {
289
- resolveSystem: notInstalled,
290
- loadEngineModule: async () => ({
291
- executablePath: () => process.execPath,
292
- }),
293
- }
423
+ { resolveSystem: noSystemChrome, loadEngineModule: cftEngine }
294
424
  );
295
- assert.equal(executable, process.execPath);
425
+ assert.equal(executable, cftChrome);
426
+ });
296
427
 
428
+ it('says how to install Chrome for Testing or ask for Google Chrome without it', async () => {
429
+ const missing = async () => ({
430
+ executablePath: () => '/nonexistent/chrome',
431
+ });
297
432
  await assert.rejects(
298
433
  () =>
299
434
  resolveLaunchExecutable(
300
435
  { engine: 'playwright' },
301
436
  {
302
- resolveSystem: notInstalled,
303
- loadEngineModule: async () => ({
304
- executablePath: () => '/nonexistent/chrome',
305
- }),
437
+ resolveSystem: noSystemChrome,
438
+ loadEngineModule: missing,
439
+ cachedBrowsers: noCachedBrowsers,
440
+ }
441
+ ),
442
+ /Chrome for Testing is not installed for playwright: run `npx playwright install chromium`, or pass channel: 'chrome'/
443
+ );
444
+ await assert.rejects(
445
+ () =>
446
+ resolveLaunchExecutable(
447
+ { engine: 'selenium' },
448
+ {
449
+ resolveSystem: noSystemChrome,
450
+ loadEngineModule: async () => {
451
+ throw new Error('not installed');
452
+ },
453
+ seleniumManagerBrowser: async () => {
454
+ throw new Error('offline');
455
+ },
456
+ cachedBrowsers: noCachedBrowsers,
306
457
  }
307
458
  ),
308
- /Chrome is not installed/
459
+ /run `npx playwright install chromium` or `npx puppeteer browsers install chrome` or `selenium-manager --browser chrome --force-browser-download`/
460
+ );
461
+ });
462
+
463
+ it("falls back to Selenium Manager's Chrome for Testing for Selenium only", async () => {
464
+ const asked = [];
465
+ const seleniumManagerBrowser = async () => {
466
+ asked.push('selenium-manager');
467
+ return cftChrome;
468
+ };
469
+ assert.equal(
470
+ await resolveLaunchExecutable(
471
+ { engine: 'selenium' },
472
+ {
473
+ resolveSystem: noSystemChrome,
474
+ loadEngineModule: missingEngine,
475
+ seleniumManagerBrowser,
476
+ cachedBrowsers: noCachedBrowsers,
477
+ }
478
+ ),
479
+ cftChrome
480
+ );
481
+ await assert.rejects(
482
+ resolveLaunchExecutable(
483
+ { engine: 'playwright' },
484
+ {
485
+ resolveSystem: noSystemChrome,
486
+ loadEngineModule: missingEngine,
487
+ seleniumManagerBrowser,
488
+ cachedBrowsers: noCachedBrowsers,
489
+ }
490
+ ),
491
+ /Chrome for Testing is not installed for playwright/
492
+ );
493
+ assert.deepEqual(asked, ['selenium-manager']);
494
+ });
495
+
496
+ it("falls back to a cached build when the engine's own is not installed", async () => {
497
+ const asked = [];
498
+ const resolved = await resolveLaunchExecutable(
499
+ { engine: 'selenium' },
500
+ {
501
+ resolveSystem: noSystemChrome,
502
+ loadEngineModule: missingEngine,
503
+ cachedBrowsers: (engine) => {
504
+ asked.push(engine);
505
+ return ['/nonexistent/chrome-linux64/chrome', cftChrome];
506
+ },
507
+ seleniumManagerBrowser: async () => {
508
+ throw new Error('Selenium Manager is asked only without a cache');
509
+ },
510
+ }
309
511
  );
512
+ assert.equal(resolved, cftChrome);
513
+ assert.deepEqual(asked, ['selenium']);
310
514
  });
311
515
  });
312
516
 
@@ -6,6 +6,7 @@ import {
6
6
  createSourceKeyResolver,
7
7
  localStatePathForProfile,
8
8
  resolveTargetKey,
9
+ targetKeystoreFor,
9
10
  } from '../../../../src/browser/migration/os-crypt-keys.js';
10
11
  import { deriveChromiumCookieKey } from '../../../../src/browser/browser-cookie-crypto.js';
11
12
 
@@ -77,6 +78,45 @@ describe('resolveTargetKey', () => {
77
78
  assert.equal(result.prefix, 'v11');
78
79
  });
79
80
 
81
+ it('tags macOS keys v10, the only tag macOS Chromium reads', async () => {
82
+ const result = await resolveTargetKey({
83
+ browser: 'chromium',
84
+ platform: 'darwin',
85
+ readSafeStoragePassword: async () => 'target-pass',
86
+ });
87
+ assert.deepEqual(
88
+ result.key,
89
+ deriveChromiumCookieKey('target-pass', 'darwin')
90
+ );
91
+ assert.equal(result.prefix, 'v10');
92
+ });
93
+
94
+ it("uses Chromium's fixed mock passwords for a mock keystore (issue #179)", async () => {
95
+ const unreadable = async () => {
96
+ throw new Error('the OS keystore must not be asked');
97
+ };
98
+ const mac = await resolveTargetKey({
99
+ browser: 'chromium',
100
+ platform: 'darwin',
101
+ keystore: 'mock',
102
+ readSafeStoragePassword: unreadable,
103
+ });
104
+ assert.deepEqual(mac, {
105
+ key: deriveChromiumCookieKey('mock_password', 'darwin'),
106
+ prefix: 'v10',
107
+ });
108
+ const linux = await resolveTargetKey({
109
+ browser: 'chrome',
110
+ platform: 'linux',
111
+ keystore: 'mock',
112
+ readSafeStoragePassword: unreadable,
113
+ });
114
+ assert.deepEqual(linux, {
115
+ key: deriveChromiumCookieKey('peanuts', 'linux'),
116
+ prefix: 'v10',
117
+ });
118
+ });
119
+
80
120
  it('refuses to derive a Windows key', async () => {
81
121
  await assert.rejects(
82
122
  resolveTargetKey({ browser: 'chrome', platform: 'win32' }),
@@ -85,6 +125,23 @@ describe('resolveTargetKey', () => {
85
125
  });
86
126
  });
87
127
 
128
+ describe('targetKeystoreFor (issue #179)', () => {
129
+ it('reports the switches that bypass the OS keystore', () => {
130
+ assert.equal(targetKeystoreFor(['--use-mock-keychain'], 'darwin'), 'mock');
131
+ assert.equal(
132
+ targetKeystoreFor(['--password-store=basic'], 'linux'),
133
+ 'mock'
134
+ );
135
+ assert.equal(targetKeystoreFor(['--use-mock-keychain'], 'linux'), 'os');
136
+ assert.equal(
137
+ targetKeystoreFor(['--password-store=gnome-libsecret'], 'linux'),
138
+ 'os'
139
+ );
140
+ assert.equal(targetKeystoreFor([], 'darwin'), 'os');
141
+ assert.equal(targetKeystoreFor(['--use-mock-keychain'], 'win32'), 'os');
142
+ });
143
+ });
144
+
88
145
  describe('localStatePathForProfile', () => {
89
146
  it('points at Local State next to the profile directory', () => {
90
147
  assert.equal(
@@ -24,6 +24,24 @@ const fixture = (name) =>
24
24
  )
25
25
  );
26
26
 
27
+ function migratedPasswords(profile) {
28
+ const db = new Database(path.join(profile, 'Login Data'));
29
+ try {
30
+ return db.prepare('SELECT origin_url,password_value FROM logins').all();
31
+ } finally {
32
+ db.close();
33
+ }
34
+ }
35
+
36
+ const decryptPassword = (row, key, platform) =>
37
+ decryptChromiumCookie({
38
+ encryptedValue: row.password_value,
39
+ key,
40
+ platform,
41
+ databaseVersion: 0,
42
+ host: 'github.com',
43
+ });
44
+
27
45
  describe('Safari data sources', () => {
28
46
  for (const format of ['binary', 'xml']) {
29
47
  it(`preserves hierarchy and reading list from ${format} plist`, async () => {
@@ -103,25 +121,31 @@ describe('Safari data sources', () => {
103
121
  ({ reason }) => reason === 'safari-reading-list-translated'
104
122
  )
105
123
  );
106
- const db = new Database(path.join(target, 'Login Data'));
107
- try {
108
- const rows = db
109
- .prepare('SELECT origin_url,password_value FROM logins')
110
- .all();
111
- assert.equal(rows.length, 1);
112
- assert.equal(
113
- decryptChromiumCookie({
114
- encryptedValue: rows[0].password_value,
115
- key,
116
- platform: 'linux',
117
- databaseVersion: 0,
118
- host: 'github.com',
119
- }),
120
- 'p"a\nss'
121
- );
122
- } finally {
123
- db.close();
124
- }
124
+ const rows = migratedPasswords(target);
125
+ assert.equal(rows.length, 1);
126
+ assert.equal(decryptPassword(rows[0], key, 'linux'), 'p"a\nss');
127
+ });
128
+
129
+ it('encrypts migrated passwords with the mock keychain key of a Chrome for Testing launch (issue #179)', async () => {
130
+ const source = await temporary();
131
+ const target = await temporary();
132
+ // No keys are injected and the OS keystore is never asked: Chromium's
133
+ // --use-mock-keychain derives its key from the fixed "mock_password".
134
+ const report = await migrateProfile({
135
+ from: { browser: 'safari', userDataDir: source },
136
+ to: target,
137
+ include: ['passwords'],
138
+ domains: ['github.com'],
139
+ passwordCsv: fixture('Passwords.csv'),
140
+ platform: 'darwin',
141
+ targetBrowser: 'chromium',
142
+ targetKeystore: 'mock',
143
+ });
144
+ assert.equal(report.migrated.passwords, 1);
145
+ const [row] = migratedPasswords(target);
146
+ assert.equal(row.password_value.subarray(0, 3).toString(), 'v10');
147
+ const key = deriveChromiumCookieKey('mock_password', 'darwin');
148
+ assert.equal(decryptPassword(row, key, 'darwin'), 'p"a\nss');
125
149
  });
126
150
 
127
151
  it('names the executing app and retry link for non-cookie protection failures', async () => {
@@ -264,6 +264,25 @@ describe('measureParity', () => {
264
264
  );
265
265
  });
266
266
 
267
+ it("accepts Chrome for Testing's default switches as requested (issue #179)", async () => {
268
+ const { session, dependencies } = fakes({
269
+ candidateCommandLine: `${CHROME} --user-data-dir=/tmp/c --remote-debugging-port=2 --test-type=gpu --use-mock-keychain about:blank`,
270
+ });
271
+ session.args = [
272
+ '--user-data-dir=/tmp/c',
273
+ '--remote-debugging-port=2',
274
+ '--test-type=gpu',
275
+ '--use-mock-keychain',
276
+ ];
277
+ session.defaultRestrictions = [
278
+ 'no-chrome-for-testing-infobar',
279
+ 'mock-keychain',
280
+ ];
281
+ const report = await measureParity({}, dependencies);
282
+ assert.equal(report.ok, true);
283
+ assert.deepEqual(report.unlisted, []);
284
+ });
285
+
267
286
  it('measures a provided session and leaves it open', async () => {
268
287
  const { calls, session, dependencies } = fakes({
269
288
  candidateCommandLine: `${CHROME} --user-data-dir=/tmp/c --remote-debugging-port=2 about:blank`,
@@ -282,6 +282,28 @@ describe('launchAndConnectRealBrowser', () => {
282
282
  await access(path.join(temporaryDirectory, 'First Run'));
283
283
  });
284
284
 
285
+ it('gives Selenium the started browser so its driver matches it (issue #179)', async () => {
286
+ temporaryDirectory = await makeDedicatedProfile();
287
+ const cft =
288
+ '/home/u/.cache/ms-playwright/chromium-1243/chrome-linux64/chrome';
289
+ let connectOptions;
290
+ await launchAndConnectRealBrowserWithDependencies(
291
+ { engine: 'selenium', userDataDir: temporaryDirectory },
292
+ {
293
+ resolveExecutable: async () => cft,
294
+ reservePort: async () => 9444,
295
+ spawnBrowser: () => fakeProcess([]),
296
+ waitForEndpoint: async () => 'http://127.0.0.1:9444',
297
+ connect: async (options) => {
298
+ connectOptions = options;
299
+ return { browser: null, page: null };
300
+ },
301
+ }
302
+ );
303
+ assert.equal(connectOptions.engine, 'selenium');
304
+ assert.equal(connectOptions.executablePath, cft);
305
+ });
306
+
285
307
  it('migrates a profile before launch and seeds the migrated cookies', async () => {
286
308
  temporaryDirectory = await makeDedicatedProfile();
287
309
  const calls = [];
@@ -516,6 +538,109 @@ describe('launchAndConnectRealBrowser', () => {
516
538
  await result.close();
517
539
  });
518
540
 
541
+ it('adds the Chrome for Testing defaults to its command line only (issue #179)', async () => {
542
+ const cft =
543
+ '/home/u/.cache/ms-playwright/chromium-1248/chrome-linux64/chrome';
544
+ const launch = async (executablePath, options = {}) => {
545
+ let spawnedArgs;
546
+ const result = await launchAndConnectRealBrowserWithDependencies(
547
+ { closeTimeout: 50, ...options },
548
+ {
549
+ resolveExecutable: async () => executablePath,
550
+ reservePort: async () => 40006,
551
+ spawnBrowser: (_executablePath, args) => {
552
+ spawnedArgs = args;
553
+ return fakeProcess([]);
554
+ },
555
+ waitForEndpoint: async () => 'http://127.0.0.1:40006',
556
+ connect: async () => ({ browser: null, page: null }),
557
+ }
558
+ );
559
+ await result.close();
560
+ return { args: spawnedArgs, defaults: result.defaultRestrictions };
561
+ };
562
+ const defaultSwitches = (args) =>
563
+ args.filter((arg) =>
564
+ ['--test-type=gpu', '--use-mock-keychain'].includes(arg)
565
+ );
566
+
567
+ const testing = await launch(cft);
568
+ assert.deepEqual(testing.defaults, [
569
+ 'no-chrome-for-testing-infobar',
570
+ 'mock-keychain',
571
+ ]);
572
+ assert.deepEqual(defaultSwitches(testing.args), [
573
+ '--test-type=gpu',
574
+ '--use-mock-keychain',
575
+ ]);
576
+
577
+ // Google Chrome keeps the clean command line of issue #103.
578
+ const chrome = await launch('/opt/google/chrome');
579
+ assert.deepEqual(chrome.defaults, []);
580
+ assert.deepEqual(defaultSwitches(chrome.args), []);
581
+
582
+ // defaultRestrictions replaces the defaults; [] turns them off.
583
+ const off = await launch(cft, { defaultRestrictions: [] });
584
+ assert.deepEqual(defaultSwitches(off.args), []);
585
+ const keychainOnly = await launch(cft, {
586
+ defaultRestrictions: ['mock-keychain'],
587
+ });
588
+ assert.deepEqual(defaultSwitches(keychainOnly.args), [
589
+ '--use-mock-keychain',
590
+ ]);
591
+ });
592
+
593
+ it('migrates passwords with the key of the browser that starts (issue #179)', async () => {
594
+ const cft =
595
+ '/Users/u/Library/Caches/ms-playwright/chromium-1248/chrome-mac-arm64/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing';
596
+ const migrateTarget = async (executablePath, options = {}) => {
597
+ let migrateOptions;
598
+ const result = await launchAndConnectRealBrowserWithDependencies(
599
+ {
600
+ closeTimeout: 50,
601
+ migrateFrom: { browser: 'chrome', include: ['passwords'] },
602
+ ...options,
603
+ },
604
+ {
605
+ platform: 'darwin',
606
+ resolveExecutable: async () => executablePath,
607
+ reservePort: async () => 40007,
608
+ spawnBrowser: () => fakeProcess([]),
609
+ waitForEndpoint: async () => 'http://127.0.0.1:40007',
610
+ connect: async () => ({ browser: null, page: null }),
611
+ migrateProfile: async (migration) => {
612
+ migrateOptions = migration;
613
+ return { migrated: {}, skipped: [], warnings: [], cookies: [] };
614
+ },
615
+ }
616
+ );
617
+ await result.close();
618
+ const { targetBrowser, targetKeystore } = migrateOptions;
619
+ return { targetBrowser, targetKeystore };
620
+ };
621
+
622
+ // Chrome for Testing keeps "Chromium Safe Storage", and its default
623
+ // mock keychain encrypts with Chromium's fixed mock password.
624
+ assert.deepEqual(await migrateTarget(cft), {
625
+ targetBrowser: 'chromium',
626
+ targetKeystore: 'mock',
627
+ });
628
+ assert.deepEqual(await migrateTarget(cft, { defaultRestrictions: [] }), {
629
+ targetBrowser: 'chromium',
630
+ targetKeystore: 'os',
631
+ });
632
+ assert.deepEqual(await migrateTarget('/opt/google/chrome'), {
633
+ targetBrowser: 'chrome',
634
+ targetKeystore: 'os',
635
+ });
636
+ assert.deepEqual(
637
+ await migrateTarget('/opt/google/chrome', {
638
+ restrictions: ['mock-keychain'],
639
+ }),
640
+ { targetBrowser: 'chrome', targetKeystore: 'mock' }
641
+ );
642
+ });
643
+
519
644
  it('terminates the spawned browser when connection fails', async () => {
520
645
  temporaryDirectory = await makeDedicatedProfile();
521
646
  let killed = false;