@dev-blinq/cucumber_client 1.0.1226-dev → 1.0.1226-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 (39) hide show
  1. package/bin/assets/bundled_scripts/recorder.js +220 -0
  2. package/bin/assets/preload/recorderv3.js +68 -5
  3. package/bin/assets/preload/unique_locators.js +1 -1
  4. package/bin/assets/scripts/aria_snapshot.js +235 -0
  5. package/bin/assets/scripts/dom_attr.js +372 -0
  6. package/bin/assets/scripts/dom_element.js +0 -0
  7. package/bin/assets/scripts/dom_parent.js +185 -0
  8. package/bin/assets/scripts/event_utils.js +105 -0
  9. package/bin/assets/scripts/pw.js +7886 -0
  10. package/bin/assets/scripts/recorder.js +1147 -0
  11. package/bin/assets/scripts/snapshot_capturer.js +155 -0
  12. package/bin/assets/scripts/unique_locators.js +841 -0
  13. package/bin/assets/scripts/yaml.js +4770 -0
  14. package/bin/assets/templates/_hooks_template.txt +37 -0
  15. package/bin/assets/templates/page_template.txt +2 -16
  16. package/bin/assets/templates/utils_template.txt +48 -63
  17. package/bin/client/apiTest/apiTest.js +6 -0
  18. package/bin/client/cli_helpers.js +11 -13
  19. package/bin/client/code_cleanup/utils.js +37 -14
  20. package/bin/client/code_gen/code_inversion.js +68 -10
  21. package/bin/client/code_gen/page_reflection.js +12 -15
  22. package/bin/client/code_gen/playwright_codeget.js +163 -33
  23. package/bin/client/cucumber/feature.js +85 -27
  24. package/bin/client/cucumber/steps_definitions.js +84 -76
  25. package/bin/client/local_agent.js +3 -3
  26. package/bin/client/project.js +7 -1
  27. package/bin/client/recorderv3/bvt_recorder.js +267 -80
  28. package/bin/client/recorderv3/implemented_steps.js +74 -12
  29. package/bin/client/recorderv3/index.js +58 -8
  30. package/bin/client/recorderv3/network.js +299 -0
  31. package/bin/client/recorderv3/step_runner.js +319 -67
  32. package/bin/client/recorderv3/step_utils.js +145 -4
  33. package/bin/client/recorderv3/update_feature.js +58 -30
  34. package/bin/client/recording.js +7 -0
  35. package/bin/client/run_cucumber.js +5 -1
  36. package/bin/client/scenario_report.js +4 -8
  37. package/bin/client/test_scenario.js +0 -1
  38. package/bin/index.js +1 -0
  39. package/package.json +15 -8
@@ -1,6 +1,5 @@
1
- import { existsSync, mkdirSync, writeFileSync } from "fs";
1
+ import { existsSync, mkdirSync, rmSync, writeFileSync } from "fs";
2
2
  import path from "path";
3
- import fs from "fs";
4
3
  import { generatePageName } from "../code_gen/playwright_codeget.js";
