browser-commander 0.28.0 → 0.29.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.
Files changed (43) hide show
  1. package/.jscpd-baseline.json +5 -19
  2. package/CHANGELOG.md +6 -0
  3. package/README.md +20 -10
  4. package/package.json +1 -1
  5. package/src/browser/browser-profile-files.js +5 -2
  6. package/src/browser/connector.js +140 -65
  7. package/src/browser/persistent-session.js +110 -38
  8. package/src/browser/session-adoption.js +96 -0
  9. package/src/browser/session-worker.js +12 -0
  10. package/src/cli/args.js +3 -0
  11. package/src/cli/commands.js +3 -0
  12. package/src/cli/high-level-methods.js +7 -0
  13. package/src/cli/sessions.js +3 -0
  14. package/src/core/commander-features.js +6 -1
  15. package/src/core/navigation-safety.js +1 -0
  16. package/src/core/page-trigger-manager.js +173 -134
  17. package/src/index.js +2 -0
  18. package/src/interactions/overlays.js +59 -14
  19. package/src/traces/bundle.js +41 -3
  20. package/src/traces/checkpoints.js +7 -3
  21. package/src/traces/links.js +44 -5
  22. package/src/traces/mutation-stream.js +8 -2
  23. package/src/traces/network.js +65 -13
  24. package/src/traces/page-capture.js +65 -21
  25. package/src/traces/recorder.js +33 -3
  26. package/src/traces/redaction.js +89 -6
  27. package/src/traces/rolling-bundle.js +240 -0
  28. package/src/traces/rolling.js +10 -113
  29. package/src/traces/stop.js +1 -0
  30. package/tests/e2e/issue-160.e2e.test.js +291 -0
  31. package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0002.html +1 -1
  32. package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0003.html +1 -1
  33. package/tests/fixtures/traces/conformance/expected/bundle/events.ndjson +2 -2
  34. package/tests/fixtures/traces/conformance/expected/bundle/manifest.json +4 -0
  35. package/tests/fixtures/traces/conformance/expected/bundle/viewer.html +1 -1
  36. package/tests/fixtures/traces/conformance/expected/trace.lino +2 -2
  37. package/tests/unit/browser/issue-160-connector.test.js +157 -0
  38. package/tests/unit/core/interrupted-navigation.test.js +32 -0
  39. package/tests/unit/core/trigger-stop-race.test.js +80 -0
  40. package/tests/unit/interactions/overlays.test.js +58 -0
  41. package/tests/unit/traces/issue-160-privacy.test.js +110 -0
  42. package/tests/unit/traces/issue-160-rotation.test.js +140 -0
  43. package/tests/unit/traces/network.test.js +55 -3
