browser-commander 0.32.0 → 0.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/README.md +48 -5
- package/package.json +1 -1
- 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/parity.js +6 -1
- package/src/browser/profile-directory.js +8 -1
- package/src/browser/real-browser.js +29 -9
- package/src/browser/webdriver.js +22 -3
- package/src/capture/screenshot.js +16 -1
- package/src/capture/viewport.js +2 -1
- package/src/exports.js +4 -0
- package/src/traces/recorder.js +18 -2
- package/tests/e2e/startup-readiness.e2e.test.js +2 -0
- package/tests/unit/browser/chrome-for-testing-cache.test.js +139 -0
- package/tests/unit/browser/launch-building-blocks.test.js +9 -2
- package/tests/unit/browser/launch-executable.test.js +93 -0
- package/tests/unit/browser/launcher.test.js +235 -31
- package/tests/unit/browser/parity.test.js +19 -0
- package/tests/unit/browser/real-browser.test.js +52 -0
- package/tests/unit/browser/webdriver.test.js +39 -0
- package/tests/unit/scripts/preflight-credentials.test.js +156 -10
- package/tests/unit/scripts/trusted-publishing.test.js +29 -7
- 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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.33.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 60fcdc4: Launches without `channel` or `executablePath` start Chrome for Testing (the one Playwright or Puppeteer downloaded, else the newest build in its download cache; Selenium tries the Playwright and Puppeteer caches, then Selenium Manager) instead of the installed Google Chrome, which is used only on request and never as a silent fallback (#179). A Chrome for Testing launch gets the new `chrome-for-testing` restriction preset by default: `no-chrome-for-testing-infobar` (`--test-type=gpu`) and `mock-keychain` (`--use-mock-keychain`); `defaultRestrictions` changes it. New exports: `isChromeForTesting`, `defaultRestrictionsFor`, `CHROME_FOR_TESTING_RESTRICTIONS`.
|
|
8
|
+
|
|
9
|
+
Trace checkpoint screenshots and default screenshots of pages without a fixed viewport use CDP `Page.captureScreenshot` without a clip, so a headful window resized during a capture keeps its real size (#180).
|
|
10
|
+
|
|
11
|
+
## 0.32.1
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- 3b7563e: CI/CD: crates.io publishing tries trusted publishing (OIDC) first and falls back to the token secret, and the release preflight verifies that fallback.
|
|
16
|
+
|
|
17
|
+
Prepared profiles keep Chrome 155's "Continue where you left off" session restore infobar closed by writing `browser.session_restore_infobar_times_shown` to the profile's `Preferences`.
|
|
18
|
+
|
|
3
19
|
## 0.32.0
|
|
4
20
|
|
|
5
21
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -390,8 +390,8 @@ see [trace and session hardening](https://github.com/link-foundation/browser-com
|
|
|
390
390
|
```javascript
|
|
391
391
|
const { browser, page, close } = await launchBrowser({
|
|
392
392
|
engine: 'playwright', // 'playwright' or 'puppeteer'
|
|
393
|
-
launch: 'real', // 'real' (start the
|
|
394
|
-
channel: 'chrome', // or executablePath;
|
|
393
|
+
launch: 'real', // 'real' (start the browser itself, attach) or 'engine'
|
|
394
|
+
channel: 'chrome', // or executablePath; default: Chrome for Testing
|
|
395
395
|
headless: false, // Run in headless mode
|
|
396
396
|
userDataDir: undefined, // Keep a profile; default is a fresh temporary one
|
|
397
397
|
restrictions: [], // Opt-in restrictions such as 'no-extensions', 'no-sync'
|
|
@@ -410,6 +410,47 @@ browser and deletes a temporary profile. The result also carries `launch`,
|
|
|
410
410
|
`userDataDir`, `temporaryProfile` and the exact `args` the browser was started
|
|
411
411
|
with.
|
|
412
412
|
|
|
413
|
+
#### Chrome for Testing by default
|
|
414
|
+
|
|
415
|
+
Without `channel` or `executablePath`, a launch starts Chrome for Testing - the
|
|
416
|
+
one Playwright or Puppeteer downloaded, or else another build in its download
|
|
417
|
+
cache (`ms-playwright`, moved by `PLAYWRIGHT_BROWSERS_PATH`; `~/.cache/puppeteer`,
|
|
418
|
+
moved by `PUPPETEER_CACHE_DIR`), newest first; Selenium tries both caches, then
|
|
419
|
+
Selenium Manager's - never your own Google Chrome. Chrome for Testing is an app of
|
|
420
|
+
its own (`com.google.chrome.for.testing` on macOS), so the Dock icon, new tabs
|
|
421
|
+
and links opened from other apps keep going to your Chrome. Without it the
|
|
422
|
+
launch fails and names the install command (`npx playwright install chromium`,
|
|
423
|
+
`npx puppeteer browsers install chrome`) and `channel: 'chrome'`, which starts
|
|
424
|
+
the installed Google Chrome instead. It is never a silent fallback.
|
|
425
|
+
|
|
426
|
+
A Chrome for Testing launch gets the `chrome-for-testing` restriction preset
|
|
427
|
+
before your own `restrictions`: `no-chrome-for-testing-infobar`
|
|
428
|
+
(`--test-type=gpu`, the only value that hides the "Chrome for Testing is only
|
|
429
|
+
for automated testing" bar) and `mock-keychain` (`--use-mock-keychain`, no
|
|
430
|
+
keychain prompt at each start). Other browsers get nothing. `defaultRestrictions`
|
|
431
|
+
replaces that choice (`[]` opts out) and the result reports what was applied:
|
|
432
|
+
|
|
433
|
+
```javascript
|
|
434
|
+
import { isChromeForTesting, launchBrowser } from 'browser-commander';
|
|
435
|
+
|
|
436
|
+
const { defaultRestrictions, close } = await launchBrowser({});
|
|
437
|
+
console.log(defaultRestrictions); // ['no-chrome-for-testing-infobar', 'mock-keychain']
|
|
438
|
+
await close();
|
|
439
|
+
|
|
440
|
+
// Keep the infobar, or start Google Chrome on purpose:
|
|
441
|
+
await launchBrowser({ defaultRestrictions: [] });
|
|
442
|
+
await launchBrowser({ channel: 'chrome' });
|
|
443
|
+
isChromeForTesting(
|
|
444
|
+
'/home/me/.cache/ms-playwright/chromium-1248/chrome-linux64/chrome'
|
|
445
|
+
); // true
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
`launchRealBrowser()` and `launchWebDriver()` take the same
|
|
449
|
+
`defaultRestrictions`. With the mock keychain, Chrome for Testing cannot decrypt
|
|
450
|
+
cookies another Chrome wrote into a copied profile; move logins with
|
|
451
|
+
`readBrowserCookies()` or `storageState`, which set cookies through the
|
|
452
|
+
browser's cookie API instead.
|
|
453
|
+
|
|
413
454
|
The `args` option allows passing custom Chrome arguments, which is useful for headless server environments (Docker, CI/CD) that require flags like `--no-sandbox`.
|
|
414
455
|
|
|
415
456
|
The `storageState` option accepts a Playwright-compatible JSON path or object.
|
|
@@ -439,11 +480,13 @@ Playwright accepts `timeout` and Puppeteer accepts `protocolTimeout`.
|
|
|
439
480
|
|
|
440
481
|
### launchRealBrowser(options)
|
|
441
482
|
|
|
442
|
-
Start
|
|
483
|
+
Start a browser and connect through `connectBrowser()`. Without `channel` or
|
|
484
|
+
`executablePath` it starts Chrome for Testing, as `launchBrowser()` does; use
|
|
443
485
|
`channel` (`chrome`, `chrome-beta`, `chrome-dev`, `chrome-canary`, `msedge`,
|
|
444
486
|
`msedge-beta`, `msedge-dev`, `msedge-canary`, `brave`, or `chromium`) or an
|
|
445
|
-
explicit `executablePath
|
|
446
|
-
profile that `close()` deletes; it rejects known
|
|
487
|
+
explicit `executablePath` for an installed browser. Without `userDataDir` it
|
|
488
|
+
creates a fresh temporary profile that `close()` deletes; it rejects known
|
|
489
|
+
default browser-profile paths,
|
|
447
490
|
protects its remote-debugging switches, reserves a loopback port (or uses
|
|
448
491
|
`remoteDebuggingPort`, never `0`), and returns the spawned `browserProcess`,
|
|
449
492
|
`cdpEndpoint`, `remoteDebuggingPort`, `executablePath`, `userDataDir`,
|
package/package.json
CHANGED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { readdirSync, statSync } from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Finding Chrome for Testing in the download caches (issue #179).
|
|
7
|
+
*
|
|
8
|
+
* The engine's own `executablePath()` names one exact build; when that build
|
|
9
|
+
* is not installed, a launch still starts another Chrome for Testing the
|
|
10
|
+
* Playwright, Puppeteer or Selenium Manager caches hold, newest version
|
|
11
|
+
* first. Mirrors `CacheLocations` in `rust/src/browser/chrome_for_testing.rs`.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** The caches each engine finds Chrome for Testing in, in order. */
|
|
15
|
+
export const ENGINE_CACHES = Object.freeze({
|
|
16
|
+
playwright: ['playwright'],
|
|
17
|
+
puppeteer: ['puppeteer'],
|
|
18
|
+
selenium: ['playwright', 'puppeteer', 'selenium'],
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// name: archive and Selenium cache platform; puppeteer: Puppeteer's prefix.
|
|
22
|
+
const PLATFORMS = {
|
|
23
|
+
'linux-x64': { name: 'linux64', puppeteer: 'linux' },
|
|
24
|
+
'darwin-x64': { name: 'mac-x64', puppeteer: 'mac' },
|
|
25
|
+
'darwin-arm64': { name: 'mac-arm64', puppeteer: 'mac_arm' },
|
|
26
|
+
'win32-x64': { name: 'win64', puppeteer: 'win64' },
|
|
27
|
+
'win32-ia32': { name: 'win32', puppeteer: 'win32' },
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Where the caches are on this machine, or `undefined` where Chrome for
|
|
32
|
+
* Testing is not built for the platform.
|
|
33
|
+
*
|
|
34
|
+
* @param {Object} [machine]
|
|
35
|
+
* @param {string} [machine.platform] - `process.platform`
|
|
36
|
+
* @param {string} [machine.arch] - `process.arch`
|
|
37
|
+
* @param {string} [machine.home] - The home directory
|
|
38
|
+
* @param {Object} [machine.env] - The environment variables
|
|
39
|
+
*/
|
|
40
|
+
export function cacheLocations({
|
|
41
|
+
platform = process.platform,
|
|
42
|
+
arch = process.arch,
|
|
43
|
+
home = os.homedir(),
|
|
44
|
+
env = process.env,
|
|
45
|
+
} = {}) {
|
|
46
|
+
const known = PLATFORMS[platformKey(platform, arch)];
|
|
47
|
+
return known ? { platform, ...known, home, env } : undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function platformKey(platform, arch) {
|
|
51
|
+
if (platform === 'darwin') {
|
|
52
|
+
return arch === 'arm64' ? 'darwin-arm64' : 'darwin-x64';
|
|
53
|
+
}
|
|
54
|
+
if (platform === 'win32') {
|
|
55
|
+
return arch === 'ia32' ? 'win32-ia32' : 'win32-x64';
|
|
56
|
+
}
|
|
57
|
+
return `${platform}-${arch}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function variable(locations, name) {
|
|
61
|
+
return locations.env[name] || undefined;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function homePath(locations, ...relative) {
|
|
65
|
+
return locations.home ? path.join(locations.home, ...relative) : undefined;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const ROOTS = {
|
|
69
|
+
playwright(locations) {
|
|
70
|
+
const root = locations.env.PLAYWRIGHT_BROWSERS_PATH;
|
|
71
|
+
if (root) {
|
|
72
|
+
// `0` keeps the browsers inside node_modules, where only the
|
|
73
|
+
// Playwright package itself knows the path.
|
|
74
|
+
return root === '0' ? undefined : root;
|
|
75
|
+
}
|
|
76
|
+
if (locations.platform === 'win32') {
|
|
77
|
+
const local = variable(locations, 'LOCALAPPDATA');
|
|
78
|
+
return local && path.join(local, 'ms-playwright');
|
|
79
|
+
}
|
|
80
|
+
if (locations.platform === 'darwin') {
|
|
81
|
+
return homePath(locations, 'Library', 'Caches', 'ms-playwright');
|
|
82
|
+
}
|
|
83
|
+
const cache = variable(locations, 'XDG_CACHE_HOME');
|
|
84
|
+
return cache
|
|
85
|
+
? path.join(cache, 'ms-playwright')
|
|
86
|
+
: homePath(locations, '.cache', 'ms-playwright');
|
|
87
|
+
},
|
|
88
|
+
puppeteer: (locations) =>
|
|
89
|
+
variable(locations, 'PUPPETEER_CACHE_DIR') ??
|
|
90
|
+
homePath(locations, '.cache', 'puppeteer'),
|
|
91
|
+
selenium: (locations) =>
|
|
92
|
+
variable(locations, 'SE_CACHE_PATH') ??
|
|
93
|
+
homePath(locations, '.cache', 'selenium'),
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
/** The binary inside an unpacked Chrome for Testing directory. */
|
|
97
|
+
function binary(locations) {
|
|
98
|
+
if (locations.platform === 'darwin') {
|
|
99
|
+
return path.join(
|
|
100
|
+
'Google Chrome for Testing.app',
|
|
101
|
+
'Contents',
|
|
102
|
+
'MacOS',
|
|
103
|
+
'Google Chrome for Testing'
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
return locations.platform === 'win32' ? 'chrome.exe' : 'chrome';
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** A dotted version (`131.0.6778.204`) or revision (`1248`) as numbers. */
|
|
110
|
+
function versionKey(text) {
|
|
111
|
+
return text !== undefined && /^\d+(\.\d+)*$/u.test(text)
|
|
112
|
+
? text.split('.').map(Number)
|
|
113
|
+
: undefined;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function compareVersions(left, right) {
|
|
117
|
+
for (let index = 0; index < Math.max(left.length, right.length); index++) {
|
|
118
|
+
const difference = (left[index] ?? -1) - (right[index] ?? -1);
|
|
119
|
+
if (difference !== 0) {
|
|
120
|
+
return difference;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return 0;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function isDirectory(file) {
|
|
127
|
+
try {
|
|
128
|
+
return statSync(file).isDirectory();
|
|
129
|
+
} catch {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** The directories in `root` whose name `key` parses, newest first. */
|
|
135
|
+
function newestFirst(root, key) {
|
|
136
|
+
let names;
|
|
137
|
+
try {
|
|
138
|
+
names = readdirSync(root);
|
|
139
|
+
} catch {
|
|
140
|
+
return [];
|
|
141
|
+
}
|
|
142
|
+
return names
|
|
143
|
+
.map((name) => ({ version: key(name), directory: path.join(root, name) }))
|
|
144
|
+
.filter(({ version, directory }) => version && isDirectory(directory))
|
|
145
|
+
.sort((left, right) => compareVersions(right.version, left.version))
|
|
146
|
+
.map(({ directory }) => directory);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const withPrefix = (prefix) => (name) =>
|
|
150
|
+
name.startsWith(prefix) ? versionKey(name.slice(prefix.length)) : undefined;
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* The Chrome for Testing binaries `cache` may hold, newest version first.
|
|
154
|
+
* The paths are candidates: the caller checks they exist.
|
|
155
|
+
*
|
|
156
|
+
* @param {'playwright'|'puppeteer'|'selenium'} cache
|
|
157
|
+
* @param {Object} locations - From {@link cacheLocations}
|
|
158
|
+
* @returns {string[]}
|
|
159
|
+
*/
|
|
160
|
+
export function chromeForTestingCandidates(cache, locations) {
|
|
161
|
+
const root = ROOTS[cache](locations);
|
|
162
|
+
if (!root) {
|
|
163
|
+
return [];
|
|
164
|
+
}
|
|
165
|
+
const archive = `chrome-${locations.name}`;
|
|
166
|
+
const file = binary(locations);
|
|
167
|
+
if (cache === 'selenium') {
|
|
168
|
+
return newestFirst(
|
|
169
|
+
path.join(root, 'chrome', locations.name),
|
|
170
|
+
versionKey
|
|
171
|
+
).map((directory) => path.join(directory, file));
|
|
172
|
+
}
|
|
173
|
+
const directories =
|
|
174
|
+
cache === 'playwright'
|
|
175
|
+
? newestFirst(root, withPrefix('chromium-'))
|
|
176
|
+
: newestFirst(
|
|
177
|
+
path.join(root, 'chrome'),
|
|
178
|
+
withPrefix(`${locations.puppeteer}-`)
|
|
179
|
+
);
|
|
180
|
+
return directories.map((directory) => path.join(directory, archive, file));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* The Chrome for Testing candidates in the caches `engine` uses, in order.
|
|
185
|
+
*
|
|
186
|
+
* @param {string} engine
|
|
187
|
+
* @param {Object} [machine] - See {@link cacheLocations}
|
|
188
|
+
* @returns {string[]}
|
|
189
|
+
*/
|
|
190
|
+
export function cachedChromeForTesting(engine, machine) {
|
|
191
|
+
const locations = cacheLocations(machine);
|
|
192
|
+
if (!locations) {
|
|
193
|
+
return [];
|
|
194
|
+
}
|
|
195
|
+
return (ENGINE_CACHES[engine] ?? []).flatMap((cache) =>
|
|
196
|
+
chromeForTestingCandidates(cache, locations)
|
|
197
|
+
);
|
|
198
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { constants } from 'node:fs';
|
|
2
|
+
import { access } from 'node:fs/promises';
|
|
3
|
+
import { cachedChromeForTesting } from './chrome-for-testing-cache.js';
|
|
4
|
+
import { LAUNCH_RESTRICTION_PRESETS } from './restrictions.js';
|
|
5
|
+
import { resolveSystemBrowserExecutable } from './system-browser.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Which browser binary an automation launch starts (issue #179).
|
|
9
|
+
*
|
|
10
|
+
* Without `channel` or `executablePath` a launch starts Chrome for Testing,
|
|
11
|
+
* the browser Playwright and Puppeteer download. It is an application of its
|
|
12
|
+
* own (`com.google.chrome.for.testing` on macOS), so the user's Google Chrome
|
|
13
|
+
* - its Dock icon, its new tabs, the links other apps open - never goes to the
|
|
14
|
+
* automation window. The installed Google Chrome is used only on request.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** Load the browser launcher of each engine that downloads a browser. */
|
|
18
|
+
export const ENGINE_BROWSERS = Object.freeze({
|
|
19
|
+
playwright: async () => (await import('playwright')).chromium,
|
|
20
|
+
puppeteer: async () => (await import('puppeteer')).default,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// Selenium has no browser of its own; it starts the one Playwright or
|
|
24
|
+
// Puppeteer downloaded, then the one Selenium Manager keeps.
|
|
25
|
+
const BUNDLED_BROWSER_ENGINES = {
|
|
26
|
+
playwright: ['playwright'],
|
|
27
|
+
puppeteer: ['puppeteer'],
|
|
28
|
+
selenium: ['playwright', 'puppeteer'],
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const PLAYWRIGHT_INSTALL = 'npx playwright install chromium';
|
|
32
|
+
const PUPPETEER_INSTALL = 'npx puppeteer browsers install chrome';
|
|
33
|
+
const INSTALL_COMMANDS = {
|
|
34
|
+
playwright: [PLAYWRIGHT_INSTALL],
|
|
35
|
+
puppeteer: [PUPPETEER_INSTALL],
|
|
36
|
+
selenium: [
|
|
37
|
+
PLAYWRIGHT_INSTALL,
|
|
38
|
+
PUPPETEER_INSTALL,
|
|
39
|
+
'selenium-manager --browser chrome --force-browser-download',
|
|
40
|
+
],
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// Chrome for Testing archives unpack to chrome-<platform>; the macOS app is
|
|
44
|
+
// "Google Chrome for Testing.app". Playwright's older own Chromium builds
|
|
45
|
+
// (chrome-linux, chrome-win, Chromium.app) are not Chrome for Testing.
|
|
46
|
+
const CHROME_FOR_TESTING_DIRECTORIES = new Set([
|
|
47
|
+
'chrome-linux64',
|
|
48
|
+
'chrome-win64',
|
|
49
|
+
'chrome-win32',
|
|
50
|
+
'chrome-mac-x64',
|
|
51
|
+
'chrome-mac-arm64',
|
|
52
|
+
]);
|
|
53
|
+
const SELENIUM_CACHE_PLATFORMS = new Set([
|
|
54
|
+
'linux64',
|
|
55
|
+
'win64',
|
|
56
|
+
'win32',
|
|
57
|
+
'mac-x64',
|
|
58
|
+
'mac-arm64',
|
|
59
|
+
]);
|
|
60
|
+
|
|
61
|
+
/** The restrictions every Chrome for Testing launch gets by default. */
|
|
62
|
+
export const CHROME_FOR_TESTING_RESTRICTIONS =
|
|
63
|
+
LAUNCH_RESTRICTION_PRESETS['chrome-for-testing'];
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Whether an executable is a Chrome for Testing build, judged by its install
|
|
67
|
+
* path: Playwright, Puppeteer, `@puppeteer/browsers` and Selenium Manager all
|
|
68
|
+
* keep it under a Chrome for Testing archive directory.
|
|
69
|
+
*
|
|
70
|
+
* @param {string} [executablePath]
|
|
71
|
+
* @returns {boolean}
|
|
72
|
+
*/
|
|
73
|
+
export function isChromeForTesting(executablePath) {
|
|
74
|
+
if (typeof executablePath !== 'string') {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
const segments = executablePath.toLowerCase().split(/[\\/]+/u);
|
|
78
|
+
if (
|
|
79
|
+
segments.some(
|
|
80
|
+
(segment) =>
|
|
81
|
+
segment.includes('chrome for testing') ||
|
|
82
|
+
CHROME_FOR_TESTING_DIRECTORIES.has(segment)
|
|
83
|
+
)
|
|
84
|
+
) {
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
// Selenium Manager: ~/.cache/selenium/chrome/<platform>/<version>/chrome
|
|
88
|
+
const selenium = segments.lastIndexOf('selenium');
|
|
89
|
+
return (
|
|
90
|
+
selenium >= 0 &&
|
|
91
|
+
segments[selenium + 1] === 'chrome' &&
|
|
92
|
+
SELENIUM_CACHE_PLATFORMS.has(segments[selenium + 2])
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* The restrictions a launch of `executablePath` gets unless the caller sets
|
|
98
|
+
* `defaultRestrictions`: the `chrome-for-testing` preset for Chrome for
|
|
99
|
+
* Testing, nothing for any other browser.
|
|
100
|
+
*
|
|
101
|
+
* @param {string} executablePath
|
|
102
|
+
* @param {string[]} [requested] - The caller's `defaultRestrictions`
|
|
103
|
+
* @returns {string[]}
|
|
104
|
+
*/
|
|
105
|
+
export function defaultRestrictionsFor(executablePath, requested) {
|
|
106
|
+
if (requested !== undefined) {
|
|
107
|
+
return requested;
|
|
108
|
+
}
|
|
109
|
+
return isChromeForTesting(executablePath)
|
|
110
|
+
? [...CHROME_FOR_TESTING_RESTRICTIONS]
|
|
111
|
+
: [];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function isExecutable(file) {
|
|
115
|
+
try {
|
|
116
|
+
await access(file, constants.X_OK);
|
|
117
|
+
return true;
|
|
118
|
+
} catch {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* The Chrome for Testing Selenium Manager keeps in `~/.cache/selenium`,
|
|
125
|
+
* downloaded when missing. `--force-browser-download` stops it from picking
|
|
126
|
+
* the installed Google Chrome instead.
|
|
127
|
+
*
|
|
128
|
+
* @returns {Promise<string|undefined>}
|
|
129
|
+
*/
|
|
130
|
+
export async function seleniumManagerChromeForTesting() {
|
|
131
|
+
// Imported here: webdriver.js imports this module.
|
|
132
|
+
const { seleniumManagerPath } = await import('./webdriver.js');
|
|
133
|
+
const { runCommand } = await import('../utilities/subprocess.js');
|
|
134
|
+
const { stdout, code } = await runCommand(
|
|
135
|
+
seleniumManagerPath(),
|
|
136
|
+
['--browser', 'chrome', '--force-browser-download', '--output', 'json'],
|
|
137
|
+
{ check: false }
|
|
138
|
+
);
|
|
139
|
+
return code === 0 ? JSON.parse(stdout)?.result?.browser_path : undefined;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function existingExecutable(find) {
|
|
143
|
+
const executable = await Promise.resolve()
|
|
144
|
+
.then(find)
|
|
145
|
+
.catch(() => undefined);
|
|
146
|
+
return executable && (await isExecutable(executable))
|
|
147
|
+
? executable
|
|
148
|
+
: undefined;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Pick the browser binary for a launch.
|
|
153
|
+
*
|
|
154
|
+
* An explicit `executablePath` or `channel` is honoured as given. Without
|
|
155
|
+
* either, the engine's downloaded Chrome for Testing is started, then any
|
|
156
|
+
* other build in its download cache, newest first; Selenium uses Playwright's,
|
|
157
|
+
* then Puppeteer's, then the cached ones, then Selenium Manager's. The
|
|
158
|
+
* installed Google Chrome is never a silent fallback: without Chrome for
|
|
159
|
+
* Testing the launch fails and says how to install it or how to ask for
|
|
160
|
+
* Google Chrome.
|
|
161
|
+
*/
|
|
162
|
+
export async function resolveLaunchExecutable(
|
|
163
|
+
{ engine, channel, executablePath },
|
|
164
|
+
{
|
|
165
|
+
resolveSystem = resolveSystemBrowserExecutable,
|
|
166
|
+
loadEngineModule,
|
|
167
|
+
seleniumManagerBrowser = seleniumManagerChromeForTesting,
|
|
168
|
+
cachedBrowsers = cachedChromeForTesting,
|
|
169
|
+
} = {}
|
|
170
|
+
) {
|
|
171
|
+
if (executablePath !== undefined || channel !== undefined) {
|
|
172
|
+
return await resolveSystem({
|
|
173
|
+
channel: channel ?? 'chrome',
|
|
174
|
+
executablePath,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
const engines = BUNDLED_BROWSER_ENGINES[engine] ?? [];
|
|
178
|
+
const loaders = loadEngineModule
|
|
179
|
+
? [loadEngineModule]
|
|
180
|
+
: engines.map((name) => ENGINE_BROWSERS[name]);
|
|
181
|
+
const finders = loaders.map(
|
|
182
|
+
(load) => async () => (await load()).executablePath()
|
|
183
|
+
);
|
|
184
|
+
const cached = await Promise.resolve()
|
|
185
|
+
.then(() => cachedBrowsers(engine))
|
|
186
|
+
.catch(() => []);
|
|
187
|
+
finders.push(...cached.map((executable) => () => executable));
|
|
188
|
+
if (engine === 'selenium') {
|
|
189
|
+
finders.push(seleniumManagerBrowser);
|
|
190
|
+
}
|
|
191
|
+
for (const find of finders) {
|
|
192
|
+
const executable = await existingExecutable(find);
|
|
193
|
+
if (executable) {
|
|
194
|
+
return executable;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const install = (INSTALL_COMMANDS[engine] ?? [])
|
|
198
|
+
.map((command) => `\`${command}\``)
|
|
199
|
+
.join(' or ');
|
|
200
|
+
throw new Error(
|
|
201
|
+
`Chrome for Testing is not installed for ${engine}: run ${install}, or pass channel: 'chrome' to start the installed Google Chrome`
|
|
202
|
+
);
|
|
203
|
+
}
|
|
@@ -32,9 +32,14 @@
|
|
|
32
32
|
},
|
|
33
33
|
{
|
|
34
34
|
"id": "mock-keychain",
|
|
35
|
-
"description": "Use a mock macOS Keychain. Real-profile data cannot be decrypted or migrated.",
|
|
35
|
+
"description": "Use a mock macOS Keychain. Real-profile data cannot be decrypted or migrated. Added by default for Chrome for Testing, which otherwise asks for its keychain entry at each start.",
|
|
36
36
|
"args": ["--use-mock-keychain"]
|
|
37
37
|
},
|
|
38
|
+
{
|
|
39
|
+
"id": "no-chrome-for-testing-infobar",
|
|
40
|
+
"description": "Hide the \"Chrome for Testing is only for automated testing\" bar; Chromium skips it only for --test-type=gpu, which also skips the unsupported-flag and other startup infobars. Added by default for Chrome for Testing.",
|
|
41
|
+
"args": ["--test-type=gpu"]
|
|
42
|
+
},
|
|
38
43
|
{
|
|
39
44
|
"id": "no-extensions",
|
|
40
45
|
"description": "Disable extensions, component extensions with background pages and default apps.",
|
|
@@ -167,6 +172,7 @@
|
|
|
167
172
|
"no-google-services",
|
|
168
173
|
"no-translate"
|
|
169
174
|
],
|
|
175
|
+
"chrome-for-testing": ["no-chrome-for-testing-infobar", "mock-keychain"],
|
|
170
176
|
"quiet-ui": [
|
|
171
177
|
"no-first-run",
|
|
172
178
|
"no-default-browser-check",
|