browser-commander 0.28.0 → 0.30.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 (57) hide show
  1. package/.jscpd-baseline.json +7 -20
  2. package/CHANGELOG.md +12 -0
  3. package/README.md +27 -10
  4. package/package.json +1 -1
  5. package/scripts/run-tests.mjs +3 -1
  6. package/src/browser/browser-profile-files.js +5 -2
  7. package/src/browser/connector.js +140 -65
  8. package/src/browser/launcher.js +19 -8
  9. package/src/browser/persistent-session.js +110 -38
  10. package/src/browser/session-adoption.js +96 -0
  11. package/src/browser/session-worker.js +12 -0
  12. package/src/cli/args.js +3 -0
  13. package/src/cli/commands.js +3 -0
  14. package/src/cli/high-level-methods.js +7 -0
  15. package/src/cli/sessions.js +3 -0
  16. package/src/core/commander-features.js +33 -25
  17. package/src/core/navigation-safety.js +1 -0
  18. package/src/core/page-trigger-manager.js +173 -134
  19. package/src/index.js +2 -0
  20. package/src/interactions/overlays.js +59 -14
  21. package/src/traces/bundle.js +41 -3
  22. package/src/traces/checkpoints.js +7 -3
  23. package/src/traces/commander-actions.js +146 -0
  24. package/src/traces/debug-session.js +119 -0
  25. package/src/traces/drain-timer.js +25 -0
  26. package/src/traces/links.js +44 -5
  27. package/src/traces/mutation-stream.js +12 -5
  28. package/src/traces/network.js +65 -13
  29. package/src/traces/observers.js +50 -46
  30. package/src/traces/page-capture.js +65 -21
  31. package/src/traces/recorder.js +79 -28
  32. package/src/traces/redaction.js +89 -6
  33. package/src/traces/rolling-bundle.js +240 -0
  34. package/src/traces/rolling.js +10 -113
  35. package/src/traces/stop.js +1 -0
  36. package/src/traces/user-actions.js +405 -0
  37. package/tests/e2e/issue-160.e2e.test.js +291 -0
  38. package/tests/e2e/issue-162-recording.e2e.test.js +175 -0
  39. package/tests/e2e/traces.e2e.test.js +4 -1
  40. package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0002.html +1 -1
  41. package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0003.html +1 -1
  42. package/tests/fixtures/traces/conformance/expected/bundle/events.ndjson +4 -4
  43. package/tests/fixtures/traces/conformance/expected/bundle/manifest.json +4 -0
  44. package/tests/fixtures/traces/conformance/expected/bundle/viewer.html +1 -1
  45. package/tests/fixtures/traces/conformance/expected/trace.lino +4 -4
  46. package/tests/fixtures/traces/conformance/scenario.json +1 -0
  47. package/tests/helpers/e2e-browser.js +2 -1
  48. package/tests/unit/browser/issue-160-connector.test.js +157 -0
  49. package/tests/unit/core/interrupted-navigation.test.js +32 -0
  50. package/tests/unit/core/trigger-stop-race.test.js +80 -0
  51. package/tests/unit/interactions/overlays.test.js +58 -0
  52. package/tests/unit/traces/issue-160-privacy.test.js +110 -0
  53. package/tests/unit/traces/issue-160-rotation.test.js +140 -0
  54. package/tests/unit/traces/issue-162-defaults.test.js +223 -0
  55. package/tests/unit/traces/issue-162-user-actions.test.js +181 -0
  56. package/tests/unit/traces/network.test.js +55 -3
  57. package/tests/unit/traces/recorder.test.js +4 -1
@@ -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,
@@ -64,8 +58,8 @@
64
58
  "326f1ff9016e1e37": 1,
65
59
  "33357ce94e07cc4b": 1,
66
60
  "35cf2769645f3234": 1,
61
+ "364e8930fe8fdf71": 1,
67
62
  "37bd4114d0089f49": 1,
68
- "39cf8134a7fdaa31": 1,
69
63
  "3adca52a3f04837c": 1,
70
64
  "3c1017def7b3216c": 1,
71
65
  "3c5cb4b62d3739e3": 1,
@@ -79,7 +73,6 @@
79
73
  "441570d51d6f70a4": 1,
80
74
  "457c71f32f53833d": 1,
81
75
  "4715a17d690865ee": 1,
82
- "47dc143be0bd8be5": 1,
83
76
  "4b0d3cc5c27227e7": 1,
84
77
  "4c3b94caa28045d0": 1,
85
78
  "4d1ff99bb19f5685": 1,
@@ -107,16 +100,15 @@
107
100
  "5ba2f3218754f04c": 1,
