automation_model 1.0.739-stage → 1.0.740-stage
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/lib/route.d.ts +64 -2
- package/lib/route.js +370 -263
- package/lib/route.js.map +1 -1
- package/lib/stable_browser.d.ts +1 -1
- package/lib/stable_browser.js +27 -14
- package/lib/stable_browser.js.map +1 -1
- package/lib/utils.d.ts +4 -2
- package/lib/utils.js +2 -4
- package/lib/utils.js.map +1 -1
- package/package.json +5 -2
package/lib/route.js
CHANGED
|
@@ -47,50 +47,367 @@ async function loadRoutes(context, template) {
|
|
|
47
47
|
}
|
|
48
48
|
return context.loadedRoutes.get(template) || [];
|
|
49
49
|
}
|
|
50
|
+
export function pathFilter(savedPath, actualPath) {
|
|
51
|
+
if (typeof savedPath !== "string")
|
|
52
|
+
return false;
|
|
53
|
+
if (savedPath.includes("*")) {
|
|
54
|
+
// Escape regex special characters in savedPath
|
|
55
|
+
const escapedPath = savedPath.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
|
|
56
|
+
// Treat it as a wildcard
|
|
57
|
+
const regex = new RegExp(escapedPath.replace(/\*/g, ".*"));
|
|
58
|
+
return regex.test(actualPath);
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
return savedPath === actualPath;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
export function queryParamsFilter(savedQueryParams, actualQueryParams) {
|
|
65
|
+
if (!savedQueryParams)
|
|
66
|
+
return true;
|
|
67
|
+
for (const [key, value] of Object.entries(savedQueryParams)) {
|
|
68
|
+
if (value === "*") {
|
|
69
|
+
// If the saved query param is a wildcard, it matches anything
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (actualQueryParams.get(key) !== value) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
export function methodFilter(savedMethod, actualMethod) {
|
|
79
|
+
if (!savedMethod)
|
|
80
|
+
return true;
|
|
81
|
+
if (savedMethod === "*") {
|
|
82
|
+
const httpMethodRegex = /^(GET|POST|PUT|DELETE|PATCH|OPTIONS|HEAD)$/;
|
|
83
|
+
return httpMethodRegex.test(actualMethod);
|
|
84
|
+
}
|
|
85
|
+
return savedMethod === actualMethod;
|
|
86
|
+
}
|
|
50
87
|
function matchRoute(routeItem, req) {
|
|
88
|
+
const debug = createDebug("automation_model:route:matchRoute");
|
|
51
89
|
const url = new URL(req.request().url());
|
|
52
|
-
const methodMatch = !routeItem.filters.method || routeItem.filters.method === req.request().method();
|
|
53
|
-
const pathMatch = routeItem.filters.path === url.pathname;
|
|
54
90
|
const queryParams = routeItem.filters.queryParams;
|
|
55
|
-
const
|
|
56
|
-
|
|
91
|
+
const methodMatch = methodFilter(routeItem.filters.method, req.request().method());
|
|
92
|
+
const pathMatch = pathFilter(routeItem.filters.path, url.pathname);
|
|
93
|
+
debug("Path match", pathMatch, routeItem.filters.path, url.pathname);
|
|
94
|
+
const queryParamsMatch = queryParamsFilter(queryParams, url.searchParams);
|
|
95
|
+
return methodMatch && pathMatch && queryParamsMatch;
|
|
96
|
+
}
|
|
97
|
+
function handleAbortRequest(action, context) {
|
|
98
|
+
if (context.tracking.timer)
|
|
99
|
+
clearTimeout(context.tracking.timer);
|
|
100
|
+
const errorCode = action.config?.errorCode ?? "failed";
|
|
101
|
+
console.log(`[abort_request] Aborting with error code: ${errorCode}`);
|
|
102
|
+
context.route.abort(errorCode);
|
|
103
|
+
context.abortActionPerformed = true;
|
|
104
|
+
context.tracking.completed = true;
|
|
105
|
+
return {
|
|
106
|
+
type: action.type,
|
|
107
|
+
description: JSON.stringify(action.config),
|
|
108
|
+
status: "success",
|
|
109
|
+
message: `Request aborted with code: ${errorCode}`,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
function handleStatusCodeVerification(action, context) {
|
|
113
|
+
const isSuccess = String(context.status) === String(action.config);
|
|
114
|
+
return {
|
|
115
|
+
type: action.type,
|
|
116
|
+
description: JSON.stringify(action.config),
|
|
117
|
+
status: isSuccess ? "success" : "fail",
|
|
118
|
+
message: `Status code verification ${isSuccess ? "passed" : "failed"}. Expected ${action.config}, got ${context.status}`,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
function handleJsonModify(action, context) {
|
|
122
|
+
if (!context.json) {
|
|
123
|
+
return {
|
|
124
|
+
type: action.type,
|
|
125
|
+
description: JSON.stringify(action.config),
|
|
126
|
+
status: "fail",
|
|
127
|
+
message: "JSON modification failed. Response is not JSON",
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
objectPath.set(context.json, action.config.path, action.config.modifyValue);
|
|
131
|
+
context.finalBody = JSON.parse(JSON.stringify(context.json));
|
|
132
|
+
return {
|
|
133
|
+
type: action.type,
|
|
134
|
+
description: JSON.stringify(action.config),
|
|
135
|
+
status: "success",
|
|
136
|
+
message: `JSON modified at path '${action.config.path}'`,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
function handleJsonWholeModify(action, context) {
|
|
140
|
+
if (!context.json) {
|
|
141
|
+
return {
|
|
142
|
+
type: action.type,
|
|
143
|
+
description: JSON.stringify(action.config),
|
|
144
|
+
status: "fail",
|
|
145
|
+
message: "JSON modification failed. Response is not JSON",
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
try {
|
|
149
|
+
const parsedConfig = typeof action.config === "string" ? JSON.parse(action.config) : action.config;
|
|
150
|
+
context.json = parsedConfig;
|
|
151
|
+
context.finalBody = JSON.parse(JSON.stringify(context.json));
|
|
152
|
+
return {
|
|
153
|
+
type: action.type,
|
|
154
|
+
description: JSON.stringify(action.config),
|
|
155
|
+
status: "success",
|
|
156
|
+
message: "Whole JSON body was replaced.",
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
catch (e) {
|
|
160
|
+
const message = `JSON modification failed. Invalid JSON in config: ${e instanceof Error ? e.message : String(e)}`;
|
|
161
|
+
return { type: action.type, description: JSON.stringify(action.config), status: "fail", message };
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
function handleStatusCodeChange(action, context) {
|
|
165
|
+
context.status = Number(action.config);
|
|
166
|
+
return {
|
|
167
|
+
type: action.type,
|
|
168
|
+
description: JSON.stringify(action.config),
|
|
169
|
+
status: "success",
|
|
170
|
+
message: `Status code changed to ${context.status}`,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
function handleChangeText(action, context) {
|
|
174
|
+
if (context.isBinary) {
|
|
175
|
+
return {
|
|
176
|
+
type: action.type,
|
|
177
|
+
description: JSON.stringify(action.config),
|
|
178
|
+
status: "fail",
|
|
179
|
+
message: "Change text action failed. Body is not text.",
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
context.body = action.config;
|
|
183
|
+
context.finalBody = context.body;
|
|
184
|
+
return {
|
|
185
|
+
type: action.type,
|
|
186
|
+
description: JSON.stringify(action.config),
|
|
187
|
+
status: "success",
|
|
188
|
+
message: "Response body text was replaced.",
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
function handleAssertJson(action, context) {
|
|
192
|
+
if (!context.json) {
|
|
193
|
+
return {
|
|
194
|
+
type: action.type,
|
|
195
|
+
description: JSON.stringify(action.config),
|
|
196
|
+
status: "fail",
|
|
197
|
+
message: "JSON assertion failed. Response is not JSON.",
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
const actual = objectPath.get(context.json, action.config.path);
|
|
201
|
+
const expected = action.config.expectedValue;
|
|
202
|
+
const isSuccess = JSON.stringify(actual) === JSON.stringify(expected);
|
|
203
|
+
return {
|
|
204
|
+
type: action.type,
|
|
205
|
+
description: JSON.stringify(action.config),
|
|
206
|
+
status: isSuccess ? "success" : "fail",
|
|
207
|
+
message: isSuccess
|
|
208
|
+
? `JSON assertion passed for path '${action.config.path}'.`
|
|
209
|
+
: `JSON assertion failed for path '${action.config.path}': expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
function handleAssertWholeJson(action, context) {
|
|
213
|
+
if (!context.json) {
|
|
214
|
+
return {
|
|
215
|
+
type: action.type,
|
|
216
|
+
description: JSON.stringify(action.config),
|
|
217
|
+
status: "fail",
|
|
218
|
+
message: "Whole JSON assertion failed. Response is not JSON.",
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
const originalJSON = JSON.stringify(context.json, null, 2);
|
|
222
|
+
let isSuccess = false;
|
|
223
|
+
let message = "";
|
|
224
|
+
if ("contains" in action.config) {
|
|
225
|
+
isSuccess = originalJSON.includes(action.config.contains);
|
|
226
|
+
message = isSuccess
|
|
227
|
+
? "Whole JSON assertion passed."
|
|
228
|
+
: `Whole JSON assertion failed. Expected to contain: "${action.config.contains}".`;
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
isSuccess = originalJSON === action.config.equals;
|
|
232
|
+
message = isSuccess
|
|
233
|
+
? "Whole JSON assertion passed."
|
|
234
|
+
: `Whole JSON assertion failed. Expected exact match: "${action.config.equals}".`;
|
|
235
|
+
}
|
|
236
|
+
return {
|
|
237
|
+
type: action.type,
|
|
238
|
+
description: JSON.stringify(action.config),
|
|
239
|
+
status: isSuccess ? "success" : "fail",
|
|
240
|
+
message,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
function handleAssertText(action, context) {
|
|
244
|
+
if (typeof context.body !== "string") {
|
|
245
|
+
return {
|
|
246
|
+
type: action.type,
|
|
247
|
+
description: JSON.stringify(action.config),
|
|
248
|
+
status: "fail",
|
|
249
|
+
message: "Text assertion failed. Body is not text.",
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
let isSuccess = false;
|
|
253
|
+
let message = "";
|
|
254
|
+
if ("contains" in action.config) {
|
|
255
|
+
isSuccess = context.body.includes(action.config.contains);
|
|
256
|
+
message = isSuccess
|
|
257
|
+
? "Text assertion passed."
|
|
258
|
+
: `Text assertion failed. Expected to contain: "${action.config.contains}".`;
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
isSuccess = context.body === action.config.equals;
|
|
262
|
+
message = isSuccess
|
|
263
|
+
? "Text assertion passed."
|
|
264
|
+
: `Text assertion failed. Expected exact match: "${action.config.equals}".`;
|
|
265
|
+
}
|
|
266
|
+
return {
|
|
267
|
+
type: action.type,
|
|
268
|
+
description: JSON.stringify(action.config),
|
|
269
|
+
status: isSuccess ? "success" : "fail",
|
|
270
|
+
message,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
function handleStubAction(stubAction, route, tracking) {
|
|
274
|
+
let actionStatus = "success";
|
|
275
|
+
const description = JSON.stringify(stubAction.config);
|
|
276
|
+
const request = route.request();
|
|
277
|
+
let stubActionPerformed = false;
|
|
278
|
+
debug(`Stub action found for ${request.url()}. Skipping fetch.`);
|
|
279
|
+
if (tracking.timer)
|
|
280
|
+
clearTimeout(tracking.timer);
|
|
281
|
+
const fullFillConfig = {};
|
|
282
|
+
if (!tracking.actionResults)
|
|
283
|
+
tracking.actionResults = [];
|
|
284
|
+
if (stubAction.config.path) {
|
|
285
|
+
const filePath = path.join(process.cwd(), "data", "fixtures", stubAction.config.path);
|
|
286
|
+
debug(`Stub action file path: ${filePath}`);
|
|
287
|
+
if (existsSync(filePath)) {
|
|
288
|
+
fullFillConfig.path = filePath;
|
|
289
|
+
debug(`Stub action fulfilled with file: ${filePath}`);
|
|
290
|
+
}
|
|
291
|
+
else {
|
|
292
|
+
actionStatus = "fail";
|
|
293
|
+
tracking.actionResults.push({
|
|
294
|
+
type: "stub_request",
|
|
295
|
+
description,
|
|
296
|
+
status: actionStatus,
|
|
297
|
+
message: `Stub action failed for ${tracking.url}: File not found at ${filePath}`,
|
|
298
|
+
});
|
|
299
|
+
stubActionPerformed = true;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
if (!fullFillConfig.path) {
|
|
303
|
+
if (stubAction.config.statusCode) {
|
|
304
|
+
fullFillConfig.status = Number(stubAction.config.statusCode);
|
|
305
|
+
}
|
|
306
|
+
if (stubAction.config.contentType) {
|
|
307
|
+
if (stubAction.config.contentType === "application/json") {
|
|
308
|
+
fullFillConfig.contentType = "application/json";
|
|
309
|
+
if (stubAction.config.body) {
|
|
310
|
+
try {
|
|
311
|
+
fullFillConfig.json = JSON.parse(stubAction.config.body);
|
|
312
|
+
}
|
|
313
|
+
catch (e) {
|
|
314
|
+
debug(`Invalid JSON in stub action body: ${stubAction.config.body}, `, e instanceof Error ? e.message : String(e));
|
|
315
|
+
debug("Invalid JSON, defaulting to empty object");
|
|
316
|
+
fullFillConfig.json = {};
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
else {
|
|
321
|
+
fullFillConfig.contentType = stubAction.config.contentType;
|
|
322
|
+
fullFillConfig.body = stubAction.config.body || "";
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
if (!fullFillConfig.json && !fullFillConfig.body) {
|
|
326
|
+
if (stubAction.config.body) {
|
|
327
|
+
fullFillConfig.body = stubAction.config.body;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
if (actionStatus === "success") {
|
|
332
|
+
try {
|
|
333
|
+
route.fulfill(fullFillConfig);
|
|
334
|
+
stubActionPerformed = true;
|
|
335
|
+
tracking.completed = true;
|
|
336
|
+
tracking.actionResults.push({
|
|
337
|
+
type: "stub_request",
|
|
338
|
+
description,
|
|
339
|
+
status: actionStatus,
|
|
340
|
+
message: `Stub action executed for ${request.url()}`,
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
catch (e) {
|
|
344
|
+
actionStatus = "fail";
|
|
345
|
+
debug(`Failed to fulfill stub request for ${request.url()}`, e);
|
|
346
|
+
tracking.actionResults.push({
|
|
347
|
+
type: "stub_request",
|
|
348
|
+
description,
|
|
349
|
+
status: actionStatus,
|
|
350
|
+
message: `Stub action failed for ${request.url()}: ${e instanceof Error ? e.message : String(e)}`,
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return stubActionPerformed;
|
|
57
355
|
}
|
|
58
356
|
export async function registerBeforeStepRoutes(context, stepName, world) {
|
|
357
|
+
const debug = createDebug("automation_model:route:registerBeforeStepRoutes");
|
|
59
358
|
const page = context.web.page;
|
|
60
359
|
if (!page)
|
|
61
360
|
throw new Error("context.web.page is missing");
|
|
62
361
|
const stepTemplate = _stepNameToTemplate(stepName);
|
|
362
|
+
debug("stepTemplate", stepTemplate);
|
|
63
363
|
const routes = await loadRoutes(context, stepTemplate);
|
|
364
|
+
debug("Routes", routes);
|
|
64
365
|
const allRouteItems = routes.flatMap((r) => r.routes);
|
|
366
|
+
debug("All route items", allRouteItems);
|
|
65
367
|
if (!context.__routeState) {
|
|
66
368
|
context.__routeState = { matched: [] };
|
|
67
369
|
}
|
|
68
370
|
for (let i = 0; i < allRouteItems.length; i++) {
|
|
69
|
-
|
|
371
|
+
let item = allRouteItems[i];
|
|
372
|
+
debug(`Setting up mandatory route with timeout ${item.timeout}ms: ${JSON.stringify(item.filters)}`);
|
|
373
|
+
let content = JSON.stringify(item);
|
|
374
|
+
try {
|
|
375
|
+
content = await replaceWithLocalTestData(content, context.web.world, true, false, content, context.web, false);
|
|
376
|
+
allRouteItems[i] = JSON.parse(content); // Modify the original array
|
|
377
|
+
item = allRouteItems[i];
|
|
378
|
+
debug(`After replacing test data: ${JSON.stringify(allRouteItems[i])}`);
|
|
379
|
+
}
|
|
380
|
+
catch (error) {
|
|
381
|
+
debug("Error replacing test data:", error);
|
|
382
|
+
}
|
|
70
383
|
if (item.mandatory) {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
384
|
+
const path = item.filters.path;
|
|
385
|
+
const queryParams = Object.entries(item.filters.queryParams || {})
|
|
386
|
+
.map(([key, value]) => `${key}=${value}`)
|
|
387
|
+
.join("&");
|
|
388
|
+
const tracking = {
|
|
389
|
+
routeItem: item,
|
|
390
|
+
url: `${path}${queryParams ? `?${queryParams}` : ""}`,
|
|
391
|
+
completed: false,
|
|
392
|
+
startedAt: Date.now(),
|
|
393
|
+
actionResults: [],
|
|
394
|
+
};
|
|
395
|
+
context.__routeState.matched.push(tracking);
|
|
81
396
|
}
|
|
82
397
|
}
|
|
83
398
|
debug("New allrouteItems", JSON.stringify(allRouteItems));
|
|
84
399
|
let message = null;
|
|
85
400
|
page.route("**/*", async (route) => {
|
|
401
|
+
const debug = createDebug("automation_model:route:intercept");
|
|
86
402
|
const request = route.request();
|
|
87
403
|
debug(`Intercepting request: ${request.method()} ${request.url()}`);
|
|
404
|
+
debug("All route items", allRouteItems);
|
|
88
405
|
const matchedItem = allRouteItems.find((item) => matchRoute(item, route));
|
|
89
406
|
if (!matchedItem)
|
|
90
407
|
return route.continue();
|
|
91
408
|
debug(`Matched route item: ${JSON.stringify(matchedItem)}`);
|
|
92
|
-
debug("Initial context route state", context.__routeState);
|
|
93
|
-
let tracking = context.__routeState.matched.find((t) => t.routeItem === matchedItem && !t.completed);
|
|
409
|
+
debug("Initial context route state", JSON.stringify(context.__routeState, null, 2));
|
|
410
|
+
let tracking = context.__routeState.matched.find((t) => JSON.stringify(t.routeItem) === JSON.stringify(matchedItem) && !t.completed);
|
|
94
411
|
debug("Tracking", tracking);
|
|
95
412
|
let stubActionPerformed = false;
|
|
96
413
|
if (!tracking) {
|
|
@@ -112,88 +429,12 @@ export async function registerBeforeStepRoutes(context, stepName, world) {
|
|
|
112
429
|
}
|
|
113
430
|
const stubAction = matchedItem.actions.find((a) => a.type === "stub_request");
|
|
114
431
|
if (stubAction) {
|
|
115
|
-
|
|
116
|
-
const description = JSON.stringify(stubAction.config);
|
|
117
|
-
debug(`Stub action found for ${request.url()}. Skipping fetch.`);
|
|
118
|
-
if (tracking.timer)
|
|
119
|
-
clearTimeout(tracking.timer);
|
|
120
|
-
const fullFillConfig = {};
|
|
121
|
-
if (stubAction.config.path) {
|
|
122
|
-
const filePath = path.join(process.cwd(), "data", "fixtures", stubAction.config.path);
|
|
123
|
-
debug(`Stub action file path: ${filePath}`);
|
|
124
|
-
if (existsSync(filePath)) {
|
|
125
|
-
fullFillConfig.path = filePath;
|
|
126
|
-
debug(`Stub action fulfilled with file: ${filePath}`);
|
|
127
|
-
}
|
|
128
|
-
else {
|
|
129
|
-
actionStatus = "fail";
|
|
130
|
-
tracking.actionResults.push({
|
|
131
|
-
type: "stub_request",
|
|
132
|
-
description,
|
|
133
|
-
status: actionStatus,
|
|
134
|
-
message: `Stub action failed for ${tracking.url}: File not found at ${filePath}`,
|
|
135
|
-
});
|
|
136
|
-
stubActionPerformed = true;
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
if (!fullFillConfig.path) {
|
|
140
|
-
if (stubAction.config.statusCode) {
|
|
141
|
-
fullFillConfig.status = Number(stubAction.config.statusCode);
|
|
142
|
-
}
|
|
143
|
-
if (stubAction.config.contentType) {
|
|
144
|
-
if (stubAction.config.contentType === "application/json") {
|
|
145
|
-
fullFillConfig.contentType = "application/json";
|
|
146
|
-
if (stubAction.config.body) {
|
|
147
|
-
try {
|
|
148
|
-
fullFillConfig.json = JSON.parse(stubAction.config.body);
|
|
149
|
-
}
|
|
150
|
-
catch (e) {
|
|
151
|
-
debug(`Invalid JSON in stub action body: ${stubAction.config.body}, `, e instanceof Error ? e.message : String(e));
|
|
152
|
-
debug("Invalid JSON, defaulting to empty object");
|
|
153
|
-
fullFillConfig.json = {};
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
else {
|
|
158
|
-
fullFillConfig.contentType = stubAction.config.contentType;
|
|
159
|
-
fullFillConfig.body = stubAction.config.body || "";
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
if (!fullFillConfig.json && !fullFillConfig.body) {
|
|
163
|
-
if (stubAction.config.body) {
|
|
164
|
-
fullFillConfig.body = stubAction.config.body;
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
if (actionStatus === "success") {
|
|
169
|
-
try {
|
|
170
|
-
route.fulfill(fullFillConfig);
|
|
171
|
-
stubActionPerformed = true;
|
|
172
|
-
tracking.completed = true;
|
|
173
|
-
tracking.actionResults.push({
|
|
174
|
-
type: "stub_request",
|
|
175
|
-
description,
|
|
176
|
-
status: actionStatus,
|
|
177
|
-
message: `Stub action executed for ${request.url()}`,
|
|
178
|
-
});
|
|
179
|
-
}
|
|
180
|
-
catch (e) {
|
|
181
|
-
actionStatus = "fail";
|
|
182
|
-
debug(`Failed to fulfill stub request for ${request.url()}`, e);
|
|
183
|
-
tracking.actionResults.push({
|
|
184
|
-
type: "stub_request",
|
|
185
|
-
description,
|
|
186
|
-
status: actionStatus,
|
|
187
|
-
message: `Stub action failed for ${request.url()}: ${e instanceof Error ? e.message : String(e)}`,
|
|
188
|
-
});
|
|
189
|
-
}
|
|
190
|
-
}
|
|
432
|
+
stubActionPerformed = handleStubAction(stubAction, route, tracking);
|
|
191
433
|
}
|
|
192
434
|
if (!stubActionPerformed) {
|
|
193
435
|
let response;
|
|
194
436
|
try {
|
|
195
437
|
response = await route.fetch();
|
|
196
|
-
// debug("Matched item response", response);
|
|
197
438
|
}
|
|
198
439
|
catch (e) {
|
|
199
440
|
console.error("Fetch failed for", request.url(), e);
|
|
@@ -201,213 +442,79 @@ export async function registerBeforeStepRoutes(context, stepName, world) {
|
|
|
201
442
|
clearTimeout(tracking.timer);
|
|
202
443
|
return route.abort();
|
|
203
444
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
const
|
|
207
|
-
!headers["content-type"]?.includes("text") &&
|
|
208
|
-
!headers["content-type"]?.includes("application/csv");
|
|
209
|
-
// debug("Matched item isBinary", isBinary);
|
|
210
|
-
const isJSON = headers["content-type"]?.includes("application/json") || headers["content-type"]?.includes("json")
|
|
211
|
-
? true
|
|
212
|
-
: false;
|
|
213
|
-
// debug("Matched item isJSON", isJSON);
|
|
214
|
-
let body;
|
|
215
|
-
if (isBinary) {
|
|
216
|
-
body = await response.body(); // returns a Buffer
|
|
217
|
-
}
|
|
218
|
-
else {
|
|
219
|
-
body = await response.text();
|
|
220
|
-
}
|
|
445
|
+
const headers = response.headers();
|
|
446
|
+
const isBinary = !headers["content-type"]?.includes("application/json") && !headers["content-type"]?.includes("text");
|
|
447
|
+
const body = isBinary ? await response.body() : await response.text();
|
|
221
448
|
let json;
|
|
222
449
|
try {
|
|
223
|
-
|
|
224
|
-
if (typeof body === "string") {
|
|
450
|
+
if (typeof body === "string")
|
|
225
451
|
json = JSON.parse(body);
|
|
226
|
-
}
|
|
227
452
|
}
|
|
228
453
|
catch (_) { }
|
|
454
|
+
const actionHandlerContext = {
|
|
455
|
+
route,
|
|
456
|
+
tracking,
|
|
457
|
+
status: response.status(),
|
|
458
|
+
body,
|
|
459
|
+
json,
|
|
460
|
+
isBinary,
|
|
461
|
+
finalBody: json ?? body,
|
|
462
|
+
abortActionPerformed: false,
|
|
463
|
+
};
|
|
229
464
|
const actionResults = [];
|
|
230
|
-
let abortActionPerformed = false;
|
|
231
|
-
let finalBody = isJSON && json ? json : body;
|
|
232
|
-
// debug("Matched item actions", matchedItem.actions);
|
|
233
465
|
for (const action of matchedItem.actions) {
|
|
234
|
-
let
|
|
235
|
-
const description = JSON.stringify(action.config);
|
|
466
|
+
let result;
|
|
236
467
|
switch (action.type) {
|
|
237
468
|
case "abort_request":
|
|
238
|
-
|
|
239
|
-
clearTimeout(tracking.timer);
|
|
240
|
-
const errorCode = action.config?.errorCode ?? "failed";
|
|
241
|
-
console.log(`[abort_request] Aborting with error code: ${errorCode}`);
|
|
242
|
-
await route.abort(errorCode);
|
|
243
|
-
abortActionPerformed = true;
|
|
244
|
-
tracking.completed = true;
|
|
469
|
+
result = handleAbortRequest(action, actionHandlerContext);
|
|
245
470
|
break;
|
|
246
471
|
case "status_code_verification":
|
|
247
|
-
|
|
248
|
-
actionStatus = "fail";
|
|
249
|
-
message = `Status code verification failed. Expected ${action.config}, got ${status}`;
|
|
250
|
-
debug(`[status_code_verification] Failed: ${message}`);
|
|
251
|
-
}
|
|
252
|
-
else {
|
|
253
|
-
console.log(`[status_code_verification] Passed`);
|
|
254
|
-
message = `Status code verification passed. Expected ${action.config}, got ${status}`;
|
|
255
|
-
}
|
|
472
|
+
result = handleStatusCodeVerification(action, actionHandlerContext);
|
|
256
473
|
break;
|
|
257
474
|
case "json_modify":
|
|
258
|
-
|
|
259
|
-
actionStatus = "fail";
|
|
260
|
-
message = "JSON modification failed. Response is not JSON";
|
|
261
|
-
debug(`[json_modify] Failed: ${message}`);
|
|
262
|
-
}
|
|
263
|
-
else {
|
|
264
|
-
if (action.config && action.config.path && action.config.modifyValue) {
|
|
265
|
-
objectPath.set(json, action.config.path, action.config.modifyValue);
|
|
266
|
-
console.log(`[json_modify] Modified path ${action.config.path} to ${action.config.modifyValue}`);
|
|
267
|
-
console.log(`[json_modify] Modified JSON`);
|
|
268
|
-
message = `JSON modified successfully`;
|
|
269
|
-
finalBody = JSON.parse(JSON.stringify(json));
|
|
270
|
-
}
|
|
271
|
-
}
|
|
475
|
+
result = handleJsonModify(action, actionHandlerContext);
|
|
272
476
|
break;
|
|
273
477
|
case "json_whole_modify":
|
|
274
|
-
|
|
275
|
-
actionStatus = "fail";
|
|
276
|
-
message = "JSON modification failed. Response is not JSON";
|
|
277
|
-
debug(`[json_whole_modify] Failed: ${message}`);
|
|
278
|
-
}
|
|
279
|
-
else {
|
|
280
|
-
try {
|
|
281
|
-
const parsedConfig = JSON.parse(action.config);
|
|
282
|
-
json = parsedConfig;
|
|
283
|
-
finalBody = JSON.parse(JSON.stringify(json));
|
|
284
|
-
}
|
|
285
|
-
catch (e) {
|
|
286
|
-
actionStatus = "fail";
|
|
287
|
-
message = `JSON modification failed. Invalid JSON: ${e instanceof Error ? e.message : String(e)}`;
|
|
288
|
-
debug(`[json_whole_modify] Failed: ${message}`);
|
|
289
|
-
break;
|
|
290
|
-
}
|
|
291
|
-
console.log(`[json_whole_modify] Whole JSON replaced`);
|
|
292
|
-
message = `JSON replaced successfully`;
|
|
293
|
-
}
|
|
478
|
+
result = handleJsonWholeModify(action, actionHandlerContext);
|
|
294
479
|
break;
|
|
295
480
|
case "status_code_change":
|
|
296
|
-
|
|
297
|
-
console.log(`[status_code_change] Status changed to ${status}`);
|
|
298
|
-
message = `Status code changed to ${status}`;
|
|
481
|
+
result = handleStatusCodeChange(action, actionHandlerContext);
|
|
299
482
|
break;
|
|
300
483
|
case "change_text":
|
|
301
|
-
|
|
302
|
-
actionStatus = "fail";
|
|
303
|
-
message = "Change text action failed. Body is not a text";
|
|
304
|
-
debug(`[change_text] Failed: ${message}`);
|
|
305
|
-
}
|
|
306
|
-
else {
|
|
307
|
-
body = action.config;
|
|
308
|
-
console.log(`[change_text] HTML body replaced`);
|
|
309
|
-
message = `HTML body replaced successfully`;
|
|
310
|
-
finalBody = body;
|
|
311
|
-
}
|
|
484
|
+
result = handleChangeText(action, actionHandlerContext);
|
|
312
485
|
break;
|
|
313
486
|
case "assert_json":
|
|
314
|
-
|
|
315
|
-
actionStatus = "fail";
|
|
316
|
-
message = "JSON assertion failed. Response is not JSON";
|
|
317
|
-
debug(`[assert_json] Failed: ${message}`);
|
|
318
|
-
}
|
|
319
|
-
else {
|
|
320
|
-
const actual = objectPath.get(json, action.config.path);
|
|
321
|
-
if (typeof actual !== "object") {
|
|
322
|
-
if (JSON.stringify(actual) !== JSON.stringify(action.config.expectedValue)) {
|
|
323
|
-
actionStatus = "fail";
|
|
324
|
-
message = `JSON assertion failed for path ${action.config.path}: expected ${JSON.stringify(action.config.expectedValue)}, got ${JSON.stringify(actual)}`;
|
|
325
|
-
debug(`[assert_json] Failed: ${message}`);
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
else if (JSON.stringify(actual) !== action.config.expectedValue) {
|
|
329
|
-
actionStatus = "fail";
|
|
330
|
-
message = `JSON assertion failed for path ${action.config.path}: expected ${action.config.expectedValue}, got ${JSON.stringify(actual)}`;
|
|
331
|
-
debug(`[assert_json] Failed: ${message}`);
|
|
332
|
-
}
|
|
333
|
-
else {
|
|
334
|
-
console.log(`[assert_json] Assertion passed for path ${action.config.path}`);
|
|
335
|
-
message = `JSON assertion passed for path ${action.config.path}`;
|
|
336
|
-
}
|
|
337
|
-
}
|
|
487
|
+
result = handleAssertJson(action, actionHandlerContext);
|
|
338
488
|
break;
|
|
339
489
|
case "assert_whole_json":
|
|
340
|
-
|
|
341
|
-
actionStatus = "fail";
|
|
342
|
-
message = "Whole JSON assertion failed. Response is not JSON";
|
|
343
|
-
debug(`[assert_whole_json] Failed: ${message}`);
|
|
344
|
-
}
|
|
345
|
-
else {
|
|
346
|
-
if (action.config.contains) {
|
|
347
|
-
const originalJSON = JSON.stringify(json, null, 2);
|
|
348
|
-
if (!originalJSON.includes(action.config.contains)) {
|
|
349
|
-
actionStatus = "fail";
|
|
350
|
-
message = `Whole JSON assertion failed. Expected to contain: "${action.config.contains}", actual: "${body}"`;
|
|
351
|
-
debug(`[assert_whole_json] Failed: ${message}`);
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
else if (action.config.equals) {
|
|
355
|
-
const originalJSON = JSON.stringify(json, null, 2);
|
|
356
|
-
if (originalJSON !== action.config.equals) {
|
|
357
|
-
actionStatus = "fail";
|
|
358
|
-
message = `Whole JSON assertion failed. Expected exact match: "${action.config.equals}", actual: "${body}"`;
|
|
359
|
-
debug(`[assert_whole_json] Failed: ${message}`);
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
else {
|
|
363
|
-
console.log(`[assert_whole_json] Assertion passed`);
|
|
364
|
-
message = `Whole JSON assertion passed.`;
|
|
365
|
-
}
|
|
366
|
-
}
|
|
490
|
+
result = handleAssertWholeJson(action, actionHandlerContext);
|
|
367
491
|
break;
|
|
368
492
|
case "assert_text":
|
|
369
|
-
|
|
370
|
-
console.error(`[assert_text] Body is not text`);
|
|
371
|
-
actionStatus = "fail";
|
|
372
|
-
message = "Text assertion failed. Body is not text";
|
|
373
|
-
debug(`[assert_text] Failed: ${message}`);
|
|
374
|
-
}
|
|
375
|
-
else {
|
|
376
|
-
if (action.config.contains && !body.includes(action.config.contains)) {
|
|
377
|
-
actionStatus = "fail";
|
|
378
|
-
message = `Text assertion failed. Expected to contain: "${action.config.contains}", actual: "${body}"`;
|
|
379
|
-
debug(`[assert_text] Failed: ${message}`);
|
|
380
|
-
}
|
|
381
|
-
else if (action.config.equals && body !== action.config.equals) {
|
|
382
|
-
actionStatus = "fail";
|
|
383
|
-
message = `Text assertion failed. Expected exact match: "${action.config.equals}", actual: "${body}"`;
|
|
384
|
-
debug(`[assert_text] Failed: ${message}`);
|
|
385
|
-
}
|
|
386
|
-
else {
|
|
387
|
-
console.log(`[assert_text] Assertion passed`);
|
|
388
|
-
message = `Text assertion passed.`;
|
|
389
|
-
}
|
|
390
|
-
}
|
|
493
|
+
result = handleAssertText(action, actionHandlerContext);
|
|
391
494
|
break;
|
|
392
495
|
default:
|
|
393
|
-
console.warn(`Unknown action type
|
|
496
|
+
console.warn(`Unknown action type`);
|
|
394
497
|
}
|
|
395
|
-
|
|
396
|
-
|
|
498
|
+
if (result)
|
|
499
|
+
actionResults.push(result);
|
|
397
500
|
}
|
|
398
501
|
tracking.completed = true;
|
|
399
502
|
tracking.actionResults = actionResults;
|
|
400
503
|
if (tracking.timer)
|
|
401
504
|
clearTimeout(tracking.timer);
|
|
402
|
-
if (!abortActionPerformed) {
|
|
505
|
+
if (!actionHandlerContext.abortActionPerformed) {
|
|
403
506
|
try {
|
|
507
|
+
const isJSON = headers["content-type"]?.includes("application/json");
|
|
404
508
|
if (isJSON) {
|
|
405
|
-
await route.fulfill({ status, json: finalBody, headers });
|
|
509
|
+
await route.fulfill({ status: actionHandlerContext.status, json: actionHandlerContext.finalBody, headers });
|
|
406
510
|
}
|
|
407
511
|
else {
|
|
408
|
-
await route.fulfill({
|
|
512
|
+
await route.fulfill({
|
|
513
|
+
status: actionHandlerContext.status,
|
|
514
|
+
body: actionHandlerContext.finalBody,
|
|
515
|
+
headers,
|
|
516
|
+
});
|
|
409
517
|
}
|
|
410
|
-
// await route.fulfill({ status, body: finalBody, headers });
|
|
411
518
|
}
|
|
412
519
|
catch (e) {
|
|
413
520
|
console.error("Failed to fulfill route:", e);
|