cdp-skill 1.0.2 → 1.0.4

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.
Files changed (78) hide show
  1. package/README.md +3 -0
  2. package/SKILL.md +34 -5
  3. package/package.json +2 -1
  4. package/src/capture/console-capture.js +241 -0
  5. package/src/capture/debug-capture.js +144 -0
  6. package/src/capture/error-aggregator.js +151 -0
  7. package/src/capture/eval-serializer.js +320 -0
  8. package/src/capture/index.js +40 -0
  9. package/src/capture/network-capture.js +211 -0
  10. package/src/capture/pdf-capture.js +256 -0
  11. package/src/capture/screenshot-capture.js +325 -0
  12. package/src/cdp/browser.js +569 -0
  13. package/src/cdp/connection.js +369 -0
  14. package/src/cdp/discovery.js +138 -0
  15. package/src/cdp/index.js +29 -0
  16. package/src/cdp/target-and-session.js +439 -0
  17. package/src/cdp-skill.js +25 -11
  18. package/src/constants.js +79 -0
  19. package/src/dom/actionability.js +638 -0
  20. package/src/dom/click-executor.js +923 -0
  21. package/src/dom/element-handle.js +496 -0
  22. package/src/dom/element-locator.js +475 -0
  23. package/src/dom/element-validator.js +120 -0
  24. package/src/dom/fill-executor.js +489 -0
  25. package/src/dom/index.js +248 -0
  26. package/src/dom/input-emulator.js +406 -0
  27. package/src/dom/keyboard-executor.js +202 -0
  28. package/src/dom/quad-helpers.js +89 -0
  29. package/src/dom/react-filler.js +94 -0
  30. package/src/dom/wait-executor.js +423 -0
  31. package/src/index.js +6 -6
  32. package/src/page/cookie-manager.js +202 -0
  33. package/src/page/dom-stability.js +181 -0
  34. package/src/page/index.js +36 -0
  35. package/src/{page.js → page/page-controller.js} +109 -839
  36. package/src/page/wait-utilities.js +302 -0
  37. package/src/page/web-storage-manager.js +108 -0
  38. package/src/runner/context-helpers.js +224 -0
  39. package/src/runner/execute-browser.js +518 -0
  40. package/src/runner/execute-form.js +315 -0
  41. package/src/runner/execute-input.js +308 -0
  42. package/src/runner/execute-interaction.js +672 -0
  43. package/src/runner/execute-navigation.js +180 -0
  44. package/src/runner/execute-query.js +771 -0
  45. package/src/runner/index.js +51 -0
  46. package/src/runner/step-executors.js +421 -0
  47. package/src/runner/step-validator.js +641 -0
  48. package/src/tests/Actionability.test.js +613 -0
  49. package/src/tests/BrowserClient.test.js +1 -1
  50. package/src/tests/ChromeDiscovery.test.js +1 -1
  51. package/src/tests/ClickExecutor.test.js +554 -0
  52. package/src/tests/ConsoleCapture.test.js +1 -1
  53. package/src/tests/ContextHelpers.test.js +453 -0
  54. package/src/tests/CookieManager.test.js +450 -0
  55. package/src/tests/DebugCapture.test.js +307 -0
  56. package/src/tests/ElementHandle.test.js +1 -1
  57. package/src/tests/ElementLocator.test.js +1 -1
  58. package/src/tests/ErrorAggregator.test.js +1 -1
  59. package/src/tests/EvalSerializer.test.js +391 -0
  60. package/src/tests/FillExecutor.test.js +611 -0
  61. package/src/tests/InputEmulator.test.js +1 -1
  62. package/src/tests/KeyboardExecutor.test.js +430 -0
  63. package/src/tests/NetworkErrorCapture.test.js +1 -1
  64. package/src/tests/PageController.test.js +1 -1
  65. package/src/tests/PdfCapture.test.js +333 -0
  66. package/src/tests/ScreenshotCapture.test.js +1 -1
  67. package/src/tests/SessionRegistry.test.js +1 -1
  68. package/src/tests/StepValidator.test.js +527 -0
  69. package/src/tests/TargetManager.test.js +1 -1
  70. package/src/tests/TestRunner.test.js +1 -1
  71. package/src/tests/WaitStrategy.test.js +1 -1
  72. package/src/tests/WaitUtilities.test.js +508 -0
  73. package/src/tests/WebStorageManager.test.js +333 -0
  74. package/src/types.js +309 -0
  75. package/src/capture.js +0 -1400
  76. package/src/cdp.js +0 -1286
  77. package/src/dom.js +0 -4379
  78. package/src/runner.js +0 -3676
