browser-commander 0.17.1 → 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 +7 -18
- package/CHANGELOG.md +48 -0
- package/README.md +84 -0
- package/eslint.config.js +5 -0
- package/package.json +4 -1
- package/scripts/version-and-commit.mjs +13 -2
- 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/scripts/check-release-format.test.js +4 -1
- package/tests/unit/scripts/push-failure-classifier.test.js +153 -0
- package/tests/unit/scripts/push-with-rebase-retry.test.js +302 -0
- 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
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
"fingerprints": {
|
|
4
4
|
"00d381dd611ae8ac": 1,
|
|
5
5
|
"029a62ef1f48e856": 1,
|
|
6
|
+
"03241c1f33f40d14": 1,
|
|
6
7
|
"038170e72e0af1f5": 1,
|
|
7
8
|
"04b28d01d0bb47dc": 1,
|
|
8
9
|
"04f605be138bc350": 1,
|
|
@@ -13,7 +14,6 @@
|
|
|
13
14
|
"0b48b63049c42591": 1,
|
|
14
15
|
"0b8c71391c107a5e": 1,
|
|
15
16
|
"0ba2d165b56db0b3": 1,
|
|
16
|
-
"0c8a29320e45bdfa": 1,
|
|
17
17
|
"0ca2d9c2f2b44d6c": 1,
|
|
18
18
|
"0caa402c4eae32d8": 1,
|
|
19
19
|
"0fd23be8410c2c95": 1,
|
|
@@ -27,9 +27,9 @@
|
|
|
27
27
|
"16340a16ed52849e": 1,
|
|
28
28
|
"17c26c0db2d35ad1": 1,
|
|
29
29
|
"193b46c0e7f72053": 1,
|
|
30
|
-
"1ac9fbd87efdb313": 1,
|
|
31
30
|
"1ad4dc7e19a77f68": 1,
|
|
32
31
|
"1c1baf1bc8ea0eae": 1,
|
|
32
|
+
"1cbd39b416a2b94b": 1,
|
|
33
33
|
"1d61f5c49f12d2dc": 1,
|
|
34
34
|
"1ef570563d4e5a08": 1,
|
|
35
35
|
"1f2104c950b12a30": 1,
|
|
@@ -75,15 +75,12 @@
|
|
|
75
75
|
"4c3b94caa28045d0": 1,
|
|
76
76
|
"4e090d6bb30e6551": 1,
|
|
77
77
|
"4f3c541b3f3e3f23": 1,
|
|
78
|
-
"4fe5663c69a426cb": 1,
|
|
79
|
-
"505d98bd14b63a6c": 1,
|
|
80
78
|
"5085765e1f103f3e": 1,
|
|
81
79
|
"5184c90c9f8406c3": 1,
|
|
82
80
|
"520a28ee2ced56c2": 1,
|
|
83
81
|
"53519ec61a6ba319": 1,
|
|
84
82
|
"53f7e3f3e444a37d": 1,
|
|
85
83
|
"5587ec0cdb7b96e6": 1,
|
|
86
|
-
"559b5241047cd753": 1,
|
|
87
84
|
"562ebfb882d4aa8f": 1,
|
|
88
85
|
"56b5df15ee61887d": 1,
|
|
89
86
|
"56b7675e4ce57335": 1,
|
|
@@ -98,12 +95,13 @@
|
|
|
98
95
|
"5ae9b1430fc38daf": 1,
|
|
99
96
|
"602bfd129bba900e": 1,
|
|
100
97
|
"608e1dd27138ec6f": 1,
|
|
98
|
+
"6098544c77ff17a2": 1,
|
|
101
99
|
"60d0c58a01fc8746": 1,
|
|
102
100
|
"61bcadcd8dc9f63e": 1,
|
|
103
|
-
"63a3b672c8d8b50b": 1,
|
|
104
101
|
"63ff613b1780c67a": 1,
|
|
105
102
|
"642cc89ed49f8a21": 1,
|
|
106
103
|
"6479c3604c153ec4": 1,
|
|
104
|
+
"6699064d6e8e09ba": 1,
|
|
107
105
|
"66fb97eebd7ff0dc": 1,
|
|
108
106
|
"67a95e428b8fa31c": 1,
|
|
109
107
|
"6880d620e0e67229": 1,
|
|
@@ -116,6 +114,7 @@
|
|
|
116
114
|
"6fa62e2642cc73c3": 1,
|
|
117
115
|
"7009c5e9bd61dbe8": 1,
|
|
118
116
|
"709348ea44394b52": 1,
|
|
117
|
+
"72705e6a2212d7b2": 1,
|
|
119
118
|
"731fbc5d4188839a": 1,
|
|
120
119
|
"73dbf56d17ab79ed": 1,
|
|
121
120
|
"760d9925f6e3ca85": 1,
|
|
@@ -138,29 +137,22 @@
|
|
|
138
137
|
"8678919f1350f154": 1,
|
|
139
138
|
"86aae81b1aa0d21b": 1,
|
|
140
139
|
"87484a2744e09748": 1,
|
|
141
|
-
"87514cf1299c9c0e": 1,
|
|
142
140
|
"876d2c8edfc8c40a": 1,
|
|
143
141
|
"8b013ac20e436d76": 1,
|
|
144
142
|
"8c2b1408886c1db1": 1,
|
|
145
|
-
"8c36ed4fc291ae47": 1,
|
|
146
|
-
"8cb9a984bf19a062": 1,
|
|
147
143
|
"8ec18cdbd4a96518": 1,
|
|
148
|
-
"8ecf2e554a04d2ef": 1,
|
|
149
144
|
"91049efadc65169b": 1,
|
|
150
145
|
"9134185980de8a17": 1,
|
|
151
146
|
"916833bafca4fa63": 1,
|
|
152
147
|
"91a538e4a0c28f25": 1,
|
|
153
|
-
"929fba5228e9fdac": 1,
|
|
154
148
|
"92ddafa7e65be7eb": 1,
|
|
155
149
|
"94821a4d1b745dfd": 1,
|
|
156
|
-
"94d89888ac9da9a9": 1,
|
|
157
150
|
"97c5a112fdde7be8": 1,
|
|
158
151
|
"98cc22079feaa004": 1,
|
|
159
152
|
"990d87f21678fa96": 1,
|
|
160
153
|
"9a72b1c2f22156ca": 1,
|
|
161
154
|
"9b6ef4df04562546": 1,
|
|
162
155
|
"9c055dfd90c369bf": 1,
|
|
163
|
-
"9cb19f1b972e811e": 1,
|
|
164
156
|
"9ee57dcd5e7a6691": 1,
|
|
165
157
|
"9fbb8296259dedbe": 1,
|
|
166
158
|
"a0183465782b0b15": 1,
|
|
@@ -173,7 +165,6 @@
|
|
|
173
165
|
"a78d76908162699e": 1,
|
|
174
166
|
"a9bd4ffef6da5faf": 1,
|
|
175
167
|
"a9d71c2e50f1b832": 1,
|
|
176
|
-
"abe5a58f2b2cc593": 1,
|
|
177
168
|
"acc95d694eb2f581": 1,
|
|
178
169
|
"ad4469f2e8131535": 1,
|
|
179
170
|
"ad4cfa76daa1b6fc": 1,
|
|
@@ -186,7 +177,6 @@
|
|
|
186
177
|
"b2a0686231220c8a": 1,
|
|
187
178
|
"b35ff46c365dcd18": 1,
|
|
188
179
|
"b41581af204af40e": 1,
|
|
189
|
-
"b62f3449cde7991d": 1,
|
|
190
180
|
"b71fa5250da28739": 1,
|
|
191
181
|
"b740ecac685a15fe": 1,
|
|
192
182
|
"b8c2d21f2a26f77d": 1,
|
|
@@ -207,6 +197,7 @@
|
|
|
207
197
|
"c415ffcd004d6518": 1,
|
|
208
198
|
"c461a83b51eb7740": 1,
|
|
209
199
|
"c9cf3c28bed33a5b": 1,
|
|
200
|
+
"cab590a1d04b5a05": 1,
|
|
210
201
|
"cbd444e9a6a9072e": 1,
|
|
211
202
|
"cd49f39b43d14635": 1,
|
|
212
203
|
"cebff7d1cc87f646": 1,
|
|
@@ -219,7 +210,6 @@
|
|
|
219
210
|
"d1a4a8e6a523b752": 1,
|
|
220
211
|
"d1e5c2961267399a": 1,
|
|
221
212
|
"d2ed3d02dd6a2d8c": 1,
|
|
222
|
-
"d4eec99f58906a63": 1,
|
|
223
213
|
"d6157a86fea41907": 1,
|
|
224
214
|
"d6fa9884eee815f0": 1,
|
|
225
215
|
"d86bafb915488255": 1,
|
|
@@ -237,7 +227,6 @@
|
|
|
237
227
|
"ea1195048ded896c": 1,
|
|
238
228
|
"ea88711c5deb1fe5": 1,
|
|
239
229
|
"eb9a70c2a21094ff": 1,
|
|
240
|
-
"ebd08637a2d7bcf3": 1,
|
|
241
230
|
"ebe03c5f8c2f5147": 1,
|
|
242
231
|
"ee19c8b9b332ee98": 1,
|
|
243
232
|
"eeca0ccf8d821f21": 1,
|
|
@@ -251,11 +240,11 @@
|
|
|
251
240
|
"f4bfa7e503443cd1": 1,
|
|
252
241
|
"f6678d6748e9c902": 1,
|
|
253
242
|
"f67a07a611f39cb2": 1,
|
|
243
|
+
"f7b1f9d699bcd580": 1,
|
|
254
244
|
"f911821aa7dda47f": 1,
|
|
255
245
|
"f97bf515bfd95829": 1,
|
|
256
246
|
"fae844842fba705b": 1,
|
|
257
247
|
"faf76ec43b2dbbaa": 1,
|
|
258
|
-
"fb3a2e9ace145907": 1,
|
|
259
248
|
"fc6ed7d760c98f51": 1,
|
|
260
249
|
"fccfd8b004c12dd4": 1,
|
|
261
250
|
"fe859df9f97c75d4": 1
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,53 @@
|
|
|
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
|
+
|
|
37
|
+
## 0.17.2
|
|
38
|
+
|
|
39
|
+
### Patch Changes
|
|
40
|
+
|
|
41
|
+
- 539ae64: Recover a release push that lost the race to `main` instead of failing the run.
|
|
42
|
+
|
|
43
|
+
The `main-writer` concurrency group serialises the three release jobs correctly,
|
|
44
|
+
but `actions/checkout` checks out `github.sha`, so every writer after the first
|
|
45
|
+
holds a tree one commit behind `main` and its push is rejected as
|
|
46
|
+
non-fast-forward. Serialisation buys ordering, not freshness. Every push to
|
|
47
|
+
`main` now rebases and retries — after classifying the rejection, because a
|
|
48
|
+
GH006/GH013 ruleset rejection also prints "rejected" and can never be rebased
|
|
49
|
+
away.
|
|
50
|
+
|
|
3
51
|
## 0.17.1
|
|
4
52
|
|
|
5
53
|
### 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",
|
|
@@ -16,6 +16,10 @@ import { readFileSync, appendFileSync, readdirSync } from 'fs';
|
|
|
16
16
|
|
|
17
17
|
import { assertGeneratedFilesAreFormatted } from './check-release-format.mjs';
|
|
18
18
|
|
|
19
|
+
import {
|
|
20
|
+
createGitRunner,
|
|
21
|
+
pushWithRebaseRetry,
|
|
22
|
+
} from '../../scripts/push-with-rebase-retry.mjs';
|
|
19
23
|
import {
|
|
20
24
|
loadCommandStream,
|
|
21
25
|
loadLinoArguments,
|
|
@@ -232,8 +236,15 @@ async function main() {
|
|
|
232
236
|
const escapedMessage = commitMessage.replace(/"/g, '\\"');
|
|
233
237
|
await $`git commit -m "${escapedMessage}"`;
|
|
234
238
|
|
|
235
|
-
// Push directly to main
|
|
236
|
-
|
|
239
|
+
// Push directly to main.
|
|
240
|
+
//
|
|
241
|
+
// The pre-emptive rebase above runs before the version bump, so it
|
|
242
|
+
// cannot cover a writer that lands between the bump and this push.
|
|
243
|
+
// pushWithRebaseRetry closes that window and, just as importantly,
|
|
244
|
+
// distinguishes a lost race from a branch-protection rejection instead
|
|
245
|
+
// of retrying one as if it were the other.
|
|
246
|
+
const git = await createGitRunner();
|
|
247
|
+
await pushWithRebaseRetry({ git, branch: 'main' });
|
|
237
248
|
|
|
238
249
|
console.log('\u2705 Version bump committed and pushed to main');
|
|
239
250
|
setOutput('version_committed', 'true');
|
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,
|