@dev-blinq/cucumber_client 1.0.1421-dev → 1.0.1421-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 (48) hide show
  1. package/bin/assets/bundled_scripts/recorder.js +105 -105
  2. package/bin/assets/preload/css_gen.js +10 -10
  3. package/bin/assets/preload/toolbar.js +27 -29
  4. package/bin/assets/preload/unique_locators.js +1 -1
  5. package/bin/assets/preload/yaml.js +288 -275
  6. package/bin/assets/scripts/aria_snapshot.js +223 -220
  7. package/bin/assets/scripts/dom_attr.js +329 -329
  8. package/bin/assets/scripts/dom_parent.js +169 -174
  9. package/bin/assets/scripts/event_utils.js +94 -94
  10. package/bin/assets/scripts/pw.js +2050 -1949
  11. package/bin/assets/scripts/recorder.js +70 -45
  12. package/bin/assets/scripts/snapshot_capturer.js +147 -147
  13. package/bin/assets/scripts/unique_locators.js +163 -44
  14. package/bin/assets/scripts/yaml.js +796 -783
  15. package/bin/assets/templates/_hooks_template.txt +6 -2
  16. package/bin/assets/templates/utils_template.txt +16 -16
  17. package/bin/client/code_cleanup/find_step_definition_references.js +0 -1
  18. package/bin/client/code_gen/api_codegen.js +2 -2
  19. package/bin/client/code_gen/code_inversion.js +63 -2
  20. package/bin/client/code_gen/function_signature.js +4 -0
  21. package/bin/client/code_gen/page_reflection.js +823 -1003
  22. package/bin/client/code_gen/playwright_codeget.js +25 -3
  23. package/bin/client/cucumber/feature_data.js +2 -2
  24. package/bin/client/cucumber/project_to_document.js +8 -2
  25. package/bin/client/cucumber/steps_definitions.js +19 -3
  26. package/bin/client/cucumber_selector.js +4 -0
  27. package/bin/client/local_agent.js +3 -2
  28. package/bin/client/parse_feature_file.js +23 -26
  29. package/bin/client/playground/projects/env.json +2 -2
  30. package/bin/client/project.js +186 -202
  31. package/bin/client/recorderv3/bvt_init.js +363 -0
  32. package/bin/client/recorderv3/bvt_recorder.js +1008 -47
  33. package/bin/client/recorderv3/implemented_steps.js +2 -0
  34. package/bin/client/recorderv3/index.js +4 -283
  35. package/bin/client/recorderv3/scriptTest.js +1 -1
  36. package/bin/client/recorderv3/services.js +810 -142
  37. package/bin/client/recorderv3/step_runner.js +37 -11
  38. package/bin/client/recorderv3/step_utils.js +495 -39
  39. package/bin/client/recorderv3/update_feature.js +32 -13
  40. package/bin/client/recorderv3/wbr_entry.js +61 -0
  41. package/bin/client/recording.js +1 -0
  42. package/bin/client/upload-service.js +4 -2
  43. package/bin/client/utils/socket_logger.js +1 -1
  44. package/bin/index.js +4 -1
  45. package/bin/logger.js +3 -2
  46. package/bin/min/consoleApi.min.cjs +2 -3
  47. package/bin/min/injectedScript.min.cjs +16 -16
  48. package/package.json +19 -9
@@ -1,1049 +1,869 @@
1
+ // code_page.ts
1
2
  import { writeFileSync, readFileSync, existsSync, mkdirSync } from "fs";
2
- import Walker from "node-source-walk";
3
+ // @ts-expect-error no type defs
4
+ import Walker from "node-source-walk"; // No official types; treated as 'any'
3
5
  import path from "node:path";
4
6
  import vm from "vm";
5
7
  import logger from "../../logger.js";
6
8
  import { convertToIdentifier } from "./utils.js";
7
9
  import prettier from "prettier";
8
- import url from "url";
10
+ import * as url from "url";
9
11
  import { getDefaultPrettierConfig } from "../code_cleanup/utils.js";
10
12
  const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
