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,320 @@
1
+ /**
2
+ * Eval Serializer Module
3
+ * Handles serialization of JavaScript values for eval results
4
+ *
5
+ * PUBLIC EXPORTS:
6
+ * - createEvalSerializer() - Factory for eval serializer
7
+ * - getEvalSerializationFunction() - Get browser-side serialization code
8
+ * - processEvalResult(serialized) - Process serialized result
9
+ *
10
+ * @module cdp-skill/capture/eval-serializer
11
+ */
12
+
13
+ /**
14
+ * Create an eval serializer for handling serialization of JavaScript values
15
+ * Provides special handling for non-JSON-serializable values
16
+ * @returns {Object} Eval serializer interface
17
+ */
18
+ export function createEvalSerializer() {
19
+ /**
20
+ * Get the serialization function that runs in browser context
21
+ * @returns {string} JavaScript function declaration
22
+ */
23
+ function getSerializationFunction() {
24
+ return `function(value) {
25
+ // Handle primitives and null
26
+ if (value === null) return { type: 'null', value: null };
27
+ if (value === undefined) return { type: 'undefined', value: null };
28
+
29
+ const type = typeof value;
30
+
31
+ // Handle special number values (FR-039)
32
+ if (type === 'number') {
33
+ if (Number.isNaN(value)) return { type: 'number', value: null, repr: 'NaN' };
34
+ if (value === Infinity) return { type: 'number', value: null, repr: 'Infinity' };
35
+ if (value === -Infinity) return { type: 'number', value: null, repr: '-Infinity' };
36
+ return { type: 'number', value: value };
37
+ }
38
+
39
+ // Handle strings, booleans, bigint
40
+ if (type === 'string') return { type: 'string', value: value };
41
+ if (type === 'boolean') return { type: 'boolean', value: value };
42
+ if (type === 'bigint') return { type: 'bigint', value: null, repr: value.toString() + 'n' };
43
+ if (type === 'symbol') return { type: 'symbol', value: null, repr: value.toString() };
44
+ if (type === 'function') return { type: 'function', value: null, repr: value.toString().substring(0, 100) };
45
+
46
+ // Handle Date (FR-040)
47
+ if (value instanceof Date) {
48
+ return {
49
+ type: 'Date',
50
+ value: value.toISOString(),
51
+ timestamp: value.getTime()
52
+ };
53
+ }
54
+
55
+ // Handle Map (FR-040)
56
+ if (value instanceof Map) {
57
+ const entries = [];
58
+ let count = 0;
59
+ for (const [k, v] of value) {
60
+ if (count >= 50) break; // Limit entries
61
+ try {
62
+ entries.push([
63
+ typeof k === 'object' ? JSON.stringify(k) : String(k),
64
+ typeof v === 'object' ? JSON.stringify(v) : String(v)
65
+ ]);
66
+ } catch (e) {
67
+ entries.push([String(k), '[Circular]']);
68
+ }
69
+ count++;
70
+ }
71
+ return {
72
+ type: 'Map',
73
+ size: value.size,
74
+ entries: entries
75
+ };
76
+ }
77
+
78
+ // Handle Set (FR-040)
79
+ if (value instanceof Set) {
80
+ const items = [];
81
+ let count = 0;
82
+ for (const item of value) {
83
+ if (count >= 50) break; // Limit items
84
+ try {
85
+ items.push(typeof item === 'object' ? JSON.stringify(item) : item);
86
+ } catch (e) {
87
+ items.push('[Circular]');
88
+ }
89
+ count++;
90
+ }
91
+ return {
92
+ type: 'Set',
93
+ size: value.size,
94
+ values: items
95
+ };
96
+ }
97
+
98
+ // Handle RegExp
99
+ if (value instanceof RegExp) {
100
+ return { type: 'RegExp', value: value.toString() };
101
+ }
102
+
103
+ // Handle Error
104
+ if (value instanceof Error) {
105
+ return {
106
+ type: 'Error',
107
+ name: value.name,
108
+ message: value.message,
109
+ stack: value.stack ? value.stack.substring(0, 500) : null
110
+ };
111
+ }
112
+
113
+ // Handle DOM Element (FR-041)
114
+ if (value instanceof Element) {
115
+ const attrs = {};
116
+ for (const attr of value.attributes) {
117
+ attrs[attr.name] = attr.value.substring(0, 100);
118
+ }
119
+ return {
120
+ type: 'Element',
121
+ tagName: value.tagName.toLowerCase(),
122
+ id: value.id || null,
123
+ className: value.className || null,
124
+ attributes: attrs,
125
+ textContent: value.textContent ? value.textContent.trim().substring(0, 200) : null,
126
+ innerHTML: value.innerHTML ? value.innerHTML.substring(0, 200) : null,
127
+ isConnected: value.isConnected,
128
+ childElementCount: value.childElementCount
129
+ };
130
+ }
131
+
132
+ // Handle NodeList
133
+ if (value instanceof NodeList || value instanceof HTMLCollection) {
134
+ const items = [];
135
+ const len = Math.min(value.length, 20);
136
+ for (let i = 0; i < len; i++) {
137
+ const el = value[i];
138
+ if (el instanceof Element) {
139
+ items.push({
140
+ tagName: el.tagName.toLowerCase(),
141
+ id: el.id || null,
142
+ className: el.className || null
143
+ });
144
+ }
145
+ }
146
+ return {
147
+ type: value instanceof NodeList ? 'NodeList' : 'HTMLCollection',
148
+ length: value.length,
149
+ items: items
150
+ };
151
+ }
152
+
153
+ // Handle Document
154
+ if (value instanceof Document) {
155
+ return {
156
+ type: 'Document',
157
+ title: value.title,
158
+ url: value.URL,
159
+ readyState: value.readyState
160
+ };
161
+ }
162
+
163
+ // Handle Window
164
+ if (value === window) {
165
+ return {
166
+ type: 'Window',
167
+ location: value.location.href,
168
+ innerWidth: value.innerWidth,
169
+ innerHeight: value.innerHeight
170
+ };
171
+ }
172
+
173
+ // Handle arrays - recursively serialize each element
174
+ if (Array.isArray(value)) {
175
+ const items = [];
176
+ const len = Math.min(value.length, 100); // Limit to 100 items
177
+ for (let i = 0; i < len; i++) {
178
+ items.push(arguments.callee(value[i])); // Recursive call
179
+ }
180
+ return {
181
+ type: 'array',
182
+ length: value.length,
183
+ items: items,
184
+ truncated: value.length > 100
185
+ };
186
+ }
187
+
188
+ // Handle plain objects - recursively serialize values
189
+ if (type === 'object') {
190
+ const keys = Object.keys(value);
191
+ const entries = {};
192
+ const len = Math.min(keys.length, 50); // Limit to 50 keys
193
+ for (let i = 0; i < len; i++) {
194
+ const k = keys[i];
195
+ entries[k] = arguments.callee(value[k]); // Recursive call
196
+ }
197
+ return {
198
+ type: 'object',
199
+ keys: keys.length,
200
+ entries: entries,
201
+ truncated: keys.length > 50
202
+ };
203
+ }
204
+
205
+ return { type: 'unknown', repr: String(value) };
206
+ }`;
207
+ }
208
+
209
+ /**
210
+ * Process the serialized result into a clean output format
211
+ * @param {Object} serialized - The serialized result from browser
212
+ * @returns {Object} Processed output
213
+ */
214
+ function processResult(serialized) {
215
+ if (!serialized || typeof serialized !== 'object') {
216
+ return { type: 'unknown', value: serialized };
217
+ }
218
+
219
+ const result = {
220
+ type: serialized.type
221
+ };
222
+
223
+ // Include value if present
224
+ if (serialized.value !== undefined) {
225
+ result.value = serialized.value;
226
+ }
227
+
228
+ // Include repr for non-serializable values
229
+ if (serialized.repr !== undefined) {
230
+ result.repr = serialized.repr;
231
+ }
232
+
233
+ // Include additional properties based on type
234
+ switch (serialized.type) {
235
+ case 'Date':
236
+ result.timestamp = serialized.timestamp;
237
+ break;
238
+ case 'Map':
239
+ result.size = serialized.size;
240
+ result.entries = serialized.entries;
241
+ break;
242
+ case 'Set':
243
+ result.size = serialized.size;
244
+ result.values = serialized.values;
245
+ break;
246
+ case 'Element':
247
+ result.tagName = serialized.tagName;
248
+ result.id = serialized.id;
249
+ result.className = serialized.className;
250
+ result.attributes = serialized.attributes;
251
+ result.textContent = serialized.textContent;
252
+ result.isConnected = serialized.isConnected;
253
+ result.childElementCount = serialized.childElementCount;
254
+ break;
255
+ case 'NodeList':
256
+ case 'HTMLCollection':
257
+ result.length = serialized.length;
258
+ result.items = serialized.items;
259
+ break;
260
+ case 'Error':
261
+ result.name = serialized.name;
262
+ result.message = serialized.message;
263
+ if (serialized.stack) result.stack = serialized.stack;
264
+ break;
265
+ case 'Document':
266
+ result.title = serialized.title;
267
+ result.url = serialized.url;
268
+ result.readyState = serialized.readyState;
269
+ break;
270
+ case 'Window':
271
+ result.location = serialized.location;
272
+ result.innerWidth = serialized.innerWidth;
273
+ result.innerHeight = serialized.innerHeight;
274
+ break;
275
+ case 'array':
276
+ result.length = serialized.length;
277
+ if (serialized.items) {
278
+ // Recursively process each item
279
+ result.items = serialized.items.map(item => processResult(item));
280
+ }
281
+ if (serialized.truncated) result.truncated = true;
282
+ break;
283
+ case 'object':
284
+ result.keys = serialized.keys;
285
+ if (serialized.entries) {
286
+ // Recursively process each entry value
287
+ result.entries = {};
288
+ for (const [k, v] of Object.entries(serialized.entries)) {
289
+ result.entries[k] = processResult(v);
290
+ }
291
+ }
292
+ if (serialized.truncated) result.truncated = true;
293
+ break;
294
+ }
295
+
296
+ return result;
297
+ }
298
+
299
+ return {
300
+ getSerializationFunction,
301
+ processResult
302
+ };
303
+ }
304
+
305
+ /**
306
+ * Get the serialization function (convenience export)
307
+ * @returns {string} JavaScript function declaration
308
+ */
309
+ export function getEvalSerializationFunction() {
310
+ return createEvalSerializer().getSerializationFunction();
311
+ }
312
+
313
+ /**
314
+ * Process a serialized eval result (convenience export)
315
+ * @param {Object} serialized - The serialized result from browser
316
+ * @returns {Object} Processed output
317
+ */
318
+ export function processEvalResult(serialized) {
319
+ return createEvalSerializer().processResult(serialized);
320
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Capture and Monitoring Module
3
+ * Re-exports all capture-related functionality
4
+ *
5
+ * @module cdp-skill/capture
6
+ */
7
+
8
+ // Console Capture
9
+ export { createConsoleCapture } from './console-capture.js';
10
+
11
+ // Screenshot Capture
12
+ export {
13
+ createScreenshotCapture,
14
+ captureViewport,
15
+ captureFullPage,
16
+ captureRegion,
17
+ saveScreenshot
18
+ } from './screenshot-capture.js';
19
+
20
+ // Network Capture
21
+ export { createNetworkCapture } from './network-capture.js';
22
+
23
+ // Error Aggregator
24
+ export {
25
+ createErrorAggregator,
26
+ aggregateErrors
27
+ } from './error-aggregator.js';
28
+
29
+ // PDF Capture
30
+ export { createPdfCapture } from './pdf-capture.js';
31
+
32
+ // Debug Capture
33
+ export { createDebugCapture } from './debug-capture.js';
34
+
35
+ // Eval Serializer
36
+ export {
37
+ createEvalSerializer,
38
+ getEvalSerializationFunction,
39
+ processEvalResult
40
+ } from './eval-serializer.js';
@@ -0,0 +1,211 @@
1
+ /**
2
+ * Network Capture Module
3
+ * Captures network failures and HTTP errors during page interaction
4
+ *
5
+ * PUBLIC EXPORTS:
6
+ * - createNetworkCapture(session, config?) - Factory for network capture
7
+ *
8
+ * @module cdp-skill/capture/network-capture
9
+ */
10
+
11
+ const DEFAULT_MAX_PENDING_REQUESTS = 10000;
12
+ const DEFAULT_REQUEST_TIMEOUT_MS = 5 * 60 * 1000;
13
+
14
+ /**
15
+ * Create a network error capture utility
16
+ * @param {import('../types.js').CDPSession} session - CDP session
17
+ * @param {Object} [config] - Configuration options
18
+ * @param {number} [config.maxPendingRequests=10000] - Maximum pending requests to track
19
+ * @param {number} [config.requestTimeoutMs=300000] - Stale request timeout
20
+ * @returns {Object} Network capture interface
21
+ */
22
+ export function createNetworkCapture(session, config = {}) {
23
+ const maxPendingRequests = config.maxPendingRequests || DEFAULT_MAX_PENDING_REQUESTS;
24
+ const requestTimeoutMs = config.requestTimeoutMs || DEFAULT_REQUEST_TIMEOUT_MS;
25
+
26
+ const requests = new Map();
27
+ let errors = [];
28
+ let httpErrors = [];
29
+ let capturing = false;
30
+ const handlers = {};
31
+ let captureOptions = {};
32
+ let cleanupIntervalId = null;
33
+
34
+ function cleanupStaleRequests() {
35
+ const now = Date.now() / 1000;
36
+ const timeoutSec = requestTimeoutMs / 1000;
37
+
38
+ for (const [requestId, request] of requests) {
39
+ if (now - request.timestamp > timeoutSec) {
40
+ requests.delete(requestId);
41
+ }
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Start capturing network errors
47
+ * @param {Object} [startOptions] - Capture options
48
+ * @param {boolean} [startOptions.captureHttpErrors=true] - Capture HTTP 4xx/5xx errors
49
+ * @param {number[]} [startOptions.ignoreStatusCodes=[]] - Status codes to ignore
50
+ * @returns {Promise<void>}
51
+ */
52
+ async function startCapture(startOptions = {}) {
53
+ if (capturing) return;
54
+
55
+ captureOptions = {
56
+ captureHttpErrors: startOptions.captureHttpErrors !== false,
57
+ ignoreStatusCodes: new Set(startOptions.ignoreStatusCodes || [])
58
+ };
59
+
60
+ await session.send('Network.enable');
61
+
62
+ handlers.requestWillBeSent = (params) => {
63
+ if (requests.size >= maxPendingRequests) {
64
+ const oldestKey = requests.keys().next().value;
65
+ requests.delete(oldestKey);
66
+ }
67
+ requests.set(params.requestId, {
68
+ url: params.request.url,
69
+ method: params.request.method,
70
+ timestamp: params.timestamp,
71
+ type: params.type
72
+ });
73
+ };
74
+
75
+ handlers.loadingFailed = (params) => {
76
+ const request = requests.get(params.requestId);
77
+ errors.push({
78
+ type: 'network-failure',
79
+ requestId: params.requestId,
80
+ url: request?.url || 'unknown',
81
+ method: request?.method || 'unknown',
82
+ resourceType: params.type,
83
+ errorText: params.errorText,
84
+ canceled: params.canceled || false,
85
+ blockedReason: params.blockedReason,
86
+ timestamp: params.timestamp
87
+ });
88
+ requests.delete(params.requestId);
89
+ };
90
+
91
+ handlers.responseReceived = (params) => {
92
+ const status = params.response.status;
93
+
94
+ if (captureOptions.captureHttpErrors && status >= 400 &&
95
+ !captureOptions.ignoreStatusCodes.has(status)) {
96
+ const request = requests.get(params.requestId);
97
+ httpErrors.push({
98
+ type: 'http-error',
99
+ requestId: params.requestId,
100
+ url: params.response.url,
101
+ method: request?.method || 'unknown',
102
+ status,
103
+ statusText: params.response.statusText,
104
+ resourceType: params.type,
105
+ mimeType: params.response.mimeType,
106
+ timestamp: params.timestamp
107
+ });
108
+ }
109
+ };
110
+
111
+ handlers.loadingFinished = (params) => {
112
+ requests.delete(params.requestId);
113
+ };
114
+
115
+ session.on('Network.requestWillBeSent', handlers.requestWillBeSent);
116
+ session.on('Network.loadingFailed', handlers.loadingFailed);
117
+ session.on('Network.responseReceived', handlers.responseReceived);
118
+ session.on('Network.loadingFinished', handlers.loadingFinished);
119
+
120
+ cleanupIntervalId = setInterval(
121
+ cleanupStaleRequests,
122
+ Math.min(requestTimeoutMs / 2, 60000)
123
+ );
124
+
125
+ capturing = true;
126
+ }
127
+
128
+ /**
129
+ * Stop capturing network errors
130
+ * @returns {Promise<void>}
131
+ */
132
+ async function stopCapture() {
133
+ if (!capturing) return;
134
+
135
+ session.off('Network.requestWillBeSent', handlers.requestWillBeSent);
136
+ session.off('Network.loadingFailed', handlers.loadingFailed);
137
+ session.off('Network.responseReceived', handlers.responseReceived);
138
+ session.off('Network.loadingFinished', handlers.loadingFinished);
139
+
140
+ if (cleanupIntervalId) {
141
+ clearInterval(cleanupIntervalId);
142
+ cleanupIntervalId = null;
143
+ }
144
+
145
+ requests.clear();
146
+ await session.send('Network.disable');
147
+ capturing = false;
148
+ }
149
+
150
+ /**
151
+ * Get network failures (connection errors, blocked requests, etc.)
152
+ * @returns {import('../types.js').NetworkError[]}
153
+ */
154
+ function getNetworkFailures() {
155
+ return [...errors];
156
+ }
157
+
158
+ /**
159
+ * Get HTTP errors (4xx and 5xx responses)
160
+ * @returns {import('../types.js').NetworkError[]}
161
+ */
162
+ function getHttpErrors() {
163
+ return [...httpErrors];
164
+ }
165
+
166
+ /**
167
+ * Get all errors sorted by timestamp
168
+ * @returns {import('../types.js').NetworkError[]}
169
+ */
170
+ function getAllErrors() {
171
+ return [...errors, ...httpErrors].sort((a, b) => a.timestamp - b.timestamp);
172
+ }
173
+
174
+ /**
175
+ * Check if any errors were captured
176
+ * @returns {boolean}
177
+ */
178
+ function hasErrors() {
179
+ return errors.length > 0 || httpErrors.length > 0;
180
+ }
181
+
182
+ /**
183
+ * Get errors by resource type
184
+ * @param {string|string[]} types - Resource type(s) to filter
185
+ * @returns {import('../types.js').NetworkError[]}
186
+ */
187
+ function getErrorsByType(types) {
188
+ const typeSet = new Set(Array.isArray(types) ? types : [types]);
189
+ return getAllErrors().filter(e => typeSet.has(e.resourceType));
190
+ }
191
+
192
+ /**
193
+ * Clear captured errors
194
+ */
195
+ function clear() {
196
+ errors = [];
197
+ httpErrors = [];
198
+ requests.clear();
199
+ }
200
+
201
+ return {
202
+ startCapture,
203
+ stopCapture,
204
+ getNetworkFailures,
205
+ getHttpErrors,
206
+ getAllErrors,
207
+ hasErrors,
208
+ getErrorsByType,
209
+ clear
210
+ };
211
+ }