browser-commander 0.19.0 → 0.21.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 +156 -0
- package/README.md +216 -32
- package/bin/browser-commander.js +16 -0
- package/eslint.config.js +10 -0
- package/examples/react-test-app/src/App.jsx +24 -23
- package/extension/background.js +14 -0
- package/extension/manifest.json +12 -0
- package/extension/relay-handler.js +353 -0
- package/package.json +25 -6
- package/scripts/run-tests.mjs +4 -2
- package/src/browser/attach/differences.js +131 -0
- package/src/browser/attach/extension-relay.js +402 -0
- package/src/browser/attach/index.js +81 -0
- package/src/browser/attach/snapshot-launch.js +146 -0
- package/src/browser/attach/snapshot.js +374 -0
- package/src/browser/attach/websocket.js +615 -0
- package/src/browser/browser-cookie-cache.js +12 -17
- package/src/browser/browser-cookie-credentials.js +6 -9
- package/src/browser/browser-cookies.js +5 -2
- package/src/browser/cdp-session.js +94 -0
- package/src/browser/connector.js +42 -4
- package/src/browser/debugging-port.js +155 -0
- package/src/browser/launch-options.js +63 -52
- package/src/browser/launch-restrictions.json +147 -0
- package/src/browser/launcher.js +329 -133
- package/src/browser/media.js +5 -0
- package/src/browser/migration/bookmarks.js +73 -0
- package/src/browser/migration/chromium-crypto.js +89 -0
- package/src/browser/migration/cookies.js +155 -0
- package/src/browser/migration/extensions.js +187 -0
- package/src/browser/migration/firefox-bookmarks.js +144 -0
- package/src/browser/migration/firefox-der.js +84 -0
- package/src/browser/migration/firefox-nss.js +281 -0
- package/src/browser/migration/firefox.js +334 -0
- package/src/browser/migration/fs-utils.js +51 -0
- package/src/browser/migration/history.js +103 -0
- package/src/browser/migration/index.js +307 -0
- package/src/browser/migration/os-crypt-keys.js +106 -0
- package/src/browser/migration/passwords.js +171 -0
- package/src/browser/migration/preferences.js +132 -0
- package/src/browser/migration/sqlite-snapshot.js +138 -0
- package/src/browser/open-in-user-browser.js +112 -0
- package/src/browser/parity.js +439 -0
- package/src/browser/profile-directory.js +102 -0
- package/src/browser/real-browser.js +513 -306
- package/src/browser/restrictions.js +148 -0
- package/src/browser/system-browser.js +232 -0
- package/src/browser/webdriver.js +572 -0
- package/src/cli/args.js +253 -0
- package/src/cli/commands.js +332 -0
- package/src/cli/dispatcher.js +99 -0
- package/src/cli/handle-methods.js +136 -0
- package/src/cli/handles.js +273 -0
- package/src/cli/high-level-methods.js +310 -0
- package/src/cli/main.js +67 -0
- package/src/cli/modules.js +100 -0
- package/src/cli/rpc-error.js +77 -0
- package/src/cli/script.js +77 -0
- package/src/cli/serve.js +144 -0
- package/src/cli/sessions.js +195 -0
- package/src/core/constants.js +7 -1
- package/src/core/engine-adapter.js +107 -1
- package/src/core/engine-detection.js +18 -3
- package/src/core/navigation-safety.js +4 -0
- package/src/core/webdriver-conversions.js +347 -0
- package/src/core/webdriver-events.js +229 -0
- package/src/core/webdriver-page.js +716 -0
- package/src/downloads/attach.js +6 -0
- package/src/downloads/manager.js +9 -0
- package/src/downloads/sources.js +71 -0
- package/src/downloads/staging.js +21 -9
- package/src/elements/selectors.js +3 -2
- package/src/elements/visibility.js +4 -0
- package/src/exports.js +61 -2
- package/src/factory.js +19 -4
- package/src/fingerprint/apply.js +6 -0
- package/src/fingerprint/automation-parity.js +7 -8
- package/src/fingerprint/limitations.js +1 -1
- package/src/fingerprint/limitations.json +45 -2
- package/src/parity/harness.js +303 -0
- package/src/parity/probe.js +838 -0
- package/src/parity/version-page.js +144 -0
- package/src/traces/bundle.js +10 -2
- package/src/traces/recorder.js +16 -3
- package/src/utilities/subprocess.js +199 -0
- package/tests/e2e/api-coverage.e2e.test.js +174 -0
- package/tests/e2e/cli.e2e.test.js +160 -0
- package/tests/e2e/downloads.e2e.test.js +79 -2
- package/tests/e2e/fingerprint-parity.e2e.test.js +104 -46
- package/tests/e2e/google-signin-probe.e2e.test.js +112 -0
- package/tests/e2e/measure-parity.e2e.test.js +118 -0
- package/tests/e2e/playwright.e2e.test.js +25 -17
- package/tests/e2e/puppeteer.e2e.test.js +17 -23
- package/tests/e2e/webdriver.e2e.test.js +239 -0
- package/tests/fixtures/firefox-nss-fixtures.mjs +211 -0
- package/tests/helpers/cli-fakes.js +214 -0
- package/tests/helpers/declared-api.js +80 -0
- package/tests/helpers/e2e-browser.js +40 -14
- package/tests/helpers/fake-chrome.js +124 -0
- package/tests/helpers/migration-fixtures.js +242 -0
- package/tests/helpers/temp-directory.js +24 -0
- package/tests/helpers/trace-fixtures.js +18 -0
- package/tests/helpers/webdriver-mocks.js +183 -0
- package/tests/unit/browser/attach/extension-relay.test.js +360 -0
- package/tests/unit/browser/attach/snapshot-launch.test.js +312 -0
- package/tests/unit/browser/attach/snapshot.test.js +390 -0
- package/tests/unit/browser/attach/websocket.test.js +240 -0
- package/tests/unit/browser/browser-cookies.test.js +2 -32
- package/tests/unit/browser/cdp-session.test.js +162 -0
- package/tests/unit/browser/connector.test.js +23 -0
- package/tests/unit/browser/launch-building-blocks.test.js +238 -0
- package/tests/unit/browser/launch-options.test.js +41 -22
- package/tests/unit/browser/launcher.test.js +281 -102
- package/tests/unit/browser/media.test.js +13 -1
- package/tests/unit/browser/migration/bookmarks.test.js +69 -0
- package/tests/unit/browser/migration/chromium-crypto.test.js +94 -0
- package/tests/unit/browser/migration/cookies.test.js +94 -0
- package/tests/unit/browser/migration/extensions.test.js +104 -0
- package/tests/unit/browser/migration/firefox-nss.test.js +113 -0
- package/tests/unit/browser/migration/firefox.test.js +139 -0
- package/tests/unit/browser/migration/history.test.js +48 -0
- package/tests/unit/browser/migration/index.test.js +117 -0
- package/tests/unit/browser/migration/os-crypt-keys.test.js +95 -0
- package/tests/unit/browser/migration/passwords.test.js +144 -0
- package/tests/unit/browser/migration/preferences.test.js +77 -0
- package/tests/unit/browser/migration/sqlite-snapshot.test.js +75 -0
- package/tests/unit/browser/open-in-user-browser.test.js +123 -0
- package/tests/unit/browser/parity.test.js +281 -0
- package/tests/unit/browser/real-browser.test.js +458 -33
- package/tests/unit/browser/webdriver.test.js +473 -0
- package/tests/unit/cli/bin.test.js +69 -0
- package/tests/unit/cli/declared-api.test.js +49 -0
- package/tests/unit/cli/dispatcher.test.js +429 -0
- package/tests/unit/cli/handles.test.js +170 -0
- package/tests/unit/cli/main.test.js +409 -0
- package/tests/unit/cli/serve.test.js +167 -0
- package/tests/unit/core/webdriver-conversions.test.js +235 -0
- package/tests/unit/core/webdriver-page.test.js +534 -0
- package/tests/unit/downloads/manager.test.js +82 -9
- package/tests/unit/downloads/staging.test.js +24 -2
- package/tests/unit/fingerprint/automation-parity.test.js +17 -4
- package/tests/unit/fingerprint/limitations.test.js +1 -1
- package/tests/unit/parity/harness.test.js +138 -0
- package/tests/unit/scripts/ci-timeout-budgets.test.js +4 -0
- package/tests/unit/scripts/ci-workflow-policy.test.js +8 -0
- package/tests/unit/scripts/feature-parity.test.js +221 -0
- package/tests/unit/scripts/npm-release-helpers.test.js +3 -2
- package/tests/unit/scripts/pre-commit-config.test.js +2 -1
- package/tests/unit/traces/bundle.test.js +24 -3
- package/tests/unit/traces/links.test.js +7 -0
- package/tests/unit/traces/reader.test.js +11 -4
- package/tests/unit/traces/recorder.test.js +4 -0
- package/tests/unit/traces/viewer.test.js +4 -0
- package/tests/unit/utilities/command-stream-only.test.js +28 -0
- package/tests/unit/utilities/subprocess.test.js +80 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Named, opt-in launch restrictions (issue #103).
|
|
3
|
+
*
|
|
4
|
+
* By default Browser Commander starts Chrome exactly like a person would:
|
|
5
|
+
* `--user-data-dir=<fresh profile> --remote-debugging-port=<reserved port>`
|
|
6
|
+
* and nothing else. Every switch the library used to add on its own - and
|
|
7
|
+
* every switch an automation engine adds - is available here by name, so a
|
|
8
|
+
* caller who wants one asks for it and the difference from a hand-started
|
|
9
|
+
* Chrome stays visible in their code:
|
|
10
|
+
*
|
|
11
|
+
* launchBrowser({ restrictions: ['no-extensions', 'no-sync'] });
|
|
12
|
+
*
|
|
13
|
+
* The catalogue lives in `launch-restrictions.json`; Python and Rust ship
|
|
14
|
+
* byte-identical copies, checked by `scripts/check-shared-fingerprint-assets.sh`.
|
|
15
|
+
*/
|
|
16
|
+
import { readFileSync } from 'node:fs';
|
|
17
|
+
|
|
18
|
+
const catalogue = JSON.parse(
|
|
19
|
+
readFileSync(new URL('./launch-restrictions.json', import.meta.url), 'utf8')
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @typedef {object} LaunchRestriction
|
|
24
|
+
* @property {string} id Stable name used in `restrictions`.
|
|
25
|
+
* @property {string} description What it changes compared with a hand-started browser.
|
|
26
|
+
* @property {string[]} [args] Chrome switches.
|
|
27
|
+
* @property {string[]} [disableFeatures] Values merged into one `--disable-features` switch.
|
|
28
|
+
* @property {Object<string,string>} [env] Environment for the browser process only.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/** @type {ReadonlyArray<LaunchRestriction>} */
|
|
32
|
+
export const LAUNCH_RESTRICTIONS = Object.freeze(
|
|
33
|
+
catalogue.restrictions.map((restriction) => Object.freeze(restriction))
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
/** Named groups of restrictions, such as the pre-#103 defaults. */
|
|
37
|
+
export const LAUNCH_RESTRICTION_PRESETS = Object.freeze(
|
|
38
|
+
Object.fromEntries(
|
|
39
|
+
Object.entries(catalogue.presets).map(([name, ids]) => [
|
|
40
|
+
name,
|
|
41
|
+
Object.freeze([...ids]),
|
|
42
|
+
])
|
|
43
|
+
)
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Throw unless `value` is an array of strings.
|
|
48
|
+
*
|
|
49
|
+
* @param {unknown} value
|
|
50
|
+
* @param {string} name - Option name used in the error
|
|
51
|
+
* @returns {string[]}
|
|
52
|
+
*/
|
|
53
|
+
export function assertStringArray(value, name) {
|
|
54
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== 'string')) {
|
|
55
|
+
throw new TypeError(`${name} must be an array of strings`);
|
|
56
|
+
}
|
|
57
|
+
return value;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function expand(names) {
|
|
61
|
+
assertStringArray(names, 'restrictions');
|
|
62
|
+
const ids = [];
|
|
63
|
+
for (const name of names) {
|
|
64
|
+
const preset = LAUNCH_RESTRICTION_PRESETS[name];
|
|
65
|
+
ids.push(...(preset ?? [name]));
|
|
66
|
+
}
|
|
67
|
+
return [...new Set(ids)];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Resolve restriction names (and preset names) into switches and environment.
|
|
72
|
+
*
|
|
73
|
+
* @param {string[]} [names]
|
|
74
|
+
* @returns {{ids: string[], args: string[], env: Object<string,string>}}
|
|
75
|
+
*/
|
|
76
|
+
export function resolveRestrictions(names = []) {
|
|
77
|
+
const ids = expand(names);
|
|
78
|
+
const args = [];
|
|
79
|
+
const disableFeatures = [];
|
|
80
|
+
const env = {};
|
|
81
|
+
for (const id of ids) {
|
|
82
|
+
const restriction = LAUNCH_RESTRICTIONS.find((entry) => entry.id === id);
|
|
83
|
+
if (!restriction) {
|
|
84
|
+
throw new RangeError(
|
|
85
|
+
`Unknown launch restriction "${id}". Expected one of ${[
|
|
86
|
+
...LAUNCH_RESTRICTIONS.map((entry) => entry.id),
|
|
87
|
+
...Object.keys(LAUNCH_RESTRICTION_PRESETS),
|
|
88
|
+
].join(', ')}`
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
args.push(...(restriction.args ?? []));
|
|
92
|
+
disableFeatures.push(...(restriction.disableFeatures ?? []));
|
|
93
|
+
Object.assign(env, restriction.env ?? {});
|
|
94
|
+
}
|
|
95
|
+
if (disableFeatures.length > 0) {
|
|
96
|
+
args.push(`--disable-features=${disableFeatures.join(',')}`);
|
|
97
|
+
}
|
|
98
|
+
return { ids, args, env };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const LIST_SWITCHES = [
|
|
102
|
+
'--disable-features',
|
|
103
|
+
'--enable-features',
|
|
104
|
+
'--disable-blink-features',
|
|
105
|
+
'--enable-blink-features',
|
|
106
|
+
];
|
|
107
|
+
|
|
108
|
+
function listSwitchOf(argument) {
|
|
109
|
+
return LIST_SWITCHES.find((prefix) => argument.startsWith(`${prefix}=`));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Merge repeated feature-list switches into one occurrence each.
|
|
114
|
+
*
|
|
115
|
+
* Chrome keeps only the last `--disable-features` (and friends), so two
|
|
116
|
+
* sources that each add one would silently cancel each other. The merged
|
|
117
|
+
* switch takes the place of the first occurrence.
|
|
118
|
+
*
|
|
119
|
+
* @param {string[]} args
|
|
120
|
+
* @returns {string[]}
|
|
121
|
+
*/
|
|
122
|
+
export function mergeFeatureSwitches(args) {
|
|
123
|
+
const values = new Map();
|
|
124
|
+
for (const argument of args) {
|
|
125
|
+
const name = listSwitchOf(argument);
|
|
126
|
+
if (name) {
|
|
127
|
+
const list = values.get(name) ?? [];
|
|
128
|
+
for (const feature of argument.slice(name.length + 1).split(',')) {
|
|
129
|
+
if (feature && !list.includes(feature)) {
|
|
130
|
+
list.push(feature);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
values.set(name, list);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const emitted = new Set();
|
|
137
|
+
const merged = [];
|
|
138
|
+
for (const argument of args) {
|
|
139
|
+
const name = listSwitchOf(argument);
|
|
140
|
+
if (!name) {
|
|
141
|
+
merged.push(argument);
|
|
142
|
+
} else if (!emitted.has(name)) {
|
|
143
|
+
emitted.add(name);
|
|
144
|
+
merged.push(`${name}=${values.get(name).join(',')}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return merged;
|
|
148
|
+
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { constants } from 'node:fs';
|
|
2
|
+
import { access } from 'node:fs/promises';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Where Chrome-family browsers are installed and where their own (default)
|
|
8
|
+
* profiles live, per platform. The launcher uses it to find an executable and
|
|
9
|
+
* to refuse a default profile; profile migration uses it to find the source.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export const CHANNEL_EXECUTABLE_NAMES = {
|
|
13
|
+
brave: ['brave-browser', 'brave-browser-stable', 'brave'],
|
|
14
|
+
chrome: ['google-chrome', 'google-chrome-stable', 'chrome'],
|
|
15
|
+
'chrome-beta': ['google-chrome-beta'],
|
|
16
|
+
'chrome-canary': ['google-chrome-canary'],
|
|
17
|
+
'chrome-dev': ['google-chrome-unstable'],
|
|
18
|
+
chromium: ['chromium', 'chromium-browser'],
|
|
19
|
+
msedge: ['microsoft-edge', 'microsoft-edge-stable', 'msedge'],
|
|
20
|
+
'msedge-beta': ['microsoft-edge-beta'],
|
|
21
|
+
'msedge-canary': ['microsoft-edge-canary'],
|
|
22
|
+
'msedge-dev': ['microsoft-edge-dev'],
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function platformPath(platform) {
|
|
26
|
+
return platform === 'win32' ? path.win32 : path;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function defaultUserDataDir(channel, homeDir = os.homedir()) {
|
|
30
|
+
const directoryName = channel.replace(/[^a-z0-9_.-]/gi, '-');
|
|
31
|
+
return path.join(
|
|
32
|
+
homeDir,
|
|
33
|
+
'.browser-commander',
|
|
34
|
+
'real-browser',
|
|
35
|
+
directoryName
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function knownDefaultUserDataDirs({
|
|
40
|
+
platform = process.platform,
|
|
41
|
+
homeDir = os.homedir(),
|
|
42
|
+
environment = process.env,
|
|
43
|
+
} = {}) {
|
|
44
|
+
const pathApi = platformPath(platform);
|
|
45
|
+
if (platform === 'darwin') {
|
|
46
|
+
const applicationSupport = pathApi.join(
|
|
47
|
+
homeDir,
|
|
48
|
+
'Library',
|
|
49
|
+
'Application Support'
|
|
50
|
+
);
|
|
51
|
+
return [
|
|
52
|
+
pathApi.join(applicationSupport, 'Google', 'Chrome'),
|
|
53
|
+
pathApi.join(applicationSupport, 'Google', 'Chrome Beta'),
|
|
54
|
+
pathApi.join(applicationSupport, 'Google', 'Chrome Canary'),
|
|
55
|
+
pathApi.join(applicationSupport, 'Google', 'Chrome Dev'),
|
|
56
|
+
pathApi.join(applicationSupport, 'Chromium'),
|
|
57
|
+
pathApi.join(applicationSupport, 'BraveSoftware', 'Brave-Browser'),
|
|
58
|
+
pathApi.join(applicationSupport, 'BraveSoftware', 'Brave-Browser-Beta'),
|
|
59
|
+
pathApi.join(
|
|
60
|
+
applicationSupport,
|
|
61
|
+
'BraveSoftware',
|
|
62
|
+
'Brave-Browser-Nightly'
|
|
63
|
+
),
|
|
64
|
+
pathApi.join(applicationSupport, 'Microsoft Edge'),
|
|
65
|
+
pathApi.join(applicationSupport, 'Microsoft Edge Beta'),
|
|
66
|
+
pathApi.join(applicationSupport, 'Microsoft Edge Canary'),
|
|
67
|
+
pathApi.join(applicationSupport, 'Microsoft Edge Dev'),
|
|
68
|
+
];
|
|
69
|
+
}
|
|
70
|
+
if (platform === 'win32') {
|
|
71
|
+
const localAppData =
|
|
72
|
+
environment.LOCALAPPDATA ?? pathApi.join(homeDir, 'AppData', 'Local');
|
|
73
|
+
return [
|
|
74
|
+
pathApi.join(localAppData, 'Google', 'Chrome', 'User Data'),
|
|
75
|
+
pathApi.join(localAppData, 'Google', 'Chrome Beta', 'User Data'),
|
|
76
|
+
pathApi.join(localAppData, 'Google', 'Chrome Dev', 'User Data'),
|
|
77
|
+
pathApi.join(localAppData, 'Google', 'Chrome SxS', 'User Data'),
|
|
78
|
+
pathApi.join(localAppData, 'Chromium', 'User Data'),
|
|
79
|
+
pathApi.join(localAppData, 'BraveSoftware', 'Brave-Browser', 'User Data'),
|
|
80
|
+
pathApi.join(
|
|
81
|
+
localAppData,
|
|
82
|
+
'BraveSoftware',
|
|
83
|
+
'Brave-Browser-Beta',
|
|
84
|
+
'User Data'
|
|
85
|
+
),
|
|
86
|
+
pathApi.join(
|
|
87
|
+
localAppData,
|
|
88
|
+
'BraveSoftware',
|
|
89
|
+
'Brave-Browser-Nightly',
|
|
90
|
+
'User Data'
|
|
91
|
+
),
|
|
92
|
+
pathApi.join(localAppData, 'Microsoft', 'Edge', 'User Data'),
|
|
93
|
+
pathApi.join(localAppData, 'Microsoft', 'Edge Beta', 'User Data'),
|
|
94
|
+
pathApi.join(localAppData, 'Microsoft', 'Edge Dev', 'User Data'),
|
|
95
|
+
pathApi.join(localAppData, 'Microsoft', 'Edge SxS', 'User Data'),
|
|
96
|
+
];
|
|
97
|
+
}
|
|
98
|
+
return [
|
|
99
|
+
pathApi.join(homeDir, '.config', 'google-chrome'),
|
|
100
|
+
pathApi.join(homeDir, '.config', 'google-chrome-beta'),
|
|
101
|
+
pathApi.join(homeDir, '.config', 'google-chrome-unstable'),
|
|
102
|
+
pathApi.join(homeDir, '.config', 'chromium'),
|
|
103
|
+
pathApi.join(homeDir, '.config', 'BraveSoftware', 'Brave-Browser'),
|
|
104
|
+
pathApi.join(homeDir, '.config', 'BraveSoftware', 'Brave-Browser-Beta'),
|
|
105
|
+
pathApi.join(homeDir, '.config', 'BraveSoftware', 'Brave-Browser-Nightly'),
|
|
106
|
+
pathApi.join(homeDir, '.config', 'microsoft-edge'),
|
|
107
|
+
pathApi.join(homeDir, '.config', 'microsoft-edge-beta'),
|
|
108
|
+
pathApi.join(homeDir, '.config', 'microsoft-edge-dev'),
|
|
109
|
+
];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Ensure Chrome is not asked to expose the user's default profile over CDP. */
|
|
113
|
+
export function assertDedicatedUserDataDir(userDataDir, platformOptions = {}) {
|
|
114
|
+
if (!userDataDir) {
|
|
115
|
+
throw new Error('launchAndConnectRealBrowser requires a userDataDir');
|
|
116
|
+
}
|
|
117
|
+
const platform = platformOptions.platform ?? process.platform;
|
|
118
|
+
const pathApi = platformPath(platform);
|
|
119
|
+
const normalize = (value) => {
|
|
120
|
+
const resolved = pathApi.resolve(value).replace(/[\\/]+$/, '');
|
|
121
|
+
return platform === 'win32' ? resolved.toLowerCase() : resolved;
|
|
122
|
+
};
|
|
123
|
+
const requested = normalize(userDataDir);
|
|
124
|
+
const isDefault = knownDefaultUserDataDirs(platformOptions).some(
|
|
125
|
+
(directory) => normalize(directory) === requested
|
|
126
|
+
);
|
|
127
|
+
if (isDefault) {
|
|
128
|
+
throw new Error(
|
|
129
|
+
'launchAndConnectRealBrowser requires a dedicated userDataDir, not a browser default profile'
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function browserInstallCandidates({
|
|
135
|
+
channel,
|
|
136
|
+
platform = process.platform,
|
|
137
|
+
environment = process.env,
|
|
138
|
+
}) {
|
|
139
|
+
const candidates = [];
|
|
140
|
+
const names = CHANNEL_EXECUTABLE_NAMES[channel];
|
|
141
|
+
if (!names) {
|
|
142
|
+
throw new Error(
|
|
143
|
+
`Unknown browser channel: ${channel}. Expected one of ${Object.keys(CHANNEL_EXECUTABLE_NAMES).join(', ')}`
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (platform === 'darwin') {
|
|
148
|
+
const applications = {
|
|
149
|
+
brave: 'Brave Browser.app/Contents/MacOS/Brave Browser',
|
|
150
|
+
chrome: 'Google Chrome.app/Contents/MacOS/Google Chrome',
|
|
151
|
+
'chrome-beta': 'Google Chrome Beta.app/Contents/MacOS/Google Chrome Beta',
|
|
152
|
+
'chrome-canary':
|
|
153
|
+
'Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary',
|
|
154
|
+
'chrome-dev': 'Google Chrome Dev.app/Contents/MacOS/Google Chrome Dev',
|
|
155
|
+
chromium: 'Chromium.app/Contents/MacOS/Chromium',
|
|
156
|
+
msedge: 'Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
|
|
157
|
+
'msedge-beta':
|
|
158
|
+
'Microsoft Edge Beta.app/Contents/MacOS/Microsoft Edge Beta',
|
|
159
|
+
'msedge-canary':
|
|
160
|
+
'Microsoft Edge Canary.app/Contents/MacOS/Microsoft Edge Canary',
|
|
161
|
+
'msedge-dev': 'Microsoft Edge Dev.app/Contents/MacOS/Microsoft Edge Dev',
|
|
162
|
+
};
|
|
163
|
+
candidates.push(path.join('/Applications', applications[channel]));
|
|
164
|
+
} else if (platform === 'win32') {
|
|
165
|
+
const roots = [
|
|
166
|
+
environment.PROGRAMFILES,
|
|
167
|
+
environment['PROGRAMFILES(X86)'],
|
|
168
|
+
environment.LOCALAPPDATA,
|
|
169
|
+
].filter(Boolean);
|
|
170
|
+
const relativePaths = {
|
|
171
|
+
brave: ['BraveSoftware', 'Brave-Browser', 'Application', 'brave.exe'],
|
|
172
|
+
chrome: ['Google', 'Chrome', 'Application', 'chrome.exe'],
|
|
173
|
+
'chrome-beta': ['Google', 'Chrome Beta', 'Application', 'chrome.exe'],
|
|
174
|
+
'chrome-canary': ['Google', 'Chrome SxS', 'Application', 'chrome.exe'],
|
|
175
|
+
'chrome-dev': ['Google', 'Chrome Dev', 'Application', 'chrome.exe'],
|
|
176
|
+
chromium: ['Chromium', 'Application', 'chrome.exe'],
|
|
177
|
+
msedge: ['Microsoft', 'Edge', 'Application', 'msedge.exe'],
|
|
178
|
+
'msedge-beta': ['Microsoft', 'Edge Beta', 'Application', 'msedge.exe'],
|
|
179
|
+
'msedge-canary': ['Microsoft', 'Edge SxS', 'Application', 'msedge.exe'],
|
|
180
|
+
'msedge-dev': ['Microsoft', 'Edge Dev', 'Application', 'msedge.exe'],
|
|
181
|
+
};
|
|
182
|
+
for (const root of roots) {
|
|
183
|
+
candidates.push(path.win32.join(root, ...relativePaths[channel]));
|
|
184
|
+
}
|
|
185
|
+
} else {
|
|
186
|
+
for (const name of names) {
|
|
187
|
+
candidates.push(`/usr/bin/${name}`, `/usr/local/bin/${name}`);
|
|
188
|
+
}
|
|
189
|
+
if (channel === 'chrome') {
|
|
190
|
+
candidates.push('/opt/google/chrome/google-chrome');
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const pathApi = platformPath(platform);
|
|
195
|
+
const delimiter = platform === 'win32' ? ';' : path.delimiter;
|
|
196
|
+
for (const directory of (environment.PATH ?? '').split(delimiter)) {
|
|
197
|
+
if (!directory) {
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
for (const name of names) {
|
|
201
|
+
candidates.push(
|
|
202
|
+
pathApi.join(directory, platform === 'win32' ? `${name}.exe` : name)
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return [...new Set(candidates)];
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Resolve a genuine installed Chrome-family browser executable. */
|
|
210
|
+
export async function resolveSystemBrowserExecutable({
|
|
211
|
+
channel = 'chrome',
|
|
212
|
+
executablePath,
|
|
213
|
+
platform = process.platform,
|
|
214
|
+
environment = process.env,
|
|
215
|
+
} = {}) {
|
|
216
|
+
const candidates = executablePath
|
|
217
|
+
? [path.resolve(executablePath)]
|
|
218
|
+
: browserInstallCandidates({ channel, platform, environment });
|
|
219
|
+
for (const candidate of candidates) {
|
|
220
|
+
try {
|
|
221
|
+
await access(candidate, constants.X_OK);
|
|
222
|
+
return candidate;
|
|
223
|
+
} catch {
|
|
224
|
+
// Continue through known locations and PATH entries.
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
throw new Error(
|
|
228
|
+
executablePath
|
|
229
|
+
? `Browser executable is not accessible: ${executablePath}`
|
|
230
|
+
: `Could not find an installed ${channel} browser; provide executablePath`
|
|
231
|
+
);
|
|
232
|
+
}
|