11
- const CodeStatus = {
12
- ADD: "add",
13
- NO_CHANGE: "no_change",
14
- UPDATED: "updated",
15
- };
13
+ export var CodeStatus;
14
+ (function (CodeStatus) {
15
+ CodeStatus["ADD"] = "add";
16
+ CodeStatus["NO_CHANGE"] = "no_change";
17
+ CodeStatus["UPDATED"] = "updated";
18
+ })(CodeStatus || (CodeStatus = {}));
16
19
  function escapeForComment(text) {
17
- return text
18
- .replace(/\\/g, "\\\\") // Escape backslashes
19
- .replace(/\*\//g, "*\\/"); // Escape comment-closing sequence
20
+ return text
21
+ .replace(/\\/g, "\\\\") // Escape backslashes
22
+ .replace(/\*\//g, "*\\/"); // Escape comment-closing sequence
20
23
  }
21
24
  function unescapeFromComment(text) {
22
- return text
23
- .replace(/\*\\/g, "*/") // Unescape comment-closing sequence
24
- .replace(/\\\\/g, "\\"); // Unescape backslashes
25
+ return text.replace(/\*\\/g, "*/").replace(/\\\\/g, "\\");
25
26
  }
26
27
  let ai_config = null;
27
28
  export function getAiConfig() {
28
- if (ai_config) {
29
+ if (ai_config)
30
+ return ai_config;
31
+ try {
32
+ ai_config = JSON.parse(readFileSync("ai_config.json", "utf8"));
33
+ }
34
+ catch {
35
+ ai_config = {};
36
+ }
37
+ if (!ai_config.locatorsMetadataDir) {
38
+ ai_config.locatorsMetadataDir = "features/step_definitions/locators";
39
+ }
29
40
  return ai_config;
30
- }
31
- try {
32
- ai_config = JSON.parse(readFileSync("ai_config.json", "utf8"));
33
- } catch (e) {
34
- ai_config = {};
35
- }
36
- if (!ai_config.locatorsMetadataDir) {
37
- ai_config.locatorsMetadataDir = "features/step_definitions/locators";
38
- }
39
- return ai_config;
40
41
  }
41
- class CodePage {
42
- constructor(sourceFileName = null) {
43
- this.sourceFileName = sourceFileName;
44
- this.cleanFileName = sourceFileName && path.basename(sourceFileName).split(".")[0];
45
- this._init();
46
- }
47
- _init() {
48
- this.nextCodePartIndex = 0;
49
- this.fileContent = null;
50
- this.imports = [];
51
- this.importsObjects = [];
52
- //this.pageClass = null;
53
- //this.classProperties = [];
54
- this.methods = [];
55
- this.variables = [];
56
- this.expressions = [];
57
- this.orderedCodeParts = [];
58
- this.cucumberCalls = [];
59
- }
60
- async save() {
61
- if (this.sourceFileName !== null) {
62
- // format the code before saving
63
- try {
64
- const fileContentNew = await prettier.format(this.fileContent, getDefaultPrettierConfig());
65
- this._init();
66
- this.generateModel(fileContentNew);
67
- } catch (e) {
68
- logger.info("failed to format the code");
69
- logger.debug(e);
70
- }
71
- if (!existsSync(this.sourceFileName)) {
72
- const stepDefinitionFolderPath = path.dirname(this.sourceFileName);
73
- if (!existsSync(stepDefinitionFolderPath)) {
74
- mkdirSync(stepDefinitionFolderPath, { recursive: true });
42
+ function getPath(comment) {
43
+ const index = comment.indexOf("@path=");
44
+ if (index === -1)
45
+ return null;
46
+ return comment.substring(index).split("\n")[0].substring(6);
47
+ }
48
+ class CodePart {
49
+ node;
50
+ name = null;
51
+ keyword = null;
52
+ pattern = null;
53
+ methodName = null;
54
+ fileContent;
55
+ start = -1;
56
+ end = -1;
57
+ codePart = "";
58
+ index;
59
+ path;
60
+ stepType;
61
+ constructor(node, fileContent, index, codePart = null) {
62
+ this.node = node;
63
+ this.fileContent = fileContent;
64
+ if (this.node !== null) {
65
+ this.start = this.node.loc.start.index;
66
+ if (this.node.leadingComments && this.node.leadingComments.length > 0) {
67
+ this.start = this.node.leadingComments[0].start;
68
+ this.path = getPath(this.node.leadingComments[0].value);
69
+ }
70
+ this.end = this.node.loc.end.index;
71
+ this.codePart = fileContent.substring(this.start, this.end);
72
+ // NOTE: the original code checked this.declarations etc.; keeping equivalent behavior is tricky here.
73
+ // Those properties exist on VariableDeclaration nodes, not on CodePart.
74
+ // We leave 'name' to be assigned in CodePage.addCodePart where appropriate.
75
75
  }
76
- }
77
- writeFileSync(this.sourceFileName, this.fileContent, "utf8");
78
- return true;
79
- } else {
80
- logger.error("sourceFileName is null");
81
- return false;
82
- }
83
- }
84
-
85
- generateModel(fileContent = null) {
86
- if (fileContent !== null) {
87
- this.fileContent = fileContent;
88
- } else {
89
- if (!existsSync(this.sourceFileName)) {
90
- const templateFilePath = path.join(__dirname, "../../assets", "templates", "page_template.txt");
91
- const content = readFileSync(templateFilePath, "utf8");
92
- this.fileContent = content;
93
- } else {
94
- this.fileContent = readFileSync(this.sourceFileName, "utf8");
95
- }
76
+ else {
77
+ this.codePart = codePart ?? "";
78
+ this.start = -1;
79
+ this.end = -1;
80
+ }
81
+ this.index = index;
82
+ }
83
+ getCode() {
84
+ if (this.start === -1 || this.end === -1)
85
+ return null;
86
+ return this.fileContent.substring(this.start, this.end);
87
+ }
88
+ equals(other) {
89
+ const maskedA = this.codePart
90
+ .replace(this.methodName ?? "", "")
91
+ .replace(this.name ?? "", "")
92
+ .replaceAll(/\s/g, "")
93
+ .trim();
94
+ const maskedB = other.codePart
95
+ .replace(other.methodName ?? "", "")
96
+ .replace(other.name ?? "", "")
97
+ .replaceAll(/\s/g, "")
98
+ .trim();
99
+ return { status: maskedA === maskedB, line: maskedA, lineRef: maskedB };
100
+ }
101
+ getMethodType() {
102
+ const pattern = /\s+source:\s*([a-zA-Z]+)/;
103
+ const match = this.codePart.match(pattern);
104
+ return match ? match[1] : null;
105
+ }
106
+ getVariableDeclerationObject(declaratinName) {
107
+ if (!this.node || !this.node.body || !this.node.body.body || this.node.body.body.length === 0)
108
+ return null;
109
+ const body = this.node.body.body;
110
+ for (let i = 0; i < body.length; i++) {
111
+ const code = body[i];
112
+ if (code.type === "VariableDeclaration") {
113
+ if (!code.declarations || code.declarations.length === 0)
114
+ continue;
115
+ const declaration = code.declarations[0];
116
+ if (declaration.id && declaration.id.name === declaratinName) {
117
+ const declerationCode = this.fileContent.substring(declaration.start, declaration.end);
118
+ const vmContext = { console, value: null };
119
+ vm.createContext(vmContext);
120
+ vm.runInContext("value = " + declerationCode + ";", vmContext);
121
+ return vmContext.value;
122
+ }
123
+ }
124
+ }
125
+ return null;
96
126
  }
97
- const walker = new Walker();
98
- let rootNode = null;
99
- walker.walk(this.fileContent, (node) => {
100
- if (
101
- node.constructor.name === "SourceLocation" ||
102
- node.constructor.name === "Position" ||
103
- node.constructor.name === "Object"
104
- ) {
105
- return;
106
- }
107
- switch (node.type) {
108
- case "ImportDeclaration":
109
- this.addCodePart(node);
110
- if (rootNode === null) {
111
- rootNode = node;
112
- }
113
- break;
114
- case "ExpressionStatement":
115
- case "VariableDeclaration":
116
- if (node.parent && node.parent.filter && node.parent.filter((n) => n === rootNode).length > 0) {
117
- this.addCodePart(node);
118
- }
119
- break;
120
- case "ClassDeclaration":
121
- case "ClassProperty":
122
- this.addCodePart(node);
123
- break;
124
- case "FunctionDeclaration":
125
- this.addCodePart(node);
126
- break;
127
- default:
128
- //console.log("node", node);
129
- break;
130
- }
127
+ }
128
+ const cutSubString = (str, substrings) => {
129
+ let result = str;
130
+ let offset = 0;
131
+ substrings.forEach(([start, end]) => {
132
+ result = result.substring(0, start - offset) + result.substring(end - offset);
133
+ offset += end - start;
131
134
  });
132
- }
133
- addCodePart(node) {
134
- const codePart = new CodePart(node, this.fileContent, this.nextCodePartIndex);
135
- this.nextCodePartIndex++;
136
- if (node.type === "ImportDeclaration") {
137
- this.imports.push(codePart);
138
- /*
139
- Example of import object
140
- this.imports[1].node.specifiers[0].type
141
- 'ImportSpecifier'
142
- this.imports[2].node.specifiers[0].type
143
- 'ImportDefaultSpecifier'
144
- this.imports[2].node.specifiers[0].local.name
145
- 'fs'
146
- this.imports[1].node.specifiers[0].local.name
147
- 'closeContext'
148
- this.imports[1].node.specifiers[0].imported.name
149
- 'closeContext'
150
- this.imports[1].node.source.value
151
- 'automation_model'
152
- this.imports[2].node.source.value
153
- 'fs'
154
- */
155
- // extract the imports and initialize the importsObjects, create an object for each import specifiers
156
- if (node.specifiers) {
157
- const importsObjects = node.specifiers.map((specifier) => {
158
- const importObject = {
159
- type: specifier.type,
160
- local: specifier.local.name,
161
- imported: specifier.imported ? specifier.imported.name : null,
162
- source: node.source.value,
163
- };
164
- return importObject;
165
- });
166
- this.importsObjects.push(...importsObjects);
167
- }
168
- } else if (node.type === "ExpressionStatement") {
169
- if (
170
- node.expression &&
171
- node.expression.type === "CallExpression" &&
172
- node.expression.callee &&
173
- node.expression.arguments &&
174
- (node.expression.arguments.length === 2 || node.expression.arguments.length === 3) &&
175
- (node.expression.callee.name === "Given" ||
176
- node.expression.callee.name === "When" ||
177
- node.expression.callee.name === "Then" ||
178
- node.expression.callee.name === "defineStep")
179
- ) {
180
- codePart.keyword = node.expression.callee.name;
181
- codePart.pattern = node.expression.arguments[0].value
182
- ? node.expression.arguments[0].value
183
- : node.expression.arguments[0].pattern;
184
- codePart.methodName = node.expression.arguments[node.expression.arguments.length - 1].name;
185
- codePart.stepType = node.expression.callee.name;
186
- this.cucumberCalls.push(codePart);
187
- }
188
- this.expressions.push(codePart);
189
- } else if (node.type === "ClassDeclaration") {
190
- //this.pageClass = codePart;
191
- } else if (node.type === "ClassProperty") {
192
- //this.classProperties.push(codePart);
193
- } else if (node.type === "VariableDeclaration") {
194
- if (
195
- node.declarations &&
196
- node.declarations.length > 0 &&
197
- node.declarations[0].init &&
198
- node.declarations[0].init.type === "ArrowFunctionExpression"
199
- ) {
200
- codePart.name = node.declarations[0].id.name;
201
- this.methods.push(codePart);
202
- } else {
203
- this.variables.push(codePart);
204
- }
205
- } else if (node.type === "FunctionDeclaration" && node.id) {
206
- this.methods.push(codePart);
207
- codePart.name = node.id.name;
135
+ return result;
136
+ };
137
+ export class CodePage {
138
+ sourceFileName;
139
+ cleanFileName;
140
+ nextCodePartIndex;
141
+ fileContent;
142
+ imports;
143
+ importsObjects;
144
+ methods;
145
+ variables;
146
+ expressions;
147
+ orderedCodeParts;
148
+ cucumberCalls;
149
+ constructor(sourceFileName = null) {
150
+ this.sourceFileName = sourceFileName;
151
+ this.cleanFileName = sourceFileName && path.basename(sourceFileName).split(".")[0];
152
+ this._init();
208
153
  }
209
- }
210
- collectAllTemplates() {
211
- const templates = [];
212
- if (this.cucumberCalls.length > 0) {
213
- for (let i = 0; i < this.cucumberCalls.length; i++) {
214
- const cucumberCall = this.cucumberCalls[i];
215
- let pattern = cucumberCall.pattern;
216
- let methodName = cucumberCall.methodName;
217
- let foundMethod = false;
218
- let params = [];
219
- let stepType = cucumberCall.stepType;
220
- let firstFind = true;
221
- let stepPaths = [];
222
- for (let j = 0; j < this.methods.length; j++) {
223
- const method = this.methods[j];
224
- if (method.name === methodName) {
225
- if (firstFind) {
226
- foundMethod = true;
227
- let paramsObj = method.node.params;
228
- if (paramsObj && paramsObj.length > 0) {
229
- params = paramsObj.map((param) => param.name);
230
- }
231
- firstFind = false;
154
+ _init() {
155
+ this.nextCodePartIndex = 0;
156
+ this.fileContent = ""; // will be set in generateModel
157
+ this.imports = [];
158
+ this.importsObjects = [];
159
+ this.methods = [];
160
+ this.variables = [];
161
+ this.expressions = [];
162
+ this.orderedCodeParts = [];
163
+ this.cucumberCalls = [];
164
+ }
165
+ async save() {
166
+ if (this.sourceFileName !== null) {
167
+ try {
168
+ const fileContentNew = await prettier.format(this.fileContent, getDefaultPrettierConfig());
169
+ this._init();
170
+ this.generateModel(fileContentNew);
171
+ }
172
+ catch (e) {
173
+ logger.error("failed to format the code");
174
+ logger.debug(e);
175
+ }
176
+ if (!existsSync(this.sourceFileName)) {
177
+ const stepDefinitionFolderPath = path.dirname(this.sourceFileName);
178
+ if (!existsSync(stepDefinitionFolderPath)) {
179
+ mkdirSync(stepDefinitionFolderPath, { recursive: true });
180
+ }
232
181
  }
233
- stepPaths.push(method.path);
234
- }
182
+ writeFileSync(this.sourceFileName, this.fileContent, "utf8");
183
+ return true;
235
184
  }
236
- if (foundMethod) {
237
- templates.push({ pattern, methodName, params, stepType, paths: stepPaths });
185
+ else {
186
+ logger.error("sourceFileName is null");
187
+ return false;
238
188
  }
239
- }
240
189
  }
241
- return templates;
242
- }
243
- getExpectedTimeout(expectedNumofCmds, finalTimeout) {
244
- const timeoutNum = parseFloat(finalTimeout);
245
- if (finalTimeout && !isNaN(timeoutNum)) {
246
- return -1;
247
- }
248
- return expectedNumofCmds * 60 * 1000;
249
- }
250
- addCucumberStep(type, cucumberLine, method, expectedNumofCmds, finalTimeout) {
251
- const result = {};
252
- let code = "\n";
253
- code += `${type}(${JSON.stringify(cucumberLine)}, ${expectedNumofCmds ? `{ timeout: ${this.getExpectedTimeout(expectedNumofCmds, finalTimeout)}}, ` : ""}${method});\n`;
254
- let existCodePart = null;
255
- for (let i = 0; i < this.cucumberCalls.length; i++) {
256
- if (
257
- this.cucumberCalls[i].pattern === cucumberLine
258
- //this.cucumberCalls[i].methodName === method &&
259
- //this.cucumberCalls[i].keyword === type
260
- ) {
261
- existCodePart = this.cucumberCalls[i];
262
- break;
263
- }
264
- }
265
- result.cucumberLine = cucumberLine;
266
- if (existCodePart) {
267
- if (existCodePart.keyword === type || existCodePart.methodName === method) {
268
- logger.debug(`step ${cucumberLine} already exist with the same code`);
269
- result.status = CodeStatus.NO_CHANGE;
270
- return result;
271
- }
272
- result.status = CodeStatus.UPDATED;
273
- result.oldCode = existCodePart.codePart.trim();
274
- result.newCode = code.trim();
275
- logger.debug(`conflict in step ${result.cucumberLine}}`);
276
- logger.debug("old code\n", result.oldCode);
277
- logger.debug("new code\n", result.newCode);
278
- this._replaceCode(code, existCodePart.start, existCodePart.end);
279
- return result;
280
- } else {
281
- let injectIndex = this._getCucumberInjectionIndex();
282
- this._insertCode(code, injectIndex);
283
- result.status = CodeStatus.ADD;
284
- result.newCode = code.trim();
285
- return result;
286
- }
287
- }
288
- addInfraCommand(methodName, description, stepVarables, stepCodeLines, protectStep = false, source = null, path = "") {
289
- let code = "\n";
290
- code += "/**\n";
291
- code += ` * ${description}\n`;
292
- //const keys = Object.keys(stepParameters);
293
- let stepParametersMap = {};
294
- // remove duplicates
295
- let i = 1;
296
- stepVarables.forEach((key) => {
297
- if (stepParametersMap[key]) {
298
- while (stepParametersMap[`${key}${i}`]) {
299
- i++;
190
+ generateModel(fileContent = null) {
191
+ if (fileContent !== null) {
192
+ this.fileContent = fileContent;
300
193
  }
301
- key = `${key}${i}`;
302
- }
303
- stepParametersMap[key] = key;
304
- });
305
- let stepVarablesNoDuplications = Object.keys(stepParametersMap).map((key) => "_" + convertToIdentifier(key));
306
- if (stepVarables.length > stepVarablesNoDuplications.length) {
307
- const varName = "dummy";
308
- i = 1;
309
- while (stepVarablesNoDuplications.length !== stepVarables.length) {
310
- if (!stepVarablesNoDuplications.includes(`${varName}${i}`)) {
311
- stepVarablesNoDuplications.push(`${varName}${i}`);
194
+ else {
195
+ if (!this.sourceFileName || !existsSync(this.sourceFileName)) {
196
+ const templateFilePath = path.join(__dirname, "../../assets", "templates", "page_template.txt");
197
+ const content = readFileSync(templateFilePath, "utf8");
198
+ this.fileContent = content;
199
+ }
200
+ else {
201
+ this.fileContent = readFileSync(this.sourceFileName, "utf8");
202
+ }
312
203
  }
313
- i++;
314
- }
315
- }
316
-
317
- stepVarablesNoDuplications.forEach((key) => {
318
- code += ` * @param {string} ${key} ${key.replaceAll("_", " ")}\n`;
319
- });
320
- let tags = [];
321
- if (protectStep) {
322
- tags.push("@protect");
323
- }
324
- if (source) {
325
- tags.push(`@${source}`);
326
- }
327
- if (tags.length > 0) {
328
- code += ` * ${tags.join(" ")}\n`;
329
- }
330
- if (path !== null) {
331
- code += ` * @path=${escapeForComment(path)}\n`;
332
- }
333
- code += " */\n";
334
- code += `async function ${methodName} (${stepVarablesNoDuplications.join(", ")}){\n`;
335
- code += `// source: ${source}\n`;
336
- code += `// implemented_at: ${new Date().toISOString()}\n`;
337
- if (stepCodeLines.length > 0) {
338
- code += ` const _params = { ${stepVarablesNoDuplications.join(", ")} };\n`;
339
- } else {
340
- code += " const _params = {};\n";
341
- }
342
-
343
- // code += " await navigate(path);\n";
344
- stepCodeLines.forEach((line) => {
345
- code += ` ${line}\n`;
346
- });
347
- code += "}\n";
348
-
349
- return this._injectMethod(methodName, code);
350
- }
351
-
352
- _injectMethod(methodName, code) {
353
- const result = {};
354
- code = code.trim();
355
- let existMethod = null;
356
- for (let i = 0; i < this.methods.length; i++) {
357
- if (this.methods[i].name === methodName) {
358
- existMethod = this.methods[i];
359
- break;
360
- }
361
- }
362
- result.methodName = methodName;
363
- if (existMethod) {
364
- let oldCode = existMethod.codePart.trim();
365
- if (oldCode === code) {
366
- logger.debug(`method ${methodName} already exist with the same code`);
367
- result.status = CodeStatus.NO_CHANGE;
368
- return result;
369
- }
370
- logger.debug(`conflict in method ${methodName}}`);
371
- logger.debug("old code\n", oldCode);
372
- logger.debug("new code\n", code.trim());
373
- this._replaceCode(code, existMethod.start, existMethod.end);
374
- result.status = CodeStatus.UPDATED;
375
- result.oldCode = oldCode;
376
- result.newCode = code;
377
- return result;
378
- } else {
379
- let injectIndex = this._getMethodInjectionIndex();
380
- this._insertCode(code, injectIndex);
381
- result.status = CodeStatus.ADD;
382
- result.newCode = code;
383
- return result;
384
- }
385
- }
386
- isHumanMethod(methodName) {
387
- // check if the fileSourceName end with utils.mjs
388
- if (path.basename(this.sourceFileName) === "utils.mjs") {
389
- return true;
390
- }
391
- let existMethod = null;
392
- for (let i = 0; i < this.methods.length; i++) {
393
- if (this.methods[i].name === methodName) {
394
- existMethod = this.methods[i];
395
- break;
396
- }
397
- }
398
- if (!existMethod) {
399
- return false;
400
- }
401
- const methodCode = existMethod.getCode();
402
- if (methodCode && (methodCode.includes("@protect") || methodCode.includes("@human"))) {
403
- return true;
404
- }
405
- return false;
406
- }
407
-
408
- _insertCode(code, injectIndex) {
409
- let newFileContent =
410
- this.fileContent.substring(0, injectIndex) + "\n" + code + this.fileContent.substring(injectIndex);
411
-
412
- this._init();
413
- try {
414
- this.generateModel(newFileContent);
415
- } catch (e) {
416
- logger.error("new code ####\n" + newFileContent);
417
- throw e;
418
- }
419
- }
420
-
421
- _elementToCode(key, element) {
422
- let code = "";
423
- code += ` ${key}:${JSON.stringify(element)}`;
424
-
425
- return code;
426
- }
427
-
428
- _removeScoresFromElement(element) {
429
- if (!element || !element.locators) {
430
- return element;
431
- }
432
- // clone the element
433
- element = JSON.parse(JSON.stringify(element));
434
- for (let i = 0; i < element.locators.length; i++) {
435
- let locator = element.locators[i];
436
- if (locator.score) {
437
- delete locator.score;
438
- }
439
- }
440
- return element;
441
- }
442
- insertElements(elements) {
443
- let code = "const elements = {\n";
444
- const keys = Object.keys(elements);
445
- const lines = [];
446
- keys.forEach((key) => {
447
- let element = this._removeScoresFromElement(elements[key]);
448
- lines.push(this._elementToCode(key, element));
449
- });
450
- code += lines.join(",\n");
451
- code += "\n};";
452
- let startEnd = this._getVariableStartEnd("elements");
453
- this._replaceCode(code, startEnd[0], startEnd[1]);
454
- }
455
- mergeSimilarElements() {
456
- const elements = this.getVariableDeclarationAsObject("elements");
457
- if (elements === null) {
458
- return;
459
- }
460
-
461
- const keys = Object.keys(elements);
462
-
463
- // Ensure each element has element_key
464
- for (let i = 0; i < keys.length; i++) {
465
- const key = keys[i];
466
- if (!elements[key].element_key) {
467
- elements[key].element_key = key;
468
- }
204
+ const walker = new Walker();
205
+ let rootNode = null;
206
+ walker.walk(this.fileContent, (node) => {
207
+ if (node.constructor.name === "SourceLocation" ||
208
+ node.constructor.name === "Position" ||
209
+ node.constructor.name === "Object") {
210
+ return;
211
+ }
212
+ switch (node.type) {
213
+ case "ImportDeclaration":
214
+ this.addCodePart(node);
215
+ if (rootNode === null) {
216
+ rootNode = node;
217
+ }
218
+ break;
219
+ case "ExpressionStatement":
220
+ case "VariableDeclaration":
221
+ if (node.parent && node.parent.filter && node.parent.filter((n) => n === rootNode).length > 0) {
222
+ this.addCodePart(node);
223
+ }
224
+ break;
225
+ case "ClassDeclaration":
226
+ case "ClassProperty":
227
+ case "FunctionDeclaration":
228
+ this.addCodePart(node);
229
+ break;
230
+ default:
231
+ break;
232
+ }
233
+ });
469
234
  }
470
-
471
- const replacedKeys = {};
472
- const mergedElements = {};
473
-
474
- // Walk from end so "later" keys win (your intended behavior)
475
- for (let i = keys.length - 1; i >= 0; i--) {
476
- const currentElement = elements[keys[i]];
477
- let foundMatch = false;
478
-
479
- for (let j = i - 1; j >= 0; j--) {
480
- const nextElement = elements[keys[j]];
481
-
482
- if (this._isSimilarElement(currentElement, nextElement)) {
483
- // Keep currentElement, replace earlier key with current's key
484
- mergedElements[currentElement.element_key] = currentElement;
485
- replacedKeys[nextElement.element_key] = currentElement.element_key;
486
- foundMatch = true;
487
-
488
- // Remove the earlier key so we don't carry it forward
489
- keys.splice(j, 1);
490
-
491
- // Since we removed an index before i, adjust i so it still points to currentElement
492
- i--;
493
-
494
- // IMPORTANT: compensate for splice so the loop’s j-- lands on the same logical next item
495
- j++;
235
+ addCodePart(node) {
236
+ const codePart = new CodePart(node, this.fileContent, this.nextCodePartIndex);
237
+ this.nextCodePartIndex++;
238
+ if (node.type === "ImportDeclaration") {
239
+ this.imports.push(codePart);
240
+ // Build ImportObject(s)
241
+ if (node.specifiers) {
242
+ const importsObjects = node.specifiers.map((specifier) => {
243
+ const importObject = {
244
+ type: specifier.type,
245
+ local: specifier.local.name,
246
+ imported: specifier.imported ? specifier.imported.name : null,
247
+ source: node.source.value,
248
+ };
249
+ return importObject;
250
+ });
251
+ this.importsObjects.push(...importsObjects);
252
+ }
496
253
  }
497
- }
498
-
499
- if (!foundMatch) {
500
- mergedElements[currentElement.element_key] = currentElement;
501
- }
502
- }
503
-
504
- // If no replacements occurred, nothing else to do
505
- if (Object.keys(replacedKeys).length === 0) {
506
- return;
507
- }
508
-
509
- // Replace all element references in file content
510
- for (const key in replacedKeys) {
511
- const regexp = new RegExp(`elements\\[\\s*["']${key}["']\\s*\\]`, "g");
512
- this.fileContent = this.fileContent.replace(regexp, () => {
513
- return `elements["${replacedKeys[key]}"]`;
514
- });
515
- }
516
-
517
- // Save merged elements back
518
- this.insertElements(mergedElements);
519
- }
520
-
521
- //const regexp = new RegExp(`elements\\[\\s*["']${key}["']\\s*\\]`, "g");
522
- /*
523
- element shape example:
524
- {
525
- locators: [
526
- { css: 'internal:text="YES Subscription"s', priority: 1 },
527
- { css: 'internal:text="YES Subscription"i', priority: 1 },
528
- { css: 'h3 >> internal:has-text="YES Subscription"i', priority: 1 },
529
- { css: 'h3 >> internal:has-text=/^YES Subscription$/', priority: 1 },
530
- { css: 'internal:role=heading[name="YES Subscription"s]', priority: 1 },
531
- ],
532
- element_name: "YES Subscription heading",
533
- element_key: "heading_yes_subscription",
534
- }
535
- */
536
-
537
- _isSimilarElement(element1, element2) {
538
- if (!element1 || !element2) return false;
539
-
540
- const locs1 = Array.isArray(element1.locators) ? element1.locators : [];
541
- const locs2 = Array.isArray(element2.locators) ? element2.locators : [];
542
-
543
- // If both empty, fallback to false (names/keys already handled)
544
- if (locs1.length === 0 && locs2.length === 0) return false;
545
- if (locs1.length === 0 || locs2.length === 0) return false;
546
-
547
- // Decide which set is "less" (subset candidate) and "more" (superset candidate)
548
- const moreLocatorsElement = locs1.length >= locs2.length ? locs1 : locs2;
549
- const lessLocatorsElement = locs1.length >= locs2.length ? locs2 : locs1;
550
-
551
- // Helper: check if all key/value pairs in 'needle' exist identically in 'hay' (superset)
552
- const locatorIncludes = (hay, needle) => {
553
- if (hay == null || needle == null) return false;
554
- // Only consider known keys; ignore undefined values
555
- // ignore priority
556
- const KEYS = ["css", "index", "text", "climb"];
557
- for (const k of KEYS) {
558
- if (needle[k] !== undefined) {
559
- if (!(k in hay)) return false;
560
- // Strict equality comparison for predictability
561
- if (hay[k] !== needle[k]) return false;
254
+ else if (node.type === "ExpressionStatement") {
255
+ if (node.expression &&
256
+ node.expression.type === "CallExpression" &&
257
+ node.expression.callee &&
258
+ node.expression.arguments &&
259
+ (node.expression.arguments.length === 2 || node.expression.arguments.length === 3) &&
260
+ (node.expression.callee.name === "Given" ||
261
+ node.expression.callee.name === "When" ||
262
+ node.expression.callee.name === "Then" ||
263
+ node.expression.callee.name === "defineStep")) {
264
+ codePart.keyword = node.expression.callee.name;
265
+ codePart.pattern = node.expression.arguments[0].value
266
+ ? node.expression.arguments[0].value
267
+ : node.expression.arguments[0].pattern;
268
+ codePart.methodName = node.expression.arguments[node.expression.arguments.length - 1].name;
269
+ codePart.stepType = node.expression.callee.name;
270
+ this.cucumberCalls.push(codePart);
271
+ }
272
+ this.expressions.push(codePart);
273
+ }
274
+ else if (node.type === "VariableDeclaration") {
275
+ if (node.declarations &&
276
+ node.declarations.length > 0 &&
277
+ node.declarations[0].init &&
278
+ node.declarations[0].init.type === "ArrowFunctionExpression") {
279
+ codePart.name = node.declarations[0].id.name;
280
+ this.methods.push(codePart);
281
+ }
282
+ else {
283
+ this.variables.push(codePart);
284
+ }
285
+ }
286
+ else if (node.type === "FunctionDeclaration" && node.id) {
287
+ this.methods.push(codePart);
288
+ codePart.name = node.id.name;
562
289
  }
563
- }
564
- return true;
565
- };
566
-
567
- // For every locator in the smaller set, we require a matching superset locator in the larger set
568
- for (const less of lessLocatorsElement) {
569
- const match = moreLocatorsElement.some((more) => locatorIncludes(more, less));
570
- if (!match) return false;
571
- }
572
-
573
- return true;
574
- }
575
- removeUnusedElements() {
576
- const usedElementsKeys = this.getUsedElementsKeys();
577
- const elements = this.getVariableDeclarationAsObject("elements");
578
- const keys = Object.keys(elements);
579
- const unusedKeys = keys.filter((key) => !usedElementsKeys.includes(key));
580
- if (unusedKeys.length === 0) {
581
- return;
582
- }
583
- for (let i = 0; i < unusedKeys.length; i++) {
584
- delete elements[unusedKeys[i]];
585
- }
586
- this.insertElements(elements);
587
- }
588
-
589
- getUsedElementsKeys() {
590
- const regexp = /elements\[[\n\r\W]*"([^"]+)"[\n\r\W]*\]/g;
591
- const keys = [];
592
- let match;
593
- while ((match = regexp.exec(this.fileContent)) !== null) {
594
- keys.push(match[1]);
595
- }
596
- return keys;
597
- }
598
- _replaceCode(code, injectIndexStart, injectIndexEnd) {
599
- let newFileContent =
600
- this.fileContent.substring(0, injectIndexStart) + code + this.fileContent.substring(injectIndexEnd);
601
- this._init();
602
- this.generateModel(newFileContent);
603
- }
604
- _getMethodInjectionIndex() {
605
- return this.fileContent.length;
606
- }
607
- _getCucumberInjectionIndex() {
608
- return this.fileContent.length;
609
- }
610
- addLocatorsMetadata(locatorsMetadata) {
611
- // create a file name based on the source file name replace .mjs with .json
612
- let locatorsMetadataFileName = this.sourceFileName.replace(".mjs", ".json");
613
- const config = getAiConfig();
614
- if (config && config.locatorsMetadataDir) {
615
- // if config.locatorsMetadataDir is set, use it to create the file path
616
- locatorsMetadataFileName = path.join(config.locatorsMetadataDir, path.basename(locatorsMetadataFileName));
617
- // check if the directory exists, if not create it
618
- if (!existsSync(path.dirname(locatorsMetadataFileName))) {
619
- mkdirSync(path.dirname(locatorsMetadataFileName), { recursive: true });
620
- }
621
- }
622
- let metadata = {};
623
- // try to read the file to metadata, protect with try catch
624
- try {
625
- // if the file exist read the content and parse it
626
- if (existsSync(locatorsMetadataFileName)) {
627
- metadata = JSON.parse(readFileSync(locatorsMetadataFileName, "utf8"));
628
- }
629
- } catch (e) {
630
- logger.info("failed to read locators metadata file", locatorsMetadataFileName);
631
- }
632
- // merge the locatorsMetadata with the metadata
633
- // go over all the keys in locatorsMetadata and add them to metadata
634
- const keys = Object.keys(locatorsMetadata);
635
- keys.forEach((key) => {
636
- metadata[key] = locatorsMetadata[key];
637
- });
638
- // save the metadata back to the file
639
- try {
640
- writeFileSync(locatorsMetadataFileName, JSON.stringify(metadata, null, 2), "utf8");
641
- } catch (e) {
642
- logger.info("failed to write locators metadata file", locatorsMetadataFileName);
643
- }
644
- }
645
- _getVariableStartEnd(variableName) {
646
- const codePart = this.variables.filter((cp) => cp.node.declarations[0].id.name === variableName)[0];
647
- if (codePart === undefined) {
648
- return null;
649
- }
650
- return [codePart.node.start, codePart.node.end];
651
- }
652
- getVariableDeclarationCode(variableName) {
653
- const codePart = this.variables.filter((cp) => cp.node.declarations[0].id.name === variableName)[0];
654
- if (codePart === undefined) {
655
- return null;
656
- }
657
- return codePart.codePart;
658
- }
659
- getVariableDeclarationAsObject(variableName) {
660
- const code = this.getVariableDeclarationCode(variableName);
661
- if (code === null) {
662
- return null;
663
- }
664
- let value = null;
665
- let vmContext = {
666
- console: console,
667
- value: value,
668
- };
669
- vm.createContext(vmContext);
670
- vm.runInContext(code + "\nvalue = " + variableName + ";", vmContext);
671
- //console.log("value", vmContext.value);
672
- return vmContext.value;
673
- }
674
- getName() {
675
- let base = path.parse(this.sourceFileName).base.split(".")[0];
676
- if (base.endsWith("_page")) {
677
- base = base.substring(0, base.length - 5);
678
- }
679
- return base;
680
- }
681
-
682
- getMethodsNames() {
683
- const methods = [];
684
- this.methods.forEach((codePart) => {
685
- if (this.codePart.name && !codePart.name.startsWith("_")) {
686
- methods.push(codePart.name);
687
- }
688
- });
689
- return methods;
690
- }
691
- getMethodParametersValues(methodName, parameters) {
692
- const params = this.getMethodParameters(methodName);
693
- const values = [];
694
- for (let i = 0; i < params.length; i++) {
695
- const param = params[i];
696
- values.push(parameters[param]);
697
- }
698
- return values;
699
- }
700
-
701
- getMethodParameters(methodName) {
702
- const codePart = this.methods.find((cp) => cp.name === methodName);
703
-
704
- if (!codePart) {
705
- // Handle the case where methodName is not found
706
- return [];
707
- }
708
-
709
- const params = codePart.node.value.params;
710
- return params.map((param) => param.name);
711
- }
712
- /*
713
- importsObjects example
714
- {
715
- type: 'ImportSpecifier',
716
- local: 'Given',
717
- imported: 'Given',
718
- source: '@dev-blinq/cucumber-js'
719
- }
720
- code example
721
- const fs = await import('fs');
722
- */
723
- getImportCode(excludeSources = []) {
724
- let code = "";
725
- this.importsObjects.forEach((importObject) => {
726
- if (excludeSources.includes(importObject.source)) {
727
- return;
728
- }
729
- if (importObject.type === "ImportSpecifier") {
730
- code += `const { ${importObject.local} } = require('${importObject.source}');\n`;
731
- } else if (importObject.type === "ImportDefaultSpecifier") {
732
- code += `const ${importObject.local} = require('${importObject.source}');\n`;
733
- }
734
- });
735
- return code;
736
- }
737
-
738
- getCodeWithoutImports(removeCucumber = true) {
739
- let text = this.fileContent;
740
- if (removeCucumber) {
741
- const expressionsLoc = [];
742
- this.expressions.forEach((codePart) => {
743
- expressionsLoc.push([codePart.start, codePart.end]);
744
- });
745
- text = cutSubString(text, expressionsLoc);
746
- }
747
- let lastImportEnd = 0;
748
- this.imports.forEach((codePart) => {
749
- lastImportEnd = codePart.end;
750
- });
751
-
752
- text = text.substring(lastImportEnd);
753
-
754
- // if a line starts with export follow by async or function, remove the export
755
- text = text.replace(/^\s*export\s+(?=async\s+function|function)/gm, "");
756
- return text;
757
- }
758
- hasMethod(methodName) {
759
- return this.methods.filter((cp) => cp.name === methodName).length > 0;
760
- }
761
- _getMethodComments(methodName) {
762
- const codePart = this.methods.filter((cp) => cp.name === methodName)[0];
763
- if (codePart === undefined) {
764
- return null;
765
- }
766
- const comments = codePart.node.leadingComments;
767
- if (comments === undefined || comments.length === 0) {
768
- return null;
769
- }
770
- const commentsValues = comments.map((comment) => comment.value);
771
- return commentsValues.join("\n");
772
- }
773
- /**
774
- * get the description and parameters of a method
775
- * @param {string} methodName
776
- * @returns a dictionary with name, description and params
777
- */
778
- getMethodCommentsData(methodName) {
779
- const codePart = this.methods.filter((cp) => cp.name === methodName)[0];
780
- if (codePart === undefined) {
781
- return null;
782
- }
783
- const comments = codePart.node.leadingComments;
784
- if (comments === undefined || comments.length === 0) {
785
- return null;
786
- }
787
- const commentsValues = comments.map((comment) => comment.value);
788
- const fullCommentText = commentsValues.join("\n");
789
- // split trim and remove * if exist from the beginning of each line
790
- const commentLines = fullCommentText.split("\n").map((line) => line.trim().replace(/^\*/, "").trim());
791
- const descriptions = [];
792
- for (let i = 0; i < commentLines.length; i++) {
793
- const line = commentLines[i];
794
- if (!line) {
795
- continue;
796
- }
797
- if (line.startsWith("@")) {
798
- break;
799
- }
800
- descriptions.push(line);
801
- }
802
- // join the descriptions into one line seperated by '.' and replace 2 dots with 1
803
- const description = descriptions.join(".").replace("..", ".");
804
- // extract parameters from the comment
805
- const params = [];
806
- for (let i = 0; i < commentLines.length; i++) {
807
- const line = commentLines[i];
808
- if (line.startsWith("@param")) {
809
- params.push(this._processParametersLine(line));
810
- }
811
- }
812
- return { name: methodName, description, params };
813
- }
814
-
815
- /**
816
- * get the method in a format that can be used in the gpt prompt
817
- * @param {string} methodName
818
- * @returns a string formated to be used in the commands gpt prompt
819
- */
820
- getMethodInCommandFormat(methodName) {
821
- const methodComment = this.getMethodCommentsData(methodName);
822
- if (methodComment === null) {
823
- return null;
824
- }
825
- return `${methodName}: ${methodComment.description}, args: ${methodComment.params
826
- .map((p) => `"${p.name}": "<${p.description.replaceAll(" ", "_")}>"`)
827
- .join(", ")}`;
828
- }
829
-
830
- _processParametersLine(paramLine) {
831
- // split by multiple spaces
832
- const paramParts = paramLine.split(/\s+/);
833
- // remove @param
834
- paramParts.shift();
835
- // remove type
836
- let type = null;
837
- if (paramParts.length > 0 && paramParts[0].startsWith("{") && paramParts[0].endsWith("}")) {
838
- type = paramParts.shift().replace("{", "").replace("}", "");
839
290
  }
840
- let name = null;
841
- if (paramParts.length > 0) {
842
- name = paramParts.shift();
291
+ findKey(funcString, key) {
292
+ const sourceRegex = new RegExp(`${key}:\\s*(.*)`);
293
+ const match = funcString.match(sourceRegex);
294
+ return match ? match[1] : null;
295
+ }
296
+ collectAllTemplates() {
297
+ const templates = [];
298
+ if (this.cucumberCalls.length > 0) {
299
+ for (let i = 0; i < this.cucumberCalls.length; i++) {
300
+ const cucumberCall = this.cucumberCalls[i];
301
+ const pattern = (cucumberCall.pattern ?? "");
302
+ const methodName = (cucumberCall.methodName ?? "");
303
+ let foundMethod = false;
304
+ let params = [];
305
+ const stepType = cucumberCall["stepType"];
306
+ let firstFind = true;
307
+ const stepPaths = [];
308
+ let source = null;
309
+ for (let j = 0; j < this.methods.length; j++) {
310
+ const method = this.methods[j];
311
+ if (method.name === methodName) {
312
+ if (firstFind) {
313
+ source = this.findKey(method.codePart, "source");
314
+ foundMethod = true;
315
+ const paramsObj = (method.node?.params ?? []);
316
+ if (paramsObj && paramsObj.length > 0) {
317
+ params = paramsObj.map((p) => p.name);
318
+ }
319
+ firstFind = false;
320
+ }
321
+ stepPaths.push(method.path ?? "");
322
+ }
323
+ }
324
+ if (foundMethod) {
325
+ templates.push({ pattern, methodName, params, stepType, paths: stepPaths, source });
326
+ }
327
+ }
328
+ }
329
+ return templates;
843
330
  }
844
- let description = null;
845
- if (paramParts.length > 0) {
846
- description = paramParts.join(" ");
331
+ getExpectedTimeout(expectedNumofCmds, finalTimeout) {
332
+ const timeoutNum = typeof finalTimeout === "string" ? parseFloat(finalTimeout) : finalTimeout;
333
+ if (finalTimeout !== undefined && !isNaN(timeoutNum)) {
334
+ return -1;
335
+ }
336
+ return (expectedNumofCmds ?? 0) * 60 * 1000;
337
+ }
338
+ addCucumberStep(type, cucumberLine, method, expectedNumofCmds, finalTimeout) {
339
+ let result = {};
340
+ let code = "\n";
341
+ code += `${type}(${JSON.stringify(cucumberLine)}, ${expectedNumofCmds ? `{ timeout: ${this.getExpectedTimeout(expectedNumofCmds, finalTimeout)}}, ` : ""}${method});\n`;
342
+ let existCodePart = null;
343
+ for (let i = 0; i < this.cucumberCalls.length; i++) {
344
+ if (this.cucumberCalls[i].pattern === cucumberLine) {
345
+ existCodePart = this.cucumberCalls[i];
346
+ break;
347
+ }
348
+ }
349
+ result.cucumberLine = cucumberLine;
350
+ if (existCodePart) {
351
+ if (existCodePart.keyword === type || existCodePart.methodName === method) {
352
+ logger.debug(`step ${cucumberLine} already exist with the same code`);
353
+ result.status = CodeStatus.NO_CHANGE;
354
+ return result;
355
+ }
356
+ result.status = CodeStatus.UPDATED;
357
+ result.oldCode = existCodePart.codePart.trim();
358
+ result.newCode = code.trim();
359
+ logger.debug(`conflict in step ${result.cucumberLine}}`);
360
+ logger.debug("old code\n", result.oldCode);
361
+ logger.debug("new code\n", result.newCode);
362
+ this._replaceCode(code, existCodePart.start, existCodePart.end);
363
+ return result;
364
+ }
365
+ else {
366
+ const injectIndex = this._getCucumberInjectionIndex();
367
+ this._insertCode(code, injectIndex);
368
+ result.status = CodeStatus.ADD;
369
+ result.newCode = code.trim();
370
+ return result;
371
+ }
847
372
  }
848
- return { type, name, description };
849
- }
850
- getMethodCodePart(methodName) {
851
- const codePart = this.methods.filter((cp) => cp.name === methodName)[0];
852
- if (codePart === undefined) {
853
- return null;
373
+ addInfraCommand(methodName, description, stepVarables, stepCodeLines, protectStep = false, source = null, codePath = "") {
374
+ let code = "\n";
375
+ code += "/**\n";
376
+ code += ` * ${description}\n`;
377
+ const stepParametersMap = {};
378
+ let i = 1;
379
+ stepVarables.forEach((origKey) => {
380
+ let key = origKey;
381
+ if (stepParametersMap[key]) {
382
+ while (stepParametersMap[`${key}${i}`])
383
+ i++;
384
+ key = `${key}${i}`;
385
+ }
386
+ stepParametersMap[key] = key;
387
+ });
388
+ const stepVarablesNoDuplications = Object.keys(stepParametersMap).map((k) => "_" + convertToIdentifier(k));
389
+ if (stepVarables.length > stepVarablesNoDuplications.length) {
390
+ const varName = "dummy";
391
+ i = 1;
392
+ while (stepVarablesNoDuplications.length !== stepVarables.length) {
393
+ if (!stepVarablesNoDuplications.includes(`${varName}${i}`)) {
394
+ stepVarablesNoDuplications.push(`${varName}${i}`);
395
+ }
396
+ i++;
397
+ }
398
+ }
399
+ stepVarablesNoDuplications.forEach((k) => {
400
+ code += ` * @param {string} ${k} ${k.replaceAll("_", " ")}\n`;
401
+ });
402
+ const tags = [];
403
+ if (protectStep)
404
+ tags.push("@protect");
405
+ if (source)
406
+ tags.push(`@${source}`);
407
+ if (tags.length > 0)
408
+ code += ` * ${tags.join(" ")}\n`;
409
+ if (codePath !== null)
410
+ code += ` * @path=${escapeForComment(codePath)}\n`;
411
+ code += " */\n";
412
+ code += `async function ${methodName} (${stepVarablesNoDuplications.join(", ")}){\n`;
413
+ code += `// source: ${source}\n`;
414
+ code += `// implemented_at: ${new Date().toISOString()}\n`;
415
+ if (stepCodeLines.length > 0) {
416
+ code += ` const _params = { ${stepVarablesNoDuplications.join(", ")} };\n`;
417
+ }
418
+ else {
419
+ code += " const _params = {};\n";
420
+ }
421
+ stepCodeLines.forEach((line) => (code += ` ${line}\n`));
422
+ code += "}\n";
423
+ return this._injectMethod(methodName, code);
424
+ }
425
+ _injectMethod(methodName, code) {
426
+ const result = { methodName };
427
+ code = code.trim();
428
+ let existMethod = null;
429
+ for (let i = 0; i < this.methods.length; i++) {
430
+ if (this.methods[i].name === methodName) {
431
+ existMethod = this.methods[i];
432
+ break;
433
+ }
434
+ }
435
+ if (existMethod) {
436
+ const oldCode = existMethod.codePart.trim();
437
+ if (oldCode === code) {
438
+ logger.debug(`method ${methodName} already exist with the same code`);
439
+ result.status = CodeStatus.NO_CHANGE;
440
+ return result;
441
+ }
442
+ logger.debug(`conflict in method ${methodName}}`);
443
+ logger.debug("old code\n", oldCode);
444
+ logger.debug("new code\n", code.trim());
445
+ this._replaceCode(code, existMethod.start, existMethod.end);
446
+ result.status = CodeStatus.UPDATED;
447
+ result.oldCode = oldCode;
448
+ result.newCode = code;
449
+ return result;
450
+ }
451
+ else {
452
+ const injectIndex = this._getMethodInjectionIndex();
453
+ this._insertCode(code, injectIndex);
454
+ result.status = CodeStatus.ADD;
455
+ result.newCode = code;
456
+ return result;
457
+ }
854
458
  }
855
- return codePart;
856
- }
857
-
858
- getStepDefinitionBreakdown(methodName) {
859
- const codePart = this.methods.filter((cp) => cp.name === methodName)[0];
860
- if (codePart === undefined) {
861
- return null;
459
+ isHumanMethod(methodName) {
460
+ if (this.sourceFileName && path.basename(this.sourceFileName) === "utils.mjs")
461
+ return true;
462
+ let existMethod = null;
463
+ for (let i = 0; i < this.methods.length; i++) {
464
+ if (this.methods[i].name === methodName) {
465
+ existMethod = this.methods[i];
466
+ break;
467
+ }
468
+ }
469
+ if (!existMethod)
470
+ return false;
471
+ const methodCode = existMethod.getCode();
472
+ if (methodCode && (methodCode.includes("@protect") || methodCode.includes("@human"))) {
473
+ return true;
474
+ }
475
+ return false;
862
476
  }
863
- const parametersNames = [];
864
- if (codePart.node && codePart.node.body && codePart.node.body.parent && codePart.node.body.parent.params) {
865
- codePart.node.body.parent.params.forEach((param) => {
866
- parametersNames.push(param.name);
867
- });
477
+ _insertCode(code, injectIndex) {
478
+ const newFileContent = this.fileContent.substring(0, injectIndex) + "\n" + code + this.fileContent.substring(injectIndex);
479
+ this._init();
480
+ try {
481
+ this.generateModel(newFileContent);
482
+ }
483
+ catch (e) {
484
+ logger.error("new code ####\n" + newFileContent);
485
+ throw e;
486
+ }
868
487
  }
869
-
870
- const commands = [];
871
- if (
872
- codePart.node &&
873
- codePart.node.type === "FunctionDeclaration" &&
874
- codePart.node.body &&
875
- codePart.node.body.body &&
876
- codePart.node.body.body.length > 0
877
- ) {
878
- const codeBody = codePart.node.body.body;
879
- for (let i = 0; i < codeBody.length; i++) {
880
- const code = codeBody[i];
881
- commands.push({
882
- type: code.type,
883
- code: this.fileContent.substring(code.start, code.end),
488
+ _elementToCode(key, element) {
489
+ let code = "";
490
+ code += ` ${key}:${JSON.stringify(element)}`;
491
+ return code;
492
+ }
493
+ _removeScoresFromElement(element) {
494
+ if (!element || !element.locators)
495
+ return element;
496
+ const clone = JSON.parse(JSON.stringify(element));
497
+ for (let i = 0; i < (clone.locators?.length ?? 0); i++) {
498
+ const locator = clone.locators[i];
499
+ if (locator.score)
500
+ delete locator.score;
501
+ }
502
+ return clone;
503
+ }
504
+ insertElements(elements) {
505
+ let code = "const elements = {\n";
506
+ const keys = Object.keys(elements);
507
+ const lines = [];
508
+ keys.forEach((key) => {
509
+ const element = this._removeScoresFromElement(elements[key]);
510
+ lines.push(this._elementToCode(key, element));
884
511
  });
885
- }
886
- }
887
- return { codeCommands: commands, parametersNames };
888
- }
889
- hasStep(step) {
890
- // go over expressions for, looking for expression that starts with Given, When or Then
891
- // Then you can find white spaces or new lines, follow by double quotes and the step
892
- // For example:
893
- // "When(\n \"create new opportunity, account: {string}, name: {string}, close date: {string}, stage: {string}, forcast: {string}\",\n create_new_opportunity_account_account_name_name_close_date_closedate_stage_stage_forcast_forcast\n);"
894
- // the step is everything between the double quotes
895
- const regexp = new RegExp(`"${step}"`, "g");
896
- for (let i = 0; i < this.cucumberCalls.length; i++) {
897
- const expression = this.cucumberCalls[i];
898
- if (expression.codePart) {
899
- if (expression.codePart.match(regexp)) {
900
- return true;
512
+ code += lines.join(",\n");
513
+ code += "\n};";
514
+ const startEnd = this._getVariableStartEnd("elements");
515
+ if (startEnd) {
516
+ this._replaceCode(code, startEnd[0], startEnd[1]);
901
517
  }
902
- }
903
518
  }
904
- return false;
905
- }
906
- hasFunctionName(functionName) {
907
- // go over all the methods and compare the name field to the functionName
908
- for (let i = 0; i < this.methods.length; i++) {
909
- if (this.methods[i].name === functionName) {
519
+ mergeSimilarElements() {
520
+ const elements = this.getVariableDeclarationAsObject("elements");
521
+ if (elements === null)
522
+ return;
523
+ const keys = Object.keys(elements);
524
+ for (let i = 0; i < keys.length; i++) {
525
+ const key = keys[i];
526
+ if (!elements[key].element_key) {
527
+ elements[key].element_key = key;
528
+ }
529
+ }
530
+ const replacedKeys = {};
531
+ const mergedElements = {};
532
+ for (let i = keys.length - 1; i >= 0; i--) {
533
+ const currentElement = elements[keys[i]];
534
+ let foundMatch = false;
535
+ for (let j = i - 1; j >= 0; j--) {
536
+ const nextElement = elements[keys[j]];
537
+ if (this._isSimilarElement(currentElement, nextElement)) {
538
+ // mergedElements[currentElement.element_key!] = currentElement;
539
+ // replacedKeys[nextElement.element_key!] = currentElement.element_key!;
540
+ foundMatch = true;
541
+ break;
542
+ // keys.splice(j, 1);
543
+ // i--;
544
+ // j++;
545
+ }
546
+ }
547
+ if (!foundMatch) {
548
+ mergedElements[currentElement.element_key] = currentElement;
549
+ }
550
+ }
551
+ if (Object.keys(replacedKeys).length === 0)
552
+ return;
553
+ for (const key in replacedKeys) {
554
+ const regexp = new RegExp(`elements\\[\\s*["']${key}["']\\s*\\]`, "g");
555
+ this.fileContent = this.fileContent.replace(regexp, () => `elements["${replacedKeys[key]}"]`);
556
+ }
557
+ this.insertElements(mergedElements);
558
+ }
559
+ _isSimilarElement(element1, element2) {
560
+ if (!element1 || !element2)
561
+ return false;
562
+ const locs1 = Array.isArray(element1.locators) ? element1.locators : [];
563
+ const locs2 = Array.isArray(element2.locators) ? element2.locators : [];
564
+ if (locs1.length === 0 && locs2.length === 0)
565
+ return false;
566
+ if (locs1.length === 0 || locs2.length === 0)
567
+ return false;
568
+ const moreLocatorsElement = locs1.length >= locs2.length ? locs1 : locs2;
569
+ const lessLocatorsElement = locs1.length >= locs2.length ? locs2 : locs1;
570
+ const locatorIncludes = (hay, needle) => {
571
+ if (hay == null || needle == null)
572
+ return false;
573
+ const KEYS = ["css", "index", "text", "climb"];
574
+ for (const k of KEYS) {
575
+ if (needle[k] !== undefined) {
576
+ if (!(k in hay))
577
+ return false;
578
+ if (hay[k] !== needle[k])
579
+ return false;
580
+ }
581
+ }
582
+ return true;
583
+ };
584
+ for (const less of lessLocatorsElement) {
585
+ const match = moreLocatorsElement.some((more) => locatorIncludes(more, less));
586
+ if (!match)
587
+ return false;
588
+ }
910
589
  return true;
911
- }
912
- return false;
913
- }
914
- }
915
- }
916
-
917
- function getPath(comment) {
918
- const index = comment.indexOf("@path=");
919
- if (index === -1) {
920
- return null;
921
- }
922
- return comment.substring(index).split("\n")[0].substring(6);
923
- }
924
-
925
- class CodePart {
926
- constructor(node, fileContent, index, codePart = null) {
927
- this.node = node;
928
- this.name = null;
929
- this.keyword = null;
930
- this.pattern = null;
931
- this.methodName = null;
932
- this.fileContent = fileContent;
933
- if (this.node !== null) {
934
- this.start = node.loc.start.index;
935
- if (node.leadingComments && node.leadingComments.length > 0) {
936
- this.start = node.leadingComments[0].start;
937
- this.path = getPath(node.leadingComments[0].value);
938
- }
939
- this.end = node.loc.end.index;
940
- this.codePart = fileContent.substring(this.start, this.end);
941
- if (this.declarations && this.declarations.length > 0 && this.declarations[0].id) {
942
- this.name = this.declarations[0].id.name;
943
- }
944
- } else {
945
- this.pagePart = codePart;
946
- this.start = -1;
947
- this.end = -1;
948
- }
949
- this.index = index;
950
- }
951
- getCode() {
952
- if (this.start === -1 || this.end === -1) {
953
- return null;
954
590
  }
955
- return this.fileContent.substring(this.start, this.end);
956
- }
957
- equals(codePart) {
958
- const maskedCodePart = this.codePart
959
- .replace(this.methodName, "")
960
- .replace(this.name, "")
961
- .replaceAll(/\s/g, "")
962
- .trim();
963
- const maskedCodePart2 = codePart.codePart
964
- .replace(codePart.methodName, "")
965
- .replace(codePart.name, "")
966
- .replaceAll(/\s/g, "")
967
- .trim();
968
- return {
969
- status: maskedCodePart === maskedCodePart2,
970
- line: maskedCodePart,
971
- lineRef: maskedCodePart2,
972
- };
973
- }
974
- getMethodType() {
975
- // use regex to find the following patern: // source: api
976
- const patern = /\s+source:\s*([a-zA-Z]+)/;
977
- if (this.codePart.match(patern)) {
978
- return this.codePart.match(patern)[1];
591
+ removeUnusedElements() {
592
+ const usedElementsKeys = this.getUsedElementsKeys();
593
+ const elements = this.getVariableDeclarationAsObject("elements");
594
+ if (!elements)
595
+ return;
596
+ const keys = Object.keys(elements);
597
+ const unusedKeys = keys.filter((key) => !usedElementsKeys.includes(key));
598
+ if (unusedKeys.length === 0)
599
+ return;
600
+ for (let i = 0; i < unusedKeys.length; i++) {
601
+ delete elements[unusedKeys[i]];
602
+ }
603
+ this.insertElements(elements);
604
+ }
605
+ getUsedElementsKeys() {
606
+ const regexp = /elements\[[\n\r\W]*"([^"]+)"[\n\r\W]*\]/g;
607
+ const keys = [];
608
+ let match;
609
+ while ((match = regexp.exec(this.fileContent)) !== null) {
610
+ keys.push(match[1]);
611
+ }
612
+ return keys;
979
613
  }
980
- return null;
981
- }
982
- getVariableDeclerationObject(declaratinName) {
983
- if (!this.node || !this.node.body || !this.node.body.body || this.node.body.body.length === 0) {
984
- return null;
614
+ _replaceCode(code, injectIndexStart, injectIndexEnd) {
615
+ const newFileContent = this.fileContent.substring(0, injectIndexStart) + code + this.fileContent.substring(injectIndexEnd);
616
+ this._init();
617
+ this.generateModel(newFileContent);
618
+ }
619
+ _getMethodInjectionIndex() {
620
+ return this.fileContent.length;
621
+ }
622
+ _getCucumberInjectionIndex() {
623
+ return this.fileContent.length;
624
+ }
625
+ addLocatorsMetadata(locatorsMetadata) {
626
+ if (!this.sourceFileName)
627
+ return;
628
+ let locatorsMetadataFileName = this.sourceFileName.replace(".mjs", ".json");
629
+ const config = getAiConfig();
630
+ if (config && config.locatorsMetadataDir) {
631
+ locatorsMetadataFileName = locatorsMetadataFileName.replace(path.join("features", "step_definitions"), path.join(config.locatorsMetadataDir));
632
+ if (!existsSync(path.dirname(locatorsMetadataFileName))) {
633
+ mkdirSync(path.dirname(locatorsMetadataFileName), { recursive: true });
634
+ }
635
+ }
636
+ let metadata = {};
637
+ try {
638
+ if (existsSync(locatorsMetadataFileName)) {
639
+ metadata = JSON.parse(readFileSync(locatorsMetadataFileName, "utf8"));
640
+ }
641
+ }
642
+ catch {
643
+ logger.error("failed to read locators metadata file", locatorsMetadataFileName);
644
+ }
645
+ const keys = Object.keys(locatorsMetadata);
646
+ keys.forEach((key) => {
647
+ metadata[key] = locatorsMetadata[key];
648
+ });
649
+ try {
650
+ writeFileSync(locatorsMetadataFileName, JSON.stringify(metadata, null, 2), "utf8");
651
+ }
652
+ catch {
653
+ logger.error("failed to write locators metadata file", locatorsMetadataFileName);
654
+ }
985
655
  }
986
- const body = this.node.body.body;
987
- for (let i = 0; i < body.length; i++) {
988
- const code = body[i];
989
- if (code.type === "VariableDeclaration") {
990
- if (!code.declarations || code.declarations.length === 0) {
991
- continue;
656
+ _getVariableStartEnd(variableName) {
657
+ const codePart = this.variables.filter((cp) => cp.node.declarations[0].id.name === variableName)[0];
658
+ if (codePart === undefined)
659
+ return null;
660
+ return [codePart.node.start, codePart.node.end];
661
+ }
662
+ getVariableDeclarationCode(variableName) {
663
+ const codePart = this.variables.filter((cp) => cp.node.declarations[0].id.name === variableName)[0];
664
+ if (codePart === undefined)
665
+ return null;
666
+ return codePart.codePart;
667
+ }
668
+ getVariableDeclarationAsObject(variableName) {
669
+ const code = this.getVariableDeclarationCode(variableName);
670
+ if (code === null)
671
+ return null;
672
+ const vmContext = { console, value: null };
673
+ vm.createContext(vmContext);
674
+ vm.runInContext(code + "\nvalue = " + variableName + ";", vmContext);
675
+ return vmContext.value;
676
+ }
677
+ getName() {
678
+ let base = path.parse(this.sourceFileName ?? "").base.split(".")[0];
679
+ if (base.endsWith("_page"))
680
+ base = base.substring(0, base.length - 5);
681
+ return base;
682
+ }
683
+ getMethodsNames() {
684
+ const methods = [];
685
+ this.methods.forEach((codePart) => {
686
+ if (codePart.name && !codePart.name.startsWith("_")) {
687
+ methods.push(codePart.name);
688
+ }
689
+ });
690
+ return methods;
691
+ }
692
+ getMethodParametersValues(methodName, parameters) {
693
+ const params = this.getMethodParameters(methodName);
694
+ const values = [];
695
+ for (let i = 0; i < params.length; i++) {
696
+ const param = params[i];
697
+ values.push(parameters[param]);
992
698
  }
993
- const declaration = code.declarations[0];
994
- if (declaration.id && declaration.id.name === declaratinName) {
995
- const declerationCode = this.fileContent.substring(declaration.start, declaration.end);
996
- let value = null;
997
- let vmContext = {
998
- console: console,
999
- value: value,
1000
- };
1001
- vm.createContext(vmContext);
1002
- vm.runInContext("value = " + declerationCode + ";", vmContext);
1003
- //console.log("value", vmContext.value);
1004
- return vmContext.value;
699
+ return values;
700
+ }
701
+ getMethodParameters(methodName) {
702
+ const codePart = this.methods.find((cp) => cp.name === methodName);
703
+ if (!codePart)
704
+ return [];
705
+ const params = codePart.node.value?.params ?? [];
706
+ return params.map((param) => param.name);
707
+ }
708
+ getImportCode(excludeSources = []) {
709
+ let code = "";
710
+ this.importsObjects.forEach((importObject) => {
711
+ if (excludeSources.includes(importObject.source))
712
+ return;
713
+ if (importObject.type === "ImportSpecifier") {
714
+ code += `const { ${importObject.local} } = require('${importObject.source}');\n`;
715
+ }
716
+ else if (importObject.type === "ImportDefaultSpecifier") {
717
+ code += `const ${importObject.local} = require('${importObject.source}');\n`;
718
+ }
719
+ });
720
+ return code;
721
+ }
722
+ getCodeWithoutImports(removeCucumber = true) {
723
+ let text = this.fileContent;
724
+ if (removeCucumber) {
725
+ const expressionsLoc = [];
726
+ this.expressions.forEach((codePart) => {
727
+ expressionsLoc.push([codePart.start, codePart.end]);
728
+ });
729
+ text = cutSubString(text, expressionsLoc);
730
+ }
731
+ let lastImportEnd = 0;
732
+ this.imports.forEach((codePart) => {
733
+ lastImportEnd = codePart.end;
734
+ });
735
+ text = text.substring(lastImportEnd);
736
+ text = text.replace(/^\s*export\s+(?=async\s+function|function)/gm, "");
737
+ return text;
738
+ }
739
+ hasMethod(methodName) {
740
+ return this.methods.filter((cp) => cp.name === methodName).length > 0;
741
+ }
742
+ _getMethodComments(methodName) {
743
+ const codePart = this.methods.filter((cp) => cp.name === methodName)[0];
744
+ if (codePart === undefined)
745
+ return null;
746
+ const comments = codePart.node.leadingComments;
747
+ if (!comments || comments.length === 0)
748
+ return null;
749
+ const commentsValues = comments.map((c) => c.value);
750
+ return commentsValues.join("\n");
751
+ }
752
+ /**
753
+ * get the description and parameters of a method
754
+ * @param methodName
755
+ * @returns a dictionary with name, description and params
756
+ */
757
+ getMethodCommentsData(methodName) {
758
+ const codePart = this.methods.filter((cp) => cp.name === methodName)[0];
759
+ if (codePart === undefined)
760
+ return null;
761
+ const comments = codePart.node.leadingComments;
762
+ if (!comments || comments.length === 0)
763
+ return null;
764
+ const commentsValues = comments.map((c) => c.value);
765
+ const fullCommentText = commentsValues.join("\n");
766
+ const commentLines = fullCommentText.split("\n").map((line) => line.trim().replace(/^\*/, "").trim());
767
+ const descriptions = [];
768
+ for (let i = 0; i < commentLines.length; i++) {
769
+ const line = commentLines[i];
770
+ if (!line)
771
+ continue;
772
+ if (line.startsWith("@"))
773
+ break;
774
+ descriptions.push(line);
775
+ }
776
+ const description = descriptions.join(".").replace("..", ".");
777
+ const params = [];
778
+ for (let i = 0; i < commentLines.length; i++) {
779
+ const line = commentLines[i];
780
+ if (line.startsWith("@param")) {
781
+ params.push(this._processParametersLine(line));
782
+ }
783
+ }
784
+ return { name: methodName, description, params };
785
+ }
786
+ /**
787
+ * get the method in a format that can be used in the gpt prompt
788
+ * @param methodName
789
+ * @returns a string formatted to be used in the commands gpt prompt
790
+ */
791
+ getMethodInCommandFormat(methodName) {
792
+ const methodComment = this.getMethodCommentsData(methodName);
793
+ if (methodComment === null)
794
+ return null;
795
+ return `${methodName}: ${methodComment.description}, args: ${methodComment.params
796
+ .map((p) => `"${p.name}": "<${(p.description ?? "").replaceAll(" ", "_")}>"`)
797
+ .join(", ")}`;
798
+ }
799
+ _processParametersLine(paramLine) {
800
+ const paramParts = paramLine.split(/\s+/);
801
+ paramParts.shift(); // @param
802
+ let type = null;
803
+ if (paramParts.length > 0 && paramParts[0].startsWith("{") && paramParts[0].endsWith("}")) {
804
+ type = paramParts.shift().replace("{", "").replace("}", "");
805
+ }
806
+ let name = null;
807
+ if (paramParts.length > 0) {
808
+ name = paramParts.shift() ?? null;
809
+ }
810
+ let description = null;
811
+ if (paramParts.length > 0) {
812
+ description = paramParts.join(" ");
1005
813
  }
1006
- }
814
+ return { type, name, description };
815
+ }
816
+ getMethodCodePart(methodName) {
817
+ const codePart = this.methods.filter((cp) => cp.name === methodName)[0];
818
+ return codePart ?? null;
819
+ }
820
+ getStepDefinitionBreakdown(methodName) {
821
+ const codePart = this.methods.filter((cp) => cp.name === methodName)[0];
822
+ if (codePart === undefined)
823
+ return null;
824
+ const parametersNames = [];
825
+ if (codePart.node && codePart.node.body && codePart.node.body.parent && codePart.node.body.parent.params) {
826
+ codePart.node.body.parent.params.forEach((param) => {
827
+ parametersNames.push(param.name);
828
+ });
829
+ }
830
+ const commands = [];
831
+ if (codePart.node &&
832
+ codePart.node.type === "FunctionDeclaration" &&
833
+ codePart.node.body &&
834
+ codePart.node.body.body &&
835
+ codePart.node.body.body.length > 0) {
836
+ const codeBody = codePart.node.body.body;
837
+ for (let i = 0; i < codeBody.length; i++) {
838
+ const code = codeBody[i];
839
+ commands.push({
840
+ type: code.type,
841
+ code: this.fileContent.substring(code.start, code.end),
842
+ });
843
+ }
844
+ }
845
+ return { codeCommands: commands, parametersNames };
846
+ }
847
+ hasStep(step) {
848
+ const regexp = new RegExp(`"${step}"`, "g");
849
+ for (let i = 0; i < this.cucumberCalls.length; i++) {
850
+ const expression = this.cucumberCalls[i];
851
+ if (expression.codePart) {
852
+ if (expression.codePart.match(regexp)) {
853
+ return true;
854
+ }
855
+ }
856
+ }
857
+ return false;
858
+ }
859
+ hasFunctionName(functionName) {
860
+ for (let i = 0; i < this.methods.length; i++) {
861
+ if (this.methods[i].name === functionName) {
862
+ return true;
863
+ }
864
+ return false; // (Preserved original behavior)
865
+ }
866
+ return false;
1007
867
  }
1008
- return null;
1009
- }
1010
868
  }
1011
-
1012
- const cutSubString = (str, substrings) => {
1013
- let result = str;
1014
- let offset = 0;
1015
- substrings.forEach((substring) => {
1016
- const start = substring[0];
1017
- const end = substring[1];
1018
- result = result.substring(0, start - offset) + result.substring(end - offset);
1019
- offset += end - start;
1020
- });
1021
- return result;
1022
- };
1023
-
1024
- // function stringifyObject(obj, indent = 4, currentIndent = "") {
1025
- // if (typeof obj !== "object" || obj === null) {
1026
- // return JSON.stringify(obj);
1027
- // }
1028
-
1029
- // if (Array.isArray(obj)) {
1030
- // const items = obj.map(
1031
- // (item) =>
1032
- // `${currentIndent}${" ".repeat(indent)}${stringifyObject(item, indent, currentIndent + " ".repeat(indent))}`
1033
- // );
1034
- // return `[\n${items.join(",\n")}\n${currentIndent}]`;
1035
- // }
1036
-
1037
- // const keys = Object.keys(obj);
1038
- // const keyValues = keys.map(
1039
- // (key) =>
1040
- // `${currentIndent}${" ".repeat(indent)}${key}: ${stringifyObject(
1041
- // obj[key],
1042
- // indent,
1043
- // currentIndent + " ".repeat(indent)
1044
- // )}`
1045
- // );
1046
-
1047
- // return `{\n${keyValues.join(",\n")}\n${currentIndent}}`;
1048
- // }
1049
- export { CodePage, CodeStatus };
869
+ export { escapeForComment, unescapeFromComment };