browser-debugger-cli 0.14.0 → 0.16.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/.claude/skills/bdg/SKILL.md +3 -2
- package/dist/cdp/methodTarget.d.ts +92 -0
- package/dist/cdp/methodTarget.js +159 -0
- package/dist/cdp/protocol.d.ts +16 -1
- package/dist/cdp/protocol.js +21 -0
- package/dist/cdp/schema.d.ts +55 -1
- package/dist/cdp/schema.js +134 -25
- package/dist/cdp/types.d.ts +3 -1
- package/dist/commands/cdp.d.ts +38 -1
- package/dist/commands/cdp.js +201 -133
- package/dist/commands/cleanup.js +21 -4
- package/dist/commands/dom/eval.d.ts +2 -1
- package/dist/commands/dom/eval.js +6 -21
- package/dist/commands/dom/formInteraction.js +8 -4
- package/dist/commands/dom/helpers/evalResult.d.ts +36 -0
- package/dist/commands/dom/helpers/evalResult.js +59 -0
- package/dist/commands/dom/helpers/index.d.ts +4 -4
- package/dist/commands/dom/helpers/index.js +3 -3
- package/dist/commands/dom/helpers/query.d.ts +2 -2
- package/dist/commands/dom/helpers/query.js +2 -2
- package/dist/commands/dom/helpers/screenshot.d.ts +21 -26
- package/dist/commands/dom/helpers/screenshot.js +50 -668
- package/dist/commands/dom/screenshot.js +56 -36
- package/dist/commands/helpJson.d.ts +1 -1
- package/dist/commands/helpJson.js +3 -3
- package/dist/commands/helpTopic.js +10 -4
- package/dist/commands/network/har.js +18 -14
- package/dist/commands/optionBehaviors.js +24 -9
- package/dist/commands/shared/CommandRunner.d.ts +5 -0
- package/dist/commands/shared/CommandRunner.js +18 -3
- package/dist/commands/shared/interrupt.d.ts +40 -0
- package/dist/commands/shared/interrupt.js +73 -0
- package/dist/commands/shared/optionTypes.d.ts +3 -0
- package/dist/commands/shared/outputFile.d.ts +2 -1
- package/dist/commands/shared/outputFile.js +7 -4
- package/dist/commands/shared/startHelpers.d.ts +26 -3
- package/dist/commands/shared/startHelpers.js +145 -23
- package/dist/commands/status.js +3 -1
- package/dist/commands/stop.js +2 -1
- package/dist/commands/types.d.ts +5 -0
- package/dist/connection/cdp.js +1 -16
- package/dist/connection/chromeIdentity.d.ts +24 -5
- package/dist/connection/chromeIdentity.js +53 -22
- package/dist/connection/launcher/flagsBuilder.d.ts +46 -0
- package/dist/connection/launcher/flagsBuilder.js +107 -23
- package/dist/connection/launcher.d.ts +35 -2
- package/dist/connection/launcher.js +99 -12
- package/dist/connection/typed-cdp.d.ts +3 -2
- package/dist/constants.d.ts +3 -5
- package/dist/constants.js +3 -5
- package/dist/daemon/SessionController.d.ts +10 -5
- package/dist/daemon/SessionController.js +15 -8
- package/dist/daemon/ipcServer.js +1 -1
- package/dist/daemon/launcher.d.ts +22 -3
- package/dist/daemon/launcher.js +45 -8
- package/dist/daemon/session/Session.d.ts +5 -1
- package/dist/daemon/session/Session.js +9 -8
- package/dist/daemon/session/TelemetryStore.d.ts +5 -0
- package/dist/daemon/session/TelemetryStore.js +4 -0
- package/dist/daemon/session/captureGate.d.ts +59 -0
- package/dist/daemon/session/captureGate.js +96 -0
- package/dist/daemon/session/chromeConnection.d.ts +16 -1
- package/dist/daemon/session/chromeConnection.js +34 -4
- package/dist/daemon/session/collectors.d.ts +15 -0
- package/dist/daemon/session/collectors.js +39 -2
- package/dist/daemon/session/commandRegistry.d.ts +14 -1
- package/dist/daemon/session/commandRegistry.js +48 -13
- package/dist/daemon/session/downloads.d.ts +32 -0
- package/dist/daemon/session/downloads.js +96 -0
- package/dist/daemon/session/interactions.d.ts +3 -2
- package/dist/daemon/session/interactions.js +7 -2
- package/dist/daemon/session/plugins.js +6 -0
- package/dist/daemon.js +18520 -17014
- package/dist/errors/CommandError.d.ts +2 -0
- package/dist/errors/issues.d.ts +1 -1
- package/dist/errors/messages.d.ts +81 -0
- package/dist/errors/messages.js +198 -6
- package/dist/index.js +1446 -1078
- package/dist/ipc/client.d.ts +20 -2
- package/dist/ipc/client.js +32 -6
- package/dist/ipc/protocol/commands.d.ts +36 -2
- package/dist/ipc/protocol/commands.js +1 -0
- package/dist/ipc/protocol/domTypes.d.ts +24 -1
- package/dist/ipc/session/queries.d.ts +3 -0
- package/dist/ipc/session/types.d.ts +5 -0
- package/dist/ipc/transport/IPCError.d.ts +9 -0
- package/dist/ipc/transport/IPCError.js +12 -0
- package/dist/ipc/transport/errors.d.ts +2 -1
- package/dist/ipc/transport/errors.js +4 -1
- package/dist/ipc/transport/index.d.ts +10 -2
- package/dist/ipc/transport/index.js +29 -4
- package/dist/runtime/dom/actionEffects.d.ts +48 -9
- package/dist/runtime/dom/actionEffects.js +269 -34
- package/dist/runtime/dom/actionEffectsScripts.d.ts +45 -0
- package/dist/runtime/dom/actionEffectsScripts.js +101 -2
- package/dist/runtime/dom/captureArea.d.ts +35 -0
- package/dist/runtime/dom/captureArea.js +203 -0
- package/dist/runtime/dom/elementInfo.d.ts +13 -4
- package/dist/runtime/dom/elementInfo.js +12 -3
- package/dist/runtime/dom/evalHelpers.d.ts +24 -4
- package/dist/runtime/dom/evalHelpers.js +40 -12
- package/dist/runtime/dom/formDiscovery.d.ts +1 -1
- package/dist/runtime/dom/frames.d.ts +2 -1
- package/dist/runtime/dom/frames.js +3 -1
- package/dist/runtime/page/bdgWorld.d.ts +9 -0
- package/dist/runtime/page/bdgWorld.js +11 -0
- package/dist/runtime/page/captureEmulation.d.ts +119 -0
- package/dist/runtime/page/captureEmulation.js +189 -0
- package/dist/runtime/page/captureScroll.d.ts +24 -0
- package/dist/runtime/page/captureScroll.js +124 -0
- package/dist/runtime/page/emulation.js +6 -5
- package/dist/runtime/page/screenshot.d.ts +41 -0
- package/dist/runtime/page/screenshot.js +394 -0
- package/dist/runtime/page/userAgent.d.ts +86 -2
- package/dist/runtime/page/userAgent.js +154 -33
- package/dist/session/paths.d.ts +52 -3
- package/dist/session/paths.js +179 -7
- package/dist/session/portClaims.d.ts +0 -8
- package/dist/session/portClaims.js +1 -22
- package/dist/session/sessionList.d.ts +5 -1
- package/dist/session/sessionList.js +5 -1
- package/dist/telemetry/downloads.d.ts +127 -0
- package/dist/telemetry/downloads.js +265 -0
- package/dist/telemetry/har/builder.d.ts +12 -1
- package/dist/telemetry/har/builder.js +32 -9
- package/dist/telemetry/har/sanitize.d.ts +28 -0
- package/dist/telemetry/har/sanitize.js +184 -0
- package/dist/telemetry/har/sanitizeBody.d.ts +78 -0
- package/dist/telemetry/har/sanitizeBody.js +541 -0
- package/dist/telemetry/har/types.d.ts +2 -0
- package/dist/telemetry/network.d.ts +4 -4
- package/dist/telemetry/network.js +38 -4
- package/dist/telemetry/networkRetention.d.ts +35 -14
- package/dist/telemetry/networkRetention.js +62 -26
- package/dist/types.d.ts +9 -14
- package/dist/ui/OutputBuilder.d.ts +3 -2
- package/dist/ui/OutputBuilder.js +4 -3
- package/dist/ui/formatters/cdp.d.ts +32 -9
- package/dist/ui/formatters/cdp.js +77 -6
- package/dist/ui/formatters/details.js +7 -15
- package/dist/ui/formatters/preview.d.ts +2 -0
- package/dist/ui/formatters/preview.js +7 -1
- package/dist/ui/formatters/sessions.d.ts +3 -2
- package/dist/ui/formatters/sessions.js +10 -3
- package/dist/ui/formatters/status.js +6 -1
- package/dist/ui/formatting.d.ts +7 -0
- package/dist/ui/formatting.js +13 -0
- package/dist/ui/logging/logger.d.ts +1 -1
- package/dist/ui/messages/chrome.d.ts +27 -6
- package/dist/ui/messages/chrome.js +78 -12
- package/dist/ui/messages/commands.d.ts +71 -3
- package/dist/ui/messages/commands.js +98 -3
- package/dist/ui/messages/networkMessages.d.ts +50 -5
- package/dist/ui/messages/networkMessages.js +50 -6
- package/dist/ui/messages/session.d.ts +8 -0
- package/dist/ui/messages/session.js +10 -0
- package/dist/utils/async.d.ts +3 -2
- package/dist/utils/async.js +16 -3
- package/dist/utils/atomicFile.d.ts +2 -1
- package/dist/utils/atomicFile.js +5 -2
- package/dist/utils/directories.d.ts +41 -0
- package/dist/utils/directories.js +48 -0
- package/dist/utils/http.d.ts +11 -4
- package/dist/utils/http.js +5 -3
- package/package.json +18 -4
- /package/dist/{commands/dom → runtime/page}/screenshotResize.d.ts +0 -0
- /package/dist/{commands/dom → runtime/page}/screenshotResize.js +0 -0
|
@@ -1,682 +1,64 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* view primitives used by page screenshots with `--scroll`.
|
|
2
|
+
* Screenshot capture for `bdg dom screenshot`: the daemon measures, captures
|
|
3
|
+
* and puts back the page's emulation (`dom_screenshot`); this writes the image
|
|
4
|
+
* it returns.
|
|
6
5
|
*/
|
|
7
|
-
import {
|
|
6
|
+
import { interruptExitCode, interruptSignal, unlessInterrupted, } from '../../shared/interrupt.js';
|
|
8
7
|
import { writeOutputFile } from '../../shared/outputFile.js';
|
|
9
|
-
import { CDPConnectionError } from '../../../connection/errors.js';
|
|
10
8
|
import { CommandError } from '../../../errors/index.js';
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import { viewportOverride } from '../../../runtime/page/emulation.js';
|
|
15
|
-
import { readSessionMetadata } from '../../../session/metadata.js';
|
|
16
|
-
import { createLogger } from '../../../ui/logging/index.js';
|
|
9
|
+
import { operationFailedError, screenshotInterruptedError } from '../../../errors/messages.js';
|
|
10
|
+
import { domScreenshot } from '../../../ipc/client.js';
|
|
11
|
+
import { sessionCommand } from '../../../ui/messages/sessionCommand.js';
|
|
17
12
|
import { EXIT_CODES } from '../../../utils/exitCodes.js';
|
|
18
|
-
const log = createLogger('dom');
|
|
19
|
-
const POST_SCROLL_NETWORK_IDLE_MS = 150;
|
|
20
|
-
const POST_SCROLL_DOM_STABLE_MS = 200;
|
|
21
|
-
const POST_SCROLL_MAX_WAIT_MS = 2000;
|
|
22
|
-
const STABILITY_CHECK_INTERVAL_MS = 50;
|
|
23
|
-
/**
|
|
24
|
-
* Wait for the page to settle after a programmatic scroll (lazy-load idle +
|
|
25
|
-
* DOM mutation idle). Uses shorter thresholds than full page load.
|
|
26
|
-
*/
|
|
27
|
-
async function waitForPostScrollStability() {
|
|
28
|
-
const deadline = Date.now() + POST_SCROLL_MAX_WAIT_MS;
|
|
29
|
-
await callBdgScript('Runtime.evaluate', {
|
|
30
|
-
expression: `
|
|
31
|
-
(() => {
|
|
32
|
-
window.__bdg_scrollStability = {
|
|
33
|
-
lastNetworkActivity: Date.now(),
|
|
34
|
-
lastDomMutation: Date.now(),
|
|
35
|
-
activeRequests: 0
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
const state = window.__bdg_scrollStability;
|
|
39
|
-
|
|
40
|
-
if (window.PerformanceObserver) {
|
|
41
|
-
const perfObserver = new PerformanceObserver((list) => {
|
|
42
|
-
for (const entry of list.getEntries()) {
|
|
43
|
-
if (entry.entryType === 'resource') {
|
|
44
|
-
state.lastNetworkActivity = Date.now();
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
});
|
|
48
|
-
try {
|
|
49
|
-
perfObserver.observe({ entryTypes: ['resource'] });
|
|
50
|
-
state.perfObserver = perfObserver;
|
|
51
|
-
} catch (e) {}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const mutationObserver = new MutationObserver(() => {
|
|
55
|
-
state.lastDomMutation = Date.now();
|
|
56
|
-
});
|
|
57
|
-
mutationObserver.observe(document.body || document.documentElement, {
|
|
58
|
-
childList: true,
|
|
59
|
-
subtree: true,
|
|
60
|
-
attributes: true
|
|
61
|
-
});
|
|
62
|
-
state.mutationObserver = mutationObserver;
|
|
63
|
-
})()
|
|
64
|
-
`,
|
|
65
|
-
returnByValue: true,
|
|
66
|
-
});
|
|
67
|
-
try {
|
|
68
|
-
while (Date.now() < deadline) {
|
|
69
|
-
const checkResult = await callBdgScript('Runtime.evaluate', {
|
|
70
|
-
expression: `
|
|
71
|
-
(() => {
|
|
72
|
-
const state = window.__bdg_scrollStability;
|
|
73
|
-
if (!state) return { networkIdle: 999, domIdle: 999 };
|
|
74
|
-
return {
|
|
75
|
-
networkIdle: Date.now() - state.lastNetworkActivity,
|
|
76
|
-
domIdle: Date.now() - state.lastDomMutation
|
|
77
|
-
};
|
|
78
|
-
})()
|
|
79
|
-
`,
|
|
80
|
-
returnByValue: true,
|
|
81
|
-
});
|
|
82
|
-
const value = checkResult.data?.result?.result?.value;
|
|
83
|
-
const networkIdle = value?.networkIdle ?? 0;
|
|
84
|
-
const domIdle = value?.domIdle ?? 0;
|
|
85
|
-
if (networkIdle >= POST_SCROLL_NETWORK_IDLE_MS && domIdle >= POST_SCROLL_DOM_STABLE_MS) {
|
|
86
|
-
log.debug(`Post-scroll stable: network ${networkIdle}ms, DOM ${domIdle}ms`);
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
89
|
-
await new Promise((resolve) => setTimeout(resolve, STABILITY_CHECK_INTERVAL_MS));
|
|
90
|
-
}
|
|
91
|
-
log.debug('Post-scroll stability timeout, proceeding anyway');
|
|
92
|
-
}
|
|
93
|
-
finally {
|
|
94
|
-
await callBdgScript('Runtime.evaluate', {
|
|
95
|
-
expression: `
|
|
96
|
-
(() => {
|
|
97
|
-
const state = window.__bdg_scrollStability;
|
|
98
|
-
if (state) {
|
|
99
|
-
state.perfObserver?.disconnect();
|
|
100
|
-
state.mutationObserver?.disconnect();
|
|
101
|
-
delete window.__bdg_scrollStability;
|
|
102
|
-
}
|
|
103
|
-
})()
|
|
104
|
-
`,
|
|
105
|
-
returnByValue: true,
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
/**
|
|
110
|
-
* Scroll an element into view before capture; returns the original scroll
|
|
111
|
-
* position so it can be restored afterwards.
|
|
112
|
-
*/
|
|
113
|
-
async function scrollToElement(selector) {
|
|
114
|
-
const result = await callBdgScript('Runtime.evaluate', {
|
|
115
|
-
expression: `
|
|
116
|
-
(() => {
|
|
117
|
-
const el = (${DEEP_QUERY_JS})(${selectorArgsJS(selector)})[0];
|
|
118
|
-
if (!el) return { found: false };
|
|
119
|
-
const originalX = window.scrollX;
|
|
120
|
-
const originalY = window.scrollY;
|
|
121
|
-
el.scrollIntoView({ block: 'center', behavior: 'instant' });
|
|
122
|
-
return { found: true, originalX, originalY };
|
|
123
|
-
})()
|
|
124
|
-
`,
|
|
125
|
-
returnByValue: true,
|
|
126
|
-
});
|
|
127
|
-
const value = result.data?.result?.result?.value;
|
|
128
|
-
if (!value?.found) {
|
|
129
|
-
const err = noNodesFoundError(selector);
|
|
130
|
-
throw new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.RESOURCE_NOT_FOUND);
|
|
131
|
-
}
|
|
132
|
-
await waitForPostScrollStability();
|
|
133
|
-
return { x: value.originalX ?? 0, y: value.originalY ?? 0 };
|
|
134
|
-
}
|
|
135
|
-
async function restoreScrollPosition(position) {
|
|
136
|
-
await callBdgScript('Runtime.evaluate', {
|
|
137
|
-
expression: `window.scrollTo(${position.x}, ${position.y})`,
|
|
138
|
-
returnByValue: true,
|
|
139
|
-
});
|
|
140
|
-
}
|
|
141
|
-
/**
|
|
142
|
-
* The window's size with its scrollbars (`innerWidth`/`innerHeight`): an
|
|
143
|
-
* override at this size keeps the page's layout, where the visible size
|
|
144
|
-
* (without scrollbars) would narrow it and move centered content.
|
|
145
|
-
*
|
|
146
|
-
* @param viewport - Visible viewport size, used when the page does not answer
|
|
147
|
-
* @returns Width and height in CSS px
|
|
148
|
-
*/
|
|
149
|
-
async function windowSize(viewport) {
|
|
150
|
-
const response = await callBdgScript('Runtime.evaluate', {
|
|
151
|
-
expression: '[window.innerWidth, window.innerHeight]',
|
|
152
|
-
returnByValue: true,
|
|
153
|
-
});
|
|
154
|
-
const value = response.data?.result?.result?.value;
|
|
155
|
-
const [width, height] = Array.isArray(value) ? value : [];
|
|
156
|
-
return {
|
|
157
|
-
width: Math.round(width ?? viewport.clientWidth),
|
|
158
|
-
height: Math.round(height ?? viewport.clientHeight),
|
|
159
|
-
};
|
|
160
|
-
}
|
|
161
|
-
/**
|
|
162
|
-
* Capture at a pixel ratio of 1 (CSS px = image px) on a high-DPI display:
|
|
163
|
-
* the viewport is overridden at the window's size (the session's
|
|
164
|
-
* `--viewport`, else the window with its scrollbars, so the layout does not
|
|
165
|
-
* change) until the returned function puts back what was there before, the
|
|
166
|
-
* session's viewport or none.
|
|
167
|
-
*
|
|
168
|
-
* @param devicePixelRatio - Page's pixel ratio
|
|
169
|
-
* @param viewport - Visible viewport size
|
|
170
|
-
* @returns Function restoring the device metrics
|
|
171
|
-
*/
|
|
172
|
-
async function useUnitPixelRatio(devicePixelRatio, viewport) {
|
|
173
|
-
if (devicePixelRatio === 1)
|
|
174
|
-
return () => Promise.resolve();
|
|
175
|
-
const sessionViewport = readSessionMetadata()?.viewport;
|
|
176
|
-
const size = sessionViewport ?? (await windowSize(viewport));
|
|
177
|
-
await callCDP('Emulation.setDeviceMetricsOverride', viewportOverride(size, 1));
|
|
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
|
-
}
|
|
194
|
-
}
|
|
195
|
-
/**
|
|
196
|
-
* Get the bounding box (border box, so padding and border are included) of an
|
|
197
|
-
* element via CDP DOM.getBoxModel.
|
|
198
|
-
*
|
|
199
|
-
* @param ref - Node reference
|
|
200
|
-
* @returns Element bounds in CSS pixels
|
|
201
|
-
*/
|
|
202
|
-
export async function getElementBounds(ref) {
|
|
203
|
-
const response = await callCDP('DOM.getBoxModel', ref);
|
|
204
|
-
const boxModel = response.data?.result;
|
|
205
|
-
if (!boxModel?.model?.border) {
|
|
206
|
-
const err = elementNotVisibleError();
|
|
207
|
-
throw new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.INVALID_ARGUMENTS);
|
|
208
|
-
}
|
|
209
|
-
const border = boxModel.model.border;
|
|
210
|
-
const x = border[0] ?? 0;
|
|
211
|
-
const y = border[1] ?? 0;
|
|
212
|
-
const width = (border[2] ?? 0) - x;
|
|
213
|
-
const height = (border[5] ?? 0) - y;
|
|
214
|
-
if (width <= 0 || height <= 0) {
|
|
215
|
-
const err = elementZeroDimensionsError();
|
|
216
|
-
throw new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.INVALID_ARGUMENTS);
|
|
217
|
-
}
|
|
218
|
-
return { x, y, width, height };
|
|
219
|
-
}
|
|
220
|
-
/**
|
|
221
|
-
* Page coordinates of the visible area's top-left corner. A capture clip is
|
|
222
|
-
* in page coordinates, so a viewport capture must start at the scroll
|
|
223
|
-
* position, not at the page origin (which shows nothing once scrolled). Read
|
|
224
|
-
* after any metrics override, which can move the scroll position.
|
|
225
|
-
*
|
|
226
|
-
* @returns Scroll offset of the visual viewport in CSS pixels
|
|
227
|
-
*/
|
|
228
|
-
async function visibleAreaOrigin() {
|
|
229
|
-
const response = await callCDP('Page.getLayoutMetrics', {});
|
|
230
|
-
const metrics = response.data?.result;
|
|
231
|
-
const viewport = metrics?.cssVisualViewport;
|
|
232
|
-
return { x: viewport?.pageX ?? 0, y: viewport?.pageY ?? 0 };
|
|
233
|
-
}
|
|
234
|
-
/**
|
|
235
|
-
* Capture a screenshot of the page. Auto-resizes oversized pages by default
|
|
236
|
-
* to keep Claude Vision token cost bounded; falls back to viewport capture
|
|
237
|
-
* when the page is taller than the tall-page threshold.
|
|
238
|
-
*/
|
|
239
|
-
export async function capturePageScreenshot(outputPath, options = {}) {
|
|
240
|
-
const format = options.format ?? 'png';
|
|
241
|
-
const quality = format === 'jpeg' ? (options.quality ?? 90) : undefined;
|
|
242
|
-
const requestedFullPage = options.fullPage ?? true;
|
|
243
|
-
const noResize = options.noResize ?? false;
|
|
244
|
-
let originalScrollPosition;
|
|
245
|
-
if (options.scroll) {
|
|
246
|
-
originalScrollPosition = await scrollToElement(options.scroll);
|
|
247
|
-
}
|
|
248
|
-
const dprResponse = await callBdgScript('Runtime.evaluate', {
|
|
249
|
-
expression: 'window.devicePixelRatio',
|
|
250
|
-
returnByValue: true,
|
|
251
|
-
});
|
|
252
|
-
const devicePixelRatio = dprResponse.data?.result?.result?.value ?? 1;
|
|
253
|
-
const metricsResponse = await callCDP('Page.getLayoutMetrics', {});
|
|
254
|
-
const metricsResult = metricsResponse.data?.result;
|
|
255
|
-
const contentSize = metricsResult?.contentSize ?? { width: 0, height: 0 };
|
|
256
|
-
const viewport = metricsResult?.visualViewport ?? { clientWidth: 0, clientHeight: 0 };
|
|
257
|
-
const pageIsTooTall = !noResize && requestedFullPage && isTallPage(contentSize.width, contentSize.height);
|
|
258
|
-
const useScroll = options.scroll !== undefined;
|
|
259
|
-
const effectiveFullPage = useScroll ? false : pageIsTooTall ? false : requestedFullPage;
|
|
260
|
-
const captureWidth = effectiveFullPage ? contentSize.width : viewport.clientWidth;
|
|
261
|
-
const captureHeight = effectiveFullPage ? contentSize.height : viewport.clientHeight;
|
|
262
|
-
const resized = shouldResize(captureWidth, captureHeight, noResize);
|
|
263
|
-
const scale = resized ? calculateResizeScale(captureWidth, captureHeight) : 1;
|
|
264
|
-
const finalWidth = Math.round(captureWidth * scale);
|
|
265
|
-
const finalHeight = Math.round(captureHeight * scale);
|
|
266
|
-
const restoreMetrics = await useUnitPixelRatio(devicePixelRatio, viewport);
|
|
267
|
-
if (devicePixelRatio !== 1) {
|
|
268
|
-
if (options.scroll) {
|
|
269
|
-
await callBdgScript('Runtime.evaluate', {
|
|
270
|
-
expression: `(${DEEP_QUERY_JS})(${selectorArgsJS(options.scroll)})[0]?.scrollIntoView({ block: 'center', behavior: 'instant' })`,
|
|
271
|
-
returnByValue: true,
|
|
272
|
-
});
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
const clipOrigin = effectiveFullPage ? { x: 0, y: 0 } : await visibleAreaOrigin();
|
|
276
|
-
let screenshotResult;
|
|
277
|
-
try {
|
|
278
|
-
const screenshotResponse = await callCDP('Page.captureScreenshot', {
|
|
279
|
-
format,
|
|
280
|
-
...(quality !== undefined && { quality }),
|
|
281
|
-
captureBeyondViewport: effectiveFullPage,
|
|
282
|
-
clip: {
|
|
283
|
-
x: clipOrigin.x,
|
|
284
|
-
y: clipOrigin.y,
|
|
285
|
-
width: captureWidth,
|
|
286
|
-
height: captureHeight,
|
|
287
|
-
scale,
|
|
288
|
-
},
|
|
289
|
-
});
|
|
290
|
-
screenshotResult = screenshotResponse.data?.result;
|
|
291
|
-
}
|
|
292
|
-
finally {
|
|
293
|
-
await restoreMetrics();
|
|
294
|
-
}
|
|
295
|
-
if (!screenshotResult?.data) {
|
|
296
|
-
throw new CDPConnectionError('No screenshot data returned', new Error('Empty response'));
|
|
297
|
-
}
|
|
298
|
-
const buffer = Buffer.from(screenshotResult.data, 'base64');
|
|
299
|
-
const absolutePath = await writeOutputFile(outputPath, buffer);
|
|
300
|
-
const result = {
|
|
301
|
-
path: absolutePath,
|
|
302
|
-
format,
|
|
303
|
-
width: finalWidth,
|
|
304
|
-
height: finalHeight,
|
|
305
|
-
size: buffer.length,
|
|
306
|
-
fullPage: effectiveFullPage,
|
|
307
|
-
captureMode: effectiveFullPage ? 'full_page' : 'viewport',
|
|
308
|
-
finalTokens: calculateImageTokens(finalWidth, finalHeight),
|
|
309
|
-
};
|
|
310
|
-
if (quality !== undefined) {
|
|
311
|
-
result.quality = quality;
|
|
312
|
-
}
|
|
313
|
-
if (!effectiveFullPage) {
|
|
314
|
-
result.viewport = {
|
|
315
|
-
width: viewport.clientWidth,
|
|
316
|
-
height: viewport.clientHeight,
|
|
317
|
-
};
|
|
318
|
-
}
|
|
319
|
-
if (resized) {
|
|
320
|
-
result.resized = true;
|
|
321
|
-
result.originalWidth = captureWidth;
|
|
322
|
-
result.originalHeight = captureHeight;
|
|
323
|
-
result.originalTokens = calculateImageTokens(captureWidth, captureHeight);
|
|
324
|
-
}
|
|
325
|
-
if (pageIsTooTall && !useScroll) {
|
|
326
|
-
const aspectRatio = Math.round((contentSize.height / contentSize.width) * 10) / 10;
|
|
327
|
-
result.fullPageSkipped = {
|
|
328
|
-
reason: 'page_too_tall',
|
|
329
|
-
originalHeight: contentSize.height,
|
|
330
|
-
aspectRatio,
|
|
331
|
-
};
|
|
332
|
-
result.warning = `Full page capture skipped: page too tall (${aspectRatio}:1 aspect ratio). Only viewport captured.`;
|
|
333
|
-
}
|
|
334
|
-
if (useScroll && options.scroll) {
|
|
335
|
-
result.scrolledTo = options.scroll;
|
|
336
|
-
}
|
|
337
|
-
if (originalScrollPosition) {
|
|
338
|
-
await restoreScrollPosition(originalScrollPosition);
|
|
339
|
-
}
|
|
340
|
-
return result;
|
|
341
|
-
}
|
|
342
|
-
/** Descendants {@link CONTENT_OVERFLOW_JS} looks at, so a huge element stays cheap */
|
|
343
|
-
const OVERFLOW_SCAN_LIMIT = 2000;
|
|
344
|
-
/**
|
|
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.
|
|
356
|
-
*/
|
|
357
|
-
const CONTENT_OVERFLOW_JS = `function () {
|
|
358
|
-
const view = this.ownerDocument.defaultView;
|
|
359
|
-
const scroller = this.ownerDocument.scrollingElement || this.ownerDocument.documentElement;
|
|
360
|
-
const own = this.getBoundingClientRect();
|
|
361
|
-
const reach = { left: own.left, top: own.top, right: own.right, bottom: own.bottom };
|
|
362
|
-
const page = { left: -view.scrollX, top: -view.scrollY, right: scroller.scrollWidth - view.scrollX, bottom: scroller.scrollHeight - view.scrollY };
|
|
363
|
-
const clips = (style) => style.overflowX !== 'visible' || style.overflowY !== 'visible';
|
|
364
|
-
let budget = ${OVERFLOW_SCAN_LIMIT};
|
|
365
|
-
const walk = (el) => {
|
|
366
|
-
for (const child of el.children) {
|
|
367
|
-
if (--budget < 0) return;
|
|
368
|
-
const style = view.getComputedStyle(child);
|
|
369
|
-
if (style.display === 'none' || style.position === 'fixed') continue;
|
|
370
|
-
const r = child.getBoundingClientRect();
|
|
371
|
-
if (r.width > 0 && r.height > 0 && style.visibility === 'visible') {
|
|
372
|
-
reach.left = Math.min(reach.left, Math.max(r.left, page.left));
|
|
373
|
-
reach.top = Math.min(reach.top, Math.max(r.top, page.top));
|
|
374
|
-
reach.right = Math.max(reach.right, Math.min(r.right, page.right));
|
|
375
|
-
reach.bottom = Math.max(reach.bottom, Math.min(r.bottom, page.bottom));
|
|
376
|
-
}
|
|
377
|
-
if (!clips(style)) walk(child);
|
|
378
|
-
}
|
|
379
|
-
};
|
|
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
|
-
};
|
|
411
|
-
}`;
|
|
412
|
-
/**
|
|
413
|
-
* The visible viewport (without scrollbars) in CSS px.
|
|
414
|
-
*
|
|
415
|
-
* @returns Width and height
|
|
416
|
-
*/
|
|
417
|
-
async function visibleViewport() {
|
|
418
|
-
const metrics = (await callCDP('Page.getLayoutMetrics', {})).data?.result;
|
|
419
|
-
const view = metrics?.cssVisualViewport ?? metrics?.visualViewport;
|
|
420
|
-
return { width: view?.clientWidth ?? 0, height: view?.clientHeight ?? 0 };
|
|
421
|
-
}
|
|
422
|
-
/**
|
|
423
|
-
* Whether an area (viewport coordinates) lies inside the viewport.
|
|
424
|
-
*
|
|
425
|
-
* @param area - Area
|
|
426
|
-
* @param view - Viewport size
|
|
427
|
-
* @returns True when fully inside
|
|
428
|
-
*/
|
|
429
|
-
function insideView(area, view) {
|
|
430
|
-
return (area.x >= 0 &&
|
|
431
|
-
area.y >= 0 &&
|
|
432
|
-
area.x + area.width <= view.width &&
|
|
433
|
-
area.y + area.height <= view.height);
|
|
434
|
-
}
|
|
435
13
|
/**
|
|
436
|
-
*
|
|
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).
|
|
14
|
+
* The error of a screenshot interrupted by Ctrl-C or SIGTERM.
|
|
443
15
|
*
|
|
444
|
-
* @param
|
|
445
|
-
* @
|
|
446
|
-
* @returns Border box, area to capture (viewport coordinates), whether it is
|
|
447
|
-
* in view, and the scroll position to restore when it scrolled
|
|
16
|
+
* @param signal - The signal
|
|
17
|
+
* @returns Command error (exit 130, or 143 for SIGTERM)
|
|
448
18
|
*/
|
|
449
|
-
|
|
450
|
-
const
|
|
451
|
-
|
|
452
|
-
let bounds = await captureArea(ref, box, padding);
|
|
453
|
-
const fits = bounds.width <= view.width && bounds.height <= view.height;
|
|
454
|
-
if (!fits) {
|
|
455
|
-
await keepLayoutWithoutScrollbars(view);
|
|
456
|
-
box = await getElementBounds(ref);
|
|
457
|
-
return { box, bounds: await captureArea(ref, box, padding), inView: false };
|
|
458
|
-
}
|
|
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 callBdgScript('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 };
|
|
19
|
+
export function screenshotInterrupted(signal) {
|
|
20
|
+
const err = screenshotInterruptedError(signal);
|
|
21
|
+
return new CommandError(err.message, { suggestion: err.suggestion }, interruptExitCode(signal));
|
|
474
22
|
}
|
|
475
23
|
/**
|
|
476
|
-
*
|
|
477
|
-
*
|
|
478
|
-
* the
|
|
479
|
-
*
|
|
480
|
-
* a phone in a `--mobile` session) until
|
|
481
|
-
* {@link restoreViewport}.
|
|
24
|
+
* Ask the daemon for the capture. An interrupt ends the wait at once and
|
|
25
|
+
* closes the connection: the daemon skips the capture if it has not started,
|
|
26
|
+
* and puts the emulation back before it runs any later page command, so this
|
|
27
|
+
* need not wait.
|
|
482
28
|
*
|
|
483
|
-
* @param
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
*
|
|
29
|
+
* @param request - What to capture and how
|
|
30
|
+
* @param interrupt - Aborted on Ctrl-C or SIGTERM
|
|
31
|
+
* @returns The daemon's response
|
|
32
|
+
* @throws CommandError (130/143) once interrupted
|
|
33
|
+
*/
|
|
34
|
+
async function requestCapture(request, interrupt) {
|
|
35
|
+
if (!interrupt)
|
|
36
|
+
return domScreenshot(request);
|
|
37
|
+
const capture = domScreenshot(request, interrupt);
|
|
38
|
+
const response = await unlessInterrupted(capture, interrupt, screenshotInterrupted);
|
|
39
|
+
if (interrupt.aborted)
|
|
40
|
+
throw screenshotInterrupted(interruptSignal(interrupt));
|
|
41
|
+
return response;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Capture the page (or the element `backendNodeId` names) and write the
|
|
45
|
+
* image.
|
|
500
46
|
*
|
|
501
|
-
* @
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
const
|
|
510
|
-
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
* not cropped away.
|
|
518
|
-
*
|
|
519
|
-
* @param ref - Node reference
|
|
520
|
-
* @param bounds - Border box (DOM.getBoxModel coordinates)
|
|
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
|
|
543
|
-
*/
|
|
544
|
-
async function paintedArea(ref, bounds) {
|
|
545
|
-
const objectGroup = `bdg-shot-${process.pid}`;
|
|
546
|
-
try {
|
|
547
|
-
const resolved = await callBdgScript('DOM.resolveNode', { ...ref, objectGroup });
|
|
548
|
-
const objectId = resolved.data?.result?.object
|
|
549
|
-
.objectId;
|
|
550
|
-
if (!objectId)
|
|
551
|
-
return bounds;
|
|
552
|
-
const response = await callCDP('Runtime.callFunctionOn', {
|
|
553
|
-
objectId,
|
|
554
|
-
functionDeclaration: CONTENT_OVERFLOW_JS,
|
|
555
|
-
returnByValue: true,
|
|
556
|
-
});
|
|
557
|
-
const overflow = response.data?.result
|
|
558
|
-
?.result?.value;
|
|
559
|
-
if (!overflow)
|
|
560
|
-
return bounds;
|
|
561
|
-
const [left, top, right, bottom] = ['left', 'top', 'right', 'bottom'].map((side) => Math.max(0, overflow[side] ?? 0));
|
|
562
|
-
if (Math.max(left, top, right, bottom) <= OVERFLOW_SLACK)
|
|
563
|
-
return bounds;
|
|
564
|
-
return {
|
|
565
|
-
x: bounds.x - left,
|
|
566
|
-
y: bounds.y - top,
|
|
567
|
-
width: bounds.width + left + right,
|
|
568
|
-
height: bounds.height + top + bottom,
|
|
569
|
-
};
|
|
570
|
-
}
|
|
571
|
-
catch (error) {
|
|
572
|
-
log.debug(`Could not measure overflowing content: ${String(error)}`);
|
|
573
|
-
return bounds;
|
|
574
|
-
}
|
|
575
|
-
finally {
|
|
576
|
-
await callCDP('Runtime.releaseObjectGroup', { objectGroup }).catch(() => undefined);
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
/**
|
|
580
|
-
* Capture a screenshot of a single element: its border box, grown to include
|
|
581
|
-
* content overflowing it ({@link captureArea}). The box model is relative to
|
|
582
|
-
* the viewport and the capture clip to the page, so the page scroll is added
|
|
583
|
-
* (the reported bounds are page coordinates, like `dom layout`'s).
|
|
584
|
-
*/
|
|
585
|
-
export async function captureElementScreenshot(outputPath, ref, options = {}) {
|
|
586
|
-
const format = options.format ?? 'png';
|
|
587
|
-
const quality = format === 'jpeg' ? (options.quality ?? 90) : undefined;
|
|
588
|
-
const noResize = options.noResize ?? false;
|
|
589
|
-
const dprResponse = await callBdgScript('Runtime.evaluate', {
|
|
590
|
-
expression: 'window.devicePixelRatio',
|
|
591
|
-
returnByValue: true,
|
|
592
|
-
});
|
|
593
|
-
const devicePixelRatio = dprResponse.data?.result?.result?.value ?? 1;
|
|
594
|
-
const before = (await callCDP('Page.getLayoutMetrics', {})).data?.result;
|
|
595
|
-
const restoreMetrics = await useUnitPixelRatio(devicePixelRatio, before?.visualViewport ?? { clientWidth: 800, clientHeight: 600 });
|
|
596
|
-
const restore = async (wide, scrolledFrom) => {
|
|
597
|
-
await (wide ? restoreViewport() : restoreMetrics());
|
|
598
|
-
if (scrolledFrom)
|
|
599
|
-
await restoreScrollPosition(scrolledFrom);
|
|
600
|
-
};
|
|
601
|
-
let measured;
|
|
602
|
-
try {
|
|
603
|
-
measured = await measureInView(ref, options.padding ?? 0);
|
|
604
|
-
}
|
|
605
|
-
catch (error) {
|
|
606
|
-
await restoreViewport();
|
|
607
|
-
throw error;
|
|
608
|
-
}
|
|
609
|
-
const { box, bounds, inView, scrolledFrom } = measured;
|
|
610
|
-
const originalWidth = bounds.width;
|
|
611
|
-
const originalHeight = bounds.height;
|
|
612
|
-
const resized = shouldResize(originalWidth, originalHeight, noResize);
|
|
613
|
-
const scale = resized ? calculateResizeScale(originalWidth, originalHeight) : 1;
|
|
614
|
-
const finalWidth = Math.round(originalWidth * scale);
|
|
615
|
-
const finalHeight = Math.round(originalHeight * scale);
|
|
616
|
-
const metricsResponse = await callCDP('Page.getLayoutMetrics', {});
|
|
617
|
-
const metricsResult = metricsResponse.data?.result;
|
|
618
|
-
const scroll = metricsResult?.cssLayoutViewport ?? { pageX: 0, pageY: 0 };
|
|
619
|
-
const onPage = (area) => ({
|
|
620
|
-
...area,
|
|
621
|
-
x: area.x + scroll.pageX,
|
|
622
|
-
y: area.y + scroll.pageY,
|
|
623
|
-
});
|
|
624
|
-
const clip = onPage(bounds);
|
|
625
|
-
let screenshotResult;
|
|
626
|
-
try {
|
|
627
|
-
const screenshotResponse = await callCDP('Page.captureScreenshot', {
|
|
628
|
-
format,
|
|
629
|
-
...(quality !== undefined && { quality }),
|
|
630
|
-
clip: { ...clip, scale },
|
|
631
|
-
captureBeyondViewport: !inView,
|
|
632
|
-
});
|
|
633
|
-
screenshotResult = screenshotResponse.data?.result;
|
|
634
|
-
}
|
|
635
|
-
finally {
|
|
636
|
-
await restore(!inView, scrolledFrom);
|
|
637
|
-
}
|
|
638
|
-
if (!screenshotResult?.data) {
|
|
639
|
-
throw new CDPConnectionError('No screenshot data returned', new Error('Empty response'));
|
|
640
|
-
}
|
|
641
|
-
const buffer = Buffer.from(screenshotResult.data, 'base64');
|
|
642
|
-
const absolutePath = await writeOutputFile(outputPath, buffer);
|
|
643
|
-
const result = {
|
|
644
|
-
path: absolutePath,
|
|
645
|
-
format,
|
|
646
|
-
width: finalWidth,
|
|
647
|
-
height: finalHeight,
|
|
648
|
-
size: buffer.length,
|
|
649
|
-
fullPage: false,
|
|
650
|
-
finalTokens: calculateImageTokens(finalWidth, finalHeight),
|
|
651
|
-
element: {
|
|
652
|
-
bounds: roundBounds(onPage(box)),
|
|
653
|
-
...(bounds !== box && { captured: roundBounds(clip) }),
|
|
654
|
-
...(options.padding && { padding: options.padding }),
|
|
655
|
-
},
|
|
656
|
-
};
|
|
657
|
-
if (quality !== undefined) {
|
|
658
|
-
result.quality = quality;
|
|
659
|
-
}
|
|
660
|
-
if (resized) {
|
|
661
|
-
result.resized = true;
|
|
662
|
-
result.originalWidth = originalWidth;
|
|
663
|
-
result.originalHeight = originalHeight;
|
|
664
|
-
result.originalTokens = calculateImageTokens(originalWidth, originalHeight);
|
|
665
|
-
}
|
|
666
|
-
return result;
|
|
667
|
-
}
|
|
668
|
-
/**
|
|
669
|
-
* Bounds in whole pixels.
|
|
670
|
-
*
|
|
671
|
-
* @param bounds - Bounds
|
|
672
|
-
* @returns Rounded bounds
|
|
673
|
-
*/
|
|
674
|
-
function roundBounds(bounds) {
|
|
675
|
-
return {
|
|
676
|
-
x: Math.round(bounds.x),
|
|
677
|
-
y: Math.round(bounds.y),
|
|
678
|
-
width: Math.round(bounds.width),
|
|
679
|
-
height: Math.round(bounds.height),
|
|
680
|
-
};
|
|
47
|
+
* @param outputPath - File to write
|
|
48
|
+
* @param request - What to capture and how
|
|
49
|
+
* @param interrupt - Aborted on Ctrl-C or SIGTERM: the capture is cancelled
|
|
50
|
+
* @returns What was captured, with the file's absolute path
|
|
51
|
+
* @throws CommandError with the daemon's error (Chrome's, when the capture
|
|
52
|
+
* failed) and exit code, or 130/143 when interrupted
|
|
53
|
+
*/
|
|
54
|
+
export async function captureScreenshot(outputPath, request, interrupt) {
|
|
55
|
+
const response = await requestCapture(request, interrupt);
|
|
56
|
+
if (response.status === 'error' || !response.data) {
|
|
57
|
+
const fallback = operationFailedError('take the screenshot', `Check the session: ${sessionCommand('bdg status')}`);
|
|
58
|
+
const suggestion = response.error === undefined ? fallback.suggestion : response.suggestion;
|
|
59
|
+
throw new CommandError(response.error ?? fallback.message, suggestion ? { suggestion } : {}, response.exitCode ?? EXIT_CODES.CDP_CONNECTION_FAILURE);
|
|
60
|
+
}
|
|
61
|
+
const path = await writeOutputFile(outputPath, Buffer.from(response.data.image, 'base64'));
|
|
62
|
+
return { path, ...response.data.screenshot };
|
|
681
63
|
}
|
|
682
64
|
//# sourceMappingURL=screenshot.js.map
|