@dev-blinq/cucumber_client 1.0.1456-stage → 1.0.1457-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.
@@ -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();
@@ -884,4 +884,4 @@ const generatePageName = (url) => {
884
884
  return "default";
885
885
  }
886
886
  };
887
- export { generateCode, generatePageName };
887
+ export { generateCode, generatePageName, _generateCodeFromCommand };
@@ -294,6 +294,21 @@ async function BVTRecorderInit({ envName, projectDir, roomId, TOKEN, socket = nu
294
294
  "recorderWindow.cleanup": async (input) => {
295
295
  return recorder.cleanup(input);
296
296
  },
297
+ "recorderWindow.getStepCodeByScenario": async (input) => {
298
+ return await recorder.getStepCodeByScenario(input);
299
+ },
300
+ "recorderWindow.setStepCodeByScenario": async (input) => {
301
+ return await recorder.setStepCodeByScenario(input);
302
+ },
303
+ "recorderWindow.getRecorderContext": async (input) => {
304
+ return await recorder.getContext();
305
+ },
306
+ "recorderWindow.addCommandToStepCode": async (input) => {
307
+ return await recorder.addCommandToStepCode(input);
308
+ },
309
+ "recorderWindow.deleteCommandFromStepCode": async (input) => {
310
+ return await recorder.deleteCommandFromStepCode(input);
311
+ },
297
312
  });
298
313
 
299
314
  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 "node:fs";
4
+ import { rm } from "fs/promises";
4
5
  import path from "node:path";
5
6
  import url from "node:url";
6
7
  import { getImplementedSteps, parseRouteFiles } from "./implemented_steps.js";
7
8
  import { NamesService, PublishService } from "./services.js";
8
9
  import { BVTStepRunner } from "./step_runner.js";
9
10
  import { readFile, writeFile } from "node: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, { getErrorMessage } 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
 
@@ -1257,7 +1269,7 @@ export class BVTRecorder {
1257
1269
  }, 100);
1258
1270
  this.timerId = timerId;
1259
1271
  }
