qat-cli 0.3.4 → 0.3.5
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/README.md +1 -1
- package/dist/cli.js +250 -178
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +163 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +41 -1
- package/dist/index.d.ts +41 -1
- package/dist/index.js +163 -6
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -524,71 +524,6 @@ function validateConfig(config) {
|
|
|
524
524
|
}
|
|
525
525
|
return merged;
|
|
526
526
|
}
|
|
527
|
-
function generateConfigFile(overrides = {}) {
|
|
528
|
-
const config = validateConfig(overrides);
|
|
529
|
-
return `// @ts-check
|
|
530
|
-
/**
|
|
531
|
-
* QAT \u914D\u7F6E\u6587\u4EF6 - Quick Auto Testing
|
|
532
|
-
* \u4FEE\u6539\u540E\u65E0\u9700\u91CD\u542F\uFF0C\u4E0B\u6B21\u8FD0\u884C qat \u547D\u4EE4\u65F6\u81EA\u52A8\u751F\u6548
|
|
533
|
-
*
|
|
534
|
-
* AI \u6A21\u578B\u914D\u7F6E\u8BF7\u8FD0\u884C: qat change
|
|
535
|
-
* AI \u72B6\u6001\u67E5\u770B\u8BF7\u8FD0\u884C: qat status
|
|
536
|
-
*/
|
|
537
|
-
import { defineConfig } from 'qat-cli';
|
|
538
|
-
|
|
539
|
-
export default defineConfig({
|
|
540
|
-
project: {
|
|
541
|
-
framework: '${config.project.framework}',
|
|
542
|
-
vite: ${config.project.vite ?? true},
|
|
543
|
-
srcDir: '${config.project.srcDir}',
|
|
544
|
-
},
|
|
545
|
-
vitest: {
|
|
546
|
-
enabled: ${config.vitest.enabled},
|
|
547
|
-
coverage: ${config.vitest.coverage},
|
|
548
|
-
globals: ${config.vitest.globals},
|
|
549
|
-
environment: '${config.vitest.environment}',
|
|
550
|
-
},
|
|
551
|
-
playwright: {
|
|
552
|
-
enabled: ${config.playwright.enabled},
|
|
553
|
-
browsers: [${config.playwright.browsers.map((b) => `'${b}'`).join(", ")}],
|
|
554
|
-
baseURL: '${config.playwright.baseURL}',
|
|
555
|
-
screenshot: '${config.playwright.screenshot}',
|
|
556
|
-
},
|
|
557
|
-
visual: {
|
|
558
|
-
enabled: ${config.visual.enabled},
|
|
559
|
-
threshold: ${config.visual.threshold},
|
|
560
|
-
baselineDir: '${config.visual.baselineDir}',
|
|
561
|
-
diffDir: '${config.visual.diffDir}',
|
|
562
|
-
},
|
|
563
|
-
lighthouse: {
|
|
564
|
-
enabled: ${config.lighthouse.enabled},
|
|
565
|
-
urls: [${config.lighthouse.urls.map((u) => `'${u}'`).join(", ")}],
|
|
566
|
-
runs: ${config.lighthouse.runs},
|
|
567
|
-
thresholds: {${Object.entries(config.lighthouse.thresholds).map(([k, v]) => `
|
|
568
|
-
${k}: ${v},`).join("")}
|
|
569
|
-
},
|
|
570
|
-
},
|
|
571
|
-
mock: {
|
|
572
|
-
enabled: ${config.mock.enabled},
|
|
573
|
-
port: ${config.mock.port},
|
|
574
|
-
routesDir: '${config.mock.routesDir}',
|
|
575
|
-
},
|
|
576
|
-
report: {
|
|
577
|
-
outputDir: '${config.report.outputDir}',
|
|
578
|
-
open: ${config.report.open},
|
|
579
|
-
},
|
|
580
|
-
});
|
|
581
|
-
`;
|
|
582
|
-
}
|
|
583
|
-
async function writeConfigFile(cwd, overrides = {}, force = false) {
|
|
584
|
-
const configPath = path2.join(cwd, "qat.config.js");
|
|
585
|
-
if (fs2.existsSync(configPath) && !force) {
|
|
586
|
-
throw new Error(`\u914D\u7F6E\u6587\u4EF6\u5DF2\u5B58\u5728: ${configPath}\uFF0C\u4F7F\u7528 --force \u8986\u76D6`);
|
|
587
|
-
}
|
|
588
|
-
const content = generateConfigFile(overrides);
|
|
589
|
-
fs2.writeFileSync(configPath, content, "utf-8");
|
|
590
|
-
return configPath;
|
|
591
|
-
}
|
|
592
527
|
function isFileNotFoundError(error) {
|
|
593
528
|
if (error instanceof Error) {
|
|
594
529
|
return error.message.includes("Cannot find") || error.message.includes("ENOENT") || error.message.includes("\u65E0\u6CD5\u52A0\u8F7D\u914D\u7F6E\u6587\u4EF6");
|
|
@@ -1088,9 +1023,15 @@ ${errorDetails}`;
|
|
|
1088
1023
|
/**
|
|
1089
1024
|
* 压缩源码:保留签名和关键逻辑,剔除注释、空行、样式块
|
|
1090
1025
|
* 目标:在保留准确性的前提下减少 token 消耗
|
|
1026
|
+
* @param code 源码内容
|
|
1027
|
+
* @param maxLength 最大长度
|
|
1028
|
+
* @param importPathRewrites import 路径重写映射(原路径→正确路径)
|
|
1091
1029
|
*/
|
|
1092
|
-
compressSourceCode(code, maxLength = 3e3) {
|
|
1030
|
+
compressSourceCode(code, maxLength = 3e3, importPathRewrites) {
|
|
1093
1031
|
let compressed = code;
|
|
1032
|
+
if (importPathRewrites && importPathRewrites.size > 0) {
|
|
1033
|
+
compressed = this.rewriteImportPaths(compressed, importPathRewrites);
|
|
1034
|
+
}
|
|
1094
1035
|
compressed = compressed.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "");
|
|
1095
1036
|
compressed = compressed.replace(/<template[^>]*>([\s\S]*?)<\/template>/gi, (_match, content) => {
|
|
1096
1037
|
return `<template>${content.replace(/\s*(?:class|style)\s*=\s*["'][^"']*["']/gi, "")}</template>`;
|
|
@@ -1128,7 +1069,8 @@ ${errorDetails}`;
|
|
|
1128
1069
|
}
|
|
1129
1070
|
buildGenerateTestUserPrompt(req) {
|
|
1130
1071
|
const importPath = this.computeTestImportPath(req.type, req.target);
|
|
1131
|
-
let prompt = `\u4E3A${req.target}\u751F\u6210${req.type}\u6D4B\u8BD5\
|
|
1072
|
+
let prompt = `\u4E3A${req.target}\u751F\u6210${req.type}\u6D4B\u8BD5\u3002
|
|
1073
|
+
\u3010\u5F3A\u5236\u3011import\u88AB\u6D4B\u6A21\u5757\u5FC5\u987B\u7528: ${importPath}
|
|
1132
1074
|
`;
|
|
1133
1075
|
if (req.analysis) {
|
|
1134
1076
|
const parts = [];
|
|
@@ -1150,13 +1092,19 @@ ${errorDetails}`;
|
|
|
1150
1092
|
if (req.analysis.computed?.length) {
|
|
1151
1093
|
parts.push(`Computed:${req.analysis.computed.join(",")}`);
|
|
1152
1094
|
}
|
|
1095
|
+
if (req.analysis.importSignatures?.length) {
|
|
1096
|
+
parts.push(`\u4F9D\u8D56\u7B7E\u540D:${req.analysis.importSignatures.map(
|
|
1097
|
+
(imp) => `${imp.source}{${Object.entries(imp.signatures).map(([k, v]) => `${k}:${v}`).join(",")}}`
|
|
1098
|
+
).join(";")}`);
|
|
1099
|
+
}
|
|
1153
1100
|
prompt += parts.join("\n") + "\n";
|
|
1154
1101
|
}
|
|
1155
1102
|
if (req.context) {
|
|
1103
|
+
const importPathRewrites = this.buildImportPathRewrites(req.type, req.target);
|
|
1156
1104
|
prompt += `
|
|
1157
1105
|
\u6E90\u7801:
|
|
1158
1106
|
\`\`\`
|
|
1159
|
-
${this.compressSourceCode(req.context)}
|
|
1107
|
+
${this.compressSourceCode(req.context, 3e3, importPathRewrites)}
|
|
1160
1108
|
\`\`\`
|
|
1161
1109
|
`;
|
|
1162
1110
|
}
|
|
@@ -1191,6 +1139,56 @@ ${this.compressSourceCode(req.context)}
|
|
|
1191
1139
|
}
|
|
1192
1140
|
return `${prefix}${cleanPath}`;
|
|
1193
1141
|
}
|
|
1142
|
+
/**
|
|
1143
|
+
* 构建 import 路径重写映射
|
|
1144
|
+
* 将源码中可能引用自身的各种写法,映射到测试文件中正确的导入路径
|
|
1145
|
+
*/
|
|
1146
|
+
buildImportPathRewrites(testType, targetPath) {
|
|
1147
|
+
const rewrites = /* @__PURE__ */ new Map();
|
|
1148
|
+
if (!targetPath) return rewrites;
|
|
1149
|
+
const correctImportPath = this.computeTestImportPath(testType, targetPath);
|
|
1150
|
+
const variations = /* @__PURE__ */ new Set();
|
|
1151
|
+
variations.add(targetPath);
|
|
1152
|
+
variations.add(targetPath.replace(/^\.\//, ""));
|
|
1153
|
+
const withoutExt = targetPath.replace(/\.(ts|js|tsx|jsx)$/, "");
|
|
1154
|
+
variations.add(withoutExt);
|
|
1155
|
+
const srcDirMatch = targetPath.match(/^(?:\.\/)?(src\/.+)$/);
|
|
1156
|
+
if (srcDirMatch) {
|
|
1157
|
+
variations.add(`@/${srcDirMatch[1]}`);
|
|
1158
|
+
variations.add(`@/${srcDirMatch[1].replace(/\.(ts|js|tsx|jsx)$/, "")}`);
|
|
1159
|
+
}
|
|
1160
|
+
const pathParts = targetPath.replace(/^\.\//, "").split("/");
|
|
1161
|
+
const fileName = pathParts[pathParts.length - 1];
|
|
1162
|
+
const fileNameNoExt = fileName.replace(/\.(ts|js|tsx|jsx|vue)$/, "");
|
|
1163
|
+
variations.add(`./${fileName}`);
|
|
1164
|
+
variations.add(`./${fileNameNoExt}`);
|
|
1165
|
+
for (let i = 1; i < pathParts.length; i++) {
|
|
1166
|
+
const relPath = "../".repeat(i) + pathParts.slice(i).join("/");
|
|
1167
|
+
variations.add(relPath);
|
|
1168
|
+
variations.add(relPath.replace(/\.(ts|js|tsx|jsx)$/, ""));
|
|
1169
|
+
}
|
|
1170
|
+
for (const variant of variations) {
|
|
1171
|
+
if (variant && variant !== correctImportPath) {
|
|
1172
|
+
rewrites.set(variant, correctImportPath);
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
return rewrites;
|
|
1176
|
+
}
|
|
1177
|
+
/**
|
|
1178
|
+
* 重写源码中的 import 路径
|
|
1179
|
+
* 将 from 'oldPath' / from "oldPath" 替换为正确的路径
|
|
1180
|
+
*/
|
|
1181
|
+
rewriteImportPaths(code, rewrites) {
|
|
1182
|
+
let result = code;
|
|
1183
|
+
for (const [oldPath, newPath] of rewrites) {
|
|
1184
|
+
const escaped = oldPath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1185
|
+
const singleQuoteRegex = new RegExp(`(from\\s+')${escaped}(')`, "g");
|
|
1186
|
+
const doubleQuoteRegex = new RegExp(`(from\\s+")${escaped}(")`, "g");
|
|
1187
|
+
result = result.replace(singleQuoteRegex, `$1${newPath}$2`);
|
|
1188
|
+
result = result.replace(doubleQuoteRegex, `$1${newPath}$2`);
|
|
1189
|
+
}
|
|
1190
|
+
return result;
|
|
1191
|
+
}
|
|
1194
1192
|
parseGenerateTestResponse(content) {
|
|
1195
1193
|
const codeBlockMatch = content.match(/```(?:typescript|ts|javascript|js)?\s*\n([\s\S]*?)```/);
|
|
1196
1194
|
const code = codeBlockMatch ? codeBlockMatch[1].trim() : content.replace(/^(?:```[\s\S]*?\n)?/, "").replace(/\n?```$/, "").trim();
|
|
@@ -1213,12 +1211,14 @@ SUGGESTIONS:- \u5EFA\u8BAE(\u6BCF\u884C\u4E00\u4E2A)`;
|
|
|
1213
1211
|
}
|
|
1214
1212
|
buildReviewTestUserPrompt(req) {
|
|
1215
1213
|
const importPath = this.computeTestImportPath(req.testType, req.target);
|
|
1216
|
-
let prompt = `\u5BA1\u67E5\u6D4B\u8BD5\u4EE3\u7801\u3002\u88AB\u6D4B:${req.target} \u7C7B\u578B:${req.testType}
|
|
1214
|
+
let prompt = `\u5BA1\u67E5\u6D4B\u8BD5\u4EE3\u7801\u3002\u88AB\u6D4B:${req.target} \u7C7B\u578B:${req.testType}
|
|
1215
|
+
\u3010\u5F3A\u5236\u3011import\u88AB\u6D4B\u6A21\u5757\u5FC5\u987B\u7528: ${importPath}
|
|
1217
1216
|
`;
|
|
1217
|
+
const importPathRewrites = this.buildImportPathRewrites(req.testType, req.target);
|
|
1218
1218
|
prompt += `
|
|
1219
1219
|
\u6E90\u7801:
|
|
1220
1220
|
\`\`\`
|
|
1221
|
-
${this.compressSourceCode(req.sourceCode, 2e3)}
|
|
1221
|
+
${this.compressSourceCode(req.sourceCode, 2e3, importPathRewrites)}
|
|
1222
1222
|
\`\`\`
|
|
1223
1223
|
`;
|
|
1224
1224
|
prompt += `
|
|
@@ -1237,6 +1237,11 @@ ${req.testCode}
|
|
|
1237
1237
|
if (req.analysis.emits?.length) {
|
|
1238
1238
|
parts.push(`Emits:${req.analysis.emits.map((e) => `${e.name}(${e.params?.join(",") || ""})`).join(",")}`);
|
|
1239
1239
|
}
|
|
1240
|
+
if (req.analysis.importSignatures?.length) {
|
|
1241
|
+
parts.push(`\u4F9D\u8D56:${req.analysis.importSignatures.map(
|
|
1242
|
+
(imp) => `${imp.source}{${Object.entries(imp.signatures).map(([k, v]) => `${k}:${v}`).join(",")}}`
|
|
1243
|
+
).join(";")}`);
|
|
1244
|
+
}
|
|
1240
1245
|
if (parts.length > 0) {
|
|
1241
1246
|
prompt += `
|
|
1242
1247
|
\u5206\u6790:${parts.join("|")}`;
|
|
@@ -1428,15 +1433,18 @@ import path4 from "path";
|
|
|
1428
1433
|
function analyzeFile(filePath) {
|
|
1429
1434
|
const absolutePath = path4.resolve(process.cwd(), filePath);
|
|
1430
1435
|
if (!fs4.existsSync(absolutePath)) {
|
|
1431
|
-
return { filePath, exports: [], apiCalls: [] };
|
|
1436
|
+
return { filePath, exports: [], imports: [], importSignatures: [], apiCalls: [] };
|
|
1432
1437
|
}
|
|
1433
1438
|
const content = fs4.readFileSync(absolutePath, "utf-8");
|
|
1434
1439
|
const ext = path4.extname(filePath);
|
|
1435
1440
|
const result = {
|
|
1436
1441
|
filePath,
|
|
1437
1442
|
exports: extractExports(content, ext),
|
|
1443
|
+
imports: extractImports(content),
|
|
1444
|
+
importSignatures: [],
|
|
1438
1445
|
apiCalls: extractAPICalls(content, filePath)
|
|
1439
1446
|
};
|
|
1447
|
+
result.importSignatures = analyzeImportSignatures(result.imports, path4.dirname(absolutePath));
|
|
1440
1448
|
if (ext === ".vue") {
|
|
1441
1449
|
result.vueAnalysis = analyzeVueComponent(content, path4.basename(filePath, ".vue"));
|
|
1442
1450
|
const scriptMatch = content.match(/<script[^>]*>([\s\S]*?)<\/script>/);
|
|
@@ -1856,6 +1864,90 @@ function generateMockRoutesFromAPICalls(apiCalls) {
|
|
|
1856
1864
|
}
|
|
1857
1865
|
return routes;
|
|
1858
1866
|
}
|
|
1867
|
+
function extractImports(content) {
|
|
1868
|
+
const imports = [];
|
|
1869
|
+
const namedImportRegex = /import\s*\{([^}]+)\}\s*from\s*['"]([^'"]+)['"]/g;
|
|
1870
|
+
let match;
|
|
1871
|
+
while ((match = namedImportRegex.exec(content)) !== null) {
|
|
1872
|
+
const names = match[1].split(",").map((n) => {
|
|
1873
|
+
const parts = n.trim().split(/\s+as\s+/);
|
|
1874
|
+
return parts[0].trim();
|
|
1875
|
+
}).filter(Boolean);
|
|
1876
|
+
imports.push({ names, source: match[2], isDefault: false, isNamespace: false });
|
|
1877
|
+
}
|
|
1878
|
+
const defaultImportRegex = /import\s+(\w+)\s+from\s*['"]([^'"]+)['"]/g;
|
|
1879
|
+
while ((match = defaultImportRegex.exec(content)) !== null) {
|
|
1880
|
+
const fullLine = content.slice(Math.max(0, match.index - 5), match.index + match[0].length);
|
|
1881
|
+
if (fullLine.includes("{")) continue;
|
|
1882
|
+
imports.push({ names: [match[1]], source: match[2], isDefault: true, isNamespace: false });
|
|
1883
|
+
}
|
|
1884
|
+
const namespaceImportRegex = /import\s*\*\s*as\s+(\w+)\s*from\s*['"]([^'"]+)['"]/g;
|
|
1885
|
+
while ((match = namespaceImportRegex.exec(content)) !== null) {
|
|
1886
|
+
imports.push({ names: [match[1]], source: match[2], isDefault: false, isNamespace: true });
|
|
1887
|
+
}
|
|
1888
|
+
return imports;
|
|
1889
|
+
}
|
|
1890
|
+
function analyzeImportSignatures(imports, baseDir) {
|
|
1891
|
+
const signatures = [];
|
|
1892
|
+
for (const imp of imports) {
|
|
1893
|
+
if (!imp.source.startsWith(".") && !imp.source.startsWith("@/") && !imp.source.startsWith("~")) {
|
|
1894
|
+
continue;
|
|
1895
|
+
}
|
|
1896
|
+
const resolvedPath = resolveImportPath(imp.source, baseDir);
|
|
1897
|
+
if (!resolvedPath || !fs4.existsSync(resolvedPath)) continue;
|
|
1898
|
+
try {
|
|
1899
|
+
const content = fs4.readFileSync(resolvedPath, "utf-8");
|
|
1900
|
+
const ext = path4.extname(resolvedPath);
|
|
1901
|
+
const exports = extractExports(content, ext);
|
|
1902
|
+
const sigMap = {};
|
|
1903
|
+
for (const name of imp.names) {
|
|
1904
|
+
const exp = exports.find((e) => e.name === name);
|
|
1905
|
+
if (exp) {
|
|
1906
|
+
const params = exp.params.length > 0 ? `(${exp.params.join(", ")})` : "";
|
|
1907
|
+
const ret = exp.returnType ? `: ${exp.returnType}` : "";
|
|
1908
|
+
const async = exp.isAsync ? "async " : "";
|
|
1909
|
+
sigMap[name] = `${async}${exp.kind}${params}${ret}`;
|
|
1910
|
+
} else if (imp.isDefault) {
|
|
1911
|
+
const defaultExp = exports.find((e) => e.kind === "default");
|
|
1912
|
+
if (defaultExp) {
|
|
1913
|
+
sigMap[name] = `default[${defaultExp.name || "anonymous"}]`;
|
|
1914
|
+
}
|
|
1915
|
+
} else if (imp.isNamespace) {
|
|
1916
|
+
sigMap[name] = `{${exports.map((e) => e.name).join(", ")}}`;
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1919
|
+
if (ext === ".vue") {
|
|
1920
|
+
const vueAnalysis = analyzeVueComponent(content, path4.basename(resolvedPath, ".vue"));
|
|
1921
|
+
if (vueAnalysis.props.length > 0) {
|
|
1922
|
+
sigMap["__props__"] = vueAnalysis.props.map((p) => `${p.name}:${p.type}${p.required ? "!" : "?"}`).join(",");
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
if (Object.keys(sigMap).length > 0) {
|
|
1926
|
+
signatures.push({ source: imp.source, signatures: sigMap });
|
|
1927
|
+
}
|
|
1928
|
+
} catch {
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
return signatures;
|
|
1932
|
+
}
|
|
1933
|
+
function resolveImportPath(importSource, baseDir) {
|
|
1934
|
+
let resolved;
|
|
1935
|
+
if (importSource.startsWith("@/") || importSource.startsWith("~")) {
|
|
1936
|
+
const relativePath = importSource.replace(/^[@~]\//, "src/");
|
|
1937
|
+
resolved = path4.resolve(process.cwd(), relativePath);
|
|
1938
|
+
} else if (importSource.startsWith(".")) {
|
|
1939
|
+
resolved = path4.resolve(baseDir, importSource);
|
|
1940
|
+
} else {
|
|
1941
|
+
return null;
|
|
1942
|
+
}
|
|
1943
|
+
const extensions = [".ts", ".tsx", ".js", ".jsx", ".vue", "/index.ts", "/index.js"];
|
|
1944
|
+
if (fs4.existsSync(resolved)) return resolved;
|
|
1945
|
+
for (const ext of extensions) {
|
|
1946
|
+
const withExt = resolved + ext;
|
|
1947
|
+
if (fs4.existsSync(withExt)) return withExt;
|
|
1948
|
+
}
|
|
1949
|
+
return null;
|
|
1950
|
+
}
|
|
1859
1951
|
|
|
1860
1952
|
// src/services/global-config.ts
|
|
1861
1953
|
import fs5 from "fs";
|
|
@@ -1960,41 +2052,13 @@ async function executeInit(options) {
|
|
|
1960
2052
|
}
|
|
1961
2053
|
}
|
|
1962
2054
|
const config = buildProjectConfig(projectInfo);
|
|
1963
|
-
let configPath;
|
|
1964
2055
|
const existingConfigPath = path6.join(process.cwd(), "qat.config.js");
|
|
1965
2056
|
const existingTsPath = path6.join(process.cwd(), "qat.config.ts");
|
|
1966
2057
|
const configExists = fs6.existsSync(existingConfigPath) || fs6.existsSync(existingTsPath);
|
|
1967
|
-
if (configExists
|
|
1968
|
-
|
|
1969
|
-
{
|
|
1970
|
-
type: "confirm",
|
|
1971
|
-
name: "overwrite",
|
|
1972
|
-
message: "\u914D\u7F6E\u6587\u4EF6 qat.config.js \u5DF2\u5B58\u5728\uFF0C\u662F\u5426\u8986\u76D6\uFF1F",
|
|
1973
|
-
default: true
|
|
1974
|
-
}
|
|
1975
|
-
]);
|
|
1976
|
-
if (!overwrite) {
|
|
1977
|
-
console.log(chalk3.gray(" \u4FDD\u7559\u73B0\u6709\u914D\u7F6E\u6587\u4EF6\uFF0C\u7EE7\u7EED\u540E\u7EED\u6B65\u9AA4..."));
|
|
1978
|
-
configPath = existingConfigPath;
|
|
1979
|
-
} else {
|
|
1980
|
-
const fileSpinner = ora("\u6B63\u5728\u8986\u76D6\u914D\u7F6E\u6587\u4EF6...").start();
|
|
1981
|
-
try {
|
|
1982
|
-
configPath = await writeConfigFile(process.cwd(), config, true);
|
|
1983
|
-
fileSpinner.succeed("\u914D\u7F6E\u6587\u4EF6\u5DF2\u8986\u76D6");
|
|
1984
|
-
} catch (error) {
|
|
1985
|
-
fileSpinner.fail("\u914D\u7F6E\u6587\u4EF6\u8986\u76D6\u5931\u8D25");
|
|
1986
|
-
throw error;
|
|
1987
|
-
}
|
|
1988
|
-
}
|
|
2058
|
+
if (!configExists) {
|
|
2059
|
+
console.log(chalk3.gray(" \u672A\u627E\u5230\u914D\u7F6E\u6587\u4EF6\uFF0C\u4F7F\u7528\u9ED8\u8BA4\u914D\u7F6E\uFF08\u9700\u8981\u81EA\u5B9A\u4E49\u65F6\u53EF\u521B\u5EFA qat.config.js\uFF09"));
|
|
1989
2060
|
} else {
|
|
1990
|
-
|
|
1991
|
-
try {
|
|
1992
|
-
configPath = await writeConfigFile(process.cwd(), config, options.force);
|
|
1993
|
-
fileSpinner.succeed("\u914D\u7F6E\u6587\u4EF6\u5DF2\u751F\u6210");
|
|
1994
|
-
} catch (error) {
|
|
1995
|
-
fileSpinner.fail("\u914D\u7F6E\u6587\u4EF6\u751F\u6210\u5931\u8D25");
|
|
1996
|
-
throw error;
|
|
1997
|
-
}
|
|
2061
|
+
console.log(chalk3.green(` \u2713 \u5DF2\u6709\u914D\u7F6E\u6587\u4EF6: ${path6.basename(fs6.existsSync(existingConfigPath) ? existingConfigPath : existingTsPath)}`));
|
|
1998
2062
|
}
|
|
1999
2063
|
const dirSpinner = ora("\u6B63\u5728\u521B\u5EFA\u6D4B\u8BD5\u76EE\u5F55...").start();
|
|
2000
2064
|
const createdDirs = createTestDirectories(config);
|
|
@@ -2023,7 +2087,7 @@ async function executeInit(options) {
|
|
|
2023
2087
|
console.log(chalk3.cyan(`
|
|
2024
2088
|
\u53D1\u73B0 ${totalFiles} \u4E2A\u53EF\u6D4B\u8BD5\u6587\u4EF6 (${components.length} \u7EC4\u4EF6, ${utilities.length} \u5DE5\u5177/\u670D\u52A1)`));
|
|
2025
2089
|
}
|
|
2026
|
-
displayResult(
|
|
2090
|
+
displayResult(createdDirs, totalFiles, projectInfo);
|
|
2027
2091
|
}
|
|
2028
2092
|
async function promptAIConfig() {
|
|
2029
2093
|
const answers = await inquirer.prompt([
|
|
@@ -2160,12 +2224,8 @@ function createTestDirectories(config) {
|
|
|
2160
2224
|
}
|
|
2161
2225
|
return dirs;
|
|
2162
2226
|
}
|
|
2163
|
-
function displayResult(
|
|
2164
|
-
const relativeConfig = path6.relative(process.cwd(), configPath);
|
|
2227
|
+
function displayResult(createdDirs, totalTestableFiles, projectInfo) {
|
|
2165
2228
|
console.log(chalk3.green("\n \u2713 \u9879\u76EE\u521D\u59CB\u5316\u5B8C\u6210!\n"));
|
|
2166
|
-
console.log(chalk3.white(" \u5DF2\u751F\u6210\u914D\u7F6E:"));
|
|
2167
|
-
console.log(chalk3.gray(` ${relativeConfig}`));
|
|
2168
|
-
console.log();
|
|
2169
2229
|
if (createdDirs.length > 0) {
|
|
2170
2230
|
console.log(chalk3.white(" \u5DF2\u521B\u5EFA\u76EE\u5F55:"));
|
|
2171
2231
|
for (const dir of createdDirs) {
|
|
@@ -2173,6 +2233,9 @@ function displayResult(configPath, createdDirs, totalTestableFiles, projectInfo)
|
|
|
2173
2233
|
}
|
|
2174
2234
|
console.log();
|
|
2175
2235
|
}
|
|
2236
|
+
console.log(chalk3.white(" \u914D\u7F6E:"));
|
|
2237
|
+
console.log(chalk3.gray(" \u9ED8\u8BA4\u914D\u7F6E\u5DF2\u5185\u7F6E\uFF0C\u9700\u8981\u81EA\u5B9A\u4E49\u65F6\u521B\u5EFA qat.config.js"));
|
|
2238
|
+
console.log();
|
|
2176
2239
|
console.log(chalk3.cyan(" \u4E0B\u4E00\u6B65:"));
|
|
2177
2240
|
if (totalTestableFiles > 0) {
|
|
2178
2241
|
console.log(chalk3.gray(` 1. qat create \u9009\u62E9\u6587\u4EF6\u5E76\u751F\u6210\u6D4B\u8BD5\u7528\u4F8B (\u53D1\u73B0 ${totalTestableFiles} \u4E2A\u53EF\u6D4B\u8BD5\u6587\u4EF6)`));
|
|
@@ -2193,7 +2256,7 @@ import chalk5 from "chalk";
|
|
|
2193
2256
|
import inquirer2 from "inquirer";
|
|
2194
2257
|
import ora3 from "ora";
|
|
2195
2258
|
import fs8 from "fs";
|
|
2196
|
-
import
|
|
2259
|
+
import path9 from "path";
|
|
2197
2260
|
|
|
2198
2261
|
// src/services/template.ts
|
|
2199
2262
|
import fs7 from "fs";
|
|
@@ -2242,7 +2305,7 @@ Handlebars.registerHelper("propTestValue", (prop) => {
|
|
|
2242
2305
|
};
|
|
2243
2306
|
return map[prop.type] || "'test-value'";
|
|
2244
2307
|
});
|
|
2245
|
-
function
|
|
2308
|
+
function resolveImportPath2(testType, targetPath) {
|
|
2246
2309
|
if (!targetPath) return targetPath;
|
|
2247
2310
|
const testDirMap = {
|
|
2248
2311
|
unit: "tests/unit",
|
|
@@ -2268,7 +2331,7 @@ function resolveImportPath(testType, targetPath) {
|
|
|
2268
2331
|
function renderTemplate(type, context) {
|
|
2269
2332
|
const templateContent = loadTemplate(type);
|
|
2270
2333
|
const template = Handlebars.compile(templateContent);
|
|
2271
|
-
const resolvedTarget =
|
|
2334
|
+
const resolvedTarget = resolveImportPath2(type, context.target);
|
|
2272
2335
|
const fullContext = {
|
|
2273
2336
|
vueVersion: 3,
|
|
2274
2337
|
typescript: true,
|
|
@@ -2652,10 +2715,11 @@ function toPascalCase(str) {
|
|
|
2652
2715
|
// src/services/test-reviewer.ts
|
|
2653
2716
|
import chalk4 from "chalk";
|
|
2654
2717
|
import ora2 from "ora";
|
|
2718
|
+
import path8 from "path";
|
|
2655
2719
|
var MAX_RETRIES = 3;
|
|
2656
2720
|
var REVIEW_THRESHOLD = 0.6;
|
|
2657
2721
|
async function generateWithReview(params) {
|
|
2658
|
-
const { testType, targetPath, sourceCode, analysis, aiConfig, framework, onAttempt } = params;
|
|
2722
|
+
const { testType, targetPath, sourceCode, analysis, aiConfig, framework, onAttempt, onProgress } = params;
|
|
2659
2723
|
const generatorProvider = createAIProvider(aiConfig);
|
|
2660
2724
|
const reviewerProvider = createAIProvider(aiConfig);
|
|
2661
2725
|
if (!generatorProvider.capabilities.generateTest) {
|
|
@@ -2667,9 +2731,12 @@ async function generateWithReview(params) {
|
|
|
2667
2731
|
let lastReview = null;
|
|
2668
2732
|
let approved = false;
|
|
2669
2733
|
let attempts = 0;
|
|
2734
|
+
const targetName = path8.basename(targetPath);
|
|
2670
2735
|
for (let i = 0; i < MAX_RETRIES; i++) {
|
|
2671
2736
|
attempts = i + 1;
|
|
2672
2737
|
onAttempt?.(attempts, MAX_RETRIES);
|
|
2738
|
+
const genLabel = attempts > 1 ? `\u27F3 \u91CD\u65B0\u751F\u6210 (${attempts}/${MAX_RETRIES})` : "\u751F\u6210\u6D4B\u8BD5\u4EE3\u7801";
|
|
2739
|
+
onProgress?.(`${genLabel} \u2190 ${targetName}`);
|
|
2673
2740
|
if (process.env.QAT_DEBUG === "true") {
|
|
2674
2741
|
console.error(chalk4.gray(`[DEBUG] \u2500\u2500\u2500 \u751F\u6210\u8005 AI \u7B2C${attempts}\u6B21\u5C1D\u8BD5 \u2500\u2500\u2500`));
|
|
2675
2742
|
}
|
|
@@ -2693,6 +2760,7 @@ ${lastReview.suggestions.map((s) => `- ${s}`).join("\n")}
|
|
|
2693
2760
|
currentCode = generateResponse.code;
|
|
2694
2761
|
currentDescription = generateResponse.description;
|
|
2695
2762
|
currentConfidence = generateResponse.confidence;
|
|
2763
|
+
onProgress?.(`\u5BA1\u8BA1\u5BA1\u67E5 \u2190 ${targetName}`);
|
|
2696
2764
|
if (process.env.QAT_DEBUG === "true") {
|
|
2697
2765
|
console.error(chalk4.gray(`[DEBUG] \u2500\u2500\u2500 \u5BA1\u8BA1\u5458 AI \u5BA1\u67E5 \u2500\u2500\u2500`));
|
|
2698
2766
|
}
|
|
@@ -2851,11 +2919,13 @@ async function executeCreate(options) {
|
|
|
2851
2919
|
const reviewReportEntries = [];
|
|
2852
2920
|
for (const testType of types) {
|
|
2853
2921
|
for (const testTarget of targets) {
|
|
2854
|
-
const spinner = ora3(`\u6B63\u5728\u751F\u6210 ${TEST_TYPE_LABELS[testType]} - ${
|
|
2922
|
+
const spinner = ora3(`\u6B63\u5728\u751F\u6210 ${TEST_TYPE_LABELS[testType]} - ${path9.basename(testTarget)}...`).start();
|
|
2855
2923
|
try {
|
|
2856
2924
|
let content;
|
|
2857
2925
|
if (useAI && testTarget) {
|
|
2858
|
-
const aiResult = await generateWithAI(testType, name, testTarget, config)
|
|
2926
|
+
const aiResult = await generateWithAI(testType, name, testTarget, config, (text) => {
|
|
2927
|
+
spinner.text = `${text}`;
|
|
2928
|
+
});
|
|
2859
2929
|
content = aiResult.code;
|
|
2860
2930
|
if (aiResult.reviewEntry) {
|
|
2861
2931
|
reviewReportEntries.push(aiResult.reviewEntry);
|
|
@@ -2896,9 +2966,9 @@ async function executeCreate(options) {
|
|
|
2896
2966
|
});
|
|
2897
2967
|
}
|
|
2898
2968
|
const outputDir = getTestOutputDir(testType);
|
|
2899
|
-
const filePath =
|
|
2969
|
+
const filePath = path9.join(outputDir, `${name}.${testType === "e2e" ? "spec" : "test"}.ts`);
|
|
2900
2970
|
if (fs8.existsSync(filePath)) {
|
|
2901
|
-
spinner.warn(`\u6D4B\u8BD5\u6587\u4EF6\u5DF2\u5B58\u5728: ${
|
|
2971
|
+
spinner.warn(`\u6D4B\u8BD5\u6587\u4EF6\u5DF2\u5B58\u5728: ${path9.relative(process.cwd(), filePath)}`);
|
|
2902
2972
|
skippedCount++;
|
|
2903
2973
|
continue;
|
|
2904
2974
|
}
|
|
@@ -2906,10 +2976,10 @@ async function executeCreate(options) {
|
|
|
2906
2976
|
fs8.mkdirSync(outputDir, { recursive: true });
|
|
2907
2977
|
}
|
|
2908
2978
|
fs8.writeFileSync(filePath, content, "utf-8");
|
|
2909
|
-
spinner.succeed(`${TEST_TYPE_LABELS[testType]} - ${
|
|
2979
|
+
spinner.succeed(`${TEST_TYPE_LABELS[testType]} - ${path9.basename(testTarget)}`);
|
|
2910
2980
|
createdFiles.push({ type: testType, filePath });
|
|
2911
2981
|
} catch (error) {
|
|
2912
|
-
spinner.fail(`${TEST_TYPE_LABELS[testType]} - ${
|
|
2982
|
+
spinner.fail(`${TEST_TYPE_LABELS[testType]} - ${path9.basename(testTarget)} \u521B\u5EFA\u5931\u8D25`);
|
|
2913
2983
|
console.log(chalk5.gray(` ${error instanceof Error ? error.message : String(error)}`));
|
|
2914
2984
|
}
|
|
2915
2985
|
}
|
|
@@ -2976,22 +3046,22 @@ async function selectTargets(types, projectInfo, srcDir) {
|
|
|
2976
3046
|
}
|
|
2977
3047
|
function generateDefaultName(type, target) {
|
|
2978
3048
|
if (!target) return `${type}-test`;
|
|
2979
|
-
const basename =
|
|
3049
|
+
const basename = path9.basename(target, path9.extname(target));
|
|
2980
3050
|
const cleaned = basename.replace(/[^a-zA-Z0-9]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
2981
3051
|
return cleaned || `${type}-test`;
|
|
2982
3052
|
}
|
|
2983
|
-
async function generateWithAI(type, name, target, config) {
|
|
3053
|
+
async function generateWithAI(type, name, target, config, onProgress) {
|
|
2984
3054
|
const provider = getAIProvider(config.ai);
|
|
2985
3055
|
if (!provider.capabilities.generateTest) {
|
|
2986
3056
|
throw new Error("\u5F53\u524D AI Provider \u4E0D\u652F\u6301\u6D4B\u8BD5\u751F\u6210");
|
|
2987
3057
|
}
|
|
2988
3058
|
let sourceCode = "";
|
|
2989
|
-
const fullPath =
|
|
3059
|
+
const fullPath = path9.resolve(process.cwd(), target);
|
|
2990
3060
|
if (fs8.existsSync(fullPath)) {
|
|
2991
3061
|
sourceCode = fs8.readFileSync(fullPath, "utf-8");
|
|
2992
3062
|
}
|
|
2993
3063
|
const analysis = analyzeFile(target);
|
|
2994
|
-
const analysisSummary = analysis.exports.length > 0 || analysis.vueAnalysis ? {
|
|
3064
|
+
const analysisSummary = analysis.exports.length > 0 || analysis.vueAnalysis || analysis.importSignatures.length > 0 ? {
|
|
2995
3065
|
exports: analysis.exports.map((e) => ({
|
|
2996
3066
|
name: e.name,
|
|
2997
3067
|
kind: e.kind,
|
|
@@ -3008,7 +3078,8 @@ async function generateWithAI(type, name, target, config) {
|
|
|
3008
3078
|
params: e.params
|
|
3009
3079
|
})),
|
|
3010
3080
|
methods: analysis.vueAnalysis?.methods,
|
|
3011
|
-
computed: analysis.vueAnalysis?.computed
|
|
3081
|
+
computed: analysis.vueAnalysis?.computed,
|
|
3082
|
+
importSignatures: analysis.importSignatures.length > 0 ? analysis.importSignatures : void 0
|
|
3012
3083
|
} : void 0;
|
|
3013
3084
|
const reviewResult = await generateWithReview({
|
|
3014
3085
|
testType: type,
|
|
@@ -3016,7 +3087,8 @@ async function generateWithAI(type, name, target, config) {
|
|
|
3016
3087
|
sourceCode,
|
|
3017
3088
|
analysis: analysisSummary,
|
|
3018
3089
|
aiConfig: config.ai,
|
|
3019
|
-
framework: "vue"
|
|
3090
|
+
framework: "vue",
|
|
3091
|
+
onProgress
|
|
3020
3092
|
});
|
|
3021
3093
|
const headerComment = [
|
|
3022
3094
|
`// AI Generated Test - ${name}`,
|
|
@@ -3060,7 +3132,7 @@ function displayCreateResult(createdFiles, skippedCount, usedAI) {
|
|
|
3060
3132
|
}
|
|
3061
3133
|
console.log();
|
|
3062
3134
|
for (const { type, filePath } of createdFiles) {
|
|
3063
|
-
const relativePath =
|
|
3135
|
+
const relativePath = path9.relative(process.cwd(), filePath);
|
|
3064
3136
|
console.log(chalk5.white(` ${chalk5.cyan(TEST_TYPE_LABELS[type])} ${chalk5.gray(relativePath)}`));
|
|
3065
3137
|
}
|
|
3066
3138
|
if (usedAI) {
|
|
@@ -3085,11 +3157,11 @@ import chalk6 from "chalk";
|
|
|
3085
3157
|
import inquirer3 from "inquirer";
|
|
3086
3158
|
import ora4 from "ora";
|
|
3087
3159
|
import fs11 from "fs";
|
|
3088
|
-
import
|
|
3160
|
+
import path13 from "path";
|
|
3089
3161
|
|
|
3090
3162
|
// src/runners/vitest-runner.ts
|
|
3091
3163
|
import { execFile } from "child_process";
|
|
3092
|
-
import
|
|
3164
|
+
import path10 from "path";
|
|
3093
3165
|
import fs9 from "fs";
|
|
3094
3166
|
import os2 from "os";
|
|
3095
3167
|
var isVerbose = () => process.env.QAT_VERBOSE === "true";
|
|
@@ -3171,7 +3243,7 @@ function buildVitestArgs(options) {
|
|
|
3171
3243
|
return args;
|
|
3172
3244
|
}
|
|
3173
3245
|
async function execVitest(args) {
|
|
3174
|
-
const tmpFile =
|
|
3246
|
+
const tmpFile = path10.join(os2.tmpdir(), `qat-vitest-result-${Date.now()}.json`);
|
|
3175
3247
|
const argsWithOutput = [...args, "--outputFile", tmpFile];
|
|
3176
3248
|
return new Promise((resolve, reject) => {
|
|
3177
3249
|
const npx = process.platform === "win32" ? "npx.cmd" : "npx";
|
|
@@ -3250,7 +3322,7 @@ function parseVitestJSON(jsonStr) {
|
|
|
3250
3322
|
for (const fileResult of data.testResults) {
|
|
3251
3323
|
const suiteTests = parseTestResults(fileResult);
|
|
3252
3324
|
suites.push({
|
|
3253
|
-
name:
|
|
3325
|
+
name: path10.basename(fileResult.name || "unknown"),
|
|
3254
3326
|
file: fileResult.name || "unknown",
|
|
3255
3327
|
type: "unit",
|
|
3256
3328
|
status: mapVitestStatus(fileResult.status),
|
|
@@ -3361,7 +3433,7 @@ function parseFromStdout(output) {
|
|
|
3361
3433
|
if (data.testResults && Array.isArray(data.testResults)) {
|
|
3362
3434
|
for (const fileResult of data.testResults) {
|
|
3363
3435
|
suites.push({
|
|
3364
|
-
name:
|
|
3436
|
+
name: path10.basename(fileResult.name || "unknown"),
|
|
3365
3437
|
file: fileResult.name || "unknown",
|
|
3366
3438
|
type: "unit",
|
|
3367
3439
|
status: mapVitestStatus(fileResult.status),
|
|
@@ -3397,7 +3469,7 @@ function parseVitestTextOutput(output, hasError) {
|
|
|
3397
3469
|
if (isPassed) totalPassed += testCount;
|
|
3398
3470
|
else totalFailed += testCount;
|
|
3399
3471
|
suites.push({
|
|
3400
|
-
name:
|
|
3472
|
+
name: path10.basename(file),
|
|
3401
3473
|
file,
|
|
3402
3474
|
type: "unit",
|
|
3403
3475
|
status: isPassed ? "passed" : "failed",
|
|
@@ -3498,7 +3570,7 @@ function extractCoverage(coverageMap) {
|
|
|
3498
3570
|
|
|
3499
3571
|
// src/runners/playwright-runner.ts
|
|
3500
3572
|
import { execFile as execFile2 } from "child_process";
|
|
3501
|
-
import
|
|
3573
|
+
import path11 from "path";
|
|
3502
3574
|
async function runPlaywright(options) {
|
|
3503
3575
|
const startTime = Date.now();
|
|
3504
3576
|
const args = buildPlaywrightArgs(options);
|
|
@@ -3698,7 +3770,7 @@ function parsePlaywrightTextOutput(output, hasError) {
|
|
|
3698
3770
|
let existingSuite = suites.find((s) => s.file === file);
|
|
3699
3771
|
if (!existingSuite) {
|
|
3700
3772
|
existingSuite = {
|
|
3701
|
-
name:
|
|
3773
|
+
name: path11.basename(file),
|
|
3702
3774
|
file,
|
|
3703
3775
|
type: "e2e",
|
|
3704
3776
|
status: "passed",
|
|
@@ -3933,7 +4005,7 @@ function calculateAverageMetrics(results) {
|
|
|
3933
4005
|
|
|
3934
4006
|
// src/services/reporter.ts
|
|
3935
4007
|
import fs10 from "fs";
|
|
3936
|
-
import
|
|
4008
|
+
import path12 from "path";
|
|
3937
4009
|
function aggregateResults(results) {
|
|
3938
4010
|
const summary = {
|
|
3939
4011
|
total: 0,
|
|
@@ -4129,13 +4201,13 @@ function generateMDReport(data) {
|
|
|
4129
4201
|
}
|
|
4130
4202
|
function writeReportToDisk(data, outputDir) {
|
|
4131
4203
|
const md = generateMDReport(data);
|
|
4132
|
-
const dir =
|
|
4204
|
+
const dir = path12.resolve(outputDir);
|
|
4133
4205
|
if (!fs10.existsSync(dir)) {
|
|
4134
4206
|
fs10.mkdirSync(dir, { recursive: true });
|
|
4135
4207
|
}
|
|
4136
|
-
const mdPath =
|
|
4208
|
+
const mdPath = path12.join(dir, "report.md");
|
|
4137
4209
|
fs10.writeFileSync(mdPath, md, "utf-8");
|
|
4138
|
-
const jsonPath =
|
|
4210
|
+
const jsonPath = path12.join(dir, "report.json");
|
|
4139
4211
|
fs10.writeFileSync(jsonPath, JSON.stringify(data, null, 2), "utf-8");
|
|
4140
4212
|
return mdPath;
|
|
4141
4213
|
}
|
|
@@ -4356,7 +4428,7 @@ async function executeRun(options) {
|
|
|
4356
4428
|
const reportData = aggregateResults(results);
|
|
4357
4429
|
const outputDir = config.report.outputDir || "qat-report";
|
|
4358
4430
|
const reportPath = writeReportToDisk(reportData, outputDir);
|
|
4359
|
-
const relativePath =
|
|
4431
|
+
const relativePath = path13.relative(process.cwd(), reportPath);
|
|
4360
4432
|
console.log(chalk6.gray(`
|
|
4361
4433
|
\u62A5\u544A\u5DF2\u751F\u6210: ${chalk6.cyan(relativePath)}`));
|
|
4362
4434
|
console.log();
|
|
@@ -4743,8 +4815,8 @@ async function checkDevServer(config) {
|
|
|
4743
4815
|
}
|
|
4744
4816
|
async function startDevServer(config) {
|
|
4745
4817
|
const { spawn } = await import("child_process");
|
|
4746
|
-
const hasPnpm = fs11.existsSync(
|
|
4747
|
-
const hasYarn = fs11.existsSync(
|
|
4818
|
+
const hasPnpm = fs11.existsSync(path13.join(process.cwd(), "pnpm-lock.yaml"));
|
|
4819
|
+
const hasYarn = fs11.existsSync(path13.join(process.cwd(), "yarn.lock"));
|
|
4748
4820
|
const pkgCmd = hasPnpm ? "pnpm" : hasYarn ? "yarn" : "npm";
|
|
4749
4821
|
console.log(chalk6.cyan(` \u6B63\u5728\u542F\u52A8 dev server (${pkgCmd} run dev) ...`));
|
|
4750
4822
|
const child = spawn(pkgCmd, ["run", "dev"], {
|
|
@@ -4773,13 +4845,13 @@ async function startDevServer(config) {
|
|
|
4773
4845
|
}
|
|
4774
4846
|
function saveRunResults(results) {
|
|
4775
4847
|
if (results.length === 0) return;
|
|
4776
|
-
const resultsPath =
|
|
4848
|
+
const resultsPath = path13.join(process.cwd(), RESULTS_DIR);
|
|
4777
4849
|
if (!fs11.existsSync(resultsPath)) {
|
|
4778
4850
|
fs11.mkdirSync(resultsPath, { recursive: true });
|
|
4779
4851
|
}
|
|
4780
4852
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
4781
4853
|
const fileName = `result-${timestamp}.json`;
|
|
4782
|
-
const filePath =
|
|
4854
|
+
const filePath = path13.join(resultsPath, fileName);
|
|
4783
4855
|
const data = {
|
|
4784
4856
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4785
4857
|
results
|
|
@@ -4789,13 +4861,13 @@ function saveRunResults(results) {
|
|
|
4789
4861
|
const files = fs11.readdirSync(resultsPath).filter((f) => f.startsWith("result-") && f.endsWith(".json")).sort();
|
|
4790
4862
|
while (files.length > 20) {
|
|
4791
4863
|
const oldest = files.shift();
|
|
4792
|
-
fs11.unlinkSync(
|
|
4864
|
+
fs11.unlinkSync(path13.join(resultsPath, oldest));
|
|
4793
4865
|
}
|
|
4794
4866
|
} catch {
|
|
4795
4867
|
}
|
|
4796
4868
|
}
|
|
4797
4869
|
function checkTestDependencies(types) {
|
|
4798
|
-
const pkgPath =
|
|
4870
|
+
const pkgPath = path13.join(process.cwd(), "package.json");
|
|
4799
4871
|
let allDeps = {};
|
|
4800
4872
|
if (fs11.existsSync(pkgPath)) {
|
|
4801
4873
|
try {
|
|
@@ -4821,8 +4893,8 @@ function checkTestDependencies(types) {
|
|
|
4821
4893
|
return missingDeps;
|
|
4822
4894
|
}
|
|
4823
4895
|
async function installTestDependencies(missingDeps) {
|
|
4824
|
-
const hasPnpm = fs11.existsSync(
|
|
4825
|
-
const hasYarn = fs11.existsSync(
|
|
4896
|
+
const hasPnpm = fs11.existsSync(path13.join(process.cwd(), "pnpm-lock.yaml"));
|
|
4897
|
+
const hasYarn = fs11.existsSync(path13.join(process.cwd(), "yarn.lock"));
|
|
4826
4898
|
const pkgManager = hasPnpm ? "pnpm" : hasYarn ? "yarn" : "npm";
|
|
4827
4899
|
const { execFile: execFile5 } = await import("child_process");
|
|
4828
4900
|
let allSuccess = true;
|
|
@@ -4990,7 +5062,7 @@ function showStatus() {
|
|
|
4990
5062
|
import chalk8 from "chalk";
|
|
4991
5063
|
import ora6 from "ora";
|
|
4992
5064
|
import fs12 from "fs";
|
|
4993
|
-
import
|
|
5065
|
+
import path14 from "path";
|
|
4994
5066
|
var RESULTS_DIR2 = ".qat-results";
|
|
4995
5067
|
function registerReportCommand(program2) {
|
|
4996
5068
|
program2.command("report").description("\u751F\u6210\u6D4B\u8BD5\u62A5\u544A - \u805A\u5408\u6240\u6709\u6D4B\u8BD5\u7ED3\u679C\u5E76\u8F93\u51FAHTML").option("-o, --output <dir>", "\u62A5\u544A\u8F93\u51FA\u76EE\u5F55").option("--open", "\u751F\u6210\u540E\u81EA\u52A8\u6253\u5F00\u62A5\u544A", false).action(async (options) => {
|
|
@@ -5026,13 +5098,13 @@ async function executeReport(options) {
|
|
|
5026
5098
|
}
|
|
5027
5099
|
function collectResults() {
|
|
5028
5100
|
const results = [];
|
|
5029
|
-
const resultsPath =
|
|
5101
|
+
const resultsPath = path14.join(process.cwd(), RESULTS_DIR2);
|
|
5030
5102
|
if (!fs12.existsSync(resultsPath)) {
|
|
5031
5103
|
return results;
|
|
5032
5104
|
}
|
|
5033
5105
|
const files = fs12.readdirSync(resultsPath).filter((f) => f.endsWith(".json")).sort().reverse();
|
|
5034
5106
|
if (files.length > 0) {
|
|
5035
|
-
const latestFile =
|
|
5107
|
+
const latestFile = path14.join(resultsPath, files[0]);
|
|
5036
5108
|
try {
|
|
5037
5109
|
const data = JSON.parse(fs12.readFileSync(latestFile, "utf-8"));
|
|
5038
5110
|
if (Array.isArray(data)) {
|
|
@@ -5046,22 +5118,22 @@ function collectResults() {
|
|
|
5046
5118
|
return results;
|
|
5047
5119
|
}
|
|
5048
5120
|
function saveResultToHistory(reportData) {
|
|
5049
|
-
const resultsPath =
|
|
5121
|
+
const resultsPath = path14.join(process.cwd(), RESULTS_DIR2);
|
|
5050
5122
|
if (!fs12.existsSync(resultsPath)) {
|
|
5051
5123
|
fs12.mkdirSync(resultsPath, { recursive: true });
|
|
5052
5124
|
}
|
|
5053
5125
|
const timestamp = new Date(reportData.timestamp).toISOString().replace(/[:.]/g, "-");
|
|
5054
5126
|
const fileName = `result-${timestamp}.json`;
|
|
5055
|
-
const filePath =
|
|
5127
|
+
const filePath = path14.join(resultsPath, fileName);
|
|
5056
5128
|
fs12.writeFileSync(filePath, JSON.stringify(reportData, null, 2), "utf-8");
|
|
5057
5129
|
const files = fs12.readdirSync(resultsPath).filter((f) => f.startsWith("result-") && f.endsWith(".json")).sort();
|
|
5058
5130
|
while (files.length > 20) {
|
|
5059
5131
|
const oldest = files.shift();
|
|
5060
|
-
fs12.unlinkSync(
|
|
5132
|
+
fs12.unlinkSync(path14.join(resultsPath, oldest));
|
|
5061
5133
|
}
|
|
5062
5134
|
}
|
|
5063
5135
|
function displayReportResult(reportPath, data) {
|
|
5064
|
-
const relativePath =
|
|
5136
|
+
const relativePath = path14.relative(process.cwd(), reportPath);
|
|
5065
5137
|
const passRate = data.summary.total > 0 ? (data.summary.passed / data.summary.total * 100).toFixed(1) : "0";
|
|
5066
5138
|
console.log();
|
|
5067
5139
|
console.log(chalk8.green(" \u2713 \u6D4B\u8BD5\u62A5\u544A\u5DF2\u751F\u6210"));
|
|
@@ -5119,7 +5191,7 @@ import ora7 from "ora";
|
|
|
5119
5191
|
|
|
5120
5192
|
// src/services/visual.ts
|
|
5121
5193
|
import fs13 from "fs";
|
|
5122
|
-
import
|
|
5194
|
+
import path15 from "path";
|
|
5123
5195
|
import pixelmatch from "pixelmatch";
|
|
5124
5196
|
import { PNG } from "pngjs";
|
|
5125
5197
|
function compareImages(baselinePath, currentPath, diffOutputPath, threshold = 0.1) {
|
|
@@ -5158,7 +5230,7 @@ function compareImages(baselinePath, currentPath, diffOutputPath, threshold = 0.
|
|
|
5158
5230
|
const passed = diffRatio <= threshold;
|
|
5159
5231
|
let diffPath;
|
|
5160
5232
|
if (diffPixels > 0) {
|
|
5161
|
-
const diffDir =
|
|
5233
|
+
const diffDir = path15.dirname(diffOutputPath);
|
|
5162
5234
|
if (!fs13.existsSync(diffDir)) {
|
|
5163
5235
|
fs13.mkdirSync(diffDir, { recursive: true });
|
|
5164
5236
|
}
|
|
@@ -5179,7 +5251,7 @@ function createBaseline(currentPath, baselinePath) {
|
|
|
5179
5251
|
if (!fs13.existsSync(currentPath)) {
|
|
5180
5252
|
throw new Error(`\u5F53\u524D\u622A\u56FE\u4E0D\u5B58\u5728: ${currentPath}`);
|
|
5181
5253
|
}
|
|
5182
|
-
const baselineDir =
|
|
5254
|
+
const baselineDir = path15.dirname(baselinePath);
|
|
5183
5255
|
if (!fs13.existsSync(baselineDir)) {
|
|
5184
5256
|
fs13.mkdirSync(baselineDir, { recursive: true });
|
|
5185
5257
|
}
|
|
@@ -5193,9 +5265,9 @@ function updateAllBaselines(currentDir, baselineDir) {
|
|
|
5193
5265
|
}
|
|
5194
5266
|
const files = fs13.readdirSync(currentDir).filter((f) => f.endsWith(".png"));
|
|
5195
5267
|
for (const file of files) {
|
|
5196
|
-
const currentPath =
|
|
5197
|
-
const baselinePath =
|
|
5198
|
-
const baselineDirAbs =
|
|
5268
|
+
const currentPath = path15.join(currentDir, file);
|
|
5269
|
+
const baselinePath = path15.join(baselineDir, file);
|
|
5270
|
+
const baselineDirAbs = path15.dirname(baselinePath);
|
|
5199
5271
|
if (!fs13.existsSync(baselineDirAbs)) {
|
|
5200
5272
|
fs13.mkdirSync(baselineDirAbs, { recursive: true });
|
|
5201
5273
|
}
|
|
@@ -5211,7 +5283,7 @@ function cleanBaselines(baselineDir) {
|
|
|
5211
5283
|
const files = fs13.readdirSync(baselineDir).filter((f) => f.endsWith(".png"));
|
|
5212
5284
|
let count = 0;
|
|
5213
5285
|
for (const file of files) {
|
|
5214
|
-
fs13.unlinkSync(
|
|
5286
|
+
fs13.unlinkSync(path15.join(baselineDir, file));
|
|
5215
5287
|
count++;
|
|
5216
5288
|
}
|
|
5217
5289
|
return count;
|
|
@@ -5223,7 +5295,7 @@ function cleanDiffs(diffDir) {
|
|
|
5223
5295
|
const files = fs13.readdirSync(diffDir).filter((f) => f.endsWith(".png"));
|
|
5224
5296
|
let count = 0;
|
|
5225
5297
|
for (const file of files) {
|
|
5226
|
-
fs13.unlinkSync(
|
|
5298
|
+
fs13.unlinkSync(path15.join(diffDir, file));
|
|
5227
5299
|
count++;
|
|
5228
5300
|
}
|
|
5229
5301
|
return count;
|
|
@@ -5235,9 +5307,9 @@ function compareDirectories(baselineDir, currentDir, diffDir, threshold = 0.1) {
|
|
|
5235
5307
|
}
|
|
5236
5308
|
const currentFiles = fs13.readdirSync(currentDir).filter((f) => f.endsWith(".png"));
|
|
5237
5309
|
for (const file of currentFiles) {
|
|
5238
|
-
const currentPath =
|
|
5239
|
-
const baselinePath =
|
|
5240
|
-
const diffPath =
|
|
5310
|
+
const currentPath = path15.join(currentDir, file);
|
|
5311
|
+
const baselinePath = path15.join(baselineDir, file);
|
|
5312
|
+
const diffPath = path15.join(diffDir, file);
|
|
5241
5313
|
if (!fs13.existsSync(baselinePath)) {
|
|
5242
5314
|
createBaseline(currentPath, baselinePath);
|
|
5243
5315
|
results.push({
|
|
@@ -5271,7 +5343,7 @@ function compareDirectories(baselineDir, currentDir, diffDir, threshold = 0.1) {
|
|
|
5271
5343
|
|
|
5272
5344
|
// src/commands/visual.ts
|
|
5273
5345
|
import fs14 from "fs";
|
|
5274
|
-
import
|
|
5346
|
+
import path16 from "path";
|
|
5275
5347
|
function registerVisualCommand(program2) {
|
|
5276
5348
|
program2.command("visual").description("\u89C6\u89C9\u56DE\u5F52\u6D4B\u8BD5 - \u622A\u56FE\u6BD4\u5BF9\u4E0E\u57FA\u7EBF\u7BA1\u7406").argument("<action>", "\u64CD\u4F5C\u7C7B\u578B (test|approve|clean)").option("--threshold <number>", "\u50CF\u7D20\u5DEE\u5F02\u9608\u503C (0-1)", "0.1").action(async (action, options) => {
|
|
5277
5349
|
try {
|
|
@@ -5346,9 +5418,9 @@ async function runVisualTest(threshold, baselineDir, diffDir, config) {
|
|
|
5346
5418
|
}
|
|
5347
5419
|
function findCurrentScreenshotsDir(baselineDir) {
|
|
5348
5420
|
const possibleDirs = [
|
|
5349
|
-
|
|
5350
|
-
|
|
5351
|
-
|
|
5421
|
+
path16.join(process.cwd(), "test-results"),
|
|
5422
|
+
path16.join(process.cwd(), "tests", "visual", "current"),
|
|
5423
|
+
path16.join(process.cwd(), baselineDir, "..", "current")
|
|
5352
5424
|
];
|
|
5353
5425
|
for (const dir of possibleDirs) {
|
|
5354
5426
|
if (fs14.existsSync(dir)) {
|
|
@@ -5364,7 +5436,7 @@ function findPngFiles(dir) {
|
|
|
5364
5436
|
if (!fs14.existsSync(d)) return;
|
|
5365
5437
|
const entries = fs14.readdirSync(d, { withFileTypes: true });
|
|
5366
5438
|
for (const entry of entries) {
|
|
5367
|
-
const fullPath =
|
|
5439
|
+
const fullPath = path16.join(d, entry.name);
|
|
5368
5440
|
if (entry.isDirectory() && entry.name !== "node_modules") {
|
|
5369
5441
|
walk(fullPath);
|
|
5370
5442
|
} else if (entry.name.endsWith(".png")) {
|
|
@@ -5393,7 +5465,7 @@ function displayVisualResults(results, threshold) {
|
|
|
5393
5465
|
console.log();
|
|
5394
5466
|
console.log(chalk9.green(" \u901A\u8FC7\u7684\u622A\u56FE:"));
|
|
5395
5467
|
for (const result of passed) {
|
|
5396
|
-
const name =
|
|
5468
|
+
const name = path16.basename(result.baselinePath);
|
|
5397
5469
|
if (result.totalPixels > 0) {
|
|
5398
5470
|
const diffPct = (result.diffRatio * 100).toFixed(2);
|
|
5399
5471
|
console.log(chalk9.green(` \u2713 ${name} (\u5DEE\u5F02: ${diffPct}%)`));
|
|
@@ -5406,7 +5478,7 @@ function displayVisualResults(results, threshold) {
|
|
|
5406
5478
|
console.log();
|
|
5407
5479
|
console.log(chalk9.red(" \u5931\u8D25\u7684\u622A\u56FE:"));
|
|
5408
5480
|
for (const result of failed) {
|
|
5409
|
-
const name =
|
|
5481
|
+
const name = path16.basename(result.baselinePath);
|
|
5410
5482
|
if (result.diffPixels === -1) {
|
|
5411
5483
|
console.log(chalk9.red(` \u2717 ${name} (\u5C3A\u5BF8\u4E0D\u5339\u914D)`));
|
|
5412
5484
|
} else {
|
|
@@ -5414,7 +5486,7 @@ function displayVisualResults(results, threshold) {
|
|
|
5414
5486
|
console.log(chalk9.red(` \u2717 ${name} (\u5DEE\u5F02: ${diffPct}%)`));
|
|
5415
5487
|
}
|
|
5416
5488
|
if (result.diffPath) {
|
|
5417
|
-
console.log(chalk9.gray(` \u5DEE\u5F02\u56FE: ${
|
|
5489
|
+
console.log(chalk9.gray(` \u5DEE\u5F02\u56FE: ${path16.relative(process.cwd(), result.diffPath)}`));
|
|
5418
5490
|
}
|
|
5419
5491
|
}
|
|
5420
5492
|
console.log();
|
|
@@ -5461,7 +5533,7 @@ import inquirer4 from "inquirer";
|
|
|
5461
5533
|
import ora8 from "ora";
|
|
5462
5534
|
import { execFile as execFile4 } from "child_process";
|
|
5463
5535
|
import fs15 from "fs";
|
|
5464
|
-
import
|
|
5536
|
+
import path17 from "path";
|
|
5465
5537
|
var DEPENDENCY_GROUPS = [
|
|
5466
5538
|
{
|
|
5467
5539
|
name: "Vitest (\u5355\u5143/\u7EC4\u4EF6/API\u6D4B\u8BD5)",
|
|
@@ -5495,7 +5567,7 @@ function registerSetupCommand(program2) {
|
|
|
5495
5567
|
async function executeSetup(options) {
|
|
5496
5568
|
console.log(chalk10.cyan("\n QAT \u4F9D\u8D56\u5B89\u88C5\u5668\n"));
|
|
5497
5569
|
const projectInfo = detectProject();
|
|
5498
|
-
if (!fs15.existsSync(
|
|
5570
|
+
if (!fs15.existsSync(path17.join(process.cwd(), "package.json"))) {
|
|
5499
5571
|
throw new Error("\u672A\u627E\u5230 package.json\uFF0C\u8BF7\u5728\u9879\u76EE\u6839\u76EE\u5F55\u6267\u884C\u6B64\u547D\u4EE4");
|
|
5500
5572
|
}
|
|
5501
5573
|
if (projectInfo.frameworkConfidence > 0) {
|
|
@@ -5529,8 +5601,8 @@ async function executeSetup(options) {
|
|
|
5529
5601
|
}
|
|
5530
5602
|
]);
|
|
5531
5603
|
if (chooseDir !== "root") {
|
|
5532
|
-
installDir =
|
|
5533
|
-
if (!fs15.existsSync(
|
|
5604
|
+
installDir = path17.join(process.cwd(), chooseDir);
|
|
5605
|
+
if (!fs15.existsSync(path17.join(installDir, "package.json"))) {
|
|
5534
5606
|
throw new Error(`${chooseDir} \u4E0B\u6CA1\u6709 package.json`);
|
|
5535
5607
|
}
|
|
5536
5608
|
}
|
|
@@ -5569,7 +5641,7 @@ ${allPackages.map((p) => ` - ${p}`).join("\n")}`,
|
|
|
5569
5641
|
const pm = getPackageManager(projectInfo.packageManager);
|
|
5570
5642
|
const installCmd = pm === "npm" ? "npm install -D" : pm === "yarn" ? "yarn add -D" : pm === "pnpm" ? "pnpm add -D" : "bun add -D";
|
|
5571
5643
|
for (const group of selectedGroups) {
|
|
5572
|
-
const dirHint = installDir !== process.cwd() ? ` (\u5728 ${
|
|
5644
|
+
const dirHint = installDir !== process.cwd() ? ` (\u5728 ${path17.relative(process.cwd(), installDir) || installDir})` : "";
|
|
5573
5645
|
console.log(chalk10.white(` ${installCmd} ${group.packages.join(" ")}${dirHint}`));
|
|
5574
5646
|
if (group.postInstall) {
|
|
5575
5647
|
for (const cmd of group.postInstall) {
|
|
@@ -5850,7 +5922,7 @@ async function executeChange(_options) {
|
|
|
5850
5922
|
}
|
|
5851
5923
|
|
|
5852
5924
|
// src/cli.ts
|
|
5853
|
-
var VERSION = "0.3.
|
|
5925
|
+
var VERSION = "0.3.05";
|
|
5854
5926
|
function printLogo() {
|
|
5855
5927
|
const logo = `
|
|
5856
5928
|
${chalk13.bold.cyan(" ___ _ _ _ _ _____ _ _ ")}
|