automation_model 1.0.752-dev → 1.0.752-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
@@ -1,84 +1,417 @@
1
1
  import fs from "fs/promises";
2
2
  import path from "path";
3
- let loadedRoutes = null;
4
- async function loadRoutes() {
5
- if (loadedRoutes !== null)
6
- return loadedRoutes;
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
+ }
7
13
  try {
8
- const dir = path.join(process.cwd(), "data", "routes");
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
+ }
9
18
  if (!(await folderExists(dir))) {
10
- loadedRoutes = [];
11
- return loadedRoutes;
19
+ context.loadedRoutes = new Map();
20
+ context.loadedRoutes.set(template, []);
21
+ return context.loadedRoutes.get(template) || [];
12
22
  }
13
23
  const files = await fs.readdir(dir);
14
24
  const jsonFiles = files.filter((f) => f.endsWith(".json"));
15
- const allRoutes = [];
25
+ const allRoutes = new Map();
16
26
  for (const file of jsonFiles) {
17
- const content = await fs.readFile(path.join(dir, file), "utf-8");
18
- const routeObj = JSON.parse(content);
19
- 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
+ }
20
40
  }
21
- loadedRoutes = allRoutes;
41
+ context.loadedRoutes = allRoutes;
42
+ debug(`Loaded ${allRoutes.size} route definitions from ${dir}`);
22
43
  }
23
44
  catch (error) {
24
45
  console.error("Error loading routes:", error);
25
- 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);
26
84
  }
27
- return loadedRoutes;
85
+ return savedMethod === actualMethod;
28
86
  }
29
87
  function matchRoute(routeItem, req) {
88
+ const debug = createDebug("automation_model:route:matchRoute");
30
89
  const url = new URL(req.request().url());
31
- const methodMatch = !routeItem.filters.method || routeItem.filters.method === req.request().method();
32
- const pathMatch = routeItem.filters.path === url.pathname;
33
- const queryMatch = !routeItem.filters.queryParams || routeItem.filters.queryParams.every((p) => url.searchParams.has(p));
34
- return methodMatch && pathMatch && queryMatch;
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
+ };
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
+ };
35
272
  }
36
- let debug = false;
37
- 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");
38
358
  const page = context.web.page;
39
359
  if (!page)
40
360
  throw new Error("context.web.page is missing");
41
361
  const stepTemplate = _stepNameToTemplate(stepName);
42
- const routes = await loadRoutes();
43
- const matchedRouteDefs = routes.filter((r) => r.template === stepTemplate);
44
- 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);
45
367
  if (!context.__routeState) {
46
368
  context.__routeState = { matched: [] };
47
369
  }
48
- // Pre-register all mandatory routes
49
- 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
+ }
50
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("&");
51
388
  const tracking = {
52
389
  routeItem: item,
53
- url: "",
390
+ url: `${path}${queryParams ? `?${queryParams}` : ""}`,
54
391
  completed: false,
55
392
  startedAt: Date.now(),
56
393
  actionResults: [],
57
394
  };
58
- tracking.timer = setTimeout(() => {
59
- if (!tracking.completed) {
60
- console.error(`[MANDATORY] Request to ${item.filters.path} did not complete within ${item.timeout}ms`);
61
- }
62
- }, item.timeout);
63
395
  context.__routeState.matched.push(tracking);
64
396
  }
65
397
  }
