browser-commander 0.17.2 → 0.18.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 +4 -17
- package/CHANGELOG.md +34 -0
- package/README.md +84 -0
- package/eslint.config.js +5 -0
- package/package.json +4 -1
- package/src/bindings.js +13 -1
- package/src/browser/connector.js +15 -3
- package/src/browser/launcher.js +14 -2
- package/src/browser/navigation.js +63 -0
- package/src/core/dialog-manager.js +48 -0
- package/src/core/navigation-manager.js +83 -87
- package/src/core/navigation-readiness.js +133 -0
- package/src/core/readiness.js +478 -0
- package/src/downloads/attach.js +58 -0
- package/src/downloads/destination.js +125 -0
- package/src/downloads/manager.js +453 -0
- package/src/downloads/naming.js +246 -0
- package/src/downloads/sources.js +188 -0
- package/src/downloads/store.js +256 -0
- package/src/exports.js +72 -0
- package/src/factory.js +55 -0
- package/src/interactions/click-activation.js +317 -0
- package/src/interactions/click-result.js +108 -0
- package/src/interactions/click.js +766 -352
- package/src/tests/index.js +75 -11
- package/src/tests/tracing.js +236 -0
- package/src/traces/bundle.js +323 -0
- package/src/traces/observers.js +246 -0
- package/src/traces/page-capture.js +365 -0
- package/src/traces/reader.js +226 -0
- package/src/traces/recorder.js +437 -0
- package/src/traces/redaction.js +231 -0
- package/src/traces/schema.js +165 -0
- package/src/traces/viewer.js +302 -0
- package/tests/e2e/click-readiness.e2e.test.js +240 -0
- package/tests/e2e/downloads.e2e.test.js +188 -0
- package/tests/e2e/traces.e2e.test.js +457 -0
- package/tests/helpers/click-fixtures.js +62 -0
- package/tests/helpers/download-fixtures.js +125 -0
- package/tests/helpers/download-server.js +109 -0
- package/tests/helpers/e2e-browser.js +67 -0
- package/tests/helpers/file-modes.js +31 -0
- package/tests/helpers/fixture-server.js +56 -0
- package/tests/helpers/mocks.js +63 -66
- package/tests/helpers/readiness-server.js +96 -0
- package/tests/helpers/temp-directory.js +44 -0
- package/tests/helpers/trace-fixtures.js +198 -0
- package/tests/helpers/trace-server.js +117 -0
- package/tests/unit/core/dialog-manager.test.js +118 -10
- package/tests/unit/core/navigation-manager.test.js +162 -0
- package/tests/unit/core/readiness.test.js +517 -0
- package/tests/unit/downloads/attach.test.js +220 -0
- package/tests/unit/downloads/destination.test.js +114 -0
- package/tests/unit/downloads/manager.test.js +354 -0
- package/tests/unit/downloads/naming.test.js +174 -0
- package/tests/unit/downloads/store.test.js +215 -0
- package/tests/unit/interactions/click-activation.test.js +283 -0
- package/tests/unit/interactions/click-result.test.js +90 -0
- package/tests/unit/interactions/click.test.js +409 -200
- package/tests/unit/tests/tracing.test.js +299 -0
- package/tests/unit/traces/bundle.test.js +260 -0
- package/tests/unit/traces/reader.test.js +208 -0
- package/tests/unit/traces/recorder.test.js +562 -0
- package/tests/unit/traces/redaction.test.js +170 -0
- package/tests/unit/traces/schema.test.js +92 -0
- package/tests/unit/traces/viewer.test.js +289 -0
package/.jscpd-baseline.json
CHANGED
|
@@ -14,7 +14,6 @@
|
|
|
14
14
|
"0b48b63049c42591": 1,
|
|
15
15
|
"0b8c71391c107a5e": 1,
|
|
16
16
|
"0ba2d165b56db0b3": 1,
|
|
17
|
-
"0c8a29320e45bdfa": 1,
|
|
18
17
|
"0ca2d9c2f2b44d6c": 1,
|
|
19
18
|
"0caa402c4eae32d8": 1,
|
|
20
19
|
"0fd23be8410c2c95": 1,
|
|
@@ -28,7 +27,6 @@
|
|
|
28
27
|
"16340a16ed52849e": 1,
|
|
29
28
|
"17c26c0db2d35ad1": 1,
|
|
30
29
|
"193b46c0e7f72053": 1,
|
|
31
|
-
"1ac9fbd87efdb313": 1,
|
|
32
30
|
"1ad4dc7e19a77f68": 1,
|
|
33
31
|
"1c1baf1bc8ea0eae": 1,
|
|
34
32
|
"1cbd39b416a2b94b": 1,
|
|
@@ -77,15 +75,12 @@
|
|
|
77
75
|
"4c3b94caa28045d0": 1,
|
|
78
76
|
"4e090d6bb30e6551": 1,
|
|
79
77
|
"4f3c541b3f3e3f23": 1,
|
|
80
|
-
"4fe5663c69a426cb": 1,
|
|
81
|
-
"505d98bd14b63a6c": 1,
|
|
82
78
|
"5085765e1f103f3e": 1,
|
|
83
79
|
"5184c90c9f8406c3": 1,
|
|
84
80
|
"520a28ee2ced56c2": 1,
|
|
85
81
|
"53519ec61a6ba319": 1,
|
|
86
82
|
"53f7e3f3e444a37d": 1,
|
|
87
83
|
"5587ec0cdb7b96e6": 1,
|
|
88
|
-
"559b5241047cd753": 1,
|
|
89
84
|
"562ebfb882d4aa8f": 1,
|
|
90
85
|
"56b5df15ee61887d": 1,
|
|
91
86
|
"56b7675e4ce57335": 1,
|
|
@@ -103,10 +98,10 @@
|
|
|
103
98
|
"6098544c77ff17a2": 1,
|
|
104
99
|
"60d0c58a01fc8746": 1,
|
|
105
100
|
"61bcadcd8dc9f63e": 1,
|
|
106
|
-
"63a3b672c8d8b50b": 1,
|
|
107
101
|
"63ff613b1780c67a": 1,
|
|
108
102
|
"642cc89ed49f8a21": 1,
|
|
109
103
|
"6479c3604c153ec4": 1,
|
|
104
|
+
"6699064d6e8e09ba": 1,
|
|
110
105
|
"66fb97eebd7ff0dc": 1,
|
|
111
106
|
"67a95e428b8fa31c": 1,
|
|
112
107
|
"6880d620e0e67229": 1,
|
|
@@ -119,6 +114,7 @@
|
|
|
119
114
|
"6fa62e2642cc73c3": 1,
|
|
120
115
|
"7009c5e9bd61dbe8": 1,
|
|
121
116
|
"709348ea44394b52": 1,
|
|
117
|
+
"72705e6a2212d7b2": 1,
|
|
122
118
|
"731fbc5d4188839a": 1,
|
|
123
119
|
"73dbf56d17ab79ed": 1,
|
|
124
120
|
"760d9925f6e3ca85": 1,
|
|
@@ -141,28 +137,22 @@
|
|
|
141
137
|
"8678919f1350f154": 1,
|
|
142
138
|
"86aae81b1aa0d21b": 1,
|
|
143
139
|
"87484a2744e09748": 1,
|
|
144
|
-
"87514cf1299c9c0e": 1,
|
|
145
140
|
"876d2c8edfc8c40a": 1,
|
|
146
141
|
"8b013ac20e436d76": 1,
|
|
147
142
|
"8c2b1408886c1db1": 1,
|
|
148
|
-
"8cb9a984bf19a062": 1,
|
|
149
143
|
"8ec18cdbd4a96518": 1,
|
|
150
|
-
"8ecf2e554a04d2ef": 1,
|
|
151
144
|
"91049efadc65169b": 1,
|
|
152
145
|
"9134185980de8a17": 1,
|
|
153
146
|
"916833bafca4fa63": 1,
|
|
154
147
|
"91a538e4a0c28f25": 1,
|
|
155
|
-
"929fba5228e9fdac": 1,
|
|
156
148
|
"92ddafa7e65be7eb": 1,
|
|
157
149
|
"94821a4d1b745dfd": 1,
|
|
158
|
-
"94d89888ac9da9a9": 1,
|
|
159
150
|
"97c5a112fdde7be8": 1,
|
|
160
151
|
"98cc22079feaa004": 1,
|
|
161
152
|
"990d87f21678fa96": 1,
|
|
162
153
|
"9a72b1c2f22156ca": 1,
|
|
163
154
|
"9b6ef4df04562546": 1,
|
|
164
155
|
"9c055dfd90c369bf": 1,
|
|
165
|
-
"9cb19f1b972e811e": 1,
|
|
166
156
|
"9ee57dcd5e7a6691": 1,
|
|
167
157
|
"9fbb8296259dedbe": 1,
|
|
168
158
|
"a0183465782b0b15": 1,
|
|
@@ -175,7 +165,6 @@
|
|
|
175
165
|
"a78d76908162699e": 1,
|
|
176
166
|
"a9bd4ffef6da5faf": 1,
|
|
177
167
|
"a9d71c2e50f1b832": 1,
|
|
178
|
-
"abe5a58f2b2cc593": 1,
|
|
179
168
|
"acc95d694eb2f581": 1,
|
|
180
169
|
"ad4469f2e8131535": 1,
|
|
181
170
|
"ad4cfa76daa1b6fc": 1,
|
|
@@ -188,7 +177,6 @@
|
|
|
188
177
|
"b2a0686231220c8a": 1,
|
|
189
178
|
"b35ff46c365dcd18": 1,
|
|
190
179
|
"b41581af204af40e": 1,
|
|
191
|
-
"b62f3449cde7991d": 1,
|
|
192
180
|
"b71fa5250da28739": 1,
|
|
193
181
|
"b740ecac685a15fe": 1,
|
|
194
182
|
"b8c2d21f2a26f77d": 1,
|
|
@@ -209,6 +197,7 @@
|
|
|
209
197
|
"c415ffcd004d6518": 1,
|
|
210
198
|
"c461a83b51eb7740": 1,
|
|
211
199
|
"c9cf3c28bed33a5b": 1,
|
|
200
|
+
"cab590a1d04b5a05": 1,
|
|
212
201
|
"cbd444e9a6a9072e": 1,
|
|
213
202
|
"cd49f39b43d14635": 1,
|
|
214
203
|
"cebff7d1cc87f646": 1,
|
|
@@ -221,7 +210,6 @@
|
|
|
221
210
|
"d1a4a8e6a523b752": 1,
|
|
222
211
|
"d1e5c2961267399a": 1,
|
|
223
212
|
"d2ed3d02dd6a2d8c": 1,
|
|
224
|
-
"d4eec99f58906a63": 1,
|
|
225
213
|
"d6157a86fea41907": 1,
|
|
226
214
|
"d6fa9884eee815f0": 1,
|
|
227
215
|
"d86bafb915488255": 1,
|
|
@@ -239,7 +227,6 @@
|
|
|
239
227
|
"ea1195048ded896c": 1,
|
|
240
228
|
"ea88711c5deb1fe5": 1,
|
|
241
229
|
"eb9a70c2a21094ff": 1,
|
|
242
|
-
"ebd08637a2d7bcf3": 1,
|
|
243
230
|
"ebe03c5f8c2f5147": 1,
|
|
244
231
|
"ee19c8b9b332ee98": 1,
|
|
245
232
|
"eeca0ccf8d821f21": 1,
|
|
@@ -253,11 +240,11 @@
|
|
|
253
240
|
"f4bfa7e503443cd1": 1,
|
|
254
241
|
"f6678d6748e9c902": 1,
|
|
255
242
|
"f67a07a611f39cb2": 1,
|
|
243
|
+
"f7b1f9d699bcd580": 1,
|
|
256
244
|
"f911821aa7dda47f": 1,
|
|
257
245
|
"f97bf515bfd95829": 1,
|
|
258
246
|
"fae844842fba705b": 1,
|
|
259
247
|
"faf76ec43b2dbbaa": 1,
|
|
260
|
-
"fb3a2e9ace145907": 1,
|
|
261
248
|
"fc6ed7d760c98f51": 1,
|
|
262
249
|
"fccfd8b004c12dd4": 1,
|
|
263
250
|
"fe859df9f97c75d4": 1
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,39 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.18.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 0915dd8: Add managed downloads, portable traces and truthful click results.
|
|
8
|
+
|
|
9
|
+
`downloads` is now accepted by `launchBrowser()`, `connectBrowser()` and
|
|
10
|
+
`launchRealBrowser()`, and by `commander.configureDownloads()` on a browser you
|
|
11
|
+
already have. The manager owns the file rather than the page: `capture()` starts
|
|
12
|
+
listening before the action runs so a fast download cannot be missed, a caller
|
|
13
|
+
can choose the saved name and have a bare UUID given the extension its content
|
|
14
|
+
proves, and the file is still there after the context and the browser are gone.
|
|
15
|
+
Failed and cancelled downloads report the failure instead of a path, and
|
|
16
|
+
`browser-commander/tests` keeps captured files in the run's artifact directory.
|
|
17
|
+
|
|
18
|
+
`startTrace()` records a session into a schema-versioned bundle - an ordered
|
|
19
|
+
NDJSON timeline, named checkpoints with full HTML, live control state and frame
|
|
20
|
+
metadata, and the DOM mutation batches between them - which `readTrace()`,
|
|
21
|
+
`diffControlState()` and the offline `writeTraceViewer()` read back with scripts
|
|
22
|
+
and network disabled. Redaction runs before anything is written, a truncated or
|
|
23
|
+
interrupted run still produces a readable partial trace, and
|
|
24
|
+
`browser-commander/tests` supports `trace: 'retain-on-failure'` in place of its
|
|
25
|
+
own artifact mechanism.
|
|
26
|
+
|
|
27
|
+
`clickElement()` now reports what was observed: `status` is `succeeded`,
|
|
28
|
+
`failed`, `timed_out`, `interrupted` or `unverified`, `effect` is `confirmed`,
|
|
29
|
+
`not-observed` or `contradicted`, and `evidence` carries the reason for both.
|
|
30
|
+
The `clicked` and `verified` booleans remain and are derived conservatively, so
|
|
31
|
+
a click that changed nothing is no longer reported as verified. `waitForReady()`
|
|
32
|
+
composes URL, network, DOM, image and custom checks under one deadline and
|
|
33
|
+
returns the same per-check evidence. `noAutoScroll` is deprecated in favour of
|
|
34
|
+
the `scroll` axis; `scroll: 'none'` now fails with a `ScrollConstraintError` on
|
|
35
|
+
engines that cannot click without scrolling instead of scrolling anyway.
|
|
36
|
+
|
|
3
37
|
## 0.17.2
|
|
4
38
|
|
|
5
39
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -599,6 +599,90 @@ await keyDown({ page, engine: 'playwright', key: 'Control' });
|
|
|
599
599
|
await keyUp({ page, engine: 'playwright', key: 'Control' });
|
|
600
600
|
```
|
|
601
601
|
|
|
602
|
+
### Managed Downloads
|
|
603
|
+
|
|
604
|
+
A download that only exists while the browser is open is not a download. Ask for
|
|
605
|
+
`downloads` at any entry point - `launchBrowser()`, `connectBrowser()`,
|
|
606
|
+
`launchRealBrowser()` or `commander.configureDownloads()` - and the manager owns
|
|
607
|
+
the file from then on:
|
|
608
|
+
|
|
609
|
+
```javascript
|
|
610
|
+
const { commander, downloads } = await launchBrowser({
|
|
611
|
+
downloads: { directory: '/tmp/reports', conflict: 'rename' },
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
// capture() starts listening before the action runs, so a download that
|
|
615
|
+
// finishes in 5ms cannot slip past the registration.
|
|
616
|
+
const artifact = await downloads.capture({
|
|
617
|
+
action: () => commander.clickButton({ selector: '#export' }),
|
|
618
|
+
filename: 'q3-report.pdf', // the page's UUID name gets the caller's name
|
|
619
|
+
timeout: 30000,
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
console.log(artifact.path, artifact.bytes, artifact.checksum);
|
|
623
|
+
|
|
624
|
+
await commander.destroy();
|
|
625
|
+
// The file is still there: it outlives the page, the context and the browser.
|
|
626
|
+
```
|
|
627
|
+
|
|
628
|
+
`downloads.on(DOWNLOAD_EVENT.COMPLETED, ...)` observes every download in the
|
|
629
|
+
session, including one a person started by hand in a visible browser, and each
|
|
630
|
+
download is reported exactly once whether it was captured or merely observed. A
|
|
631
|
+
failed or cancelled download raises the failure rather than returning a path.
|
|
632
|
+
|
|
633
|
+
### Portable Traces
|
|
634
|
+
|
|
635
|
+
`startTrace()` records a session into a schema-versioned bundle any of the three
|
|
636
|
+
languages can read - the manifest, an ordered NDJSON timeline, per-checkpoint
|
|
637
|
+
DOM snapshots and the mutation batches between them:
|
|
638
|
+
|
|
639
|
+
```javascript
|
|
640
|
+
import { startTrace, readTrace, writeTraceViewer } from 'browser-commander';
|
|
641
|
+
|
|
642
|
+
const trace = await startTrace({
|
|
643
|
+
commander,
|
|
644
|
+
output: '/tmp/traces/checkout',
|
|
645
|
+
mode: 'continuous', // keeps DOM mutations between checkpoints
|
|
646
|
+
privacy: { redact: [/password/i, /token/i] },
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
await commander.goto({ url: 'https://example.com/checkout' });
|
|
650
|
+
await trace.checkpoint('cart');
|
|
651
|
+
await commander.clickButton({ selector: '#pay' });
|
|
652
|
+
await trace.checkpoint('paid');
|
|
653
|
+
|
|
654
|
+
const { path } = await trace.stop();
|
|
655
|
+
const bundle = await readTrace(path);
|
|
656
|
+
await writeTraceViewer(bundle, '/tmp/traces/checkout/viewer.html');
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
Redaction runs before anything reaches disk, and a run cut short by a closed
|
|
660
|
+
page or a size limit still leaves a readable partial trace. The viewer seeks,
|
|
661
|
+
diffs and replays the captured DOM with scripts and network disabled.
|
|
662
|
+
|
|
663
|
+
### Truthful Click Results
|
|
664
|
+
|
|
665
|
+
`clickElement()` reports what was observed, not what was attempted:
|
|
666
|
+
|
|
667
|
+
```javascript
|
|
668
|
+
const result = await clickElement({
|
|
669
|
+
page,
|
|
670
|
+
engine,
|
|
671
|
+
locatorOrElement: '#submit',
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
result.status; // 'succeeded' | 'failed' | 'timed_out' | 'interrupted' | 'unverified'
|
|
675
|
+
result.effect; // 'confirmed' | 'not-observed' | 'contradicted'
|
|
676
|
+
result.evidence; // why status and effect say what they say
|
|
677
|
+
result.clicked; // still here: whether the click reached the element
|
|
678
|
+
result.verified; // still here, now derived from effect === 'confirmed'
|
|
679
|
+
```
|
|
680
|
+
|
|
681
|
+
The `scroll` axis (`auto`, `preserve`, `none`) replaces the deprecated
|
|
682
|
+
`noAutoScroll` flag. `scroll: 'none'` never scrolls: on an engine that cannot
|
|
683
|
+
deliver a click without scrolling it throws `ScrollConstraintError` naming the
|
|
684
|
+
alternatives, rather than scrolling the page and reporting success.
|
|
685
|
+
|
|
602
686
|
### commander.destroy()
|
|
603
687
|
|
|
604
688
|
```javascript
|
package/eslint.config.js
CHANGED
|
@@ -22,6 +22,7 @@ export default [
|
|
|
22
22
|
__filename: 'readonly',
|
|
23
23
|
// Node.js 18+ globals
|
|
24
24
|
fetch: 'readonly',
|
|
25
|
+
performance: 'readonly',
|
|
25
26
|
// Web/Node.js shared globals
|
|
26
27
|
setTimeout: 'readonly',
|
|
27
28
|
clearTimeout: 'readonly',
|
|
@@ -50,6 +51,10 @@ export default [
|
|
|
50
51
|
// Chrome verbatim rather than executed by Node).
|
|
51
52
|
document: 'readonly',
|
|
52
53
|
window: 'readonly',
|
|
54
|
+
location: 'readonly',
|
|
55
|
+
MutationObserver: 'readonly',
|
|
56
|
+
DOMParser: 'readonly',
|
|
57
|
+
Node: 'readonly',
|
|
53
58
|
navigator: 'readonly',
|
|
54
59
|
Navigator: 'readonly',
|
|
55
60
|
screen: 'readonly',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "browser-commander",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "Universal browser automation library that supports both Playwright and Puppeteer with a unified API",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -16,6 +16,9 @@
|
|
|
16
16
|
"test:e2e:playwright": "node scripts/run-tests.mjs --env RUN_E2E=true tests/e2e/playwright.e2e.test.js",
|
|
17
17
|
"test:e2e:puppeteer": "node scripts/run-tests.mjs --env RUN_E2E=true tests/e2e/puppeteer.e2e.test.js",
|
|
18
18
|
"test:e2e:parity": "node scripts/run-tests.mjs --env RUN_E2E=true tests/e2e/fingerprint-parity.e2e.test.js",
|
|
19
|
+
"test:e2e:click-readiness": "node scripts/run-tests.mjs --env RUN_E2E=true tests/e2e/click-readiness.e2e.test.js",
|
|
20
|
+
"test:e2e:downloads": "node scripts/run-tests.mjs --env RUN_E2E=true tests/e2e/downloads.e2e.test.js",
|
|
21
|
+
"test:e2e:traces": "node scripts/run-tests.mjs --env RUN_E2E=true tests/e2e/traces.e2e.test.js",
|
|
19
22
|
"test:all": "npm run test:unit && npm run test:e2e",
|
|
20
23
|
"lint": "eslint .",
|
|
21
24
|
"lint:fix": "eslint . --fix",
|
package/src/bindings.js
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
setContent,
|
|
12
12
|
waitForNavigation,
|
|
13
13
|
waitForPageReady,
|
|
14
|
+
waitForReady,
|
|
14
15
|
waitAfterAction,
|
|
15
16
|
} from './browser/navigation.js';
|
|
16
17
|
import { emulateMedia } from './browser/media.js';
|
|
@@ -137,6 +138,15 @@ export function createBoundFunctions(options = {}) {
|
|
|
137
138
|
log,
|
|
138
139
|
wait: waitBound,
|
|
139
140
|
});
|
|
141
|
+
const waitForReadyBound = (opts) =>
|
|
142
|
+
waitForReady({
|
|
143
|
+
...opts,
|
|
144
|
+
page,
|
|
145
|
+
engine,
|
|
146
|
+
navigationManager,
|
|
147
|
+
networkTracker,
|
|
148
|
+
log,
|
|
149
|
+
});
|
|
140
150
|
const waitAfterActionBound = (opts) =>
|
|
141
151
|
waitAfterAction({
|
|
142
152
|
...opts,
|
|
@@ -199,7 +209,8 @@ export function createBoundFunctions(options = {}) {
|
|
|
199
209
|
scrollIntoViewIfNeeded({ ...opts, page, engine, wait: waitBound, log });
|
|
200
210
|
|
|
201
211
|
// Bound click - now navigation-aware
|
|
202
|
-
const clickElementBound = (opts) =>
|
|
212
|
+
const clickElementBound = (opts) =>
|
|
213
|
+
clickElement({ ...opts, page, engine, log });
|
|
203
214
|
const clickButtonBound = (opts) =>
|
|
204
215
|
clickButton({
|
|
205
216
|
...opts,
|
|
@@ -323,6 +334,7 @@ export function createBoundFunctions(options = {}) {
|
|
|
323
334
|
getUrl: getUrlBound,
|
|
324
335
|
waitForNavigation: waitForNavigationBound,
|
|
325
336
|
waitForPageReady: waitForPageReadyBound,
|
|
337
|
+
waitForReady: waitForReadyBound,
|
|
326
338
|
waitAfterAction: waitAfterActionBound,
|
|
327
339
|
getAttribute: getAttributeWrapped,
|
|
328
340
|
isVisible: isVisibleWrapped,
|
package/src/browser/connector.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { attachDownloads } from '../downloads/attach.js';
|
|
1
2
|
import {
|
|
2
3
|
loadStorageState,
|
|
3
4
|
restorePlaywrightStorageState,
|
|
@@ -109,7 +110,8 @@ async function connectPuppeteer({ options, loadPuppeteer, storageState }) {
|
|
|
109
110
|
* @param {Object[]|string|Object} [options.storageState] - Playwright-compatible state path or object
|
|
110
111
|
* @param {Object[]} [options.seedCookies] - Cookies to seed after connecting
|
|
111
112
|
* @param {boolean} [options.verbose=false] - Enable connection logging
|
|
112
|
-
* @
|
|
113
|
+
* @param {boolean|Object} [options.downloads] - Manage downloads: true for defaults, or {directory, persist, conflict}
|
|
114
|
+
* @returns {Promise<{browser: Object, page: Object, downloads: Object|null}>} Raw handles and the download manager when one was requested
|
|
113
115
|
*/
|
|
114
116
|
export async function connectBrowser(options = {}) {
|
|
115
117
|
return await connectBrowserWithDependencies(options);
|
|
@@ -120,7 +122,7 @@ export async function connectBrowser(options = {}) {
|
|
|
120
122
|
*
|
|
121
123
|
* @param {Object} options - See {@link connectBrowser}
|
|
122
124
|
* @param {Object} dependencies - Optional engine module loaders
|
|
123
|
-
* @returns {Promise<{browser: Object, page: Object}>} Raw
|
|
125
|
+
* @returns {Promise<{browser: Object, page: Object, downloads: Object|null}>} Raw handles and the download manager when one was requested
|
|
124
126
|
*/
|
|
125
127
|
export async function connectBrowserWithDependencies(
|
|
126
128
|
options = {},
|
|
@@ -157,7 +159,17 @@ export async function connectBrowserWithDependencies(
|
|
|
157
159
|
if (verbose) {
|
|
158
160
|
console.log(`Connected to browser with ${engine} engine`);
|
|
159
161
|
}
|
|
160
|
-
|
|
162
|
+
|
|
163
|
+
// An attached browser gets the same managed lifecycle as a launched one:
|
|
164
|
+
// the manager is built from the browser and page, not from how we got them.
|
|
165
|
+
const downloads = await attachDownloads({
|
|
166
|
+
engine,
|
|
167
|
+
browser: result.browser,
|
|
168
|
+
page: result.page,
|
|
169
|
+
downloads: normalizedOptions.downloads,
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
return { ...result, downloads };
|
|
161
173
|
}
|
|
162
174
|
|
|
163
175
|
export { buildPlaywrightConnectOptions, buildPuppeteerConnectOptions };
|
package/src/browser/launcher.js
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
resolveChromeArgs,
|
|
9
9
|
} from './launch-options.js';
|
|
10
10
|
import { applyFingerprint } from '../fingerprint/apply.js';
|
|
11
|
+
import { attachDownloads } from '../downloads/attach.js';
|
|
11
12
|
import {
|
|
12
13
|
loadStorageState,
|
|
13
14
|
restorePlaywrightStorageState,
|
|
@@ -31,7 +32,8 @@ import {
|
|
|
31
32
|
* @param {string|Object} [options.storageState] - Playwright-compatible storage state path or object
|
|
32
33
|
* @param {Object} [options.fingerprint] - Environment fields to present to pages, such as userAgent, timezone, locale, hardwareConcurrency or screen. Applied over CDP after launch; see src/fingerprint/profile.js for the full field list and presets.js for ready-made profiles.
|
|
33
34
|
* @param {boolean} [options.automationParity=true] - Keep the command line indistinguishable from a hand-started Chrome. Turning this off restores the pre-parity switches and makes navigator.webdriver true.
|
|
34
|
-
* @
|
|
35
|
+
* @param {boolean|Object} [options.downloads] - Manage downloads: true for defaults, or {directory, persist, conflict}. The directory may be an absolute path, 'user-downloads' or 'temporary'.
|
|
36
|
+
* @returns {Promise<{browser: Object, page: Object, downloads: Object|null}>} - Browser, page and the download manager when one was requested
|
|
35
37
|
*/
|
|
36
38
|
export async function launchBrowser(options = {}) {
|
|
37
39
|
const {
|
|
@@ -49,6 +51,7 @@ export async function launchBrowser(options = {}) {
|
|
|
49
51
|
storageState,
|
|
50
52
|
fingerprint,
|
|
51
53
|
automationParity = true,
|
|
54
|
+
downloads,
|
|
52
55
|
} = options;
|
|
53
56
|
|
|
54
57
|
const resolvedChromeArgs = resolveChromeArgs({
|
|
@@ -169,5 +172,14 @@ export async function launchBrowser(options = {}) {
|
|
|
169
172
|
}
|
|
170
173
|
}
|
|
171
174
|
|
|
172
|
-
|
|
175
|
+
// Downloads are armed before the caller can navigate: a download triggered
|
|
176
|
+
// by the first page load still lands in the managed directory.
|
|
177
|
+
const downloadManager = await attachDownloads({
|
|
178
|
+
engine,
|
|
179
|
+
browser,
|
|
180
|
+
page,
|
|
181
|
+
downloads,
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
return { browser, page, downloads: downloadManager };
|
|
173
185
|
}
|
|
@@ -8,6 +8,13 @@
|
|
|
8
8
|
import { TIMING } from '../core/constants.js';
|
|
9
9
|
import { isNavigationError } from '../core/navigation-safety.js';
|
|
10
10
|
import { isActionStoppedError } from '../core/page-trigger-manager.js';
|
|
11
|
+
import {
|
|
12
|
+
READINESS_STATUS,
|
|
13
|
+
createDeadline,
|
|
14
|
+
networkIdleFor,
|
|
15
|
+
runReadinessChecks,
|
|
16
|
+
urlStableFor,
|
|
17
|
+
} from '../core/readiness.js';
|
|
11
18
|
|
|
12
19
|
/**
|
|
13
20
|
* Default verification function for navigation operations.
|
|
@@ -492,6 +499,62 @@ export async function waitForPageReady(options = {}) {
|
|
|
492
499
|
return true;
|
|
493
500
|
}
|
|
494
501
|
|
|
502
|
+
/**
|
|
503
|
+
* Wait for the page to be ready and return structured evidence.
|
|
504
|
+
*
|
|
505
|
+
* Unlike {@link waitForPageReady}, which answers with a single boolean, this
|
|
506
|
+
* reports which checks passed, which failed, which were skipped and which never
|
|
507
|
+
* got to run before the shared deadline expired.
|
|
508
|
+
*
|
|
509
|
+
* @param {Object} options - Configuration options
|
|
510
|
+
* @param {Object} options.page - Browser page object
|
|
511
|
+
* @param {string} [options.engine] - Engine type
|
|
512
|
+
* @param {Object} [options.navigationManager] - NavigationManager instance
|
|
513
|
+
* @param {Object} [options.networkTracker] - NetworkTracker instance
|
|
514
|
+
* @param {Function} [options.log] - Logger instance
|
|
515
|
+
* @param {number} [options.timeout=30000] - Total budget in milliseconds
|
|
516
|
+
* @param {string} [options.reason='page ready'] - Reason for waiting
|
|
517
|
+
* @param {Array} [options.checks] - Composable readiness checks
|
|
518
|
+
* @returns {Promise<Object>} Structured readiness result
|
|
519
|
+
*/
|
|
520
|
+
export async function waitForReady(options = {}) {
|
|
521
|
+
const {
|
|
522
|
+
page,
|
|
523
|
+
engine,
|
|
524
|
+
navigationManager,
|
|
525
|
+
networkTracker,
|
|
526
|
+
log = { debug: () => {} },
|
|
527
|
+
timeout = 30000,
|
|
528
|
+
reason = 'page ready',
|
|
529
|
+
checks,
|
|
530
|
+
} = options;
|
|
531
|
+
|
|
532
|
+
if (navigationManager?.waitForReady) {
|
|
533
|
+
return navigationManager.waitForReady({ timeout, reason, checks });
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
const deadline = createDeadline({ timeout });
|
|
537
|
+
const result = await runReadinessChecks({
|
|
538
|
+
checks: checks ?? [urlStableFor(), networkIdleFor()],
|
|
539
|
+
deadline,
|
|
540
|
+
context: {
|
|
541
|
+
page,
|
|
542
|
+
engine,
|
|
543
|
+
log,
|
|
544
|
+
networkTracker,
|
|
545
|
+
getAdapter: async () => {
|
|
546
|
+
const { createEngineAdapter } =
|
|
547
|
+
await import('../core/engine-adapter.js');
|
|
548
|
+
return createEngineAdapter(page, engine);
|
|
549
|
+
},
|
|
550
|
+
},
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
return { ...result, reason, url: page?.url?.() ?? null };
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
export { READINESS_STATUS };
|
|
557
|
+
|
|
495
558
|
/**
|
|
496
559
|
* Wait for any ongoing navigation and network requests to complete.
|
|
497
560
|
* Use this after actions that might trigger navigation (like clicks).
|
|
@@ -28,6 +28,12 @@ export function createDialogManager(options = {}) {
|
|
|
28
28
|
// User-registered dialog handlers
|
|
29
29
|
const handlers = [];
|
|
30
30
|
|
|
31
|
+
// Passive observers. They are told about every dialog and are never
|
|
32
|
+
// responsible for answering one, so watching dialogs - which is what a
|
|
33
|
+
// trace recorder does - cannot leave a page waiting forever for an answer
|
|
34
|
+
// nobody was going to give.
|
|
35
|
+
const observers = [];
|
|
36
|
+
|
|
31
37
|
// Whether we are currently listening to the page's dialog event
|
|
32
38
|
let isListening = false;
|
|
33
39
|
|
|
@@ -45,6 +51,14 @@ export function createDialogManager(options = {}) {
|
|
|
45
51
|
|
|
46
52
|
log.debug(() => `💬 Dialog event: type="${type}", message="${message}"`);
|
|
47
53
|
|
|
54
|
+
for (const observer of observers) {
|
|
55
|
+
try {
|
|
56
|
+
await observer(dialog);
|
|
57
|
+
} catch (e) {
|
|
58
|
+
log.debug(() => `⚠️ Error in dialog observer: ${e.message}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
48
62
|
if (handlers.length === 0) {
|
|
49
63
|
log.debug(
|
|
50
64
|
() =>
|
|
@@ -121,6 +135,36 @@ export function createDialogManager(options = {}) {
|
|
|
121
135
|
log.debug(() => '🔌 All dialog handlers cleared');
|
|
122
136
|
}
|
|
123
137
|
|
|
138
|
+
/**
|
|
139
|
+
* Watch dialogs without taking responsibility for answering them
|
|
140
|
+
* @param {Function} observer - Async function receiving the dialog object
|
|
141
|
+
* @returns {Function} - Removes this observer again
|
|
142
|
+
*/
|
|
143
|
+
function observeDialogs(observer) {
|
|
144
|
+
if (typeof observer !== 'function') {
|
|
145
|
+
throw new Error('Dialog observer must be a function');
|
|
146
|
+
}
|
|
147
|
+
observers.push(observer);
|
|
148
|
+
log.debug(
|
|
149
|
+
() => `👁 Dialog observer registered (total: ${observers.length})`
|
|
150
|
+
);
|
|
151
|
+
return () => unobserveDialogs(observer);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Remove a dialog observer
|
|
156
|
+
* @param {Function} observer - The observer to remove
|
|
157
|
+
*/
|
|
158
|
+
function unobserveDialogs(observer) {
|
|
159
|
+
const index = observers.indexOf(observer);
|
|
160
|
+
if (index !== -1) {
|
|
161
|
+
observers.splice(index, 1);
|
|
162
|
+
log.debug(
|
|
163
|
+
() => `👁 Dialog observer removed (remaining: ${observers.length})`
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
124
168
|
/**
|
|
125
169
|
* Start listening for dialog events on the page
|
|
126
170
|
*/
|
|
@@ -151,6 +195,10 @@ export function createDialogManager(options = {}) {
|
|
|
151
195
|
offDialog,
|
|
152
196
|
clearDialogHandlers,
|
|
153
197
|
|
|
198
|
+
// Passive observation (issue #87)
|
|
199
|
+
observeDialogs,
|
|
200
|
+
unobserveDialogs,
|
|
201
|
+
|
|
154
202
|
// Lifecycle
|
|
155
203
|
startListening,
|
|
156
204
|
stopListening,
|