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
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keyboard Executor
|
|
3
|
+
* Type and select operations for form inputs
|
|
4
|
+
*
|
|
5
|
+
* EXPORTS:
|
|
6
|
+
* - createKeyboardExecutor(session, elementLocator, inputEmulator) → KeyboardExecutor
|
|
7
|
+
* Methods: executeType, executeSelect
|
|
8
|
+
*
|
|
9
|
+
* DEPENDENCIES:
|
|
10
|
+
* - ./element-validator.js: createElementValidator
|
|
11
|
+
* - ../utils.js: elementNotFoundError, elementNotEditableError
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { createElementValidator } from './element-validator.js';
|
|
15
|
+
import { elementNotFoundError, elementNotEditableError } from '../utils.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Create a keyboard executor for handling type and select operations
|
|
19
|
+
* @param {Object} session - CDP session
|
|
20
|
+
* @param {Object} elementLocator - Element locator instance
|
|
21
|
+
* @param {Object} inputEmulator - Input emulator instance
|
|
22
|
+
* @returns {Object} Keyboard executor interface
|
|
23
|
+
*/
|
|
24
|
+
export function createKeyboardExecutor(session, elementLocator, inputEmulator) {
|
|
25
|
+
const validator = createElementValidator(session);
|
|
26
|
+
|
|
27
|
+
async function executeType(params) {
|
|
28
|
+
const { selector, text, delay = 0 } = params;
|
|
29
|
+
|
|
30
|
+
if (!selector || text === undefined) {
|
|
31
|
+
throw new Error('Type requires selector and text');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const element = await elementLocator.findElement(selector);
|
|
35
|
+
if (!element) {
|
|
36
|
+
throw elementNotFoundError(selector, 0);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const editableCheck = await validator.isEditable(element._handle.objectId);
|
|
40
|
+
if (!editableCheck.editable) {
|
|
41
|
+
await element._handle.dispose();
|
|
42
|
+
throw elementNotEditableError(selector, editableCheck.reason);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
await element._handle.scrollIntoView({ block: 'center' });
|
|
47
|
+
await element._handle.waitForStability({ frames: 2, timeout: 500 });
|
|
48
|
+
|
|
49
|
+
await element._handle.focus();
|
|
50
|
+
|
|
51
|
+
await inputEmulator.type(String(text), { delay });
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
selector,
|
|
55
|
+
typed: String(text),
|
|
56
|
+
length: String(text).length
|
|
57
|
+
};
|
|
58
|
+
} finally {
|
|
59
|
+
await element._handle.dispose();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function executeSelect(params) {
|
|
64
|
+
let selector;
|
|
65
|
+
let start = null;
|
|
66
|
+
let end = null;
|
|
67
|
+
|
|
68
|
+
if (typeof params === 'string') {
|
|
69
|
+
selector = params;
|
|
70
|
+
} else if (params && typeof params === 'object') {
|
|
71
|
+
selector = params.selector;
|
|
72
|
+
start = params.start !== undefined ? params.start : null;
|
|
73
|
+
end = params.end !== undefined ? params.end : null;
|
|
74
|
+
} else {
|
|
75
|
+
throw new Error('Select requires a selector string or params object');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!selector) {
|
|
79
|
+
throw new Error('Select requires selector');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const element = await elementLocator.findElement(selector);
|
|
83
|
+
if (!element) {
|
|
84
|
+
throw elementNotFoundError(selector, 0);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
await element._handle.scrollIntoView({ block: 'center' });
|
|
89
|
+
await element._handle.waitForStability({ frames: 2, timeout: 500 });
|
|
90
|
+
|
|
91
|
+
await element._handle.focus();
|
|
92
|
+
|
|
93
|
+
const result = await session.send('Runtime.callFunctionOn', {
|
|
94
|
+
objectId: element._handle.objectId,
|
|
95
|
+
functionDeclaration: `function(start, end) {
|
|
96
|
+
const el = this;
|
|
97
|
+
const tagName = el.tagName.toLowerCase();
|
|
98
|
+
|
|
99
|
+
if (tagName === 'input' || tagName === 'textarea') {
|
|
100
|
+
const len = el.value.length;
|
|
101
|
+
const selStart = start !== null ? Math.min(start, len) : 0;
|
|
102
|
+
const selEnd = end !== null ? Math.min(end, len) : len;
|
|
103
|
+
|
|
104
|
+
el.focus();
|
|
105
|
+
el.setSelectionRange(selStart, selEnd);
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
success: true,
|
|
109
|
+
start: selStart,
|
|
110
|
+
end: selEnd,
|
|
111
|
+
selectedText: el.value.substring(selStart, selEnd),
|
|
112
|
+
totalLength: len
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (el.isContentEditable) {
|
|
117
|
+
const range = document.createRange();
|
|
118
|
+
const text = el.textContent || '';
|
|
119
|
+
const len = text.length;
|
|
120
|
+
const selStart = start !== null ? Math.min(start, len) : 0;
|
|
121
|
+
const selEnd = end !== null ? Math.min(end, len) : len;
|
|
122
|
+
|
|
123
|
+
let currentPos = 0;
|
|
124
|
+
let startNode = null, startOffset = 0;
|
|
125
|
+
let endNode = null, endOffset = 0;
|
|
126
|
+
|
|
127
|
+
function findPosition(node, target) {
|
|
128
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
129
|
+
const nodeLen = node.textContent.length;
|
|
130
|
+
if (!startNode && currentPos + nodeLen >= selStart) {
|
|
131
|
+
startNode = node;
|
|
132
|
+
startOffset = selStart - currentPos;
|
|
133
|
+
}
|
|
134
|
+
if (!endNode && currentPos + nodeLen >= selEnd) {
|
|
135
|
+
endNode = node;
|
|
136
|
+
endOffset = selEnd - currentPos;
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
currentPos += nodeLen;
|
|
140
|
+
} else {
|
|
141
|
+
for (const child of node.childNodes) {
|
|
142
|
+
if (findPosition(child, target)) return true;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
findPosition(el, null);
|
|
149
|
+
|
|
150
|
+
if (startNode && endNode) {
|
|
151
|
+
range.setStart(startNode, startOffset);
|
|
152
|
+
range.setEnd(endNode, endOffset);
|
|
153
|
+
|
|
154
|
+
const selection = window.getSelection();
|
|
155
|
+
selection.removeAllRanges();
|
|
156
|
+
selection.addRange(range);
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
success: true,
|
|
160
|
+
start: selStart,
|
|
161
|
+
end: selEnd,
|
|
162
|
+
selectedText: text.substring(selStart, selEnd),
|
|
163
|
+
totalLength: len
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
success: false,
|
|
170
|
+
reason: 'Element does not support text selection'
|
|
171
|
+
};
|
|
172
|
+
}`,
|
|
173
|
+
arguments: [
|
|
174
|
+
{ value: start },
|
|
175
|
+
{ value: end }
|
|
176
|
+
],
|
|
177
|
+
returnByValue: true
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const selectionResult = result.result.value;
|
|
181
|
+
|
|
182
|
+
if (!selectionResult.success) {
|
|
183
|
+
throw new Error(selectionResult.reason || 'Selection failed');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
selector,
|
|
188
|
+
start: selectionResult.start,
|
|
189
|
+
end: selectionResult.end,
|
|
190
|
+
selectedText: selectionResult.selectedText,
|
|
191
|
+
totalLength: selectionResult.totalLength
|
|
192
|
+
};
|
|
193
|
+
} finally {
|
|
194
|
+
await element._handle.dispose();
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
executeType,
|
|
200
|
+
executeSelect
|
|
201
|
+
};
|
|
202
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quad Helpers
|
|
3
|
+
* Geometry calculations for content quads (used by CDP for element positioning)
|
|
4
|
+
*
|
|
5
|
+
* EXPORTS:
|
|
6
|
+
* - calculateQuadCenter(quad) → {x, y} - Get center point of a quad
|
|
7
|
+
* - calculateQuadArea(quad) → number - Calculate area using shoelace formula
|
|
8
|
+
* - isPointInQuad(quad, x, y) → boolean - Ray casting point-in-polygon test
|
|
9
|
+
* - getLargestQuad(quads) → quad|null - Find largest quad by area
|
|
10
|
+
*
|
|
11
|
+
* DEPENDENCIES: None
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Calculate center point of a quad
|
|
16
|
+
* Quads are arrays of 8 numbers: [x1,y1, x2,y2, x3,y3, x4,y4]
|
|
17
|
+
* @param {number[]} quad - Quad coordinates
|
|
18
|
+
* @returns {{x: number, y: number}}
|
|
19
|
+
*/
|
|
20
|
+
export function calculateQuadCenter(quad) {
|
|
21
|
+
let x = 0, y = 0;
|
|
22
|
+
for (let i = 0; i < 8; i += 2) {
|
|
23
|
+
x += quad[i];
|
|
24
|
+
y += quad[i + 1];
|
|
25
|
+
}
|
|
26
|
+
return { x: x / 4, y: y / 4 };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Calculate area of a quad using shoelace formula
|
|
31
|
+
* @param {number[]} quad - Quad coordinates
|
|
32
|
+
* @returns {number}
|
|
33
|
+
*/
|
|
34
|
+
export function calculateQuadArea(quad) {
|
|
35
|
+
let area = 0;
|
|
36
|
+
for (let i = 0; i < 4; i++) {
|
|
37
|
+
const j = (i + 1) % 4;
|
|
38
|
+
area += quad[i * 2] * quad[j * 2 + 1];
|
|
39
|
+
area -= quad[j * 2] * quad[i * 2 + 1];
|
|
40
|
+
}
|
|
41
|
+
return Math.abs(area) / 2;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Check if a point is inside a quad using ray casting algorithm
|
|
46
|
+
* @param {number[]} quad - Quad coordinates
|
|
47
|
+
* @param {number} x - Point x
|
|
48
|
+
* @param {number} y - Point y
|
|
49
|
+
* @returns {boolean}
|
|
50
|
+
*/
|
|
51
|
+
export function isPointInQuad(quad, x, y) {
|
|
52
|
+
const points = [];
|
|
53
|
+
for (let i = 0; i < 8; i += 2) {
|
|
54
|
+
points.push({ x: quad[i], y: quad[i + 1] });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let inside = false;
|
|
58
|
+
for (let i = 0, j = points.length - 1; i < points.length; j = i++) {
|
|
59
|
+
const xi = points[i].x, yi = points[i].y;
|
|
60
|
+
const xj = points[j].x, yj = points[j].y;
|
|
61
|
+
|
|
62
|
+
if (((yi > y) !== (yj > y)) && (x < (xj - xi) * (y - yi) / (yj - yi) + xi)) {
|
|
63
|
+
inside = !inside;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return inside;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Get the largest quad from an array (most likely the visible content area)
|
|
71
|
+
* @param {number[][]} quads - Array of quads
|
|
72
|
+
* @returns {number[]|null}
|
|
73
|
+
*/
|
|
74
|
+
export function getLargestQuad(quads) {
|
|
75
|
+
if (!quads || quads.length === 0) return null;
|
|
76
|
+
if (quads.length === 1) return quads[0];
|
|
77
|
+
|
|
78
|
+
let largest = quads[0];
|
|
79
|
+
let largestArea = calculateQuadArea(quads[0]);
|
|
80
|
+
|
|
81
|
+
for (let i = 1; i < quads.length; i++) {
|
|
82
|
+
const area = calculateQuadArea(quads[i]);
|
|
83
|
+
if (area > largestArea) {
|
|
84
|
+
largestArea = area;
|
|
85
|
+
largest = quads[i];
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return largest;
|
|
89
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React Input Filler
|
|
3
|
+
* Handles React controlled component input filling
|
|
4
|
+
*
|
|
5
|
+
* EXPORTS:
|
|
6
|
+
* - createReactInputFiller(session) → ReactInputFiller
|
|
7
|
+
* Methods: fillByObjectId, fillBySelector
|
|
8
|
+
*
|
|
9
|
+
* DEPENDENCIES: None (uses session directly)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Create a React input filler for handling React controlled components
|
|
14
|
+
* @param {Object} session - CDP session
|
|
15
|
+
* @returns {Object} React input filler interface
|
|
16
|
+
*/
|
|
17
|
+
export function createReactInputFiller(session) {
|
|
18
|
+
if (!session) {
|
|
19
|
+
throw new Error('CDP session is required');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function fillByObjectId(objectId, value) {
|
|
23
|
+
const result = await session.send('Runtime.callFunctionOn', {
|
|
24
|
+
objectId,
|
|
25
|
+
functionDeclaration: `function(newValue) {
|
|
26
|
+
const el = this;
|
|
27
|
+
const prototype = el.tagName === 'TEXTAREA'
|
|
28
|
+
? window.HTMLTextAreaElement.prototype
|
|
29
|
+
: window.HTMLInputElement.prototype;
|
|
30
|
+
const nativeValueSetter = Object.getOwnPropertyDescriptor(prototype, 'value').set;
|
|
31
|
+
nativeValueSetter.call(el, newValue);
|
|
32
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
33
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
34
|
+
return { success: true, value: el.value };
|
|
35
|
+
}`,
|
|
36
|
+
arguments: [{ value: String(value) }],
|
|
37
|
+
returnByValue: true
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
if (result.exceptionDetails) {
|
|
41
|
+
const errorText = result.exceptionDetails.exception?.description ||
|
|
42
|
+
result.exceptionDetails.text ||
|
|
43
|
+
'Unknown error during React fill';
|
|
44
|
+
throw new Error(`React fill failed: ${errorText}`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return result.result.value;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function fillBySelector(selector, value) {
|
|
51
|
+
const result = await session.send('Runtime.evaluate', {
|
|
52
|
+
expression: `
|
|
53
|
+
(function(selector, newValue) {
|
|
54
|
+
const el = document.querySelector(selector);
|
|
55
|
+
if (!el) {
|
|
56
|
+
return { success: false, error: 'Element not found: ' + selector };
|
|
57
|
+
}
|
|
58
|
+
const prototype = el.tagName === 'TEXTAREA'
|
|
59
|
+
? window.HTMLTextAreaElement.prototype
|
|
60
|
+
: window.HTMLInputElement.prototype;
|
|
61
|
+
const descriptor = Object.getOwnPropertyDescriptor(prototype, 'value');
|
|
62
|
+
if (!descriptor || !descriptor.set) {
|
|
63
|
+
return { success: false, error: 'Cannot get native value setter' };
|
|
64
|
+
}
|
|
65
|
+
const nativeValueSetter = descriptor.set;
|
|
66
|
+
nativeValueSetter.call(el, newValue);
|
|
67
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
68
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
69
|
+
return { success: true, value: el.value };
|
|
70
|
+
})(${JSON.stringify(selector)}, ${JSON.stringify(String(value))})
|
|
71
|
+
`,
|
|
72
|
+
returnByValue: true
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
if (result.exceptionDetails) {
|
|
76
|
+
const errorText = result.exceptionDetails.exception?.description ||
|
|
77
|
+
result.exceptionDetails.text ||
|
|
78
|
+
'Unknown error during React fill';
|
|
79
|
+
throw new Error(`React fill failed: ${errorText}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const fillResult = result.result.value;
|
|
83
|
+
if (!fillResult.success) {
|
|
84
|
+
throw new Error(fillResult.error);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return fillResult;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
fillByObjectId,
|
|
92
|
+
fillBySelector
|
|
93
|
+
};
|
|
94
|
+
}
|