@@ -1,10 +1,8 @@
1
1
  {
2
2
  "version": 1,
3
3
  "fingerprints": {
4
- "00504cdec3a262ed": 1,
5
4
  "00d381dd611ae8ac": 1,
6
5
  "029a62ef1f48e856": 1,
7
- "02fc9852e9ba4c49": 1,
8
6
  "038170e72e0af1f5": 1,
9
7
  "0413f601c8911a92": 1,
10
8
  "04b28d01d0bb47dc": 1,
@@ -14,22 +12,18 @@
14
12
  "08e28c7706cdddd6": 1,
15
13
  "090a69d9528e4cdb": 1,
16
14
  "09694dc8fb9d683c": 1,
17
- "0a3459aad03492bb": 1,
18
15
  "0ae281facb3c28dd": 1,
19
- "0b48b63049c42591": 1,
20
16
  "0b8c71391c107a5e": 1,
21
17
  "0ba2d165b56db0b3": 1,
22
18
  "0ca2d9c2f2b44d6c": 1,
23
19
  "0caa402c4eae32d8": 1,
24
20
  "0fd23be8410c2c95": 1,
25
21
  "0fe4f3b5e5e939b3": 1,
26
- "126a1cfe8f2264ce": 1,
27
22
  "1273365073edd790": 1,
28
23
  "13c4b58d3ea0719b": 1,
29
24
  "14beedf656e57fba": 1,
30
25
  "150c8efe188ac225": 1,
31
26
  "16340a16ed52849e": 1,
32
- "17c26c0db2d35ad1": 1,
33
27
  "19ff70fc80fad524": 1,
34
28
  "1a9f0f0f68ed445c": 1,
35
29
  "1ad4dc7e19a77f68": 1,
@@ -42,7 +36,6 @@
42
36
  "22755b35701c5bb6": 1,
43
37
  "2296c06fa00da254": 1,
44
38
  "229b903386b17538": 1,
45
- "22ab9a1ca6316d24": 1,
46
39
  "22cbd954358d25f8": 1,
47
40
  "230bfe57cfda164d": 1,
48
41
  "2357ae472525f6bf": 1,
@@ -51,6 +44,7 @@
51
44
  "266b9cb95f783cdf": 1,
52
45
  "26bb09996580437c": 1,
53
46
  "26ebb2e251c9a39e": 1,
47
+ "282eb8d84bbe54a4": 1,
54
48
  "29edfb9e08094991": 1,
55
49
  "2a78ee4b05679c8d": 1,
56
50
  "2b1a759e2cfd21a2": 1,
@@ -65,7 +59,6 @@
65
59
  "33357ce94e07cc4b": 1,
66
60
  "35cf2769645f3234": 1,
67
61
  "37bd4114d0089f49": 1,
68
- "39cf8134a7fdaa31": 1,
69
62
  "3adca52a3f04837c": 1,
70
63
  "3c1017def7b3216c": 1,
71
64
  "3c5cb4b62d3739e3": 1,
@@ -79,7 +72,6 @@
79
72
  "441570d51d6f70a4": 1,
80
73
  "457c71f32f53833d": 1,
81
74
  "4715a17d690865ee": 1,
82
- "47dc143be0bd8be5": 1,
83
75
  "4b0d3cc5c27227e7": 1,
84
76
  "4c3b94caa28045d0": 1,
85
77
  "4d1ff99bb19f5685": 1,
@@ -107,16 +99,15 @@
107
99
  "5ba2f3218754f04c": 1,
108
100
  "60052da86e7bb241": 1,
109
101
  "608e1dd27138ec6f": 1,
110
- "60a3149247760d9b": 1,
111
102
  "60d0c58a01fc8746": 1,
112
103
  "61ae5da6cc003836": 1,
113
104
  "61bcadcd8dc9f63e": 1,
114
105
  "63ff613b1780c67a": 1,
115
106
  "642cc89ed49f8a21": 1,
116
107
  "6479c3604c153ec4": 1,
108
+ "65ce52f8afd6a952": 1,
117
109
  "66fb97eebd7ff0dc": 1,
118
110
  "67a95e428b8fa31c": 1,
119
- "67c309590c5d69a1": 1,
120
111
  "6880d620e0e67229": 1,
121
112
  "69d50e6f6a5f2ee9": 1,
122
113
  "6a7ed9ad3fc00a39": 1,
@@ -153,6 +144,7 @@
153
144
  "86aae81b1aa0d21b": 1,
154
145
  "87484a2744e09748": 1,
155
146
  "8c2b1408886c1db1": 1,
147
+ "8d24ce7d6e8f6795": 1,
156
148
  "8e55579a412bc058": 1,
157
149
  "8ec18cdbd4a96518": 1,
158
150
  "901317bc4d7d30d0": 1,
@@ -178,10 +170,8 @@
178
170
  "a6ad7ab6c05656dc": 1,
179
171
  "a6ffab1aacd00af6": 1,
180
172
  "a71a4b720d466e32": 1,
181
- "a71fab6edf38c846": 1,
182
173
  "a78d76908162699e": 1,
183
174
  "a9bd4ffef6da5faf": 1,
184
- "ac3fe4ef966e0209": 1,
185
175
  "acc95d694eb2f581": 1,
186
176
  "ad4cfa76daa1b6fc": 1,
187
177
  "ad50b48938af8aef": 1,
@@ -203,7 +193,6 @@
203
193
  "bb5b0c8d22c9667a": 1,
204
194
  "bc9115be3a462930": 1,
205
195
  "bd0ae42c13d5b4f9": 1,
206
- "bd17b166d89cb709": 1,
207
196
  "bdb8ca169be36b80": 1,
208
197
  "bdcd11c2bfb5fde0": 1,
209
198
  "be0900fb3e87164e": 1,
@@ -235,8 +224,6 @@
235
224
  "d6fa9884eee815f0": 1,
236
225
  "d86bafb915488255": 1,
237
226
  "da2c10a66acdb52f": 1,
238
- "dabb9fc09638e178": 1,
239
- "dcc616702e5b0cb4": 1,
240
227
  "de0bf2317c53d427": 1,
241
228
  "de1b481bfe8c3e58": 1,
242
229
  "df2dc922f40485e7": 1,
@@ -256,7 +243,6 @@
256
243
  "eb9a70c2a21094ff": 1,
257
244
  "ebe03c5f8c2f5147": 1,
258
245
  "ee19c8b9b332ee98": 1,
259
- "ef6b9112e659147b": 1,
260
246
  "f07f979f8c2706e8": 1,
261
247
  "f38f72095a67c95e": 1,
262
248
  "f40ba84763547f41": 1,
@@ -265,7 +251,6 @@
265
251
  "f4bfa7e503443cd1": 1,
266
252
  "f6678d6748e9c902": 1,
267
253
  "f67a07a611f39cb2": 1,
268
- "f762b61eef0af530": 1,
269
254
  "f7b1f9d699bcd580": 1,
270
255
  "f81e07af5d3a1c3c": 1,
271
256
  "f904844f89cab699": 1,
@@ -275,6 +260,7 @@
275
260
  "fb4a079a529a6285": 1,
276
261
  "fc0d7bde0033411d": 1,
277
262
  "fccfd8b004c12dd4": 1,
278
- "fe859df9f97c75d4": 1
263
+ "fe859df9f97c75d4": 1,
264
+ "ff3b6c3e0b8bf33b": 1
279
265
  }
280
266
  }
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.29.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 4939956: Harden trace privacy and bounded mutation capture; rotate continuous bundles before writes and retain segments by default. Fix CDP selection cleanup and zero-page reconnect, add persistent profile adoption, idle heartbeats and tab enforcement, preserve queued trigger starts during stop, and make overlay dismissal report failures safely. Export trace rendering/summary tools and document the 0.28 trigger migration and packaged guides.
8
+
3
9
  ## 0.28.0