398
+ debug("New allrouteItems", JSON.stringify(allRouteItems));
399
+ let message = null;
66
400
  page.route("**/*", async (route) => {
401
+ const debug = createDebug("automation_model:route:intercept");
67
402
  const request = route.request();
68
- // print the url if debug is enabled
69
- if (debug) {
70
- console.log(`Intercepting request: ${request.method()} ${request.url()}`);
71
- }
403
+ debug(`Intercepting request: ${request.method()} ${request.url()}`);
404
+ debug("All route items", allRouteItems);
72
405
  const matchedItem = allRouteItems.find((item) => matchRoute(item, route));
73
406
  if (!matchedItem)
74
407
  return route.continue();
75
- if (debug) {
76
- console.log(`Matched route item: ${JSON.stringify(matchedItem)}`);
77
- }
78
- // Find pre-registered tracker
79
- let tracking = context.__routeState.matched.find((t) => t.routeItem === matchedItem && !t.completed);
80
- // 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;
81
413
  if (!tracking) {
414
+ debug("Tracking not found, creating tracking");
82
415
  tracking = {
83
416
  routeItem: matchedItem,
84
417
  url: request.url(),
@@ -86,119 +419,154 @@ export async function registerBeforeStepRoutes(context, stepName) {
86
419
  startedAt: Date.now(),
87
420
  actionResults: [],
88
421
  };
422
+ debug("Created tracking", tracking);
89
423
  context.__routeState.matched.push(tracking);
424
+ debug("Current route state", context.__routeState);
90
425
  }
91
426
  else {
92
427
  tracking.url = request.url();
428
+ debug("Updating tracking", tracking);
93
429
  }
94
- let response;
95
- try {
96
- response = await route.fetch();
430
+ const stubAction = matchedItem.actions.find((a) => a.type === "stub_request");
431
+ if (stubAction) {
432
+ stubActionPerformed = handleStubAction(stubAction, route, tracking);
97
433
  }
98
- catch (e) {
99
- console.error("Fetch failed for", request.url(), e);
100
- if (tracking?.timer)
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)
101
504
  clearTimeout(tracking.timer);
102
- return route.abort();
103
- }
104
- let status = response.status();
105
- let body = await response.text();
106
- let headers = response.headers();
107
- let json;
108
- try {
109
- json = JSON.parse(body);
110
- }
111
- catch (_) { }
112
- const actionResults = [];
113
- for (const action of matchedItem.actions) {
114
- let actionStatus = "success";
115
- const description = JSON.stringify(action.config);
116
- switch (action.type) {
117
- case "status_code_verification":
118
- if (status !== action.config) {
119
- console.error(`[status_code_verification] Expected ${action.config}, got ${status}`);
120
- actionStatus = "fail";
121
- }
122
- else {
123
- console.log(`[status_code_verification] Passed`);
124
- }
125
- break;
126
- case "json_modify":
127
- if (!json) {
128
- console.error(`[json_modify] Response is not JSON`);
129
- 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 });
130
510
  }
131
511
  else {
132
- for (const mod of action.config) {
133
- const pathParts = mod.path.split(".");
134
- let obj = json;
135
- for (let i = 0; i < pathParts.length - 1; i++) {
136
- obj = obj?.[pathParts[i]];
137
- if (!obj)
138
- break;
139
- }
140
- const lastKey = pathParts[pathParts.length - 1];
141
- if (obj)
142
- obj[lastKey] = mod.value;
143
- }
144
- console.log(`[json_modify] Modified JSON`);
512
+ await route.fulfill({
513
+ status: actionHandlerContext.status,
514
+ body: actionHandlerContext.finalBody,
515
+ headers,
516
+ });
145
517
  }
146
- break;
147
- case "status_code_change":
148
- status = action.config;
149
- console.log(`[status_code_change] Status changed to ${status}`);
150
- break;
151
- case "change_text":
152
- if (!headers["content-type"]?.includes("text/html")) {
153
- console.error(`[change_text] Content-Type is not text/html`);
154
- actionStatus = "fail";
155
- }
156
- else {
157
- body = action.config;
158
- console.log(`[change_text] HTML body replaced`);
159
- }
160
- break;
161
- default:
162
- console.warn(`Unknown action type: ${action.type}`);
518
+ }
519
+ catch (e) {
520
+ console.error("Failed to fulfill route:", e);
521
+ }
163
522
  }
164
- actionResults.push({ type: action.type, description, status: actionStatus });
165
523
  }
