@dev-blinq/cucumber_client 1.0.1276-dev → 1.0.1276-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 (37) hide show
  1. package/bin/assets/bundled_scripts/recorder.js +121 -121
  2. package/bin/assets/preload/recorderv3.js +3 -1
  3. package/bin/assets/scripts/dom_parent.js +4 -0
  4. package/bin/assets/scripts/recorder.js +11 -4
  5. package/bin/assets/scripts/unique_locators.js +837 -815
  6. package/bin/assets/templates/_hooks_template.txt +37 -0
  7. package/bin/assets/templates/page_template.txt +2 -16
  8. package/bin/assets/templates/utils_template.txt +1 -46
  9. package/bin/client/apiTest/apiTest.js +6 -0
  10. package/bin/client/cli_helpers.js +11 -13
  11. package/bin/client/code_cleanup/utils.js +5 -1
  12. package/bin/client/code_gen/code_inversion.js +53 -4
  13. package/bin/client/code_gen/page_reflection.js +838 -902
  14. package/bin/client/code_gen/playwright_codeget.js +43 -12
  15. package/bin/client/cucumber/feature.js +89 -27
  16. package/bin/client/cucumber/project_to_document.js +1 -1
  17. package/bin/client/cucumber/steps_definitions.js +84 -81
  18. package/bin/client/cucumber_selector.js +17 -1
  19. package/bin/client/local_agent.js +7 -6
  20. package/bin/client/project.js +186 -196
  21. package/bin/client/recorderv3/bvt_recorder.js +170 -60
  22. package/bin/client/recorderv3/implemented_steps.js +74 -16
  23. package/bin/client/recorderv3/index.js +50 -25
  24. package/bin/client/recorderv3/network.js +299 -0
  25. package/bin/client/recorderv3/services.js +4 -16
  26. package/bin/client/recorderv3/step_runner.js +332 -69
  27. package/bin/client/recorderv3/step_utils.js +579 -7
  28. package/bin/client/recorderv3/update_feature.js +32 -30
  29. package/bin/client/run_cucumber.js +5 -1
  30. package/bin/client/scenario_report.js +0 -5
  31. package/bin/client/test_scenario.js +0 -1
  32. package/bin/client/utils/socket_logger.js +132 -0
  33. package/bin/index.js +1 -0
  34. package/bin/logger.js +3 -2
  35. package/bin/min/consoleApi.min.cjs +2 -3
  36. package/bin/min/injectedScript.min.cjs +16 -16
  37. package/package.json +24 -14
