@dev-blinq/cucumber_client 1.0.1723-dev → 1.0.1724-dev
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/client/code_gen/code_inversion.js +2 -3
- package/bin/client/code_gen/page_reflection.js +2 -6
- package/bin/client/codemod/index.js +8 -0
- package/bin/client/codemod/locators_array/find_misstructured_elements.js +142 -0
- package/bin/client/codemod/locators_array/fix_misstructured_elements.js +140 -0
- package/bin/client/codemod/locators_array/index.js +114 -0
- package/bin/client/codemod/types.js +1 -0
- package/bin/client/recorderv3/bvt_recorder.js +11 -11
- package/bin/index.js +1 -0
- package/package.json +2 -1
- package/bin/client/code_cleanup/codemod/find_misstructured_elements.js +0 -164
- package/bin/client/code_cleanup/codemod/fix_misstructured_elements.js +0 -246
- /package/bin/client/{code_cleanup/codemod → codemod}/find_harcoded_locators.js +0 -0
- /package/bin/client/{code_cleanup/codemod → codemod}/fix_hardcoded_locators.js +0 -0
|
@@ -604,15 +604,14 @@ const invertStableCommand = (call, elements, stepParams) => {
|
|
|
604
604
|
|
|
605
605
|
// Set locators if element exists
|
|
606
606
|
if (step.element && elements[step.element.key]) {
|
|
607
|
-
|
|
608
|
-
const locBundle = elements[step.element.key]?.locators;
|
|
607
|
+
const locBundle = elements[step.element.key];
|
|
609
608
|
step.locators = locBundle;
|
|
610
609
|
}
|
|
611
610
|
|
|
612
611
|
if (step.type === Types.CLICK) {
|
|
613
612
|
// handle click with parametric locators
|
|
614
613
|
// check if locators are parametric , i.e. contains `{variable}`
|
|
615
|
-
if (step.locators &&
|
|
614
|
+
if (step.locators && step.locators.locators && Array.isArray(step.locators.locators)) {
|
|
616
615
|
let hasParametricLocators = false;
|
|
617
616
|
let variable = "";
|
|
618
617
|
const regex = /{([^}]+)}/g;
|
|
@@ -567,16 +567,12 @@ export class CodePage {
|
|
|
567
567
|
if (!element || !element.locators)
|
|
568
568
|
return element;
|
|
569
569
|
const clone = JSON.parse(JSON.stringify(element));
|
|
570
|
-
const locatorsArray = Array.isArray(clone.locators)
|
|
571
|
-
? clone.locators
|
|
572
|
-
: (clone.locators.locators ?? []);
|
|
570
|
+
const locatorsArray = Array.isArray(clone.locators) ? clone.locators : [];
|
|
573
571
|
for (let i = 0; i < locatorsArray.length; i++) {
|
|
574
572
|
if (locatorsArray[i].score)
|
|
575
573
|
delete locatorsArray[i].score;
|
|
576
574
|
}
|
|
577
|
-
clone.locators = Array.isArray(clone.locators)
|
|
578
|
-
? locatorsArray
|
|
579
|
-
: { ...clone.locators, locators: locatorsArray };
|
|
575
|
+
clone.locators = Array.isArray(clone.locators) ? locatorsArray : [];
|
|
580
576
|
return clone;
|
|
581
577
|
}
|
|
582
578
|
insertElements(elements) {
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { parse } from "@babel/parser";
|
|
4
|
+
import traverse from "@babel/traverse";
|
|
5
|
+
import * as t from "@babel/types";
|
|
6
|
+
const IGNORE_DIRS = new Set(["node_modules", ".git", ".idea"]);
|
|
7
|
+
function propName(prop) {
|
|
8
|
+
if (!t.isObjectProperty(prop))
|
|
9
|
+
return undefined;
|
|
10
|
+
const key = prop.key;
|
|
11
|
+
if (t.isIdentifier(key))
|
|
12
|
+
return key.name;
|
|
13
|
+
if (t.isStringLiteral(key))
|
|
14
|
+
return key.value;
|
|
15
|
+
if (t.isNumericLiteral(key))
|
|
16
|
+
return String(key.value);
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
function isNumericKey(name) {
|
|
20
|
+
return typeof name === "string" && /^\d+$/.test(name);
|
|
21
|
+
}
|
|
22
|
+
function getElementLabel(props) {
|
|
23
|
+
const labelProp = props.find((p) => ["element_name", "element_key"].includes(propName(p) ?? ""));
|
|
24
|
+
if (!labelProp)
|
|
25
|
+
return undefined;
|
|
26
|
+
const value = labelProp.value;
|
|
27
|
+
if (t.isStringLiteral(value))
|
|
28
|
+
return value.value;
|
|
29
|
+
if (t.isTemplateLiteral(value)) {
|
|
30
|
+
return value.quasis.map((q) => q.value.cooked ?? q.value.raw).join("${}");
|
|
31
|
+
}
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
function analyzeAst(filePath, code, results) {
|
|
35
|
+
const ast = parse(code, {
|
|
36
|
+
sourceType: "module",
|
|
37
|
+
plugins: ["jsx", "typescript", "classProperties", "optionalChaining", "nullishCoalescingOperator", "topLevelAwait"],
|
|
38
|
+
});
|
|
39
|
+
traverse(ast, {
|
|
40
|
+
ObjectExpression(pathObj) {
|
|
41
|
+
const props = pathObj.node.properties.filter(t.isObjectProperty);
|
|
42
|
+
if (props.length === 0)
|
|
43
|
+
return;
|
|
44
|
+
const names = props.map(propName).filter(Boolean);
|
|
45
|
+
const hasElementMeta = names.some((n) => n === "element_name" || n === "element_key");
|
|
46
|
+
if (!hasElementMeta)
|
|
47
|
+
return;
|
|
48
|
+
const hasLocators = names.includes("locators");
|
|
49
|
+
const numericProps = names.filter(isNumericKey);
|
|
50
|
+
if (!hasLocators && numericProps.length > 0) {
|
|
51
|
+
results.push({
|
|
52
|
+
file: filePath,
|
|
53
|
+
line: pathObj.node.loc?.start.line ?? 0,
|
|
54
|
+
element: getElementLabel(props) ?? "<unknown>",
|
|
55
|
+
numericCount: numericProps.length,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
function findLineNumber(content, searchValue) {
|
|
62
|
+
if (!searchValue)
|
|
63
|
+
return 0;
|
|
64
|
+
const idx = content.indexOf(searchValue);
|
|
65
|
+
if (idx === -1)
|
|
66
|
+
return 0;
|
|
67
|
+
return content.slice(0, idx).split("\n").length;
|
|
68
|
+
}
|
|
69
|
+
function analyzeJson(filePath, content, results, logger) {
|
|
70
|
+
let data;
|
|
71
|
+
try {
|
|
72
|
+
data = JSON.parse(content);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
logger.warn(`Skipping ${filePath}: invalid JSON`);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const visit = (node) => {
|
|
79
|
+
if (!node || typeof node !== "object")
|
|
80
|
+
return;
|
|
81
|
+
if (Array.isArray(node)) {
|
|
82
|
+
node.forEach(visit);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const record = node;
|
|
86
|
+
const keys = Object.keys(record);
|
|
87
|
+
const hasElementMeta = "element_name" in record || "element_key" in record;
|
|
88
|
+
const hasLocators = "locators" in record;
|
|
89
|
+
const numericKeys = keys.filter(isNumericKey);
|
|
90
|
+
if (hasElementMeta && !hasLocators && numericKeys.length > 0) {
|
|
91
|
+
results.push({
|
|
92
|
+
file: filePath,
|
|
93
|
+
line: findLineNumber(content, record.element_name ?? record.element_key),
|
|
94
|
+
element: record.element_name ?? record.element_key ?? "<unknown>",
|
|
95
|
+
numericCount: numericKeys.length,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
Object.values(record).forEach(visit);
|
|
99
|
+
};
|
|
100
|
+
visit(data);
|
|
101
|
+
}
|
|
102
|
+
async function walk(dir, results, logger) {
|
|
103
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
104
|
+
for (const entry of entries) {
|
|
105
|
+
if (IGNORE_DIRS.has(entry.name))
|
|
106
|
+
continue;
|
|
107
|
+
const resolved = path.join(dir, entry.name);
|
|
108
|
+
if (entry.isDirectory()) {
|
|
109
|
+
await walk(resolved, results, logger);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (!entry.isFile())
|
|
113
|
+
continue;
|
|
114
|
+
if (entry.name.endsWith(".mjs") || entry.name.endsWith(".js")) {
|
|
115
|
+
const code = await fs.readFile(resolved, "utf8");
|
|
116
|
+
try {
|
|
117
|
+
analyzeAst(resolved, code, results);
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
logger.error(`Failed to analyze ${resolved}: ${err.message}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
else if (entry.name.endsWith(".json")) {
|
|
124
|
+
const content = await fs.readFile(resolved, "utf8");
|
|
125
|
+
try {
|
|
126
|
+
analyzeJson(resolved, content, results, logger);
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
logger.error(`Failed to analyze ${resolved}: ${err.message}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Scans all JS/MJS/JSON files under `root` and returns an array of issues:
|
|
136
|
+
* [{ file, line, element, numericCount }]
|
|
137
|
+
*/
|
|
138
|
+
export async function findIssues(root, logger) {
|
|
139
|
+
const results = [];
|
|
140
|
+
await walk(root, results, logger);
|
|
141
|
+
return results;
|
|
142
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { parse } from "@babel/parser";
|
|
4
|
+
import traverse from "@babel/traverse";
|
|
5
|
+
import * as t from "@babel/types";
|
|
6
|
+
function isNumericKey(key) {
|
|
7
|
+
return typeof key === "string" && /^\d+$/.test(key);
|
|
8
|
+
}
|
|
9
|
+
function propName(prop) {
|
|
10
|
+
if (!t.isObjectProperty(prop))
|
|
11
|
+
return undefined;
|
|
12
|
+
const k = prop.key;
|
|
13
|
+
if (t.isIdentifier(k))
|
|
14
|
+
return k.name;
|
|
15
|
+
if (t.isStringLiteral(k))
|
|
16
|
+
return k.value;
|
|
17
|
+
if (t.isNumericLiteral(k))
|
|
18
|
+
return String(k.value);
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
function stringifyValue(node, source) {
|
|
22
|
+
if (typeof node.start === "number" && typeof node.end === "number") {
|
|
23
|
+
return source.slice(node.start, node.end);
|
|
24
|
+
}
|
|
25
|
+
return JSON.stringify(null);
|
|
26
|
+
}
|
|
27
|
+
function leadingWhitespace(text, index) {
|
|
28
|
+
const lineStart = text.lastIndexOf("\n", index - 1) + 1;
|
|
29
|
+
const match = text.slice(lineStart, index).match(/^[ \t]*/);
|
|
30
|
+
return match ? match[0] : "";
|
|
31
|
+
}
|
|
32
|
+
function buildLocatorsArray(numericProps, source, indent) {
|
|
33
|
+
const elements = numericProps
|
|
34
|
+
.sort((a, b) => Number(propName(a)) - Number(propName(b)))
|
|
35
|
+
.map((p) => `${indent} ${stringifyValue(p.value, source)}`);
|
|
36
|
+
if (elements.length === 0)
|
|
37
|
+
return "locators: []";
|
|
38
|
+
return [indent + "locators: [", elements.join(",\n"), indent + "]"].join("\n");
|
|
39
|
+
}
|
|
40
|
+
function fixJsPreserveFormat(code, logger) {
|
|
41
|
+
let touched = false;
|
|
42
|
+
const replacements = [];
|
|
43
|
+
const ast = parse(code, {
|
|
44
|
+
sourceType: "module",
|
|
45
|
+
plugins: ["jsx", "typescript", "classProperties", "optionalChaining", "nullishCoalescingOperator", "topLevelAwait"],
|
|
46
|
+
ranges: true,
|
|
47
|
+
});
|
|
48
|
+
traverse(ast, {
|
|
49
|
+
ObjectExpression(pathObj) {
|
|
50
|
+
const props = pathObj.node.properties.filter(t.isObjectProperty);
|
|
51
|
+
if (!props.length)
|
|
52
|
+
return;
|
|
53
|
+
const names = props.map(propName).filter(Boolean);
|
|
54
|
+
const hasElementMeta = names.some((n) => n === "element_name" || n === "element_key");
|
|
55
|
+
if (!hasElementMeta)
|
|
56
|
+
return;
|
|
57
|
+
const hasLocators = names.includes("locators");
|
|
58
|
+
const numericProps = props.filter((p) => isNumericKey(propName(p)));
|
|
59
|
+
if (hasLocators || numericProps.length === 0)
|
|
60
|
+
return;
|
|
61
|
+
if (typeof pathObj.node.start !== "number" || typeof pathObj.node.end !== "number")
|
|
62
|
+
return;
|
|
63
|
+
const leading = leadingWhitespace(code, pathObj.node.start);
|
|
64
|
+
const otherProps = props.filter((p) => !numericProps.includes(p));
|
|
65
|
+
const otherTexts = otherProps.map((p) => code.slice(p.start ?? 0, p.end ?? 0));
|
|
66
|
+
const locatorsText = buildLocatorsArray(numericProps, code, `${leading} `);
|
|
67
|
+
const pieces = [locatorsText, ...otherTexts];
|
|
68
|
+
const baseIndent = `${leading} `;
|
|
69
|
+
const normalized = pieces.map((p) => (p.startsWith(baseIndent) ? p : baseIndent + p));
|
|
70
|
+
const newObjectText = "{\n" + normalized.join(",\n") + "\n" + leading + "}";
|
|
71
|
+
replacements.push({ start: pathObj.node.start, end: pathObj.node.end, text: newObjectText });
|
|
72
|
+
touched = true;
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
if (!touched)
|
|
76
|
+
return code;
|
|
77
|
+
let out = code;
|
|
78
|
+
replacements
|
|
79
|
+
.sort((a, b) => b.start - a.start)
|
|
80
|
+
.forEach(({ start, end, text }) => {
|
|
81
|
+
out = out.slice(0, start) + text + out.slice(end);
|
|
82
|
+
});
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
function fixJson(content, logger) {
|
|
86
|
+
let data;
|
|
87
|
+
try {
|
|
88
|
+
data = JSON.parse(content);
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return content;
|
|
92
|
+
}
|
|
93
|
+
let touched = false;
|
|
94
|
+
const visit = (node, logger) => {
|
|
95
|
+
if (!node || typeof node !== "object")
|
|
96
|
+
return;
|
|
97
|
+
if (Array.isArray(node))
|
|
98
|
+
return node.forEach((n) => visit(n, logger));
|
|
99
|
+
const record = node;
|
|
100
|
+
const keys = Object.keys(record);
|
|
101
|
+
const hasMeta = "element_name" in record || "element_key" in record;
|
|
102
|
+
const hasLoc = "locators" in record;
|
|
103
|
+
const nums = keys.filter(isNumericKey);
|
|
104
|
+
if (hasMeta && !hasLoc && nums.length) {
|
|
105
|
+
record.locators = nums.sort((a, b) => Number(a) - Number(b)).map((k) => record[k]);
|
|
106
|
+
nums.forEach((k) => delete record[k]);
|
|
107
|
+
touched = true;
|
|
108
|
+
}
|
|
109
|
+
Object.values(record).forEach((v) => visit(v, logger));
|
|
110
|
+
};
|
|
111
|
+
visit(data, logger);
|
|
112
|
+
return touched ? `${JSON.stringify(data, null, 2)}\n` : content;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Fixes all files referenced in `issues` under `root`.
|
|
116
|
+
* `issues` is the array returned by findIssues().
|
|
117
|
+
* Returns an array of { filePath, original, fixed } for files that changed.
|
|
118
|
+
*/
|
|
119
|
+
export async function fixIssues(issues, root, logger) {
|
|
120
|
+
// Deduplicate — multiple issues can point to the same file
|
|
121
|
+
const filePaths = [...new Set(issues.map((i) => i.file))];
|
|
122
|
+
const changes = [];
|
|
123
|
+
for (const filePath of filePaths) {
|
|
124
|
+
const absolutePath = path.isAbsolute(filePath) ? filePath : path.join(root, filePath);
|
|
125
|
+
const ext = path.extname(absolutePath).toLowerCase();
|
|
126
|
+
const original = await fs.readFile(absolutePath, "utf8");
|
|
127
|
+
let fixed = original;
|
|
128
|
+
if (ext === ".json") {
|
|
129
|
+
fixed = fixJson(original, logger);
|
|
130
|
+
}
|
|
131
|
+
else if (ext === ".mjs" || ext === ".js") {
|
|
132
|
+
fixed = fixJsPreserveFormat(original, logger);
|
|
133
|
+
}
|
|
134
|
+
if (fixed === original)
|
|
135
|
+
continue;
|
|
136
|
+
await fs.writeFile(absolutePath, fixed, "utf8");
|
|
137
|
+
changes.push({ filePath: absolutePath, original, fixed });
|
|
138
|
+
}
|
|
139
|
+
return changes;
|
|
140
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { findIssues } from "./find_misstructured_elements";
|
|
2
|
+
import { fixIssues } from "./fix_misstructured_elements";
|
|
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 {};
|
|
@@ -16,7 +16,6 @@ import { unEscapeNonPrintables } from "../cucumber/utils.js";
|
|
|
16
16
|
import { findAvailablePort, getRunsServiceBaseURL } from "../utils/index.js";
|
|
17
17
|
import socketLogger, { getErrorMessage } from "../utils/socket_logger.js";
|
|
18
18
|
import { tmpdir } from "os";
|
|
19
|
-
import { faker } from "@faker-js/faker/locale/en_US";
|
|
20
19
|
import { chromium } from "playwright-core";
|
|
21
20
|
import { axiosClient } from "../utils/axiosClient.js";
|
|
22
21
|
import { _generateCodeFromCommand } from "../code_gen/playwright_codeget.js";
|
|
@@ -1229,9 +1228,9 @@ export class BVTRecorder {
|
|
|
1229
1228
|
datasets,
|
|
1230
1229
|
};
|
|
1231
1230
|
}
|
|
1232
|
-
async generateLocatorSummaries({ allStrategyLocators, element_name }) {
|
|
1231
|
+
async generateLocatorSummaries({ allStrategyLocators, element_name, }) {
|
|
1233
1232
|
const input = {
|
|
1234
|
-
[element_name ?? "element"]: allStrategyLocators
|
|
1233
|
+
[element_name ?? "element"]: allStrategyLocators,
|
|
1235
1234
|
};
|
|
1236
1235
|
const result = await this.namesService.generateLocatorDescriptions({ locatorsObj: input });
|
|
1237
1236
|
return result[element_name ?? "element"];
|
|
@@ -1583,29 +1582,30 @@ export class BVTRecorder {
|
|
|
1583
1582
|
this.bvtContext.STORE_DETAILED_NETWORK_DATA = false;
|
|
1584
1583
|
}
|
|
1585
1584
|
}
|
|
1586
|
-
async fakeParams(params) {
|
|
1585
|
+
async fakeParams(params, faker) {
|
|
1586
|
+
if (!faker) {
|
|
1587
|
+
faker = await import("@faker-js/faker/locale/en_US").then((mod) => mod.faker);
|
|
1588
|
+
}
|
|
1587
1589
|
const newFakeParams = {};
|
|
1588
|
-
Object.
|
|
1589
|
-
if (!
|
|
1590
|
-
newFakeParams[key] =
|
|
1590
|
+
Object.keys(params).forEach((key) => {
|
|
1591
|
+
if (!params[key].startsWith("{{") || !params[key].endsWith("}}")) {
|
|
1592
|
+
newFakeParams[key] = params[key];
|
|
1591
1593
|
return;
|
|
1592
1594
|
}
|
|
1593
1595
|
try {
|
|
1594
1596
|
const value = params[key].substring(2, params[key].length - 2).trim();
|
|
1595
1597
|
const faking = value.split("(")[0].split(".");
|
|
1596
1598
|
let argument = value.substring(value.indexOf("(") + 1, value.lastIndexOf(")"));
|
|
1597
|
-
argument = isNaN(Number(argument))
|
|
1599
|
+
argument = isNaN(Number(argument)) ? argument : Number(argument);
|
|
1598
1600
|
let fakeFunc = faker;
|
|
1599
1601
|
faking.forEach((f) => {
|
|
1600
|
-
//@ts-expect-error Trying to support both old and new faker versions
|
|
1601
1602
|
fakeFunc = fakeFunc[f];
|
|
1602
1603
|
});
|
|
1603
|
-
//@ts-expect-error Trying to support both old and new faker versions
|
|
1604
1604
|
const newValue = fakeFunc(argument);
|
|
1605
1605
|
newFakeParams[key] = newValue;
|
|
1606
1606
|
}
|
|
1607
1607
|
catch (error) {
|
|
1608
|
-
newFakeParams[key] =
|
|
1608
|
+
newFakeParams[key] = params[key];
|
|
1609
1609
|
}
|
|
1610
1610
|
});
|
|
1611
1611
|
return newFakeParams;
|
package/bin/index.js
CHANGED
|
@@ -13,6 +13,7 @@ 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
|
+
export * from "./client/codemod/index.js";
|
|
16
17
|
export * from "./client/code_cleanup/find_step_definition_references.js";
|
|
17
18
|
export * from "./client/recorderv3/bvt_init.js";
|
|
18
19
|
export * from "./client/recorderv3/step_utils.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dev-blinq/cucumber_client",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1724-dev",
|
|
4
4
|
"description": " ",
|
|
5
5
|
"main": "bin/index.js",
|
|
6
6
|
"types": "bin/index.d.ts",
|
|
@@ -62,6 +62,7 @@
|
|
|
62
62
|
"winston-daily-rotate-file": "^4.7.1"
|
|
63
63
|
},
|
|
64
64
|
"devDependencies": {
|
|
65
|
+
"@types/babel__traverse": "^7.28.0",
|
|
65
66
|
"@types/prettier": "^2.7.3",
|
|
66
67
|
"chai": "^5.1.2",
|
|
67
68
|
"cpx": "^1.5.0",
|
|
@@ -1,164 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { promises as fs } from "node:fs";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { fileURLToPath } from "node:url";
|
|
5
|
-
import { parse } from "@babel/parser";
|
|
6
|
-
import traverseImport from "@babel/traverse";
|
|
7
|
-
|
|
8
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
-
const __dirname = path.dirname(__filename);
|
|
10
|
-
|
|
11
|
-
const ROOT = process.cwd();
|
|
12
|
-
const IGNORE_DIRS = new Set(["node_modules", ".git", ".idea"]);
|
|
13
|
-
|
|
14
|
-
const results = [];
|
|
15
|
-
|
|
16
|
-
const traverse = typeof traverseImport === "function" ? traverseImport : traverseImport.default;
|
|
17
|
-
|
|
18
|
-
function propName(prop) {
|
|
19
|
-
if (prop.type !== "ObjectProperty") return undefined;
|
|
20
|
-
const key = prop.key;
|
|
21
|
-
if (key.type === "Identifier") return key.name;
|
|
22
|
-
if (key.type === "StringLiteral") return key.value;
|
|
23
|
-
if (key.type === "NumericLiteral") return String(key.value);
|
|
24
|
-
return undefined;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function isNumericKey(name) {
|
|
28
|
-
return typeof name === "string" && /^\d+$/.test(name);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function getElementLabel(props) {
|
|
32
|
-
const labelProp = props.find(
|
|
33
|
-
(p) => p.type === "ObjectProperty" && ["element_name", "element_key"].includes(propName(p))
|
|
34
|
-
);
|
|
35
|
-
if (!labelProp) return undefined;
|
|
36
|
-
|
|
37
|
-
const value = labelProp.value;
|
|
38
|
-
if (value.type === "StringLiteral") return value.value;
|
|
39
|
-
if (value.type === "TemplateLiteral") {
|
|
40
|
-
return value.quasis.map((q) => q.value.cooked ?? q.value.raw).join("${}");
|
|
41
|
-
}
|
|
42
|
-
return undefined;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function reportIssue({ file, line, element, numericCount }) {
|
|
46
|
-
results.push({ file, line, element: element ?? "<unknown>", numericCount });
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function analyzeAst(filePath, code) {
|
|
50
|
-
const ast = parse(code, {
|
|
51
|
-
sourceType: "module",
|
|
52
|
-
plugins: ["jsx", "typescript", "classProperties", "optionalChaining", "nullishCoalescingOperator", "topLevelAwait"],
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
traverse(ast, {
|
|
56
|
-
ObjectExpression(pathObj) {
|
|
57
|
-
const props = pathObj.node.properties.filter((p) => p.type === "ObjectProperty");
|
|
58
|
-
if (props.length === 0) return;
|
|
59
|
-
|
|
60
|
-
const names = props.map(propName).filter(Boolean);
|
|
61
|
-
const hasElementMeta = names.some((n) => n === "element_name" || n === "element_key");
|
|
62
|
-
if (!hasElementMeta) return;
|
|
63
|
-
|
|
64
|
-
const hasLocators = names.includes("locators");
|
|
65
|
-
const numericProps = names.filter(isNumericKey);
|
|
66
|
-
|
|
67
|
-
if (!hasLocators && numericProps.length > 0) {
|
|
68
|
-
const element = getElementLabel(props);
|
|
69
|
-
const line = pathObj.node.loc?.start.line ?? 0;
|
|
70
|
-
reportIssue({ file: filePath, line, element, numericCount: numericProps.length });
|
|
71
|
-
}
|
|
72
|
-
},
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function findLineNumber(content, searchValue) {
|
|
77
|
-
if (!searchValue) return 0;
|
|
78
|
-
const idx = content.indexOf(searchValue);
|
|
79
|
-
if (idx === -1) return 0;
|
|
80
|
-
return content.slice(0, idx).split("\n").length;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function analyzeJson(filePath, content) {
|
|
84
|
-
let data;
|
|
85
|
-
try {
|
|
86
|
-
data = JSON.parse(content);
|
|
87
|
-
} catch (err) {
|
|
88
|
-
console.warn(`Skipping ${filePath}: invalid JSON`);
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const visit = (node) => {
|
|
93
|
-
if (!node || typeof node !== "object") return;
|
|
94
|
-
if (Array.isArray(node)) {
|
|
95
|
-
node.forEach(visit);
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const keys = Object.keys(node);
|
|
100
|
-
const hasElementMeta = "element_name" in node || "element_key" in node;
|
|
101
|
-
const hasLocators = "locators" in node;
|
|
102
|
-
const numericKeys = keys.filter(isNumericKey);
|
|
103
|
-
|
|
104
|
-
if (hasElementMeta && !hasLocators && numericKeys.length > 0) {
|
|
105
|
-
const element = node.element_name || node.element_key || "<unknown>";
|
|
106
|
-
const line = findLineNumber(content, element);
|
|
107
|
-
reportIssue({ file: filePath, line, element, numericCount: numericKeys.length });
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
Object.values(node).forEach(visit);
|
|
111
|
-
};
|
|
112
|
-
|
|
113
|
-
visit(data);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
async function walk(dir) {
|
|
117
|
-
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
118
|
-
for (const entry of entries) {
|
|
119
|
-
if (IGNORE_DIRS.has(entry.name)) continue;
|
|
120
|
-
const resolved = path.join(dir, entry.name);
|
|
121
|
-
if (entry.isDirectory()) {
|
|
122
|
-
await walk(resolved);
|
|
123
|
-
continue;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
if (!entry.isFile()) continue;
|
|
127
|
-
|
|
128
|
-
if (entry.name.endsWith(".mjs") || entry.name.endsWith(".js")) {
|
|
129
|
-
const code = await fs.readFile(resolved, "utf8");
|
|
130
|
-
try {
|
|
131
|
-
analyzeAst(resolved, code);
|
|
132
|
-
} catch (err) {
|
|
133
|
-
console.error(`Failed to analyze ${resolved}:`, err.message);
|
|
134
|
-
}
|
|
135
|
-
} else if (entry.name.endsWith(".json")) {
|
|
136
|
-
const content = await fs.readFile(resolved, "utf8");
|
|
137
|
-
try {
|
|
138
|
-
analyzeJson(resolved, content);
|
|
139
|
-
} catch (err) {
|
|
140
|
-
console.error(`Failed to analyze ${resolved}:`, err.message);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
async function main() {
|
|
147
|
-
await walk(ROOT);
|
|
148
|
-
|
|
149
|
-
if (results.length === 0) {
|
|
150
|
-
console.log("No misstructured elements found (numeric keys without locators).");
|
|
151
|
-
return;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
for (const r of results) {
|
|
155
|
-
console.log(`${r.file}:${r.line} — ${r.element} (${r.numericCount} numeric keys, missing locators)`);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
process.exitCode = 1;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
main().catch((err) => {
|
|
162
|
-
console.error(err);
|
|
163
|
-
process.exit(1);
|
|
164
|
-
});
|
|
@@ -1,246 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Low-noise codemod: fixes objects with numeric keys but no `locators` by splicing text in place.
|
|
4
|
-
* - Clones the workspace; originals untouched.
|
|
5
|
-
* - For each flagged object, inserts a `locators` array built from numeric keys and removes those keys.
|
|
6
|
-
* - Preserves surrounding formatting; only the object literal text is changed.
|
|
7
|
-
*
|
|
8
|
-
* Usage:
|
|
9
|
-
* node find-misstructured-elements.mjs | node fix-misstructured-elements.mjs
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import fs from "node:fs";
|
|
13
|
-
import { promises as fsPromises } from "node:fs";
|
|
14
|
-
import path from "node:path";
|
|
15
|
-
import os from "node:os";
|
|
16
|
-
import { parse } from "@babel/parser";
|
|
17
|
-
import traverseImport from "@babel/traverse";
|
|
18
|
-
import { spawnSync } from "node:child_process";
|
|
19
|
-
|
|
20
|
-
const traverse = typeof traverseImport === "function" ? traverseImport : traverseImport.default;
|
|
21
|
-
const ROOT = process.cwd();
|
|
22
|
-
|
|
23
|
-
const INPUT = await readStdin();
|
|
24
|
-
if (!INPUT.trim()) {
|
|
25
|
-
console.error("No input provided. Pipe find-misstructured-elements.mjs into this script.");
|
|
26
|
-
process.exit(1);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const filesToFix = parseInputLines(INPUT);
|
|
30
|
-
if (filesToFix.size === 0) {
|
|
31
|
-
console.log("No actionable lines detected.");
|
|
32
|
-
process.exit(0);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const INPLACE = process.argv.includes("--inplace");
|
|
36
|
-
|
|
37
|
-
const { cloneRoot, mapper } = INPLACE ? { cloneRoot: ROOT, mapper: (p) => p } : await cloneWorkspace(ROOT);
|
|
38
|
-
if (!INPLACE) {
|
|
39
|
-
console.log(`Cloned workspace to: ${cloneRoot}`);
|
|
40
|
-
await initGitRepo(cloneRoot);
|
|
41
|
-
} else {
|
|
42
|
-
console.log("Running in-place (original files will be modified).");
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const changes = [];
|
|
46
|
-
|
|
47
|
-
for (const originalPath of filesToFix) {
|
|
48
|
-
const clonedPath = mapper(originalPath);
|
|
49
|
-
const ext = path.extname(clonedPath).toLowerCase();
|
|
50
|
-
const original = await fsPromises.readFile(clonedPath, "utf8");
|
|
51
|
-
let fixed = original;
|
|
52
|
-
|
|
53
|
-
if (ext === ".json") {
|
|
54
|
-
fixed = fixJson(original);
|
|
55
|
-
} else if (ext === ".mjs" || ext === ".js") {
|
|
56
|
-
fixed = fixJsPreserveFormat(original);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
if (fixed === original) continue;
|
|
60
|
-
|
|
61
|
-
const diff = makeDiff(originalPath, clonedPath, fixed);
|
|
62
|
-
await fsPromises.writeFile(clonedPath, fixed, "utf8");
|
|
63
|
-
changes.push({ filePath: originalPath, diff });
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if (changes.length === 0) {
|
|
67
|
-
console.log("No changes needed.");
|
|
68
|
-
process.exit(0);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
for (const { filePath, diff } of changes) {
|
|
72
|
-
console.log(`\n=== ${filePath} (cloned) ===`);
|
|
73
|
-
console.log(diff.trimEnd());
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
process.exit(1);
|
|
77
|
-
|
|
78
|
-
// ---------- helpers ----------
|
|
79
|
-
|
|
80
|
-
function parseInputLines(text) {
|
|
81
|
-
const set = new Set();
|
|
82
|
-
for (const line of text.split(/\r?\n/)) {
|
|
83
|
-
const m = line.match(/^(.*?):\d+ — /);
|
|
84
|
-
if (m) set.add(m[1]);
|
|
85
|
-
}
|
|
86
|
-
return set;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function isNumericKey(key) {
|
|
90
|
-
return typeof key === "string" && /^\d+$/.test(key);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function propName(prop) {
|
|
94
|
-
if (prop.type !== "ObjectProperty") return undefined;
|
|
95
|
-
const k = prop.key;
|
|
96
|
-
if (k.type === "Identifier") return k.name;
|
|
97
|
-
if (k.type === "StringLiteral") return k.value;
|
|
98
|
-
if (k.type === "NumericLiteral") return String(k.value);
|
|
99
|
-
return undefined;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function stringifyValue(node, source) {
|
|
103
|
-
// Slice raw text to preserve formatting
|
|
104
|
-
if (typeof node.start === "number" && typeof node.end === "number") {
|
|
105
|
-
return source.slice(node.start, node.end);
|
|
106
|
-
}
|
|
107
|
-
return JSON.stringify(null);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function buildLocatorsArray(numericProps, source, indent) {
|
|
111
|
-
const elements = numericProps
|
|
112
|
-
.sort((a, b) => Number(propName(a)) - Number(propName(b)))
|
|
113
|
-
.map((p) => indent + " " + stringifyValue(p.value, source));
|
|
114
|
-
if (elements.length === 0) return "locators: []";
|
|
115
|
-
return [indent + "locators: [", elements.join(",\n"), indent + "]"].join("\n");
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function fixJsPreserveFormat(code) {
|
|
119
|
-
let touched = false;
|
|
120
|
-
const replacements = [];
|
|
121
|
-
|
|
122
|
-
const ast = parse(code, {
|
|
123
|
-
sourceType: "module",
|
|
124
|
-
plugins: ["jsx", "typescript", "classProperties", "optionalChaining", "nullishCoalescingOperator", "topLevelAwait"],
|
|
125
|
-
ranges: true,
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
traverse(ast, {
|
|
129
|
-
ObjectExpression(pathObj) {
|
|
130
|
-
const props = pathObj.node.properties.filter((p) => p.type === "ObjectProperty");
|
|
131
|
-
if (!props.length) return;
|
|
132
|
-
|
|
133
|
-
const names = props.map(propName).filter(Boolean);
|
|
134
|
-
const hasElementMeta = names.some((n) => n === "element_name" || n === "element_key");
|
|
135
|
-
if (!hasElementMeta) return;
|
|
136
|
-
|
|
137
|
-
const hasLocators = names.includes("locators");
|
|
138
|
-
const numericProps = props.filter((p) => isNumericKey(propName(p)));
|
|
139
|
-
if (hasLocators || numericProps.length === 0) return;
|
|
140
|
-
|
|
141
|
-
if (typeof pathObj.node.start !== "number" || typeof pathObj.node.end !== "number") return;
|
|
142
|
-
|
|
143
|
-
const objectText = code.slice(pathObj.node.start, pathObj.node.end);
|
|
144
|
-
const leading = leadingWhitespace(code, pathObj.node.start);
|
|
145
|
-
|
|
146
|
-
const otherProps = props.filter((p) => !numericProps.includes(p));
|
|
147
|
-
const otherTexts = otherProps.map((p) => code.slice(p.start, p.end));
|
|
148
|
-
|
|
149
|
-
const locatorsText = buildLocatorsArray(numericProps, code, leading + " ");
|
|
150
|
-
const pieces = [locatorsText, ...otherTexts];
|
|
151
|
-
const baseIndent = leading + " ";
|
|
152
|
-
const normalized = pieces.map((p) => (p.startsWith(baseIndent) ? p : baseIndent + p));
|
|
153
|
-
const newObjectText = "{\n" + normalized.join(",\n") + "\n" + leading + "}";
|
|
154
|
-
|
|
155
|
-
replacements.push({ start: pathObj.node.start, end: pathObj.node.end, text: newObjectText });
|
|
156
|
-
touched = true;
|
|
157
|
-
},
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
if (!touched) return code;
|
|
161
|
-
|
|
162
|
-
let out = code;
|
|
163
|
-
replacements
|
|
164
|
-
.sort((a, b) => b.start - a.start)
|
|
165
|
-
.forEach(({ start, end, text }) => {
|
|
166
|
-
out = out.slice(0, start) + text + out.slice(end);
|
|
167
|
-
});
|
|
168
|
-
return out;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function fixJson(content) {
|
|
172
|
-
let data;
|
|
173
|
-
try {
|
|
174
|
-
data = JSON.parse(content);
|
|
175
|
-
} catch {
|
|
176
|
-
return content;
|
|
177
|
-
}
|
|
178
|
-
let touched = false;
|
|
179
|
-
|
|
180
|
-
const visit = (node) => {
|
|
181
|
-
if (!node || typeof node !== "object") return;
|
|
182
|
-
if (Array.isArray(node)) return node.forEach(visit);
|
|
183
|
-
|
|
184
|
-
const keys = Object.keys(node);
|
|
185
|
-
const hasMeta = "element_name" in node || "element_key" in node;
|
|
186
|
-
const hasLoc = "locators" in node;
|
|
187
|
-
const nums = keys.filter(isNumericKey);
|
|
188
|
-
|
|
189
|
-
if (hasMeta && !hasLoc && nums.length) {
|
|
190
|
-
node.locators = nums.sort((a, b) => Number(a) - Number(b)).map((k) => node[k]);
|
|
191
|
-
nums.forEach((k) => delete node[k]);
|
|
192
|
-
touched = true;
|
|
193
|
-
}
|
|
194
|
-
Object.values(node).forEach(visit);
|
|
195
|
-
};
|
|
196
|
-
|
|
197
|
-
visit(data);
|
|
198
|
-
return touched ? JSON.stringify(data, null, 2) + "\n" : content;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
async function cloneWorkspace(root) {
|
|
202
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "codemod-clone-"));
|
|
203
|
-
const cloneRoot = path.join(tmpDir, path.basename(root));
|
|
204
|
-
|
|
205
|
-
if (fsPromises.cp) {
|
|
206
|
-
await fsPromises.cp(root, cloneRoot, { recursive: true, force: true });
|
|
207
|
-
} else {
|
|
208
|
-
const res = spawnSync("cp", ["-R", ".", cloneRoot], { cwd: root, stdio: "inherit" });
|
|
209
|
-
if (res.status !== 0) throw new Error("Failed to clone workspace");
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
const mapper = (orig) => path.join(cloneRoot, path.relative(root, orig));
|
|
213
|
-
return { cloneRoot, mapper };
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
async function initGitRepo(cloneRoot) {
|
|
217
|
-
// Skip if already a repo
|
|
218
|
-
if (fs.existsSync(path.join(cloneRoot, ".git"))) return;
|
|
219
|
-
const run = (args) => spawnSync("git", args, { cwd: cloneRoot, stdio: "inherit" });
|
|
220
|
-
run(["init"]);
|
|
221
|
-
run(["add", "."]);
|
|
222
|
-
run(["commit", "-m", "chore: baseline before codemod"]);
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
function makeDiff(originalPath, clonedPath, fixed) {
|
|
226
|
-
const tmpOutDir = fs.mkdtempSync(path.join(os.tmpdir(), "codemod-out-"));
|
|
227
|
-
const fixedPath = path.join(tmpOutDir, path.basename(clonedPath));
|
|
228
|
-
fs.writeFileSync(fixedPath, fixed, "utf8");
|
|
229
|
-
const diff = spawnSync("diff", ["-u", originalPath, fixedPath], { encoding: "utf8" });
|
|
230
|
-
return diff.stdout || diff.stderr || "";
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
async function readStdin() {
|
|
234
|
-
return await new Promise((resolve) => {
|
|
235
|
-
let d = "";
|
|
236
|
-
process.stdin.setEncoding("utf8");
|
|
237
|
-
process.stdin.on("data", (c) => (d += c));
|
|
238
|
-
process.stdin.on("end", () => resolve(d));
|
|
239
|
-
});
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
function leadingWhitespace(text, index) {
|
|
243
|
-
const lineStart = text.lastIndexOf("\n", index - 1) + 1;
|
|
244
|
-
const m = text.slice(lineStart, index).match(/^[ \t]*/);
|
|
245
|
-
return m ? m[0] : "";
|
|
246
|
-
}
|
|
File without changes
|
|
File without changes
|