cdp-skill 1.0.8 → 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 +151 -239
- 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 +245 -69
- 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 +8 -7
- 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 +225 -84
- package/src/runner/step-registry.js +1064 -0
- package/src/runner/step-validator.js +16 -754
- 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 +2 -457
- 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
|
@@ -7,10 +7,10 @@
|
|
|
7
7
|
* - validateStepInternal(step) → string[] - Internal per-step validation
|
|
8
8
|
*
|
|
9
9
|
* DEPENDENCIES:
|
|
10
|
-
* - ./
|
|
10
|
+
* - ./step-registry.js: getAllStepTypes, getStepConfig, validateHooks
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import {
|
|
13
|
+
import { getAllStepTypes, getStepConfig, validateHooks } from './step-registry.js';
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* Validate a single step definition
|
|
@@ -25,10 +25,11 @@ export function validateStepInternal(step) {
|
|
|
25
25
|
return errors;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
const
|
|
28
|
+
const allStepTypes = getAllStepTypes();
|
|
29
|
+
const definedActions = allStepTypes.filter(type => step[type] !== undefined);
|
|
29
30
|
|
|
30
31
|
if (definedActions.length === 0) {
|
|
31
|
-
errors.push(`unknown step type, expected one of: ${
|
|
32
|
+
errors.push(`unknown step type, expected one of: ${allStepTypes.join(', ')}`);
|
|
32
33
|
return errors;
|
|
33
34
|
}
|
|
34
35
|
|
|
@@ -40,759 +41,20 @@ export function validateStepInternal(step) {
|
|
|
40
41
|
const action = definedActions[0];
|
|
41
42
|
const params = step[action];
|
|
42
43
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
errors.push('goto requires a non-empty URL string');
|
|
49
|
-
}
|
|
50
|
-
} else if (params && typeof params === 'object') {
|
|
51
|
-
if (!params.url || typeof params.url !== 'string') {
|
|
52
|
-
errors.push('goto requires a non-empty url property');
|
|
53
|
-
}
|
|
54
|
-
// Validate waitUntil if provided
|
|
55
|
-
if (params.waitUntil !== undefined) {
|
|
56
|
-
const validWaitUntil = ['commit', 'domcontentloaded', 'load', 'networkidle'];
|
|
57
|
-
if (!validWaitUntil.includes(params.waitUntil)) {
|
|
58
|
-
errors.push(`goto waitUntil must be one of: ${validWaitUntil.join(', ')}`);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
} else {
|
|
62
|
-
errors.push('goto requires a URL string or object with url property');
|
|
63
|
-
}
|
|
64
|
-
break;
|
|
65
|
-
|
|
66
|
-
case 'reload':
|
|
67
|
-
// reload can be boolean true or object with options
|
|
68
|
-
if (params !== true && typeof params !== 'object') {
|
|
69
|
-
errors.push('reload requires true or params object');
|
|
70
|
-
}
|
|
71
|
-
if (typeof params === 'object' && params.waitUntil !== undefined) {
|
|
72
|
-
const validWaitUntil = ['commit', 'domcontentloaded', 'load', 'networkidle'];
|
|
73
|
-
if (!validWaitUntil.includes(params.waitUntil)) {
|
|
74
|
-
errors.push(`reload waitUntil must be one of: ${validWaitUntil.join(', ')}`);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
break;
|
|
78
|
-
|
|
79
|
-
case 'wait':
|
|
80
|
-
// Support numeric value for simple delay: { "wait": 2000 }
|
|
81
|
-
if (typeof params === 'number') {
|
|
82
|
-
if (params < 0) {
|
|
83
|
-
errors.push('wait time must be a non-negative number');
|
|
84
|
-
}
|
|
85
|
-
} else if (typeof params === 'string') {
|
|
86
|
-
if (params.length === 0) {
|
|
87
|
-
errors.push('wait selector cannot be empty');
|
|
88
|
-
}
|
|
89
|
-
} else if (params && typeof params === 'object') {
|
|
90
|
-
const hasSelector = params.selector !== undefined;
|
|
91
|
-
const hasText = params.text !== undefined;
|
|
92
|
-
const hasTextRegex = params.textRegex !== undefined;
|
|
93
|
-
const hasTime = params.time !== undefined;
|
|
94
|
-
const hasUrlContains = params.urlContains !== undefined;
|
|
95
|
-
if (!hasSelector && !hasText && !hasTextRegex && !hasTime && !hasUrlContains) {
|
|
96
|
-
errors.push('wait requires selector, text, textRegex, time, or urlContains');
|
|
97
|
-
}
|
|
98
|
-
if (hasSelector && typeof params.selector !== 'string') {
|
|
99
|
-
errors.push('wait selector must be a string');
|
|
100
|
-
}
|
|
101
|
-
if (hasText && typeof params.text !== 'string') {
|
|
102
|
-
errors.push('wait text must be a string');
|
|
103
|
-
}
|
|
104
|
-
if (hasTextRegex && typeof params.textRegex !== 'string') {
|
|
105
|
-
errors.push('wait textRegex must be a string');
|
|
106
|
-
}
|
|
107
|
-
if (hasTime && (typeof params.time !== 'number' || params.time < 0)) {
|
|
108
|
-
errors.push('wait time must be a non-negative number');
|
|
109
|
-
}
|
|
110
|
-
if (hasUrlContains && typeof params.urlContains !== 'string') {
|
|
111
|
-
errors.push('wait urlContains must be a string');
|
|
112
|
-
}
|
|
113
|
-
if (params.minCount !== undefined && (typeof params.minCount !== 'number' || params.minCount < 0)) {
|
|
114
|
-
errors.push('wait minCount must be a non-negative number');
|
|
115
|
-
}
|
|
116
|
-
if (params.caseSensitive !== undefined && typeof params.caseSensitive !== 'boolean') {
|
|
117
|
-
errors.push('wait caseSensitive must be a boolean');
|
|
118
|
-
}
|
|
119
|
-
if (params.hidden !== undefined && typeof params.hidden !== 'boolean') {
|
|
120
|
-
errors.push('wait hidden must be a boolean');
|
|
121
|
-
}
|
|
122
|
-
} else {
|
|
123
|
-
errors.push('wait requires a number (ms), selector string, or params object');
|
|
124
|
-
}
|
|
125
|
-
break;
|
|
126
|
-
|
|
127
|
-
case 'click':
|
|
128
|
-
if (typeof params === 'string') {
|
|
129
|
-
if (params.length === 0) {
|
|
130
|
-
errors.push('click selector cannot be empty');
|
|
131
|
-
}
|
|
132
|
-
} else if (params && typeof params === 'object') {
|
|
133
|
-
// Check for coordinate-based click
|
|
134
|
-
const hasCoordinates = typeof params.x === 'number' && typeof params.y === 'number';
|
|
135
|
-
// Check for text-based click
|
|
136
|
-
const hasText = typeof params.text === 'string';
|
|
137
|
-
// Check for multi-selector fallback
|
|
138
|
-
const hasSelectors = Array.isArray(params.selectors);
|
|
139
|
-
if (!params.selector && !params.ref && !hasCoordinates && !hasText && !hasSelectors) {
|
|
140
|
-
errors.push('click requires selector, ref, text, selectors array, or x/y coordinates');
|
|
141
|
-
} else if (params.selector && typeof params.selector !== 'string') {
|
|
142
|
-
errors.push('click selector must be a string');
|
|
143
|
-
} else if (params.ref && typeof params.ref !== 'string') {
|
|
144
|
-
errors.push('click ref must be a string');
|
|
145
|
-
} else if (hasText && params.text.length === 0) {
|
|
146
|
-
errors.push('click text cannot be empty');
|
|
147
|
-
} else if (hasSelectors && params.selectors.length === 0) {
|
|
148
|
-
errors.push('click selectors array cannot be empty');
|
|
149
|
-
} else if (hasCoordinates) {
|
|
150
|
-
if (params.x < 0 || params.y < 0) {
|
|
151
|
-
errors.push('click coordinates must be non-negative');
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
} else {
|
|
155
|
-
errors.push('click requires a selector string or params object');
|
|
156
|
-
}
|
|
157
|
-
break;
|
|
158
|
-
|
|
159
|
-
case 'fill':
|
|
160
|
-
if (!params || typeof params !== 'object') {
|
|
161
|
-
errors.push('fill requires an object with selector/ref/label and value');
|
|
162
|
-
} else {
|
|
163
|
-
if (!params.selector && !params.ref && !params.label) {
|
|
164
|
-
errors.push('fill requires selector, ref, or label');
|
|
165
|
-
} else if (params.selector && typeof params.selector !== 'string') {
|
|
166
|
-
errors.push('fill selector must be a string');
|
|
167
|
-
} else if (params.ref && typeof params.ref !== 'string') {
|
|
168
|
-
errors.push('fill ref must be a string');
|
|
169
|
-
} else if (params.label && typeof params.label !== 'string') {
|
|
170
|
-
errors.push('fill label must be a string');
|
|
171
|
-
}
|
|
172
|
-
if (params.value === undefined) {
|
|
173
|
-
errors.push('fill requires value');
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
break;
|
|
177
|
-
|
|
178
|
-
case 'fillForm':
|
|
179
|
-
if (!params || typeof params !== 'object') {
|
|
180
|
-
errors.push('fillForm requires an object mapping selectors/refs to values');
|
|
181
|
-
} else {
|
|
182
|
-
// Support both formats:
|
|
183
|
-
// Simple: {"#firstName": "John", "#lastName": "Doe"}
|
|
184
|
-
// Extended: {"fields": {"#firstName": "John"}, "react": true}
|
|
185
|
-
let fields;
|
|
186
|
-
if (params.fields && typeof params.fields === 'object') {
|
|
187
|
-
fields = params.fields;
|
|
188
|
-
// Validate react option if present
|
|
189
|
-
if (params.react !== undefined && typeof params.react !== 'boolean') {
|
|
190
|
-
errors.push('fillForm react option must be a boolean');
|
|
191
|
-
}
|
|
192
|
-
} else {
|
|
193
|
-
fields = params;
|
|
194
|
-
}
|
|
195
|
-
const entries = Object.entries(fields);
|
|
196
|
-
if (entries.length === 0) {
|
|
197
|
-
errors.push('fillForm requires at least one field');
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
break;
|
|
201
|
-
|
|
202
|
-
case 'press':
|
|
203
|
-
if (typeof params !== 'string' || params.length === 0) {
|
|
204
|
-
errors.push('press requires a non-empty key string');
|
|
205
|
-
}
|
|
206
|
-
break;
|
|
207
|
-
|
|
208
|
-
case 'query':
|
|
209
|
-
if (typeof params === 'string') {
|
|
210
|
-
if (params.length === 0) {
|
|
211
|
-
errors.push('query selector cannot be empty');
|
|
212
|
-
}
|
|
213
|
-
} else if (params && typeof params === 'object') {
|
|
214
|
-
// Support both CSS selector and role-based queries
|
|
215
|
-
if (!params.selector && !params.role) {
|
|
216
|
-
errors.push('query requires selector or role');
|
|
217
|
-
}
|
|
218
|
-
// Role can be string or array of strings (compound roles)
|
|
219
|
-
if (params.role) {
|
|
220
|
-
if (typeof params.role !== 'string' && !Array.isArray(params.role)) {
|
|
221
|
-
errors.push('query role must be a string or array of strings');
|
|
222
|
-
}
|
|
223
|
-
if (Array.isArray(params.role) && !params.role.every(r => typeof r === 'string')) {
|
|
224
|
-
errors.push('query role array must contain only strings');
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
// Validate nameExact and nameRegex are not both set
|
|
228
|
-
if (params.nameExact && params.nameRegex) {
|
|
229
|
-
errors.push('query cannot have both nameExact and nameRegex');
|
|
230
|
-
}
|
|
231
|
-
} else {
|
|
232
|
-
errors.push('query requires a selector string or params object');
|
|
233
|
-
}
|
|
234
|
-
break;
|
|
235
|
-
|
|
236
|
-
case 'inspect':
|
|
237
|
-
// inspect can be boolean or object with options
|
|
238
|
-
break;
|
|
239
|
-
|
|
240
|
-
case 'scroll':
|
|
241
|
-
if (typeof params === 'string') {
|
|
242
|
-
if (!['top', 'bottom', 'up', 'down'].includes(params) && params.length === 0) {
|
|
243
|
-
errors.push('scroll requires direction (top/bottom/up/down) or selector');
|
|
244
|
-
}
|
|
245
|
-
} else if (params && typeof params === 'object') {
|
|
246
|
-
// selector, x, y, deltaX, deltaY are all valid
|
|
247
|
-
} else if (typeof params !== 'string') {
|
|
248
|
-
errors.push('scroll requires direction string or params object');
|
|
249
|
-
}
|
|
250
|
-
break;
|
|
251
|
-
|
|
252
|
-
case 'console':
|
|
253
|
-
// console can be boolean or object with filter options
|
|
254
|
-
break;
|
|
255
|
-
|
|
256
|
-
case 'pdf':
|
|
257
|
-
if (typeof params === 'string') {
|
|
258
|
-
if (params.length === 0) {
|
|
259
|
-
errors.push('pdf path cannot be empty');
|
|
260
|
-
}
|
|
261
|
-
} else if (params && typeof params === 'object') {
|
|
262
|
-
if (!params.path) {
|
|
263
|
-
errors.push('pdf requires path');
|
|
264
|
-
} else if (typeof params.path !== 'string') {
|
|
265
|
-
errors.push('pdf path must be a string');
|
|
266
|
-
}
|
|
267
|
-
} else {
|
|
268
|
-
errors.push('pdf requires a path string or params object');
|
|
269
|
-
}
|
|
270
|
-
break;
|
|
271
|
-
|
|
272
|
-
case 'eval':
|
|
273
|
-
if (typeof params === 'string') {
|
|
274
|
-
if (params.length === 0) {
|
|
275
|
-
errors.push('eval expression cannot be empty');
|
|
276
|
-
}
|
|
277
|
-
} else if (params && typeof params === 'object') {
|
|
278
|
-
if (!params.expression) {
|
|
279
|
-
errors.push('eval requires expression');
|
|
280
|
-
} else if (typeof params.expression !== 'string') {
|
|
281
|
-
errors.push('eval expression must be a string');
|
|
282
|
-
}
|
|
283
|
-
} else {
|
|
284
|
-
errors.push('eval requires an expression string or params object');
|
|
285
|
-
}
|
|
286
|
-
break;
|
|
287
|
-
|
|
288
|
-
case 'snapshot':
|
|
289
|
-
// snapshot can be boolean or object with options
|
|
290
|
-
if (params !== true && params !== false && typeof params !== 'object') {
|
|
291
|
-
errors.push('snapshot requires true or params object');
|
|
292
|
-
}
|
|
293
|
-
if (typeof params === 'object') {
|
|
294
|
-
if (params.mode && !['ai', 'full'].includes(params.mode)) {
|
|
295
|
-
errors.push('snapshot mode must be "ai" or "full"');
|
|
296
|
-
}
|
|
297
|
-
if (params.detail && !['summary', 'interactive', 'full'].includes(params.detail)) {
|
|
298
|
-
errors.push('snapshot detail must be "summary", "interactive", or "full"');
|
|
299
|
-
}
|
|
300
|
-
if (params.inlineLimit !== undefined && (typeof params.inlineLimit !== 'number' || params.inlineLimit < 0)) {
|
|
301
|
-
errors.push('snapshot inlineLimit must be a non-negative number');
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
break;
|
|
305
|
-
|
|
306
|
-
case 'snapshotSearch':
|
|
307
|
-
// snapshotSearch requires at least one search criterion
|
|
308
|
-
if (!params || typeof params !== 'object') {
|
|
309
|
-
errors.push('snapshotSearch requires an object with search parameters');
|
|
310
|
-
} else {
|
|
311
|
-
const hasText = params.text !== undefined;
|
|
312
|
-
const hasPattern = params.pattern !== undefined;
|
|
313
|
-
const hasRole = params.role !== undefined;
|
|
314
|
-
if (!hasText && !hasPattern && !hasRole) {
|
|
315
|
-
errors.push('snapshotSearch requires at least one of: text, pattern, or role');
|
|
316
|
-
}
|
|
317
|
-
if (hasText && typeof params.text !== 'string') {
|
|
318
|
-
errors.push('snapshotSearch text must be a string');
|
|
319
|
-
}
|
|
320
|
-
if (hasPattern && typeof params.pattern !== 'string') {
|
|
321
|
-
errors.push('snapshotSearch pattern must be a string (regex)');
|
|
322
|
-
}
|
|
323
|
-
if (hasRole && typeof params.role !== 'string') {
|
|
324
|
-
errors.push('snapshotSearch role must be a string');
|
|
325
|
-
}
|
|
326
|
-
if (params.limit !== undefined && (typeof params.limit !== 'number' || params.limit < 1)) {
|
|
327
|
-
errors.push('snapshotSearch limit must be a positive number');
|
|
328
|
-
}
|
|
329
|
-
if (params.context !== undefined && (typeof params.context !== 'number' || params.context < 0)) {
|
|
330
|
-
errors.push('snapshotSearch context must be a non-negative number');
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
break;
|
|
334
|
-
|
|
335
|
-
case 'hover':
|
|
336
|
-
if (typeof params === 'string') {
|
|
337
|
-
if (params.length === 0) {
|
|
338
|
-
errors.push('hover selector cannot be empty');
|
|
339
|
-
}
|
|
340
|
-
} else if (params && typeof params === 'object') {
|
|
341
|
-
if (!params.selector && !params.ref) {
|
|
342
|
-
errors.push('hover requires selector or ref');
|
|
343
|
-
}
|
|
344
|
-
} else {
|
|
345
|
-
errors.push('hover requires a selector string or params object');
|
|
346
|
-
}
|
|
347
|
-
break;
|
|
348
|
-
|
|
349
|
-
case 'viewport':
|
|
350
|
-
// Support both device preset strings and explicit config objects
|
|
351
|
-
if (typeof params === 'string') {
|
|
352
|
-
// Device preset name - validation happens at execution time
|
|
353
|
-
if (params.length === 0) {
|
|
354
|
-
errors.push('viewport preset name cannot be empty');
|
|
355
|
-
}
|
|
356
|
-
} else if (params && typeof params === 'object') {
|
|
357
|
-
if (!params.width || typeof params.width !== 'number') {
|
|
358
|
-
errors.push('viewport requires numeric width');
|
|
359
|
-
}
|
|
360
|
-
if (!params.height || typeof params.height !== 'number') {
|
|
361
|
-
errors.push('viewport requires numeric height');
|
|
362
|
-
}
|
|
363
|
-
} else {
|
|
364
|
-
errors.push('viewport requires a device preset string or object with width and height');
|
|
365
|
-
}
|
|
366
|
-
break;
|
|
367
|
-
|
|
368
|
-
case 'cookies':
|
|
369
|
-
if (!params || typeof params !== 'object') {
|
|
370
|
-
errors.push('cookies requires an object with action (get, set, or clear)');
|
|
371
|
-
} else {
|
|
372
|
-
if (params.set && !Array.isArray(params.set)) {
|
|
373
|
-
errors.push('cookies set requires an array of cookie objects');
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
break;
|
|
377
|
-
|
|
378
|
-
case 'back':
|
|
379
|
-
if (params !== true && typeof params !== 'object') {
|
|
380
|
-
errors.push('back requires true or params object');
|
|
381
|
-
}
|
|
382
|
-
break;
|
|
383
|
-
|
|
384
|
-
case 'forward':
|
|
385
|
-
if (params !== true && typeof params !== 'object') {
|
|
386
|
-
errors.push('forward requires true or params object');
|
|
387
|
-
}
|
|
388
|
-
break;
|
|
389
|
-
|
|
390
|
-
case 'waitForNavigation':
|
|
391
|
-
if (params !== true && typeof params !== 'object') {
|
|
392
|
-
errors.push('waitForNavigation requires true or params object');
|
|
393
|
-
}
|
|
394
|
-
if (typeof params === 'object' && params.timeout !== undefined) {
|
|
395
|
-
if (typeof params.timeout !== 'number' || params.timeout < 0) {
|
|
396
|
-
errors.push('waitForNavigation timeout must be a non-negative number');
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
break;
|
|
400
|
-
|
|
401
|
-
case 'listTabs':
|
|
402
|
-
// listTabs can be boolean true
|
|
403
|
-
if (params !== true) {
|
|
404
|
-
errors.push('listTabs requires true');
|
|
405
|
-
}
|
|
406
|
-
break;
|
|
407
|
-
|
|
408
|
-
case 'closeTab':
|
|
409
|
-
if (typeof params !== 'string' || params.length === 0) {
|
|
410
|
-
errors.push('closeTab requires a non-empty targetId string');
|
|
411
|
-
}
|
|
412
|
-
break;
|
|
413
|
-
|
|
414
|
-
case 'openTab':
|
|
415
|
-
// openTab can be:
|
|
416
|
-
// - true: just open a blank tab
|
|
417
|
-
// - string: open tab and navigate to URL
|
|
418
|
-
// - object with options: {url: "...", viewport: {...}}
|
|
419
|
-
if (params !== true && typeof params !== 'string' && (typeof params !== 'object' || params === null)) {
|
|
420
|
-
errors.push('openTab must be true, a URL string, or an options object');
|
|
421
|
-
}
|
|
422
|
-
if (typeof params === 'object' && params !== null && params.url !== undefined && typeof params.url !== 'string') {
|
|
423
|
-
errors.push('openTab url must be a string');
|
|
424
|
-
}
|
|
425
|
-
break;
|
|
426
|
-
|
|
427
|
-
case 'type':
|
|
428
|
-
if (!params || typeof params !== 'object') {
|
|
429
|
-
errors.push('type requires an object with selector and text');
|
|
430
|
-
} else {
|
|
431
|
-
if (!params.selector) {
|
|
432
|
-
errors.push('type requires selector');
|
|
433
|
-
} else if (typeof params.selector !== 'string') {
|
|
434
|
-
errors.push('type selector must be a string');
|
|
435
|
-
}
|
|
436
|
-
if (params.text === undefined) {
|
|
437
|
-
errors.push('type requires text');
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
break;
|
|
441
|
-
|
|
442
|
-
case 'select':
|
|
443
|
-
if (typeof params === 'string') {
|
|
444
|
-
if (params.length === 0) {
|
|
445
|
-
errors.push('select selector cannot be empty');
|
|
446
|
-
}
|
|
447
|
-
} else if (params && typeof params === 'object') {
|
|
448
|
-
if (!params.selector) {
|
|
449
|
-
errors.push('select requires selector');
|
|
450
|
-
} else if (typeof params.selector !== 'string') {
|
|
451
|
-
errors.push('select selector must be a string');
|
|
452
|
-
}
|
|
453
|
-
if (params.start !== undefined && typeof params.start !== 'number') {
|
|
454
|
-
errors.push('select start must be a number');
|
|
455
|
-
}
|
|
456
|
-
if (params.end !== undefined && typeof params.end !== 'number') {
|
|
457
|
-
errors.push('select end must be a number');
|
|
458
|
-
}
|
|
459
|
-
} else {
|
|
460
|
-
errors.push('select requires a selector string or params object');
|
|
461
|
-
}
|
|
462
|
-
break;
|
|
463
|
-
|
|
464
|
-
case 'validate':
|
|
465
|
-
if (typeof params !== 'string' || params.length === 0) {
|
|
466
|
-
errors.push('validate requires a non-empty selector string');
|
|
467
|
-
}
|
|
468
|
-
break;
|
|
469
|
-
|
|
470
|
-
case 'submit':
|
|
471
|
-
if (typeof params === 'string') {
|
|
472
|
-
if (params.length === 0) {
|
|
473
|
-
errors.push('submit requires a non-empty form selector');
|
|
474
|
-
}
|
|
475
|
-
} else if (params && typeof params === 'object') {
|
|
476
|
-
if (!params.selector) {
|
|
477
|
-
errors.push('submit requires selector');
|
|
478
|
-
} else if (typeof params.selector !== 'string') {
|
|
479
|
-
errors.push('submit selector must be a string');
|
|
480
|
-
}
|
|
481
|
-
} else {
|
|
482
|
-
errors.push('submit requires a selector string or params object');
|
|
483
|
-
}
|
|
484
|
-
break;
|
|
485
|
-
|
|
486
|
-
case 'assert':
|
|
487
|
-
if (!params || typeof params !== 'object') {
|
|
488
|
-
errors.push('assert requires an object with url, text, or selector');
|
|
489
|
-
} else {
|
|
490
|
-
const hasUrl = params.url !== undefined;
|
|
491
|
-
const hasText = params.text !== undefined;
|
|
492
|
-
if (!hasUrl && !hasText) {
|
|
493
|
-
errors.push('assert requires url or text');
|
|
494
|
-
}
|
|
495
|
-
if (hasUrl && typeof params.url !== 'object') {
|
|
496
|
-
errors.push('assert url must be an object (e.g., { contains: "..." })');
|
|
497
|
-
}
|
|
498
|
-
if (hasUrl && params.url && !params.url.contains && !params.url.equals && !params.url.startsWith && !params.url.endsWith && !params.url.matches) {
|
|
499
|
-
errors.push('assert url requires contains, equals, startsWith, endsWith, or matches');
|
|
500
|
-
}
|
|
501
|
-
if (hasText && typeof params.text !== 'string') {
|
|
502
|
-
errors.push('assert text must be a string');
|
|
503
|
-
}
|
|
504
|
-
if (params.selector && typeof params.selector !== 'string') {
|
|
505
|
-
errors.push('assert selector must be a string');
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
break;
|
|
509
|
-
|
|
510
|
-
case 'queryAll':
|
|
511
|
-
if (!params || typeof params !== 'object') {
|
|
512
|
-
errors.push('queryAll requires an object mapping names to selectors');
|
|
513
|
-
} else {
|
|
514
|
-
const entries = Object.entries(params);
|
|
515
|
-
if (entries.length === 0) {
|
|
516
|
-
errors.push('queryAll requires at least one query');
|
|
517
|
-
}
|
|
518
|
-
for (const [name, selector] of entries) {
|
|
519
|
-
if (typeof selector !== 'string' && typeof selector !== 'object') {
|
|
520
|
-
errors.push(`queryAll "${name}" must be a selector string or query object`);
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
break;
|
|
525
|
-
|
|
526
|
-
case 'switchToFrame':
|
|
527
|
-
// Can be string (selector/name), number (index), or object
|
|
528
|
-
if (params === null || params === undefined) {
|
|
529
|
-
errors.push('switchToFrame requires a selector, index, or options object');
|
|
530
|
-
}
|
|
531
|
-
break;
|
|
532
|
-
|
|
533
|
-
case 'switchToMainFrame':
|
|
534
|
-
// No validation needed, params can be true or anything
|
|
535
|
-
break;
|
|
536
|
-
|
|
537
|
-
case 'listFrames':
|
|
538
|
-
// No validation needed
|
|
539
|
-
break;
|
|
540
|
-
|
|
541
|
-
case 'drag':
|
|
542
|
-
if (!params || typeof params !== 'object') {
|
|
543
|
-
errors.push('drag requires an object with source and target');
|
|
544
|
-
} else {
|
|
545
|
-
if (!params.source) {
|
|
546
|
-
errors.push('drag requires a source selector or coordinates');
|
|
547
|
-
}
|
|
548
|
-
if (!params.target) {
|
|
549
|
-
errors.push('drag requires a target selector or coordinates');
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
break;
|
|
553
|
-
|
|
554
|
-
case 'formState':
|
|
555
|
-
if (typeof params !== 'string' && (!params || !params.selector)) {
|
|
556
|
-
errors.push('formState requires a selector string or object with selector');
|
|
557
|
-
}
|
|
558
|
-
break;
|
|
559
|
-
|
|
560
|
-
case 'extract':
|
|
561
|
-
if (typeof params !== 'string' && (!params || !params.selector)) {
|
|
562
|
-
errors.push('extract requires a selector string or object with selector');
|
|
563
|
-
}
|
|
564
|
-
break;
|
|
565
|
-
|
|
566
|
-
case 'selectOption':
|
|
567
|
-
// selectOption: {"selector": "#dropdown", "value": "optionValue"}
|
|
568
|
-
// or: {"selector": "#dropdown", "label": "Option Text"}
|
|
569
|
-
// or: {"selector": "#dropdown", "index": 2}
|
|
570
|
-
if (!params || typeof params !== 'object') {
|
|
571
|
-
errors.push('selectOption requires an object with selector and value/label/index');
|
|
572
|
-
} else {
|
|
573
|
-
if (!params.selector) {
|
|
574
|
-
errors.push('selectOption requires selector');
|
|
575
|
-
}
|
|
576
|
-
if (params.value === undefined && params.label === undefined && params.index === undefined && !params.values) {
|
|
577
|
-
errors.push('selectOption requires value, label, index, or values');
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
break;
|
|
581
|
-
|
|
582
|
-
case 'getDom':
|
|
583
|
-
// getDom: true (full page) or selector string or object with selector
|
|
584
|
-
if (params !== true && typeof params !== 'string' && (typeof params !== 'object' || params === null)) {
|
|
585
|
-
errors.push('getDom requires true, a selector string, or an options object');
|
|
586
|
-
}
|
|
587
|
-
if (typeof params === 'object' && params !== null && params.selector && typeof params.selector !== 'string') {
|
|
588
|
-
errors.push('getDom selector must be a string');
|
|
589
|
-
}
|
|
590
|
-
break;
|
|
591
|
-
|
|
592
|
-
case 'getBox':
|
|
593
|
-
// getBox: "s1e1" or ["s1e1", "s1e2"] or {"refs": ["s1e1", "s1e2"]}
|
|
594
|
-
// Versioned ref format: s{snapshotId}e{elementId}
|
|
595
|
-
if (typeof params === 'string') {
|
|
596
|
-
if (!/^s\d+e\d+$/.test(params)) {
|
|
597
|
-
errors.push('getBox ref must be in format "s{N}e{M}" (e.g., "s1e1", "s2e34")');
|
|
598
|
-
}
|
|
599
|
-
} else if (Array.isArray(params)) {
|
|
600
|
-
if (params.length === 0) {
|
|
601
|
-
errors.push('getBox refs array cannot be empty');
|
|
602
|
-
}
|
|
603
|
-
for (const ref of params) {
|
|
604
|
-
if (typeof ref !== 'string' || !/^s\d+e\d+$/.test(ref)) {
|
|
605
|
-
errors.push('getBox refs must be strings in format "s{N}e{M}"');
|
|
606
|
-
break;
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
} else if (typeof params === 'object' && params !== null) {
|
|
610
|
-
if (!params.refs && !params.ref) {
|
|
611
|
-
errors.push('getBox requires ref or refs');
|
|
612
|
-
}
|
|
613
|
-
} else {
|
|
614
|
-
errors.push('getBox requires a ref string, array of refs, or options object');
|
|
615
|
-
}
|
|
616
|
-
break;
|
|
617
|
-
|
|
618
|
-
case 'fillActive':
|
|
619
|
-
// fillActive: "text" or {"value": "text", "clear": true}
|
|
620
|
-
if (typeof params === 'string') {
|
|
621
|
-
// Simple string value is fine
|
|
622
|
-
} else if (typeof params === 'object' && params !== null) {
|
|
623
|
-
if (params.value === undefined) {
|
|
624
|
-
errors.push('fillActive requires value');
|
|
625
|
-
}
|
|
626
|
-
} else {
|
|
627
|
-
errors.push('fillActive requires a string value or options object with value');
|
|
628
|
-
}
|
|
629
|
-
break;
|
|
630
|
-
|
|
631
|
-
case 'refAt':
|
|
632
|
-
// refAt: {"x": 100, "y": 200}
|
|
633
|
-
if (!params || typeof params !== 'object') {
|
|
634
|
-
errors.push('refAt requires an object with x and y coordinates');
|
|
635
|
-
} else {
|
|
636
|
-
if (typeof params.x !== 'number') {
|
|
637
|
-
errors.push('refAt requires x coordinate as a number');
|
|
638
|
-
}
|
|
639
|
-
if (typeof params.y !== 'number') {
|
|
640
|
-
errors.push('refAt requires y coordinate as a number');
|
|
641
|
-
}
|
|
642
|
-
}
|
|
643
|
-
break;
|
|
644
|
-
|
|
645
|
-
case 'elementsAt':
|
|
646
|
-
// elementsAt: [{"x": 100, "y": 200}, {"x": 300, "y": 400}]
|
|
647
|
-
if (!Array.isArray(params)) {
|
|
648
|
-
errors.push('elementsAt requires an array of {x, y} coordinates');
|
|
649
|
-
} else if (params.length === 0) {
|
|
650
|
-
errors.push('elementsAt array cannot be empty');
|
|
651
|
-
} else {
|
|
652
|
-
for (let i = 0; i < params.length; i++) {
|
|
653
|
-
const coord = params[i];
|
|
654
|
-
if (!coord || typeof coord !== 'object') {
|
|
655
|
-
errors.push(`elementsAt[${i}] must be an object with x and y`);
|
|
656
|
-
} else if (typeof coord.x !== 'number' || typeof coord.y !== 'number') {
|
|
657
|
-
errors.push(`elementsAt[${i}] requires x and y as numbers`);
|
|
658
|
-
}
|
|
659
|
-
}
|
|
660
|
-
}
|
|
661
|
-
break;
|
|
662
|
-
|
|
663
|
-
case 'elementsNear':
|
|
664
|
-
// elementsNear: {"x": 100, "y": 200, "radius": 50}
|
|
665
|
-
if (!params || typeof params !== 'object') {
|
|
666
|
-
errors.push('elementsNear requires an object with x, y, and optional radius');
|
|
667
|
-
} else {
|
|
668
|
-
if (typeof params.x !== 'number') {
|
|
669
|
-
errors.push('elementsNear requires x coordinate as a number');
|
|
670
|
-
}
|
|
671
|
-
if (typeof params.y !== 'number') {
|
|
672
|
-
errors.push('elementsNear requires y coordinate as a number');
|
|
673
|
-
}
|
|
674
|
-
if (params.radius !== undefined && typeof params.radius !== 'number') {
|
|
675
|
-
errors.push('elementsNear radius must be a number');
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
break;
|
|
679
|
-
|
|
680
|
-
case 'pageFunction':
|
|
681
|
-
// pageFunction: "() => document.title" or {fn, refs, timeout}
|
|
682
|
-
if (typeof params === 'string') {
|
|
683
|
-
if (params.length === 0) {
|
|
684
|
-
errors.push('pageFunction requires a non-empty function string');
|
|
685
|
-
}
|
|
686
|
-
} else if (params && typeof params === 'object') {
|
|
687
|
-
if (!params.fn || typeof params.fn !== 'string') {
|
|
688
|
-
errors.push('pageFunction requires a non-empty fn string');
|
|
689
|
-
}
|
|
690
|
-
if (params.refs !== undefined && typeof params.refs !== 'boolean') {
|
|
691
|
-
errors.push('pageFunction refs must be a boolean');
|
|
692
|
-
}
|
|
693
|
-
if (params.timeout !== undefined && (typeof params.timeout !== 'number' || params.timeout < 0)) {
|
|
694
|
-
errors.push('pageFunction timeout must be a non-negative number');
|
|
695
|
-
}
|
|
696
|
-
} else {
|
|
697
|
-
errors.push('pageFunction requires a function string or params object');
|
|
698
|
-
}
|
|
699
|
-
break;
|
|
700
|
-
|
|
701
|
-
case 'poll':
|
|
702
|
-
// poll: "() => condition" or {fn, interval, timeout}
|
|
703
|
-
if (typeof params === 'string') {
|
|
704
|
-
if (params.length === 0) {
|
|
705
|
-
errors.push('poll requires a non-empty function string');
|
|
706
|
-
}
|
|
707
|
-
} else if (params && typeof params === 'object') {
|
|
708
|
-
if (!params.fn || typeof params.fn !== 'string') {
|
|
709
|
-
errors.push('poll requires a non-empty fn string');
|
|
710
|
-
}
|
|
711
|
-
if (params.interval !== undefined && (typeof params.interval !== 'number' || params.interval < 0)) {
|
|
712
|
-
errors.push('poll interval must be a non-negative number');
|
|
713
|
-
}
|
|
714
|
-
if (params.timeout !== undefined && (typeof params.timeout !== 'number' || params.timeout < 0)) {
|
|
715
|
-
errors.push('poll timeout must be a non-negative number');
|
|
716
|
-
}
|
|
717
|
-
} else {
|
|
718
|
-
errors.push('poll requires a function string or params object');
|
|
719
|
-
}
|
|
720
|
-
break;
|
|
721
|
-
|
|
722
|
-
case 'pipeline':
|
|
723
|
-
// pipeline: [{find, fill}, ...] or {steps, timeout}
|
|
724
|
-
{
|
|
725
|
-
const pipelineSteps = Array.isArray(params) ? params : (params && params.steps);
|
|
726
|
-
if (!Array.isArray(pipelineSteps) || pipelineSteps.length === 0) {
|
|
727
|
-
errors.push('pipeline requires a non-empty array of micro-operations');
|
|
728
|
-
} else {
|
|
729
|
-
for (let pi = 0; pi < pipelineSteps.length; pi++) {
|
|
730
|
-
const op = pipelineSteps[pi];
|
|
731
|
-
if (!op || typeof op !== 'object') {
|
|
732
|
-
errors.push(`pipeline step ${pi}: must be an object`);
|
|
733
|
-
continue;
|
|
734
|
-
}
|
|
735
|
-
const hasMicroOp = op.find || op.waitFor || op.sleep !== undefined || op.return;
|
|
736
|
-
if (!hasMicroOp) {
|
|
737
|
-
errors.push(`pipeline step ${pi}: unrecognized micro-op (need find, waitFor, sleep, or return)`);
|
|
738
|
-
}
|
|
739
|
-
if (op.find) {
|
|
740
|
-
const hasAction = op.fill !== undefined || op.click !== undefined ||
|
|
741
|
-
op.type !== undefined || op.check !== undefined || op.select !== undefined;
|
|
742
|
-
if (!hasAction) {
|
|
743
|
-
errors.push(`pipeline step ${pi}: find requires an action (fill, click, type, check, or select)`);
|
|
744
|
-
}
|
|
745
|
-
}
|
|
746
|
-
}
|
|
747
|
-
}
|
|
748
|
-
if (!Array.isArray(params) && params && params.timeout !== undefined) {
|
|
749
|
-
if (typeof params.timeout !== 'number' || params.timeout < 0) {
|
|
750
|
-
errors.push('pipeline timeout must be a non-negative number');
|
|
751
|
-
}
|
|
752
|
-
}
|
|
753
|
-
}
|
|
754
|
-
break;
|
|
755
|
-
|
|
756
|
-
case 'writeSiteProfile':
|
|
757
|
-
if (!params || typeof params !== 'object') {
|
|
758
|
-
errors.push('writeSiteProfile requires an object with domain and content');
|
|
759
|
-
} else {
|
|
760
|
-
if (!params.domain || typeof params.domain !== 'string') {
|
|
761
|
-
errors.push('writeSiteProfile requires a non-empty domain string');
|
|
762
|
-
}
|
|
763
|
-
if (!params.content || typeof params.content !== 'string') {
|
|
764
|
-
errors.push('writeSiteProfile requires a non-empty content string');
|
|
765
|
-
}
|
|
766
|
-
}
|
|
767
|
-
break;
|
|
768
|
-
|
|
769
|
-
case 'readSiteProfile':
|
|
770
|
-
if (typeof params === 'string') {
|
|
771
|
-
if (params.length === 0) {
|
|
772
|
-
errors.push('readSiteProfile requires a non-empty domain string');
|
|
773
|
-
}
|
|
774
|
-
} else if (params && typeof params === 'object') {
|
|
775
|
-
if (!params.domain || typeof params.domain !== 'string') {
|
|
776
|
-
errors.push('readSiteProfile requires a non-empty domain string');
|
|
777
|
-
}
|
|
778
|
-
} else {
|
|
779
|
-
errors.push('readSiteProfile requires a domain string or object with domain');
|
|
780
|
-
}
|
|
781
|
-
break;
|
|
44
|
+
// Get step configuration from registry
|
|
45
|
+
const stepConfig = getStepConfig(action);
|
|
46
|
+
if (!stepConfig) {
|
|
47
|
+
errors.push(`No configuration found for step type: ${action}`);
|
|
48
|
+
return errors;
|
|
782
49
|
}
|
|
783
50
|
|
|
51
|
+
// Run step-specific validation from registry
|
|
52
|
+
const stepErrors = stepConfig.validate(params);
|
|
53
|
+
errors.push(...stepErrors);
|
|
54
|
+
|
|
784
55
|
// Validate hooks on action steps (readyWhen, settledWhen, observe)
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
errors.push('readyWhen must be a function string');
|
|
788
|
-
}
|
|
789
|
-
if (params.settledWhen !== undefined && typeof params.settledWhen !== 'string') {
|
|
790
|
-
errors.push('settledWhen must be a function string');
|
|
791
|
-
}
|
|
792
|
-
if (params.observe !== undefined && typeof params.observe !== 'string') {
|
|
793
|
-
errors.push('observe must be a function string');
|
|
794
|
-
}
|
|
795
|
-
}
|
|
56
|
+
const hookErrors = validateHooks(params);
|
|
57
|
+
errors.push(...hookErrors);
|
|
796
58
|
|
|
797
59
|
return errors;
|
|
798
60
|
}
|