@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,787 @@
|
|
|
1
|
+
import { resolveTemplate } from './templating.js';
|
|
2
|
+
import { resolveTargetWithFallback, selectorToTarget } from './target.js';
|
|
3
|
+
import { search as jmesSearch } from '@jmespath-community/jmespath';
|
|
4
|
+
/**
|
|
5
|
+
* Executes a navigate step
|
|
6
|
+
*/
|
|
7
|
+
async function executeNavigate(ctx, step) {
|
|
8
|
+
try {
|
|
9
|
+
await ctx.page.goto(step.params.url, {
|
|
10
|
+
waitUntil: step.params.waitUntil ?? 'networkidle',
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
catch (err) {
|
|
14
|
+
// Ignore timeout errors — the page content is usually loaded even if
|
|
15
|
+
// networkidle never fires (common on SPAs with long-polling / websockets).
|
|
16
|
+
if (err?.name === 'TimeoutError' || err?.message?.includes('Timeout')) {
|
|
17
|
+
// Navigation reached the page but the load event didn't settle in time.
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
throw err;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Executes an extract_title step
|
|
25
|
+
*/
|
|
26
|
+
async function executeExtractTitle(ctx, step) {
|
|
27
|
+
const title = await ctx.page.title();
|
|
28
|
+
ctx.collectibles[step.params.out] = title;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Executes an extract_text step
|
|
32
|
+
*/
|
|
33
|
+
async function executeExtractText(ctx, step) {
|
|
34
|
+
// Support both legacy selector and new target
|
|
35
|
+
const targetOrAnyOf = step.params.target ?? (step.params.selector ? selectorToTarget(step.params.selector) : null);
|
|
36
|
+
if (!targetOrAnyOf) {
|
|
37
|
+
throw new Error('ExtractText step must have either "target" or "selector"');
|
|
38
|
+
}
|
|
39
|
+
// Resolve target with fallback and scope
|
|
40
|
+
const { locator, matchedTarget, matchedCount } = await resolveTargetWithFallback(ctx.page, targetOrAnyOf, step.params.scope);
|
|
41
|
+
// Log matched target for diagnostics (if hint provided, include it)
|
|
42
|
+
if (step.params.hint) {
|
|
43
|
+
console.log(`[ExtractText:${step.id}] Matched target: ${JSON.stringify(matchedTarget)}, count: ${matchedCount}, hint: ${step.params.hint}`);
|
|
44
|
+
}
|
|
45
|
+
const count = matchedCount;
|
|
46
|
+
if (count === 0) {
|
|
47
|
+
// No elements found, use default if provided
|
|
48
|
+
ctx.collectibles[step.params.out] = step.params.default ?? '';
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (step.params.first === true) {
|
|
52
|
+
// Get first element only (explicit first: true)
|
|
53
|
+
const text = await locator.first().textContent();
|
|
54
|
+
ctx.collectibles[step.params.out] = step.params.trim ?? true ? text?.trim() ?? '' : text ?? '';
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
// Get all elements (default behavior for scraping)
|
|
58
|
+
const texts = [];
|
|
59
|
+
for (let i = 0; i < count; i++) {
|
|
60
|
+
const text = await locator.nth(i).textContent();
|
|
61
|
+
const processed = step.params.trim ?? true ? text?.trim() ?? '' : text ?? '';
|
|
62
|
+
texts.push(processed);
|
|
63
|
+
}
|
|
64
|
+
ctx.collectibles[step.params.out] = texts;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Executes a sleep step
|
|
69
|
+
*/
|
|
70
|
+
async function executeSleep(ctx, step) {
|
|
71
|
+
await new Promise((resolve) => setTimeout(resolve, step.params.durationMs));
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Executes a wait_for step
|
|
75
|
+
*/
|
|
76
|
+
async function executeWaitFor(ctx, step) {
|
|
77
|
+
const timeout = step.timeoutMs ?? step.params.timeoutMs ?? 30000;
|
|
78
|
+
// Support both legacy selector and new target
|
|
79
|
+
const targetOrAnyOf = step.params.target ?? (step.params.selector ? selectorToTarget(step.params.selector) : null);
|
|
80
|
+
if (targetOrAnyOf) {
|
|
81
|
+
const { locator, matchedTarget, matchedCount } = await resolveTargetWithFallback(ctx.page, targetOrAnyOf, step.params.scope);
|
|
82
|
+
// Log matched target for diagnostics
|
|
83
|
+
if (step.params.hint) {
|
|
84
|
+
console.log(`[WaitFor:${step.id}] Matched target: ${JSON.stringify(matchedTarget)}, count: ${matchedCount}, hint: ${step.params.hint}`);
|
|
85
|
+
}
|
|
86
|
+
if (step.params.visible ?? true) {
|
|
87
|
+
await locator.first().waitFor({ state: 'visible', timeout });
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
await locator.first().waitFor({ state: 'attached', timeout });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
else if (step.params.url) {
|
|
94
|
+
if (typeof step.params.url === 'string') {
|
|
95
|
+
await ctx.page.waitForURL(step.params.url, { timeout });
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
// For pattern matching, use a function matcher
|
|
99
|
+
const urlPattern = step.params.url.pattern;
|
|
100
|
+
const exactMatch = step.params.url.exact ?? false;
|
|
101
|
+
await ctx.page.waitForURL((url) => {
|
|
102
|
+
if (exactMatch) {
|
|
103
|
+
return url.href === urlPattern;
|
|
104
|
+
}
|
|
105
|
+
return url.href.includes(urlPattern);
|
|
106
|
+
}, { timeout });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
else if (step.params.loadState) {
|
|
110
|
+
await ctx.page.waitForLoadState(step.params.loadState, { timeout });
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
throw new Error('wait_for step must specify selector, url, or loadState');
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Executes a click step
|
|
118
|
+
*/
|
|
119
|
+
async function executeClick(ctx, step) {
|
|
120
|
+
// Support both legacy selector and new target
|
|
121
|
+
const targetOrAnyOf = step.params.target ?? (step.params.selector ? selectorToTarget(step.params.selector) : null);
|
|
122
|
+
if (!targetOrAnyOf) {
|
|
123
|
+
throw new Error('Click step must have either "target" or "selector"');
|
|
124
|
+
}
|
|
125
|
+
// Resolve target with fallback and scope
|
|
126
|
+
const { locator, matchedTarget, matchedCount } = await resolveTargetWithFallback(ctx.page, targetOrAnyOf, step.params.scope);
|
|
127
|
+
// Log matched target for diagnostics
|
|
128
|
+
if (step.params.hint) {
|
|
129
|
+
console.log(`[Click:${step.id}] Matched target: ${JSON.stringify(matchedTarget)}, count: ${matchedCount}, hint: ${step.params.hint}`);
|
|
130
|
+
}
|
|
131
|
+
const target = step.params.first ?? true ? locator.first() : locator;
|
|
132
|
+
if (step.params.waitForVisible ?? true) {
|
|
133
|
+
await target.waitFor({ state: 'visible' });
|
|
134
|
+
}
|
|
135
|
+
await target.click();
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Executes a fill step
|
|
139
|
+
*/
|
|
140
|
+
async function executeFill(ctx, step) {
|
|
141
|
+
// Support both legacy selector and new target
|
|
142
|
+
const targetOrAnyOf = step.params.target ?? (step.params.selector ? selectorToTarget(step.params.selector) : null);
|
|
143
|
+
if (!targetOrAnyOf) {
|
|
144
|
+
throw new Error('Fill step must have either "target" or "selector"');
|
|
145
|
+
}
|
|
146
|
+
// Resolve target with fallback and scope
|
|
147
|
+
const { locator, matchedTarget, matchedCount } = await resolveTargetWithFallback(ctx.page, targetOrAnyOf, step.params.scope);
|
|
148
|
+
// Log matched target for diagnostics
|
|
149
|
+
if (step.params.hint) {
|
|
150
|
+
console.log(`[Fill:${step.id}] Matched target: ${JSON.stringify(matchedTarget)}, count: ${matchedCount}, hint: ${step.params.hint}`);
|
|
151
|
+
}
|
|
152
|
+
const target = step.params.first ?? true ? locator.first() : locator;
|
|
153
|
+
await target.waitFor({ state: 'visible' });
|
|
154
|
+
if (step.params.clear ?? true) {
|
|
155
|
+
await target.fill(step.params.value);
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
await target.type(step.params.value);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Executes an extract_attribute step
|
|
163
|
+
*/
|
|
164
|
+
async function executeExtractAttribute(ctx, step) {
|
|
165
|
+
// Support both legacy selector and new target
|
|
166
|
+
const targetOrAnyOf = step.params.target ?? (step.params.selector ? selectorToTarget(step.params.selector) : null);
|
|
167
|
+
if (!targetOrAnyOf) {
|
|
168
|
+
throw new Error('ExtractAttribute step must have either "target" or "selector"');
|
|
169
|
+
}
|
|
170
|
+
// Resolve target with fallback and scope
|
|
171
|
+
const { locator, matchedTarget, matchedCount } = await resolveTargetWithFallback(ctx.page, targetOrAnyOf, step.params.scope);
|
|
172
|
+
// Log matched target for diagnostics
|
|
173
|
+
if (step.params.hint) {
|
|
174
|
+
console.log(`[ExtractAttribute:${step.id}] Matched target: ${JSON.stringify(matchedTarget)}, count: ${matchedCount}, hint: ${step.params.hint}`);
|
|
175
|
+
}
|
|
176
|
+
const count = matchedCount;
|
|
177
|
+
if (count === 0) {
|
|
178
|
+
ctx.collectibles[step.params.out] = step.params.default ?? '';
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (step.params.first === true) {
|
|
182
|
+
// Get first element only (explicit first: true)
|
|
183
|
+
const value = await locator.first().getAttribute(step.params.attribute);
|
|
184
|
+
ctx.collectibles[step.params.out] = value ?? step.params.default ?? '';
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
// Get all elements (default behavior for scraping)
|
|
188
|
+
const values = [];
|
|
189
|
+
for (let i = 0; i < count; i++) {
|
|
190
|
+
const value = await locator.nth(i).getAttribute(step.params.attribute);
|
|
191
|
+
values.push(value);
|
|
192
|
+
}
|
|
193
|
+
ctx.collectibles[step.params.out] = values;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Executes an assert step
|
|
198
|
+
*/
|
|
199
|
+
async function executeAssert(ctx, step) {
|
|
200
|
+
const errors = [];
|
|
201
|
+
// Support both legacy selector and new target
|
|
202
|
+
const targetOrAnyOf = step.params.target ?? (step.params.selector ? selectorToTarget(step.params.selector) : null);
|
|
203
|
+
if (targetOrAnyOf) {
|
|
204
|
+
const { locator, matchedTarget, matchedCount } = await resolveTargetWithFallback(ctx.page, targetOrAnyOf, step.params.scope);
|
|
205
|
+
// Log matched target for diagnostics
|
|
206
|
+
if (step.params.hint) {
|
|
207
|
+
console.log(`[Assert:${step.id}] Matched target: ${JSON.stringify(matchedTarget)}, count: ${matchedCount}, hint: ${step.params.hint}`);
|
|
208
|
+
}
|
|
209
|
+
if (matchedCount === 0) {
|
|
210
|
+
errors.push(`Element not found: ${JSON.stringify(matchedTarget)}`);
|
|
211
|
+
}
|
|
212
|
+
else if (step.params.visible !== undefined) {
|
|
213
|
+
const isVisible = await locator.first().isVisible();
|
|
214
|
+
if (step.params.visible && !isVisible) {
|
|
215
|
+
errors.push(`Element not visible: ${JSON.stringify(matchedTarget)}`);
|
|
216
|
+
}
|
|
217
|
+
else if (!step.params.visible && isVisible) {
|
|
218
|
+
errors.push(`Element should not be visible: ${JSON.stringify(matchedTarget)}`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if (step.params.textIncludes) {
|
|
222
|
+
const text = await locator.first().textContent();
|
|
223
|
+
if (!text || !text.includes(step.params.textIncludes)) {
|
|
224
|
+
errors.push(`Element text does not include "${step.params.textIncludes}": ${JSON.stringify(matchedTarget)}`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
if (step.params.urlIncludes) {
|
|
229
|
+
const url = ctx.page.url();
|
|
230
|
+
if (!url.includes(step.params.urlIncludes)) {
|
|
231
|
+
errors.push(`URL does not include "${step.params.urlIncludes}": ${url}`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if (errors.length > 0) {
|
|
235
|
+
const message = step.params.message || errors.join('; ');
|
|
236
|
+
throw new Error(`Assertion failed: ${message}`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Executes a set_var step
|
|
241
|
+
* Note: Value may contain templates that need to be resolved
|
|
242
|
+
*/
|
|
243
|
+
async function executeSetVar(ctx, step) {
|
|
244
|
+
// If value is a string, it might contain templates - resolve them
|
|
245
|
+
let resolvedValue = step.params.value;
|
|
246
|
+
if (typeof resolvedValue === 'string') {
|
|
247
|
+
// Resolve templates in the value
|
|
248
|
+
const varContext = {
|
|
249
|
+
inputs: ctx.inputs,
|
|
250
|
+
vars: ctx.vars,
|
|
251
|
+
};
|
|
252
|
+
resolvedValue = resolveTemplate(resolvedValue, varContext);
|
|
253
|
+
}
|
|
254
|
+
ctx.vars[step.params.name] = resolvedValue;
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Extract value from object using JMESPath expression.
|
|
258
|
+
* Returns { value, hint? } where hint contains diagnostic info on failure.
|
|
259
|
+
*
|
|
260
|
+
* JMESPath supports:
|
|
261
|
+
* - "results[0].name" - array access and nested fields
|
|
262
|
+
* - "results[*].name" - wildcard to get all names
|
|
263
|
+
* - "results[*].{name: name, url: url}" - object projection
|
|
264
|
+
* - "results[?status == 'active']" - filtering
|
|
265
|
+
* - "results | [0]" - piping
|
|
266
|
+
*
|
|
267
|
+
* For backward compatibility, JSONPath-style paths starting with "$." are
|
|
268
|
+
* automatically converted (the "$." prefix is stripped).
|
|
269
|
+
*/
|
|
270
|
+
function getByPath(obj, path) {
|
|
271
|
+
let trimmed = path.trim();
|
|
272
|
+
// Backward compatibility: strip JSONPath-style "$." prefix
|
|
273
|
+
// $.results[0].name -> results[0].name
|
|
274
|
+
if (trimmed.startsWith('$.')) {
|
|
275
|
+
trimmed = trimmed.slice(2);
|
|
276
|
+
}
|
|
277
|
+
else if (trimmed === '$') {
|
|
278
|
+
// "$" alone means root in JSONPath - return the whole object
|
|
279
|
+
return { value: obj };
|
|
280
|
+
}
|
|
281
|
+
// Empty path returns the whole object
|
|
282
|
+
if (!trimmed) {
|
|
283
|
+
return { value: obj };
|
|
284
|
+
}
|
|
285
|
+
try {
|
|
286
|
+
const result = jmesSearch(obj, trimmed);
|
|
287
|
+
// Check for null/undefined results and provide diagnostic hint
|
|
288
|
+
if (result === null || result === undefined) {
|
|
289
|
+
return {
|
|
290
|
+
value: result,
|
|
291
|
+
hint: `JMESPath '${trimmed}' matched nothing (returned ${result}). Verify the path exists. Try a simpler path like 'data' or 'keys(@)' to inspect the structure.`,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
// Check for empty array results
|
|
295
|
+
if (Array.isArray(result) && result.length === 0) {
|
|
296
|
+
return {
|
|
297
|
+
value: result,
|
|
298
|
+
hint: `JMESPath '${trimmed}' returned an empty array. The path may be correct but no items matched.`,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
return { value: result };
|
|
302
|
+
}
|
|
303
|
+
catch (error) {
|
|
304
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
305
|
+
return {
|
|
306
|
+
value: undefined,
|
|
307
|
+
hint: `JMESPath syntax error in '${trimmed}': ${errorMessage}. See https://jmespath.org for syntax reference.`,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
function sleepMs(ms) {
|
|
312
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Executes a network_find step. If waitForMs is set and no match is found initially, polls the buffer until a match appears or timeout.
|
|
316
|
+
*/
|
|
317
|
+
async function executeNetworkFind(ctx, step) {
|
|
318
|
+
if (!ctx.networkCapture) {
|
|
319
|
+
throw new Error('network_find requires an active browser session with network capture. Run the flow in a context that has network capture enabled.');
|
|
320
|
+
}
|
|
321
|
+
const where = step.params.where ?? {};
|
|
322
|
+
const pick = step.params.pick ?? 'last';
|
|
323
|
+
const waitForMs = step.params.waitForMs ?? 0;
|
|
324
|
+
const pollIntervalMs = Math.min(Math.max(step.params.pollIntervalMs ?? 400, 100), 5000);
|
|
325
|
+
// When matching on response body, the capture's response handler is async (await response.body()).
|
|
326
|
+
// Give in-flight handlers time to complete before the first lookup so entries have responseBodyText.
|
|
327
|
+
if (where.responseContains != null) {
|
|
328
|
+
await sleepMs(Math.min(pollIntervalMs * 4, 2000));
|
|
329
|
+
}
|
|
330
|
+
let requestId = ctx.networkCapture.getRequestIdByIndex(where, pick);
|
|
331
|
+
if (requestId == null && waitForMs > 0) {
|
|
332
|
+
const deadline = Date.now() + waitForMs;
|
|
333
|
+
while (Date.now() < deadline) {
|
|
334
|
+
await sleepMs(pollIntervalMs);
|
|
335
|
+
requestId = ctx.networkCapture.getRequestIdByIndex(where, pick);
|
|
336
|
+
if (requestId != null)
|
|
337
|
+
break;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
if (requestId == null) {
|
|
341
|
+
// Get ALL captured requests to help debug (larger buffer to catch the request)
|
|
342
|
+
const allRequests = ctx.networkCapture.list(100, 'all');
|
|
343
|
+
// Build search terms from the where clause for relevance filtering
|
|
344
|
+
const searchTerms = [];
|
|
345
|
+
if (where.urlIncludes) {
|
|
346
|
+
// Split URL pattern into searchable terms (e.g., "/api/discovery/search" -> ["api", "discovery", "search"])
|
|
347
|
+
searchTerms.push(...where.urlIncludes.split(/[\/\-_.]/).filter(s => s.length > 2));
|
|
348
|
+
}
|
|
349
|
+
if (where.urlRegex) {
|
|
350
|
+
// Extract alphanumeric words from regex
|
|
351
|
+
searchTerms.push(...where.urlRegex.match(/[a-zA-Z]{3,}/g) || []);
|
|
352
|
+
}
|
|
353
|
+
// Find relevant requests (those that match any search term)
|
|
354
|
+
let relevantRequests = allRequests;
|
|
355
|
+
if (searchTerms.length > 0) {
|
|
356
|
+
relevantRequests = allRequests.filter(r => {
|
|
357
|
+
const urlLower = r.url.toLowerCase();
|
|
358
|
+
return searchTerms.some(term => urlLower.includes(term.toLowerCase()));
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
// If no relevant requests found, fall back to API requests, then all
|
|
362
|
+
let displayRequests = relevantRequests;
|
|
363
|
+
let filterDesc = `matching "${searchTerms.join('", "')}"`;
|
|
364
|
+
if (displayRequests.length === 0) {
|
|
365
|
+
displayRequests = allRequests.filter(r => r.resourceType === 'xhr' ||
|
|
366
|
+
r.resourceType === 'fetch' ||
|
|
367
|
+
/\/api\//i.test(r.url) ||
|
|
368
|
+
/graphql/i.test(r.url));
|
|
369
|
+
filterDesc = 'API/XHR';
|
|
370
|
+
}
|
|
371
|
+
if (displayRequests.length === 0) {
|
|
372
|
+
displayRequests = allRequests;
|
|
373
|
+
filterDesc = 'all';
|
|
374
|
+
}
|
|
375
|
+
const sampleUrls = displayRequests
|
|
376
|
+
.slice(-15)
|
|
377
|
+
.map(r => ` ${r.method} ${r.url}`)
|
|
378
|
+
.join('\n');
|
|
379
|
+
const debugInfo = displayRequests.length > 0
|
|
380
|
+
? `\n\nCaptured requests (${filterDesc}, showing ${Math.min(displayRequests.length, 15)} of ${allRequests.length} total):\n${sampleUrls}`
|
|
381
|
+
: `\n\nNo requests captured (0 total). The request may not have been triggered yet.`;
|
|
382
|
+
const msg = `network_find: no request matched (where: ${JSON.stringify(where)}, pick: ${pick})${waitForMs > 0 ? ` within ${waitForMs}ms` : ''}. Ensure the request is triggered before this step (e.g. by navigation or a prior interaction), or increase waitForMs.${debugInfo}`;
|
|
383
|
+
console.warn(`[${step.id}] ${msg}`);
|
|
384
|
+
throw new Error(msg);
|
|
385
|
+
}
|
|
386
|
+
ctx.vars[step.params.saveAs] = requestId;
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Executes a network_replay step
|
|
390
|
+
*/
|
|
391
|
+
async function executeNetworkReplay(ctx, step) {
|
|
392
|
+
if (!ctx.networkCapture) {
|
|
393
|
+
throw new Error('network_replay requires an active browser session with network capture. Run the flow in a context that has network capture enabled.');
|
|
394
|
+
}
|
|
395
|
+
const requestId = step.params.requestId;
|
|
396
|
+
const overrides = step.params.overrides
|
|
397
|
+
? {
|
|
398
|
+
url: step.params.overrides.url,
|
|
399
|
+
setQuery: step.params.overrides.setQuery,
|
|
400
|
+
setHeaders: step.params.overrides.setHeaders,
|
|
401
|
+
body: step.params.overrides.body,
|
|
402
|
+
urlReplace: step.params.overrides.urlReplace,
|
|
403
|
+
bodyReplace: step.params.overrides.bodyReplace,
|
|
404
|
+
}
|
|
405
|
+
: undefined;
|
|
406
|
+
let result;
|
|
407
|
+
try {
|
|
408
|
+
result = await ctx.networkCapture.replay(requestId, overrides);
|
|
409
|
+
}
|
|
410
|
+
catch (err) {
|
|
411
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
412
|
+
if (msg.includes('Request not found')) {
|
|
413
|
+
throw new Error(`${msg} The request may not have been captured yet. Ensure a network_find step runs before network_replay and triggers the request (e.g. by navigating or interacting first). Use waitForMs in network_find to wait for the request to appear (e.g. waitForMs: 10000).`);
|
|
414
|
+
}
|
|
415
|
+
throw err;
|
|
416
|
+
}
|
|
417
|
+
// Check for auth failure in network_replay response
|
|
418
|
+
if (ctx.authMonitor?.isEnabled() && ctx.currentStepId) {
|
|
419
|
+
// Get the original request URL from the captured entry
|
|
420
|
+
const entry = ctx.networkCapture.get(requestId);
|
|
421
|
+
const url = entry?.url || '';
|
|
422
|
+
if (ctx.authMonitor.isAuthFailure(url, result.status)) {
|
|
423
|
+
ctx.authMonitor.recordFailure({
|
|
424
|
+
url,
|
|
425
|
+
status: result.status,
|
|
426
|
+
stepId: ctx.currentStepId,
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
if (step.params.saveAs) {
|
|
431
|
+
ctx.vars[step.params.saveAs] = {
|
|
432
|
+
status: result.status,
|
|
433
|
+
contentType: result.contentType,
|
|
434
|
+
body: result.body,
|
|
435
|
+
bodySize: result.bodySize,
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
// Use 'path' with fallback to deprecated 'jsonPath' for backward compatibility
|
|
439
|
+
const pathExpr = step.params.response.path || step.params.response.jsonPath;
|
|
440
|
+
let outValue;
|
|
441
|
+
if (step.params.response.as === 'json') {
|
|
442
|
+
try {
|
|
443
|
+
outValue = JSON.parse(result.body);
|
|
444
|
+
}
|
|
445
|
+
catch {
|
|
446
|
+
throw new Error(`network_replay: response body is not valid JSON (status ${result.status})`);
|
|
447
|
+
}
|
|
448
|
+
if (pathExpr) {
|
|
449
|
+
const pathResult = getByPath(outValue, pathExpr);
|
|
450
|
+
outValue = pathResult.value;
|
|
451
|
+
// Store hint if path extraction had issues
|
|
452
|
+
if (pathResult.hint) {
|
|
453
|
+
ctx.vars['__jmespath_hint'] = pathResult.hint;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
else {
|
|
458
|
+
if (pathExpr) {
|
|
459
|
+
const pathResult = getByPath(JSON.parse(result.body), pathExpr);
|
|
460
|
+
outValue = pathResult.value;
|
|
461
|
+
// Store hint if path extraction had issues
|
|
462
|
+
if (pathResult.hint) {
|
|
463
|
+
ctx.vars['__jmespath_hint'] = pathResult.hint;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
else {
|
|
467
|
+
outValue = result.body;
|
|
468
|
+
}
|
|
469
|
+
if (typeof outValue === 'object' && outValue !== null) {
|
|
470
|
+
outValue = JSON.stringify(outValue);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
ctx.collectibles[step.params.out] = outValue;
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Executes a network_extract step (from var set by network_replay saveAs or similar)
|
|
477
|
+
*/
|
|
478
|
+
async function executeNetworkExtract(ctx, step) {
|
|
479
|
+
// Check vars first, then collectibles (network_replay uses 'out' for collectibles, 'saveAs' for vars)
|
|
480
|
+
const raw = ctx.vars[step.params.fromVar] ?? ctx.collectibles[step.params.fromVar];
|
|
481
|
+
if (raw === undefined) {
|
|
482
|
+
throw new Error(`network_extract: var "${step.params.fromVar}" is not set (checked vars and collectibles)`);
|
|
483
|
+
}
|
|
484
|
+
// Replay saveAs stores { body, status, contentType, bodySize }; support that or raw string
|
|
485
|
+
const bodyStr = raw && typeof raw === 'object' && 'body' in raw && typeof raw.body === 'string'
|
|
486
|
+
? raw.body
|
|
487
|
+
: typeof raw === 'string'
|
|
488
|
+
? raw
|
|
489
|
+
: JSON.stringify(raw);
|
|
490
|
+
// Use 'path' with fallback to deprecated 'jsonPath' for backward compatibility
|
|
491
|
+
const pathExpr = step.params.path || step.params.jsonPath;
|
|
492
|
+
// Collect hints from JMESPath operations
|
|
493
|
+
const hints = [];
|
|
494
|
+
let value;
|
|
495
|
+
if (step.params.as === 'json') {
|
|
496
|
+
const parsed = JSON.parse(bodyStr);
|
|
497
|
+
if (pathExpr) {
|
|
498
|
+
const pathResult = getByPath(parsed, pathExpr);
|
|
499
|
+
value = pathResult.value;
|
|
500
|
+
if (pathResult.hint) {
|
|
501
|
+
hints.push(pathResult.hint);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
else {
|
|
505
|
+
value = parsed;
|
|
506
|
+
}
|
|
507
|
+
// Note: JMESPath handles projections natively, e.g., "results[*].{id: id, name: name}"
|
|
508
|
+
// The deprecated 'transform' parameter is no longer needed
|
|
509
|
+
}
|
|
510
|
+
else {
|
|
511
|
+
if (pathExpr) {
|
|
512
|
+
const pathResult = getByPath(JSON.parse(bodyStr), pathExpr);
|
|
513
|
+
value = pathResult.value;
|
|
514
|
+
if (pathResult.hint) {
|
|
515
|
+
hints.push(pathResult.hint);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
else {
|
|
519
|
+
value = bodyStr;
|
|
520
|
+
}
|
|
521
|
+
if (typeof value === 'object' && value !== null) {
|
|
522
|
+
value = JSON.stringify(value);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
ctx.collectibles[step.params.out] = value;
|
|
526
|
+
// Store hints in a special variable for propagation to run results
|
|
527
|
+
if (hints.length > 0) {
|
|
528
|
+
const existingHints = ctx.vars['__jmespath_hints'] || [];
|
|
529
|
+
ctx.vars['__jmespath_hints'] = [...existingHints, ...hints];
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
533
|
+
* Executes a select_option step
|
|
534
|
+
*/
|
|
535
|
+
async function executeSelectOption(ctx, step) {
|
|
536
|
+
// Support both legacy selector and new target
|
|
537
|
+
const targetOrAnyOf = step.params.target ?? (step.params.selector ? selectorToTarget(step.params.selector) : null);
|
|
538
|
+
if (!targetOrAnyOf) {
|
|
539
|
+
throw new Error('SelectOption step must have either "target" or "selector"');
|
|
540
|
+
}
|
|
541
|
+
// Resolve target with fallback and scope
|
|
542
|
+
const { locator, matchedTarget, matchedCount } = await resolveTargetWithFallback(ctx.currentFrame ?? ctx.page, targetOrAnyOf, step.params.scope);
|
|
543
|
+
// Log matched target for diagnostics
|
|
544
|
+
if (step.params.hint) {
|
|
545
|
+
console.log(`[SelectOption:${step.id}] Matched target: ${JSON.stringify(matchedTarget)}, count: ${matchedCount}, hint: ${step.params.hint}`);
|
|
546
|
+
}
|
|
547
|
+
const target = step.params.first ?? true ? locator.first() : locator;
|
|
548
|
+
// Convert value to Playwright's selectOption format
|
|
549
|
+
const values = Array.isArray(step.params.value) ? step.params.value : [step.params.value];
|
|
550
|
+
const selectOptions = values.map(v => {
|
|
551
|
+
if (typeof v === 'string') {
|
|
552
|
+
return { value: v };
|
|
553
|
+
}
|
|
554
|
+
else if ('label' in v) {
|
|
555
|
+
return { label: v.label };
|
|
556
|
+
}
|
|
557
|
+
else if ('index' in v) {
|
|
558
|
+
return { index: v.index };
|
|
559
|
+
}
|
|
560
|
+
return v;
|
|
561
|
+
});
|
|
562
|
+
await target.selectOption(selectOptions);
|
|
563
|
+
}
|
|
564
|
+
/**
|
|
565
|
+
* Executes a press_key step
|
|
566
|
+
*/
|
|
567
|
+
async function executePressKey(ctx, step) {
|
|
568
|
+
const times = step.params.times ?? 1;
|
|
569
|
+
const delayMs = step.params.delayMs ?? 0;
|
|
570
|
+
// If target is specified, focus it first
|
|
571
|
+
const targetOrAnyOf = step.params.target ?? (step.params.selector ? selectorToTarget(step.params.selector) : null);
|
|
572
|
+
if (targetOrAnyOf) {
|
|
573
|
+
const { locator, matchedTarget, matchedCount } = await resolveTargetWithFallback(ctx.currentFrame ?? ctx.page, targetOrAnyOf, step.params.scope);
|
|
574
|
+
if (step.params.hint) {
|
|
575
|
+
console.log(`[PressKey:${step.id}] Matched target: ${JSON.stringify(matchedTarget)}, count: ${matchedCount}, hint: ${step.params.hint}`);
|
|
576
|
+
}
|
|
577
|
+
await locator.first().focus();
|
|
578
|
+
}
|
|
579
|
+
// Press the key the specified number of times
|
|
580
|
+
// Note: keyboard is accessed from the page, not the frame
|
|
581
|
+
for (let i = 0; i < times; i++) {
|
|
582
|
+
await ctx.page.keyboard.press(step.params.key);
|
|
583
|
+
if (delayMs > 0 && i < times - 1) {
|
|
584
|
+
await sleepMs(delayMs);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* Executes an upload_file step
|
|
590
|
+
*/
|
|
591
|
+
async function executeUploadFile(ctx, step) {
|
|
592
|
+
const path = await import('path');
|
|
593
|
+
// Support both legacy selector and new target
|
|
594
|
+
const targetOrAnyOf = step.params.target ?? (step.params.selector ? selectorToTarget(step.params.selector) : null);
|
|
595
|
+
if (!targetOrAnyOf) {
|
|
596
|
+
throw new Error('UploadFile step must have either "target" or "selector"');
|
|
597
|
+
}
|
|
598
|
+
// Resolve target with fallback and scope
|
|
599
|
+
const { locator, matchedTarget, matchedCount } = await resolveTargetWithFallback(ctx.currentFrame ?? ctx.page, targetOrAnyOf, step.params.scope);
|
|
600
|
+
if (step.params.hint) {
|
|
601
|
+
console.log(`[UploadFile:${step.id}] Matched target: ${JSON.stringify(matchedTarget)}, count: ${matchedCount}, hint: ${step.params.hint}`);
|
|
602
|
+
}
|
|
603
|
+
const target = step.params.first ?? true ? locator.first() : locator;
|
|
604
|
+
// Resolve file paths (relative to pack directory if not absolute)
|
|
605
|
+
const files = Array.isArray(step.params.files) ? step.params.files : [step.params.files];
|
|
606
|
+
const resolvedFiles = files.map(f => {
|
|
607
|
+
if (path.isAbsolute(f)) {
|
|
608
|
+
return f;
|
|
609
|
+
}
|
|
610
|
+
return ctx.packDir ? path.join(ctx.packDir, f) : f;
|
|
611
|
+
});
|
|
612
|
+
await target.setInputFiles(resolvedFiles);
|
|
613
|
+
}
|
|
614
|
+
/**
|
|
615
|
+
* Executes a frame step
|
|
616
|
+
*/
|
|
617
|
+
async function executeFrame(ctx, step) {
|
|
618
|
+
if (step.params.action === 'exit') {
|
|
619
|
+
// Return to main frame
|
|
620
|
+
ctx.currentFrame = undefined;
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
// Enter frame
|
|
624
|
+
const frameSpec = step.params.frame;
|
|
625
|
+
let frame = null;
|
|
626
|
+
if (typeof frameSpec === 'string') {
|
|
627
|
+
// Try as name first, then as CSS selector
|
|
628
|
+
frame = ctx.page.frame(frameSpec);
|
|
629
|
+
if (!frame) {
|
|
630
|
+
// Try as CSS selector - get the frame from the iframe element
|
|
631
|
+
// We need to use elementHandle to get the actual frame
|
|
632
|
+
const iframeElement = await ctx.page.locator(frameSpec).first().elementHandle();
|
|
633
|
+
if (iframeElement) {
|
|
634
|
+
frame = await iframeElement.contentFrame();
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
else if ('name' in frameSpec) {
|
|
639
|
+
frame = ctx.page.frame({ name: frameSpec.name });
|
|
640
|
+
}
|
|
641
|
+
else if ('url' in frameSpec) {
|
|
642
|
+
frame = ctx.page.frame({ url: frameSpec.url });
|
|
643
|
+
}
|
|
644
|
+
if (!frame) {
|
|
645
|
+
throw new Error(`Frame not found: ${JSON.stringify(frameSpec)}`);
|
|
646
|
+
}
|
|
647
|
+
ctx.currentFrame = frame;
|
|
648
|
+
}
|
|
649
|
+
/**
|
|
650
|
+
* Executes a new_tab step
|
|
651
|
+
*/
|
|
652
|
+
async function executeNewTab(ctx, step) {
|
|
653
|
+
if (!ctx.browserContext) {
|
|
654
|
+
throw new Error('new_tab requires a browser context. Make sure the runner provides browserContext in StepContext.');
|
|
655
|
+
}
|
|
656
|
+
const pages = ctx.browserContext.pages();
|
|
657
|
+
const currentTabIndex = pages.indexOf(ctx.page);
|
|
658
|
+
// Create new page
|
|
659
|
+
const newPage = await ctx.browserContext.newPage();
|
|
660
|
+
// Navigate if URL provided
|
|
661
|
+
if (step.params.url) {
|
|
662
|
+
try {
|
|
663
|
+
await newPage.goto(step.params.url, { waitUntil: 'networkidle' });
|
|
664
|
+
}
|
|
665
|
+
catch (err) {
|
|
666
|
+
if (err?.name === 'TimeoutError' || err?.message?.includes('Timeout')) {
|
|
667
|
+
// Page loaded but networkidle didn't settle — continue anyway.
|
|
668
|
+
}
|
|
669
|
+
else {
|
|
670
|
+
throw err;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
// Save tab index if requested
|
|
675
|
+
if (step.params.saveTabIndexAs) {
|
|
676
|
+
const newPages = ctx.browserContext.pages();
|
|
677
|
+
ctx.vars[step.params.saveTabIndexAs] = newPages.indexOf(newPage);
|
|
678
|
+
}
|
|
679
|
+
// Store previous tab index and switch to new tab
|
|
680
|
+
ctx.previousTabIndex = currentTabIndex;
|
|
681
|
+
// Note: The runner should update ctx.page to newPage after this step
|
|
682
|
+
ctx.vars['__newPage'] = newPage;
|
|
683
|
+
}
|
|
684
|
+
/**
|
|
685
|
+
* Executes a switch_tab step
|
|
686
|
+
*/
|
|
687
|
+
async function executeSwitchTab(ctx, step) {
|
|
688
|
+
if (!ctx.browserContext) {
|
|
689
|
+
throw new Error('switch_tab requires a browser context. Make sure the runner provides browserContext in StepContext.');
|
|
690
|
+
}
|
|
691
|
+
const pages = ctx.browserContext.pages();
|
|
692
|
+
const currentTabIndex = pages.indexOf(ctx.page);
|
|
693
|
+
let targetIndex;
|
|
694
|
+
if (step.params.tab === 'last') {
|
|
695
|
+
targetIndex = pages.length - 1;
|
|
696
|
+
}
|
|
697
|
+
else if (step.params.tab === 'previous') {
|
|
698
|
+
if (ctx.previousTabIndex === undefined) {
|
|
699
|
+
throw new Error('switch_tab: no previous tab to switch to');
|
|
700
|
+
}
|
|
701
|
+
targetIndex = ctx.previousTabIndex;
|
|
702
|
+
}
|
|
703
|
+
else {
|
|
704
|
+
targetIndex = step.params.tab;
|
|
705
|
+
}
|
|
706
|
+
if (targetIndex < 0 || targetIndex >= pages.length) {
|
|
707
|
+
throw new Error(`switch_tab: tab index ${targetIndex} out of range (0-${pages.length - 1})`);
|
|
708
|
+
}
|
|
709
|
+
const targetPage = pages[targetIndex];
|
|
710
|
+
// Close current tab if requested
|
|
711
|
+
if (step.params.closeCurrentTab) {
|
|
712
|
+
await ctx.page.close();
|
|
713
|
+
}
|
|
714
|
+
// Store previous tab index and switch
|
|
715
|
+
ctx.previousTabIndex = currentTabIndex;
|
|
716
|
+
// Note: The runner should update ctx.page to targetPage after this step
|
|
717
|
+
ctx.vars['__newPage'] = targetPage;
|
|
718
|
+
await targetPage.bringToFront();
|
|
719
|
+
}
|
|
720
|
+
/**
|
|
721
|
+
* Executes a single DSL step
|
|
722
|
+
*/
|
|
723
|
+
export async function executeStep(ctx, step) {
|
|
724
|
+
switch (step.type) {
|
|
725
|
+
case 'navigate':
|
|
726
|
+
await executeNavigate(ctx, step);
|
|
727
|
+
break;
|
|
728
|
+
case 'extract_title':
|
|
729
|
+
await executeExtractTitle(ctx, step);
|
|
730
|
+
break;
|
|
731
|
+
case 'extract_text':
|
|
732
|
+
await executeExtractText(ctx, step);
|
|
733
|
+
break;
|
|
734
|
+
case 'extract_attribute':
|
|
735
|
+
await executeExtractAttribute(ctx, step);
|
|
736
|
+
break;
|
|
737
|
+
case 'sleep':
|
|
738
|
+
await executeSleep(ctx, step);
|
|
739
|
+
break;
|
|
740
|
+
case 'wait_for':
|
|
741
|
+
await executeWaitFor(ctx, step);
|
|
742
|
+
break;
|
|
743
|
+
case 'click':
|
|
744
|
+
await executeClick(ctx, step);
|
|
745
|
+
break;
|
|
746
|
+
case 'fill':
|
|
747
|
+
await executeFill(ctx, step);
|
|
748
|
+
break;
|
|
749
|
+
case 'assert':
|
|
750
|
+
await executeAssert(ctx, step);
|
|
751
|
+
break;
|
|
752
|
+
case 'set_var':
|
|
753
|
+
await executeSetVar(ctx, step);
|
|
754
|
+
break;
|
|
755
|
+
case 'network_find':
|
|
756
|
+
await executeNetworkFind(ctx, step);
|
|
757
|
+
break;
|
|
758
|
+
case 'network_replay':
|
|
759
|
+
await executeNetworkReplay(ctx, step);
|
|
760
|
+
break;
|
|
761
|
+
case 'network_extract':
|
|
762
|
+
await executeNetworkExtract(ctx, step);
|
|
763
|
+
break;
|
|
764
|
+
case 'select_option':
|
|
765
|
+
await executeSelectOption(ctx, step);
|
|
766
|
+
break;
|
|
767
|
+
case 'press_key':
|
|
768
|
+
await executePressKey(ctx, step);
|
|
769
|
+
break;
|
|
770
|
+
case 'upload_file':
|
|
771
|
+
await executeUploadFile(ctx, step);
|
|
772
|
+
break;
|
|
773
|
+
case 'frame':
|
|
774
|
+
await executeFrame(ctx, step);
|
|
775
|
+
break;
|
|
776
|
+
case 'new_tab':
|
|
777
|
+
await executeNewTab(ctx, step);
|
|
778
|
+
break;
|
|
779
|
+
case 'switch_tab':
|
|
780
|
+
await executeSwitchTab(ctx, step);
|
|
781
|
+
break;
|
|
782
|
+
default:
|
|
783
|
+
// TypeScript exhaustiveness check
|
|
784
|
+
const _exhaustive = step;
|
|
785
|
+
throw new Error(`Unknown step type: ${_exhaustive.type}`);
|
|
786
|
+
}
|
|
787
|
+
}
|