automation_model 1.0.779-dev → 1.0.779-stage

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/route.js CHANGED
@@ -2,273 +2,580 @@ 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
- let loadedRoutes = null;
6
- async function loadRoutes() {
7
- if (loadedRoutes !== null)
8
- return 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
+ }
9
13
  try {
10
14
  let dir = path.join(process.cwd(), "data", "routes");
11
15
  if (process.env.TEMP_RUN === "true") {
12
16
  dir = path.join(tmpdir(), "blinq_temp_routes");
13
17
  }
14
18
  if (!(await folderExists(dir))) {
15
- loadedRoutes = [];
16
- return loadedRoutes;
19
+ context.loadedRoutes = new Map();
20
+ context.loadedRoutes.set(template, []);
21
+ return context.loadedRoutes.get(template) || [];
17
22
  }
18
23
  const files = await fs.readdir(dir);
19
24
  const jsonFiles = files.filter((f) => f.endsWith(".json"));
20
- const allRoutes = [];
25
+ const allRoutes = new Map();
21
26
  for (const file of jsonFiles) {
22
- const content = await fs.readFile(path.join(dir, file), "utf-8");
23
- const routeObj = JSON.parse(content);
24
- 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
+ }
25
40
  }
26
- loadedRoutes = allRoutes;
27
- console.log(`Loaded ${allRoutes.length} route definitions from ${dir}`);
41
+ context.loadedRoutes = allRoutes;
42
+ debug(`Loaded ${allRoutes.size} route definitions from ${dir}`);
28
43
  }
29
44
  catch (error) {
30
45
  console.error("Error loading routes:", error);
31
- 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;
32
62
  }
33
- return loadedRoutes;
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;
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
+ };
42
272
  }
43
- let debug = false;
44
- export async function registerBeforeStepRoutes(context, stepName) {
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");
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();
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: [],
64
394
  };
65
- // tracking.timer = setTimeout(() => {
66
- // if (!tracking.completed) {
67
- // console.error(`[MANDATORY] Request to ${item.filters.path} did not complete within ${item.timeout}ms`);
68
- // }
69
- // }, item.timeout);
70
395
  context.__routeState.matched.push(tracking);
71
396
  }
72
397
  }
