cdp-skill 1.0.16 → 1.0.17
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 +4 -4
- package/SKILL.md +276 -170
- package/package.json +9 -8
- package/{src → scripts}/aria/index.js +1 -1
- package/scripts/aria/role-query.js +295 -0
- package/{src → scripts}/aria.js +11 -5
- package/{src → scripts}/capture/console-capture.js +11 -9
- package/{src → scripts}/capture/screenshot-capture.js +8 -9
- package/{src → scripts}/cdp/connection.js +30 -6
- package/{src → scripts}/cdp-skill.js +7 -6
- package/{src → scripts}/diff.js +7 -6
- package/{src → scripts}/dom/LazyResolver.js +23 -12
- package/{src → scripts}/dom/actionability.js +39 -22
- package/{src → scripts}/dom/click-executor.js +90 -53
- package/{src → scripts}/dom/element-locator.js +4 -4
- package/{src → scripts}/dom/fill-executor.js +8 -4
- package/{src → scripts}/dom/input-emulator.js +47 -9
- package/{src → scripts}/dom/react-filler.js +11 -3
- package/{src → scripts}/dom/wait-executor.js +10 -2
- package/{src → scripts}/page/dialog-handler.js +7 -3
- package/{src → scripts}/page/dom-stability.js +17 -10
- package/{src → scripts}/page/page-controller.js +41 -34
- package/{src → scripts}/runner/context-helpers.js +7 -0
- package/{src → scripts}/runner/execute-browser.js +3 -118
- package/{src → scripts}/runner/execute-dynamic.js +46 -11
- package/{src → scripts}/runner/execute-form.js +6 -4
- package/{src → scripts}/runner/execute-input.js +127 -100
- package/{src → scripts}/runner/execute-interaction.js +31 -46
- package/{src → scripts}/runner/execute-navigation.js +14 -12
- package/{src → scripts}/runner/step-executors.js +28 -9
- package/{src → scripts}/runner/step-registry.js +57 -8
- package/{src → scripts}/runner/step-validator.js +13 -3
- package/{src → scripts}/tests/ExecuteInput.test.js +58 -188
- package/src/aria/role-query.js +0 -1229
- package/src/aria/snapshot.js +0 -459
- /package/{src → scripts}/aria/output-processor.js +0 -0
- /package/{src → scripts}/capture/debug-capture.js +0 -0
- /package/{src → scripts}/capture/error-aggregator.js +0 -0
- /package/{src → scripts}/capture/eval-serializer.js +0 -0
- /package/{src → scripts}/capture/index.js +0 -0
- /package/{src → scripts}/capture/network-capture.js +0 -0
- /package/{src → scripts}/capture/pdf-capture.js +0 -0
- /package/{src → scripts}/cdp/browser.js +0 -0
- /package/{src → scripts}/cdp/discovery.js +0 -0
- /package/{src → scripts}/cdp/index.js +0 -0
- /package/{src → scripts}/cdp/target-and-session.js +0 -0
- /package/{src → scripts}/constants.js +0 -0
- /package/{src → scripts}/dom/element-handle.js +0 -0
- /package/{src → scripts}/dom/element-validator.js +0 -0
- /package/{src → scripts}/dom/index.js +0 -0
- /package/{src → scripts}/dom/keyboard-executor.js +0 -0
- /package/{src → scripts}/dom/quad-helpers.js +0 -0
- /package/{src → scripts}/index.js +0 -0
- /package/{src → scripts}/page/cookie-manager.js +0 -0
- /package/{src → scripts}/page/index.js +0 -0
- /package/{src → scripts}/page/wait-utilities.js +0 -0
- /package/{src → scripts}/page/web-storage-manager.js +0 -0
- /package/{src → scripts}/runner/execute-query.js +0 -0
- /package/{src → scripts}/runner/index.js +0 -0
- /package/{src → scripts}/tests/Actionability.test.js +0 -0
- /package/{src → scripts}/tests/Aria.test.js +0 -0
- /package/{src → scripts}/tests/BrowserClient.test.js +0 -0
- /package/{src → scripts}/tests/CDPConnection.test.js +0 -0
- /package/{src → scripts}/tests/ChromeDiscovery.test.js +0 -0
- /package/{src → scripts}/tests/ClickExecutor.test.js +0 -0
- /package/{src → scripts}/tests/ConsoleCapture.test.js +0 -0
- /package/{src → scripts}/tests/ContextHelpers.test.js +0 -0
- /package/{src → scripts}/tests/CookieManager.test.js +0 -0
- /package/{src → scripts}/tests/DebugCapture.test.js +0 -0
- /package/{src → scripts}/tests/ElementHandle.test.js +0 -0
- /package/{src → scripts}/tests/ElementLocator.test.js +0 -0
- /package/{src → scripts}/tests/ErrorAggregator.test.js +0 -0
- /package/{src → scripts}/tests/EvalSerializer.test.js +0 -0
- /package/{src → scripts}/tests/ExecuteBrowser.test.js +0 -0
- /package/{src → scripts}/tests/ExecuteDynamic.test.js +0 -0
- /package/{src → scripts}/tests/ExecuteForm.test.js +0 -0
- /package/{src → scripts}/tests/ExecuteInteraction.test.js +0 -0
- /package/{src → scripts}/tests/ExecuteQuery.test.js +0 -0
- /package/{src → scripts}/tests/FillExecutor.test.js +0 -0
- /package/{src → scripts}/tests/InputEmulator.test.js +0 -0
- /package/{src → scripts}/tests/KeyboardExecutor.test.js +0 -0
- /package/{src → scripts}/tests/LazyResolver.test.js +0 -0
- /package/{src → scripts}/tests/NetworkErrorCapture.test.js +0 -0
- /package/{src → scripts}/tests/PageController.test.js +0 -0
- /package/{src → scripts}/tests/PdfCapture.test.js +0 -0
- /package/{src → scripts}/tests/ScreenshotCapture.test.js +0 -0
- /package/{src → scripts}/tests/SessionRegistry.test.js +0 -0
- /package/{src → scripts}/tests/StepValidator.test.js +0 -0
- /package/{src → scripts}/tests/TargetManager.test.js +0 -0
- /package/{src → scripts}/tests/TestRunner.test.js +0 -0
- /package/{src → scripts}/tests/WaitStrategy.test.js +0 -0
- /package/{src → scripts}/tests/WaitUtilities.test.js +0 -0
- /package/{src → scripts}/tests/WebStorageManager.test.js +0 -0
- /package/{src → scripts}/tests/integration.test.js +0 -0
- /package/{src → scripts}/types.js +0 -0
- /package/{src → scripts}/utils/backoff.js +0 -0
- /package/{src → scripts}/utils/cdp-helpers.js +0 -0
- /package/{src → scripts}/utils/devices.js +0 -0
- /package/{src → scripts}/utils/errors.js +0 -0
- /package/{src → scripts}/utils/index.js +0 -0
- /package/{src → scripts}/utils/temp.js +0 -0
- /package/{src → scripts}/utils/validators.js +0 -0
- /package/{src → scripts}/utils.js +0 -0
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import { createQueryOutputProcessor } from './output-processor.js';
|
|
2
|
+
|
|
3
|
+
// ============================================================================
|
|
4
|
+
// Role Query Executor (from RoleQueryExecutor.js)
|
|
5
|
+
// ============================================================================
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Create a role query executor for advanced role-based queries
|
|
9
|
+
* @param {Object} session - CDP session
|
|
10
|
+
* @param {Object} elementLocator - Element locator instance
|
|
11
|
+
* @returns {Object} Role query executor interface
|
|
12
|
+
*/
|
|
13
|
+
export function createRoleQueryExecutor(session, elementLocator, options = {}) {
|
|
14
|
+
const getFrameContext = options.getFrameContext || null;
|
|
15
|
+
const outputProcessor = createQueryOutputProcessor(session);
|
|
16
|
+
|
|
17
|
+
async function releaseObject(objectId) {
|
|
18
|
+
try {
|
|
19
|
+
await session.send('Runtime.releaseObject', { objectId });
|
|
20
|
+
} catch {
|
|
21
|
+
// Ignore
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Query elements by one or more roles
|
|
27
|
+
* @param {string[]} roles - Array of roles to query
|
|
28
|
+
* @param {Object} filters - Filter options
|
|
29
|
+
* @returns {Promise<Object[]>} Array of element handles
|
|
30
|
+
*/
|
|
31
|
+
async function queryByRoles(roles, filters) {
|
|
32
|
+
const { name, nameExact, nameRegex, checked, disabled, level } = filters;
|
|
33
|
+
|
|
34
|
+
// Map ARIA roles to common HTML element selectors
|
|
35
|
+
const ROLE_SELECTORS = {
|
|
36
|
+
button: ['button', 'input[type="button"]', 'input[type="submit"]', 'input[type="reset"]', '[role="button"]'],
|
|
37
|
+
textbox: ['input:not([type])', 'input[type="text"]', 'input[type="email"]', 'input[type="password"]', 'input[type="search"]', 'input[type="tel"]', 'input[type="url"]', 'textarea', '[role="textbox"]'],
|
|
38
|
+
checkbox: ['input[type="checkbox"]', '[role="checkbox"]'],
|
|
39
|
+
link: ['a[href]', '[role="link"]'],
|
|
40
|
+
heading: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', '[role="heading"]'],
|
|
41
|
+
listitem: ['li', '[role="listitem"]'],
|
|
42
|
+
option: ['option', '[role="option"]'],
|
|
43
|
+
combobox: ['select', '[role="combobox"]'],
|
|
44
|
+
radio: ['input[type="radio"]', '[role="radio"]'],
|
|
45
|
+
img: ['img[alt]', '[role="img"]'],
|
|
46
|
+
tab: ['[role="tab"]'],
|
|
47
|
+
tabpanel: ['[role="tabpanel"]'],
|
|
48
|
+
menu: ['[role="menu"]'],
|
|
49
|
+
menuitem: ['[role="menuitem"]'],
|
|
50
|
+
dialog: ['dialog', '[role="dialog"]'],
|
|
51
|
+
alert: ['[role="alert"]'],
|
|
52
|
+
navigation: ['nav', '[role="navigation"]'],
|
|
53
|
+
main: ['main', '[role="main"]'],
|
|
54
|
+
search: ['[role="search"]'],
|
|
55
|
+
form: ['form', '[role="form"]']
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// Build selectors for all requested roles
|
|
59
|
+
const allSelectors = [];
|
|
60
|
+
for (const r of roles) {
|
|
61
|
+
const selectors = ROLE_SELECTORS[r] || [`[role="${r}"]`];
|
|
62
|
+
allSelectors.push(...selectors);
|
|
63
|
+
}
|
|
64
|
+
const selectorString = allSelectors.join(', ');
|
|
65
|
+
|
|
66
|
+
// Build filter conditions
|
|
67
|
+
const nameFilter = (name !== undefined && name !== null) ? JSON.stringify(name) : null;
|
|
68
|
+
const nameExactFlag = nameExact === true;
|
|
69
|
+
const nameRegexPattern = nameRegex ? JSON.stringify(nameRegex) : null;
|
|
70
|
+
const checkedFilter = checked !== undefined ? checked : null;
|
|
71
|
+
const disabledFilter = disabled !== undefined ? disabled : null;
|
|
72
|
+
const levelFilter = level !== undefined ? level : null;
|
|
73
|
+
const rolesForLevel = roles; // For heading level detection
|
|
74
|
+
|
|
75
|
+
const expression = `
|
|
76
|
+
(function() {
|
|
77
|
+
const selectors = ${JSON.stringify(selectorString)};
|
|
78
|
+
const nameFilter = ${nameFilter};
|
|
79
|
+
const nameExact = ${nameExactFlag};
|
|
80
|
+
const nameRegex = ${nameRegexPattern};
|
|
81
|
+
const checkedFilter = ${checkedFilter !== null ? checkedFilter : 'null'};
|
|
82
|
+
const disabledFilter = ${disabledFilter !== null ? disabledFilter : 'null'};
|
|
83
|
+
const levelFilter = ${levelFilter !== null ? levelFilter : 'null'};
|
|
84
|
+
const rolesForLevel = ${JSON.stringify(rolesForLevel)};
|
|
85
|
+
|
|
86
|
+
const elements = Array.from(document.querySelectorAll(selectors));
|
|
87
|
+
|
|
88
|
+
return elements.filter(el => {
|
|
89
|
+
// Filter by accessible name if specified
|
|
90
|
+
if (nameFilter !== null || nameRegex !== null) {
|
|
91
|
+
const accessibleName = (
|
|
92
|
+
el.getAttribute('aria-label') ||
|
|
93
|
+
el.textContent?.trim() ||
|
|
94
|
+
el.getAttribute('title') ||
|
|
95
|
+
el.getAttribute('placeholder') ||
|
|
96
|
+
el.value ||
|
|
97
|
+
''
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
if (nameFilter !== null) {
|
|
101
|
+
if (nameExact) {
|
|
102
|
+
// Exact match
|
|
103
|
+
if (accessibleName !== nameFilter) return false;
|
|
104
|
+
} else {
|
|
105
|
+
// Contains match (case-insensitive)
|
|
106
|
+
if (!accessibleName.toLowerCase().includes(nameFilter.toLowerCase())) return false;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (nameRegex !== null) {
|
|
111
|
+
// Regex match
|
|
112
|
+
try {
|
|
113
|
+
const regex = new RegExp(nameRegex);
|
|
114
|
+
if (!regex.test(accessibleName)) return false;
|
|
115
|
+
} catch (e) {
|
|
116
|
+
// Invalid regex, skip filter
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Filter by checked state if specified
|
|
122
|
+
if (checkedFilter !== null) {
|
|
123
|
+
const isChecked = el.checked === true || el.getAttribute('aria-checked') === 'true';
|
|
124
|
+
if (isChecked !== checkedFilter) return false;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Filter by disabled state if specified
|
|
128
|
+
if (disabledFilter !== null) {
|
|
129
|
+
const isDisabled = el.disabled === true || el.getAttribute('aria-disabled') === 'true';
|
|
130
|
+
if (isDisabled !== disabledFilter) return false;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Filter by heading level if specified
|
|
134
|
+
if (levelFilter !== null && rolesForLevel.includes('heading')) {
|
|
135
|
+
const tagName = el.tagName.toLowerCase();
|
|
136
|
+
let headingLevel = null;
|
|
137
|
+
|
|
138
|
+
// Check aria-level first
|
|
139
|
+
const ariaLevel = el.getAttribute('aria-level');
|
|
140
|
+
if (ariaLevel) {
|
|
141
|
+
headingLevel = parseInt(ariaLevel, 10);
|
|
142
|
+
} else if (tagName.match(/^h[1-6]$/)) {
|
|
143
|
+
// Extract level from h1-h6 tag
|
|
144
|
+
headingLevel = parseInt(tagName.charAt(1), 10);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (headingLevel !== levelFilter) return false;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return true;
|
|
151
|
+
});
|
|
152
|
+
})()
|
|
153
|
+
`;
|
|
154
|
+
|
|
155
|
+
let result;
|
|
156
|
+
try {
|
|
157
|
+
const evalArgs = { expression, returnByValue: false };
|
|
158
|
+
if (getFrameContext) {
|
|
159
|
+
const contextId = getFrameContext();
|
|
160
|
+
if (contextId) evalArgs.contextId = contextId;
|
|
161
|
+
}
|
|
162
|
+
result = await session.send('Runtime.evaluate', evalArgs);
|
|
163
|
+
} catch (error) {
|
|
164
|
+
throw new Error(`Role query error: ${error.message}`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (result.exceptionDetails) {
|
|
168
|
+
throw new Error(`Role query error: ${result.exceptionDetails.text}`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (!result.result.objectId) return [];
|
|
172
|
+
|
|
173
|
+
const arrayObjectId = result.result.objectId;
|
|
174
|
+
let props;
|
|
175
|
+
try {
|
|
176
|
+
props = await session.send('Runtime.getProperties', {
|
|
177
|
+
objectId: arrayObjectId,
|
|
178
|
+
ownProperties: true
|
|
179
|
+
});
|
|
180
|
+
} catch (error) {
|
|
181
|
+
await releaseObject(arrayObjectId);
|
|
182
|
+
throw new Error(`Role query error: ${error.message}`);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const { createElementHandle } = await import('../dom/element-handle.js');
|
|
186
|
+
const elements = props.result
|
|
187
|
+
.filter(p => /^\d+$/.test(p.name) && p.value && p.value.objectId)
|
|
188
|
+
.map(p => createElementHandle(session, p.value.objectId, {
|
|
189
|
+
selector: `[role="${roles.join('|')}"]`
|
|
190
|
+
}));
|
|
191
|
+
|
|
192
|
+
await releaseObject(arrayObjectId);
|
|
193
|
+
return elements;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Execute a role-based query with advanced options
|
|
198
|
+
* @param {Object} params - Query parameters
|
|
199
|
+
* @returns {Promise<Object>} Query results
|
|
200
|
+
*/
|
|
201
|
+
async function execute(params) {
|
|
202
|
+
const {
|
|
203
|
+
role,
|
|
204
|
+
name,
|
|
205
|
+
nameExact,
|
|
206
|
+
nameRegex,
|
|
207
|
+
checked,
|
|
208
|
+
disabled,
|
|
209
|
+
level,
|
|
210
|
+
limit = 10,
|
|
211
|
+
output = 'text',
|
|
212
|
+
clean = false,
|
|
213
|
+
metadata = false,
|
|
214
|
+
countOnly = false,
|
|
215
|
+
refs = false
|
|
216
|
+
} = params;
|
|
217
|
+
|
|
218
|
+
// Handle compound roles
|
|
219
|
+
const roles = Array.isArray(role) ? role : [role];
|
|
220
|
+
|
|
221
|
+
// Build query expression
|
|
222
|
+
const elements = await queryByRoles(roles, {
|
|
223
|
+
name,
|
|
224
|
+
nameExact,
|
|
225
|
+
nameRegex,
|
|
226
|
+
checked,
|
|
227
|
+
disabled,
|
|
228
|
+
level
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// Count-only mode
|
|
232
|
+
if (countOnly) {
|
|
233
|
+
// Dispose all elements
|
|
234
|
+
for (const el of elements) {
|
|
235
|
+
try { await el.dispose(); } catch { /* ignore */ }
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
role: roles.length === 1 ? roles[0] : roles,
|
|
240
|
+
total: elements.length,
|
|
241
|
+
countOnly: true
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const results = [];
|
|
246
|
+
const count = Math.min(elements.length, limit);
|
|
247
|
+
|
|
248
|
+
for (let i = 0; i < count; i++) {
|
|
249
|
+
const el = elements[i];
|
|
250
|
+
try {
|
|
251
|
+
const resultItem = {
|
|
252
|
+
index: i + 1,
|
|
253
|
+
value: await outputProcessor.processOutput(el, output, { clean })
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
// Add element metadata if requested
|
|
257
|
+
if (metadata) {
|
|
258
|
+
resultItem.metadata = await outputProcessor.getElementMetadata(el);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Add element ref if requested
|
|
262
|
+
if (refs) {
|
|
263
|
+
resultItem.ref = el.objectId;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
results.push(resultItem);
|
|
267
|
+
} catch (e) {
|
|
268
|
+
results.push({ index: i + 1, value: null, error: e.message });
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Dispose all elements
|
|
273
|
+
for (const el of elements) {
|
|
274
|
+
try { await el.dispose(); } catch { /* ignore */ }
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return {
|
|
278
|
+
role: roles.length === 1 ? roles[0] : roles,
|
|
279
|
+
name: name || null,
|
|
280
|
+
nameExact: nameExact || false,
|
|
281
|
+
nameRegex: nameRegex || null,
|
|
282
|
+
checked: checked !== undefined ? checked : null,
|
|
283
|
+
disabled: disabled !== undefined ? disabled : null,
|
|
284
|
+
level: level !== undefined ? level : null,
|
|
285
|
+
total: elements.length,
|
|
286
|
+
showing: count,
|
|
287
|
+
results
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return {
|
|
292
|
+
execute,
|
|
293
|
+
queryByRoles
|
|
294
|
+
};
|
|
295
|
+
}
|
package/{src → scripts}/aria.js
RENAMED
|
@@ -475,7 +475,7 @@ export function createRoleQueryExecutor(session, elementLocator, options = {}) {
|
|
|
475
475
|
// The snapshot script runs entirely in the browser context
|
|
476
476
|
const SNAPSHOT_SCRIPT = `
|
|
477
477
|
(function generateAriaSnapshot(rootSelector, options) {
|
|
478
|
-
const { mode = 'ai', maxDepth = 50, maxElements = 0, includeText = false, includeFrames = false, viewportOnly = false, pierceShadow = false, preserveRefs = false, since = null, internal = false, frameIdentifier = 'f0' } = options || {};
|
|
478
|
+
const { mode = 'ai', maxDepth = 50, maxElements = 0, maxNameLength = 150, includeText = false, includeFrames = false, viewportOnly = false, pierceShadow = false, preserveRefs = false, since = null, internal = false, frameIdentifier = 'f0' } = options || {};
|
|
479
479
|
|
|
480
480
|
// Viewport dimensions for viewport-only mode
|
|
481
481
|
const viewportWidth = window.innerWidth;
|
|
@@ -628,7 +628,7 @@ const SNAPSHOT_SCRIPT = `
|
|
|
628
628
|
'searchbox', 'slider', 'spinbutton', 'textbox', 'tree'];
|
|
629
629
|
|
|
630
630
|
// Interactable roles for AI mode
|
|
631
|
-
const INTERACTABLE_ROLES = ['button', 'checkbox', 'combobox', 'link', 'listbox', 'menuitem',
|
|
631
|
+
const INTERACTABLE_ROLES = ['button', 'checkbox', 'combobox', 'heading', 'link', 'listbox', 'menuitem',
|
|
632
632
|
'menuitemcheckbox', 'menuitemradio', 'option', 'radio', 'searchbox', 'slider', 'spinbutton',
|
|
633
633
|
'switch', 'tab', 'textbox', 'treeitem'];
|
|
634
634
|
|
|
@@ -766,6 +766,11 @@ const SNAPSHOT_SCRIPT = `
|
|
|
766
766
|
return text.replace(/\\s+/g, ' ').trim();
|
|
767
767
|
}
|
|
768
768
|
|
|
769
|
+
function truncateName(text) {
|
|
770
|
+
if (!text || maxNameLength <= 0 || text.length <= maxNameLength) return text;
|
|
771
|
+
return text.substring(0, maxNameLength) + '…';
|
|
772
|
+
}
|
|
773
|
+
|
|
769
774
|
function getCheckedState(el, role) {
|
|
770
775
|
if (!CHECKED_ROLES.includes(role)) return undefined;
|
|
771
776
|
|
|
@@ -1017,7 +1022,7 @@ const SNAPSHOT_SCRIPT = `
|
|
|
1017
1022
|
if (mode === 'ai' && !visible) return null;
|
|
1018
1023
|
|
|
1019
1024
|
const role = getAriaRole(el);
|
|
1020
|
-
const name = getAccessibleName(el);
|
|
1025
|
+
const name = truncateName(getAccessibleName(el));
|
|
1021
1026
|
|
|
1022
1027
|
// Skip elements without semantic meaning
|
|
1023
1028
|
if (!role && mode === 'ai') {
|
|
@@ -1415,6 +1420,7 @@ export function createAriaSnapshot(session, options = {}) {
|
|
|
1415
1420
|
* @param {string} options.detail - Detail level: 'summary', 'interactive', or 'full' (default: 'full')
|
|
1416
1421
|
* @param {number} options.maxDepth - Maximum tree depth (default: 50)
|
|
1417
1422
|
* @param {number} options.maxElements - Maximum elements to include (default: unlimited)
|
|
1423
|
+
* @param {number} options.maxNameLength - Truncate accessible names longer than this (default: 150, 0 to disable)
|
|
1418
1424
|
* @param {boolean} options.includeText - Include static text nodes in output (default: false for ai mode)
|
|
1419
1425
|
* @param {boolean} options.includeFrames - Include same-origin iframe content (default: false)
|
|
1420
1426
|
* @param {boolean} options.viewportOnly - Only include elements visible in viewport (default: false)
|
|
@@ -1424,13 +1430,13 @@ export function createAriaSnapshot(session, options = {}) {
|
|
|
1424
1430
|
* @returns {Promise<Object>} Snapshot result with tree, yaml, refs, and snapshotId
|
|
1425
1431
|
*/
|
|
1426
1432
|
async function generate(options = {}) {
|
|
1427
|
-
const { root = null, mode = 'ai', detail = 'full', maxDepth = 50, maxElements = 0, includeText = false, includeFrames = false, viewportOnly = false, pierceShadow = false, preserveRefs = false, since = null, internal = false } = options;
|
|
1433
|
+
const { root = null, mode = 'ai', detail = 'full', maxDepth = 50, maxElements = 0, maxNameLength = 150, includeText = false, includeFrames = false, viewportOnly = false, pierceShadow = false, preserveRefs = false, since = null, internal = false } = options;
|
|
1428
1434
|
|
|
1429
1435
|
// Get frame identifier for ref generation (f0 for main frame, f1, f2, etc. for iframes)
|
|
1430
1436
|
const frameIdentifier = getFrameIdentifier ? await getFrameIdentifier() : 'f0';
|
|
1431
1437
|
|
|
1432
1438
|
const evalArgs = {
|
|
1433
|
-
expression: `(${SNAPSHOT_SCRIPT})(${JSON.stringify(root)}, ${JSON.stringify({ mode, detail, maxDepth, maxElements, includeText, includeFrames, viewportOnly, pierceShadow, preserveRefs, since, internal, frameIdentifier })})`,
|
|
1439
|
+
expression: `(${SNAPSHOT_SCRIPT})(${JSON.stringify(root)}, ${JSON.stringify({ mode, detail, maxDepth, maxElements, maxNameLength, includeText, includeFrames, viewportOnly, pierceShadow, preserveRefs, since, internal, frameIdentifier })})`,
|
|
1434
1440
|
returnByValue: true,
|
|
1435
1441
|
awaitPromise: false
|
|
1436
1442
|
};
|
|
@@ -122,18 +122,20 @@ export function createConsoleCapture(session, options = {}) {
|
|
|
122
122
|
async function stopCapture() {
|
|
123
123
|
if (!capturing) return;
|
|
124
124
|
|
|
125
|
-
|
|
126
|
-
|
|
125
|
+
try {
|
|
126
|
+
if (handlers.consoleAPICalled) {
|
|
127
|
+
session.off('Runtime.consoleAPICalled', handlers.consoleAPICalled);
|
|
128
|
+
}
|
|
129
|
+
if (handlers.exceptionThrown) {
|
|
130
|
+
session.off('Runtime.exceptionThrown', handlers.exceptionThrown);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
await session.send('Runtime.disable');
|
|
134
|
+
} finally {
|
|
127
135
|
handlers.consoleAPICalled = null;
|
|
128
|
-
}
|
|
129
|
-
if (handlers.exceptionThrown) {
|
|
130
|
-
session.off('Runtime.exceptionThrown', handlers.exceptionThrown);
|
|
131
136
|
handlers.exceptionThrown = null;
|
|
137
|
+
capturing = false;
|
|
132
138
|
}
|
|
133
|
-
|
|
134
|
-
await session.send('Runtime.disable');
|
|
135
|
-
|
|
136
|
-
capturing = false;
|
|
137
139
|
}
|
|
138
140
|
|
|
139
141
|
/**
|
|
@@ -86,14 +86,15 @@ export function createScreenshotCapture(session, options = {}) {
|
|
|
86
86
|
};
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
89
|
+
try {
|
|
90
|
+
const result = await session.send('Page.captureScreenshot', params);
|
|
91
|
+
return Buffer.from(result.data, 'base64');
|
|
92
|
+
} finally {
|
|
93
|
+
// Reset background override even if screenshot fails
|
|
94
|
+
if (captureOptions.omitBackground) {
|
|
95
|
+
await session.send('Emulation.setDefaultBackgroundColorOverride');
|
|
96
|
+
}
|
|
94
97
|
}
|
|
95
|
-
|
|
96
|
-
return Buffer.from(result.data, 'base64');
|
|
97
98
|
}
|
|
98
99
|
|
|
99
100
|
/**
|
|
@@ -226,7 +227,6 @@ export function createScreenshotCapture(session, options = {}) {
|
|
|
226
227
|
*/
|
|
227
228
|
async function captureToFile(filePath, captureOptions = {}, elementLocator = null) {
|
|
228
229
|
let buffer;
|
|
229
|
-
let elementBox = null;
|
|
230
230
|
|
|
231
231
|
// Support element screenshot via selector
|
|
232
232
|
if (captureOptions.selector && elementLocator) {
|
|
@@ -241,7 +241,6 @@ export function createScreenshotCapture(session, options = {}) {
|
|
|
241
241
|
throw new Error(`Element has no visible dimensions: ${captureOptions.selector}`);
|
|
242
242
|
}
|
|
243
243
|
|
|
244
|
-
elementBox = box;
|
|
245
244
|
buffer = await captureElement(box, captureOptions);
|
|
246
245
|
} else if (captureOptions.fullPage) {
|
|
247
246
|
buffer = await captureFullPage(captureOptions);
|
|
@@ -121,6 +121,7 @@ export function createConnection(wsUrl, options = {}) {
|
|
|
121
121
|
connected = false;
|
|
122
122
|
connecting = false;
|
|
123
123
|
rejectPendingCommands('Connection closed');
|
|
124
|
+
emit('__connection_closed');
|
|
124
125
|
|
|
125
126
|
if (wasConnected && !intentionalClose && autoReconnect) {
|
|
126
127
|
attemptReconnect();
|
|
@@ -164,13 +165,24 @@ export function createConnection(wsUrl, options = {}) {
|
|
|
164
165
|
|
|
165
166
|
function doReconnect() {
|
|
166
167
|
return new Promise((resolve, reject) => {
|
|
167
|
-
|
|
168
|
-
|
|
168
|
+
// Close old WebSocket if still open to prevent stale event handlers
|
|
169
|
+
if (ws) {
|
|
170
|
+
try { ws.close(); } catch { /* already closed */ }
|
|
171
|
+
ws = null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const newWs = new WebSocket(wsUrl);
|
|
175
|
+
|
|
176
|
+
newWs.addEventListener('open', () => {
|
|
177
|
+
ws = newWs;
|
|
169
178
|
connected = true;
|
|
170
179
|
setupWebSocketListeners();
|
|
171
180
|
resolve();
|
|
172
181
|
});
|
|
173
|
-
|
|
182
|
+
|
|
183
|
+
newWs.addEventListener('error', (event) => {
|
|
184
|
+
// Don't assign to ws if connection failed
|
|
185
|
+
try { newWs.close(); } catch { /* already closing */ }
|
|
174
186
|
reject(new Error(`CDP reconnection error: ${event.message || 'Connection failed'}`));
|
|
175
187
|
});
|
|
176
188
|
});
|
|
@@ -206,6 +218,7 @@ export function createConnection(wsUrl, options = {}) {
|
|
|
206
218
|
connected = false;
|
|
207
219
|
connecting = false;
|
|
208
220
|
rejectPendingCommands('Connection closed');
|
|
221
|
+
emit('__connection_closed');
|
|
209
222
|
|
|
210
223
|
if (wasConnected && !intentionalClose && autoReconnect) {
|
|
211
224
|
attemptReconnect();
|
|
@@ -300,20 +313,31 @@ export function createConnection(wsUrl, options = {}) {
|
|
|
300
313
|
*/
|
|
301
314
|
function waitForEvent(event, predicate = () => true, timeout = 30000) {
|
|
302
315
|
return new Promise((resolve, reject) => {
|
|
303
|
-
|
|
316
|
+
function cleanup() {
|
|
317
|
+
clearTimeout(timer);
|
|
304
318
|
off(event, handler);
|
|
319
|
+
off('__connection_closed', closeHandler);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const timer = setTimeout(() => {
|
|
323
|
+
cleanup();
|
|
305
324
|
reject(new Error(`Timeout waiting for event: ${event}`));
|
|
306
325
|
}, timeout);
|
|
307
326
|
|
|
308
327
|
const handler = (params) => {
|
|
309
328
|
if (predicate(params)) {
|
|
310
|
-
|
|
311
|
-
off(event, handler);
|
|
329
|
+
cleanup();
|
|
312
330
|
resolve(params);
|
|
313
331
|
}
|
|
314
332
|
};
|
|
315
333
|
|
|
334
|
+
const closeHandler = () => {
|
|
335
|
+
cleanup();
|
|
336
|
+
reject(new Error(`Connection closed while waiting for event: ${event}`));
|
|
337
|
+
};
|
|
338
|
+
|
|
316
339
|
on(event, handler);
|
|
340
|
+
on('__connection_closed', closeHandler);
|
|
317
341
|
});
|
|
318
342
|
}
|
|
319
343
|
|
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
* or reads from stdin (fallback).
|
|
7
7
|
*
|
|
8
8
|
* Usage:
|
|
9
|
-
* node
|
|
10
|
-
* echo '{"steps":[...]}' | node
|
|
11
|
-
* node
|
|
9
|
+
* node scripts/cdp-skill.js '{"steps":[{"goto":"https://google.com"}]}'
|
|
10
|
+
* echo '{"steps":[...]}' | node scripts/cdp-skill.js
|
|
11
|
+
* node scripts/cdp-skill.js --debug '{"steps":[...]}' # Enable debug logging
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { createBrowser, getChromeStatus } from './cdp/index.js';
|
|
@@ -62,7 +62,8 @@ function generateDebugFilename(steps, status, tabId) {
|
|
|
62
62
|
const actionKeys = ['goto', 'click', 'fill', 'type', 'press', 'scroll', 'snapshot',
|
|
63
63
|
'query', 'hover', 'wait', 'sleep', 'pageFunction', 'newTab', 'closeTab',
|
|
64
64
|
'selectOption', 'select', 'viewport', 'cookies', 'back', 'forward', 'drag',
|
|
65
|
-
'frame', 'elementsAt', 'extract', 'formState', 'assert', 'validate', 'submit'
|
|
65
|
+
'frame', 'elementsAt', 'extract', 'formState', 'assert', 'validate', 'submit',
|
|
66
|
+
'upload'];
|
|
66
67
|
for (const key of actionKeys) {
|
|
67
68
|
if (step[key] !== undefined) return key;
|
|
68
69
|
}
|
|
@@ -507,7 +508,7 @@ async function main() {
|
|
|
507
508
|
|
|
508
509
|
// Extract top-level fields
|
|
509
510
|
const tab = json.tab || null;
|
|
510
|
-
const timeout = json.timeout
|
|
511
|
+
const timeout = json.timeout ?? 30000;
|
|
511
512
|
let host = 'localhost';
|
|
512
513
|
let port = 9222;
|
|
513
514
|
let headless = false;
|
|
@@ -689,7 +690,7 @@ async function main() {
|
|
|
689
690
|
const frameContextProvider = () => pageController.getFrameContext();
|
|
690
691
|
const frameIdentifierProvider = () => pageController.getFrameIdentifier();
|
|
691
692
|
const elementLocator = createElementLocator(session, { getFrameContext: frameContextProvider });
|
|
692
|
-
const inputEmulator = createInputEmulator(session);
|
|
693
|
+
const inputEmulator = createInputEmulator(session, { getFrameContext: frameContextProvider });
|
|
693
694
|
const screenshotCapture = createScreenshotCapture(session);
|
|
694
695
|
const consoleCapture = createConsoleCapture(session);
|
|
695
696
|
const pdfCapture = createPdfCapture(session);
|
package/{src → scripts}/diff.js
RENAMED
|
@@ -19,9 +19,10 @@ export function createSnapshotDiffer() {
|
|
|
19
19
|
|
|
20
20
|
const lines = yaml.split('\n');
|
|
21
21
|
for (const line of lines) {
|
|
22
|
-
// Match lines like: - button "Submit" [ref=
|
|
23
|
-
// Or: - heading "Title" [level=1] [ref=
|
|
24
|
-
|
|
22
|
+
// Match lines like: - button "Submit" [ref=f0s1e1]
|
|
23
|
+
// Or: - heading "Title" [level=1] [ref=f0s2e3]
|
|
24
|
+
// Ref format: f{frameId}s{snapshotId}e{elementNumber} where frameId can be a number or [name]
|
|
25
|
+
const refMatch = line.match(/\[ref=(f(?:\d+|\[[^\]]+\])s\d+e\d+)\]/);
|
|
25
26
|
if (refMatch) {
|
|
26
27
|
const ref = refMatch[1];
|
|
27
28
|
|
|
@@ -126,16 +127,16 @@ export function createSnapshotDiffer() {
|
|
|
126
127
|
}
|
|
127
128
|
|
|
128
129
|
/**
|
|
129
|
-
* Extract ref from a line like "- link \"text\" [ref=
|
|
130
|
+
* Extract ref from a line like "- link \"text\" [ref=f0s1e42]"
|
|
130
131
|
*/
|
|
131
132
|
function extractRef(line) {
|
|
132
|
-
const match = line.match(/\[ref=(s\d+e\d+)\]/);
|
|
133
|
+
const match = line.match(/\[ref=(f(?:\d+|\[[^\]]+\])s\d+e\d+)\]/);
|
|
133
134
|
return match ? match[1] : null;
|
|
134
135
|
}
|
|
135
136
|
|
|
136
137
|
/**
|
|
137
138
|
* Format refs as a compact list
|
|
138
|
-
* e.g., [
|
|
139
|
+
* e.g., [f0s1e1, f0s1e2, f0s1e5] -> "f0s1e1, f0s1e2, f0s1e5"
|
|
139
140
|
*/
|
|
140
141
|
function formatRefs(lines, maxRefs = 10) {
|
|
141
142
|
const refs = lines.map(extractRef).filter(Boolean);
|
|
@@ -417,16 +417,20 @@ export function createLazyResolver(session, options = {}) {
|
|
|
417
417
|
return null;
|
|
418
418
|
}
|
|
419
419
|
|
|
420
|
-
//
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
const result = searchInRoot(host.shadowRoot, []);
|
|
420
|
+
// Walk the tree to find shadow roots (avoids querySelectorAll('*'))
|
|
421
|
+
function walkForShadowRoots(node) {
|
|
422
|
+
if (node.shadowRoot) {
|
|
423
|
+
const result = searchInRoot(node.shadowRoot, []);
|
|
425
424
|
if (result) return result;
|
|
426
425
|
}
|
|
426
|
+
const children = node.children || [];
|
|
427
|
+
for (const child of children) {
|
|
428
|
+
const result = walkForShadowRoots(child);
|
|
429
|
+
if (result) return result;
|
|
430
|
+
}
|
|
431
|
+
return null;
|
|
427
432
|
}
|
|
428
|
-
|
|
429
|
-
return null;
|
|
433
|
+
return walkForShadowRoots(document.body);
|
|
430
434
|
})()
|
|
431
435
|
`;
|
|
432
436
|
|
|
@@ -474,20 +478,27 @@ export function createLazyResolver(session, options = {}) {
|
|
|
474
478
|
return rect.width > 0 && rect.height > 0;
|
|
475
479
|
}
|
|
476
480
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
if (host.shadowRoot) {
|
|
481
|
+
function walkForShadowRoots(node) {
|
|
482
|
+
if (node.shadowRoot) {
|
|
480
483
|
const selectors = ROLE_SELECTORS[targetRole] || ['[role="' + targetRole + '"]'];
|
|
481
|
-
const elements =
|
|
484
|
+
const elements = node.shadowRoot.querySelectorAll(selectors.join(', '));
|
|
482
485
|
for (const el of elements) {
|
|
483
486
|
if (!isVisible(el)) continue;
|
|
484
487
|
const elName = getAccessibleName(el);
|
|
485
488
|
if (targetName && !elName.toLowerCase().includes(targetName.toLowerCase())) continue;
|
|
486
489
|
return el;
|
|
487
490
|
}
|
|
491
|
+
const found = walkForShadowRoots(node.shadowRoot);
|
|
492
|
+
if (found) return found;
|
|
493
|
+
}
|
|
494
|
+
const children = node.children || [];
|
|
495
|
+
for (const child of children) {
|
|
496
|
+
const found = walkForShadowRoots(child);
|
|
497
|
+
if (found) return found;
|
|
488
498
|
}
|
|
499
|
+
return null;
|
|
489
500
|
}
|
|
490
|
-
return
|
|
501
|
+
return walkForShadowRoots(document.body);
|
|
491
502
|
})()
|
|
492
503
|
`;
|
|
493
504
|
|