@dev-blinq/cucumber_client 1.0.1689-dev → 1.0.1691-dev

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.
@@ -455,6 +455,46 @@ export class CodePage {
455
455
  code += "}\n";
456
456
  return this._injectMethod(methodName, code);
457
457
  }
458
+ _replaceSelectedCode(code, injectIndexEnd) {
459
+ const newFileContent = this.fileContent.substring(0, injectIndexEnd - 2) +
460
+ "\n " +
461
+ code +
462
+ "\n}\n" +
463
+ this.fileContent.substring(injectIndexEnd);
464
+ this._init();
465
+ this.generateModel(newFileContent);
466
+ }
467
+ _injectOneCommand(methodName, code) {
468
+ const result = { methodName };
469
+ code = code.trim();
470
+ const existMethod = this.methods.find((m) => m.name === methodName);
471
+ if (!existMethod) {
472
+ throw new Error(`method ${methodName} not found for injection`);
473
+ }
474
+ const oldCode = existMethod.codePart.trim();
475
+ this._replaceSelectedCode(code, existMethod.end);
476
+ result.status = CodeStatus.UPDATED;
477
+ result.oldCode = oldCode;
478
+ result.newCode = code;
479
+ return result;
480
+ }
481
+ _removeSelectedCode(commands, injectIndexStart, injectIndexEnd) {
482
+ let newFileContent = this.fileContent.substring(injectIndexStart, injectIndexEnd);
483
+ commands.forEach((cmd) => {
484
+ newFileContent = newFileContent.replace(cmd, "");
485
+ });
486
+ newFileContent =
487
+ this.fileContent.substring(0, injectIndexStart) + newFileContent + this.fileContent.substring(injectIndexEnd);
488
+ this._init();
489
+ this.generateModel(newFileContent);
490
+ }
491
+ _removeCommands(methodName, commands) {
492
+ const existMethod = this.methods.find((m) => m.name === methodName);
493
+ if (!existMethod) {
494
+ throw new Error(`method ${methodName} not found for removal`);
495
+ }
496
+ this._removeSelectedCode(commands, existMethod.start, existMethod.end);
497
+ }
458
498
  _injectMethod(methodName, code) {
459
499
  const result = { methodName };
460
500
  code = code.trim();
@@ -804,4 +804,4 @@ const generatePageName = (url) => {
804
804
  return "default";
805
805
  }
806
806
  };
807
- export { generateCode, generatePageName };
807
+ export { generateCode, generatePageName, _generateCodeFromCommand };
@@ -352,6 +352,21 @@ async function BVTRecorderInit({ envName, projectDir, roomId, TOKEN, socket = nu
352
352
  "recorderWindow.cleanup": async (input) => {
353
353
  return recorder.cleanup(input);
354
354
  },
355
+ "recorderWindow.getStepCodeByScenario": async (input) => {
356
+ return await recorder.getStepCodeByScenario(input);
357
+ },
358
+ "recorderWindow.setStepCodeByScenario": async (input) => {
359
+ return await recorder.setStepCodeByScenario(input);
360
+ },
361
+ "recorderWindow.getRecorderContext": async (input) => {
362
+ return await recorder.getContext();
363
+ },
364
+ "recorderWindow.addCommandToStepCode": async (input) => {
365
+ return await recorder.addCommandToStepCode(input);
366
+ },
367
+ "recorderWindow.deleteCommandFromStepCode": async (input) => {
368
+ return await recorder.deleteCommandFromStepCode(input);
369
+ },
355
370
  });
356
371
 