@@ -1,925 +1,861 @@
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
- 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
- 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 });
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
- }
96
- }
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
- }
131
- });
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;
208
- }
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;
232
- }
233
- stepPaths.push(method.path);
234
- }
235
- }
236
- if (foundMethod) {
237
- templates.push({ pattern, methodName, params, stepType, paths: stepPaths });
238
- }
239
- }
240
- }
241
- return templates;
242
- }
243
- getExpectedTimeout(expectedNumofCmds) {
244
- return expectedNumofCmds * 60 * 1000;
245
- }
246
- addCucumberStep(type, cucumberLine, method, expectedNumofCmds) {
247
- const result = {};
248
- let code = "\n";
249
- code += `${type}(${JSON.stringify(cucumberLine)}, ${expectedNumofCmds ? `{ timeout: ${this.getExpectedTimeout(expectedNumofCmds)}}, ` : ""}${method});\n`;
250
- let existCodePart = null;
251
- for (let i = 0; i < this.cucumberCalls.length; i++) {
252
- if (
253
- this.cucumberCalls[i].pattern === cucumberLine
254
- //this.cucumberCalls[i].methodName === method &&
255
- //this.cucumberCalls[i].keyword === type
256
- ) {
257
- existCodePart = this.cucumberCalls[i];
258
- break;
259
- }
260
- }
261
- result.cucumberLine = cucumberLine;
262
- if (existCodePart) {
263
- if (existCodePart.keyword === type || existCodePart.methodName === method) {
264
- logger.debug(`step ${cucumberLine} already exist with the same code`);
265
- result.status = CodeStatus.NO_CHANGE;
266
- return result;
267
- }
268
- result.status = CodeStatus.UPDATED;
269
- result.oldCode = existCodePart.codePart.trim();
270
- result.newCode = code.trim();
271
- logger.debug(`conflict in step ${result.cucumberLine}}`);
272
- logger.debug("old code\n", result.oldCode);
273
- logger.debug("new code\n", result.newCode);
274
- this._replaceCode(code, existCodePart.start, existCodePart.end);
275
- return result;
276
- } else {
277
- let injectIndex = this._getCucumberInjectionIndex();
278
- this._insertCode(code, injectIndex);
279
- result.status = CodeStatus.ADD;
280
- result.newCode = code.trim();
281
- return result;
282
- }
283
- }
284
- addInfraCommand(methodName, description, stepVarables, stepCodeLines, protectStep = false, source = null, path = "") {
285
- let code = "\n";
286
- code += "/**\n";
287
- code += ` * ${description}\n`;
288
- //const keys = Object.keys(stepParameters);
289
- let stepParametersMap = {};
290
- // remove duplicates
291
- let i = 1;
292
- stepVarables.forEach((key) => {
293
- if (stepParametersMap[key]) {
294
- while (stepParametersMap[`${key}${i}`]) {
295
- i++;
296
- }
297
- key = `${key}${i}`;
298
- }
299
- stepParametersMap[key] = key;
300
- });
301
- let stepVarablesNoDuplications = Object.keys(stepParametersMap).map((key) => "_" + convertToIdentifier(key));
302
- if (stepVarables.length > stepVarablesNoDuplications.length) {
303
- const varName = "dummy";
304
- i = 1;
305
- while (stepVarablesNoDuplications.length !== stepVarables.length) {
306
- if (!stepVarablesNoDuplications.includes(`${varName}${i}`)) {
307
- stepVarablesNoDuplications.push(`${varName}${i}`);
308
- }
309
- i++;
310
- }
311
- }
312
-
313
- stepVarablesNoDuplications.forEach((key) => {
314
- code += ` * @param {string} ${key} ${key.replaceAll("_", " ")}\n`;
315
- });
316
- let tags = [];
317
- if (protectStep) {
318
- tags.push("@protect");
319
- }
320
- if (source) {
321
- tags.push(`@${source}`);
322
- }
323
- if (tags.length > 0) {
324
- code += ` * ${tags.join(" ")}\n`;
325
- }
326
- if (path !== null) {
327
- code += ` * @path=${escapeForComment(path)}\n`;
328
- }
329
- code += " */\n";
330
- code += `async function ${methodName} (${stepVarablesNoDuplications.join(", ")}){\n`;
331
- code += `// source: ${source}\n`;
332
- code += `// implemented_at: ${new Date().toISOString()}\n`;
333
- if (stepCodeLines.length > 0) {
334
- code += ` const _params = { ${stepVarablesNoDuplications.join(", ")} };\n`;
335
- } else {
336
- code += " const _params = {};\n";
337
- }
338
-
339
- // code += " await navigate(path);\n";
340
- stepCodeLines.forEach((line) => {
341
- code += ` ${line}\n`;
342
- });
343
- code += "}\n";
344
-
345
- return this._injectMethod(methodName, code);
346
- }
347
-
348
- _injectMethod(methodName, code) {
349
- const result = {};
350
- code = code.trim();
351
- let existMethod = null;
352
- for (let i = 0; i < this.methods.length; i++) {
353
- if (this.methods[i].name === methodName) {
354
- existMethod = this.methods[i];
355
- break;
356
- }
357
- }
358
- result.methodName = methodName;
359
- if (existMethod) {
360
- let oldCode = existMethod.codePart.trim();
361
- if (oldCode === code) {
362
- logger.debug(`method ${methodName} already exist with the same code`);
363
- result.status = CodeStatus.NO_CHANGE;
364
- return result;
365
- }
366
- logger.debug(`conflict in method ${methodName}}`);
367
- logger.debug("old code\n", oldCode);
368
- logger.debug("new code\n", code.trim());
369
- this._replaceCode(code, existMethod.start, existMethod.end);
370
- result.status = CodeStatus.UPDATED;
371
- result.oldCode = oldCode;
372
- result.newCode = code;
373
- return result;
374
- } else {
375
- let injectIndex = this._getMethodInjectionIndex();
376
- this._insertCode(code, injectIndex);
377
- result.status = CodeStatus.ADD;
378
- result.newCode = code;
379
- return result;
380
- }
381
- }
382
- isHumanMethod(methodName) {
383
- // check if the fileSourceName end with utils.mjs
384
- if (path.basename(this.sourceFileName) === "utils.mjs") {
385
- return true;
386
- }
387
- let existMethod = null;
388
- for (let i = 0; i < this.methods.length; i++) {
389
- if (this.methods[i].name === methodName) {
390
- existMethod = this.methods[i];
391
- break;
392
- }
393
- }
394
- if (!existMethod) {
395
- return false;
396
- }
397
- const methodCode = existMethod.getCode();
398
- if (methodCode && (methodCode.includes("@protect") || methodCode.includes("@human"))) {
399
- return true;
400
- }
401
- return false;
402
- }
403
-
404
- _insertCode(code, injectIndex) {
405
- let newFileContent =
406
- this.fileContent.substring(0, injectIndex) + "\n" + code + this.fileContent.substring(injectIndex);
407
-
408
- this._init();
29
+ if (ai_config)
30
+ return ai_config;
409
31
  try {
410
- this.generateModel(newFileContent);
411
- } catch (e) {
412
- logger.error("new code ####\n" + newFileContent);
413
- throw e;
414
- }
415
- }
416
-
417
- _elementToCode(key, element) {
418
- let code = "";
419
- code += ` ${key}:${JSON.stringify(element)}`;
420
-
421
- return code;
422
- }
423
-
424
- _removeScoresFromElement(element) {
425
- if (!element || !element.locators) {
426
- return element;
427
- }
428
- // clone the element
429
- element = JSON.parse(JSON.stringify(element));
430
- for (let i = 0; i < element.locators.length; i++) {
431
- let locator = element.locators[i];
432
- if (locator.score) {
433
- delete locator.score;
434
- }
435
- }
436
- return element;
437
- }
438
- insertElements(elements) {
439
- let code = "const elements = {\n";
440
- const keys = Object.keys(elements);
441
- const lines = [];
442
- keys.forEach((key) => {
443
- let element = this._removeScoresFromElement(elements[key]);
444
- lines.push(this._elementToCode(key, element));
445
- });
446
- code += lines.join(",\n");
447
- code += "\n};";
448
- let startEnd = this._getVariableStartEnd("elements");
449
- this._replaceCode(code, startEnd[0], startEnd[1]);
450
- }
451
- removeUnusedElements() {
452
- const usedElementsKeys = this.getUsedElementsKeys();
453
- const elements = this.getVariableDeclarationAsObject("elements");
454
- const keys = Object.keys(elements);
455
- const unusedKeys = keys.filter((key) => !usedElementsKeys.includes(key));
456
- if (unusedKeys.length === 0) {
457
- return;
458
- }
459
- for (let i = 0; i < unusedKeys.length; i++) {
460
- delete elements[unusedKeys[i]];
461
- }
462
- this.insertElements(elements);
463
- }
464
-
465
- getUsedElementsKeys() {
466
- const regexp = /elements\[[\n\r\W]*"([^"]+)"[\n\r\W]*\]/g;
467
- const keys = [];
468
- let match;
469
- while ((match = regexp.exec(this.fileContent)) !== null) {
470
- keys.push(match[1]);
471
- }
472
- return keys;
473
- }
474
- _replaceCode(code, injectIndexStart, injectIndexEnd) {
475
- let newFileContent =
476
- this.fileContent.substring(0, injectIndexStart) + code + this.fileContent.substring(injectIndexEnd);
477
- this._init();
478
- this.generateModel(newFileContent);
479
- }
480
- _getMethodInjectionIndex() {
481
- return this.fileContent.length;
482
- }
483
- _getCucumberInjectionIndex() {
484
- return this.fileContent.length;
485
- }
486
- addLocatorsMetadata(locatorsMetadata) {
487
- // create a file name based on the source file name replace .mjs with .json
488
- let locatorsMetadataFileName = this.sourceFileName.replace(".mjs", ".json");
489
- const config = getAiConfig();
490
- if (config && config.locatorsMetadataDir) {
491
- // if config.locatorsMetadataDir is set, use it to create the file path
492
- locatorsMetadataFileName = path.join(config.locatorsMetadataDir, path.basename(locatorsMetadataFileName));
493
- // check if the directory exists, if not create it
494
- if (!existsSync(path.dirname(locatorsMetadataFileName))) {
495
- mkdirSync(path.dirname(locatorsMetadataFileName), { recursive: true });
496
- }
497
- }
498
- let metadata = {};
499
- // try to read the file to metadata, protect with try catch
500
- try {
501
- // if the file exist read the content and parse it
502
- if (existsSync(locatorsMetadataFileName)) {
503
- metadata = JSON.parse(readFileSync(locatorsMetadataFileName, "utf8"));
504
- }
505
- } catch (e) {
506
- logger.info("failed to read locators metadata file", locatorsMetadataFileName);
507
- }
508
- // merge the locatorsMetadata with the metadata
509
- // go over all the keys in locatorsMetadata and add them to metadata
510
- const keys = Object.keys(locatorsMetadata);
511
- keys.forEach((key) => {
512
- metadata[key] = locatorsMetadata[key];
513
- });
514
- // save the metadata back to the file
515
- try {
516
- writeFileSync(locatorsMetadataFileName, JSON.stringify(metadata, null, 2), "utf8");
517
- } catch (e) {
518
- logger.info("failed to write locators metadata file", locatorsMetadataFileName);
519
- }
520
- }
521
- _getVariableStartEnd(variableName) {
522
- const codePart = this.variables.filter((cp) => cp.node.declarations[0].id.name === variableName)[0];
523
- if (codePart === undefined) {
524
- return null;
525
- }
526
- return [codePart.node.start, codePart.node.end];
527
- }
528
- getVariableDeclarationCode(variableName) {
529
- const codePart = this.variables.filter((cp) => cp.node.declarations[0].id.name === variableName)[0];
530
- if (codePart === undefined) {
531
- return null;
532
- }
533
- return codePart.codePart;
534
- }
535
- getVariableDeclarationAsObject(variableName) {
536
- const code = this.getVariableDeclarationCode(variableName);
537
- if (code === null) {
538
- return null;
539
- }
540
- let value = null;
541
- let vmContext = {
542
- console: console,
543
- value: value,
544
- };
545
- vm.createContext(vmContext);
546
- vm.runInContext(code + "\nvalue = " + variableName + ";", vmContext);
547
- //console.log("value", vmContext.value);
548
- return vmContext.value;
549
- }
550
- getName() {
551
- let base = path.parse(this.sourceFileName).base.split(".")[0];
552
- if (base.endsWith("_page")) {
553
- base = base.substring(0, base.length - 5);
554
- }
555
- return base;
556
- }
557
-
558
- getMethodsNames() {
559
- const methods = [];
560
- this.methods.forEach((codePart) => {
561
- if (this.codePart.name && !codePart.name.startsWith("_")) {
562
- methods.push(codePart.name);
563
- }
564
- });
565
- return methods;
566
- }
567
- getMethodParametersValues(methodName, parameters) {
568
- const params = this.getMethodParameters(methodName);
569
- const values = [];
570
- for (let i = 0; i < params.length; i++) {
571
- const param = params[i];
572
- values.push(parameters[param]);
573
- }
574
- return values;
575
- }
576
-
577
- getMethodParameters(methodName) {
578
- const codePart = this.methods.find((cp) => cp.name === methodName);
579
-
580
- if (!codePart) {
581
- // Handle the case where methodName is not found
582
- return [];
583
- }
584
-
585
- const params = codePart.node.value.params;
586
- return params.map((param) => param.name);
587
- }
588
- /*
589
- importsObjects example
590
- {
591
- type: 'ImportSpecifier',
592
- local: 'Given',
593
- imported: 'Given',
594
- source: '@dev-blinq/cucumber-js'
595
- }
596
- code example
597
- const fs = await import('fs');
598
- */
599
- getImportCode(excludeSources = []) {
600
- let code = "";
601
- this.importsObjects.forEach((importObject) => {
602
- if (excludeSources.includes(importObject.source)) {
603
- return;
604
- }
605
- if (importObject.type === "ImportSpecifier") {
606
- code += `const { ${importObject.local} } = require('${importObject.source}');\n`;
607
- } else if (importObject.type === "ImportDefaultSpecifier") {
608
- code += `const ${importObject.local} = require('${importObject.source}');\n`;
609
- }
610
- });
611
- return code;
612
- }
613
-
614
- getCodeWithoutImports(removeCucumber = true) {
615
- let text = this.fileContent;
616
- if (removeCucumber) {
617
- const expressionsLoc = [];
618
- this.expressions.forEach((codePart) => {
619
- expressionsLoc.push([codePart.start, codePart.end]);
620
- });
621
- text = cutSubString(text, expressionsLoc);
622
- }
623
- let lastImportEnd = 0;
624
- this.imports.forEach((codePart) => {
625
- lastImportEnd = codePart.end;
626
- });
627
-
628
- text = text.substring(lastImportEnd);
629
-
630
- // if a line starts with export follow by async or function, remove the export
631
- text = text.replace(/^\s*export\s+(?=async\s+function|function)/gm, "");
632
- return text;
633
- }
634
- hasMethod(methodName) {
635
- return this.methods.filter((cp) => cp.name === methodName).length > 0;
636
- }
637
- _getMethodComments(methodName) {
638
- const codePart = this.methods.filter((cp) => cp.name === methodName)[0];
639
- if (codePart === undefined) {
640
- return null;
641
- }
642
- const comments = codePart.node.leadingComments;
643
- if (comments === undefined || comments.length === 0) {
644
- return null;
645
- }
646
- const commentsValues = comments.map((comment) => comment.value);
647
- return commentsValues.join("\n");
648
- }
649
- /**
650
- * get the description and parameters of a method
651
- * @param {string} methodName
652
- * @returns a dictionary with name, description and params
653
- */
654
- getMethodCommentsData(methodName) {
655
- const codePart = this.methods.filter((cp) => cp.name === methodName)[0];
656
- if (codePart === undefined) {
657
- return null;
658
- }
659
- const comments = codePart.node.leadingComments;
660
- if (comments === undefined || comments.length === 0) {
661
- return null;
662
- }
663
- const commentsValues = comments.map((comment) => comment.value);
664
- const fullCommentText = commentsValues.join("\n");
665
- // split trim and remove * if exist from the beginning of each line
666
- const commentLines = fullCommentText.split("\n").map((line) => line.trim().replace(/^\*/, "").trim());
667
- const descriptions = [];
668
- for (let i = 0; i < commentLines.length; i++) {
669
- const line = commentLines[i];
670
- if (!line) {
671
- continue;
672
- }
673
- if (line.startsWith("@")) {
674
- break;
675
- }
676
- descriptions.push(line);
677
- }
678
- // join the descriptions into one line seperated by '.' and replace 2 dots with 1
679
- const description = descriptions.join(".").replace("..", ".");
680
- // extract parameters from the comment
681
- const params = [];
682
- for (let i = 0; i < commentLines.length; i++) {
683
- const line = commentLines[i];
684
- if (line.startsWith("@param")) {
685
- params.push(this._processParametersLine(line));
686
- }
687
- }
688
- return { name: methodName, description, params };
689
- }
690
-
691
- /**
692
- * get the method in a format that can be used in the gpt prompt
693
- * @param {string} methodName
694
- * @returns a string formated to be used in the commands gpt prompt
695
- */
696
- getMethodInCommandFormat(methodName) {
697
- const methodComment = this.getMethodCommentsData(methodName);
698
- if (methodComment === null) {
699
- return null;
700
- }
701
- return `${methodName}: ${methodComment.description}, args: ${methodComment.params
702
- .map((p) => `"${p.name}": "<${p.description.replaceAll(" ", "_")}>"`)
703
- .join(", ")}`;
704
- }
705
-
706
- _processParametersLine(paramLine) {
707
- // split by multiple spaces
708
- const paramParts = paramLine.split(/\s+/);
709
- // remove @param
710
- paramParts.shift();
711
- // remove type
712
- let type = null;
713
- if (paramParts.length > 0 && paramParts[0].startsWith("{") && paramParts[0].endsWith("}")) {
714
- type = paramParts.shift().replace("{", "").replace("}", "");
715
- }
716
- let name = null;
717
- if (paramParts.length > 0) {
718
- name = paramParts.shift();
719
- }
720
- let description = null;
721
- if (paramParts.length > 0) {
722
- description = paramParts.join(" ");
723
- }
724
- return { type, name, description };
725
- }
726
- getMethodCodePart(methodName) {
727
- const codePart = this.methods.filter((cp) => cp.name === methodName)[0];
728
- if (codePart === undefined) {
729
- return null;
730
- }
731
- return codePart;
732
- }
733
-
734
- getStepDefinitionBreakdown(methodName) {
735
- const codePart = this.methods.filter((cp) => cp.name === methodName)[0];
736
- if (codePart === undefined) {
737
- return null;
738
- }
739
- const parametersNames = [];
740
- if (codePart.node && codePart.node.body && codePart.node.body.parent && codePart.node.body.parent.params) {
741
- codePart.node.body.parent.params.forEach((param) => {
742
- parametersNames.push(param.name);
743
- });
744
- }
745
-
746
- const commands = [];
747
- if (
748
- codePart.node &&
749
- codePart.node.type === "FunctionDeclaration" &&
750
- codePart.node.body &&
751
- codePart.node.body.body &&
752
- codePart.node.body.body.length > 0
753
- ) {
754
- const codeBody = codePart.node.body.body;
755
- for (let i = 0; i < codeBody.length; i++) {
756
- const code = codeBody[i];
757
- commands.push({
758
- type: code.type,
759
- code: this.fileContent.substring(code.start, code.end),
760
- });
761
- }
762
- }
763
- return { codeCommands: commands, parametersNames };
764
- }
765
- hasStep(step) {
766
- // go over expressions for, looking for expression that starts with Given, When or Then
767
- // Then you can find white spaces or new lines, follow by double quotes and the step
768
- // For example:
769
- // "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);"
770
- // the step is everything between the double quotes
771
- const regexp = new RegExp(`"${step}"`, "g");
772
- for (let i = 0; i < this.cucumberCalls.length; i++) {
773
- const expression = this.cucumberCalls[i];
774
- if (expression.codePart) {
775
- if (expression.codePart.match(regexp)) {
776
- return true;
777
- }
778
- }
779
- }
780
- return false;
781
- }
782
- hasFunctionName(functionName) {
783
- // go over all the methods and compare the name field to the functionName
784
- for (let i = 0; i < this.methods.length; i++) {
785
- if (this.methods[i].name === functionName) {
786
- return true;
787
- }
788
- return false;
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";
789
39
  }
790
- }
40
+ return ai_config;
791
41
  }
