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
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 is optional on first call. `tab` required on subsequent calls.
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`, `verify`, `force`, `debug`, `timeout`
402
+ Options: `selector`, `ref`, `x`/`y`, `force`, `debug`, `timeout`, `jsClick`, `nativeOnly`
398
403
 
399
- Returns: `{clicked: true}`. With `verify`: adds `{targetReceived: true/false}`. With navigation: adds `{navigated: true, newUrl: "..."}`.
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.2",
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
+ }