cdp-skill 1.0.7 → 1.0.14
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 +80 -35
- package/SKILL.md +198 -1344
- package/install.js +1 -0
- package/package.json +1 -1
- package/src/aria/index.js +8 -0
- package/src/aria/output-processor.js +173 -0
- package/src/aria/role-query.js +1229 -0
- package/src/aria/snapshot.js +459 -0
- package/src/aria.js +237 -43
- package/src/cdp/browser.js +22 -4
- package/src/cdp-skill.js +268 -68
- package/src/dom/click-executor.js +240 -76
- package/src/dom/element-locator.js +34 -25
- package/src/dom/fill-executor.js +55 -27
- package/src/page/dialog-handler.js +119 -0
- package/src/page/page-controller.js +190 -3
- package/src/runner/context-helpers.js +33 -55
- package/src/runner/execute-dynamic.js +34 -143
- package/src/runner/execute-form.js +11 -11
- package/src/runner/execute-input.js +2 -2
- package/src/runner/execute-interaction.js +99 -120
- package/src/runner/execute-navigation.js +11 -26
- package/src/runner/execute-query.js +8 -5
- package/src/runner/step-executors.js +256 -95
- package/src/runner/step-registry.js +1064 -0
- package/src/runner/step-validator.js +16 -740
- package/src/tests/Aria.test.js +1025 -0
- package/src/tests/ContextHelpers.test.js +39 -28
- package/src/tests/ExecuteBrowser.test.js +572 -0
- package/src/tests/ExecuteDynamic.test.js +34 -736
- package/src/tests/ExecuteForm.test.js +700 -0
- package/src/tests/ExecuteInput.test.js +540 -0
- package/src/tests/ExecuteInteraction.test.js +319 -0
- package/src/tests/ExecuteQuery.test.js +820 -0
- package/src/tests/FillExecutor.test.js +2 -2
- package/src/tests/StepValidator.test.js +222 -76
- package/src/tests/TestRunner.test.js +36 -25
- package/src/tests/integration.test.js +2 -1
- package/src/types.js +9 -9
- package/src/utils/backoff.js +118 -0
- package/src/utils/cdp-helpers.js +130 -0
- package/src/utils/devices.js +140 -0
- package/src/utils/errors.js +242 -0
- package/src/utils/index.js +65 -0
- package/src/utils/temp.js +75 -0
- package/src/utils/validators.js +433 -0
- package/src/utils.js +14 -1142
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Temp Directory Utilities
|
|
3
|
+
* Handles temp directory creation and path resolution for CDP skill outputs
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import os from 'os';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import fs from 'fs/promises';
|
|
9
|
+
|
|
10
|
+
let _tempDir = null;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Get the platform-specific temp directory for CDP skill outputs (screenshots, PDFs, etc.)
|
|
14
|
+
* Creates the directory if it doesn't exist
|
|
15
|
+
* @returns {Promise<string>} Absolute path to temp directory
|
|
16
|
+
*/
|
|
17
|
+
export async function getTempDir() {
|
|
18
|
+
if (_tempDir) return _tempDir;
|
|
19
|
+
|
|
20
|
+
const baseTemp = os.tmpdir();
|
|
21
|
+
_tempDir = path.join(baseTemp, 'cdp-skill');
|
|
22
|
+
|
|
23
|
+
await fs.mkdir(_tempDir, { recursive: true });
|
|
24
|
+
return _tempDir;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Get the temp directory synchronously (returns cached value or creates new)
|
|
29
|
+
* Note: First call should use getTempDir() to ensure directory exists
|
|
30
|
+
* @returns {string} Absolute path to temp directory
|
|
31
|
+
*/
|
|
32
|
+
export function getTempDirSync() {
|
|
33
|
+
if (_tempDir) return _tempDir;
|
|
34
|
+
|
|
35
|
+
const baseTemp = os.tmpdir();
|
|
36
|
+
_tempDir = path.join(baseTemp, 'cdp-skill');
|
|
37
|
+
return _tempDir;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Resolve a file path, using temp directory for relative paths
|
|
42
|
+
* @param {string} filePath - File path (relative or absolute)
|
|
43
|
+
* @param {string} [extension] - Default extension to add if missing
|
|
44
|
+
* @returns {Promise<string>} Absolute path
|
|
45
|
+
*/
|
|
46
|
+
export async function resolveTempPath(filePath, extension) {
|
|
47
|
+
// If already absolute, use as-is
|
|
48
|
+
if (path.isAbsolute(filePath)) {
|
|
49
|
+
return filePath;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// For relative paths, put in temp directory
|
|
53
|
+
const tempDir = await getTempDir();
|
|
54
|
+
let resolved = path.join(tempDir, filePath);
|
|
55
|
+
|
|
56
|
+
// Add extension if missing
|
|
57
|
+
if (extension && !path.extname(resolved)) {
|
|
58
|
+
resolved += extension;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return resolved;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Generate a unique temp file path with timestamp
|
|
66
|
+
* @param {string} prefix - File prefix (e.g., 'screenshot', 'page')
|
|
67
|
+
* @param {string} extension - File extension (e.g., '.png', '.pdf')
|
|
68
|
+
* @returns {Promise<string>} Unique absolute path in temp directory
|
|
69
|
+
*/
|
|
70
|
+
export async function generateTempPath(prefix, extension) {
|
|
71
|
+
const tempDir = await getTempDir();
|
|
72
|
+
const timestamp = Date.now();
|
|
73
|
+
const random = Math.random().toString(36).substring(2, 8);
|
|
74
|
+
return path.join(tempDir, `${prefix}-${timestamp}-${random}${extension}`);
|
|
75
|
+
}
|
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validation Utilities
|
|
3
|
+
* Key validation and form validation helpers
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const VALID_KEY_NAMES = new Set([
|
|
7
|
+
// Standard keys
|
|
8
|
+
'Enter', 'Tab', 'Escape', 'Backspace', 'Delete', 'Space',
|
|
9
|
+
// Arrow keys
|
|
10
|
+
'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight',
|
|
11
|
+
// Modifier keys
|
|
12
|
+
'Shift', 'Control', 'Alt', 'Meta',
|
|
13
|
+
// Function keys
|
|
14
|
+
'F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9', 'F10', 'F11', 'F12',
|
|
15
|
+
// Navigation keys
|
|
16
|
+
'Home', 'End', 'PageUp', 'PageDown', 'Insert',
|
|
17
|
+
// Additional common keys
|
|
18
|
+
'CapsLock', 'NumLock', 'ScrollLock', 'Pause', 'PrintScreen',
|
|
19
|
+
'ContextMenu',
|
|
20
|
+
// Numpad keys
|
|
21
|
+
'Numpad0', 'Numpad1', 'Numpad2', 'Numpad3', 'Numpad4',
|
|
22
|
+
'Numpad5', 'Numpad6', 'Numpad7', 'Numpad8', 'Numpad9',
|
|
23
|
+
'NumpadAdd', 'NumpadSubtract', 'NumpadMultiply', 'NumpadDivide',
|
|
24
|
+
'NumpadDecimal', 'NumpadEnter'
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
const MODIFIER_ALIASES = new Set([
|
|
28
|
+
'control', 'ctrl', 'alt', 'meta', 'cmd', 'command', 'shift'
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Create a key validator for validating key names against known CDP key codes
|
|
33
|
+
* @returns {Object} Key validator with validation methods
|
|
34
|
+
*/
|
|
35
|
+
export function createKeyValidator() {
|
|
36
|
+
function isKnownKey(keyName) {
|
|
37
|
+
if (!keyName || typeof keyName !== 'string') {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
if (VALID_KEY_NAMES.has(keyName)) {
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
// Check for single character keys (a-z, A-Z, 0-9, punctuation)
|
|
44
|
+
if (keyName.length === 1) {
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function isModifierAlias(part) {
|
|
51
|
+
return MODIFIER_ALIASES.has(part.toLowerCase());
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function getKnownKeysSample() {
|
|
55
|
+
return ['Enter', 'Tab', 'Escape', 'Backspace', 'ArrowUp', 'ArrowDown', 'F1-F12'].join(', ');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function validateCombo(combo) {
|
|
59
|
+
const parts = combo.split('+');
|
|
60
|
+
const warnings = [];
|
|
61
|
+
let mainKey = null;
|
|
62
|
+
|
|
63
|
+
for (const part of parts) {
|
|
64
|
+
const trimmed = part.trim();
|
|
65
|
+
if (!trimmed) {
|
|
66
|
+
return {
|
|
67
|
+
valid: false,
|
|
68
|
+
warning: `Invalid key combo "${combo}": empty key part`
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Check if it's a modifier
|
|
73
|
+
if (isModifierAlias(trimmed) || VALID_KEY_NAMES.has(trimmed) &&
|
|
74
|
+
['Shift', 'Control', 'Alt', 'Meta'].includes(trimmed)) {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// This should be the main key
|
|
79
|
+
if (mainKey !== null) {
|
|
80
|
+
return {
|
|
81
|
+
valid: false,
|
|
82
|
+
warning: `Invalid key combo "${combo}": multiple main keys specified`
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
mainKey = trimmed;
|
|
86
|
+
|
|
87
|
+
if (!isKnownKey(trimmed)) {
|
|
88
|
+
warnings.push(`Unknown key "${trimmed}" in combo`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (mainKey === null) {
|
|
93
|
+
return {
|
|
94
|
+
valid: false,
|
|
95
|
+
warning: `Invalid key combo "${combo}": no main key specified`
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
valid: true,
|
|
101
|
+
warning: warnings.length > 0 ? warnings.join('; ') : null
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function validate(keyName) {
|
|
106
|
+
if (!keyName || typeof keyName !== 'string') {
|
|
107
|
+
return {
|
|
108
|
+
valid: false,
|
|
109
|
+
warning: 'Key name must be a non-empty string'
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Handle key combos (e.g., "Control+a")
|
|
114
|
+
if (keyName.includes('+')) {
|
|
115
|
+
return validateCombo(keyName);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (isKnownKey(keyName)) {
|
|
119
|
+
return { valid: true, warning: null };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
valid: true, // Still allow unknown keys to pass through
|
|
124
|
+
warning: `Unknown key name "${keyName}". Known keys: ${getKnownKeysSample()}`
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function getValidKeyNames() {
|
|
129
|
+
return new Set(VALID_KEY_NAMES);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
isKnownKey,
|
|
134
|
+
isModifierAlias,
|
|
135
|
+
validate,
|
|
136
|
+
validateCombo,
|
|
137
|
+
getKnownKeysSample,
|
|
138
|
+
getValidKeyNames
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Create a form validator for handling form validation queries and submit operations
|
|
144
|
+
* @param {Object} session - CDP session
|
|
145
|
+
* @param {Object} elementLocator - Element locator instance
|
|
146
|
+
* @returns {Object} Form validator with validation methods
|
|
147
|
+
*/
|
|
148
|
+
export function createFormValidator(session, elementLocator) {
|
|
149
|
+
/**
|
|
150
|
+
* Query validation state of an element using HTML5 constraint validation API
|
|
151
|
+
* @param {string} selector - CSS selector for the input/form element
|
|
152
|
+
* @returns {Promise<{valid: boolean, message: string, validity: Object}>}
|
|
153
|
+
*/
|
|
154
|
+
async function validateElement(selector) {
|
|
155
|
+
const element = await elementLocator.findElement(selector);
|
|
156
|
+
if (!element) {
|
|
157
|
+
throw new Error(`Element not found: ${selector}`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
const result = await session.send('Runtime.callFunctionOn', {
|
|
162
|
+
objectId: element._handle.objectId,
|
|
163
|
+
functionDeclaration: `function() {
|
|
164
|
+
if (!this.checkValidity) {
|
|
165
|
+
return { valid: true, message: '', validity: null, supported: false };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const valid = this.checkValidity();
|
|
169
|
+
const message = this.validationMessage || '';
|
|
170
|
+
|
|
171
|
+
// Get detailed validity state
|
|
172
|
+
const validity = this.validity ? {
|
|
173
|
+
valueMissing: this.validity.valueMissing,
|
|
174
|
+
typeMismatch: this.validity.typeMismatch,
|
|
175
|
+
patternMismatch: this.validity.patternMismatch,
|
|
176
|
+
tooLong: this.validity.tooLong,
|
|
177
|
+
tooShort: this.validity.tooShort,
|
|
178
|
+
rangeUnderflow: this.validity.rangeUnderflow,
|
|
179
|
+
rangeOverflow: this.validity.rangeOverflow,
|
|
180
|
+
stepMismatch: this.validity.stepMismatch,
|
|
181
|
+
badInput: this.validity.badInput,
|
|
182
|
+
customError: this.validity.customError
|
|
183
|
+
} : null;
|
|
184
|
+
|
|
185
|
+
return { valid, message, validity, supported: true };
|
|
186
|
+
}`,
|
|
187
|
+
returnByValue: true
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
return result.result.value;
|
|
191
|
+
} finally {
|
|
192
|
+
await element._handle.dispose();
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Submit a form and report validation errors
|
|
198
|
+
* @param {string} selector - CSS selector for the form element
|
|
199
|
+
* @param {Object} options - Submit options
|
|
200
|
+
* @param {boolean} options.validate - Check validation before submitting (default: true)
|
|
201
|
+
* @param {boolean} options.reportValidity - Show browser validation UI (default: false)
|
|
202
|
+
* @returns {Promise<{submitted: boolean, valid: boolean, errors: Array}>}
|
|
203
|
+
*/
|
|
204
|
+
async function submitForm(selector, options = {}) {
|
|
205
|
+
const { validate = true, reportValidity = false } = options;
|
|
206
|
+
|
|
207
|
+
const element = await elementLocator.findElement(selector);
|
|
208
|
+
if (!element) {
|
|
209
|
+
throw new Error(`Form not found: ${selector}`);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
try {
|
|
213
|
+
const result = await session.send('Runtime.callFunctionOn', {
|
|
214
|
+
objectId: element._handle.objectId,
|
|
215
|
+
functionDeclaration: `function(validate, reportValidity) {
|
|
216
|
+
// Check if this is a form element
|
|
217
|
+
if (this.tagName !== 'FORM') {
|
|
218
|
+
return { submitted: false, error: 'Element is not a form', valid: null, errors: [] };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const errors = [];
|
|
222
|
+
let formValid = true;
|
|
223
|
+
|
|
224
|
+
if (validate) {
|
|
225
|
+
// Get all form elements and check validity
|
|
226
|
+
const elements = this.elements;
|
|
227
|
+
for (let i = 0; i < elements.length; i++) {
|
|
228
|
+
const el = elements[i];
|
|
229
|
+
if (el.checkValidity && !el.checkValidity()) {
|
|
230
|
+
formValid = false;
|
|
231
|
+
errors.push({
|
|
232
|
+
name: el.name || el.id || 'unknown',
|
|
233
|
+
type: el.type || el.tagName.toLowerCase(),
|
|
234
|
+
message: el.validationMessage,
|
|
235
|
+
value: el.value
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (!formValid) {
|
|
241
|
+
if (reportValidity) {
|
|
242
|
+
this.reportValidity();
|
|
243
|
+
}
|
|
244
|
+
return { submitted: false, valid: false, errors };
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Submit the form
|
|
249
|
+
this.submit();
|
|
250
|
+
return { submitted: true, valid: true, errors: [] };
|
|
251
|
+
}`,
|
|
252
|
+
arguments: [
|
|
253
|
+
{ value: validate },
|
|
254
|
+
{ value: reportValidity }
|
|
255
|
+
],
|
|
256
|
+
returnByValue: true
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
return result.result.value;
|
|
260
|
+
} finally {
|
|
261
|
+
await element._handle.dispose();
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Get all validation errors for a form
|
|
267
|
+
* @param {string} selector - CSS selector for the form element
|
|
268
|
+
* @returns {Promise<Array<{name: string, type: string, message: string}>>}
|
|
269
|
+
*/
|
|
270
|
+
async function getFormErrors(selector) {
|
|
271
|
+
const element = await elementLocator.findElement(selector);
|
|
272
|
+
if (!element) {
|
|
273
|
+
throw new Error(`Form not found: ${selector}`);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
try {
|
|
277
|
+
const result = await session.send('Runtime.callFunctionOn', {
|
|
278
|
+
objectId: element._handle.objectId,
|
|
279
|
+
functionDeclaration: `function() {
|
|
280
|
+
if (this.tagName !== 'FORM') {
|
|
281
|
+
return { error: 'Element is not a form', errors: [] };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const errors = [];
|
|
285
|
+
const elements = this.elements;
|
|
286
|
+
|
|
287
|
+
for (let i = 0; i < elements.length; i++) {
|
|
288
|
+
const el = elements[i];
|
|
289
|
+
if (el.checkValidity && !el.checkValidity()) {
|
|
290
|
+
errors.push({
|
|
291
|
+
name: el.name || el.id || 'unknown',
|
|
292
|
+
type: el.type || el.tagName.toLowerCase(),
|
|
293
|
+
message: el.validationMessage,
|
|
294
|
+
value: el.value
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return { errors };
|
|
300
|
+
}`,
|
|
301
|
+
returnByValue: true
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
return result.result.value.errors;
|
|
305
|
+
} finally {
|
|
306
|
+
await element._handle.dispose();
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Get complete form state including all fields and their values
|
|
312
|
+
* @param {string} selector - CSS selector for the form element
|
|
313
|
+
* @returns {Promise<Object>} Form state object
|
|
314
|
+
*/
|
|
315
|
+
async function getFormState(selector) {
|
|
316
|
+
const element = await elementLocator.findElement(selector);
|
|
317
|
+
if (!element) {
|
|
318
|
+
throw new Error(`Form not found: ${selector}`);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
try {
|
|
322
|
+
const result = await session.send('Runtime.callFunctionOn', {
|
|
323
|
+
objectId: element._handle.objectId,
|
|
324
|
+
functionDeclaration: `function() {
|
|
325
|
+
if (this.tagName !== 'FORM') {
|
|
326
|
+
return { error: 'Element is not a form' };
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const form = this;
|
|
330
|
+
const fields = [];
|
|
331
|
+
let formValid = true;
|
|
332
|
+
|
|
333
|
+
// Get form attributes
|
|
334
|
+
const action = form.action || '';
|
|
335
|
+
const method = (form.method || 'get').toUpperCase();
|
|
336
|
+
const enctype = form.enctype || 'application/x-www-form-urlencoded';
|
|
337
|
+
|
|
338
|
+
// Get associated label for an element
|
|
339
|
+
function getLabel(el) {
|
|
340
|
+
// Try label with for attribute (use CSS.escape to prevent selector injection)
|
|
341
|
+
if (el.id) {
|
|
342
|
+
const label = document.querySelector('label[for="' + CSS.escape(el.id) + '"]');
|
|
343
|
+
if (label) return label.textContent.trim();
|
|
344
|
+
}
|
|
345
|
+
// Try parent label
|
|
346
|
+
const parentLabel = el.closest('label');
|
|
347
|
+
if (parentLabel) {
|
|
348
|
+
// Get text content excluding the input's text
|
|
349
|
+
const clone = parentLabel.cloneNode(true);
|
|
350
|
+
const inputs = clone.querySelectorAll('input, textarea, select');
|
|
351
|
+
inputs.forEach(i => i.remove());
|
|
352
|
+
return clone.textContent.trim();
|
|
353
|
+
}
|
|
354
|
+
// Try aria-label
|
|
355
|
+
if (el.getAttribute('aria-label')) {
|
|
356
|
+
return el.getAttribute('aria-label');
|
|
357
|
+
}
|
|
358
|
+
// Try placeholder
|
|
359
|
+
if (el.placeholder) {
|
|
360
|
+
return el.placeholder;
|
|
361
|
+
}
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const elements = form.elements;
|
|
366
|
+
for (let i = 0; i < elements.length; i++) {
|
|
367
|
+
const el = elements[i];
|
|
368
|
+
const tagName = el.tagName.toLowerCase();
|
|
369
|
+
const type = el.type ? el.type.toLowerCase() : tagName;
|
|
370
|
+
|
|
371
|
+
// Skip buttons and hidden fields
|
|
372
|
+
if (type === 'submit' || type === 'reset' || type === 'button' || type === 'hidden') {
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Get validation state
|
|
377
|
+
const valid = el.checkValidity ? el.checkValidity() : true;
|
|
378
|
+
if (!valid) formValid = false;
|
|
379
|
+
|
|
380
|
+
// Get value (mask passwords)
|
|
381
|
+
let value;
|
|
382
|
+
if (type === 'password') {
|
|
383
|
+
value = el.value ? '••••••••' : '';
|
|
384
|
+
} else if (type === 'checkbox' || type === 'radio') {
|
|
385
|
+
value = el.checked;
|
|
386
|
+
} else if (tagName === 'select') {
|
|
387
|
+
const selected = [];
|
|
388
|
+
for (let j = 0; j < el.selectedOptions.length; j++) {
|
|
389
|
+
selected.push(el.selectedOptions[j].text);
|
|
390
|
+
}
|
|
391
|
+
value = el.multiple ? selected : (selected[0] || '');
|
|
392
|
+
} else {
|
|
393
|
+
value = el.value || '';
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
fields.push({
|
|
397
|
+
name: el.name || el.id || null,
|
|
398
|
+
type: type,
|
|
399
|
+
label: getLabel(el),
|
|
400
|
+
value: value,
|
|
401
|
+
required: el.required || false,
|
|
402
|
+
valid: valid,
|
|
403
|
+
validationMessage: el.validationMessage || null,
|
|
404
|
+
disabled: el.disabled || false,
|
|
405
|
+
readOnly: el.readOnly || false
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return {
|
|
410
|
+
action,
|
|
411
|
+
method,
|
|
412
|
+
enctype,
|
|
413
|
+
fields,
|
|
414
|
+
valid: formValid,
|
|
415
|
+
fieldCount: fields.length
|
|
416
|
+
};
|
|
417
|
+
}`,
|
|
418
|
+
returnByValue: true
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
return result.result.value;
|
|
422
|
+
} finally {
|
|
423
|
+
await element._handle.dispose();
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
return {
|
|
428
|
+
validateElement,
|
|
429
|
+
submitForm,
|
|
430
|
+
getFormErrors,
|
|
431
|
+
getFormState
|
|
432
|
+
};
|
|
433
|
+
}
|