357
372
  socket.on("targetBrowser.command.event", async (input) => {
@@ -1,22 +1,34 @@
1
1
  // define the jsdoc type for the input
2
2
  import { closeContext, initContext, _getDataFile, resetTestData } from "automation_model";
3
3
  import { existsSync, readdirSync, readFileSync, rmSync } from "fs";
4
+ import { rm } from "fs/promises";
4
5
  import path from "path";
5
6
  import url from "url";
6
7
  import { getImplementedSteps, parseRouteFiles } from "./implemented_steps.js";
7
8
  import { NamesService, RemoteBrowserService, PublishService } from "./services.js";
8
9
  import { BVTStepRunner } from "./step_runner.js";
9
10
  import { readFile, writeFile } from "fs/promises";
10
- import { loadStepDefinitions, getCommandsForImplementedStep } from "./step_utils.js";
11
+ import {
12
+ loadStepDefinitions,
13
+ getCommandsForImplementedStep,
14
+ updateStepDefinitions,
15
+ getCodePage,
16
+ getCucumberStep,
17
+ _toRecordingStep,
18
+ toMethodName,
19
+ } from "./step_utils.js";
11
20
  import { parseStepTextParameters } from "../cucumber/utils.js";
12
21
  import { AstBuilder, GherkinClassicTokenMatcher, Parser } from "@cucumber/gherkin";
13
22
  import chokidar from "chokidar";
14
23
  import { unEscapeNonPrintables } from "../cucumber/utils.js";
15
- import { findAvailablePort } from "../utils/index.js";
24
+ import { findAvailablePort, getRunsServiceBaseURL } from "../utils/index.js";
16
25
  import socketLogger from "../utils/socket_logger.js";
17
26
  import { tmpdir } from "os";
18
27
  import { faker } from "@faker-js/faker/locale/en_US";
19
28
  import { chromium } from "playwright-core";
29
+ import { axiosClient } from "../utils/axiosClient.js";
30
+ import { _generateCodeFromCommand } from "../code_gen/playwright_codeget.js";
31
+ import { Recording } from "../recording.js";
20
32
 
21
33
  const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
22
34
 
@@ -1252,7 +1264,7 @@ export class BVTRecorder {
1252
1264
  }, 100);
1253
1265
  this.timerId = timerId;
1254
1266
  }