1260
- async runStep({ step, parametersMap, tags, isFirstStep, listenNetwork }, options) {
1272
+ async runStep({ step, parametersMap, tags, isFirstStep, listenNetwork, AICode }, options) {
1261
1273
  const { skipAfter = true, skipBefore = !isFirstStep } = options || {};
1262
1274
 
1263
1275
  const env = path.basename(this.envName, ".json");
@@ -1299,6 +1311,7 @@ export class BVTRecorder {
1299
1311
  envPath: this.envName,
1300
1312
  tags,
1301
1313
  config: this.config,
1314
+ AICode,
1302
1315
  },
1303
1316
  this.bvtContext,
1304
1317
  {
@@ -1328,6 +1341,7 @@ export class BVTRecorder {
1328
1341
  isEditing,
1329
1342
  projectId: path.basename(this.projectDir),
1330
1343
  env: env ?? this.envName,
1344
+ AICode,
1331
1345
  });
1332
1346
  if (res.success) {
1333
1347
  await this.cleanup({ tags: scenario.tags });
@@ -1583,6 +1597,271 @@ export class BVTRecorder {
1583
1597
  }
1584
1598
  return result;
1585
1599
  }
1600
+ async setStepCodeByScenario({
1601
+ function_name,
1602
+ mjs_file_content,
1603
+ user_request,
1604
+ selectedTarget,
1605
+ page_context,
1606
+ AIMemory,
1607
+ steps_context,
1608
+ }) {
1609
+ const runsURL = getRunsServiceBaseURL();
1610
+ const url = `${runsURL}/process-user-request/generate-code-with-context`;
1611
+ try {
1612
+ const result = await axiosClient({
1613
+ url,
1614
+ method: "POST",
1615
+ data: {
1616
+ function_name,
1617
+ mjs_file_content,
1618
+ user_request,
1619
+ selectedTarget,
1620
+ page_context,
1621
+ AIMemory,
1622
+ steps_context,
1623
+ },
1624
+ headers: {
1625
+ Authorization: `Bearer ${this.TOKEN}`,
1626
+ "X-Source": "recorder",
1627
+ },
1628
+ });
1629
+ if (result.status !== 200) {
1630
+ return { success: false, message: "Error while fetching code changes" };
1631
+ }
1632
+ return { success: true, data: result.data };
1633
+ } catch (error) {
1634
+ // @ts-ignore
1635
+ const reason = error?.response?.data?.error || "";
1636
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
1637
+ throw new Error(`Failed to fetch code changes: ${errorMessage} \n ${reason}`);
1638
+ }
1639
+ }
1640
+
1641
+ async getStepCodeByScenario({ featureName, scenarioName, projectId, branch }) {
1642
+ try {
1643
+ const runsURL = getRunsServiceBaseURL();
1644
+ const ssoURL = runsURL.replace("/runs", "/auth");
1645
+ const privateRepoURL = `${ssoURL}/isRepoPrivate?project_id=${projectId}`;
1646
+
1647
+ const isPrivateRepoReq = await axiosClient({
1648
+ url: privateRepoURL,
1649
+ method: "GET",
1650
+ headers: {
1651
+ Authorization: `Bearer ${this.TOKEN}`,
1652
+ "X-Source": "recorder",
1653
+ },
1654
+ });
1655
+
1656
+ if (isPrivateRepoReq.status !== 200) {
1657
+ return { success: false, message: "Error while checking repo privacy" };
1658
+ }
1659
+
1660
+ const isPrivateRepo = isPrivateRepoReq.data.isPrivate ? isPrivateRepoReq.data.isPrivate : false;
1661
+
1662
+ const workspaceURL = runsURL.replace("/runs", "/workspace");
1663
+ const url = `${workspaceURL}/get-step-code-by-scenario`;
1664
+
1665
+ const result = await axiosClient({
1666
+ url,
1667
+ method: "POST",
1668
+ data: {
1669
+ scenarioName,
1670
+ featureName,
1671
+ projectId,
1672
+ isPrivateRepo,
1673
+ branch,
1674
+ },
1675
+ headers: {
1676
+ Authorization: `Bearer ${this.TOKEN}`,
1677
+ "X-Source": "recorder",
1678
+ },
1679
+ });
1680
+ if (result.status !== 200) {
1681
+ return { success: false, message: "Error while getting step code" };
1682
+ }
1683
+ return { success: true, data: result.data.stepInfo };
1684
+ } catch (error) {
1685
+ const reason = error?.response?.data?.error || "";
1686
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
1687
+ throw new Error(`Failed to get step code: ${errorMessage} \n ${reason}`);
1688
+ }
1689
+ }
1690
+ async getContext() {
1691
+ await this.page.waitForLoadState("domcontentloaded");
1692
+ await this.page.waitForSelector("body");
1693
+ await this.page.waitForTimeout(500);
1694
+ return await this.page.evaluate(() => {
1695
+ return document.documentElement.outerHTML;
1696
+ });
1697
+ }
1698
+ async deleteCommandFromStepCode({ scenario, AICode, command }) {
1699
+ if (!AICode || AICode.length === 0) {
1700
+ console.log("No AI code available to delete.");
1701
+ return;
1702
+ }
1703
+
1704
+ const __temp_features_FolderName = "__temp_features" + Math.random().toString(36).substring(2, 7);
1705
+ const tempFolderPath = path.join(this.projectDir, __temp_features_FolderName);
1706
+ process.env.tempFeaturesFolderPath = __temp_features_FolderName;
1707
+ process.env.TESTCASE_REPORT_FOLDER_PATH = tempFolderPath;
1708
+
1709
+ try {
1710
+ await this.stepRunner.copyCodetoTempFolder({ tempFolderPath, AICode });
1711
+ await this.stepRunner.writeWrapperCode(tempFolderPath);
1712
+ const codeView = AICode.find((f) => f.stepName === scenario.step.text);
1713
+
1714
+ if (!codeView) {
1715
+ throw new Error("Step code not found for step: " + scenario.step.text);
1716
+ }
1717
+
1718
+ const functionName = codeView.functionName;
1719
+ const mjsPath = path
1720
+ .normalize(codeView.mjsFile)
1721
+ .split(path.sep)
1722
+ .filter((part) => part !== "features")
1723
+ .join(path.sep);
1724
+ const codePath = path.join(tempFolderPath, mjsPath);
1725
+
1726
+ if (!existsSync(codePath)) {
1727
+ throw new Error("Step code file not found: " + codePath);
1728
+ }
1729
+
1730
+ const codePage = getCodePage(codePath);
1731
+
1732
+ const elements = codePage.getVariableDeclarationAsObject("elements");
1733
+
1734
+ const cucumberStep = getCucumberStep({ step: scenario.step });
1735
+ cucumberStep.text = scenario.step.text;
1736
+ const stepCommands = scenario.step.commands;
1737
+ const cmd = _toRecordingStep(command, scenario.step.name);
1738
+
1739
+ const recording = new Recording();
1740
+ recording.loadFromObject({ steps: stepCommands, step: cucumberStep });
1741
+ const step = { ...recording.steps[0], ...cmd };
1742
+ const result = _generateCodeFromCommand(step, elements, {});
1743
+
1744
+ codePage._removeCommands(functionName, result.codeLines);
1745
+ codePage.removeUnusedElements();
1746
+ codePage.save();
1747
+
1748
+ await rm(tempFolderPath, { recursive: true, force: true });
1749
+
1750
+ return { code: codePage.fileContent, mjsFile: codeView.mjsFile };
1751
+ } catch (error) {
1752
+ await rm(tempFolderPath, { recursive: true, force: true });
1753
+ throw error;
1754
+ }
1755
+ }
1756
+ async addCommandToStepCode({ scenario, AICode }) {
1757
+ if (!AICode || AICode.length === 0) {
1758
+ console.log("No AI code available to add.");
1759
+ return;
1760
+ }
1761
+
1762
+ const __temp_features_FolderName = "__temp_features" + Math.random().toString(36).substring(2, 7);
1763
+ const tempFolderPath = path.join(this.projectDir, __temp_features_FolderName);
1764
+ process.env.tempFeaturesFolderPath = __temp_features_FolderName;
1765
+ process.env.TESTCASE_REPORT_FOLDER_PATH = tempFolderPath;
1766
+
1767
+ try {
1768
+ await this.stepRunner.copyCodetoTempFolder({ tempFolderPath, AICode });
1769
+ await this.stepRunner.writeWrapperCode(tempFolderPath);
1770
+
1771
+ let codeView = AICode.find((f) => f.stepName === scenario.step.text);
1772
+
1773
+ if (codeView) {
1774
+ scenario.step.commands = [scenario.step.commands.pop()];
1775
+ const functionName = codeView.functionName;
1776
+ const mjsPath = path
1777
+ .normalize(codeView.mjsFile)
1778
+ .split(path.sep)
1779
+ .filter((part) => part !== "features")
1780
+ .join(path.sep);
1781
+ const codePath = path.join(tempFolderPath, mjsPath);
1782
+
1783
+ if (!existsSync(codePath)) {
1784
+ throw new Error("Step code file not found: " + codePath);
1785
+ }
1786
+
1787
+ const codePage = getCodePage(codePath);
1788
+ const elements = codePage.getVariableDeclarationAsObject("elements");
1789
+
1790
+ const cucumberStep = getCucumberStep({ step: scenario.step });
1791
+ cucumberStep.text = scenario.step.text;
1792
+ const stepCommands = scenario.step.commands;
1793
+ const cmd = _toRecordingStep(scenario.step.commands[0], scenario.step.name);
1794
+
1795
+ const recording = new Recording();
1796
+ recording.loadFromObject({ steps: stepCommands, step: cucumberStep });
1797
+ const step = { ...recording.steps[0], ...cmd };
1798
+
1799
+ const result = _generateCodeFromCommand(step, elements, {});
1800
+ codePage.insertElements(result.elements);
1801
+
1802
+ codePage._injectOneCommand(functionName, result.codeLines.join("\n"));
1803
+ codePage.save();
1804
+
1805
+ await rm(tempFolderPath, { recursive: true, force: true });
1806
+
1807
+ return { code: codePage.fileContent, newStep: false, mjsFile: codeView.mjsFile };
1808
+ }
1809
+ console.log("Step code not found for step: ", scenario.step.text);
1810
+
1811
+ codeView = AICode[0];
1812
+ const functionName = toMethodName(scenario.step.text);
1813
+ const codeLines = [];
1814
+ const mjsPath = path
1815
+ .normalize(codeView.mjsFile)
1816
+ .split(path.sep)
1817
+ .filter((part) => part !== "features")
1818
+ .join(path.sep);
1819
+ const codePath = path.join(tempFolderPath, mjsPath);
1820
+
1821
+ if (!existsSync(codePath)) {
1822
+ throw new Error("Step code file not found: " + codePath);
1823
+ }
1824
+
1825
+ const codePage = getCodePage(codePath);
1826
+ const elements = codePage.getVariableDeclarationAsObject("elements");
1827
+ let newElements = { ...elements };
1828
+
1829
+ const cucumberStep = getCucumberStep({ step: scenario.step });
1830
+ cucumberStep.text = scenario.step.text;
1831
+ const stepCommands = scenario.step.commands;
1832
+ stepCommands.forEach((command) => {
1833
+ const cmd = _toRecordingStep(command, scenario.step.name);
1834
+
1835
+ const recording = new Recording();
1836
+ recording.loadFromObject({ steps: stepCommands, step: cucumberStep });
1837
+ const step = { ...recording.steps[0], ...cmd };
1838
+ const result = _generateCodeFromCommand(step, elements, {});
1839
+ newElements = { ...result.elements };
1840
+ codeLines.push(...result.codeLines);
1841
+ });
1842
+
1843
+ codePage.insertElements(newElements);
1844
+ codePage.addInfraCommand(
1845
+ functionName,
1846
+ cucumberStep.text,
1847
+ cucumberStep.getVariablesList(),
1848
+ codeLines,
1849
+ false,
1850
+ "recorder"
1851
+ );
1852
+
1853
+ const keyword = (cucumberStep.keywordAlias ?? cucumberStep.keyword).trim();
1854
+ codePage.addCucumberStep(keyword, cucumberStep.getTemplate(), functionName, stepCommands.length);
1855
+ codePage.save();
1856
+
1857
+ await rm(tempFolderPath, { recursive: true, force: true });
1858
+
1859
+ return { code: codePage.fileContent, newStep: true, functionName, mjsFile: codeView.mjsFile };
1860
+ } catch (error) {
1861
+ await rm(tempFolderPath, { recursive: true, force: true });
1862
+ throw error;
1863
+ }
1864
+ }
1586
1865
  async cleanup({ tags }) {
1587
1866
  const noopStep = {
1588
1867
  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,
@@ -72,7 +72,7 @@ export class BVTStepRunner {
72
72
  }
73
73
  }
74
74
 
75
- async copyCodetoTempFolder({ step, parametersMap, tempFolderPath }) {
75
+ async copyCodetoTempFolder({ step, parametersMap, tempFolderPath, AICode }) {
76
76
  if (!fs.existsSync(tempFolderPath)) {
77
77
  fs.mkdirSync(tempFolderPath);
78
78
  }
@@ -83,6 +83,18 @@ export class BVTStepRunner {
83
83
  overwrite: true,
84
84
  recursive: true,
85
85
  });
86
+
87
+ // If AICode is provided, save it as well
88
+ if (AICode) {
89
+ for (const { mjsFileContent, mjsFile } of AICode) {
90
+ const mjsPath = path
91
+ .normalize(mjsFile)
92
+ .split(path.sep)
93
+ .filter((part) => part !== "features")
94
+ .join(path.sep);
95
+ writeFileSync(path.join(tempFolderPath, mjsPath), mjsFileContent);
96
+ }
97
+ }
86
98
  }
87
99
 
88
100
  async writeTempFeatureFile({ step, parametersMap, tempFolderPath, tags }) {
@@ -249,7 +261,7 @@ export class BVTStepRunner {
249
261
  return { result, info };
250
262
  }
251
263
 
252
- async runStep({ step, parametersMap, envPath, tags, config }, bvtContext, options) {
264
+ async runStep({ step, parametersMap, envPath, tags, config, AICode }, bvtContext, options) {
253
265
  // Create a new AbortController for this specific step execution
254
266
  this.#currentStepController = new AbortController();
255
267
  const { signal } = this.#currentStepController;
@@ -303,7 +315,7 @@ export class BVTStepRunner {
303
315
  process.env.tempFeaturesFolderPath = __temp_features_FolderName;
304
316
  process.env.TESTCASE_REPORT_FOLDER_PATH = tempFolderPath;
305
317
 
306
- await this.copyCodetoTempFolder({ step, parametersMap, tempFolderPath });
318
+ await this.copyCodetoTempFolder({ step, parametersMap, tempFolderPath, AICode });
307
319
 
308
320
  // Write abort wrapper code with this step's signal
309
321
  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 {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dev-blinq/cucumber_client",
3
- "version": "1.0.1456-stage",
3
+ "version": "1.0.1457-stage",
4
4
  "description": " ",
5
5
  "main": "bin/index.js",
6
6
  "types": "bin/index.d.ts",