@showrun/core 0.1.0
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/LICENSE +21 -0
- package/dist/__tests__/dsl-validation.test.d.ts +2 -0
- package/dist/__tests__/dsl-validation.test.d.ts.map +1 -0
- package/dist/__tests__/dsl-validation.test.js +203 -0
- package/dist/__tests__/pack-versioning.test.d.ts +2 -0
- package/dist/__tests__/pack-versioning.test.d.ts.map +1 -0
- package/dist/__tests__/pack-versioning.test.js +165 -0
- package/dist/__tests__/validator.test.d.ts +2 -0
- package/dist/__tests__/validator.test.d.ts.map +1 -0
- package/dist/__tests__/validator.test.js +149 -0
- package/dist/authResilience.d.ts +146 -0
- package/dist/authResilience.d.ts.map +1 -0
- package/dist/authResilience.js +378 -0
- package/dist/browserLauncher.d.ts +74 -0
- package/dist/browserLauncher.d.ts.map +1 -0
- package/dist/browserLauncher.js +159 -0
- package/dist/browserPersistence.d.ts +49 -0
- package/dist/browserPersistence.d.ts.map +1 -0
- package/dist/browserPersistence.js +143 -0
- package/dist/context.d.ts +10 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +30 -0
- package/dist/dsl/builders.d.ts +340 -0
- package/dist/dsl/builders.d.ts.map +1 -0
- package/dist/dsl/builders.js +416 -0
- package/dist/dsl/conditions.d.ts +33 -0
- package/dist/dsl/conditions.d.ts.map +1 -0
- package/dist/dsl/conditions.js +169 -0
- package/dist/dsl/interpreter.d.ts +24 -0
- package/dist/dsl/interpreter.d.ts.map +1 -0
- package/dist/dsl/interpreter.js +491 -0
- package/dist/dsl/stepHandlers.d.ts +32 -0
- package/dist/dsl/stepHandlers.d.ts.map +1 -0
- package/dist/dsl/stepHandlers.js +787 -0
- package/dist/dsl/target.d.ts +28 -0
- package/dist/dsl/target.d.ts.map +1 -0
- package/dist/dsl/target.js +110 -0
- package/dist/dsl/templating.d.ts +21 -0
- package/dist/dsl/templating.d.ts.map +1 -0
- package/dist/dsl/templating.js +73 -0
- package/dist/dsl/types.d.ts +695 -0
- package/dist/dsl/types.d.ts.map +1 -0
- package/dist/dsl/types.js +7 -0
- package/dist/dsl/validation.d.ts +15 -0
- package/dist/dsl/validation.d.ts.map +1 -0
- package/dist/dsl/validation.js +974 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20 -0
- package/dist/jsonPackValidator.d.ts +11 -0
- package/dist/jsonPackValidator.d.ts.map +1 -0
- package/dist/jsonPackValidator.js +61 -0
- package/dist/loader.d.ts +35 -0
- package/dist/loader.d.ts.map +1 -0
- package/dist/loader.js +107 -0
- package/dist/networkCapture.d.ts +107 -0
- package/dist/networkCapture.d.ts.map +1 -0
- package/dist/networkCapture.js +390 -0
- package/dist/packUtils.d.ts +36 -0
- package/dist/packUtils.d.ts.map +1 -0
- package/dist/packUtils.js +97 -0
- package/dist/packVersioning.d.ts +25 -0
- package/dist/packVersioning.d.ts.map +1 -0
- package/dist/packVersioning.js +137 -0
- package/dist/runner.d.ts +62 -0
- package/dist/runner.d.ts.map +1 -0
- package/dist/runner.js +170 -0
- package/dist/types.d.ts +336 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/dist/validator.d.ts +20 -0
- package/dist/validator.d.ts.map +1 -0
- package/dist/validator.js +68 -0
- package/package.json +49 -0
|
@@ -0,0 +1,974 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validation errors
|
|
3
|
+
*/
|
|
4
|
+
export class ValidationError extends Error {
|
|
5
|
+
constructor(message) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = 'ValidationError';
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* If errors array is provided, push the message; otherwise throw.
|
|
12
|
+
*/
|
|
13
|
+
function addError(errors, message) {
|
|
14
|
+
if (errors) {
|
|
15
|
+
errors.push(message);
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
throw new ValidationError(message);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Validates a Target object
|
|
23
|
+
*/
|
|
24
|
+
function validateTarget(target, errors, prefix) {
|
|
25
|
+
const pfx = prefix ? `${prefix}: ` : '';
|
|
26
|
+
if (!target || typeof target !== 'object') {
|
|
27
|
+
addError(errors, `${pfx}Target must be an object`);
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
const t = target;
|
|
31
|
+
if (typeof t.kind !== 'string') {
|
|
32
|
+
addError(errors, `${pfx}Target must have a string "kind"`);
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
switch (t.kind) {
|
|
36
|
+
case 'css':
|
|
37
|
+
if (typeof t.selector !== 'string' || !t.selector) {
|
|
38
|
+
addError(errors, `${pfx}CSS target must have a non-empty string "selector"`);
|
|
39
|
+
}
|
|
40
|
+
break;
|
|
41
|
+
case 'text':
|
|
42
|
+
if (typeof t.text !== 'string' || !t.text) {
|
|
43
|
+
addError(errors, `${pfx}Text target must have a non-empty string "text"`);
|
|
44
|
+
}
|
|
45
|
+
if (t.exact !== undefined && typeof t.exact !== 'boolean') {
|
|
46
|
+
addError(errors, `${pfx}Text target "exact" must be a boolean`);
|
|
47
|
+
}
|
|
48
|
+
break;
|
|
49
|
+
case 'role': {
|
|
50
|
+
const validRoles = [
|
|
51
|
+
'button', 'checkbox', 'combobox', 'dialog', 'gridcell', 'link', 'listbox',
|
|
52
|
+
'menuitem', 'option', 'radio', 'searchbox', 'slider', 'switch', 'tab',
|
|
53
|
+
'tabpanel', 'textbox', 'treeitem', 'article', 'banner', 'complementary',
|
|
54
|
+
'contentinfo', 'form', 'main', 'navigation', 'region', 'search', 'alert',
|
|
55
|
+
'log', 'marquee', 'status', 'timer'
|
|
56
|
+
];
|
|
57
|
+
if (!validRoles.includes(t.role)) {
|
|
58
|
+
addError(errors, `${pfx}Role target must have a valid role: ${validRoles.join(', ')}`);
|
|
59
|
+
}
|
|
60
|
+
if (t.name !== undefined && typeof t.name !== 'string') {
|
|
61
|
+
addError(errors, `${pfx}Role target "name" must be a string`);
|
|
62
|
+
}
|
|
63
|
+
if (t.exact !== undefined && typeof t.exact !== 'boolean') {
|
|
64
|
+
addError(errors, `${pfx}Role target "exact" must be a boolean`);
|
|
65
|
+
}
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
case 'label':
|
|
69
|
+
if (typeof t.text !== 'string' || !t.text) {
|
|
70
|
+
addError(errors, `${pfx}Label target must have a non-empty string "text"`);
|
|
71
|
+
}
|
|
72
|
+
if (t.exact !== undefined && typeof t.exact !== 'boolean') {
|
|
73
|
+
addError(errors, `${pfx}Label target "exact" must be a boolean`);
|
|
74
|
+
}
|
|
75
|
+
break;
|
|
76
|
+
case 'placeholder':
|
|
77
|
+
if (typeof t.text !== 'string' || !t.text) {
|
|
78
|
+
addError(errors, `${pfx}Placeholder target must have a non-empty string "text"`);
|
|
79
|
+
}
|
|
80
|
+
if (t.exact !== undefined && typeof t.exact !== 'boolean') {
|
|
81
|
+
addError(errors, `${pfx}Placeholder target "exact" must be a boolean`);
|
|
82
|
+
}
|
|
83
|
+
break;
|
|
84
|
+
case 'altText':
|
|
85
|
+
if (typeof t.text !== 'string' || !t.text) {
|
|
86
|
+
addError(errors, `${pfx}AltText target must have a non-empty string "text"`);
|
|
87
|
+
}
|
|
88
|
+
if (t.exact !== undefined && typeof t.exact !== 'boolean') {
|
|
89
|
+
addError(errors, `${pfx}AltText target "exact" must be a boolean`);
|
|
90
|
+
}
|
|
91
|
+
break;
|
|
92
|
+
case 'testId':
|
|
93
|
+
if (typeof t.id !== 'string' || !t.id) {
|
|
94
|
+
addError(errors, `${pfx}TestId target must have a non-empty string "id"`);
|
|
95
|
+
}
|
|
96
|
+
break;
|
|
97
|
+
default:
|
|
98
|
+
addError(errors, `${pfx}Unknown target kind: ${t.kind}`);
|
|
99
|
+
}
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Validates TargetOrAnyOf (single target or anyOf array)
|
|
104
|
+
*/
|
|
105
|
+
function validateTargetOrAnyOf(targetOrAnyOf, errors, prefix) {
|
|
106
|
+
if (!targetOrAnyOf) {
|
|
107
|
+
return; // Optional, so empty is OK
|
|
108
|
+
}
|
|
109
|
+
const pfx = prefix ? `${prefix}: ` : '';
|
|
110
|
+
if (typeof targetOrAnyOf === 'object' && 'anyOf' in targetOrAnyOf) {
|
|
111
|
+
const anyOf = targetOrAnyOf.anyOf;
|
|
112
|
+
if (!Array.isArray(anyOf) || anyOf.length === 0) {
|
|
113
|
+
addError(errors, `${pfx}Target "anyOf" must be a non-empty array`);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
for (const target of anyOf) {
|
|
117
|
+
validateTarget(target, errors, prefix);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
validateTarget(targetOrAnyOf, errors, prefix);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Validates a SkipCondition
|
|
126
|
+
*/
|
|
127
|
+
function validateSkipCondition(condition, errors, prefix) {
|
|
128
|
+
const pfx = prefix ? `${prefix}: ` : '';
|
|
129
|
+
if (!condition || typeof condition !== 'object') {
|
|
130
|
+
addError(errors, `${pfx}skip_if condition must be an object`);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
const c = condition;
|
|
134
|
+
const keys = Object.keys(c);
|
|
135
|
+
if (keys.length !== 1) {
|
|
136
|
+
addError(errors, `${pfx}skip_if condition must have exactly one key`);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const key = keys[0];
|
|
140
|
+
switch (key) {
|
|
141
|
+
case 'url_includes':
|
|
142
|
+
if (typeof c.url_includes !== 'string' || !c.url_includes) {
|
|
143
|
+
addError(errors, `${pfx}skip_if "url_includes" must be a non-empty string`);
|
|
144
|
+
}
|
|
145
|
+
break;
|
|
146
|
+
case 'url_matches':
|
|
147
|
+
if (typeof c.url_matches !== 'string' || !c.url_matches) {
|
|
148
|
+
addError(errors, `${pfx}skip_if "url_matches" must be a non-empty string`);
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
// Validate it's a valid regex
|
|
152
|
+
try {
|
|
153
|
+
new RegExp(c.url_matches);
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
addError(errors, `${pfx}skip_if "url_matches" must be a valid regex`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
break;
|
|
160
|
+
case 'element_visible':
|
|
161
|
+
validateTargetOrAnyOf(c.element_visible, errors, prefix);
|
|
162
|
+
break;
|
|
163
|
+
case 'element_exists':
|
|
164
|
+
validateTargetOrAnyOf(c.element_exists, errors, prefix);
|
|
165
|
+
break;
|
|
166
|
+
case 'var_equals': {
|
|
167
|
+
if (!c.var_equals || typeof c.var_equals !== 'object') {
|
|
168
|
+
addError(errors, `${pfx}skip_if "var_equals" must be an object`);
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
const varEquals = c.var_equals;
|
|
172
|
+
if (typeof varEquals.name !== 'string' || !varEquals.name) {
|
|
173
|
+
addError(errors, `${pfx}skip_if "var_equals.name" must be a non-empty string`);
|
|
174
|
+
}
|
|
175
|
+
if (varEquals.value === undefined) {
|
|
176
|
+
addError(errors, `${pfx}skip_if "var_equals.value" is required`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
case 'var_truthy':
|
|
182
|
+
if (typeof c.var_truthy !== 'string' || !c.var_truthy) {
|
|
183
|
+
addError(errors, `${pfx}skip_if "var_truthy" must be a non-empty string`);
|
|
184
|
+
}
|
|
185
|
+
break;
|
|
186
|
+
case 'var_falsy':
|
|
187
|
+
if (typeof c.var_falsy !== 'string' || !c.var_falsy) {
|
|
188
|
+
addError(errors, `${pfx}skip_if "var_falsy" must be a non-empty string`);
|
|
189
|
+
}
|
|
190
|
+
break;
|
|
191
|
+
case 'all':
|
|
192
|
+
if (!Array.isArray(c.all) || c.all.length === 0) {
|
|
193
|
+
addError(errors, `${pfx}skip_if "all" must be a non-empty array`);
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
for (const subCondition of c.all) {
|
|
197
|
+
validateSkipCondition(subCondition, errors, prefix);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
break;
|
|
201
|
+
case 'any':
|
|
202
|
+
if (!Array.isArray(c.any) || c.any.length === 0) {
|
|
203
|
+
addError(errors, `${pfx}skip_if "any" must be a non-empty array`);
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
for (const subCondition of c.any) {
|
|
207
|
+
validateSkipCondition(subCondition, errors, prefix);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
break;
|
|
211
|
+
default:
|
|
212
|
+
addError(errors, `${pfx}Unknown skip_if condition type: ${key}. Valid types: url_includes, url_matches, element_visible, element_exists, var_equals, var_truthy, var_falsy, all, any`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Allowed params per step type — used to reject unknown/hallucinated keys.
|
|
217
|
+
*/
|
|
218
|
+
const ALLOWED_PARAMS = {
|
|
219
|
+
navigate: ['url', 'waitUntil'],
|
|
220
|
+
extract_title: ['out'],
|
|
221
|
+
extract_text: ['selector', 'target', 'out', 'first', 'trim', 'default', 'hint', 'scope', 'near'],
|
|
222
|
+
sleep: ['durationMs'],
|
|
223
|
+
wait_for: ['selector', 'target', 'url', 'loadState', 'visible', 'timeoutMs', 'hint', 'scope', 'near'],
|
|
224
|
+
click: ['selector', 'target', 'first', 'waitForVisible', 'hint', 'scope', 'near'],
|
|
225
|
+
fill: ['selector', 'target', 'value', 'first', 'clear', 'hint', 'scope', 'near'],
|
|
226
|
+
extract_attribute: ['selector', 'target', 'attribute', 'out', 'first', 'default', 'hint', 'scope', 'near'],
|
|
227
|
+
assert: ['selector', 'target', 'visible', 'textIncludes', 'urlIncludes', 'message', 'hint', 'scope', 'near'],
|
|
228
|
+
set_var: ['name', 'value'],
|
|
229
|
+
network_find: ['where', 'pick', 'saveAs', 'waitForMs', 'pollIntervalMs'],
|
|
230
|
+
network_replay: ['requestId', 'overrides', 'auth', 'out', 'saveAs', 'response'],
|
|
231
|
+
network_extract: ['fromVar', 'as', 'path', 'jsonPath', 'transform', 'out'],
|
|
232
|
+
select_option: ['selector', 'target', 'value', 'first', 'hint', 'scope', 'near'],
|
|
233
|
+
press_key: ['key', 'selector', 'target', 'times', 'delayMs', 'hint', 'scope', 'near'],
|
|
234
|
+
upload_file: ['selector', 'target', 'files', 'first', 'hint', 'scope', 'near'],
|
|
235
|
+
frame: ['frame', 'action'],
|
|
236
|
+
new_tab: ['url', 'saveTabIndexAs'],
|
|
237
|
+
switch_tab: ['tab', 'closeCurrentTab'],
|
|
238
|
+
};
|
|
239
|
+
/**
|
|
240
|
+
* Validates a single step, pushing errors to the array.
|
|
241
|
+
*/
|
|
242
|
+
function validateStep(step, stepIndex, errors) {
|
|
243
|
+
if (!step || typeof step !== 'object') {
|
|
244
|
+
errors.push(`Step ${stepIndex}: Step must be an object`);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
const s = step;
|
|
248
|
+
const stepId = typeof s.id === 'string' && s.id ? s.id : '?';
|
|
249
|
+
const stepType = typeof s.type === 'string' && s.type ? s.type : '?';
|
|
250
|
+
const prefix = `Step ${stepIndex} (id="${stepId}", type="${stepType}")`;
|
|
251
|
+
// Check required fields
|
|
252
|
+
if (typeof s.id !== 'string' || !s.id) {
|
|
253
|
+
errors.push(`${prefix}: must have a non-empty string "id"`);
|
|
254
|
+
}
|
|
255
|
+
if (typeof s.type !== 'string' || !s.type) {
|
|
256
|
+
errors.push(`${prefix}: must have a non-empty string "type"`);
|
|
257
|
+
}
|
|
258
|
+
// Validate optional common fields
|
|
259
|
+
if (s.label !== undefined && typeof s.label !== 'string') {
|
|
260
|
+
errors.push(`${prefix}: "label" must be a string`);
|
|
261
|
+
}
|
|
262
|
+
if (s.timeoutMs !== undefined && (typeof s.timeoutMs !== 'number' || s.timeoutMs < 0)) {
|
|
263
|
+
errors.push(`${prefix}: "timeoutMs" must be a non-negative number`);
|
|
264
|
+
}
|
|
265
|
+
if (s.optional !== undefined && typeof s.optional !== 'boolean') {
|
|
266
|
+
errors.push(`${prefix}: "optional" must be a boolean`);
|
|
267
|
+
}
|
|
268
|
+
if (s.onError !== undefined) {
|
|
269
|
+
if (s.onError !== 'stop' && s.onError !== 'continue') {
|
|
270
|
+
errors.push(`${prefix}: "onError" must be "stop" or "continue"`);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
if (s.once !== undefined) {
|
|
274
|
+
if (s.once !== 'session' && s.once !== 'profile') {
|
|
275
|
+
errors.push(`${prefix}: "once" must be "session" or "profile"`);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
if (s.skip_if !== undefined) {
|
|
279
|
+
validateSkipCondition(s.skip_if, errors, prefix);
|
|
280
|
+
}
|
|
281
|
+
if (!s.params || typeof s.params !== 'object') {
|
|
282
|
+
errors.push(`${prefix}: must have a "params" object`);
|
|
283
|
+
return; // Can't check params further
|
|
284
|
+
}
|
|
285
|
+
const params = s.params;
|
|
286
|
+
// If type is unknown, we can't validate params
|
|
287
|
+
if (typeof s.type !== 'string' || !s.type) {
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
// Validate step type and params
|
|
291
|
+
switch (s.type) {
|
|
292
|
+
case 'navigate':
|
|
293
|
+
if (typeof params.url !== 'string' || !params.url) {
|
|
294
|
+
errors.push(`${prefix}: Navigate step must have a non-empty string "url" in params`);
|
|
295
|
+
}
|
|
296
|
+
if (params.waitUntil !== undefined) {
|
|
297
|
+
const validWaitUntil = ['load', 'domcontentloaded', 'networkidle', 'commit'];
|
|
298
|
+
if (!validWaitUntil.includes(params.waitUntil)) {
|
|
299
|
+
errors.push(`${prefix}: Navigate step "waitUntil" must be one of: ${validWaitUntil.join(', ')}`);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
break;
|
|
303
|
+
case 'extract_title':
|
|
304
|
+
if (typeof params.out !== 'string' || !params.out) {
|
|
305
|
+
errors.push(`${prefix}: ExtractTitle step must have a non-empty string "out" in params`);
|
|
306
|
+
}
|
|
307
|
+
break;
|
|
308
|
+
case 'extract_text':
|
|
309
|
+
// Must have either selector (legacy) or target (new)
|
|
310
|
+
if (!params.selector && !params.target) {
|
|
311
|
+
errors.push(`${prefix}: ExtractText step must have either "selector" or "target" in params`);
|
|
312
|
+
}
|
|
313
|
+
if (params.selector !== undefined && typeof params.selector !== 'string') {
|
|
314
|
+
errors.push(`${prefix}: ExtractText step "selector" must be a string`);
|
|
315
|
+
}
|
|
316
|
+
if (params.target !== undefined) {
|
|
317
|
+
validateTargetOrAnyOf(params.target, errors, prefix);
|
|
318
|
+
}
|
|
319
|
+
if (typeof params.out !== 'string' || !params.out) {
|
|
320
|
+
errors.push(`${prefix}: ExtractText step must have a non-empty string "out" in params`);
|
|
321
|
+
}
|
|
322
|
+
if (params.first !== undefined && typeof params.first !== 'boolean') {
|
|
323
|
+
errors.push(`${prefix}: ExtractText step "first" must be a boolean`);
|
|
324
|
+
}
|
|
325
|
+
if (params.trim !== undefined && typeof params.trim !== 'boolean') {
|
|
326
|
+
errors.push(`${prefix}: ExtractText step "trim" must be a boolean`);
|
|
327
|
+
}
|
|
328
|
+
if (params.default !== undefined && typeof params.default !== 'string') {
|
|
329
|
+
errors.push(`${prefix}: ExtractText step "default" must be a string`);
|
|
330
|
+
}
|
|
331
|
+
if (params.hint !== undefined && typeof params.hint !== 'string') {
|
|
332
|
+
errors.push(`${prefix}: ExtractText step "hint" must be a string`);
|
|
333
|
+
}
|
|
334
|
+
if (params.scope !== undefined) {
|
|
335
|
+
validateTarget(params.scope, errors, prefix);
|
|
336
|
+
}
|
|
337
|
+
if (params.near !== undefined && params.near !== null) {
|
|
338
|
+
const near = params.near;
|
|
339
|
+
if (typeof near !== 'object' || near.kind !== 'text') {
|
|
340
|
+
errors.push(`${prefix}: ExtractText step "near" must be an object with kind: "text"`);
|
|
341
|
+
}
|
|
342
|
+
else {
|
|
343
|
+
if (typeof near.text !== 'string') {
|
|
344
|
+
errors.push(`${prefix}: ExtractText step "near.text" must be a string`);
|
|
345
|
+
}
|
|
346
|
+
if (near.exact !== undefined && typeof near.exact !== 'boolean') {
|
|
347
|
+
errors.push(`${prefix}: ExtractText step "near.exact" must be a boolean`);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
break;
|
|
352
|
+
case 'sleep':
|
|
353
|
+
if (typeof params.durationMs !== 'number' || params.durationMs < 0) {
|
|
354
|
+
errors.push(`${prefix}: Sleep step must have a non-negative number "durationMs" in params`);
|
|
355
|
+
}
|
|
356
|
+
break;
|
|
357
|
+
case 'wait_for':
|
|
358
|
+
if (!params.selector && !params.target && !params.url && !params.loadState) {
|
|
359
|
+
errors.push(`${prefix}: WaitFor step must have one of: selector, target, url, or loadState`);
|
|
360
|
+
}
|
|
361
|
+
if (params.selector !== undefined && typeof params.selector !== 'string') {
|
|
362
|
+
errors.push(`${prefix}: WaitFor step "selector" must be a string`);
|
|
363
|
+
}
|
|
364
|
+
if (params.target !== undefined) {
|
|
365
|
+
validateTargetOrAnyOf(params.target, errors, prefix);
|
|
366
|
+
}
|
|
367
|
+
if (params.hint !== undefined && typeof params.hint !== 'string') {
|
|
368
|
+
errors.push(`${prefix}: WaitFor step "hint" must be a string`);
|
|
369
|
+
}
|
|
370
|
+
if (params.scope !== undefined) {
|
|
371
|
+
validateTarget(params.scope, errors, prefix);
|
|
372
|
+
}
|
|
373
|
+
if (params.near !== undefined && params.near !== null) {
|
|
374
|
+
const near = params.near;
|
|
375
|
+
if (typeof near !== 'object' || near.kind !== 'text') {
|
|
376
|
+
errors.push(`${prefix}: WaitFor step "near" must be an object with kind: "text"`);
|
|
377
|
+
}
|
|
378
|
+
else {
|
|
379
|
+
if (typeof near.text !== 'string') {
|
|
380
|
+
errors.push(`${prefix}: WaitFor step "near.text" must be a string`);
|
|
381
|
+
}
|
|
382
|
+
if (near.exact !== undefined && typeof near.exact !== 'boolean') {
|
|
383
|
+
errors.push(`${prefix}: WaitFor step "near.exact" must be a boolean`);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
if (params.visible !== undefined && typeof params.visible !== 'boolean') {
|
|
388
|
+
errors.push(`${prefix}: WaitFor step "visible" must be a boolean`);
|
|
389
|
+
}
|
|
390
|
+
if (params.url !== undefined && params.url !== null) {
|
|
391
|
+
if (typeof params.url === 'string') {
|
|
392
|
+
// Valid string URL
|
|
393
|
+
}
|
|
394
|
+
else if (typeof params.url === 'object') {
|
|
395
|
+
const urlObj = params.url;
|
|
396
|
+
if (typeof urlObj.pattern !== 'string') {
|
|
397
|
+
errors.push(`${prefix}: WaitFor step "url" object must have a string "pattern"`);
|
|
398
|
+
}
|
|
399
|
+
if (urlObj.exact !== undefined && typeof urlObj.exact !== 'boolean') {
|
|
400
|
+
errors.push(`${prefix}: WaitFor step "url" object "exact" must be a boolean`);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
else {
|
|
404
|
+
errors.push(`${prefix}: WaitFor step "url" must be a string or object with pattern`);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
if (params.loadState !== undefined) {
|
|
408
|
+
const validLoadStates = ['load', 'domcontentloaded', 'networkidle'];
|
|
409
|
+
if (!validLoadStates.includes(params.loadState)) {
|
|
410
|
+
errors.push(`${prefix}: WaitFor step "loadState" must be one of: ${validLoadStates.join(', ')}`);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
if (params.timeoutMs !== undefined && (typeof params.timeoutMs !== 'number' || params.timeoutMs < 0)) {
|
|
414
|
+
errors.push(`${prefix}: WaitFor step "timeoutMs" must be a non-negative number`);
|
|
415
|
+
}
|
|
416
|
+
break;
|
|
417
|
+
case 'click':
|
|
418
|
+
// Must have either selector (legacy) or target (new)
|
|
419
|
+
if (!params.selector && !params.target) {
|
|
420
|
+
errors.push(`${prefix}: Click step must have either "selector" or "target" in params`);
|
|
421
|
+
}
|
|
422
|
+
if (params.selector !== undefined && typeof params.selector !== 'string') {
|
|
423
|
+
errors.push(`${prefix}: Click step "selector" must be a string`);
|
|
424
|
+
}
|
|
425
|
+
if (params.target !== undefined) {
|
|
426
|
+
validateTargetOrAnyOf(params.target, errors, prefix);
|
|
427
|
+
}
|
|
428
|
+
if (params.first !== undefined && typeof params.first !== 'boolean') {
|
|
429
|
+
errors.push(`${prefix}: Click step "first" must be a boolean`);
|
|
430
|
+
}
|
|
431
|
+
if (params.waitForVisible !== undefined && typeof params.waitForVisible !== 'boolean') {
|
|
432
|
+
errors.push(`${prefix}: Click step "waitForVisible" must be a boolean`);
|
|
433
|
+
}
|
|
434
|
+
if (params.hint !== undefined && typeof params.hint !== 'string') {
|
|
435
|
+
errors.push(`${prefix}: Click step "hint" must be a string`);
|
|
436
|
+
}
|
|
437
|
+
if (params.scope !== undefined) {
|
|
438
|
+
validateTarget(params.scope, errors, prefix);
|
|
439
|
+
}
|
|
440
|
+
if (params.near !== undefined && params.near !== null) {
|
|
441
|
+
const near = params.near;
|
|
442
|
+
if (typeof near !== 'object' || near.kind !== 'text') {
|
|
443
|
+
errors.push(`${prefix}: Click step "near" must be an object with kind: "text"`);
|
|
444
|
+
}
|
|
445
|
+
else {
|
|
446
|
+
if (typeof near.text !== 'string') {
|
|
447
|
+
errors.push(`${prefix}: Click step "near.text" must be a string`);
|
|
448
|
+
}
|
|
449
|
+
if (near.exact !== undefined && typeof near.exact !== 'boolean') {
|
|
450
|
+
errors.push(`${prefix}: Click step "near.exact" must be a boolean`);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
break;
|
|
455
|
+
case 'fill':
|
|
456
|
+
// Must have either selector (legacy) or target (new)
|
|
457
|
+
if (!params.selector && !params.target) {
|
|
458
|
+
errors.push(`${prefix}: Fill step must have either "selector" or "target" in params`);
|
|
459
|
+
}
|
|
460
|
+
if (params.selector !== undefined && typeof params.selector !== 'string') {
|
|
461
|
+
errors.push(`${prefix}: Fill step "selector" must be a string`);
|
|
462
|
+
}
|
|
463
|
+
if (params.target !== undefined) {
|
|
464
|
+
validateTargetOrAnyOf(params.target, errors, prefix);
|
|
465
|
+
}
|
|
466
|
+
if (typeof params.value !== 'string') {
|
|
467
|
+
errors.push(`${prefix}: Fill step must have a string "value" in params`);
|
|
468
|
+
}
|
|
469
|
+
if (params.first !== undefined && typeof params.first !== 'boolean') {
|
|
470
|
+
errors.push(`${prefix}: Fill step "first" must be a boolean`);
|
|
471
|
+
}
|
|
472
|
+
if (params.clear !== undefined && typeof params.clear !== 'boolean') {
|
|
473
|
+
errors.push(`${prefix}: Fill step "clear" must be a boolean`);
|
|
474
|
+
}
|
|
475
|
+
if (params.hint !== undefined && typeof params.hint !== 'string') {
|
|
476
|
+
errors.push(`${prefix}: Fill step "hint" must be a string`);
|
|
477
|
+
}
|
|
478
|
+
if (params.scope !== undefined) {
|
|
479
|
+
validateTarget(params.scope, errors, prefix);
|
|
480
|
+
}
|
|
481
|
+
if (params.near !== undefined && params.near !== null) {
|
|
482
|
+
const near = params.near;
|
|
483
|
+
if (typeof near !== 'object' || near.kind !== 'text') {
|
|
484
|
+
errors.push(`${prefix}: Fill step "near" must be an object with kind: "text"`);
|
|
485
|
+
}
|
|
486
|
+
else {
|
|
487
|
+
if (typeof near.text !== 'string') {
|
|
488
|
+
errors.push(`${prefix}: Fill step "near.text" must be a string`);
|
|
489
|
+
}
|
|
490
|
+
if (near.exact !== undefined && typeof near.exact !== 'boolean') {
|
|
491
|
+
errors.push(`${prefix}: Fill step "near.exact" must be a boolean`);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
break;
|
|
496
|
+
case 'extract_attribute':
|
|
497
|
+
// Must have either selector (legacy) or target (new)
|
|
498
|
+
if (!params.selector && !params.target) {
|
|
499
|
+
errors.push(`${prefix}: ExtractAttribute step must have either "selector" or "target" in params`);
|
|
500
|
+
}
|
|
501
|
+
if (params.selector !== undefined && typeof params.selector !== 'string') {
|
|
502
|
+
errors.push(`${prefix}: ExtractAttribute step "selector" must be a string`);
|
|
503
|
+
}
|
|
504
|
+
if (params.target !== undefined) {
|
|
505
|
+
validateTargetOrAnyOf(params.target, errors, prefix);
|
|
506
|
+
}
|
|
507
|
+
if (typeof params.attribute !== 'string' || !params.attribute) {
|
|
508
|
+
errors.push(`${prefix}: ExtractAttribute step must have a non-empty string "attribute" in params`);
|
|
509
|
+
}
|
|
510
|
+
if (typeof params.out !== 'string' || !params.out) {
|
|
511
|
+
errors.push(`${prefix}: ExtractAttribute step must have a non-empty string "out" in params`);
|
|
512
|
+
}
|
|
513
|
+
if (params.first !== undefined && typeof params.first !== 'boolean') {
|
|
514
|
+
errors.push(`${prefix}: ExtractAttribute step "first" must be a boolean`);
|
|
515
|
+
}
|
|
516
|
+
if (params.default !== undefined && typeof params.default !== 'string') {
|
|
517
|
+
errors.push(`${prefix}: ExtractAttribute step "default" must be a string`);
|
|
518
|
+
}
|
|
519
|
+
if (params.hint !== undefined && typeof params.hint !== 'string') {
|
|
520
|
+
errors.push(`${prefix}: ExtractAttribute step "hint" must be a string`);
|
|
521
|
+
}
|
|
522
|
+
if (params.scope !== undefined) {
|
|
523
|
+
validateTarget(params.scope, errors, prefix);
|
|
524
|
+
}
|
|
525
|
+
if (params.near !== undefined && params.near !== null) {
|
|
526
|
+
const near = params.near;
|
|
527
|
+
if (typeof near !== 'object' || near.kind !== 'text') {
|
|
528
|
+
errors.push(`${prefix}: ExtractAttribute step "near" must be an object with kind: "text"`);
|
|
529
|
+
}
|
|
530
|
+
else {
|
|
531
|
+
if (typeof near.text !== 'string') {
|
|
532
|
+
errors.push(`${prefix}: ExtractAttribute step "near.text" must be a string`);
|
|
533
|
+
}
|
|
534
|
+
if (near.exact !== undefined && typeof near.exact !== 'boolean') {
|
|
535
|
+
errors.push(`${prefix}: ExtractAttribute step "near.exact" must be a boolean`);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
break;
|
|
540
|
+
case 'assert':
|
|
541
|
+
if (!params.selector && !params.target && !params.urlIncludes) {
|
|
542
|
+
errors.push(`${prefix}: Assert step must have at least one of: selector, target, or urlIncludes`);
|
|
543
|
+
}
|
|
544
|
+
if (params.selector !== undefined && typeof params.selector !== 'string') {
|
|
545
|
+
errors.push(`${prefix}: Assert step "selector" must be a string`);
|
|
546
|
+
}
|
|
547
|
+
if (params.target !== undefined) {
|
|
548
|
+
validateTargetOrAnyOf(params.target, errors, prefix);
|
|
549
|
+
}
|
|
550
|
+
if (params.visible !== undefined && typeof params.visible !== 'boolean') {
|
|
551
|
+
errors.push(`${prefix}: Assert step "visible" must be a boolean`);
|
|
552
|
+
}
|
|
553
|
+
if (params.textIncludes !== undefined && typeof params.textIncludes !== 'string') {
|
|
554
|
+
errors.push(`${prefix}: Assert step "textIncludes" must be a string`);
|
|
555
|
+
}
|
|
556
|
+
if (params.urlIncludes !== undefined && typeof params.urlIncludes !== 'string') {
|
|
557
|
+
errors.push(`${prefix}: Assert step "urlIncludes" must be a string`);
|
|
558
|
+
}
|
|
559
|
+
if (params.message !== undefined && typeof params.message !== 'string') {
|
|
560
|
+
errors.push(`${prefix}: Assert step "message" must be a string`);
|
|
561
|
+
}
|
|
562
|
+
if (params.hint !== undefined && typeof params.hint !== 'string') {
|
|
563
|
+
errors.push(`${prefix}: Assert step "hint" must be a string`);
|
|
564
|
+
}
|
|
565
|
+
if (params.scope !== undefined) {
|
|
566
|
+
validateTarget(params.scope, errors, prefix);
|
|
567
|
+
}
|
|
568
|
+
if (params.near !== undefined && params.near !== null) {
|
|
569
|
+
const near = params.near;
|
|
570
|
+
if (typeof near !== 'object' || near.kind !== 'text') {
|
|
571
|
+
errors.push(`${prefix}: Assert step "near" must be an object with kind: "text"`);
|
|
572
|
+
}
|
|
573
|
+
else {
|
|
574
|
+
if (typeof near.text !== 'string') {
|
|
575
|
+
errors.push(`${prefix}: Assert step "near.text" must be a string`);
|
|
576
|
+
}
|
|
577
|
+
if (near.exact !== undefined && typeof near.exact !== 'boolean') {
|
|
578
|
+
errors.push(`${prefix}: Assert step "near.exact" must be a boolean`);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
break;
|
|
583
|
+
case 'set_var': {
|
|
584
|
+
if (typeof params.name !== 'string' || !params.name) {
|
|
585
|
+
errors.push(`${prefix}: SetVar step must have a non-empty string "name" in params`);
|
|
586
|
+
}
|
|
587
|
+
const valueType = typeof params.value;
|
|
588
|
+
if (valueType !== 'string' && valueType !== 'number' && valueType !== 'boolean') {
|
|
589
|
+
errors.push(`${prefix}: SetVar step "value" must be a string, number, or boolean`);
|
|
590
|
+
}
|
|
591
|
+
break;
|
|
592
|
+
}
|
|
593
|
+
case 'network_find': {
|
|
594
|
+
if (!params.where || typeof params.where !== 'object') {
|
|
595
|
+
errors.push(`${prefix}: NetworkFind step must have a "where" object in params`);
|
|
596
|
+
}
|
|
597
|
+
else {
|
|
598
|
+
const where = params.where;
|
|
599
|
+
if (where.urlIncludes !== undefined && typeof where.urlIncludes !== 'string') {
|
|
600
|
+
errors.push(`${prefix}: NetworkFind step "where.urlIncludes" must be a string`);
|
|
601
|
+
}
|
|
602
|
+
if (where.urlRegex !== undefined) {
|
|
603
|
+
if (typeof where.urlRegex !== 'string') {
|
|
604
|
+
errors.push(`${prefix}: NetworkFind step "where.urlRegex" must be a string`);
|
|
605
|
+
}
|
|
606
|
+
else {
|
|
607
|
+
try {
|
|
608
|
+
new RegExp(where.urlRegex);
|
|
609
|
+
}
|
|
610
|
+
catch {
|
|
611
|
+
errors.push(`${prefix}: NetworkFind step "where.urlRegex" is not a valid regex`);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
if (where.method !== undefined) {
|
|
616
|
+
const validMethods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'];
|
|
617
|
+
if (!validMethods.includes(where.method)) {
|
|
618
|
+
errors.push(`${prefix}: NetworkFind step "where.method" must be one of: ${validMethods.join(', ')}`);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
if (where.status !== undefined && (typeof where.status !== 'number' || where.status < 0)) {
|
|
622
|
+
errors.push(`${prefix}: NetworkFind step "where.status" must be a non-negative number`);
|
|
623
|
+
}
|
|
624
|
+
if (where.contentTypeIncludes !== undefined && typeof where.contentTypeIncludes !== 'string') {
|
|
625
|
+
errors.push(`${prefix}: NetworkFind step "where.contentTypeIncludes" must be a string`);
|
|
626
|
+
}
|
|
627
|
+
if (where.responseContains !== undefined) {
|
|
628
|
+
if (typeof where.responseContains !== 'string') {
|
|
629
|
+
errors.push(`${prefix}: NetworkFind step "where.responseContains" must be a string`);
|
|
630
|
+
}
|
|
631
|
+
else if (where.responseContains.length > 2000) {
|
|
632
|
+
errors.push(`${prefix}: NetworkFind step "where.responseContains" must be at most 2000 characters`);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
if (params.pick !== undefined && params.pick !== 'first' && params.pick !== 'last') {
|
|
637
|
+
errors.push(`${prefix}: NetworkFind step "pick" must be "first" or "last"`);
|
|
638
|
+
}
|
|
639
|
+
if (typeof params.saveAs !== 'string' || !params.saveAs) {
|
|
640
|
+
errors.push(`${prefix}: NetworkFind step must have a non-empty string "saveAs" in params`);
|
|
641
|
+
}
|
|
642
|
+
else if (params.saveAs.length > 500) {
|
|
643
|
+
errors.push(`${prefix}: NetworkFind step "saveAs" must be at most 500 characters`);
|
|
644
|
+
}
|
|
645
|
+
if (params.waitForMs !== undefined && (typeof params.waitForMs !== 'number' || params.waitForMs < 0)) {
|
|
646
|
+
errors.push(`${prefix}: NetworkFind step "waitForMs" must be a non-negative number`);
|
|
647
|
+
}
|
|
648
|
+
if (params.pollIntervalMs !== undefined && (typeof params.pollIntervalMs !== 'number' || params.pollIntervalMs < 100)) {
|
|
649
|
+
errors.push(`${prefix}: NetworkFind step "pollIntervalMs" must be at least 100`);
|
|
650
|
+
}
|
|
651
|
+
break;
|
|
652
|
+
}
|
|
653
|
+
case 'network_replay': {
|
|
654
|
+
if (typeof params.requestId !== 'string' || !params.requestId) {
|
|
655
|
+
errors.push(`${prefix}: NetworkReplay step must have a non-empty string "requestId" in params`);
|
|
656
|
+
}
|
|
657
|
+
else if (params.requestId.length > 2000) {
|
|
658
|
+
errors.push(`${prefix}: NetworkReplay step "requestId" must be at most 2000 characters`);
|
|
659
|
+
}
|
|
660
|
+
const SENSITIVE_HEADERS = new Set([
|
|
661
|
+
'authorization',
|
|
662
|
+
'cookie',
|
|
663
|
+
'set-cookie',
|
|
664
|
+
'x-api-key',
|
|
665
|
+
'proxy-authorization',
|
|
666
|
+
]);
|
|
667
|
+
if (params.overrides && typeof params.overrides === 'object') {
|
|
668
|
+
const overrides = params.overrides;
|
|
669
|
+
if (overrides.setHeaders && typeof overrides.setHeaders === 'object') {
|
|
670
|
+
for (const key of Object.keys(overrides.setHeaders)) {
|
|
671
|
+
if (SENSITIVE_HEADERS.has(key.toLowerCase())) {
|
|
672
|
+
errors.push(`${prefix}: NetworkReplay step "overrides.setHeaders" cannot set sensitive header: ${key}`);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
if (overrides.urlReplace !== undefined) {
|
|
677
|
+
if (typeof overrides.urlReplace !== 'object' || overrides.urlReplace === null) {
|
|
678
|
+
errors.push(`${prefix}: NetworkReplay step "overrides.urlReplace" must be { find: string, replace: string }`);
|
|
679
|
+
}
|
|
680
|
+
else {
|
|
681
|
+
const ur = overrides.urlReplace;
|
|
682
|
+
if (typeof ur.find !== 'string' || typeof ur.replace !== 'string') {
|
|
683
|
+
errors.push(`${prefix}: NetworkReplay step "overrides.urlReplace" must have string "find" and "replace"`);
|
|
684
|
+
}
|
|
685
|
+
else {
|
|
686
|
+
try {
|
|
687
|
+
new RegExp(ur.find);
|
|
688
|
+
}
|
|
689
|
+
catch {
|
|
690
|
+
errors.push(`${prefix}: NetworkReplay step "overrides.urlReplace.find" is not a valid regex`);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
if (overrides.bodyReplace !== undefined) {
|
|
696
|
+
if (typeof overrides.bodyReplace !== 'object' || overrides.bodyReplace === null) {
|
|
697
|
+
errors.push(`${prefix}: NetworkReplay step "overrides.bodyReplace" must be { find: string, replace: string }`);
|
|
698
|
+
}
|
|
699
|
+
else {
|
|
700
|
+
const br = overrides.bodyReplace;
|
|
701
|
+
if (typeof br.find !== 'string' || typeof br.replace !== 'string') {
|
|
702
|
+
errors.push(`${prefix}: NetworkReplay step "overrides.bodyReplace" must have string "find" and "replace"`);
|
|
703
|
+
}
|
|
704
|
+
else {
|
|
705
|
+
try {
|
|
706
|
+
new RegExp(br.find);
|
|
707
|
+
}
|
|
708
|
+
catch {
|
|
709
|
+
errors.push(`${prefix}: NetworkReplay step "overrides.bodyReplace.find" is not a valid regex`);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
if (params.auth !== 'browser_context') {
|
|
716
|
+
errors.push(`${prefix}: NetworkReplay step "auth" must be "browser_context"`);
|
|
717
|
+
}
|
|
718
|
+
if (typeof params.out !== 'string' || !params.out) {
|
|
719
|
+
errors.push(`${prefix}: NetworkReplay step must have a non-empty string "out" in params`);
|
|
720
|
+
}
|
|
721
|
+
if (!params.response || typeof params.response !== 'object') {
|
|
722
|
+
errors.push(`${prefix}: NetworkReplay step must have a "response" object in params`);
|
|
723
|
+
}
|
|
724
|
+
else {
|
|
725
|
+
const resp = params.response;
|
|
726
|
+
if (resp.as !== 'json' && resp.as !== 'text') {
|
|
727
|
+
errors.push(`${prefix}: NetworkReplay step "response.as" must be "json" or "text"`);
|
|
728
|
+
}
|
|
729
|
+
if (resp.jsonPath !== undefined && typeof resp.jsonPath !== 'string') {
|
|
730
|
+
errors.push(`${prefix}: NetworkReplay step "response.jsonPath" must be a string`);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
break;
|
|
734
|
+
}
|
|
735
|
+
case 'network_extract':
|
|
736
|
+
if (typeof params.fromVar !== 'string' || !params.fromVar) {
|
|
737
|
+
errors.push(`${prefix}: NetworkExtract step must have a non-empty string "fromVar" in params`);
|
|
738
|
+
}
|
|
739
|
+
if (params.as !== 'json' && params.as !== 'text') {
|
|
740
|
+
errors.push(`${prefix}: NetworkExtract step "as" must be "json" or "text"`);
|
|
741
|
+
}
|
|
742
|
+
if (params.jsonPath !== undefined && typeof params.jsonPath !== 'string') {
|
|
743
|
+
errors.push(`${prefix}: NetworkExtract step "jsonPath" must be a string`);
|
|
744
|
+
}
|
|
745
|
+
if (params.transform !== undefined) {
|
|
746
|
+
if (typeof params.transform !== 'object' || params.transform === null || Array.isArray(params.transform)) {
|
|
747
|
+
errors.push(`${prefix}: NetworkExtract step "transform" must be an object mapping field names to jsonPath expressions`);
|
|
748
|
+
}
|
|
749
|
+
else {
|
|
750
|
+
for (const [key, val] of Object.entries(params.transform)) {
|
|
751
|
+
if (typeof val !== 'string') {
|
|
752
|
+
errors.push(`${prefix}: NetworkExtract step "transform.${key}" must be a string (jsonPath expression)`);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
if (typeof params.out !== 'string' || !params.out) {
|
|
758
|
+
errors.push(`${prefix}: NetworkExtract step must have a non-empty string "out" in params`);
|
|
759
|
+
}
|
|
760
|
+
break;
|
|
761
|
+
case 'select_option':
|
|
762
|
+
// Must have either selector (legacy) or target (new)
|
|
763
|
+
if (!params.selector && !params.target) {
|
|
764
|
+
errors.push(`${prefix}: SelectOption step must have either "selector" or "target" in params`);
|
|
765
|
+
}
|
|
766
|
+
if (params.selector !== undefined && typeof params.selector !== 'string') {
|
|
767
|
+
errors.push(`${prefix}: SelectOption step "selector" must be a string`);
|
|
768
|
+
}
|
|
769
|
+
if (params.target !== undefined) {
|
|
770
|
+
validateTargetOrAnyOf(params.target, errors, prefix);
|
|
771
|
+
}
|
|
772
|
+
if (params.value === undefined || params.value === null) {
|
|
773
|
+
errors.push(`${prefix}: SelectOption step must have a "value" in params`);
|
|
774
|
+
}
|
|
775
|
+
else {
|
|
776
|
+
// Validate value format
|
|
777
|
+
const isValidSelectValue = (v) => {
|
|
778
|
+
if (typeof v === 'string')
|
|
779
|
+
return true;
|
|
780
|
+
if (typeof v === 'object' && v !== null) {
|
|
781
|
+
if ('label' in v && typeof v.label === 'string')
|
|
782
|
+
return true;
|
|
783
|
+
if ('index' in v && typeof v.index === 'number')
|
|
784
|
+
return true;
|
|
785
|
+
}
|
|
786
|
+
return false;
|
|
787
|
+
};
|
|
788
|
+
if (Array.isArray(params.value)) {
|
|
789
|
+
for (const v of params.value) {
|
|
790
|
+
if (!isValidSelectValue(v)) {
|
|
791
|
+
errors.push(`${prefix}: SelectOption step "value" must be string, { label: string }, or { index: number }`);
|
|
792
|
+
break;
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
else {
|
|
797
|
+
if (!isValidSelectValue(params.value)) {
|
|
798
|
+
errors.push(`${prefix}: SelectOption step "value" must be string, { label: string }, or { index: number }`);
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
if (params.first !== undefined && typeof params.first !== 'boolean') {
|
|
803
|
+
errors.push(`${prefix}: SelectOption step "first" must be a boolean`);
|
|
804
|
+
}
|
|
805
|
+
if (params.hint !== undefined && typeof params.hint !== 'string') {
|
|
806
|
+
errors.push(`${prefix}: SelectOption step "hint" must be a string`);
|
|
807
|
+
}
|
|
808
|
+
if (params.scope !== undefined) {
|
|
809
|
+
validateTarget(params.scope, errors, prefix);
|
|
810
|
+
}
|
|
811
|
+
break;
|
|
812
|
+
case 'press_key':
|
|
813
|
+
if (typeof params.key !== 'string' || !params.key) {
|
|
814
|
+
errors.push(`${prefix}: PressKey step must have a non-empty string "key" in params`);
|
|
815
|
+
}
|
|
816
|
+
if (params.selector !== undefined && typeof params.selector !== 'string') {
|
|
817
|
+
errors.push(`${prefix}: PressKey step "selector" must be a string`);
|
|
818
|
+
}
|
|
819
|
+
if (params.target !== undefined) {
|
|
820
|
+
validateTargetOrAnyOf(params.target, errors, prefix);
|
|
821
|
+
}
|
|
822
|
+
if (params.times !== undefined && (typeof params.times !== 'number' || params.times < 1)) {
|
|
823
|
+
errors.push(`${prefix}: PressKey step "times" must be a positive number`);
|
|
824
|
+
}
|
|
825
|
+
if (params.delayMs !== undefined && (typeof params.delayMs !== 'number' || params.delayMs < 0)) {
|
|
826
|
+
errors.push(`${prefix}: PressKey step "delayMs" must be a non-negative number`);
|
|
827
|
+
}
|
|
828
|
+
if (params.hint !== undefined && typeof params.hint !== 'string') {
|
|
829
|
+
errors.push(`${prefix}: PressKey step "hint" must be a string`);
|
|
830
|
+
}
|
|
831
|
+
if (params.scope !== undefined) {
|
|
832
|
+
validateTarget(params.scope, errors, prefix);
|
|
833
|
+
}
|
|
834
|
+
break;
|
|
835
|
+
case 'upload_file':
|
|
836
|
+
// Must have either selector (legacy) or target (new)
|
|
837
|
+
if (!params.selector && !params.target) {
|
|
838
|
+
errors.push(`${prefix}: UploadFile step must have either "selector" or "target" in params`);
|
|
839
|
+
}
|
|
840
|
+
if (params.selector !== undefined && typeof params.selector !== 'string') {
|
|
841
|
+
errors.push(`${prefix}: UploadFile step "selector" must be a string`);
|
|
842
|
+
}
|
|
843
|
+
if (params.target !== undefined) {
|
|
844
|
+
validateTargetOrAnyOf(params.target, errors, prefix);
|
|
845
|
+
}
|
|
846
|
+
if (params.files === undefined || params.files === null) {
|
|
847
|
+
errors.push(`${prefix}: UploadFile step must have "files" in params`);
|
|
848
|
+
}
|
|
849
|
+
else if (typeof params.files !== 'string' && !Array.isArray(params.files)) {
|
|
850
|
+
errors.push(`${prefix}: UploadFile step "files" must be a string or array of strings`);
|
|
851
|
+
}
|
|
852
|
+
else if (Array.isArray(params.files)) {
|
|
853
|
+
for (const f of params.files) {
|
|
854
|
+
if (typeof f !== 'string') {
|
|
855
|
+
errors.push(`${prefix}: UploadFile step "files" array must contain only strings`);
|
|
856
|
+
break;
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
if (params.first !== undefined && typeof params.first !== 'boolean') {
|
|
861
|
+
errors.push(`${prefix}: UploadFile step "first" must be a boolean`);
|
|
862
|
+
}
|
|
863
|
+
if (params.hint !== undefined && typeof params.hint !== 'string') {
|
|
864
|
+
errors.push(`${prefix}: UploadFile step "hint" must be a string`);
|
|
865
|
+
}
|
|
866
|
+
if (params.scope !== undefined) {
|
|
867
|
+
validateTarget(params.scope, errors, prefix);
|
|
868
|
+
}
|
|
869
|
+
break;
|
|
870
|
+
case 'frame':
|
|
871
|
+
if (params.frame === undefined || params.frame === null) {
|
|
872
|
+
errors.push(`${prefix}: Frame step must have "frame" in params`);
|
|
873
|
+
}
|
|
874
|
+
else if (typeof params.frame !== 'string' && typeof params.frame !== 'object') {
|
|
875
|
+
errors.push(`${prefix}: Frame step "frame" must be a string, { name: string }, or { url: string }`);
|
|
876
|
+
}
|
|
877
|
+
else if (typeof params.frame === 'object') {
|
|
878
|
+
if (!('name' in params.frame) && !('url' in params.frame)) {
|
|
879
|
+
errors.push(`${prefix}: Frame step "frame" object must have "name" or "url"`);
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
if (params.action !== 'enter' && params.action !== 'exit') {
|
|
883
|
+
errors.push(`${prefix}: Frame step "action" must be "enter" or "exit"`);
|
|
884
|
+
}
|
|
885
|
+
break;
|
|
886
|
+
case 'new_tab':
|
|
887
|
+
if (params.url !== undefined && typeof params.url !== 'string') {
|
|
888
|
+
errors.push(`${prefix}: NewTab step "url" must be a string`);
|
|
889
|
+
}
|
|
890
|
+
if (params.saveTabIndexAs !== undefined && typeof params.saveTabIndexAs !== 'string') {
|
|
891
|
+
errors.push(`${prefix}: NewTab step "saveTabIndexAs" must be a string`);
|
|
892
|
+
}
|
|
893
|
+
break;
|
|
894
|
+
case 'switch_tab':
|
|
895
|
+
if (params.tab === undefined || params.tab === null) {
|
|
896
|
+
errors.push(`${prefix}: SwitchTab step must have "tab" in params`);
|
|
897
|
+
}
|
|
898
|
+
else {
|
|
899
|
+
if (typeof params.tab !== 'number' && params.tab !== 'last' && params.tab !== 'previous') {
|
|
900
|
+
errors.push(`${prefix}: SwitchTab step "tab" must be a number, "last", or "previous"`);
|
|
901
|
+
}
|
|
902
|
+
if (typeof params.tab === 'number' && params.tab < 0) {
|
|
903
|
+
errors.push(`${prefix}: SwitchTab step "tab" index must be non-negative`);
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
if (params.closeCurrentTab !== undefined && typeof params.closeCurrentTab !== 'boolean') {
|
|
907
|
+
errors.push(`${prefix}: SwitchTab step "closeCurrentTab" must be a boolean`);
|
|
908
|
+
}
|
|
909
|
+
break;
|
|
910
|
+
default:
|
|
911
|
+
errors.push(`${prefix}: Unknown step type: ${s.type}. Supported types: navigate, extract_title, extract_text, extract_attribute, sleep, wait_for, click, fill, assert, set_var, network_find, network_replay, network_extract, select_option, press_key, upload_file, frame, new_tab, switch_tab`);
|
|
912
|
+
}
|
|
913
|
+
// Check for unknown params
|
|
914
|
+
const allowed = ALLOWED_PARAMS[s.type];
|
|
915
|
+
if (allowed) {
|
|
916
|
+
const unknown = Object.keys(params).filter(k => !allowed.includes(k));
|
|
917
|
+
if (unknown.length > 0) {
|
|
918
|
+
const evalLike = unknown.filter(k => ['eval', 'expression', 'evaluate', 'exec', 'script', 'code', 'js', 'javascript', 'function'].includes(k.toLowerCase()));
|
|
919
|
+
let hint = '';
|
|
920
|
+
if (evalLike.length > 0) {
|
|
921
|
+
hint = '. To extract/transform data from JSON responses, use the network_extract step with a JMESPath "path" expression instead of eval';
|
|
922
|
+
}
|
|
923
|
+
errors.push(`${prefix}: Unknown param(s) ${unknown.map(k => `"${k}"`).join(', ')} in "${s.type}" step. Allowed params: ${allowed.join(', ')}${hint}`);
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
// Check for eval() in string param values
|
|
927
|
+
for (const [key, val] of Object.entries(params)) {
|
|
928
|
+
if (typeof val === 'string' && /\beval\s*\(/.test(val)) {
|
|
929
|
+
errors.push(`${prefix}: param "${key}" contains eval() which is not supported. To extract/transform data from JSON responses, use the network_extract step with a JMESPath "path" expression`);
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
/**
|
|
934
|
+
* Validates a flow (array of steps).
|
|
935
|
+
*
|
|
936
|
+
* When `collectedErrors` is provided, all errors are pushed to the array
|
|
937
|
+
* (used by validateJsonTaskPack to report every problem at once).
|
|
938
|
+
* When omitted, throws on the first error (backwards-compatible for runtime/interpreter).
|
|
939
|
+
*/
|
|
940
|
+
export function validateFlow(steps, collectedErrors) {
|
|
941
|
+
if (!Array.isArray(steps)) {
|
|
942
|
+
const msg = 'Flow must be an array of steps';
|
|
943
|
+
if (collectedErrors) {
|
|
944
|
+
collectedErrors.push(msg);
|
|
945
|
+
return;
|
|
946
|
+
}
|
|
947
|
+
throw new ValidationError(msg);
|
|
948
|
+
}
|
|
949
|
+
// Empty flow is allowed (e.g. user or AI deleted all steps)
|
|
950
|
+
// Check for unique IDs
|
|
951
|
+
const ids = new Set();
|
|
952
|
+
for (let i = 0; i < steps.length; i++) {
|
|
953
|
+
const stepErrors = [];
|
|
954
|
+
validateStep(steps[i], i, stepErrors);
|
|
955
|
+
// Duplicate ID check
|
|
956
|
+
const step = steps[i];
|
|
957
|
+
const stepId = typeof step?.id === 'string' ? step.id : undefined;
|
|
958
|
+
if (stepId) {
|
|
959
|
+
if (ids.has(stepId)) {
|
|
960
|
+
stepErrors.push(`Step ${i} (id="${stepId}"): Duplicate step ID`);
|
|
961
|
+
}
|
|
962
|
+
ids.add(stepId);
|
|
963
|
+
}
|
|
964
|
+
if (stepErrors.length > 0) {
|
|
965
|
+
if (collectedErrors) {
|
|
966
|
+
collectedErrors.push(...stepErrors);
|
|
967
|
+
}
|
|
968
|
+
else {
|
|
969
|
+
// Backwards compat: throw on first error
|
|
970
|
+
throw new ValidationError(stepErrors[0]);
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
}
|