automation_model 1.0.799-dev → 1.0.799-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.
Files changed (44) hide show
  1. package/lib/api.js +40 -12
  2. package/lib/api.js.map +1 -1
  3. package/lib/auto_page.d.ts +1 -1
  4. package/lib/auto_page.js +103 -69
  5. package/lib/auto_page.js.map +1 -1
  6. package/lib/browser_manager.d.ts +2 -3
  7. package/lib/browser_manager.js +115 -71
  8. package/lib/browser_manager.js.map +1 -1
  9. package/lib/bruno.js.map +1 -1
  10. package/lib/check_performance.d.ts +1 -0
  11. package/lib/check_performance.js +57 -0
  12. package/lib/check_performance.js.map +1 -0
  13. package/lib/command_common.d.ts +2 -2
  14. package/lib/command_common.js +42 -24
  15. package/lib/command_common.js.map +1 -1
  16. package/lib/file_checker.js +7 -0
  17. package/lib/file_checker.js.map +1 -1
  18. package/lib/index.js +1 -0
  19. package/lib/index.js.map +1 -1
  20. package/lib/init_browser.d.ts +1 -2
  21. package/lib/init_browser.js +137 -128
  22. package/lib/init_browser.js.map +1 -1
  23. package/lib/locator_log.js.map +1 -1
  24. package/lib/network.d.ts +2 -2
  25. package/lib/network.js +183 -120
  26. package/lib/network.js.map +1 -1
  27. package/lib/route.d.ts +64 -2
  28. package/lib/route.js +496 -244
  29. package/lib/route.js.map +1 -1
  30. package/lib/scripts/axe.mini.js +23978 -1
  31. package/lib/snapshot_validation.js +3 -0
  32. package/lib/snapshot_validation.js.map +1 -1
  33. package/lib/stable_browser.d.ts +14 -8
  34. package/lib/stable_browser.js +456 -90
  35. package/lib/stable_browser.js.map +1 -1
  36. package/lib/table_helper.js +14 -0
  37. package/lib/table_helper.js.map +1 -1
  38. package/lib/test_context.d.ts +1 -0
  39. package/lib/test_context.js +1 -0
  40. package/lib/test_context.js.map +1 -1
  41. package/lib/utils.d.ts +7 -3
  42. package/lib/utils.js +162 -25
  43. package/lib/utils.js.map +1 -1
  44. package/package.json +19 -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