@@ -0,0 +1,256 @@
1
+ /**
2
+ * PDF Capture Module
3
+ * Generate PDF documents from page content
4
+ *
5
+ * PUBLIC EXPORTS:
6
+ * - createPdfCapture(session) - Factory for PDF capture
7
+ *
8
+ * @module cdp-skill/capture/pdf-capture
9
+ */
10
+
11
+ import fs from 'fs/promises';
12
+ import path from 'path';
13
+
14
+ /**
15
+ * Create a PDF capture utility
16
+ * @param {import('../types.js').CDPSession} session - CDP session
17
+ * @returns {Object} PDF capture interface
18
+ */
19
+ export function createPdfCapture(session) {
20
+ /**
21
+ * Generate PDF from current page
22
+ * @param {import('../types.js').PdfOptions} [options] - PDF options
23
+ * @returns {Promise<Buffer>} PDF buffer
24
+ */
25
+ async function generatePdf(options = {}) {
26
+ const params = {
27
+ landscape: options.landscape || false,
28
+ displayHeaderFooter: options.displayHeaderFooter || false,
29
+ headerTemplate: options.headerTemplate || '',
30
+ footerTemplate: options.footerTemplate || '',
31
+ printBackground: options.printBackground !== false,
32
+ scale: options.scale || 1,
33
+ paperWidth: options.paperWidth || 8.5,
34
+ paperHeight: options.paperHeight || 11,
35
+ marginTop: options.marginTop || 0.4,
36
+ marginBottom: options.marginBottom || 0.4,
37
+ marginLeft: options.marginLeft || 0.4,
38
+ marginRight: options.marginRight || 0.4,
39
+ pageRanges: options.pageRanges || '',
40
+ preferCSSPageSize: options.preferCSSPageSize || false
41
+ };
42
+
43
+ const result = await session.send('Page.printToPDF', params);
44
+ return Buffer.from(result.data, 'base64');
45
+ }
46
+
47
+ function formatFileSize(bytes) {
48
+ if (bytes < 1024) return `${bytes} B`;
49
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
50
+ return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
51
+ }
52
+
53
+ /**
54
+ * Extract metadata from PDF buffer
55
+ * @param {Buffer} buffer - PDF buffer
56
+ * @returns {{fileSize: number, fileSizeFormatted: string, pageCount: number, dimensions: Object}}
57
+ */
58
+ function extractPdfMetadata(buffer) {
59
+ const fileSize = buffer.length;
60
+ const content = buffer.toString('binary');
61
+
62
+ // Count pages by looking for /Type /Page entries
63
+ const pageMatches = content.match(/\/Type\s*\/Page[^s]/g);
64
+ const pageCount = pageMatches ? pageMatches.length : 1;
65
+
66
+ // Try to extract media box dimensions (default page size)
67
+ let dimensions = { width: 612, height: 792 }; // Default Letter size in points
68
+ const mediaBoxMatch = content.match(/\/MediaBox\s*\[\s*(\d+(?:\.\d+)?)\s+(\d+(?:\.\d+)?)\s+(\d+(?:\.\d+)?)\s+(\d+(?:\.\d+)?)\s*\]/);
69
+ if (mediaBoxMatch) {
70
+ dimensions = {
71
+ width: parseFloat(mediaBoxMatch[3]) - parseFloat(mediaBoxMatch[1]),
72
+ height: parseFloat(mediaBoxMatch[4]) - parseFloat(mediaBoxMatch[2])
73
+ };
74
+ }
75
+
76
+ return {
77
+ fileSize,
78
+ fileSizeFormatted: formatFileSize(fileSize),
79
+ pageCount,
80
+ dimensions: {
81
+ width: dimensions.width,
82
+ height: dimensions.height,
83
+ unit: 'points'
84
+ }
85
+ };
86
+ }
87
+
88
+ /**
89
+ * Validate PDF buffer
90
+ * @param {Buffer} buffer - PDF buffer
91
+ * @returns {{valid: boolean, errors: string[], warnings: string[]}}
92
+ */
93
+ function validatePdf(buffer) {
94
+ const content = buffer.toString('binary');
95
+ const errors = [];
96
+ const warnings = [];
97
+
98
+ // Check PDF header
99
+ if (!content.startsWith('%PDF-')) {
100
+ errors.push('Invalid PDF: missing PDF header');
101
+ }
102
+
103
+ // Check for EOF marker
104
+ if (!content.includes('%%EOF')) {
105
+ warnings.push('PDF may be truncated: missing EOF marker');
106
+ }
107
+
108
+ // Check for xref table or xref stream
109
+ if (!content.includes('xref') && !content.includes('/XRef')) {
110
+ warnings.push('PDF may have structural issues: no cross-reference found');
111
+ }
112
+
113
+ // Check minimum size (a valid PDF should be at least a few hundred bytes)
114
+ if (buffer.length < 100) {
115
+ errors.push('PDF file is too small to be valid');
116
+ }
117
+
118
+ return {
119
+ valid: errors.length === 0,
120
+ errors,
121
+ warnings
122
+ };
123
+ }
124
+
125
+ /**
126
+ * Generate PDF of specific element
127
+ * @param {string} selector - Element selector
128
+ * @param {import('../types.js').PdfOptions} [options] - PDF options
129
+ * @param {Object} elementLocator - Element locator instance
130
+ * @returns {Promise<Buffer>} PDF buffer
131
+ */
132
+ async function generateElementPdf(selector, options = {}, elementLocator) {
133
+ if (!elementLocator) {
134
+ throw new Error('Element locator required for element PDF');
135
+ }
136
+
137
+ // Find the element
138
+ const element = await elementLocator.querySelector(selector);
139
+ if (!element) {
140
+ throw new Error(`Element not found: ${selector}`);
141
+ }
142
+
143
+ try {
144
+ // Get the element's HTML and create a print-optimized version
145
+ const elementHtml = await element.evaluate(`function() {
146
+ const clone = this.cloneNode(true);
147
+ // Create a wrapper with print-friendly styles
148
+ const wrapper = document.createElement('div');
149
+ wrapper.style.cssText = 'width: 100%; margin: 0; padding: 0;';
150
+ wrapper.appendChild(clone);
151
+ return wrapper.outerHTML;
152
+ }`);
153
+
154
+ // Store original body content
155
+ await session.send('Runtime.evaluate', {
156
+ expression: `
157
+ window.__originalBody = document.body.innerHTML;
158
+ window.__originalStyles = document.body.style.cssText;
159
+ `
160
+ });
161
+
162
+ // Replace body with element content for printing
163
+ await session.send('Runtime.evaluate', {
164
+ expression: `
165
+ document.body.innerHTML = ${JSON.stringify(elementHtml)};
166
+ document.body.style.cssText = 'margin: 0; padding: 20px;';
167
+ `
168
+ });
169
+
170
+ // Generate the PDF
171
+ const buffer = await generatePdf(options);
172
+
173
+ // Restore original body
174
+ await session.send('Runtime.evaluate', {
175
+ expression: `
176
+ document.body.innerHTML = window.__originalBody;
177
+ document.body.style.cssText = window.__originalStyles;
178
+ delete window.__originalBody;
179
+ delete window.__originalStyles;
180
+ `
181
+ });
182
+
183
+ return buffer;
184
+ } finally {
185
+ await element.dispose();
186
+ }
187
+ }
188
+
189
+ /**
190
+ * Save PDF to file
191
+ * @param {string} filePath - Destination path
192
+ * @param {Object} [options] - PDF options
193
+ * @param {string} [options.selector] - Element selector for element PDF
194
+ * @param {boolean} [options.validate] - Validate PDF structure
195
+ * @param {Object} [elementLocator] - Element locator for selector option
196
+ * @returns {Promise<Object>} Result with path, metadata, and optional validation
197
+ */
198
+ async function saveToFile(filePath, options = {}, elementLocator = null) {
199
+ let buffer;
200
+
201
+ // Support element PDF via selector
202
+ if (options.selector && elementLocator) {
203
+ buffer = await generateElementPdf(options.selector, options, elementLocator);
204
+ } else {
205
+ buffer = await generatePdf(options);
206
+ }
207
+
208
+ const absolutePath = path.resolve(filePath);
209
+ const dir = path.dirname(absolutePath);
210
+
211
+ try {
212
+ await fs.mkdir(dir, { recursive: true });
213
+ } catch (err) {
214
+ if (err.code === 'EACCES' || err.code === 'EPERM') {
215
+ throw new Error(`Permission denied: cannot create directory "${dir}"`);
216
+ }
217
+ if (err.code === 'EROFS') {
218
+ throw new Error(`Read-only filesystem: cannot create directory "${dir}"`);
219
+ }
220
+ throw new Error(`Failed to create directory "${dir}": ${err.message}`);
221
+ }
222
+
223
+ try {
224
+ await fs.writeFile(absolutePath, buffer);
225
+ } catch (err) {
226
+ if (err.code === 'ENOSPC') {
227
+ throw new Error(`Disk full: cannot write PDF to "${absolutePath}"`);
228
+ }
229
+ if (err.code === 'EACCES' || err.code === 'EPERM') {
230
+ throw new Error(`Permission denied: cannot write to "${absolutePath}"`);
231
+ }
232
+ if (err.code === 'EROFS') {
233
+ throw new Error(`Read-only filesystem: cannot write to "${absolutePath}"`);
234
+ }
235
+ throw new Error(`Failed to save PDF to "${absolutePath}": ${err.message}`);
236
+ }
237
+
238
+ // Extract metadata
239
+ const metadata = extractPdfMetadata(buffer);
240
+
241
+ // Optionally validate
242
+ let validation = null;
243
+ if (options.validate) {
244
+ validation = validatePdf(buffer);
245
+ }
246
+
247
+ return {
248
+ path: absolutePath,
249
+ ...metadata,
250
+ validation,
251
+ selector: options.selector || null
252
+ };
253
+ }
254
+
255
+ return { generatePdf, saveToFile, extractPdfMetadata, validatePdf };
256
+ }
@@ -0,0 +1,325 @@
1
+ /**
2
+ * Screenshot Capture Module
3
+ * CDP-based screenshot capture for viewport, full page, and regions
4
+ *
5
+ * PUBLIC EXPORTS:
6
+ * - createScreenshotCapture(session, options?) - Factory for screenshot capture
7
+ * - captureViewport(session, options?) - Capture viewport screenshot
8
+ * - captureFullPage(session, options?) - Capture full page screenshot
9
+ * - captureRegion(session, region, options?) - Capture specific region
10
+ * - saveScreenshot(buffer, filePath) - Save screenshot to file
11
+ *
12
+ * @module cdp-skill/capture/screenshot-capture
13
+ */
14
+
15
+ import fs from 'fs/promises';
16
+ import path from 'path';
17
+
18
+ const DEFAULT_MAX_DIMENSION = 16384;
19
+ const VALID_FORMATS = ['png', 'jpeg', 'webp'];
20
+
21
+ /**
22
+ * Create a screenshot capture utility
23
+ * @param {import('../types.js').CDPSession} session - CDP session
24
+ * @param {Object} [options] - Configuration options
25
+ * @param {number} [options.maxDimension=16384] - Maximum dimension for full page captures
26
+ * @returns {Object} Screenshot capture interface
27
+ */
28
+ export function createScreenshotCapture(session, options = {}) {
29
+ const maxDimension = options.maxDimension || DEFAULT_MAX_DIMENSION;
30
+
31
+ function validateFormat(format) {
32
+ if (!VALID_FORMATS.includes(format)) {
33
+ throw new Error(
34
+ `Invalid screenshot format "${format}". Valid formats are: ${VALID_FORMATS.join(', ')}`
35
+ );
36
+ }
37
+ }
38
+
39
+ function validateQuality(quality, format) {
40
+ if (quality === undefined) return;
41
+ if (format === 'png') {
42
+ throw new Error('Quality option is only supported for jpeg and webp formats, not png');
43
+ }
44
+ if (typeof quality !== 'number' || quality < 0 || quality > 100) {
45
+ throw new Error('Quality must be a number between 0 and 100');
46
+ }
47
+ }
48
+
49
+ function validateOptions(opts = {}) {
50
+ const format = opts.format || 'png';
51
+ validateFormat(format);
52
+ validateQuality(opts.quality, format);
53
+ return { ...opts, format };
54
+ }
55
+
56
+ /**
57
+ * Capture viewport screenshot
58
+ * @param {import('../types.js').ScreenshotOptions} [captureOptions] - Screenshot options
59
+ * @returns {Promise<Buffer>} PNG/JPEG/WebP image buffer
60
+ */
61
+ async function captureViewport(captureOptions = {}) {
62
+ const validated = validateOptions(captureOptions);
63
+ const params = { format: validated.format };
64
+
65
+ if (params.format !== 'png' && validated.quality !== undefined) {
66
+ params.quality = validated.quality;
67
+ }
68
+
69
+ // Support omitBackground option
70
+ if (captureOptions.omitBackground) {
71
+ params.fromSurface = true;
72
+ // Enable transparent background
73
+ await session.send('Emulation.setDefaultBackgroundColorOverride', {
74
+ color: { r: 0, g: 0, b: 0, a: 0 }
75
+ });
76
+ }
77
+
78
+ // Support clip option for region capture
79
+ if (captureOptions.clip) {
80
+ params.clip = {
81
+ x: captureOptions.clip.x,
82
+ y: captureOptions.clip.y,
83
+ width: captureOptions.clip.width,
84
+ height: captureOptions.clip.height,
85
+ scale: captureOptions.clip.scale || 1
86
+ };
87
+ }
88
+
89
+ const result = await session.send('Page.captureScreenshot', params);
90
+
91
+ // Reset background override if we changed it
92
+ if (captureOptions.omitBackground) {
93
+ await session.send('Emulation.setDefaultBackgroundColorOverride');
94
+ }
95
+
96
+ return Buffer.from(result.data, 'base64');
97
+ }
98
+
99
+ /**
100
+ * Capture full page screenshot
101
+ * @param {import('../types.js').ScreenshotOptions} [captureOptions] - Screenshot options
102
+ * @returns {Promise<Buffer>} PNG/JPEG/WebP image buffer
103
+ */
104
+ async function captureFullPage(captureOptions = {}) {
105
+ const validated = validateOptions(captureOptions);
106
+
107
+ const metrics = await session.send('Page.getLayoutMetrics');
108
+ const { contentSize } = metrics;
109
+
110
+ const width = Math.ceil(contentSize.width);
111
+ const height = Math.ceil(contentSize.height);
112
+
113
+ if (width > maxDimension || height > maxDimension) {
114
+ throw new Error(
115
+ `Page dimensions (${width}x${height}) exceed maximum allowed (${maxDimension}x${maxDimension}). ` +
116
+ `Consider using captureViewport() or captureRegion() instead.`
117
+ );
118
+ }
119
+
120
+ const params = {
121
+ format: validated.format,
122
+ captureBeyondViewport: true,
123
+ clip: { x: 0, y: 0, width, height, scale: 1 }
124
+ };
125
+
126
+ if (params.format !== 'png' && validated.quality !== undefined) {
127
+ params.quality = validated.quality;
128
+ }
129
+
130
+ const result = await session.send('Page.captureScreenshot', params);
131
+ return Buffer.from(result.data, 'base64');
132
+ }
133
+
134
+ /**
135
+ * Capture specific region
136
+ * @param {import('../types.js').ClipRegion} region - Region to capture
137
+ * @param {import('../types.js').ScreenshotOptions} [captureOptions] - Screenshot options
138
+ * @returns {Promise<Buffer>} PNG/JPEG/WebP image buffer
139
+ */
140
+ async function captureRegion(region, captureOptions = {}) {
141
+ const validated = validateOptions(captureOptions);
142
+ const params = {
143
+ format: validated.format,
144
+ clip: {
145
+ x: region.x,
146
+ y: region.y,
147
+ width: region.width,
148
+ height: region.height,
149
+ scale: captureOptions.scale || 1
150
+ }
151
+ };
152
+
153
+ if (params.format !== 'png' && validated.quality !== undefined) {
154
+ params.quality = validated.quality;
155
+ }
156
+
157
+ const result = await session.send('Page.captureScreenshot', params);
158
+ return Buffer.from(result.data, 'base64');
159
+ }
160
+
161
+ /**
162
+ * Capture element by bounding box
163
+ * @param {import('../types.js').BoundingBox} boundingBox - Element bounding box
164
+ * @param {Object} [captureOptions] - Screenshot options
165
+ * @param {number} [captureOptions.padding=0] - Padding around element
166
+ * @returns {Promise<Buffer>} PNG/JPEG/WebP image buffer
167
+ */
168
+ async function captureElement(boundingBox, captureOptions = {}) {
169
+ const padding = captureOptions.padding || 0;
170
+ return captureRegion({
171
+ x: Math.max(0, boundingBox.x - padding),
172
+ y: Math.max(0, boundingBox.y - padding),
173
+ width: boundingBox.width + (padding * 2),
174
+ height: boundingBox.height + (padding * 2)
175
+ }, captureOptions);
176
+ }
177
+
178
+ /**
179
+ * Save buffer to file
180
+ * @param {Buffer} buffer - Image buffer
181
+ * @param {string} filePath - Destination path
182
+ * @returns {Promise<string>} Absolute path
183
+ */
184
+ async function saveToFile(buffer, filePath) {
185
+ const absolutePath = path.resolve(filePath);
186
+ const dir = path.dirname(absolutePath);
187
+
188
+ try {
189
+ await fs.mkdir(dir, { recursive: true });
190
+ } catch (err) {
191
+ if (err.code === 'EACCES' || err.code === 'EPERM') {
192
+ throw new Error(`Permission denied: cannot create directory "${dir}"`);
193
+ }
194
+ if (err.code === 'EROFS') {
195
+ throw new Error(`Read-only filesystem: cannot create directory "${dir}"`);
196
+ }
197
+ throw new Error(`Failed to create directory "${dir}": ${err.message}`);
198
+ }
199
+
200
+ try {
201
+ await fs.writeFile(absolutePath, buffer);
202
+ } catch (err) {
203
+ if (err.code === 'ENOSPC') {
204
+ throw new Error(`Disk full: cannot write screenshot to "${absolutePath}"`);
205
+ }
206
+ if (err.code === 'EACCES' || err.code === 'EPERM') {
207
+ throw new Error(`Permission denied: cannot write to "${absolutePath}"`);
208
+ }
209
+ if (err.code === 'EROFS') {
210
+ throw new Error(`Read-only filesystem: cannot write to "${absolutePath}"`);
211
+ }
212
+ throw new Error(`Failed to save screenshot to "${absolutePath}": ${err.message}`);
213
+ }
214
+
215
+ return absolutePath;
216
+ }
217
+
218
+ /**
219
+ * Capture and save to file
220
+ * @param {string} filePath - Destination path
221
+ * @param {Object} [captureOptions] - Screenshot options
222
+ * @param {boolean} [captureOptions.fullPage=false] - Capture full page
223
+ * @param {string} [captureOptions.selector] - Element selector to capture
224
+ * @param {Object} [elementLocator] - Element locator for selector capture
225
+ * @returns {Promise<string>} Absolute path
226
+ */
227
+ async function captureToFile(filePath, captureOptions = {}, elementLocator = null) {
228
+ let buffer;
229
+ let elementBox = null;
230
+
231
+ // Support element screenshot via selector
232
+ if (captureOptions.selector && elementLocator) {
233
+ const element = await elementLocator.querySelector(captureOptions.selector);
234
+ if (!element) {
235
+ throw new Error(`Element not found: ${captureOptions.selector}`);
236
+ }
237
+ const box = await element.getBoundingBox();
238
+ await element.dispose();
239
+
240
+ if (!box || box.width === 0 || box.height === 0) {
241
+ throw new Error(`Element has no visible dimensions: ${captureOptions.selector}`);
242
+ }
243
+
244
+ elementBox = box;
245
+ buffer = await captureElement(box, captureOptions);
246
+ } else if (captureOptions.fullPage) {
247
+ buffer = await captureFullPage(captureOptions);
248
+ } else {
249
+ buffer = await captureViewport(captureOptions);
250
+ }
251
+
252
+ return saveToFile(buffer, filePath);
253
+ }
254
+
255
+ /**
256
+ * Get viewport dimensions
257
+ * @returns {Promise<{width: number, height: number}>}
258
+ */
259
+ async function getViewportDimensions() {
260
+ const result = await session.send('Runtime.evaluate', {
261
+ expression: '({ width: window.innerWidth, height: window.innerHeight })',
262
+ returnByValue: true
263
+ });
264
+ return result.result.value;
265
+ }
266
+
267
+ return {
268
+ captureViewport,
269
+ captureFullPage,
270
+ captureRegion,
271
+ captureElement,
272
+ saveToFile,
273
+ captureToFile,
274
+ getViewportDimensions
275
+ };
276
+ }
277
+
278
+ /**
279
+ * Convenience function to capture viewport
280
+ * @param {import('../types.js').CDPSession} session - CDP session
281
+ * @param {import('../types.js').ScreenshotOptions} [options] - Screenshot options
282
+ * @returns {Promise<Buffer>}
283
+ */
284
+ export async function captureViewport(session, options = {}) {
285
+ const capture = createScreenshotCapture(session, options);
286
+ return capture.captureViewport(options);
287
+ }
288
+
289
+ /**
290
+ * Convenience function to capture full page
291
+ * @param {import('../types.js').CDPSession} session - CDP session
292
+ * @param {import('../types.js').ScreenshotOptions} [options] - Screenshot options
293
+ * @returns {Promise<Buffer>}
294
+ */
295
+ export async function captureFullPage(session, options = {}) {
296
+ const capture = createScreenshotCapture(session, options);
297
+ return capture.captureFullPage(options);
298
+ }
299
+
300
+ /**
301
+ * Convenience function to capture a region
302
+ * @param {import('../types.js').CDPSession} session - CDP session
303
+ * @param {import('../types.js').ClipRegion} region - Region to capture
304
+ * @param {import('../types.js').ScreenshotOptions} [options] - Screenshot options
305
+ * @returns {Promise<Buffer>}
306
+ */
307
+ export async function captureRegion(session, region, options = {}) {
308
+ const capture = createScreenshotCapture(session, options);
309
+ return capture.captureRegion(region, options);
310
+ }
311
+
312
+ /**
313
+ * Save a screenshot buffer to file
314
+ * @param {Buffer} buffer - Screenshot buffer
315
+ * @param {string} filePath - Destination path
316
+ * @returns {Promise<string>} Absolute path
317
+ */
318
+ export async function saveScreenshot(buffer, filePath) {
319
+ const absolutePath = path.resolve(filePath);
320
+ const dir = path.dirname(absolutePath);
321
+
322
+ await fs.mkdir(dir, { recursive: true });
323
+ await fs.writeFile(absolutePath, buffer);
324
+ return absolutePath;
325
+ }