automation_model 1.0.802-dev → 1.0.802-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/api.js +40 -12
- package/lib/api.js.map +1 -1
- package/lib/auto_page.d.ts +1 -1
- package/lib/auto_page.js +103 -69
- package/lib/auto_page.js.map +1 -1
- package/lib/browser_manager.d.ts +2 -7
- package/lib/browser_manager.js +105 -102
- package/lib/browser_manager.js.map +1 -1
- package/lib/bruno.js.map +1 -1
- package/lib/check_performance.d.ts +1 -0
- package/lib/check_performance.js +57 -0
- package/lib/check_performance.js.map +1 -0
- package/lib/command_common.d.ts +2 -2
- package/lib/command_common.js +42 -24
- package/lib/command_common.js.map +1 -1
- package/lib/constants.d.ts +4 -0
- package/lib/constants.js +2 -0
- package/lib/constants.js.map +1 -0
- package/lib/file_checker.js +7 -0
- package/lib/file_checker.js.map +1 -1
- package/lib/index.js +1 -0
- package/lib/index.js.map +1 -1
- package/lib/init_browser.d.ts +1 -2
- package/lib/init_browser.js +137 -128
- package/lib/init_browser.js.map +1 -1
- package/lib/locator_log.js.map +1 -1
- package/lib/network.d.ts +2 -2
- package/lib/network.js +183 -120
- package/lib/network.js.map +1 -1
- package/lib/route.d.ts +64 -2
- package/lib/route.js +496 -251
- package/lib/route.js.map +1 -1
- package/lib/scripts/axe.mini.js +23978 -1
- package/lib/snapshot_validation.js +3 -0
- package/lib/snapshot_validation.js.map +1 -1
- package/lib/stable_browser.d.ts +14 -8
- package/lib/stable_browser.js +464 -91
- package/lib/stable_browser.js.map +1 -1
- package/lib/table_helper.js +14 -0
- package/lib/table_helper.js.map +1 -1
- package/lib/test_context.d.ts +1 -0
- package/lib/test_context.js +1 -0
- package/lib/test_context.js.map +1 -1
- package/lib/utils.d.ts +7 -3
- package/lib/utils.js +162 -25
- package/lib/utils.js.map +1 -1
- package/package.json +21 -12
package/lib/route.js
CHANGED
|
@@ -2,62 +2,392 @@ import fs from "fs/promises";
|
|
|
2
2
|
import path from "path";
|
|
3
3
|
import objectPath from "object-path";
|
|
4
4
|
import { tmpdir } from "os";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
import createDebug from "debug";
|
|
6
|
+
import { existsSync } from "fs";
|
|
7
|
+
import { replaceWithLocalTestData } from "./utils.js";
|
|
8
|
+
const debug = createDebug("automation_model:route");
|
|
9
|
+
async function loadRoutes(context, template) {
|
|
10
|
+
if (context.loadedRoutes instanceof Map && context.loadedRoutes.has(template)) {
|
|
11
|
+
return context.loadedRoutes.get(template) || [];
|
|
12
|
+
}
|
|
8
13
|
try {
|
|
9
14
|
let dir = path.join(process.cwd(), "data", "routes");
|
|
10
15
|
if (process.env.TEMP_RUN === "true") {
|
|
11
16
|
dir = path.join(tmpdir(), "blinq_temp_routes");
|
|
12
17
|
}
|
|
13
18
|
if (!(await folderExists(dir))) {
|
|
14
|
-
context.loadedRoutes =
|
|
15
|
-
|
|
19
|
+
context.loadedRoutes = new Map();
|
|
20
|
+
context.loadedRoutes.set(template, []);
|
|
21
|
+
return context.loadedRoutes.get(template) || [];
|
|
16
22
|
}
|
|
17
23
|
const files = await fs.readdir(dir);
|
|
18
24
|
const jsonFiles = files.filter((f) => f.endsWith(".json"));
|
|
19
|
-
const allRoutes =
|
|
25
|
+
const allRoutes = new Map();
|
|
20
26
|
for (const file of jsonFiles) {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
27
|
+
let content = await fs.readFile(path.join(dir, file), "utf-8");
|
|
28
|
+
try {
|
|
29
|
+
const routeObj = JSON.parse(content);
|
|
30
|
+
const template = routeObj.template;
|
|
31
|
+
if (!allRoutes.has(template)) {
|
|
32
|
+
allRoutes.set(template, []);
|
|
33
|
+
}
|
|
34
|
+
allRoutes.get(template)?.push(routeObj);
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
debug("Error parsing route file:", error);
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
24
40
|
}
|
|
25
41
|
context.loadedRoutes = allRoutes;
|
|
26
|
-
|
|
27
|
-
console.log(`Loaded ${allRoutes.length} route definitions from ${dir}`);
|
|
42
|
+
debug(`Loaded ${allRoutes.size} route definitions from ${dir}`);
|
|
28
43
|
}
|
|
29
44
|
catch (error) {
|
|
30
45
|
console.error("Error loading routes:", error);
|
|
31
|
-
context.loadedRoutes =
|
|
46
|
+
context.loadedRoutes = new Map();
|
|
47
|
+
}
|
|
48
|
+
return context.loadedRoutes.get(template) || [];
|
|
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);
|
|
32
84
|
}
|
|
33
|
-
return
|
|
85
|
+
return savedMethod === actualMethod;
|
|
34
86
|
}
|
|
35
87
|
function matchRoute(routeItem, req) {
|
|
88
|
+
const debug = createDebug("automation_model:route:matchRoute");
|
|
36
89
|
const url = new URL(req.request().url());
|
|
37
|
-
const methodMatch = !routeItem.filters.method || routeItem.filters.method === req.request().method();
|
|
38
|
-
const pathMatch = routeItem.filters.path === url.pathname;
|
|
39
90
|
const queryParams = routeItem.filters.queryParams;
|
|
40
|
-
const
|
|
41
|
-
|
|
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;
|
|
42
355
|
}
|
|
43
|
-
let debug = false;
|
|
44
356
|
export async function registerBeforeStepRoutes(context, stepName, world) {
|
|
357
|
+
const debug = createDebug("automation_model:route:registerBeforeStepRoutes");
|
|
45
358
|
const page = context.web.page;
|
|
46
359
|
if (!page)
|
|
47
360
|
throw new Error("context.web.page is missing");
|
|
48
361
|
const stepTemplate = _stepNameToTemplate(stepName);
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
|
|
362
|
+
debug("stepTemplate", stepTemplate);
|
|
363
|
+
const routes = await loadRoutes(context, stepTemplate);
|
|
364
|
+
debug("Routes", routes);
|
|
365
|
+
const allRouteItems = routes.flatMap((r) => r.routes);
|
|
366
|
+
debug("All route items", allRouteItems);
|
|
52
367
|
if (!context.__routeState) {
|
|
53
368
|
context.__routeState = { matched: [] };
|
|
54
369
|
}
|
|
55
|
-
|
|
56
|
-
|
|
370
|
+
for (let i = 0; i < allRouteItems.length; i++) {
|
|
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
|
+
}
|
|
57
383
|
if (item.mandatory) {
|
|
384
|
+
const path = item.filters.path;
|
|
385
|
+
const queryParams = Object.entries(item.filters.queryParams || {})
|
|
386
|
+
.map(([key, value]) => `${key}=${value}`)
|
|
387
|
+
.join("&");
|
|
58
388
|
const tracking = {
|
|
59
389
|
routeItem: item,
|
|
60
|
-
url: ""
|
|
390
|
+
url: `${path}${queryParams ? `?${queryParams}` : ""}`,
|
|
61
391
|
completed: false,
|
|
62
392
|
startedAt: Date.now(),
|
|
63
393
|
actionResults: [],
|
|
@@ -65,248 +395,154 @@ export async function registerBeforeStepRoutes(context, stepName, world) {
|
|
|
65
395
|
context.__routeState.matched.push(tracking);
|
|
66
396
|
}
|
|
67
397
|
}
|
|
398
|
+
debug("New allrouteItems", JSON.stringify(allRouteItems));
|
|
68
399
|
let message = null;
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
}
|
|
100
|
-
let response;
|
|
101
|
-
try {
|
|
102
|
-
response = await route.fetch();
|
|
103
|
-
}
|
|
104
|
-
catch (e) {
|
|
105
|
-
console.error("Fetch failed for", request.url(), e);
|
|
106
|
-
if (tracking?.timer)
|
|
107
|
-
clearTimeout(tracking.timer);
|
|
108
|
-
return route.abort();
|
|
109
|
-
}
|
|
110
|
-
let status = response.status();
|
|
111
|
-
let headers = response.headers();
|
|
112
|
-
const isBinary = !headers["content-type"]?.includes("application/json") &&
|
|
113
|
-
!headers["content-type"]?.includes("text") &&
|
|
114
|
-
!headers["content-type"]?.includes("application/csv");
|
|
115
|
-
let body;
|
|
116
|
-
if (isBinary) {
|
|
117
|
-
body = await response.body(); // returns a Buffer
|
|
118
|
-
}
|
|
119
|
-
else {
|
|
120
|
-
body = await response.text();
|
|
121
|
-
}
|
|
122
|
-
let json;
|
|
123
|
-
try {
|
|
124
|
-
// check if the body is string
|
|
125
|
-
if (typeof body === "string") {
|
|
126
|
-
json = JSON.parse(body);
|
|
400
|
+
if (stepTemplate === "Reset browser session {string}" || stepTemplate === "reset browser session {string}")
|
|
401
|
+
return;
|
|
402
|
+
if (!Array.isArray(allRouteItems) || allRouteItems.length === 0)
|
|
403
|
+
return;
|
|
404
|
+
try {
|
|
405
|
+
page.route("**/*", async (route) => {
|
|
406
|
+
const debug = createDebug("automation_model:route:intercept");
|
|
407
|
+
const request = route.request();
|
|
408
|
+
debug(`Intercepting request: ${request.method()} ${request.url()}`);
|
|
409
|
+
debug("All route items", allRouteItems);
|
|
410
|
+
const matchedItem = allRouteItems.find((item) => matchRoute(item, route));
|
|
411
|
+
if (!matchedItem)
|
|
412
|
+
return route.continue();
|
|
413
|
+
debug(`Matched route item: ${JSON.stringify(matchedItem)}`);
|
|
414
|
+
debug("Initial context route state", JSON.stringify(context.__routeState, null, 2));
|
|
415
|
+
let tracking = context.__routeState.matched.find((t) => JSON.stringify(t.routeItem) === JSON.stringify(matchedItem) && !t.completed);
|
|
416
|
+
debug("Tracking", tracking);
|
|
417
|
+
let stubActionPerformed = false;
|
|
418
|
+
if (!tracking) {
|
|
419
|
+
debug("Tracking not found, creating tracking");
|
|
420
|
+
tracking = {
|
|
421
|
+
routeItem: matchedItem,
|
|
422
|
+
url: request.url(),
|
|
423
|
+
completed: false,
|
|
424
|
+
startedAt: Date.now(),
|
|
425
|
+
actionResults: [],
|
|
426
|
+
};
|
|
427
|
+
debug("Created tracking", tracking);
|
|
428
|
+
context.__routeState.matched.push(tracking);
|
|
429
|
+
debug("Current route state", context.__routeState);
|
|
127
430
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
431
|
+
else {
|
|
432
|
+
tracking.url = request.url();
|
|
433
|
+
debug("Updating tracking", tracking);
|
|
434
|
+
}
|
|
435
|
+
const stubAction = matchedItem.actions.find((a) => a.type === "stub_request");
|
|
436
|
+
if (stubAction) {
|
|
437
|
+
stubActionPerformed = handleStubAction(stubAction, route, tracking);
|
|
438
|
+
}
|
|
439
|
+
if (!stubActionPerformed) {
|
|
440
|
+
let response;
|
|
441
|
+
try {
|
|
442
|
+
response = await route.fetch();
|
|
443
|
+
}
|
|
444
|
+
catch (e) {
|
|
445
|
+
console.error("Fetch failed for", request.url(), e);
|
|
137
446
|
if (tracking?.timer)
|
|
138
447
|
clearTimeout(tracking.timer);
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
if (
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
console.log(`[json_modify] Modified path ${action.config.path} to ${action.config.modifyValue}`);
|
|
167
|
-
console.log(`[json_modify] Modified JSON`);
|
|
168
|
-
message = `JSON modified successfully`;
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
break;
|
|
172
|
-
case "json_whole_modify":
|
|
173
|
-
if (!json) {
|
|
174
|
-
actionStatus = "fail";
|
|
175
|
-
tracking.actionResults = actionResults;
|
|
176
|
-
message = "JSON modification failed. Response is not JSON";
|
|
177
|
-
}
|
|
178
|
-
else {
|
|
179
|
-
try {
|
|
180
|
-
const parsedConfig = JSON.parse(action.config);
|
|
181
|
-
json = parsedConfig; // Replace whole JSON with new value
|
|
182
|
-
}
|
|
183
|
-
catch (e) {
|
|
184
|
-
actionStatus = "fail";
|
|
185
|
-
tracking.actionResults = actionResults;
|
|
186
|
-
message = `JSON modification failed. Invalid JSON: ${e instanceof Error ? e.message : String(e)}`;
|
|
187
|
-
console.error(`[json_whole_modify] Invalid JSON:`, e);
|
|
448
|
+
return route.abort();
|
|
449
|
+
}
|
|
450
|
+
const headers = response.headers();
|
|
451
|
+
const isBinary = !headers["content-type"]?.includes("application/json") && !headers["content-type"]?.includes("text");
|
|
452
|
+
const body = isBinary ? await response.body() : await response.text();
|
|
453
|
+
let json;
|
|
454
|
+
try {
|
|
455
|
+
if (typeof body === "string")
|
|
456
|
+
json = JSON.parse(body);
|
|
457
|
+
}
|
|
458
|
+
catch (_) { }
|
|
459
|
+
const actionHandlerContext = {
|
|
460
|
+
route,
|
|
461
|
+
tracking,
|
|
462
|
+
status: response.status(),
|
|
463
|
+
body,
|
|
464
|
+
json,
|
|
465
|
+
isBinary,
|
|
466
|
+
finalBody: json ?? body,
|
|
467
|
+
abortActionPerformed: false,
|
|
468
|
+
};
|
|
469
|
+
const actionResults = [];
|
|
470
|
+
for (const action of matchedItem.actions) {
|
|
471
|
+
let result;
|
|
472
|
+
switch (action.type) {
|
|
473
|
+
case "abort_request":
|
|
474
|
+
result = handleAbortRequest(action, actionHandlerContext);
|
|
188
475
|
break;
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
message = "JSON assertion failed. Response is not JSON";
|
|
216
|
-
}
|
|
217
|
-
else {
|
|
218
|
-
const actual = objectPath.get(json, action.config.path);
|
|
219
|
-
if (typeof actual !== "object") {
|
|
220
|
-
if (JSON.stringify(actual) !== JSON.stringify(action.config.expectedValue)) {
|
|
221
|
-
actionStatus = "fail";
|
|
222
|
-
tracking.actionResults = actionResults;
|
|
223
|
-
message = `JSON assertion failed for path ${action.config.path}: expected ${JSON.stringify(action.config.expectedValue)}, got ${JSON.stringify(actual)}`;
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
else if (JSON.stringify(actual) !== action.config.expectedValue) {
|
|
227
|
-
actionStatus = "fail";
|
|
228
|
-
tracking.actionResults = actionResults;
|
|
229
|
-
message = `JSON assertion failed for path ${action.config.path}: expected ${action.config.expectedValue}, got ${JSON.stringify(actual)}`;
|
|
230
|
-
}
|
|
231
|
-
else {
|
|
232
|
-
console.log(`[assert_json] Assertion passed for path ${action.config.path}`);
|
|
233
|
-
message = `JSON assertion passed for path ${action.config.path}`;
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
break;
|
|
237
|
-
case "assert_whole_json":
|
|
238
|
-
if (!json) {
|
|
239
|
-
actionStatus = "fail";
|
|
240
|
-
tracking.actionResults = actionResults;
|
|
241
|
-
message = "Whole JSON assertion failed. Response is not JSON";
|
|
476
|
+
case "status_code_verification":
|
|
477
|
+
result = handleStatusCodeVerification(action, actionHandlerContext);
|
|
478
|
+
break;
|
|
479
|
+
case "json_modify":
|
|
480
|
+
result = handleJsonModify(action, actionHandlerContext);
|
|
481
|
+
break;
|
|
482
|
+
case "json_whole_modify":
|
|
483
|
+
result = handleJsonWholeModify(action, actionHandlerContext);
|
|
484
|
+
break;
|
|
485
|
+
case "status_code_change":
|
|
486
|
+
result = handleStatusCodeChange(action, actionHandlerContext);
|
|
487
|
+
break;
|
|
488
|
+
case "change_text":
|
|
489
|
+
result = handleChangeText(action, actionHandlerContext);
|
|
490
|
+
break;
|
|
491
|
+
case "assert_json":
|
|
492
|
+
result = handleAssertJson(action, actionHandlerContext);
|
|
493
|
+
break;
|
|
494
|
+
case "assert_whole_json":
|
|
495
|
+
result = handleAssertWholeJson(action, actionHandlerContext);
|
|
496
|
+
break;
|
|
497
|
+
case "assert_text":
|
|
498
|
+
result = handleAssertText(action, actionHandlerContext);
|
|
499
|
+
break;
|
|
500
|
+
default:
|
|
501
|
+
console.warn(`Unknown action type`);
|
|
242
502
|
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
}
|
|
503
|
+
if (result)
|
|
504
|
+
actionResults.push(result);
|
|
505
|
+
}
|
|
506
|
+
tracking.completed = true;
|
|
507
|
+
tracking.actionResults = actionResults;
|
|
508
|
+
if (tracking.timer)
|
|
509
|
+
clearTimeout(tracking.timer);
|
|
510
|
+
if (!actionHandlerContext.abortActionPerformed) {
|
|
511
|
+
try {
|
|
512
|
+
const isJSON = headers["content-type"]?.includes("application/json");
|
|
513
|
+
if (isJSON) {
|
|
514
|
+
await route.fulfill({
|
|
515
|
+
status: actionHandlerContext.status,
|
|
516
|
+
json: actionHandlerContext.finalBody,
|
|
517
|
+
headers,
|
|
518
|
+
});
|
|
259
519
|
}
|
|
260
520
|
else {
|
|
261
|
-
|
|
262
|
-
|
|
521
|
+
await route.fulfill({
|
|
522
|
+
status: actionHandlerContext.status,
|
|
523
|
+
body: actionHandlerContext.finalBody,
|
|
524
|
+
headers,
|
|
525
|
+
});
|
|
263
526
|
}
|
|
264
527
|
}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
if (typeof body !== "string") {
|
|
268
|
-
console.error(`[assert_text] Body is not text`);
|
|
269
|
-
actionStatus = "fail";
|
|
270
|
-
tracking.actionResults = actionResults;
|
|
271
|
-
message = "Text assertion failed. Body is not text";
|
|
528
|
+
catch (e) {
|
|
529
|
+
console.error("Failed to fulfill route:", e);
|
|
272
530
|
}
|
|
273
|
-
|
|
274
|
-
if (action.config.contains && !body.includes(action.config.contains)) {
|
|
275
|
-
actionStatus = "fail";
|
|
276
|
-
tracking.actionResults = actionResults;
|
|
277
|
-
message = `Text assertion failed. Expected to contain: "${action.config.contains}", actual: "${body}"`;
|
|
278
|
-
}
|
|
279
|
-
else if (action.config.equals && body !== action.config.equals) {
|
|
280
|
-
actionStatus = "fail";
|
|
281
|
-
tracking.actionResults = actionResults;
|
|
282
|
-
message = `Text assertion failed. Expected exact match: "${action.config.equals}", actual: "${body}"`;
|
|
283
|
-
}
|
|
284
|
-
else {
|
|
285
|
-
console.log(`[assert_text] Assertion passed`);
|
|
286
|
-
message = `Text assertion passed.`;
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
break;
|
|
290
|
-
default:
|
|
291
|
-
console.warn(`Unknown action type: ${action.type}`);
|
|
531
|
+
}
|
|
292
532
|
}
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
clearTimeout(tracking.timer);
|
|
299
|
-
const responseBody = isBinary ? body : json ? JSON.stringify(json) : body;
|
|
300
|
-
if (!abortActionPerformed) {
|
|
301
|
-
await route.fulfill({ status, body: responseBody, headers });
|
|
302
|
-
}
|
|
303
|
-
});
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
catch (error) {
|
|
536
|
+
console.log(JSON.stringify(error));
|
|
537
|
+
}
|
|
304
538
|
}
|
|
305
539
|
export async function registerAfterStepRoutes(context, world) {
|
|
306
540
|
const state = context.__routeState;
|
|
541
|
+
debug("state in afterStepRoutes", JSON.stringify(state));
|
|
307
542
|
if (!state)
|
|
308
543
|
return [];
|
|
309
544
|
const mandatoryRoutes = state.matched.filter((tracked) => tracked.routeItem.mandatory);
|
|
545
|
+
debug("mandatoryRoutes in afterStepRoutes", mandatoryRoutes);
|
|
310
546
|
if (mandatoryRoutes.length === 0) {
|
|
311
547
|
context.__routeState = null;
|
|
312
548
|
return [];
|
|
@@ -314,21 +550,24 @@ export async function registerAfterStepRoutes(context, world) {
|
|
|
314
550
|
const maxTimeout = Math.max(...mandatoryRoutes.map((r) => r.routeItem.timeout));
|
|
315
551
|
const startTime = Date.now();
|
|
316
552
|
const mandatoryRouteReached = mandatoryRoutes.map((r) => true);
|
|
553
|
+
debug("mandatoryRouteReached initialized to", mandatoryRouteReached);
|
|
317
554
|
await new Promise((resolve) => {
|
|
318
555
|
const interval = setInterval(() => {
|
|
319
556
|
const now = Date.now();
|
|
320
557
|
const allCompleted = mandatoryRoutes.every((r) => r.completed);
|
|
558
|
+
debug("allCompleted in afterStepRoutes", allCompleted);
|
|
321
559
|
const allTimedOut = mandatoryRoutes.every((r) => r.completed || now - startTime >= r.routeItem.timeout);
|
|
560
|
+
debug("allTimedOut in afterStepRoutes", allTimedOut);
|
|
322
561
|
for (const r of mandatoryRoutes) {
|
|
323
562
|
const elapsed = now - startTime;
|
|
563
|
+
// debug(`Elapsed time for route ${r.url}: ${elapsed}ms`);
|
|
324
564
|
if (!r.completed && elapsed >= r.routeItem.timeout) {
|
|
325
565
|
mandatoryRouteReached[mandatoryRoutes.indexOf(r)] = false;
|
|
326
|
-
|
|
327
|
-
// `[MANDATORY] Request to ${r.routeItem.filters.path} did not complete within ${r.routeItem.timeout}ms (elapsed: ${elapsed})`
|
|
328
|
-
// );
|
|
566
|
+
debug(`Route ${r.url} timed out after ${elapsed}ms`);
|
|
329
567
|
}
|
|
330
568
|
}
|
|
331
569
|
if (allCompleted || allTimedOut) {
|
|
570
|
+
debug("allCompleted", allCompleted, "allTimedOut", allTimedOut);
|
|
332
571
|
clearInterval(interval);
|
|
333
572
|
resolve();
|
|
334
573
|
}
|
|
@@ -336,6 +575,11 @@ export async function registerAfterStepRoutes(context, world) {
|
|
|
336
575
|
});
|
|
337
576
|
context.results = mandatoryRoutes.map((tracked) => {
|
|
338
577
|
const { routeItem, url, completed, actionResults = [] } = tracked;
|
|
578
|
+
debug("tracked in afterStepRoutes", {
|
|
579
|
+
url,
|
|
580
|
+
completed,
|
|
581
|
+
actionResults,
|
|
582
|
+
});
|
|
339
583
|
const actions = actionResults.map((ar) => {
|
|
340
584
|
let status = ar.status;
|
|
341
585
|
if (!completed)
|
|
@@ -347,6 +591,7 @@ export async function registerAfterStepRoutes(context, world) {
|
|
|
347
591
|
message: ar.message || null,
|
|
348
592
|
};
|
|
349
593
|
});
|
|
594
|
+
debug("actions in afterStepRoutes", actions);
|
|
350
595
|
let overallStatus;
|
|
351
596
|
if (!completed) {
|
|
352
597
|
overallStatus = "timeout";
|