108
101
  "60052da86e7bb241": 1,
109
102
  "608e1dd27138ec6f": 1,
110
- "60a3149247760d9b": 1,
111
103
  "60d0c58a01fc8746": 1,
112
104
  "61ae5da6cc003836": 1,
113
105
  "61bcadcd8dc9f63e": 1,
114
106
  "63ff613b1780c67a": 1,
115
107
  "642cc89ed49f8a21": 1,
116
108
  "6479c3604c153ec4": 1,
109
+ "65ce52f8afd6a952": 1,
117
110
  "66fb97eebd7ff0dc": 1,
118
111
  "67a95e428b8fa31c": 1,
119
- "67c309590c5d69a1": 1,
120
112
  "6880d620e0e67229": 1,
121
113
  "69d50e6f6a5f2ee9": 1,
122
114
  "6a7ed9ad3fc00a39": 1,
@@ -153,6 +145,7 @@
153
145
  "86aae81b1aa0d21b": 1,
154
146
  "87484a2744e09748": 1,
155
147
  "8c2b1408886c1db1": 1,
148
+ "8d24ce7d6e8f6795": 1,
156
149
  "8e55579a412bc058": 1,
157
150
  "8ec18cdbd4a96518": 1,
158
151
  "901317bc4d7d30d0": 1,
@@ -175,13 +168,12 @@
175
168
  "a36f66312d5bfbf8": 1,
176
169
  "a4c8498d4f2d2191": 1,
177
170
  "a5298242b5439858": 1,
171
+ "a5861532b8efced8": 1,
178
172
  "a6ad7ab6c05656dc": 1,
179
173
  "a6ffab1aacd00af6": 1,
180
174
  "a71a4b720d466e32": 1,
181
- "a71fab6edf38c846": 1,
182
175
  "a78d76908162699e": 1,
183
176
  "a9bd4ffef6da5faf": 1,
184
- "ac3fe4ef966e0209": 1,
185
177
  "acc95d694eb2f581": 1,
186
178
  "ad4cfa76daa1b6fc": 1,
187
179
  "ad50b48938af8aef": 1,
@@ -203,7 +195,6 @@
203
195
  "bb5b0c8d22c9667a": 1,
204
196
  "bc9115be3a462930": 1,
205
197
  "bd0ae42c13d5b4f9": 1,
206
- "bd17b166d89cb709": 1,
207
198
  "bdb8ca169be36b80": 1,
208
199
  "bdcd11c2bfb5fde0": 1,
209
200
  "be0900fb3e87164e": 1,
@@ -221,7 +212,6 @@
221
212
  "c714beebc66381dd": 1,
222
213
  "c9cf3c28bed33a5b": 1,
223
214
  "ca7e08d1eec2ad8c": 1,
224
- "cab590a1d04b5a05": 1,
225
215
  "cbd444e9a6a9072e": 1,
226
216
  "cebff7d1cc87f646": 1,
227
217
  "cfc928aa393146b6": 1,
@@ -235,8 +225,6 @@
235
225
  "d6fa9884eee815f0": 1,
236
226
  "d86bafb915488255": 1,
237
227
  "da2c10a66acdb52f": 1,
238
- "dabb9fc09638e178": 1,
239
- "dcc616702e5b0cb4": 1,
240
228
  "de0bf2317c53d427": 1,
241
229
  "de1b481bfe8c3e58": 1,
242
230
  "df2dc922f40485e7": 1,
@@ -256,7 +244,6 @@
256
244
  "eb9a70c2a21094ff": 1,
257
245
  "ebe03c5f8c2f5147": 1,
258
246
  "ee19c8b9b332ee98": 1,
259
- "ef6b9112e659147b": 1,
260
247
  "f07f979f8c2706e8": 1,
261
248
  "f38f72095a67c95e": 1,
262
249
  "f40ba84763547f41": 1,
@@ -265,7 +252,6 @@
265
252
  "f4bfa7e503443cd1": 1,
266
253
  "f6678d6748e9c902": 1,
267
254
  "f67a07a611f39cb2": 1,
268
- "f762b61eef0af530": 1,
269
255
  "f7b1f9d699bcd580": 1,
270
256
  "f81e07af5d3a1c3c": 1,
271
257
  "f904844f89cab699": 1,
@@ -275,6 +261,7 @@
275
261
  "fb4a079a529a6285": 1,
276
262
  "fc0d7bde0033411d": 1,
277
263
  "fccfd8b004c12dd4": 1,