73
- page.route("**/*", async (route) => {
74
- const request = route.request();
75
- // print the url if debug is enabled
76
- if (debug) {
77
- console.log(`Intercepting request: ${request.method()} ${request.url()}`);
78
- }
79
- const matchedItem = allRouteItems.find((item) => matchRoute(item, route));
80
- if (debug) {
81
- console.log("Matched route item:", matchedItem);
82
- }
83
- if (!matchedItem)
84
- return route.continue();
85
- if (debug) {
86
- console.log(`Matched route item: ${JSON.stringify(matchedItem)}`);
87
- }
88
- // Find pre-registered tracker
89
- let tracking = context.__routeState.matched.find((t) => t.routeItem === matchedItem && !t.completed);
90
- // If not mandatory, register dynamically
91
- if (!tracking) {
92
- tracking = {
93
- routeItem: matchedItem,
94
- url: request.url(),
95
- completed: false,
96
- startedAt: Date.now(),
97
- actionResults: [],
98
- };
99
- context.__routeState.matched.push(tracking);
100
- }
101
- else {
102
- tracking.url = request.url();
103
- }
104
- let response;
105
- try {
106
- response = await route.fetch();
107
- }
108
- catch (e) {
109
- console.error("Fetch failed for", request.url(), e);
110
- if (tracking?.timer)
111
- clearTimeout(tracking.timer);
112
- return route.abort();
113
- }
114
- let status = response.status();
115
- let headers = response.headers();
116
- const isBinary = !headers["content-type"]?.includes("application/json") && !headers["content-type"]?.includes("text");
117
- let body;
118
- if (isBinary) {
119
- body = await response.body(); // returns a Buffer
120
- }
121
- else {
122
- body = await response.text();
123
- }
124
- let json;
125
- try {
126
- // check if the body is string
127
- if (typeof body === "string") {
128
- json = JSON.parse(body);
398
+ debug("New allrouteItems", JSON.stringify(allRouteItems));
399
+ let message = null;
400
+ try {
401
+ page.route("**/*", async (route) => {
402
+ const debug = createDebug("automation_model:route:intercept");
403
+ const request = route.request();
404
+ debug(`Intercepting request: ${request.method()} ${request.url()}`);
405
+ debug("All route items", allRouteItems);
406
+ const matchedItem = allRouteItems.find((item) => matchRoute(item, route));
407
+ if (!matchedItem)
408
+ return route.continue();
409
+ debug(`Matched route item: ${JSON.stringify(matchedItem)}`);
410
+ debug("Initial context route state", JSON.stringify(context.__routeState, null, 2));
411
+ let tracking = context.__routeState.matched.find((t) => JSON.stringify(t.routeItem) === JSON.stringify(matchedItem) && !t.completed);
412
+ debug("Tracking", tracking);
413
+ let stubActionPerformed = false;
414
+ if (!tracking) {
415
+ debug("Tracking not found, creating tracking");
416
+ tracking = {
417
+ routeItem: matchedItem,
418
+ url: request.url(),
419
+ completed: false,
420
+ startedAt: Date.now(),
421
+ actionResults: [],
422
+ };
423
+ debug("Created tracking", tracking);
424
+ context.__routeState.matched.push(tracking);
425
+ debug("Current route state", context.__routeState);
129
426
  }
130
- }
131
- catch (_) { }
132
- const actionResults = [];
133
- let abortActionPerformed = false;
134
- for (const action of matchedItem.actions) {
135
- let actionStatus = "success";
136
- const description = JSON.stringify(action.config);
137
- switch (action.type) {
138
- case "abort_request":
427
+ else {
428
+ tracking.url = request.url();
429
+ debug("Updating tracking", tracking);
430
+ }
431
+ const stubAction = matchedItem.actions.find((a) => a.type === "stub_request");
432
+ if (stubAction) {
433
+ stubActionPerformed = handleStubAction(stubAction, route, tracking);
434
+ }
435
+ if (!stubActionPerformed) {
436
+ let response;
437
+ try {
438
+ response = await route.fetch();
439
+ }
440
+ catch (e) {
441
+ console.error("Fetch failed for", request.url(), e);
139
442
  if (tracking?.timer)
140
443
  clearTimeout(tracking.timer);
141
- const errorCode = action.config?.errorCode ?? "failed";
142
- console.log(`[abort_request] Aborting with error code: ${errorCode}`);
143
- await route.abort(errorCode);
144
- abortActionPerformed = true;
145
- tracking.completed = true;
146
- actionResults.push({
147
- type: action.type,
148
- description: JSON.stringify(action.config),
149
- status: "success",
150
- });
151
- break;
152
- case "status_code_verification":
153
- if (status !== action.config) {
154
- console.error(`[status_code_verification] Expected ${action.config}, got ${status}`);
155
- actionStatus = "fail";
156
- }
157
- else {
158
- console.log(`[status_code_verification] Passed`);
159
- }
160
- break;
161
- case "json_modify":
162
- if (!json) {
163
- console.error(`[json_modify] Response is not JSON`);
164
- actionStatus = "fail";
165
- }
166
- else {
167
- for (const mod of action.config) {
168
- objectPath.set(json, mod.path, mod.value);
169
- console.log(`[json_modify] Modified path ${mod.path} to ${mod.value}`);
170
- }
171
- console.log(`[json_modify] Modified JSON`);
172
- }
173
- break;
174
- case "status_code_change":
175
- status = action.config;
176
- console.log(`[status_code_change] Status changed to ${status}`);
177
- break;
178
- case "change_text":
179
- if (!headers["content-type"]?.includes("text/html")) {
180
- console.error(`[change_text] Content-Type is not text/html`);
181
- actionStatus = "fail";
182
- }
183
- else {
184
- body = action.config;
185
- console.log(`[change_text] HTML body replaced`);
186
- }
187
- break;
188
- case "assert_json":
189
- if (!json) {
190
- console.error(`[assert_json] Response is not JSON`);
191
- actionStatus = "fail";
192
- }
193
- else {
194
- for (const check of action.config) {
195
- const actual = getValue(json, check.path);
196
- const expected = check.expected;
197
- if (JSON.stringify(actual) !== JSON.stringify(expected)) {
198
- console.error(`[assert_json] Path ${check.path}: expected ${expected}, got ${actual}`);
199
- actionStatus = "fail";
200
- }
201
- else {
202
- console.log(`[assert_json] Assertion passed for path ${check.path}`);
203
- }
204
- }
205
- }
206
- break;
207
- case "assert_text":
208
- if (typeof body !== "string") {
209
- console.error(`[assert_text] Body is not text`);
210
- actionStatus = "fail";
444
+ return route.abort();
445
+ }
446
+ const headers = response.headers();
447
+ const isBinary = !headers["content-type"]?.includes("application/json") && !headers["content-type"]?.includes("text");
448
+ const body = isBinary ? await response.body() : await response.text();
449
+ let json;
450
+ try {
451
+ if (typeof body === "string")
452
+ json = JSON.parse(body);
453
+ }
454
+ catch (_) { }
455
+ const actionHandlerContext = {
456
+ route,
457
+ tracking,
458
+ status: response.status(),
459
+ body,
460
+ json,
461
+ isBinary,
462
+ finalBody: json ?? body,
463
+ abortActionPerformed: false,
464
+ };
465
+ const actionResults = [];
466
+ for (const action of matchedItem.actions) {
467
+ let result;
468
+ switch (action.type) {
469
+ case "abort_request":
470
+ result = handleAbortRequest(action, actionHandlerContext);
471
+ break;
472
+ case "status_code_verification":
473
+ result = handleStatusCodeVerification(action, actionHandlerContext);
474
+ break;
475
+ case "json_modify":
476
+ result = handleJsonModify(action, actionHandlerContext);
477
+ break;
478
+ case "json_whole_modify":
479
+ result = handleJsonWholeModify(action, actionHandlerContext);
480
+ break;
481
+ case "status_code_change":
482
+ result = handleStatusCodeChange(action, actionHandlerContext);
483
+ break;
484
+ case "change_text":
485
+ result = handleChangeText(action, actionHandlerContext);
486
+ break;
487
+ case "assert_json":
488
+ result = handleAssertJson(action, actionHandlerContext);
489
+ break;
490
+ case "assert_whole_json":
491
+ result = handleAssertWholeJson(action, actionHandlerContext);
492
+ break;
493
+ case "assert_text":
494
+ result = handleAssertText(action, actionHandlerContext);
495
+ break;
496
+ default:
497
+ console.warn(`Unknown action type`);
211
498
  }
212
- else {
213
- if (action.config.contains && !body.includes(action.config.contains)) {
214
- console.error(`[assert_text] Expected to contain: "${action.config.contains}"`);
215
- actionStatus = "fail";
216
- }
217
- else if (action.config.equals && body !== action.config.equals) {
218
- console.error(`[assert_text] Expected exact match`);
219
- actionStatus = "fail";
499
+ if (result)
500
+ actionResults.push(result);
501
+ }
502
+ tracking.completed = true;
503
+ tracking.actionResults = actionResults;
504
+ if (tracking.timer)
505
+ clearTimeout(tracking.timer);
506
+ if (!actionHandlerContext.abortActionPerformed) {
507
+ try {
508
+ const isJSON = headers["content-type"]?.includes("application/json");
509
+ if (isJSON) {
510
+ await route.fulfill({
511
+ status: actionHandlerContext.status,
512
+ json: actionHandlerContext.finalBody,
513
+ headers,
514
+ });
220
515
  }
221
516
  else {
222
- console.log(`[assert_text] Assertion passed`);
517
+ await route.fulfill({
518
+ status: actionHandlerContext.status,
519
+ body: actionHandlerContext.finalBody,
520
+ headers,
521
+ });
223
522
  }
224
523
  }
225
- break;
226
- default:
227
- console.warn(`Unknown action type: ${action.type}`);
524
+ catch (e) {
525
+ console.error("Failed to fulfill route:", e);
526
+ }
527
+ }
228
528
  }
229
- actionResults.push({ type: action.type, description, status: actionStatus });
230
- }
231
- tracking.completed = true;
232
- tracking.actionResults = actionResults;
233
- console.log("Current action results:", tracking.actionResults);
234
- if (tracking.timer)
235
- clearTimeout(tracking.timer);
236
- const responseBody = isBinary ? body : json ? JSON.stringify(json) : body;
237
- if (!abortActionPerformed) {
238
- await route.fulfill({ status, body: responseBody, headers });
239
- }
240
- });
529
+ });
530
+ }
531
+ catch (error) {
532
+ console.log(JSON.stringify(error));
533
+ }
241
534
  }
242
- export async function registerAfterStepRoutes(context) {
535
+ export async function registerAfterStepRoutes(context, world) {
243
536
  const state = context.__routeState;
537
+ debug("state in afterStepRoutes", JSON.stringify(state));
244
538
  if (!state)
245
539
  return [];
246
540
  const mandatoryRoutes = state.matched.filter((tracked) => tracked.routeItem.mandatory);
541
+ debug("mandatoryRoutes in afterStepRoutes", mandatoryRoutes);
247
542
  if (mandatoryRoutes.length === 0) {
248
543
  context.__routeState = null;
249
544
  return [];
250
545
  }
251
546
  const maxTimeout = Math.max(...mandatoryRoutes.map((r) => r.routeItem.timeout));
252
547
  const startTime = Date.now();
548
+ const mandatoryRouteReached = mandatoryRoutes.map((r) => true);
549
+ debug("mandatoryRouteReached initialized to", mandatoryRouteReached);
253
550
  await new Promise((resolve) => {
254
551
  const interval = setInterval(() => {
255
552
  const now = Date.now();
256
553
  const allCompleted = mandatoryRoutes.every((r) => r.completed);
554
+ debug("allCompleted in afterStepRoutes", allCompleted);
257
555
  const allTimedOut = mandatoryRoutes.every((r) => r.completed || now - startTime >= r.routeItem.timeout);
556
+ debug("allTimedOut in afterStepRoutes", allTimedOut);
258
557
  for (const r of mandatoryRoutes) {
259
558
  const elapsed = now - startTime;
559
+ // debug(`Elapsed time for route ${r.url}: ${elapsed}ms`);
260
560
  if (!r.completed && elapsed >= r.routeItem.timeout) {
261
- console.error(`[MANDATORY] Request to ${r.routeItem.filters.path} did not complete within ${r.routeItem.timeout}ms (elapsed: ${elapsed})`);
561
+ mandatoryRouteReached[mandatoryRoutes.indexOf(r)] = false;
562
+ debug(`Route ${r.url} timed out after ${elapsed}ms`);
262
563
  }
263
564
  }
264
565
  if (allCompleted || allTimedOut) {
566
+ debug("allCompleted", allCompleted, "allTimedOut", allTimedOut);
265
567
  clearInterval(interval);
266
568
  resolve();
267
569
  }
268
570
  }, 100);
269
571
  });
270
- const results = mandatoryRoutes.map((tracked) => {
572
+ context.results = mandatoryRoutes.map((tracked) => {
271
573
  const { routeItem, url, completed, actionResults = [] } = tracked;
574
+ debug("tracked in afterStepRoutes", {
575
+ url,
576
+ completed,
577
+ actionResults,
578
+ });
272
579
  const actions = actionResults.map((ar) => {
273
580
  let status = ar.status;
274
581
  if (!completed)
@@ -277,8 +584,10 @@ export async function registerAfterStepRoutes(context) {
277
584
  type: ar.type,
278
585
  description: ar.description,
279
586
  status,
587
+ message: ar.message || null,
280
588
  };
281
589
  });
590
+ debug("actions in afterStepRoutes", actions);
282
591
  let overallStatus;
283
592
  if (!completed) {
284
593
  overallStatus = "timeout";
@@ -303,10 +612,29 @@ export async function registerAfterStepRoutes(context) {
303
612
  console.error("Failed to unroute:", e);
304
613
  }
305
614
  context.__routeState = null;
306
- context.routeResults = results;
307
- return results;
615
+ if (context.results && context.results.length > 0) {
616
+ if (world && world.attach) {
617
+ await world.attach(JSON.stringify(context.results), "application/json+intercept-results");
618
+ }
619
+ }
620
+ const hasFailed = context.results.some((r) => r.overallStatus === "fail" || r.overallStatus === "timeout");
621
+ if (hasFailed) {
622
+ const errorMessage = context.results
623
+ .filter((r) => r.overallStatus === "fail" || r.overallStatus === "timeout")
624
+ .map((r) => `Route to ${r.url} failed with status: ${r.overallStatus}`)
625
+ .join("\n");
626
+ throw new Error(`Route verification failed:\n${errorMessage}`);
627
+ }
628
+ const hasTimedOut = context.results.some((r) => r.overallStatus === "timeout");
629
+ if (hasTimedOut) {
630
+ const timeoutMessage = context.results
631
+ .filter((r) => r.overallStatus === "timeout")
632
+ .map((r) => `Mandatory Route to ${r.url} timed out after ${r.actions[0]?.description}`)
633
+ .join("\n");
634
+ throw new Error(`Mandatory Route verification timed out:\n${timeoutMessage}`);
635
+ }
636
+ return context.results;
308
637
  }
309
- // Helper functions
310
638
  const toCucumberExpression = (text) => text.replaceAll("/", "\\\\/").replaceAll("(", "\\\\(").replaceAll("{", "\\\\{");
311
639
  function extractQuotedText(inputString) {
312
640
  const regex = /("[^"]*")/g;