browser-commander 0.27.0 → 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.jscpd-baseline.json +5 -19
- package/CHANGELOG.md +22 -0
- package/README.md +23 -9
- package/eslint.config.js +11 -0
- package/package.json +5 -2
- package/src/browser/browser-profile-files.js +5 -2
- package/src/browser/connector.js +215 -36
- package/src/browser/launch-options.js +2 -1
- package/src/browser/launch-restrictions.json +33 -2
- package/src/browser/launcher.js +26 -1
- package/src/browser/persistent-session.js +432 -0
- package/src/browser/profile-directory.js +20 -0
- package/src/browser/real-browser.js +7 -2
- package/src/browser/restrictions.js +13 -4
- package/src/browser/session-adoption.js +96 -0
- package/src/browser/session-watchdog.js +71 -0
- package/src/browser/session-worker.js +82 -0
- package/src/browser/webdriver.js +4 -0
- package/src/capture/encoding.js +327 -0
- package/src/capture/errors.js +12 -0
- package/src/capture/files.js +9 -0
- package/src/capture/index.js +5 -0
- package/src/capture/recording.js +136 -0
- package/src/capture/screenshot.js +222 -0
- package/src/capture/video.js +186 -0
- package/src/capture/viewport.js +68 -0
- package/src/capture/worker.js +22 -0
- package/src/cli/args.js +69 -1
- package/src/cli/commands.js +116 -2
- package/src/cli/high-level-methods.js +47 -3
- package/src/cli/sessions.js +14 -3
- package/src/core/commander-features.js +110 -0
- package/src/core/constants.js +1 -0
- package/src/core/navigation-manager.js +4 -2
- package/src/core/navigation-readiness.js +30 -2
- package/src/core/navigation-safety.js +52 -0
- package/src/core/page-trigger-manager.js +280 -156
- package/src/core/readiness.js +10 -2
- package/src/elements/locators.js +26 -32
- package/src/elements/selectors.js +37 -81
- package/src/elements/visibility.js +32 -10
- package/src/factory.js +13 -12
- package/src/index.js +4 -0
- package/src/interactions/click.js +44 -0
- package/src/interactions/overlays.js +73 -0
- package/src/traces/bundle.js +55 -8
- package/src/traces/checkpoints.js +103 -0
- package/src/traces/links.js +157 -2
- package/src/traces/mutation-stream.js +9 -2
- package/src/traces/network.js +309 -0
- package/src/traces/page-capture.js +203 -68
- package/src/traces/reader.js +8 -10
- package/src/traces/recorder.js +253 -247
- package/src/traces/redaction.js +89 -6
- package/src/traces/render.js +168 -0
- package/src/traces/rolling-bundle.js +240 -0
- package/src/traces/rolling.js +77 -0
- package/src/traces/start-cleanup.js +15 -0
- package/src/traces/stop.js +160 -0
- package/src/traces/storage.js +43 -0
- package/src/utilities/subprocess.js +32 -0
- package/tests/e2e/cli.e2e.test.js +20 -1
- package/tests/e2e/click-readiness.e2e.test.js +1 -1
- package/tests/e2e/issue-160.e2e.test.js +291 -0
- package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0002.html +1 -1
- package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0003.html +1 -1
- package/tests/fixtures/traces/conformance/expected/bundle/events.ndjson +2 -2
- package/tests/fixtures/traces/conformance/expected/bundle/manifest.json +4 -0
- package/tests/fixtures/traces/conformance/expected/bundle/viewer.html +1 -1
- package/tests/fixtures/traces/conformance/expected/trace.lino +2 -2
- package/tests/unit/browser/issue-160-connector.test.js +157 -0
- package/tests/unit/browser/launch-building-blocks.test.js +5 -1
- package/tests/unit/browser/launcher.test.js +5 -1
- package/tests/unit/browser/persistent-session.test.js +59 -0
- package/tests/unit/browser/quiet-ui.test.js +16 -0
- package/tests/unit/browser/tab-selection.test.js +38 -0
- package/tests/unit/capture/capture.test.js +52 -0
- package/tests/unit/capture/cleanup.test.js +24 -0
- package/tests/unit/capture/viewport.test.js +168 -0
- package/tests/unit/cli/dispatcher.test.js +25 -0
- package/tests/unit/core/interrupted-navigation.test.js +32 -0
- package/tests/unit/core/readiness.test.js +21 -0
- package/tests/unit/core/trigger-readiness.test.js +39 -0
- package/tests/unit/core/trigger-stop-race.test.js +80 -0
- package/tests/unit/elements/visible-count.test.js +24 -0
- package/tests/unit/elements/wait-navigation.test.js +26 -0
- package/tests/unit/interactions/navigation-timeout.test.js +39 -0
- package/tests/unit/interactions/overlays.test.js +58 -0
- package/tests/unit/scripts/ci-timeout-budgets.test.js +1 -0
- package/tests/unit/traces/bundle.test.js +16 -0
- package/tests/unit/traces/issue-160-privacy.test.js +110 -0
- package/tests/unit/traces/issue-160-rotation.test.js +140 -0
- package/tests/unit/traces/network.test.js +149 -0
|
@@ -28,10 +28,12 @@ export function captureSnapshotInPage(options) {
|
|
|
28
28
|
const {
|
|
29
29
|
redactSelectors = [],
|
|
30
30
|
redactAttributes = [],
|
|
31
|
+
useDefaults = true,
|
|
31
32
|
redacted = '[redacted]',
|
|
32
33
|
html: wantHtml = true,
|
|
33
34
|
liveControlState = true,
|
|
34
35
|
openShadowRoots = true,
|
|
36
|
+
ignoreSelectors = [],
|
|
35
37
|
maxHtmlBytes = 0,
|
|
36
38
|
} = options || {};
|
|
37
39
|
|
|
@@ -120,6 +122,13 @@ export function captureSnapshotInPage(options) {
|
|
|
120
122
|
// `<input>` otherwise shows the value the server sent, not the one the user
|
|
121
123
|
// typed.
|
|
122
124
|
const carryLiveState = (original, copy, secret) => {
|
|
125
|
+
if (
|
|
126
|
+
useDefaults &&
|
|
127
|
+
original.localName === 'meta' &&
|
|
128
|
+
/token|csrf|xsrf|secret/i.test(original.getAttribute('name') || '')
|
|
129
|
+
) {
|
|
130
|
+
copy.setAttribute('content', redacted);
|
|
131
|
+
}
|
|
123
132
|
for (const attribute of redactAttributes) {
|
|
124
133
|
if (copy.hasAttribute && copy.hasAttribute(attribute)) {
|
|
125
134
|
copy.setAttribute(attribute, redacted);
|
|
@@ -147,35 +156,96 @@ export function captureSnapshotInPage(options) {
|
|
|
147
156
|
} else if (secret) {
|
|
148
157
|
copy.textContent = redacted;
|
|
149
158
|
}
|
|
150
|
-
if (original.shadowRoot && openShadowRoots) {
|
|
159
|
+
if (!secret && original.shadowRoot && openShadowRoots) {
|
|
151
160
|
// Shadow content is not serialized by `outerHTML`; a declarative
|
|
152
161
|
// template keeps it in the same document the viewer renders.
|
|
153
162
|
const template = document.createElement('template');
|
|
154
163
|
template.setAttribute('shadowrootmode', 'open');
|
|
155
|
-
|
|
164
|
+
for (const child of original.shadowRoot.childNodes) {
|
|
165
|
+
{
|
|
166
|
+
const cloned = cloneWithState(child);
|
|
167
|
+
if (cloned) {
|
|
168
|
+
template.content.appendChild(cloned);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
156
172
|
copy.appendChild(template);
|
|
157
173
|
}
|
|
158
174
|
};
|
|
159
175
|
|
|
176
|
+
const cloneWithState = (original) => {
|
|
177
|
+
if (
|
|
178
|
+
original.nodeType === 1 &&
|
|
179
|
+
ignoreSelectors.some((selector) => original.matches(selector))
|
|
180
|
+
) {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
const copy = original.cloneNode(false);
|
|
184
|
+
if (original.nodeType !== 1) {
|
|
185
|
+
return copy;
|
|
186
|
+
}
|
|
187
|
+
const secret = isSecret(original);
|
|
188
|
+
if (!secret) {
|
|
189
|
+
for (const child of original.childNodes) {
|
|
190
|
+
{
|
|
191
|
+
const cloned = cloneWithState(child);
|
|
192
|
+
if (cloned) {
|
|
193
|
+
copy.appendChild(cloned);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
carryLiveState(original, copy, secret);
|
|
199
|
+
return copy;
|
|
200
|
+
};
|
|
201
|
+
|
|
160
202
|
let html = null;
|
|
161
203
|
let truncated = false;
|
|
162
204
|
if (wantHtml) {
|
|
163
|
-
const clone = document.documentElement
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
205
|
+
const clone = cloneWithState(document.documentElement);
|
|
206
|
+
|
|
207
|
+
html = `<!DOCTYPE html>\n${clone?.outerHTML ?? ''}`;
|
|
208
|
+
const encoded = new globalThis.TextEncoder().encode(html);
|
|
209
|
+
if (maxHtmlBytes > 0 && encoded.length > maxHtmlBytes) {
|
|
210
|
+
const marker = '<!-- trace HTML truncated -->';
|
|
211
|
+
const prefix = new globalThis.TextDecoder().decode(
|
|
212
|
+
encoded.slice(0, Math.max(0, maxHtmlBytes - marker.length - 3))
|
|
213
|
+
);
|
|
214
|
+
html = `${prefix}${marker}`;
|
|
215
|
+
truncated = true;
|
|
172
216
|
}
|
|
217
|
+
}
|
|
173
218
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
219
|
+
const visibleText = [];
|
|
220
|
+
const collectText = (node) => {
|
|
221
|
+
if (node.nodeType === 3 && node.parentElement) {
|
|
222
|
+
const parent = node.parentElement;
|
|
223
|
+
if (
|
|
224
|
+
!parent.getClientRects().length ||
|
|
225
|
+
window.getComputedStyle(parent).visibility === 'hidden' ||
|
|
226
|
+
ignoreSelectors.some((selector) => parent.closest(selector))
|
|
227
|
+
) {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
const secret = redactSelectors.some((selector) =>
|
|
231
|
+
parent.closest(selector)
|
|
232
|
+
);
|
|
233
|
+
if (node.nodeValue.trim()) {
|
|
234
|
+
visibleText.push(
|
|
235
|
+
`${cssPath(parent)}: ${secret ? redacted : node.nodeValue.trim()}`
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
} else {
|
|
239
|
+
for (const child of node.childNodes ?? []) {
|
|
240
|
+
collectText(child);
|
|
241
|
+
}
|
|
242
|
+
if (node.shadowRoot && openShadowRoots) {
|
|
243
|
+
collectText(node.shadowRoot);
|
|
244
|
+
}
|
|
178
245
|
}
|
|
246
|
+
};
|
|
247
|
+
if (options?.captureText) {
|
|
248
|
+
collectText(document.documentElement);
|
|
179
249
|
}
|
|
180
250
|
|
|
181
251
|
return {
|
|
@@ -190,6 +260,7 @@ export function captureSnapshotInPage(options) {
|
|
|
190
260
|
? cssPath(document.activeElement)
|
|
191
261
|
: null,
|
|
192
262
|
controls,
|
|
263
|
+
...(options?.captureText ? { text: visibleText.join('\n') } : {}),
|
|
193
264
|
frames: [...document.querySelectorAll('iframe, frame')].map((frame) => ({
|
|
194
265
|
path: cssPath(frame),
|
|
195
266
|
src: frame.getAttribute('src'),
|
|
@@ -223,8 +294,11 @@ export function installMutationRecorderInPage(options) {
|
|
|
223
294
|
const {
|
|
224
295
|
globalName = '__browserCommanderTrace__',
|
|
225
296
|
redactSelectors = [],
|
|
297
|
+
redactAttributes = [],
|
|
298
|
+
useDefaults = true,
|
|
226
299
|
redacted = '[redacted]',
|
|
227
300
|
maxQueued = 5000,
|
|
301
|
+
ignoreSelectors = [],
|
|
228
302
|
liveState = true,
|
|
229
303
|
} = options || {};
|
|
230
304
|
|
|
@@ -237,20 +311,18 @@ export function installMutationRecorderInPage(options) {
|
|
|
237
311
|
queue: [],
|
|
238
312
|
dropped: 0,
|
|
239
313
|
sequence: 0,
|
|
240
|
-
// Every document gets its own name, so a batch drained from an iframe is
|
|
241
|
-
// never mistaken for one drained from the page that holds it.
|
|
242
314
|
frameId: `frame-${Math.random().toString(36).slice(2, 10)}`,
|
|
243
315
|
frameName: window.name || null,
|
|
244
316
|
mainFrame: window.top === window,
|
|
245
317
|
};
|
|
246
318
|
|
|
247
|
-
const
|
|
319
|
+
const matchesAny = (node, selectors) => {
|
|
248
320
|
const element =
|
|
249
321
|
node && node.nodeType === 1 ? node : node && node.parentElement;
|
|
250
322
|
if (!element || !element.matches) {
|
|
251
323
|
return false;
|
|
252
324
|
}
|
|
253
|
-
return
|
|
325
|
+
return selectors.some((selector) => {
|
|
254
326
|
try {
|
|
255
327
|
return element.matches(selector) || element.closest(selector) !== null;
|
|
256
328
|
} catch {
|
|
@@ -258,6 +330,43 @@ export function installMutationRecorderInPage(options) {
|
|
|
258
330
|
}
|
|
259
331
|
});
|
|
260
332
|
};
|
|
333
|
+
const isSecret = (node) =>
|
|
334
|
+
matchesAny(node, redactSelectors) ||
|
|
335
|
+
(useDefaults &&
|
|
336
|
+
node?.localName === 'meta' &&
|
|
337
|
+
/token|csrf|xsrf|secret|password/i.test(node.getAttribute('name') ?? ''));
|
|
338
|
+
const isIgnored = (node) => matchesAny(node, ignoreSelectors);
|
|
339
|
+
const sanitized = (node) => {
|
|
340
|
+
const clone = node.cloneNode(true);
|
|
341
|
+
for (const selector of ignoreSelectors) {
|
|
342
|
+
for (const element of clone.querySelectorAll(selector)) {
|
|
343
|
+
element.remove();
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
for (const selector of redactSelectors) {
|
|
347
|
+
for (const element of clone.querySelectorAll(selector)) {
|
|
348
|
+
element.textContent = redacted;
|
|
349
|
+
if (element.hasAttribute('value')) {
|
|
350
|
+
element.setAttribute('value', redacted);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
for (const element of [clone, ...clone.querySelectorAll('*')]) {
|
|
355
|
+
for (const attribute of redactAttributes) {
|
|
356
|
+
if (element.hasAttribute(attribute)) {
|
|
357
|
+
element.setAttribute(attribute, redacted);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
if (
|
|
361
|
+
useDefaults &&
|
|
362
|
+
element.localName === 'meta' &&
|
|
363
|
+
/token|csrf|xsrf|secret/i.test(element.getAttribute('name') || '')
|
|
364
|
+
) {
|
|
365
|
+
element.setAttribute('content', redacted);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
return clone;
|
|
369
|
+
};
|
|
261
370
|
|
|
262
371
|
// A path the viewer can look the node up by: replay needs to find the same
|
|
263
372
|
// element again in a parsed copy of the checkpoint.
|
|
@@ -268,11 +377,9 @@ export function installMutationRecorderInPage(options) {
|
|
|
268
377
|
steps.unshift(`#${at.id}`);
|
|
269
378
|
break;
|
|
270
379
|
}
|
|
271
|
-
const siblings = at.parentElement
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
)
|
|
275
|
-
: [];
|
|
380
|
+
const siblings = [...(at.parentElement?.children ?? [])].filter(
|
|
381
|
+
(c) => c.localName === at.localName
|
|
382
|
+
);
|
|
276
383
|
const nth =
|
|
277
384
|
siblings.length > 1 ? `:nth-of-type(${siblings.indexOf(at) + 1})` : '';
|
|
278
385
|
steps.unshift(at.localName + nth);
|
|
@@ -288,12 +395,16 @@ export function installMutationRecorderInPage(options) {
|
|
|
288
395
|
};
|
|
289
396
|
|
|
290
397
|
const describe = (node) => {
|
|
291
|
-
if (!node) {
|
|
398
|
+
if (!node || isIgnored(node)) {
|
|
292
399
|
return null;
|
|
293
400
|
}
|
|
294
401
|
if (node.nodeType === 3) {
|
|
295
402
|
return {
|
|
296
403
|
type: 'text',
|
|
404
|
+
visible: node.parentElement
|
|
405
|
+
? node.parentElement.getClientRects().length > 0 &&
|
|
406
|
+
window.getComputedStyle(node.parentElement).visibility !== 'hidden'
|
|
407
|
+
: true,
|
|
297
408
|
path: pathOf(node.parentElement),
|
|
298
409
|
text: isSecret(node) ? redacted : String(node.nodeValue).slice(0, 2000),
|
|
299
410
|
};
|
|
@@ -301,12 +412,36 @@ export function installMutationRecorderInPage(options) {
|
|
|
301
412
|
if (node.nodeType !== 1) {
|
|
302
413
|
return { type: 'node', nodeType: node.nodeType };
|
|
303
414
|
}
|
|
415
|
+
const clone = sanitized(node);
|
|
304
416
|
return {
|
|
305
417
|
type: 'element',
|
|
306
418
|
tag: node.localName,
|
|
307
419
|
id: node.id || null,
|
|
308
420
|
path: pathOf(node),
|
|
309
|
-
html: isSecret(node) ? redacted : String(
|
|
421
|
+
html: isSecret(node) ? redacted : String(clone.outerHTML).slice(0, 4000),
|
|
422
|
+
text: isSecret(node)
|
|
423
|
+
? redacted
|
|
424
|
+
: String(clone.textContent).slice(0, 2000),
|
|
425
|
+
visible:
|
|
426
|
+
node.getClientRects().length > 0 &&
|
|
427
|
+
window.getComputedStyle(node).visibility !== 'hidden',
|
|
428
|
+
};
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
// Mutation targets identify an existing node. Only additions need markup.
|
|
432
|
+
const identify = (node) => {
|
|
433
|
+
if (!node || isIgnored(node)) {
|
|
434
|
+
return null;
|
|
435
|
+
}
|
|
436
|
+
const element = node.nodeType === 1 ? node : node.parentElement;
|
|
437
|
+
return {
|
|
438
|
+
type: node.nodeType === 3 ? 'text' : 'element',
|
|
439
|
+
path: pathOf(element),
|
|
440
|
+
tag: element?.localName,
|
|
441
|
+
id: element?.id || null,
|
|
442
|
+
visible:
|
|
443
|
+
Boolean(element?.getClientRects().length) &&
|
|
444
|
+
window.getComputedStyle(element).visibility !== 'hidden',
|
|
310
445
|
};
|
|
311
446
|
};
|
|
312
447
|
|
|
@@ -364,41 +499,45 @@ export function installMutationRecorderInPage(options) {
|
|
|
364
499
|
return;
|
|
365
500
|
}
|
|
366
501
|
push({
|
|
367
|
-
records: records
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
502
|
+
records: records
|
|
503
|
+
.filter((record) => !isIgnored(record.target))
|
|
504
|
+
.map((record) => {
|
|
505
|
+
const entry = {
|
|
506
|
+
kind: record.type,
|
|
507
|
+
target: identify(record.target),
|
|
508
|
+
};
|
|
509
|
+
if (record.type === 'attributes') {
|
|
510
|
+
entry.attribute = record.attributeName;
|
|
511
|
+
const secret =
|
|
512
|
+
isSecret(record.target) ||
|
|
513
|
+
redactAttributes.includes(record.attributeName) ||
|
|
514
|
+
(useDefaults &&
|
|
515
|
+
/token|csrf|xsrf|secret|password/i.test(record.attributeName));
|
|
516
|
+
entry.before = secret ? redacted : record.oldValue;
|
|
517
|
+
entry.after =
|
|
518
|
+
secret || !record.target.getAttribute
|
|
519
|
+
? redacted
|
|
520
|
+
: record.target.getAttribute(record.attributeName);
|
|
521
|
+
} else if (record.type === 'characterData') {
|
|
522
|
+
entry.before = isSecret(record.target) ? redacted : record.oldValue;
|
|
523
|
+
entry.after = isSecret(record.target)
|
|
380
524
|
? redacted
|
|
381
|
-
: record.target.
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
entry.previous = describe(record.previousSibling);
|
|
398
|
-
entry.next = describe(record.nextSibling);
|
|
399
|
-
}
|
|
400
|
-
return entry;
|
|
401
|
-
}),
|
|
525
|
+
: record.target.nodeValue;
|
|
526
|
+
} else {
|
|
527
|
+
// Where a node went is as much of the change as what it was:
|
|
528
|
+
// appending every addition to the end replays an insertion before a
|
|
529
|
+
// sibling in the wrong place, and drops moves and removals entirely.
|
|
530
|
+
entry.added = [...record.addedNodes]
|
|
531
|
+
.filter((node) => !isIgnored(node))
|
|
532
|
+
.map((node) => describeAt(node, record));
|
|
533
|
+
entry.removed = [...record.removedNodes]
|
|
534
|
+
.filter((node) => !isIgnored(node))
|
|
535
|
+
.map((node) => describeAt(node, record));
|
|
536
|
+
entry.previous = identify(record.previousSibling);
|
|
537
|
+
entry.next = identify(record.nextSibling);
|
|
538
|
+
}
|
|
539
|
+
return entry;
|
|
540
|
+
}),
|
|
402
541
|
});
|
|
403
542
|
});
|
|
404
543
|
|
|
@@ -420,7 +559,6 @@ export function installMutationRecorderInPage(options) {
|
|
|
420
559
|
// into a field and then leaving it fires `input` for each keystroke and
|
|
421
560
|
// `change` for the value it ended at, and a checkbox fires both for the
|
|
422
561
|
// one click. Replaying a state that is already on screen shows nothing, so
|
|
423
|
-
// a repeat of what this element last reported is not recorded.
|
|
424
562
|
const repeats = (element, property, after) => {
|
|
425
563
|
const seen = lastLive.get(element) || {};
|
|
426
564
|
const asText = JSON.stringify(after === undefined ? null : after);
|
|
@@ -435,16 +573,13 @@ export function installMutationRecorderInPage(options) {
|
|
|
435
573
|
const liveEntry = (target, property, after, before) => ({
|
|
436
574
|
kind: 'live-state',
|
|
437
575
|
property,
|
|
438
|
-
target:
|
|
576
|
+
target: identify(target),
|
|
439
577
|
before: before === undefined ? null : before,
|
|
440
578
|
after,
|
|
441
579
|
});
|
|
442
580
|
|
|
443
581
|
const pushLive = (entry) => {
|
|
444
|
-
//
|
|
445
|
-
// replaying, so consecutive scrolls of one element collapse into the
|
|
446
|
-
// position it ended at. Every other property keeps each step, because
|
|
447
|
-
// "stepwise replay must show each state" (issue #93).
|
|
582
|
+
// Consecutive scrolls collapse; other state changes retain each step.
|
|
448
583
|
const last = state.queue[state.queue.length - 1];
|
|
449
584
|
const only = last && last.records.length === 1 ? last.records[0] : null;
|
|
450
585
|
if (
|
|
@@ -470,7 +605,7 @@ export function installMutationRecorderInPage(options) {
|
|
|
470
605
|
|
|
471
606
|
const onValue = (event) => {
|
|
472
607
|
const element = event.target;
|
|
473
|
-
if (!element || element.nodeType !== 1) {
|
|
608
|
+
if (!element || element.nodeType !== 1 || isIgnored(element)) {
|
|
474
609
|
return;
|
|
475
610
|
}
|
|
476
611
|
if (element.type === 'checkbox' || element.type === 'radio') {
|
|
@@ -517,7 +652,7 @@ export function installMutationRecorderInPage(options) {
|
|
|
517
652
|
pushLive({
|
|
518
653
|
kind: 'live-state',
|
|
519
654
|
property: 'scroll',
|
|
520
|
-
target: scrolling ?
|
|
655
|
+
target: scrolling ? identify(scrolling) : null,
|
|
521
656
|
before: null,
|
|
522
657
|
after: scrolling
|
|
523
658
|
? { top: scrolling.scrollTop, left: scrolling.scrollLeft }
|
package/src/traces/reader.js
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
* than rejected.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import fs from 'node:fs/promises';
|
|
11
10
|
import path from 'node:path';
|
|
11
|
+
import { readTraceText } from './storage.js';
|
|
12
12
|
import {
|
|
13
13
|
assertReadableManifest,
|
|
14
14
|
createManifest,
|
|
@@ -18,15 +18,8 @@ import {
|
|
|
18
18
|
TRACE_OUTCOME,
|
|
19
19
|
} from './schema.js';
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
return await fs.readFile(file, 'utf8');
|
|
24
|
-
} catch (error) {
|
|
25
|
-
if (error.code === 'ENOENT') {
|
|
26
|
-
return null;
|
|
27
|
-
}
|
|
28
|
-
throw error;
|
|
29
|
-
}
|
|
21
|
+
function readIfPresent(file) {
|
|
22
|
+
return readTraceText(file);
|
|
30
23
|
}
|
|
31
24
|
|
|
32
25
|
/**
|
|
@@ -64,6 +57,11 @@ export function parseNdjson(body) {
|
|
|
64
57
|
*/
|
|
65
58
|
export async function readTrace(bundlePath) {
|
|
66
59
|
const root = path.resolve(bundlePath);
|
|
60
|
+
const segments = await readIfPresent(path.join(root, 'segments.json'));
|
|
61
|
+
if (segments) {
|
|
62
|
+
const { readRollingTrace } = await import('./rolling.js');
|
|
63
|
+
return readRollingTrace(root, JSON.parse(segments), readTrace);
|
|
64
|
+
}
|
|
67
65
|
const manifestBody = await readIfPresent(
|
|
68
66
|
path.join(root, TRACE_FILES.MANIFEST)
|
|
69
67
|
);
|