4
10
 
5
11
  ### Minor Changes
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  A universal browser automation library for JavaScript/TypeScript that supports Playwright, Puppeteer, and Selenium with a unified API. The key focus is on **stoppable page triggers** - ensuring automation logic is properly mounted/unmounted during page navigation.
4
4
 
5
- See [engine support and native/CLI defaults](../docs/engine-support.md) for the
5
+ See [engine support and native/CLI defaults](https://github.com/link-foundation/browser-commander/blob/main/docs/engine-support.md) for the
6
6
  cross-language API matrix and Selenium examples.
7
7
 
8
8
  ## Installation
@@ -119,7 +119,7 @@ const { browser, page } = await launchBrowser({
119
119
  ```
120
120
 
121
121
  Every switch Browser Commander used to add on its own is available as a named
122
- [launch restriction](../docs/feature-parity.md#launch-command-line-and-opt-in-restrictions);
122
+ [launch restriction](https://github.com/link-foundation/browser-commander/blob/main/docs/feature-parity.md#launch-command-line-and-opt-in-restrictions);
123
123
  `restrictions: ['legacy-defaults']` restores the old `CHROME_ARGS`. Pass
124
124
  `launch: 'engine'` to use Playwright's `launchPersistentContext()` or
125
125
  `puppeteer.launch()` instead; the engine then adds its own switches, and
@@ -379,6 +379,10 @@ commander.pageTrigger({
379
379
  });
380
380
  ```
381
381
 
382
+ For trace budgets/privacy, persistent session adoption and heartbeats, CDP
383
+ `noDefaults`, overlay reporting, and the `readyOn`/`concurrency` trigger migration,
384
+ see [trace and session hardening](https://github.com/link-foundation/browser-commander/blob/f5e426d6933c681189c4989bac182a1558d8edb8/docs/trace-session-hardening.md).
385
+
382
386
  ## API Reference
383
387
 
384
388
  ### launchBrowser(options)
@@ -520,7 +524,7 @@ headless. `connectWebDriver({ serverUrl, capabilities })` opens a session on a
520
524
  server that is already running (a Selenium Grid, a cloud provider); pass
521
525
  `capabilities: { webSocketUrl: true }` for BiDi. Media emulation, fingerprint
522
526
  overrides and managed downloads need CDP and throw on this engine; see
523
- [docs/feature-parity.md](../docs/feature-parity.md#webdriver-engine).
527
+ [docs/feature-parity.md](https://github.com/link-foundation/browser-commander/blob/main/docs/feature-parity.md#webdriver-engine).
524
528
 
525
529
  ### listBrowserProfiles(options)
526
530
 
@@ -686,6 +690,8 @@ const commander = makeBrowserCommander({
686
690
  verbose: false, // Enable debug logging
687
691
  enableNetworkTracking: true, // Track HTTP requests
688
692
  enableNavigationManager: true, // Enable navigation events
693
+ overlays: [{ selector: '.consent-close', timeout: 1000 }],
694
+ onOverlayDismiss: (report) => console.log(report.selector, report.status),
689
695
  });
690
696
  ```
691
697
 
@@ -693,10 +699,14 @@ const commander = makeBrowserCommander({
693
699
 
694
700
  ```javascript
695
701
  const unregister = commander.pageTrigger({
696
- name: 'trigger-name', // For debugging
697
- condition: (ctx) => boolean, // When to run (receives {url, commander})
698
- action: async (ctx) => void, // What to do
699
- priority: 0, // Higher runs first
702
+ name: 'application-ready',
703
+ readyOn: 'domcontentloaded', // Default: networkidle
704
+ concurrency: 'queue', // Default: skip; also restart
705
+ condition: ({ url }) => url.includes('/application'),
706
+ action: async (ctx) => {
707
+ ctx.checkStopped();
708
+ },
709
+ priority: 0, // Higher runs first
700
710
  });
701
711
  ```
702
712
 
@@ -802,7 +812,7 @@ failed or cancelled download raises the failure rather than returning a path.
802
812
 
803
813
  ### Portable Traces
804
814
 
805
- See [capture, debugging and reusable sessions](../docs/capture-and-debugging.md)
815
+ See [capture, debugging and reusable sessions](https://github.com/link-foundation/browser-commander/blob/main/docs/capture-and-debugging.md)
806
816
  for screenshots, recordings, full/text DOM links, network/HAR, rotation,
807
817
  persistent browsers, early trigger readiness and engine capability limits.
808
818
 
@@ -1028,7 +1038,7 @@ This is the **official extensibility mechanism** while awaiting browser-commande
1028
1038
 
1029
1039
  ## Command Line
1030
1040
 
1031
- The package installs a `browser-commander` command. The Rust and Python packages ship the same command, and the contract all three follow is [docs/cli-and-bridge.md](../docs/cli-and-bridge.md). Every command prints exactly one JSON document to stdout. The exit code is `0` on success and `1` on error. `doctor` exits `2` when it finds an unlisted difference, and a usage error exits `64`.
1041
+ The package installs a `browser-commander` command. The Rust and Python packages ship the same command, and the contract all three follow is [docs/cli-and-bridge.md](https://github.com/link-foundation/browser-commander/blob/main/docs/cli-and-bridge.md). Every command prints exactly one JSON document to stdout. The exit code is `0` on success and `1` on error. `doctor` exits `2` when it finds an unlisted difference, and a usage error exits `64`.
1032
1042
 
1033
1043
  ```bash
1034
1044
  npx browser-commander version
@@ -1068,4 +1078,4 @@ See [src/ARCHITECTURE.md](src/ARCHITECTURE.md) for detailed architecture documen
1068
1078
 
1069
1079
  [UNLICENSE](../LICENSE)
1070
1080
 
1071
- See [navigation budgets, launch diagnostics, reusable helpers and sessions](../docs/navigation-launch-and-sessions.md) for the shared API contracts and engine limitations.
1081
+ See [navigation budgets, launch diagnostics, reusable helpers and sessions](https://github.com/link-foundation/browser-commander/blob/main/docs/navigation-launch-and-sessions.md) for the shared API contracts and engine limitations.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "browser-commander",
3
- "version": "0.28.0",
3
+ "version": "0.29.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": {
@@ -10,13 +10,16 @@ export function localStatePathForProfile(profileDir) {
10
10
  }
11
11
 
12
12
  /** Resolve existing ancestors even when the target profile does not exist. */
13
- export function physicalPath(value) {
13
+ export function physicalPath(value, resolve = realpathSync) {
14
14
  let existing = path.resolve(value);
15
15
  const suffix = [];
16
16
  while (true) {
17
17
  try {
18
- return path.join(realpathSync(existing), ...suffix.reverse());
18
+ return path.join(resolve(existing), ...suffix.reverse());
19
19
  } catch (error) {
20
+ if (['EPERM', 'EACCES'].includes(error.code)) {
21
+ return path.resolve(value);
22
+ }
20
23
  if (!['ENOENT', 'ENOTDIR'].includes(error.code)) {
21
24
  throw error;
22
25
  }
@@ -45,11 +45,17 @@ 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
+ try {
49
+ await selectWebDriverTab(connected.driver, options);
50
+ } catch (error) {
51
+ await ignoreCleanup(() => connected.close());
52
+ throw error;
53
+ }
49
54
  return {
50
55
  ...connected,
51
56
  browser: connected.driver,
52
57
  disconnect: connected.close,
58
+ detach: connected.close,
53
59
  };
54
60
  }
55
61
 
@@ -60,11 +66,14 @@ async function selectWebDriverTab(driver, options) {
60
66
  const original = await driver.getWindowHandle();
61
67
  const handles = await driver.getAllWindowHandles();
62
68
  let selected = original;
63
- if (options.targetId || options.url) {
69
+ const matchers = urlMatchers(options);
70
+ if (options.targetId || matchers.length) {
64
71
  selected = null;
72
+ const tabs = [];
65
73
  for (const handle of handles) {
66
74
  if (
67
75
  options.targetId &&
76
+ !options.fallback &&
68
77
  handle !== options.targetId &&
69
78
  handle !== `CDwindow-${options.targetId}`
70
79
  ) {
@@ -72,22 +81,26 @@ async function selectWebDriverTab(driver, options) {
72
81
  }
73
82
  await driver.switchTo().window(handle);
74
83
  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;
84
+ tabs.push({ handle, url });
87
85
  }
86
+ selected = matchers.length
87
+ ? matchers
88
+ .map(
89
+ (matcher) =>
90
+ tabs.find((tab) => matchesUrl(matcher, tab.url))?.handle
91
+ )
92
+ .find(Boolean)
93
+ : tabs.find(
94
+ (tab) =>
95
+ tab.handle === options.targetId ||
96
+ tab.handle === `CDwindow-${options.targetId}`
97
+ )?.handle;
88
98
  if (!selected) {
89
- await driver.switchTo().window(original);
90
- throw new Error('No tab matches the requested targetId or URL');
99
+ if (!options.fallback) {
100
+ await driver.switchTo().window(original);
101
+ throw new Error('No tab matches the requested targetId or URL');
102
+ }
103
+ selected = handles.includes(original) ? original : handles[0];
91
104
  }
92
105
  }
93
106
  if (options.singleTab) {
@@ -108,8 +121,13 @@ function addDefinedOptions(options, values) {
108
121
  return options;
109
122
  }
110
123
 
111
- function buildPlaywrightConnectOptions({ slowMo, timeout, headers }) {
112
- return addDefinedOptions({}, { slowMo, timeout, headers });
124
+ function buildPlaywrightConnectOptions({
125
+ slowMo,
126
+ timeout,
127
+ headers,
128
+ noDefaults,
129
+ }) {
130
+ return addDefinedOptions({}, { slowMo, timeout, headers, noDefaults });
113
131
  }
114
132
 
115
133
  function buildPuppeteerConnectOptions({
@@ -130,6 +148,55 @@ function buildPuppeteerConnectOptions({
130
148
  );
131
149
  }
132
150
 
151
+ export function matchesUrl(matcher, url) {
152
+ if (matcher instanceof RegExp) {
153
+ matcher.lastIndex = 0;
154
+ return matcher.test(url);
155
+ }
156
+ return typeof matcher === 'function' ? matcher(url) : matcher === url;
157
+ }
158
+
159
+ function urlMatchers(options) {
160
+ return (
161
+ options.urlMatchers ??
162
+ (Array.isArray(options.url)
163
+ ? options.url
164
+ : options.url
165
+ ? [options.url]
166
+ : [])
167
+ );
168
+ }
169
+
170
+ async function ignoreCleanup(cleanup) {
171
+ try {
172
+ await cleanup();
173
+ } catch {
174
+ /* Preserve the connection failure. */
175
+ }
176
+ }
177
+
178
+ function requestedPage(pages, targets, options) {
179
+ const matchers = urlMatchers(options);
180
+ if (!options.targetId && !matchers.length) {
181
+ return undefined;
182
+ }
183
+ const candidates = options.targetId
184
+ ? pages.filter((page) => targets.get(page) === options.targetId)
185
+ : pages;
186
+ const eligible = candidates.length || !options.fallback ? candidates : pages;
187
+ const selected = matchers.length
188
+ ? matchers
189
+ .map((matcher) =>
190
+ eligible.find((page) => matchesUrl(matcher, page.url()))
191
+ )
192
+ .find(Boolean)
193
+ : candidates[0];
194
+ if (!selected && !options.fallback) {
195
+ throw new RangeError('No tab matches the requested targetId/URL');
196
+ }
197
+ return selected;
198
+ }
199
+
133
200
  async function prepareStorageState({ storageState, seedCookies }) {
134
201
  if (seedCookies !== undefined && !Array.isArray(seedCookies)) {
135
202
  throw new TypeError('seedCookies must be an array');
@@ -180,30 +247,10 @@ export async function pickForegroundPage(pages, options = {}) {
180
247
  // Non-Chromium engines do not expose CDP. Explicit URL matching still
181
248
  // works there, and normal visibility is not affected by this emulation.
182
249
  } 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');
250
+ await ignoreCleanup(() => session?.detach?.());
205
251
  }
206
252
  }
253
+ let selected = requestedPage(pages, targets, options);
207
254
  if (!selected) {
208
255
  for (const page of pages) {
209
256
  if (typeof page?.evaluate !== 'function') {
@@ -232,20 +279,38 @@ export async function pickForegroundPage(pages, options = {}) {
232
279
  async function connectPlaywright({ options, loadPlaywright, storageState }) {
233
280
  const { chromium } = await loadPlaywright();
234
281
  const endpoint = options.cdpEndpoint ?? options.wsEndpoint;
235
- const browser = await chromium.connectOverCDP(
236
- endpoint,
237
- buildPlaywrightConnectOptions(options)
238
- );
239
- const context = browser.contexts()[0];
240
- if (!context) {
241
- throw new Error('Connected Playwright browser has no default context');
282
+ const settings = buildPlaywrightConnectOptions(options);
283
+ let browser;
284
+ try {
285
+ browser = await chromium.connectOverCDP(endpoint, settings);
286
+ } catch (error) {
287
+ if (
288
+ settings.noDefaults !== undefined ||
289
+ !/Browser\.setDownloadBehavior/.test(error.message) ||
290
+ !/Browser context management is not supported/.test(error.message)
291
+ ) {
292
+ throw error;
293
+ }
294
+ browser = await chromium.connectOverCDP(endpoint, {
295
+ ...settings,
296
+ noDefaults: true,
297
+ });
242
298
  }
243
- const page =
244
- (await pickForegroundPage(context.pages(), options)) ??
245
- (await context.newPage());
299
+ try {
300
+ const context = browser.contexts()[0];
301
+ if (!context) {
302
+ throw new Error('Connected Playwright browser has no default context');
303
+ }
304
+ const page =
305
+ (await pickForegroundPage(context.pages(), options)) ??
306
+ (await context.newPage());
246
307
 
247
- await restorePlaywrightStorageState({ context, storageState });
248
- return { browser, page, detach: browser.close?.bind(browser) };
308
+ await restorePlaywrightStorageState({ context, storageState });
309
+ return { browser, page, detach: browser.close?.bind(browser) };
310
+ } catch (error) {
311
+ await ignoreCleanup(() => browser.close?.());
312
+ throw error;
313
+ }
249
314
  }
250
315
 
251
316
  async function connectPuppeteer({ options, loadPuppeteer, storageState }) {
@@ -254,12 +319,17 @@ async function connectPuppeteer({ options, loadPuppeteer, storageState }) {
254
319
  const browser = await puppeteer.connect(
255
320
  buildPuppeteerConnectOptions(options)
256
321
  );
257
- const page =
258
- (await pickForegroundPage(await browser.pages(), options)) ??
259
- (await browser.newPage());
322
+ try {
323
+ const page =
324
+ (await pickForegroundPage(await browser.pages(), options)) ??
325
+ (await browser.newPage());
260
326
 
261
- await restorePuppeteerStorageState({ page, storageState });
262
- return { browser, page, detach: browser.disconnect?.bind(browser) };
327
+ await restorePuppeteerStorageState({ page, storageState });
328
+ return { browser, page, detach: browser.disconnect?.bind(browser) };
329
+ } catch (error) {
330
+ await ignoreCleanup(() => browser.disconnect?.());
331
+ throw error;
332
+ }
263
333
  }
264
334
 
265
335
  /**
@@ -313,7 +383,7 @@ export async function connectBrowserWithDependencies(
313
383
  });
314
384
  return { ...result, downloads };
315
385
  } catch (error) {
316
- await result.close();
386
+ await ignoreCleanup(() => result.close());
317
387
  throw error;
318
388
  }
319
389
  }
@@ -346,14 +416,19 @@ export async function connectBrowserWithDependencies(
346
416
 
347
417
  // An attached browser gets the same managed lifecycle as a launched one:
348
418
  // the manager is built from the browser and page, not from how we got them.
349
- const downloads = await attachDownloads({
350
- engine,
351
- browser: result.browser,
352
- page: result.page,
353
- downloads: normalizedOptions.downloads,
354
- });
419
+ try {
420
+ const downloads = await attachDownloads({
421
+ engine,
422
+ browser: result.browser,
423
+ page: result.page,
424
+ downloads: normalizedOptions.downloads,
425
+ });
355
426
 
356
- return { ...result, downloads };
427
+ return { ...result, downloads };
428
+ } catch (error) {
429
+ await ignoreCleanup(() => result.detach?.());
430
+ throw error;
431
+ }
357
432
  }
358
433
 
359
434
  export { buildPlaywrightConnectOptions, buildPuppeteerConnectOptions };