166
- tracking.completed = true;
167
- tracking.actionResults = actionResults;
168
- if (tracking.timer)
169
- clearTimeout(tracking.timer);
170
- const responseBody = json ? JSON.stringify(json) : body;
171
- await route.fulfill({ status, body: responseBody, headers });
172
524
  });
173
525
  }
174
- export async function registerAfterStepRoutes(context) {
526
+ export async function registerAfterStepRoutes(context, world) {
175
527
  const state = context.__routeState;
528
+ debug("state in afterStepRoutes", JSON.stringify(state));
176
529
  if (!state)
177
530
  return [];
178
531
  const mandatoryRoutes = state.matched.filter((tracked) => tracked.routeItem.mandatory);
532
+ debug("mandatoryRoutes in afterStepRoutes", mandatoryRoutes);
179
533
  if (mandatoryRoutes.length === 0) {
180
534
  context.__routeState = null;
181
535
  return [];
182
536
  }
183
537
  const maxTimeout = Math.max(...mandatoryRoutes.map((r) => r.routeItem.timeout));
184
538
  const startTime = Date.now();
539
+ const mandatoryRouteReached = mandatoryRoutes.map((r) => true);
540
+ debug("mandatoryRouteReached initialized to", mandatoryRouteReached);
185
541
  await new Promise((resolve) => {
186
542
  const interval = setInterval(() => {
543
+ const now = Date.now();
187
544
  const allCompleted = mandatoryRoutes.every((r) => r.completed);
188
- const elapsed = Date.now() - startTime;
189
- if (allCompleted || elapsed >= maxTimeout) {
190
- mandatoryRoutes.forEach((r) => {
191
- if (!r.completed) {
192
- console.error(`[MANDATORY] Request to ${r.routeItem.filters.path} did not complete within ${r.routeItem.timeout}ms`);
193
- }
194
- });
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);
195
558
  clearInterval(interval);
196
559
  resolve();
197
560
  }
198
561
  }, 100);
199
562
  });
200
- const results = mandatoryRoutes.map((tracked) => {
563
+ context.results = mandatoryRoutes.map((tracked) => {
201
564
  const { routeItem, url, completed, actionResults = [] } = tracked;
565
+ debug("tracked in afterStepRoutes", {
566
+ url,
567
+ completed,
568
+ actionResults,
569
+ });
202
570
  const actions = actionResults.map((ar) => {
203
571
  let status = ar.status;
204
572
  if (!completed)
@@ -207,8 +575,10 @@ export async function registerAfterStepRoutes(context) {
207
575
  type: ar.type,
208
576
  description: ar.description,
209
577
  status,
578
+ message: ar.message || null,
210
579
  };
211
580
  });
581
+ debug("actions in afterStepRoutes", actions);
212
582
  let overallStatus;
213
583
  if (!completed) {
214
584
  overallStatus = "timeout";
@@ -226,11 +596,36 @@ export async function registerAfterStepRoutes(context) {
226
596
  overallStatus,
227
597
  };
228
598
  });
599
+ try {
600
+ await context.web.page.unroute("**/*");
601
+ }
602
+ catch (e) {
603
+ console.error("Failed to unroute:", e);
604
+ }
229
605
  context.__routeState = null;
230
- context.routeResults = results;
231
- return results;
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;
232
628
  }
233
- // Helper functions
234
629
  const toCucumberExpression = (text) => text.replaceAll("/", "\\\\/").replaceAll("(", "\\\\(").replaceAll("{", "\\\\{");
235
630
  function extractQuotedText(inputString) {
236
631
  const regex = /("[^"]*")/g;
@@ -245,7 +640,7 @@ function extractQuotedText(inputString) {
245
640
  }
246
641
  return matches;
247
642
  }
248
- function _stepNameToTemplate(stepName) {
643
+ export function _stepNameToTemplate(stepName) {
249
644
  if (stepName.includes("{string}")) {
250
645
  return stepName;
251
646
  }
@@ -265,4 +660,23 @@ async function folderExists(path) {
265
660
  return false;
266
661
  }
267
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
+ };
268
682
  //# sourceMappingURL=route.js.map