1255
- async runStep({ step, parametersMap, tags, isFirstStep, listenNetwork }, options) {
1267
+ async runStep({ step, parametersMap, tags, isFirstStep, listenNetwork, AICode }, options) {
1256
1268
  const { skipAfter = true, skipBefore = !isFirstStep } = options || {};
1257
1269
 
1258
1270
  const env = path.basename(this.envName, ".json");
@@ -1294,6 +1306,7 @@ export class BVTRecorder {
1294
1306
  envPath: this.envName,
1295
1307
  tags,
1296
1308
  config: this.config,
1309
+ AICode,
1297
1310
  },
1298
1311
  this.bvtContext,
1299
1312
  {
@@ -1313,7 +1326,7 @@ export class BVTRecorder {
1313
1326
  this.bvtContext.navigate = false;
1314
1327
  }
1315
1328
  }
1316
- async saveScenario({ scenario, featureName, override, isSingleStep, branch, isEditing, env }) {
1329
+ async saveScenario({ scenario, featureName, override, isSingleStep, branch, isEditing, env, AICode }) {
1317
1330
  // await updateStepDefinitions({ scenario, featureName, projectDir: this.projectDir }); // updates mjs files
1318
1331
  // if (!isSingleStep) await updateFeatureFile({ featureName, scenario, override, projectDir: this.projectDir }); // updates gherkin files
1319
1332
  const res = await this.workspaceService.saveScenario({
@@ -1325,6 +1338,7 @@ export class BVTRecorder {
1325
1338
  isEditing,
1326
1339
  projectId: path.basename(this.projectDir),
1327
1340
  env: env ?? this.envName,
1341
+ AICode,
1328
1342
  });
1329
1343
  if (res.success) {
1330
1344
  await this.cleanup({ tags: scenario.tags });
@@ -1580,6 +1594,266 @@ export class BVTRecorder {
1580
1594
  }
1581
1595
  return result;
1582
1596
  }
1597
+ async setStepCodeByScenario({
1598
+ function_name,
1599
+ mjs_file_content,
1600
+ user_request,
1601
+ selectedTarget,
1602
+ page_context,
1603
+ AIMemory,
1604
+ }) {
1605
+ const runsURL = getRunsServiceBaseURL();
1606
+ const url = `${runsURL}/process-user-request/generate-code-with-context`;
1607
+ try {
1608
+ const result = await axiosClient({
1609
+ url,
1610
+ method: "POST",
1611
+ data: {
1612
+ function_name,
1613
+ mjs_file_content,
1614
+ user_request,
1615
+ selectedTarget,
1616
+ page_context,
1617
+ AIMemory,
1618
+ },
1619
+ headers: {
1620
+ Authorization: `Bearer ${this.TOKEN}`,
1621
+ "X-Source": "recorder",
1622
+ },
1623
+ });
1624
+ if (result.status !== 200) {
1625
+ return { success: false, message: "Error while fetching code changes" };
1626
+ }
1627
+ return { success: true, data: result.data };
1628
+ } catch (error) {
1629
+ // @ts-ignore
1630
+ const reason = error?.response?.data?.error || "";
1631
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
1632
+ throw new Error(`Failed to fetch code changes: ${errorMessage} \n ${reason}`);
1633
+ }
1634
+ }
1635
+
1636
+ async getStepCodeByScenario({ featureName, scenarioName, projectId, branch }) {
1637
+ try {
1638
+ const runsURL = getRunsServiceBaseURL();
1639
+ const ssoURL = runsURL.replace("/runs", "/auth");
1640
+ const privateRepoURL = `${ssoURL}/isRepoPrivate?project_id=${projectId}`;
1641
+
1642
+ const isPrivateRepoReq = await axiosClient({
1643
+ url: privateRepoURL,
1644
+ method: "GET",
1645
+ headers: {
1646
+ Authorization: `Bearer ${this.TOKEN}`,
1647
+ "X-Source": "recorder",
1648
+ },
1649
+ });
1650
+
1651
+ if (isPrivateRepoReq.status !== 200) {
1652
+ return { success: false, message: "Error while checking repo privacy" };
1653
+ }
1654
+
1655
+ const isPrivateRepo = isPrivateRepoReq.data.isPrivate ? isPrivateRepoReq.data.isPrivate : false;
1656
+
1657
+ const workspaceURL = runsURL.replace("/runs", "/workspace");
1658
+ const url = `${workspaceURL}/get-step-code-by-scenario`;
1659
+
1660
+ const result = await axiosClient({
1661
+ url,
1662
+ method: "POST",
1663
+ data: {
1664
+ scenarioName,
1665
+ featureName,
1666
+ projectId,
1667
+ isPrivateRepo,
1668
+ branch,
1669
+ },
1670
+ headers: {
1671
+ Authorization: `Bearer ${this.TOKEN}`,
1672
+ "X-Source": "recorder",
1673
+ },
1674
+ });
1675
+ if (result.status !== 200) {
1676
+ return { success: false, message: "Error while getting step code" };
1677
+ }
1678
+ return { success: true, data: result.data.stepInfo };
1679
+ } catch (error) {
1680
+ const reason = error?.response?.data?.error || "";
1681
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
1682
+ throw new Error(`Failed to get step code: ${errorMessage} \n ${reason}`);
1683
+ }
1684
+ }
1685
+ async getContext() {
1686
+ return await this.page.evaluate(() => {
1687
+ return document.documentElement.outerHTML;
1688
+ });
1689
+ }
1690
+ async deleteCommandFromStepCode({ scenario, AICode, command }) {
1691
+ if (!AICode || AICode.length === 0) {
1692
+ console.log("No AI code available to delete.");
1693
+ return;
1694
+ }
1695
+
1696
+ const __temp_features_FolderName = "__temp_features" + Math.random().toString(36).substring(2, 7);
1697
+ const tempFolderPath = path.join(this.projectDir, __temp_features_FolderName);
1698
+ process.env.tempFeaturesFolderPath = __temp_features_FolderName;
1699
+ process.env.TESTCASE_REPORT_FOLDER_PATH = tempFolderPath;
1700
+
1701
+ try {
1702
+ await this.stepRunner.copyCodetoTempFolder({ tempFolderPath, AICode });
1703
+ await this.stepRunner.writeWrapperCode(tempFolderPath);
1704
+ const codeView = AICode.find((f) => f.stepName === scenario.step.text);
1705
+
1706
+ if (!codeView) {
1707
+ throw new Error("Step code not found for step: " + scenario.step.text);
1708
+ }
1709
+
1710
+ const functionName = codeView.functionName;
1711
+ const mjsPath = path
1712
+ .normalize(codeView.mjsFile)
1713
+ .split(path.sep)
1714
+ .filter((part) => part !== "features")
1715
+ .join(path.sep);
1716
+ const codePath = path.join(tempFolderPath, mjsPath);
1717
+
1718
+ if (!existsSync(codePath)) {
1719
+ throw new Error("Step code file not found: " + codePath);
1720
+ }
1721
+
1722
+ const codePage = getCodePage(codePath);
1723
+
1724
+ const elements = codePage.getVariableDeclarationAsObject("elements");
1725
+
1726
+ const cucumberStep = getCucumberStep({ step: scenario.step });
1727
+ cucumberStep.text = scenario.step.text;
1728
+ const stepCommands = scenario.step.commands;
1729
+ const cmd = _toRecordingStep(command, scenario.step.name);
1730
+
1731
+ const recording = new Recording();
1732
+ recording.loadFromObject({ steps: stepCommands, step: cucumberStep });
1733
+ const step = { ...recording.steps[0], ...cmd };
1734
+ const result = _generateCodeFromCommand(step, elements, {});
1735
+
1736
+ codePage._removeCommands(functionName, result.codeLines);
1737
+ codePage.removeUnusedElements();
1738
+ codePage.save();
1739
+
1740
+ await rm(tempFolderPath, { recursive: true, force: true });
1741
+
1742
+ return { code: codePage.fileContent, mjsFile: codeView.mjsFile };
1743
+ } catch (error) {
1744
+ await rm(tempFolderPath, { recursive: true, force: true });
1745
+ throw error;
1746
+ }
1747
+ }
1748
+ async addCommandToStepCode({ scenario, AICode }) {
1749
+ if (!AICode || AICode.length === 0) {
1750
+ console.log("No AI code available to add.");
1751
+ return;
1752
+ }
1753
+
1754
+ const __temp_features_FolderName = "__temp_features" + Math.random().toString(36).substring(2, 7);
1755
+ const tempFolderPath = path.join(this.projectDir, __temp_features_FolderName);
1756
+ process.env.tempFeaturesFolderPath = __temp_features_FolderName;
1757
+ process.env.TESTCASE_REPORT_FOLDER_PATH = tempFolderPath;
1758
+
1759
+ try {
1760
+ await this.stepRunner.copyCodetoTempFolder({ tempFolderPath, AICode });
1761
+ await this.stepRunner.writeWrapperCode(tempFolderPath);
1762
+
1763
+ let codeView = AICode.find((f) => f.stepName === scenario.step.text);
1764
+
1765
+ if (codeView) {
1766
+ scenario.step.commands = [scenario.step.commands.pop()];
1767
+ const functionName = codeView.functionName;
1768
+ const mjsPath = path
1769
+ .normalize(codeView.mjsFile)
1770
+ .split(path.sep)
1771
+ .filter((part) => part !== "features")
1772
+ .join(path.sep);
1773
+ const codePath = path.join(tempFolderPath, mjsPath);
1774
+
1775
+ if (!existsSync(codePath)) {
1776
+ throw new Error("Step code file not found: " + codePath);
1777
+ }
1778
+
1779
+ const codePage = getCodePage(codePath);
1780
+ const elements = codePage.getVariableDeclarationAsObject("elements");
1781
+
1782
+ const cucumberStep = getCucumberStep({ step: scenario.step });
1783
+ cucumberStep.text = scenario.step.text;
1784
+ const stepCommands = scenario.step.commands;
1785
+ const cmd = _toRecordingStep(scenario.step.commands[0], scenario.step.name);
1786
+
1787
+ const recording = new Recording();
1788
+ recording.loadFromObject({ steps: stepCommands, step: cucumberStep });
1789
+ const step = { ...recording.steps[0], ...cmd };
1790
+
1791
+ const result = _generateCodeFromCommand(step, elements, {});
1792
+ codePage.insertElements(result.elements);
1793
+
1794
+ codePage._injectOneCommand(functionName, result.codeLines.join("\n"));
1795
+ codePage.save();
1796
+
1797
+ await rm(tempFolderPath, { recursive: true, force: true });
1798
+
1799
+ return { code: codePage.fileContent, newStep: false, mjsFile: codeView.mjsFile };
1800
+ }
1801
+ console.log("Step code not found for step: ", scenario.step.text);
1802
+
1803
+ codeView = AICode[0];
1804
+ const functionName = toMethodName(scenario.step.text);
1805
+ const codeLines = [];
1806
+ const mjsPath = path
1807
+ .normalize(codeView.mjsFile)
1808
+ .split(path.sep)
1809
+ .filter((part) => part !== "features")
1810
+ .join(path.sep);
1811
+ const codePath = path.join(tempFolderPath, mjsPath);
1812
+
1813
+ if (!existsSync(codePath)) {
1814
+ throw new Error("Step code file not found: " + codePath);
1815
+ }
1816
+
1817
+ const codePage = getCodePage(codePath);
1818
+ const elements = codePage.getVariableDeclarationAsObject("elements");
1819
+ let newElements = { ...elements };
1820
+
1821
+ const cucumberStep = getCucumberStep({ step: scenario.step });
1822
+ cucumberStep.text = scenario.step.text;
1823
+ const stepCommands = scenario.step.commands;
1824
+ stepCommands.forEach((command) => {
1825
+ const cmd = _toRecordingStep(command, scenario.step.name);
1826
+
1827
+ const recording = new Recording();
1828
+ recording.loadFromObject({ steps: stepCommands, step: cucumberStep });
1829
+ const step = { ...recording.steps[0], ...cmd };
1830
+ const result = _generateCodeFromCommand(step, elements, {});
1831
+ newElements = { ...result.elements };
1832
+ codeLines.push(...result.codeLines);
1833
+ });
1834
+
1835
+ codePage.insertElements(newElements);
1836
+ codePage.addInfraCommand(
1837
+ functionName,
1838
+ cucumberStep.text,
1839
+ cucumberStep.getVariablesList(),
1840
+ codeLines,
1841
+ false,
1842
+ "recorder"
1843
+ );
1844
+
1845
+ const keyword = (cucumberStep.keywordAlias ?? cucumberStep.keyword).trim();
1846
+ codePage.addCucumberStep(keyword, cucumberStep.getTemplate(), functionName, stepCommands.length);
1847
+ codePage.save();
1848
+
1849
+ await rm(tempFolderPath, { recursive: true, force: true });
1850
+
1851
+ return { code: codePage.fileContent, newStep: true, functionName, mjsFile: codeView.mjsFile };
1852
+ } catch (error) {
1853
+ await rm(tempFolderPath, { recursive: true, force: true });
1854
+ throw error;
1855
+ }
1856
+ }
1583
1857
  async cleanup({ tags }) {
1584
1858
  const noopStep = {
1585
1859
  text: "Noop",
@@ -153,7 +153,7 @@ export class PublishService {
153
153
  constructor(TOKEN) {
154
154
  this.TOKEN = TOKEN;
155
155
  }
156
- async saveScenario({ scenario, featureName, override, branch, isEditing, projectId, env }) {
156
+ async saveScenario({ scenario, featureName, override, branch, isEditing, projectId, env, AICode }) {
157
157
  const runsURL = getRunsServiceBaseURL();
158
158
  const workspaceURL = runsURL.replace("/runs", "/workspace");
159
159
  const url = `${workspaceURL}/publish-recording`;
@@ -166,6 +166,7 @@ export class PublishService {
166
166
  featureName,
167
167
  override,
168
168
  env,
169
+ AICode,
169
170
  },
170
171
  params: {
171
172
  branch,
@@ -73,7 +73,7 @@ export class BVTStepRunner {
73
73
  }
74
74
  }
75
75
 
76
- async copyCodetoTempFolder({ step, parametersMap, tempFolderPath }) {
76
+ async copyCodetoTempFolder({ step, parametersMap, tempFolderPath, AICode }) {
77
77
  if (!fs.existsSync(tempFolderPath)) {
78
78
  fs.mkdirSync(tempFolderPath);
79
79
  }
@@ -84,6 +84,18 @@ export class BVTStepRunner {
84
84
  overwrite: true,
85
85
  recursive: true,
86
86
  });
87
+
88
+ // If AICode is provided, save it as well
89
+ if (AICode) {
90
+ for (const { mjsFileContent, mjsFile } of AICode) {
91
+ const mjsPath = path
92
+ .normalize(mjsFile)
93
+ .split(path.sep)
94
+ .filter((part) => part !== "features")
95
+ .join(path.sep);
96
+ writeFileSync(path.join(tempFolderPath, mjsPath), mjsFileContent);
97
+ }
98
+ }
87
99
  }
88
100
 
89
101
  async writeTempFeatureFile({ step, parametersMap, tempFolderPath, tags }) {
@@ -250,7 +262,7 @@ export class BVTStepRunner {
250
262
  return { result, info };
251
263
  }
252
264
 
253
- async runStep({ step, parametersMap, envPath, tags, config }, bvtContext, options) {
265
+ async runStep({ step, parametersMap, envPath, tags, config, AICode }, bvtContext, options) {
254
266
  // Create a new AbortController for this specific step execution
255
267
  this.#currentStepController = new AbortController();
256
268
  const { signal } = this.#currentStepController;
@@ -304,7 +316,7 @@ export class BVTStepRunner {
304
316
  process.env.tempFeaturesFolderPath = __temp_features_FolderName;
305
317
  process.env.TESTCASE_REPORT_FOLDER_PATH = tempFolderPath;
306
318
 
307
- await this.copyCodetoTempFolder({ step, parametersMap, tempFolderPath });
319
+ await this.copyCodetoTempFolder({ step, parametersMap, tempFolderPath, AICode });
308
320
 
309
321
  // Write abort wrapper code with this step's signal
310
322
  await this.writeWrapperCode(tempFolderPath, signal);
@@ -48,7 +48,7 @@ const replaceLastOccurence = (str, search, replacement) => {
48
48
  return str.substring(0, lastIndex) + replacement + str.substring(lastIndex + search.length);
49
49
  };
50
50
 
51
- const _toRecordingStep = (cmd, stepText) => {
51
+ export const _toRecordingStep = (cmd, stepText) => {
52
52
  switch (cmd.type) {
53
53
  case "hover_element": {
54
54
  return {
@@ -535,7 +535,7 @@ export async function saveRecording({ step, cucumberStep, codePage, projectDir,
535
535
  const isUtilStep = makeStepTextUnique(step, stepsDefinitions);
536
536
 
537
537
  if (isUtilStep) {
538
- if (!step.renamedText) {
538
+ if (!step.renamedText || step.renamedText === step.text) {
539
539
  return;
540
540
  }
541
541
  step.text = step.text.trim();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dev-blinq/cucumber_client",
3
- "version": "1.0.1689-dev",
3
+ "version": "1.0.1691-dev",
4
4
  "description": " ",
5
5
  "main": "bin/index.js",
6
6
  "types": "bin/index.d.ts",