@dev-blinq/cucumber_client 1.0.1185-dev → 1.0.1185-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 (47) hide show
  1. package/bin/assets/bundled_scripts/recorder.js +220 -0
  2. package/bin/assets/preload/accessibility.js +1 -1
  3. package/bin/assets/preload/find_context.js +1 -1
  4. package/bin/assets/preload/generateSelector.js +24 -0
  5. package/bin/assets/preload/locators.js +18 -0
  6. package/bin/assets/preload/recorderv3.js +80 -9
  7. package/bin/assets/preload/unique_locators.js +24 -3
  8. package/bin/assets/scripts/aria_snapshot.js +235 -0
  9. package/bin/assets/scripts/dom_attr.js +372 -0
  10. package/bin/assets/scripts/dom_element.js +0 -0
  11. package/bin/assets/scripts/dom_parent.js +185 -0
  12. package/bin/assets/scripts/event_utils.js +105 -0
  13. package/bin/assets/scripts/pw.js +7886 -0
  14. package/bin/assets/scripts/recorder.js +1147 -0
  15. package/bin/assets/scripts/snapshot_capturer.js +155 -0
  16. package/bin/assets/scripts/unique_locators.js +844 -0
  17. package/bin/assets/scripts/yaml.js +4770 -0
  18. package/bin/assets/templates/page_template.txt +2 -16
  19. package/bin/assets/templates/utils_template.txt +65 -12
  20. package/bin/client/cli_helpers.js +0 -1
  21. package/bin/client/code_cleanup/utils.js +43 -14
  22. package/bin/client/code_gen/code_inversion.js +44 -12
  23. package/bin/client/code_gen/index.js +3 -0
  24. package/bin/client/code_gen/page_reflection.js +37 -20
  25. package/bin/client/code_gen/playwright_codeget.js +149 -43
  26. package/bin/client/cucumber/feature.js +96 -42
  27. package/bin/client/cucumber/project_to_document.js +8 -7
  28. package/bin/client/cucumber/steps_definitions.js +49 -16
  29. package/bin/client/local_agent.js +9 -7
  30. package/bin/client/operations/dump_tree.js +159 -5
  31. package/bin/client/playground/playground.js +1 -1
  32. package/bin/client/project.js +6 -2
  33. package/bin/client/recorderv3/bvt_recorder.js +279 -81
  34. package/bin/client/recorderv3/cli.js +1 -0
  35. package/bin/client/recorderv3/implemented_steps.js +111 -11
  36. package/bin/client/recorderv3/index.js +48 -4
  37. package/bin/client/recorderv3/network.js +299 -0
  38. package/bin/client/recorderv3/step_runner.js +183 -13
  39. package/bin/client/recorderv3/step_utils.js +159 -14
  40. package/bin/client/recorderv3/update_feature.js +53 -28
  41. package/bin/client/recording.js +8 -0
  42. package/bin/client/run_cucumber.js +16 -2
  43. package/bin/client/scenario_report.js +112 -55
  44. package/bin/client/test_scenario.js +0 -1
  45. package/bin/index.js +1 -0
  46. package/package.json +15 -8
  47. package/bin/client/code_gen/get_implemented_steps.js +0 -27
@@ -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,26 +8,36 @@ 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";
15
+ import { locateDefinitionPath } from "../cucumber/steps_definitions.js";
16
+ import { tmpdir } from "os";
14
17
 
15
18
  // let copiedCodeToTemp = false;
