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
|
@@ -0,0 +1,1064 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Step Registry
|
|
3
|
+
* Centralized registry for step validation, execution, and metadata
|
|
4
|
+
* Eliminates duplication between step-validator.js and step-executors.js
|
|
5
|
+
*
|
|
6
|
+
* EXPORTS:
|
|
7
|
+
* - STEP_TYPES: Object mapping step names to their string keys
|
|
8
|
+
* - STEP_CONFIG: Registry of all step configurations
|
|
9
|
+
* - getStepConfig(stepType): Get configuration for a specific step type
|
|
10
|
+
* - getAllStepTypes(): Get array of all valid step type strings
|
|
11
|
+
* - getVisualActions(): Get array of actions that trigger screenshots
|
|
12
|
+
*
|
|
13
|
+
* DEPENDENCIES: None (pure registry)
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Step type constants
|
|
18
|
+
*/
|
|
19
|
+
export const STEP_TYPES = {
|
|
20
|
+
GOTO: 'goto',
|
|
21
|
+
RELOAD: 'reload',
|
|
22
|
+
WAIT: 'wait',
|
|
23
|
+
SLEEP: 'sleep',
|
|
24
|
+
CLICK: 'click',
|
|
25
|
+
FILL: 'fill',
|
|
26
|
+
PRESS: 'press',
|
|
27
|
+
QUERY: 'query',
|
|
28
|
+
QUERY_ALL: 'queryAll',
|
|
29
|
+
INSPECT: 'inspect',
|
|
30
|
+
SCROLL: 'scroll',
|
|
31
|
+
CONSOLE: 'console',
|
|
32
|
+
PDF: 'pdf',
|
|
33
|
+
SNAPSHOT: 'snapshot',
|
|
34
|
+
SNAPSHOT_SEARCH: 'snapshotSearch',
|
|
35
|
+
HOVER: 'hover',
|
|
36
|
+
VIEWPORT: 'viewport',
|
|
37
|
+
COOKIES: 'cookies',
|
|
38
|
+
BACK: 'back',
|
|
39
|
+
FORWARD: 'forward',
|
|
40
|
+
WAIT_FOR_NAVIGATION: 'waitForNavigation',
|
|
41
|
+
LIST_TABS: 'listTabs',
|
|
42
|
+
CLOSE_TAB: 'closeTab',
|
|
43
|
+
NEW_TAB: 'newTab',
|
|
44
|
+
SELECT_TEXT: 'selectText',
|
|
45
|
+
SELECT_OPTION: 'selectOption',
|
|
46
|
+
SUBMIT: 'submit',
|
|
47
|
+
ASSERT: 'assert',
|
|
48
|
+
FRAME: 'frame',
|
|
49
|
+
DRAG: 'drag',
|
|
50
|
+
GET: 'get',
|
|
51
|
+
GET_DOM: 'getDom',
|
|
52
|
+
GET_BOX: 'getBox',
|
|
53
|
+
ELEMENTS_AT: 'elementsAt',
|
|
54
|
+
PAGE_FUNCTION: 'pageFunction',
|
|
55
|
+
POLL: 'poll',
|
|
56
|
+
WRITE_SITE_PROFILE: 'writeSiteProfile',
|
|
57
|
+
READ_SITE_PROFILE: 'readSiteProfile',
|
|
58
|
+
SWITCH_TAB: 'switchTab',
|
|
59
|
+
GET_URL: 'getUrl',
|
|
60
|
+
GET_TITLE: 'getTitle'
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Step configuration registry
|
|
65
|
+
* Each step has: validate, isVisual, and hooks configuration
|
|
66
|
+
*/
|
|
67
|
+
export const STEP_CONFIG = {
|
|
68
|
+
[STEP_TYPES.GOTO]: {
|
|
69
|
+
validate: (params) => {
|
|
70
|
+
const errors = [];
|
|
71
|
+
if (typeof params === 'string') {
|
|
72
|
+
if (params.length === 0) {
|
|
73
|
+
errors.push('goto requires a non-empty URL string');
|
|
74
|
+
}
|
|
75
|
+
} else if (params && typeof params === 'object') {
|
|
76
|
+
if (!params.url || typeof params.url !== 'string') {
|
|
77
|
+
errors.push('goto requires a non-empty url property');
|
|
78
|
+
}
|
|
79
|
+
if (params.waitUntil !== undefined) {
|
|
80
|
+
const validWaitUntil = ['commit', 'domcontentloaded', 'load', 'networkidle'];
|
|
81
|
+
if (!validWaitUntil.includes(params.waitUntil)) {
|
|
82
|
+
errors.push(`goto waitUntil must be one of: ${validWaitUntil.join(', ')}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
} else {
|
|
86
|
+
errors.push('goto requires a URL string or object with url property');
|
|
87
|
+
}
|
|
88
|
+
return errors;
|
|
89
|
+
},
|
|
90
|
+
isVisual: true,
|
|
91
|
+
hooks: ['readyWhen', 'settledWhen', 'observe']
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
[STEP_TYPES.RELOAD]: {
|
|
95
|
+
validate: (params) => {
|
|
96
|
+
const errors = [];
|
|
97
|
+
if (params !== true && (typeof params !== 'object' || params === null)) {
|
|
98
|
+
errors.push('reload requires true or params object');
|
|
99
|
+
}
|
|
100
|
+
if (typeof params === 'object' && params !== null && params.waitUntil !== undefined) {
|
|
101
|
+
const validWaitUntil = ['commit', 'domcontentloaded', 'load', 'networkidle'];
|
|
102
|
+
if (!validWaitUntil.includes(params.waitUntil)) {
|
|
103
|
+
errors.push(`reload waitUntil must be one of: ${validWaitUntil.join(', ')}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return errors;
|
|
107
|
+
},
|
|
108
|
+
isVisual: true,
|
|
109
|
+
hooks: ['readyWhen', 'settledWhen', 'observe']
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
[STEP_TYPES.WAIT]: {
|
|
113
|
+
validate: (params) => {
|
|
114
|
+
const errors = [];
|
|
115
|
+
if (typeof params === 'number') {
|
|
116
|
+
errors.push('wait no longer accepts a number — use { "sleep": N } for time delays');
|
|
117
|
+
} else if (typeof params === 'string') {
|
|
118
|
+
if (params.length === 0) {
|
|
119
|
+
errors.push('wait selector cannot be empty');
|
|
120
|
+
}
|
|
121
|
+
} else if (params && typeof params === 'object') {
|
|
122
|
+
const hasSelector = params.selector !== undefined;
|
|
123
|
+
const hasText = params.text !== undefined;
|
|
124
|
+
const hasTextRegex = params.textRegex !== undefined;
|
|
125
|
+
const hasTime = params.time !== undefined;
|
|
126
|
+
const hasUrlContains = params.urlContains !== undefined;
|
|
127
|
+
if (hasTime) {
|
|
128
|
+
errors.push('wait no longer accepts time — use { "sleep": N } for time delays');
|
|
129
|
+
}
|
|
130
|
+
if (!hasSelector && !hasText && !hasTextRegex && !hasTime && !hasUrlContains) {
|
|
131
|
+
errors.push('wait requires selector, text, textRegex, or urlContains');
|
|
132
|
+
}
|
|
133
|
+
if (hasSelector && typeof params.selector !== 'string') {
|
|
134
|
+
errors.push('wait selector must be a string');
|
|
135
|
+
}
|
|
136
|
+
if (hasText && typeof params.text !== 'string') {
|
|
137
|
+
errors.push('wait text must be a string');
|
|
138
|
+
}
|
|
139
|
+
if (hasTextRegex && typeof params.textRegex !== 'string') {
|
|
140
|
+
errors.push('wait textRegex must be a string');
|
|
141
|
+
}
|
|
142
|
+
if (hasUrlContains && typeof params.urlContains !== 'string') {
|
|
143
|
+
errors.push('wait urlContains must be a string');
|
|
144
|
+
}
|
|
145
|
+
if (params.minCount !== undefined && (typeof params.minCount !== 'number' || params.minCount < 0)) {
|
|
146
|
+
errors.push('wait minCount must be a non-negative number');
|
|
147
|
+
}
|
|
148
|
+
if (params.caseSensitive !== undefined && typeof params.caseSensitive !== 'boolean') {
|
|
149
|
+
errors.push('wait caseSensitive must be a boolean');
|
|
150
|
+
}
|
|
151
|
+
if (params.hidden !== undefined && typeof params.hidden !== 'boolean') {
|
|
152
|
+
errors.push('wait hidden must be a boolean');
|
|
153
|
+
}
|
|
154
|
+
} else {
|
|
155
|
+
errors.push('wait requires a number (ms), selector string, or params object');
|
|
156
|
+
}
|
|
157
|
+
return errors;
|
|
158
|
+
},
|
|
159
|
+
isVisual: true,
|
|
160
|
+
hooks: ['readyWhen', 'settledWhen', 'observe']
|
|
161
|
+
},
|
|
162
|
+
|
|
163
|
+
[STEP_TYPES.SLEEP]: {
|
|
164
|
+
validate: (params) => {
|
|
165
|
+
const errors = [];
|
|
166
|
+
if (typeof params !== 'number') {
|
|
167
|
+
errors.push('sleep requires a number (milliseconds)');
|
|
168
|
+
} else if (params < 0) {
|
|
169
|
+
errors.push('sleep time must be non-negative');
|
|
170
|
+
} else if (params > 60000) {
|
|
171
|
+
errors.push('sleep time must not exceed 60000ms');
|
|
172
|
+
}
|
|
173
|
+
return errors;
|
|
174
|
+
},
|
|
175
|
+
isVisual: false,
|
|
176
|
+
hooks: []
|
|
177
|
+
},
|
|
178
|
+
|
|
179
|
+
[STEP_TYPES.CLICK]: {
|
|
180
|
+
validate: (params) => {
|
|
181
|
+
const errors = [];
|
|
182
|
+
if (typeof params === 'string') {
|
|
183
|
+
if (params.length === 0) {
|
|
184
|
+
errors.push('click selector cannot be empty');
|
|
185
|
+
}
|
|
186
|
+
} else if (params && typeof params === 'object') {
|
|
187
|
+
const hasCoordinates = typeof params.x === 'number' && typeof params.y === 'number';
|
|
188
|
+
const hasText = typeof params.text === 'string';
|
|
189
|
+
const hasSelectors = Array.isArray(params.selectors);
|
|
190
|
+
if (!params.selector && !params.ref && !hasCoordinates && !hasText && !hasSelectors) {
|
|
191
|
+
errors.push('click requires selector, ref, text, selectors array, or x/y coordinates');
|
|
192
|
+
} else if (params.selector && typeof params.selector !== 'string') {
|
|
193
|
+
errors.push('click selector must be a string');
|
|
194
|
+
} else if (params.ref && typeof params.ref !== 'string') {
|
|
195
|
+
errors.push('click ref must be a string');
|
|
196
|
+
} else if (hasText && params.text.length === 0) {
|
|
197
|
+
errors.push('click text cannot be empty');
|
|
198
|
+
} else if (hasSelectors && params.selectors.length === 0) {
|
|
199
|
+
errors.push('click selectors array cannot be empty');
|
|
200
|
+
} else if (hasCoordinates) {
|
|
201
|
+
if (params.x < 0 || params.y < 0) {
|
|
202
|
+
errors.push('click coordinates must be non-negative');
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
} else {
|
|
206
|
+
errors.push('click requires a selector string or params object');
|
|
207
|
+
}
|
|
208
|
+
return errors;
|
|
209
|
+
},
|
|
210
|
+
isVisual: true,
|
|
211
|
+
hooks: ['readyWhen', 'settledWhen', 'observe']
|
|
212
|
+
},
|
|
213
|
+
|
|
214
|
+
[STEP_TYPES.FILL]: {
|
|
215
|
+
validate: (params) => {
|
|
216
|
+
const errors = [];
|
|
217
|
+
if (typeof params === 'string') {
|
|
218
|
+
// Shape 1: focused mode — string value is fine (even empty)
|
|
219
|
+
} else if (params && typeof params === 'object') {
|
|
220
|
+
const hasTargeting = params.selector || params.ref || params.label;
|
|
221
|
+
const hasFields = params.fields && typeof params.fields === 'object';
|
|
222
|
+
|
|
223
|
+
if (hasTargeting) {
|
|
224
|
+
// Shape 2: single field with targeting
|
|
225
|
+
if (params.selector && typeof params.selector !== 'string') {
|
|
226
|
+
errors.push('fill selector must be a string');
|
|
227
|
+
} else if (params.ref && typeof params.ref !== 'string') {
|
|
228
|
+
errors.push('fill ref must be a string');
|
|
229
|
+
} else if (params.label && typeof params.label !== 'string') {
|
|
230
|
+
errors.push('fill label must be a string');
|
|
231
|
+
}
|
|
232
|
+
if (params.value === undefined) {
|
|
233
|
+
errors.push('fill requires value');
|
|
234
|
+
}
|
|
235
|
+
} else if (hasFields) {
|
|
236
|
+
// Shape 4: batch with options
|
|
237
|
+
const entries = Object.entries(params.fields);
|
|
238
|
+
if (entries.length === 0) {
|
|
239
|
+
errors.push('fill requires at least one field');
|
|
240
|
+
}
|
|
241
|
+
if (params.react !== undefined && typeof params.react !== 'boolean') {
|
|
242
|
+
errors.push('fill react option must be a boolean');
|
|
243
|
+
}
|
|
244
|
+
} else if (params.value !== undefined) {
|
|
245
|
+
// Shape 3: focused with options (has value but no targeting key)
|
|
246
|
+
} else {
|
|
247
|
+
// Shape 5: batch (plain object mapping selectors→values)
|
|
248
|
+
const optionKeys = new Set(['clear', 'react', 'force', 'exact', 'timeout', 'readyWhen', 'settledWhen', 'observe', 'optional']);
|
|
249
|
+
const fieldEntries = Object.entries(params).filter(([k]) => !optionKeys.has(k));
|
|
250
|
+
if (fieldEntries.length === 0) {
|
|
251
|
+
errors.push('fill requires at least one field mapping (selector → value)');
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
} else {
|
|
255
|
+
errors.push('fill requires a string, object with selector/ref/label and value, or object mapping selectors to values');
|
|
256
|
+
}
|
|
257
|
+
return errors;
|
|
258
|
+
},
|
|
259
|
+
isVisual: true,
|
|
260
|
+
hooks: ['readyWhen', 'settledWhen', 'observe']
|
|
261
|
+
},
|
|
262
|
+
|
|
263
|
+
[STEP_TYPES.PRESS]: {
|
|
264
|
+
validate: (params) => {
|
|
265
|
+
const errors = [];
|
|
266
|
+
if (typeof params !== 'string' || params.length === 0) {
|
|
267
|
+
errors.push('press requires a non-empty key string');
|
|
268
|
+
}
|
|
269
|
+
return errors;
|
|
270
|
+
},
|
|
271
|
+
isVisual: true,
|
|
272
|
+
hooks: ['readyWhen', 'settledWhen', 'observe']
|
|
273
|
+
},
|
|
274
|
+
|
|
275
|
+
[STEP_TYPES.QUERY]: {
|
|
276
|
+
validate: (params) => {
|
|
277
|
+
const errors = [];
|
|
278
|
+
if (typeof params === 'string') {
|
|
279
|
+
if (params.length === 0) {
|
|
280
|
+
errors.push('query selector cannot be empty');
|
|
281
|
+
}
|
|
282
|
+
} else if (params && typeof params === 'object') {
|
|
283
|
+
if (!params.selector && !params.role) {
|
|
284
|
+
errors.push('query requires selector or role');
|
|
285
|
+
}
|
|
286
|
+
if (params.role) {
|
|
287
|
+
if (typeof params.role !== 'string' && !Array.isArray(params.role)) {
|
|
288
|
+
errors.push('query role must be a string or array of strings');
|
|
289
|
+
}
|
|
290
|
+
if (Array.isArray(params.role) && !params.role.every(r => typeof r === 'string')) {
|
|
291
|
+
errors.push('query role array must contain only strings');
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
if (params.nameExact && params.nameRegex) {
|
|
295
|
+
errors.push('query cannot have both nameExact and nameRegex');
|
|
296
|
+
}
|
|
297
|
+
} else {
|
|
298
|
+
errors.push('query requires a selector string or params object');
|
|
299
|
+
}
|
|
300
|
+
return errors;
|
|
301
|
+
},
|
|
302
|
+
isVisual: true,
|
|
303
|
+
hooks: ['readyWhen', 'settledWhen', 'observe']
|
|
304
|
+
},
|
|
305
|
+
|
|
306
|
+
[STEP_TYPES.QUERY_ALL]: {
|
|
307
|
+
validate: (params) => {
|
|
308
|
+
const errors = [];
|
|
309
|
+
if (!params || typeof params !== 'object') {
|
|
310
|
+
errors.push('queryAll requires an object mapping names to selectors');
|
|
311
|
+
} else {
|
|
312
|
+
const entries = Object.entries(params);
|
|
313
|
+
if (entries.length === 0) {
|
|
314
|
+
errors.push('queryAll requires at least one query');
|
|
315
|
+
}
|
|
316
|
+
for (const [name, selector] of entries) {
|
|
317
|
+
if (typeof selector !== 'string' && typeof selector !== 'object') {
|
|
318
|
+
errors.push(`queryAll "${name}" must be a selector string or query object`);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return errors;
|
|
323
|
+
},
|
|
324
|
+
isVisual: true,
|
|
325
|
+
hooks: ['readyWhen', 'settledWhen', 'observe']
|
|
326
|
+
},
|
|
327
|
+
|
|
328
|
+
[STEP_TYPES.INSPECT]: {
|
|
329
|
+
validate: (params) => {
|
|
330
|
+
const errors = [];
|
|
331
|
+
if (params !== true && params !== false && (typeof params !== 'object' || params === null)) {
|
|
332
|
+
errors.push('inspect requires true, false, or params object');
|
|
333
|
+
}
|
|
334
|
+
return errors;
|
|
335
|
+
},
|
|
336
|
+
isVisual: true,
|
|
337
|
+
hooks: ['readyWhen', 'settledWhen', 'observe']
|
|
338
|
+
},
|
|
339
|
+
|
|
340
|
+
[STEP_TYPES.SCROLL]: {
|
|
341
|
+
validate: (params) => {
|
|
342
|
+
const errors = [];
|
|
343
|
+
if (typeof params === 'string') {
|
|
344
|
+
if (!['top', 'bottom', 'up', 'down'].includes(params)) {
|
|
345
|
+
errors.push(`Invalid scroll direction: ${params}. Must be one of: top, bottom, up, down`);
|
|
346
|
+
}
|
|
347
|
+
} else if (params && typeof params === 'object') {
|
|
348
|
+
// selector, x, y, deltaX, deltaY are all valid
|
|
349
|
+
} else {
|
|
350
|
+
errors.push('scroll requires direction string or params object');
|
|
351
|
+
}
|
|
352
|
+
return errors;
|
|
353
|
+
},
|
|
354
|
+
isVisual: true,
|
|
355
|
+
hooks: ['readyWhen', 'settledWhen', 'observe']
|
|
356
|
+
},
|
|
357
|
+
|
|
358
|
+
[STEP_TYPES.CONSOLE]: {
|
|
359
|
+
validate: (params) => {
|
|
360
|
+
const errors = [];
|
|
361
|
+
if (params !== true && params !== false && typeof params !== 'object') {
|
|
362
|
+
errors.push('console requires true, false, or params object');
|
|
363
|
+
}
|
|
364
|
+
return errors;
|
|
365
|
+
},
|
|
366
|
+
isVisual: false,
|
|
367
|
+
hooks: []
|
|
368
|
+
},
|
|
369
|
+
|
|
370
|
+
[STEP_TYPES.PDF]: {
|
|
371
|
+
validate: (params) => {
|
|
372
|
+
const errors = [];
|
|
373
|
+
if (typeof params === 'string') {
|
|
374
|
+
if (params.length === 0) {
|
|
375
|
+
errors.push('pdf path cannot be empty');
|
|
376
|
+
}
|
|
377
|
+
} else if (params && typeof params === 'object') {
|
|
378
|
+
if (!params.path) {
|
|
379
|
+
errors.push('pdf requires path');
|
|
380
|
+
} else if (typeof params.path !== 'string') {
|
|
381
|
+
errors.push('pdf path must be a string');
|
|
382
|
+
}
|
|
383
|
+
} else {
|
|
384
|
+
errors.push('pdf requires a path string or params object');
|
|
385
|
+
}
|
|
386
|
+
return errors;
|
|
387
|
+
},
|
|
388
|
+
isVisual: false,
|
|
389
|
+
hooks: []
|
|
390
|
+
},
|
|
391
|
+
|
|
392
|
+
[STEP_TYPES.SNAPSHOT]: {
|
|
393
|
+
validate: (params) => {
|
|
394
|
+
const errors = [];
|
|
395
|
+
if (params !== true && params !== false && (typeof params !== 'object' || params === null)) {
|
|
396
|
+
errors.push('snapshot requires true or params object');
|
|
397
|
+
}
|
|
398
|
+
if (typeof params === 'object' && params !== null) {
|
|
399
|
+
if (params.mode && !['ai', 'full'].includes(params.mode)) {
|
|
400
|
+
errors.push('snapshot mode must be "ai" or "full"');
|
|
401
|
+
}
|
|
402
|
+
if (params.detail && !['summary', 'interactive', 'full'].includes(params.detail)) {
|
|
403
|
+
errors.push('snapshot detail must be "summary", "interactive", or "full"');
|
|
404
|
+
}
|
|
405
|
+
if (params.inlineLimit !== undefined && (typeof params.inlineLimit !== 'number' || params.inlineLimit < 0)) {
|
|
406
|
+
errors.push('snapshot inlineLimit must be a non-negative number');
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
return errors;
|
|
410
|
+
},
|
|
411
|
+
isVisual: true,
|
|
412
|
+
hooks: ['readyWhen', 'settledWhen', 'observe']
|
|
413
|
+
},
|
|
414
|
+
|
|
415
|
+
[STEP_TYPES.SNAPSHOT_SEARCH]: {
|
|
416
|
+
validate: (params) => {
|
|
417
|
+
const errors = [];
|
|
418
|
+
if (!params || typeof params !== 'object') {
|
|
419
|
+
errors.push('snapshotSearch requires an object with search parameters');
|
|
420
|
+
} else {
|
|
421
|
+
const hasText = params.text !== undefined;
|
|
422
|
+
const hasPattern = params.pattern !== undefined;
|
|
423
|
+
const hasRole = params.role !== undefined;
|
|
424
|
+
if (!hasText && !hasPattern && !hasRole) {
|
|
425
|
+
errors.push('snapshotSearch requires at least one of: text, pattern, or role');
|
|
426
|
+
}
|
|
427
|
+
if (hasText && typeof params.text !== 'string') {
|
|
428
|
+
errors.push('snapshotSearch text must be a string');
|
|
429
|
+
}
|
|
430
|
+
if (hasPattern && typeof params.pattern !== 'string') {
|
|
431
|
+
errors.push('snapshotSearch pattern must be a string (regex)');
|
|
432
|
+
}
|
|
433
|
+
if (hasRole && typeof params.role !== 'string') {
|
|
434
|
+
errors.push('snapshotSearch role must be a string');
|
|
435
|
+
}
|
|
436
|
+
if (params.limit !== undefined && (typeof params.limit !== 'number' || params.limit < 1)) {
|
|
437
|
+
errors.push('snapshotSearch limit must be a positive number');
|
|
438
|
+
}
|
|
439
|
+
if (params.context !== undefined && (typeof params.context !== 'number' || params.context < 0)) {
|
|
440
|
+
errors.push('snapshotSearch context must be a non-negative number');
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
return errors;
|
|
444
|
+
},
|
|
445
|
+
isVisual: true,
|
|
446
|
+
hooks: ['readyWhen', 'settledWhen', 'observe']
|
|
447
|
+
},
|
|
448
|
+
|
|
449
|
+
[STEP_TYPES.HOVER]: {
|
|
450
|
+
validate: (params) => {
|
|
451
|
+
const errors = [];
|
|
452
|
+
if (typeof params === 'string') {
|
|
453
|
+
if (params.length === 0) {
|
|
454
|
+
errors.push('hover selector cannot be empty');
|
|
455
|
+
}
|
|
456
|
+
} else if (params && typeof params === 'object') {
|
|
457
|
+
const hasCoordinates = typeof params.x === 'number' && typeof params.y === 'number';
|
|
458
|
+
const hasText = typeof params.text === 'string';
|
|
459
|
+
if (!params.selector && !params.ref && !hasCoordinates && !hasText) {
|
|
460
|
+
errors.push('hover requires selector, ref, text, or x/y coordinates');
|
|
461
|
+
}
|
|
462
|
+
if (hasText && params.text.length === 0) {
|
|
463
|
+
errors.push('hover text cannot be empty');
|
|
464
|
+
}
|
|
465
|
+
if (hasCoordinates && (params.x < 0 || params.y < 0)) {
|
|
466
|
+
errors.push('hover coordinates must be non-negative');
|
|
467
|
+
}
|
|
468
|
+
} else {
|
|
469
|
+
errors.push('hover requires a selector string or params object');
|
|
470
|
+
}
|
|
471
|
+
return errors;
|
|
472
|
+
},
|
|
473
|
+
isVisual: true,
|
|
474
|
+
hooks: ['readyWhen', 'settledWhen', 'observe']
|
|
475
|
+
},
|
|
476
|
+
|
|
477
|
+
[STEP_TYPES.VIEWPORT]: {
|
|
478
|
+
validate: (params) => {
|
|
479
|
+
const errors = [];
|
|
480
|
+
if (typeof params === 'string') {
|
|
481
|
+
if (params.length === 0) {
|
|
482
|
+
errors.push('viewport preset name cannot be empty');
|
|
483
|
+
}
|
|
484
|
+
} else if (params && typeof params === 'object') {
|
|
485
|
+
if (!params.width || typeof params.width !== 'number') {
|
|
486
|
+
errors.push('viewport requires numeric width');
|
|
487
|
+
}
|
|
488
|
+
if (!params.height || typeof params.height !== 'number') {
|
|
489
|
+
errors.push('viewport requires numeric height');
|
|
490
|
+
}
|
|
491
|
+
} else {
|
|
492
|
+
errors.push('viewport requires a device preset string or object with width and height');
|
|
493
|
+
}
|
|
494
|
+
return errors;
|
|
495
|
+
},
|
|
496
|
+
isVisual: false,
|
|
497
|
+
hooks: []
|
|
498
|
+
},
|
|
499
|
+
|
|
500
|
+
[STEP_TYPES.COOKIES]: {
|
|
501
|
+
validate: (params) => {
|
|
502
|
+
const errors = [];
|
|
503
|
+
if (!params || typeof params !== 'object') {
|
|
504
|
+
errors.push('cookies requires an object with action (get, set, or clear)');
|
|
505
|
+
} else {
|
|
506
|
+
if (params.set && !Array.isArray(params.set)) {
|
|
507
|
+
errors.push('cookies set requires an array of cookie objects');
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
return errors;
|
|
511
|
+
},
|
|
512
|
+
isVisual: false,
|
|
513
|
+
hooks: []
|
|
514
|
+
},
|
|
515
|
+
|
|
516
|
+
[STEP_TYPES.BACK]: {
|
|
517
|
+
validate: (params) => {
|
|
518
|
+
const errors = [];
|
|
519
|
+
if (params !== true && (typeof params !== 'object' || params === null)) {
|
|
520
|
+
errors.push('back requires true or params object');
|
|
521
|
+
}
|
|
522
|
+
return errors;
|
|
523
|
+
},
|
|
524
|
+
isVisual: false,
|
|
525
|
+
hooks: []
|
|
526
|
+
},
|
|
527
|
+
|
|
528
|
+
[STEP_TYPES.FORWARD]: {
|
|
529
|
+
validate: (params) => {
|
|
530
|
+
const errors = [];
|
|
531
|
+
if (params !== true && (typeof params !== 'object' || params === null)) {
|
|
532
|
+
errors.push('forward requires true or params object');
|
|
533
|
+
}
|
|
534
|
+
return errors;
|
|
535
|
+
},
|
|
536
|
+
isVisual: false,
|
|
537
|
+
hooks: []
|
|
538
|
+
},
|
|
539
|
+
|
|
540
|
+
[STEP_TYPES.WAIT_FOR_NAVIGATION]: {
|
|
541
|
+
validate: (params) => {
|
|
542
|
+
const errors = [];
|
|
543
|
+
if (params !== true && (typeof params !== 'object' || params === null)) {
|
|
544
|
+
errors.push('waitForNavigation requires true or params object');
|
|
545
|
+
}
|
|
546
|
+
if (typeof params === 'object' && params !== null && params.timeout !== undefined) {
|
|
547
|
+
if (typeof params.timeout !== 'number' || params.timeout < 0) {
|
|
548
|
+
errors.push('waitForNavigation timeout must be a non-negative number');
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
return errors;
|
|
552
|
+
},
|
|
553
|
+
isVisual: false,
|
|
554
|
+
hooks: []
|
|
555
|
+
},
|
|
556
|
+
|
|
557
|
+
[STEP_TYPES.LIST_TABS]: {
|
|
558
|
+
validate: (params) => {
|
|
559
|
+
const errors = [];
|
|
560
|
+
if (params !== true) {
|
|
561
|
+
errors.push('listTabs requires true');
|
|
562
|
+
}
|
|
563
|
+
return errors;
|
|
564
|
+
},
|
|
565
|
+
isVisual: false,
|
|
566
|
+
hooks: []
|
|
567
|
+
},
|
|
568
|
+
|
|
569
|
+
[STEP_TYPES.CLOSE_TAB]: {
|
|
570
|
+
validate: (params) => {
|
|
571
|
+
const errors = [];
|
|
572
|
+
if (typeof params !== 'string' || params.length === 0) {
|
|
573
|
+
errors.push('closeTab requires a non-empty targetId string');
|
|
574
|
+
}
|
|
575
|
+
return errors;
|
|
576
|
+
},
|
|
577
|
+
isVisual: false,
|
|
578
|
+
hooks: []
|
|
579
|
+
},
|
|
580
|
+
|
|
581
|
+
[STEP_TYPES.NEW_TAB]: {
|
|
582
|
+
validate: (params) => {
|
|
583
|
+
const errors = [];
|
|
584
|
+
if (params !== true && typeof params !== 'string' && (typeof params !== 'object' || params === null)) {
|
|
585
|
+
errors.push('newTab must be true, a URL string, or an options object');
|
|
586
|
+
}
|
|
587
|
+
if (typeof params === 'object' && params !== null) {
|
|
588
|
+
if (params.url !== undefined && typeof params.url !== 'string') {
|
|
589
|
+
errors.push('newTab url must be a string');
|
|
590
|
+
}
|
|
591
|
+
if (params.host !== undefined && typeof params.host !== 'string') {
|
|
592
|
+
errors.push('newTab host must be a string');
|
|
593
|
+
}
|
|
594
|
+
if (params.port !== undefined && typeof params.port !== 'number') {
|
|
595
|
+
errors.push('newTab port must be a number');
|
|
596
|
+
}
|
|
597
|
+
if (params.headless !== undefined && typeof params.headless !== 'boolean') {
|
|
598
|
+
errors.push('newTab headless must be a boolean');
|
|
599
|
+
}
|
|
600
|
+
if (params.timeout !== undefined && typeof params.timeout !== 'number') {
|
|
601
|
+
errors.push('newTab timeout must be a number');
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
return errors;
|
|
605
|
+
},
|
|
606
|
+
isVisual: true,
|
|
607
|
+
hooks: ['readyWhen', 'settledWhen', 'observe']
|
|
608
|
+
},
|
|
609
|
+
|
|
610
|
+
[STEP_TYPES.SELECT_TEXT]: {
|
|
611
|
+
validate: (params) => {
|
|
612
|
+
const errors = [];
|
|
613
|
+
if (typeof params === 'string') {
|
|
614
|
+
if (params.length === 0) {
|
|
615
|
+
errors.push('selectText selector cannot be empty');
|
|
616
|
+
}
|
|
617
|
+
} else if (params && typeof params === 'object') {
|
|
618
|
+
if (!params.selector) {
|
|
619
|
+
errors.push('selectText requires selector');
|
|
620
|
+
} else if (typeof params.selector !== 'string') {
|
|
621
|
+
errors.push('selectText selector must be a string');
|
|
622
|
+
}
|
|
623
|
+
if (params.start !== undefined && typeof params.start !== 'number') {
|
|
624
|
+
errors.push('selectText start must be a number');
|
|
625
|
+
}
|
|
626
|
+
if (params.end !== undefined && typeof params.end !== 'number') {
|
|
627
|
+
errors.push('selectText end must be a number');
|
|
628
|
+
}
|
|
629
|
+
} else {
|
|
630
|
+
errors.push('selectText requires a selector string or params object');
|
|
631
|
+
}
|
|
632
|
+
return errors;
|
|
633
|
+
},
|
|
634
|
+
isVisual: true,
|
|
635
|
+
hooks: ['readyWhen', 'settledWhen', 'observe']
|
|
636
|
+
},
|
|
637
|
+
|
|
638
|
+
[STEP_TYPES.SELECT_OPTION]: {
|
|
639
|
+
validate: (params) => {
|
|
640
|
+
const errors = [];
|
|
641
|
+
if (!params || typeof params !== 'object') {
|
|
642
|
+
errors.push('selectOption requires an object with selector and value/label/index');
|
|
643
|
+
} else {
|
|
644
|
+
if (!params.selector) {
|
|
645
|
+
errors.push('selectOption requires selector');
|
|
646
|
+
}
|
|
647
|
+
if (params.value === undefined && params.label === undefined && params.index === undefined && !params.values) {
|
|
648
|
+
errors.push('selectOption requires value, label, index, or values');
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
return errors;
|
|
652
|
+
},
|
|
653
|
+
isVisual: true,
|
|
654
|
+
hooks: ['readyWhen', 'settledWhen', 'observe']
|
|
655
|
+
},
|
|
656
|
+
|
|
657
|
+
[STEP_TYPES.SUBMIT]: {
|
|
658
|
+
validate: (params) => {
|
|
659
|
+
const errors = [];
|
|
660
|
+
if (typeof params === 'string') {
|
|
661
|
+
if (params.length === 0) {
|
|
662
|
+
errors.push('submit requires a non-empty form selector');
|
|
663
|
+
}
|
|
664
|
+
} else if (params && typeof params === 'object') {
|
|
665
|
+
if (!params.selector) {
|
|
666
|
+
errors.push('submit requires selector');
|
|
667
|
+
} else if (typeof params.selector !== 'string') {
|
|
668
|
+
errors.push('submit selector must be a string');
|
|
669
|
+
}
|
|
670
|
+
} else {
|
|
671
|
+
errors.push('submit requires a selector string or params object');
|
|
672
|
+
}
|
|
673
|
+
return errors;
|
|
674
|
+
},
|
|
675
|
+
isVisual: true,
|
|
676
|
+
hooks: ['readyWhen', 'settledWhen', 'observe']
|
|
677
|
+
},
|
|
678
|
+
|
|
679
|
+
[STEP_TYPES.ASSERT]: {
|
|
680
|
+
validate: (params) => {
|
|
681
|
+
const errors = [];
|
|
682
|
+
if (!params || typeof params !== 'object') {
|
|
683
|
+
errors.push('assert requires an object with url, text, or selector');
|
|
684
|
+
} else {
|
|
685
|
+
const hasUrl = params.url !== undefined;
|
|
686
|
+
const hasText = params.text !== undefined;
|
|
687
|
+
if (!hasUrl && !hasText) {
|
|
688
|
+
errors.push('assert requires url or text');
|
|
689
|
+
}
|
|
690
|
+
if (hasUrl && typeof params.url !== 'object') {
|
|
691
|
+
errors.push('assert url must be an object (e.g., { contains: "..." })');
|
|
692
|
+
}
|
|
693
|
+
if (hasUrl && params.url && !params.url.contains && !params.url.equals && !params.url.startsWith && !params.url.endsWith && !params.url.matches) {
|
|
694
|
+
errors.push('assert url requires contains, equals, startsWith, endsWith, or matches');
|
|
695
|
+
}
|
|
696
|
+
if (hasText && typeof params.text !== 'string') {
|
|
697
|
+
errors.push('assert text must be a string');
|
|
698
|
+
}
|
|
699
|
+
if (params.selector && typeof params.selector !== 'string') {
|
|
700
|
+
errors.push('assert selector must be a string');
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
return errors;
|
|
704
|
+
},
|
|
705
|
+
isVisual: true,
|
|
706
|
+
hooks: ['readyWhen', 'settledWhen', 'observe']
|
|
707
|
+
},
|
|
708
|
+
|
|
709
|
+
[STEP_TYPES.FRAME]: {
|
|
710
|
+
validate: (params) => {
|
|
711
|
+
const errors = [];
|
|
712
|
+
if (typeof params === 'string') {
|
|
713
|
+
if (params.length === 0) {
|
|
714
|
+
errors.push('frame requires a non-empty selector string');
|
|
715
|
+
}
|
|
716
|
+
} else if (typeof params === 'number') {
|
|
717
|
+
if (params < 0) {
|
|
718
|
+
errors.push('frame index must be non-negative');
|
|
719
|
+
}
|
|
720
|
+
} else if (params && typeof params === 'object') {
|
|
721
|
+
// Accept any object (name, list, etc.)
|
|
722
|
+
} else {
|
|
723
|
+
errors.push('frame requires a selector, index, "top", or options object');
|
|
724
|
+
}
|
|
725
|
+
return errors;
|
|
726
|
+
},
|
|
727
|
+
isVisual: false,
|
|
728
|
+
hooks: []
|
|
729
|
+
},
|
|
730
|
+
|
|
731
|
+
[STEP_TYPES.DRAG]: {
|
|
732
|
+
validate: (params) => {
|
|
733
|
+
const errors = [];
|
|
734
|
+
if (!params || typeof params !== 'object') {
|
|
735
|
+
errors.push('drag requires an object with source and target');
|
|
736
|
+
} else {
|
|
737
|
+
if (!params.source) {
|
|
738
|
+
errors.push('drag requires a source selector or coordinates');
|
|
739
|
+
}
|
|
740
|
+
if (!params.target) {
|
|
741
|
+
errors.push('drag requires a target selector or coordinates');
|
|
742
|
+
}
|
|
743
|
+
if (params.method !== undefined && !['auto', 'mouse', 'html5'].includes(params.method)) {
|
|
744
|
+
errors.push('drag method must be "auto", "mouse", or "html5"');
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
return errors;
|
|
748
|
+
},
|
|
749
|
+
isVisual: true,
|
|
750
|
+
hooks: ['readyWhen', 'settledWhen', 'observe']
|
|
751
|
+
},
|
|
752
|
+
|
|
753
|
+
[STEP_TYPES.GET]: {
|
|
754
|
+
validate: (params) => {
|
|
755
|
+
const errors = [];
|
|
756
|
+
if (typeof params === 'string') {
|
|
757
|
+
if (params.length === 0) {
|
|
758
|
+
errors.push('get selector cannot be empty');
|
|
759
|
+
}
|
|
760
|
+
} else if (params && typeof params === 'object') {
|
|
761
|
+
if (!params.selector && !params.ref) {
|
|
762
|
+
errors.push('get requires selector or ref');
|
|
763
|
+
}
|
|
764
|
+
if (params.mode && !['text', 'html', 'value', 'box', 'attributes'].includes(params.mode)) {
|
|
765
|
+
errors.push('get mode must be one of: text, html, value, box, attributes');
|
|
766
|
+
}
|
|
767
|
+
} else {
|
|
768
|
+
errors.push('get requires a selector string or object with selector/ref');
|
|
769
|
+
}
|
|
770
|
+
return errors;
|
|
771
|
+
},
|
|
772
|
+
isVisual: true,
|
|
773
|
+
hooks: ['readyWhen', 'settledWhen', 'observe']
|
|
774
|
+
},
|
|
775
|
+
|
|
776
|
+
[STEP_TYPES.GET_DOM]: {
|
|
777
|
+
validate: (params) => {
|
|
778
|
+
const errors = [];
|
|
779
|
+
if (params !== true && typeof params !== 'string' && (typeof params !== 'object' || params === null)) {
|
|
780
|
+
errors.push('getDom requires true, a selector string, or an options object');
|
|
781
|
+
}
|
|
782
|
+
if (typeof params === 'object' && params !== null && params.selector && typeof params.selector !== 'string') {
|
|
783
|
+
errors.push('getDom selector must be a string');
|
|
784
|
+
}
|
|
785
|
+
return errors;
|
|
786
|
+
},
|
|
787
|
+
isVisual: false,
|
|
788
|
+
hooks: []
|
|
789
|
+
},
|
|
790
|
+
|
|
791
|
+
[STEP_TYPES.GET_BOX]: {
|
|
792
|
+
validate: (params) => {
|
|
793
|
+
const errors = [];
|
|
794
|
+
if (typeof params === 'string') {
|
|
795
|
+
if (!/^s\d+e\d+$/.test(params)) {
|
|
796
|
+
errors.push('getBox ref must be in format "s{N}e{M}" (e.g., "s1e1", "s2e34")');
|
|
797
|
+
}
|
|
798
|
+
} else if (Array.isArray(params)) {
|
|
799
|
+
if (params.length === 0) {
|
|
800
|
+
errors.push('getBox refs array cannot be empty');
|
|
801
|
+
}
|
|
802
|
+
for (const ref of params) {
|
|
803
|
+
if (typeof ref !== 'string' || !/^s\d+e\d+$/.test(ref)) {
|
|
804
|
+
errors.push('getBox refs must be strings in format "s{N}e{M}"');
|
|
805
|
+
break;
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
} else if (typeof params === 'object' && params !== null) {
|
|
809
|
+
if (!params.refs && !params.ref) {
|
|
810
|
+
errors.push('getBox requires ref or refs');
|
|
811
|
+
}
|
|
812
|
+
} else {
|
|
813
|
+
errors.push('getBox requires a ref string, array of refs, or options object');
|
|
814
|
+
}
|
|
815
|
+
return errors;
|
|
816
|
+
},
|
|
817
|
+
isVisual: false,
|
|
818
|
+
hooks: []
|
|
819
|
+
},
|
|
820
|
+
|
|
821
|
+
[STEP_TYPES.ELEMENTS_AT]: {
|
|
822
|
+
validate: (params) => {
|
|
823
|
+
const errors = [];
|
|
824
|
+
if (Array.isArray(params)) {
|
|
825
|
+
if (params.length === 0) {
|
|
826
|
+
errors.push('elementsAt array cannot be empty');
|
|
827
|
+
} else {
|
|
828
|
+
for (let i = 0; i < params.length; i++) {
|
|
829
|
+
const coord = params[i];
|
|
830
|
+
if (!coord || typeof coord !== 'object') {
|
|
831
|
+
errors.push(`elementsAt[${i}] must be an object with x and y`);
|
|
832
|
+
} else if (typeof coord.x !== 'number' || typeof coord.y !== 'number') {
|
|
833
|
+
errors.push(`elementsAt[${i}] requires x and y as numbers`);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
} else if (params && typeof params === 'object') {
|
|
838
|
+
if (typeof params.x !== 'number') {
|
|
839
|
+
errors.push('elementsAt requires x coordinate as a number');
|
|
840
|
+
}
|
|
841
|
+
if (typeof params.y !== 'number') {
|
|
842
|
+
errors.push('elementsAt requires y coordinate as a number');
|
|
843
|
+
}
|
|
844
|
+
if (params.radius !== undefined && typeof params.radius !== 'number') {
|
|
845
|
+
errors.push('elementsAt radius must be a number');
|
|
846
|
+
}
|
|
847
|
+
} else {
|
|
848
|
+
errors.push('elementsAt requires {x, y}, [{x,y}, ...], or {x, y, radius}');
|
|
849
|
+
}
|
|
850
|
+
return errors;
|
|
851
|
+
},
|
|
852
|
+
isVisual: false,
|
|
853
|
+
hooks: []
|
|
854
|
+
},
|
|
855
|
+
|
|
856
|
+
[STEP_TYPES.PAGE_FUNCTION]: {
|
|
857
|
+
validate: (params) => {
|
|
858
|
+
const errors = [];
|
|
859
|
+
if (typeof params === 'string') {
|
|
860
|
+
if (params.length === 0) {
|
|
861
|
+
errors.push('pageFunction requires a non-empty string');
|
|
862
|
+
}
|
|
863
|
+
} else if (params && typeof params === 'object') {
|
|
864
|
+
const hasFn = params.fn && typeof params.fn === 'string';
|
|
865
|
+
const hasExpression = params.expression && typeof params.expression === 'string';
|
|
866
|
+
if (!hasFn && !hasExpression) {
|
|
867
|
+
errors.push('pageFunction requires a non-empty fn or expression string');
|
|
868
|
+
}
|
|
869
|
+
if (params.refs !== undefined && typeof params.refs !== 'boolean') {
|
|
870
|
+
errors.push('pageFunction refs must be a boolean');
|
|
871
|
+
}
|
|
872
|
+
if (params.timeout !== undefined && (typeof params.timeout !== 'number' || params.timeout < 0)) {
|
|
873
|
+
errors.push('pageFunction timeout must be a non-negative number');
|
|
874
|
+
}
|
|
875
|
+
} else {
|
|
876
|
+
errors.push('pageFunction requires a function/expression string or params object');
|
|
877
|
+
}
|
|
878
|
+
return errors;
|
|
879
|
+
},
|
|
880
|
+
isVisual: true,
|
|
881
|
+
hooks: ['readyWhen', 'settledWhen', 'observe']
|
|
882
|
+
},
|
|
883
|
+
|
|
884
|
+
[STEP_TYPES.POLL]: {
|
|
885
|
+
validate: (params) => {
|
|
886
|
+
const errors = [];
|
|
887
|
+
if (typeof params === 'string') {
|
|
888
|
+
if (params.length === 0) {
|
|
889
|
+
errors.push('poll requires a non-empty function string');
|
|
890
|
+
}
|
|
891
|
+
} else if (params && typeof params === 'object') {
|
|
892
|
+
if (!params.fn || typeof params.fn !== 'string') {
|
|
893
|
+
errors.push('poll requires a non-empty fn string');
|
|
894
|
+
}
|
|
895
|
+
if (params.interval !== undefined && (typeof params.interval !== 'number' || params.interval < 0)) {
|
|
896
|
+
errors.push('poll interval must be a non-negative number');
|
|
897
|
+
}
|
|
898
|
+
if (params.timeout !== undefined && (typeof params.timeout !== 'number' || params.timeout < 0)) {
|
|
899
|
+
errors.push('poll timeout must be a non-negative number');
|
|
900
|
+
}
|
|
901
|
+
} else {
|
|
902
|
+
errors.push('poll requires a function string or params object');
|
|
903
|
+
}
|
|
904
|
+
return errors;
|
|
905
|
+
},
|
|
906
|
+
isVisual: false,
|
|
907
|
+
hooks: []
|
|
908
|
+
},
|
|
909
|
+
|
|
910
|
+
[STEP_TYPES.WRITE_SITE_PROFILE]: {
|
|
911
|
+
validate: (params) => {
|
|
912
|
+
const errors = [];
|
|
913
|
+
if (!params || typeof params !== 'object') {
|
|
914
|
+
errors.push('writeSiteProfile requires an object with domain and content');
|
|
915
|
+
} else {
|
|
916
|
+
const providedKeys = Object.keys(params).join(', ');
|
|
917
|
+
if (!params.domain || typeof params.domain !== 'string') {
|
|
918
|
+
errors.push(`writeSiteProfile requires a non-empty domain string (got keys: ${providedKeys})`);
|
|
919
|
+
}
|
|
920
|
+
if (!params.content || typeof params.content !== 'string') {
|
|
921
|
+
errors.push(`writeSiteProfile requires a non-empty content string (got keys: ${providedKeys})`);
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
return errors;
|
|
925
|
+
},
|
|
926
|
+
isVisual: false,
|
|
927
|
+
hooks: []
|
|
928
|
+
},
|
|
929
|
+
|
|
930
|
+
[STEP_TYPES.READ_SITE_PROFILE]: {
|
|
931
|
+
validate: (params) => {
|
|
932
|
+
const errors = [];
|
|
933
|
+
if (typeof params === 'string') {
|
|
934
|
+
if (params.length === 0) {
|
|
935
|
+
errors.push('readSiteProfile requires a non-empty domain string');
|
|
936
|
+
}
|
|
937
|
+
} else if (params && typeof params === 'object') {
|
|
938
|
+
if (!params.domain || typeof params.domain !== 'string') {
|
|
939
|
+
errors.push('readSiteProfile requires a non-empty domain string');
|
|
940
|
+
}
|
|
941
|
+
} else {
|
|
942
|
+
errors.push('readSiteProfile requires a domain string or object with domain');
|
|
943
|
+
}
|
|
944
|
+
return errors;
|
|
945
|
+
},
|
|
946
|
+
isVisual: false,
|
|
947
|
+
hooks: []
|
|
948
|
+
},
|
|
949
|
+
|
|
950
|
+
[STEP_TYPES.SWITCH_TAB]: {
|
|
951
|
+
validate: (params) => {
|
|
952
|
+
const errors = [];
|
|
953
|
+
if (typeof params === 'string') {
|
|
954
|
+
if (params.length === 0) {
|
|
955
|
+
errors.push('switchTab requires a non-empty alias or targetId string');
|
|
956
|
+
}
|
|
957
|
+
} else if (params && typeof params === 'object') {
|
|
958
|
+
if (!params.targetId && !params.url) {
|
|
959
|
+
errors.push('switchTab object requires targetId or url');
|
|
960
|
+
}
|
|
961
|
+
if (params.url !== undefined && typeof params.url !== 'string') {
|
|
962
|
+
errors.push('switchTab url must be a string (regex pattern)');
|
|
963
|
+
}
|
|
964
|
+
if (params.targetId !== undefined && typeof params.targetId !== 'string') {
|
|
965
|
+
errors.push('switchTab targetId must be a string');
|
|
966
|
+
}
|
|
967
|
+
if (params.host !== undefined && typeof params.host !== 'string') {
|
|
968
|
+
errors.push('switchTab host must be a string');
|
|
969
|
+
}
|
|
970
|
+
if (params.port !== undefined && typeof params.port !== 'number') {
|
|
971
|
+
errors.push('switchTab port must be a number');
|
|
972
|
+
}
|
|
973
|
+
} else {
|
|
974
|
+
errors.push('switchTab requires a string (alias/targetId) or object with {targetId} or {url}');
|
|
975
|
+
}
|
|
976
|
+
return errors;
|
|
977
|
+
},
|
|
978
|
+
isVisual: true,
|
|
979
|
+
hooks: ['readyWhen', 'settledWhen', 'observe']
|
|
980
|
+
},
|
|
981
|
+
|
|
982
|
+
[STEP_TYPES.GET_URL]: {
|
|
983
|
+
validate: (params) => {
|
|
984
|
+
const errors = [];
|
|
985
|
+
if (params !== true) {
|
|
986
|
+
errors.push('getUrl requires true');
|
|
987
|
+
}
|
|
988
|
+
return errors;
|
|
989
|
+
},
|
|
990
|
+
isVisual: false,
|
|
991
|
+
hooks: []
|
|
992
|
+
},
|
|
993
|
+
|
|
994
|
+
[STEP_TYPES.GET_TITLE]: {
|
|
995
|
+
validate: (params) => {
|
|
996
|
+
const errors = [];
|
|
997
|
+
if (params !== true) {
|
|
998
|
+
errors.push('getTitle requires true');
|
|
999
|
+
}
|
|
1000
|
+
return errors;
|
|
1001
|
+
},
|
|
1002
|
+
isVisual: false,
|
|
1003
|
+
hooks: []
|
|
1004
|
+
}
|
|
1005
|
+
};
|
|
1006
|
+
|
|
1007
|
+
/**
|
|
1008
|
+
* Get configuration for a specific step type
|
|
1009
|
+
* @param {string} stepType - Step type name
|
|
1010
|
+
* @returns {Object|null} Step configuration or null if not found
|
|
1011
|
+
*/
|
|
1012
|
+
export function getStepConfig(stepType) {
|
|
1013
|
+
return STEP_CONFIG[stepType] || null;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
/**
|
|
1017
|
+
* Get array of all valid step type strings
|
|
1018
|
+
* @returns {string[]} Array of step type names
|
|
1019
|
+
*/
|
|
1020
|
+
export function getAllStepTypes() {
|
|
1021
|
+
return Object.values(STEP_TYPES);
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
/**
|
|
1025
|
+
* Get array of visual action step types (trigger screenshots)
|
|
1026
|
+
* @returns {string[]} Array of visual action names
|
|
1027
|
+
*/
|
|
1028
|
+
export function getVisualActions() {
|
|
1029
|
+
return Object.values(STEP_TYPES).filter(stepType => {
|
|
1030
|
+
const config = STEP_CONFIG[stepType];
|
|
1031
|
+
return config && config.isVisual;
|
|
1032
|
+
});
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
/**
|
|
1036
|
+
* Check if a step type supports hooks (readyWhen, settledWhen, observe)
|
|
1037
|
+
* @param {string} stepType - Step type name
|
|
1038
|
+
* @returns {boolean} True if step supports hooks
|
|
1039
|
+
*/
|
|
1040
|
+
export function stepSupportsHooks(stepType) {
|
|
1041
|
+
const config = STEP_CONFIG[stepType];
|
|
1042
|
+
return config && config.hooks && config.hooks.length > 0;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
/**
|
|
1046
|
+
* Validate hook parameters on an action step
|
|
1047
|
+
* @param {Object} params - Step parameters
|
|
1048
|
+
* @returns {string[]} Array of validation errors
|
|
1049
|
+
*/
|
|
1050
|
+
export function validateHooks(params) {
|
|
1051
|
+
const errors = [];
|
|
1052
|
+
if (typeof params === 'object' && params !== null) {
|
|
1053
|
+
if (params.readyWhen !== undefined && typeof params.readyWhen !== 'string') {
|
|
1054
|
+
errors.push('readyWhen must be a function string');
|
|
1055
|
+
}
|
|
1056
|
+
if (params.settledWhen !== undefined && typeof params.settledWhen !== 'string') {
|
|
1057
|
+
errors.push('settledWhen must be a function string');
|
|
1058
|
+
}
|
|
1059
|
+
if (params.observe !== undefined && typeof params.observe !== 'string') {
|
|
1060
|
+
errors.push('observe must be a function string');
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
return errors;
|
|
1064
|
+
}
|