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,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
|
+
}
|