16
19
  async function withAbort(fn, signal) {
17
20
  if (!signal) {
18
21
  return await fn();
19
22
  }
23
+ return new Promise((resolve, reject) => {
24
+ const abortHandler = () => reject(new Error("Aborted"));
25
+ signal.addEventListener("abort", abortHandler, { once: true });
20
26
 
21
- const abortPromise = new Promise((_, reject) => {
22
- signal.addEventListener("abort", () => reject(new Error("Aborted")), { once: true });
27
+ fn()
28
+ .then(resolve)
29
+ .catch(reject)
30
+ .finally(() => {
31
+ signal.removeEventListener("abort", abortHandler);
32
+ });
23
33
  });
24
-
25
- return await Promise.race([fn(), abortPromise]);
26
34
  }
27
35
  export class BVTStepRunner {
28
36
  #currentStepController;
29
37
  #port;
30
- constructor({ projectDir }) {
38
+ constructor({ projectDir, sendExecutionStatus }) {
31
39
  this.projectDir = projectDir;
40
+ this.sendExecutionStatus = sendExecutionStatus;
32
41
  }
33
42
  setRemoteDebugPort(port) {
34
43
  this.#port = port;
@@ -55,11 +64,12 @@ export class BVTStepRunner {
55
64
  // copiedCodeToTemp = true;
56
65
  }
57
66
 
58
- async writeTempFeatureFile({ step, parametersMap, tempFolderPath }) {
67
+ async writeTempFeatureFile({ step, parametersMap, tempFolderPath, tags }) {
59
68
  const tFilePath = path.join(tempFolderPath, "__temp.feature");
60
69
  // console.log(tFilePath);
61
70
  let tFileContent = `# temp feature file
62
71
  Feature: Temp feature
72
+ ${tags ? tags.join(" ") : ""}
63
73
  Scenario Outline: Temp Scenario
64
74
  Given ${escapeString(step.text)}
65
75
  `;
@@ -67,7 +77,135 @@ export class BVTStepRunner {
67
77
  writeFileSync(tFilePath, tFileContent);
68
78
  return tFilePath;
69
79
  }
70
- async runStep({ step, parametersMap, envPath }, bvtContext, options) {
80
+
81
+ executeStepRemote = async ({ feature_file_path, scenario, tempFolderPath, stepText, config }, options) => {
82
+ const { skipAfter = true, skipBefore = true } = options || {};
83
+ const environment = {
84
+ ...process.env,
85
+ };
86
+
87
+ try {
88
+ const { loadConfiguration, loadSupport, runCucumber } = await import("@dev-blinq/cucumber-js/api");
89
+ const { runConfiguration } = await loadConfiguration(
90
+ {
91
+ provided: {
92
+ name: [scenario],
93
+ paths: [feature_file_path],
94
+ import: [path.join(tempFolderPath, "step_definitions", "**", "*.mjs")],
95
+ // format: ["bvt"],
96
+ },
97
+ },
98
+ { cwd: process.cwd(), env: environment }
99
+ );
100
+ // const files = glob.sync(path.join(tempFolderPath, "step_definitions", "**", "*.mjs"));
101
+ // console.log("Files found:", files);
102
+ const support = await loadSupport(runConfiguration, { cwd: process.cwd(), env: environment });
103
+ // console.log("found ", support.stepDefinitions.length, "step definitions");
104
+ // support.stepDefinitions.map((step) => {
105
+ // console.log("step", step.pattern);
106
+ // });
107
+
108
+ support.afterTestRunHookDefinitions = [];
109
+ if (skipAfter) {
110
+ // ignore afterAll/after hooks
111
+ support.afterTestCaseHookDefinitions = [];
112
+ }
113
+ if (skipBefore && !config.legacySyntax) {
114
+ // ignore beforeAll/before hooks
115
+ support.beforeTestCaseHookDefinitions = support.beforeTestCaseHookDefinitions.filter((hook) => {
116
+ return hook.uri.endsWith("utils.mjs");
117
+ });
118
+ }
119
+ support.beforeTestRunHookDefinitions = [];
120
+
121
+ let errorMesssage = null;
122
+ let info = null;
123
+ let errInfo = null;
124
+ const result = await runCucumber({ ...runConfiguration, support }, environment, (message) => {
125
+ if (message.testStepFinished) {
126
+ const { testStepFinished } = message;
127
+ const { testStepResult } = testStepFinished;
128
+ if (testStepResult.status === "FAILED" || testStepResult.status === "AMBIGUOUS") {
129
+ if (!errorMesssage) {
130
+ errorMesssage = testStepResult.message;
131
+ if (info) {
132
+ errInfo = info;
133
+ }
134
+ }
135
+ }
136
+ if (testStepResult.status === "UNDEFINED") {
137
+ if (!errorMesssage) {
138
+ errorMesssage = `step ${JSON.stringify(stepText)} is ${testStepResult.status}`;
139
+ if (info) {
140
+ errInfo = info;
141
+ }
142
+ }
143
+ }
144
+ }
145
+ if (message.attachment) {
146
+ const attachment = message.attachment;
147
+ if (attachment.mediaType === "application/json" && attachment.body) {
148
+ const body = JSON.parse(attachment.body);
149
+ info = body.info;
150
+ const result = body.result;
151
+
152
+ if (result.status === "PASSED") {
153
+ this.sendExecutionStatus({
154
+ type: "cmdExecutionSuccess",
155
+ cmdId: body.cmdId,
156
+ });
157
+ } else {
158
+ this.sendExecutionStatus({
159
+ type: "cmdExecutionError",
160
+ cmdId: body.cmdId,
161
+ error: {
162
+ message: result.message,
163
+ info,
164
+ },
165
+ });
166
+ }
167
+ } else if (attachment.mediaType === "application/json+intercept-results" && attachment.body) {
168
+ const body = JSON.parse(attachment.body);
169
+ if (body) {
170
+ this.sendExecutionStatus({
171
+ type: "interceptResults",
172
+ interceptResults: body,
173
+ });
174
+ }
175
+ }
176
+ }
177
+ });
178
+ if (errorMesssage) {
179
+ const bvtError = new Error(errorMesssage);
180
+ Object.assign(bvtError, { info: errInfo });
181
+ throw bvtError;
182
+ }
183
+
184
+ return {
185
+ result,
186
+ info,
187
+ };
188
+ } catch (error) {
189
+ console.error("Error running cucumber-js", error);
190
+ throw error;
191
+ }
192
+ };
193
+
194
+ async runStep({ step, parametersMap, envPath, tags, config }, bvtContext, options) {
195
+ let cmdIDs = (step.commands || []).map((cmd) => cmd.cmdId);
196
+ if (bvtContext.web) {
197
+ bvtContext.web.getCmdId = () => {
198
+ if (cmdIDs.length === 0) {
199
+ cmdIDs = (step.commands || []).map((cmd) => cmd.cmdId);
200
+ }
201
+ const cId = cmdIDs.shift();
202
+ this.sendExecutionStatus({
203
+ type: "cmdExecutionStart",
204
+ cmdId: cId,
205
+ });
206
+ return cId;
207
+ };
208
+ }
71
209
  let codePage; // = getCodePage();
72
210
  // const tempFolderPath = process.env.tempFeaturesFolderPath;
73
211
  const __temp_features_FolderName = "__temp_features" + Math.random().toString(36).substring(2, 7);
@@ -96,7 +234,8 @@ export class BVTStepRunner {
96
234
  if (!existsSync(stepDefinitionFolderPath)) {
97
235
  mkdirSync(stepDefinitionFolderPath, { recursive: true });
98
236
  }
99
- const stepDefsFilePath = path.join(stepDefinitionFolderPath, pageName + "_page.mjs");
237
+ const stepDefsFilePath = locateDefinitionPath(tempFolderPath, pageName);
238
+ //path.join(stepDefinitionFolderPath, pageName + "_page.mjs");
100
239
  codePage = getCodePage(stepDefsFilePath);
101
240
  codePage = await saveRecording({ step, cucumberStep, codePage, projectDir: this.projectDir, stepsDefinitions });
102
241
  if (codePage) {
@@ -106,19 +245,49 @@ export class BVTStepRunner {
106
245
  if (!codePage) {
107
246
  codePage = getUtilsCodePage(this.projectDir);
108
247
  }
248
+ } else {
249
+ let routesPath = path.join(tmpdir(), `blinq_temp_routes`);
250
+ if (process.env.TEMP_RUN === "true") {
251
+ // console.log("Save routes in temp folder for running:", routesPath);
252
+ if (existsSync(routesPath)) {
253
+ // console.log("Removing existing temp_routes_folder:", routesPath);
254
+ rmSync(routesPath, { recursive: true });
255
+ }
256
+ mkdirSync(routesPath, { recursive: true });
257
+ // console.log("Created temp_routes_folder:", routesPath);
258
+ saveRoutes({ step, folderPath: routesPath });
259
+ } else {
260
+ // console.log("Saving routes in project directory:", this.projectDir);
261
+ if (existsSync(routesPath)) {
262
+ // remove the folder
263
+ try {
264
+ rmSync(routesPath, { recursive: true });
265
+ // console.log("Removed temp_routes_folder:", routesPath);
266
+ } catch (error) {
267
+ // console.error("Error removing temp_routes folder", error);
268
+ }
269
+ }
270
+ routesPath = path.join(this.projectDir, "data", "routes");
271
+ // console.log("Saving routes to:", routesPath);
272
+ if (!existsSync(routesPath)) {
273
+ mkdirSync(routesPath, { recursive: true });
274
+ }
275
+ saveRoutes({ step, folderPath: routesPath });
276
+ }
109
277
  }
110
- const feature_file_path = await this.writeTempFeatureFile({ step, parametersMap, tempFolderPath });
278
+ const feature_file_path = await this.writeTempFeatureFile({ step, parametersMap, tempFolderPath, tags });
111
279
  // console.log({ feature_file_path, step_text: step.text });
112
280
 
113
281
  const stepExecController = new AbortController();
114
282
  this.#currentStepController = stepExecController;
115
- await withAbort(async () => {
116
- await stepsDefinitions.executeStepRemote(
283
+ const { result, info } = await withAbort(async () => {
284
+ return await this.executeStepRemote(
117
285
  {
118
286
  feature_file_path,
119
287
  tempFolderPath,
120
288
  stepText: step.text,
121
289
  scenario: "Temp Scenario",
290
+ config,
122
291
  },
123
292
  options
124
293
  );
@@ -128,5 +297,6 @@ export class BVTStepRunner {
128
297
  fs.rmSync(tempFolderPath, { recursive: true });
129
298
  }
130
299
  });
300
+ return { result, info };
131
301
  }
132
302
  }
@@ -1,14 +1,16 @@
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";
5
- import { CodePage } from "../code_gen/page_reflection.js";
5
+ import { CodePage, getAiConfig } from "../code_gen/page_reflection.js";
6
6
  import { generateCode, generatePageName } from "../code_gen/playwright_codeget.js";
7
7
  import { invertCodeToCommand } from "../code_gen/code_inversion.js";
8
8
  import { Step } from "../cucumber/feature.js";
9
- import { StepsDefinitions } from "../cucumber/steps_definitions.js";
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,72 @@ 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
+ if (process.env.TEMP_RUN === "true") {
114
+ // console.log("Save routes in temp folder for running:", routesPath);
115
+ if (existsSync(routesPath)) {
116
+ // console.log("Removing existing temp_routes_folder:", routesPath);
117
+ rmSync(routesPath, { recursive: true });
118
+ }
119
+ mkdirSync(routesPath, { recursive: true });
120
+ // console.log("Created temp_routes_folder:", routesPath);
121
+ saveRoutes({ step, folderPath: routesPath });
122
+ } else {
123
+ // console.log("Saving routes in project directory:", projectDir);
124
+ if (existsSync(routesPath)) {
125
+ // remove the folder
126
+ try {
127
+ rmSync(routesPath, { recursive: true });
128
+ // console.log("Removed temp_routes_folder:", routesPath);
129
+ } catch (error) {
130
+ // console.error("Error removing temp_routes folder", error);
131
+ }
132
+ }
133
+ routesPath = path.join(projectDir, "data", "routes");
134
+ // console.log("Saving routes to:", routesPath);
135
+ if (!existsSync(routesPath)) {
136
+ mkdirSync(routesPath, { recursive: true });
137
+ }
138
+ saveRoutes({ step, folderPath: routesPath });
139
+ }
85
140
  cucumberStep.text = step.text;
86
141
  const recording = new Recording();
87
142
  const steps = step.commands;
@@ -108,6 +163,7 @@ export async function saveRecording({ step, cucumberStep, codePage, projectDir,
108
163
  isStaticToken,
109
164
  status,
110
165
  } = step.commands[0].value;
166
+
111
167
  const result = await generateApiCode(
112
168
  {
113
169
  url,
@@ -132,11 +188,15 @@ export async function saveRecording({ step, cucumberStep, codePage, projectDir,
132
188
  step.keyword,
133
189
  stepsDefinitions
134
190
  );
135
- stepsDefinitions.addStep({
136
- name: step.text,
137
- file: result.codePage.sourceFileName,
138
- source: "recorder",
139
- });
191
+
192
+ if (!step.isImplemented) {
193
+ stepsDefinitions.addStep({
194
+ name: step.text,
195
+ file: result.codePage.sourceFileName,
196
+ source: "recorder",
197
+ });
198
+ }
199
+
140
200
  cucumberStep.methodName = result.methodName;
141
201
  return result.codePage;
142
202
  } else {
@@ -164,7 +224,13 @@ export async function saveRecording({ step, cucumberStep, codePage, projectDir,
164
224
  path
165
225
  );
166
226
  const keyword = (cucumberStep.keywordAlias ?? cucumberStep.keyword).trim();
167
- const stepResult = codePage.addCucumberStep(keyword, cucumberStep.getTemplate(), methodName, steps.length);
227
+ const stepResult = codePage.addCucumberStep(
228
+ keyword,
229
+ cucumberStep.getTemplate(),
230
+ methodName,
231
+ steps.length,
232
+ step.finalTimeout
233
+ );
168
234
 
169
235
  if (!step.isImplemented) {
170
236
  stepsDefinitions.addStep({
@@ -185,7 +251,17 @@ export async function saveRecording({ step, cucumberStep, codePage, projectDir,
185
251
 
186
252
  const getLocatorsJson = (file) => {
187
253
  if (!file) return {};
188
- const locatorsFilePath = file.replace(".mjs", ".json");
254
+ let locatorsFilePath = file.replace(".mjs", ".json");
255
+ const originLocatorsFilePath = locatorsFilePath;
256
+ const config = getAiConfig();
257
+ if (config && config.locatorsMetadataDir) {
258
+ // if config.locatorsMetadataDir is set, use it to create the file path
259
+ locatorsFilePath = path.join(config.locatorsMetadataDir, path.basename(locatorsFilePath));
260
+ if (!existsSync(locatorsFilePath)) {
261
+ // if the file does not exist in the config directory, use the original path
262
+ locatorsFilePath = originLocatorsFilePath;
263
+ }
264
+ }
189
265
  if (!existsSync(locatorsFilePath)) {
190
266
  return {};
191
267
  }
@@ -227,7 +303,7 @@ export const getCommandsForImplementedStep = (stepName, stepsDefinitions, stepPa
227
303
 
228
304
  isUtilStep = codePage.sourceFileName.endsWith("utils.mjs");
229
305
  for (const { code } of codeCommands) {
230
- const command = invertCodeToCommand(code, elements, stepParams)[0];
306
+ const command = invertCodeToCommand(code, elements, stepParams, stepsDefinitions, codePage, stepName)[0];
231
307
  if (command === undefined || command.type === null) continue;
232
308
  if (command.element) {
233
309
  const key = command.element.key;
@@ -297,6 +373,7 @@ export async function updateStepDefinitions({ scenario, featureName, projectDir
297
373
  const steps = scenario.steps;
298
374
 
299
375
  const stepsDefinitions = new StepsDefinitions(projectDir);
376
+ const featureFolder = path.join(projectDir, "features");
300
377
  stepsDefinitions.load(false);
301
378
  // const parameters = scenario.parameters;
302
379
  // await saveRecordings({ steps, parameters, codePage, projectDir });
@@ -308,13 +385,41 @@ export async function updateStepDefinitions({ scenario, featureName, projectDir
308
385
  }
309
386
  }
310
387
  if ((step.isImplemented && !step.shouldOverride) || step.commands.length === 0) {
388
+ let routesPath = path.join(tmpdir(), `blinq_temp_routes`);
389
+ if (process.env.TEMP_RUN === "true") {
390
+ // console.log("Save routes in temp folder for running:", routesPath);
391
+ if (existsSync(routesPath)) {
392
+ // console.log("Removing existing temp_routes_folder:", routesPath);
393
+ rmSync(routesPath, { recursive: true });
394
+ }
395
+ mkdirSync(routesPath, { recursive: true });
396
+ // console.log("Created temp_routes_folder:", routesPath);
397
+ saveRoutes({ step, folderPath: routesPath });
398
+ } else {
399
+ // console.log("Saving routes in project directory:", projectDir);
400
+ if (existsSync(routesPath)) {
401
+ // remove the folder
402
+ try {
403
+ rmSync(routesPath, { recursive: true });
404
+ // console.log("Removed temp_routes_folder:", routesPath);
405
+ } catch (error) {
406
+ // console.error("Error removing temp_routes folder", error);
407
+ }
408
+ }
409
+ routesPath = path.join(projectDir, "data", "routes");
410
+ // console.log("Saving routes to:", routesPath);
411
+ if (!existsSync(routesPath)) {
412
+ mkdirSync(routesPath, { recursive: true });
413
+ }
414
+ saveRoutes({ step, folderPath: routesPath });
415
+ }
311
416
  continue;
312
417
  }
313
418
  const cucumberStep = getCucumberStep({ step });
314
419
  const pageName = generatePageName(step.startFrame?.url ?? "default");
315
- const stepDefsFilePath = path.join(stepDefinitionFolderPath, pageName + "_page.mjs");
420
+ const stepDefsFilePath = locateDefinitionPath(featureFolder, pageName);
421
+ // path.join(stepDefinitionFolderPath, pageName + "_page.mjs");
316
422
  let codePage = getCodePage(stepDefsFilePath);
317
-
318
423
  codePage = await saveRecording({ step, cucumberStep, codePage, projectDir, stepsDefinitions });
319
424
  if (!codePage) {
320
425
  continue;
@@ -326,3 +431,43 @@ export async function updateStepDefinitions({ scenario, featureName, projectDir
326
431
  }
327
432
  writeFileSync(utilsFilePath, utilsContent, "utf8");
328
433
  }
434
+
435
+ export function saveRoutes({ step, folderPath }) {
436
+ const routeItems = step.routeItems;
437
+ if (!routeItems || routeItems.length === 0) {
438
+ return;
439
+ }
440
+ const cucumberStep = getCucumberStep({ step });
441
+ const template = cucumberStep.getTemplate();
442
+ const stepNameHash = createHash("sha256").update(template).digest("hex");
443
+ // console.log("Saving routes for step:", step.text, "with hash:", stepNameHash);
444
+ const routeItemsWithFilters = routeItems.map((routeItem) => {
445
+ const oldFilters = routeItem.filters;
446
+ const queryParamsObject = {};
447
+ oldFilters.queryParams.forEach((queryParam) => {
448
+ queryParamsObject[queryParam.key] = queryParam.value;
449
+ });
450
+ const newFilters = { path: oldFilters.path, method: oldFilters.method, queryParams: queryParamsObject };
451
+ return {
452
+ ...routeItem,
453
+ filters: newFilters,
454
+ };
455
+ });
456
+
457
+ const routesFilePath = path.join(folderPath, stepNameHash + ".json");
458
+ // console.log("Routes file path:", routesFilePath);
459
+ const routesData = {
460
+ template,
461
+ routes: routeItemsWithFilters,
462
+ };
463
+ // console.log("Routes data to save:", routesData);
464
+ if (!existsSync(folderPath)) {
465
+ mkdirSync(folderPath, { recursive: true });
466
+ }
467
+ try {
468
+ writeFileSync(routesFilePath, JSON.stringify(routesData, null, 2), "utf8");
469
+ // console.log("Saved routes to", routesFilePath);
470
+ } catch (error) {
471
+ console.error("Failed to save routes to", routesFilePath, "Error:", error);
472
+ }
473
+ }
@@ -1,6 +1,7 @@
1
1
  import { existsSync, readFileSync, writeFileSync } from "fs";
2
2
  import path from "path";
3
-
3
+ import { getDefaultPrettierConfig } from "../code_cleanup/utils.js";
4
+ import prettier from "prettier";
4
5
  export function containsScenario({ featureFileContent, scenarioName }) {
5
6
  const lines = featureFileContent.split("\n");
6
7
  for (const line of lines) {
@@ -209,14 +210,12 @@ const GherkinToObject = (gherkin) => {
209
210
  steps: [],
210
211
  };
211
212
  while (idx < lines.length && lines[idx].startsWith("@")) {
212
- skipEmptyLines();
213
213
  const tags = [...lines[idx].matchAll(/@([^@]+)/g)].map((match) => match[1].trim());
214
214
  scenario.tags.push(...(tags ?? []));
215
215
  idx++;
216
+ skipEmptyLines();
216
217
  }
217
218
 
218
- skipEmptyLines();
219
-
220
219
  if (idx < lines.length && (lines[idx].startsWith("Scenario:") || lines[idx].startsWith("Scenario Outline:"))) {
221
220
  scenario.name = lines[idx].substring(lines[idx].indexOf(":") + 1).trim();
222
221
  idx++;
@@ -239,32 +238,32 @@ const GherkinToObject = (gherkin) => {
239
238
  !lines[idx].startsWith("@")
240
239
  ) {
241
240
  const line = lines[idx++];
242
- if (line.startsWith("#")) {
243
- const comment = line;
244
- if (comment) {
245
- const command = {
246
- type: "comment",
247
- text: comment,
248
- };
249
- scenario.steps.push(command);
250
- }
251
- } else if (line.startsWith("Examples:")) {
252
- obj.hasParams = true;
253
- const command = {
241
+ let command;
242
+ if (line.startsWith("Examples:")) {
243
+ scenario.hasParams = true;
244
+ command = {
254
245
  type: "examples",
255
246
  lines: [],
256
247
  };
257
-
258
248
  while (idx < lines.length && lines[idx].startsWith("|")) {
259
249
  const line = lines[idx++];
260
250
  command.lines.push(line);
261
251
  }
262
252
  } else {
263
- scenario.steps.push({
264
- type: "step",
265
- text: line,
266
- });
253
+ if (line.startsWith("#")) {
254
+ command = {
255
+ type: "comment",
256
+ text: line,
257
+ };
258
+ } else {
259
+ command = {
260
+ type: "step",
261
+ text: line,
262
+ };
263
+ }
267
264
  }
265
+ scenario.steps.push(command);
266
+ skipEmptyLines();
268
267
  }
269
268
 
270
269
  return scenario;
@@ -273,7 +272,6 @@ const GherkinToObject = (gherkin) => {
273
272
  while (idx < lines.length) {
274
273
  const scenario = getScenario();
275
274
  if (scenario === -1) break;
276
-
277
275
  if (scenario.error) {
278
276
  return {
279
277
  error: scenario.error,
@@ -299,8 +297,7 @@ function updateExistingScenario({ featureFileContent, scenarioName, scenarioCont
299
297
  skipScenarioIndex = i;
300
298
  continue;
301
299
  }
302
- let scenarioContent = `${featureFileObject.hasParams ? "Scenario Outline" : "Scenario"}: ${scenario.name}`;
303
-
300
+ let scenarioContent = `${scenario.hasParams ? "Scenario Outline" : "Scenario"}: ${scenario.name}`;
304
301
  let tagsLine;
305
302
  if (scenario.tags?.length > 0) {
306
303
  tagsLine = `${scenario.tags.map((t) => `@${t}`).join(" ")}`;
@@ -323,7 +320,6 @@ function updateExistingScenario({ featureFileContent, scenarioName, scenarioCont
323
320
  if (skipScenarioIndex !== -1) {
324
321
  finalContent = results.join("\n") + "\n" + scenarioContent;
325
322
  }
326
-
327
323
  return finalContent;
328
324
  }
329
325
  export async function updateFeatureFile({ featureName, scenario, override, projectDir }) {
@@ -333,6 +329,8 @@ export async function updateFeatureFile({ featureName, scenario, override, proje
333
329
  { scenario },
334
330
  isFeatureFileExists ? GherkinToObject(readFileSync(featureFilePath, "utf8")) : undefined
335
331
  );
332
+ const prettierConfig = getDefaultPrettierConfig();
333
+ // Format the code using Prettier
336
334
 
337
335
  if (isFeatureFileExists) {
338
336
  const featureFileContent = readFileSync(featureFilePath, "utf8");
@@ -341,7 +339,7 @@ export async function updateFeatureFile({ featureName, scenario, override, proje
341
339
  if (!override) {
342
340
  throw new Error(`Scenario "${scenario.name}" already exists in feature "${featureName}"`);
343
341
  } else {
344
- const updatedFeatureFileContent = updateExistingScenario({
342
+ let updatedFeatureFileContent = updateExistingScenario({
345
343
  featureFileContent,
346
344
  scenarioName: scenario.name,
347
345
  scenarioContent,
@@ -349,14 +347,41 @@ export async function updateFeatureFile({ featureName, scenario, override, proje
349
347
  if (updatedFeatureFileContent === "error") {
350
348
  throw new Error(`Error while parsing feature file: Invalid gherkin`);
351
349
  }
350
+ try {
351
+ updatedFeatureFileContent = await prettier.format(updatedFeatureFileContent, {
352
+ ...prettierConfig,
353
+ parser: "gherkin",
354
+ plugins: ["prettier-plugin-gherkin"],
355
+ });
356
+ } catch (error) {
357
+ console.error("Error formatting feature file content with Prettier:", error);
358
+ }
352
359
  writeFileSync(featureFilePath, updatedFeatureFileContent);
353
360
  return;
354
361
  }
355
362
  }
356
- const updatedFeatureFileContent = featureFileContent + "\n" + scenarioContent;
363
+ let updatedFeatureFileContent = featureFileContent + "\n" + scenarioContent;
364
+ try {
365
+ updatedFeatureFileContent = await prettier.format(updatedFeatureFileContent, {
366
+ ...prettierConfig,
367
+ parser: "gherkin",
368
+ plugins: ["prettier-plugin-gherkin"],
369
+ });
370
+ } catch (error) {
371
+ console.error("Error formatting feature file content with Prettier:", error);
372
+ }
357
373
  writeFileSync(featureFilePath, updatedFeatureFileContent);
358
374
  } else {
359
- const featureFileContent = `Feature: ${featureName}\n${scenarioContent}`;
375
+ let featureFileContent = `Feature: ${featureName}\n${scenarioContent}`;
376
+ try {
377
+ featureFileContent = await prettier.format(featureFileContent, {
378
+ ...prettierConfig,
379
+ parser: "gherkin",
380
+ plugins: ["prettier-plugin-gherkin"],
381
+ });
382
+ } catch (error) {
383
+ console.error("Error formatting feature file content with Prettier:", error);
384
+ }
360
385
  writeFileSync(featureFilePath, featureFileContent);
361
386
  }
362
387
  }