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,489 @@
1
+ /**
2
+ * Fill Executor
3
+ * High-level form filling operations with actionability checking
4
+ *
5
+ * EXPORTS:
6
+ * - createFillExecutor(session, elementLocator, inputEmulator, ariaSnapshot?) → FillExecutor
7
+ * Methods: execute, executeBatch
8
+ *
9
+ * DEPENDENCIES:
10
+ * - ./actionability.js: createActionabilityChecker
11
+ * - ./element-validator.js: createElementValidator
12
+ * - ./react-filler.js: createReactInputFiller
13
+ * - ../utils.js: sleep, elementNotFoundError, elementNotEditableError, connectionError, releaseObject, resetInputState
14
+ */
15
+
16
+ import { createActionabilityChecker } from './actionability.js';
17
+ import { createElementValidator } from './element-validator.js';
18
+ import { createReactInputFiller } from './react-filler.js';
19
+ import {
20
+ sleep,
21
+ elementNotFoundError,
22
+ elementNotEditableError,
23
+ connectionError,
24
+ releaseObject,
25
+ resetInputState
26
+ } from '../utils.js';
27
+
28
+ /**
29
+ * Create a fill executor for handling fill operations
30
+ * @param {Object} session - CDP session
31
+ * @param {Object} elementLocator - Element locator instance
32
+ * @param {Object} inputEmulator - Input emulator instance
33
+ * @param {Object} [ariaSnapshot] - Optional ARIA snapshot instance
34
+ * @returns {Object} Fill executor interface
35
+ */
36
+ export function createFillExecutor(session, elementLocator, inputEmulator, ariaSnapshot = null) {
37
+ if (!session) throw new Error('CDP session is required');
38
+ if (!elementLocator) throw new Error('Element locator is required');
39
+ if (!inputEmulator) throw new Error('Input emulator is required');
40
+
41
+ const actionabilityChecker = createActionabilityChecker(session);
42
+ const elementValidator = createElementValidator(session);
43
+ const reactInputFiller = createReactInputFiller(session);
44
+
45
+ async function fillByRef(ref, value, opts = {}) {
46
+ const { clear = true, react = false } = opts;
47
+
48
+ if (!ariaSnapshot) {
49
+ throw new Error('ariaSnapshot is required for ref-based fills');
50
+ }
51
+
52
+ const refInfo = await ariaSnapshot.getElementByRef(ref);
53
+ if (!refInfo) {
54
+ throw elementNotFoundError(`ref:${ref}`, 0);
55
+ }
56
+
57
+ if (refInfo.stale) {
58
+ throw new Error(`Element ref:${ref} is no longer attached to the DOM. Page content may have changed. Run 'snapshot' again to get fresh refs.`);
59
+ }
60
+
61
+ if (refInfo.isVisible === false) {
62
+ throw new Error(`Element ref:${ref} exists but is not visible. It may be hidden or have zero dimensions.`);
63
+ }
64
+
65
+ const elementResult = await session.send('Runtime.evaluate', {
66
+ expression: `(function() {
67
+ const el = window.__ariaRefs && window.__ariaRefs.get(${JSON.stringify(ref)});
68
+ return el;
69
+ })()`,
70
+ returnByValue: false
71
+ });
72
+
73
+ if (!elementResult.result.objectId) {
74
+ throw elementNotFoundError(`ref:${ref}`, 0);
75
+ }
76
+
77
+ const objectId = elementResult.result.objectId;
78
+
79
+ const editableCheck = await elementValidator.isEditable(objectId);
80
+ if (!editableCheck.editable) {
81
+ await releaseObject(session, objectId);
82
+ throw elementNotEditableError(`ref:${ref}`, editableCheck.reason);
83
+ }
84
+
85
+ try {
86
+ if (react) {
87
+ await reactInputFiller.fillByObjectId(objectId, value);
88
+ return { filled: true, ref, method: 'react' };
89
+ }
90
+
91
+ await session.send('Runtime.callFunctionOn', {
92
+ objectId,
93
+ functionDeclaration: `function() {
94
+ this.scrollIntoView({ block: 'center', behavior: 'instant' });
95
+ }`
96
+ });
97
+
98
+ await sleep(100);
99
+
100
+ const x = refInfo.box.x + refInfo.box.width / 2;
101
+ const y = refInfo.box.y + refInfo.box.height / 2;
102
+ await inputEmulator.click(x, y);
103
+
104
+ await session.send('Runtime.callFunctionOn', {
105
+ objectId,
106
+ functionDeclaration: `function() { this.focus(); }`
107
+ });
108
+
109
+ if (clear) {
110
+ await inputEmulator.selectAll();
111
+ }
112
+
113
+ await inputEmulator.type(String(value));
114
+
115
+ return { filled: true, ref, method: 'keyboard' };
116
+ } finally {
117
+ await releaseObject(session, objectId);
118
+ }
119
+ }
120
+
121
+ async function fillBySelector(selector, value, opts = {}) {
122
+ const { clear = true, react = false, force = false, timeout = 5000 } = opts; // Reduced from 30s
123
+
124
+ const waitResult = await actionabilityChecker.waitForActionable(selector, 'fill', {
125
+ timeout,
126
+ force
127
+ });
128
+
129
+ if (!waitResult.success) {
130
+ if (waitResult.missingState === 'editable') {
131
+ throw elementNotEditableError(selector, waitResult.error);
132
+ }
133
+ throw new Error(`Element not actionable: ${waitResult.error}`);
134
+ }
135
+
136
+ const objectId = waitResult.objectId;
137
+
138
+ try {
139
+ if (react) {
140
+ await reactInputFiller.fillByObjectId(objectId, value);
141
+ return { filled: true, selector, method: 'react' };
142
+ }
143
+
144
+ const point = await actionabilityChecker.getClickablePoint(objectId);
145
+ if (!point) {
146
+ throw new Error('Could not determine click point for element');
147
+ }
148
+
149
+ await inputEmulator.click(point.x, point.y);
150
+
151
+ await session.send('Runtime.callFunctionOn', {
152
+ objectId,
153
+ functionDeclaration: `function() { this.focus(); }`
154
+ });
155
+
156
+ if (clear) {
157
+ await inputEmulator.selectAll();
158
+ }
159
+
160
+ await inputEmulator.type(String(value));
161
+
162
+ return { filled: true, selector, method: 'keyboard' };
163
+ } catch (e) {
164
+ await resetInputState(session);
165
+ throw e;
166
+ } finally {
167
+ await releaseObject(session, objectId);
168
+ }
169
+ }
170
+
171
+ /**
172
+ * Find an input element by its associated label text
173
+ * Search order: label[for] → nested input in label → aria-label → placeholder
174
+ * @param {string} labelText - Label text to search for
175
+ * @param {Object} [opts] - Options
176
+ * @param {boolean} [opts.exact=false] - Require exact match
177
+ * @returns {Promise<{objectId: string, method: string}|null>} Element info or null
178
+ */
179
+ async function findInputByLabel(labelText, opts = {}) {
180
+ const { exact = false } = opts;
181
+ const labelTextJson = JSON.stringify(labelText);
182
+ const labelTextLowerJson = JSON.stringify(labelText.toLowerCase());
183
+
184
+ const expression = `
185
+ (function() {
186
+ const labelText = ${labelTextJson};
187
+ const labelTextLower = ${labelTextLowerJson};
188
+ const exact = ${exact};
189
+
190
+ function matchesText(text) {
191
+ if (!text) return false;
192
+ if (exact) {
193
+ return text.trim() === labelText;
194
+ }
195
+ return text.toLowerCase().includes(labelTextLower);
196
+ }
197
+
198
+ function isEditable(el) {
199
+ if (!el || !el.isConnected) return false;
200
+ const tag = el.tagName.toLowerCase();
201
+ if (tag === 'textarea') return true;
202
+ if (tag === 'select') return true;
203
+ if (el.isContentEditable) return true;
204
+ if (tag === 'input') {
205
+ const type = (el.type || 'text').toLowerCase();
206
+ const editableTypes = ['text', 'password', 'email', 'number', 'search', 'tel', 'url', 'date', 'datetime-local', 'month', 'time', 'week'];
207
+ return editableTypes.includes(type);
208
+ }
209
+ return false;
210
+ }
211
+
212
+ function isVisible(el) {
213
+ if (!el.isConnected) return false;
214
+ const style = window.getComputedStyle(el);
215
+ if (style.display === 'none' || style.visibility === 'hidden') return false;
216
+ const rect = el.getBoundingClientRect();
217
+ return rect.width > 0 && rect.height > 0;
218
+ }
219
+
220
+ // 1. Search label[for] pointing to an input
221
+ const labels = document.querySelectorAll('label[for]');
222
+ for (const label of labels) {
223
+ if (matchesText(label.textContent)) {
224
+ const input = document.getElementById(label.getAttribute('for'));
225
+ if (input && isEditable(input) && isVisible(input)) {
226
+ return { element: input, method: 'label-for' };
227
+ }
228
+ }
229
+ }
230
+
231
+ // 2. Search for nested input inside label
232
+ const allLabels = document.querySelectorAll('label');
233
+ for (const label of allLabels) {
234
+ if (matchesText(label.textContent)) {
235
+ const input = label.querySelector('input, textarea, select');
236
+ if (input && isEditable(input) && isVisible(input)) {
237
+ return { element: input, method: 'label-nested' };
238
+ }
239
+ }
240
+ }
241
+
242
+ // 3. Search by aria-label attribute
243
+ const ariaElements = document.querySelectorAll('[aria-label]');
244
+ for (const el of ariaElements) {
245
+ if (matchesText(el.getAttribute('aria-label'))) {
246
+ if (isEditable(el) && isVisible(el)) {
247
+ return { element: el, method: 'aria-label' };
248
+ }
249
+ }
250
+ }
251
+
252
+ // 4. Search by aria-labelledby
253
+ const ariaLabelledByElements = document.querySelectorAll('[aria-labelledby]');
254
+ for (const el of ariaLabelledByElements) {
255
+ const labelId = el.getAttribute('aria-labelledby');
256
+ const labelEl = document.getElementById(labelId);
257
+ if (labelEl && matchesText(labelEl.textContent)) {
258
+ if (isEditable(el) && isVisible(el)) {
259
+ return { element: el, method: 'aria-labelledby' };
260
+ }
261
+ }
262
+ }
263
+
264
+ // 5. Search by placeholder attribute
265
+ const placeholderElements = document.querySelectorAll('[placeholder]');
266
+ for (const el of placeholderElements) {
267
+ if (matchesText(el.getAttribute('placeholder'))) {
268
+ if (isEditable(el) && isVisible(el)) {
269
+ return { element: el, method: 'placeholder' };
270
+ }
271
+ }
272
+ }
273
+
274
+ return null;
275
+ })()
276
+ `;
277
+
278
+ let result;
279
+ try {
280
+ result = await session.send('Runtime.evaluate', {
281
+ expression,
282
+ returnByValue: false
283
+ });
284
+ } catch (error) {
285
+ throw connectionError(error.message, 'Runtime.evaluate (findInputByLabel)');
286
+ }
287
+
288
+ if (result.exceptionDetails) {
289
+ throw new Error(`Label search error: ${result.exceptionDetails.text}`);
290
+ }
291
+
292
+ if (result.result.subtype === 'null' || result.result.type === 'undefined') {
293
+ return null;
294
+ }
295
+
296
+ // The result is an object with element and method
297
+ // We need to get the element's objectId
298
+ const objId = result.result.objectId;
299
+ const propsResult = await session.send('Runtime.getProperties', {
300
+ objectId: objId,
301
+ ownProperties: true
302
+ });
303
+
304
+ let elementObjectId = null;
305
+ let method = null;
306
+
307
+ for (const prop of propsResult.result) {
308
+ if (prop.name === 'element' && prop.value && prop.value.objectId) {
309
+ elementObjectId = prop.value.objectId;
310
+ }
311
+ if (prop.name === 'method' && prop.value) {
312
+ method = prop.value.value;
313
+ }
314
+ }
315
+
316
+ // Release the wrapper object
317
+ await releaseObject(session, objId);
318
+
319
+ if (!elementObjectId) {
320
+ return null;
321
+ }
322
+
323
+ return { objectId: elementObjectId, method };
324
+ }
325
+
326
+ /**
327
+ * Fill an input field by its label text
328
+ * @param {string} label - Label text to find
329
+ * @param {*} value - Value to fill
330
+ * @param {Object} [opts] - Options
331
+ * @returns {Promise<Object>} Fill result
332
+ */
333
+ async function fillByLabel(label, value, opts = {}) {
334
+ const { clear = true, react = false, exact = false } = opts;
335
+
336
+ const inputInfo = await findInputByLabel(label, { exact });
337
+ if (!inputInfo) {
338
+ throw elementNotFoundError(`label:"${label}"`, 0);
339
+ }
340
+
341
+ const { objectId, method: foundMethod } = inputInfo;
342
+
343
+ const editableCheck = await elementValidator.isEditable(objectId);
344
+ if (!editableCheck.editable) {
345
+ await releaseObject(session, objectId);
346
+ throw elementNotEditableError(`label:"${label}"`, editableCheck.reason);
347
+ }
348
+
349
+ try {
350
+ if (react) {
351
+ await reactInputFiller.fillByObjectId(objectId, value);
352
+ return { filled: true, label, method: 'react', foundBy: foundMethod };
353
+ }
354
+
355
+ // Scroll into view
356
+ await session.send('Runtime.callFunctionOn', {
357
+ objectId,
358
+ functionDeclaration: `function() {
359
+ this.scrollIntoView({ block: 'center', behavior: 'instant' });
360
+ }`
361
+ });
362
+
363
+ await sleep(100);
364
+
365
+ // Get element bounds for clicking
366
+ const boxResult = await session.send('Runtime.callFunctionOn', {
367
+ objectId,
368
+ functionDeclaration: `function() {
369
+ const rect = this.getBoundingClientRect();
370
+ return { x: rect.x, y: rect.y, width: rect.width, height: rect.height };
371
+ }`,
372
+ returnByValue: true
373
+ });
374
+
375
+ const box = boxResult.result.value;
376
+ const x = box.x + box.width / 2;
377
+ const y = box.y + box.height / 2;
378
+ await inputEmulator.click(x, y);
379
+
380
+ // Focus the element
381
+ await session.send('Runtime.callFunctionOn', {
382
+ objectId,
383
+ functionDeclaration: `function() { this.focus(); }`
384
+ });
385
+
386
+ if (clear) {
387
+ await inputEmulator.selectAll();
388
+ }
389
+
390
+ await inputEmulator.type(String(value));
391
+
392
+ return { filled: true, label, method: 'keyboard', foundBy: foundMethod };
393
+ } catch (e) {
394
+ await resetInputState(session);
395
+ throw e;
396
+ } finally {
397
+ await releaseObject(session, objectId);
398
+ }
399
+ }
400
+
401
+ async function execute(params) {
402
+ let { selector, ref, label, value, clear = true, react = false, exact = false } = params;
403
+
404
+ if (value === undefined) {
405
+ throw new Error('Fill requires value');
406
+ }
407
+
408
+ // Detect if selector looks like a ref (e.g., "e1", "e12", "e123")
409
+ // This allows {"fill": {"selector": "e1", "value": "..."}} to work like {"fill": {"ref": "e1", "value": "..."}}
410
+ if (!ref && selector && /^e\d+$/.test(selector)) {
411
+ ref = selector;
412
+ }
413
+
414
+ // Handle fill by ref
415
+ if (ref && ariaSnapshot) {
416
+ return fillByRef(ref, value, { clear, react });
417
+ }
418
+
419
+ // Handle fill by label
420
+ if (label) {
421
+ return fillByLabel(label, value, { clear, react, exact });
422
+ }
423
+
424
+ if (!selector) {
425
+ throw new Error('Fill requires selector, ref, or label');
426
+ }
427
+
428
+ return fillBySelector(selector, value, { clear, react });
429
+ }
430
+
431
+ async function executeBatch(params) {
432
+ if (!params || typeof params !== 'object') {
433
+ throw new Error('fillForm requires an object mapping selectors to values');
434
+ }
435
+
436
+ // Support both formats:
437
+ // Simple: {"#firstName": "John", "#lastName": "Doe"}
438
+ // Extended: {"fields": {"#firstName": "John"}, "react": true}
439
+ let fields;
440
+ let useReact = false;
441
+
442
+ if (params.fields && typeof params.fields === 'object') {
443
+ // Extended format with fields and react options
444
+ fields = params.fields;
445
+ useReact = params.react === true;
446
+ } else {
447
+ // Simple format - params is the fields object directly
448
+ fields = params;
449
+ }
450
+
451
+ const entries = Object.entries(fields);
452
+ if (entries.length === 0) {
453
+ throw new Error('fillForm requires at least one field');
454
+ }
455
+
456
+ const results = [];
457
+ const errors = [];
458
+
459
+ for (const [selector, value] of entries) {
460
+ try {
461
+ const isRef = /^e\d+$/.test(selector);
462
+
463
+ if (isRef) {
464
+ await fillByRef(selector, value, { clear: true, react: useReact });
465
+ } else {
466
+ await fillBySelector(selector, value, { clear: true, react: useReact });
467
+ }
468
+
469
+ results.push({ selector, status: 'filled', value: String(value) });
470
+ } catch (error) {
471
+ errors.push({ selector, error: error.message });
472
+ results.push({ selector, status: 'failed', error: error.message });
473
+ }
474
+ }
475
+
476
+ return {
477
+ total: entries.length,
478
+ filled: results.filter(r => r.status === 'filled').length,
479
+ failed: errors.length,
480
+ results,
481
+ errors: errors.length > 0 ? errors : undefined
482
+ };
483
+ }
484
+
485
+ return {
486
+ execute,
487
+ executeBatch
488
+ };
489
+ }