browser-commander 0.26.3 → 0.28.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 +24 -23
- package/CHANGELOG.md +22 -0
- package/README.md +6 -0
- package/eslint.config.js +11 -0
- package/package.json +9 -4
- package/src/bindings.js +19 -0
- package/src/browser/attach/snapshot-launch.js +8 -6
- package/src/browser/attach/snapshot.js +39 -2
- package/src/browser/browser-cookie-database.js +71 -2
- package/src/browser/browser-cookie-session.js +183 -0
- package/src/browser/browser-cookies.js +29 -0
- package/src/browser/connector.js +117 -13
- package/src/browser/debugging-port.js +4 -2
- package/src/browser/default-browser.js +33 -9
- package/src/browser/launch-diagnostics.js +102 -0
- package/src/browser/launch-options.js +2 -1
- package/src/browser/launch-restrictions.json +33 -2
- package/src/browser/launcher.js +54 -9
- package/src/browser/migration/sqlite-snapshot.js +5 -27
- package/src/browser/navigation.js +173 -98
- package/src/browser/persistent-session.js +360 -0
- package/src/browser/profile-directory.js +20 -0
- package/src/browser/real-browser.js +142 -70
- package/src/browser/restrictions.js +13 -4
- package/src/browser/session-cookies.js +78 -0
- package/src/browser/session-persistence.js +72 -0
- package/src/browser/session-watchdog.js +71 -0
- package/src/browser/session-worker.js +70 -0
- package/src/browser/storage-state.js +32 -6
- 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 +66 -1
- package/src/cli/commands.js +113 -2
- package/src/cli/high-level-methods.js +40 -3
- package/src/cli/sessions.js +11 -3
- package/src/core/commander-features.js +105 -0
- package/src/core/constants.js +1 -0
- package/src/core/dialog-manager.js +7 -0
- package/src/core/managed-navigation.js +101 -0
- package/src/core/navigation-manager.js +66 -80
- package/src/core/navigation-operation.js +25 -0
- package/src/core/navigation-readiness.js +68 -16
- package/src/core/navigation-safety.js +51 -0
- package/src/core/network-tracker.js +39 -92
- package/src/core/page-session.js +3 -2
- package/src/core/page-trigger-manager.js +130 -42
- package/src/core/readiness.js +76 -14
- package/src/core/subscriptions.js +20 -0
- package/src/core/webdriver-page.js +38 -0
- package/src/elements/locators.js +61 -37
- package/src/elements/reusable.js +92 -0
- package/src/elements/selectors.js +37 -81
- package/src/elements/visibility.js +38 -21
- package/src/exports.js +14 -0
- package/src/factory.js +17 -16
- package/src/high-level/universal-logic.js +57 -21
- package/src/index.js +2 -0
- package/src/interactions/click.js +47 -0
- package/src/interactions/overlays.js +28 -0
- package/src/interactions/scroll.js +8 -2
- package/src/traces/bundle.js +14 -5
- package/src/traces/checkpoints.js +99 -0
- package/src/traces/links.js +117 -1
- package/src/traces/mutation-stream.js +1 -0
- package/src/traces/network.js +257 -0
- package/src/traces/page-capture.js +143 -52
- package/src/traces/reader.js +8 -10
- package/src/traces/recorder.js +221 -245
- package/src/traces/render.js +168 -0
- package/src/traces/rolling.js +180 -0
- package/src/traces/start-cleanup.js +15 -0
- package/src/traces/stop.js +159 -0
- package/src/traces/storage.js +43 -0
- package/src/utilities/subprocess.js +47 -4
- package/tests/e2e/cli.e2e.test.js +20 -1
- package/tests/e2e/click-readiness.e2e.test.js +1 -1
- package/tests/e2e/navigation-budget.e2e.test.js +132 -0
- package/tests/e2e/reusable-session.e2e.test.js +231 -0
- package/tests/unit/browser/attach/snapshot-launch.test.js +2 -2
- package/tests/unit/browser/attach/snapshot.test.js +33 -0
- package/tests/unit/browser/default-browser.test.js +18 -0
- package/tests/unit/browser/launch-building-blocks.test.js +5 -1
- package/tests/unit/browser/launch-diagnostics.test.js +52 -0
- 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/real-browser.test.js +1 -1
- package/tests/unit/browser/session-workflows.test.js +206 -0
- package/tests/unit/browser/storage-state.test.js +7 -2
- 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/navigation-budget.test.js +120 -0
- package/tests/unit/core/navigation-manager.test.js +23 -1
- package/tests/unit/core/readiness.test.js +21 -0
- package/tests/unit/core/trigger-readiness.test.js +39 -0
- package/tests/unit/core/webdriver-page.test.js +33 -0
- package/tests/unit/elements/reusable-helpers.test.js +83 -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/scripts/ci-timeout-budgets.test.js +1 -0
- package/tests/unit/traces/bundle.test.js +16 -0
- package/tests/unit/traces/links.test.js +2 -2
- package/tests/unit/traces/network.test.js +97 -0
|
@@ -32,6 +32,7 @@ export function captureSnapshotInPage(options) {
|
|
|
32
32
|
html: wantHtml = true,
|
|
33
33
|
liveControlState = true,
|
|
34
34
|
openShadowRoots = true,
|
|
35
|
+
ignoreSelectors = [],
|
|
35
36
|
maxHtmlBytes = 0,
|
|
36
37
|
} = options || {};
|
|
37
38
|
|
|
@@ -147,37 +148,93 @@ export function captureSnapshotInPage(options) {
|
|
|
147
148
|
} else if (secret) {
|
|
148
149
|
copy.textContent = redacted;
|
|
149
150
|
}
|
|
150
|
-
if (original.shadowRoot && openShadowRoots) {
|
|
151
|
+
if (!secret && original.shadowRoot && openShadowRoots) {
|
|
151
152
|
// Shadow content is not serialized by `outerHTML`; a declarative
|
|
152
153
|
// template keeps it in the same document the viewer renders.
|
|
153
154
|
const template = document.createElement('template');
|
|
154
155
|
template.setAttribute('shadowrootmode', 'open');
|
|
155
|
-
|
|
156
|
+
for (const child of original.shadowRoot.childNodes) {
|
|
157
|
+
{
|
|
158
|
+
const cloned = cloneWithState(child);
|
|
159
|
+
if (cloned) {
|
|
160
|
+
template.content.appendChild(cloned);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
156
164
|
copy.appendChild(template);
|
|
157
165
|
}
|
|
158
166
|
};
|
|
159
167
|
|
|
168
|
+
const cloneWithState = (original) => {
|
|
169
|
+
if (
|
|
170
|
+
original.nodeType === 1 &&
|
|
171
|
+
ignoreSelectors.some((selector) => original.matches(selector))
|
|
172
|
+
) {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
const copy = original.cloneNode(false);
|
|
176
|
+
if (original.nodeType !== 1) {
|
|
177
|
+
return copy;
|
|
178
|
+
}
|
|
179
|
+
const secret = isSecret(original);
|
|
180
|
+
if (!secret) {
|
|
181
|
+
for (const child of original.childNodes) {
|
|
182
|
+
{
|
|
183
|
+
const cloned = cloneWithState(child);
|
|
184
|
+
if (cloned) {
|
|
185
|
+
copy.appendChild(cloned);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
carryLiveState(original, copy, secret);
|
|
191
|
+
return copy;
|
|
192
|
+
};
|
|
193
|
+
|
|
160
194
|
let html = null;
|
|
161
195
|
let truncated = false;
|
|
162
196
|
if (wantHtml) {
|
|
163
|
-
const clone = document.documentElement
|
|
164
|
-
const originals = document.documentElement.querySelectorAll('*');
|
|
165
|
-
const copies = clone.querySelectorAll('*');
|
|
166
|
-
for (let index = 0; index < originals.length; index++) {
|
|
167
|
-
const copy = copies[index];
|
|
168
|
-
if (!copy) {
|
|
169
|
-
break;
|
|
170
|
-
}
|
|
171
|
-
carryLiveState(originals[index], copy, isSecret(originals[index]));
|
|
172
|
-
}
|
|
197
|
+
const clone = cloneWithState(document.documentElement);
|
|
173
198
|
|
|
174
|
-
html = `<!DOCTYPE html>\n${clone
|
|
199
|
+
html = `<!DOCTYPE html>\n${clone?.outerHTML ?? ''}`;
|
|
175
200
|
if (maxHtmlBytes > 0 && html.length > maxHtmlBytes) {
|
|
176
201
|
html = html.slice(0, maxHtmlBytes);
|
|
177
202
|
truncated = true;
|
|
178
203
|
}
|
|
179
204
|
}
|
|
180
205
|
|
|
206
|
+
const visibleText = [];
|
|
207
|
+
const collectText = (node) => {
|
|
208
|
+
if (node.nodeType === 3 && node.parentElement) {
|
|
209
|
+
const parent = node.parentElement;
|
|
210
|
+
if (
|
|
211
|
+
!parent.getClientRects().length ||
|
|
212
|
+
window.getComputedStyle(parent).visibility === 'hidden' ||
|
|
213
|
+
ignoreSelectors.some((selector) => parent.closest(selector))
|
|
214
|
+
) {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
const secret = redactSelectors.some((selector) =>
|
|
218
|
+
parent.closest(selector)
|
|
219
|
+
);
|
|
220
|
+
if (node.nodeValue.trim()) {
|
|
221
|
+
visibleText.push(
|
|
222
|
+
`${cssPath(parent)}: ${secret ? redacted : node.nodeValue.trim()}`
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
} else {
|
|
226
|
+
for (const child of node.childNodes ?? []) {
|
|
227
|
+
collectText(child);
|
|
228
|
+
}
|
|
229
|
+
if (node.shadowRoot && openShadowRoots) {
|
|
230
|
+
collectText(node.shadowRoot);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
if (options?.captureText) {
|
|
235
|
+
collectText(document.documentElement);
|
|
236
|
+
}
|
|
237
|
+
|
|
181
238
|
return {
|
|
182
239
|
html,
|
|
183
240
|
state: {
|
|
@@ -190,6 +247,7 @@ export function captureSnapshotInPage(options) {
|
|
|
190
247
|
? cssPath(document.activeElement)
|
|
191
248
|
: null,
|
|
192
249
|
controls,
|
|
250
|
+
...(options?.captureText ? { text: visibleText.join('\n') } : {}),
|
|
193
251
|
frames: [...document.querySelectorAll('iframe, frame')].map((frame) => ({
|
|
194
252
|
path: cssPath(frame),
|
|
195
253
|
src: frame.getAttribute('src'),
|
|
@@ -225,6 +283,7 @@ export function installMutationRecorderInPage(options) {
|
|
|
225
283
|
redactSelectors = [],
|
|
226
284
|
redacted = '[redacted]',
|
|
227
285
|
maxQueued = 5000,
|
|
286
|
+
ignoreSelectors = [],
|
|
228
287
|
liveState = true,
|
|
229
288
|
} = options || {};
|
|
230
289
|
|
|
@@ -244,13 +303,13 @@ export function installMutationRecorderInPage(options) {
|
|
|
244
303
|
mainFrame: window.top === window,
|
|
245
304
|
};
|
|
246
305
|
|
|
247
|
-
const
|
|
306
|
+
const matchesAny = (node, selectors) => {
|
|
248
307
|
const element =
|
|
249
308
|
node && node.nodeType === 1 ? node : node && node.parentElement;
|
|
250
309
|
if (!element || !element.matches) {
|
|
251
310
|
return false;
|
|
252
311
|
}
|
|
253
|
-
return
|
|
312
|
+
return selectors.some((selector) => {
|
|
254
313
|
try {
|
|
255
314
|
return element.matches(selector) || element.closest(selector) !== null;
|
|
256
315
|
} catch {
|
|
@@ -258,6 +317,25 @@ export function installMutationRecorderInPage(options) {
|
|
|
258
317
|
}
|
|
259
318
|
});
|
|
260
319
|
};
|
|
320
|
+
const isSecret = (node) => matchesAny(node, redactSelectors);
|
|
321
|
+
const isIgnored = (node) => matchesAny(node, ignoreSelectors);
|
|
322
|
+
const sanitized = (node) => {
|
|
323
|
+
const clone = node.cloneNode(true);
|
|
324
|
+
for (const selector of ignoreSelectors) {
|
|
325
|
+
for (const element of clone.querySelectorAll(selector)) {
|
|
326
|
+
element.remove();
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
for (const selector of redactSelectors) {
|
|
330
|
+
for (const element of clone.querySelectorAll(selector)) {
|
|
331
|
+
element.textContent = redacted;
|
|
332
|
+
if (element.hasAttribute('value')) {
|
|
333
|
+
element.setAttribute('value', redacted);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
return clone;
|
|
338
|
+
};
|
|
261
339
|
|
|
262
340
|
// A path the viewer can look the node up by: replay needs to find the same
|
|
263
341
|
// element again in a parsed copy of the checkpoint.
|
|
@@ -288,12 +366,16 @@ export function installMutationRecorderInPage(options) {
|
|
|
288
366
|
};
|
|
289
367
|
|
|
290
368
|
const describe = (node) => {
|
|
291
|
-
if (!node) {
|
|
369
|
+
if (!node || isIgnored(node)) {
|
|
292
370
|
return null;
|
|
293
371
|
}
|
|
294
372
|
if (node.nodeType === 3) {
|
|
295
373
|
return {
|
|
296
374
|
type: 'text',
|
|
375
|
+
visible: node.parentElement
|
|
376
|
+
? node.parentElement.getClientRects().length > 0 &&
|
|
377
|
+
window.getComputedStyle(node.parentElement).visibility !== 'hidden'
|
|
378
|
+
: true,
|
|
297
379
|
path: pathOf(node.parentElement),
|
|
298
380
|
text: isSecret(node) ? redacted : String(node.nodeValue).slice(0, 2000),
|
|
299
381
|
};
|
|
@@ -301,12 +383,19 @@ export function installMutationRecorderInPage(options) {
|
|
|
301
383
|
if (node.nodeType !== 1) {
|
|
302
384
|
return { type: 'node', nodeType: node.nodeType };
|
|
303
385
|
}
|
|
386
|
+
const clone = sanitized(node);
|
|
304
387
|
return {
|
|
305
388
|
type: 'element',
|
|
306
389
|
tag: node.localName,
|
|
307
390
|
id: node.id || null,
|
|
308
391
|
path: pathOf(node),
|
|
309
|
-
html: isSecret(node) ? redacted : String(
|
|
392
|
+
html: isSecret(node) ? redacted : String(clone.outerHTML).slice(0, 4000),
|
|
393
|
+
text: isSecret(node)
|
|
394
|
+
? redacted
|
|
395
|
+
: String(clone.textContent).slice(0, 2000),
|
|
396
|
+
visible:
|
|
397
|
+
node.getClientRects().length > 0 &&
|
|
398
|
+
window.getComputedStyle(node).visibility !== 'hidden',
|
|
310
399
|
};
|
|
311
400
|
};
|
|
312
401
|
|
|
@@ -364,41 +453,43 @@ export function installMutationRecorderInPage(options) {
|
|
|
364
453
|
return;
|
|
365
454
|
}
|
|
366
455
|
push({
|
|
367
|
-
records: records
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
secret
|
|
456
|
+
records: records
|
|
457
|
+
.filter((record) => !isIgnored(record.target))
|
|
458
|
+
.map((record) => {
|
|
459
|
+
const entry = {
|
|
460
|
+
kind: record.type,
|
|
461
|
+
target: describe(record.target),
|
|
462
|
+
};
|
|
463
|
+
if (record.type === 'attributes') {
|
|
464
|
+
entry.attribute = record.attributeName;
|
|
465
|
+
const secret =
|
|
466
|
+
isSecret(record.target) ||
|
|
467
|
+
redactSelectors.includes(record.attributeName);
|
|
468
|
+
entry.before = secret ? redacted : record.oldValue;
|
|
469
|
+
entry.after =
|
|
470
|
+
secret || !record.target.getAttribute
|
|
471
|
+
? redacted
|
|
472
|
+
: record.target.getAttribute(record.attributeName);
|
|
473
|
+
} else if (record.type === 'characterData') {
|
|
474
|
+
entry.before = isSecret(record.target) ? redacted : record.oldValue;
|
|
475
|
+
entry.after = isSecret(record.target)
|
|
380
476
|
? 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
|
-
}),
|
|
477
|
+
: record.target.nodeValue;
|
|
478
|
+
} else {
|
|
479
|
+
// Where a node went is as much of the change as what it was:
|
|
480
|
+
// appending every addition to the end replays an insertion before a
|
|
481
|
+
// sibling in the wrong place, and drops moves and removals entirely.
|
|
482
|
+
entry.added = [...record.addedNodes]
|
|
483
|
+
.filter((node) => !isIgnored(node))
|
|
484
|
+
.map((node) => describeAt(node, record));
|
|
485
|
+
entry.removed = [...record.removedNodes]
|
|
486
|
+
.filter((node) => !isIgnored(node))
|
|
487
|
+
.map((node) => describeAt(node, record));
|
|
488
|
+
entry.previous = describe(record.previousSibling);
|
|
489
|
+
entry.next = describe(record.nextSibling);
|
|
490
|
+
}
|
|
491
|
+
return entry;
|
|
492
|
+
}),
|
|
402
493
|
});
|
|
403
494
|
});
|
|
404
495
|
|
|
@@ -470,7 +561,7 @@ export function installMutationRecorderInPage(options) {
|
|
|
470
561
|
|
|
471
562
|
const onValue = (event) => {
|
|
472
563
|
const element = event.target;
|
|
473
|
-
if (!element || element.nodeType !== 1) {
|
|
564
|
+
if (!element || element.nodeType !== 1 || isIgnored(element)) {
|
|
474
565
|
return;
|
|
475
566
|
}
|
|
476
567
|
if (element.type === 'checkbox' || element.type === 'radio') {
|
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
|
);
|