@dev-blinq/cucumber_client 1.0.1407-dev → 1.0.1407-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.
- package/bin/assets/bundled_scripts/recorder.js +105 -105
- package/bin/assets/preload/css_gen.js +10 -10
- package/bin/assets/preload/toolbar.js +27 -29
- package/bin/assets/preload/unique_locators.js +1 -1
- package/bin/assets/preload/yaml.js +288 -275
- package/bin/assets/scripts/aria_snapshot.js +223 -220
- package/bin/assets/scripts/dom_attr.js +329 -329
- package/bin/assets/scripts/dom_parent.js +169 -174
- package/bin/assets/scripts/event_utils.js +94 -94
- package/bin/assets/scripts/pw.js +2050 -1949
- package/bin/assets/scripts/recorder.js +70 -45
- package/bin/assets/scripts/snapshot_capturer.js +147 -147
- package/bin/assets/scripts/unique_locators.js +163 -44
- package/bin/assets/scripts/yaml.js +796 -783
- package/bin/assets/templates/_hooks_template.txt +6 -2
- package/bin/assets/templates/utils_template.txt +16 -16
- package/bin/client/code_cleanup/find_step_definition_references.js +0 -1
- package/bin/client/code_cleanup/utils.js +5 -1
- package/bin/client/code_gen/api_codegen.js +2 -2
- package/bin/client/code_gen/code_inversion.js +63 -2
- package/bin/client/code_gen/function_signature.js +4 -0
- package/bin/client/code_gen/page_reflection.js +846 -906
- package/bin/client/code_gen/playwright_codeget.js +27 -3
- package/bin/client/cucumber/feature.js +4 -0
- package/bin/client/cucumber/feature_data.js +2 -2
- package/bin/client/cucumber/project_to_document.js +8 -2
- package/bin/client/cucumber/steps_definitions.js +6 -3
- package/bin/client/cucumber_selector.js +17 -1
- package/bin/client/local_agent.js +3 -2
- package/bin/client/parse_feature_file.js +23 -26
- package/bin/client/playground/projects/env.json +2 -2
- package/bin/client/project.js +186 -202
- package/bin/client/recorderv3/bvt_init.js +349 -0
- package/bin/client/recorderv3/bvt_recorder.js +1038 -76
- package/bin/client/recorderv3/implemented_steps.js +2 -0
- package/bin/client/recorderv3/index.js +4 -311
- package/bin/client/recorderv3/scriptTest.js +1 -1
- package/bin/client/recorderv3/services.js +814 -154
- package/bin/client/recorderv3/step_runner.js +36 -10
- package/bin/client/recorderv3/step_utils.js +499 -37
- package/bin/client/recorderv3/update_feature.js +9 -5
- package/bin/client/recorderv3/wbr_entry.js +61 -0
- package/bin/client/recording.js +1 -0
- package/bin/client/upload-service.js +3 -2
- package/bin/client/utils/socket_logger.js +132 -0
- package/bin/index.js +4 -1
- package/bin/logger.js +3 -2
- package/bin/min/consoleApi.min.cjs +2 -3
- package/bin/min/injectedScript.min.cjs +16 -16
- package/package.json +19 -9
|
@@ -1,929 +1,869 @@
|
|
|
1
|
+
// code_page.ts
|
|
1
2
|
import { writeFileSync, readFileSync, existsSync, mkdirSync } from "fs";
|
|
2
|
-
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
+
return text
|
|
21
|
+
.replace(/\\/g, "\\\\") // Escape backslashes
|
|
22
|
+
.replace(/\*\//g, "*\\/"); // Escape comment-closing sequence
|
|
20
23
|
}
|
|
21
24
|
function unescapeFromComment(text) {
|
|
22
|
-
|
|
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
|
-
|
|
29
|
-
|
|
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, 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++;
|
|
300
|
-
}
|
|
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}`);
|
|
312
|
-
}
|
|
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();
|
|
29
|
+
if (ai_config)
|
|
30
|
+
return ai_config;
|
|
413
31
|
try {
|
|
414
|
-
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
|
|
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
|
-
removeUnusedElements() {
|
|
456
|
-
const usedElementsKeys = this.getUsedElementsKeys();
|
|
457
|
-
const elements = this.getVariableDeclarationAsObject("elements");
|
|
458
|
-
const keys = Object.keys(elements);
|
|
459
|
-
const unusedKeys = keys.filter((key) => !usedElementsKeys.includes(key));
|
|
460
|
-
if (unusedKeys.length === 0) {
|
|
461
|
-
return;
|
|
462
|
-
}
|
|
463
|
-
for (let i = 0; i < unusedKeys.length; i++) {
|
|
464
|
-
delete elements[unusedKeys[i]];
|
|
465
|
-
}
|
|
466
|
-
this.insertElements(elements);
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
getUsedElementsKeys() {
|
|
470
|
-
const regexp = /elements\[[\n\r\W]*"([^"]+)"[\n\r\W]*\]/g;
|
|
471
|
-
const keys = [];
|
|
472
|
-
let match;
|
|
473
|
-
while ((match = regexp.exec(this.fileContent)) !== null) {
|
|
474
|
-
keys.push(match[1]);
|
|
475
|
-
}
|
|
476
|
-
return keys;
|
|
477
|
-
}
|
|
478
|
-
_replaceCode(code, injectIndexStart, injectIndexEnd) {
|
|
479
|
-
let newFileContent =
|
|
480
|
-
this.fileContent.substring(0, injectIndexStart) + code + this.fileContent.substring(injectIndexEnd);
|
|
481
|
-
this._init();
|
|
482
|
-
this.generateModel(newFileContent);
|
|
483
|
-
}
|
|
484
|
-
_getMethodInjectionIndex() {
|
|
485
|
-
return this.fileContent.length;
|
|
486
|
-
}
|
|
487
|
-
_getCucumberInjectionIndex() {
|
|
488
|
-
return this.fileContent.length;
|
|
489
|
-
}
|
|
490
|
-
addLocatorsMetadata(locatorsMetadata) {
|
|
491
|
-
// create a file name based on the source file name replace .mjs with .json
|
|
492
|
-
let locatorsMetadataFileName = this.sourceFileName.replace(".mjs", ".json");
|
|
493
|
-
const config = getAiConfig();
|
|
494
|
-
if (config && config.locatorsMetadataDir) {
|
|
495
|
-
// if config.locatorsMetadataDir is set, use it to create the file path
|
|
496
|
-
locatorsMetadataFileName = path.join(config.locatorsMetadataDir, path.basename(locatorsMetadataFileName));
|
|
497
|
-
// check if the directory exists, if not create it
|
|
498
|
-
if (!existsSync(path.dirname(locatorsMetadataFileName))) {
|
|
499
|
-
mkdirSync(path.dirname(locatorsMetadataFileName), { recursive: true });
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
let metadata = {};
|
|
503
|
-
// try to read the file to metadata, protect with try catch
|
|
504
|
-
try {
|
|
505
|
-
// if the file exist read the content and parse it
|
|
506
|
-
if (existsSync(locatorsMetadataFileName)) {
|
|
507
|
-
metadata = JSON.parse(readFileSync(locatorsMetadataFileName, "utf8"));
|
|
508
|
-
}
|
|
509
|
-
} catch (e) {
|
|
510
|
-
logger.info("failed to read locators metadata file", locatorsMetadataFileName);
|
|
511
|
-
}
|
|
512
|
-
// merge the locatorsMetadata with the metadata
|
|
513
|
-
// go over all the keys in locatorsMetadata and add them to metadata
|
|
514
|
-
const keys = Object.keys(locatorsMetadata);
|
|
515
|
-
keys.forEach((key) => {
|
|
516
|
-
metadata[key] = locatorsMetadata[key];
|
|
517
|
-
});
|
|
518
|
-
// save the metadata back to the file
|
|
519
|
-
try {
|
|
520
|
-
writeFileSync(locatorsMetadataFileName, JSON.stringify(metadata, null, 2), "utf8");
|
|
521
|
-
} catch (e) {
|
|
522
|
-
logger.info("failed to write locators metadata file", locatorsMetadataFileName);
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
_getVariableStartEnd(variableName) {
|
|
526
|
-
const codePart = this.variables.filter((cp) => cp.node.declarations[0].id.name === variableName)[0];
|
|
527
|
-
if (codePart === undefined) {
|
|
528
|
-
return null;
|
|
529
|
-
}
|
|
530
|
-
return [codePart.node.start, codePart.node.end];
|
|
531
|
-
}
|
|
532
|
-
getVariableDeclarationCode(variableName) {
|
|
533
|
-
const codePart = this.variables.filter((cp) => cp.node.declarations[0].id.name === variableName)[0];
|
|
534
|
-
if (codePart === undefined) {
|
|
535
|
-
return null;
|
|
536
|
-
}
|
|
537
|
-
return codePart.codePart;
|
|
538
|
-
}
|
|
539
|
-
getVariableDeclarationAsObject(variableName) {
|
|
540
|
-
const code = this.getVariableDeclarationCode(variableName);
|
|
541
|
-
if (code === null) {
|
|
542
|
-
return null;
|
|
543
|
-
}
|
|
544
|
-
let value = null;
|
|
545
|
-
let vmContext = {
|
|
546
|
-
console: console,
|
|
547
|
-
value: value,
|
|
548
|
-
};
|
|
549
|
-
vm.createContext(vmContext);
|
|
550
|
-
vm.runInContext(code + "\nvalue = " + variableName + ";", vmContext);
|
|
551
|
-
//console.log("value", vmContext.value);
|
|
552
|
-
return vmContext.value;
|
|
553
|
-
}
|
|
554
|
-
getName() {
|
|
555
|
-
let base = path.parse(this.sourceFileName).base.split(".")[0];
|
|
556
|
-
if (base.endsWith("_page")) {
|
|
557
|
-
base = base.substring(0, base.length - 5);
|
|
558
|
-
}
|
|
559
|
-
return base;
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
getMethodsNames() {
|
|
563
|
-
const methods = [];
|
|
564
|
-
this.methods.forEach((codePart) => {
|
|
565
|
-
if (this.codePart.name && !codePart.name.startsWith("_")) {
|
|
566
|
-
methods.push(codePart.name);
|
|
567
|
-
}
|
|
568
|
-
});
|
|
569
|
-
return methods;
|
|
570
|
-
}
|
|
571
|
-
getMethodParametersValues(methodName, parameters) {
|
|
572
|
-
const params = this.getMethodParameters(methodName);
|
|
573
|
-
const values = [];
|
|
574
|
-
for (let i = 0; i < params.length; i++) {
|
|
575
|
-
const param = params[i];
|
|
576
|
-
values.push(parameters[param]);
|
|
577
|
-
}
|
|
578
|
-
return values;
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
getMethodParameters(methodName) {
|
|
582
|
-
const codePart = this.methods.find((cp) => cp.name === methodName);
|
|
583
|
-
|
|
584
|
-
if (!codePart) {
|
|
585
|
-
// Handle the case where methodName is not found
|
|
586
|
-
return [];
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
const params = codePart.node.value.params;
|
|
590
|
-
return params.map((param) => param.name);
|
|
591
|
-
}
|
|
592
|
-
/*
|
|
593
|
-
importsObjects example
|
|
594
|
-
{
|
|
595
|
-
type: 'ImportSpecifier',
|
|
596
|
-
local: 'Given',
|
|
597
|
-
imported: 'Given',
|
|
598
|
-
source: '@dev-blinq/cucumber-js'
|
|
599
|
-
}
|
|
600
|
-
code example
|
|
601
|
-
const fs = await import('fs');
|
|
602
|
-
*/
|
|
603
|
-
getImportCode(excludeSources = []) {
|
|
604
|
-
let code = "";
|
|
605
|
-
this.importsObjects.forEach((importObject) => {
|
|
606
|
-
if (excludeSources.includes(importObject.source)) {
|
|
607
|
-
return;
|
|
608
|
-
}
|
|
609
|
-
if (importObject.type === "ImportSpecifier") {
|
|
610
|
-
code += `const { ${importObject.local} } = require('${importObject.source}');\n`;
|
|
611
|
-
} else if (importObject.type === "ImportDefaultSpecifier") {
|
|
612
|
-
code += `const ${importObject.local} = require('${importObject.source}');\n`;
|
|
613
|
-
}
|
|
614
|
-
});
|
|
615
|
-
return code;
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
getCodeWithoutImports(removeCucumber = true) {
|
|
619
|
-
let text = this.fileContent;
|
|
620
|
-
if (removeCucumber) {
|
|
621
|
-
const expressionsLoc = [];
|
|
622
|
-
this.expressions.forEach((codePart) => {
|
|
623
|
-
expressionsLoc.push([codePart.start, codePart.end]);
|
|
624
|
-
});
|
|
625
|
-
text = cutSubString(text, expressionsLoc);
|
|
626
|
-
}
|
|
627
|
-
let lastImportEnd = 0;
|
|
628
|
-
this.imports.forEach((codePart) => {
|
|
629
|
-
lastImportEnd = codePart.end;
|
|
630
|
-
});
|
|
631
|
-
|
|
632
|
-
text = text.substring(lastImportEnd);
|
|
633
|
-
|
|
634
|
-
// if a line starts with export follow by async or function, remove the export
|
|
635
|
-
text = text.replace(/^\s*export\s+(?=async\s+function|function)/gm, "");
|
|
636
|
-
return text;
|
|
637
|
-
}
|
|
638
|
-
hasMethod(methodName) {
|
|
639
|
-
return this.methods.filter((cp) => cp.name === methodName).length > 0;
|
|
640
|
-
}
|
|
641
|
-
_getMethodComments(methodName) {
|
|
642
|
-
const codePart = this.methods.filter((cp) => cp.name === methodName)[0];
|
|
643
|
-
if (codePart === undefined) {
|
|
644
|
-
return null;
|
|
645
|
-
}
|
|
646
|
-
const comments = codePart.node.leadingComments;
|
|
647
|
-
if (comments === undefined || comments.length === 0) {
|
|
648
|
-
return null;
|
|
649
|
-
}
|
|
650
|
-
const commentsValues = comments.map((comment) => comment.value);
|
|
651
|
-
return commentsValues.join("\n");
|
|
652
|
-
}
|
|
653
|
-
/**
|
|
654
|
-
* get the description and parameters of a method
|
|
655
|
-
* @param {string} methodName
|
|
656
|
-
* @returns a dictionary with name, description and params
|
|
657
|
-
*/
|
|
658
|
-
getMethodCommentsData(methodName) {
|
|
659
|
-
const codePart = this.methods.filter((cp) => cp.name === methodName)[0];
|
|
660
|
-
if (codePart === undefined) {
|
|
661
|
-
return null;
|
|
662
|
-
}
|
|
663
|
-
const comments = codePart.node.leadingComments;
|
|
664
|
-
if (comments === undefined || comments.length === 0) {
|
|
665
|
-
return null;
|
|
666
|
-
}
|
|
667
|
-
const commentsValues = comments.map((comment) => comment.value);
|
|
668
|
-
const fullCommentText = commentsValues.join("\n");
|
|
669
|
-
// split trim and remove * if exist from the beginning of each line
|
|
670
|
-
const commentLines = fullCommentText.split("\n").map((line) => line.trim().replace(/^\*/, "").trim());
|
|
671
|
-
const descriptions = [];
|
|
672
|
-
for (let i = 0; i < commentLines.length; i++) {
|
|
673
|
-
const line = commentLines[i];
|
|
674
|
-
if (!line) {
|
|
675
|
-
continue;
|
|
676
|
-
}
|
|
677
|
-
if (line.startsWith("@")) {
|
|
678
|
-
break;
|
|
679
|
-
}
|
|
680
|
-
descriptions.push(line);
|
|
681
|
-
}
|
|
682
|
-
// join the descriptions into one line seperated by '.' and replace 2 dots with 1
|
|
683
|
-
const description = descriptions.join(".").replace("..", ".");
|
|
684
|
-
// extract parameters from the comment
|
|
685
|
-
const params = [];
|
|
686
|
-
for (let i = 0; i < commentLines.length; i++) {
|
|
687
|
-
const line = commentLines[i];
|
|
688
|
-
if (line.startsWith("@param")) {
|
|
689
|
-
params.push(this._processParametersLine(line));
|
|
690
|
-
}
|
|
691
|
-
}
|
|
692
|
-
return { name: methodName, description, params };
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
/**
|
|
696
|
-
* get the method in a format that can be used in the gpt prompt
|
|
697
|
-
* @param {string} methodName
|
|
698
|
-
* @returns a string formated to be used in the commands gpt prompt
|
|
699
|
-
*/
|
|
700
|
-
getMethodInCommandFormat(methodName) {
|
|
701
|
-
const methodComment = this.getMethodCommentsData(methodName);
|
|
702
|
-
if (methodComment === null) {
|
|
703
|
-
return null;
|
|
704
|
-
}
|
|
705
|
-
return `${methodName}: ${methodComment.description}, args: ${methodComment.params
|
|
706
|
-
.map((p) => `"${p.name}": "<${p.description.replaceAll(" ", "_")}>"`)
|
|
707
|
-
.join(", ")}`;
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
_processParametersLine(paramLine) {
|
|
711
|
-
// split by multiple spaces
|
|
712
|
-
const paramParts = paramLine.split(/\s+/);
|
|
713
|
-
// remove @param
|
|
714
|
-
paramParts.shift();
|
|
715
|
-
// remove type
|
|
716
|
-
let type = null;
|
|
717
|
-
if (paramParts.length > 0 && paramParts[0].startsWith("{") && paramParts[0].endsWith("}")) {
|
|
718
|
-
type = paramParts.shift().replace("{", "").replace("}", "");
|
|
719
|
-
}
|
|
720
|
-
let name = null;
|
|
721
|
-
if (paramParts.length > 0) {
|
|
722
|
-
name = paramParts.shift();
|
|
723
|
-
}
|
|
724
|
-
let description = null;
|
|
725
|
-
if (paramParts.length > 0) {
|
|
726
|
-
description = paramParts.join(" ");
|
|
727
|
-
}
|
|
728
|
-
return { type, name, description };
|
|
729
|
-
}
|
|
730
|
-
getMethodCodePart(methodName) {
|
|
731
|
-
const codePart = this.methods.filter((cp) => cp.name === methodName)[0];
|
|
732
|
-
if (codePart === undefined) {
|
|
733
|
-
return null;
|
|
734
|
-
}
|
|
735
|
-
return codePart;
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
getStepDefinitionBreakdown(methodName) {
|
|
739
|
-
const codePart = this.methods.filter((cp) => cp.name === methodName)[0];
|
|
740
|
-
if (codePart === undefined) {
|
|
741
|
-
return null;
|
|
742
|
-
}
|
|
743
|
-
const parametersNames = [];
|
|
744
|
-
if (codePart.node && codePart.node.body && codePart.node.body.parent && codePart.node.body.parent.params) {
|
|
745
|
-
codePart.node.body.parent.params.forEach((param) => {
|
|
746
|
-
parametersNames.push(param.name);
|
|
747
|
-
});
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
const commands = [];
|
|
751
|
-
if (
|
|
752
|
-
codePart.node &&
|
|
753
|
-
codePart.node.type === "FunctionDeclaration" &&
|
|
754
|
-
codePart.node.body &&
|
|
755
|
-
codePart.node.body.body &&
|
|
756
|
-
codePart.node.body.body.length > 0
|
|
757
|
-
) {
|
|
758
|
-
const codeBody = codePart.node.body.body;
|
|
759
|
-
for (let i = 0; i < codeBody.length; i++) {
|
|
760
|
-
const code = codeBody[i];
|
|
761
|
-
commands.push({
|
|
762
|
-
type: code.type,
|
|
763
|
-
code: this.fileContent.substring(code.start, code.end),
|
|
764
|
-
});
|
|
765
|
-
}
|
|
766
|
-
}
|
|
767
|
-
return { codeCommands: commands, parametersNames };
|
|
768
|
-
}
|
|
769
|
-
hasStep(step) {
|
|
770
|
-
// go over expressions for, looking for expression that starts with Given, When or Then
|
|
771
|
-
// Then you can find white spaces or new lines, follow by double quotes and the step
|
|
772
|
-
// For example:
|
|
773
|
-
// "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);"
|
|
774
|
-
// the step is everything between the double quotes
|
|
775
|
-
const regexp = new RegExp(`"${step}"`, "g");
|
|
776
|
-
for (let i = 0; i < this.cucumberCalls.length; i++) {
|
|
777
|
-
const expression = this.cucumberCalls[i];
|
|
778
|
-
if (expression.codePart) {
|
|
779
|
-
if (expression.codePart.match(regexp)) {
|
|
780
|
-
return true;
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
}
|
|
784
|
-
return false;
|
|
785
|
-
}
|
|
786
|
-
hasFunctionName(functionName) {
|
|
787
|
-
// go over all the methods and compare the name field to the functionName
|
|
788
|
-
for (let i = 0; i < this.methods.length; i++) {
|
|
789
|
-
if (this.methods[i].name === functionName) {
|
|
790
|
-
return true;
|
|
791
|
-
}
|
|
792
|
-
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";
|
|
793
39
|
}
|
|
794
|
-
|
|
40
|
+
return ai_config;
|
|
795
41
|
}
|
|
796
|
-
|
|
797
42
|
function getPath(comment) {
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
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);
|
|
803
47
|
}
|
|
804
|
-
|
|
805
48
|
class CodePart {
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
return vmContext.value;
|
|
885
|
-
}
|
|
886
|
-
}
|
|
887
|
-
}
|
|
888
|
-
return null;
|
|
889
|
-
}
|
|
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
|
+
}
|
|
890
127
|
}
|
|
891
|
-
|
|
892
128
|
const cutSubString = (str, substrings) => {
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
});
|
|
901
|
-
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;
|
|
902
136
|
};
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
//
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
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.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
|
+
}
|
|
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
|
+
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;
|
|
330
|
+
}
|
|
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
|
+
}
|
|
372
|
+
}
|
|
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
|
+
}
|
|
458
|
+
}
|
|
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;
|
|
476
|
+
}
|
|
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
|
+
}
|
|
487
|
+
}
|
|
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));
|
|
511
|
+
});
|
|
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]);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
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
|
+
}
|
|
589
|
+
return true;
|
|
590
|
+
}
|
|
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;
|
|
613
|
+
}
|
|
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
|
+
}
|
|
655
|
+
}
|
|
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]);
|
|
698
|
+
}
|
|
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(" ");
|
|
813
|
+
}
|
|
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;
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
export { escapeForComment, unescapeFromComment };
|