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.
- package/README.md +3 -0
- package/SKILL.md +34 -5
- package/package.json +2 -1
- package/src/capture/console-capture.js +241 -0
- package/src/capture/debug-capture.js +144 -0
- package/src/capture/error-aggregator.js +151 -0
- package/src/capture/eval-serializer.js +320 -0
- package/src/capture/index.js +40 -0
- package/src/capture/network-capture.js +211 -0
- package/src/capture/pdf-capture.js +256 -0
- package/src/capture/screenshot-capture.js +325 -0
- package/src/cdp/browser.js +569 -0
- package/src/cdp/connection.js +369 -0
- package/src/cdp/discovery.js +138 -0
- package/src/cdp/index.js +29 -0
- package/src/cdp/target-and-session.js +439 -0
- package/src/cdp-skill.js +25 -11
- package/src/constants.js +79 -0
- package/src/dom/actionability.js +638 -0
- package/src/dom/click-executor.js +923 -0
- package/src/dom/element-handle.js +496 -0
- package/src/dom/element-locator.js +475 -0
- package/src/dom/element-validator.js +120 -0
- package/src/dom/fill-executor.js +489 -0
- package/src/dom/index.js +248 -0
- package/src/dom/input-emulator.js +406 -0
- package/src/dom/keyboard-executor.js +202 -0
- package/src/dom/quad-helpers.js +89 -0
- package/src/dom/react-filler.js +94 -0
- package/src/dom/wait-executor.js +423 -0
- package/src/index.js +6 -6
- package/src/page/cookie-manager.js +202 -0
- package/src/page/dom-stability.js +181 -0
- package/src/page/index.js +36 -0
- package/src/{page.js → page/page-controller.js} +109 -839
- package/src/page/wait-utilities.js +302 -0
- package/src/page/web-storage-manager.js +108 -0
- package/src/runner/context-helpers.js +224 -0
- package/src/runner/execute-browser.js +518 -0
- package/src/runner/execute-form.js +315 -0
- package/src/runner/execute-input.js +308 -0
- package/src/runner/execute-interaction.js +672 -0
- package/src/runner/execute-navigation.js +180 -0
- package/src/runner/execute-query.js +771 -0
- package/src/runner/index.js +51 -0
- package/src/runner/step-executors.js +421 -0
- package/src/runner/step-validator.js +641 -0
- package/src/tests/Actionability.test.js +613 -0
- package/src/tests/BrowserClient.test.js +1 -1
- package/src/tests/ChromeDiscovery.test.js +1 -1
- package/src/tests/ClickExecutor.test.js +554 -0
- package/src/tests/ConsoleCapture.test.js +1 -1
- package/src/tests/ContextHelpers.test.js +453 -0
- package/src/tests/CookieManager.test.js +450 -0
- package/src/tests/DebugCapture.test.js +307 -0
- package/src/tests/ElementHandle.test.js +1 -1
- package/src/tests/ElementLocator.test.js +1 -1
- package/src/tests/ErrorAggregator.test.js +1 -1
- package/src/tests/EvalSerializer.test.js +391 -0
- package/src/tests/FillExecutor.test.js +611 -0
- package/src/tests/InputEmulator.test.js +1 -1
- package/src/tests/KeyboardExecutor.test.js +430 -0
- package/src/tests/NetworkErrorCapture.test.js +1 -1
- package/src/tests/PageController.test.js +1 -1
- package/src/tests/PdfCapture.test.js +333 -0
- package/src/tests/ScreenshotCapture.test.js +1 -1
- package/src/tests/SessionRegistry.test.js +1 -1
- package/src/tests/StepValidator.test.js +527 -0
- package/src/tests/TargetManager.test.js +1 -1
- package/src/tests/TestRunner.test.js +1 -1
- package/src/tests/WaitStrategy.test.js +1 -1
- package/src/tests/WaitUtilities.test.js +508 -0
- package/src/tests/WebStorageManager.test.js +333 -0
- package/src/types.js +309 -0
- package/src/capture.js +0 -1400
- package/src/cdp.js +0 -1286
- package/src/dom.js +0 -4379
- package/src/runner.js +0 -3676
package/README.md
CHANGED
|
@@ -56,11 +56,13 @@ node src/cdp-skill.js '{"steps":[{"goto":"https://google.com"}]}'
|
|
|
56
56
|
- **CSS queries** - Traditional selector-based queries
|
|
57
57
|
- **Multi-query** - Batch multiple queries in one step
|
|
58
58
|
- **Page inspection** - Quick overview of page structure
|
|
59
|
+
- **Coordinate discovery** - `refAt`, `elementsAt`, `elementsNear` for visual-based targeting
|
|
59
60
|
|
|
60
61
|
### Frame Support
|
|
61
62
|
- **List frames** - Enumerate all iframes
|
|
62
63
|
- **Switch context** - Execute in iframe by selector, index, or name
|
|
63
64
|
- **Cross-origin detection** - Identifies cross-origin frames in snapshots
|
|
65
|
+
- **Shadow DOM** - Pierce shadow roots with `pierceShadow` option in snapshots
|
|
64
66
|
|
|
65
67
|
### Screenshots & PDF
|
|
66
68
|
- **Viewport capture** - Current view
|
|
@@ -115,6 +117,7 @@ src/
|
|
|
115
117
|
├── dom.js # Element location, input emulation, clicks
|
|
116
118
|
├── aria.js # Accessibility snapshots, role queries
|
|
117
119
|
├── capture.js # Screenshots, PDF, console, network
|
|
120
|
+
├── diff.js # Snapshot diffing, context capture
|
|
118
121
|
├── runner.js # Step validation and execution
|
|
119
122
|
├── utils.js # Errors, key validation, device presets
|
|
120
123
|
└── index.js # Public API exports
|
package/SKILL.md
CHANGED
|
@@ -87,13 +87,18 @@ node src/cdp-skill.js '{"steps":[{"closeTab":"t1"}]}'
|
|
|
87
87
|
"host": "localhost",
|
|
88
88
|
"port": 9222,
|
|
89
89
|
"tab": "t1",
|
|
90
|
-
"timeout": 10000
|
|
90
|
+
"timeout": 10000,
|
|
91
|
+
"headless": false
|
|
91
92
|
},
|
|
92
93
|
"steps": [...]
|
|
93
94
|
}
|
|
94
95
|
```
|
|
95
96
|
|
|
96
|
-
Config
|
|
97
|
+
Config options:
|
|
98
|
+
- `host`, `port` - CDP connection (default: localhost:9222)
|
|
99
|
+
- `tab` - Tab ID to use (required on subsequent calls)
|
|
100
|
+
- `timeout` - Command timeout in ms (default: 30000)
|
|
101
|
+
- `headless` - Run Chrome in headless mode (default: false). Prevents Chrome from stealing focus. Chrome auto-launches if not running.
|
|
97
102
|
|
|
98
103
|
## Output Schema
|
|
99
104
|
|
|
@@ -394,9 +399,28 @@ Returns: `{frameId, url, name}`
|
|
|
394
399
|
{"click": {"ref": "e4"}}
|
|
395
400
|
{"click": {"x": 450, "y": 200}}
|
|
396
401
|
```
|
|
397
|
-
Options: `selector`, `ref`, `x`/`y`, `
|
|
402
|
+
Options: `selector`, `ref`, `x`/`y`, `force`, `debug`, `timeout`, `jsClick`, `nativeOnly`
|
|
398
403
|
|
|
399
|
-
Returns: `{clicked: true
|
|
404
|
+
Returns: `{clicked: true, method: "cdp"|"jsClick"|"jsClick-auto"}`. With navigation: adds `{navigated: true, newUrl: "..."}`.
|
|
405
|
+
|
|
406
|
+
**Automatic Click Verification**
|
|
407
|
+
Clicks are automatically verified - if CDP mouse events don't reach the target element (common on React, Vue, Next.js sites), the system automatically falls back to JavaScript click. The `method` field shows what was used:
|
|
408
|
+
- `"cdp"` - CDP mouse events worked
|
|
409
|
+
- `"jsClick"` - User requested `jsClick: true`
|
|
410
|
+
- `"jsClick-auto"` - CDP failed, automatic fallback to JavaScript click
|
|
411
|
+
|
|
412
|
+
**click** - Force JavaScript click
|
|
413
|
+
```json
|
|
414
|
+
{"click": {"selector": "#submit", "jsClick": true}}
|
|
415
|
+
{"click": {"ref": "e4", "jsClick": true}}
|
|
416
|
+
```
|
|
417
|
+
Use `jsClick: true` to skip CDP and use JavaScript `element.click()` directly.
|
|
418
|
+
|
|
419
|
+
**click** - Disable auto-fallback
|
|
420
|
+
```json
|
|
421
|
+
{"click": {"selector": "#btn", "nativeOnly": true}}
|
|
422
|
+
```
|
|
423
|
+
Use `nativeOnly: true` to disable the automatic jsClick fallback. The click will use CDP only and report `targetReceived: false` if the click didn't reach the element.
|
|
400
424
|
|
|
401
425
|
**click** - Multi-selector fallback
|
|
402
426
|
```json
|
|
@@ -538,7 +562,12 @@ Options:
|
|
|
538
562
|
- `offsetX`/`offsetY`: offset from element center (default: 0)
|
|
539
563
|
- `steps` (default: 10), `delay` (ms, default: 0)
|
|
540
564
|
|
|
541
|
-
Returns: `{dragged: true, source: {x, y}, target: {x, y}, steps}`
|
|
565
|
+
Returns: `{dragged: true, method: "html5-dnd"|"range-input"|"mouse-events", source: {x, y}, target: {x, y}, steps}`
|
|
566
|
+
|
|
567
|
+
The `method` field indicates which drag strategy was used:
|
|
568
|
+
- `"html5-dnd"` - HTML5 Drag and Drop API (for draggable elements)
|
|
569
|
+
- `"range-input"` - Direct value manipulation (for `<input type="range">` sliders)
|
|
570
|
+
- `"mouse-events"` - JavaScript mouse event simulation (for custom drag implementations)
|
|
542
571
|
|
|
543
572
|
**selectOption** - Select option(s) in a native `<select>` dropdown
|
|
544
573
|
```json
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cdp-skill",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "Browser automation skill using Chrome DevTools Protocol for Claude Code and AI agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
"browser",
|
|
42
42
|
"automation",
|
|
43
43
|
"claude-code",
|
|
44
|
+
"codex",
|
|
44
45
|
"ai-agent",
|
|
45
46
|
"skill",
|
|
46
47
|
"testing",
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Console Capture Module
|
|
3
|
+
* Captures browser console messages and exceptions during test execution
|
|
4
|
+
*
|
|
5
|
+
* PUBLIC EXPORTS:
|
|
6
|
+
* - createConsoleCapture(session, options?) - Factory for console capture
|
|
7
|
+
*
|
|
8
|
+
* @module cdp-skill/capture/console-capture
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const DEFAULT_MAX_MESSAGES = 10000;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Create a console capture utility for capturing console messages and exceptions
|
|
15
|
+
* Listens only to Runtime.consoleAPICalled to avoid duplicate messages
|
|
16
|
+
* @param {import('../types.js').CDPSession} session - CDP session
|
|
17
|
+
* @param {Object} [options] - Configuration options
|
|
18
|
+
* @param {number} [options.maxMessages=10000] - Maximum messages to store
|
|
19
|
+
* @returns {Object} Console capture interface
|
|
20
|
+
*/
|
|
21
|
+
export function createConsoleCapture(session, options = {}) {
|
|
22
|
+
const maxMessages = options.maxMessages || DEFAULT_MAX_MESSAGES;
|
|
23
|
+
let messages = [];
|
|
24
|
+
let capturing = false;
|
|
25
|
+
const handlers = {
|
|
26
|
+
consoleAPICalled: null,
|
|
27
|
+
exceptionThrown: null
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
function mapConsoleType(type) {
|
|
31
|
+
const mapping = {
|
|
32
|
+
'log': 'log',
|
|
33
|
+
'debug': 'debug',
|
|
34
|
+
'info': 'info',
|
|
35
|
+
'error': 'error',
|
|
36
|
+
'warning': 'warning',
|
|
37
|
+
'warn': 'warning',
|
|
38
|
+
'dir': 'log',
|
|
39
|
+
'dirxml': 'log',
|
|
40
|
+
'table': 'log',
|
|
41
|
+
'trace': 'log',
|
|
42
|
+
'assert': 'error',
|
|
43
|
+
'count': 'log',
|
|
44
|
+
'timeEnd': 'log'
|
|
45
|
+
};
|
|
46
|
+
return mapping[type] || 'log';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function formatArgs(args) {
|
|
50
|
+
if (!Array.isArray(args)) return '[invalid args]';
|
|
51
|
+
return args.map(arg => {
|
|
52
|
+
try {
|
|
53
|
+
if (arg.value !== undefined) return String(arg.value);
|
|
54
|
+
if (arg.description) return arg.description;
|
|
55
|
+
if (arg.unserializableValue) return arg.unserializableValue;
|
|
56
|
+
if (arg.preview?.description) return arg.preview.description;
|
|
57
|
+
return `[${arg.type || 'unknown'}]`;
|
|
58
|
+
} catch {
|
|
59
|
+
return '[unserializable]';
|
|
60
|
+
}
|
|
61
|
+
}).join(' ');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function extractExceptionMessage(exceptionDetails) {
|
|
65
|
+
if (exceptionDetails.exception?.description) return exceptionDetails.exception.description;
|
|
66
|
+
if (exceptionDetails.text) return exceptionDetails.text;
|
|
67
|
+
return 'Unknown exception';
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function addMessage(message) {
|
|
71
|
+
messages.push(message);
|
|
72
|
+
if (messages.length > maxMessages) {
|
|
73
|
+
messages.shift();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Start capturing console messages
|
|
79
|
+
* @returns {Promise<void>}
|
|
80
|
+
*/
|
|
81
|
+
async function startCapture() {
|
|
82
|
+
if (capturing) return;
|
|
83
|
+
|
|
84
|
+
await session.send('Runtime.enable');
|
|
85
|
+
|
|
86
|
+
handlers.consoleAPICalled = (params) => {
|
|
87
|
+
addMessage({
|
|
88
|
+
type: 'console',
|
|
89
|
+
level: mapConsoleType(params.type),
|
|
90
|
+
text: formatArgs(params.args),
|
|
91
|
+
args: params.args,
|
|
92
|
+
stackTrace: params.stackTrace,
|
|
93
|
+
timestamp: params.timestamp
|
|
94
|
+
});
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
handlers.exceptionThrown = (params) => {
|
|
98
|
+
const exception = params.exceptionDetails;
|
|
99
|
+
addMessage({
|
|
100
|
+
type: 'exception',
|
|
101
|
+
level: 'error',
|
|
102
|
+
text: exception.text || extractExceptionMessage(exception),
|
|
103
|
+
exception: exception.exception,
|
|
104
|
+
stackTrace: exception.stackTrace,
|
|
105
|
+
url: exception.url,
|
|
106
|
+
line: exception.lineNumber,
|
|
107
|
+
column: exception.columnNumber,
|
|
108
|
+
timestamp: params.timestamp
|
|
109
|
+
});
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
session.on('Runtime.consoleAPICalled', handlers.consoleAPICalled);
|
|
113
|
+
session.on('Runtime.exceptionThrown', handlers.exceptionThrown);
|
|
114
|
+
|
|
115
|
+
capturing = true;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Stop capturing console messages
|
|
120
|
+
* @returns {Promise<void>}
|
|
121
|
+
*/
|
|
122
|
+
async function stopCapture() {
|
|
123
|
+
if (!capturing) return;
|
|
124
|
+
|
|
125
|
+
if (handlers.consoleAPICalled) {
|
|
126
|
+
session.off('Runtime.consoleAPICalled', handlers.consoleAPICalled);
|
|
127
|
+
handlers.consoleAPICalled = null;
|
|
128
|
+
}
|
|
129
|
+
if (handlers.exceptionThrown) {
|
|
130
|
+
session.off('Runtime.exceptionThrown', handlers.exceptionThrown);
|
|
131
|
+
handlers.exceptionThrown = null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
await session.send('Runtime.disable');
|
|
135
|
+
|
|
136
|
+
capturing = false;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Get all captured messages
|
|
141
|
+
* @returns {import('../types.js').ConsoleMessage[]}
|
|
142
|
+
*/
|
|
143
|
+
function getMessages() {
|
|
144
|
+
return [...messages];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Get messages since a timestamp
|
|
149
|
+
* @param {number} timestamp - CDP timestamp
|
|
150
|
+
* @returns {import('../types.js').ConsoleMessage[]}
|
|
151
|
+
*/
|
|
152
|
+
function getMessagesSince(timestamp) {
|
|
153
|
+
return messages.filter(m => m.timestamp && m.timestamp >= timestamp);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Get messages between timestamps
|
|
158
|
+
* @param {number} startTimestamp - Start timestamp
|
|
159
|
+
* @param {number} endTimestamp - End timestamp
|
|
160
|
+
* @returns {import('../types.js').ConsoleMessage[]}
|
|
161
|
+
*/
|
|
162
|
+
function getMessagesBetween(startTimestamp, endTimestamp) {
|
|
163
|
+
return messages.filter(m =>
|
|
164
|
+
m.timestamp && m.timestamp >= startTimestamp && m.timestamp <= endTimestamp
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Get messages by log level
|
|
170
|
+
* @param {string|string[]} levels - Log level(s) to filter
|
|
171
|
+
* @returns {import('../types.js').ConsoleMessage[]}
|
|
172
|
+
*/
|
|
173
|
+
function getMessagesByLevel(levels) {
|
|
174
|
+
const levelSet = new Set(Array.isArray(levels) ? levels : [levels]);
|
|
175
|
+
return messages.filter(m => levelSet.has(m.level));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Get messages by type
|
|
180
|
+
* @param {string|string[]} types - Message type(s) to filter
|
|
181
|
+
* @returns {import('../types.js').ConsoleMessage[]}
|
|
182
|
+
*/
|
|
183
|
+
function getMessagesByType(types) {
|
|
184
|
+
const typeSet = new Set(Array.isArray(types) ? types : [types]);
|
|
185
|
+
return messages.filter(m => typeSet.has(m.type));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Get error messages only
|
|
190
|
+
* @returns {import('../types.js').ConsoleMessage[]}
|
|
191
|
+
*/
|
|
192
|
+
function getErrors() {
|
|
193
|
+
return messages.filter(m => m.level === 'error' || m.type === 'exception');
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Get warning messages only
|
|
198
|
+
* @returns {import('../types.js').ConsoleMessage[]}
|
|
199
|
+
*/
|
|
200
|
+
function getWarnings() {
|
|
201
|
+
return messages.filter(m => m.level === 'warning');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Check if any errors were captured
|
|
206
|
+
* @returns {boolean}
|
|
207
|
+
*/
|
|
208
|
+
function hasErrors() {
|
|
209
|
+
return messages.some(m => m.level === 'error' || m.type === 'exception');
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Clear captured messages
|
|
214
|
+
*/
|
|
215
|
+
function clear() {
|
|
216
|
+
messages = [];
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Clear browser console
|
|
221
|
+
* @returns {Promise<void>}
|
|
222
|
+
*/
|
|
223
|
+
async function clearBrowserConsole() {
|
|
224
|
+
await session.send('Console.clearMessages');
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
startCapture,
|
|
229
|
+
stopCapture,
|
|
230
|
+
getMessages,
|
|
231
|
+
getMessagesSince,
|
|
232
|
+
getMessagesBetween,
|
|
233
|
+
getMessagesByLevel,
|
|
234
|
+
getMessagesByType,
|
|
235
|
+
getErrors,
|
|
236
|
+
getWarnings,
|
|
237
|
+
hasErrors,
|
|
238
|
+
clear,
|
|
239
|
+
clearBrowserConsole
|
|
240
|
+
};
|
|
241
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Debug Capture Module
|
|
3
|
+
* Captures debugging state (screenshots, DOM) before/after actions
|
|
4
|
+
*
|
|
5
|
+
* PUBLIC EXPORTS:
|
|
6
|
+
* - createDebugCapture(session, screenshotCapture, options?) - Factory for debug capture
|
|
7
|
+
*
|
|
8
|
+
* @module cdp-skill/capture/debug-capture
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import fs from 'fs/promises';
|
|
12
|
+
import path from 'path';
|
|
13
|
+
import os from 'os';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Create a debug capture utility for capturing debugging state before/after actions
|
|
17
|
+
* @param {import('../types.js').CDPSession} session - CDP session
|
|
18
|
+
* @param {Object} screenshotCapture - Screenshot capture instance
|
|
19
|
+
* @param {Object} [options] - Configuration options
|
|
20
|
+
* @param {string} [options.outputDir] - Output directory (defaults to platform temp dir)
|
|
21
|
+
* @param {boolean} [options.captureScreenshots=true] - Whether to capture screenshots
|
|
22
|
+
* @param {boolean} [options.captureDom=true] - Whether to capture DOM
|
|
23
|
+
* @returns {Object} Debug capture interface
|
|
24
|
+
*/
|
|
25
|
+
export function createDebugCapture(session, screenshotCapture, options = {}) {
|
|
26
|
+
// Default to platform-specific temp directory
|
|
27
|
+
const defaultOutputDir = path.join(os.tmpdir(), 'cdp-skill', 'debug-captures');
|
|
28
|
+
const outputDir = options.outputDir || defaultOutputDir;
|
|
29
|
+
const captureScreenshots = options.captureScreenshots !== false;
|
|
30
|
+
const captureDom = options.captureDom !== false;
|
|
31
|
+
let stepIndex = 0;
|
|
32
|
+
|
|
33
|
+
async function ensureOutputDir() {
|
|
34
|
+
try {
|
|
35
|
+
await fs.mkdir(outputDir, { recursive: true });
|
|
36
|
+
} catch (e) {
|
|
37
|
+
// Ignore if already exists
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Capture current state
|
|
43
|
+
* @param {string} prefix - File name prefix
|
|
44
|
+
* @returns {Promise<{timestamp: string, screenshot?: string, screenshotError?: string, dom?: string, domError?: string}>}
|
|
45
|
+
*/
|
|
46
|
+
async function captureState(prefix) {
|
|
47
|
+
await ensureOutputDir();
|
|
48
|
+
const result = { timestamp: new Date().toISOString() };
|
|
49
|
+
|
|
50
|
+
if (captureScreenshots) {
|
|
51
|
+
try {
|
|
52
|
+
const screenshotPath = path.join(outputDir, `${prefix}.png`);
|
|
53
|
+
const buffer = await screenshotCapture.captureViewport();
|
|
54
|
+
await fs.writeFile(screenshotPath, buffer);
|
|
55
|
+
result.screenshot = screenshotPath;
|
|
56
|
+
} catch (e) {
|
|
57
|
+
result.screenshotError = e.message;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (captureDom) {
|
|
62
|
+
try {
|
|
63
|
+
const domPath = path.join(outputDir, `${prefix}.html`);
|
|
64
|
+
const domResult = await session.send('Runtime.evaluate', {
|
|
65
|
+
expression: 'document.documentElement.outerHTML',
|
|
66
|
+
returnByValue: true
|
|
67
|
+
});
|
|
68
|
+
if (domResult.result && domResult.result.value) {
|
|
69
|
+
await fs.writeFile(domPath, domResult.result.value);
|
|
70
|
+
result.dom = domPath;
|
|
71
|
+
}
|
|
72
|
+
} catch (e) {
|
|
73
|
+
result.domError = e.message;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return result;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Capture state before an action
|
|
82
|
+
* @param {string} action - Action name
|
|
83
|
+
* @param {Object} [params] - Action parameters
|
|
84
|
+
* @returns {Promise<Object>} Capture result
|
|
85
|
+
*/
|
|
86
|
+
async function captureBefore(action, params) {
|
|
87
|
+
stepIndex++;
|
|
88
|
+
const prefix = `step-${String(stepIndex).padStart(3, '0')}-${action}-before`;
|
|
89
|
+
return captureState(prefix);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Capture state after an action
|
|
94
|
+
* @param {string} action - Action name
|
|
95
|
+
* @param {Object} [params] - Action parameters
|
|
96
|
+
* @param {string} status - Action status ('ok' or 'error')
|
|
97
|
+
* @returns {Promise<Object>} Capture result
|
|
98
|
+
*/
|
|
99
|
+
async function captureAfter(action, params, status) {
|
|
100
|
+
const prefix = `step-${String(stepIndex).padStart(3, '0')}-${action}-after-${status}`;
|
|
101
|
+
return captureState(prefix);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Get current page info
|
|
106
|
+
* @returns {Promise<{url: string, title: string, readyState: string, scrollX: number, scrollY: number, innerWidth: number, innerHeight: number, documentWidth: number, documentHeight: number} | {error: string}>}
|
|
107
|
+
*/
|
|
108
|
+
async function getPageInfo() {
|
|
109
|
+
try {
|
|
110
|
+
const result = await session.send('Runtime.evaluate', {
|
|
111
|
+
expression: `({
|
|
112
|
+
url: window.location.href,
|
|
113
|
+
title: document.title,
|
|
114
|
+
readyState: document.readyState,
|
|
115
|
+
scrollX: window.scrollX,
|
|
116
|
+
scrollY: window.scrollY,
|
|
117
|
+
innerWidth: window.innerWidth,
|
|
118
|
+
innerHeight: window.innerHeight,
|
|
119
|
+
documentWidth: document.documentElement.scrollWidth,
|
|
120
|
+
documentHeight: document.documentElement.scrollHeight
|
|
121
|
+
})`,
|
|
122
|
+
returnByValue: true
|
|
123
|
+
});
|
|
124
|
+
return result.result.value;
|
|
125
|
+
} catch (e) {
|
|
126
|
+
return { error: e.message };
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Reset step counter
|
|
132
|
+
*/
|
|
133
|
+
function reset() {
|
|
134
|
+
stepIndex = 0;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
captureBefore,
|
|
139
|
+
captureAfter,
|
|
140
|
+
captureState,
|
|
141
|
+
getPageInfo,
|
|
142
|
+
reset
|
|
143
|
+
};
|
|
144
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error Aggregator Module
|
|
3
|
+
* Combines console and network errors into unified reports
|
|
4
|
+
*
|
|
5
|
+
* PUBLIC EXPORTS:
|
|
6
|
+
* - createErrorAggregator(consoleCapture, networkCapture) - Factory for aggregator
|
|
7
|
+
* - aggregateErrors(consoleCapture, networkCapture) - Convenience function
|
|
8
|
+
*
|
|
9
|
+
* @module cdp-skill/capture/error-aggregator
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Create an error aggregator that combines console and network errors
|
|
14
|
+
* @param {Object} consoleCapture - Console capture instance
|
|
15
|
+
* @param {Object} networkCapture - Network capture instance
|
|
16
|
+
* @returns {Object} Error aggregator interface
|
|
17
|
+
*/
|
|
18
|
+
export function createErrorAggregator(consoleCapture, networkCapture) {
|
|
19
|
+
if (!consoleCapture) throw new Error('consoleCapture is required');
|
|
20
|
+
if (!networkCapture) throw new Error('networkCapture is required');
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Get summary of all errors
|
|
24
|
+
* @returns {{hasErrors: boolean, hasWarnings: boolean, counts: Object, errors: Object}}
|
|
25
|
+
*/
|
|
26
|
+
function getSummary() {
|
|
27
|
+
const consoleErrors = consoleCapture.getErrors();
|
|
28
|
+
const consoleWarnings = consoleCapture.getWarnings();
|
|
29
|
+
const networkFailures = networkCapture.getNetworkFailures();
|
|
30
|
+
const httpErrs = networkCapture.getHttpErrors();
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
hasErrors: consoleErrors.length > 0 || networkFailures.length > 0 ||
|
|
34
|
+
httpErrs.some(e => e.status >= 500),
|
|
35
|
+
hasWarnings: consoleWarnings.length > 0 ||
|
|
36
|
+
httpErrs.some(e => e.status >= 400 && e.status < 500),
|
|
37
|
+
counts: {
|
|
38
|
+
consoleErrors: consoleErrors.length,
|
|
39
|
+
consoleWarnings: consoleWarnings.length,
|
|
40
|
+
networkFailures: networkFailures.length,
|
|
41
|
+
httpClientErrors: httpErrs.filter(e => e.status >= 400 && e.status < 500).length,
|
|
42
|
+
httpServerErrors: httpErrs.filter(e => e.status >= 500).length
|
|
43
|
+
},
|
|
44
|
+
errors: {
|
|
45
|
+
console: consoleErrors,
|
|
46
|
+
network: networkFailures,
|
|
47
|
+
http: httpErrs
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Get all errors sorted by timestamp
|
|
54
|
+
* @returns {Array} Combined errors with source annotation
|
|
55
|
+
*/
|
|
56
|
+
function getAllErrorsChronological() {
|
|
57
|
+
const all = [
|
|
58
|
+
...consoleCapture.getErrors().map(e => ({ ...e, source: 'console' })),
|
|
59
|
+
...networkCapture.getAllErrors().map(e => ({ ...e, source: 'network' }))
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
return all.sort((a, b) => (a.timestamp || 0) - (b.timestamp || 0));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Get critical errors only (exceptions, network failures, 5xx)
|
|
67
|
+
* @returns {Array}
|
|
68
|
+
*/
|
|
69
|
+
function getCriticalErrors() {
|
|
70
|
+
return [
|
|
71
|
+
...consoleCapture.getErrors().filter(e => e.type === 'exception'),
|
|
72
|
+
...networkCapture.getNetworkFailures(),
|
|
73
|
+
...networkCapture.getHttpErrors().filter(e => e.status >= 500)
|
|
74
|
+
];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Generate formatted error report
|
|
79
|
+
* @returns {string} Formatted report text
|
|
80
|
+
*/
|
|
81
|
+
function formatReport() {
|
|
82
|
+
const summary = getSummary();
|
|
83
|
+
const lines = ['=== Error Report ==='];
|
|
84
|
+
|
|
85
|
+
if (summary.counts.consoleErrors > 0) {
|
|
86
|
+
lines.push('\n## Console Errors');
|
|
87
|
+
for (const error of summary.errors.console) {
|
|
88
|
+
lines.push(` [${error.level.toUpperCase()}] ${error.text}`);
|
|
89
|
+
if (error.url) {
|
|
90
|
+
lines.push(` at ${error.url}:${error.line || '?'}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (summary.counts.networkFailures > 0) {
|
|
96
|
+
lines.push('\n## Network Failures');
|
|
97
|
+
for (const error of summary.errors.network) {
|
|
98
|
+
lines.push(` [FAILED] ${error.method} ${error.url}`);
|
|
99
|
+
lines.push(` Error: ${error.errorText}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (summary.counts.httpServerErrors > 0 || summary.counts.httpClientErrors > 0) {
|
|
104
|
+
lines.push('\n## HTTP Errors');
|
|
105
|
+
for (const error of summary.errors.http) {
|
|
106
|
+
lines.push(` [${error.status}] ${error.method} ${error.url}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (!summary.hasErrors && !summary.hasWarnings) {
|
|
111
|
+
lines.push('\nNo errors or warnings captured.');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return lines.join('\n');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Get JSON representation
|
|
119
|
+
* @returns {Object} JSON-serializable report
|
|
120
|
+
*/
|
|
121
|
+
function toJSON() {
|
|
122
|
+
return {
|
|
123
|
+
timestamp: new Date().toISOString(),
|
|
124
|
+
summary: getSummary(),
|
|
125
|
+
all: getAllErrorsChronological()
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
getSummary,
|
|
131
|
+
getAllErrorsChronological,
|
|
132
|
+
getCriticalErrors,
|
|
133
|
+
formatReport,
|
|
134
|
+
toJSON
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Aggregate errors from console and network captures
|
|
140
|
+
* @param {Object} consoleCapture - Console capture instance
|
|
141
|
+
* @param {Object} networkCapture - Network capture instance
|
|
142
|
+
* @returns {{summary: Object, critical: Array, report: string}}
|
|
143
|
+
*/
|
|
144
|
+
export function aggregateErrors(consoleCapture, networkCapture) {
|
|
145
|
+
const aggregator = createErrorAggregator(consoleCapture, networkCapture);
|
|
146
|
+
return {
|
|
147
|
+
summary: aggregator.getSummary(),
|
|
148
|
+
critical: aggregator.getCriticalErrors(),
|
|
149
|
+
report: aggregator.formatReport()
|
|
150
|
+
};
|
|
151
|
+
}
|