- async function loadRoutes(context) {
6
- if (context.loadedRoutes !== null)
7
- return context.loadedRoutes;
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
- return context.loadedRoutes;
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
- const content = await fs.readFile(path.join(dir, file), "utf-8");
22
- const routeObj = JSON.parse(content);
23
- allRoutes.push(routeObj);
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
- if (debug)
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 context.loadedRoutes;
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 queryMatch = !queryParams || Object.entries(queryParams).every(([key, value]) => url.searchParams.get(key) === value);
41
- return methodMatch && pathMatch && queryMatch;
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
- const routes = await loadRoutes(context);
50
- const matchedRouteDefs = routes.filter((r) => r.template === stepTemplate);
51
- const allRouteItems = matchedRouteDefs.flatMap((r) => r.routes);
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
- // Pre-register all mandatory routes
56
- for (const item of allRouteItems) {
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,241 +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
- page.route("**/*", async (route) => {
70
- const request = route.request();
71
- // print the url if debug is enabled
72
- if (debug) {
73
- console.log(`Intercepting request: ${request.method()} ${request.url()}`);
74
- }
75
- const matchedItem = allRouteItems.find((item) => matchRoute(item, route));
76
- if (debug) {
77
- console.log("Matched route item:", matchedItem);
78
- }
79
- if (!matchedItem)
80
- return route.continue();
81
- if (debug) {
82
- console.log(`Matched route item: ${JSON.stringify(matchedItem)}`);
83
- }
84
- // Find pre-registered tracker
85
- let tracking = context.__routeState.matched.find((t) => t.routeItem === matchedItem && !t.completed);
86
- // If not mandatory, register dynamically
87
- if (!tracking) {
88
- tracking = {
89
- routeItem: matchedItem,
90
- url: request.url(),
91
- completed: false,
92
- startedAt: Date.now(),
93
- actionResults: [],
94
- };
95
- context.__routeState.matched.push(tracking);
96
- }
97
- else {
98
- tracking.url = request.url();
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
- catch (_) { }
130
- const actionResults = [];
131
- let abortActionPerformed = false;
132
- for (const action of matchedItem.actions) {
133
- let actionStatus = "success";
134
- const description = JSON.stringify(action.config);
135
- switch (action.type) {
136
- case "abort_request":
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
- const errorCode = action.config?.errorCode ?? "failed";
140
- console.log(`[abort_request] Aborting with error code: ${errorCode}`);
141
- await route.abort(errorCode);
142
- abortActionPerformed = true;
143
- tracking.completed = true;
144
- break;
145
- case "status_code_verification":
146
- if (String(status) !== String(action.config)) {
147
- actionStatus = "fail";
148
- tracking.actionResults = actionResults;
149
- message = `Status code verification failed. Expected ${action.config}, got ${status}`;
150
- }
151
- else {
152
- console.log(`[status_code_verification] Passed`);
153
- message = `Status code verification passed. Expected ${action.config}, got ${status}`;
154
- }
155
- break;
156
- case "json_modify":
157
- if (!json) {
158
- // console.error(`[json_modify] Response is not JSON`);
159
- actionStatus = "fail";
160
- tracking.actionResults = actionResults;
161
- message = "JSON modification failed. Response is not JSON";
162
- }
163
- else {
164
- if (action.config && action.config.path && action.config.modifyValue) {
165
- objectPath.set(json, action.config.path, action.config.modifyValue);
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
- console.log(`[json_whole_modify] Whole JSON replaced`);
191
- message = `JSON replaced successfully`;
192
- }
193
- break;
194
- case "status_code_change":
195
- status = Number(action.config);
196
- console.log(`[status_code_change] Status changed to ${status}`);
197
- message = `Status code changed to ${status}`;
198
- break;
199
- case "change_text":
200
- if (isBinary) {
201
- actionStatus = "fail";
202
- tracking.actionResults = actionResults;
203
- message = "Change text action failed. Body is not a text";
204
- }
205
- else {
206
- body = action.config;
207
- console.log(`[change_text] HTML body replaced`);
208
- message = `HTML body replaced successfully`;
209
- }
210
- break;
211
- case "assert_json":
212
- if (!json) {
213
- actionStatus = "fail";
214
- tracking.actionResults = actionResults;
215
- message = "JSON assertion failed. Response is not JSON";
216
- }
217
- else {
218
- const actual = objectPath.get(json, action.config.path);
219
- if (JSON.stringify(actual) !== JSON.stringify(action.config.expectedValue)) {
220
- actionStatus = "fail";
221
- tracking.actionResults = actionResults;
222
- message = `JSON assertion failed for path ${action.config.path}: expected ${JSON.stringify(action.config.expectedValue)}, got ${JSON.stringify(actual)}`;
223
- }
224
- else {
225
- console.log(`[assert_json] Assertion passed for path ${action.config.path}`);
226
- message = `JSON assertion passed for path ${action.config.path}`;
227
- }
228
- }
229
- break;
230
- case "assert_whole_json":
231
- if (!json) {
232
- actionStatus = "fail";
233
- tracking.actionResults = actionResults;
234
- 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`);
235
502
  }
236
- else {
237
- if (action.config.contains) {
238
- const originalJSON = JSON.stringify(json, null, 2);
239
- if (!originalJSON.includes(action.config.contains)) {
240
- actionStatus = "fail";
241
- tracking.actionResults = actionResults;
242
- message = `Whole JSON assertion failed. Expected to contain: "${action.config.contains}", actual: "${body}"`;
243
- }
244
- }
245
- else if (action.config.equals) {
246
- const originalJSON = JSON.stringify(json, null, 2);
247
- if (originalJSON !== action.config.equals) {
248
- actionStatus = "fail";
249
- tracking.actionResults = actionResults;
250
- message = `Whole JSON assertion failed. Expected exact match: "${action.config.equals}", actual: "${body}"`;
251
- }
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
+ });
252
519
  }
253
520
  else {
254
- console.log(`[assert_whole_json] Assertion passed`);
255
- message = `Whole JSON assertion passed.`;
521
+ await route.fulfill({
522
+ status: actionHandlerContext.status,
523
+ body: actionHandlerContext.finalBody,
524
+ headers,
525
+ });
256
526
  }
257
527
  }
258
- break;
259
- case "assert_text":
260
- if (typeof body !== "string") {
261
- console.error(`[assert_text] Body is not text`);
262
- actionStatus = "fail";
263
- tracking.actionResults = actionResults;
264
- message = "Text assertion failed. Body is not text";
528
+ catch (e) {
529
+ console.error("Failed to fulfill route:", e);
265
530
  }
266
- else {
267
- if (action.config.contains && !body.includes(action.config.contains)) {
268
- actionStatus = "fail";
269
- tracking.actionResults = actionResults;
270
- message = `Text assertion failed. Expected to contain: "${action.config.contains}", actual: "${body}"`;
271
- }
272
- else if (action.config.equals && body !== action.config.equals) {
273
- actionStatus = "fail";
274
- tracking.actionResults = actionResults;
275
- message = `Text assertion failed. Expected exact match: "${action.config.equals}", actual: "${body}"`;
276
- }
277
- else {
278
- console.log(`[assert_text] Assertion passed`);
279
- message = `Text assertion passed.`;
280
- }
281
- }
282
- break;
283
- default:
284
- console.warn(`Unknown action type: ${action.type}`);
531
+ }
285
532
  }
286
- actionResults.push({ type: action.type, description, status: actionStatus, message: message });
287
- }
288
- tracking.completed = true;
289
- tracking.actionResults = actionResults;
290
- if (tracking.timer)
291
- clearTimeout(tracking.timer);
292
- const responseBody = isBinary ? body : json ? JSON.stringify(json) : body;
293
- if (!abortActionPerformed) {
294
- await route.fulfill({ status, body: responseBody, headers });
295
- }
296
- });
533
+ });
534
+ }
535
+ catch (error) {
536
+ console.log(JSON.stringify(error));
537
+ }
297
538
  }
298
539
  export async function registerAfterStepRoutes(context, world) {
299
540
  const state = context.__routeState;
541
+ debug("state in afterStepRoutes", JSON.stringify(state));
300
542
  if (!state)
301
543
  return [];
302
544
  const mandatoryRoutes = state.matched.filter((tracked) => tracked.routeItem.mandatory);
545
+ debug("mandatoryRoutes in afterStepRoutes", mandatoryRoutes);
303
546
  if (mandatoryRoutes.length === 0) {
304
547
  context.__routeState = null;
305
548
  return [];
@@ -307,21 +550,24 @@ export async function registerAfterStepRoutes(context, world) {
307
550
  const maxTimeout = Math.max(...mandatoryRoutes.map((r) => r.routeItem.timeout));
308
551
  const startTime = Date.now();
309
552
  const mandatoryRouteReached = mandatoryRoutes.map((r) => true);
553
+ debug("mandatoryRouteReached initialized to", mandatoryRouteReached);
310
554
  await new Promise((resolve) => {
311
555
  const interval = setInterval(() => {
312
556
  const now = Date.now();
313
557
  const allCompleted = mandatoryRoutes.every((r) => r.completed);
558
+ debug("allCompleted in afterStepRoutes", allCompleted);
314
559
  const allTimedOut = mandatoryRoutes.every((r) => r.completed || now - startTime >= r.routeItem.timeout);
560
+ debug("allTimedOut in afterStepRoutes", allTimedOut);
315
561
  for (const r of mandatoryRoutes) {
316
562
  const elapsed = now - startTime;
563
+ // debug(`Elapsed time for route ${r.url}: ${elapsed}ms`);
317
564
  if (!r.completed && elapsed >= r.routeItem.timeout) {
318
565
  mandatoryRouteReached[mandatoryRoutes.indexOf(r)] = false;
319
- // console.error(
320
- // `[MANDATORY] Request to ${r.routeItem.filters.path} did not complete within ${r.routeItem.timeout}ms (elapsed: ${elapsed})`
321
- // );
566
+ debug(`Route ${r.url} timed out after ${elapsed}ms`);
322
567
  }
323
568
  }
324
569
  if (allCompleted || allTimedOut) {
570
+ debug("allCompleted", allCompleted, "allTimedOut", allTimedOut);
325
571
  clearInterval(interval);
326
572
  resolve();
327
573
  }
@@ -329,6 +575,11 @@ export async function registerAfterStepRoutes(context, world) {
329
575
  });
330
576
  context.results = mandatoryRoutes.map((tracked) => {
331
577
  const { routeItem, url, completed, actionResults = [] } = tracked;
578
+ debug("tracked in afterStepRoutes", {
579
+ url,
580
+ completed,
581
+ actionResults,
582
+ });
332
583
  const actions = actionResults.map((ar) => {
333
584
  let status = ar.status;
334
585
  if (!completed)
@@ -340,6 +591,7 @@ export async function registerAfterStepRoutes(context, world) {
340
591
  message: ar.message || null,
341
592
  };
342
593
  });
594
+ debug("actions in afterStepRoutes", actions);
343
595
  let overallStatus;
344
596
  if (!completed) {
345
597
  overallStatus = "timeout";