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