browser-commander 0.27.0 → 0.28.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 +4 -0
- package/eslint.config.js +11 -0
- package/package.json +5 -2
- package/src/browser/connector.js +117 -13
- package/src/browser/launch-options.js +2 -1
- package/src/browser/launch-restrictions.json +33 -2
- package/src/browser/launcher.js +26 -1
- package/src/browser/persistent-session.js +360 -0
- package/src/browser/profile-directory.js +20 -0
- package/src/browser/real-browser.js +7 -2
- package/src/browser/restrictions.js +13 -4
- package/src/browser/session-watchdog.js +71 -0
- package/src/browser/session-worker.js +70 -0
- package/src/browser/webdriver.js +4 -0
- package/src/capture/encoding.js +327 -0
- package/src/capture/errors.js +12 -0
- package/src/capture/files.js +9 -0
- package/src/capture/index.js +5 -0
- package/src/capture/recording.js +136 -0
- package/src/capture/screenshot.js +222 -0
- package/src/capture/video.js +186 -0
- package/src/capture/viewport.js +68 -0
- package/src/capture/worker.js +22 -0
- package/src/cli/args.js +66 -1
- package/src/cli/commands.js +113 -2
- package/src/cli/high-level-methods.js +40 -3
- package/src/cli/sessions.js +11 -3
- package/src/core/commander-features.js +105 -0
- package/src/core/constants.js +1 -0
- package/src/core/navigation-manager.js +4 -2
- package/src/core/navigation-readiness.js +30 -2
- package/src/core/navigation-safety.js +51 -0
- package/src/core/page-trigger-manager.js +115 -30
- package/src/core/readiness.js +10 -2
- package/src/elements/locators.js +26 -32
- package/src/elements/selectors.js +37 -81
- package/src/elements/visibility.js +32 -10
- package/src/factory.js +13 -12
- package/src/index.js +2 -0
- package/src/interactions/click.js +44 -0
- package/src/interactions/overlays.js +28 -0
- package/src/traces/bundle.js +14 -5
- package/src/traces/checkpoints.js +99 -0
- package/src/traces/links.js +117 -1
- package/src/traces/mutation-stream.js +1 -0
- package/src/traces/network.js +257 -0
- package/src/traces/page-capture.js +143 -52
- package/src/traces/reader.js +8 -10
- package/src/traces/recorder.js +221 -245
- package/src/traces/render.js +168 -0
- package/src/traces/rolling.js +180 -0
- package/src/traces/start-cleanup.js +15 -0
- package/src/traces/stop.js +159 -0
- package/src/traces/storage.js +43 -0
- package/src/utilities/subprocess.js +32 -0
- package/tests/e2e/cli.e2e.test.js +20 -1
- package/tests/e2e/click-readiness.e2e.test.js +1 -1
- package/tests/unit/browser/launch-building-blocks.test.js +5 -1
- package/tests/unit/browser/launcher.test.js +5 -1
- package/tests/unit/browser/persistent-session.test.js +59 -0
- package/tests/unit/browser/quiet-ui.test.js +16 -0
- package/tests/unit/browser/tab-selection.test.js +38 -0
- package/tests/unit/capture/capture.test.js +52 -0
- package/tests/unit/capture/cleanup.test.js +24 -0
- package/tests/unit/capture/viewport.test.js +168 -0
- package/tests/unit/cli/dispatcher.test.js +25 -0
- package/tests/unit/core/readiness.test.js +21 -0
- package/tests/unit/core/trigger-readiness.test.js +39 -0
- package/tests/unit/elements/visible-count.test.js +24 -0
- package/tests/unit/elements/wait-navigation.test.js +26 -0
- package/tests/unit/interactions/navigation-timeout.test.js +39 -0
- package/tests/unit/scripts/ci-timeout-budgets.test.js +1 -0
- package/tests/unit/traces/bundle.test.js +16 -0
- package/tests/unit/traces/network.test.js +97 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.28.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- cc2a209: Add portable screenshots, bounded recordings and GIF/APNG/WebP encoding, trace
|
|
8
|
+
rendering and summaries, full/text DOM links, bounded redacted network/HAR,
|
|
9
|
+
continuous trace rotation/gzip, and detached reusable browser sessions with idle
|
|
10
|
+
shutdown. Add early trigger readiness, explicit trigger concurrency, visible
|
|
11
|
+
counts and configured overlay dismissal. Fix navigation-interrupted waits,
|
|
12
|
+
Trusted Types capture, Chrome panel restrictions and deterministic tab selection.
|
|
13
|
+
Close ordinary launched CLI browsers even when their connection supports detach;
|
|
14
|
+
only explicitly kept-open sessions survive dispatcher cleanup.
|
|
15
|
+
Add stable viewport capture with native-view CDP, viewport-only engine fallbacks
|
|
16
|
+
and displayed-window regression coverage; document full-page compositor effects.
|
|
17
|
+
Preserve readiness timer expiry when the rounded clock still reports budget.
|
|
18
|
+
|
|
3
19
|
## 0.27.0
|
|
4
20
|
|
|
5
21
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -802,6 +802,10 @@ failed or cancelled download raises the failure rather than returning a path.
|
|
|
802
802
|
|
|
803
803
|
### Portable Traces
|
|
804
804
|
|
|
805
|
+
See [capture, debugging and reusable sessions](../docs/capture-and-debugging.md)
|
|
806
|
+
for screenshots, recordings, full/text DOM links, network/HAR, rotation,
|
|
807
|
+
persistent browsers, early trigger readiness and engine capability limits.
|
|
808
|
+
|
|
805
809
|
`startTrace()` records a session into a schema-versioned bundle any of the three
|
|
806
810
|
languages can read - the manifest, an ordered NDJSON timeline, per-checkpoint
|
|
807
811
|
DOM snapshots and the mutation batches between them:
|
package/eslint.config.js
CHANGED
|
@@ -141,6 +141,17 @@ export default [
|
|
|
141
141
|
'max-lines-per-function': 'off',
|
|
142
142
|
},
|
|
143
143
|
},
|
|
144
|
+
{
|
|
145
|
+
// Browser init scripts are serialized as one self-contained function.
|
|
146
|
+
// Keep its DOM/redaction helpers inside the payload.
|
|
147
|
+
files: ['src/traces/page-capture.js'],
|
|
148
|
+
rules: {
|
|
149
|
+
'max-lines-per-function': [
|
|
150
|
+
'warn',
|
|
151
|
+
{ max: 350, skipBlankLines: true, skipComments: true },
|
|
152
|
+
],
|
|
153
|
+
},
|
|
154
|
+
},
|
|
144
155
|
{
|
|
145
156
|
// Test files have different requirements
|
|
146
157
|
files: ['tests/**/*.js', '**/*.test.js'],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "browser-commander",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.28.0",
|
|
4
4
|
"description": "Universal browser automation library that supports Playwright, Puppeteer, and Selenium with a unified API",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -83,10 +83,13 @@
|
|
|
83
83
|
"bplist-parser": "^0.5.0",
|
|
84
84
|
"command-stream": "^1.4.0",
|
|
85
85
|
"csv-parse": "^7.0.3",
|
|
86
|
+
"gifenc": "^1.0.3",
|
|
86
87
|
"links-notation": "^0.23.0",
|
|
87
88
|
"log-lazy": "^1.0.4",
|
|
88
89
|
"plist": "^5.0.0",
|
|
89
|
-
"test-anywhere": "^0.9.1"
|
|
90
|
+
"test-anywhere": "^0.9.1",
|
|
91
|
+
"upng-js": "^2.1.0",
|
|
92
|
+
"webp-wasm": "^1.0.6"
|
|
90
93
|
},
|
|
91
94
|
"devDependencies": {
|
|
92
95
|
"@changesets/cli": "^3.0.3",
|
package/src/browser/connector.js
CHANGED
|
@@ -45,6 +45,7 @@ async function connectSelenium(options, dependencies) {
|
|
|
45
45
|
...options,
|
|
46
46
|
debuggerAddress: new URL(options.cdpEndpoint).host,
|
|
47
47
|
});
|
|
48
|
+
await selectWebDriverTab(connected.driver, options);
|
|
48
49
|
return {
|
|
49
50
|
...connected,
|
|
50
51
|
browser: connected.driver,
|
|
@@ -52,6 +53,52 @@ async function connectSelenium(options, dependencies) {
|
|
|
52
53
|
};
|
|
53
54
|
}
|
|
54
55
|
|
|
56
|
+
async function selectWebDriverTab(driver, options) {
|
|
57
|
+
if (!driver?.getAllWindowHandles) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const original = await driver.getWindowHandle();
|
|
61
|
+
const handles = await driver.getAllWindowHandles();
|
|
62
|
+
let selected = original;
|
|
63
|
+
if (options.targetId || options.url) {
|
|
64
|
+
selected = null;
|
|
65
|
+
for (const handle of handles) {
|
|
66
|
+
if (
|
|
67
|
+
options.targetId &&
|
|
68
|
+
handle !== options.targetId &&
|
|
69
|
+
handle !== `CDwindow-${options.targetId}`
|
|
70
|
+
) {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
await driver.switchTo().window(handle);
|
|
74
|
+
const url = await driver.getCurrentUrl();
|
|
75
|
+
if (
|
|
76
|
+
options.url &&
|
|
77
|
+
!(typeof options.url === 'function'
|
|
78
|
+
? options.url(url)
|
|
79
|
+
: options.url instanceof RegExp
|
|
80
|
+
? options.url.test(url)
|
|
81
|
+
: url === options.url)
|
|
82
|
+
) {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
selected = handle;
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
if (!selected) {
|
|
89
|
+
await driver.switchTo().window(original);
|
|
90
|
+
throw new Error('No tab matches the requested targetId or URL');
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (options.singleTab) {
|
|
94
|
+
for (const handle of handles.filter((handle) => handle !== selected)) {
|
|
95
|
+
await driver.switchTo().window(handle);
|
|
96
|
+
await driver.close();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
await driver.switchTo().window(selected);
|
|
100
|
+
}
|
|
101
|
+
|
|
55
102
|
function addDefinedOptions(options, values) {
|
|
56
103
|
for (const [key, value] of Object.entries(values)) {
|
|
57
104
|
if (value !== undefined) {
|
|
@@ -111,19 +158,75 @@ async function prepareStorageState({ storageState, seedCookies }) {
|
|
|
111
158
|
* @param {Object[]} pages - Engine page handles
|
|
112
159
|
* @returns {Promise<Object|undefined>}
|
|
113
160
|
*/
|
|
114
|
-
export async function pickForegroundPage(pages) {
|
|
161
|
+
export async function pickForegroundPage(pages, options = {}) {
|
|
162
|
+
const targets = new Map();
|
|
163
|
+
// Playwright emulates focus on every attached tab. Remove the emulation
|
|
164
|
+
// before observing visibility; DevTools target order has no active-tab flag.
|
|
115
165
|
for (const page of pages) {
|
|
116
|
-
|
|
117
|
-
|
|
166
|
+
let session;
|
|
167
|
+
try {
|
|
168
|
+
session =
|
|
169
|
+
(await page.context?.().newCDPSession?.(page)) ??
|
|
170
|
+
(await page.target?.().createCDPSession?.());
|
|
171
|
+
if (!session) {
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
const { targetInfo } = await session.send('Target.getTargetInfo');
|
|
175
|
+
targets.set(page, targetInfo.targetId);
|
|
176
|
+
await session.send('Emulation.setFocusEmulationEnabled', {
|
|
177
|
+
enabled: false,
|
|
178
|
+
});
|
|
179
|
+
} catch {
|
|
180
|
+
// Non-Chromium engines do not expose CDP. Explicit URL matching still
|
|
181
|
+
// works there, and normal visibility is not affected by this emulation.
|
|
182
|
+
} finally {
|
|
183
|
+
await session?.detach?.().catch(() => {});
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
let selected;
|
|
187
|
+
if (options.targetId || options.url) {
|
|
188
|
+
selected = pages.find((page) => {
|
|
189
|
+
if (options.targetId && targets.get(page) !== options.targetId) {
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
const url = page.url();
|
|
193
|
+
const matcher = options.url;
|
|
194
|
+
if (matcher instanceof RegExp) {
|
|
195
|
+
matcher.lastIndex = 0;
|
|
196
|
+
return matcher.test(url);
|
|
197
|
+
}
|
|
198
|
+
if (typeof matcher === 'function') {
|
|
199
|
+
return matcher(url);
|
|
200
|
+
}
|
|
201
|
+
return !matcher || matcher === url;
|
|
202
|
+
});
|
|
203
|
+
if (!selected) {
|
|
204
|
+
throw new RangeError('No tab matches the requested targetId/URL');
|
|
118
205
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
206
|
+
}
|
|
207
|
+
if (!selected) {
|
|
208
|
+
for (const page of pages) {
|
|
209
|
+
if (typeof page?.evaluate !== 'function') {
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
const state = await page
|
|
213
|
+
.evaluate(() => document.visibilityState)
|
|
214
|
+
.catch(() => null);
|
|
215
|
+
if (state === 'visible') {
|
|
216
|
+
selected = page;
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
selected ??= pages[0];
|
|
222
|
+
if (options.singleTab && selected) {
|
|
223
|
+
for (const page of pages) {
|
|
224
|
+
if (page !== selected) {
|
|
225
|
+
await page.close();
|
|
226
|
+
}
|
|
124
227
|
}
|
|
125
228
|
}
|
|
126
|
-
return
|
|
229
|
+
return selected;
|
|
127
230
|
}
|
|
128
231
|
|
|
129
232
|
async function connectPlaywright({ options, loadPlaywright, storageState }) {
|
|
@@ -138,10 +241,11 @@ async function connectPlaywright({ options, loadPlaywright, storageState }) {
|
|
|
138
241
|
throw new Error('Connected Playwright browser has no default context');
|
|
139
242
|
}
|
|
140
243
|
const page =
|
|
141
|
-
(await pickForegroundPage(context.pages())) ??
|
|
244
|
+
(await pickForegroundPage(context.pages(), options)) ??
|
|
245
|
+
(await context.newPage());
|
|
142
246
|
|
|
143
247
|
await restorePlaywrightStorageState({ context, storageState });
|
|
144
|
-
return { browser, page };
|
|
248
|
+
return { browser, page, detach: browser.close?.bind(browser) };
|
|
145
249
|
}
|
|
146
250
|
|
|
147
251
|
async function connectPuppeteer({ options, loadPuppeteer, storageState }) {
|
|
@@ -151,11 +255,11 @@ async function connectPuppeteer({ options, loadPuppeteer, storageState }) {
|
|
|
151
255
|
buildPuppeteerConnectOptions(options)
|
|
152
256
|
);
|
|
153
257
|
const page =
|
|
154
|
-
(await pickForegroundPage(await browser.pages())) ??
|
|
258
|
+
(await pickForegroundPage(await browser.pages(), options)) ??
|
|
155
259
|
(await browser.newPage());
|
|
156
260
|
|
|
157
261
|
await restorePuppeteerStorageState({ page, storageState });
|
|
158
|
-
return { browser, page };
|
|
262
|
+
return { browser, page, detach: browser.disconnect?.bind(browser) };
|
|
159
263
|
}
|
|
160
264
|
|
|
161
265
|
/**
|
|
@@ -26,6 +26,7 @@ export function resolveChromeArgs({
|
|
|
26
26
|
extraArgs = [],
|
|
27
27
|
ignoreDefaultArgs = [],
|
|
28
28
|
restrictions = [],
|
|
29
|
+
disableFeatures = [],
|
|
29
30
|
} = {}) {
|
|
30
31
|
assertStringArray(args, 'args');
|
|
31
32
|
assertStringArray(extraArgs, 'extraArgs');
|
|
@@ -41,7 +42,7 @@ export function resolveChromeArgs({
|
|
|
41
42
|
|
|
42
43
|
return {
|
|
43
44
|
args: mergeFeatureSwitches([
|
|
44
|
-
...resolveRestrictions(restrictions).args,
|
|
45
|
+
...resolveRestrictions(restrictions, { disableFeatures }).args,
|
|
45
46
|
...args,
|
|
46
47
|
...extraArgs,
|
|
47
48
|
]),
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
"--disable-session-crashed-bubble",
|
|
18
18
|
"--hide-crash-restore-bubble",
|
|
19
19
|
"--disable-crash-restore"
|
|
20
|
-
]
|
|
20
|
+
],
|
|
21
|
+
"disableFeatures": ["SessionRestoreInfobar"]
|
|
21
22
|
},
|
|
22
23
|
{
|
|
23
24
|
"id": "no-infobars",
|
|
@@ -60,7 +61,12 @@
|
|
|
60
61
|
{
|
|
61
62
|
"id": "no-translate",
|
|
62
63
|
"description": "Disable the Translate feature and its bubble.",
|
|
63
|
-
"disableFeatures": ["Translate"]
|
|
64
|
+
"disableFeatures": ["Translate"],
|
|
65
|
+
"preferences": {
|
|
66
|
+
"translate": {
|
|
67
|
+
"enabled": false
|
|
68
|
+
}
|
|
69
|
+
}
|
|
64
70
|
},
|
|
65
71
|
{
|
|
66
72
|
"id": "no-component-update",
|
|
@@ -124,6 +130,24 @@
|
|
|
124
130
|
"id": "start-maximized",
|
|
125
131
|
"description": "Start with a maximized window.",
|
|
126
132
|
"args": ["--start-maximized"]
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
"id": "no-save-bubbles",
|
|
136
|
+
"description": "Suppress password and payment-card saving bubbles and browser promos.",
|
|
137
|
+
"disableFeatures": [
|
|
138
|
+
"AutofillSaveCardUi",
|
|
139
|
+
"ChromeWhatsNewUI",
|
|
140
|
+
"PromotionalTabs"
|
|
141
|
+
],
|
|
142
|
+
"preferences": {
|
|
143
|
+
"credentials_enable_service": false,
|
|
144
|
+
"profile": {
|
|
145
|
+
"password_manager_enabled": false
|
|
146
|
+
},
|
|
147
|
+
"autofill": {
|
|
148
|
+
"credit_card_enabled": false
|
|
149
|
+
}
|
|
150
|
+
}
|
|
127
151
|
}
|
|
128
152
|
],
|
|
129
153
|
"presets": {
|
|
@@ -142,6 +166,13 @@
|
|
|
142
166
|
"no-default-browser-check",
|
|
143
167
|
"no-google-services",
|
|
144
168
|
"no-translate"
|
|
169
|
+
],
|
|
170
|
+
"quiet-ui": [
|
|
171
|
+
"no-first-run",
|
|
172
|
+
"no-default-browser-check",
|
|
173
|
+
"no-crash-restore",
|
|
174
|
+
"no-translate",
|
|
175
|
+
"no-save-bubbles"
|
|
145
176
|
]
|
|
146
177
|
}
|
|
147
178
|
}
|
package/src/browser/launcher.js
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
|
|
8
8
|
import { assertSupportedEngine } from './connector.js';
|
|
9
9
|
import { emulateMedia } from './media.js';
|
|
10
|
+
import { reserveLoopbackPort } from './debugging-port.js';
|
|
10
11
|
import {
|
|
11
12
|
buildPlaywrightLaunchOptions,
|
|
12
13
|
buildPuppeteerLaunchOptions,
|
|
@@ -14,6 +15,7 @@ import {
|
|
|
14
15
|
} from './launch-options.js';
|
|
15
16
|
import {
|
|
16
17
|
createTemporaryUserDataDir,
|
|
18
|
+
configureUserDataDir,
|
|
17
19
|
removeUserDataDir,
|
|
18
20
|
} from './profile-directory.js';
|
|
19
21
|
import { launchRealBrowser } from './real-browser.js';
|
|
@@ -168,6 +170,7 @@ async function launchWithEngine(options, dependencies) {
|
|
|
168
170
|
extraArgs,
|
|
169
171
|
ignoreDefaultArgs,
|
|
170
172
|
restrictions,
|
|
173
|
+
disableFeatures: options.disableFeatures,
|
|
171
174
|
});
|
|
172
175
|
const restrictionEnv = resolveRestrictions(restrictions).env;
|
|
173
176
|
const childEnv =
|
|
@@ -177,6 +180,13 @@ async function launchWithEngine(options, dependencies) {
|
|
|
177
180
|
const temporaryProfile = !requestedUserDataDir;
|
|
178
181
|
const userDataDir =
|
|
179
182
|
requestedUserDataDir ?? (await createTemporaryUserDataDir());
|
|
183
|
+
await configureUserDataDir(userDataDir, {
|
|
184
|
+
preferences: {
|
|
185
|
+
...resolveRestrictions(restrictions).preferences,
|
|
186
|
+
...options.preferences,
|
|
187
|
+
},
|
|
188
|
+
localState: options.localState,
|
|
189
|
+
});
|
|
180
190
|
const engineModule = await (
|
|
181
191
|
dependencies.loadEngineModule ?? loadEngine[engine]
|
|
182
192
|
)();
|
|
@@ -307,6 +317,15 @@ export async function launchBrowserWithDependencies(
|
|
|
307
317
|
options = {},
|
|
308
318
|
dependencies = {}
|
|
309
319
|
) {
|
|
320
|
+
if (options.keepOpen) {
|
|
321
|
+
const { connectOrLaunch } = await import('./persistent-session.js');
|
|
322
|
+
return connectOrLaunch({
|
|
323
|
+
...options,
|
|
324
|
+
userDataDir: options.userDataDir ?? (await createTemporaryUserDataDir()),
|
|
325
|
+
remoteDebuggingPort:
|
|
326
|
+
options.remoteDebuggingPort ?? (await reserveLoopbackPort()),
|
|
327
|
+
});
|
|
328
|
+
}
|
|
310
329
|
sessionPersistencePath(options);
|
|
311
330
|
if (isSafariChannel(options.channel ?? options.browser)) {
|
|
312
331
|
validateLaunchMode({
|
|
@@ -355,7 +374,13 @@ async function launchNonSafariBrowser(options, dependencies) {
|
|
|
355
374
|
}
|
|
356
375
|
}
|
|
357
376
|
// Validate arguments before anything is started or written to disk.
|
|
358
|
-
resolveChromeArgs({
|
|
377
|
+
resolveChromeArgs({
|
|
378
|
+
args,
|
|
379
|
+
extraArgs,
|
|
380
|
+
ignoreDefaultArgs,
|
|
381
|
+
restrictions,
|
|
382
|
+
disableFeatures: options.disableFeatures,
|
|
383
|
+
});
|
|
359
384
|
const resolvedStorageState = await loadStorageState(storageState);
|
|
360
385
|
|
|
361
386
|
if (verbose) {
|