792
-
793
42
  function getPath(comment) {
794
- const index = comment.indexOf("@path=");
795
- if (index === -1) {
796
- return null;
797
- }
798
- return comment.substring(index).split("\n")[0].substring(6);
43
+ const index = comment.indexOf("@path=");
44
+ if (index === -1)
45
+ return null;
46
+ return comment.substring(index).split("\n")[0].substring(6);
799
47
  }
800
-
801
48
  class CodePart {
802
- constructor(node, fileContent, index, codePart = null) {
803
- this.node = node;
804
- this.name = null;
805
- this.keyword = null;
806
- this.pattern = null;
807
- this.methodName = null;
808
- this.fileContent = fileContent;
809
- if (this.node !== null) {
810
- this.start = node.loc.start.index;
811
- if (node.leadingComments && node.leadingComments.length > 0) {
812
- this.start = node.leadingComments[0].start;
813
- this.path = getPath(node.leadingComments[0].value);
814
- }
815
- this.end = node.loc.end.index;
816
- this.codePart = fileContent.substring(this.start, this.end);
817
- if (this.declarations && this.declarations.length > 0 && this.declarations[0].id) {
818
- this.name = this.declarations[0].id.name;
819
- }
820
- } else {
821
- this.pagePart = codePart;
822
- this.start = -1;
823
- this.end = -1;
824
- }
825
- this.index = index;
826
- }
827
- getCode() {
828
- if (this.start === -1 || this.end === -1) {
829
- return null;
830
- }
831
- return this.fileContent.substring(this.start, this.end);
832
- }
833
- equals(codePart) {
834
- const maskedCodePart = this.codePart
835
- .replace(this.methodName, "")
836
- .replace(this.name, "")
837
- .replaceAll(/\s/g, "")
838
- .trim();
839
- const maskedCodePart2 = codePart.codePart
840
- .replace(codePart.methodName, "")
841
- .replace(codePart.name, "")
842
- .replaceAll(/\s/g, "")
843
- .trim();
844
- return {
845
- status: maskedCodePart === maskedCodePart2,
846
- line: maskedCodePart,
847
- lineRef: maskedCodePart2,
848
- };
849
- }
850
- getMethodType() {
851
- // use regex to find the following patern: // source: api
852
- const patern = /\s+source:\s*([a-zA-Z]+)/;
853
- if (this.codePart.match(patern)) {
854
- return this.codePart.match(patern)[1];
855
- }
856
- return null;
857
- }
858
- getVariableDeclerationObject(declaratinName) {
859
- if (!this.node || !this.node.body || !this.node.body.body || this.node.body.body.length === 0) {
860
- return null;
861
- }
862
- const body = this.node.body.body;
863
- for (let i = 0; i < body.length; i++) {
864
- const code = body[i];
865
- if (code.type === "VariableDeclaration") {
866
- if (!code.declarations || code.declarations.length === 0) {
867
- continue;
868
- }
869
- const declaration = code.declarations[0];
870
- if (declaration.id && declaration.id.name === declaratinName) {
871
- const declerationCode = this.fileContent.substring(declaration.start, declaration.end);
872
- let value = null;
873
- let vmContext = {
874
- console: console,
875
- value: value,
876
- };
877
- vm.createContext(vmContext);
878
- vm.runInContext("value = " + declerationCode + ";", vmContext);
879
- //console.log("value", vmContext.value);
880
- return vmContext.value;
881
- }
882
- }
883
- }
884
- return null;
885
- }
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
+ }
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;
126
+ }
886
127
  }