278
- "fe859df9f97c75d4": 1
264
+ "fe859df9f97c75d4": 1,
265
+ "ff3b6c3e0b8bf33b": 1
279
266
  }
280
267
  }
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.30.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 0abb9c9: Enable complete JavaScript session recording by default for commanders, launches, and explicit traces. Record trusted user actions with private target descriptions alongside commander actions, DOM changes, network metadata, and a self-contained trace.lino export. Use debug: false or BROWSER_COMMANDER_DEBUG=0 to opt out.
8
+
9
+ ## 0.29.0
10
+
11
+ ### Minor Changes
12
+
13
+ - 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.
14
+
3
15
  ## 0.28.0
4
16
 
5
17
  ### 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,10 +812,17 @@ 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
 
819
+ JavaScript commanders and browser launches record complete sessions by default
820
+ under `./browser-commander-traces/`, including trusted user actions, DOM changes,
821
+ network metadata and `trace.lino`. Await `commander.ready` before using its raw
822
+ page, and call `commander.destroy()` or the launch's `close()` to finish the files.
823
+ Use `debug: false` or `BROWSER_COMMANDER_DEBUG=0` to opt out; `debug: { output }`
824
+ chooses the directory. `commander.tracePath` exposes its path.
825
+
809
826
  `startTrace()` records a session into a schema-versioned bundle any of the three
810
827
  languages can read - the manifest, an ordered NDJSON timeline, per-checkpoint
811
828
  DOM snapshots and the mutation batches between them:
@@ -1028,7 +1045,7 @@ This is the **official extensibility mechanism** while awaiting browser-commande
1028
1045
 
1029
1046
  ## Command Line
1030
1047
 
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`.
1048
+ 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
1049
 
1033
1050
  ```bash
1034
1051
  npx browser-commander version
@@ -1068,4 +1085,4 @@ See [src/ARCHITECTURE.md](src/ARCHITECTURE.md) for detailed architecture documen
1068
1085
 
1069
1086
  [UNLICENSE](../LICENSE)
1070
1087
 
1071
- See [navigation budgets, launch diagnostics, reusable helpers and sessions](../docs/navigation-launch-and-sessions.md) for the shared API contracts and engine limitations.
1088
+ 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.30.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": {
@@ -177,7 +177,9 @@ function runNodeTests(options, testFiles) {
177
177
  process.execPath,
178
178
  buildNodeTestArgs(options, testFiles),
179
179
  {
180
- env: { ...process.env, ...options.env },
180
+ // Most fixtures exercise unrelated APIs with incomplete fake pages. The
181
+ // default-recording regressions explicitly unset this harness opt-out.
182
+ env: { BROWSER_COMMANDER_DEBUG: '0', ...process.env, ...options.env },
181
183
  stdio: 'inherit',
182
184
  }
183
185
  );
@@ -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 };
@@ -8,6 +8,7 @@ import {
8
8
  import { assertSupportedEngine } from './connector.js';
9
9
  import { emulateMedia } from './media.js';
10
10
  import { reserveLoopbackPort } from './debugging-port.js';
11
+ import { attachLaunchDebug } from '../traces/debug-session.js';
11
12
  import {
12
13
  buildPlaywrightLaunchOptions,
13
14
  buildPuppeteerLaunchOptions,
@@ -319,12 +320,16 @@ export async function launchBrowserWithDependencies(
319
320
  ) {
320
321
  if (options.keepOpen) {
321
322
  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
- });
323
+ return attachLaunchDebug(
324
+ await connectOrLaunch({
325
+ ...options,
326
+ userDataDir:
327
+ options.userDataDir ?? (await createTemporaryUserDataDir()),
328
+ remoteDebuggingPort:
329
+ options.remoteDebuggingPort ?? (await reserveLoopbackPort()),
330
+ }),
331
+ options
332
+ );
328
333
  }
329
334
  sessionPersistencePath(options);
330
335
  if (isSafariChannel(options.channel ?? options.browser)) {
@@ -333,11 +338,17 @@ export async function launchBrowserWithDependencies(
333
338
  launch: options.launch ?? 'real',
334
339
  attach: options.attach,
335
340
  });
336
- return await launchSafari(options, dependencies);
341
+ return attachLaunchDebug(
342
+ await launchSafari(options, dependencies),
343
+ options
344
+ );
337
345
  }
338
346
  const result = await launchNonSafariBrowser(options, dependencies);
339
347
  try {
340
- return await installSessionPersistence(result, options);
348
+ return await attachLaunchDebug(
349
+ await installSessionPersistence(result, options),
350
+ options
351
+ );
341
352
  } catch (error) {
342
353
  await result.close();
343
354
  throw error;