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,315 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Form Executors
|
|
3
|
+
* Form validation, submission, state, and assertion step executors
|
|
4
|
+
*
|
|
5
|
+
* EXPORTS:
|
|
6
|
+
* - executeFormState(formValidator, selector) → Promise<Object>
|
|
7
|
+
* - executeExtract(deps, params) → Promise<Object>
|
|
8
|
+
* - executeValidate(elementLocator, selector) → Promise<Object>
|
|
9
|
+
* - executeSubmit(elementLocator, params) → Promise<Object>
|
|
10
|
+
* - executeAssert(pageController, elementLocator, params) → Promise<Object>
|
|
11
|
+
*
|
|
12
|
+
* DEPENDENCIES:
|
|
13
|
+
* - ../utils.js: createFormValidator
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { createFormValidator } from '../utils.js';
|
|
17
|
+
|
|
18
|
+
export async function executeFormState(formValidator, selector) {
|
|
19
|
+
if (!formValidator) {
|
|
20
|
+
throw new Error('Form validator not available');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const formSelector = typeof selector === 'string' ? selector : selector.selector;
|
|
24
|
+
if (!formSelector) {
|
|
25
|
+
throw new Error('formState requires a selector');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return formValidator.getFormState(formSelector);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Execute an extract step - extract structured data from tables/lists (Feature 11)
|
|
33
|
+
* @param {Object} deps - Dependencies
|
|
34
|
+
* @param {string|Object} params - Selector or options
|
|
35
|
+
* @returns {Promise<Object>} Extracted data
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
export async function executeExtract(deps, params) {
|
|
39
|
+
const { pageController } = deps;
|
|
40
|
+
const session = pageController.session;
|
|
41
|
+
|
|
42
|
+
const selector = typeof params === 'string' ? params : params.selector;
|
|
43
|
+
const type = typeof params === 'object' ? params.type : null; // 'table' or 'list'
|
|
44
|
+
const limit = typeof params === 'object' ? params.limit : 100;
|
|
45
|
+
|
|
46
|
+
if (!selector) {
|
|
47
|
+
throw new Error('extract requires a selector');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const result = await session.send('Runtime.evaluate', {
|
|
51
|
+
expression: `
|
|
52
|
+
(function() {
|
|
53
|
+
const selector = ${JSON.stringify(selector)};
|
|
54
|
+
const typeHint = ${JSON.stringify(type)};
|
|
55
|
+
const limit = ${limit};
|
|
56
|
+
const el = document.querySelector(selector);
|
|
57
|
+
|
|
58
|
+
if (!el) {
|
|
59
|
+
return { error: 'Element not found: ' + selector };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const tagName = el.tagName.toLowerCase();
|
|
63
|
+
|
|
64
|
+
// Auto-detect type if not specified
|
|
65
|
+
let detectedType = typeHint;
|
|
66
|
+
if (!detectedType) {
|
|
67
|
+
if (tagName === 'table') {
|
|
68
|
+
detectedType = 'table';
|
|
69
|
+
} else if (tagName === 'ul' || tagName === 'ol' || el.getAttribute('role') === 'list') {
|
|
70
|
+
detectedType = 'list';
|
|
71
|
+
} else if (el.querySelector('table')) {
|
|
72
|
+
detectedType = 'table';
|
|
73
|
+
// Use the inner table
|
|
74
|
+
} else if (el.querySelector('ul, ol, [role="list"]')) {
|
|
75
|
+
detectedType = 'list';
|
|
76
|
+
} else {
|
|
77
|
+
// Try to detect based on structure
|
|
78
|
+
const rows = el.querySelectorAll('[role="row"], tr');
|
|
79
|
+
if (rows.length > 0) {
|
|
80
|
+
detectedType = 'table';
|
|
81
|
+
} else {
|
|
82
|
+
const items = el.querySelectorAll('[role="listitem"], li');
|
|
83
|
+
if (items.length > 0) {
|
|
84
|
+
detectedType = 'list';
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (detectedType === 'table') {
|
|
91
|
+
// Extract table data
|
|
92
|
+
const tableEl = tagName === 'table' ? el : el.querySelector('table');
|
|
93
|
+
if (!tableEl) {
|
|
94
|
+
return { error: 'No table found', type: 'table' };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const headers = [];
|
|
98
|
+
const rows = [];
|
|
99
|
+
|
|
100
|
+
// Get headers from thead or first row
|
|
101
|
+
const headerRow = tableEl.querySelector('thead tr') || tableEl.querySelector('tr');
|
|
102
|
+
if (headerRow) {
|
|
103
|
+
const headerCells = headerRow.querySelectorAll('th, td');
|
|
104
|
+
for (const cell of headerCells) {
|
|
105
|
+
headers.push((cell.textContent || '').trim());
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Get data rows
|
|
110
|
+
const dataRows = tableEl.querySelectorAll('tbody tr, tr');
|
|
111
|
+
let count = 0;
|
|
112
|
+
for (const row of dataRows) {
|
|
113
|
+
// Skip header row
|
|
114
|
+
if (row === headerRow) continue;
|
|
115
|
+
if (count >= limit) break;
|
|
116
|
+
|
|
117
|
+
const cells = row.querySelectorAll('td, th');
|
|
118
|
+
const rowData = [];
|
|
119
|
+
for (const cell of cells) {
|
|
120
|
+
rowData.push((cell.textContent || '').trim());
|
|
121
|
+
}
|
|
122
|
+
if (rowData.length > 0) {
|
|
123
|
+
rows.push(rowData);
|
|
124
|
+
count++;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
type: 'table',
|
|
130
|
+
headers,
|
|
131
|
+
rows,
|
|
132
|
+
rowCount: rows.length
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (detectedType === 'list') {
|
|
137
|
+
// Extract list data
|
|
138
|
+
const listEl = (tagName === 'ul' || tagName === 'ol') ? el :
|
|
139
|
+
el.querySelector('ul, ol, [role="list"]');
|
|
140
|
+
|
|
141
|
+
const items = [];
|
|
142
|
+
const listItems = listEl ?
|
|
143
|
+
listEl.querySelectorAll(':scope > li, :scope > [role="listitem"]') :
|
|
144
|
+
el.querySelectorAll('[role="listitem"], li');
|
|
145
|
+
|
|
146
|
+
let count = 0;
|
|
147
|
+
for (const item of listItems) {
|
|
148
|
+
if (count >= limit) break;
|
|
149
|
+
const text = (item.textContent || '').trim();
|
|
150
|
+
if (text) {
|
|
151
|
+
items.push(text);
|
|
152
|
+
count++;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
type: 'list',
|
|
158
|
+
items,
|
|
159
|
+
itemCount: items.length
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return { error: 'Could not detect data type. Use type: "table" or "list" option.', detectedType };
|
|
164
|
+
})()
|
|
165
|
+
`,
|
|
166
|
+
returnByValue: true
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
if (result.exceptionDetails) {
|
|
170
|
+
throw new Error('Extract error: ' + result.exceptionDetails.text);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const data = result.result.value;
|
|
174
|
+
if (data.error) {
|
|
175
|
+
throw new Error(data.error);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return data;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Execute a selectOption step - selects option(s) in a native <select> element
|
|
183
|
+
* Following Puppeteer's approach: set option.selected and dispatch events
|
|
184
|
+
*
|
|
185
|
+
* Usage:
|
|
186
|
+
* {"selectOption": {"selector": "#dropdown", "value": "optionValue"}}
|
|
187
|
+
* {"selectOption": {"selector": "#dropdown", "label": "Option Text"}}
|
|
188
|
+
* {"selectOption": {"selector": "#dropdown", "index": 2}}
|
|
189
|
+
* {"selectOption": {"selector": "#dropdown", "values": ["a", "b"]}} // multiple select
|
|
190
|
+
*/
|
|
191
|
+
|
|
192
|
+
export async function executeValidate(elementLocator, selector) {
|
|
193
|
+
const formValidator = createFormValidator(elementLocator.session, elementLocator);
|
|
194
|
+
return formValidator.validateElement(selector);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Execute a submit step - submit a form with validation error reporting
|
|
199
|
+
*/
|
|
200
|
+
|
|
201
|
+
export async function executeSubmit(elementLocator, params) {
|
|
202
|
+
const selector = typeof params === 'string' ? params : params.selector;
|
|
203
|
+
const options = typeof params === 'object' ? params : {};
|
|
204
|
+
|
|
205
|
+
const formValidator = createFormValidator(elementLocator.session, elementLocator);
|
|
206
|
+
return formValidator.submitForm(selector, options);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Execute an assert step - validates conditions about the page
|
|
211
|
+
* Supports URL assertions and text assertions
|
|
212
|
+
*/
|
|
213
|
+
|
|
214
|
+
export async function executeAssert(pageController, elementLocator, params) {
|
|
215
|
+
const result = {
|
|
216
|
+
passed: true,
|
|
217
|
+
assertions: []
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
// URL assertion
|
|
221
|
+
if (params.url) {
|
|
222
|
+
const currentUrl = await pageController.getUrl();
|
|
223
|
+
const urlAssertion = { type: 'url', actual: currentUrl };
|
|
224
|
+
|
|
225
|
+
if (params.url.contains) {
|
|
226
|
+
urlAssertion.expected = { contains: params.url.contains };
|
|
227
|
+
urlAssertion.passed = currentUrl.includes(params.url.contains);
|
|
228
|
+
} else if (params.url.equals) {
|
|
229
|
+
urlAssertion.expected = { equals: params.url.equals };
|
|
230
|
+
urlAssertion.passed = currentUrl === params.url.equals;
|
|
231
|
+
} else if (params.url.startsWith) {
|
|
232
|
+
urlAssertion.expected = { startsWith: params.url.startsWith };
|
|
233
|
+
urlAssertion.passed = currentUrl.startsWith(params.url.startsWith);
|
|
234
|
+
} else if (params.url.endsWith) {
|
|
235
|
+
urlAssertion.expected = { endsWith: params.url.endsWith };
|
|
236
|
+
urlAssertion.passed = currentUrl.endsWith(params.url.endsWith);
|
|
237
|
+
} else if (params.url.matches) {
|
|
238
|
+
urlAssertion.expected = { matches: params.url.matches };
|
|
239
|
+
const regex = new RegExp(params.url.matches);
|
|
240
|
+
urlAssertion.passed = regex.test(currentUrl);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
result.assertions.push(urlAssertion);
|
|
244
|
+
if (!urlAssertion.passed) {
|
|
245
|
+
result.passed = false;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Text assertion
|
|
250
|
+
if (params.text) {
|
|
251
|
+
const selector = params.selector || 'body';
|
|
252
|
+
const caseSensitive = params.caseSensitive !== false;
|
|
253
|
+
const textAssertion = { type: 'text', expected: params.text, selector };
|
|
254
|
+
|
|
255
|
+
try {
|
|
256
|
+
// Get the text content of the target element
|
|
257
|
+
const textResult = await pageController.session.send('Runtime.evaluate', {
|
|
258
|
+
expression: `
|
|
259
|
+
(function() {
|
|
260
|
+
const el = document.querySelector(${JSON.stringify(selector)});
|
|
261
|
+
return el ? el.textContent : null;
|
|
262
|
+
})()
|
|
263
|
+
`,
|
|
264
|
+
returnByValue: true
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
const actualText = textResult.result.value;
|
|
268
|
+
textAssertion.found = actualText !== null;
|
|
269
|
+
|
|
270
|
+
if (actualText === null) {
|
|
271
|
+
textAssertion.passed = false;
|
|
272
|
+
textAssertion.error = `Element not found: ${selector}`;
|
|
273
|
+
} else {
|
|
274
|
+
if (caseSensitive) {
|
|
275
|
+
textAssertion.passed = actualText.includes(params.text);
|
|
276
|
+
} else {
|
|
277
|
+
textAssertion.passed = actualText.toLowerCase().includes(params.text.toLowerCase());
|
|
278
|
+
}
|
|
279
|
+
textAssertion.actualLength = actualText.length;
|
|
280
|
+
}
|
|
281
|
+
} catch (e) {
|
|
282
|
+
textAssertion.passed = false;
|
|
283
|
+
textAssertion.error = e.message;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
result.assertions.push(textAssertion);
|
|
287
|
+
if (!textAssertion.passed) {
|
|
288
|
+
result.passed = false;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Throw error if assertion failed (makes the step fail)
|
|
293
|
+
if (!result.passed) {
|
|
294
|
+
const failedAssertions = result.assertions.filter(a => !a.passed);
|
|
295
|
+
const messages = failedAssertions.map(a => {
|
|
296
|
+
if (a.type === 'url') {
|
|
297
|
+
return `URL assertion failed: expected ${JSON.stringify(a.expected)}, actual "${a.actual}"`;
|
|
298
|
+
} else if (a.type === 'text') {
|
|
299
|
+
if (a.error) return `Text assertion failed: ${a.error}`;
|
|
300
|
+
return `Text assertion failed: "${a.expected}" not found in ${a.selector}`;
|
|
301
|
+
}
|
|
302
|
+
return 'Assertion failed';
|
|
303
|
+
});
|
|
304
|
+
throw new Error(messages.join('; '));
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return result;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Execute a queryAll step - runs multiple queries and returns results
|
|
312
|
+
* @param {Object} elementLocator - Element locator
|
|
313
|
+
* @param {Object} params - Object mapping names to selectors
|
|
314
|
+
* @returns {Promise<Object>} Results keyed by query name
|
|
315
|
+
*/
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Input Executors
|
|
3
|
+
* Fill and select step executors
|
|
4
|
+
*
|
|
5
|
+
* EXPORTS:
|
|
6
|
+
* - executeFill(elementLocator, inputEmulator, params) → Promise<void>
|
|
7
|
+
* - executeFillActive(pageController, inputEmulator, params) → Promise<Object>
|
|
8
|
+
* - executeSelectOption(elementLocator, params) → Promise<Object>
|
|
9
|
+
*
|
|
10
|
+
* DEPENDENCIES:
|
|
11
|
+
* - ../dom/index.js: createElementValidator, createReactInputFiller
|
|
12
|
+
* - ../utils.js: elementNotFoundError, elementNotEditableError, resetInputState
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { createElementValidator, createReactInputFiller } from '../dom/index.js';
|
|
16
|
+
import { elementNotFoundError, elementNotEditableError, resetInputState } from '../utils.js';
|
|
17
|
+
|
|
18
|
+
export async function executeFill(elementLocator, inputEmulator, params) {
|
|
19
|
+
const { selector, value, react } = params;
|
|
20
|
+
|
|
21
|
+
if (!selector || value === undefined) {
|
|
22
|
+
throw new Error('Fill requires selector and value');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const element = await elementLocator.findElement(selector);
|
|
26
|
+
if (!element) {
|
|
27
|
+
throw elementNotFoundError(selector, 0);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Validate element is editable before attempting fill
|
|
31
|
+
const validator = createElementValidator(elementLocator.session);
|
|
32
|
+
const editableCheck = await validator.isEditable(element._handle.objectId);
|
|
33
|
+
if (!editableCheck.editable) {
|
|
34
|
+
await element._handle.dispose();
|
|
35
|
+
throw elementNotEditableError(selector, editableCheck.reason);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Try fast path first - scroll to center with short stability check
|
|
39
|
+
let actionable;
|
|
40
|
+
try {
|
|
41
|
+
await element._handle.scrollIntoView({ block: 'center' });
|
|
42
|
+
// Use short stability timeout - most elements stabilize quickly
|
|
43
|
+
await element._handle.waitForStability({ frames: 2, timeout: 300 });
|
|
44
|
+
actionable = await element._handle.isActionable();
|
|
45
|
+
} catch (e) {
|
|
46
|
+
// Stability check failed, check actionability anyway
|
|
47
|
+
actionable = await element._handle.isActionable();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// If not actionable, try alternative scroll strategies
|
|
51
|
+
if (!actionable.actionable) {
|
|
52
|
+
let lastError = new Error(`Element not actionable: ${actionable.reason}`);
|
|
53
|
+
|
|
54
|
+
for (const strategy of ['end', 'start', 'nearest']) {
|
|
55
|
+
try {
|
|
56
|
+
await element._handle.scrollIntoView({ block: strategy });
|
|
57
|
+
await element._handle.waitForStability({ frames: 2, timeout: 500 });
|
|
58
|
+
actionable = await element._handle.isActionable();
|
|
59
|
+
|
|
60
|
+
if (actionable.actionable) break;
|
|
61
|
+
lastError = new Error(`Element not actionable: ${actionable.reason}`);
|
|
62
|
+
} catch (e) {
|
|
63
|
+
lastError = e;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!actionable.actionable) {
|
|
68
|
+
await element._handle.dispose();
|
|
69
|
+
await resetInputState(elementLocator.session);
|
|
70
|
+
throw lastError;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
// Use React-specific fill approach if react option is set
|
|
76
|
+
if (react) {
|
|
77
|
+
const reactFiller = createReactInputFiller(elementLocator.session);
|
|
78
|
+
await reactFiller.fillByObjectId(element._handle.objectId, value);
|
|
79
|
+
return; // Success
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Standard fill approach using keyboard events
|
|
83
|
+
const box = await element._handle.getBoundingBox();
|
|
84
|
+
const x = box.x + box.width / 2;
|
|
85
|
+
const y = box.y + box.height / 2;
|
|
86
|
+
|
|
87
|
+
// Click to focus
|
|
88
|
+
await inputEmulator.click(x, y);
|
|
89
|
+
|
|
90
|
+
// Focus element directly - more reliable than relying on click
|
|
91
|
+
await element._handle.focus();
|
|
92
|
+
|
|
93
|
+
if (params.clear !== false) {
|
|
94
|
+
await inputEmulator.selectAll();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
await inputEmulator.type(String(value));
|
|
98
|
+
} catch (e) {
|
|
99
|
+
await resetInputState(elementLocator.session);
|
|
100
|
+
throw e;
|
|
101
|
+
} finally {
|
|
102
|
+
await element._handle.dispose();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Execute a selectOption step - select option in dropdown
|
|
108
|
+
*/
|
|
109
|
+
export async function executeSelectOption(elementLocator, params) {
|
|
110
|
+
const selector = params.selector;
|
|
111
|
+
const value = params.value;
|
|
112
|
+
const label = params.label;
|
|
113
|
+
const index = params.index;
|
|
114
|
+
const values = params.values; // for multi-select
|
|
115
|
+
|
|
116
|
+
if (!selector) {
|
|
117
|
+
throw new Error('selectOption requires selector');
|
|
118
|
+
}
|
|
119
|
+
if (value === undefined && label === undefined && index === undefined && !values) {
|
|
120
|
+
throw new Error('selectOption requires value, label, index, or values');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const element = await elementLocator.findElement(selector);
|
|
124
|
+
if (!element) {
|
|
125
|
+
throw elementNotFoundError(selector, 0);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
const result = await elementLocator.session.send('Runtime.callFunctionOn', {
|
|
130
|
+
objectId: element._handle.objectId,
|
|
131
|
+
functionDeclaration: `function(matchBy, matchValue, matchValues) {
|
|
132
|
+
const el = this;
|
|
133
|
+
|
|
134
|
+
// Validate element is a select
|
|
135
|
+
if (!(el instanceof HTMLSelectElement)) {
|
|
136
|
+
return { error: 'Element is not a <select> element' };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const selectedValues = [];
|
|
140
|
+
const isMultiple = el.multiple;
|
|
141
|
+
const options = Array.from(el.options);
|
|
142
|
+
|
|
143
|
+
// Build match function based on type
|
|
144
|
+
let matchFn;
|
|
145
|
+
if (matchBy === 'value') {
|
|
146
|
+
const valuesToMatch = matchValues ? matchValues : [matchValue];
|
|
147
|
+
matchFn = (opt) => valuesToMatch.includes(opt.value);
|
|
148
|
+
} else if (matchBy === 'label') {
|
|
149
|
+
matchFn = (opt) => opt.textContent.trim() === matchValue || opt.label === matchValue;
|
|
150
|
+
} else if (matchBy === 'index') {
|
|
151
|
+
matchFn = (opt, idx) => idx === matchValue;
|
|
152
|
+
} else {
|
|
153
|
+
return { error: 'Invalid match type' };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// For single-select, deselect all first
|
|
157
|
+
if (!isMultiple) {
|
|
158
|
+
for (const option of options) {
|
|
159
|
+
option.selected = false;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Select matching options
|
|
164
|
+
let matched = false;
|
|
165
|
+
for (let i = 0; i < options.length; i++) {
|
|
166
|
+
const option = options[i];
|
|
167
|
+
if (matchFn(option, i)) {
|
|
168
|
+
option.selected = true;
|
|
169
|
+
selectedValues.push(option.value);
|
|
170
|
+
matched = true;
|
|
171
|
+
if (!isMultiple) break; // Single select stops at first match
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (!matched) {
|
|
176
|
+
return {
|
|
177
|
+
error: 'No option matched',
|
|
178
|
+
matchBy,
|
|
179
|
+
matchValue: matchValues || matchValue,
|
|
180
|
+
availableOptions: options.slice(0, 10).map(o => ({ value: o.value, label: o.textContent.trim() }))
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Dispatch events (same as Puppeteer)
|
|
185
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
186
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
success: true,
|
|
190
|
+
selected: selectedValues,
|
|
191
|
+
multiple: isMultiple
|
|
192
|
+
};
|
|
193
|
+
}`,
|
|
194
|
+
arguments: [
|
|
195
|
+
{ value: values ? 'value' : (value !== undefined ? 'value' : (label !== undefined ? 'label' : 'index')) },
|
|
196
|
+
{ value: value !== undefined ? value : (label !== undefined ? label : index) },
|
|
197
|
+
{ value: values || null }
|
|
198
|
+
],
|
|
199
|
+
returnByValue: true
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
const selectResult = result.result.value;
|
|
203
|
+
|
|
204
|
+
if (selectResult.error) {
|
|
205
|
+
const errorMsg = selectResult.error;
|
|
206
|
+
if (selectResult.availableOptions) {
|
|
207
|
+
throw new Error(`${errorMsg}. Available options: ${JSON.stringify(selectResult.availableOptions)}`);
|
|
208
|
+
}
|
|
209
|
+
throw new Error(errorMsg);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
selected: selectResult.selected,
|
|
214
|
+
multiple: selectResult.multiple
|
|
215
|
+
};
|
|
216
|
+
} finally {
|
|
217
|
+
await element._handle.dispose();
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Execute a getDom step - get raw HTML of page or element
|
|
223
|
+
* @param {Object} pageController - Page controller
|
|
224
|
+
* @param {boolean|string|Object} params - true for full page, selector string, or options object
|
|
225
|
+
* @returns {Promise<Object>} DOM content
|
|
226
|
+
*/
|
|
227
|
+
|
|
228
|
+
export async function executeFillActive(pageController, inputEmulator, params) {
|
|
229
|
+
const session = pageController.session;
|
|
230
|
+
|
|
231
|
+
// Parse params
|
|
232
|
+
const value = typeof params === 'string' ? params : params.value;
|
|
233
|
+
const clear = typeof params === 'object' ? params.clear !== false : true;
|
|
234
|
+
|
|
235
|
+
// Check if there's an active element and if it's editable
|
|
236
|
+
const checkResult = await session.send('Runtime.evaluate', {
|
|
237
|
+
expression: `(function() {
|
|
238
|
+
const el = document.activeElement;
|
|
239
|
+
if (!el || el === document.body || el === document.documentElement) {
|
|
240
|
+
return { error: 'No element is focused' };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const tag = el.tagName;
|
|
244
|
+
const isInput = tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT';
|
|
245
|
+
const isContentEditable = el.isContentEditable;
|
|
246
|
+
|
|
247
|
+
if (!isInput && !isContentEditable) {
|
|
248
|
+
return { error: 'Focused element is not editable', tag: tag };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Check if disabled or readonly
|
|
252
|
+
if (el.disabled) {
|
|
253
|
+
return { error: 'Focused element is disabled', tag: tag };
|
|
254
|
+
}
|
|
255
|
+
if (el.readOnly) {
|
|
256
|
+
return { error: 'Focused element is readonly', tag: tag };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Build selector for reporting
|
|
260
|
+
let selector = tag.toLowerCase();
|
|
261
|
+
if (el.id) {
|
|
262
|
+
selector = '#' + el.id;
|
|
263
|
+
} else if (el.name) {
|
|
264
|
+
selector = '[name="' + el.name + '"]';
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return {
|
|
268
|
+
editable: true,
|
|
269
|
+
tag: tag,
|
|
270
|
+
type: tag === 'INPUT' ? (el.type || 'text') : null,
|
|
271
|
+
selector: selector,
|
|
272
|
+
valueBefore: el.value || ''
|
|
273
|
+
};
|
|
274
|
+
})()`,
|
|
275
|
+
returnByValue: true
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
if (checkResult.exceptionDetails) {
|
|
279
|
+
throw new Error(`fillActive error: ${checkResult.exceptionDetails.text}`);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const check = checkResult.result.value;
|
|
283
|
+
if (check.error) {
|
|
284
|
+
throw new Error(check.error);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Clear existing content if requested
|
|
288
|
+
if (clear) {
|
|
289
|
+
await inputEmulator.selectAll();
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Type the new value
|
|
293
|
+
await inputEmulator.type(String(value));
|
|
294
|
+
|
|
295
|
+
return {
|
|
296
|
+
filled: true,
|
|
297
|
+
tag: check.tag,
|
|
298
|
+
type: check.type,
|
|
299
|
+
selector: check.selector,
|
|
300
|
+
valueBefore: check.valueBefore,
|
|
301
|
+
valueAfter: value
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Execute a refAt step - get or create a ref for the element at given coordinates
|
|
307
|
+
* Uses document.elementFromPoint to find the element, then assigns/retrieves a ref
|
|
308
|
+
*/
|