automation_model 1.0.751-dev → 1.0.751-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/README.md +1 -0
- package/lib/api.js +11 -7
- package/lib/api.js.map +1 -1
- package/lib/auto_page.d.ts +3 -1
- package/lib/auto_page.js +66 -16
- package/lib/auto_page.js.map +1 -1
- package/lib/browser_manager.js +57 -32
- 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.js +17 -1
- package/lib/command_common.js.map +1 -1
- package/lib/file_checker.js +136 -25
- 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 +122 -126
- package/lib/init_browser.js.map +1 -1
- package/lib/locator_log.js.map +1 -1
- package/lib/network.d.ts +2 -0
- package/lib/network.js +398 -87
- package/lib/network.js.map +1 -1
- package/lib/route.d.ts +66 -16
- package/lib/route.js +560 -127
- package/lib/route.js.map +1 -1
- package/lib/scripts/axe.mini.js +23994 -1
- package/lib/snapshot_validation.js.map +1 -1
- package/lib/stable_browser.d.ts +15 -4
- package/lib/stable_browser.js +566 -87
- 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 +5 -2
- package/lib/utils.js +52 -9
- package/lib/utils.js.map +1 -1
- package/package.json +20 -11
package/lib/route.js
CHANGED
|
@@ -1,74 +1,417 @@
|
|
|
1
1
|
import fs from "fs/promises";
|
|
2
2
|
import path from "path";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
3
|
+
import objectPath from "object-path";
|
|
4
|
+
import { tmpdir } from "os";
|
|
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
|
+
}
|
|
13
|
+
try {
|
|
14
|
+
let dir = path.join(process.cwd(), "data", "routes");
|
|
15
|
+
if (process.env.TEMP_RUN === "true") {
|
|
16
|
+
dir = path.join(tmpdir(), "blinq_temp_routes");
|
|
17
|
+
}
|
|
18
|
+
if (!(await folderExists(dir))) {
|
|
19
|
+
context.loadedRoutes = new Map();
|
|
20
|
+
context.loadedRoutes.set(template, []);
|
|
21
|
+
return context.loadedRoutes.get(template) || [];
|
|
22
|
+
}
|
|
23
|
+
const files = await fs.readdir(dir);
|
|
24
|
+
const jsonFiles = files.filter((f) => f.endsWith(".json"));
|
|
25
|
+
const allRoutes = new Map();
|
|
26
|
+
for (const file of jsonFiles) {
|
|
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
|
+
}
|
|
40
|
+
}
|
|
41
|
+
context.loadedRoutes = allRoutes;
|
|
42
|
+
debug(`Loaded ${allRoutes.size} route definitions from ${dir}`);
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
console.error("Error loading routes:", error);
|
|
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);
|
|
84
|
+
}
|
|
85
|
+
return savedMethod === actualMethod;
|
|
18
86
|
}
|
|
19
87
|
function matchRoute(routeItem, req) {
|
|
88
|
+
const debug = createDebug("automation_model:route:matchRoute");
|
|
20
89
|
const url = new URL(req.request().url());
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
-
const
|
|
24
|
-
|
|
90
|
+
const queryParams = routeItem.filters.queryParams;
|
|
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
|
+
};
|
|
25
190
|
}
|
|
26
|
-
|
|
27
|
-
|
|
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;
|
|
355
|
+
}
|
|
356
|
+
export async function registerBeforeStepRoutes(context, stepName, world) {
|
|
357
|
+
const debug = createDebug("automation_model:route:registerBeforeStepRoutes");
|
|
28
358
|
const page = context.web.page;
|
|
29
359
|
if (!page)
|
|
30
360
|
throw new Error("context.web.page is missing");
|
|
31
361
|
const stepTemplate = _stepNameToTemplate(stepName);
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
|
|
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);
|
|
35
367
|
if (!context.__routeState) {
|
|
36
368
|
context.__routeState = { matched: [] };
|
|
37
369
|
}
|
|
38
|
-
|
|
39
|
-
|
|
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
|
+
}
|
|
40
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("&");
|
|
41
388
|
const tracking = {
|
|
42
389
|
routeItem: item,
|
|
43
|
-
url: ""
|
|
390
|
+
url: `${path}${queryParams ? `?${queryParams}` : ""}`,
|
|
44
391
|
completed: false,
|
|
45
392
|
startedAt: Date.now(),
|
|
46
393
|
actionResults: [],
|
|
47
394
|
};
|
|
48
|
-
tracking.timer = setTimeout(() => {
|
|
49
|
-
if (!tracking.completed) {
|
|
50
|
-
console.error(`[MANDATORY] Request to ${item.filters.path} did not complete within ${item.timeout}ms`);
|
|
51
|
-
}
|
|
52
|
-
}, item.timeout);
|
|
53
395
|
context.__routeState.matched.push(tracking);
|
|
54
396
|
}
|
|
55
397
|
}
|
|
398
|
+
debug("New allrouteItems", JSON.stringify(allRouteItems));
|
|
399
|
+
let message = null;
|
|
56
400
|
page.route("**/*", async (route) => {
|
|
401
|
+
const debug = createDebug("automation_model:route:intercept");
|
|
57
402
|
const request = route.request();
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
console.log(`Intercepting request: ${request.method()} ${request.url()}`);
|
|
61
|
-
}
|
|
403
|
+
debug(`Intercepting request: ${request.method()} ${request.url()}`);
|
|
404
|
+
debug("All route items", allRouteItems);
|
|
62
405
|
const matchedItem = allRouteItems.find((item) => matchRoute(item, route));
|
|
63
406
|
if (!matchedItem)
|
|
64
407
|
return route.continue();
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
let
|
|
70
|
-
// If not mandatory, register dynamically
|
|
408
|
+
debug(`Matched route item: ${JSON.stringify(matchedItem)}`);
|
|
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);
|
|
411
|
+
debug("Tracking", tracking);
|
|
412
|
+
let stubActionPerformed = false;
|
|
71
413
|
if (!tracking) {
|
|
414
|
+
debug("Tracking not found, creating tracking");
|
|
72
415
|
tracking = {
|
|
73
416
|
routeItem: matchedItem,
|
|
74
417
|
url: request.url(),
|
|
@@ -76,119 +419,154 @@ export async function registerBeforeStepRoutes(context, stepName) {
|
|
|
76
419
|
startedAt: Date.now(),
|
|
77
420
|
actionResults: [],
|
|
78
421
|
};
|
|
422
|
+
debug("Created tracking", tracking);
|
|
79
423
|
context.__routeState.matched.push(tracking);
|
|
424
|
+
debug("Current route state", context.__routeState);
|
|
80
425
|
}
|
|
81
426
|
else {
|
|
82
427
|
tracking.url = request.url();
|
|
428
|
+
debug("Updating tracking", tracking);
|
|
83
429
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
430
|
+
const stubAction = matchedItem.actions.find((a) => a.type === "stub_request");
|
|
431
|
+
if (stubAction) {
|
|
432
|
+
stubActionPerformed = handleStubAction(stubAction, route, tracking);
|
|
87
433
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
434
|
+
if (!stubActionPerformed) {
|
|
435
|
+
let response;
|
|
436
|
+
try {
|
|
437
|
+
response = await route.fetch();
|
|
438
|
+
}
|
|
439
|
+
catch (e) {
|
|
440
|
+
console.error("Fetch failed for", request.url(), e);
|
|
441
|
+
if (tracking?.timer)
|
|
442
|
+
clearTimeout(tracking.timer);
|
|
443
|
+
return route.abort();
|
|
444
|
+
}
|
|
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();
|
|
448
|
+
let json;
|
|
449
|
+
try {
|
|
450
|
+
if (typeof body === "string")
|
|
451
|
+
json = JSON.parse(body);
|
|
452
|
+
}
|
|
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
|
+
};
|
|
464
|
+
const actionResults = [];
|
|
465
|
+
for (const action of matchedItem.actions) {
|
|
466
|
+
let result;
|
|
467
|
+
switch (action.type) {
|
|
468
|
+
case "abort_request":
|
|
469
|
+
result = handleAbortRequest(action, actionHandlerContext);
|
|
470
|
+
break;
|
|
471
|
+
case "status_code_verification":
|
|
472
|
+
result = handleStatusCodeVerification(action, actionHandlerContext);
|
|
473
|
+
break;
|
|
474
|
+
case "json_modify":
|
|
475
|
+
result = handleJsonModify(action, actionHandlerContext);
|
|
476
|
+
break;
|
|
477
|
+
case "json_whole_modify":
|
|
478
|
+
result = handleJsonWholeModify(action, actionHandlerContext);
|
|
479
|
+
break;
|
|
480
|
+
case "status_code_change":
|
|
481
|
+
result = handleStatusCodeChange(action, actionHandlerContext);
|
|
482
|
+
break;
|
|
483
|
+
case "change_text":
|
|
484
|
+
result = handleChangeText(action, actionHandlerContext);
|
|
485
|
+
break;
|
|
486
|
+
case "assert_json":
|
|
487
|
+
result = handleAssertJson(action, actionHandlerContext);
|
|
488
|
+
break;
|
|
489
|
+
case "assert_whole_json":
|
|
490
|
+
result = handleAssertWholeJson(action, actionHandlerContext);
|
|
491
|
+
break;
|
|
492
|
+
case "assert_text":
|
|
493
|
+
result = handleAssertText(action, actionHandlerContext);
|
|
494
|
+
break;
|
|
495
|
+
default:
|
|
496
|
+
console.warn(`Unknown action type`);
|
|
497
|
+
}
|
|
498
|
+
if (result)
|
|
499
|
+
actionResults.push(result);
|
|
500
|
+
}
|
|
501
|
+
tracking.completed = true;
|
|
502
|
+
tracking.actionResults = actionResults;
|
|
503
|
+
if (tracking.timer)
|
|
91
504
|
clearTimeout(tracking.timer);
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
let json;
|
|
98
|
-
try {
|
|
99
|
-
json = JSON.parse(body);
|
|
100
|
-
}
|
|
101
|
-
catch (_) { }
|
|
102
|
-
const actionResults = [];
|
|
103
|
-
for (const action of matchedItem.actions) {
|
|
104
|
-
let actionStatus = "success";
|
|
105
|
-
const description = JSON.stringify(action.config);
|
|
106
|
-
switch (action.type) {
|
|
107
|
-
case "status_code_verification":
|
|
108
|
-
if (status !== action.config) {
|
|
109
|
-
console.error(`[status_code_verification] Expected ${action.config}, got ${status}`);
|
|
110
|
-
actionStatus = "fail";
|
|
111
|
-
}
|
|
112
|
-
else {
|
|
113
|
-
console.log(`[status_code_verification] Passed`);
|
|
114
|
-
}
|
|
115
|
-
break;
|
|
116
|
-
case "json_modify":
|
|
117
|
-
if (!json) {
|
|
118
|
-
console.error(`[json_modify] Response is not JSON`);
|
|
119
|
-
actionStatus = "fail";
|
|
120
|
-
}
|
|
121
|
-
else {
|
|
122
|
-
for (const mod of action.config) {
|
|
123
|
-
const pathParts = mod.path.split(".");
|
|
124
|
-
let obj = json;
|
|
125
|
-
for (let i = 0; i < pathParts.length - 1; i++) {
|
|
126
|
-
obj = obj?.[pathParts[i]];
|
|
127
|
-
if (!obj)
|
|
128
|
-
break;
|
|
129
|
-
}
|
|
130
|
-
const lastKey = pathParts[pathParts.length - 1];
|
|
131
|
-
if (obj)
|
|
132
|
-
obj[lastKey] = mod.value;
|
|
133
|
-
}
|
|
134
|
-
console.log(`[json_modify] Modified JSON`);
|
|
135
|
-
}
|
|
136
|
-
break;
|
|
137
|
-
case "status_code_change":
|
|
138
|
-
status = action.config;
|
|
139
|
-
console.log(`[status_code_change] Status changed to ${status}`);
|
|
140
|
-
break;
|
|
141
|
-
case "change_text":
|
|
142
|
-
if (!headers["content-type"]?.includes("text/html")) {
|
|
143
|
-
console.error(`[change_text] Content-Type is not text/html`);
|
|
144
|
-
actionStatus = "fail";
|
|
505
|
+
if (!actionHandlerContext.abortActionPerformed) {
|
|
506
|
+
try {
|
|
507
|
+
const isJSON = headers["content-type"]?.includes("application/json");
|
|
508
|
+
if (isJSON) {
|
|
509
|
+
await route.fulfill({ status: actionHandlerContext.status, json: actionHandlerContext.finalBody, headers });
|
|
145
510
|
}
|
|
146
511
|
else {
|
|
147
|
-
|
|
148
|
-
|
|
512
|
+
await route.fulfill({
|
|
513
|
+
status: actionHandlerContext.status,
|
|
514
|
+
body: actionHandlerContext.finalBody,
|
|
515
|
+
headers,
|
|
516
|
+
});
|
|
149
517
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
console.
|
|
518
|
+
}
|
|
519
|
+
catch (e) {
|
|
520
|
+
console.error("Failed to fulfill route:", e);
|
|
521
|
+
}
|
|
153
522
|
}
|
|
154
|
-
|
|
155
|
-
}
|
|
156
|
-
tracking.completed = true;
|
|
157
|
-
tracking.actionResults = actionResults;
|
|
158
|
-
if (tracking.timer)
|
|
159
|
-
clearTimeout(tracking.timer);
|
|
160
|
-
const responseBody = json ? JSON.stringify(json) : body;
|
|
161
|
-
await route.fulfill({ status, body: responseBody, headers });
|
|
523
|
+
}
|
|
162
524
|
});
|
|
163
525
|
}
|
|
164
|
-
export async function registerAfterStepRoutes(context) {
|
|
526
|
+
export async function registerAfterStepRoutes(context, world) {
|
|
165
527
|
const state = context.__routeState;
|
|
528
|
+
debug("state in afterStepRoutes", JSON.stringify(state));
|
|
166
529
|
if (!state)
|
|
167
530
|
return [];
|
|
168
531
|
const mandatoryRoutes = state.matched.filter((tracked) => tracked.routeItem.mandatory);
|
|
532
|
+
debug("mandatoryRoutes in afterStepRoutes", mandatoryRoutes);
|
|
169
533
|
if (mandatoryRoutes.length === 0) {
|
|
170
534
|
context.__routeState = null;
|
|
171
535
|
return [];
|
|
172
536
|
}
|
|
173
537
|
const maxTimeout = Math.max(...mandatoryRoutes.map((r) => r.routeItem.timeout));
|
|
174
538
|
const startTime = Date.now();
|
|
539
|
+
const mandatoryRouteReached = mandatoryRoutes.map((r) => true);
|
|
540
|
+
debug("mandatoryRouteReached initialized to", mandatoryRouteReached);
|
|
175
541
|
await new Promise((resolve) => {
|
|
176
542
|
const interval = setInterval(() => {
|
|
543
|
+
const now = Date.now();
|
|
177
544
|
const allCompleted = mandatoryRoutes.every((r) => r.completed);
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
545
|
+
debug("allCompleted in afterStepRoutes", allCompleted);
|
|
546
|
+
const allTimedOut = mandatoryRoutes.every((r) => r.completed || now - startTime >= r.routeItem.timeout);
|
|
547
|
+
debug("allTimedOut in afterStepRoutes", allTimedOut);
|
|
548
|
+
for (const r of mandatoryRoutes) {
|
|
549
|
+
const elapsed = now - startTime;
|
|
550
|
+
// debug(`Elapsed time for route ${r.url}: ${elapsed}ms`);
|
|
551
|
+
if (!r.completed && elapsed >= r.routeItem.timeout) {
|
|
552
|
+
mandatoryRouteReached[mandatoryRoutes.indexOf(r)] = false;
|
|
553
|
+
debug(`Route ${r.url} timed out after ${elapsed}ms`);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
if (allCompleted || allTimedOut) {
|
|
557
|
+
debug("allCompleted", allCompleted, "allTimedOut", allTimedOut);
|
|
185
558
|
clearInterval(interval);
|
|
186
559
|
resolve();
|
|
187
560
|
}
|
|
188
561
|
}, 100);
|
|
189
562
|
});
|
|
190
|
-
|
|
563
|
+
context.results = mandatoryRoutes.map((tracked) => {
|
|
191
564
|
const { routeItem, url, completed, actionResults = [] } = tracked;
|
|
565
|
+
debug("tracked in afterStepRoutes", {
|
|
566
|
+
url,
|
|
567
|
+
completed,
|
|
568
|
+
actionResults,
|
|
569
|
+
});
|
|
192
570
|
const actions = actionResults.map((ar) => {
|
|
193
571
|
let status = ar.status;
|
|
194
572
|
if (!completed)
|
|
@@ -197,8 +575,10 @@ export async function registerAfterStepRoutes(context) {
|
|
|
197
575
|
type: ar.type,
|
|
198
576
|
description: ar.description,
|
|
199
577
|
status,
|
|
578
|
+
message: ar.message || null,
|
|
200
579
|
};
|
|
201
580
|
});
|
|
581
|
+
debug("actions in afterStepRoutes", actions);
|
|
202
582
|
let overallStatus;
|
|
203
583
|
if (!completed) {
|
|
204
584
|
overallStatus = "timeout";
|
|
@@ -216,11 +596,36 @@ export async function registerAfterStepRoutes(context) {
|
|
|
216
596
|
overallStatus,
|
|
217
597
|
};
|
|
218
598
|
});
|
|
599
|
+
try {
|
|
600
|
+
await context.web.page.unroute("**/*");
|
|
601
|
+
}
|
|
602
|
+
catch (e) {
|
|
603
|
+
console.error("Failed to unroute:", e);
|
|
604
|
+
}
|
|
219
605
|
context.__routeState = null;
|
|
220
|
-
context.
|
|
221
|
-
|
|
606
|
+
if (context.results && context.results.length > 0) {
|
|
607
|
+
if (world && world.attach) {
|
|
608
|
+
await world.attach(JSON.stringify(context.results), "application/json+intercept-results");
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
const hasFailed = context.results.some((r) => r.overallStatus === "fail" || r.overallStatus === "timeout");
|
|
612
|
+
if (hasFailed) {
|
|
613
|
+
const errorMessage = context.results
|
|
614
|
+
.filter((r) => r.overallStatus === "fail" || r.overallStatus === "timeout")
|
|
615
|
+
.map((r) => `Route to ${r.url} failed with status: ${r.overallStatus}`)
|
|
616
|
+
.join("\n");
|
|
617
|
+
throw new Error(`Route verification failed:\n${errorMessage}`);
|
|
618
|
+
}
|
|
619
|
+
const hasTimedOut = context.results.some((r) => r.overallStatus === "timeout");
|
|
620
|
+
if (hasTimedOut) {
|
|
621
|
+
const timeoutMessage = context.results
|
|
622
|
+
.filter((r) => r.overallStatus === "timeout")
|
|
623
|
+
.map((r) => `Mandatory Route to ${r.url} timed out after ${r.actions[0]?.description}`)
|
|
624
|
+
.join("\n");
|
|
625
|
+
throw new Error(`Mandatory Route verification timed out:\n${timeoutMessage}`);
|
|
626
|
+
}
|
|
627
|
+
return context.results;
|
|
222
628
|
}
|
|
223
|
-
// Helper functions
|
|
224
629
|
const toCucumberExpression = (text) => text.replaceAll("/", "\\\\/").replaceAll("(", "\\\\(").replaceAll("{", "\\\\{");
|
|
225
630
|
function extractQuotedText(inputString) {
|
|
226
631
|
const regex = /("[^"]*")/g;
|
|
@@ -235,7 +640,7 @@ function extractQuotedText(inputString) {
|
|
|
235
640
|
}
|
|
236
641
|
return matches;
|
|
237
642
|
}
|
|
238
|
-
function _stepNameToTemplate(stepName) {
|
|
643
|
+
export function _stepNameToTemplate(stepName) {
|
|
239
644
|
if (stepName.includes("{string}")) {
|
|
240
645
|
return stepName;
|
|
241
646
|
}
|
|
@@ -246,4 +651,32 @@ function _stepNameToTemplate(stepName) {
|
|
|
246
651
|
});
|
|
247
652
|
return result;
|
|
248
653
|
}
|
|
654
|
+
async function folderExists(path) {
|
|
655
|
+
try {
|
|
656
|
+
const stat = await fs.stat(path);
|
|
657
|
+
return stat.isDirectory();
|
|
658
|
+
}
|
|
659
|
+
catch (err) {
|
|
660
|
+
return false;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
const getValue = (data, pattern) => {
|
|
664
|
+
const path = pattern.split(".");
|
|
665
|
+
let lengthExists = false;
|
|
666
|
+
if (path[path.length - 1] === "length") {
|
|
667
|
+
path.pop();
|
|
668
|
+
lengthExists = true;
|
|
669
|
+
}
|
|
670
|
+
const value = objectPath.get(data, pattern);
|
|
671
|
+
if (lengthExists && Array.isArray(value)) {
|
|
672
|
+
return value?.length;
|
|
673
|
+
}
|
|
674
|
+
else if (hasValue(value)) {
|
|
675
|
+
return value;
|
|
676
|
+
}
|
|
677
|
+
return undefined;
|
|
678
|
+
};
|
|
679
|
+
const hasValue = (value) => {
|
|
680
|
+
return value !== undefined;
|
|
681
|
+
};
|
|
249
682
|
//# sourceMappingURL=route.js.map
|