@dev-blinq/cucumber_client 1.0.1176-dev → 1.0.1176-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 +85 -11
  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 +112 -18
  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 +152 -48
  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 +59 -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 +236 -79
  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 +45 -4
  37. package/bin/client/recorderv3/network.js +299 -0
  38. package/bin/client/recorderv3/step_runner.js +179 -13
  39. package/bin/client/recorderv3/step_utils.js +159 -14
  40. package/bin/client/recorderv3/update_feature.js +54 -29
  41. package/bin/client/recording.js +8 -0
  42. package/bin/client/run_cucumber.js +116 -4
  43. package/bin/client/scenario_report.js +112 -50
  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,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.paramKey] = queryParam.paramValue;
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) {
@@ -96,7 +97,7 @@ export function getCommandContent(command) {
96
97
  return `send a ${command.value.method} API request to ${command.value.url}`;
97
98
  }
98
99
  case "verify_page_snapshot": {
99
- return `verify page snapshot stored in ${command.value}`;
100
+ return `verify page snapshot stored in ${command.parameters[0]}`;
100
101
  }
101
102
  default: {
102
103
  return "";
@@ -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
  }
@@ -5,9 +5,14 @@ import { Step } from "../client/cucumber/feature.js";
5
5
  This list need to be in sync with the list exist in the commands.js file
6
6
  */
7
7
  const Types = {
8
+ API: "api",
8
9
  CLICK: "click_element",
9
10
  CLICK_SIMPLE: "click_simple",
11
+ PARAMETERIZED_CLICK: "parameterized_click",
12
+ CONTEXT_CLICK: "context_click",
10
13
  NAVIGATE: "navigate",
14
+ GO_BACK: "browser_go_back",
15
+ GO_FORWARD: "browser_go_forward",
11
16
  FILL: "fill_element",
12
17
  FILL_SIMPLE: "fill_simple",
13
18
  EXECUTE: "execute_page_method",
@@ -29,6 +34,7 @@ const Types = {
29
34
  SET_COMBO: "set_combo",
30
35
  HOVER: "hover_element",
31
36
  EXTRACT_ATTRIBUTE: "extract_attribute",
37
+ EXTRACT_PROPERTY: "extract_property",
32
38
  CLOSE_PAGE: "close_page",
33
39
  SET_DATE_TIME: "set_date_time",
34
40
  SET_VIEWPORT: "set_viewport",
@@ -40,11 +46,13 @@ const Types = {
40
46
  SET_INPUT: "set_input",
41
47
  WAIT_FOR_USER_INPUT: "wait_for_user_input",
42
48
  VERIFY_ATTRIBUTE: "verify_element_attribute",
49
+ VERIFY_PROPERTY: "verify_element_property",
43
50
  FILL_UNKNOWN: "fill_unknown",
44
51
  VERIFY_TEXT_RELATED_TO_TEXT: "verify_text_in_relation",
45
52
  VERIFY_FILE_EXISTS: "verify_file_exists",
46
53
  SET_INPUT_FILES: "set_input_files",
47
54
  VERIFY_PAGE_SNAPSHOT: "verify_page_snapshot",
55
+ CONDITIONAL_WAIT: "conditional_wait",
48
56
  };
49
57
  class Recording {
50
58
  steps = [];
@@ -1,4 +1,4 @@
1
- import { existsSync, mkdirSync } from "fs";
1
+ import { existsSync, mkdirSync, writeFileSync, readFileSync } from "fs";
2
2
  import logger from "../logger.js";
3
3
  import path from "path";
4
4
  import { scenarioResolution } from "./cucumber/feature.js";
@@ -13,6 +13,77 @@ import crypto from "crypto";
13
13
  //import debug from "debug";
14
14
  process.env.NODE_TLS_REJECT_UNAUTHORIZED = 0;
15
15
 
16
+ // Initialize the editorLogs array to collect all logs
17
+ const editorLogs = [];
18
+
19
+ // Store original console methods
20
+ const originalConsoleLog = console.log;
21
+ const originalConsoleError = console.error;
22
+
23
+ // Store original process stdout and stderr write methods
24
+ const originalStdoutWrite = process.stdout.write;
25
+ const originalStderrWrite = process.stderr.write;
26
+
27
+ // Override console.log
28
+ console.log = function (...args) {
29
+ const logEntry = {
30
+ message: args.map((arg) => (typeof arg === "object" ? JSON.stringify(arg) : String(arg))).join(" "),
31
+ };
32
+
33
+ editorLogs.push(logEntry);
34
+ originalConsoleLog.apply(console, args);
35
+ };
36
+
37
+ // Override console.error
38
+ console.error = function (...args) {
39
+ const logEntry = {
40
+ message: args.map((arg) => (typeof arg === "object" ? JSON.stringify(arg) : String(arg))).join(" "),
41
+ };
42
+
43
+ editorLogs.push(logEntry);
44
+ originalConsoleError.apply(console, args);
45
+ };
46
+
47
+ // Override process.stdout.write
48
+ process.stdout.write = function (chunk, encoding, callback) {
49
+ const logEntry = {
50
+ message: chunk.toString(),
51
+ };
52
+
53
+ editorLogs.push(logEntry);
54
+ return originalStdoutWrite.apply(process.stdout, arguments);
55
+ };
56
+
57
+ // Override process.stderr.write
58
+ process.stderr.write = function (chunk, encoding, callback) {
59
+ const logEntry = {
60
+ message: chunk.toString(),
61
+ };
62
+
63
+ editorLogs.push(logEntry);
64
+ return originalStderrWrite.apply(process.stderr, arguments);
65
+ };
66
+
67
+ // Function to write logs to file
68
+ const writeLogsToFile = (filePath) => {
69
+ try {
70
+ const dirPath = path.dirname(filePath);
71
+
72
+ // Create directory if it doesn't exist
73
+ if (!existsSync(dirPath)) {
74
+ mkdirSync(dirPath, { recursive: true });
75
+ }
76
+
77
+ // Format logs as plain message on each line
78
+ const logText = editorLogs.map((log) => `${log.message}`).join("\n");
79
+
80
+ // Write logs to plain text file
81
+ writeFileSync(filePath, logText, { encoding: "utf8" });
82
+ } catch (error) {
83
+ logger.error(`Failed to write logs to file: ${error.message}`);
84
+ }
85
+ };
86
+
16
87
  //do something when app is closing
17
88
 
18
89
  const runCucumber = async (
@@ -60,6 +131,10 @@ const runCucumber = async (
60
131
  } else if (!process.env.NODE_ENV_BLINQ) {
61
132
  serviceUrl = "https://logic.blinq.io";
62
133
  logger.info("Running in prod mode");
134
+ } else if (process.env.WHITELABEL === "true") {
135
+ // if it's a whitelabel it will use "api" instead of "logic"
136
+ serviceUrl = process.env.NODE_ENV_BLINQ;
137
+ logger.info("Running in custom mode: " + serviceUrl);
63
138
  } else {
64
139
  serviceUrl = process.env.NODE_ENV_BLINQ.replace("api", "logic");
65
140
  logger.info("Running in custom mode: " + serviceUrl);
@@ -93,7 +168,14 @@ const runCucumber = async (
93
168
  }
94
169
  return true;
95
170
  });
96
- process.on("exit", async () => await aiAgent.endScenario());
171
+ process.on("exit", async () => {
172
+ // Write logs to file before exiting
173
+ if (result.scenarioPath) {
174
+ const logsFilePath = path.join(result.scenarioPath, "editorLogs.log");
175
+ writeLogsToFile(logsFilePath);
176
+ }
177
+ await aiAgent.endScenario();
178
+ });
97
179
  const context = {};
98
180
  result.context = context;
99
181
  if (process.env.E2E === "true") {
@@ -105,6 +187,9 @@ const runCucumber = async (
105
187
  envFile = await aiAgent.initProjectAndEnvironment(context);
106
188
  } catch (e) {
107
189
  logger.error(e.message);
190
+ // Write logs to file before exiting due to error
191
+ const logsFilePath = path.join(result.scenarioPath, "editorLogs.log");
192
+ writeLogsToFile(logsFilePath);
108
193
  if (exit) {
109
194
  process.exit(1);
110
195
  }
@@ -118,6 +203,9 @@ const runCucumber = async (
118
203
  }
119
204
  if (!existsSync(fullFeatureFilePath)) {
120
205
  logger.error("Feature file not found: " + fullFeatureFilePath);
206
+ // Write logs to file before exiting due to error
207
+ const logsFilePath = path.join(result.scenarioPath, "editorLogs.log");
208
+ writeLogsToFile(logsFilePath);
121
209
  if (exit) {
122
210
  process.exit(1);
123
211
  }
@@ -128,6 +216,9 @@ const runCucumber = async (
128
216
  feature = await scenarioResolution(fullFeatureFilePath);
129
217
  } catch (e) {
130
218
  logger.error("Error parsing feature file: " + fullFeatureFilePath);
219
+ // Write logs to file before exiting due to error
220
+ const logsFilePath = path.join(result.scenarioPath, "editorLogs.log");
221
+ writeLogsToFile(logsFilePath);
131
222
  if (exit) {
132
223
  process.exit(1);
133
224
  }
@@ -136,6 +227,9 @@ const runCucumber = async (
136
227
  const scenario = feature.getScenario(scenarioName);
137
228
  if (!scenario) {
138
229
  logger.error("Scenario not found: " + scenarioName);
230
+ // Write logs to file before exiting due to error
231
+ const logsFilePath = path.join(result.scenarioPath, "editorLogs.log");
232
+ writeLogsToFile(logsFilePath);
139
233
  if (exit) {
140
234
  process.exit(1);
141
235
  }
@@ -146,6 +240,15 @@ const runCucumber = async (
146
240
 
147
241
  let scenarioId = findNextIdInFolder("./reports");
148
242
  let scenarioPath = "./reports" + "/" + scenarioId;
243
+ const dataFilePath = path.join(scenarioPath, "data.json");
244
+ mkdirSync(path.dirname(dataFilePath), { recursive: true });
245
+ if (process.env.NODE_ENV_BLINQ === "local") {
246
+ let dataPath = aiAgent.project.rootFolder + "/data/data.json";
247
+ if (existsSync(dataPath)) {
248
+ const poolData = JSON.parse(readFileSync(dataPath, "utf-8"));
249
+ writeFileSync(dataFilePath, JSON.stringify(poolData, null, 2), "utf-8");
250
+ }
251
+ }
149
252
  result.scenarioPath = scenarioPath;
150
253
  aiAgent.initTestData(envFile, path.join(scenarioPath, "data.json"));
151
254
  let featureFileRelative = path.relative(projectDir, fullFeatureFilePath);
@@ -256,7 +359,7 @@ const runCucumber = async (
256
359
  await aiAgent.createNewStepLocal(
257
360
  featureName,
258
361
  cucumberStep,
259
- feature,
362
+ feature.comments,
260
363
  userData,
261
364
  first,
262
365
  previousTasks,
@@ -283,6 +386,9 @@ const runCucumber = async (
283
386
  ) {
284
387
  aiAgent.evaluateScenario();
285
388
  }
389
+ // Write logs to file on successful completion
390
+ const logsFilePath = path.join(scenarioPath, "editorLogs.log");
391
+ writeLogsToFile(logsFilePath);
286
392
  await aiAgent.endScenario();
287
393
  } catch (e) {
288
394
  if (reconnect) {
@@ -303,7 +409,12 @@ const runCucumber = async (
303
409
  logger.error(e.stack);
304
410
  message = e.message + "\n" + e.stack;
305
411
  }
412
+ aiAgent.scenarioReport.updateLastCommand({ status: false, error: message });
413
+ // Write logs to file on error
414
+ const logsFilePath = path.join(result.scenarioPath, "editorLogs.log");
415
+ writeLogsToFile(logsFilePath);
306
416
  await aiAgent.endScenario();
417
+
307
418
  if (exit) {
308
419
  process.exit(1);
309
420
  }
@@ -316,4 +427,5 @@ const runCucumber = async (
316
427
  return result;
317
428
  }
318
429
  };
319
- export { runCucumber };
430
+
431
+ export { runCucumber, editorLogs, writeLogsToFile };