@dev-blinq/cucumber_client 1.0.1176-dev → 1.0.1176-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 +220 -0
- package/bin/assets/preload/accessibility.js +1 -1
- package/bin/assets/preload/find_context.js +1 -1
- package/bin/assets/preload/generateSelector.js +24 -0
- package/bin/assets/preload/locators.js +18 -0
- package/bin/assets/preload/recorderv3.js +85 -11
- package/bin/assets/preload/unique_locators.js +24 -3
- package/bin/assets/scripts/aria_snapshot.js +235 -0
- package/bin/assets/scripts/dom_attr.js +372 -0
- package/bin/assets/scripts/dom_element.js +0 -0
- package/bin/assets/scripts/dom_parent.js +185 -0
- package/bin/assets/scripts/event_utils.js +105 -0
- package/bin/assets/scripts/pw.js +7886 -0
- package/bin/assets/scripts/recorder.js +1147 -0
- package/bin/assets/scripts/snapshot_capturer.js +155 -0
- package/bin/assets/scripts/unique_locators.js +844 -0
- package/bin/assets/scripts/yaml.js +4770 -0
- package/bin/assets/templates/page_template.txt +2 -16
- package/bin/assets/templates/utils_template.txt +65 -12
- package/bin/client/cli_helpers.js +0 -1
- package/bin/client/code_cleanup/utils.js +43 -14
- package/bin/client/code_gen/code_inversion.js +112 -18
- package/bin/client/code_gen/index.js +3 -0
- package/bin/client/code_gen/page_reflection.js +37 -20
- package/bin/client/code_gen/playwright_codeget.js +152 -48
- package/bin/client/cucumber/feature.js +96 -42
- package/bin/client/cucumber/project_to_document.js +8 -7
- package/bin/client/cucumber/steps_definitions.js +59 -16
- package/bin/client/local_agent.js +9 -7
- package/bin/client/operations/dump_tree.js +159 -5
- package/bin/client/playground/playground.js +1 -1
- package/bin/client/project.js +6 -2
- package/bin/client/recorderv3/bvt_recorder.js +236 -79
- package/bin/client/recorderv3/cli.js +1 -0
- package/bin/client/recorderv3/implemented_steps.js +111 -11
- package/bin/client/recorderv3/index.js +45 -4
- package/bin/client/recorderv3/network.js +299 -0
- package/bin/client/recorderv3/step_runner.js +179 -13
- package/bin/client/recorderv3/step_utils.js +159 -14
- package/bin/client/recorderv3/update_feature.js +54 -29
- package/bin/client/recording.js +8 -0
- package/bin/client/run_cucumber.js +116 -4
- package/bin/client/scenario_report.js +112 -50
- package/bin/client/test_scenario.js +0 -1
- package/bin/index.js +1 -0
- package/package.json +15 -8
- package/bin/client/code_gen/get_implemented_steps.js +0 -27
|
@@ -1,14 +1,16 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
2
2
|
import path from "path";
|
|
3
3
|
import url from "url";
|
|
4
4
|
import logger from "../../logger.js";
|
|
5
|
-
import { CodePage } from "../code_gen/page_reflection.js";
|
|
5
|
+
import { CodePage, getAiConfig } from "../code_gen/page_reflection.js";
|
|
6
6
|
import { generateCode, generatePageName } from "../code_gen/playwright_codeget.js";
|
|
7
7
|
import { invertCodeToCommand } from "../code_gen/code_inversion.js";
|
|
8
8
|
import { Step } from "../cucumber/feature.js";
|
|
9
|
-
import { StepsDefinitions } from "../cucumber/steps_definitions.js";
|
|
9
|
+
import { locateDefinitionPath, StepsDefinitions } from "../cucumber/steps_definitions.js";
|
|
10
10
|
import { Recording } from "../recording.js";
|
|
11
11
|
import { generateApiCode } from "../code_gen/api_codegen.js";
|
|
12
|
+
import { tmpdir } from "os";
|
|
13
|
+
import { createHash } from "crypto";
|
|
12
14
|
|
|
13
15
|
const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
|
|
14
16
|
|
|
@@ -69,19 +71,72 @@ function makeStepTextUnique(step, stepsDefinitions) {
|
|
|
69
71
|
}
|
|
70
72
|
|
|
71
73
|
export async function saveRecording({ step, cucumberStep, codePage, projectDir, stepsDefinitions }) {
|
|
72
|
-
|
|
74
|
+
let routesPath = path.join(tmpdir(), "blinq_temp_routes");
|
|
75
|
+
|
|
76
|
+
if (process.env.TEMP_RUN) {
|
|
77
|
+
if (existsSync(routesPath)) {
|
|
78
|
+
rmSync(routesPath, { recursive: true });
|
|
79
|
+
}
|
|
80
|
+
mkdirSync(routesPath, { recursive: true });
|
|
81
|
+
saveRoutes({ step, folderPath: routesPath });
|
|
82
|
+
} else {
|
|
83
|
+
if (existsSync(routesPath)) {
|
|
84
|
+
// remove the folder
|
|
85
|
+
try {
|
|
86
|
+
rmSync(routesPath, { recursive: true });
|
|
87
|
+
// console.log("Removed temp_routes_folder:", routesPath);
|
|
88
|
+
} catch (error) {
|
|
89
|
+
// console.error("Error removing temp_routes folder", error);
|
|
90
|
+
}
|
|
91
|
+
routesPath = path.join(projectDir, "data", "routes");
|
|
92
|
+
if (!existsSync(routesPath)) {
|
|
93
|
+
mkdirSync(routesPath, { recursive: true });
|
|
94
|
+
}
|
|
95
|
+
saveRoutes({ step, folderPath: routesPath });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
73
99
|
if (step.isImplementedWhileRecording && !process.env.TEMP_RUN) {
|
|
74
100
|
return;
|
|
75
101
|
}
|
|
102
|
+
|
|
76
103
|
if (step.isImplemented && step.shouldOverride) {
|
|
77
104
|
let stepDef = stepsDefinitions.findMatchingStep(step.text);
|
|
78
105
|
codePage = getCodePage(stepDef.file);
|
|
79
106
|
} else {
|
|
80
107
|
const isUtilStep = makeStepTextUnique(step, stepsDefinitions);
|
|
108
|
+
|
|
81
109
|
if (isUtilStep) {
|
|
82
110
|
return;
|
|
83
111
|
}
|
|
84
112
|
}
|
|
113
|
+
if (process.env.TEMP_RUN === "true") {
|
|
114
|
+
// console.log("Save routes in temp folder for running:", routesPath);
|
|
115
|
+
if (existsSync(routesPath)) {
|
|
116
|
+
// console.log("Removing existing temp_routes_folder:", routesPath);
|
|
117
|
+
rmSync(routesPath, { recursive: true });
|
|
118
|
+
}
|
|
119
|
+
mkdirSync(routesPath, { recursive: true });
|
|
120
|
+
// console.log("Created temp_routes_folder:", routesPath);
|
|
121
|
+
saveRoutes({ step, folderPath: routesPath });
|
|
122
|
+
} else {
|
|
123
|
+
// console.log("Saving routes in project directory:", projectDir);
|
|
124
|
+
if (existsSync(routesPath)) {
|
|
125
|
+
// remove the folder
|
|
126
|
+
try {
|
|
127
|
+
rmSync(routesPath, { recursive: true });
|
|
128
|
+
// console.log("Removed temp_routes_folder:", routesPath);
|
|
129
|
+
} catch (error) {
|
|
130
|
+
// console.error("Error removing temp_routes folder", error);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
routesPath = path.join(projectDir, "data", "routes");
|
|
134
|
+
// console.log("Saving routes to:", routesPath);
|
|
135
|
+
if (!existsSync(routesPath)) {
|
|
136
|
+
mkdirSync(routesPath, { recursive: true });
|
|
137
|
+
}
|
|
138
|
+
saveRoutes({ step, folderPath: routesPath });
|
|
139
|
+
}
|
|
85
140
|
cucumberStep.text = step.text;
|
|
86
141
|
const recording = new Recording();
|
|
87
142
|
const steps = step.commands;
|
|
@@ -108,6 +163,7 @@ export async function saveRecording({ step, cucumberStep, codePage, projectDir,
|
|
|
108
163
|
isStaticToken,
|
|
109
164
|
status,
|
|
110
165
|
} = step.commands[0].value;
|
|
166
|
+
|
|
111
167
|
const result = await generateApiCode(
|
|
112
168
|
{
|
|
113
169
|
url,
|
|
@@ -132,11 +188,15 @@ export async function saveRecording({ step, cucumberStep, codePage, projectDir,
|
|
|
132
188
|
step.keyword,
|
|
133
189
|
stepsDefinitions
|
|
134
190
|
);
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
191
|
+
|
|
192
|
+
if (!step.isImplemented) {
|
|
193
|
+
stepsDefinitions.addStep({
|
|
194
|
+
name: step.text,
|
|
195
|
+
file: result.codePage.sourceFileName,
|
|
196
|
+
source: "recorder",
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
140
200
|
cucumberStep.methodName = result.methodName;
|
|
141
201
|
return result.codePage;
|
|
142
202
|
} else {
|
|
@@ -164,7 +224,13 @@ export async function saveRecording({ step, cucumberStep, codePage, projectDir,
|
|
|
164
224
|
path
|
|
165
225
|
);
|
|
166
226
|
const keyword = (cucumberStep.keywordAlias ?? cucumberStep.keyword).trim();
|
|
167
|
-
const stepResult = codePage.addCucumberStep(
|
|
227
|
+
const stepResult = codePage.addCucumberStep(
|
|
228
|
+
keyword,
|
|
229
|
+
cucumberStep.getTemplate(),
|
|
230
|
+
methodName,
|
|
231
|
+
steps.length,
|
|
232
|
+
step.finalTimeout
|
|
233
|
+
);
|
|
168
234
|
|
|
169
235
|
if (!step.isImplemented) {
|
|
170
236
|
stepsDefinitions.addStep({
|
|
@@ -185,7 +251,17 @@ export async function saveRecording({ step, cucumberStep, codePage, projectDir,
|
|
|
185
251
|
|
|
186
252
|
const getLocatorsJson = (file) => {
|
|
187
253
|
if (!file) return {};
|
|
188
|
-
|
|
254
|
+
let locatorsFilePath = file.replace(".mjs", ".json");
|
|
255
|
+
const originLocatorsFilePath = locatorsFilePath;
|
|
256
|
+
const config = getAiConfig();
|
|
257
|
+
if (config && config.locatorsMetadataDir) {
|
|
258
|
+
// if config.locatorsMetadataDir is set, use it to create the file path
|
|
259
|
+
locatorsFilePath = path.join(config.locatorsMetadataDir, path.basename(locatorsFilePath));
|
|
260
|
+
if (!existsSync(locatorsFilePath)) {
|
|
261
|
+
// if the file does not exist in the config directory, use the original path
|
|
262
|
+
locatorsFilePath = originLocatorsFilePath;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
189
265
|
if (!existsSync(locatorsFilePath)) {
|
|
190
266
|
return {};
|
|
191
267
|
}
|
|
@@ -227,7 +303,7 @@ export const getCommandsForImplementedStep = (stepName, stepsDefinitions, stepPa
|
|
|
227
303
|
|
|
228
304
|
isUtilStep = codePage.sourceFileName.endsWith("utils.mjs");
|
|
229
305
|
for (const { code } of codeCommands) {
|
|
230
|
-
const command = invertCodeToCommand(code, elements, stepParams)[0];
|
|
306
|
+
const command = invertCodeToCommand(code, elements, stepParams, stepsDefinitions, codePage, stepName)[0];
|
|
231
307
|
if (command === undefined || command.type === null) continue;
|
|
232
308
|
if (command.element) {
|
|
233
309
|
const key = command.element.key;
|
|
@@ -297,6 +373,7 @@ export async function updateStepDefinitions({ scenario, featureName, projectDir
|
|
|
297
373
|
const steps = scenario.steps;
|
|
298
374
|
|
|
299
375
|
const stepsDefinitions = new StepsDefinitions(projectDir);
|
|
376
|
+
const featureFolder = path.join(projectDir, "features");
|
|
300
377
|
stepsDefinitions.load(false);
|
|
301
378
|
// const parameters = scenario.parameters;
|
|
302
379
|
// await saveRecordings({ steps, parameters, codePage, projectDir });
|
|
@@ -308,13 +385,41 @@ export async function updateStepDefinitions({ scenario, featureName, projectDir
|
|
|
308
385
|
}
|
|
309
386
|
}
|
|
310
387
|
if ((step.isImplemented && !step.shouldOverride) || step.commands.length === 0) {
|
|
388
|
+
let routesPath = path.join(tmpdir(), `blinq_temp_routes`);
|
|
389
|
+
if (process.env.TEMP_RUN === "true") {
|
|
390
|
+
// console.log("Save routes in temp folder for running:", routesPath);
|
|
391
|
+
if (existsSync(routesPath)) {
|
|
392
|
+
// console.log("Removing existing temp_routes_folder:", routesPath);
|
|
393
|
+
rmSync(routesPath, { recursive: true });
|
|
394
|
+
}
|
|
395
|
+
mkdirSync(routesPath, { recursive: true });
|
|
396
|
+
// console.log("Created temp_routes_folder:", routesPath);
|
|
397
|
+
saveRoutes({ step, folderPath: routesPath });
|
|
398
|
+
} else {
|
|
399
|
+
// console.log("Saving routes in project directory:", projectDir);
|
|
400
|
+
if (existsSync(routesPath)) {
|
|
401
|
+
// remove the folder
|
|
402
|
+
try {
|
|
403
|
+
rmSync(routesPath, { recursive: true });
|
|
404
|
+
// console.log("Removed temp_routes_folder:", routesPath);
|
|
405
|
+
} catch (error) {
|
|
406
|
+
// console.error("Error removing temp_routes folder", error);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
routesPath = path.join(projectDir, "data", "routes");
|
|
410
|
+
// console.log("Saving routes to:", routesPath);
|
|
411
|
+
if (!existsSync(routesPath)) {
|
|
412
|
+
mkdirSync(routesPath, { recursive: true });
|
|
413
|
+
}
|
|
414
|
+
saveRoutes({ step, folderPath: routesPath });
|
|
415
|
+
}
|
|
311
416
|
continue;
|
|
312
417
|
}
|
|
313
418
|
const cucumberStep = getCucumberStep({ step });
|
|
314
419
|
const pageName = generatePageName(step.startFrame?.url ?? "default");
|
|
315
|
-
const stepDefsFilePath =
|
|
420
|
+
const stepDefsFilePath = locateDefinitionPath(featureFolder, pageName);
|
|
421
|
+
// path.join(stepDefinitionFolderPath, pageName + "_page.mjs");
|
|
316
422
|
let codePage = getCodePage(stepDefsFilePath);
|
|
317
|
-
|
|
318
423
|
codePage = await saveRecording({ step, cucumberStep, codePage, projectDir, stepsDefinitions });
|
|
319
424
|
if (!codePage) {
|
|
320
425
|
continue;
|
|
@@ -326,3 +431,43 @@ export async function updateStepDefinitions({ scenario, featureName, projectDir
|
|
|
326
431
|
}
|
|
327
432
|
writeFileSync(utilsFilePath, utilsContent, "utf8");
|
|
328
433
|
}
|
|
434
|
+
|
|
435
|
+
export function saveRoutes({ step, folderPath }) {
|
|
436
|
+
const routeItems = step.routeItems;
|
|
437
|
+
if (!routeItems || routeItems.length === 0) {
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
const cucumberStep = getCucumberStep({ step });
|
|
441
|
+
const template = cucumberStep.getTemplate();
|
|
442
|
+
const stepNameHash = createHash("sha256").update(template).digest("hex");
|
|
443
|
+
// console.log("Saving routes for step:", step.text, "with hash:", stepNameHash);
|
|
444
|
+
const routeItemsWithFilters = routeItems.map((routeItem) => {
|
|
445
|
+
const oldFilters = routeItem.filters;
|
|
446
|
+
const queryParamsObject = {};
|
|
447
|
+
oldFilters.queryParams.forEach((queryParam) => {
|
|
448
|
+
queryParamsObject[queryParam.paramKey] = queryParam.paramValue;
|
|
449
|
+
});
|
|
450
|
+
const newFilters = { path: oldFilters.path, method: oldFilters.method, queryParams: queryParamsObject };
|
|
451
|
+
return {
|
|
452
|
+
...routeItem,
|
|
453
|
+
filters: newFilters,
|
|
454
|
+
};
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
const routesFilePath = path.join(folderPath, stepNameHash + ".json");
|
|
458
|
+
// console.log("Routes file path:", routesFilePath);
|
|
459
|
+
const routesData = {
|
|
460
|
+
template,
|
|
461
|
+
routes: routeItemsWithFilters,
|
|
462
|
+
};
|
|
463
|
+
// console.log("Routes data to save:", routesData);
|
|
464
|
+
if (!existsSync(folderPath)) {
|
|
465
|
+
mkdirSync(folderPath, { recursive: true });
|
|
466
|
+
}
|
|
467
|
+
try {
|
|
468
|
+
writeFileSync(routesFilePath, JSON.stringify(routesData, null, 2), "utf8");
|
|
469
|
+
// console.log("Saved routes to", routesFilePath);
|
|
470
|
+
} catch (error) {
|
|
471
|
+
console.error("Failed to save routes to", routesFilePath, "Error:", error);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
2
2
|
import path from "path";
|
|
3
|
-
|
|
3
|
+
import { getDefaultPrettierConfig } from "../code_cleanup/utils.js";
|
|
4
|
+
import prettier from "prettier";
|
|
4
5
|
export function containsScenario({ featureFileContent, scenarioName }) {
|
|
5
6
|
const lines = featureFileContent.split("\n");
|
|
6
7
|
for (const line of lines) {
|
|
@@ -96,7 +97,7 @@ export function getCommandContent(command) {
|
|
|
96
97
|
return `send a ${command.value.method} API request to ${command.value.url}`;
|
|
97
98
|
}
|
|
98
99
|
case "verify_page_snapshot": {
|
|
99
|
-
return `verify page snapshot stored in ${command.
|
|
100
|
+
return `verify page snapshot stored in ${command.parameters[0]}`;
|
|
100
101
|
}
|
|
101
102
|
default: {
|
|
102
103
|
return "";
|
|
@@ -209,14 +210,12 @@ const GherkinToObject = (gherkin) => {
|
|
|
209
210
|
steps: [],
|
|
210
211
|
};
|
|
211
212
|
while (idx < lines.length && lines[idx].startsWith("@")) {
|
|
212
|
-
skipEmptyLines();
|
|
213
213
|
const tags = [...lines[idx].matchAll(/@([^@]+)/g)].map((match) => match[1].trim());
|
|
214
214
|
scenario.tags.push(...(tags ?? []));
|
|
215
215
|
idx++;
|
|
216
|
+
skipEmptyLines();
|
|
216
217
|
}
|
|
217
218
|
|
|
218
|
-
skipEmptyLines();
|
|
219
|
-
|
|
220
219
|
if (idx < lines.length && (lines[idx].startsWith("Scenario:") || lines[idx].startsWith("Scenario Outline:"))) {
|
|
221
220
|
scenario.name = lines[idx].substring(lines[idx].indexOf(":") + 1).trim();
|
|
222
221
|
idx++;
|
|
@@ -239,32 +238,32 @@ const GherkinToObject = (gherkin) => {
|
|
|
239
238
|
!lines[idx].startsWith("@")
|
|
240
239
|
) {
|
|
241
240
|
const line = lines[idx++];
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
type: "comment",
|
|
247
|
-
text: comment,
|
|
248
|
-
};
|
|
249
|
-
scenario.steps.push(command);
|
|
250
|
-
}
|
|
251
|
-
} else if (line.startsWith("Examples:")) {
|
|
252
|
-
obj.hasParams = true;
|
|
253
|
-
const command = {
|
|
241
|
+
let command;
|
|
242
|
+
if (line.startsWith("Examples:")) {
|
|
243
|
+
scenario.hasParams = true;
|
|
244
|
+
command = {
|
|
254
245
|
type: "examples",
|
|
255
246
|
lines: [],
|
|
256
247
|
};
|
|
257
|
-
|
|
258
248
|
while (idx < lines.length && lines[idx].startsWith("|")) {
|
|
259
249
|
const line = lines[idx++];
|
|
260
250
|
command.lines.push(line);
|
|
261
251
|
}
|
|
262
252
|
} else {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
253
|
+
if (line.startsWith("#")) {
|
|
254
|
+
command = {
|
|
255
|
+
type: "comment",
|
|
256
|
+
text: line,
|
|
257
|
+
};
|
|
258
|
+
} else {
|
|
259
|
+
command = {
|
|
260
|
+
type: "step",
|
|
261
|
+
text: line,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
267
264
|
}
|
|
265
|
+
scenario.steps.push(command);
|
|
266
|
+
skipEmptyLines();
|
|
268
267
|
}
|
|
269
268
|
|
|
270
269
|
return scenario;
|
|
@@ -273,7 +272,6 @@ const GherkinToObject = (gherkin) => {
|
|
|
273
272
|
while (idx < lines.length) {
|
|
274
273
|
const scenario = getScenario();
|
|
275
274
|
if (scenario === -1) break;
|
|
276
|
-
|
|
277
275
|
if (scenario.error) {
|
|
278
276
|
return {
|
|
279
277
|
error: scenario.error,
|
|
@@ -299,8 +297,7 @@ function updateExistingScenario({ featureFileContent, scenarioName, scenarioCont
|
|
|
299
297
|
skipScenarioIndex = i;
|
|
300
298
|
continue;
|
|
301
299
|
}
|
|
302
|
-
let scenarioContent = `${
|
|
303
|
-
|
|
300
|
+
let scenarioContent = `${scenario.hasParams ? "Scenario Outline" : "Scenario"}: ${scenario.name}`;
|
|
304
301
|
let tagsLine;
|
|
305
302
|
if (scenario.tags?.length > 0) {
|
|
306
303
|
tagsLine = `${scenario.tags.map((t) => `@${t}`).join(" ")}`;
|
|
@@ -323,7 +320,6 @@ function updateExistingScenario({ featureFileContent, scenarioName, scenarioCont
|
|
|
323
320
|
if (skipScenarioIndex !== -1) {
|
|
324
321
|
finalContent = results.join("\n") + "\n" + scenarioContent;
|
|
325
322
|
}
|
|
326
|
-
|
|
327
323
|
return finalContent;
|
|
328
324
|
}
|
|
329
325
|
export async function updateFeatureFile({ featureName, scenario, override, projectDir }) {
|
|
@@ -333,6 +329,8 @@ export async function updateFeatureFile({ featureName, scenario, override, proje
|
|
|
333
329
|
{ scenario },
|
|
334
330
|
isFeatureFileExists ? GherkinToObject(readFileSync(featureFilePath, "utf8")) : undefined
|
|
335
331
|
);
|
|
332
|
+
const prettierConfig = getDefaultPrettierConfig();
|
|
333
|
+
// Format the code using Prettier
|
|
336
334
|
|
|
337
335
|
if (isFeatureFileExists) {
|
|
338
336
|
const featureFileContent = readFileSync(featureFilePath, "utf8");
|
|
@@ -341,7 +339,7 @@ export async function updateFeatureFile({ featureName, scenario, override, proje
|
|
|
341
339
|
if (!override) {
|
|
342
340
|
throw new Error(`Scenario "${scenario.name}" already exists in feature "${featureName}"`);
|
|
343
341
|
} else {
|
|
344
|
-
|
|
342
|
+
let updatedFeatureFileContent = updateExistingScenario({
|
|
345
343
|
featureFileContent,
|
|
346
344
|
scenarioName: scenario.name,
|
|
347
345
|
scenarioContent,
|
|
@@ -349,14 +347,41 @@ export async function updateFeatureFile({ featureName, scenario, override, proje
|
|
|
349
347
|
if (updatedFeatureFileContent === "error") {
|
|
350
348
|
throw new Error(`Error while parsing feature file: Invalid gherkin`);
|
|
351
349
|
}
|
|
350
|
+
try {
|
|
351
|
+
updatedFeatureFileContent = await prettier.format(updatedFeatureFileContent, {
|
|
352
|
+
...prettierConfig,
|
|
353
|
+
parser: "gherkin",
|
|
354
|
+
plugins: ["prettier-plugin-gherkin"],
|
|
355
|
+
});
|
|
356
|
+
} catch (error) {
|
|
357
|
+
console.error("Error formatting feature file content with Prettier:", error);
|
|
358
|
+
}
|
|
352
359
|
writeFileSync(featureFilePath, updatedFeatureFileContent);
|
|
353
360
|
return;
|
|
354
361
|
}
|
|
355
362
|
}
|
|
356
|
-
|
|
363
|
+
let updatedFeatureFileContent = featureFileContent + "\n" + scenarioContent;
|
|
364
|
+
try {
|
|
365
|
+
updatedFeatureFileContent = await prettier.format(updatedFeatureFileContent, {
|
|
366
|
+
...prettierConfig,
|
|
367
|
+
parser: "gherkin",
|
|
368
|
+
plugins: ["prettier-plugin-gherkin"],
|
|
369
|
+
});
|
|
370
|
+
} catch (error) {
|
|
371
|
+
console.error("Error formatting feature file content with Prettier:", error);
|
|
372
|
+
}
|
|
357
373
|
writeFileSync(featureFilePath, updatedFeatureFileContent);
|
|
358
374
|
} else {
|
|
359
|
-
|
|
375
|
+
let featureFileContent = `Feature: ${featureName}\n${scenarioContent}`;
|
|
376
|
+
try {
|
|
377
|
+
featureFileContent = await prettier.format(featureFileContent, {
|
|
378
|
+
...prettierConfig,
|
|
379
|
+
parser: "gherkin",
|
|
380
|
+
plugins: ["prettier-plugin-gherkin"],
|
|
381
|
+
});
|
|
382
|
+
} catch (error) {
|
|
383
|
+
console.error("Error formatting feature file content with Prettier:", error);
|
|
384
|
+
}
|
|
360
385
|
writeFileSync(featureFilePath, featureFileContent);
|
|
361
386
|
}
|
|
362
387
|
}
|
package/bin/client/recording.js
CHANGED
|
@@ -5,9 +5,14 @@ import { Step } from "../client/cucumber/feature.js";
|
|
|
5
5
|
This list need to be in sync with the list exist in the commands.js file
|
|
6
6
|
*/
|
|
7
7
|
const Types = {
|
|
8
|
+
API: "api",
|
|
8
9
|
CLICK: "click_element",
|
|
9
10
|
CLICK_SIMPLE: "click_simple",
|
|
11
|
+
PARAMETERIZED_CLICK: "parameterized_click",
|
|
12
|
+
CONTEXT_CLICK: "context_click",
|
|
10
13
|
NAVIGATE: "navigate",
|
|
14
|
+
GO_BACK: "browser_go_back",
|
|
15
|
+
GO_FORWARD: "browser_go_forward",
|
|
11
16
|
FILL: "fill_element",
|
|
12
17
|
FILL_SIMPLE: "fill_simple",
|
|
13
18
|
EXECUTE: "execute_page_method",
|
|
@@ -29,6 +34,7 @@ const Types = {
|
|
|
29
34
|
SET_COMBO: "set_combo",
|
|
30
35
|
HOVER: "hover_element",
|
|
31
36
|
EXTRACT_ATTRIBUTE: "extract_attribute",
|
|
37
|
+
EXTRACT_PROPERTY: "extract_property",
|
|
32
38
|
CLOSE_PAGE: "close_page",
|
|
33
39
|
SET_DATE_TIME: "set_date_time",
|
|
34
40
|
SET_VIEWPORT: "set_viewport",
|
|
@@ -40,11 +46,13 @@ const Types = {
|
|
|
40
46
|
SET_INPUT: "set_input",
|
|
41
47
|
WAIT_FOR_USER_INPUT: "wait_for_user_input",
|
|
42
48
|
VERIFY_ATTRIBUTE: "verify_element_attribute",
|
|
49
|
+
VERIFY_PROPERTY: "verify_element_property",
|
|
43
50
|
FILL_UNKNOWN: "fill_unknown",
|
|
44
51
|
VERIFY_TEXT_RELATED_TO_TEXT: "verify_text_in_relation",
|
|
45
52
|
VERIFY_FILE_EXISTS: "verify_file_exists",
|
|
46
53
|
SET_INPUT_FILES: "set_input_files",
|
|
47
54
|
VERIFY_PAGE_SNAPSHOT: "verify_page_snapshot",
|
|
55
|
+
CONDITIONAL_WAIT: "conditional_wait",
|
|
48
56
|
};
|
|
49
57
|
class Recording {
|
|
50
58
|
steps = [];
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, mkdirSync } from "fs";
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync } from "fs";
|
|
2
2
|
import logger from "../logger.js";
|
|
3
3
|
import path from "path";
|
|
4
4
|
import { scenarioResolution } from "./cucumber/feature.js";
|
|
@@ -13,6 +13,77 @@ import crypto from "crypto";
|
|
|
13
13
|
//import debug from "debug";
|
|
14
14
|
process.env.NODE_TLS_REJECT_UNAUTHORIZED = 0;
|
|
15
15
|
|
|
16
|
+
// Initialize the editorLogs array to collect all logs
|
|
17
|
+
const editorLogs = [];
|
|
18
|
+
|
|
19
|
+
// Store original console methods
|
|
20
|
+
const originalConsoleLog = console.log;
|
|
21
|
+
const originalConsoleError = console.error;
|
|
22
|
+
|
|
23
|
+
// Store original process stdout and stderr write methods
|
|
24
|
+
const originalStdoutWrite = process.stdout.write;
|
|
25
|
+
const originalStderrWrite = process.stderr.write;
|
|
26
|
+
|
|
27
|
+
// Override console.log
|
|
28
|
+
console.log = function (...args) {
|
|
29
|
+
const logEntry = {
|
|
30
|
+
message: args.map((arg) => (typeof arg === "object" ? JSON.stringify(arg) : String(arg))).join(" "),
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
editorLogs.push(logEntry);
|
|
34
|
+
originalConsoleLog.apply(console, args);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Override console.error
|
|
38
|
+
console.error = function (...args) {
|
|
39
|
+
const logEntry = {
|
|
40
|
+
message: args.map((arg) => (typeof arg === "object" ? JSON.stringify(arg) : String(arg))).join(" "),
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
editorLogs.push(logEntry);
|
|
44
|
+
originalConsoleError.apply(console, args);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// Override process.stdout.write
|
|
48
|
+
process.stdout.write = function (chunk, encoding, callback) {
|
|
49
|
+
const logEntry = {
|
|
50
|
+
message: chunk.toString(),
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
editorLogs.push(logEntry);
|
|
54
|
+
return originalStdoutWrite.apply(process.stdout, arguments);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// Override process.stderr.write
|
|
58
|
+
process.stderr.write = function (chunk, encoding, callback) {
|
|
59
|
+
const logEntry = {
|
|
60
|
+
message: chunk.toString(),
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
editorLogs.push(logEntry);
|
|
64
|
+
return originalStderrWrite.apply(process.stderr, arguments);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// Function to write logs to file
|
|
68
|
+
const writeLogsToFile = (filePath) => {
|
|
69
|
+
try {
|
|
70
|
+
const dirPath = path.dirname(filePath);
|
|
71
|
+
|
|
72
|
+
// Create directory if it doesn't exist
|
|
73
|
+
if (!existsSync(dirPath)) {
|
|
74
|
+
mkdirSync(dirPath, { recursive: true });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Format logs as plain message on each line
|
|
78
|
+
const logText = editorLogs.map((log) => `${log.message}`).join("\n");
|
|
79
|
+
|
|
80
|
+
// Write logs to plain text file
|
|
81
|
+
writeFileSync(filePath, logText, { encoding: "utf8" });
|
|
82
|
+
} catch (error) {
|
|
83
|
+
logger.error(`Failed to write logs to file: ${error.message}`);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
16
87
|
//do something when app is closing
|
|
17
88
|
|
|
18
89
|
const runCucumber = async (
|
|
@@ -60,6 +131,10 @@ const runCucumber = async (
|
|
|
60
131
|
} else if (!process.env.NODE_ENV_BLINQ) {
|
|
61
132
|
serviceUrl = "https://logic.blinq.io";
|
|
62
133
|
logger.info("Running in prod mode");
|
|
134
|
+
} else if (process.env.WHITELABEL === "true") {
|
|
135
|
+
// if it's a whitelabel it will use "api" instead of "logic"
|
|
136
|
+
serviceUrl = process.env.NODE_ENV_BLINQ;
|
|
137
|
+
logger.info("Running in custom mode: " + serviceUrl);
|
|
63
138
|
} else {
|
|
64
139
|
serviceUrl = process.env.NODE_ENV_BLINQ.replace("api", "logic");
|
|
65
140
|
logger.info("Running in custom mode: " + serviceUrl);
|
|
@@ -93,7 +168,14 @@ const runCucumber = async (
|
|
|
93
168
|
}
|
|
94
169
|
return true;
|
|
95
170
|
});
|
|
96
|
-
process.on("exit", async () =>
|
|
171
|
+
process.on("exit", async () => {
|
|
172
|
+
// Write logs to file before exiting
|
|
173
|
+
if (result.scenarioPath) {
|
|
174
|
+
const logsFilePath = path.join(result.scenarioPath, "editorLogs.log");
|
|
175
|
+
writeLogsToFile(logsFilePath);
|
|
176
|
+
}
|
|
177
|
+
await aiAgent.endScenario();
|
|
178
|
+
});
|
|
97
179
|
const context = {};
|
|
98
180
|
result.context = context;
|
|
99
181
|
if (process.env.E2E === "true") {
|
|
@@ -105,6 +187,9 @@ const runCucumber = async (
|
|
|
105
187
|
envFile = await aiAgent.initProjectAndEnvironment(context);
|
|
106
188
|
} catch (e) {
|
|
107
189
|
logger.error(e.message);
|
|
190
|
+
// Write logs to file before exiting due to error
|
|
191
|
+
const logsFilePath = path.join(result.scenarioPath, "editorLogs.log");
|
|
192
|
+
writeLogsToFile(logsFilePath);
|
|
108
193
|
if (exit) {
|
|
109
194
|
process.exit(1);
|
|
110
195
|
}
|
|
@@ -118,6 +203,9 @@ const runCucumber = async (
|
|
|
118
203
|
}
|
|
119
204
|
if (!existsSync(fullFeatureFilePath)) {
|
|
120
205
|
logger.error("Feature file not found: " + fullFeatureFilePath);
|
|
206
|
+
// Write logs to file before exiting due to error
|
|
207
|
+
const logsFilePath = path.join(result.scenarioPath, "editorLogs.log");
|
|
208
|
+
writeLogsToFile(logsFilePath);
|
|
121
209
|
if (exit) {
|
|
122
210
|
process.exit(1);
|
|
123
211
|
}
|
|
@@ -128,6 +216,9 @@ const runCucumber = async (
|
|
|
128
216
|
feature = await scenarioResolution(fullFeatureFilePath);
|
|
129
217
|
} catch (e) {
|
|
130
218
|
logger.error("Error parsing feature file: " + fullFeatureFilePath);
|
|
219
|
+
// Write logs to file before exiting due to error
|
|
220
|
+
const logsFilePath = path.join(result.scenarioPath, "editorLogs.log");
|
|
221
|
+
writeLogsToFile(logsFilePath);
|
|
131
222
|
if (exit) {
|
|
132
223
|
process.exit(1);
|
|
133
224
|
}
|
|
@@ -136,6 +227,9 @@ const runCucumber = async (
|
|
|
136
227
|
const scenario = feature.getScenario(scenarioName);
|
|
137
228
|
if (!scenario) {
|
|
138
229
|
logger.error("Scenario not found: " + scenarioName);
|
|
230
|
+
// Write logs to file before exiting due to error
|
|
231
|
+
const logsFilePath = path.join(result.scenarioPath, "editorLogs.log");
|
|
232
|
+
writeLogsToFile(logsFilePath);
|
|
139
233
|
if (exit) {
|
|
140
234
|
process.exit(1);
|
|
141
235
|
}
|
|
@@ -146,6 +240,15 @@ const runCucumber = async (
|
|
|
146
240
|
|
|
147
241
|
let scenarioId = findNextIdInFolder("./reports");
|
|
148
242
|
let scenarioPath = "./reports" + "/" + scenarioId;
|
|
243
|
+
const dataFilePath = path.join(scenarioPath, "data.json");
|
|
244
|
+
mkdirSync(path.dirname(dataFilePath), { recursive: true });
|
|
245
|
+
if (process.env.NODE_ENV_BLINQ === "local") {
|
|
246
|
+
let dataPath = aiAgent.project.rootFolder + "/data/data.json";
|
|
247
|
+
if (existsSync(dataPath)) {
|
|
248
|
+
const poolData = JSON.parse(readFileSync(dataPath, "utf-8"));
|
|
249
|
+
writeFileSync(dataFilePath, JSON.stringify(poolData, null, 2), "utf-8");
|
|
250
|
+
}
|
|
251
|
+
}
|
|
149
252
|
result.scenarioPath = scenarioPath;
|
|
150
253
|
aiAgent.initTestData(envFile, path.join(scenarioPath, "data.json"));
|
|
151
254
|
let featureFileRelative = path.relative(projectDir, fullFeatureFilePath);
|
|
@@ -256,7 +359,7 @@ const runCucumber = async (
|
|
|
256
359
|
await aiAgent.createNewStepLocal(
|
|
257
360
|
featureName,
|
|
258
361
|
cucumberStep,
|
|
259
|
-
feature,
|
|
362
|
+
feature.comments,
|
|
260
363
|
userData,
|
|
261
364
|
first,
|
|
262
365
|
previousTasks,
|
|
@@ -283,6 +386,9 @@ const runCucumber = async (
|
|
|
283
386
|
) {
|
|
284
387
|
aiAgent.evaluateScenario();
|
|
285
388
|
}
|
|
389
|
+
// Write logs to file on successful completion
|
|
390
|
+
const logsFilePath = path.join(scenarioPath, "editorLogs.log");
|
|
391
|
+
writeLogsToFile(logsFilePath);
|
|
286
392
|
await aiAgent.endScenario();
|
|
287
393
|
} catch (e) {
|
|
288
394
|
if (reconnect) {
|
|
@@ -303,7 +409,12 @@ const runCucumber = async (
|
|
|
303
409
|
logger.error(e.stack);
|
|
304
410
|
message = e.message + "\n" + e.stack;
|
|
305
411
|
}
|
|
412
|
+
aiAgent.scenarioReport.updateLastCommand({ status: false, error: message });
|
|
413
|
+
// Write logs to file on error
|
|
414
|
+
const logsFilePath = path.join(result.scenarioPath, "editorLogs.log");
|
|
415
|
+
writeLogsToFile(logsFilePath);
|
|
306
416
|
await aiAgent.endScenario();
|
|
417
|
+
|
|
307
418
|
if (exit) {
|
|
308
419
|
process.exit(1);
|
|
309
420
|
}
|
|
@@ -316,4 +427,5 @@ const runCucumber = async (
|
|
|
316
427
|
return result;
|
|
317
428
|
}
|
|
318
429
|
};
|
|
319
|
-
|
|
430
|
+
|
|
431
|
+
export { runCucumber, editorLogs, writeLogsToFile };
|