@dev-blinq/cucumber_client 1.0.1256-dev → 1.0.1256-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 +159 -14071
- package/bin/assets/preload/recorderv3.js +3 -1
- package/bin/assets/scripts/dom_parent.js +26 -0
- package/bin/assets/scripts/recorder.js +9 -8
- package/bin/assets/scripts/unique_locators.js +847 -547
- package/bin/assets/templates/_hooks_template.txt +37 -0
- package/bin/assets/templates/page_template.txt +2 -16
- package/bin/assets/templates/utils_template.txt +44 -71
- package/bin/client/apiTest/apiTest.js +6 -0
- package/bin/client/cli_helpers.js +11 -13
- package/bin/client/code_cleanup/utils.js +5 -1
- package/bin/client/code_gen/code_inversion.js +61 -4
- package/bin/client/code_gen/page_reflection.js +10 -3
- package/bin/client/code_gen/playwright_codeget.js +52 -13
- package/bin/client/cucumber/feature.js +89 -27
- package/bin/client/cucumber/project_to_document.js +1 -1
- package/bin/client/cucumber/steps_definitions.js +84 -76
- package/bin/client/cucumber_selector.js +13 -1
- package/bin/client/local_agent.js +3 -3
- package/bin/client/project.js +7 -1
- package/bin/client/recorderv3/bvt_recorder.js +285 -80
- package/bin/client/recorderv3/implemented_steps.js +74 -16
- package/bin/client/recorderv3/index.js +47 -25
- package/bin/client/recorderv3/network.js +299 -0
- package/bin/client/recorderv3/services.js +4 -16
- package/bin/client/recorderv3/step_runner.js +326 -67
- package/bin/client/recorderv3/step_utils.js +152 -5
- package/bin/client/recorderv3/update_feature.js +66 -34
- package/bin/client/recording.js +3 -0
- package/bin/client/run_cucumber.js +5 -1
- package/bin/client/scenario_report.js +0 -5
- package/bin/client/test_scenario.js +0 -1
- package/bin/client/utils/socket_logger.js +132 -0
- package/bin/index.js +1 -0
- package/package.json +15 -12
|
@@ -1,4 +1,4 @@
|
|
|
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";
|
|
@@ -9,6 +9,8 @@ import { Step } from "../cucumber/feature.js";
|
|
|
9
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,74 @@ 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
|
+
|
|
114
|
+
if (process.env.TEMP_RUN === "true") {
|
|
115
|
+
console.log("Save routes in temp folder for running:", routesPath);
|
|
116
|
+
if (existsSync(routesPath)) {
|
|
117
|
+
console.log("Removing existing temp_routes_folder:", routesPath);
|
|
118
|
+
rmSync(routesPath, { recursive: true });
|
|
119
|
+
}
|
|
120
|
+
mkdirSync(routesPath, { recursive: true });
|
|
121
|
+
console.log("Created temp_routes_folder:", routesPath);
|
|
122
|
+
saveRoutes({ step, folderPath: routesPath });
|
|
123
|
+
} else {
|
|
124
|
+
console.log("Saving routes in project directory:", projectDir);
|
|
125
|
+
if (existsSync(routesPath)) {
|
|
126
|
+
// remove the folder
|
|
127
|
+
try {
|
|
128
|
+
rmSync(routesPath, { recursive: true });
|
|
129
|
+
console.log("Removed temp_routes_folder:", routesPath);
|
|
130
|
+
} catch (error) {
|
|
131
|
+
console.error("Error removing temp_routes folder", error);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
routesPath = path.join(projectDir, "data", "routes");
|
|
135
|
+
console.log("Saving routes to:", routesPath);
|
|
136
|
+
if (!existsSync(routesPath)) {
|
|
137
|
+
mkdirSync(routesPath, { recursive: true });
|
|
138
|
+
}
|
|
139
|
+
saveRoutes({ step, folderPath: routesPath });
|
|
140
|
+
}
|
|
141
|
+
|
|
85
142
|
cucumberStep.text = step.text;
|
|
86
143
|
const recording = new Recording();
|
|
87
144
|
const steps = step.commands;
|
|
@@ -108,6 +165,7 @@ export async function saveRecording({ step, cucumberStep, codePage, projectDir,
|
|
|
108
165
|
isStaticToken,
|
|
109
166
|
status,
|
|
110
167
|
} = step.commands[0].value;
|
|
168
|
+
|
|
111
169
|
const result = await generateApiCode(
|
|
112
170
|
{
|
|
113
171
|
url,
|
|
@@ -132,6 +190,7 @@ export async function saveRecording({ step, cucumberStep, codePage, projectDir,
|
|
|
132
190
|
step.keyword,
|
|
133
191
|
stepsDefinitions
|
|
134
192
|
);
|
|
193
|
+
|
|
135
194
|
if (!step.isImplemented) {
|
|
136
195
|
stepsDefinitions.addStep({
|
|
137
196
|
name: step.text,
|
|
@@ -139,6 +198,7 @@ export async function saveRecording({ step, cucumberStep, codePage, projectDir,
|
|
|
139
198
|
source: "recorder",
|
|
140
199
|
});
|
|
141
200
|
}
|
|
201
|
+
|
|
142
202
|
cucumberStep.methodName = result.methodName;
|
|
143
203
|
return result.codePage;
|
|
144
204
|
} else {
|
|
@@ -156,17 +216,29 @@ export async function saveRecording({ step, cucumberStep, codePage, projectDir,
|
|
|
156
216
|
if (step.commands && step.commands.length > 0 && step.commands[0]) {
|
|
157
217
|
path = step.commands[0].lastKnownUrlPath;
|
|
158
218
|
}
|
|
219
|
+
let protect = false;
|
|
220
|
+
if (step.commands && step.commands.length > 0 && step.commands[0].type) {
|
|
221
|
+
if (step.commands[0].type === "verify_element_property" || step.commands[0].type === "conditional_wait") {
|
|
222
|
+
protect = true;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
159
225
|
const infraResult = codePage.addInfraCommand(
|
|
160
226
|
methodName,
|
|
161
227
|
description,
|
|
162
228
|
cucumberStep.getVariablesList(),
|
|
163
229
|
generateCodeResult.codeLines,
|
|
164
|
-
|
|
230
|
+
protect,
|
|
165
231
|
"recorder",
|
|
166
232
|
path
|
|
167
233
|
);
|
|
168
234
|
const keyword = (cucumberStep.keywordAlias ?? cucumberStep.keyword).trim();
|
|
169
|
-
const stepResult = codePage.addCucumberStep(
|
|
235
|
+
const stepResult = codePage.addCucumberStep(
|
|
236
|
+
keyword,
|
|
237
|
+
cucumberStep.getTemplate(),
|
|
238
|
+
methodName,
|
|
239
|
+
steps.length,
|
|
240
|
+
step.finalTimeout
|
|
241
|
+
);
|
|
170
242
|
|
|
171
243
|
if (!step.isImplemented) {
|
|
172
244
|
stepsDefinitions.addStep({
|
|
@@ -306,6 +378,12 @@ export async function updateStepDefinitions({ scenario, featureName, projectDir
|
|
|
306
378
|
const utilsTemplateFilePath = path.join(__dirname, "../../assets", "templates", "utils_template.txt");
|
|
307
379
|
const utilsContent = readFileSync(utilsTemplateFilePath, "utf8");
|
|
308
380
|
writeFileSync(utilsFilePath, utilsContent, "utf8");
|
|
381
|
+
const hooksTemplateFilePath = path.join(__dirname, "../../assets", "templates", "_hooks_template.txt");
|
|
382
|
+
if (existsSync(hooksTemplateFilePath)) {
|
|
383
|
+
const hooksFilePath = path.join(stepDefinitionFolderPath, "_hooks.mjs");
|
|
384
|
+
const hooksContent = readFileSync(hooksTemplateFilePath, "utf8");
|
|
385
|
+
writeFileSync(hooksFilePath, hooksContent, "utf8");
|
|
386
|
+
}
|
|
309
387
|
const steps = scenario.steps;
|
|
310
388
|
|
|
311
389
|
const stepsDefinitions = new StepsDefinitions(projectDir);
|
|
@@ -321,6 +399,34 @@ export async function updateStepDefinitions({ scenario, featureName, projectDir
|
|
|
321
399
|
}
|
|
322
400
|
}
|
|
323
401
|
if ((step.isImplemented && !step.shouldOverride) || step.commands.length === 0) {
|
|
402
|
+
let routesPath = path.join(tmpdir(), `blinq_temp_routes`);
|
|
403
|
+
if (process.env.TEMP_RUN === "true") {
|
|
404
|
+
console.log("Save routes in temp folder for running:", routesPath);
|
|
405
|
+
if (existsSync(routesPath)) {
|
|
406
|
+
console.log("Removing existing temp_routes_folder:", routesPath);
|
|
407
|
+
rmSync(routesPath, { recursive: true });
|
|
408
|
+
}
|
|
409
|
+
mkdirSync(routesPath, { recursive: true });
|
|
410
|
+
console.log("Created temp_routes_folder:", routesPath);
|
|
411
|
+
saveRoutes({ step, folderPath: routesPath });
|
|
412
|
+
} else {
|
|
413
|
+
console.log("Saving routes in project directory:", projectDir);
|
|
414
|
+
if (existsSync(routesPath)) {
|
|
415
|
+
// remove the folder
|
|
416
|
+
try {
|
|
417
|
+
rmSync(routesPath, { recursive: true });
|
|
418
|
+
console.log("Removed temp_routes_folder:", routesPath);
|
|
419
|
+
} catch (error) {
|
|
420
|
+
console.error("Error removing temp_routes folder", error);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
routesPath = path.join(projectDir, "data", "routes");
|
|
424
|
+
console.log("Saving routes to:", routesPath);
|
|
425
|
+
if (!existsSync(routesPath)) {
|
|
426
|
+
mkdirSync(routesPath, { recursive: true });
|
|
427
|
+
}
|
|
428
|
+
saveRoutes({ step, folderPath: routesPath });
|
|
429
|
+
}
|
|
324
430
|
continue;
|
|
325
431
|
}
|
|
326
432
|
const cucumberStep = getCucumberStep({ step });
|
|
@@ -328,7 +434,6 @@ export async function updateStepDefinitions({ scenario, featureName, projectDir
|
|
|
328
434
|
const stepDefsFilePath = locateDefinitionPath(featureFolder, pageName);
|
|
329
435
|
// path.join(stepDefinitionFolderPath, pageName + "_page.mjs");
|
|
330
436
|
let codePage = getCodePage(stepDefsFilePath);
|
|
331
|
-
|
|
332
437
|
codePage = await saveRecording({ step, cucumberStep, codePage, projectDir, stepsDefinitions });
|
|
333
438
|
if (!codePage) {
|
|
334
439
|
continue;
|
|
@@ -340,3 +445,45 @@ export async function updateStepDefinitions({ scenario, featureName, projectDir
|
|
|
340
445
|
}
|
|
341
446
|
writeFileSync(utilsFilePath, utilsContent, "utf8");
|
|
342
447
|
}
|
|
448
|
+
|
|
449
|
+
export function saveRoutes({ step, folderPath }) {
|
|
450
|
+
const routeItems = step.routeItems;
|
|
451
|
+
if (!routeItems || routeItems.length === 0) {
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
const cucumberStep = getCucumberStep({ step });
|
|
455
|
+
const template = cucumberStep.getTemplate();
|
|
456
|
+
const stepNameHash = createHash("sha256").update(template).digest("hex");
|
|
457
|
+
console.log("Saving routes for step:", step.text, "with hash:", stepNameHash);
|
|
458
|
+
|
|
459
|
+
const routeItemsWithFilters = routeItems.map((routeItem) => {
|
|
460
|
+
const oldFilters = routeItem.filters;
|
|
461
|
+
const queryParamsObject = {};
|
|
462
|
+
oldFilters.queryParams.forEach((queryParam) => {
|
|
463
|
+
queryParamsObject[queryParam.key] = queryParam.value;
|
|
464
|
+
});
|
|
465
|
+
const newFilters = { path: oldFilters.path, method: oldFilters.method, queryParams: queryParamsObject };
|
|
466
|
+
return {
|
|
467
|
+
...routeItem,
|
|
468
|
+
filters: newFilters,
|
|
469
|
+
};
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
const routesFilePath = path.join(folderPath, stepNameHash + ".json");
|
|
473
|
+
console.log("Routes file path:", routesFilePath);
|
|
474
|
+
const routesData = {
|
|
475
|
+
template,
|
|
476
|
+
routes: routeItemsWithFilters,
|
|
477
|
+
};
|
|
478
|
+
console.log("Routes data to save:", routesData);
|
|
479
|
+
|
|
480
|
+
if (!existsSync(folderPath)) {
|
|
481
|
+
mkdirSync(folderPath, { recursive: true });
|
|
482
|
+
}
|
|
483
|
+
try {
|
|
484
|
+
writeFileSync(routesFilePath, JSON.stringify(routesData, null, 2), "utf8");
|
|
485
|
+
console.log("Saved routes to", routesFilePath);
|
|
486
|
+
} catch (error) {
|
|
487
|
+
console.error("Failed to save routes to", routesFilePath, "Error:", error);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
@@ -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) {
|
|
@@ -63,7 +64,7 @@ const escapeNonPrintables = (text) => {
|
|
|
63
64
|
export function getCommandContent(command) {
|
|
64
65
|
switch (command.type) {
|
|
65
66
|
case "click_element": {
|
|
66
|
-
return
|
|
67
|
+
return `${command.count === 2 ? "Double click" : "Click"} on ${escapeNonPrintables(command.element.name)}`;
|
|
67
68
|
}
|
|
68
69
|
case "fill_element": {
|
|
69
70
|
return `fill ${escapeNonPrintables(command.element.name)} with ${escapeNonPrintables(command.parameters[0])}${command.parameters[1] ? ` and press enter key` : ""}`;
|
|
@@ -81,7 +82,7 @@ export function getCommandContent(command) {
|
|
|
81
82
|
return `verify the element ${escapeNonPrintables(command.element.name)} contains text ${escapeNonPrintables(command.parameters[0])}`;
|
|
82
83
|
}
|
|
83
84
|
case "context_click": {
|
|
84
|
-
return
|
|
85
|
+
return `${command.count === 2 ? "Double click" : "Click"} on ${escapeNonPrintables(command.label)} in the context of ${escapeNonPrintables(command.value)}`;
|
|
85
86
|
}
|
|
86
87
|
case "hover_element": {
|
|
87
88
|
return `hover over ${escapeNonPrintables(command.element.name)}`;
|
|
@@ -98,6 +99,9 @@ export function getCommandContent(command) {
|
|
|
98
99
|
case "verify_page_snapshot": {
|
|
99
100
|
return `verify page snapshot stored in ${command.parameters[0]}`;
|
|
100
101
|
}
|
|
102
|
+
case "parameterized_click": {
|
|
103
|
+
return `${command.count === 2 ? "Parameterized double click" : "Parameterized click"} on ${escapeNonPrintables(command.element.name)}`;
|
|
104
|
+
}
|
|
101
105
|
default: {
|
|
102
106
|
return "";
|
|
103
107
|
}
|
|
@@ -125,10 +129,14 @@ export function getExamplesContent(parametersMap) {
|
|
|
125
129
|
function getTagsContent(scenario, featureFileObject) {
|
|
126
130
|
let oldTags = featureFileObject?.scenarios?.find((s) => s.name === scenario.name)?.tags ?? [];
|
|
127
131
|
for (const tag of scenario.tags) {
|
|
128
|
-
if (
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
+
if (tag === "global_test_data") {
|
|
133
|
+
if (!oldTags.includes("global_test_data")) {
|
|
134
|
+
oldTags.push("global_test_data");
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (tag === "remove_global_test_data") {
|
|
139
|
+
oldTags = oldTags.filter((t) => t !== "global_test_data");
|
|
132
140
|
}
|
|
133
141
|
}
|
|
134
142
|
|
|
@@ -209,14 +217,12 @@ const GherkinToObject = (gherkin) => {
|
|
|
209
217
|
steps: [],
|
|
210
218
|
};
|
|
211
219
|
while (idx < lines.length && lines[idx].startsWith("@")) {
|
|
212
|
-
skipEmptyLines();
|
|
213
220
|
const tags = [...lines[idx].matchAll(/@([^@]+)/g)].map((match) => match[1].trim());
|
|
214
221
|
scenario.tags.push(...(tags ?? []));
|
|
215
222
|
idx++;
|
|
223
|
+
skipEmptyLines();
|
|
216
224
|
}
|
|
217
225
|
|
|
218
|
-
skipEmptyLines();
|
|
219
|
-
|
|
220
226
|
if (idx < lines.length && (lines[idx].startsWith("Scenario:") || lines[idx].startsWith("Scenario Outline:"))) {
|
|
221
227
|
scenario.name = lines[idx].substring(lines[idx].indexOf(":") + 1).trim();
|
|
222
228
|
idx++;
|
|
@@ -239,32 +245,32 @@ const GherkinToObject = (gherkin) => {
|
|
|
239
245
|
!lines[idx].startsWith("@")
|
|
240
246
|
) {
|
|
241
247
|
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 = {
|
|
248
|
+
let command;
|
|
249
|
+
if (line.startsWith("Examples:")) {
|
|
250
|
+
scenario.hasParams = true;
|
|
251
|
+
command = {
|
|
254
252
|
type: "examples",
|
|
255
253
|
lines: [],
|
|
256
254
|
};
|
|
257
|
-
|
|
258
255
|
while (idx < lines.length && lines[idx].startsWith("|")) {
|
|
259
256
|
const line = lines[idx++];
|
|
260
257
|
command.lines.push(line);
|
|
261
258
|
}
|
|
262
259
|
} else {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
260
|
+
if (line.startsWith("#")) {
|
|
261
|
+
command = {
|
|
262
|
+
type: "comment",
|
|
263
|
+
text: line,
|
|
264
|
+
};
|
|
265
|
+
} else {
|
|
266
|
+
command = {
|
|
267
|
+
type: "step",
|
|
268
|
+
text: line,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
267
271
|
}
|
|
272
|
+
scenario.steps.push(command);
|
|
273
|
+
skipEmptyLines();
|
|
268
274
|
}
|
|
269
275
|
|
|
270
276
|
return scenario;
|
|
@@ -273,7 +279,6 @@ const GherkinToObject = (gherkin) => {
|
|
|
273
279
|
while (idx < lines.length) {
|
|
274
280
|
const scenario = getScenario();
|
|
275
281
|
if (scenario === -1) break;
|
|
276
|
-
|
|
277
282
|
if (scenario.error) {
|
|
278
283
|
return {
|
|
279
284
|
error: scenario.error,
|
|
@@ -299,8 +304,7 @@ function updateExistingScenario({ featureFileContent, scenarioName, scenarioCont
|
|
|
299
304
|
skipScenarioIndex = i;
|
|
300
305
|
continue;
|
|
301
306
|
}
|
|
302
|
-
let scenarioContent = `${
|
|
303
|
-
|
|
307
|
+
let scenarioContent = `${scenario.hasParams ? "Scenario Outline" : "Scenario"}: ${scenario.name}`;
|
|
304
308
|
let tagsLine;
|
|
305
309
|
if (scenario.tags?.length > 0) {
|
|
306
310
|
tagsLine = `${scenario.tags.map((t) => `@${t}`).join(" ")}`;
|
|
@@ -323,7 +327,6 @@ function updateExistingScenario({ featureFileContent, scenarioName, scenarioCont
|
|
|
323
327
|
if (skipScenarioIndex !== -1) {
|
|
324
328
|
finalContent = results.join("\n") + "\n" + scenarioContent;
|
|
325
329
|
}
|
|
326
|
-
|
|
327
330
|
return finalContent;
|
|
328
331
|
}
|
|
329
332
|
export async function updateFeatureFile({ featureName, scenario, override, projectDir }) {
|
|
@@ -333,6 +336,8 @@ export async function updateFeatureFile({ featureName, scenario, override, proje
|
|
|
333
336
|
{ scenario },
|
|
334
337
|
isFeatureFileExists ? GherkinToObject(readFileSync(featureFilePath, "utf8")) : undefined
|
|
335
338
|
);
|
|
339
|
+
const prettierConfig = getDefaultPrettierConfig();
|
|
340
|
+
// Format the code using Prettier
|
|
336
341
|
|
|
337
342
|
if (isFeatureFileExists) {
|
|
338
343
|
const featureFileContent = readFileSync(featureFilePath, "utf8");
|
|
@@ -341,7 +346,7 @@ export async function updateFeatureFile({ featureName, scenario, override, proje
|
|
|
341
346
|
if (!override) {
|
|
342
347
|
throw new Error(`Scenario "${scenario.name}" already exists in feature "${featureName}"`);
|
|
343
348
|
} else {
|
|
344
|
-
|
|
349
|
+
let updatedFeatureFileContent = updateExistingScenario({
|
|
345
350
|
featureFileContent,
|
|
346
351
|
scenarioName: scenario.name,
|
|
347
352
|
scenarioContent,
|
|
@@ -349,14 +354,41 @@ export async function updateFeatureFile({ featureName, scenario, override, proje
|
|
|
349
354
|
if (updatedFeatureFileContent === "error") {
|
|
350
355
|
throw new Error(`Error while parsing feature file: Invalid gherkin`);
|
|
351
356
|
}
|
|
357
|
+
try {
|
|
358
|
+
updatedFeatureFileContent = await prettier.format(updatedFeatureFileContent, {
|
|
359
|
+
...prettierConfig,
|
|
360
|
+
parser: "gherkin",
|
|
361
|
+
plugins: ["prettier-plugin-gherkin"],
|
|
362
|
+
});
|
|
363
|
+
} catch (error) {
|
|
364
|
+
console.error("Error formatting feature file content with Prettier:", error);
|
|
365
|
+
}
|
|
352
366
|
writeFileSync(featureFilePath, updatedFeatureFileContent);
|
|
353
367
|
return;
|
|
354
368
|
}
|
|
355
369
|
}
|
|
356
|
-
|
|
370
|
+
let updatedFeatureFileContent = featureFileContent + "\n" + scenarioContent;
|
|
371
|
+
try {
|
|
372
|
+
updatedFeatureFileContent = await prettier.format(updatedFeatureFileContent, {
|
|
373
|
+
...prettierConfig,
|
|
374
|
+
parser: "gherkin",
|
|
375
|
+
plugins: ["prettier-plugin-gherkin"],
|
|
376
|
+
});
|
|
377
|
+
} catch (error) {
|
|
378
|
+
console.error("Error formatting feature file content with Prettier:", error);
|
|
379
|
+
}
|
|
357
380
|
writeFileSync(featureFilePath, updatedFeatureFileContent);
|
|
358
381
|
} else {
|
|
359
|
-
|
|
382
|
+
let featureFileContent = `Feature: ${featureName}\n${scenarioContent}`;
|
|
383
|
+
try {
|
|
384
|
+
featureFileContent = await prettier.format(featureFileContent, {
|
|
385
|
+
...prettierConfig,
|
|
386
|
+
parser: "gherkin",
|
|
387
|
+
plugins: ["prettier-plugin-gherkin"],
|
|
388
|
+
});
|
|
389
|
+
} catch (error) {
|
|
390
|
+
console.error("Error formatting feature file content with Prettier:", error);
|
|
391
|
+
}
|
|
360
392
|
writeFileSync(featureFilePath, featureFileContent);
|
|
361
393
|
}
|
|
362
394
|
}
|
package/bin/client/recording.js
CHANGED
|
@@ -11,6 +11,8 @@ const Types = {
|
|
|
11
11
|
PARAMETERIZED_CLICK: "parameterized_click",
|
|
12
12
|
CONTEXT_CLICK: "context_click",
|
|
13
13
|
NAVIGATE: "navigate",
|
|
14
|
+
GO_BACK: "browser_go_back",
|
|
15
|
+
GO_FORWARD: "browser_go_forward",
|
|
14
16
|
FILL: "fill_element",
|
|
15
17
|
FILL_SIMPLE: "fill_simple",
|
|
16
18
|
EXECUTE: "execute_page_method",
|
|
@@ -50,6 +52,7 @@ const Types = {
|
|
|
50
52
|
VERIFY_FILE_EXISTS: "verify_file_exists",
|
|
51
53
|
SET_INPUT_FILES: "set_input_files",
|
|
52
54
|
VERIFY_PAGE_SNAPSHOT: "verify_page_snapshot",
|
|
55
|
+
CONDITIONAL_WAIT: "conditional_wait",
|
|
53
56
|
};
|
|
54
57
|
class Recording {
|
|
55
58
|
steps = [];
|
|
@@ -131,6 +131,10 @@ const runCucumber = async (
|
|
|
131
131
|
} else if (!process.env.NODE_ENV_BLINQ) {
|
|
132
132
|
serviceUrl = "https://logic.blinq.io";
|
|
133
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);
|
|
134
138
|
} else {
|
|
135
139
|
serviceUrl = process.env.NODE_ENV_BLINQ.replace("api", "logic");
|
|
136
140
|
logger.info("Running in custom mode: " + serviceUrl);
|
|
@@ -355,7 +359,7 @@ const runCucumber = async (
|
|
|
355
359
|
await aiAgent.createNewStepLocal(
|
|
356
360
|
featureName,
|
|
357
361
|
cucumberStep,
|
|
358
|
-
feature,
|
|
362
|
+
feature.comments,
|
|
359
363
|
userData,
|
|
360
364
|
first,
|
|
361
365
|
previousTasks,
|
|
@@ -33,17 +33,12 @@ const findNextIdInFolder = (folder) => {
|
|
|
33
33
|
// get temp file path from --temp-file arg
|
|
34
34
|
const getTempFilePath = () => {
|
|
35
35
|
let tempFilePath = null;
|
|
36
|
-
|
|
37
36
|
for (const arg of process.argv) {
|
|
38
37
|
const [key, path] = arg.split("=");
|
|
39
38
|
if (key === "--temp-file" && !!path) {
|
|
40
39
|
tempFilePath = path;
|
|
41
40
|
}
|
|
42
41
|
}
|
|
43
|
-
|
|
44
|
-
if (tempFilePath === null) {
|
|
45
|
-
tempFilePath = process.env.TEMP_FILE_PATH;
|
|
46
|
-
}
|
|
47
42
|
return tempFilePath;
|
|
48
43
|
};
|
|
49
44
|
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {Object} SocketLoggerEventPayload
|
|
3
|
+
* @property {string} level Log level (e.g. "info", "warn", "error", "debug")
|
|
4
|
+
* @property {string} context Log context/subsystem (e.g. "BVTRecorder")
|
|
5
|
+
* @property {*} data The log message payload (string, object, etc)
|
|
6
|
+
* @property {string} timestamp ISO string when the log was emitted
|
|
7
|
+
* @property {number} dataSize Size of data in bytes (-1 if unknown)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {Object} SocketLoggerInitOptions
|
|
12
|
+
* @property {string=} context Default context for all logs (optional)
|
|
13
|
+
* @property {string=} eventName Default event name (default: "recorder.log")
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* SocketLogger - Singleton for structured socket-based logging.
|
|
18
|
+
*
|
|
19
|
+
* @namespace SocketLogger
|
|
20
|
+
* @property {function(import('socket.io-client').Socket|import('socket.io').Socket, SocketLoggerInitOptions=):void} init
|
|
21
|
+
* @property {function(string, (string|*), Object=, string=, string=):void} log
|
|
22
|
+
* @property {function((string|*), Object=):void} info
|
|
23
|
+
* @property {function((string|*), Object=):void} warn
|
|
24
|
+
* @property {function((string|*), Object=):void} debug
|
|
25
|
+
* @property {function((string|*), Object=):void} error
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* import logger from "./socket_logger.js";
|
|
29
|
+
* logger.init(socket, { context: "BVTRecorder" });
|
|
30
|
+
* logger.info("Step started", { step: 2 });
|
|
31
|
+
* logger.error("Failed!", { error: "bad stuff" });
|
|
32
|
+
*/
|
|
33
|
+
const SocketLogger = (function () {
|
|
34
|
+
/** @type {import('socket.io-client').Socket|import('socket.io').Socket|null} */
|
|
35
|
+
let socket = null;
|
|
36
|
+
/** @type {string} */
|
|
37
|
+
let defaultContext = "";
|
|
38
|
+
/** @type {string} */
|
|
39
|
+
let defaultEventName = "recorder.log";
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Initialize the logger (call once).
|
|
43
|
+
* @param {import('socket.io-client').Socket|import('socket.io').Socket} sock
|
|
44
|
+
* @param {SocketLoggerInitOptions=} opts
|
|
45
|
+
*/
|
|
46
|
+
function init(sock, opts) {
|
|
47
|
+
socket = sock;
|
|
48
|
+
defaultContext = (opts && opts.context) || "";
|
|
49
|
+
defaultEventName = (opts && opts.eventName) || "recorder.log";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Low-level log method (most users use info/warn/debug/error).
|
|
54
|
+
* @param {string} level Log level ("info", "warn", "debug", "error")
|
|
55
|
+
* @param {string|*} message The log message or object
|
|
56
|
+
* @param {Object=} extra Extra fields (will be merged into log payload)
|
|
57
|
+
* @param {string=} eventName Override event name for this log (default: "recorder.log")
|
|
58
|
+
* @param {string=} context Override log context for this log (default: set in init)
|
|
59
|
+
*/
|
|
60
|
+
function log(level, message, extra, eventName, context) {
|
|
61
|
+
if (!socket || typeof socket.emit !== "function") return;
|
|
62
|
+
/** @type {*} */
|
|
63
|
+
var data = typeof message === "object" ? message : { message: message };
|
|
64
|
+
/** @type {number} */
|
|
65
|
+
var dataSize = 0;
|
|
66
|
+
try {
|
|
67
|
+
dataSize = Buffer.byteLength(JSON.stringify(data || ""), "utf8");
|
|
68
|
+
} catch (e) {
|
|
69
|
+
dataSize = -1;
|
|
70
|
+
}
|
|
71
|
+
/** @type {SocketLoggerEventPayload} */
|
|
72
|
+
var eventPayload = Object.assign(
|
|
73
|
+
{
|
|
74
|
+
level: level,
|
|
75
|
+
context: context || defaultContext,
|
|
76
|
+
data: data,
|
|
77
|
+
timestamp: new Date().toISOString(),
|
|
78
|
+
dataSize: dataSize,
|
|
79
|
+
},
|
|
80
|
+
extra || {}
|
|
81
|
+
);
|
|
82
|
+
// @ts-ignore
|
|
83
|
+
try {
|
|
84
|
+
if (socket) {
|
|
85
|
+
socket.emit(eventName || defaultEventName, eventPayload);
|
|
86
|
+
}
|
|
87
|
+
} catch (e) {
|
|
88
|
+
console.error("Socket logging error:", e);
|
|
89
|
+
console.log("Socket event payload:", eventPayload);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Write an info-level log event.
|
|
95
|
+
* @param {string|*} msg The message or object
|
|
96
|
+
* @param {Object=} ext Any extra fields/metadata
|
|
97
|
+
*/
|
|
98
|
+
function info(msg, ext) {
|
|
99
|
+
log("info", msg, ext);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Write a warn-level log event.
|
|
104
|
+
* @param {string|*} msg The message or object
|
|
105
|
+
* @param {Object=} ext Any extra fields/metadata
|
|
106
|
+
*/
|
|
107
|
+
function warn(msg, ext) {
|
|
108
|
+
log("warn", msg, ext);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Write a debug-level log event.
|
|
113
|
+
* @param {string|*} msg The message or object
|
|
114
|
+
* @param {Object=} ext Any extra fields/metadata
|
|
115
|
+
*/
|
|
116
|
+
function debug(msg, ext) {
|
|
117
|
+
log("debug", msg, ext);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Write an error-level log event.
|
|
122
|
+
* @param {string|*} msg The message or object
|
|
123
|
+
* @param {Object=} ext Any extra fields/metadata
|
|
124
|
+
*/
|
|
125
|
+
function error(msg, ext) {
|
|
126
|
+
log("error", msg, ext);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return { init, log, info, warn, debug, error };
|
|
130
|
+
})();
|
|
131
|
+
|
|
132
|
+
export default SocketLogger;
|
package/bin/index.js
CHANGED
|
@@ -13,4 +13,5 @@ export * from "./client/cucumber/feature_data.js";
|
|
|
13
13
|
export * from "./client/cucumber/steps_definitions.js";
|
|
14
14
|
export * from "./client/profiler.js";
|
|
15
15
|
export * from "./client/code_cleanup/utils.js";
|
|
16
|
+
|
|
16
17
|
export * from "./client/code_cleanup/find_step_definition_references.js";
|