@dev-blinq/cucumber_client 1.0.1475-dev → 1.0.1475-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 +49 -49
- package/bin/assets/scripts/recorder.js +87 -34
- package/bin/assets/scripts/snapshot_capturer.js +10 -17
- package/bin/assets/scripts/unique_locators.js +78 -28
- package/bin/assets/templates/_hooks_template.txt +6 -2
- package/bin/assets/templates/utils_template.txt +16 -16
- package/bin/client/code_cleanup/codemod/find_harcoded_locators.js +173 -0
- package/bin/client/code_cleanup/codemod/fix_hardcoded_locators.js +247 -0
- package/bin/client/code_cleanup/utils.js +16 -7
- package/bin/client/code_gen/code_inversion.js +125 -1
- package/bin/client/code_gen/duplication_analysis.js +2 -1
- package/bin/client/code_gen/function_signature.js +8 -0
- package/bin/client/code_gen/index.js +4 -0
- package/bin/client/code_gen/page_reflection.js +90 -9
- package/bin/client/code_gen/playwright_codeget.js +173 -77
- package/bin/client/codemod/find_harcoded_locators.js +173 -0
- package/bin/client/codemod/fix_hardcoded_locators.js +247 -0
- package/bin/client/codemod/index.js +8 -0
- package/bin/client/codemod/locators_array/find_misstructured_elements.js +148 -0
- package/bin/client/codemod/locators_array/fix_misstructured_elements.js +144 -0
- package/bin/client/codemod/locators_array/index.js +114 -0
- package/bin/client/codemod/types.js +1 -0
- package/bin/client/cucumber/feature.js +4 -17
- package/bin/client/cucumber/steps_definitions.js +17 -12
- package/bin/client/recorderv3/bvt_init.js +310 -0
- package/bin/client/recorderv3/bvt_recorder.js +1560 -1183
- package/bin/client/recorderv3/constants.js +45 -0
- package/bin/client/recorderv3/implemented_steps.js +2 -0
- package/bin/client/recorderv3/index.js +3 -293
- package/bin/client/recorderv3/mixpanel.js +39 -0
- package/bin/client/recorderv3/services.js +839 -142
- package/bin/client/recorderv3/step_runner.js +36 -7
- package/bin/client/recorderv3/step_utils.js +316 -98
- package/bin/client/recorderv3/update_feature.js +85 -37
- package/bin/client/recorderv3/utils.js +80 -0
- package/bin/client/recorderv3/wbr_entry.js +61 -0
- package/bin/client/recording.js +1 -0
- package/bin/client/types/locators.js +2 -0
- package/bin/client/upload-service.js +2 -0
- package/bin/client/utils/app_dir.js +21 -0
- package/bin/client/utils/socket_logger.js +100 -125
- package/bin/index.js +5 -0
- package/package.json +21 -6
- package/bin/client/recorderv3/app_dir.js +0 -23
- package/bin/client/recorderv3/network.js +0 -299
- package/bin/client/recorderv3/scriptTest.js +0 -5
- package/bin/client/recorderv3/ws_server.js +0 -72
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { parse } from "@babel/parser";
|
|
4
|
+
import traverseImport from "@babel/traverse";
|
|
5
|
+
import * as t from "@babel/types";
|
|
6
|
+
// Normalize babel traverse to handle both CJS (callable export) and ESM (default export)
|
|
7
|
+
const traverse = typeof traverseImport === "function"
|
|
8
|
+
? traverseImport
|
|
9
|
+
: (traverseImport.default ?? traverseImport);
|
|
10
|
+
function isNumericKey(key) {
|
|
11
|
+
return typeof key === "string" && /^\d+$/.test(key);
|
|
12
|
+
}
|
|
13
|
+
function propName(prop) {
|
|
14
|
+
if (!t.isObjectProperty(prop))
|
|
15
|
+
return undefined;
|
|
16
|
+
const k = prop.key;
|
|
17
|
+
if (t.isIdentifier(k))
|
|
18
|
+
return k.name;
|
|
19
|
+
if (t.isStringLiteral(k))
|
|
20
|
+
return k.value;
|
|
21
|
+
if (t.isNumericLiteral(k))
|
|
22
|
+
return String(k.value);
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
function stringifyValue(node, source) {
|
|
26
|
+
if (typeof node.start === "number" && typeof node.end === "number") {
|
|
27
|
+
return source.slice(node.start, node.end);
|
|
28
|
+
}
|
|
29
|
+
return JSON.stringify(null);
|
|
30
|
+
}
|
|
31
|
+
function leadingWhitespace(text, index) {
|
|
32
|
+
const lineStart = text.lastIndexOf("\n", index - 1) + 1;
|
|
33
|
+
const match = text.slice(lineStart, index).match(/^[ \t]*/);
|
|
34
|
+
return match ? match[0] : "";
|
|
35
|
+
}
|
|
36
|
+
function buildLocatorsArray(numericProps, source, indent) {
|
|
37
|
+
const elements = numericProps
|
|
38
|
+
.sort((a, b) => Number(propName(a)) - Number(propName(b)))
|
|
39
|
+
.map((p) => `${indent} ${stringifyValue(p.value, source)}`);
|
|
40
|
+
if (elements.length === 0)
|
|
41
|
+
return "locators: []";
|
|
42
|
+
return [indent + "locators: [", elements.join(",\n"), indent + "]"].join("\n");
|
|
43
|
+
}
|
|
44
|
+
function fixJsPreserveFormat(code, logger) {
|
|
45
|
+
let touched = false;
|
|
46
|
+
const replacements = [];
|
|
47
|
+
const ast = parse(code, {
|
|
48
|
+
sourceType: "module",
|
|
49
|
+
plugins: ["jsx", "typescript", "classProperties", "optionalChaining", "nullishCoalescingOperator", "topLevelAwait"],
|
|
50
|
+
ranges: true,
|
|
51
|
+
});
|
|
52
|
+
traverse(ast, {
|
|
53
|
+
ObjectExpression(pathObj) {
|
|
54
|
+
const props = pathObj.node.properties.filter(t.isObjectProperty);
|
|
55
|
+
if (!props.length)
|
|
56
|
+
return;
|
|
57
|
+
const names = props.map(propName).filter(Boolean);
|
|
58
|
+
const hasElementMeta = names.some((n) => n === "element_name" || n === "element_key");
|
|
59
|
+
if (!hasElementMeta)
|
|
60
|
+
return;
|
|
61
|
+
const hasLocators = names.includes("locators");
|
|
62
|
+
const numericProps = props.filter((p) => isNumericKey(propName(p)));
|
|
63
|
+
if (hasLocators || numericProps.length === 0)
|
|
64
|
+
return;
|
|
65
|
+
if (typeof pathObj.node.start !== "number" || typeof pathObj.node.end !== "number")
|
|
66
|
+
return;
|
|
67
|
+
const leading = leadingWhitespace(code, pathObj.node.start);
|
|
68
|
+
const otherProps = props.filter((p) => !numericProps.includes(p));
|
|
69
|
+
const otherTexts = otherProps.map((p) => code.slice(p.start ?? 0, p.end ?? 0));
|
|
70
|
+
const locatorsText = buildLocatorsArray(numericProps, code, `${leading} `);
|
|
71
|
+
const pieces = [locatorsText, ...otherTexts];
|
|
72
|
+
const baseIndent = `${leading} `;
|
|
73
|
+
const normalized = pieces.map((p) => (p.startsWith(baseIndent) ? p : baseIndent + p));
|
|
74
|
+
const newObjectText = "{\n" + normalized.join(",\n") + "\n" + leading + "}";
|
|
75
|
+
replacements.push({ start: pathObj.node.start, end: pathObj.node.end, text: newObjectText });
|
|
76
|
+
touched = true;
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
if (!touched)
|
|
80
|
+
return code;
|
|
81
|
+
let out = code;
|
|
82
|
+
replacements
|
|
83
|
+
.sort((a, b) => b.start - a.start)
|
|
84
|
+
.forEach(({ start, end, text }) => {
|
|
85
|
+
out = out.slice(0, start) + text + out.slice(end);
|
|
86
|
+
});
|
|
87
|
+
return out;
|
|
88
|
+
}
|
|
89
|
+
function fixJson(content, logger) {
|
|
90
|
+
let data;
|
|
91
|
+
try {
|
|
92
|
+
data = JSON.parse(content);
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return content;
|
|
96
|
+
}
|
|
97
|
+
let touched = false;
|
|
98
|
+
const visit = (node, logger) => {
|
|
99
|
+
if (!node || typeof node !== "object")
|
|
100
|
+
return;
|
|
101
|
+
if (Array.isArray(node))
|
|
102
|
+
return node.forEach((n) => visit(n, logger));
|
|
103
|
+
const record = node;
|
|
104
|
+
const keys = Object.keys(record);
|
|
105
|
+
const hasMeta = "element_name" in record || "element_key" in record;
|
|
106
|
+
const hasLoc = "locators" in record;
|
|
107
|
+
const nums = keys.filter(isNumericKey);
|
|
108
|
+
if (hasMeta && !hasLoc && nums.length) {
|
|
109
|
+
record.locators = nums.sort((a, b) => Number(a) - Number(b)).map((k) => record[k]);
|
|
110
|
+
nums.forEach((k) => delete record[k]);
|
|
111
|
+
touched = true;
|
|
112
|
+
}
|
|
113
|
+
Object.values(record).forEach((v) => visit(v, logger));
|
|
114
|
+
};
|
|
115
|
+
visit(data, logger);
|
|
116
|
+
return touched ? `${JSON.stringify(data, null, 2)}\n` : content;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Fixes all files referenced in `issues` under `root`.
|
|
120
|
+
* `issues` is the array returned by findIssues().
|
|
121
|
+
* Returns an array of { filePath, original, fixed } for files that changed.
|
|
122
|
+
*/
|
|
123
|
+
export async function fixIssues(issues, root, logger) {
|
|
124
|
+
// Deduplicate — multiple issues can point to the same file
|
|
125
|
+
const filePaths = [...new Set(issues.map((i) => i.file))];
|
|
126
|
+
const changes = [];
|
|
127
|
+
for (const filePath of filePaths) {
|
|
128
|
+
const absolutePath = path.isAbsolute(filePath) ? filePath : path.join(root, filePath);
|
|
129
|
+
const ext = path.extname(absolutePath).toLowerCase();
|
|
130
|
+
const original = await fs.readFile(absolutePath, "utf8");
|
|
131
|
+
let fixed = original;
|
|
132
|
+
if (ext === ".json") {
|
|
133
|
+
fixed = fixJson(original, logger);
|
|
134
|
+
}
|
|
135
|
+
else if (ext === ".mjs" || ext === ".js") {
|
|
136
|
+
fixed = fixJsPreserveFormat(original, logger);
|
|
137
|
+
}
|
|
138
|
+
if (fixed === original)
|
|
139
|
+
continue;
|
|
140
|
+
await fs.writeFile(absolutePath, fixed, "utf8");
|
|
141
|
+
changes.push({ filePath: absolutePath, original, fixed });
|
|
142
|
+
}
|
|
143
|
+
return changes;
|
|
144
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { findIssues } from "./find_misstructured_elements.js";
|
|
2
|
+
import { fixIssues } from "./fix_misstructured_elements.js";
|
|
3
|
+
/**
|
|
4
|
+
* Converts legacy locator objects that use numeric keys (e.g., `{ 0: {...}, 1: {...} }`)
|
|
5
|
+
* into a `locators` array, across all JS/MJS/JSON files under a codebase root.
|
|
6
|
+
* The codemod is idempotent, preserves formatting where possible, and reports timing metrics.
|
|
7
|
+
*
|
|
8
|
+
* @param clonedRoot Absolute path to the cloned project directory to scan and fix.
|
|
9
|
+
* @param logger Logger instance used for progress, issue, and summary output.
|
|
10
|
+
* @returns Structured codemod run result containing issues found, file changes written, and metrics.
|
|
11
|
+
*
|
|
12
|
+
* @example Running the codemod standalone:
|
|
13
|
+
* ```ts
|
|
14
|
+
* import { locatorObjectToArray } from "./code_cleanup/codemod/locators_array";
|
|
15
|
+
* import { consoleLogger } from "./logger";
|
|
16
|
+
*
|
|
17
|
+
* const result = await locatorObjectToArray("/tmp/repo", consoleLogger);
|
|
18
|
+
* console.log(result.message); // e.g., "Fixed 3 file(s)"
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* @example Running multiple codemods in sequence:
|
|
22
|
+
* ```ts
|
|
23
|
+
* const codemods = [locatorObjectToArray]; // add other codemods as needed
|
|
24
|
+
* for (const mod of codemods) {
|
|
25
|
+
* const outcome = await mod("/tmp/repo", consoleLogger);
|
|
26
|
+
* if (!outcome.success) throw new Error(outcome.message);
|
|
27
|
+
* }
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
async function locatorObjectToArray(clonedRoot, logger) {
|
|
31
|
+
const start = Date.now();
|
|
32
|
+
logger.info(`Scanning ${clonedRoot} for misstructured locator objects...`);
|
|
33
|
+
const findStart = Date.now();
|
|
34
|
+
const issues = [];
|
|
35
|
+
try {
|
|
36
|
+
issues.push(...(await findIssues(clonedRoot, logger)));
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
const metricsOnFindError = {
|
|
40
|
+
issueCount: 0,
|
|
41
|
+
affectedFiles: 0,
|
|
42
|
+
findDurationMs: Date.now() - findStart,
|
|
43
|
+
fixDurationMs: 0,
|
|
44
|
+
filesFixed: 0,
|
|
45
|
+
wasAffected: false,
|
|
46
|
+
totalDurationMs: Date.now() - start,
|
|
47
|
+
};
|
|
48
|
+
logger.error(`Error during issue finding: ${error.message}`);
|
|
49
|
+
return {
|
|
50
|
+
name: "Locator Object to Array",
|
|
51
|
+
description: "Converts locator objects with numeric keys to arrays.",
|
|
52
|
+
success: false,
|
|
53
|
+
message: `Failed to find issues: ${error.message}`,
|
|
54
|
+
details: { issues: [], changes: [], metrics: metricsOnFindError },
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
const metricsBase = {
|
|
58
|
+
issueCount: issues.length,
|
|
59
|
+
affectedFiles: [...new Set(issues.map((i) => i.file))].length,
|
|
60
|
+
findDurationMs: Date.now() - findStart,
|
|
61
|
+
fixDurationMs: 0,
|
|
62
|
+
filesFixed: 0,
|
|
63
|
+
wasAffected: issues.length > 0,
|
|
64
|
+
totalDurationMs: 0,
|
|
65
|
+
};
|
|
66
|
+
if (issues.length === 0) {
|
|
67
|
+
metricsBase.totalDurationMs = Date.now() - start;
|
|
68
|
+
logger.info("No issues — nothing to fix");
|
|
69
|
+
logger.info(`Summary: issues=0 files=0 fixed=0 find=${metricsBase.findDurationMs}ms ` +
|
|
70
|
+
`fix=${metricsBase.fixDurationMs}ms total=${metricsBase.totalDurationMs}ms`);
|
|
71
|
+
return {
|
|
72
|
+
name: "Locator Object to Array",
|
|
73
|
+
description: "Converts locator objects with numeric keys to arrays.",
|
|
74
|
+
success: true,
|
|
75
|
+
message: "No issues found",
|
|
76
|
+
details: { issues: [], changes: [], metrics: metricsBase },
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
logger.info(`Found ${issues.length} issue(s) across ${metricsBase.affectedFiles} file(s)`);
|
|
80
|
+
issues.forEach((i) => logger.info(` issue: ${i.file}:${i.line} — ${i.element} (${i.numericCount} numeric keys)`));
|
|
81
|
+
const fixStart = Date.now();
|
|
82
|
+
const changes = [];
|
|
83
|
+
try {
|
|
84
|
+
changes.push(...(await fixIssues(issues, clonedRoot, logger)));
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
metricsBase.fixDurationMs = Date.now() - fixStart;
|
|
88
|
+
metricsBase.totalDurationMs = Date.now() - start;
|
|
89
|
+
logger.error(`Error during issue fixing: ${error.message}`);
|
|
90
|
+
return {
|
|
91
|
+
name: "Locator Object to Array",
|
|
92
|
+
description: "Converts locator objects with numeric keys to arrays.",
|
|
93
|
+
success: false,
|
|
94
|
+
message: `Failed to fix issues: ${error.message}`,
|
|
95
|
+
details: { issues, changes: [], metrics: metricsBase },
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
metricsBase.fixDurationMs = Date.now() - fixStart;
|
|
99
|
+
metricsBase.filesFixed = changes.length;
|
|
100
|
+
metricsBase.totalDurationMs = Date.now() - start;
|
|
101
|
+
logger.info(`Fixed ${changes.length} file(s) in ${metricsBase.fixDurationMs}ms`);
|
|
102
|
+
changes.forEach((c) => logger.info(` fixed: ${c.filePath}`));
|
|
103
|
+
logger.info(`Summary: issues=${metricsBase.issueCount} files=${metricsBase.affectedFiles} ` +
|
|
104
|
+
`fixed=${metricsBase.filesFixed} find=${metricsBase.findDurationMs}ms ` +
|
|
105
|
+
`fix=${metricsBase.fixDurationMs}ms total=${metricsBase.totalDurationMs}ms`);
|
|
106
|
+
return {
|
|
107
|
+
name: "Locator Object to Array",
|
|
108
|
+
description: "Converts locator objects with numeric keys to arrays.",
|
|
109
|
+
success: true,
|
|
110
|
+
message: `Fixed ${changes.length} file(s)`,
|
|
111
|
+
details: { issues, changes, metrics: metricsBase },
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
export { locatorObjectToArray };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -6,7 +6,6 @@ import os from "os";
|
|
|
6
6
|
import path from "path";
|
|
7
7
|
import { parseStepTextParameters, toCucumberExpression, unEscapeNonPrintables } from "./utils.js";
|
|
8
8
|
import stream from "stream";
|
|
9
|
-
import { testStringForRegex } from "../recorderv3/update_feature.js";
|
|
10
9
|
import { generateTestData } from "./feature_data.js";
|
|
11
10
|
import socketLogger from "../utils/socket_logger.js";
|
|
12
11
|
class DataTable {
|
|
@@ -490,8 +489,8 @@ const scenarioResolution = async (featureFilePath) => {
|
|
|
490
489
|
let result = generateTestData(featureFilePath);
|
|
491
490
|
if (result.changed) {
|
|
492
491
|
fs.writeFileSync(tmpFeatureFilePath, result.newContent);
|
|
493
|
-
|
|
494
|
-
|
|
492
|
+
socketLogger.info("Generated fake data for feature", undefined, "scenarioResolution");
|
|
493
|
+
socketLogger.info("Variables generated:", result.variables, "scenarioResolution");
|
|
495
494
|
for (let key in result.variables) {
|
|
496
495
|
console.log(`${key}: ${result.variables[key].fake}`);
|
|
497
496
|
}
|
|
@@ -503,8 +502,7 @@ const scenarioResolution = async (featureFilePath) => {
|
|
|
503
502
|
fs.copyFileSync(featureFilePath, tmpFeatureFilePath);
|
|
504
503
|
}
|
|
505
504
|
const writable = new stream.Writable({
|
|
506
|
-
write:
|
|
507
|
-
//console.log(chunk.toString());
|
|
505
|
+
write: (chunk, encoding, next) => {
|
|
508
506
|
next();
|
|
509
507
|
},
|
|
510
508
|
});
|
|
@@ -521,23 +519,12 @@ const scenarioResolution = async (featureFilePath) => {
|
|
|
521
519
|
// load the support code upfront
|
|
522
520
|
const support = await loadSupport(runConfiguration, environment);
|
|
523
521
|
// run cucumber, using the support code we loaded already
|
|
524
|
-
await runCucumber({ ...runConfiguration, support }, environment,
|
|
525
|
-
// if (event.source) {
|
|
526
|
-
// scenarioInfo.source = event.source.data;
|
|
527
|
-
// }
|
|
528
|
-
// if (event.pickle && event.pickle.name === scenarioName) {
|
|
529
|
-
// scenarioInfo.pickle = event.pickle;
|
|
530
|
-
// }
|
|
522
|
+
await runCucumber({ ...runConfiguration, support }, environment, (event) => {
|
|
531
523
|
if (event.gherkinDocument) {
|
|
532
524
|
gherkinDocument = event.gherkinDocument;
|
|
533
525
|
}
|
|
534
|
-
//console.log(event);
|
|
535
|
-
//console.log(JSON.stringify(event, null, 2));
|
|
536
|
-
// console.log("");
|
|
537
526
|
});
|
|
538
527
|
const feature = new Feature(gherkinDocument, featureFileContent);
|
|
539
|
-
//const scenario = feature.getScenario(scenarioName);
|
|
540
|
-
//return scenario;
|
|
541
528
|
return feature;
|
|
542
529
|
} finally {
|
|
543
530
|
try {
|
|
@@ -36,6 +36,19 @@ class StepsDefinitions {
|
|
|
36
36
|
// }
|
|
37
37
|
// });
|
|
38
38
|
const { expressions, methods } = codePage;
|
|
39
|
+
|
|
40
|
+
if (codePage.fileContent.includes('from "./utils.mjs"')) {
|
|
41
|
+
const filePath = path.join(
|
|
42
|
+
this.baseFolder,
|
|
43
|
+
this.isTemp ? (process.env.tempFeaturesFolderPath ?? "__temp_features") : "features",
|
|
44
|
+
"step_definitions",
|
|
45
|
+
"utils.mjs"
|
|
46
|
+
);
|
|
47
|
+
const utilsCodePage = new CodePage(filePath);
|
|
48
|
+
utilsCodePage.generateModel();
|
|
49
|
+
methods.push(...utilsCodePage.methods);
|
|
50
|
+
}
|
|
51
|
+
|
|
39
52
|
for (let i = 0; i < expressions.length; i++) {
|
|
40
53
|
const expression = expressions[i];
|
|
41
54
|
const pattern = expression.pattern;
|
|
@@ -87,18 +100,10 @@ class StepsDefinitions {
|
|
|
87
100
|
this.initPage(codePage, mjsFile);
|
|
88
101
|
}
|
|
89
102
|
let stepCount = Object.keys(this.steps).length;
|
|
90
|
-
if (this.steps["Before"])
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
if (this.steps["
|
|
94
|
-
stepCount--;
|
|
95
|
-
}
|
|
96
|
-
if (this.steps["BeforeAll"]) {
|
|
97
|
-
stepCount--;
|
|
98
|
-
}
|
|
99
|
-
if (this.steps["AfterAll"]) {
|
|
100
|
-
stepCount--;
|
|
101
|
-
}
|
|
103
|
+
if (this.steps["Before"]) stepCount--;
|
|
104
|
+
if (this.steps["After"]) stepCount--;
|
|
105
|
+
if (this.steps["BeforeAll"]) stepCount--;
|
|
106
|
+
if (this.steps["AfterAll"]) stepCount--;
|
|
102
107
|
if (print) {
|
|
103
108
|
logger.info("total steps definitions found", stepCount);
|
|
104
109
|
}
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import { io } from "socket.io-client";
|
|
2
|
+
import { BVTRecorder } from "./bvt_recorder.js";
|
|
3
|
+
import { compareWithScenario } from "../code_gen/duplication_analysis.js";
|
|
4
|
+
import { getAppDataDir } from "../utils/app_dir.js";
|
|
5
|
+
import { readdir } from "fs/promises";
|
|
6
|
+
import socketLogger, { getErrorMessage, responseSize } from "../utils/socket_logger.js";
|
|
7
|
+
import { MIXPANEL_EVENTS, mixpanelTrackEvent } from "./mixpanel.js";
|
|
8
|
+
const port = process.env.EDITOR_PORT || 3003;
|
|
9
|
+
const WS_URL = process.env.WORKER_WS_SERVER_URL || "http://localhost:" + port;
|
|
10
|
+
const userId = process.env.USER_ID || "";
|
|
11
|
+
const SocketIOEvents = {
|
|
12
|
+
REQUEST: "request",
|
|
13
|
+
RESPONSE: "response",
|
|
14
|
+
CONNECT: "connect",
|
|
15
|
+
DISCONNECT: "disconnect",
|
|
16
|
+
CREATE_ROOM: "createRoom",
|
|
17
|
+
JOIN_ROOM: "joinRoom",
|
|
18
|
+
};
|
|
19
|
+
class PromisifiedSocketServer {
|
|
20
|
+
socket;
|
|
21
|
+
routes;
|
|
22
|
+
constructor(socket, routes) {
|
|
23
|
+
this.socket = socket;
|
|
24
|
+
this.routes = routes;
|
|
25
|
+
}
|
|
26
|
+
init() {
|
|
27
|
+
this.socket.on(SocketIOEvents.REQUEST, async (data) => {
|
|
28
|
+
const { event, input, id, roomId, socketId } = data;
|
|
29
|
+
if (event !== "recorderWindow.getCurrentChromiumPath") {
|
|
30
|
+
socketLogger.info("Received request", { event, input, id, roomId, socketId });
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
const handler = this.routes[event];
|
|
34
|
+
if (!handler) {
|
|
35
|
+
socketLogger.error(`No handler found for event: ${event}`, undefined, event);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const response = await handler(input);
|
|
39
|
+
if (event !== "recorderWindow.getCurrentChromiumPath") {
|
|
40
|
+
socketLogger.info(`Sending response for ${event}, ${responseSize(response)} bytes`);
|
|
41
|
+
}
|
|
42
|
+
this.socket.emit(SocketIOEvents.RESPONSE, { id, value: response, roomId, socketId });
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
socketLogger.error("Error handling request", {
|
|
46
|
+
input,
|
|
47
|
+
id,
|
|
48
|
+
roomId,
|
|
49
|
+
socketId,
|
|
50
|
+
error: error instanceof Error ? `${error.message}\n${error.stack}` : error,
|
|
51
|
+
}, event);
|
|
52
|
+
this.socket.emit(SocketIOEvents.RESPONSE, {
|
|
53
|
+
id,
|
|
54
|
+
error: {
|
|
55
|
+
message: error?.message,
|
|
56
|
+
code: error?.code,
|
|
57
|
+
info: error?.info,
|
|
58
|
+
stack: error?.stack,
|
|
59
|
+
},
|
|
60
|
+
roomId,
|
|
61
|
+
socketId,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const timeOutForFunction = async (promise, timeout = 5000) => {
|
|
68
|
+
const timeoutPromise = new Promise((resolve) => setTimeout(() => resolve(), timeout));
|
|
69
|
+
try {
|
|
70
|
+
const res = await Promise.race([promise, timeoutPromise]);
|
|
71
|
+
return res;
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
socketLogger.error(error, undefined, "timeOutForFunction");
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
const CLIENT_IDENTIFIER = "cucumber_client/bvt_recorder";
|
|
79
|
+
async function BVTRecorderInit({ envName, projectDir, roomId, TOKEN, socket = null }) {
|
|
80
|
+
console.log(`Connecting to ${WS_URL}`);
|
|
81
|
+
socket = socket || io(WS_URL);
|
|
82
|
+
socketLogger.init(socket, { context: "BVTRecorder", eventName: "BVTRecorder.log" });
|
|
83
|
+
socket.on(SocketIOEvents.CONNECT, () => {
|
|
84
|
+
socketLogger.info(`${roomId} Connected to BVTRecorder server`);
|
|
85
|
+
});
|
|
86
|
+
socket.on(SocketIOEvents.DISCONNECT, (reason) => {
|
|
87
|
+
socketLogger.info(`${roomId} Disconnected from server: ${reason}`);
|
|
88
|
+
});
|
|
89
|
+
socket.emit(SocketIOEvents.JOIN_ROOM, { id: roomId, window: CLIENT_IDENTIFIER });
|
|
90
|
+
const recorder = new BVTRecorder({
|
|
91
|
+
envName,
|
|
92
|
+
projectDir,
|
|
93
|
+
TOKEN,
|
|
94
|
+
sendEvent: (event, data) => {
|
|
95
|
+
socketLogger.info("Sending event", { event, data, roomId });
|
|
96
|
+
socket.emit(event, data, roomId);
|
|
97
|
+
},
|
|
98
|
+
logger: socketLogger,
|
|
99
|
+
userId,
|
|
100
|
+
});
|
|
101
|
+
// emit connected event for every 50 ms until connection_ack message is recieved
|
|
102
|
+
let connected = false;
|
|
103
|
+
const interval = setInterval(() => {
|
|
104
|
+
if (connected) {
|
|
105
|
+
clearInterval(interval);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
socket.emit("BVTRecorder.connected", { roomId, window: "cucumber_client/bvt_recorder" }, roomId);
|
|
109
|
+
}, 50);
|
|
110
|
+
const promisifiedSocketServer = new PromisifiedSocketServer(socket, {
|
|
111
|
+
"recorderWindow.connectionAck": async (input) => {
|
|
112
|
+
mixpanelTrackEvent(MIXPANEL_EVENTS.RECORDER_CONNECTED, userId);
|
|
113
|
+
connected = true;
|
|
114
|
+
clearInterval(interval);
|
|
115
|
+
},
|
|
116
|
+
"recorderWindow.openBrowser": async (input) => {
|
|
117
|
+
return recorder
|
|
118
|
+
.openBrowser(input)
|
|
119
|
+
.then(() => {
|
|
120
|
+
mixpanelTrackEvent(MIXPANEL_EVENTS.CHROMIUM_LOADED, userId);
|
|
121
|
+
socketLogger.info("BVTRecorder.browserOpened");
|
|
122
|
+
socket.emit("BVTRecorder.browserOpened", { roomId, window: "cucumber_client/bvt_recorder" });
|
|
123
|
+
})
|
|
124
|
+
.catch((e) => {
|
|
125
|
+
socketLogger.error(`Error opening browser: ${getErrorMessage(e)}`, undefined, "recorderWindow.openBrowser");
|
|
126
|
+
socket.emit("BVTRecorder.browserLaunchFailed", { roomId, window: "cucumber_client/bvt_recorder" });
|
|
127
|
+
});
|
|
128
|
+
},
|
|
129
|
+
"recorderWindow.closeBrowser": async (input) => {
|
|
130
|
+
return recorder.closeBrowser(input);
|
|
131
|
+
},
|
|
132
|
+
"recorderWindow.reOpenBrowser": async (input) => {
|
|
133
|
+
return recorder
|
|
134
|
+
.reOpenBrowser(input)
|
|
135
|
+
.then(() => {
|
|
136
|
+
socketLogger.info("BVTRecorder.browserOpened");
|
|
137
|
+
socket.emit("BVTRecorder.browserOpened", null, roomId);
|
|
138
|
+
})
|
|
139
|
+
.catch((e) => {
|
|
140
|
+
socketLogger.error(`Error reopening browser: ${getErrorMessage(e)}`, undefined, "recorderWindow.reOpenBrowser");
|
|
141
|
+
socket.emit("BVTRecorder.browserLaunchFailed", null, roomId);
|
|
142
|
+
});
|
|
143
|
+
},
|
|
144
|
+
"recorderWindow.startRecordingInput": async (input) => {
|
|
145
|
+
return timeOutForFunction(recorder.startRecordingInput(input));
|
|
146
|
+
},
|
|
147
|
+
"recorderWindow.stopRecordingInput": async (input) => {
|
|
148
|
+
return timeOutForFunction(recorder.stopRecordingInput(input));
|
|
149
|
+
},
|
|
150
|
+
"recorderWindow.startRecordingText": async (input) => {
|
|
151
|
+
// console.log("--- {{ }} -- : recorderWindow.startRecordingText", input);
|
|
152
|
+
return timeOutForFunction(recorder.startRecordingText(input));
|
|
153
|
+
},
|
|
154
|
+
"recorderWindow.stopRecordingText": async (input) => {
|
|
155
|
+
return timeOutForFunction(recorder.stopRecordingText(input));
|
|
156
|
+
},
|
|
157
|
+
"recorderWindow.startRecordingContext": async (input) => {
|
|
158
|
+
return timeOutForFunction(recorder.startRecordingContext(input));
|
|
159
|
+
},
|
|
160
|
+
"recorderWindow.stopRecordingContext": async (input) => {
|
|
161
|
+
return timeOutForFunction(recorder.stopRecordingContext(input));
|
|
162
|
+
},
|
|
163
|
+
"recorderWindow.runStep": async (input) => {
|
|
164
|
+
return recorder.runStep(input);
|
|
165
|
+
},
|
|
166
|
+
"recorderWindow.saveScenario": async (input) => {
|
|
167
|
+
return recorder.saveScenario(input);
|
|
168
|
+
},
|
|
169
|
+
"recorderWindow.getImplementedSteps": async (input) => {
|
|
170
|
+
return (await recorder.getImplementedSteps(input)).implementedSteps;
|
|
171
|
+
},
|
|
172
|
+
"recorderWindow.getImplementedScenarios": async (input) => {
|
|
173
|
+
return (await recorder.getImplementedSteps(input)).scenarios;
|
|
174
|
+
},
|
|
175
|
+
"recorderWindow.getCurrentChromiumPath": async () => {
|
|
176
|
+
return recorder.getCurrentChromiumPath();
|
|
177
|
+
},
|
|
178
|
+
"recorderWindow.overwriteTestData": async (input) => {
|
|
179
|
+
return await recorder.overwriteTestData(input);
|
|
180
|
+
},
|
|
181
|
+
"recorderWindow.generateStepName": async (input) => {
|
|
182
|
+
return recorder.generateStepName(input);
|
|
183
|
+
},
|
|
184
|
+
"recorderWindow.getFeatureAndScenario": async (input) => {
|
|
185
|
+
return recorder.generateScenarioAndFeatureNames(input);
|
|
186
|
+
},
|
|
187
|
+
"recorderWindow.generateCommandName": async (input) => {
|
|
188
|
+
return recorder.generateCommandName(input);
|
|
189
|
+
},
|
|
190
|
+
"recorderWindow.loadTestData": async (input) => {
|
|
191
|
+
return recorder.loadTestData(input);
|
|
192
|
+
},
|
|
193
|
+
"recorderWindow.discard": async (input) => {
|
|
194
|
+
return await recorder.discardTestData(input);
|
|
195
|
+
},
|
|
196
|
+
"recorderWindow.addToTestData": async (input) => {
|
|
197
|
+
return await recorder.addToTestData(input);
|
|
198
|
+
},
|
|
199
|
+
"recorderWindow.getScenarios": async () => {
|
|
200
|
+
return recorder.getScenarios();
|
|
201
|
+
},
|
|
202
|
+
"recorderWindow.setShouldTakeScreenshot": async (input) => {
|
|
203
|
+
return recorder.setShouldTakeScreenshot(input);
|
|
204
|
+
},
|
|
205
|
+
"recorderWindow.compareWithScenario": async ({ projectDir, scenario }, roomId) => {
|
|
206
|
+
return await compareWithScenario(getAppDataDir(projectDir), scenario);
|
|
207
|
+
},
|
|
208
|
+
"recorderWindow.getCommandsForImplementedStep": async (input) => {
|
|
209
|
+
return recorder.getCommandsForImplementedStep(input);
|
|
210
|
+
},
|
|
211
|
+
"recorderWindow.getNumberOfOccurrences": async (input) => {
|
|
212
|
+
return recorder.getNumberOfOccurrences(input);
|
|
213
|
+
},
|
|
214
|
+
"recorderWindow.getFakeParams": async ({ parametersMap }) => {
|
|
215
|
+
return recorder.fakeParams(parametersMap);
|
|
216
|
+
},
|
|
217
|
+
"recorderWindow.abortExecution": async (_) => {
|
|
218
|
+
return recorder.abortExecution();
|
|
219
|
+
},
|
|
220
|
+
"recorderWindow.pauseExecution": async (input) => {
|
|
221
|
+
return recorder.pauseExecution(input);
|
|
222
|
+
},
|
|
223
|
+
"recorderWindow.resumeExecution": async (input) => {
|
|
224
|
+
return recorder.resumeExecution(input);
|
|
225
|
+
},
|
|
226
|
+
"recorderWindow.loadExistingScenario": async (input) => {
|
|
227
|
+
return recorder.loadExistingScenario(input);
|
|
228
|
+
},
|
|
229
|
+
"recorderWindow.findRelatedTextInAllFrames": async (input) => {
|
|
230
|
+
return recorder.findRelatedTextInAllFrames(input);
|
|
231
|
+
},
|
|
232
|
+
"recorderWindow.getReportFolder": async (input) => {
|
|
233
|
+
return recorder.getReportFolder();
|
|
234
|
+
},
|
|
235
|
+
"recorderWindow.getSnapshotFiles": async (input) => {
|
|
236
|
+
const snapshotFolder = recorder.getSnapshotFolder();
|
|
237
|
+
if (snapshotFolder) {
|
|
238
|
+
const files = await readdir(snapshotFolder);
|
|
239
|
+
const ymlFiles = files.filter((file) => file.endsWith(".yml") || file.endsWith(".yaml"));
|
|
240
|
+
return { folder: snapshotFolder, files: ymlFiles };
|
|
241
|
+
}
|
|
242
|
+
else
|
|
243
|
+
return { folder: null, files: [] };
|
|
244
|
+
},
|
|
245
|
+
"recorderWindow.getCurrentPageTitle": async () => {
|
|
246
|
+
return await recorder.getCurrentPageTitle();
|
|
247
|
+
},
|
|
248
|
+
"recorderWindow.getCurrentPageUrl": async () => {
|
|
249
|
+
return recorder.getCurrentPageUrl();
|
|
250
|
+
},
|
|
251
|
+
"recorderWindow.sendAriaSnapshot": async (input) => {
|
|
252
|
+
const snapshot = input?.snapshot;
|
|
253
|
+
const deselect = input?.deselect;
|
|
254
|
+
if (deselect === true) {
|
|
255
|
+
return await recorder.deselectAriaElements();
|
|
256
|
+
}
|
|
257
|
+
if (snapshot !== null) {
|
|
258
|
+
return await recorder.processAriaSnapshot(snapshot);
|
|
259
|
+
}
|
|
260
|
+
},
|
|
261
|
+
"recorderWindow.revertMode": async () => {
|
|
262
|
+
await recorder.revertMode();
|
|
263
|
+
},
|
|
264
|
+
"recorderWindow.setMode": async (input) => {
|
|
265
|
+
const mode = input?.mode;
|
|
266
|
+
return recorder.setMode(mode);
|
|
267
|
+
},
|
|
268
|
+
"recorderWindow.getStepsAndCommandsForScenario": async (input) => {
|
|
269
|
+
return await recorder.getStepsAndCommandsForScenario(input);
|
|
270
|
+
},
|
|
271
|
+
"recorderWindow.initExecution": async (input) => {
|
|
272
|
+
return await recorder.initExecution(input);
|
|
273
|
+
},
|
|
274
|
+
"recorderWindow.cleanupExecution": async (input) => {
|
|
275
|
+
return await recorder.cleanupExecution(input);
|
|
276
|
+
},
|
|
277
|
+
"recorderWindow.resetExecution": async (input) => {
|
|
278
|
+
return await recorder.resetExecution(input);
|
|
279
|
+
},
|
|
280
|
+
"recorderWindow.stopRecordingNetwork": async (input) => {
|
|
281
|
+
return recorder.stopRecordingNetwork(input);
|
|
282
|
+
},
|
|
283
|
+
"recorderWindow.cleanup": async (input) => {
|
|
284
|
+
return recorder.cleanup(input);
|
|
285
|
+
},
|
|
286
|
+
"recorderWindow.getStepCodeByScenario": async (input) => {
|
|
287
|
+
return await recorder.getStepCodeByScenario(input);
|
|
288
|
+
},
|
|
289
|
+
"recorderWindow.setStepCodeByScenario": async (input) => {
|
|
290
|
+
return await recorder.setStepCodeByScenario(input);
|
|
291
|
+
},
|
|
292
|
+
"recorderWindow.getRecorderContext": async (input) => {
|
|
293
|
+
return await recorder.getContext();
|
|
294
|
+
},
|
|
295
|
+
"recorderWindow.addCommandToStepCode": async (input) => {
|
|
296
|
+
return await recorder.addCommandToStepCode(input);
|
|
297
|
+
},
|
|
298
|
+
"recorderWindow.deleteCommandFromStepCode": async (input) => {
|
|
299
|
+
return await recorder.deleteCommandFromStepCode(input);
|
|
300
|
+
},
|
|
301
|
+
"recorderWindow.generateLocatorSummaries": async (input) => {
|
|
302
|
+
return await recorder.generateLocatorSummaries(input);
|
|
303
|
+
},
|
|
304
|
+
});
|
|
305
|
+
socket.on("targetBrowser.command.event", async (input) => {
|
|
306
|
+
return recorder.onAction(input);
|
|
307
|
+
});
|
|
308
|
+
promisifiedSocketServer.init();
|
|
309
|
+
}
|
|
310
|
+
export { BVTRecorderInit };
|