browser-commander 0.27.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.
- package/.jscpd-baseline.json +5 -19
- package/CHANGELOG.md +22 -0
- package/README.md +23 -9
- package/eslint.config.js +11 -0
- package/package.json +5 -2
- package/src/browser/browser-profile-files.js +5 -2
- package/src/browser/connector.js +215 -36
- 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 +432 -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-adoption.js +96 -0
- package/src/browser/session-watchdog.js +71 -0
- package/src/browser/session-worker.js +82 -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 +69 -1
- package/src/cli/commands.js +116 -2
- package/src/cli/high-level-methods.js +47 -3
- package/src/cli/sessions.js +14 -3
- package/src/core/commander-features.js +110 -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 +52 -0
- package/src/core/page-trigger-manager.js +280 -156
- 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 +4 -0
- package/src/interactions/click.js +44 -0
- package/src/interactions/overlays.js +73 -0
- package/src/traces/bundle.js +55 -8
- package/src/traces/checkpoints.js +103 -0
- package/src/traces/links.js +157 -2
- package/src/traces/mutation-stream.js +9 -2
- package/src/traces/network.js +309 -0
- package/src/traces/page-capture.js +203 -68
- package/src/traces/reader.js +8 -10
- package/src/traces/recorder.js +253 -247
- package/src/traces/redaction.js +89 -6
- package/src/traces/render.js +168 -0
- package/src/traces/rolling-bundle.js +240 -0
- package/src/traces/rolling.js +77 -0
- package/src/traces/start-cleanup.js +15 -0
- package/src/traces/stop.js +160 -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/e2e/issue-160.e2e.test.js +291 -0
- package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0002.html +1 -1
- package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0003.html +1 -1
- package/tests/fixtures/traces/conformance/expected/bundle/events.ndjson +2 -2
- package/tests/fixtures/traces/conformance/expected/bundle/manifest.json +4 -0
- package/tests/fixtures/traces/conformance/expected/bundle/viewer.html +1 -1
- package/tests/fixtures/traces/conformance/expected/trace.lino +2 -2
- package/tests/unit/browser/issue-160-connector.test.js +157 -0
- 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/interrupted-navigation.test.js +32 -0
- package/tests/unit/core/readiness.test.js +21 -0
- package/tests/unit/core/trigger-readiness.test.js +39 -0
- package/tests/unit/core/trigger-stop-race.test.js +80 -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/interactions/overlays.test.js +58 -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/issue-160-privacy.test.js +110 -0
- package/tests/unit/traces/issue-160-rotation.test.js +140 -0
- package/tests/unit/traces/network.test.js +149 -0
package/.jscpd-baseline.json
CHANGED
|
@@ -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,27 @@
|
|
|
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
|
+
|
|
9
|
+
## 0.28.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- cc2a209: Add portable screenshots, bounded recordings and GIF/APNG/WebP encoding, trace
|
|
14
|
+
rendering and summaries, full/text DOM links, bounded redacted network/HAR,
|
|
15
|
+
continuous trace rotation/gzip, and detached reusable browser sessions with idle
|
|
16
|
+
shutdown. Add early trigger readiness, explicit trigger concurrency, visible
|
|
17
|
+
counts and configured overlay dismissal. Fix navigation-interrupted waits,
|
|
18
|
+
Trusted Types capture, Chrome panel restrictions and deterministic tab selection.
|
|
19
|
+
Close ordinary launched CLI browsers even when their connection supports detach;
|
|
20
|
+
only explicitly kept-open sessions survive dispatcher cleanup.
|
|
21
|
+
Add stable viewport capture with native-view CDP, viewport-only engine fallbacks
|
|
22
|
+
and displayed-window regression coverage; document full-page compositor effects.
|
|
23
|
+
Preserve readiness timer expiry when the rounded clock still reports budget.
|
|
24
|
+
|
|
3
25
|
## 0.27.0
|
|
4
26
|
|
|
5
27
|
### 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](
|
|
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](
|
|
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](
|
|
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: '
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
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,6 +812,10 @@ failed or cancelled download raises the failure rather than returning a path.
|
|
|
802
812
|
|
|
803
813
|
### Portable Traces
|
|
804
814
|
|
|
815
|
+
See [capture, debugging and reusable sessions](https://github.com/link-foundation/browser-commander/blob/main/docs/capture-and-debugging.md)
|
|
816
|
+
for screenshots, recordings, full/text DOM links, network/HAR, rotation,
|
|
817
|
+
persistent browsers, early trigger readiness and engine capability limits.
|
|
818
|
+
|
|
805
819
|
`startTrace()` records a session into a schema-versioned bundle any of the three
|
|
806
820
|
languages can read - the manifest, an ordered NDJSON timeline, per-checkpoint
|
|
807
821
|
DOM snapshots and the mutation batches between them:
|
|
@@ -1024,7 +1038,7 @@ This is the **official extensibility mechanism** while awaiting browser-commande
|
|
|
1024
1038
|
|
|
1025
1039
|
## Command Line
|
|
1026
1040
|
|
|
1027
|
-
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](
|
|
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`.
|
|
1028
1042
|
|
|
1029
1043
|
```bash
|
|
1030
1044
|
npx browser-commander version
|
|
@@ -1064,4 +1078,4 @@ See [src/ARCHITECTURE.md](src/ARCHITECTURE.md) for detailed architecture documen
|
|
|
1064
1078
|
|
|
1065
1079
|
[UNLICENSE](../LICENSE)
|
|
1066
1080
|
|
|
1067
|
-
See [navigation budgets, launch diagnostics, reusable helpers and sessions](
|
|
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/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.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": {
|
|
@@ -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",
|
|
@@ -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(
|
|
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
|
}
|
package/src/browser/connector.js
CHANGED
|
@@ -45,13 +45,73 @@ async function connectSelenium(options, dependencies) {
|
|
|
45
45
|
...options,
|
|
46
46
|
debuggerAddress: new URL(options.cdpEndpoint).host,
|
|
47
47
|
});
|
|
48
|
+
try {
|
|
49
|
+
await selectWebDriverTab(connected.driver, options);
|
|
50
|
+
} catch (error) {
|
|
51
|
+
await ignoreCleanup(() => connected.close());
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
48
54
|
return {
|
|
49
55
|
...connected,
|
|
50
56
|
browser: connected.driver,
|
|
51
57
|
disconnect: connected.close,
|
|
58
|
+
detach: connected.close,
|
|
52
59
|
};
|
|
53
60
|
}
|
|
54
61
|
|
|
62
|
+
async function selectWebDriverTab(driver, options) {
|
|
63
|
+
if (!driver?.getAllWindowHandles) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const original = await driver.getWindowHandle();
|
|
67
|
+
const handles = await driver.getAllWindowHandles();
|
|
68
|
+
let selected = original;
|
|
69
|
+
const matchers = urlMatchers(options);
|
|
70
|
+
if (options.targetId || matchers.length) {
|
|
71
|
+
selected = null;
|
|
72
|
+
const tabs = [];
|
|
73
|
+
for (const handle of handles) {
|
|
74
|
+
if (
|
|
75
|
+
options.targetId &&
|
|
76
|
+
!options.fallback &&
|
|
77
|
+
handle !== options.targetId &&
|
|
78
|
+
handle !== `CDwindow-${options.targetId}`
|
|
79
|
+
) {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
await driver.switchTo().window(handle);
|
|
83
|
+
const url = await driver.getCurrentUrl();
|
|
84
|
+
tabs.push({ handle, url });
|
|
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;
|
|
98
|
+
if (!selected) {
|
|
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];
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (options.singleTab) {
|
|
107
|
+
for (const handle of handles.filter((handle) => handle !== selected)) {
|
|
108
|
+
await driver.switchTo().window(handle);
|
|
109
|
+
await driver.close();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
await driver.switchTo().window(selected);
|
|
113
|
+
}
|
|
114
|
+
|
|
55
115
|
function addDefinedOptions(options, values) {
|
|
56
116
|
for (const [key, value] of Object.entries(values)) {
|
|
57
117
|
if (value !== undefined) {
|
|
@@ -61,8 +121,13 @@ function addDefinedOptions(options, values) {
|
|
|
61
121
|
return options;
|
|
62
122
|
}
|
|
63
123
|
|
|
64
|
-
function buildPlaywrightConnectOptions({
|
|
65
|
-
|
|
124
|
+
function buildPlaywrightConnectOptions({
|
|
125
|
+
slowMo,
|
|
126
|
+
timeout,
|
|
127
|
+
headers,
|
|
128
|
+
noDefaults,
|
|
129
|
+
}) {
|
|
130
|
+
return addDefinedOptions({}, { slowMo, timeout, headers, noDefaults });
|
|
66
131
|
}
|
|
67
132
|
|
|
68
133
|
function buildPuppeteerConnectOptions({
|
|
@@ -83,6 +148,55 @@ function buildPuppeteerConnectOptions({
|
|
|
83
148
|
);
|
|
84
149
|
}
|
|
85
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
|
+
|
|
86
200
|
async function prepareStorageState({ storageState, seedCookies }) {
|
|
87
201
|
if (seedCookies !== undefined && !Array.isArray(seedCookies)) {
|
|
88
202
|
throw new TypeError('seedCookies must be an array');
|
|
@@ -111,37 +225,92 @@ async function prepareStorageState({ storageState, seedCookies }) {
|
|
|
111
225
|
* @param {Object[]} pages - Engine page handles
|
|
112
226
|
* @returns {Promise<Object|undefined>}
|
|
113
227
|
*/
|
|
114
|
-
export async function pickForegroundPage(pages) {
|
|
228
|
+
export async function pickForegroundPage(pages, options = {}) {
|
|
229
|
+
const targets = new Map();
|
|
230
|
+
// Playwright emulates focus on every attached tab. Remove the emulation
|
|
231
|
+
// before observing visibility; DevTools target order has no active-tab flag.
|
|
115
232
|
for (const page of pages) {
|
|
116
|
-
|
|
117
|
-
|
|
233
|
+
let session;
|
|
234
|
+
try {
|
|
235
|
+
session =
|
|
236
|
+
(await page.context?.().newCDPSession?.(page)) ??
|
|
237
|
+
(await page.target?.().createCDPSession?.());
|
|
238
|
+
if (!session) {
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
const { targetInfo } = await session.send('Target.getTargetInfo');
|
|
242
|
+
targets.set(page, targetInfo.targetId);
|
|
243
|
+
await session.send('Emulation.setFocusEmulationEnabled', {
|
|
244
|
+
enabled: false,
|
|
245
|
+
});
|
|
246
|
+
} catch {
|
|
247
|
+
// Non-Chromium engines do not expose CDP. Explicit URL matching still
|
|
248
|
+
// works there, and normal visibility is not affected by this emulation.
|
|
249
|
+
} finally {
|
|
250
|
+
await ignoreCleanup(() => session?.detach?.());
|
|
118
251
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
252
|
+
}
|
|
253
|
+
let selected = requestedPage(pages, targets, options);
|
|
254
|
+
if (!selected) {
|
|
255
|
+
for (const page of pages) {
|
|
256
|
+
if (typeof page?.evaluate !== 'function') {
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
const state = await page
|
|
260
|
+
.evaluate(() => document.visibilityState)
|
|
261
|
+
.catch(() => null);
|
|
262
|
+
if (state === 'visible') {
|
|
263
|
+
selected = page;
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
124
266
|
}
|
|
125
267
|
}
|
|
126
|
-
|
|
268
|
+
selected ??= pages[0];
|
|
269
|
+
if (options.singleTab && selected) {
|
|
270
|
+
for (const page of pages) {
|
|
271
|
+
if (page !== selected) {
|
|
272
|
+
await page.close();
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return selected;
|
|
127
277
|
}
|
|
128
278
|
|
|
129
279
|
async function connectPlaywright({ options, loadPlaywright, storageState }) {
|
|
130
280
|
const { chromium } = await loadPlaywright();
|
|
131
281
|
const endpoint = options.cdpEndpoint ?? options.wsEndpoint;
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
+
});
|
|
139
298
|
}
|
|
140
|
-
|
|
141
|
-
|
|
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());
|
|
142
307
|
|
|
143
|
-
|
|
144
|
-
|
|
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
|
+
}
|
|
145
314
|
}
|
|
146
315
|
|
|
147
316
|
async function connectPuppeteer({ options, loadPuppeteer, storageState }) {
|
|
@@ -150,12 +319,17 @@ async function connectPuppeteer({ options, loadPuppeteer, storageState }) {
|
|
|
150
319
|
const browser = await puppeteer.connect(
|
|
151
320
|
buildPuppeteerConnectOptions(options)
|
|
152
321
|
);
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
322
|
+
try {
|
|
323
|
+
const page =
|
|
324
|
+
(await pickForegroundPage(await browser.pages(), options)) ??
|
|
325
|
+
(await browser.newPage());
|
|
156
326
|
|
|
157
|
-
|
|
158
|
-
|
|
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
|
+
}
|
|
159
333
|
}
|
|
160
334
|
|
|
161
335
|
/**
|
|
@@ -209,7 +383,7 @@ export async function connectBrowserWithDependencies(
|
|
|
209
383
|
});
|
|
210
384
|
return { ...result, downloads };
|
|
211
385
|
} catch (error) {
|
|
212
|
-
await result.close();
|
|
386
|
+
await ignoreCleanup(() => result.close());
|
|
213
387
|
throw error;
|
|
214
388
|
}
|
|
215
389
|
}
|
|
@@ -242,14 +416,19 @@ export async function connectBrowserWithDependencies(
|
|
|
242
416
|
|
|
243
417
|
// An attached browser gets the same managed lifecycle as a launched one:
|
|
244
418
|
// the manager is built from the browser and page, not from how we got them.
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
419
|
+
try {
|
|
420
|
+
const downloads = await attachDownloads({
|
|
421
|
+
engine,
|
|
422
|
+
browser: result.browser,
|
|
423
|
+
page: result.page,
|
|
424
|
+
downloads: normalizedOptions.downloads,
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
return { ...result, downloads };
|
|
428
|
+
} catch (error) {
|
|
429
|
+
await ignoreCleanup(() => result.detach?.());
|
|
430
|
+
throw error;
|
|
431
|
+
}
|
|
253
432
|
}
|
|
254
433
|
|
|
255
434
|
export { buildPlaywrightConnectOptions, buildPuppeteerConnectOptions };
|
|
@@ -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
|
]),
|