browser-commander 0.20.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 +145 -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 +19 -4
- 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/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/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/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/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/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/feature-parity.test.js +221 -0
- package/tests/unit/traces/reader.test.js +1 -0
- package/tests/unit/traces/recorder.test.js +1 -0
- package/tests/unit/utilities/command-stream-only.test.js +28 -0
- package/tests/unit/utilities/subprocess.test.js +80 -0
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
import {
|
|
2
|
+
copyFile,
|
|
3
|
+
mkdir,
|
|
4
|
+
open,
|
|
5
|
+
readdir,
|
|
6
|
+
readFile,
|
|
7
|
+
stat,
|
|
8
|
+
writeFile,
|
|
9
|
+
} from 'node:fs/promises';
|
|
10
|
+
import os from 'node:os';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
|
|
13
|
+
import BetterSqlite3 from 'better-sqlite3';
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
browserProfileRoot,
|
|
17
|
+
normalizeCookieBrowser,
|
|
18
|
+
} from '../browser-profiles.js';
|
|
19
|
+
import { pathExists } from '../migration/fs-utils.js';
|
|
20
|
+
import { withDatabaseSnapshot } from '../migration/sqlite-snapshot.js';
|
|
21
|
+
import {
|
|
22
|
+
createTemporaryUserDataDir,
|
|
23
|
+
LOCAL_STATE_FILE,
|
|
24
|
+
prepareUserDataDir,
|
|
25
|
+
removeUserDataDir,
|
|
26
|
+
} from '../profile-directory.js';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Snapshot of the real data folder (issue #102, addendum, mode 1).
|
|
30
|
+
*
|
|
31
|
+
* Chrome 136 and later ignore `--remote-debugging-port` for the default user
|
|
32
|
+
* data directory, so the closest thing to "the real folder" that CDP allows is
|
|
33
|
+
* a copy of it in a temporary user data directory. The copy is made read-only
|
|
34
|
+
* from the source, also while the source browser runs:
|
|
35
|
+
*
|
|
36
|
+
* 1. `Local State` (profile-wide settings and, on Windows, the cookie and
|
|
37
|
+
* password key) is copied from `<root>/Local State` to `<target>/Local
|
|
38
|
+
* State`.
|
|
39
|
+
* 2. The one profile directory `<root>/<profile>` is copied to
|
|
40
|
+
* `<target>/<profile>` recursively, keeping the relative layout, so the
|
|
41
|
+
* target is a user data directory with that profile.
|
|
42
|
+
* 3. Entries matching {@link snapshotSkipReason} are not copied and are listed
|
|
43
|
+
* in `skipped`: lock files and sockets, caches, crash reports, the open-tabs
|
|
44
|
+
* session files, and SQLite `-journal`/`-wal`/`-shm` sidecars (the backup
|
|
45
|
+
* snapshot already contains their committed content). Symbolic links and
|
|
46
|
+
* special files are never followed or copied.
|
|
47
|
+
* 4. A file whose first 16 bytes are the SQLite header `SQLite format 3\0` is
|
|
48
|
+
* copied through the SQLite Online Backup API snapshot
|
|
49
|
+
* (`withDatabaseSnapshot`), which is consistent even while the browser
|
|
50
|
+
* writes to it; every other file is a plain byte copy.
|
|
51
|
+
* 5. A file or directory that cannot be read is listed in `skipped` with
|
|
52
|
+
* reason `unreadable` and the error message; it never fails the snapshot.
|
|
53
|
+
* 6. The target gets Chrome's `First Run` sentinel (and the initial Local
|
|
54
|
+
* State when the source had none), and in the copied `Preferences`
|
|
55
|
+
* `profile.exit_type` is set to `Normal` and `profile.exited_cleanly` to
|
|
56
|
+
* true, because a running browser records itself as crashed until it
|
|
57
|
+
* exits and the copy would otherwise show the "restore pages" bubble.
|
|
58
|
+
*
|
|
59
|
+
* Nothing is ever written to the source. Only Chromium-family browsers
|
|
60
|
+
* (chrome, chromium, edge, brave) are supported.
|
|
61
|
+
*/
|
|
62
|
+
|
|
63
|
+
/** The 16-byte header every SQLite 3 database file starts with. */
|
|
64
|
+
export const SQLITE_HEADER = Buffer.from('SQLite format 3\0', 'latin1');
|
|
65
|
+
|
|
66
|
+
/** Directory names (at any depth) that are caches. */
|
|
67
|
+
const CACHE_NAMES = new Set([
|
|
68
|
+
'Cache',
|
|
69
|
+
'Code Cache',
|
|
70
|
+
'GPUCache',
|
|
71
|
+
'DawnCache',
|
|
72
|
+
'DawnGraphiteCache',
|
|
73
|
+
'DawnWebGPUCache',
|
|
74
|
+
'GrShaderCache',
|
|
75
|
+
'ShaderCache',
|
|
76
|
+
]);
|
|
77
|
+
|
|
78
|
+
/** Open-tabs session state; the launch opens its own start page. */
|
|
79
|
+
const SESSION_NAMES = new Set([
|
|
80
|
+
'Sessions',
|
|
81
|
+
'Current Session',
|
|
82
|
+
'Current Tabs',
|
|
83
|
+
'Last Session',
|
|
84
|
+
'Last Tabs',
|
|
85
|
+
]);
|
|
86
|
+
|
|
87
|
+
const SQLITE_SIDECAR_SUFFIXES = ['-journal', '-wal', '-shm'];
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Why an entry is not copied, or null when it is.
|
|
91
|
+
*
|
|
92
|
+
* @param {string} relativePath - Path relative to the source root, with `/`
|
|
93
|
+
* separators (for example `Default/Service Worker/CacheStorage`)
|
|
94
|
+
* @returns {('lock'|'cache'|'crash-reports'|'session'|'sqlite-sidecar'|null)}
|
|
95
|
+
*/
|
|
96
|
+
export function snapshotSkipReason(relativePath) {
|
|
97
|
+
const name = relativePath.split('/').at(-1);
|
|
98
|
+
if (name.startsWith('Singleton') || name === 'lockfile' || name === 'LOCK') {
|
|
99
|
+
return 'lock';
|
|
100
|
+
}
|
|
101
|
+
if (
|
|
102
|
+
CACHE_NAMES.has(name) ||
|
|
103
|
+
relativePath.endsWith('Service Worker/CacheStorage')
|
|
104
|
+
) {
|
|
105
|
+
return 'cache';
|
|
106
|
+
}
|
|
107
|
+
if (name === 'Crashpad') {
|
|
108
|
+
return 'crash-reports';
|
|
109
|
+
}
|
|
110
|
+
if (SESSION_NAMES.has(name)) {
|
|
111
|
+
return 'session';
|
|
112
|
+
}
|
|
113
|
+
if (SQLITE_SIDECAR_SUFFIXES.some((suffix) => name.endsWith(suffix))) {
|
|
114
|
+
return 'sqlite-sidecar';
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* True when a file starts with the SQLite 3 header.
|
|
121
|
+
*
|
|
122
|
+
* @param {string} filePath
|
|
123
|
+
* @returns {Promise<boolean>}
|
|
124
|
+
*/
|
|
125
|
+
export async function isSqliteFile(filePath) {
|
|
126
|
+
const handle = await open(filePath, 'r');
|
|
127
|
+
try {
|
|
128
|
+
const header = Buffer.alloc(SQLITE_HEADER.length);
|
|
129
|
+
const { bytesRead } = await handle.read(header, 0, header.length, 0);
|
|
130
|
+
return bytesRead === header.length && header.equals(SQLITE_HEADER);
|
|
131
|
+
} finally {
|
|
132
|
+
await handle.close();
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function assertProfileName(profile) {
|
|
137
|
+
if (
|
|
138
|
+
typeof profile !== 'string' ||
|
|
139
|
+
profile === '' ||
|
|
140
|
+
profile === '.' ||
|
|
141
|
+
profile === '..' ||
|
|
142
|
+
/[/\\]/u.test(profile)
|
|
143
|
+
) {
|
|
144
|
+
throw new TypeError(
|
|
145
|
+
`profile must be a profile directory name such as "Default" or "Profile 1", got ${JSON.stringify(profile)}`
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function isInside(child, parent) {
|
|
151
|
+
const relative = path.relative(path.resolve(parent), path.resolve(child));
|
|
152
|
+
return (
|
|
153
|
+
relative === '' ||
|
|
154
|
+
(!relative.startsWith('..') && !path.isAbsolute(relative))
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function prepareTarget(to, sourceRoot) {
|
|
159
|
+
if (!to) {
|
|
160
|
+
return await createTemporaryUserDataDir();
|
|
161
|
+
}
|
|
162
|
+
if (isInside(to, sourceRoot)) {
|
|
163
|
+
throw new Error(
|
|
164
|
+
`The snapshot target ${to} is inside the source ${sourceRoot}`
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
await mkdir(to, { recursive: true });
|
|
168
|
+
if ((await readdir(to)).length > 0) {
|
|
169
|
+
throw new Error(`The snapshot target ${to} is not empty`);
|
|
170
|
+
}
|
|
171
|
+
return to;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Copy a database snapshot to its place in the target. The Online Backup API
|
|
176
|
+
* snapshot is a single self-contained file; the locked-file fallback is a copy
|
|
177
|
+
* with its `-wal`/`-journal` sidecars, which is folded into one file with
|
|
178
|
+
* another backup (of the temporary copy, never of the source).
|
|
179
|
+
*/
|
|
180
|
+
async function copyDatabase(snapshotPath, destination) {
|
|
181
|
+
const hasSidecar =
|
|
182
|
+
(await pathExists(`${snapshotPath}-wal`)) ||
|
|
183
|
+
(await pathExists(`${snapshotPath}-journal`));
|
|
184
|
+
if (!hasSidecar) {
|
|
185
|
+
await copyFile(snapshotPath, destination);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const database = new BetterSqlite3(snapshotPath, { fileMustExist: true });
|
|
189
|
+
try {
|
|
190
|
+
await database.backup(destination);
|
|
191
|
+
} finally {
|
|
192
|
+
database.close();
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Copy the directory tree below one entry, recording the outcome. */
|
|
197
|
+
class SnapshotCopier {
|
|
198
|
+
constructor(sourceRoot, target) {
|
|
199
|
+
this.sourceRoot = sourceRoot;
|
|
200
|
+
this.target = target;
|
|
201
|
+
this.copied = { files: 0, databases: 0 };
|
|
202
|
+
this.skipped = [];
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
skip(item, reason, detail) {
|
|
206
|
+
this.skipped.push(detail ? { item, reason, detail } : { item, reason });
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async copyFile(relativePath) {
|
|
210
|
+
const source = path.join(this.sourceRoot, ...relativePath.split('/'));
|
|
211
|
+
const destination = path.join(this.target, ...relativePath.split('/'));
|
|
212
|
+
try {
|
|
213
|
+
if (await isSqliteFile(source)) {
|
|
214
|
+
await withDatabaseSnapshot({
|
|
215
|
+
sourcePath: source,
|
|
216
|
+
read: (snapshotPath) => copyDatabase(snapshotPath, destination),
|
|
217
|
+
});
|
|
218
|
+
this.copied.databases += 1;
|
|
219
|
+
} else {
|
|
220
|
+
await copyFile(source, destination);
|
|
221
|
+
this.copied.files += 1;
|
|
222
|
+
}
|
|
223
|
+
} catch (error) {
|
|
224
|
+
this.skip(relativePath, 'unreadable', error.message);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async copyDirectory(relativePath) {
|
|
229
|
+
let entries;
|
|
230
|
+
try {
|
|
231
|
+
entries = await readdir(
|
|
232
|
+
path.join(this.sourceRoot, ...relativePath.split('/')),
|
|
233
|
+
{ withFileTypes: true }
|
|
234
|
+
);
|
|
235
|
+
} catch (error) {
|
|
236
|
+
this.skip(relativePath, 'unreadable', error.message);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
await mkdir(path.join(this.target, ...relativePath.split('/')), {
|
|
240
|
+
recursive: true,
|
|
241
|
+
});
|
|
242
|
+
entries.sort((left, right) => left.name.localeCompare(right.name));
|
|
243
|
+
for (const entry of entries) {
|
|
244
|
+
await this.copyEntry(`${relativePath}/${entry.name}`, entry);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async copyEntry(relativePath, entry) {
|
|
249
|
+
const reason = snapshotSkipReason(relativePath);
|
|
250
|
+
if (reason) {
|
|
251
|
+
this.skip(relativePath, reason);
|
|
252
|
+
} else if (entry.isSymbolicLink()) {
|
|
253
|
+
this.skip(relativePath, 'symlink');
|
|
254
|
+
} else if (entry.isDirectory()) {
|
|
255
|
+
await this.copyDirectory(relativePath);
|
|
256
|
+
} else if (entry.isFile()) {
|
|
257
|
+
await this.copyFile(relativePath);
|
|
258
|
+
} else {
|
|
259
|
+
this.skip(relativePath, 'special-file');
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Mark the copied profile as cleanly exited, so the copy of a running browser
|
|
266
|
+
* does not offer to restore pages. The source is never touched.
|
|
267
|
+
*/
|
|
268
|
+
async function markExitedCleanly(profileDir) {
|
|
269
|
+
const preferencesPath = path.join(profileDir, 'Preferences');
|
|
270
|
+
let preferences;
|
|
271
|
+
try {
|
|
272
|
+
preferences = JSON.parse(await readFile(preferencesPath, 'utf8'));
|
|
273
|
+
} catch {
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
if (!preferences || typeof preferences !== 'object') {
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
preferences.profile = {
|
|
280
|
+
...(preferences.profile ?? {}),
|
|
281
|
+
exit_type: 'Normal',
|
|
282
|
+
exited_cleanly: true,
|
|
283
|
+
};
|
|
284
|
+
await writeFile(preferencesPath, JSON.stringify(preferences));
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Copy a real Chromium profile into a user data directory that can be
|
|
289
|
+
* launched with remote debugging.
|
|
290
|
+
*
|
|
291
|
+
* @param {Object} options
|
|
292
|
+
* @param {string} options.browser - chrome, chromium, edge (msedge) or brave
|
|
293
|
+
* @param {string} [options.profile='Default'] - Profile directory name
|
|
294
|
+
* @param {string} [options.userDataDir] - Source user data directory; defaults to the browser's real one
|
|
295
|
+
* @param {string} [options.to] - Target user data directory (created; must be empty); defaults to a new temporary one
|
|
296
|
+
* @param {string} [options.platform=process.platform]
|
|
297
|
+
* @param {string} [options.homeDir=os.homedir()]
|
|
298
|
+
* @param {Object<string,string>} [options.environment=process.env]
|
|
299
|
+
* @returns {Promise<{source: {browser: string, profile: string, userDataDir: string}, target: string, copied: {files: number, databases: number}, skipped: Array<{item: string, reason: string, detail: (string|undefined)}>, warnings: Array<{item: string, reason: string, detail: string}>}>}
|
|
300
|
+
*/
|
|
301
|
+
export async function snapshotUserDataDir({
|
|
302
|
+
browser,
|
|
303
|
+
profile = 'Default',
|
|
304
|
+
userDataDir,
|
|
305
|
+
to,
|
|
306
|
+
platform = process.platform,
|
|
307
|
+
homeDir = os.homedir(),
|
|
308
|
+
environment = process.env,
|
|
309
|
+
} = {}) {
|
|
310
|
+
if (!browser) {
|
|
311
|
+
throw new TypeError('snapshotUserDataDir requires a browser');
|
|
312
|
+
}
|
|
313
|
+
const normalizedBrowser = normalizeCookieBrowser(browser);
|
|
314
|
+
if (normalizedBrowser === 'firefox') {
|
|
315
|
+
throw new Error(
|
|
316
|
+
'A profile snapshot is only supported for Chromium-family browsers (chrome, chromium, edge, brave): Firefox cannot be driven over CDP by this launcher'
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
assertProfileName(profile);
|
|
320
|
+
const sourceRoot =
|
|
321
|
+
userDataDir ??
|
|
322
|
+
browserProfileRoot(normalizedBrowser, { platform, homeDir, environment });
|
|
323
|
+
const sourceProfile = path.join(sourceRoot, profile);
|
|
324
|
+
const profileStat = await stat(sourceProfile).catch(() => null);
|
|
325
|
+
if (!profileStat?.isDirectory()) {
|
|
326
|
+
throw new Error(
|
|
327
|
+
`No ${normalizedBrowser} profile "${profile}" found at ${sourceProfile}`
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const target = await prepareTarget(to, sourceRoot);
|
|
332
|
+
try {
|
|
333
|
+
return await copyProfile({
|
|
334
|
+
sourceRoot,
|
|
335
|
+
profile,
|
|
336
|
+
target,
|
|
337
|
+
source: { browser: normalizedBrowser, profile, userDataDir: sourceRoot },
|
|
338
|
+
});
|
|
339
|
+
} catch (error) {
|
|
340
|
+
if (!to) {
|
|
341
|
+
await removeUserDataDir(target);
|
|
342
|
+
}
|
|
343
|
+
throw error;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
async function copyProfile({ sourceRoot, profile, target, source }) {
|
|
348
|
+
const copier = new SnapshotCopier(sourceRoot, target);
|
|
349
|
+
const warnings = [];
|
|
350
|
+
const localState = await stat(path.join(sourceRoot, LOCAL_STATE_FILE)).catch(
|
|
351
|
+
() => null
|
|
352
|
+
);
|
|
353
|
+
if (localState?.isFile()) {
|
|
354
|
+
await copier.copyFile(LOCAL_STATE_FILE);
|
|
355
|
+
} else {
|
|
356
|
+
warnings.push({
|
|
357
|
+
item: LOCAL_STATE_FILE,
|
|
358
|
+
reason: 'local-state-missing',
|
|
359
|
+
detail:
|
|
360
|
+
'The source has no Local State; on Windows it holds the key cookies and passwords are encrypted with, so they cannot be decrypted in the copy.',
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
await copier.copyDirectory(profile);
|
|
364
|
+
await prepareUserDataDir(target);
|
|
365
|
+
await markExitedCleanly(path.join(target, profile));
|
|
366
|
+
|
|
367
|
+
return {
|
|
368
|
+
source,
|
|
369
|
+
target,
|
|
370
|
+
copied: copier.copied,
|
|
371
|
+
skipped: copier.skipped,
|
|
372
|
+
warnings,
|
|
373
|
+
};
|
|
374
|
+
}
|