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
@@ -42,6 +42,10 @@ import {
42
42
  } from './profile-directory.js';
43
43
  import { buildRealBrowserArgs } from './real-browser.js';
44
44
  import { resolveRestrictions } from './restrictions.js';
45
+ import {
46
+ defaultRestrictionsFor,
47
+ isChromeForTesting,
48
+ } from './launch-executable.js';
45
49
  import { findBrowserSource } from './browser-sources.js';
46
50
  import {
47
51
  isSafariChannel,
@@ -218,7 +222,12 @@ export async function resolveWebDriverExecutable({
218
222
  };
219
223
  }
220
224
 
221
- const onPath = await findOnPath(name, { environment, platform, checkAccess });
225
+ // A driver on PATH belongs to an installed browser; Chrome for Testing gets
226
+ // the driver of its own release from Selenium Manager (issue #179).
227
+ const ownDriver = isChromeForTesting(executablePath);
228
+ const onPath =
229
+ !ownDriver &&
230
+ (await findOnPath(name, { environment, platform, checkAccess }));
222
231
  if (onPath) {
223
232
  return {
224
233
  driverPath: onPath,
@@ -227,11 +236,39 @@ export async function resolveWebDriverExecutable({
227
236
  };
228
237
  }
229
238
 
239
+ return await askSeleniumManager({
240
+ browser,
241
+ name,
242
+ executablePath,
243
+ ownDriver,
244
+ environment,
245
+ platform,
246
+ runCommand,
247
+ managerPath,
248
+ });
249
+ }
250
+
251
+ /** Ask Selenium Manager for the driver of `browser` (and the browser itself). */
252
+ async function askSeleniumManager({
253
+ browser,
254
+ name,
255
+ executablePath,
256
+ ownDriver,
257
+ environment,
258
+ platform,
259
+ runCommand,
260
+ managerPath,
261
+ }) {
230
262
  const manager = managerPath ?? seleniumManagerPath({ platform, environment });
231
- const args = ['--browser', browser, '--output', 'json'];
232
- if (executablePath) {
233
- args.push('--browser-path', executablePath);
234
- }
263
+ const args = [
264
+ '--browser',
265
+ browser,
266
+ '--output',
267
+ 'json',
268
+ ...(executablePath ? ['--browser-path', executablePath] : []),
269
+ // Selenium Manager itself prefers a driver on PATH, whatever its release.
270
+ ...(ownDriver ? ['--skip-driver-in-path'] : []),
271
+ ];
235
272
  const { stdout, code } = await runCommand(manager, args, { check: false });
236
273
  let output;
237
274
  try {
@@ -480,13 +517,14 @@ async function teardown({
480
517
  * @param {boolean} [options.bidi=false] - Enable WebDriver BiDi (events, preload scripts, full-page screenshots)
481
518
  * @param {string[]} [options.args] - Extra browser arguments
482
519
  * @param {string[]} [options.restrictions] - Chrome launch restrictions (launch-restrictions.json)
520
+ * @param {string[]} [options.defaultRestrictions] - Restrictions that replace the browser's defaults (the 'chrome-for-testing' preset for Chrome for Testing, none otherwise); [] turns them off
483
521
  * @param {boolean} [options.automationParity=true] - Keep navigator.webdriver false in headless Chrome
484
522
  * @param {Object<string,string>} [options.env] - Extra environment for the driver and browser
485
523
  * @param {number} [options.startupTimeout=30000]
486
524
  * @param {boolean} [options.verbose=false] - Mirror the driver server's output
487
525
  * @param {'managed'|'caller'} [options.interruption='managed'] - Who handles Ctrl+C for the driver server and the browser under it; see `startProcess`
488
526
  * @param {Object} [dependencies] - Injected selenium modules, runCommand, startProcess, fetch, reservePort, environment, platform (tests)
489
- * @returns {Promise<{driver: Object, page: Object, close: function(): Promise<void>, serverUrl: string, driverProcess: Object, driverPath: string, driverSource: string, browser: string, userDataDir: string, temporaryProfile: boolean, args: Array<string>, bidi: boolean}>}
527
+ * @returns {Promise<{driver: Object, page: Object, close: function(): Promise<void>, serverUrl: string, driverProcess: Object, driverPath: string, driverSource: string, browser: string, userDataDir: string, temporaryProfile: boolean, args: Array<string>, bidi: boolean, defaultRestrictions: Array<string>}>}
490
528
  */
491
529
  export async function launchWebDriver(options = {}, dependencies = {}) {
492
530
  if (!isSafariChannel(options.browser ?? 'chrome')) {
@@ -550,6 +588,17 @@ function withDriverContext(error, { serverUrl, exitCode, output }) {
550
588
  );
551
589
  }
552
590
 
591
+ // Chrome for Testing - the engines' download or Selenium Manager's - gets its
592
+ // default restrictions like any other launch of it (issue #179).
593
+ function launchDefaultRestrictions(
594
+ { browser = 'chrome', debuggerAddress, defaultRestrictions },
595
+ browserPath
596
+ ) {
597
+ return browser === 'chrome' && !debuggerAddress
598
+ ? defaultRestrictionsFor(browserPath, defaultRestrictions)
599
+ : [];
600
+ }
601
+
553
602
  async function launchWebDriverOnce(options, dependencies) {
554
603
  const {
555
604
  browser = 'chrome',
@@ -593,12 +642,17 @@ async function launchWebDriverOnce(options, dependencies) {
593
642
  executablePath,
594
643
  });
595
644
 
645
+ const defaultRestrictions = launchDefaultRestrictions(
646
+ options,
647
+ resolved.browserPath
648
+ );
649
+ const launchRestrictions = [...defaultRestrictions, ...restrictions];
596
650
  const temporaryProfile = !safari && !debuggerAddress && !requestedUserDataDir;
597
651
  const userDataDir =
598
652
  safari || debuggerAddress
599
653
  ? undefined
600
654
  : await resolveProfile(browser, requestedUserDataDir);
601
- const childEnv = driverEnvironment(restrictions, env);
655
+ const childEnv = driverEnvironment(launchRestrictions, env);
602
656
 
603
657
  let driverProcess;
604
658
  let driver;
@@ -636,7 +690,7 @@ async function launchWebDriverOnce(options, dependencies) {
636
690
  remoteDebuggingPort:
637
691
  browser === 'chrome' ? await reservePort() : undefined,
638
692
  headless,
639
- restrictions,
693
+ restrictions: launchRestrictions,
640
694
  disableFeatures,
641
695
  args,
642
696
  automationParity,
@@ -675,6 +729,7 @@ async function launchWebDriverOnce(options, dependencies) {
675
729
  temporaryProfile,
676
730
  args: built.args,
677
731
  bidi,
732
+ defaultRestrictions,
678
733
  };
679
734
  } catch (error) {
680
735
  const exitCode = driverProcess?.exitCode;
@@ -10,6 +10,7 @@ import { captureStableViewport, validateStableViewport } from './viewport.js';
10
10
  * @property {string} [path]
11
11
  * @property {boolean} [fullPage]
12
12
  * @property {boolean} [stableViewport] Capture only the existing view without visual styling.
13
+ * Default captures already do so without a fixed viewport; false keeps the engine capture.
13
14
  * @property {string} [selector]
14
15
  * @property {Object} [element]
15
16
  * @property {{x:number,y:number,width:number,height:number}} [clip]
@@ -61,7 +62,7 @@ export async function screenshot(options = {}) {
61
62
  if (options.waitForFonts) {
62
63
  await page.evaluate(() => document.fonts.ready.then(() => true));
63
64
  }
64
- if (options.stableViewport) {
65
+ if (capturesView(options, engine, css)) {
65
66
  if (engine === 'playwright') {
66
67
  native.scale = scale;
67
68
  native.caret = 'initial';
@@ -123,6 +124,20 @@ export async function screenshot(options = {}) {
123
124
  }
124
125
  }
125
126
 
127
+ /**
128
+ * A default capture of a page without a fixed viewport takes the view as it
129
+ * is: a clipped engine capture can leave a resized window stuck (issue #180).
130
+ */
131
+ function capturesView(options, engine, css) {
132
+ const { selector, element, clip, fullPage, omitBackground } = options;
133
+ return (
134
+ options.stableViewport ??
135
+ (['playwright', 'puppeteer'].includes(engine) &&
136
+ ![selector, element, clip, fullPage, omitBackground, css].some(Boolean) &&
137
+ (options.scale ?? 'device') === 'device')
138
+ );
139
+ }
140
+
126
141
  async function finishScreenshot(
127
142
  bytes,
128
143
  { page, engine, scale, format, quality, output }
@@ -28,7 +28,8 @@ export async function captureStableViewport(page, engine, native) {
28
28
  }
29
29
  let session;
30
30
  if (
31
- (engine === 'playwright' && typeof page.context === 'function') ||
31
+ (engine === 'playwright' &&
32
+ typeof page.context?.()?.newCDPSession === 'function') ||
32
33
  (engine === 'puppeteer' &&
33
34
  (typeof page.createCDPSession === 'function' ||
34
35
  typeof page.target === 'function'))
package/src/exports.js CHANGED
@@ -104,6 +104,11 @@ export {
104
104
  LAUNCH_MODES,
105
105
  resolveLaunchExecutable,
106
106
  } from './browser/launcher.js';
107
+ export {
108
+ CHROME_FOR_TESTING_RESTRICTIONS,
109
+ defaultRestrictionsFor,
110
+ isChromeForTesting,
111
+ } from './browser/launch-executable.js';
107
112
  export {
108
113
  LAUNCH_RESTRICTIONS,
109
114
  LAUNCH_RESTRICTION_PRESETS,
@@ -21,6 +21,7 @@ import { startTraceDrain } from './drain-timer.js';
21
21
  import { attachRecorderObservers } from './observers.js';
22
22
  import { attachNetwork, validateNetworkOptions } from './network.js';
23
23
  import { startRecording } from '../capture/recording.js';
24
+ import { captureStableViewport } from '../capture/viewport.js';
24
25
  import { normalizePrivacyOptions, redactValue } from './redaction.js';
25
26
  import path from 'node:path';
26
27
  import {
@@ -47,6 +48,17 @@ const DEFAULT_DOM_OPTIONS = Object.freeze({
47
48
  openShadowRoots: true,
48
49
  });
49
50
 
51
+ /** The CDP-capable engine a bare page belongs to, or null for any other page. */
52
+ function pageEngine(page) {
53
+ if (typeof page.context === 'function') {
54
+ return 'playwright';
55
+ }
56
+ return typeof page.createCDPSession === 'function' ||
57
+ typeof page.target === 'function'
58
+ ? 'puppeteer'
59
+ : null;
60
+ }
61
+
50
62
  function normalizeMode(mode) {
51
63
  const values = Object.values(TRACE_MODE);
52
64
  if (!values.includes(mode)) {
@@ -275,12 +287,16 @@ export async function startTrace(options = {}) {
275
287
  return null;
276
288
  }
277
289
  try {
290
+ // A clipped engine capture can leave a resized headful window drawn at
291
+ // its old size (issue #180); without a fixed viewport, capture the view.
278
292
  const shot = await withDeadline(
279
- page.screenshot({ type: 'png' }),
293
+ captureStableViewport(page, engine ?? pageEngine(page), {
294
+ type: 'png',
295
+ }),
280
296
  captureTimeoutMs,
281
297
  'trace screenshot'
282
298
  );
283
- return Buffer.isBuffer(shot) ? shot : Buffer.from(shot);
299
+ return shot.bytes;
284
300
  } catch (error) {
285
301
  await bundle.drop({
286
302
  reason: TRACE_DROP_REASON.CAPTURE_FAILED,
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Issue #179: without a channel or executablePath every engine starts Chrome
3
+ * for Testing, hides its "only for automated testing" infobar and keeps it
4
+ * away from the user's keychain.
5
+ */
6
+ // feature-parity: launch.chrome-for-testing@native-typed
7
+ import assert from 'node:assert/strict';
8
+ import { describe, it } from 'node:test';
9
+
10
+ import { isChromeForTesting } from '../../src/browser/launch-executable.js';
11
+ import { launchBrowser } from '../../src/browser/launcher.js';
12
+ import { readBrowserVersionPage } from '../../src/browser/parity.js';
13
+ import { SANDBOX_ARGS } from '../helpers/e2e-browser.js';
14
+
15
+ function skipReason() {
16
+ if (!process.env.RUN_E2E) {
17
+ return 'set RUN_E2E=true';
18
+ }
19
+ if (process.platform === 'linux' && !process.env.DISPLAY) {
20
+ return 'the infobar is only drawn in a window; run under xvfb-run';
21
+ }
22
+ return false;
23
+ }
24
+
25
+ /** What chrome://version reports about the browser a session started. */
26
+ async function versionPage(engine, session) {
27
+ if (engine !== 'selenium') {
28
+ return await readBrowserVersionPage(session.page);
29
+ }
30
+ await session.browser.get('chrome://version');
31
+ return await session.browser.executeScript(`
32
+ const text = (id) => (document.getElementById(id)?.textContent ?? '').trim();
33
+ return { commandLine: text('command_line'), executablePath: text('executable_path') };
34
+ `);
35
+ }
36
+
37
+ /** Height of the browser UI above the page: tab strip, toolbar, infobars. */
38
+ async function frameHeight(session) {
39
+ await session.page.goto('data:text/html,<title>frame</title>');
40
+ // The infobar appears just after the first window is shown.
41
+ await new Promise((resolve) => setTimeout(resolve, 1500));
42
+ return await session.page.evaluate(
43
+ () => globalThis.outerHeight - globalThis.innerHeight
44
+ );
45
+ }
46
+
47
+ describe(
48
+ 'Chrome for Testing by default (issue #179)',
49
+ { skip: skipReason(), timeout: 180000 },
50
+ () => {
51
+ for (const engine of ['playwright', 'puppeteer', 'selenium']) {
52
+ it(`${engine} starts Chrome for Testing without its infobar or keychain`, async () => {
53
+ const session = await launchBrowser({
54
+ engine,
55
+ headless: false,
56
+ args: SANDBOX_ARGS,
57
+ });
58
+ try {
59
+ const { commandLine, executablePath } = await versionPage(
60
+ engine,
61
+ session
62
+ );
63
+ assert.equal(
64
+ isChromeForTesting(executablePath),
65
+ true,
66
+ executablePath
67
+ );
68
+ assert.deepEqual(session.defaultRestrictions, [
69
+ 'no-chrome-for-testing-infobar',
70
+ 'mock-keychain',
71
+ ]);
72
+ assert.match(commandLine, /--test-type=gpu(\s|$)/u);
73
+ assert.match(commandLine, /--use-mock-keychain(\s|$)/u);
74
+ } finally {
75
+ await session.close();
76
+ }
77
+ });
78
+ }
79
+
80
+ it('the default restrictions remove the infobar the opt-out shows', async () => {
81
+ const heights = {};
82
+ for (const [name, defaultRestrictions] of [
83
+ ['defaults', undefined],
84
+ ['opt-out', []],
85
+ ]) {
86
+ const session = await launchBrowser({
87
+ engine: 'playwright',
88
+ headless: false,
89
+ args: SANDBOX_ARGS,
90
+ ...(defaultRestrictions ? { defaultRestrictions } : {}),
91
+ });
92
+ try {
93
+ heights[name] = await frameHeight(session);
94
+ } finally {
95
+ await session.close();
96
+ }
97
+ }
98
+ // The infobar is about 40px tall; nothing else differs between the two.
99
+ assert.ok(
100
+ heights['opt-out'] - heights.defaults >= 20,
101
+ JSON.stringify(heights)
102
+ );
103
+ });
104
+ }
105
+ );
@@ -44,6 +44,7 @@ it(
44
44
  async () => {
45
45
  const sibling = await launchBrowser({
46
46
  debug: false,
47
+ channel: 'chrome',
47
48
  headless: true,
48
49
  ...CHROME_LAUNCH_OPTIONS,
49
50
  });
@@ -58,6 +59,7 @@ it(
58
59
  launchAndConnectRealBrowserWithDependencies(
59
60
  {
60
61
  debug: false,
62
+ channel: 'chrome',
61
63
  headless: true,
62
64
  persistSessionCookies: suppliedProfile,
63
65
  ...(suppliedProfile ? { userDataDir: root } : {}),
@@ -109,6 +109,19 @@ describe('browser hand-off (issue #174)', () => {
109
109
  assert.deepEqual(await session.handoff(), handoffFor(endpoint));
110
110
  });
111
111
 
112
+ it('reattaches Selenium with the driver of the handed-off browser (issue #179)', async () => {
113
+ const endpoint = await fakeEndpoint();
114
+ const cft =
115
+ '/home/u/.cache/ms-playwright/chromium-1243/chrome-linux64/chrome';
116
+ const calls = [];
117
+ await reconnectBrowserWithDependencies(
118
+ handoffFor(endpoint, { executablePath: cft }),
119
+ {},
120
+ { connect: fakeConnect(calls) }
121
+ );
122
+ assert.equal(calls[0][1].executablePath, cft);
123
+ });
124
+
112
125
  it('reads a hand-off from a JSON file', async () => {
113
126
  const endpoint = await fakeEndpoint();
114
127
  const directory = await mkdtemp(path.join(os.tmpdir(), 'bc-handoff-'));
@@ -0,0 +1,139 @@
1
+ import assert from 'node:assert/strict';
2
+ import { chmod, mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
3
+ import { tmpdir } from 'node:os';
4
+ import path from 'node:path';
5
+ import { after, describe, it } from 'node:test';
6
+ import {
7
+ cacheLocations,
8
+ cachedChromeForTesting,
9
+ chromeForTestingCandidates,
10
+ } from '../../../src/browser/chrome-for-testing-cache.js';
11
+ import { isChromeForTesting } from '../../../src/browser/launch-executable.js';
12
+
13
+ const home = await mkdtemp(path.join(tmpdir(), 'browser-commander-cache-'));
14
+ after(() => rm(home, { recursive: true, force: true }));
15
+
16
+ async function install(...segments) {
17
+ const file = path.join(home, ...segments);
18
+ await mkdir(path.dirname(file), { recursive: true });
19
+ await writeFile(file, '#!/bin/sh\n');
20
+ await chmod(file, 0o755);
21
+ return file;
22
+ }
23
+
24
+ const linux = (env = {}) =>
25
+ cacheLocations({ platform: 'linux', arch: 'x64', home, env });
26
+
27
+ describe('Chrome for Testing in the download caches (issue #179)', () => {
28
+ it('lists the Playwright builds newest revision first', async () => {
29
+ const old = await install(
30
+ '.cache/ms-playwright/chromium-999/chrome-linux64/chrome'
31
+ );
32
+ const recent = await install(
33
+ '.cache/ms-playwright/chromium-1248/chrome-linux64/chrome'
34
+ );
35
+ await mkdir(
36
+ path.join(home, '.cache/ms-playwright/chromium_headless_shell-1300'),
37
+ {
38
+ recursive: true,
39
+ }
40
+ );
41
+ assert.deepEqual(chromeForTestingCandidates('playwright', linux()), [
42
+ recent,
43
+ old,
44
+ ]);
45
+ });
46
+
47
+ it('follows PLAYWRIGHT_BROWSERS_PATH and XDG_CACHE_HOME', async () => {
48
+ const moved = await install('moved/chromium-1300/chrome-linux64/chrome');
49
+ assert.deepEqual(
50
+ chromeForTestingCandidates(
51
+ 'playwright',
52
+ linux({ PLAYWRIGHT_BROWSERS_PATH: path.join(home, 'moved') })
53
+ ),
54
+ [moved]
55
+ );
56
+ assert.deepEqual(
57
+ chromeForTestingCandidates(
58
+ 'playwright',
59
+ linux({ PLAYWRIGHT_BROWSERS_PATH: '0' })
60
+ ),
61
+ []
62
+ );
63
+ const xdg = await install(
64
+ 'xdg/ms-playwright/chromium-1/chrome-linux64/chrome'
65
+ );
66
+ assert.deepEqual(
67
+ chromeForTestingCandidates(
68
+ 'playwright',
69
+ linux({ XDG_CACHE_HOME: path.join(home, 'xdg') })
70
+ ),
71
+ [xdg]
72
+ );
73
+ });
74
+
75
+ it('compares Puppeteer and Selenium versions numerically', async () => {
76
+ const newer = await install(
77
+ '.cache/puppeteer/chrome/linux-131.0.6778.204/chrome-linux64/chrome'
78
+ );
79
+ const older = await install(
80
+ '.cache/puppeteer/chrome/linux-99.0.1.1/chrome-linux64/chrome'
81
+ );
82
+ assert.deepEqual(chromeForTestingCandidates('puppeteer', linux()), [
83
+ newer,
84
+ older,
85
+ ]);
86
+ const selenium = await install(
87
+ '.cache/selenium/chrome/linux64/131.0.6778.85/chrome'
88
+ );
89
+ assert.deepEqual(chromeForTestingCandidates('selenium', linux()), [
90
+ selenium,
91
+ ]);
92
+ for (const file of [newer, older, selenium]) {
93
+ assert.ok(isChromeForTesting(file), file);
94
+ }
95
+ });
96
+
97
+ it('searches the caches each engine uses, in order', () => {
98
+ const machine = { platform: 'linux', arch: 'x64', home, env: {} };
99
+ const cacheOf = (file) =>
100
+ ['ms-playwright', 'puppeteer', 'selenium'].find((cache) =>
101
+ file.split(path.sep).includes(cache)
102
+ );
103
+ const caches = (engine) => [
104
+ ...new Set(cachedChromeForTesting(engine, machine).map(cacheOf)),
105
+ ];
106
+ assert.deepEqual(caches('playwright'), ['ms-playwright']);
107
+ assert.deepEqual(caches('puppeteer'), ['puppeteer']);
108
+ assert.deepEqual(caches('selenium'), [
109
+ 'ms-playwright',
110
+ 'puppeteer',
111
+ 'selenium',
112
+ ]);
113
+ });
114
+
115
+ it('knows the macOS and Windows layouts and skips other platforms', () => {
116
+ assert.equal(
117
+ cacheLocations({ platform: 'darwin', arch: 'arm64', home, env: {} }).name,
118
+ 'mac-arm64'
119
+ );
120
+ assert.equal(
121
+ cacheLocations({ platform: 'win32', arch: 'ia32', home, env: {} })
122
+ .puppeteer,
123
+ 'win32'
124
+ );
125
+ assert.equal(
126
+ cacheLocations({ platform: 'linux', arch: 'arm64', home, env: {} }),
127
+ undefined
128
+ );
129
+ assert.deepEqual(
130
+ cachedChromeForTesting('playwright', {
131
+ platform: 'linux',
132
+ arch: 'arm64',
133
+ home,
134
+ env: {},
135
+ }),
136
+ []
137
+ );
138
+ });
139
+ });
@@ -0,0 +1,94 @@
1
+ // feature-parity: launch.chrome-for-testing@native-typed
2
+ import { describe, it } from 'node:test';
3
+ import assert from 'node:assert/strict';
4
+
5
+ import {
6
+ CHROME_FOR_TESTING_RESTRICTIONS,
7
+ defaultRestrictionsFor,
8
+ isChromeForTesting,
9
+ } from '../../../src/browser/launch-executable.js';
10
+ import { resolveRestrictions } from '../../../src/browser/restrictions.js';
11
+ import * as api from '../../../src/index.js';
12
+
13
+ describe('isChromeForTesting (issue #179)', () => {
14
+ it('recognises the Chrome for Testing install layouts', () => {
15
+ for (const executable of [
16
+ // Playwright
17
+ '/home/u/.cache/ms-playwright/chromium-1248/chrome-linux64/chrome',
18
+ '/Users/u/Library/Caches/ms-playwright/chromium-1248/chrome-mac-arm64/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing',
19
+ 'C:\\Users\\u\\AppData\\Local\\ms-playwright\\chromium-1248\\chrome-win64\\chrome.exe',
20
+ // Puppeteer and @puppeteer/browsers
21
+ '/home/u/.cache/puppeteer/chrome/linux-131.0.6778.85/chrome-linux64/chrome',
22
+ '/Users/u/.cache/puppeteer/chrome/mac_arm-131.0.6778.85/chrome-mac-arm64/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing',
23
+ 'C:\\Users\\u\\.cache\\puppeteer\\chrome\\win32-131.0.6778.85\\chrome-win32\\chrome.exe',
24
+ '/Users/u/Library/Caches/ms-playwright/chromium-1248/chrome-mac-x64/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing',
25
+ // Selenium Manager
26
+ '/home/u/.cache/selenium/chrome/linux64/131.0.6778.85/chrome',
27
+ 'C:\\Users\\u\\.cache\\selenium\\chrome\\win64\\131.0.6778.85\\chrome.exe',
28
+ // An app copied anywhere keeps its name
29
+ '/Applications/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing',
30
+ ]) {
31
+ assert.equal(isChromeForTesting(executable), true, executable);
32
+ }
33
+ });
34
+
35
+ it('leaves every other browser alone', () => {
36
+ for (const executable of [
37
+ undefined,
38
+ '',
39
+ '/usr/bin/google-chrome',
40
+ '/opt/google/chrome/chrome',
41
+ '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
42
+ 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
43
+ '/usr/bin/chromium',
44
+ '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
45
+ // Playwright's own Chromium builds before Chrome for Testing
46
+ '/home/u/.cache/ms-playwright/chromium-1100/chrome-linux/chrome',
47
+ '/Users/u/Library/Caches/ms-playwright/chromium-1100/chrome-mac/Chromium.app/Contents/MacOS/Chromium',
48
+ // Selenium Manager's Firefox cache
49
+ '/home/u/.cache/selenium/firefox/linux64/133.0/firefox',
50
+ ]) {
51
+ assert.equal(isChromeForTesting(executable), false, String(executable));
52
+ }
53
+ });
54
+ });
55
+
56
+ describe('defaultRestrictionsFor (issue #179)', () => {
57
+ const cft =
58
+ '/home/u/.cache/ms-playwright/chromium-1248/chrome-linux64/chrome';
59
+
60
+ it('gives Chrome for Testing the chrome-for-testing preset', () => {
61
+ assert.deepEqual(defaultRestrictionsFor(cft), [
62
+ 'no-chrome-for-testing-infobar',
63
+ 'mock-keychain',
64
+ ]);
65
+ assert.deepEqual(defaultRestrictionsFor('/usr/bin/google-chrome'), []);
66
+ });
67
+
68
+ it("uses the caller's defaultRestrictions as given", () => {
69
+ assert.deepEqual(defaultRestrictionsFor(cft, []), []);
70
+ assert.deepEqual(
71
+ defaultRestrictionsFor('/usr/bin/google-chrome', ['mock-keychain']),
72
+ ['mock-keychain']
73
+ );
74
+ // The preset itself is never handed out to be mutated.
75
+ defaultRestrictionsFor(cft).push('no-sync');
76
+ assert.deepEqual(CHROME_FOR_TESTING_RESTRICTIONS, [
77
+ 'no-chrome-for-testing-infobar',
78
+ 'mock-keychain',
79
+ ]);
80
+ });
81
+
82
+ it('hides the infobar with --test-type=gpu and skips the keychain prompt', () => {
83
+ assert.deepEqual(resolveRestrictions(['chrome-for-testing']).args, [
84
+ '--test-type=gpu',
85
+ '--use-mock-keychain',
86
+ ]);
87
+ assert.deepEqual(api.LAUNCH_RESTRICTION_PRESETS['chrome-for-testing'], [
88
+ 'no-chrome-for-testing-infobar',
89
+ 'mock-keychain',
90
+ ]);
91
+ assert.equal(api.isChromeForTesting, isChromeForTesting);
92
+ assert.equal(api.defaultRestrictionsFor, defaultRestrictionsFor);
93
+ });
94
+ });