cdp-skill 1.0.2 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/README.md +3 -0
  2. package/SKILL.md +34 -5
  3. package/package.json +2 -1
  4. package/src/capture/console-capture.js +241 -0
  5. package/src/capture/debug-capture.js +144 -0
  6. package/src/capture/error-aggregator.js +151 -0
  7. package/src/capture/eval-serializer.js +320 -0
  8. package/src/capture/index.js +40 -0
  9. package/src/capture/network-capture.js +211 -0
  10. package/src/capture/pdf-capture.js +256 -0
  11. package/src/capture/screenshot-capture.js +325 -0
  12. package/src/cdp/browser.js +569 -0
  13. package/src/cdp/connection.js +369 -0
  14. package/src/cdp/discovery.js +138 -0
  15. package/src/cdp/index.js +29 -0
  16. package/src/cdp/target-and-session.js +439 -0
  17. package/src/cdp-skill.js +25 -11
  18. package/src/constants.js +79 -0
  19. package/src/dom/actionability.js +638 -0
  20. package/src/dom/click-executor.js +923 -0
  21. package/src/dom/element-handle.js +496 -0
  22. package/src/dom/element-locator.js +475 -0
  23. package/src/dom/element-validator.js +120 -0
  24. package/src/dom/fill-executor.js +489 -0
  25. package/src/dom/index.js +248 -0
  26. package/src/dom/input-emulator.js +406 -0
  27. package/src/dom/keyboard-executor.js +202 -0
  28. package/src/dom/quad-helpers.js +89 -0
  29. package/src/dom/react-filler.js +94 -0
  30. package/src/dom/wait-executor.js +423 -0
  31. package/src/index.js +6 -6
  32. package/src/page/cookie-manager.js +202 -0
  33. package/src/page/dom-stability.js +181 -0
  34. package/src/page/index.js +36 -0
  35. package/src/{page.js → page/page-controller.js} +109 -839
  36. package/src/page/wait-utilities.js +302 -0
  37. package/src/page/web-storage-manager.js +108 -0
  38. package/src/runner/context-helpers.js +224 -0
  39. package/src/runner/execute-browser.js +518 -0
  40. package/src/runner/execute-form.js +315 -0
  41. package/src/runner/execute-input.js +308 -0
  42. package/src/runner/execute-interaction.js +672 -0
  43. package/src/runner/execute-navigation.js +180 -0
  44. package/src/runner/execute-query.js +771 -0
  45. package/src/runner/index.js +51 -0
  46. package/src/runner/step-executors.js +421 -0
  47. package/src/runner/step-validator.js +641 -0
  48. package/src/tests/Actionability.test.js +613 -0
  49. package/src/tests/BrowserClient.test.js +1 -1
  50. package/src/tests/ChromeDiscovery.test.js +1 -1
  51. package/src/tests/ClickExecutor.test.js +554 -0
  52. package/src/tests/ConsoleCapture.test.js +1 -1
  53. package/src/tests/ContextHelpers.test.js +453 -0
  54. package/src/tests/CookieManager.test.js +450 -0
  55. package/src/tests/DebugCapture.test.js +307 -0
  56. package/src/tests/ElementHandle.test.js +1 -1
  57. package/src/tests/ElementLocator.test.js +1 -1
  58. package/src/tests/ErrorAggregator.test.js +1 -1
  59. package/src/tests/EvalSerializer.test.js +391 -0
  60. package/src/tests/FillExecutor.test.js +611 -0
  61. package/src/tests/InputEmulator.test.js +1 -1
  62. package/src/tests/KeyboardExecutor.test.js +430 -0
  63. package/src/tests/NetworkErrorCapture.test.js +1 -1
  64. package/src/tests/PageController.test.js +1 -1
  65. package/src/tests/PdfCapture.test.js +333 -0
  66. package/src/tests/ScreenshotCapture.test.js +1 -1
  67. package/src/tests/SessionRegistry.test.js +1 -1
  68. package/src/tests/StepValidator.test.js +527 -0
  69. package/src/tests/TargetManager.test.js +1 -1
  70. package/src/tests/TestRunner.test.js +1 -1
  71. package/src/tests/WaitStrategy.test.js +1 -1
  72. package/src/tests/WaitUtilities.test.js +508 -0
  73. package/src/tests/WebStorageManager.test.js +333 -0
  74. package/src/types.js +309 -0
  75. package/src/capture.js +0 -1400
  76. package/src/cdp.js +0 -1286
  77. package/src/dom.js +0 -4379
  78. package/src/runner.js +0 -3676
@@ -0,0 +1,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
+ */