browser-commander 0.17.2 → 0.19.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 +12 -17
- package/CHANGELOG.md +68 -0
- package/README.md +132 -0
- package/eslint.config.js +5 -0
- package/package.json +5 -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 +476 -0
- package/src/downloads/naming.js +246 -0
- package/src/downloads/sources.js +238 -0
- package/src/downloads/staging.js +146 -0
- package/src/downloads/store.js +256 -0
- package/src/exports.js +86 -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 +338 -0
- package/src/traces/deadline.js +33 -0
- package/src/traces/identity.js +127 -0
- package/src/traces/init-script.js +95 -0
- package/src/traces/links.js +623 -0
- package/src/traces/mutation-stream.js +232 -0
- package/src/traces/observers.js +272 -0
- package/src/traces/page-capture.js +601 -0
- package/src/traces/reader.js +226 -0
- package/src/traces/recorder.js +454 -0
- package/src/traces/redaction.js +231 -0
- package/src/traces/schema.js +229 -0
- package/src/traces/viewer.js +448 -0
- package/tests/e2e/click-readiness.e2e.test.js +240 -0
- package/tests/e2e/downloads.e2e.test.js +235 -0
- package/tests/e2e/traces.e2e.test.js +742 -0
- package/tests/helpers/click-fixtures.js +62 -0
- package/tests/helpers/download-fixtures.js +125 -0
- package/tests/helpers/download-server.js +129 -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 +274 -0
- package/tests/helpers/trace-server.js +173 -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 +485 -0
- package/tests/unit/downloads/naming.test.js +174 -0
- package/tests/unit/downloads/staging.test.js +117 -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/links.test.js +557 -0
- package/tests/unit/traces/reader.test.js +208 -0
- package/tests/unit/traces/recorder.test.js +730 -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 +294 -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,
|
|
@@ -48,6 +46,7 @@
|
|
|
48
46
|
"266b9cb95f783cdf": 1,
|
|
49
47
|
"26bb09996580437c": 1,
|
|
50
48
|
"26ebb2e251c9a39e": 1,
|
|
49
|
+
"29edfb9e08094991": 1,
|
|
51
50
|
"2a78ee4b05679c8d": 1,
|
|
52
51
|
"2b1a759e2cfd21a2": 1,
|
|
53
52
|
"2c0f8b5c6f28420b": 1,
|
|
@@ -57,6 +56,7 @@
|
|
|
57
56
|
"2f9e15467cc2f12a": 1,
|
|
58
57
|
"3101802d3ea65339": 1,
|
|
59
58
|
"33357ce94e07cc4b": 1,
|
|
59
|
+
"35cf2769645f3234": 1,
|
|
60
60
|
"37bd4114d0089f49": 1,
|
|
61
61
|
"389df6fb4b8315a1": 1,
|
|
62
62
|
"39eaada6d1879f4a": 1,
|
|
@@ -77,15 +77,12 @@
|
|
|
77
77
|
"4c3b94caa28045d0": 1,
|
|
78
78
|
"4e090d6bb30e6551": 1,
|
|
79
79
|
"4f3c541b3f3e3f23": 1,
|
|
80
|
-
"4fe5663c69a426cb": 1,
|
|
81
|
-
"505d98bd14b63a6c": 1,
|
|
82
80
|
"5085765e1f103f3e": 1,
|
|
83
81
|
"5184c90c9f8406c3": 1,
|
|
84
82
|
"520a28ee2ced56c2": 1,
|
|
85
83
|
"53519ec61a6ba319": 1,
|
|
86
84
|
"53f7e3f3e444a37d": 1,
|
|
87
85
|
"5587ec0cdb7b96e6": 1,
|
|
88
|
-
"559b5241047cd753": 1,
|
|
89
86
|
"562ebfb882d4aa8f": 1,
|
|
90
87
|
"56b5df15ee61887d": 1,
|
|
91
88
|
"56b7675e4ce57335": 1,
|
|
@@ -102,11 +99,12 @@
|
|
|
102
99
|
"608e1dd27138ec6f": 1,
|
|
103
100
|
"6098544c77ff17a2": 1,
|
|
104
101
|
"60d0c58a01fc8746": 1,
|
|
102
|
+
"61ae5da6cc003836": 1,
|
|
105
103
|
"61bcadcd8dc9f63e": 1,
|
|
106
|
-
"63a3b672c8d8b50b": 1,
|
|
107
104
|
"63ff613b1780c67a": 1,
|
|
108
105
|
"642cc89ed49f8a21": 1,
|
|
109
106
|
"6479c3604c153ec4": 1,
|
|
107
|
+
"6699064d6e8e09ba": 1,
|
|
110
108
|
"66fb97eebd7ff0dc": 1,
|
|
111
109
|
"67a95e428b8fa31c": 1,
|
|
112
110
|
"6880d620e0e67229": 1,
|
|
@@ -115,10 +113,13 @@
|
|
|
115
113
|
"6af3c307939ef75e": 1,
|
|
116
114
|
"6b35b48512756192": 1,
|
|
117
115
|
"6cd00d6b6a039022": 1,
|
|
116
|
+
"6da3fe1d15b66ae0": 1,
|
|
118
117
|
"6e2457cd3e1f69e7": 1,
|
|
118
|
+
"6f30e86d1407e88c": 1,
|
|
119
119
|
"6fa62e2642cc73c3": 1,
|
|
120
120
|
"7009c5e9bd61dbe8": 1,
|
|
121
121
|
"709348ea44394b52": 1,
|
|
122
|
+
"72705e6a2212d7b2": 1,
|
|
122
123
|
"731fbc5d4188839a": 1,
|
|
123
124
|
"73dbf56d17ab79ed": 1,
|
|
124
125
|
"760d9925f6e3ca85": 1,
|
|
@@ -141,28 +142,22 @@
|
|
|
141
142
|
"8678919f1350f154": 1,
|
|
142
143
|
"86aae81b1aa0d21b": 1,
|
|
143
144
|
"87484a2744e09748": 1,
|
|
144
|
-
"87514cf1299c9c0e": 1,
|
|
145
145
|
"876d2c8edfc8c40a": 1,
|
|
146
146
|
"8b013ac20e436d76": 1,
|
|
147
147
|
"8c2b1408886c1db1": 1,
|
|
148
|
-
"8cb9a984bf19a062": 1,
|
|
149
148
|
"8ec18cdbd4a96518": 1,
|
|
150
|
-
"8ecf2e554a04d2ef": 1,
|
|
151
149
|
"91049efadc65169b": 1,
|
|
152
150
|
"9134185980de8a17": 1,
|
|
153
151
|
"916833bafca4fa63": 1,
|
|
154
152
|
"91a538e4a0c28f25": 1,
|
|
155
|
-
"929fba5228e9fdac": 1,
|
|
156
153
|
"92ddafa7e65be7eb": 1,
|
|
157
154
|
"94821a4d1b745dfd": 1,
|
|
158
|
-
"94d89888ac9da9a9": 1,
|
|
159
155
|
"97c5a112fdde7be8": 1,
|
|
160
156
|
"98cc22079feaa004": 1,
|
|
161
157
|
"990d87f21678fa96": 1,
|
|
162
158
|
"9a72b1c2f22156ca": 1,
|
|
163
159
|
"9b6ef4df04562546": 1,
|
|
164
160
|
"9c055dfd90c369bf": 1,
|
|
165
|
-
"9cb19f1b972e811e": 1,
|
|
166
161
|
"9ee57dcd5e7a6691": 1,
|
|
167
162
|
"9fbb8296259dedbe": 1,
|
|
168
163
|
"a0183465782b0b15": 1,
|
|
@@ -175,25 +170,26 @@
|
|
|
175
170
|
"a78d76908162699e": 1,
|
|
176
171
|
"a9bd4ffef6da5faf": 1,
|
|
177
172
|
"a9d71c2e50f1b832": 1,
|
|
178
|
-
"abe5a58f2b2cc593": 1,
|
|
179
173
|
"acc95d694eb2f581": 1,
|
|
180
174
|
"ad4469f2e8131535": 1,
|
|
181
175
|
"ad4cfa76daa1b6fc": 1,
|
|
176
|
+
"ad50b48938af8aef": 1,
|
|
182
177
|
"adae1329361a81a1": 1,
|
|
183
178
|
"ae477ba64bafba3f": 1,
|
|
184
179
|
"aee1e5c65f783848": 1,
|
|
185
180
|
"aee45e8b58c2d87f": 1,
|
|
181
|
+
"af702b20ebd45ef2": 1,
|
|
186
182
|
"af9d6a2f4eba146e": 1,
|
|
187
183
|
"b010686af23efa90": 1,
|
|
188
184
|
"b2a0686231220c8a": 1,
|
|
189
185
|
"b35ff46c365dcd18": 1,
|
|
190
186
|
"b41581af204af40e": 1,
|
|
191
|
-
"b62f3449cde7991d": 1,
|
|
192
187
|
"b71fa5250da28739": 1,
|
|
193
188
|
"b740ecac685a15fe": 1,
|
|
194
189
|
"b8c2d21f2a26f77d": 1,
|
|
195
190
|
"bb269582b94ed42d": 1,
|
|
196
191
|
"bb3e6d0f7e7edccd": 1,
|
|
192
|
+
"bc9115be3a462930": 1,
|
|
197
193
|
"bccf0d5cc57c08df": 1,
|
|
198
194
|
"bd0ae42c13d5b4f9": 1,
|
|
199
195
|
"bd6c62a6e1c6a071": 1,
|
|
@@ -209,6 +205,7 @@
|
|
|
209
205
|
"c415ffcd004d6518": 1,
|
|
210
206
|
"c461a83b51eb7740": 1,
|
|
211
207
|
"c9cf3c28bed33a5b": 1,
|
|
208
|
+
"cab590a1d04b5a05": 1,
|
|
212
209
|
"cbd444e9a6a9072e": 1,
|
|
213
210
|
"cd49f39b43d14635": 1,
|
|
214
211
|
"cebff7d1cc87f646": 1,
|
|
@@ -221,7 +218,6 @@
|
|
|
221
218
|
"d1a4a8e6a523b752": 1,
|
|
222
219
|
"d1e5c2961267399a": 1,
|
|
223
220
|
"d2ed3d02dd6a2d8c": 1,
|
|
224
|
-
"d4eec99f58906a63": 1,
|
|
225
221
|
"d6157a86fea41907": 1,
|
|
226
222
|
"d6fa9884eee815f0": 1,
|
|
227
223
|
"d86bafb915488255": 1,
|
|
@@ -239,7 +235,6 @@
|
|
|
239
235
|
"ea1195048ded896c": 1,
|
|
240
236
|
"ea88711c5deb1fe5": 1,
|
|
241
237
|
"eb9a70c2a21094ff": 1,
|
|
242
|
-
"ebd08637a2d7bcf3": 1,
|
|
243
238
|
"ebe03c5f8c2f5147": 1,
|
|
244
239
|
"ee19c8b9b332ee98": 1,
|
|
245
240
|
"eeca0ccf8d821f21": 1,
|
|
@@ -253,11 +248,11 @@
|
|
|
253
248
|
"f4bfa7e503443cd1": 1,
|
|
254
249
|
"f6678d6748e9c902": 1,
|
|
255
250
|
"f67a07a611f39cb2": 1,
|
|
251
|
+
"f7b1f9d699bcd580": 1,
|
|
256
252
|
"f911821aa7dda47f": 1,
|
|
257
253
|
"f97bf515bfd95829": 1,
|
|
258
254
|
"fae844842fba705b": 1,
|
|
259
255
|
"faf76ec43b2dbbaa": 1,
|
|
260
|
-
"fb3a2e9ace145907": 1,
|
|
261
256
|
"fc6ed7d760c98f51": 1,
|
|
262
257
|
"fccfd8b004c12dd4": 1,
|
|
263
258
|
"fe859df9f97c75d4": 1
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,73 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.19.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 4401d63: Record traces continuously across navigation and live state, and export them as
|
|
8
|
+
Links Notation.
|
|
9
|
+
|
|
10
|
+
`startTrace({mode: 'continuous'})` now keeps recording when the page navigates,
|
|
11
|
+
changes route or creates a frame: the observers are reinstalled from an init
|
|
12
|
+
script that runs before the page's own code, so nothing that happens during a
|
|
13
|
+
load is lost. What a person changes but the DOM does not show - typing,
|
|
14
|
+
checking, selecting, focus and scroll - is recorded as semantic live-state
|
|
15
|
+
records and applied on replay, child-list records carry the position a node was
|
|
16
|
+
inserted at or removed from, and every record carries stable trace, browser
|
|
17
|
+
context, page, navigation, frame and action identities. `initialCheckpoint` (on
|
|
18
|
+
by default) captures the page before the first action so the first interval has
|
|
19
|
+
a base to replay onto, and the viewer says what it is: a diagnostic replay of
|
|
20
|
+
what was recorded, not a re-execution of the session.
|
|
21
|
+
|
|
22
|
+
`writeTraceLinks(trace, './run.lino', {include})` writes a portable Links
|
|
23
|
+
Notation view of a bundle, and `startTrace({links: {output}})` writes the same
|
|
24
|
+
view incrementally as the run happens. One link per line: one per ordered
|
|
25
|
+
timeline event with its sequence, time, kind, owner ids, actor, action, target
|
|
26
|
+
and outcome; one per checkpoint naming its HTML, state and screenshot members by
|
|
27
|
+
their path inside the bundle; one per control that changed between two
|
|
28
|
+
checkpoints. Dropped and partial records are explicit, binary content is never
|
|
29
|
+
duplicated, and redaction is whatever the bundle already decided. The JSON
|
|
30
|
+
bundle stays authoritative - this is an adapter, not a replacement.
|
|
31
|
+
|
|
32
|
+
A managed download whose CDP completion arrives before the file does is no
|
|
33
|
+
longer reported as a missing file: the staged bytes are waited for, and a
|
|
34
|
+
timeout or a genuinely missing file is still a failure rather than a path that
|
|
35
|
+
does not exist.
|
|
36
|
+
|
|
37
|
+
## 0.18.0
|
|
38
|
+
|
|
39
|
+
### Minor Changes
|
|
40
|
+
|
|
41
|
+
- 0915dd8: Add managed downloads, portable traces and truthful click results.
|
|
42
|
+
|
|
43
|
+
`downloads` is now accepted by `launchBrowser()`, `connectBrowser()` and
|
|
44
|
+
`launchRealBrowser()`, and by `commander.configureDownloads()` on a browser you
|
|
45
|
+
already have. The manager owns the file rather than the page: `capture()` starts
|
|
46
|
+
listening before the action runs so a fast download cannot be missed, a caller
|
|
47
|
+
can choose the saved name and have a bare UUID given the extension its content
|
|
48
|
+
proves, and the file is still there after the context and the browser are gone.
|
|
49
|
+
Failed and cancelled downloads report the failure instead of a path, and
|
|
50
|
+
`browser-commander/tests` keeps captured files in the run's artifact directory.
|
|
51
|
+
|
|
52
|
+
`startTrace()` records a session into a schema-versioned bundle - an ordered
|
|
53
|
+
NDJSON timeline, named checkpoints with full HTML, live control state and frame
|
|
54
|
+
metadata, and the DOM mutation batches between them - which `readTrace()`,
|
|
55
|
+
`diffControlState()` and the offline `writeTraceViewer()` read back with scripts
|
|
56
|
+
and network disabled. Redaction runs before anything is written, a truncated or
|
|
57
|
+
interrupted run still produces a readable partial trace, and
|
|
58
|
+
`browser-commander/tests` supports `trace: 'retain-on-failure'` in place of its
|
|
59
|
+
own artifact mechanism.
|
|
60
|
+
|
|
61
|
+
`clickElement()` now reports what was observed: `status` is `succeeded`,
|
|
62
|
+
`failed`, `timed_out`, `interrupted` or `unverified`, `effect` is `confirmed`,
|
|
63
|
+
`not-observed` or `contradicted`, and `evidence` carries the reason for both.
|
|
64
|
+
The `clicked` and `verified` booleans remain and are derived conservatively, so
|
|
65
|
+
a click that changed nothing is no longer reported as verified. `waitForReady()`
|
|
66
|
+
composes URL, network, DOM, image and custom checks under one deadline and
|
|
67
|
+
returns the same per-check evidence. `noAutoScroll` is deprecated in favour of
|
|
68
|
+
the `scroll` axis; `scroll: 'none'` now fails with a `ScrollConstraintError` on
|
|
69
|
+
engines that cannot click without scrolling instead of scrolling anyway.
|
|
70
|
+
|
|
3
71
|
## 0.17.2
|
|
4
72
|
|
|
5
73
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -599,6 +599,138 @@ 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. In `continuous`
|
|
661
|
+
mode the observers are reinstalled from an init script before the page's own
|
|
662
|
+
code runs, so a trace keeps recording across navigations, SPA route changes and
|
|
663
|
+
same-document updates, and it records what a person cannot see in the DOM -
|
|
664
|
+
typing, checking, selecting, focus and scroll - as semantic live-state records
|
|
665
|
+
(issue #93). `initialCheckpoint` (on by default) captures the page as it was
|
|
666
|
+
before the first action, so the first interval has a base to replay onto. The
|
|
667
|
+
viewer seeks, diffs and replays the captured DOM with scripts and network
|
|
668
|
+
disabled; it is a diagnostic replay of what was recorded, not a pixel-accurate
|
|
669
|
+
re-execution of the session.
|
|
670
|
+
|
|
671
|
+
#### Links Notation Export
|
|
672
|
+
|
|
673
|
+
The bundle stays authoritative, and `writeTraceLinks()` writes a second,
|
|
674
|
+
portable view of it for tools that keep semantic, actor-aware histories - audit
|
|
675
|
+
graphs, agent memory, cross-run diffing (issue #94):
|
|
676
|
+
|
|
677
|
+
```javascript
|
|
678
|
+
import { readTrace, writeTraceLinks } from 'browser-commander';
|
|
679
|
+
|
|
680
|
+
const trace = await readTrace('./run.bc-trace');
|
|
681
|
+
await writeTraceLinks(trace, './run.lino', {
|
|
682
|
+
include: ['timeline', 'checkpoints', 'control-diffs'],
|
|
683
|
+
});
|
|
684
|
+
```
|
|
685
|
+
|
|
686
|
+
A long-lived agent can have the same export written as the run happens, so a
|
|
687
|
+
process that is killed still leaves everything it had recorded:
|
|
688
|
+
|
|
689
|
+
```javascript
|
|
690
|
+
const trace = await commander.startTrace({
|
|
691
|
+
output: './run.bc-trace',
|
|
692
|
+
links: { output: './run.lino' },
|
|
693
|
+
});
|
|
694
|
+
// trace.links === './run.lino'
|
|
695
|
+
```
|
|
696
|
+
|
|
697
|
+
One link per line: one `(timeline: ...)` link per ordered event carrying its
|
|
698
|
+
sequence, time, kind, owner ids, actor, action, target and outcome; one
|
|
699
|
+
`(checkpoint: ...)` link naming its HTML, state and screenshot members by their
|
|
700
|
+
path inside the bundle; one `(control-diff: ...)` link per control that changed
|
|
701
|
+
between two checkpoints, with `path`, `before`, `after` and who changed it.
|
|
702
|
+
Dropped and partial records are written as such rather than left out, no binary
|
|
703
|
+
content is ever copied into the export, and redaction is whatever the bundle
|
|
704
|
+
already decided - the export only reads what was written there. The
|
|
705
|
+
representation is pinned by a golden test against
|
|
706
|
+
[`links-notation`](https://www.npmjs.com/package/links-notation) 0.20, whose
|
|
707
|
+
Python and Rust implementations read the same file;
|
|
708
|
+
`experiments/trace-links-export.mjs` records a real run and parses its export
|
|
709
|
+
from Python to prove it.
|
|
710
|
+
|
|
711
|
+
### Truthful Click Results
|
|
712
|
+
|
|
713
|
+
`clickElement()` reports what was observed, not what was attempted:
|
|
714
|
+
|
|
715
|
+
```javascript
|
|
716
|
+
const result = await clickElement({
|
|
717
|
+
page,
|
|
718
|
+
engine,
|
|
719
|
+
locatorOrElement: '#submit',
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
result.status; // 'succeeded' | 'failed' | 'timed_out' | 'interrupted' | 'unverified'
|
|
723
|
+
result.effect; // 'confirmed' | 'not-observed' | 'contradicted'
|
|
724
|
+
result.evidence; // why status and effect say what they say
|
|
725
|
+
result.clicked; // still here: whether the click reached the element
|
|
726
|
+
result.verified; // still here, now derived from effect === 'confirmed'
|
|
727
|
+
```
|
|
728
|
+
|
|
729
|
+
The `scroll` axis (`auto`, `preserve`, `none`) replaces the deprecated
|
|
730
|
+
`noAutoScroll` flag. `scroll: 'none'` never scrolls: on an engine that cannot
|
|
731
|
+
deliver a click without scrolling it throws `ScrollConstraintError` naming the
|
|
732
|
+
alternatives, rather than scrolling the page and reporting success.
|
|
733
|
+
|
|
602
734
|
### commander.destroy()
|
|
603
735
|
|
|
604
736
|
```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.19.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",
|
|
@@ -65,6 +68,7 @@
|
|
|
65
68
|
},
|
|
66
69
|
"dependencies": {
|
|
67
70
|
"better-sqlite3": "^12.11.1",
|
|
71
|
+
"links-notation": "^0.20.0",
|
|
68
72
|
"log-lazy": "^1.0.4",
|
|
69
73
|
"test-anywhere": "^0.9.1"
|
|
70
74
|
},
|
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).
|