5
4
  import {
6
5
  executeStep,
@@ -9,43 +8,69 @@ import {
9
8
  getUtilsCodePage,
10
9
  loadStepDefinitions,
11
10
  saveRecording,
11
+ saveRoutes,
12
12
  } from "./step_utils.js";
13
13
  import { escapeString, getExamplesContent } from "./update_feature.js";
14
+ import fs from "fs";
14
15
  import { locateDefinitionPath } from "../cucumber/steps_definitions.js";
16
+ import { tmpdir } from "os";
15
17
 
16
- // let copiedCodeToTemp = false;
17
- async function withAbort(fn, signal) {
18
- if (!signal) {
19
- return await fn();
20
- }
21
-
22
- const abortPromise = new Promise((_, reject) => {
23
- signal.addEventListener("abort", () => reject(new Error("Aborted")), { once: true });
24
- });
25
-
26
- return await Promise.race([fn(), abortPromise]);
27
- }
28
18
  export class BVTStepRunner {
29
- #currentStepController;
19
+ #currentStepController = null;
30
20
  #port;
31
- constructor({ projectDir }) {
21
+ #lastAttemptedCmdId = null;
22
+
23
+ constructor({ projectDir, sendExecutionStatus, bvtContext }) {
32
24
  this.projectDir = projectDir;
25
+ this.sendExecutionStatus = sendExecutionStatus;
26
+ this.bvtContext = bvtContext;
27
+ this.liveExecutionMap = new Map();
33
28
  }
29
+
34
30
  setRemoteDebugPort(port) {
35
31
  this.#port = port;
36
32
  }
33
+
34
+ // Abort the current cucumber step execution by signaling the wrapper
37
35
  async abortExecution() {
36
+ if (this.bvtContext.web.pausedCmd) {
37
+ this.bvtContext.web.pausedCmd = null;
38
+ }
39
+ this.liveExecutionMap.clear();
38
40
  if (this.#currentStepController) {
39
41
  this.#currentStepController.abort();
40
42
  }
41
43
  }
42
44
 
45
+ async pauseExecution(cmdId) {
46
+ if (this.bvtContext.web) {
47
+ this.bvtContext.web.pausedCmd = this.liveExecutionMap.get(cmdId);
48
+ }
49
+ }
50
+
51
+ async resumeExecution(cmdId) {
52
+ if (this.bvtContext.web.pausedCmd) {
53
+ const { resolve } = this.bvtContext.web.pausedCmd;
54
+ if (resolve) {
55
+ resolve();
56
+ }
57
+ this.bvtContext.web.pausedCmd = null;
58
+ } else {
59
+ if (this.liveExecutionMap.has(cmdId)) {
60
+ const { resolve } = this.liveExecutionMap.get(cmdId);
61
+ if (resolve) {
62
+ resolve();
63
+ }
64
+ } else {
65
+ console.warn(`No paused command found for cmdId: ${cmdId}`);
66
+ }
67
+ }
68
+ }
69
+
43
70
  async copyCodetoTempFolder({ step, parametersMap, tempFolderPath }) {
44
- // const tempFolderPath = path.join(this.projectDir, "__temp_features");
45
71
  if (!fs.existsSync(tempFolderPath)) {
46
72
  fs.mkdirSync(tempFolderPath);
47
73
  }
48
- //copy all files from "./features" "./temp" folder
49
74
  if (fs.existsSync(tempFolderPath)) {
50
75
  fs.rmSync(tempFolderPath, { recursive: true });
51
76
  }
@@ -53,14 +78,13 @@ export class BVTStepRunner {
53
78
  overwrite: true,
54
79
  recursive: true,
55
80
  });
56
- // copiedCodeToTemp = true;
57
81
  }
58
82
 
59
- async writeTempFeatureFile({ step, parametersMap, tempFolderPath }) {
83
+ async writeTempFeatureFile({ step, parametersMap, tempFolderPath, tags }) {
60
84
  const tFilePath = path.join(tempFolderPath, "__temp.feature");
61
- // console.log(tFilePath);
62
85
  let tFileContent = `# temp feature file
63
86
  Feature: Temp feature
87
+ ${tags ? tags.join(" ") : ""}
64
88
  Scenario Outline: Temp Scenario
65
89
  Given ${escapeString(step.text)}
66
90
  `;
@@ -68,67 +92,295 @@ export class BVTStepRunner {
68
92
  writeFileSync(tFilePath, tFileContent);
69
93
  return tFilePath;
70
94
  }
71
- async runStep({ step, parametersMap, envPath }, bvtContext, options) {
72
- let codePage; // = getCodePage();
73
- // const tempFolderPath = process.env.tempFeaturesFolderPath;
74
- const __temp_features_FolderName = "__temp_features" + Math.random().toString(36).substring(2, 7);
75
- const tempFolderPath = path.join(this.projectDir, __temp_features_FolderName);
76
- process.env.tempFeaturesFolderPath = __temp_features_FolderName;
77
- process.env.TESTCASE_REPORT_FOLDER_PATH = tempFolderPath;
78
- // if (!copiedCodeToTemp) {
79
- // await this.copyCodetoTempFolder({ step, parametersMap, tempFolderPath });
80
- // }
81
- await this.copyCodetoTempFolder({ step, parametersMap, tempFolderPath });
82
- // console.log({ feature_file_path });
83
- let stepsDefinitions = loadStepDefinitions(this.projectDir, false, true);
84
- // console.log({ stepsDefinitions });
85
- const cucumberStep = getCucumberStep({ step });
86
- if (cucumberStep.parameters && Array.isArray(cucumberStep.parameters)) {
87
- cucumberStep.parameters.forEach((param) => {
88
- if (param.variableName) {
89
- param.callValue = parametersMap[param.variableName];
90
- }
95
+
96
+ // Generate wrapper code that integrates AbortSignal into cucumber step definitions
97
+ generateWrapperCode() {
98
+ return `
99
+ import {setDefinitionFunctionWrapper} from "@dev-blinq/cucumber-js";
100
+
101
+ setDefinitionFunctionWrapper((fn) => {
102
+ return async function (...args) {
103
+ const signal = global.__BVT_STEP_ABORT_SIGNAL;
104
+ if (signal) {
105
+ signal.throwIfAborted?.();
106
+ const abortHandler = () => {
107
+ throw new Error("Aborted");
108
+ };
109
+ signal.addEventListener("abort", abortHandler, { once: true });
110
+ try {
111
+ return await fn.apply(this, args);
112
+ } finally {
113
+ signal.removeEventListener("abort", abortHandler);
114
+ }
115
+ }
116
+ return await fn.apply(this, args);
117
+ };
118
+ });
119
+
120
+ `;
121
+ }
122
+
123
+ // Write the wrapper code to temp folder
124
+ async writeWrapperCode(tempFolderPath, abortSignal) {
125
+ // tempFolderPath/step_definitions/utils.mjs -> Make a file name that follows this file but always before the next file
126
+ let fileName = "utils" + Math.random().toString(36).substring(2, 7) + ".mjs";
127
+ while (existsSync(path.join(tempFolderPath, "step_definitions", fileName))) {
128
+ fileName = "utils" + Math.random().toString(36).substring(2, 7) + ".mjs";
129
+ }
130
+ const wrapperCode = this.generateWrapperCode();
131
+
132
+ // Ensure directory exists
133
+ const stepDefinitionFolderPath = path.join(tempFolderPath, "step_definitions");
134
+ if (!existsSync(stepDefinitionFolderPath)) {
135
+ mkdirSync(stepDefinitionFolderPath, { recursive: true });
136
+ }
137
+
138
+ writeFileSync(path.join(stepDefinitionFolderPath, fileName), wrapperCode);
139
+
140
+ // Set the abort signal globally so the wrapper can access it
141
+ global.__BVT_STEP_ABORT_SIGNAL = abortSignal;
142
+
143
+ return path.join(stepDefinitionFolderPath, fileName);
144
+ }
145
+
146
+ // Execute cucumber step - simplified without abort signal handling at this level
147
+ async executeStepWithAbort({ feature_file_path, scenario, tempFolderPath, stepText, config }, options) {
148
+ const { skipAfter = true, skipBefore = true } = options || {};
149
+
150
+ const environment = { ...process.env };
151
+ const { loadConfiguration, loadSupport, runCucumber } = await import("@dev-blinq/cucumber-js/api");
152
+
153
+ const { runConfiguration } = await loadConfiguration(
154
+ {
155
+ provided: {
156
+ name: [scenario],
157
+ paths: [feature_file_path],
158
+ import: [path.join(tempFolderPath, "step_definitions", "**", "*.mjs")],
159
+ },
160
+ },
161
+ { cwd: process.cwd(), env: environment }
162
+ );
163
+
164
+ const support = await loadSupport(runConfiguration, { cwd: process.cwd(), env: environment });
165
+
166
+ support.afterTestRunHookDefinitions = [];
167
+ if (skipAfter) {
168
+ support.afterTestCaseHookDefinitions = [];
169
+ }
170
+ if (skipBefore && !config.legacySyntax) {
171
+ support.beforeTestCaseHookDefinitions = support.beforeTestCaseHookDefinitions.filter((hook) => {
172
+ return hook.uri.endsWith("utils.mjs");
91
173
  });
92
174
  }
175
+ support.beforeTestRunHookDefinitions = [];
93
176
 
94
- if (!step.isImplemented && step.commands.length > 0) {
95
- const pageName = generatePageName(step.startFrame?.url ?? "default");
96
- const stepDefinitionFolderPath = path.join(tempFolderPath, "step_definitions");
97
- if (!existsSync(stepDefinitionFolderPath)) {
98
- mkdirSync(stepDefinitionFolderPath, { recursive: true });
99
- }
100
- const stepDefsFilePath = locateDefinitionPath(tempFolderPath, pageName);
101
- //path.join(stepDefinitionFolderPath, pageName + "_page.mjs");
102
- codePage = getCodePage(stepDefsFilePath);
103
- codePage = await saveRecording({ step, cucumberStep, codePage, projectDir: this.projectDir, stepsDefinitions });
104
- if (codePage) {
105
- await codePage.save(stepDefsFilePath);
106
- // console.log("saved code page: ", stepDefsFilePath);
177
+ let errorMessage = null;
178
+ let info = null;
179
+ let errInfo = null;
180
+
181
+ const result = await runCucumber({ ...runConfiguration, support }, environment, (message) => {
182
+ if (message.testStepFinished) {
183
+ const { testStepFinished } = message;
184
+ const { testStepResult } = testStepFinished;
185
+ if (testStepResult.status === "FAILED" || testStepResult.status === "AMBIGUOUS") {
186
+ if (!errorMessage) {
187
+ errorMessage = testStepResult.message;
188
+ if (info) {
189
+ errInfo = info;
190
+ }
191
+ }
192
+ }
193
+ if (testStepResult.status === "UNDEFINED") {
194
+ if (!errorMessage) {
195
+ errorMessage = `step ${JSON.stringify(stepText)} is ${testStepResult.status}`;
196
+ if (info) {
197
+ errInfo = info;
198
+ }
199
+ }
200
+ }
107
201
  }
108
- if (!codePage) {
109
- codePage = getUtilsCodePage(this.projectDir);
202
+ if (message.attachment) {
203
+ const attachment = message.attachment;
204
+ if (attachment.mediaType === "application/json" && attachment.body) {
205
+ const body = JSON.parse(attachment.body);
206
+ info = body.info;
207
+ const result = body.result;
208
+
209
+ if (result.status === "PASSED") {
210
+ this.sendExecutionStatus({
211
+ type: "cmdExecutionSuccess",
212
+ cmdId: body.cmdId,
213
+ selectedStrategy: info?.selectedStrategy,
214
+ });
215
+ } else {
216
+ this.sendExecutionStatus({
217
+ type: "cmdExecutionError",
218
+ cmdId: body.cmdId,
219
+ error: {
220
+ message: result.message,
221
+ info,
222
+ },
223
+ });
224
+ }
225
+ } else if (attachment.mediaType === "application/json+intercept-results" && attachment.body) {
226
+ const body = JSON.parse(attachment.body);
227
+ if (body) {
228
+ this.sendExecutionStatus({
229
+ type: "interceptResults",
230
+ interceptResults: body,
231
+ });
232
+ }
233
+ }
110
234
  }
235
+ });
236
+
237
+ if (errorMessage) {
238
+ const bvtError = new Error(errorMessage);
239
+ Object.assign(bvtError, { info: errInfo });
240
+ throw bvtError;
111
241
  }
112
- const feature_file_path = await this.writeTempFeatureFile({ step, parametersMap, tempFolderPath });
113
- // console.log({ feature_file_path, step_text: step.text });
114
242
 
115
- const stepExecController = new AbortController();
116
- this.#currentStepController = stepExecController;
117
- await withAbort(async () => {
118
- await stepsDefinitions.executeStepRemote(
243
+ return { result, info };
244
+ }
245
+
246
+ async runStep({ step, parametersMap, envPath, tags, config }, bvtContext, options) {
247
+ // Create a new AbortController for this specific step execution
248
+ this.#currentStepController = new AbortController();
249
+ const { signal } = this.#currentStepController;
250
+
251
+ try {
252
+ this.#lastAttemptedCmdId = null;
253
+ let cmdIDs = (step.commands || []).map((cmd) => cmd.cmdId);
254
+ bvtContext.web.pausedCmd = null;
255
+
256
+ // Clear the liveExecutionMap and set up new entries for this step
257
+ this.liveExecutionMap.clear();
258
+
259
+ for (const cmdId of cmdIDs) {
260
+ this.liveExecutionMap.set(cmdId, {
261
+ resolve: () => {},
262
+ reject: () => {},
263
+ });
264
+ }
265
+
266
+ if (bvtContext.web) {
267
+ bvtContext.web.getCmdId = () => {
268
+ if (cmdIDs.length === 0) {
269
+ cmdIDs = (step.commands || []).map((cmd) => cmd.cmdId);
270
+ }
271
+ const cId = cmdIDs.shift();
272
+ this.sendExecutionStatus({
273
+ type: "cmdExecutionStart",
274
+ cmdId: cId,
275
+ });
276
+ this.#lastAttemptedCmdId = cId;
277
+ return cId;
278
+ };
279
+ }
280
+
281
+ const __temp_features_FolderName = "__temp_features" + Math.random().toString(36).substring(2, 7);
282
+ const tempFolderPath = path.join(this.projectDir, __temp_features_FolderName);
283
+ process.env.tempFeaturesFolderPath = __temp_features_FolderName;
284
+ process.env.TESTCASE_REPORT_FOLDER_PATH = tempFolderPath;
285
+
286
+ await this.copyCodetoTempFolder({ step, parametersMap, tempFolderPath });
287
+
288
+ // Write abort wrapper code with this step's signal
289
+ await this.writeWrapperCode(tempFolderPath, signal);
290
+
291
+ let stepsDefinitions = loadStepDefinitions(this.projectDir, false, true);
292
+ const cucumberStep = getCucumberStep({ step });
293
+
294
+ if (cucumberStep.parameters && Array.isArray(cucumberStep.parameters)) {
295
+ cucumberStep.parameters.forEach((param) => {
296
+ if (param.variableName) {
297
+ param.callValue = parametersMap[param.variableName];
298
+ }
299
+ });
300
+ }
301
+
302
+ if (!step.isImplemented && step.commands.length > 0) {
303
+ const pageName = generatePageName(step.startFrame?.url ?? "default");
304
+ const stepDefinitionFolderPath = path.join(tempFolderPath, "step_definitions");
305
+ if (!existsSync(stepDefinitionFolderPath)) {
306
+ mkdirSync(stepDefinitionFolderPath, { recursive: true });
307
+ }
308
+ const stepDefsFilePath = locateDefinitionPath(tempFolderPath, pageName);
309
+ let codePage = getCodePage(stepDefsFilePath);
310
+ codePage = await saveRecording({
311
+ step,
312
+ cucumberStep,
313
+ codePage,
314
+ projectDir: this.projectDir,
315
+ stepsDefinitions,
316
+ });
317
+ if (codePage) {
318
+ await codePage.save(stepDefsFilePath);
319
+ }
320
+ if (!codePage) {
321
+ codePage = getUtilsCodePage(this.projectDir);
322
+ }
323
+ } else {
324
+ let routesPath = path.join(tmpdir(), `blinq_temp_routes`);
325
+ if (process.env.TEMP_RUN === "true") {
326
+ if (existsSync(routesPath)) {
327
+ rmSync(routesPath, { recursive: true });
328
+ }
329
+ mkdirSync(routesPath, { recursive: true });
330
+ saveRoutes({ step, folderPath: routesPath });
331
+ } else {
332
+ if (existsSync(routesPath)) {
333
+ try {
334
+ rmSync(routesPath, { recursive: true });
335
+ } catch (error) {
336
+ console.error("Error removing temp_routes folder", error);
337
+ }
338
+ }
339
+ routesPath = path.join(this.projectDir, "data", "routes");
340
+ if (!existsSync(routesPath)) {
341
+ mkdirSync(routesPath, { recursive: true });
342
+ }
343
+ saveRoutes({ step, folderPath: routesPath });
344
+ }
345
+ }
346
+
347
+ const feature_file_path = await this.writeTempFeatureFile({
348
+ step,
349
+ parametersMap,
350
+ tempFolderPath,
351
+ tags,
352
+ });
353
+
354
+ // Execute the cucumber step - if wrapper throws "Aborted", it will propagate up
355
+ const { result, info } = await this.executeStepWithAbort(
119
356
  {
120
357
  feature_file_path,
121
358
  tempFolderPath,
122
359
  stepText: step.text,
123
360
  scenario: "Temp Scenario",
361
+ config,
124
362
  },
125
363
  options
126
364
  );
127
- }, stepExecController.signal).finally(() => {
128
- // rm temp folder
129
- if (fs.existsSync(tempFolderPath)) {
130
- fs.rmSync(tempFolderPath, { recursive: true });
365
+
366
+ return { result, info };
367
+ } catch (error) {
368
+ if (error.message && error.message.includes("Aborted")) {
369
+ throw new Error("Aborted");
370
+ } else throw error;
371
+ } finally {
372
+ // Clean up this step's controller and global reference
373
+ this.#currentStepController = null;
374
+ global.__BVT_STEP_ABORT_SIGNAL = null;
375
+
376
+ // Clean up temp folder
377
+ const __temp_features_FolderName = process.env.tempFeaturesFolderPath;
378
+ if (__temp_features_FolderName) {
379
+ const tempFolderPath = path.join(this.projectDir, __temp_features_FolderName);
380
+ if (fs.existsSync(tempFolderPath)) {
381
+ fs.rmSync(tempFolderPath, { recursive: true });
382
+ }
131
383
  }
132
- });
384
+ }
133
385
  }
134
386
  }
@@ -1,4 +1,4 @@
1
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
1
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
2
2
  import path from "path";
3
3
  import url from "url";
4
4
  import logger from "../../logger.js";
@@ -9,6 +9,8 @@ import { Step } from "../cucumber/feature.js";
9
9
  import { locateDefinitionPath, StepsDefinitions } from "../cucumber/steps_definitions.js";
10
10
  import { Recording } from "../recording.js";
11
11
  import { generateApiCode } from "../code_gen/api_codegen.js";
12
+ import { tmpdir } from "os";
13
+ import { createHash } from "crypto";
12
14
 
13
15
  const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
14
16
 
@@ -69,19 +71,74 @@ function makeStepTextUnique(step, stepsDefinitions) {
69
71
  }
70
72
 
71
73
  export async function saveRecording({ step, cucumberStep, codePage, projectDir, stepsDefinitions }) {
72
- // console.log("saveRecording", step.text);
74
+ let routesPath = path.join(tmpdir(), "blinq_temp_routes");
75
+
76
+ if (process.env.TEMP_RUN) {
77
+ if (existsSync(routesPath)) {
78
+ rmSync(routesPath, { recursive: true });
79
+ }
80
+ mkdirSync(routesPath, { recursive: true });
81
+ saveRoutes({ step, folderPath: routesPath });
82
+ } else {
83
+ if (existsSync(routesPath)) {
84
+ // remove the folder
85
+ try {
86
+ rmSync(routesPath, { recursive: true });
87
+ console.log("Removed temp_routes_folder:", routesPath);
88
+ } catch (error) {
89
+ console.error("Error removing temp_routes folder", error);
90
+ }
91
+ routesPath = path.join(projectDir, "data", "routes");
92
+ if (!existsSync(routesPath)) {
93
+ mkdirSync(routesPath, { recursive: true });
94
+ }
95
+ saveRoutes({ step, folderPath: routesPath });
96
+ }
97
+ }
98
+
73
99
  if (step.isImplementedWhileRecording && !process.env.TEMP_RUN) {
74
100
  return;
75
101
  }
102
+
76
103
  if (step.isImplemented && step.shouldOverride) {
77
104
  let stepDef = stepsDefinitions.findMatchingStep(step.text);
78
105
  codePage = getCodePage(stepDef.file);
79
106
  } else {
80
107
  const isUtilStep = makeStepTextUnique(step, stepsDefinitions);
108
+
81
109
  if (isUtilStep) {
82
110
  return;
83
111
  }
84
112
  }
113
+
114
+ if (process.env.TEMP_RUN === "true") {
115
+ console.log("Save routes in temp folder for running:", routesPath);
116
+ if (existsSync(routesPath)) {
117
+ console.log("Removing existing temp_routes_folder:", routesPath);
118
+ rmSync(routesPath, { recursive: true });
119
+ }
120
+ mkdirSync(routesPath, { recursive: true });
121
+ console.log("Created temp_routes_folder:", routesPath);
122
+ saveRoutes({ step, folderPath: routesPath });
123
+ } else {
124
+ console.log("Saving routes in project directory:", projectDir);
125
+ if (existsSync(routesPath)) {
126
+ // remove the folder
127
+ try {
128
+ rmSync(routesPath, { recursive: true });
129
+ console.log("Removed temp_routes_folder:", routesPath);
130
+ } catch (error) {
131
+ console.error("Error removing temp_routes folder", error);
132
+ }
133
+ }
134
+ routesPath = path.join(projectDir, "data", "routes");
135
+ console.log("Saving routes to:", routesPath);
136
+ if (!existsSync(routesPath)) {
137
+ mkdirSync(routesPath, { recursive: true });
138
+ }
139
+ saveRoutes({ step, folderPath: routesPath });
140
+ }
141
+
85
142
  cucumberStep.text = step.text;
86
143
  const recording = new Recording();
87
144
  const steps = step.commands;
@@ -108,6 +165,7 @@ export async function saveRecording({ step, cucumberStep, codePage, projectDir,
108
165
  isStaticToken,
109
166
  status,
110
167
  } = step.commands[0].value;
168
+
111
169
  const result = await generateApiCode(
112
170
  {
113
171
  url,
@@ -132,6 +190,7 @@ export async function saveRecording({ step, cucumberStep, codePage, projectDir,
132
190
  step.keyword,
133
191
  stepsDefinitions
134
192
  );
193
+
135
194
  if (!step.isImplemented) {
136
195
  stepsDefinitions.addStep({
137
196
  name: step.text,
@@ -139,6 +198,7 @@ export async function saveRecording({ step, cucumberStep, codePage, projectDir,
139
198
  source: "recorder",
140
199
  });
141
200
  }
201
+
142
202
  cucumberStep.methodName = result.methodName;
143
203
  return result.codePage;
144
204
  } else {
@@ -166,7 +226,13 @@ export async function saveRecording({ step, cucumberStep, codePage, projectDir,
166
226
  path
167
227
  );
168
228
  const keyword = (cucumberStep.keywordAlias ?? cucumberStep.keyword).trim();
169
- const stepResult = codePage.addCucumberStep(keyword, cucumberStep.getTemplate(), methodName, steps.length);
229
+ const stepResult = codePage.addCucumberStep(
230
+ keyword,
231
+ cucumberStep.getTemplate(),
232
+ methodName,
233
+ steps.length,
234
+ step.finalTimeout
235
+ );
170
236
 
171
237
  if (!step.isImplemented) {
172
238
  stepsDefinitions.addStep({
@@ -306,6 +372,12 @@ export async function updateStepDefinitions({ scenario, featureName, projectDir
306
372
  const utilsTemplateFilePath = path.join(__dirname, "../../assets", "templates", "utils_template.txt");
307
373
  const utilsContent = readFileSync(utilsTemplateFilePath, "utf8");
308
374
  writeFileSync(utilsFilePath, utilsContent, "utf8");
375
+ const hooksTemplateFilePath = path.join(__dirname, "../../assets", "templates", "_hooks_template.txt");
376
+ if (existsSync(hooksTemplateFilePath)) {
377
+ const hooksFilePath = path.join(stepDefinitionFolderPath, "_hooks.mjs");
378
+ const hooksContent = readFileSync(hooksTemplateFilePath, "utf8");
379
+ writeFileSync(hooksFilePath, hooksContent, "utf8");
380
+ }
309
381
  const steps = scenario.steps;
310
382
 
311
383
  const stepsDefinitions = new StepsDefinitions(projectDir);
@@ -321,6 +393,34 @@ export async function updateStepDefinitions({ scenario, featureName, projectDir
321
393
  }
322
394
  }
323
395
  if ((step.isImplemented && !step.shouldOverride) || step.commands.length === 0) {
396
+ let routesPath = path.join(tmpdir(), `blinq_temp_routes`);
397
+ if (process.env.TEMP_RUN === "true") {
398
+ console.log("Save routes in temp folder for running:", routesPath);
399
+ if (existsSync(routesPath)) {
400
+ console.log("Removing existing temp_routes_folder:", routesPath);
401
+ rmSync(routesPath, { recursive: true });
402
+ }
403
+ mkdirSync(routesPath, { recursive: true });
404
+ console.log("Created temp_routes_folder:", routesPath);
405
+ saveRoutes({ step, folderPath: routesPath });
406
+ } else {
407
+ console.log("Saving routes in project directory:", projectDir);
408
+ if (existsSync(routesPath)) {
409
+ // remove the folder
410
+ try {
411
+ rmSync(routesPath, { recursive: true });
412
+ console.log("Removed temp_routes_folder:", routesPath);
413
+ } catch (error) {
414
+ console.error("Error removing temp_routes folder", error);
415
+ }
416
+ }
417
+ routesPath = path.join(projectDir, "data", "routes");
418
+ console.log("Saving routes to:", routesPath);
419
+ if (!existsSync(routesPath)) {
420
+ mkdirSync(routesPath, { recursive: true });
421
+ }
422
+ saveRoutes({ step, folderPath: routesPath });
423
+ }
324
424
  continue;
325
425
  }
326
426
  const cucumberStep = getCucumberStep({ step });
@@ -328,7 +428,6 @@ export async function updateStepDefinitions({ scenario, featureName, projectDir
328
428
  const stepDefsFilePath = locateDefinitionPath(featureFolder, pageName);
329
429
  // path.join(stepDefinitionFolderPath, pageName + "_page.mjs");
330
430
  let codePage = getCodePage(stepDefsFilePath);
331
-
332
431
  codePage = await saveRecording({ step, cucumberStep, codePage, projectDir, stepsDefinitions });
333
432
  if (!codePage) {
334
433
  continue;
@@ -340,3 +439,45 @@ export async function updateStepDefinitions({ scenario, featureName, projectDir
340
439
  }
341
440
  writeFileSync(utilsFilePath, utilsContent, "utf8");
342
441
  }
442
+
443
+ export function saveRoutes({ step, folderPath }) {
444
+ const routeItems = step.routeItems;
445
+ if (!routeItems || routeItems.length === 0) {
446
+ return;
447
+ }
448
+ const cucumberStep = getCucumberStep({ step });
449
+ const template = cucumberStep.getTemplate();
450
+ const stepNameHash = createHash("sha256").update(template).digest("hex");
451
+ console.log("Saving routes for step:", step.text, "with hash:", stepNameHash);
452
+
453
+ const routeItemsWithFilters = routeItems.map((routeItem) => {
454
+ const oldFilters = routeItem.filters;
455
+ const queryParamsObject = {};
456
+ oldFilters.queryParams.forEach((queryParam) => {
457
+ queryParamsObject[queryParam.key] = queryParam.value;
458
+ });
459
+ const newFilters = { path: oldFilters.path, method: oldFilters.method, queryParams: queryParamsObject };
460
+ return {
461
+ ...routeItem,
462
+ filters: newFilters,
463
+ };
464
+ });
465
+
466
+ const routesFilePath = path.join(folderPath, stepNameHash + ".json");
467
+ console.log("Routes file path:", routesFilePath);
468
+ const routesData = {
469
+ template,
470
+ routes: routeItemsWithFilters,
471
+ };
472
+ console.log("Routes data to save:", routesData);
473
+
474
+ if (!existsSync(folderPath)) {
475
+ mkdirSync(folderPath, { recursive: true });
476
+ }
477
+ try {
478
+ writeFileSync(routesFilePath, JSON.stringify(routesData, null, 2), "utf8");
479
+ console.log("Saved routes to", routesFilePath);
480
+ } catch (error) {
481
+ console.error("Failed to save routes to", routesFilePath, "Error:", error);
482
+ }
483
+ }