887
-
888
128
  const cutSubString = (str, substrings) => {
889
- let result = str;
890
- let offset = 0;
891
- substrings.forEach((substring) => {
892
- const start = substring[0];
893
- const end = substring[1];
894
- result = result.substring(0, start - offset) + result.substring(end - offset);
895
- offset += end - start;
896
- });
897
- return result;
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;
134
+ });
135
+ return result;
898
136
  };
899
-
900
- // function stringifyObject(obj, indent = 4, currentIndent = "") {
901
- // if (typeof obj !== "object" || obj === null) {
902
- // return JSON.stringify(obj);
903
- // }
904
-
905
- // if (Array.isArray(obj)) {
906
- // const items = obj.map(
907
- // (item) =>
908
- // `${currentIndent}${" ".repeat(indent)}${stringifyObject(item, indent, currentIndent + " ".repeat(indent))}`
909
- // );
910
- // return `[\n${items.join(",\n")}\n${currentIndent}]`;
911
- // }
912
-
913
- // const keys = Object.keys(obj);
914
- // const keyValues = keys.map(
915
- // (key) =>
916
- // `${currentIndent}${" ".repeat(indent)}${key}: ${stringifyObject(
917
- // obj[key],
918
- // indent,
919
- // currentIndent + " ".repeat(indent)
920
- // )}`
921
- // );
922
-
923
- // return `{\n${keyValues.join(",\n")}\n${currentIndent}}`;
924
- // }
925
- export { CodePage, CodeStatus };
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();
153
+ }
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.info("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
+ }
181
+ }
182
+ writeFileSync(this.sourceFileName, this.fileContent, "utf8");
183
+ return true;
184
+ }
185
+ else {
186
+ logger.error("sourceFileName is null");
187
+ return false;
188
+ }
189
+ }
190
+ generateModel(fileContent = null) {
191
+ if (fileContent !== null) {
192
+ this.fileContent = fileContent;
193
+ }
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
+ }
203
+ }
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
+ });
234
+ }
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
+ }
253
+ }
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;
289
+ }
290
+ }
291
+ collectAllTemplates() {
292
+ const templates = [];
293
+ if (this.cucumberCalls.length > 0) {
294
+ for (let i = 0; i < this.cucumberCalls.length; i++) {
295
+ const cucumberCall = this.cucumberCalls[i];
296
+ const pattern = (cucumberCall.pattern ?? "");
297
+ const methodName = (cucumberCall.methodName ?? "");
298
+ let foundMethod = false;
299
+ let params = [];
300
+ const stepType = cucumberCall["stepType"];
301
+ let firstFind = true;
302
+ const stepPaths = [];
303
+ for (let j = 0; j < this.methods.length; j++) {
304
+ const method = this.methods[j];
305
+ if (method.name === methodName) {
306
+ if (firstFind) {
307
+ foundMethod = true;
308
+ const paramsObj = (method.node?.params ?? []);
309
+ if (paramsObj && paramsObj.length > 0) {
310
+ params = paramsObj.map((p) => p.name);
311
+ }
312
+ firstFind = false;
313
+ }
314
+ stepPaths.push(method.path ?? "");
315
+ }
316
+ }
317
+ if (foundMethod) {
318
+ templates.push({ pattern, methodName, params, stepType, paths: stepPaths });
319
+ }
320
+ }
321
+ }
322
+ return templates;
323
+ }
324
+ getExpectedTimeout(expectedNumofCmds, finalTimeout) {
325
+ const timeoutNum = typeof finalTimeout === "string" ? parseFloat(finalTimeout) : finalTimeout;
326
+ if (finalTimeout !== undefined && !isNaN(timeoutNum)) {
327
+ return -1;
328
+ }
329
+ return (expectedNumofCmds ?? 0) * 60 * 1000;
330
+ }
331
+ addCucumberStep(type, cucumberLine, method, expectedNumofCmds, finalTimeout) {
332
+ let result = {};
333
+ let code = "\n";
334
+ code += `${type}(${JSON.stringify(cucumberLine)}, ${expectedNumofCmds ? `{ timeout: ${this.getExpectedTimeout(expectedNumofCmds, finalTimeout)}}, ` : ""}${method});\n`;
335
+ let existCodePart = null;
336
+ for (let i = 0; i < this.cucumberCalls.length; i++) {
337
+ if (this.cucumberCalls[i].pattern === cucumberLine) {
338
+ existCodePart = this.cucumberCalls[i];
339
+ break;
340
+ }
341
+ }
342
+ result.cucumberLine = cucumberLine;
343
+ if (existCodePart) {
344
+ if (existCodePart.keyword === type || existCodePart.methodName === method) {
345
+ logger.debug(`step ${cucumberLine} already exist with the same code`);
346
+ result.status = CodeStatus.NO_CHANGE;
347
+ return result;
348
+ }
349
+ result.status = CodeStatus.UPDATED;
350
+ result.oldCode = existCodePart.codePart.trim();
351
+ result.newCode = code.trim();
352
+ logger.debug(`conflict in step ${result.cucumberLine}}`);
353
+ logger.debug("old code\n", result.oldCode);
354
+ logger.debug("new code\n", result.newCode);
355
+ this._replaceCode(code, existCodePart.start, existCodePart.end);
356
+ return result;
357
+ }
358
+ else {
359
+ const injectIndex = this._getCucumberInjectionIndex();
360
+ this._insertCode(code, injectIndex);
361
+ result.status = CodeStatus.ADD;
362
+ result.newCode = code.trim();
363
+ return result;
364
+ }
365
+ }
366
+ addInfraCommand(methodName, description, stepVarables, stepCodeLines, protectStep = false, source = null, codePath = "") {
367
+ let code = "\n";
368
+ code += "/**\n";
369
+ code += ` * ${description}\n`;
370
+ const stepParametersMap = {};
371
+ let i = 1;
372
+ stepVarables.forEach((origKey) => {
373
+ let key = origKey;
374
+ if (stepParametersMap[key]) {
375
+ while (stepParametersMap[`${key}${i}`])
376
+ i++;
377
+ key = `${key}${i}`;
378
+ }
379
+ stepParametersMap[key] = key;
380
+ });
381
+ const stepVarablesNoDuplications = Object.keys(stepParametersMap).map((k) => "_" + convertToIdentifier(k));
382
+ if (stepVarables.length > stepVarablesNoDuplications.length) {
383
+ const varName = "dummy";
384
+ i = 1;
385
+ while (stepVarablesNoDuplications.length !== stepVarables.length) {
386
+ if (!stepVarablesNoDuplications.includes(`${varName}${i}`)) {
387
+ stepVarablesNoDuplications.push(`${varName}${i}`);
388
+ }
389
+ i++;
390
+ }
391
+ }
392
+ stepVarablesNoDuplications.forEach((k) => {
393
+ code += ` * @param {string} ${k} ${k.replaceAll("_", " ")}\n`;
394
+ });
395
+ const tags = [];
396
+ if (protectStep)
397
+ tags.push("@protect");
398
+ if (source)
399
+ tags.push(`@${source}`);
400
+ if (tags.length > 0)
401
+ code += ` * ${tags.join(" ")}\n`;
402
+ if (codePath !== null)
403
+ code += ` * @path=${escapeForComment(codePath)}\n`;
404
+ code += " */\n";
405
+ code += `async function ${methodName} (${stepVarablesNoDuplications.join(", ")}){\n`;
406
+ code += `// source: ${source}\n`;
407
+ code += `// implemented_at: ${new Date().toISOString()}\n`;
408
+ if (stepCodeLines.length > 0) {
409
+ code += ` const _params = { ${stepVarablesNoDuplications.join(", ")} };\n`;
410
+ }
411
+ else {
412
+ code += " const _params = {};\n";
413
+ }
414
+ stepCodeLines.forEach((line) => (code += ` ${line}\n`));
415
+ code += "}\n";
416
+ return this._injectMethod(methodName, code);
417
+ }
418
+ _injectMethod(methodName, code) {
419
+ const result = { methodName };
420
+ code = code.trim();
421
+ let existMethod = null;
422
+ for (let i = 0; i < this.methods.length; i++) {
423
+ if (this.methods[i].name === methodName) {
424
+ existMethod = this.methods[i];
425
+ break;
426
+ }
427
+ }
428
+ if (existMethod) {
429
+ const oldCode = existMethod.codePart.trim();
430
+ if (oldCode === code) {
431
+ logger.debug(`method ${methodName} already exist with the same code`);
432
+ result.status = CodeStatus.NO_CHANGE;
433
+ return result;
434
+ }
435
+ logger.debug(`conflict in method ${methodName}}`);
436
+ logger.debug("old code\n", oldCode);
437
+ logger.debug("new code\n", code.trim());
438
+ this._replaceCode(code, existMethod.start, existMethod.end);
439
+ result.status = CodeStatus.UPDATED;
440
+ result.oldCode = oldCode;
441
+ result.newCode = code;
442
+ return result;
443
+ }
444
+ else {
445
+ const injectIndex = this._getMethodInjectionIndex();
446
+ this._insertCode(code, injectIndex);
447
+ result.status = CodeStatus.ADD;
448
+ result.newCode = code;
449
+ return result;
450
+ }
451
+ }
452
+ isHumanMethod(methodName) {
453
+ if (this.sourceFileName && path.basename(this.sourceFileName) === "utils.mjs")
454
+ return true;
455
+ let existMethod = null;
456
+ for (let i = 0; i < this.methods.length; i++) {
457
+ if (this.methods[i].name === methodName) {
458
+ existMethod = this.methods[i];
459
+ break;
460
+ }
461
+ }
462
+ if (!existMethod)
463
+ return false;
464
+ const methodCode = existMethod.getCode();
465
+ if (methodCode && (methodCode.includes("@protect") || methodCode.includes("@human"))) {
466
+ return true;
467
+ }
468
+ return false;
469
+ }
470
+ _insertCode(code, injectIndex) {
471
+ const newFileContent = this.fileContent.substring(0, injectIndex) + "\n" + code + this.fileContent.substring(injectIndex);
472
+ this._init();
473
+ try {
474
+ this.generateModel(newFileContent);
475
+ }
476
+ catch (e) {
477
+ logger.error("new code ####\n" + newFileContent);
478
+ throw e;
479
+ }
480
+ }
481
+ _elementToCode(key, element) {
482
+ let code = "";
483
+ code += ` ${key}:${JSON.stringify(element)}`;
484
+ return code;
485
+ }
486
+ _removeScoresFromElement(element) {
487
+ if (!element || !element.locators)
488
+ return element;
489
+ const clone = JSON.parse(JSON.stringify(element));
490
+ for (let i = 0; i < (clone.locators?.length ?? 0); i++) {
491
+ const locator = clone.locators[i];
492
+ if (locator.score)
493
+ delete locator.score;
494
+ }
495
+ return clone;
496
+ }
497
+ insertElements(elements) {
498
+ let code = "const elements = {\n";
499
+ const keys = Object.keys(elements);
500
+ const lines = [];
501
+ keys.forEach((key) => {
502
+ const element = this._removeScoresFromElement(elements[key]);
503
+ lines.push(this._elementToCode(key, element));
504
+ });
505
+ code += lines.join(",\n");
506
+ code += "\n};";
507
+ const startEnd = this._getVariableStartEnd("elements");
508
+ if (startEnd) {
509
+ this._replaceCode(code, startEnd[0], startEnd[1]);
510
+ }
511
+ }
512
+ mergeSimilarElements() {
513
+ const elements = this.getVariableDeclarationAsObject("elements");
514
+ if (elements === null)
515
+ return;
516
+ const keys = Object.keys(elements);
517
+ for (let i = 0; i < keys.length; i++) {
518
+ const key = keys[i];
519
+ if (!elements[key].element_key) {
520
+ elements[key].element_key = key;
521
+ }
522
+ }
523
+ const replacedKeys = {};
524
+ const mergedElements = {};
525
+ for (let i = keys.length - 1; i >= 0; i--) {
526
+ const currentElement = elements[keys[i]];
527
+ let foundMatch = false;
528
+ for (let j = i - 1; j >= 0; j--) {
529
+ const nextElement = elements[keys[j]];
530
+ if (this._isSimilarElement(currentElement, nextElement)) {
531
+ mergedElements[currentElement.element_key] = currentElement;
532
+ replacedKeys[nextElement.element_key] = currentElement.element_key;
533
+ foundMatch = true;
534
+ keys.splice(j, 1);
535
+ i--;
536
+ j++;
537
+ }
538
+ }
539
+ if (!foundMatch) {
540
+ mergedElements[currentElement.element_key] = currentElement;
541
+ }
542
+ }
543
+ if (Object.keys(replacedKeys).length === 0)
544
+ return;
545
+ for (const key in replacedKeys) {
546
+ const regexp = new RegExp(`elements\\[\\s*["']${key}["']\\s*\\]`, "g");
547
+ this.fileContent = this.fileContent.replace(regexp, () => `elements["${replacedKeys[key]}"]`);
548
+ }
549
+ this.insertElements(mergedElements);
550
+ }
551
+ _isSimilarElement(element1, element2) {
552
+ if (!element1 || !element2)
553
+ return false;
554
+ const locs1 = Array.isArray(element1.locators) ? element1.locators : [];
555
+ const locs2 = Array.isArray(element2.locators) ? element2.locators : [];
556
+ if (locs1.length === 0 && locs2.length === 0)
557
+ return false;
558
+ if (locs1.length === 0 || locs2.length === 0)
559
+ return false;
560
+ const moreLocatorsElement = locs1.length >= locs2.length ? locs1 : locs2;
561
+ const lessLocatorsElement = locs1.length >= locs2.length ? locs2 : locs1;
562
+ const locatorIncludes = (hay, needle) => {
563
+ if (hay == null || needle == null)
564
+ return false;
565
+ const KEYS = ["css", "index", "text", "climb"];
566
+ for (const k of KEYS) {
567
+ if (needle[k] !== undefined) {
568
+ if (!(k in hay))
569
+ return false;
570
+ if (hay[k] !== needle[k])
571
+ return false;
572
+ }
573
+ }
574
+ return true;
575
+ };
576
+ for (const less of lessLocatorsElement) {
577
+ const match = moreLocatorsElement.some((more) => locatorIncludes(more, less));
578
+ if (!match)
579
+ return false;
580
+ }
581
+ return true;
582
+ }
583
+ removeUnusedElements() {
584
+ const usedElementsKeys = this.getUsedElementsKeys();
585
+ const elements = this.getVariableDeclarationAsObject("elements");
586
+ if (!elements)
587
+ return;
588
+ const keys = Object.keys(elements);
589
+ const unusedKeys = keys.filter((key) => !usedElementsKeys.includes(key));
590
+ if (unusedKeys.length === 0)
591
+ return;
592
+ for (let i = 0; i < unusedKeys.length; i++) {
593
+ delete elements[unusedKeys[i]];
594
+ }
595
+ this.insertElements(elements);
596
+ }
597
+ getUsedElementsKeys() {
598
+ const regexp = /elements\[[\n\r\W]*"([^"]+)"[\n\r\W]*\]/g;
599
+ const keys = [];
600
+ let match;
601
+ while ((match = regexp.exec(this.fileContent)) !== null) {
602
+ keys.push(match[1]);
603
+ }
604
+ return keys;
605
+ }
606
+ _replaceCode(code, injectIndexStart, injectIndexEnd) {
607
+ const newFileContent = this.fileContent.substring(0, injectIndexStart) + code + this.fileContent.substring(injectIndexEnd);
608
+ this._init();
609
+ this.generateModel(newFileContent);
610
+ }
611
+ _getMethodInjectionIndex() {
612
+ return this.fileContent.length;
613
+ }
614
+ _getCucumberInjectionIndex() {
615
+ return this.fileContent.length;
616
+ }
617
+ addLocatorsMetadata(locatorsMetadata) {
618
+ if (!this.sourceFileName)
619
+ return;
620
+ let locatorsMetadataFileName = this.sourceFileName.replace(".mjs", ".json");
621
+ const config = getAiConfig();
622
+ if (config && config.locatorsMetadataDir) {
623
+ locatorsMetadataFileName = path.join(config.locatorsMetadataDir, path.basename(locatorsMetadataFileName));
624
+ if (!existsSync(path.dirname(locatorsMetadataFileName))) {
625
+ mkdirSync(path.dirname(locatorsMetadataFileName), { recursive: true });
626
+ }
627
+ }
628
+ let metadata = {};
629
+ try {
630
+ if (existsSync(locatorsMetadataFileName)) {
631
+ metadata = JSON.parse(readFileSync(locatorsMetadataFileName, "utf8"));
632
+ }
633
+ }
634
+ catch {
635
+ logger.info("failed to read locators metadata file", locatorsMetadataFileName);
636
+ }
637
+ const keys = Object.keys(locatorsMetadata);
638
+ keys.forEach((key) => {
639
+ metadata[key] = locatorsMetadata[key];
640
+ });
641
+ try {
642
+ writeFileSync(locatorsMetadataFileName, JSON.stringify(metadata, null, 2), "utf8");
643
+ }
644
+ catch {
645
+ logger.info("failed to write locators metadata file", locatorsMetadataFileName);
646
+ }
647
+ }
648
+ _getVariableStartEnd(variableName) {
649
+ const codePart = this.variables.filter((cp) => cp.node.declarations[0].id.name === variableName)[0];
650
+ if (codePart === undefined)
651
+ return null;
652
+ return [codePart.node.start, codePart.node.end];
653
+ }
654
+ getVariableDeclarationCode(variableName) {
655
+ const codePart = this.variables.filter((cp) => cp.node.declarations[0].id.name === variableName)[0];
656
+ if (codePart === undefined)
657
+ return null;
658
+ return codePart.codePart;
659
+ }
660
+ getVariableDeclarationAsObject(variableName) {
661
+ const code = this.getVariableDeclarationCode(variableName);
662
+ if (code === null)
663
+ return null;
664
+ const vmContext = { console, value: null };
665
+ vm.createContext(vmContext);
666
+ vm.runInContext(code + "\nvalue = " + variableName + ";", vmContext);
667
+ return vmContext.value;
668
+ }
669
+ getName() {
670
+ let base = path.parse(this.sourceFileName ?? "").base.split(".")[0];
671
+ if (base.endsWith("_page"))
672
+ base = base.substring(0, base.length - 5);
673
+ return base;
674
+ }
675
+ getMethodsNames() {
676
+ const methods = [];
677
+ this.methods.forEach((codePart) => {
678
+ if (codePart.name && !codePart.name.startsWith("_")) {
679
+ methods.push(codePart.name);
680
+ }
681
+ });
682
+ return methods;
683
+ }
684
+ getMethodParametersValues(methodName, parameters) {
685
+ const params = this.getMethodParameters(methodName);
686
+ const values = [];
687
+ for (let i = 0; i < params.length; i++) {
688
+ const param = params[i];
689
+ values.push(parameters[param]);
690
+ }
691
+ return values;
692
+ }
693
+ getMethodParameters(methodName) {
694
+ const codePart = this.methods.find((cp) => cp.name === methodName);
695
+ if (!codePart)
696
+ return [];
697
+ const params = codePart.node.value?.params ?? [];
698
+ return params.map((param) => param.name);
699
+ }
700
+ getImportCode(excludeSources = []) {
701
+ let code = "";
702
+ this.importsObjects.forEach((importObject) => {
703
+ if (excludeSources.includes(importObject.source))
704
+ return;
705
+ if (importObject.type === "ImportSpecifier") {
706
+ code += `const { ${importObject.local} } = require('${importObject.source}');\n`;
707
+ }
708
+ else if (importObject.type === "ImportDefaultSpecifier") {
709
+ code += `const ${importObject.local} = require('${importObject.source}');\n`;
710
+ }
711
+ });
712
+ return code;
713
+ }
714
+ getCodeWithoutImports(removeCucumber = true) {
715
+ let text = this.fileContent;
716
+ if (removeCucumber) {
717
+ const expressionsLoc = [];
718
+ this.expressions.forEach((codePart) => {
719
+ expressionsLoc.push([codePart.start, codePart.end]);
720
+ });
721
+ text = cutSubString(text, expressionsLoc);
722
+ }
723
+ let lastImportEnd = 0;
724
+ this.imports.forEach((codePart) => {
725
+ lastImportEnd = codePart.end;
726
+ });
727
+ text = text.substring(lastImportEnd);
728
+ text = text.replace(/^\s*export\s+(?=async\s+function|function)/gm, "");
729
+ return text;
730
+ }
731
+ hasMethod(methodName) {
732
+ return this.methods.filter((cp) => cp.name === methodName).length > 0;
733
+ }
734
+ _getMethodComments(methodName) {
735
+ const codePart = this.methods.filter((cp) => cp.name === methodName)[0];
736
+ if (codePart === undefined)
737
+ return null;
738
+ const comments = codePart.node.leadingComments;
739
+ if (!comments || comments.length === 0)
740
+ return null;
741
+ const commentsValues = comments.map((c) => c.value);
742
+ return commentsValues.join("\n");
743
+ }
744
+ /**
745
+ * get the description and parameters of a method
746
+ * @param methodName
747
+ * @returns a dictionary with name, description and params
748
+ */
749
+ getMethodCommentsData(methodName) {
750
+ const codePart = this.methods.filter((cp) => cp.name === methodName)[0];
751
+ if (codePart === undefined)
752
+ return null;
753
+ const comments = codePart.node.leadingComments;
754
+ if (!comments || comments.length === 0)
755
+ return null;
756
+ const commentsValues = comments.map((c) => c.value);
757
+ const fullCommentText = commentsValues.join("\n");
758
+ const commentLines = fullCommentText.split("\n").map((line) => line.trim().replace(/^\*/, "").trim());
759
+ const descriptions = [];
760
+ for (let i = 0; i < commentLines.length; i++) {
761
+ const line = commentLines[i];
762
+ if (!line)
763
+ continue;
764
+ if (line.startsWith("@"))
765
+ break;
766
+ descriptions.push(line);
767
+ }
768
+ const description = descriptions.join(".").replace("..", ".");
769
+ const params = [];
770
+ for (let i = 0; i < commentLines.length; i++) {
771
+ const line = commentLines[i];
772
+ if (line.startsWith("@param")) {
773
+ params.push(this._processParametersLine(line));
774
+ }
775
+ }
776
+ return { name: methodName, description, params };
777
+ }
778
+ /**
779
+ * get the method in a format that can be used in the gpt prompt
780
+ * @param methodName
781
+ * @returns a string formatted to be used in the commands gpt prompt
782
+ */
783
+ getMethodInCommandFormat(methodName) {
784
+ const methodComment = this.getMethodCommentsData(methodName);
785
+ if (methodComment === null)
786
+ return null;
787
+ return `${methodName}: ${methodComment.description}, args: ${methodComment.params
788
+ .map((p) => `"${p.name}": "<${(p.description ?? "").replaceAll(" ", "_")}>"`)
789
+ .join(", ")}`;
790
+ }
791
+ _processParametersLine(paramLine) {
792
+ const paramParts = paramLine.split(/\s+/);
793
+ paramParts.shift(); // @param
794
+ let type = null;
795
+ if (paramParts.length > 0 && paramParts[0].startsWith("{") && paramParts[0].endsWith("}")) {
796
+ type = paramParts.shift().replace("{", "").replace("}", "");
797
+ }
798
+ let name = null;
799
+ if (paramParts.length > 0) {
800
+ name = paramParts.shift() ?? null;
801
+ }
802
+ let description = null;
803
+ if (paramParts.length > 0) {
804
+ description = paramParts.join(" ");
805
+ }
806
+ return { type, name, description };
807
+ }
808
+ getMethodCodePart(methodName) {
809
+ const codePart = this.methods.filter((cp) => cp.name === methodName)[0];
810
+ return codePart ?? null;
811
+ }
812
+ getStepDefinitionBreakdown(methodName) {
813
+ const codePart = this.methods.filter((cp) => cp.name === methodName)[0];
814
+ if (codePart === undefined)
815
+ return null;
816
+ const parametersNames = [];
817
+ if (codePart.node && codePart.node.body && codePart.node.body.parent && codePart.node.body.parent.params) {
818
+ codePart.node.body.parent.params.forEach((param) => {
819
+ parametersNames.push(param.name);
820
+ });
821
+ }
822
+ const commands = [];
823
+ if (codePart.node &&
824
+ codePart.node.type === "FunctionDeclaration" &&
825
+ codePart.node.body &&
826
+ codePart.node.body.body &&
827
+ codePart.node.body.body.length > 0) {
828
+ const codeBody = codePart.node.body.body;
829
+ for (let i = 0; i < codeBody.length; i++) {
830
+ const code = codeBody[i];
831
+ commands.push({
832
+ type: code.type,
833
+ code: this.fileContent.substring(code.start, code.end),
834
+ });
835
+ }
836
+ }
837
+ return { codeCommands: commands, parametersNames };
838
+ }
839
+ hasStep(step) {
840
+ const regexp = new RegExp(`"${step}"`, "g");
841
+ for (let i = 0; i < this.cucumberCalls.length; i++) {
842
+ const expression = this.cucumberCalls[i];
843
+ if (expression.codePart) {
844
+ if (expression.codePart.match(regexp)) {
845
+ return true;
846
+ }
847
+ }
848
+ }
849
+ return false;
850
+ }
851
+ hasFunctionName(functionName) {
852
+ for (let i = 0; i < this.methods.length; i++) {
853
+ if (this.methods[i].name === functionName) {
854
+ return true;
855
+ }
856
+ return false; // (Preserved original behavior)
857
+ }
858
+ return false;
859
+ }
860
+ }
861
+ export { escapeForComment, unescapeFromComment };