browser-commander 0.21.0 → 0.21.2
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/.jscpd.json +2 -1
- package/.prettierignore +1 -0
- package/CHANGELOG.md +20 -0
- package/package.json +6 -3
- package/src/browser/attach/snapshot.js +4 -4
- package/src/browser/browser-cookie-cache.js +14 -2
- package/src/browser/profile-directory.js +109 -6
- package/src/browser/real-browser.js +54 -16
- package/src/cli/args.js +47 -0
- package/src/cli/sessions.js +4 -0
- package/src/traces/bundle.js +3 -2
- package/src/traces/observers.js +14 -5
- package/src/traces/recorder.js +3 -0
- package/src/traces/viewer.js +16 -8
- package/tests/e2e/playwright.e2e.test.js +1 -0
- package/tests/e2e/puppeteer.e2e.test.js +1 -0
- package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0001.html +2 -0
- package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0001.png +1 -0
- package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0001.state.json +54 -0
- package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0002.html +2 -0
- package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0002.png +1 -0
- package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0002.state.json +60 -0
- package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0003.html +2 -0
- package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0003.png +1 -0
- package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0003.state.json +60 -0
- package/tests/fixtures/traces/conformance/expected/bundle/events.ndjson +18 -0
- package/tests/fixtures/traces/conformance/expected/bundle/manifest.json +77 -0
- package/tests/fixtures/traces/conformance/expected/bundle/mutations/0001.ndjson +2 -0
- package/tests/fixtures/traces/conformance/expected/bundle/mutations/0002.ndjson +1 -0
- package/tests/fixtures/traces/conformance/expected/bundle/viewer.html +309 -0
- package/tests/fixtures/traces/conformance/expected/redaction.json +86 -0
- package/tests/fixtures/traces/conformance/expected/trace.lino +27 -0
- package/tests/fixtures/traces/conformance/scenario.json +249 -0
- package/tests/unit/browser/attach/extension-relay.test.js +1 -1
- package/tests/unit/browser/attach/snapshot-launch.test.js +10 -1
- package/tests/unit/browser/attach/snapshot.test.js +23 -4
- package/tests/unit/browser/browser-cookie-lock.test.js +102 -0
- package/tests/unit/browser/launch-building-blocks.test.js +127 -4
- package/tests/unit/browser/migration/index.test.js +1 -1
- package/tests/unit/browser/open-in-user-browser.test.js +1 -1
- package/tests/unit/browser/parity.test.js +1 -1
- package/tests/unit/browser/real-browser.test.js +1 -1
- package/tests/unit/browser/storage-state.test.js +2 -0
- package/tests/unit/cli/main.test.js +30 -2
- package/tests/unit/core/navigation-manager.test.js +11 -1
- package/tests/unit/playwright-protocol-coverage.test.js +231 -0
- package/tests/unit/puppeteer-bindings-coverage.test.js +214 -0
- package/tests/unit/scripts/ci-timeout-budgets.test.js +23 -0
- package/tests/unit/scripts/feature-parity.test.js +111 -5
- package/tests/unit/traces/conformance.test.js +92 -0
- package/tests/unit/traces/reader.test.js +1 -1
- package/tests/unit/traces/recorder.test.js +1 -1
- package/tests/unit/traces/viewer.test.js +78 -0
package/.jscpd.json
CHANGED
package/.prettierignore
CHANGED
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.21.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 917d611: Keep the real-browser launcher under the ESLint complexity limit, fail `npm run lint` on any warning, and record that `node-pty`'s install script (pulled in by `command-stream`) stays blocked.
|
|
8
|
+
|
|
9
|
+
## 0.21.1
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 4caa385: Suppress Chromium's default-browser prompt through profile settings on fresh and snapshot launches, and expose configurable Preferences and Local State values.
|
|
14
|
+
|
|
15
|
+
Update the locked brace-expansion dependency to 5.0.12 to resolve its CPU and stack exhaustion advisories.
|
|
16
|
+
|
|
17
|
+
Retry transient Windows credential-lock EPERM within the existing bounded deadline, preserving the underlying error when contention persists.
|
|
18
|
+
|
|
19
|
+
Accept a `monotonic` clock in `startTrace` and use the injected `now` for interaction durations, so a trace recorded with a fixed clock is byte-for-byte reproducible; the Python and Rust recorders are checked against such a recording.
|
|
20
|
+
|
|
21
|
+
Escape recorded text in the offline trace viewer. A recorded page's URL, title, form control values and event fields were inserted into the viewer as HTML, so a page could run script in the viewer of whoever opened its trace.
|
|
22
|
+
|
|
3
23
|
## 0.21.0
|
|
4
24
|
|
|
5
25
|
### Minor Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "browser-commander",
|
|
3
|
-
"version": "0.21.
|
|
3
|
+
"version": "0.21.2",
|
|
4
4
|
"description": "Universal browser automation library that supports both Playwright and Puppeteer with a unified API",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"test:e2e:api-coverage": "node scripts/run-tests.mjs --env RUN_E2E=true tests/e2e/api-coverage.e2e.test.js",
|
|
28
28
|
"test:e2e:google-probe": "node scripts/run-tests.mjs --env RUN_E2E=true --env RUN_GOOGLE_PROBE=true tests/e2e/google-signin-probe.e2e.test.js",
|
|
29
29
|
"test:all": "npm run test:unit && npm run test:e2e",
|
|
30
|
-
"lint": "eslint .",
|
|
30
|
+
"lint": "eslint . --max-warnings 0",
|
|
31
31
|
"lint:fix": "eslint . --fix",
|
|
32
32
|
"format": "prettier --write .",
|
|
33
33
|
"format:check": "prettier --check .",
|
|
@@ -97,10 +97,13 @@
|
|
|
97
97
|
"playwright": "^1.62.1",
|
|
98
98
|
"prettier": "^3.9.6",
|
|
99
99
|
"puppeteer": "^25.10.0",
|
|
100
|
-
"selenium-webdriver": "^4.49.0"
|
|
100
|
+
"selenium-webdriver": "^4.49.0",
|
|
101
|
+
"typescript": "^5.9.3",
|
|
102
|
+
"yaml": "^2.9.1"
|
|
101
103
|
},
|
|
102
104
|
"allowScripts": {
|
|
103
105
|
"better-sqlite3@13.0.3": true,
|
|
106
|
+
"node-pty": false,
|
|
104
107
|
"puppeteer": false
|
|
105
108
|
}
|
|
106
109
|
}
|
|
@@ -155,9 +155,9 @@ function isInside(child, parent) {
|
|
|
155
155
|
);
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
-
async function prepareTarget(to, sourceRoot) {
|
|
158
|
+
async function prepareTarget(to, sourceRoot, profile) {
|
|
159
159
|
if (!to) {
|
|
160
|
-
return await createTemporaryUserDataDir();
|
|
160
|
+
return await createTemporaryUserDataDir({ profileDirectory: profile });
|
|
161
161
|
}
|
|
162
162
|
if (isInside(to, sourceRoot)) {
|
|
163
163
|
throw new Error(
|
|
@@ -328,7 +328,7 @@ export async function snapshotUserDataDir({
|
|
|
328
328
|
);
|
|
329
329
|
}
|
|
330
330
|
|
|
331
|
-
const target = await prepareTarget(to, sourceRoot);
|
|
331
|
+
const target = await prepareTarget(to, sourceRoot, profile);
|
|
332
332
|
try {
|
|
333
333
|
return await copyProfile({
|
|
334
334
|
sourceRoot,
|
|
@@ -361,7 +361,7 @@ async function copyProfile({ sourceRoot, profile, target, source }) {
|
|
|
361
361
|
});
|
|
362
362
|
}
|
|
363
363
|
await copier.copyDirectory(profile);
|
|
364
|
-
await prepareUserDataDir(target);
|
|
364
|
+
await prepareUserDataDir(target, { profileDirectory: profile });
|
|
365
365
|
await markExitedCleanly(path.join(target, profile));
|
|
366
366
|
|
|
367
367
|
return {
|
|
@@ -162,13 +162,21 @@ async function wait(milliseconds) {
|
|
|
162
162
|
|
|
163
163
|
async function acquireCredentialLock(lockPath, cachedPath, options) {
|
|
164
164
|
const startedAt = options.now();
|
|
165
|
+
let contention;
|
|
165
166
|
while (options.now() - startedAt <= LOCK_WAIT_MILLISECONDS) {
|
|
166
167
|
try {
|
|
167
168
|
return await open(lockPath, 'wx', 0o600);
|
|
168
169
|
} catch (error) {
|
|
169
|
-
|
|
170
|
+
// Windows can report EPERM while another process's unlinked lock is
|
|
171
|
+
// still pending deletion. Retry within the same bounded lock deadline.
|
|
172
|
+
// Preserve the original error if it proves to be permanent.
|
|
173
|
+
if (
|
|
174
|
+
error.code !== 'EEXIST' &&
|
|
175
|
+
!(options.platform === 'win32' && error.code === 'EPERM')
|
|
176
|
+
) {
|
|
170
177
|
throw error;
|
|
171
178
|
}
|
|
179
|
+
contention = error;
|
|
172
180
|
const cached = await readFreshJson(
|
|
173
181
|
cachedPath,
|
|
174
182
|
options.ttlSeconds,
|
|
@@ -184,7 +192,9 @@ async function acquireCredentialLock(lockPath, cachedPath, options) {
|
|
|
184
192
|
await wait(50);
|
|
185
193
|
}
|
|
186
194
|
}
|
|
187
|
-
throw new Error('timed out waiting for another cookie credential reader'
|
|
195
|
+
throw new Error('timed out waiting for another cookie credential reader', {
|
|
196
|
+
cause: contention,
|
|
197
|
+
});
|
|
188
198
|
}
|
|
189
199
|
|
|
190
200
|
async function loadOrCreateCredential({
|
|
@@ -194,6 +204,7 @@ async function loadOrCreateCredential({
|
|
|
194
204
|
create,
|
|
195
205
|
now,
|
|
196
206
|
metadata,
|
|
207
|
+
platform = process.platform,
|
|
197
208
|
}) {
|
|
198
209
|
if (!cache.enabled) {
|
|
199
210
|
return { key: Buffer.from(await create()), savedAt: now() / 1000 };
|
|
@@ -214,6 +225,7 @@ async function loadOrCreateCredential({
|
|
|
214
225
|
now,
|
|
215
226
|
refresh,
|
|
216
227
|
ttlSeconds: cache.ttlSeconds,
|
|
228
|
+
platform,
|
|
217
229
|
});
|
|
218
230
|
if (acquired.cached) {
|
|
219
231
|
return {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
1
|
+
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
|
|
@@ -14,6 +14,7 @@ export const FIRST_RUN_SENTINEL = 'First Run';
|
|
|
14
14
|
|
|
15
15
|
/** Chrome's profile-wide settings file, next to the profile directories. */
|
|
16
16
|
export const LOCAL_STATE_FILE = 'Local State';
|
|
17
|
+
export const PREFERENCES_FILE = path.join('Default', 'Preferences');
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* Local State written into a brand-new user data directory.
|
|
@@ -51,11 +52,111 @@ export const TEMPORARY_PROFILE_PREFIX = 'browser-commander-profile-';
|
|
|
51
52
|
* @param {string} userDataDir
|
|
52
53
|
* @returns {Promise<string>} The same directory
|
|
53
54
|
*/
|
|
54
|
-
|
|
55
|
+
function assertObject(value, name) {
|
|
56
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
57
|
+
throw new TypeError(`${name} must be a JSON object`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function deepMerge(target, source) {
|
|
62
|
+
for (const [key, value] of Object.entries(source)) {
|
|
63
|
+
if (['__proto__', 'prototype', 'constructor'].includes(key)) {
|
|
64
|
+
throw new TypeError(`profile setting key ${key} is not allowed`);
|
|
65
|
+
}
|
|
66
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
67
|
+
target[key] = deepMerge(
|
|
68
|
+
target[key] &&
|
|
69
|
+
typeof target[key] === 'object' &&
|
|
70
|
+
!Array.isArray(target[key])
|
|
71
|
+
? target[key]
|
|
72
|
+
: {},
|
|
73
|
+
value
|
|
74
|
+
);
|
|
75
|
+
} else {
|
|
76
|
+
target[key] = value;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return target;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function mergeJsonFile(file, defaults, overrides) {
|
|
83
|
+
let current = {};
|
|
84
|
+
try {
|
|
85
|
+
current = JSON.parse(await readFile(file, 'utf8'));
|
|
86
|
+
assertObject(current, file);
|
|
87
|
+
} catch (error) {
|
|
88
|
+
if (error.code !== 'ENOENT') {
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
const merged = deepMerge(deepMerge(current, defaults), overrides);
|
|
93
|
+
await mkdir(path.dirname(file), { recursive: true });
|
|
94
|
+
await writeFile(file, JSON.stringify(merged));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Merge launch settings into a fresh, migrated, or snapshotted profile. */
|
|
98
|
+
export async function configureUserDataDir(
|
|
99
|
+
userDataDir,
|
|
100
|
+
{
|
|
101
|
+
defaultBrowserCheck,
|
|
102
|
+
preferences = {},
|
|
103
|
+
localState = {},
|
|
104
|
+
profileDirectory = 'Default',
|
|
105
|
+
} = {}
|
|
106
|
+
) {
|
|
107
|
+
assertObject(preferences, 'preferences');
|
|
108
|
+
assertObject(localState, 'localState');
|
|
109
|
+
if (
|
|
110
|
+
defaultBrowserCheck !== undefined &&
|
|
111
|
+
typeof defaultBrowserCheck !== 'boolean'
|
|
112
|
+
) {
|
|
113
|
+
throw new TypeError('defaultBrowserCheck must be a boolean');
|
|
114
|
+
}
|
|
115
|
+
if (
|
|
116
|
+
typeof profileDirectory !== 'string' ||
|
|
117
|
+
!profileDirectory ||
|
|
118
|
+
path.basename(profileDirectory) !== profileDirectory ||
|
|
119
|
+
profileDirectory === '.' ||
|
|
120
|
+
profileDirectory === '..'
|
|
121
|
+
) {
|
|
122
|
+
throw new TypeError('profileDirectory must be one profile directory name');
|
|
123
|
+
}
|
|
124
|
+
const profilePreferences = deepMerge(
|
|
125
|
+
{ browser: { check_default_browser: false } },
|
|
126
|
+
preferences
|
|
127
|
+
);
|
|
128
|
+
assertObject(profilePreferences.browser, 'preferences.browser');
|
|
129
|
+
if (defaultBrowserCheck !== undefined) {
|
|
130
|
+
profilePreferences.browser.check_default_browser = defaultBrowserCheck;
|
|
131
|
+
}
|
|
132
|
+
await mergeJsonFile(
|
|
133
|
+
path.join(userDataDir, profileDirectory, 'Preferences'),
|
|
134
|
+
{},
|
|
135
|
+
profilePreferences
|
|
136
|
+
);
|
|
137
|
+
const check = profilePreferences.browser?.check_default_browser === true;
|
|
138
|
+
await mergeJsonFile(
|
|
139
|
+
path.join(userDataDir, LOCAL_STATE_FILE),
|
|
140
|
+
{
|
|
141
|
+
browser: {
|
|
142
|
+
default_browser_infobar_declined_count: check ? 0 : 5,
|
|
143
|
+
default_browser_declined_count: check ? 0 : 5,
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
localState
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export async function prepareUserDataDir(userDataDir, options = {}) {
|
|
151
|
+
if (options.firstRun !== undefined && typeof options.firstRun !== 'boolean') {
|
|
152
|
+
throw new TypeError('firstRun must be a boolean');
|
|
153
|
+
}
|
|
55
154
|
await mkdir(userDataDir, { recursive: true });
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
155
|
+
if (options.firstRun !== true) {
|
|
156
|
+
await writeFile(path.join(userDataDir, FIRST_RUN_SENTINEL), '', {
|
|
157
|
+
flag: 'a',
|
|
158
|
+
});
|
|
159
|
+
}
|
|
59
160
|
try {
|
|
60
161
|
await writeFile(
|
|
61
162
|
path.join(userDataDir, LOCAL_STATE_FILE),
|
|
@@ -67,6 +168,7 @@ export async function prepareUserDataDir(userDataDir) {
|
|
|
67
168
|
throw error;
|
|
68
169
|
}
|
|
69
170
|
}
|
|
171
|
+
await configureUserDataDir(userDataDir, options);
|
|
70
172
|
return userDataDir;
|
|
71
173
|
}
|
|
72
174
|
|
|
@@ -79,11 +181,12 @@ export async function prepareUserDataDir(userDataDir) {
|
|
|
79
181
|
*/
|
|
80
182
|
export async function createTemporaryUserDataDir({
|
|
81
183
|
parent = os.tmpdir(),
|
|
184
|
+
...options
|
|
82
185
|
} = {}) {
|
|
83
186
|
const userDataDir = await mkdtemp(
|
|
84
187
|
path.join(parent, TEMPORARY_PROFILE_PREFIX)
|
|
85
188
|
);
|
|
86
|
-
return await prepareUserDataDir(userDataDir);
|
|
189
|
+
return await prepareUserDataDir(userDataDir, options);
|
|
87
190
|
}
|
|
88
191
|
|
|
89
192
|
/**
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
watchDevToolsOutput,
|
|
22
22
|
} from './debugging-port.js';
|
|
23
23
|
import {
|
|
24
|
+
configureUserDataDir,
|
|
24
25
|
createTemporaryUserDataDir,
|
|
25
26
|
prepareUserDataDir,
|
|
26
27
|
removeUserDataDir,
|
|
@@ -324,6 +325,9 @@ function createCloser({
|
|
|
324
325
|
* @param {string[]} [options.restrictions] - Opt-in restrictions from launch-restrictions.json, such as 'no-extensions'
|
|
325
326
|
* @param {string[]} [options.args] - Additional browser arguments
|
|
326
327
|
* @param {string[]} [options.extraArgs] - Additional browser arguments appended after args
|
|
328
|
+
* @param {boolean} [options.defaultBrowserCheck=false] - Allow the disposable browser to ask to become the default
|
|
329
|
+
* @param {Object} [options.preferences] - Deep-merged into Default/Preferences before launch
|
|
330
|
+
* @param {Object} [options.localState] - Deep-merged into Local State before launch
|
|
327
331
|
* @param {Object<string,string>} [options.env] - Extra environment for the browser process only
|
|
328
332
|
* @param {boolean} [options.automationParity=true] - Add the AutomationControlled off switch when a custom argument turns the feature on
|
|
329
333
|
* @param {number} [options.startupTimeout=30000] - CDP readiness timeout in milliseconds
|
|
@@ -455,6 +459,7 @@ async function prepareProfile({
|
|
|
455
459
|
requestedUserDataDir,
|
|
456
460
|
argOptions,
|
|
457
461
|
snapshot,
|
|
462
|
+
firstRun,
|
|
458
463
|
}) {
|
|
459
464
|
if (attach) {
|
|
460
465
|
const snapshotLaunch = await prepareSnapshotLaunch({
|
|
@@ -469,8 +474,8 @@ async function prepareProfile({
|
|
|
469
474
|
}
|
|
470
475
|
return {
|
|
471
476
|
userDataDir: requestedUserDataDir
|
|
472
|
-
? await prepareUserDataDir(requestedUserDataDir)
|
|
473
|
-
: await createTemporaryUserDataDir(),
|
|
477
|
+
? await prepareUserDataDir(requestedUserDataDir, { firstRun })
|
|
478
|
+
: await createTemporaryUserDataDir({ firstRun }),
|
|
474
479
|
};
|
|
475
480
|
}
|
|
476
481
|
|
|
@@ -516,6 +521,28 @@ async function runPreLaunchMigration({
|
|
|
516
521
|
}
|
|
517
522
|
}
|
|
518
523
|
|
|
524
|
+
function mergeSeedCookies(seedCookies, migratedCookies) {
|
|
525
|
+
return migratedCookies.length > 0
|
|
526
|
+
? [...(seedCookies ?? []), ...migratedCookies]
|
|
527
|
+
: seedCookies;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Runs a launch step and, if it throws, removes the profile this launch
|
|
531
|
+
// created before rethrowing. A caller-supplied profile is never removed.
|
|
532
|
+
async function removingTemporaryProfileOnError(
|
|
533
|
+
{ userDataDir, temporaryProfile },
|
|
534
|
+
step
|
|
535
|
+
) {
|
|
536
|
+
try {
|
|
537
|
+
return await step();
|
|
538
|
+
} catch (error) {
|
|
539
|
+
if (temporaryProfile) {
|
|
540
|
+
await removeUserDataDir(userDataDir);
|
|
541
|
+
}
|
|
542
|
+
throw error;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
519
546
|
export async function launchAndConnectRealBrowserWithDependencies(
|
|
520
547
|
options = {},
|
|
521
548
|
dependencies = {}
|
|
@@ -541,6 +568,10 @@ export async function launchAndConnectRealBrowserWithDependencies(
|
|
|
541
568
|
wsEndpoint,
|
|
542
569
|
migrateFrom,
|
|
543
570
|
attach,
|
|
571
|
+
defaultBrowserCheck,
|
|
572
|
+
preferences,
|
|
573
|
+
localState,
|
|
574
|
+
firstRun,
|
|
544
575
|
...connectionOptions
|
|
545
576
|
} = options;
|
|
546
577
|
const argOptions = {
|
|
@@ -573,6 +604,7 @@ export async function launchAndConnectRealBrowserWithDependencies(
|
|
|
573
604
|
requestedUserDataDir,
|
|
574
605
|
argOptions,
|
|
575
606
|
snapshot: dependencies.snapshotUserDataDir,
|
|
607
|
+
firstRun,
|
|
576
608
|
});
|
|
577
609
|
const childEnv = browserEnvironment(restrictions, env);
|
|
578
610
|
|
|
@@ -584,9 +616,20 @@ export async function launchAndConnectRealBrowserWithDependencies(
|
|
|
584
616
|
migrate: dependencies.migrateProfile ?? migrateProfile,
|
|
585
617
|
});
|
|
586
618
|
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
619
|
+
// Migration and snapshot attach can copy a preference that enables the
|
|
620
|
+
// default-browser infobar, so apply launch settings after either copy.
|
|
621
|
+
const profile = { userDataDir, temporaryProfile };
|
|
622
|
+
await removingTemporaryProfileOnError(profile, () =>
|
|
623
|
+
configureUserDataDir(userDataDir, {
|
|
624
|
+
defaultBrowserCheck,
|
|
625
|
+
preferences,
|
|
626
|
+
localState,
|
|
627
|
+
profileDirectory: attach?.profile ?? 'Default',
|
|
628
|
+
})
|
|
629
|
+
);
|
|
630
|
+
|
|
631
|
+
const launched = await removingTemporaryProfileOnError(profile, () =>
|
|
632
|
+
spawnOnFreePort({
|
|
590
633
|
requestedPort: remoteDebuggingPort,
|
|
591
634
|
portAttempts,
|
|
592
635
|
executablePath,
|
|
@@ -596,13 +639,8 @@ export async function launchAndConnectRealBrowserWithDependencies(
|
|
|
596
639
|
userDataDir,
|
|
597
640
|
startupTimeout,
|
|
598
641
|
dependencies,
|
|
599
|
-
})
|
|
600
|
-
|
|
601
|
-
if (temporaryProfile) {
|
|
602
|
-
await removeUserDataDir(userDataDir);
|
|
603
|
-
}
|
|
604
|
-
throw error;
|
|
605
|
-
}
|
|
642
|
+
})
|
|
643
|
+
);
|
|
606
644
|
const { browserProcess, cdpEndpoint: resolvedCdpEndpoint } = launched;
|
|
607
645
|
if (temporaryProfile) {
|
|
608
646
|
// The profile goes away with the browser, also when the user closes the
|
|
@@ -614,10 +652,10 @@ export async function launchAndConnectRealBrowserWithDependencies(
|
|
|
614
652
|
|
|
615
653
|
try {
|
|
616
654
|
const connect = dependencies.connect ?? connectBrowser;
|
|
617
|
-
const seedCookies =
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
655
|
+
const seedCookies = mergeSeedCookies(
|
|
656
|
+
connectionOptions.seedCookies,
|
|
657
|
+
migratedCookies
|
|
658
|
+
);
|
|
621
659
|
const connection = await connect({
|
|
622
660
|
engine,
|
|
623
661
|
cdpEndpoint: resolvedCdpEndpoint,
|
package/src/cli/args.js
CHANGED
|
@@ -27,6 +27,10 @@ const BROWSER_OPTIONS = Object.freeze({
|
|
|
27
27
|
launch: STRING,
|
|
28
28
|
restriction: STRINGS,
|
|
29
29
|
arg: STRINGS,
|
|
30
|
+
pref: STRINGS,
|
|
31
|
+
'local-state': STRINGS,
|
|
32
|
+
'default-browser-check': FLAG,
|
|
33
|
+
'first-run': FLAG,
|
|
30
34
|
});
|
|
31
35
|
|
|
32
36
|
/** Options accepted by every page command. */
|
|
@@ -182,6 +186,45 @@ export function requireOption(options, name, command) {
|
|
|
182
186
|
* `--restriction` → `restrictions`), with unset options left out.
|
|
183
187
|
*/
|
|
184
188
|
export function launchParams(options) {
|
|
189
|
+
const settings = (entries, flag) => {
|
|
190
|
+
if (entries === undefined) {
|
|
191
|
+
return undefined;
|
|
192
|
+
}
|
|
193
|
+
const result = {};
|
|
194
|
+
for (const entry of entries) {
|
|
195
|
+
const separator = entry.indexOf('=');
|
|
196
|
+
const path = entry.slice(0, separator).split('.');
|
|
197
|
+
if (
|
|
198
|
+
separator < 1 ||
|
|
199
|
+
path.some(
|
|
200
|
+
(part) =>
|
|
201
|
+
!part || ['__proto__', 'prototype', 'constructor'].includes(part)
|
|
202
|
+
)
|
|
203
|
+
) {
|
|
204
|
+
throw new UsageError(`${flag} requires a dotted key=value`);
|
|
205
|
+
}
|
|
206
|
+
let value = entry.slice(separator + 1);
|
|
207
|
+
try {
|
|
208
|
+
value = JSON.parse(value);
|
|
209
|
+
} catch {
|
|
210
|
+
// Unquoted CLI text is a string.
|
|
211
|
+
}
|
|
212
|
+
let parent = result;
|
|
213
|
+
for (const part of path.slice(0, -1)) {
|
|
214
|
+
parent[part] ??= {};
|
|
215
|
+
if (
|
|
216
|
+
!parent[part] ||
|
|
217
|
+
typeof parent[part] !== 'object' ||
|
|
218
|
+
Array.isArray(parent[part])
|
|
219
|
+
) {
|
|
220
|
+
throw new UsageError(`${flag} has a conflicting key: ${entry}`);
|
|
221
|
+
}
|
|
222
|
+
parent = parent[part];
|
|
223
|
+
}
|
|
224
|
+
parent[path.at(-1)] = value;
|
|
225
|
+
}
|
|
226
|
+
return result;
|
|
227
|
+
};
|
|
185
228
|
const params = {
|
|
186
229
|
engine: options.engine,
|
|
187
230
|
browser: options.browser,
|
|
@@ -191,6 +234,10 @@ export function launchParams(options) {
|
|
|
191
234
|
launch: options.launch,
|
|
192
235
|
restrictions: options.restriction,
|
|
193
236
|
args: options.arg,
|
|
237
|
+
preferences: settings(options.pref, '--pref'),
|
|
238
|
+
localState: settings(options.localState, '--local-state'),
|
|
239
|
+
defaultBrowserCheck: options.defaultBrowserCheck,
|
|
240
|
+
firstRun: options.firstRun,
|
|
194
241
|
};
|
|
195
242
|
return Object.fromEntries(
|
|
196
243
|
Object.entries(params).filter(([, value]) => value !== undefined)
|
package/src/cli/sessions.js
CHANGED
package/src/traces/bundle.js
CHANGED
|
@@ -32,7 +32,7 @@ export const TRACE_DIRECTORY_MODE = 0o700;
|
|
|
32
32
|
/**
|
|
33
33
|
* Open a bundle for writing.
|
|
34
34
|
*
|
|
35
|
-
* @param {Object} options - `{output, limits, strict, now, onEvent}`
|
|
35
|
+
* @param {Object} options - `{output, limits, strict, now, monotonic, onEvent}`
|
|
36
36
|
* @returns {Promise<Object>} The bundle writer
|
|
37
37
|
*/
|
|
38
38
|
export async function openTraceBundle(options = {}) {
|
|
@@ -41,6 +41,7 @@ export async function openTraceBundle(options = {}) {
|
|
|
41
41
|
strict = false,
|
|
42
42
|
limits = {},
|
|
43
43
|
now = () => Date.now(),
|
|
44
|
+
monotonic = () => performance.now(),
|
|
44
45
|
onEvent = null,
|
|
45
46
|
} = options;
|
|
46
47
|
|
|
@@ -119,7 +120,7 @@ export async function openTraceBundle(options = {}) {
|
|
|
119
120
|
const record = {
|
|
120
121
|
sequence: ++sequence,
|
|
121
122
|
at: new Date(now()).toISOString(),
|
|
122
|
-
monotonicMs: Math.round(
|
|
123
|
+
monotonicMs: Math.round(monotonic()),
|
|
123
124
|
...event,
|
|
124
125
|
};
|
|
125
126
|
const line = `${JSON.stringify(record)}\n`;
|
package/src/traces/observers.js
CHANGED
|
@@ -51,7 +51,7 @@ export function interactionTarget(argument) {
|
|
|
51
51
|
/**
|
|
52
52
|
* Subscribe to everything a running trace listens to.
|
|
53
53
|
*
|
|
54
|
-
* @param {Object} options - `{commander, page, eventSources, record, note, identity}`
|
|
54
|
+
* @param {Object} options - `{commander, page, eventSources, record, note, identity, now}`
|
|
55
55
|
* @param {Object} [options.commander] - The commander being traced, when there
|
|
56
56
|
* is one; interactions and managed downloads are reached through it
|
|
57
57
|
* @param {Object} options.page - The page being traced
|
|
@@ -61,10 +61,19 @@ export function interactionTarget(argument) {
|
|
|
61
61
|
* @param {Object} [options.identity] - Trace identity, see `identity.js`; the
|
|
62
62
|
* navigation observer is what moves it on to the next navigation, and the
|
|
63
63
|
* interaction observer is what names an action (issue #93)
|
|
64
|
+
* @param {Function} [options.now] - Wall clock in milliseconds, for durations
|
|
64
65
|
* @returns {Function[]} One detach function per attached observer
|
|
65
66
|
*/
|
|
66
67
|
export function attachTimelineObservers(options) {
|
|
67
|
-
const {
|
|
68
|
+
const {
|
|
69
|
+
commander,
|
|
70
|
+
page,
|
|
71
|
+
eventSources,
|
|
72
|
+
record,
|
|
73
|
+
note,
|
|
74
|
+
identity,
|
|
75
|
+
now = () => Date.now(),
|
|
76
|
+
} = options;
|
|
68
77
|
const detachers = [];
|
|
69
78
|
|
|
70
79
|
function subscribe(source, attach) {
|
|
@@ -234,7 +243,7 @@ export function attachTimelineObservers(options) {
|
|
|
234
243
|
}
|
|
235
244
|
originals.set(name, original);
|
|
236
245
|
commander[name] = async (...args) => {
|
|
237
|
-
const startedMs =
|
|
246
|
+
const startedMs = now();
|
|
238
247
|
const target = interactionTarget(args[0]);
|
|
239
248
|
// Named before the call, so everything the action causes can be traced
|
|
240
249
|
// back to it even when the action itself ends in an exception.
|
|
@@ -245,7 +254,7 @@ export function attachTimelineObservers(options) {
|
|
|
245
254
|
actionId,
|
|
246
255
|
action: name,
|
|
247
256
|
target,
|
|
248
|
-
durationMs:
|
|
257
|
+
durationMs: now() - startedMs,
|
|
249
258
|
ok: true,
|
|
250
259
|
});
|
|
251
260
|
return result;
|
|
@@ -254,7 +263,7 @@ export function attachTimelineObservers(options) {
|
|
|
254
263
|
actionId,
|
|
255
264
|
action: name,
|
|
256
265
|
target,
|
|
257
|
-
durationMs:
|
|
266
|
+
durationMs: now() - startedMs,
|
|
258
267
|
ok: false,
|
|
259
268
|
error: error.message,
|
|
260
269
|
});
|
package/src/traces/recorder.js
CHANGED
|
@@ -110,6 +110,7 @@ export async function startTrace(options = {}) {
|
|
|
110
110
|
commanderVersion = null,
|
|
111
111
|
log = commander?.log,
|
|
112
112
|
now = () => Date.now(),
|
|
113
|
+
monotonic,
|
|
113
114
|
} = options;
|
|
114
115
|
|
|
115
116
|
if (!page) {
|
|
@@ -150,6 +151,7 @@ export async function startTrace(options = {}) {
|
|
|
150
151
|
limits,
|
|
151
152
|
strict,
|
|
152
153
|
now,
|
|
154
|
+
monotonic,
|
|
153
155
|
onEvent: (event) => linksSink?.event(event),
|
|
154
156
|
});
|
|
155
157
|
const startedAt = new Date(now()).toISOString();
|
|
@@ -323,6 +325,7 @@ export async function startTrace(options = {}) {
|
|
|
323
325
|
record,
|
|
324
326
|
note,
|
|
325
327
|
identity,
|
|
328
|
+
now,
|
|
326
329
|
});
|
|
327
330
|
|
|
328
331
|
// Registered before the first record, so a navigation that starts in the same
|