@rettangoli/vt 0.0.14 → 1.0.0-rc12
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 +164 -64
- package/package.json +9 -2
- package/src/capture/capture-scheduler.js +206 -0
- package/src/capture/playwright-runner.js +405 -0
- package/src/capture/result-collector.js +234 -0
- package/src/capture/screenshot-naming.js +13 -0
- package/src/capture/spec-loader.js +119 -0
- package/src/capture/worker-plan.js +66 -0
- package/src/cli/generate-options.js +94 -0
- package/src/cli/generate.js +118 -28
- package/src/cli/index.js +3 -1
- package/src/cli/report-options.js +43 -0
- package/src/cli/report.js +88 -151
- package/src/cli/screenshot.js +8 -0
- package/src/cli/service-runtime.js +116 -0
- package/src/cli/templates/default.html +5 -3
- package/src/cli/templates/index.html +9 -7
- package/src/cli/templates/report.html +8 -6
- package/src/common.js +124 -185
- package/src/createSteps.js +810 -44
- package/src/report/report-model.js +76 -0
- package/src/report/report-render.js +22 -0
- package/src/section-page-key.js +14 -0
- package/src/selector-filter.js +140 -0
- package/src/step-commands.js +37 -0
- package/src/validation.js +636 -0
- package/src/viewport.js +99 -0
- package/docker/Dockerfile +0 -21
- package/docker/build-and-push.sh +0 -16
package/src/createSteps.js
CHANGED
|
@@ -1,18 +1,452 @@
|
|
|
1
|
+
function parseStepCommand(stepString) {
|
|
2
|
+
const tokens = stepString.trim().split(/\s+/).filter(Boolean);
|
|
3
|
+
const [command, ...args] = tokens;
|
|
4
|
+
return { command, args };
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function parseNamedArgs(args) {
|
|
8
|
+
const named = {};
|
|
9
|
+
const positional = [];
|
|
10
|
+
args.forEach((token) => {
|
|
11
|
+
const separatorIndex = token.indexOf("=");
|
|
12
|
+
if (separatorIndex <= 0) {
|
|
13
|
+
positional.push(token);
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
const key = token.slice(0, separatorIndex);
|
|
17
|
+
const value = token.slice(separatorIndex + 1);
|
|
18
|
+
named[key] = value;
|
|
19
|
+
});
|
|
20
|
+
return { named, positional };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function toNumber(value, fieldName) {
|
|
24
|
+
const parsed = Number(value);
|
|
25
|
+
if (!Number.isFinite(parsed)) {
|
|
26
|
+
throw new Error(`Invalid ${fieldName}: expected a finite number, got "${value}".`);
|
|
27
|
+
}
|
|
28
|
+
return parsed;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function toPositiveInteger(value, fieldName) {
|
|
32
|
+
const parsed = toNumber(value, fieldName);
|
|
33
|
+
if (!Number.isInteger(parsed) || parsed < 1) {
|
|
34
|
+
throw new Error(`Invalid ${fieldName}: expected an integer >= 1, got "${value}".`);
|
|
35
|
+
}
|
|
36
|
+
return parsed;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function parseTimeoutValue(value) {
|
|
40
|
+
if (value === undefined) {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
const timeout = toNumber(value, "timeout");
|
|
44
|
+
if (timeout < 0) {
|
|
45
|
+
throw new Error(`Invalid timeout: expected >= 0, got ${timeout}.`);
|
|
46
|
+
}
|
|
47
|
+
return timeout;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function deepEqual(left, right) {
|
|
51
|
+
if (Object.is(left, right)) {
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
if (typeof left !== typeof right) {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
if (left === null || right === null) {
|
|
58
|
+
return left === right;
|
|
59
|
+
}
|
|
60
|
+
if (typeof left !== "object") {
|
|
61
|
+
return left === right;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (Array.isArray(left) || Array.isArray(right)) {
|
|
65
|
+
if (!Array.isArray(left) || !Array.isArray(right)) {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
if (left.length !== right.length) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
for (let index = 0; index < left.length; index += 1) {
|
|
72
|
+
if (!deepEqual(left[index], right[index])) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const leftKeys = Object.keys(left);
|
|
80
|
+
const rightKeys = Object.keys(right);
|
|
81
|
+
if (leftKeys.length !== rightKeys.length) {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
for (const key of leftKeys) {
|
|
85
|
+
if (!Object.prototype.hasOwnProperty.call(right, key)) {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
if (!deepEqual(left[key], right[key])) {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function isPlainObject(value) {
|
|
96
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function formatValue(value) {
|
|
100
|
+
if (value === undefined) {
|
|
101
|
+
return "undefined";
|
|
102
|
+
}
|
|
103
|
+
if (typeof value === "string") {
|
|
104
|
+
return `"${value}"`;
|
|
105
|
+
}
|
|
106
|
+
try {
|
|
107
|
+
return JSON.stringify(value);
|
|
108
|
+
} catch {
|
|
109
|
+
return String(value);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function requireSelectedElement(command, selectedElement) {
|
|
114
|
+
if (!selectedElement) {
|
|
115
|
+
throw new Error(`\`${command}\` requires a \`select\` block target.`);
|
|
116
|
+
}
|
|
117
|
+
return selectedElement;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const WAIT_FOR_STATES = new Set(["attached", "detached", "visible", "hidden"]);
|
|
121
|
+
|
|
122
|
+
const STRUCTURED_ACTIONS = new Set([
|
|
123
|
+
"assert",
|
|
124
|
+
"blur",
|
|
125
|
+
"check",
|
|
126
|
+
"clear",
|
|
127
|
+
"click",
|
|
128
|
+
"customEvent",
|
|
129
|
+
"dblclick",
|
|
130
|
+
"focus",
|
|
131
|
+
"goto",
|
|
132
|
+
"hover",
|
|
133
|
+
"keypress",
|
|
134
|
+
"mouseDown",
|
|
135
|
+
"mouseUp",
|
|
136
|
+
"move",
|
|
137
|
+
"rclick",
|
|
138
|
+
"rightMouseDown",
|
|
139
|
+
"rightMouseUp",
|
|
140
|
+
"scroll",
|
|
141
|
+
"select",
|
|
142
|
+
"selectOption",
|
|
143
|
+
"setViewport",
|
|
144
|
+
"screenshot",
|
|
145
|
+
"uncheck",
|
|
146
|
+
"upload",
|
|
147
|
+
"wait",
|
|
148
|
+
"waitFor",
|
|
149
|
+
"write",
|
|
150
|
+
]);
|
|
151
|
+
|
|
152
|
+
function assertStructuredKeys(stepObject, allowedKeys, actionName) {
|
|
153
|
+
const unknownKeys = Object.keys(stepObject).filter((key) => !allowedKeys.has(key));
|
|
154
|
+
if (unknownKeys.length > 0) {
|
|
155
|
+
throw new Error(
|
|
156
|
+
`Structured action "${actionName}" has unknown keys: ${unknownKeys.join(", ")}.`,
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function requireStepAction(stepObject) {
|
|
162
|
+
if (!isPlainObject(stepObject)) {
|
|
163
|
+
throw new Error("Invalid step: expected an object.");
|
|
164
|
+
}
|
|
165
|
+
if (typeof stepObject.action !== "string" || stepObject.action.trim().length === 0) {
|
|
166
|
+
throw new Error("Structured step requires non-empty string `action`.");
|
|
167
|
+
}
|
|
168
|
+
const action = stepObject.action.trim();
|
|
169
|
+
if (!STRUCTURED_ACTIONS.has(action)) {
|
|
170
|
+
throw new Error(`Unknown structured action: "${action}".`);
|
|
171
|
+
}
|
|
172
|
+
return action;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function requireStructuredString(stepObject, key, actionName) {
|
|
176
|
+
const value = stepObject[key];
|
|
177
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
178
|
+
throw new Error(`Structured action "${actionName}" requires non-empty string \`${key}\`.`);
|
|
179
|
+
}
|
|
180
|
+
return value;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function requireStructuredNumber(stepObject, key, actionName) {
|
|
184
|
+
const value = stepObject[key];
|
|
185
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
186
|
+
throw new Error(`Structured action "${actionName}" requires finite number \`${key}\`.`);
|
|
187
|
+
}
|
|
188
|
+
return value;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function optionalStructuredNumber(stepObject, key, actionName) {
|
|
192
|
+
if (!Object.prototype.hasOwnProperty.call(stepObject, key)) {
|
|
193
|
+
return undefined;
|
|
194
|
+
}
|
|
195
|
+
return requireStructuredNumber(stepObject, key, actionName);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function requireCoordinatesPair(stepObject, actionName) {
|
|
199
|
+
const hasX = Object.prototype.hasOwnProperty.call(stepObject, "x");
|
|
200
|
+
const hasY = Object.prototype.hasOwnProperty.call(stepObject, "y");
|
|
201
|
+
if (hasX !== hasY) {
|
|
202
|
+
throw new Error(`Structured action "${actionName}" requires both \`x\` and \`y\` together.`);
|
|
203
|
+
}
|
|
204
|
+
if (!hasX) {
|
|
205
|
+
return [];
|
|
206
|
+
}
|
|
207
|
+
const x = requireStructuredNumber(stepObject, "x", actionName);
|
|
208
|
+
const y = requireStructuredNumber(stepObject, "y", actionName);
|
|
209
|
+
return [String(x), String(y)];
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function normalizeStructuredActionStep(stepObject) {
|
|
213
|
+
const action = requireStepAction(stepObject);
|
|
214
|
+
|
|
215
|
+
if (action === "assert") {
|
|
216
|
+
assertStructuredKeys(
|
|
217
|
+
stepObject,
|
|
218
|
+
new Set(["action", "type", "match", "selector", "timeoutMs", "value", "global", "fn", "args"]),
|
|
219
|
+
action,
|
|
220
|
+
);
|
|
221
|
+
const assertionConfig = { ...stepObject };
|
|
222
|
+
delete assertionConfig.action;
|
|
223
|
+
return { kind: "assert", assertionConfig };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (action === "select") {
|
|
227
|
+
assertStructuredKeys(stepObject, new Set(["action", "testId", "steps"]), action);
|
|
228
|
+
const testId = requireStructuredString(stepObject, "testId", action);
|
|
229
|
+
if (!Array.isArray(stepObject.steps)) {
|
|
230
|
+
throw new Error('Structured action "select" requires array `steps`.');
|
|
231
|
+
}
|
|
232
|
+
const nestedSteps = stepObject.steps.map((nestedStep) => normalizeStepValue(nestedStep));
|
|
233
|
+
return { kind: "block", command: "select", args: [testId], nestedSteps };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (action === "click" || action === "dblclick" || action === "hover" || action === "rclick") {
|
|
237
|
+
assertStructuredKeys(stepObject, new Set(["action", "x", "y"]), action);
|
|
238
|
+
return { kind: "command", command: action, args: requireCoordinatesPair(stepObject, action) };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (action === "move") {
|
|
242
|
+
assertStructuredKeys(stepObject, new Set(["action", "x", "y"]), action);
|
|
243
|
+
const x = requireStructuredNumber(stepObject, "x", action);
|
|
244
|
+
const y = requireStructuredNumber(stepObject, "y", action);
|
|
245
|
+
return { kind: "command", command: action, args: [String(x), String(y)] };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (action === "scroll") {
|
|
249
|
+
assertStructuredKeys(stepObject, new Set(["action", "deltaX", "deltaY"]), action);
|
|
250
|
+
const deltaX = requireStructuredNumber(stepObject, "deltaX", action);
|
|
251
|
+
const deltaY = requireStructuredNumber(stepObject, "deltaY", action);
|
|
252
|
+
return { kind: "command", command: action, args: [String(deltaX), String(deltaY)] };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (action === "goto") {
|
|
256
|
+
assertStructuredKeys(stepObject, new Set(["action", "url"]), action);
|
|
257
|
+
return { kind: "command", command: action, args: [requireStructuredString(stepObject, "url", action)] };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (action === "keypress") {
|
|
261
|
+
assertStructuredKeys(stepObject, new Set(["action", "key"]), action);
|
|
262
|
+
return { kind: "command", command: action, args: [requireStructuredString(stepObject, "key", action)] };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (action === "wait") {
|
|
266
|
+
assertStructuredKeys(stepObject, new Set(["action", "ms"]), action);
|
|
267
|
+
const ms = requireStructuredNumber(stepObject, "ms", action);
|
|
268
|
+
return { kind: "command", command: action, args: [String(ms)] };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (action === "setViewport") {
|
|
272
|
+
assertStructuredKeys(stepObject, new Set(["action", "width", "height"]), action);
|
|
273
|
+
const width = requireStructuredNumber(stepObject, "width", action);
|
|
274
|
+
const height = requireStructuredNumber(stepObject, "height", action);
|
|
275
|
+
return { kind: "command", command: action, args: [String(width), String(height)] };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (action === "write") {
|
|
279
|
+
assertStructuredKeys(stepObject, new Set(["action", "value"]), action);
|
|
280
|
+
const value = stepObject.value;
|
|
281
|
+
if (typeof value !== "string") {
|
|
282
|
+
throw new Error('Structured action "write" requires string `value`.');
|
|
283
|
+
}
|
|
284
|
+
return { kind: "command", command: action, args: [value] };
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (action === "upload") {
|
|
288
|
+
assertStructuredKeys(stepObject, new Set(["action", "files"]), action);
|
|
289
|
+
if (!Array.isArray(stepObject.files) || stepObject.files.length === 0) {
|
|
290
|
+
throw new Error('Structured action "upload" requires non-empty array `files`.');
|
|
291
|
+
}
|
|
292
|
+
stepObject.files.forEach((filePath, index) => {
|
|
293
|
+
if (typeof filePath !== "string" || filePath.length === 0) {
|
|
294
|
+
throw new Error(
|
|
295
|
+
`Structured action "upload" requires each file path to be a non-empty string (index ${index}).`,
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
return { kind: "command", command: action, args: [...stepObject.files] };
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (action === "waitFor") {
|
|
303
|
+
assertStructuredKeys(stepObject, new Set(["action", "selector", "state", "timeoutMs"]), action);
|
|
304
|
+
const args = [];
|
|
305
|
+
if (Object.prototype.hasOwnProperty.call(stepObject, "selector")) {
|
|
306
|
+
const selector = requireStructuredString(stepObject, "selector", action);
|
|
307
|
+
args.push(`selector=${selector}`);
|
|
308
|
+
}
|
|
309
|
+
if (Object.prototype.hasOwnProperty.call(stepObject, "state")) {
|
|
310
|
+
const state = requireStructuredString(stepObject, "state", action);
|
|
311
|
+
if (!WAIT_FOR_STATES.has(state)) {
|
|
312
|
+
throw new Error(
|
|
313
|
+
`Structured action "waitFor" has invalid state "${state}". Supported: attached, detached, visible, hidden.`,
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
args.push(`state=${state}`);
|
|
317
|
+
}
|
|
318
|
+
const timeoutMs = optionalStructuredNumber(stepObject, "timeoutMs", action);
|
|
319
|
+
if (timeoutMs !== undefined) {
|
|
320
|
+
args.push(`timeoutMs=${timeoutMs}`);
|
|
321
|
+
}
|
|
322
|
+
return { kind: "command", command: action, args };
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (action === "selectOption") {
|
|
326
|
+
assertStructuredKeys(stepObject, new Set(["action", "value", "label", "index"]), action);
|
|
327
|
+
const hasValue = Object.prototype.hasOwnProperty.call(stepObject, "value");
|
|
328
|
+
const hasLabel = Object.prototype.hasOwnProperty.call(stepObject, "label");
|
|
329
|
+
const hasIndex = Object.prototype.hasOwnProperty.call(stepObject, "index");
|
|
330
|
+
const setCount = [hasValue, hasLabel, hasIndex].filter(Boolean).length;
|
|
331
|
+
|
|
332
|
+
if (setCount !== 1) {
|
|
333
|
+
throw new Error(
|
|
334
|
+
'Structured action "selectOption" requires exactly one of `value`, `label`, or `index`.',
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (hasValue) {
|
|
339
|
+
return {
|
|
340
|
+
kind: "command",
|
|
341
|
+
command: action,
|
|
342
|
+
args: [`value=${requireStructuredString(stepObject, "value", action)}`],
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
if (hasLabel) {
|
|
346
|
+
return {
|
|
347
|
+
kind: "command",
|
|
348
|
+
command: action,
|
|
349
|
+
args: [`label=${requireStructuredString(stepObject, "label", action)}`],
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
return {
|
|
353
|
+
kind: "command",
|
|
354
|
+
command: action,
|
|
355
|
+
args: [`index=${requireStructuredNumber(stepObject, "index", action)}`],
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (action === "customEvent") {
|
|
360
|
+
assertStructuredKeys(stepObject, new Set(["action", "name", "detail"]), action);
|
|
361
|
+
const eventName = requireStructuredString(stepObject, "name", action);
|
|
362
|
+
const args = [eventName];
|
|
363
|
+
if (stepObject.detail !== undefined) {
|
|
364
|
+
if (!isPlainObject(stepObject.detail)) {
|
|
365
|
+
throw new Error('Structured action "customEvent" requires object `detail` when provided.');
|
|
366
|
+
}
|
|
367
|
+
Object.entries(stepObject.detail).forEach(([key, value]) => {
|
|
368
|
+
const formattedValue = typeof value === "string" ? value : JSON.stringify(value);
|
|
369
|
+
args.push(`${key}=${formattedValue}`);
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
return { kind: "command", command: action, args };
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (
|
|
376
|
+
action === "blur"
|
|
377
|
+
|| action === "check"
|
|
378
|
+
|| action === "clear"
|
|
379
|
+
|| action === "focus"
|
|
380
|
+
|| action === "mouseDown"
|
|
381
|
+
|| action === "mouseUp"
|
|
382
|
+
|| action === "rightMouseDown"
|
|
383
|
+
|| action === "rightMouseUp"
|
|
384
|
+
|| action === "screenshot"
|
|
385
|
+
|| action === "uncheck"
|
|
386
|
+
) {
|
|
387
|
+
assertStructuredKeys(stepObject, new Set(["action"]), action);
|
|
388
|
+
return { kind: "command", command: action, args: [] };
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
throw new Error(`Unknown structured action: "${action}".`);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function normalizeLegacyBlockStep(stepObject) {
|
|
395
|
+
const keys = Object.keys(stepObject);
|
|
396
|
+
if (keys.length !== 1) {
|
|
397
|
+
throw new Error(`Step object must have exactly one key, got ${keys.length}.`);
|
|
398
|
+
}
|
|
399
|
+
const [key] = keys;
|
|
400
|
+
if (key === "assert") {
|
|
401
|
+
return { kind: "assert", assertionConfig: stepObject.assert };
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const nestedStepValues = stepObject[key];
|
|
405
|
+
if (!Array.isArray(nestedStepValues)) {
|
|
406
|
+
throw new Error(`Block step "${key}" must contain an array of nested steps.`);
|
|
407
|
+
}
|
|
408
|
+
const { command, args } = parseStepCommand(key);
|
|
409
|
+
const nestedSteps = nestedStepValues.map((nestedStep) => normalizeStepValue(nestedStep));
|
|
410
|
+
return { kind: "block", command, args, nestedSteps };
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function normalizeStepValue(step) {
|
|
414
|
+
if (!isPlainObject(step)) {
|
|
415
|
+
throw new Error("Invalid step: expected an object.");
|
|
416
|
+
}
|
|
417
|
+
if (Object.prototype.hasOwnProperty.call(step, "action")) {
|
|
418
|
+
return normalizeStructuredActionStep(step);
|
|
419
|
+
}
|
|
420
|
+
return normalizeLegacyBlockStep(step);
|
|
421
|
+
}
|
|
422
|
+
|
|
1
423
|
async function click(page, args, context, selectedElement) {
|
|
2
424
|
if (selectedElement) {
|
|
3
425
|
await selectedElement.click();
|
|
4
426
|
} else if (args.length >= 2) {
|
|
5
|
-
await page.mouse.click(
|
|
427
|
+
await page.mouse.click(
|
|
428
|
+
toNumber(args[0], "x"),
|
|
429
|
+
toNumber(args[1], "y"),
|
|
430
|
+
{ button: "left" },
|
|
431
|
+
);
|
|
6
432
|
} else {
|
|
7
|
-
|
|
433
|
+
throw new Error("`click` requires a `select` block target or `x y` coordinates.");
|
|
8
434
|
}
|
|
9
435
|
}
|
|
10
436
|
|
|
11
437
|
async function customEvent(page, args) {
|
|
438
|
+
if (args.length === 0) {
|
|
439
|
+
throw new Error("`customEvent` requires an event name.");
|
|
440
|
+
}
|
|
12
441
|
const [eventName, ...params] = args;
|
|
13
442
|
const payload = {};
|
|
14
|
-
params.forEach(param => {
|
|
15
|
-
const [key, value] = param.split(
|
|
443
|
+
params.forEach((param) => {
|
|
444
|
+
const [key, value] = param.split("=");
|
|
445
|
+
if (!key || value === undefined) {
|
|
446
|
+
throw new Error(
|
|
447
|
+
`Invalid customEvent argument "${param}". Expected key=value.`,
|
|
448
|
+
);
|
|
449
|
+
}
|
|
16
450
|
payload[key] = value;
|
|
17
451
|
});
|
|
18
452
|
await page.evaluate(({ eventName, payload }) => {
|
|
@@ -21,6 +455,9 @@ async function customEvent(page, args) {
|
|
|
21
455
|
}
|
|
22
456
|
|
|
23
457
|
async function goto(page, args) {
|
|
458
|
+
if (!args[0]) {
|
|
459
|
+
throw new Error("`goto` requires a URL argument.");
|
|
460
|
+
}
|
|
24
461
|
await page.goto(args[0], { waitUntil: "networkidle" });
|
|
25
462
|
// Normalize font rendering for consistent screenshots
|
|
26
463
|
await page.addStyleTag({
|
|
@@ -35,6 +472,9 @@ async function goto(page, args) {
|
|
|
35
472
|
}
|
|
36
473
|
|
|
37
474
|
async function keypress(page, args) {
|
|
475
|
+
if (!args[0]) {
|
|
476
|
+
throw new Error("`keypress` requires a key argument.");
|
|
477
|
+
}
|
|
38
478
|
await page.keyboard.press(args[0]);
|
|
39
479
|
}
|
|
40
480
|
|
|
@@ -46,47 +486,352 @@ async function mouseUp(page) {
|
|
|
46
486
|
await page.mouse.up();
|
|
47
487
|
}
|
|
48
488
|
|
|
49
|
-
async function
|
|
489
|
+
async function rightMouseDown(page) {
|
|
50
490
|
await page.mouse.down({ button: 'right' });
|
|
51
491
|
}
|
|
52
492
|
|
|
53
|
-
async function
|
|
493
|
+
async function rightMouseUp(page) {
|
|
54
494
|
await page.mouse.up({ button: 'right' });
|
|
55
495
|
}
|
|
56
496
|
|
|
57
497
|
async function move(page, args) {
|
|
58
|
-
|
|
498
|
+
if (args.length < 2) {
|
|
499
|
+
throw new Error("`move` requires `x y` coordinates.");
|
|
500
|
+
}
|
|
501
|
+
await page.mouse.move(toNumber(args[0], "x"), toNumber(args[1], "y"));
|
|
59
502
|
}
|
|
60
503
|
|
|
61
|
-
async function scroll(page, args){
|
|
62
|
-
|
|
504
|
+
async function scroll(page, args) {
|
|
505
|
+
if (args.length < 2) {
|
|
506
|
+
throw new Error("`scroll` requires `deltaX deltaY` values.");
|
|
507
|
+
}
|
|
508
|
+
await page.mouse.wheel(toNumber(args[0], "deltaX"), toNumber(args[1], "deltaY"));
|
|
63
509
|
}
|
|
64
510
|
|
|
65
511
|
async function rclick(page, args, context, selectedElement) {
|
|
66
512
|
if (selectedElement) {
|
|
67
|
-
await selectedElement.click({ button:
|
|
513
|
+
await selectedElement.click({ button: "right" });
|
|
68
514
|
} else if (args.length >= 2) {
|
|
69
|
-
await page.mouse.click(
|
|
515
|
+
await page.mouse.click(
|
|
516
|
+
toNumber(args[0], "x"),
|
|
517
|
+
toNumber(args[1], "y"),
|
|
518
|
+
{ button: "right" },
|
|
519
|
+
);
|
|
70
520
|
} else {
|
|
71
|
-
|
|
521
|
+
throw new Error("`rclick` requires a `select` block target or `x y` coordinates.");
|
|
72
522
|
}
|
|
73
523
|
}
|
|
74
524
|
|
|
75
525
|
async function wait(page, args) {
|
|
76
|
-
|
|
526
|
+
if (!args[0]) {
|
|
527
|
+
throw new Error("`wait` requires a millisecond duration.");
|
|
528
|
+
}
|
|
529
|
+
await page.waitForTimeout(toNumber(args[0], "ms"));
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
async function setViewport(page, args) {
|
|
533
|
+
if (args.length < 2) {
|
|
534
|
+
throw new Error("`setViewport` requires `width height`.");
|
|
535
|
+
}
|
|
536
|
+
await page.setViewportSize({
|
|
537
|
+
width: toPositiveInteger(args[0], "width"),
|
|
538
|
+
height: toPositiveInteger(args[1], "height"),
|
|
539
|
+
});
|
|
77
540
|
}
|
|
78
541
|
|
|
79
542
|
async function write(page, args, context, selectedElement) {
|
|
543
|
+
const target = requireSelectedElement("write", selectedElement);
|
|
544
|
+
const textToWrite = args.join(" ");
|
|
545
|
+
await target.fill(textToWrite);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
async function hover(page, args, context, selectedElement) {
|
|
80
549
|
if (selectedElement) {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
} else {
|
|
84
|
-
console.warn('`write` command called without a `select` block.');
|
|
550
|
+
await selectedElement.hover();
|
|
551
|
+
return;
|
|
85
552
|
}
|
|
553
|
+
if (args.length >= 2) {
|
|
554
|
+
await page.mouse.move(toNumber(args[0], "x"), toNumber(args[1], "y"));
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
throw new Error("`hover` requires a `select` block target or `x y` coordinates.");
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
async function dblclick(page, args, context, selectedElement) {
|
|
561
|
+
if (selectedElement) {
|
|
562
|
+
await selectedElement.dblclick();
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
if (args.length >= 2) {
|
|
566
|
+
await page.mouse.dblclick(toNumber(args[0], "x"), toNumber(args[1], "y"));
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
throw new Error("`dblclick` requires a `select` block target or `x y` coordinates.");
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
async function focus(page, args, context, selectedElement) {
|
|
573
|
+
const target = requireSelectedElement("focus", selectedElement);
|
|
574
|
+
await target.focus();
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
async function blur(page, args, context, selectedElement) {
|
|
578
|
+
const target = requireSelectedElement("blur", selectedElement);
|
|
579
|
+
await target.evaluate((element) => element.blur());
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
async function clear(page, args, context, selectedElement) {
|
|
583
|
+
const target = requireSelectedElement("clear", selectedElement);
|
|
584
|
+
await target.fill("");
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
async function check(page, args, context, selectedElement) {
|
|
588
|
+
const target = requireSelectedElement("check", selectedElement);
|
|
589
|
+
await target.check();
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
async function uncheck(page, args, context, selectedElement) {
|
|
593
|
+
const target = requireSelectedElement("uncheck", selectedElement);
|
|
594
|
+
await target.uncheck();
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
async function selectOption(page, args, context, selectedElement) {
|
|
598
|
+
const target = requireSelectedElement("selectOption", selectedElement);
|
|
599
|
+
const { named, positional } = parseNamedArgs(args);
|
|
600
|
+
|
|
601
|
+
const hasNamed =
|
|
602
|
+
named.value !== undefined
|
|
603
|
+
|| named.label !== undefined
|
|
604
|
+
|| named.index !== undefined;
|
|
605
|
+
|
|
606
|
+
if (hasNamed) {
|
|
607
|
+
const option = {};
|
|
608
|
+
if (named.value !== undefined) {
|
|
609
|
+
option.value = named.value;
|
|
610
|
+
}
|
|
611
|
+
if (named.label !== undefined) {
|
|
612
|
+
option.label = named.label;
|
|
613
|
+
}
|
|
614
|
+
if (named.index !== undefined) {
|
|
615
|
+
option.index = toNumber(named.index, "index");
|
|
616
|
+
}
|
|
617
|
+
await target.selectOption(option);
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
if (positional.length === 0) {
|
|
622
|
+
throw new Error(
|
|
623
|
+
"`selectOption` requires an option value/label or key=value args (value=, label=, index=).",
|
|
624
|
+
);
|
|
625
|
+
}
|
|
626
|
+
await target.selectOption(positional[0]);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
async function upload(page, args, context, selectedElement) {
|
|
630
|
+
const target = requireSelectedElement("upload", selectedElement);
|
|
631
|
+
const files = args.filter((token) => token.length > 0);
|
|
632
|
+
if (files.length === 0) {
|
|
633
|
+
throw new Error("`upload` requires one or more file paths.");
|
|
634
|
+
}
|
|
635
|
+
await target.setInputFiles(files);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
async function waitFor(page, args, context, selectedElement) {
|
|
639
|
+
const { named, positional } = parseNamedArgs(args);
|
|
640
|
+
const state = named.state ?? positional[1] ?? "visible";
|
|
641
|
+
const timeout = parseTimeoutValue(named.timeoutMs ?? named.timeout ?? positional[2]);
|
|
642
|
+
const waitOptions = { state };
|
|
643
|
+
if (timeout !== undefined) {
|
|
644
|
+
waitOptions.timeout = timeout;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
if (selectedElement) {
|
|
648
|
+
await selectedElement.waitFor(waitOptions);
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
const selector = named.selector ?? positional[0];
|
|
653
|
+
if (!selector) {
|
|
654
|
+
throw new Error(
|
|
655
|
+
"`waitFor` requires a selector (or a selected element in a `select` block).",
|
|
656
|
+
);
|
|
657
|
+
}
|
|
658
|
+
await page.waitForSelector(selector, waitOptions);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function requireAssertType(assertionConfig) {
|
|
662
|
+
if (!isPlainObject(assertionConfig)) {
|
|
663
|
+
throw new Error("Structured assert step must be an object.");
|
|
664
|
+
}
|
|
665
|
+
const { type } = assertionConfig;
|
|
666
|
+
if (typeof type !== "string" || type.trim().length === 0) {
|
|
667
|
+
throw new Error("Structured assert step requires a non-empty `type`.");
|
|
668
|
+
}
|
|
669
|
+
return type;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function requireMatchMode(assertionConfig, defaultMode = "includes") {
|
|
673
|
+
const mode = assertionConfig.match ?? defaultMode;
|
|
674
|
+
if (mode !== "includes" && mode !== "equals") {
|
|
675
|
+
throw new Error(`Unsupported assert match mode "${mode}". Supported: includes, equals.`);
|
|
676
|
+
}
|
|
677
|
+
return mode;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
async function assertStructured(page, assertionConfig, selectedElement) {
|
|
681
|
+
const type = requireAssertType(assertionConfig);
|
|
682
|
+
|
|
683
|
+
if (type === "url") {
|
|
684
|
+
if (typeof assertionConfig.value !== "string" || assertionConfig.value.length === 0) {
|
|
685
|
+
throw new Error("`assert.type=url` requires non-empty string `value`.");
|
|
686
|
+
}
|
|
687
|
+
const currentUrl = page.url();
|
|
688
|
+
const expected = assertionConfig.value;
|
|
689
|
+
const matchMode = requireMatchMode(assertionConfig);
|
|
690
|
+
const ok = matchMode === "equals"
|
|
691
|
+
? currentUrl === expected
|
|
692
|
+
: currentUrl.includes(expected);
|
|
693
|
+
if (!ok) {
|
|
694
|
+
throw new Error(
|
|
695
|
+
`assert url failed: expected "${currentUrl}" to ${matchMode} "${expected}".`,
|
|
696
|
+
);
|
|
697
|
+
}
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
if (type === "exists") {
|
|
702
|
+
const timeout = parseTimeoutValue(assertionConfig.timeoutMs);
|
|
703
|
+
if (selectedElement && assertionConfig.selector === undefined) {
|
|
704
|
+
const count = await selectedElement.count();
|
|
705
|
+
if (count < 1) {
|
|
706
|
+
throw new Error("assert exists failed: selected element was not found.");
|
|
707
|
+
}
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
if (typeof assertionConfig.selector !== "string" || assertionConfig.selector.length === 0) {
|
|
712
|
+
throw new Error("`assert.type=exists` requires `selector` when not in a select block.");
|
|
713
|
+
}
|
|
714
|
+
const locator = page.locator(assertionConfig.selector);
|
|
715
|
+
if (timeout !== undefined) {
|
|
716
|
+
await locator.first().waitFor({ state: "attached", timeout });
|
|
717
|
+
}
|
|
718
|
+
const count = await locator.count();
|
|
719
|
+
if (count < 1) {
|
|
720
|
+
throw new Error(`assert exists failed: selector "${assertionConfig.selector}" matched 0 elements.`);
|
|
721
|
+
}
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
if (type === "visible" || type === "hidden") {
|
|
726
|
+
const timeout = parseTimeoutValue(assertionConfig.timeoutMs);
|
|
727
|
+
const waitOptions = { state: type };
|
|
728
|
+
if (timeout !== undefined) {
|
|
729
|
+
waitOptions.timeout = timeout;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
if (selectedElement && assertionConfig.selector === undefined) {
|
|
733
|
+
await selectedElement.waitFor(waitOptions);
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
if (typeof assertionConfig.selector !== "string" || assertionConfig.selector.length === 0) {
|
|
738
|
+
throw new Error(`\`assert.type=${type}\` requires \`selector\` when not in a select block.`);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
await page.waitForSelector(assertionConfig.selector, waitOptions);
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
if (type === "text") {
|
|
746
|
+
const expected = assertionConfig.value;
|
|
747
|
+
if (typeof expected !== "string") {
|
|
748
|
+
throw new Error("`assert.type=text` requires string `value`.");
|
|
749
|
+
}
|
|
750
|
+
const matchMode = requireMatchMode(assertionConfig);
|
|
751
|
+
|
|
752
|
+
let actualText = "";
|
|
753
|
+
if (selectedElement && assertionConfig.selector === undefined) {
|
|
754
|
+
actualText = (await selectedElement.textContent()) ?? "";
|
|
755
|
+
} else {
|
|
756
|
+
if (typeof assertionConfig.selector !== "string" || assertionConfig.selector.length === 0) {
|
|
757
|
+
throw new Error("`assert.type=text` requires `selector` when not in a select block.");
|
|
758
|
+
}
|
|
759
|
+
actualText = (await page.locator(assertionConfig.selector).first().textContent()) ?? "";
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
const ok = matchMode === "equals"
|
|
763
|
+
? actualText === expected
|
|
764
|
+
: actualText.includes(expected);
|
|
765
|
+
if (!ok) {
|
|
766
|
+
throw new Error(
|
|
767
|
+
`assert text failed: expected "${actualText}" to ${matchMode} "${expected}".`,
|
|
768
|
+
);
|
|
769
|
+
}
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
if (type === "js") {
|
|
774
|
+
const hasGlobal = typeof assertionConfig.global === "string" && assertionConfig.global.length > 0;
|
|
775
|
+
const hasFn = typeof assertionConfig.fn === "string" && assertionConfig.fn.length > 0;
|
|
776
|
+
if (hasGlobal === hasFn) {
|
|
777
|
+
throw new Error("`assert.type=js` requires exactly one of `global` or `fn`.");
|
|
778
|
+
}
|
|
779
|
+
if (!Object.prototype.hasOwnProperty.call(assertionConfig, "value")) {
|
|
780
|
+
throw new Error("`assert.type=js` requires `value`.");
|
|
781
|
+
}
|
|
782
|
+
const args = assertionConfig.args ?? [];
|
|
783
|
+
if (!Array.isArray(args)) {
|
|
784
|
+
throw new Error("`assert.type=js` expects `args` to be an array when provided.");
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
let actual;
|
|
788
|
+
try {
|
|
789
|
+
actual = await page.evaluate(async ({ globalPath, fnPath, fnArgs }) => {
|
|
790
|
+
const resolvePath = (root, dottedPath) => {
|
|
791
|
+
return dottedPath.split(".").reduce((acc, key) => {
|
|
792
|
+
if (acc === null || acc === undefined) {
|
|
793
|
+
return undefined;
|
|
794
|
+
}
|
|
795
|
+
return acc[key];
|
|
796
|
+
}, root);
|
|
797
|
+
};
|
|
798
|
+
|
|
799
|
+
if (globalPath) {
|
|
800
|
+
return resolvePath(window, globalPath);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
const fn = resolvePath(window, fnPath);
|
|
804
|
+
if (typeof fn !== "function") {
|
|
805
|
+
throw new Error(`Expected function at window.${fnPath}.`);
|
|
806
|
+
}
|
|
807
|
+
return await fn(...fnArgs);
|
|
808
|
+
}, {
|
|
809
|
+
globalPath: hasGlobal ? assertionConfig.global : null,
|
|
810
|
+
fnPath: hasFn ? assertionConfig.fn : null,
|
|
811
|
+
fnArgs: args,
|
|
812
|
+
});
|
|
813
|
+
} catch (error) {
|
|
814
|
+
throw new Error(`assert js failed: ${error?.message ?? String(error)}.`);
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
if (!deepEqual(actual, assertionConfig.value)) {
|
|
818
|
+
throw new Error(
|
|
819
|
+
`assert js failed: expected ${formatValue(assertionConfig.value)}, got ${formatValue(actual)}.`,
|
|
820
|
+
);
|
|
821
|
+
}
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
throw new Error(
|
|
826
|
+
`Unsupported assert type "${type}". Supported: url, exists, visible, hidden, text, js.`,
|
|
827
|
+
);
|
|
86
828
|
}
|
|
87
829
|
|
|
88
830
|
async function select(page, args) {
|
|
89
831
|
const testId = args[0];
|
|
832
|
+
if (!testId) {
|
|
833
|
+
throw new Error("`select` requires a test id.");
|
|
834
|
+
}
|
|
90
835
|
const hostElementLocator = page.getByTestId(testId);
|
|
91
836
|
|
|
92
837
|
const interactiveElementLocator = hostElementLocator.locator(
|
|
@@ -103,61 +848,82 @@ async function select(page, args) {
|
|
|
103
848
|
}
|
|
104
849
|
|
|
105
850
|
export function createSteps(page, context) {
|
|
106
|
-
let screenshotIndex = 0;
|
|
107
|
-
|
|
108
851
|
async function screenshot() {
|
|
109
|
-
|
|
110
|
-
const screenshotPath = await context.takeAndSaveScreenshot(page, `${context.baseName}-${screenshotIndex}`);
|
|
852
|
+
const screenshotPath = await context.takeAndSaveScreenshot(page, context.baseName);
|
|
111
853
|
console.log(`Screenshot saved: ${screenshotPath}`);
|
|
112
854
|
}
|
|
113
855
|
|
|
114
856
|
const actionHandlers = {
|
|
857
|
+
blur,
|
|
858
|
+
check,
|
|
859
|
+
clear,
|
|
115
860
|
click,
|
|
116
861
|
customEvent,
|
|
862
|
+
dblclick,
|
|
863
|
+
focus,
|
|
117
864
|
goto,
|
|
865
|
+
hover,
|
|
118
866
|
keypress,
|
|
119
867
|
mouseDown,
|
|
120
868
|
mouseUp,
|
|
121
869
|
move,
|
|
122
870
|
rclick,
|
|
871
|
+
rightMouseDown,
|
|
872
|
+
rightMouseUp,
|
|
123
873
|
scroll,
|
|
124
|
-
|
|
125
|
-
rMouseUp,
|
|
874
|
+
setViewport,
|
|
126
875
|
screenshot,
|
|
127
876
|
select,
|
|
877
|
+
selectOption,
|
|
878
|
+
uncheck,
|
|
879
|
+
upload,
|
|
128
880
|
wait,
|
|
881
|
+
waitFor,
|
|
129
882
|
write,
|
|
130
883
|
};
|
|
131
884
|
|
|
132
|
-
async function
|
|
133
|
-
|
|
885
|
+
async function executeCommand(command, args, selectedElement) {
|
|
886
|
+
if (!command) {
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
134
889
|
const actionFn = actionHandlers[command];
|
|
135
890
|
if (actionFn) {
|
|
136
891
|
await actionFn(page, args, context, selectedElement);
|
|
137
892
|
} else {
|
|
138
|
-
|
|
893
|
+
throw new Error(`Unknown step command: "${command}"`);
|
|
139
894
|
}
|
|
140
895
|
}
|
|
141
896
|
|
|
897
|
+
async function executeNormalizedStep(normalizedStep, selectedElement) {
|
|
898
|
+
if (normalizedStep.kind === "assert") {
|
|
899
|
+
await assertStructured(page, normalizedStep.assertionConfig, selectedElement);
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
if (normalizedStep.kind === "block") {
|
|
904
|
+
const { command, args, nestedSteps } = normalizedStep;
|
|
905
|
+
const blockFn = actionHandlers[command];
|
|
906
|
+
if (!blockFn) {
|
|
907
|
+
throw new Error(`Unsupported block command: "${command}".`);
|
|
908
|
+
}
|
|
909
|
+
const blockSelectedElement = await blockFn(page, args, context, null);
|
|
910
|
+
for (const nestedStep of nestedSteps) {
|
|
911
|
+
await executeNormalizedStep(nestedStep, blockSelectedElement);
|
|
912
|
+
}
|
|
913
|
+
return;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
await executeCommand(normalizedStep.command, normalizedStep.args, selectedElement);
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
async function executeStepValue(step, selectedElement) {
|
|
920
|
+
const normalizedStep = normalizeStepValue(step);
|
|
921
|
+
await executeNormalizedStep(normalizedStep, selectedElement);
|
|
922
|
+
}
|
|
923
|
+
|
|
142
924
|
return {
|
|
143
925
|
async executeStep(step) {
|
|
144
|
-
|
|
145
|
-
await executeSingleStep(step, null);
|
|
146
|
-
} else if (typeof step === 'object' && step !== null) {
|
|
147
|
-
const blockCommandString = Object.keys(step)[0];
|
|
148
|
-
const nestedStepStrings = step[blockCommandString];
|
|
149
|
-
const [command, ...args] = blockCommandString.split(" ");
|
|
150
|
-
|
|
151
|
-
const blockFn = actionHandlers[command];
|
|
152
|
-
if (blockFn) {
|
|
153
|
-
const selectedElement = await blockFn(page, args, context, null);
|
|
154
|
-
for (const nestedStep of nestedStepStrings) {
|
|
155
|
-
await executeSingleStep(nestedStep, selectedElement);
|
|
156
|
-
}
|
|
157
|
-
} else {
|
|
158
|
-
console.warn(`Unsupported block command: "${command}".`);
|
|
159
|
-
}
|
|
160
|
-
}
|
|
926
|
+
await executeStepValue(step, null);
|
|
161
927
|
}
|
|
162
928
|
};
|
|
163
|
-
}
|
|
929
|
+
}
|