browser-debugger-cli 0.11.0 → 0.12.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/README.md +142 -79
- package/dist/commands/css.d.ts +13 -0
- package/dist/commands/css.js +53 -0
- package/dist/commands/dom/audit.d.ts +14 -0
- package/dist/commands/dom/audit.js +87 -0
- package/dist/commands/dom/formInteraction.js +36 -6
- package/dist/commands/dom/helpers/keyAttributes.d.ts +3 -2
- package/dist/commands/dom/helpers/keyAttributes.js +6 -4
- package/dist/commands/dom/helpers/screenshot.d.ts +1 -0
- package/dist/commands/dom/helpers/screenshot.js +158 -38
- package/dist/commands/dom/index.js +4 -1
- package/dist/commands/dom/screenshot.js +10 -6
- package/dist/commands/dom/wait.js +5 -3
- package/dist/commands/helpJson.js +1 -1
- package/dist/commands/optionBehaviors.js +21 -6
- package/dist/commands/page.js +7 -4
- package/dist/commands/peek.d.ts +7 -0
- package/dist/commands/peek.js +65 -23
- package/dist/commands/shared/optionTypes.d.ts +5 -1
- package/dist/commands/start.d.ts +13 -0
- package/dist/commands/start.js +19 -2
- package/dist/commands/tail.d.ts +7 -1
- package/dist/commands/tail.js +13 -62
- package/dist/commands.js +2 -0
- package/dist/daemon/session/commandRegistry.js +7 -1
- package/dist/daemon/session/plugins.js +4 -52
- package/dist/daemon.js +7986 -6848
- package/dist/errors/messages.d.ts +34 -4
- package/dist/errors/messages.js +68 -5
- package/dist/index.js +610 -186
- package/dist/ipc/client.d.ts +4 -0
- package/dist/ipc/client.js +8 -0
- package/dist/ipc/protocol/auditTypes.d.ts +129 -0
- package/dist/ipc/protocol/auditTypes.js +6 -0
- package/dist/ipc/protocol/commands.d.ts +23 -0
- package/dist/ipc/protocol/commands.js +2 -0
- package/dist/ipc/protocol/domTypes.d.ts +4 -0
- package/dist/ipc/protocol/inspectTypes.d.ts +71 -8
- package/dist/runtime/css/search.d.ts +39 -0
- package/dist/runtime/css/search.js +122 -0
- package/dist/runtime/dom/actionEffects.d.ts +4 -1
- package/dist/runtime/dom/actionEffects.js +8 -4
- package/dist/runtime/dom/audit.d.ts +19 -0
- package/dist/runtime/dom/audit.js +36 -0
- package/dist/runtime/dom/auditModel.d.ts +45 -0
- package/dist/runtime/dom/auditModel.js +215 -0
- package/dist/runtime/dom/auditScripts.d.ts +107 -0
- package/dist/runtime/dom/auditScripts.js +112 -0
- package/dist/runtime/dom/elementGeometry.d.ts +8 -2
- package/dist/runtime/dom/elementGeometry.js +24 -8
- package/dist/runtime/dom/elementInfo.d.ts +3 -2
- package/dist/runtime/dom/elementInfo.js +8 -2
- package/dist/runtime/dom/formFillHelpers/fill.js +2 -2
- package/dist/runtime/dom/inspect.d.ts +7 -0
- package/dist/runtime/dom/inspect.js +88 -23
- package/dist/runtime/dom/inspectAllStyles.d.ts +16 -4
- package/dist/runtime/dom/inspectAllStyles.js +89 -7
- package/dist/runtime/dom/inspectCascade.d.ts +19 -2
- package/dist/runtime/dom/inspectCascade.js +214 -44
- package/dist/runtime/dom/inspectCascadeModel.d.ts +8 -0
- package/dist/runtime/dom/inspectCascadeModel.js +108 -34
- package/dist/runtime/dom/inspectHints.d.ts +26 -3
- package/dist/runtime/dom/inspectHints.js +125 -9
- package/dist/runtime/dom/inspectModel.d.ts +3 -0
- package/dist/runtime/dom/inspectModel.js +30 -7
- package/dist/runtime/dom/inspectPaintModel.d.ts +48 -22
- package/dist/runtime/dom/inspectPaintModel.js +180 -68
- package/dist/runtime/dom/inspectRules.d.ts +19 -0
- package/dist/runtime/dom/inspectRules.js +21 -5
- package/dist/runtime/dom/inspectScripts.d.ts +85 -12
- package/dist/runtime/dom/inspectScripts.js +314 -28
- package/dist/runtime/dom/inspectTree.js +10 -2
- package/dist/runtime/dom/inspectWhyModel.d.ts +2 -1
- package/dist/runtime/dom/inspectWhyModel.js +52 -10
- package/dist/runtime/dom/layout.js +31 -9
- package/dist/runtime/dom/reactEventHelpers.d.ts +7 -0
- package/dist/runtime/dom/reactEventHelpers.js +27 -9
- package/dist/runtime/page/emulation.d.ts +13 -4
- package/dist/runtime/page/emulation.js +69 -4
- package/dist/runtime/page/userAgent.d.ts +17 -0
- package/dist/runtime/page/userAgent.js +57 -0
- package/dist/types.d.ts +4 -0
- package/dist/ui/formatters/audit.d.ts +19 -0
- package/dist/ui/formatters/audit.js +106 -0
- package/dist/ui/formatters/dom.d.ts +1 -1
- package/dist/ui/formatters/dom.js +6 -3
- package/dist/ui/formatters/inspect.js +42 -15
- package/dist/ui/formatters/status.js +1 -1
- package/dist/ui/messages/commands.d.ts +44 -7
- package/dist/ui/messages/commands.js +83 -11
- package/dist/ui/messages/preview.d.ts +6 -0
- package/dist/ui/messages/preview.js +9 -1
- package/dist/utils/cssValues.js +36 -4
- package/dist/utils/decisionTrees.js +0 -5
- package/dist/utils/suggestions.d.ts +4 -2
- package/dist/utils/suggestions.js +7 -5
- package/dist/utils/taskMappings.js +1 -1
- package/package.json +3 -2
|
@@ -175,14 +175,22 @@ async function useUnitPixelRatio(devicePixelRatio, viewport) {
|
|
|
175
175
|
const sessionViewport = readSessionMetadata()?.viewport;
|
|
176
176
|
const size = sessionViewport ?? (await windowSize(viewport));
|
|
177
177
|
await callCDP('Emulation.setDeviceMetricsOverride', viewportOverride(size, 1));
|
|
178
|
-
return
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
178
|
+
return restoreSessionMetrics;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Put back the session's device metrics: its `--viewport` (and a phone's
|
|
182
|
+
* touch input, which a capture beyond the viewport turns off), else none.
|
|
183
|
+
*/
|
|
184
|
+
async function restoreSessionMetrics() {
|
|
185
|
+
const sessionViewport = readSessionMetadata()?.viewport;
|
|
186
|
+
if (!sessionViewport) {
|
|
187
|
+
await callCDP('Emulation.clearDeviceMetricsOverride', {});
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
await callCDP('Emulation.setDeviceMetricsOverride', viewportOverride(sessionViewport));
|
|
191
|
+
if (sessionViewport.mobile) {
|
|
192
|
+
await callCDP('Emulation.setTouchEmulationEnabled', { enabled: true, maxTouchPoints: 5 });
|
|
193
|
+
}
|
|
186
194
|
}
|
|
187
195
|
/**
|
|
188
196
|
* Get the bounding box (border box, so padding and border are included) of an
|
|
@@ -334,13 +342,17 @@ export async function capturePageScreenshot(outputPath, options = {}) {
|
|
|
334
342
|
/** Descendants {@link CONTENT_OVERFLOW_JS} looks at, so a huge element stays cheap */
|
|
335
343
|
const OVERFLOW_SCAN_LIMIT = 2000;
|
|
336
344
|
/**
|
|
337
|
-
* Page-side distances (CSS px, never negative) by which an element
|
|
338
|
-
*
|
|
339
|
-
* absolutely positioned and transformed
|
|
340
|
-
*
|
|
341
|
-
*
|
|
342
|
-
*
|
|
343
|
-
* element clips its
|
|
345
|
+
* Page-side distances (CSS px, never negative) by which what an element
|
|
346
|
+
* paints reaches beyond its border box on each side: its rendered
|
|
347
|
+
* descendants (uncleared floats, absolutely positioned and transformed
|
|
348
|
+
* children), its text (descenders past a tight line height, read from its
|
|
349
|
+
* scroll size beyond its client size, in transformed px, to the left on an
|
|
350
|
+
* RTL element), and its own outer box shadows and outline (a focus ring).
|
|
351
|
+
* Descendants of an element that clips its overflow (`overflow` other than
|
|
352
|
+
* `visible`) are cut off by it and not counted, nor are fixed ones (they
|
|
353
|
+
* belong to the viewport) or what lies outside the document (skip links at
|
|
354
|
+
* -9999px). Only the shadows and outline count when the element clips its
|
|
355
|
+
* own overflow.
|
|
344
356
|
*/
|
|
345
357
|
const CONTENT_OVERFLOW_JS = `function () {
|
|
346
358
|
const view = this.ownerDocument.defaultView;
|
|
@@ -365,8 +377,37 @@ const CONTENT_OVERFLOW_JS = `function () {
|
|
|
365
377
|
if (!clips(style)) walk(child);
|
|
366
378
|
}
|
|
367
379
|
};
|
|
368
|
-
|
|
369
|
-
|
|
380
|
+
const ownStyle = view.getComputedStyle(this);
|
|
381
|
+
if (!clips(ownStyle)) {
|
|
382
|
+
walk(this);
|
|
383
|
+
const scaleX = this.offsetWidth ? own.width / this.offsetWidth : 1;
|
|
384
|
+
const scaleY = this.offsetHeight ? own.height / this.offsetHeight : 1;
|
|
385
|
+
const wider = Math.max(0, this.scrollWidth - this.clientWidth) * scaleX;
|
|
386
|
+
const taller = Math.max(0, this.scrollHeight - this.clientHeight) * scaleY;
|
|
387
|
+
if (ownStyle.direction === 'rtl') reach.left = Math.min(reach.left, own.left - wider);
|
|
388
|
+
else reach.right = Math.max(reach.right, own.right + wider);
|
|
389
|
+
reach.bottom = Math.max(reach.bottom, own.bottom + taller);
|
|
390
|
+
}
|
|
391
|
+
const ink = { left: 0, top: 0, right: 0, bottom: 0 };
|
|
392
|
+
const grow = (side, amount) => { ink[side] = Math.max(ink[side], amount); };
|
|
393
|
+
for (const layer of ownStyle.boxShadow === 'none' ? [] : ownStyle.boxShadow.split(/,(?![^(]*\\))/)) {
|
|
394
|
+
if (/\\binset\\b/.test(layer)) continue;
|
|
395
|
+
const [x = 0, y = 0, blur = 0, spread = 0] = (layer.replace(/(rgba?|hsla?|color|oklch|lab|lch)\\([^)]*\\)/g, '').match(/-?[\\d.]+px/g) || []).map(parseFloat);
|
|
396
|
+
grow('left', blur + spread - x);
|
|
397
|
+
grow('right', blur + spread + x);
|
|
398
|
+
grow('top', blur + spread - y);
|
|
399
|
+
grow('bottom', blur + spread + y);
|
|
400
|
+
}
|
|
401
|
+
if (ownStyle.outlineStyle !== 'none') {
|
|
402
|
+
const outline = parseFloat(ownStyle.outlineWidth) + parseFloat(ownStyle.outlineOffset);
|
|
403
|
+
['left', 'top', 'right', 'bottom'].forEach((side) => grow(side, outline));
|
|
404
|
+
}
|
|
405
|
+
return {
|
|
406
|
+
left: Math.max(own.left - reach.left, ink.left),
|
|
407
|
+
top: Math.max(own.top - reach.top, ink.top),
|
|
408
|
+
right: Math.max(reach.right - own.right, ink.right),
|
|
409
|
+
bottom: Math.max(reach.bottom - own.bottom, ink.bottom)
|
|
410
|
+
};
|
|
370
411
|
}`;
|
|
371
412
|
/**
|
|
372
413
|
* The visible viewport (without scrollbars) in CSS px.
|
|
@@ -393,27 +434,80 @@ function insideView(area, view) {
|
|
|
393
434
|
}
|
|
394
435
|
/**
|
|
395
436
|
* Measure the area to capture and, when it fits in the viewport but is not
|
|
396
|
-
* in view, scroll it to the middle first
|
|
397
|
-
* keeps the page as it is; one
|
|
398
|
-
* without its scrollbar,
|
|
399
|
-
*
|
|
437
|
+
* in view, scroll it to the middle first (the returned position puts the
|
|
438
|
+
* page back). A capture inside the viewport keeps the page as it is; one
|
|
439
|
+
* beyond it makes Chrome lay the page out without its scrollbar, so for an
|
|
440
|
+
* area larger than the viewport the scrollbars are hidden first and the
|
|
441
|
+
* area measured in that layout (centered content would else move by half
|
|
442
|
+
* the scrollbar's width).
|
|
400
443
|
*
|
|
401
444
|
* @param ref - Node reference
|
|
402
|
-
* @
|
|
445
|
+
* @param padding - Extra space around the area (CSS px)
|
|
446
|
+
* @returns Border box, area to capture (viewport coordinates), whether it is
|
|
447
|
+
* in view, and the scroll position to restore when it scrolled
|
|
403
448
|
*/
|
|
404
|
-
async function measureInView(ref) {
|
|
449
|
+
async function measureInView(ref, padding) {
|
|
405
450
|
const view = await visibleViewport();
|
|
406
451
|
let box = await getElementBounds(ref);
|
|
407
|
-
let bounds = await captureArea(ref, box);
|
|
452
|
+
let bounds = await captureArea(ref, box, padding);
|
|
408
453
|
const fits = bounds.width <= view.width && bounds.height <= view.height;
|
|
409
|
-
if (fits
|
|
410
|
-
|
|
411
|
-
const dy = bounds.y + bounds.height / 2 - view.height / 2;
|
|
412
|
-
await callCDP('Runtime.evaluate', { expression: `window.scrollBy(${dx}, ${dy})` });
|
|
454
|
+
if (!fits) {
|
|
455
|
+
await keepLayoutWithoutScrollbars(view);
|
|
413
456
|
box = await getElementBounds(ref);
|
|
414
|
-
bounds
|
|
457
|
+
return { box, bounds: await captureArea(ref, box, padding), inView: false };
|
|
415
458
|
}
|
|
416
|
-
|
|
459
|
+
if (insideView(bounds, view))
|
|
460
|
+
return { box, bounds, inView: true };
|
|
461
|
+
const scrolledFrom = await scrollPosition();
|
|
462
|
+
const dx = bounds.x + bounds.width / 2 - view.width / 2;
|
|
463
|
+
const dy = bounds.y + bounds.height / 2 - view.height / 2;
|
|
464
|
+
await callCDP('Runtime.evaluate', { expression: `window.scrollBy(${dx}, ${dy})` });
|
|
465
|
+
try {
|
|
466
|
+
box = await getElementBounds(ref);
|
|
467
|
+
bounds = await captureArea(ref, box, padding);
|
|
468
|
+
}
|
|
469
|
+
catch (error) {
|
|
470
|
+
await restoreScrollPosition(scrolledFrom);
|
|
471
|
+
throw error;
|
|
472
|
+
}
|
|
473
|
+
return { box, bounds, inView: insideView(bounds, view), scrolledFrom };
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Lay the page out at its current width without scrollbars: a capture
|
|
477
|
+
* beyond the viewport hides them, and without this the page would widen by
|
|
478
|
+
* the scrollbar and centered content move after it was measured. The
|
|
479
|
+
* viewport is overridden at the visible width (CSS px, pixel ratio 1; still
|
|
480
|
+
* a phone in a `--mobile` session) until
|
|
481
|
+
* {@link restoreViewport}.
|
|
482
|
+
*
|
|
483
|
+
* @param view - Visible viewport size
|
|
484
|
+
*/
|
|
485
|
+
async function keepLayoutWithoutScrollbars(view) {
|
|
486
|
+
const phone = readSessionMetadata()?.viewport?.mobile;
|
|
487
|
+
await callCDP('Emulation.setScrollbarsHidden', { hidden: true });
|
|
488
|
+
await callCDP('Emulation.setDeviceMetricsOverride', viewportOverride({ ...view, ...(phone && { mobile: true }) }, 1));
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Put back the viewport a capture changed: the session's `--viewport`, else
|
|
492
|
+
* none, with scrollbars shown.
|
|
493
|
+
*/
|
|
494
|
+
async function restoreViewport() {
|
|
495
|
+
await callCDP('Emulation.setScrollbarsHidden', { hidden: false });
|
|
496
|
+
await restoreSessionMetrics();
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* The page's scroll position.
|
|
500
|
+
*
|
|
501
|
+
* @returns Scroll offsets in CSS px
|
|
502
|
+
*/
|
|
503
|
+
async function scrollPosition() {
|
|
504
|
+
const response = await callCDP('Runtime.evaluate', {
|
|
505
|
+
expression: '[window.scrollX, window.scrollY]',
|
|
506
|
+
returnByValue: true,
|
|
507
|
+
});
|
|
508
|
+
const value = response.data?.result?.result?.value;
|
|
509
|
+
const [x, y] = Array.isArray(value) ? value : [];
|
|
510
|
+
return { x: x ?? 0, y: y ?? 0 };
|
|
417
511
|
}
|
|
418
512
|
/** Overflow (px) below which the capture keeps to the border box (subpixel rounding) */
|
|
419
513
|
const OVERFLOW_SLACK = 1;
|
|
@@ -424,9 +518,30 @@ const OVERFLOW_SLACK = 1;
|
|
|
424
518
|
*
|
|
425
519
|
* @param ref - Node reference
|
|
426
520
|
* @param bounds - Border box (DOM.getBoxModel coordinates)
|
|
427
|
-
* @
|
|
521
|
+
* @param padding - Extra space around it (CSS px)
|
|
522
|
+
* @returns The area, or the border box (with the padding) when nothing
|
|
523
|
+
* overflows (or the page cannot be asked)
|
|
524
|
+
*/
|
|
525
|
+
async function captureArea(ref, bounds, padding) {
|
|
526
|
+
const area = await paintedArea(ref, bounds);
|
|
527
|
+
return padding > 0
|
|
528
|
+
? {
|
|
529
|
+
x: area.x - padding,
|
|
530
|
+
y: area.y - padding,
|
|
531
|
+
width: area.width + 2 * padding,
|
|
532
|
+
height: area.height + 2 * padding,
|
|
533
|
+
}
|
|
534
|
+
: area;
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* The border box grown to what the element paints beyond it
|
|
538
|
+
* ({@link CONTENT_OVERFLOW_JS}).
|
|
539
|
+
*
|
|
540
|
+
* @param ref - Node reference
|
|
541
|
+
* @param bounds - Border box
|
|
542
|
+
* @returns The area
|
|
428
543
|
*/
|
|
429
|
-
async function
|
|
544
|
+
async function paintedArea(ref, bounds) {
|
|
430
545
|
const objectGroup = `bdg-shot-${process.pid}`;
|
|
431
546
|
try {
|
|
432
547
|
const resolved = await callCDP('DOM.resolveNode', { ...ref, objectGroup });
|
|
@@ -478,16 +593,20 @@ export async function captureElementScreenshot(outputPath, ref, options = {}) {
|
|
|
478
593
|
const devicePixelRatio = dprResponse.data?.result?.result?.value ?? 1;
|
|
479
594
|
const before = (await callCDP('Page.getLayoutMetrics', {})).data?.result;
|
|
480
595
|
const restoreMetrics = await useUnitPixelRatio(devicePixelRatio, before?.visualViewport ?? { clientWidth: 800, clientHeight: 600 });
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
596
|
+
const restore = async (wide, scrolledFrom) => {
|
|
597
|
+
await (wide ? restoreViewport() : restoreMetrics());
|
|
598
|
+
if (scrolledFrom)
|
|
599
|
+
await restoreScrollPosition(scrolledFrom);
|
|
600
|
+
};
|
|
601
|
+
let measured;
|
|
484
602
|
try {
|
|
485
|
-
|
|
603
|
+
measured = await measureInView(ref, options.padding ?? 0);
|
|
486
604
|
}
|
|
487
605
|
catch (error) {
|
|
488
|
-
await
|
|
606
|
+
await restoreViewport();
|
|
489
607
|
throw error;
|
|
490
608
|
}
|
|
609
|
+
const { box, bounds, inView, scrolledFrom } = measured;
|
|
491
610
|
const originalWidth = bounds.width;
|
|
492
611
|
const originalHeight = bounds.height;
|
|
493
612
|
const resized = shouldResize(originalWidth, originalHeight, noResize);
|
|
@@ -514,7 +633,7 @@ export async function captureElementScreenshot(outputPath, ref, options = {}) {
|
|
|
514
633
|
screenshotResult = screenshotResponse.data?.result;
|
|
515
634
|
}
|
|
516
635
|
finally {
|
|
517
|
-
await
|
|
636
|
+
await restore(!inView, scrolledFrom);
|
|
518
637
|
}
|
|
519
638
|
if (!screenshotResult?.data) {
|
|
520
639
|
throw new CDPConnectionError('No screenshot data returned', new Error('Empty response'));
|
|
@@ -532,6 +651,7 @@ export async function captureElementScreenshot(outputPath, ref, options = {}) {
|
|
|
532
651
|
element: {
|
|
533
652
|
bounds: roundBounds(onPage(box)),
|
|
534
653
|
...(bounds !== box && { captured: roundBounds(clip) }),
|
|
654
|
+
...(options.padding && { padding: options.padding }),
|
|
535
655
|
},
|
|
536
656
|
};
|
|
537
657
|
if (quality !== undefined) {
|
|
@@ -22,6 +22,7 @@ import { registerFormCommand } from './form.js';
|
|
|
22
22
|
import { handleDomFrames } from './frames.js';
|
|
23
23
|
import { DOM_GET_DEFAULT_SELECTOR, handleDomGet } from './get.js';
|
|
24
24
|
import { registerInspectCommand } from './inspect.js';
|
|
25
|
+
import { registerAuditCommand } from './audit.js';
|
|
25
26
|
import { registerLayoutCommand } from './layout.js';
|
|
26
27
|
import { registerListenersCommand } from './listeners.js';
|
|
27
28
|
import { handleDomQuery } from './query.js';
|
|
@@ -41,6 +42,7 @@ export function registerDomCommands(program) {
|
|
|
41
42
|
registerFormCommand(dom);
|
|
42
43
|
registerListenersCommand(dom);
|
|
43
44
|
registerLayoutCommand(dom);
|
|
45
|
+
registerAuditCommand(dom);
|
|
44
46
|
registerInspectCommand(dom);
|
|
45
47
|
registerWaitCommand(dom);
|
|
46
48
|
dom
|
|
@@ -97,7 +99,8 @@ export function registerDomCommands(program) {
|
|
|
97
99
|
.argument('<path>', 'Output file path, or directory for --follow mode')
|
|
98
100
|
.argument('[selector]', 'Element to capture: CSS selector or index from a query (same as --selector / --index)')
|
|
99
101
|
.option('--selector <selector>', 'CSS selector for element capture')
|
|
100
|
-
.option('--index <number>', 'Cached element index (0-based) from previous query', integerOption(0))
|
|
102
|
+
.option('--index <number>', 'Cached element index (0-based) from a previous query; with --selector, which match', integerOption(0))
|
|
103
|
+
.option('--padding <px>', 'Element capture: extra space around it (shadows and focus rings are included anyway)', integerOption(0, 500))
|
|
101
104
|
.option('--format <format>', 'Image format: png or jpeg/jpg (default: from the file extension, else png)', screenshotFormatOption)
|
|
102
105
|
.option('--quality <number>', 'JPEG quality 0-100 (default: 90)', integerOption(0, 100))
|
|
103
106
|
.option('--no-full-page', 'Capture viewport only (default: full page)')
|
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { extname } from 'path';
|
|
5
5
|
import { DomElementResolver } from './DomElementResolver.js';
|
|
6
|
-
import { capturePageScreenshot, captureElementScreenshot, resolveSelector, } from './helpers/index.js';
|
|
6
|
+
import { capturePageScreenshot, captureElementScreenshot, resolveSelector, selectMatch, } from './helpers/index.js';
|
|
7
7
|
import { runCommand } from '../shared/CommandRunner.js';
|
|
8
8
|
import { assertFilePath, outputPathError } from '../shared/outputFile.js';
|
|
9
9
|
import { positiveIntRule } from '../shared/validation.js';
|
|
10
10
|
import { CommandError } from '../../errors/index.js';
|
|
11
|
-
import {
|
|
11
|
+
import { conflictingTargetError, genericError } from '../../errors/messages.js';
|
|
12
12
|
import { missingArgumentError } from '../../errors/messages.js';
|
|
13
13
|
import { OutputBuilder, buildSuccessResponse } from '../../ui/OutputBuilder.js';
|
|
14
14
|
import { formatDomScreenshot } from '../../ui/formatters/dom.js';
|
|
@@ -60,12 +60,16 @@ function buildElementScreenshotOptions(options) {
|
|
|
60
60
|
format: options.format,
|
|
61
61
|
quality: options.quality,
|
|
62
62
|
noResize: options.resize === false,
|
|
63
|
+
padding: options.padding,
|
|
63
64
|
});
|
|
64
65
|
}
|
|
65
66
|
function hasElementTarget(options) {
|
|
66
67
|
return options.selector !== undefined || options.index !== undefined;
|
|
67
68
|
}
|
|
68
69
|
async function resolveElementNodeId(options) {
|
|
70
|
+
if (options.selector !== undefined && options.index !== undefined) {
|
|
71
|
+
return { backendNodeId: await selectMatch(options.selector, options.index) };
|
|
72
|
+
}
|
|
69
73
|
if (options.index !== undefined) {
|
|
70
74
|
const resolver = DomElementResolver.getInstance();
|
|
71
75
|
const node = await resolver.getNodeIdForIndex(options.index);
|
|
@@ -207,8 +211,8 @@ function reportSequenceError(error, captured, json) {
|
|
|
207
211
|
process.exit(exitCode);
|
|
208
212
|
}
|
|
209
213
|
/**
|
|
210
|
-
* Reject options that would be ignored: `--
|
|
211
|
-
*
|
|
214
|
+
* Reject options that would be ignored: `--quality` for a PNG, and
|
|
215
|
+
* `--padding` without an element.
|
|
212
216
|
*
|
|
213
217
|
* @param outputPath - File to write
|
|
214
218
|
* @param options - Command options
|
|
@@ -216,8 +220,8 @@ function reportSequenceError(error, captured, json) {
|
|
|
216
220
|
*/
|
|
217
221
|
function assertScreenshotOptions(outputPath, options) {
|
|
218
222
|
let message;
|
|
219
|
-
if (options.
|
|
220
|
-
message =
|
|
223
|
+
if (options.padding !== undefined && !hasElementTarget(options)) {
|
|
224
|
+
message = '--padding applies to element captures; name an element (selector or index)';
|
|
221
225
|
}
|
|
222
226
|
else if (options.quality !== undefined &&
|
|
223
227
|
!options.follow &&
|
|
@@ -24,7 +24,7 @@ export function registerWaitCommand(dom) {
|
|
|
24
24
|
dom
|
|
25
25
|
.command('wait')
|
|
26
26
|
.description('Wait until elements appear, become visible, contain a text or are gone (or the page loads)')
|
|
27
|
-
.argument('[selector]', 'CSS selector (:has-text, :visible allowed; shadow DOM and same-origin iframes searched);
|
|
27
|
+
.argument('[selector]', 'CSS selector (:has-text, :visible allowed; shadow DOM and same-origin iframes searched); without one, waits for the page to load (--load)')
|
|
28
28
|
.option('--text <text>', 'A match must contain this text (case-insensitive)')
|
|
29
29
|
.option('--visible', 'Only count visible matches')
|
|
30
30
|
.option('--gone', 'Wait until no element matches (none visible, with --visible)')
|
|
@@ -44,7 +44,9 @@ export function registerWaitCommand(dom) {
|
|
|
44
44
|
* @returns Command result
|
|
45
45
|
*/
|
|
46
46
|
async function waitFor(selector, options) {
|
|
47
|
-
const
|
|
47
|
+
const load = options.load === true ||
|
|
48
|
+
(selector === undefined && options.text === undefined && !options.gone && !options.visible);
|
|
49
|
+
const needsSelector = options.text !== undefined || options.gone === true || !load;
|
|
48
50
|
if (selector === undefined && needsSelector) {
|
|
49
51
|
const err = waitTargetRequiredError();
|
|
50
52
|
return {
|
|
@@ -58,7 +60,7 @@ async function waitFor(selector, options) {
|
|
|
58
60
|
...filterDefined({ selector, text: options.text }),
|
|
59
61
|
...(options.gone && { gone: true }),
|
|
60
62
|
...(options.visible && { visible: true }),
|
|
61
|
-
...(
|
|
63
|
+
...(load && { load: true }),
|
|
62
64
|
timeout: options.timeout,
|
|
63
65
|
});
|
|
64
66
|
if (response.status === 'error' || !response.data) {
|
|
@@ -97,7 +97,7 @@ function convertCommand(command) {
|
|
|
97
97
|
function generateRuntimeState() {
|
|
98
98
|
const sessionActive = readLiveDaemonPid() !== null;
|
|
99
99
|
const availableCommands = sessionActive
|
|
100
|
-
? ['peek', '
|
|
100
|
+
? ['peek', 'details', 'dom', 'network', 'console', 'cdp', 'status', 'sessions', 'stop']
|
|
101
101
|
: ['bdg <url>', 'sessions', 'cleanup', '--help', '--version'];
|
|
102
102
|
return {
|
|
103
103
|
sessionActive,
|
|
@@ -10,7 +10,7 @@ import { MAX_EDGE_PX, PIXELS_PER_TOKEN, TALL_PAGE_THRESHOLD, } from './dom/scree
|
|
|
10
10
|
/** What DOM actions report about the network requests they triggered */
|
|
11
11
|
const TRIGGERED_REQUESTS_BEHAVIOR = 'Requests (and WebSocket connections) that start after the action begins are returned as triggeredRequests (method, url, status, durationMs; pending when still running at return, loading when the response arrived but its body is still streaming; with resourceType; human output lists documents, XHR/fetch and WebSockets first (up to 10) and counts static assets on one line; absent when network telemetry is off). Attribution is by time: requests a page timer or poller starts meanwhile are listed too, whether or not the action caused them';
|
|
12
12
|
/** What every DOM action reports about the page besides its requests */
|
|
13
|
-
const ACTION_EFFECTS_BEHAVIOR = 'The result also says what changed on the page: a navigation (Page: navigated to <url> (status), or URL changed to <url> (same document); JSON navigation { url, sameDocument, status }), and messages that appeared or changed in alert/status/aria-live elements or flash/error/toast-like classes (New text: "…" (element); JSON messages [{ text, element }], at most 3; after a navigation every message on the new page counts; texts of only digits and time units, such as clocks and counters, are left out, but other text that changes on its own, such as a rotating banner, can show up). Both are absent when nothing changed. Cost: one page script sent before the action without waiting for it and one read after it, a few ms; when the page does not answer (a pending navigation) bdg waits at most 200 ms for the snapshot and 250 ms per read, and the navigation is still reported from CDP events';
|
|
13
|
+
const ACTION_EFFECTS_BEHAVIOR = 'The result also says what changed on the page: a navigation (Page: navigated to <url> (status), or URL changed to <url> (same document); JSON navigation { url, sameDocument, status }), and messages that appeared or changed in alert/status/aria-live elements or flash/error/toast-like classes (New text: "…" (element); JSON messages [{ text, element }], at most 3, with "(+N more)" and moreMessages for the rest; after a navigation every message on the new page counts; texts of only digits and time units, such as clocks and counters, are left out, but other text that changes on its own, such as a rotating banner, can show up). Both are absent when nothing changed. Cost: one page script sent before the action without waiting for it and one read after it, a few ms; when the page does not answer (a pending navigation) bdg waits at most 200 ms for the snapshot and 250 ms per read, and the navigation is still reported from CDP events';
|
|
14
14
|
/** What click and pressKey report when the page was still changing as they returned */
|
|
15
15
|
const STILL_CHANGING_BEHAVIOR = 'When the page was still changing as the action returned, the status line says (page still changing), a note below it says what was pending and suggests bdg dom wait <selector>, and JSON has settled: false with pending { requests (document, fetch/XHR and script requests still running), navigation (a new page still loading), loading (a loading indicator that appeared, e.g. "div#loading"), domChanging (DOM changes kept coming in bursts over a second look 250 ms later; a single render, ticking text and style animations do not count), busy (the page did not answer within 250 ms: a long script) }; absent when the page looked settled (exit code stays 0). A result a timer renders later, with no DOM change, request or loading indicator before it, is not detected. Cost: nothing extra, except 250 ms plus one read when the DOM looked busy. Not checked with --no-wait';
|
|
16
16
|
/** What hover and pressKey report about elements they showed */
|
|
@@ -25,8 +25,12 @@ const NO_WAIT_TRIGGERED_REQUESTS = 'Returns immediately without waiting for netw
|
|
|
25
25
|
const OPTION_BEHAVIORS = {
|
|
26
26
|
'screenshot:--selector': {
|
|
27
27
|
default: 'Captures the page (full page unless --no-full-page)',
|
|
28
|
-
whenEnabled: 'Captures one element; the selector (or a query index) can also be given as the second argument: bdg dom screenshot out.png "#sel".
|
|
29
|
-
automaticBehavior:
|
|
28
|
+
whenEnabled: 'Captures one element; the selector (or a query index) can also be given as the second argument: bdg dom screenshot out.png "#sel". With --index it picks that match of the selector (--selector ".item" --index 2). A positional and an option naming different elements exits 81',
|
|
29
|
+
automaticBehavior: "The capture covers the border box plus what overflows it: uncleared floats, positioned children, text past a tight line height, and the element's own box shadows and outline (a focus ring); not what an overflow: hidden ancestor cuts off, nor fixed descendants. JSON element.bounds is the border box and element.captured the larger area when it grew, which human output notes. An element smaller than the viewport is scrolled into view for the capture and the page scroll put back afterwards; a larger one is captured with the page laid out at its width without scrollbars, so it does not shift",
|
|
30
|
+
},
|
|
31
|
+
'screenshot:--padding': {
|
|
32
|
+
default: 'The element capture is its painted area (border box, overflowing content, shadows, outline)',
|
|
33
|
+
whenEnabled: 'Adds that many CSS px of the page around the element capture on every side (0-500); without an element it exits 81',
|
|
30
34
|
},
|
|
31
35
|
'screenshot:--no-resize': {
|
|
32
36
|
default: `Images auto-resized to max ${MAX_EDGE_PX}px longest edge for Claude Vision optimization (~1,600 tokens)`,
|
|
@@ -208,10 +212,16 @@ const OPTION_BEHAVIORS = {
|
|
|
208
212
|
whenEnabled: 'Lists every listener of framework roots individually',
|
|
209
213
|
tokenImpact: 'On React pages --all adds a row per event type and phase (about 140 rows, 60 KB of JSON)',
|
|
210
214
|
},
|
|
215
|
+
'audit:--level': {
|
|
216
|
+
default: 'Text must reach WCAG AA: 4.5, or 3 for large text (24px, or 18.66px bold); every text-drawing element is checked, composited like dom inspect',
|
|
217
|
+
whenEnabled: '--level AAA asks 7, or 4.5 for large text',
|
|
218
|
+
automaticBehavior: 'One walk over the rendered elements (open shadow roots included, at most 20000; capped says when it stopped). Findings are sorted weakest first; --limit (default 20) lists that many per check and the rest are counted. Overflow leaves out content inside horizontal scrollers and visually-hidden 1px text; identical findings are grouped (×N)',
|
|
219
|
+
tokenImpact: 'About one line per finding; --limit bounds it',
|
|
220
|
+
},
|
|
211
221
|
'layout:--index': {
|
|
212
222
|
default: 'Reports every match of the selector (human output lists the first 20, JSON up to 100 plus an omitted count); a numeric argument reports that cached query element',
|
|
213
223
|
whenEnabled: 'Reports only the nth match (0-based); out of range exits 81',
|
|
214
|
-
automaticBehavior: 'Coordinates are CSS px: bounds relative to the top-level page (iframe offsets and page scroll included), viewport relative to the visible area. Iframes and overflow containers (scroll lists, overflow: hidden) clip what counts as visible (clippedBy names the one cutting it off). scrollBy brings the whole element into view and is limited to how far the page can scroll: for an element out of view it centres it (aligns its start when it is larger than the viewport; human output says "to centre it"), for a partly visible one it is the smallest scroll that shows all of it (the part cut off at the top or bottom; the start of one larger than the viewport; "partly visible (87%); scroll up 5px to see all of it"). Elements a page script moves on scroll (floating menus) may move again after it; fixed and sticky elements (page scroll does not move them, or only until they stick) and ones beyond that range get offScreenReason instead, which says "page scrolling is locked (…)" when the page cannot scroll because body/html is position: fixed or overflow: hidden, so in-flow content is not called fixed; a visible dialog (dialog[open], [aria-modal=true], [role=dialog|alertdialog]) is named as the likely cause ("likely by dialog div#consent"). page.viewport is the layout viewport without scrollbars, as dom scroll reports it; page.colorScheme is the prefers-color-scheme media feature the page sees (not the theme it renders). Content in a closed <details> or under content-visibility: hidden is hidden. coveredBy is the
|
|
224
|
+
automaticBehavior: 'Coordinates are CSS px: bounds relative to the top-level page (iframe offsets and page scroll included), viewport relative to the visible area. Iframes and overflow containers (scroll lists, overflow: hidden) clip what counts as visible (clippedBy names the one cutting it off). scrollBy brings the whole element into view and is limited to how far the page can scroll: for an element out of view it centres it (aligns its start when it is larger than the viewport; human output says "to centre it"), for a partly visible one it is the smallest scroll that shows all of it (the part cut off at the top or bottom; the start of one larger than the viewport; "partly visible (87%); scroll up 5px to see all of it"). Elements a page script moves on scroll (floating menus) may move again after it; fixed and sticky elements (page scroll does not move them, or only until they stick) and ones beyond that range get offScreenReason instead, which says "page scrolling is locked (…)" when the page cannot scroll because body/html is position: fixed or overflow: hidden, so in-flow content is not called fixed; a visible dialog (dialog[open], [aria-modal=true], [role=dialog|alertdialog]) is named as the likely cause ("likely by dialog div#consent"). page.viewport is the layout viewport without scrollbars, as dom scroll reports it; page.colorScheme is the prefers-color-scheme media feature the page sees (not the theme it renders). Content in a closed <details> or under content-visibility: hidden is hidden. coveredBy is the first element painted above it at the center of the largest visible box that paints there (the background of a sticky header rather than the transparent logo on it; inside a shadow host, what its shadow root paints), else the topmost one with coverTransparent (none for pointer-events: none, nor for an element of the same click target: an overlay inside the link, button or label the element is in, a link to the same URL, or the textless absolutely positioned overlay link spanning the card that holds plain content); inert elements are flagged, not hidden',
|
|
215
225
|
tokenImpact: 'About one line per element; a cheap alternative to screenshots for "where is it?"',
|
|
216
226
|
},
|
|
217
227
|
'inspect:--index': {
|
|
@@ -286,11 +296,11 @@ const OPTION_BEHAVIORS = {
|
|
|
286
296
|
},
|
|
287
297
|
'peek:-f': {
|
|
288
298
|
default: 'Shows snapshot of current data',
|
|
289
|
-
whenEnabled: 'Continuous monitoring - refreshes every second
|
|
299
|
+
whenEnabled: 'Continuous monitoring (like tail -f): refreshes every second, or every --interval ms (100-60000). Replaces the deprecated bdg tail',
|
|
290
300
|
},
|
|
291
301
|
'peek:--follow': {
|
|
292
302
|
default: 'Shows snapshot of current data',
|
|
293
|
-
whenEnabled: 'Continuous monitoring - refreshes every second
|
|
303
|
+
whenEnabled: 'Continuous monitoring (like tail -f): refreshes every second, or every --interval ms (100-60000). Replaces the deprecated bdg tail',
|
|
294
304
|
},
|
|
295
305
|
'peek:-v': {
|
|
296
306
|
default: 'Compact output (truncated URLs, no resource types)',
|
|
@@ -326,6 +336,11 @@ const OPTION_BEHAVIORS = {
|
|
|
326
336
|
whenEnabled: 'The page gets exactly that viewport (CSS px, e.g. 1280x800) for the whole session, through navigations and reloads (Emulation.setDeviceMetricsOverride at the display pixel ratio); a launched Chrome also opens its window at that size, so tabs the page opens get it too',
|
|
327
337
|
automaticBehavior: 'Works with --chrome-ws-url: the override belongs to the session, and Chrome drops it when the session ends, so the attached browser gets its own size back. bdg status shows the resulting layout viewport without the scrollbar (Viewport: 1265×800 (emulated 1280x800)). Invalid sizes (not WxH, a side outside 1-10000) exit 81',
|
|
328
338
|
},
|
|
339
|
+
'bdg:--mobile': {
|
|
340
|
+
default: 'A desktop viewport: classic scrollbars take ~15px of the width, no touch, a desktop user agent',
|
|
341
|
+
whenEnabled: 'Emulates a phone for the whole session: a mobile viewport (390x844 unless --viewport) at pixel ratio 3 with mobile layout (meta viewport, overlay scrollbars, so 100vw fits), touch (pointer: coarse, maxTouchPoints 5) and an Android Chrome user agent with mobile client hints; bdg page emulate --mobile turns it on mid-session, --viewport WxH without --mobile or --reset turns it off',
|
|
342
|
+
automaticBehavior: 'Screenshots keep the mobile layout and are taken at pixel ratio 1 (CSS px = image px); bdg status shows "(emulated 390x844, phone)"',
|
|
343
|
+
},
|
|
329
344
|
'bdg:--color-scheme': {
|
|
330
345
|
default: 'The page sees the system setting for prefers-color-scheme (headless Chrome follows the OS, so a dark OS renders dark pages); bdg status and dom layout show which one',
|
|
331
346
|
whenEnabled: 'Emulates prefers-color-scheme: light or dark for the whole session (Emulation.setEmulatedMedia); other values exit 81 with a suggestion',
|
package/dist/commands/page.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { Option } from 'commander';
|
|
6
6
|
import { noActiveSessionError, runCommand } from './shared/CommandRunner.js';
|
|
7
7
|
import { jsonOption } from './shared/commonOptions.js';
|
|
8
|
-
import { parseColorScheme,
|
|
8
|
+
import { parseColorScheme, requestedViewport } from './start.js';
|
|
9
9
|
import { CommandError } from '../errors/index.js';
|
|
10
10
|
import { javascriptNavigationError } from '../errors/messages.js';
|
|
11
11
|
import { getStatus, pageEmulate, pageNavigate } from '../ipc/client.js';
|
|
@@ -127,12 +127,13 @@ async function showPageInfo(options) {
|
|
|
127
127
|
function emulationRequest(options) {
|
|
128
128
|
if (options.reset)
|
|
129
129
|
return { reset: true };
|
|
130
|
-
if (options.viewport === undefined && options.colorScheme === undefined) {
|
|
130
|
+
if (options.viewport === undefined && options.colorScheme === undefined && !options.mobile) {
|
|
131
131
|
const err = pageEmulateNothingError();
|
|
132
132
|
throw new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.INVALID_ARGUMENTS);
|
|
133
133
|
}
|
|
134
|
+
const viewport = requestedViewport(options.viewport, options.mobile);
|
|
134
135
|
return {
|
|
135
|
-
...(
|
|
136
|
+
...(viewport && { viewport }),
|
|
136
137
|
...(options.colorScheme !== undefined && {
|
|
137
138
|
colorScheme: parseColorScheme(options.colorScheme),
|
|
138
139
|
}),
|
|
@@ -188,11 +189,13 @@ export function registerPageCommands(program) {
|
|
|
188
189
|
page
|
|
189
190
|
.command('emulate')
|
|
190
191
|
.description(PAGE_EMULATE_DESCRIPTION)
|
|
191
|
-
.option('--viewport <WxH>', 'Viewport size in CSS px, e.g. 900x700')
|
|
192
|
+
.option('--viewport <WxH>', 'Viewport size in CSS px, e.g. 900x700 (a desktop one unless --mobile)')
|
|
192
193
|
.option('--color-scheme <scheme>', 'Emulate prefers-color-scheme: light or dark')
|
|
194
|
+
.option('--mobile', 'Emulate a phone: mobile viewport (390x844 unless --viewport), touch, mobile user agent')
|
|
193
195
|
.addOption(new Option('--reset', 'Back to the browser window size and the system setting').conflicts([
|
|
194
196
|
'viewport',
|
|
195
197
|
'colorScheme',
|
|
198
|
+
'mobile',
|
|
196
199
|
]))
|
|
197
200
|
.addOption(jsonOption())
|
|
198
201
|
.action(async (options) => {
|
package/dist/commands/peek.d.ts
CHANGED
|
@@ -2,5 +2,12 @@
|
|
|
2
2
|
* Peek command for previewing collected session data.
|
|
3
3
|
*/
|
|
4
4
|
import type { Command } from 'commander';
|
|
5
|
+
import type { PeekCommandOptions } from './shared/optionTypes.js';
|
|
6
|
+
/**
|
|
7
|
+
* Watch the session data (`peek --follow`, and the deprecated `tail`).
|
|
8
|
+
*
|
|
9
|
+
* @param options - Peek options (follow implied)
|
|
10
|
+
*/
|
|
11
|
+
export declare function followPreview(options: PeekCommandOptions): Promise<void>;
|
|
5
12
|
export declare function registerPeekCommand(program: Command): void;
|
|
6
13
|
//# sourceMappingURL=peek.d.ts.map
|