qat-cli 0.3.1 → 0.3.3
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 +1319 -1233
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +308 -129
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +128 -120
- package/dist/index.d.ts +128 -120
- package/dist/index.js +307 -128
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -11,11 +11,11 @@ import { Command } from "commander";
|
|
|
11
11
|
import chalk12 from "chalk";
|
|
12
12
|
|
|
13
13
|
// src/commands/init.ts
|
|
14
|
-
import
|
|
14
|
+
import chalk2 from "chalk";
|
|
15
15
|
import inquirer from "inquirer";
|
|
16
|
-
import
|
|
17
|
-
import
|
|
18
|
-
import
|
|
16
|
+
import ora from "ora";
|
|
17
|
+
import fs6 from "fs";
|
|
18
|
+
import path6 from "path";
|
|
19
19
|
|
|
20
20
|
// src/services/detector.ts
|
|
21
21
|
import fs from "fs";
|
|
@@ -1017,8 +1017,13 @@ ${error.actual ? `\u5B9E\u9645\u503C: ${error.actual}` : ""}`;
|
|
|
1017
1017
|
6. \u5982\u679C\u6709 props/emits \u4FE1\u606F\uFF0C\u52A1\u5FC5\u9488\u5BF9\u6BCF\u4E2A prop \u548C emit \u751F\u6210\u6D4B\u8BD5`;
|
|
1018
1018
|
}
|
|
1019
1019
|
buildGenerateTestUserPrompt(req) {
|
|
1020
|
+
const importPath = this.computeTestImportPath(req.type, req.target);
|
|
1020
1021
|
let prompt = `\u8BF7\u4E3A\u4EE5\u4E0B\u6587\u4EF6\u751F\u6210${req.type}\u6D4B\u8BD5\u4EE3\u7801:
|
|
1021
1022
|
\u76EE\u6807\u6587\u4EF6: ${req.target}
|
|
1023
|
+
\u6D4B\u8BD5\u6587\u4EF6\u5C06\u653E\u5728: ${this.getTestOutputDir(req.type)}/
|
|
1024
|
+
\u6B63\u786E\u7684 import \u8DEF\u5F84: ${importPath}
|
|
1025
|
+
|
|
1026
|
+
\u91CD\u8981\uFF1Aimport \u8BED\u53E5\u4E2D\u5FC5\u987B\u4F7F\u7528\u4E0A\u8FF0\u6B63\u786E\u7684\u76F8\u5BF9\u8DEF\u5F84 ${importPath}\uFF0C\u4E0D\u8981\u4F7F\u7528 ${req.target} \u6216\u5176\u4ED6\u8DEF\u5F84\uFF01
|
|
1022
1027
|
`;
|
|
1023
1028
|
if (req.analysis) {
|
|
1024
1029
|
prompt += "\n\u6E90\u7801\u5206\u6790\u7ED3\u679C:\n";
|
|
@@ -1068,6 +1073,46 @@ ${req.context}
|
|
|
1068
1073
|
}
|
|
1069
1074
|
return prompt;
|
|
1070
1075
|
}
|
|
1076
|
+
/**
|
|
1077
|
+
* 根据测试类型和源文件路径,计算从测试文件到源文件的正确相对导入路径
|
|
1078
|
+
*/
|
|
1079
|
+
computeTestImportPath(testType, targetPath) {
|
|
1080
|
+
if (!targetPath) return targetPath;
|
|
1081
|
+
const testDirMap = {
|
|
1082
|
+
unit: "tests/unit",
|
|
1083
|
+
component: "tests/component",
|
|
1084
|
+
e2e: "tests/e2e",
|
|
1085
|
+
api: "tests/api",
|
|
1086
|
+
visual: "tests/visual",
|
|
1087
|
+
performance: "tests/e2e"
|
|
1088
|
+
};
|
|
1089
|
+
const testDir = testDirMap[testType];
|
|
1090
|
+
if (!testDir) return targetPath;
|
|
1091
|
+
if (targetPath.startsWith("../") || targetPath.startsWith("./../")) {
|
|
1092
|
+
return targetPath;
|
|
1093
|
+
}
|
|
1094
|
+
const depth = testDir.split("/").length;
|
|
1095
|
+
const prefix = "../".repeat(depth);
|
|
1096
|
+
let cleanPath = targetPath.replace(/^\.\//, "");
|
|
1097
|
+
if (!cleanPath.endsWith(".vue")) {
|
|
1098
|
+
cleanPath = cleanPath.replace(/\.(ts|js|tsx|jsx)$/, "");
|
|
1099
|
+
}
|
|
1100
|
+
return `${prefix}${cleanPath}`;
|
|
1101
|
+
}
|
|
1102
|
+
/**
|
|
1103
|
+
* 获取测试输出目录
|
|
1104
|
+
*/
|
|
1105
|
+
getTestOutputDir(testType) {
|
|
1106
|
+
const dirMap = {
|
|
1107
|
+
unit: "tests/unit",
|
|
1108
|
+
component: "tests/component",
|
|
1109
|
+
e2e: "tests/e2e",
|
|
1110
|
+
api: "tests/api",
|
|
1111
|
+
visual: "tests/visual",
|
|
1112
|
+
performance: "tests/e2e"
|
|
1113
|
+
};
|
|
1114
|
+
return dirMap[testType] || "tests/unit";
|
|
1115
|
+
}
|
|
1071
1116
|
parseGenerateTestResponse(content) {
|
|
1072
1117
|
const codeBlockMatch = content.match(/```(?:typescript|ts|javascript|js)?\s*\n([\s\S]*?)```/);
|
|
1073
1118
|
const code = codeBlockMatch ? codeBlockMatch[1].trim() : content.replace(/^(?:```[\s\S]*?\n)?/, "").replace(/\n?```$/, "").trim();
|
|
@@ -1096,10 +1141,12 @@ ISSUES: \u95EE\u9898\u5217\u8868\uFF08\u6BCF\u884C\u4E00\u4E2A\uFF0C\u683C\u5F0F
|
|
|
1096
1141
|
SUGGESTIONS: \u6539\u8FDB\u5EFA\u8BAE\u5217\u8868\uFF08\u6BCF\u884C\u4E00\u4E2A\uFF0C\u683C\u5F0F "- \u5EFA\u8BAE\u63CF\u8FF0"\uFF09`;
|
|
1097
1142
|
}
|
|
1098
1143
|
buildReviewTestUserPrompt(req) {
|
|
1144
|
+
const importPath = this.computeTestImportPath(req.testType, req.target);
|
|
1099
1145
|
let prompt = `\u8BF7\u5BA1\u67E5\u4EE5\u4E0B\u6D4B\u8BD5\u7528\u4F8B\u662F\u5426\u4E0E\u6E90\u7801\u8D34\u5207\u4E14\u51C6\u786E\u3002
|
|
1100
1146
|
|
|
1101
1147
|
\u88AB\u6D4B\u6587\u4EF6: ${req.target}
|
|
1102
1148
|
\u6D4B\u8BD5\u7C7B\u578B: ${req.testType}
|
|
1149
|
+
\u6B63\u786E\u7684 import \u8DEF\u5F84\u5E94\u4E3A: ${importPath}\uFF08\u6D4B\u8BD5\u6587\u4EF6\u4F4D\u4E8E ${this.getTestOutputDir(req.testType)}/\uFF09
|
|
1103
1150
|
|
|
1104
1151
|
--- \u6E90\u7801\u5185\u5BB9 ---
|
|
1105
1152
|
\`\`\`typescript
|
|
@@ -1262,99 +1309,6 @@ async function testAIConnection(config) {
|
|
|
1262
1309
|
return provider.testConnection();
|
|
1263
1310
|
}
|
|
1264
1311
|
|
|
1265
|
-
// src/services/test-reviewer.ts
|
|
1266
|
-
import chalk2 from "chalk";
|
|
1267
|
-
import ora from "ora";
|
|
1268
|
-
var MAX_RETRIES = 3;
|
|
1269
|
-
var REVIEW_THRESHOLD = 0.6;
|
|
1270
|
-
async function generateWithReview(params) {
|
|
1271
|
-
const { testType, targetPath, sourceCode, analysis, aiConfig, framework, onAttempt } = params;
|
|
1272
|
-
const generatorProvider = createAIProvider(aiConfig);
|
|
1273
|
-
const reviewerProvider = createAIProvider(aiConfig);
|
|
1274
|
-
if (!generatorProvider.capabilities.generateTest) {
|
|
1275
|
-
throw new Error("\u5F53\u524D AI Provider \u4E0D\u652F\u6301\u6D4B\u8BD5\u751F\u6210");
|
|
1276
|
-
}
|
|
1277
|
-
let currentCode = "";
|
|
1278
|
-
let currentDescription = "";
|
|
1279
|
-
let currentConfidence = 0;
|
|
1280
|
-
let lastReview = null;
|
|
1281
|
-
let approved = false;
|
|
1282
|
-
let attempts = 0;
|
|
1283
|
-
for (let i = 0; i < MAX_RETRIES; i++) {
|
|
1284
|
-
attempts = i + 1;
|
|
1285
|
-
onAttempt?.(attempts, MAX_RETRIES);
|
|
1286
|
-
const generationPrompt = i === 0 ? void 0 : `\u4E0A\u6B21\u751F\u6210\u7684\u6D4B\u8BD5\u672A\u901A\u8FC7\u5BA1\u8BA1\uFF0C\u8BF7\u6839\u636E\u4EE5\u4E0B\u53CD\u9988\u91CD\u65B0\u751F\u6210\uFF1A
|
|
1287
|
-
\u5BA1\u8BA1\u8BC4\u5206: ${(lastReview.score * 100).toFixed(0)}%
|
|
1288
|
-
\u5BA1\u8BA1\u610F\u89C1: ${lastReview.feedback}
|
|
1289
|
-
\u5177\u4F53\u95EE\u9898:
|
|
1290
|
-
${lastReview.issues.map((issue) => `- ${issue}`).join("\n")}
|
|
1291
|
-
\u6539\u8FDB\u5EFA\u8BAE:
|
|
1292
|
-
${lastReview.suggestions.map((s) => `- ${s}`).join("\n")}
|
|
1293
|
-
|
|
1294
|
-
\u8BF7\u9488\u5BF9\u4EE5\u4E0A\u95EE\u9898\u91CD\u65B0\u751F\u6210\u66F4\u8D34\u5207\u3001\u66F4\u51C6\u786E\u7684\u6D4B\u8BD5\u7528\u4F8B\u3002`;
|
|
1295
|
-
const generateResponse = await generatorProvider.generateTest({
|
|
1296
|
-
type: testType,
|
|
1297
|
-
target: targetPath,
|
|
1298
|
-
context: i === 0 ? sourceCode : `${sourceCode}
|
|
1299
|
-
|
|
1300
|
-
--- \u5BA1\u8BA1\u53CD\u9988 ---
|
|
1301
|
-
${generationPrompt}`,
|
|
1302
|
-
framework: framework || void 0,
|
|
1303
|
-
analysis
|
|
1304
|
-
});
|
|
1305
|
-
currentCode = generateResponse.code;
|
|
1306
|
-
currentDescription = generateResponse.description;
|
|
1307
|
-
currentConfidence = generateResponse.confidence;
|
|
1308
|
-
const reviewRequest = {
|
|
1309
|
-
target: targetPath,
|
|
1310
|
-
sourceCode,
|
|
1311
|
-
analysis,
|
|
1312
|
-
testCode: currentCode,
|
|
1313
|
-
testType,
|
|
1314
|
-
generationDescription: currentDescription
|
|
1315
|
-
};
|
|
1316
|
-
lastReview = await reviewerProvider.reviewTest(reviewRequest);
|
|
1317
|
-
approved = lastReview.approved && lastReview.score >= REVIEW_THRESHOLD;
|
|
1318
|
-
if (approved) {
|
|
1319
|
-
break;
|
|
1320
|
-
}
|
|
1321
|
-
}
|
|
1322
|
-
return {
|
|
1323
|
-
code: currentCode,
|
|
1324
|
-
description: currentDescription,
|
|
1325
|
-
confidence: currentConfidence,
|
|
1326
|
-
approved,
|
|
1327
|
-
attempts,
|
|
1328
|
-
reviewScore: lastReview?.score ?? 0,
|
|
1329
|
-
reviewFeedback: lastReview?.feedback ?? "",
|
|
1330
|
-
reviewIssues: lastReview?.issues ?? [],
|
|
1331
|
-
reviewSuggestions: lastReview?.suggestions ?? []
|
|
1332
|
-
};
|
|
1333
|
-
}
|
|
1334
|
-
function printReviewReport(report) {
|
|
1335
|
-
if (report.length === 0) return;
|
|
1336
|
-
const approved = report.filter((r) => r.approved);
|
|
1337
|
-
const failed = report.filter((r) => !r.approved);
|
|
1338
|
-
console.log();
|
|
1339
|
-
console.log(chalk2.cyan(" \u5BA1\u8BA1\u62A5\u544A:"));
|
|
1340
|
-
console.log(chalk2.gray(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
1341
|
-
for (const entry of approved) {
|
|
1342
|
-
console.log(` ${chalk2.green("\u2713")} ${chalk2.gray(entry.target)} ${chalk2.green(`(${(entry.score * 100).toFixed(0)}%`)}${entry.attempts > 1 ? chalk2.yellow(` ${entry.attempts}\u6B21\u5C1D\u8BD5`) : ""})`);
|
|
1343
|
-
}
|
|
1344
|
-
for (const entry of failed) {
|
|
1345
|
-
console.log(` ${chalk2.red("\u2717")} ${chalk2.gray(entry.target)} ${chalk2.red(`(${(entry.score * 100).toFixed(0)}%)`)}`);
|
|
1346
|
-
if (entry.issues.length > 0) {
|
|
1347
|
-
for (const issue of entry.issues.slice(0, 3)) {
|
|
1348
|
-
console.log(chalk2.gray(` - ${issue}`));
|
|
1349
|
-
}
|
|
1350
|
-
}
|
|
1351
|
-
if (entry.feedback) {
|
|
1352
|
-
console.log(chalk2.gray(` \u53CD\u9988: ${entry.feedback}`));
|
|
1353
|
-
}
|
|
1354
|
-
}
|
|
1355
|
-
console.log();
|
|
1356
|
-
}
|
|
1357
|
-
|
|
1358
1312
|
// src/services/source-analyzer.ts
|
|
1359
1313
|
import fs4 from "fs";
|
|
1360
1314
|
import path4 from "path";
|
|
@@ -1790,132 +1744,496 @@ function generateMockRoutesFromAPICalls(apiCalls) {
|
|
|
1790
1744
|
return routes;
|
|
1791
1745
|
}
|
|
1792
1746
|
|
|
1793
|
-
// src/services/
|
|
1747
|
+
// src/services/global-config.ts
|
|
1794
1748
|
import fs5 from "fs";
|
|
1795
1749
|
import path5 from "path";
|
|
1796
|
-
import
|
|
1797
|
-
|
|
1798
|
-
var
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
component: "component-test.hbs",
|
|
1803
|
-
e2e: "e2e-test.hbs",
|
|
1804
|
-
api: "api-test.hbs",
|
|
1805
|
-
visual: "visual-test.hbs",
|
|
1806
|
-
performance: "performance-test.hbs"
|
|
1807
|
-
};
|
|
1808
|
-
var customTemplates = /* @__PURE__ */ new Map();
|
|
1809
|
-
Handlebars.registerHelper("eq", (a, b) => a === b);
|
|
1810
|
-
Handlebars.registerHelper("neq", (a, b) => a !== b);
|
|
1811
|
-
Handlebars.registerHelper("includes", (arr, val) => Array.isArray(arr) && arr.includes(val));
|
|
1812
|
-
Handlebars.registerHelper("join", (arr, sep) => Array.isArray(arr) ? arr.join(sep) : "");
|
|
1813
|
-
Handlebars.registerHelper("camelCase", (str) => toCamelCase(str));
|
|
1814
|
-
Handlebars.registerHelper("pascalCase", (str) => toPascalCase(str));
|
|
1815
|
-
Handlebars.registerHelper("length", (arr) => Array.isArray(arr) ? arr.length : 0);
|
|
1816
|
-
Handlebars.registerHelper("gt", (a, b) => Number(a) > Number(b));
|
|
1817
|
-
Handlebars.registerHelper("and", (...args) => {
|
|
1818
|
-
args.pop();
|
|
1819
|
-
return args.every(Boolean);
|
|
1820
|
-
});
|
|
1821
|
-
Handlebars.registerHelper("or", (...args) => {
|
|
1822
|
-
args.pop();
|
|
1823
|
-
return args.some(Boolean);
|
|
1824
|
-
});
|
|
1825
|
-
Handlebars.registerHelper("propDefaultValue", (prop) => {
|
|
1826
|
-
if (!prop.defaultValue) return "undefined";
|
|
1827
|
-
const map = { String: "''", Number: "0", Boolean: "false", Array: "[]", Object: "{}" };
|
|
1828
|
-
return map[prop.type] || prop.defaultValue;
|
|
1829
|
-
});
|
|
1830
|
-
Handlebars.registerHelper("propTestValue", (prop) => {
|
|
1831
|
-
const map = {
|
|
1832
|
-
String: "'test-value'",
|
|
1833
|
-
Number: "42",
|
|
1834
|
-
Boolean: "true",
|
|
1835
|
-
Array: "[1, 2, 3]",
|
|
1836
|
-
Object: '{ key: "value" }'
|
|
1837
|
-
};
|
|
1838
|
-
return map[prop.type] || "'test-value'";
|
|
1839
|
-
});
|
|
1840
|
-
function renderTemplate(type, context) {
|
|
1841
|
-
const templateContent = loadTemplate(type);
|
|
1842
|
-
const template = Handlebars.compile(templateContent);
|
|
1843
|
-
const fullContext = {
|
|
1844
|
-
vueVersion: 3,
|
|
1845
|
-
typescript: true,
|
|
1846
|
-
imports: [],
|
|
1847
|
-
extraImports: [],
|
|
1848
|
-
globalPlugins: [],
|
|
1849
|
-
globalStubs: [],
|
|
1850
|
-
mountOptions: "",
|
|
1851
|
-
hasAnalysis: false,
|
|
1852
|
-
exports: [],
|
|
1853
|
-
functionExports: [],
|
|
1854
|
-
valueExports: [],
|
|
1855
|
-
props: [],
|
|
1856
|
-
emits: [],
|
|
1857
|
-
methods: [],
|
|
1858
|
-
computed: [],
|
|
1859
|
-
isVueComponent: false,
|
|
1860
|
-
requiredProps: [],
|
|
1861
|
-
optionalProps: [],
|
|
1862
|
-
...context,
|
|
1863
|
-
framework: context.framework || "vue",
|
|
1864
|
-
camelName: context.camelName || toCamelCase(context.name),
|
|
1865
|
-
pascalName: context.pascalName || toPascalCase(context.name)
|
|
1866
|
-
};
|
|
1867
|
-
if (fullContext.exports && fullContext.exports.length > 0) {
|
|
1868
|
-
fullContext.hasAnalysis = true;
|
|
1869
|
-
fullContext.functionExports = fullContext.exports.filter(
|
|
1870
|
-
(e) => e.kind === "function" || e.kind === "default"
|
|
1871
|
-
);
|
|
1872
|
-
fullContext.valueExports = fullContext.exports.filter(
|
|
1873
|
-
(e) => e.kind !== "function" && e.kind !== "default" && e.kind !== "type"
|
|
1874
|
-
);
|
|
1875
|
-
}
|
|
1876
|
-
if (fullContext.props && fullContext.props.length > 0) {
|
|
1877
|
-
fullContext.isVueComponent = true;
|
|
1878
|
-
fullContext.requiredProps = fullContext.props.filter((p) => p.required);
|
|
1879
|
-
fullContext.optionalProps = fullContext.props.filter((p) => !p.required);
|
|
1750
|
+
import os from "os";
|
|
1751
|
+
var QAT_DIR = path5.join(os.homedir(), ".qat");
|
|
1752
|
+
var AI_CONFIG_PATH = path5.join(QAT_DIR, "ai.json");
|
|
1753
|
+
function loadGlobalAIConfig() {
|
|
1754
|
+
if (!fs5.existsSync(AI_CONFIG_PATH)) {
|
|
1755
|
+
return null;
|
|
1880
1756
|
}
|
|
1881
|
-
|
|
1882
|
-
|
|
1757
|
+
try {
|
|
1758
|
+
const content = fs5.readFileSync(AI_CONFIG_PATH, "utf-8");
|
|
1759
|
+
const config = JSON.parse(content);
|
|
1760
|
+
if (!config.baseUrl || !config.model) {
|
|
1761
|
+
return null;
|
|
1762
|
+
}
|
|
1763
|
+
return config;
|
|
1764
|
+
} catch {
|
|
1765
|
+
return null;
|
|
1883
1766
|
}
|
|
1884
|
-
return template(fullContext);
|
|
1885
1767
|
}
|
|
1886
|
-
function
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
const templateDir = getTemplateDir();
|
|
1890
|
-
const templateFile = TEMPLATE_MAP[type];
|
|
1891
|
-
const templatePath = path5.join(templateDir, templateFile);
|
|
1892
|
-
if (fs5.existsSync(templatePath)) {
|
|
1893
|
-
return fs5.readFileSync(templatePath, "utf-8");
|
|
1768
|
+
function saveGlobalAIConfig(config) {
|
|
1769
|
+
if (!fs5.existsSync(QAT_DIR)) {
|
|
1770
|
+
fs5.mkdirSync(QAT_DIR, { recursive: true });
|
|
1894
1771
|
}
|
|
1895
|
-
|
|
1772
|
+
fs5.writeFileSync(AI_CONFIG_PATH, JSON.stringify(config, null, 2), "utf-8");
|
|
1896
1773
|
}
|
|
1897
|
-
function
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1774
|
+
function toAIConfig(globalConfig) {
|
|
1775
|
+
return {
|
|
1776
|
+
provider: globalConfig.provider || "openai",
|
|
1777
|
+
apiKey: globalConfig.apiKey || void 0,
|
|
1778
|
+
baseUrl: globalConfig.baseUrl,
|
|
1779
|
+
model: globalConfig.model
|
|
1780
|
+
};
|
|
1781
|
+
}
|
|
1782
|
+
function maskApiKey(apiKey) {
|
|
1783
|
+
if (!apiKey) return "(\u672A\u8BBE\u7F6E)";
|
|
1784
|
+
if (apiKey.length <= 8) return "****";
|
|
1785
|
+
return apiKey.slice(0, 4) + "****" + apiKey.slice(-4);
|
|
1786
|
+
}
|
|
1787
|
+
function getAIConfigPath() {
|
|
1788
|
+
return AI_CONFIG_PATH;
|
|
1903
1789
|
}
|
|
1904
|
-
function getBuiltinTemplate(type) {
|
|
1905
|
-
const templates = {
|
|
1906
|
-
unit: `import { describe, it, expect } from 'vitest';
|
|
1907
|
-
{{#if hasAnalysis}}
|
|
1908
|
-
{{#each valueExports}}
|
|
1909
|
-
import { {{name}} } from '{{../target}}';
|
|
1910
|
-
{{/each}}
|
|
1911
|
-
{{#each functionExports}}
|
|
1912
|
-
import { {{name}} } from '{{../target}}';
|
|
1913
|
-
{{/each}}
|
|
1914
|
-
{{else}}
|
|
1915
|
-
import { {{camelName}} } from '{{target}}';
|
|
1916
|
-
{{/if}}
|
|
1917
1790
|
|
|
1918
|
-
|
|
1791
|
+
// src/commands/init.ts
|
|
1792
|
+
function registerInitCommand(program2) {
|
|
1793
|
+
program2.command("init").description("\u521D\u59CB\u5316\u6D4B\u8BD5\u9879\u76EE - \u68C0\u6D4B\u9879\u76EE\u3001\u751F\u6210\u914D\u7F6E\u3001\u81EA\u52A8\u521B\u5EFA\u6D4B\u8BD5\u7528\u4F8B").option("-f, --force", "\u5F3A\u5236\u8986\u76D6\u5DF2\u6709\u914D\u7F6E\u6587\u4EF6").action(async (options) => {
|
|
1794
|
+
try {
|
|
1795
|
+
await executeInit(options);
|
|
1796
|
+
} catch (error) {
|
|
1797
|
+
console.error(chalk2.red(`
|
|
1798
|
+
\u2717 ${error instanceof Error ? error.message : String(error)}
|
|
1799
|
+
`));
|
|
1800
|
+
process.exit(1);
|
|
1801
|
+
}
|
|
1802
|
+
});
|
|
1803
|
+
}
|
|
1804
|
+
async function executeInit(options) {
|
|
1805
|
+
const spinner = ora("\u6B63\u5728\u68C0\u6D4B\u9879\u76EE\u7ED3\u6784...").start();
|
|
1806
|
+
const projectInfo = detectProject();
|
|
1807
|
+
spinner.stop();
|
|
1808
|
+
displayProjectInfo(projectInfo);
|
|
1809
|
+
if (!projectInfo.isVue) {
|
|
1810
|
+
const { proceed } = await inquirer.prompt([
|
|
1811
|
+
{
|
|
1812
|
+
type: "confirm",
|
|
1813
|
+
name: "proceed",
|
|
1814
|
+
message: "\u672A\u68C0\u6D4B\u5230 Vue \u9879\u76EE\uFF0C\u662F\u5426\u7EE7\u7EED\u521D\u59CB\u5316\uFF1F",
|
|
1815
|
+
default: false
|
|
1816
|
+
}
|
|
1817
|
+
]);
|
|
1818
|
+
if (!proceed) {
|
|
1819
|
+
console.log(chalk2.gray("\n \u5DF2\u53D6\u6D88\u521D\u59CB\u5316\n"));
|
|
1820
|
+
return;
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
let globalAI = loadGlobalAIConfig();
|
|
1824
|
+
if (!globalAI) {
|
|
1825
|
+
console.log(chalk2.cyan(" AI \u6A21\u578B\u914D\u7F6E (\u9996\u6B21\u4F7F\u7528\u9700\u914D\u7F6E\uFF0C\u4E4B\u540E\u53EF\u901A\u8FC7 qat change \u4FEE\u6539)\n"));
|
|
1826
|
+
globalAI = await promptAIConfig();
|
|
1827
|
+
saveGlobalAIConfig(globalAI);
|
|
1828
|
+
console.log(chalk2.gray(` \u914D\u7F6E\u5DF2\u4FDD\u5B58\u81F3 ${getAIConfigPath()}
|
|
1829
|
+
`));
|
|
1830
|
+
} else {
|
|
1831
|
+
console.log(chalk2.green(` \u2713 \u5F53\u524D AI \u6A21\u578B: ${chalk2.white(globalAI.model)} @ ${chalk2.gray(globalAI.baseUrl)} (${maskApiKey(globalAI.apiKey)})
|
|
1832
|
+
`));
|
|
1833
|
+
}
|
|
1834
|
+
const aiConfig = toAIConfig(globalAI);
|
|
1835
|
+
if (aiConfig.apiKey || aiConfig.baseUrl) {
|
|
1836
|
+
const testSpinner = ora(`\u6B63\u5728\u6D4B\u8BD5 AI \u8FDE\u901A\u6027 (${globalAI.model})...`).start();
|
|
1837
|
+
try {
|
|
1838
|
+
const result = await testAIConnection(aiConfig);
|
|
1839
|
+
if (result.ok) {
|
|
1840
|
+
testSpinner.succeed(`AI \u8FDE\u901A\u6B63\u5E38 ${chalk2.gray(`${globalAI.model} (${result.latencyMs}ms)`)}`);
|
|
1841
|
+
} else {
|
|
1842
|
+
testSpinner.fail(`AI \u8FDE\u901A\u5F02\u5E38: ${result.message}`);
|
|
1843
|
+
console.log(chalk2.yellow(" \u53EF\u8FD0\u884C qat change \u4FEE\u6539 AI \u914D\u7F6E\u3002"));
|
|
1844
|
+
}
|
|
1845
|
+
} catch (error) {
|
|
1846
|
+
testSpinner.fail(`AI \u8FDE\u901A\u6D4B\u8BD5\u5931\u8D25: ${error instanceof Error ? error.message : String(error)}`);
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
const config = buildProjectConfig(projectInfo);
|
|
1850
|
+
let configPath;
|
|
1851
|
+
const existingConfigPath = path6.join(process.cwd(), "qat.config.js");
|
|
1852
|
+
const existingTsPath = path6.join(process.cwd(), "qat.config.ts");
|
|
1853
|
+
const configExists = fs6.existsSync(existingConfigPath) || fs6.existsSync(existingTsPath);
|
|
1854
|
+
if (configExists && !options.force) {
|
|
1855
|
+
const { overwrite } = await inquirer.prompt([
|
|
1856
|
+
{
|
|
1857
|
+
type: "confirm",
|
|
1858
|
+
name: "overwrite",
|
|
1859
|
+
message: "\u914D\u7F6E\u6587\u4EF6 qat.config.js \u5DF2\u5B58\u5728\uFF0C\u662F\u5426\u8986\u76D6\uFF1F",
|
|
1860
|
+
default: true
|
|
1861
|
+
}
|
|
1862
|
+
]);
|
|
1863
|
+
if (!overwrite) {
|
|
1864
|
+
console.log(chalk2.gray(" \u4FDD\u7559\u73B0\u6709\u914D\u7F6E\u6587\u4EF6\uFF0C\u7EE7\u7EED\u540E\u7EED\u6B65\u9AA4..."));
|
|
1865
|
+
configPath = existingConfigPath;
|
|
1866
|
+
} else {
|
|
1867
|
+
const fileSpinner = ora("\u6B63\u5728\u8986\u76D6\u914D\u7F6E\u6587\u4EF6...").start();
|
|
1868
|
+
try {
|
|
1869
|
+
configPath = await writeConfigFile(process.cwd(), config, true);
|
|
1870
|
+
fileSpinner.succeed("\u914D\u7F6E\u6587\u4EF6\u5DF2\u8986\u76D6");
|
|
1871
|
+
} catch (error) {
|
|
1872
|
+
fileSpinner.fail("\u914D\u7F6E\u6587\u4EF6\u8986\u76D6\u5931\u8D25");
|
|
1873
|
+
throw error;
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
} else {
|
|
1877
|
+
const fileSpinner = ora("\u6B63\u5728\u751F\u6210\u914D\u7F6E\u6587\u4EF6...").start();
|
|
1878
|
+
try {
|
|
1879
|
+
configPath = await writeConfigFile(process.cwd(), config, options.force);
|
|
1880
|
+
fileSpinner.succeed("\u914D\u7F6E\u6587\u4EF6\u5DF2\u751F\u6210");
|
|
1881
|
+
} catch (error) {
|
|
1882
|
+
fileSpinner.fail("\u914D\u7F6E\u6587\u4EF6\u751F\u6210\u5931\u8D25");
|
|
1883
|
+
throw error;
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
const dirSpinner = ora("\u6B63\u5728\u521B\u5EFA\u6D4B\u8BD5\u76EE\u5F55...").start();
|
|
1887
|
+
const createdDirs = createTestDirectories(config);
|
|
1888
|
+
dirSpinner.succeed("\u6D4B\u8BD5\u76EE\u5F55\u5DF2\u521B\u5EFA");
|
|
1889
|
+
if (config.mock?.enabled !== false) {
|
|
1890
|
+
const mockDir = config.mock?.routesDir || DEFAULT_CONFIG.mock.routesDir;
|
|
1891
|
+
initMockRoutesDir(mockDir);
|
|
1892
|
+
const srcDir2 = config.project?.srcDir || "src";
|
|
1893
|
+
const apiCalls = scanAPICalls(srcDir2);
|
|
1894
|
+
if (apiCalls.length > 0) {
|
|
1895
|
+
const mockRoutes = generateMockRoutesFromAPICalls(apiCalls);
|
|
1896
|
+
const mockFilePath = path6.join(process.cwd(), mockDir, "auto-generated.json");
|
|
1897
|
+
if (!fs6.existsSync(mockFilePath)) {
|
|
1898
|
+
fs6.writeFileSync(mockFilePath, JSON.stringify(mockRoutes, null, 2), "utf-8");
|
|
1899
|
+
console.log(chalk2.green(` \u81EA\u52A8\u53D1\u73B0 ${apiCalls.length} \u4E2A API \u63A5\u53E3\uFF0C\u5DF2\u751F\u6210 Mock \u8DEF\u7531`));
|
|
1900
|
+
}
|
|
1901
|
+
} else {
|
|
1902
|
+
console.log(chalk2.gray(" \u672A\u53D1\u73B0 API \u8C03\u7528\uFF0C\u5DF2\u751F\u6210\u793A\u4F8B Mock \u8DEF\u7531"));
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
const srcDir = config.project?.srcDir || "src";
|
|
1906
|
+
const components = discoverVueComponents(process.cwd(), srcDir);
|
|
1907
|
+
const utilities = discoverUtilityFiles(process.cwd(), srcDir);
|
|
1908
|
+
const totalFiles = components.length + utilities.length;
|
|
1909
|
+
if (totalFiles > 0) {
|
|
1910
|
+
console.log(chalk2.cyan(`
|
|
1911
|
+
\u53D1\u73B0 ${totalFiles} \u4E2A\u53EF\u6D4B\u8BD5\u6587\u4EF6 (${components.length} \u7EC4\u4EF6, ${utilities.length} \u5DE5\u5177/\u670D\u52A1)`));
|
|
1912
|
+
}
|
|
1913
|
+
displayResult(configPath, createdDirs, totalFiles, projectInfo);
|
|
1914
|
+
}
|
|
1915
|
+
async function promptAIConfig() {
|
|
1916
|
+
const answers = await inquirer.prompt([
|
|
1917
|
+
{
|
|
1918
|
+
type: "input",
|
|
1919
|
+
name: "apiKey",
|
|
1920
|
+
message: "API Key (Ollama \u672C\u5730\u53EF\u7559\u7A7A):",
|
|
1921
|
+
default: ""
|
|
1922
|
+
},
|
|
1923
|
+
{
|
|
1924
|
+
type: "input",
|
|
1925
|
+
name: "baseUrl",
|
|
1926
|
+
message: "API Base URL:",
|
|
1927
|
+
default: "https://api.deepseek.com/v1",
|
|
1928
|
+
validate: (input) => {
|
|
1929
|
+
if (!input.trim()) return "Base URL \u4E0D\u80FD\u4E3A\u7A7A";
|
|
1930
|
+
if (!input.trim().startsWith("http")) return "URL \u5FC5\u987B\u4EE5 http(s):// \u5F00\u5934";
|
|
1931
|
+
return true;
|
|
1932
|
+
}
|
|
1933
|
+
},
|
|
1934
|
+
{
|
|
1935
|
+
type: "input",
|
|
1936
|
+
name: "model",
|
|
1937
|
+
message: "\u6A21\u578B\u540D\u79F0:",
|
|
1938
|
+
default: "deepseek-chat",
|
|
1939
|
+
validate: (input) => {
|
|
1940
|
+
if (!input.trim()) return "\u6A21\u578B\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A";
|
|
1941
|
+
return true;
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
]);
|
|
1945
|
+
return {
|
|
1946
|
+
provider: "openai",
|
|
1947
|
+
apiKey: answers.apiKey?.trim() || "",
|
|
1948
|
+
baseUrl: answers.baseUrl?.trim() || "https://api.deepseek.com/v1",
|
|
1949
|
+
model: answers.model?.trim() || "deepseek-chat"
|
|
1950
|
+
};
|
|
1951
|
+
}
|
|
1952
|
+
function buildProjectConfig(projectInfo) {
|
|
1953
|
+
return {
|
|
1954
|
+
project: {
|
|
1955
|
+
framework: projectInfo.framework,
|
|
1956
|
+
uiLibrary: projectInfo.uiLibrary !== "none" ? projectInfo.uiLibrary : void 0,
|
|
1957
|
+
monorepo: projectInfo.monorepo !== "none" ? projectInfo.monorepo : void 0,
|
|
1958
|
+
vite: projectInfo.isVite,
|
|
1959
|
+
srcDir: projectInfo.srcDir,
|
|
1960
|
+
appDir: projectInfo.appDirs.length > 0 ? projectInfo.appDirs[0] : void 0
|
|
1961
|
+
},
|
|
1962
|
+
vitest: {
|
|
1963
|
+
enabled: true,
|
|
1964
|
+
coverage: true,
|
|
1965
|
+
globals: true,
|
|
1966
|
+
environment: "happy-dom"
|
|
1967
|
+
},
|
|
1968
|
+
playwright: {
|
|
1969
|
+
enabled: true,
|
|
1970
|
+
browsers: ["chromium"],
|
|
1971
|
+
baseURL: "http://localhost:5173",
|
|
1972
|
+
screenshot: "only-on-failure"
|
|
1973
|
+
},
|
|
1974
|
+
visual: {
|
|
1975
|
+
enabled: true,
|
|
1976
|
+
threshold: 0.1,
|
|
1977
|
+
baselineDir: "tests/visual/baseline",
|
|
1978
|
+
diffDir: "tests/visual/diff"
|
|
1979
|
+
},
|
|
1980
|
+
lighthouse: {
|
|
1981
|
+
enabled: true,
|
|
1982
|
+
urls: ["http://localhost:5173"],
|
|
1983
|
+
runs: 3,
|
|
1984
|
+
thresholds: {
|
|
1985
|
+
performance: 80,
|
|
1986
|
+
accessibility: 90
|
|
1987
|
+
}
|
|
1988
|
+
},
|
|
1989
|
+
mock: {
|
|
1990
|
+
enabled: true,
|
|
1991
|
+
port: 3456,
|
|
1992
|
+
routesDir: "tests/mock/routes"
|
|
1993
|
+
}
|
|
1994
|
+
};
|
|
1995
|
+
}
|
|
1996
|
+
function displayProjectInfo(info) {
|
|
1997
|
+
console.log(chalk2.cyan("\n \u9879\u76EE\u68C0\u6D4B\u7ED3\u679C:"));
|
|
1998
|
+
console.log(chalk2.gray(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
1999
|
+
const items = [
|
|
2000
|
+
["\u9879\u76EE\u540D\u79F0", info.name],
|
|
2001
|
+
["\u6846\u67B6", info.frameworkConfidence > 0.5 ? `${info.frameworkDisplayName} (\u7F6E\u4FE1\u5EA6 ${Math.round(info.frameworkConfidence * 100)}%)` : info.frameworkDisplayName],
|
|
2002
|
+
["Vue \u9879\u76EE", info.isVue ? `\u662F (v${info.vueVersion})` : "\u5426"],
|
|
2003
|
+
["UI \u7EC4\u4EF6\u5E93", info.uiLibrary !== "none" ? info.uiLibrary : "\u672A\u68C0\u6D4B\u5230"],
|
|
2004
|
+
["Vite \u6784\u5EFA", info.isVite ? "\u662F" : "\u5426"],
|
|
2005
|
+
["TypeScript", info.typescript ? "\u662F" : "\u5426"],
|
|
2006
|
+
["\u5305\u7BA1\u7406\u5668", info.packageManager],
|
|
2007
|
+
["Monorepo", info.monorepo !== "none" ? info.monorepo : "\u5426"],
|
|
2008
|
+
["\u6E90\u7801\u76EE\u5F55", info.srcDir]
|
|
2009
|
+
];
|
|
2010
|
+
if (info.appDirs.length > 0) {
|
|
2011
|
+
items.push(["\u5B50\u9879\u76EE", info.appDirs.join(", ")]);
|
|
2012
|
+
}
|
|
2013
|
+
if (info.testFrameworks.length > 0) {
|
|
2014
|
+
items.push(["\u5DF2\u6709\u6D4B\u8BD5\u6846\u67B6", info.testFrameworks.join(", ")]);
|
|
2015
|
+
}
|
|
2016
|
+
if (info.componentDirs.length > 0) {
|
|
2017
|
+
items.push(["\u7EC4\u4EF6\u76EE\u5F55", info.componentDirs.join(", ")]);
|
|
2018
|
+
}
|
|
2019
|
+
for (const [label, value] of items) {
|
|
2020
|
+
const displayValue = value === true ? chalk2.green("\u2713") : value === false ? chalk2.red("\u2717") : String(value);
|
|
2021
|
+
console.log(` ${chalk2.white(label.padEnd(12))} ${displayValue}`);
|
|
2022
|
+
}
|
|
2023
|
+
console.log();
|
|
2024
|
+
}
|
|
2025
|
+
function createTestDirectories(config) {
|
|
2026
|
+
const dirs = [];
|
|
2027
|
+
const dirMap = {
|
|
2028
|
+
"tests": true,
|
|
2029
|
+
"tests/unit": config.vitest?.enabled !== false,
|
|
2030
|
+
"tests/component": config.vitest?.enabled !== false,
|
|
2031
|
+
"tests/e2e": config.playwright?.enabled !== false,
|
|
2032
|
+
"tests/api": config.mock?.enabled !== false,
|
|
2033
|
+
"tests/visual": config.visual?.enabled !== false,
|
|
2034
|
+
"tests/visual/baseline": config.visual?.enabled !== false,
|
|
2035
|
+
"tests/visual/diff": config.visual?.enabled !== false,
|
|
2036
|
+
"tests/mock": config.mock?.enabled !== false,
|
|
2037
|
+
"tests/mock/routes": config.mock?.enabled !== false
|
|
2038
|
+
};
|
|
2039
|
+
for (const [dir, shouldCreate] of Object.entries(dirMap)) {
|
|
2040
|
+
if (shouldCreate) {
|
|
2041
|
+
const fullPath = path6.join(process.cwd(), dir);
|
|
2042
|
+
if (!fs6.existsSync(fullPath)) {
|
|
2043
|
+
fs6.mkdirSync(fullPath, { recursive: true });
|
|
2044
|
+
dirs.push(dir);
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2048
|
+
return dirs;
|
|
2049
|
+
}
|
|
2050
|
+
function displayResult(configPath, createdDirs, totalTestableFiles, projectInfo) {
|
|
2051
|
+
const relativeConfig = path6.relative(process.cwd(), configPath);
|
|
2052
|
+
console.log(chalk2.green("\n \u2713 \u9879\u76EE\u521D\u59CB\u5316\u5B8C\u6210!\n"));
|
|
2053
|
+
console.log(chalk2.white(" \u5DF2\u751F\u6210\u914D\u7F6E:"));
|
|
2054
|
+
console.log(chalk2.gray(` ${relativeConfig}`));
|
|
2055
|
+
console.log();
|
|
2056
|
+
if (createdDirs.length > 0) {
|
|
2057
|
+
console.log(chalk2.white(" \u5DF2\u521B\u5EFA\u76EE\u5F55:"));
|
|
2058
|
+
for (const dir of createdDirs) {
|
|
2059
|
+
console.log(chalk2.gray(` ${dir}/`));
|
|
2060
|
+
}
|
|
2061
|
+
console.log();
|
|
2062
|
+
}
|
|
2063
|
+
console.log(chalk2.cyan(" \u4E0B\u4E00\u6B65:"));
|
|
2064
|
+
if (totalTestableFiles > 0) {
|
|
2065
|
+
console.log(chalk2.gray(` 1. qat create \u9009\u62E9\u6587\u4EF6\u5E76\u751F\u6210\u6D4B\u8BD5\u7528\u4F8B (\u53D1\u73B0 ${totalTestableFiles} \u4E2A\u53EF\u6D4B\u8BD5\u6587\u4EF6)`));
|
|
2066
|
+
} else {
|
|
2067
|
+
console.log(chalk2.gray(" 1. qat create \u521B\u5EFA\u6D4B\u8BD5\u7528\u4F8B"));
|
|
2068
|
+
}
|
|
2069
|
+
console.log(chalk2.gray(" 2. qat run \u6267\u884C\u6D4B\u8BD5"));
|
|
2070
|
+
console.log(chalk2.gray(" 3. qat status \u67E5\u770B AI \u6A21\u578B\u72B6\u6001"));
|
|
2071
|
+
if (projectInfo.testFrameworks.length === 0) {
|
|
2072
|
+
console.log();
|
|
2073
|
+
console.log(chalk2.yellow(" \u26A0 \u672A\u68C0\u6D4B\u5230\u6D4B\u8BD5\u6846\u67B6\u4F9D\u8D56\uFF0C\u8FD0\u884C qat run \u65F6\u4F1A\u63D0\u793A\u5B89\u88C5"));
|
|
2074
|
+
}
|
|
2075
|
+
console.log();
|
|
2076
|
+
}
|
|
2077
|
+
|
|
2078
|
+
// src/commands/create.ts
|
|
2079
|
+
import chalk4 from "chalk";
|
|
2080
|
+
import inquirer2 from "inquirer";
|
|
2081
|
+
import ora3 from "ora";
|
|
2082
|
+
import fs8 from "fs";
|
|
2083
|
+
import path8 from "path";
|
|
2084
|
+
|
|
2085
|
+
// src/services/template.ts
|
|
2086
|
+
import fs7 from "fs";
|
|
2087
|
+
import path7 from "path";
|
|
2088
|
+
import { fileURLToPath } from "url";
|
|
2089
|
+
import Handlebars from "handlebars";
|
|
2090
|
+
var __filename = fileURLToPath(import.meta.url);
|
|
2091
|
+
var __dirname = path7.dirname(__filename);
|
|
2092
|
+
var TEMPLATE_MAP = {
|
|
2093
|
+
unit: "unit-test.hbs",
|
|
2094
|
+
component: "component-test.hbs",
|
|
2095
|
+
e2e: "e2e-test.hbs",
|
|
2096
|
+
api: "api-test.hbs",
|
|
2097
|
+
visual: "visual-test.hbs",
|
|
2098
|
+
performance: "performance-test.hbs"
|
|
2099
|
+
};
|
|
2100
|
+
var customTemplates = /* @__PURE__ */ new Map();
|
|
2101
|
+
Handlebars.registerHelper("eq", (a, b) => a === b);
|
|
2102
|
+
Handlebars.registerHelper("neq", (a, b) => a !== b);
|
|
2103
|
+
Handlebars.registerHelper("includes", (arr, val) => Array.isArray(arr) && arr.includes(val));
|
|
2104
|
+
Handlebars.registerHelper("join", (arr, sep) => Array.isArray(arr) ? arr.join(sep) : "");
|
|
2105
|
+
Handlebars.registerHelper("camelCase", (str) => toCamelCase(str));
|
|
2106
|
+
Handlebars.registerHelper("pascalCase", (str) => toPascalCase(str));
|
|
2107
|
+
Handlebars.registerHelper("length", (arr) => Array.isArray(arr) ? arr.length : 0);
|
|
2108
|
+
Handlebars.registerHelper("gt", (a, b) => Number(a) > Number(b));
|
|
2109
|
+
Handlebars.registerHelper("and", (...args) => {
|
|
2110
|
+
args.pop();
|
|
2111
|
+
return args.every(Boolean);
|
|
2112
|
+
});
|
|
2113
|
+
Handlebars.registerHelper("or", (...args) => {
|
|
2114
|
+
args.pop();
|
|
2115
|
+
return args.some(Boolean);
|
|
2116
|
+
});
|
|
2117
|
+
Handlebars.registerHelper("propDefaultValue", (prop) => {
|
|
2118
|
+
if (!prop.defaultValue) return "undefined";
|
|
2119
|
+
const map = { String: "''", Number: "0", Boolean: "false", Array: "[]", Object: "{}" };
|
|
2120
|
+
return map[prop.type] || prop.defaultValue;
|
|
2121
|
+
});
|
|
2122
|
+
Handlebars.registerHelper("propTestValue", (prop) => {
|
|
2123
|
+
const map = {
|
|
2124
|
+
String: "'test-value'",
|
|
2125
|
+
Number: "42",
|
|
2126
|
+
Boolean: "true",
|
|
2127
|
+
Array: "[1, 2, 3]",
|
|
2128
|
+
Object: '{ key: "value" }'
|
|
2129
|
+
};
|
|
2130
|
+
return map[prop.type] || "'test-value'";
|
|
2131
|
+
});
|
|
2132
|
+
function resolveImportPath(testType, targetPath) {
|
|
2133
|
+
if (!targetPath) return targetPath;
|
|
2134
|
+
const testDirMap = {
|
|
2135
|
+
unit: "tests/unit",
|
|
2136
|
+
component: "tests/component",
|
|
2137
|
+
e2e: "tests/e2e",
|
|
2138
|
+
api: "tests/api",
|
|
2139
|
+
visual: "tests/visual",
|
|
2140
|
+
performance: "tests/e2e"
|
|
2141
|
+
};
|
|
2142
|
+
const testDir = testDirMap[testType];
|
|
2143
|
+
if (!testDir) return targetPath;
|
|
2144
|
+
if (targetPath.startsWith("../") || targetPath.startsWith("./../")) {
|
|
2145
|
+
return targetPath;
|
|
2146
|
+
}
|
|
2147
|
+
const depth = testDir.split("/").length;
|
|
2148
|
+
const prefix = "../".repeat(depth);
|
|
2149
|
+
let cleanPath = targetPath.replace(/^\.\//, "");
|
|
2150
|
+
if (!cleanPath.endsWith(".vue")) {
|
|
2151
|
+
cleanPath = cleanPath.replace(/\.(ts|js|tsx|jsx)$/, "");
|
|
2152
|
+
}
|
|
2153
|
+
return `${prefix}${cleanPath}`;
|
|
2154
|
+
}
|
|
2155
|
+
function renderTemplate(type, context) {
|
|
2156
|
+
const templateContent = loadTemplate(type);
|
|
2157
|
+
const template = Handlebars.compile(templateContent);
|
|
2158
|
+
const resolvedTarget = resolveImportPath(type, context.target);
|
|
2159
|
+
const fullContext = {
|
|
2160
|
+
vueVersion: 3,
|
|
2161
|
+
typescript: true,
|
|
2162
|
+
imports: [],
|
|
2163
|
+
extraImports: [],
|
|
2164
|
+
globalPlugins: [],
|
|
2165
|
+
globalStubs: [],
|
|
2166
|
+
mountOptions: "",
|
|
2167
|
+
hasAnalysis: false,
|
|
2168
|
+
exports: [],
|
|
2169
|
+
functionExports: [],
|
|
2170
|
+
valueExports: [],
|
|
2171
|
+
props: [],
|
|
2172
|
+
emits: [],
|
|
2173
|
+
methods: [],
|
|
2174
|
+
computed: [],
|
|
2175
|
+
isVueComponent: false,
|
|
2176
|
+
requiredProps: [],
|
|
2177
|
+
optionalProps: [],
|
|
2178
|
+
...context,
|
|
2179
|
+
// 使用计算后的正确路径
|
|
2180
|
+
target: resolvedTarget,
|
|
2181
|
+
framework: context.framework || "vue",
|
|
2182
|
+
camelName: context.camelName || toCamelCase(context.name),
|
|
2183
|
+
pascalName: context.pascalName || toPascalCase(context.name)
|
|
2184
|
+
};
|
|
2185
|
+
if (fullContext.exports && fullContext.exports.length > 0) {
|
|
2186
|
+
fullContext.hasAnalysis = true;
|
|
2187
|
+
fullContext.functionExports = fullContext.exports.filter(
|
|
2188
|
+
(e) => e.kind === "function" || e.kind === "default"
|
|
2189
|
+
);
|
|
2190
|
+
fullContext.valueExports = fullContext.exports.filter(
|
|
2191
|
+
(e) => e.kind !== "function" && e.kind !== "default" && e.kind !== "type"
|
|
2192
|
+
);
|
|
2193
|
+
}
|
|
2194
|
+
if (fullContext.props && fullContext.props.length > 0) {
|
|
2195
|
+
fullContext.isVueComponent = true;
|
|
2196
|
+
fullContext.requiredProps = fullContext.props.filter((p) => p.required);
|
|
2197
|
+
fullContext.optionalProps = fullContext.props.filter((p) => !p.required);
|
|
2198
|
+
}
|
|
2199
|
+
if (fullContext.emits && fullContext.emits.length > 0) {
|
|
2200
|
+
fullContext.isVueComponent = true;
|
|
2201
|
+
}
|
|
2202
|
+
return template(fullContext);
|
|
2203
|
+
}
|
|
2204
|
+
function loadTemplate(type) {
|
|
2205
|
+
const custom = customTemplates.get(type);
|
|
2206
|
+
if (custom) return custom;
|
|
2207
|
+
const templateDir = getTemplateDir();
|
|
2208
|
+
const templateFile = TEMPLATE_MAP[type];
|
|
2209
|
+
const templatePath = path7.join(templateDir, templateFile);
|
|
2210
|
+
if (fs7.existsSync(templatePath)) {
|
|
2211
|
+
return fs7.readFileSync(templatePath, "utf-8");
|
|
2212
|
+
}
|
|
2213
|
+
return getBuiltinTemplate(type);
|
|
2214
|
+
}
|
|
2215
|
+
function getTemplateDir() {
|
|
2216
|
+
const projectTemplates = path7.join(process.cwd(), "templates");
|
|
2217
|
+
if (fs7.existsSync(projectTemplates)) {
|
|
2218
|
+
return projectTemplates;
|
|
2219
|
+
}
|
|
2220
|
+
return path7.join(__dirname, "..", "templates");
|
|
2221
|
+
}
|
|
2222
|
+
function getBuiltinTemplate(type) {
|
|
2223
|
+
const templates = {
|
|
2224
|
+
unit: `import { describe, it, expect } from 'vitest';
|
|
2225
|
+
{{#if hasAnalysis}}
|
|
2226
|
+
{{#each valueExports}}
|
|
2227
|
+
import { {{name}} } from '{{../target}}';
|
|
2228
|
+
{{/each}}
|
|
2229
|
+
{{#each functionExports}}
|
|
2230
|
+
import { {{name}} } from '{{../target}}';
|
|
2231
|
+
{{/each}}
|
|
2232
|
+
{{else}}
|
|
2233
|
+
import { {{camelName}} } from '{{target}}';
|
|
2234
|
+
{{/if}}
|
|
2235
|
+
|
|
2236
|
+
describe('{{name}}', () => {
|
|
1919
2237
|
{{#if hasAnalysis}}
|
|
1920
2238
|
{{#each functionExports}}
|
|
1921
2239
|
describe('{{name}}()', () => {
|
|
@@ -2191,647 +2509,127 @@ test.describe('{{name}} performance', () => {
|
|
|
2191
2509
|
return {
|
|
2192
2510
|
domContentLoaded: entry?.domContentLoadedEventEnd - entry?.domContentLoadedEventStart ?? 0,
|
|
2193
2511
|
loadComplete: entry?.loadEventEnd - entry?.loadEventStart ?? 0,
|
|
2194
|
-
domInteractive: entry?.domInteractive ?? 0,
|
|
2195
|
-
};
|
|
2196
|
-
});
|
|
2197
|
-
|
|
2198
|
-
// DOM \u5185\u5BB9\u52A0\u8F7D\u5E94\u5728 2s \u5185
|
|
2199
|
-
expect(performanceMetrics.domInteractive).toBeLessThan(2000);
|
|
2200
|
-
});
|
|
2201
|
-
|
|
2202
|
-
test('should not have memory leaks', async ({ page }) => {
|
|
2203
|
-
await page.goto('/');
|
|
2204
|
-
|
|
2205
|
-
const metrics = await page.metrics();
|
|
2206
|
-
expect(metrics.JSHeapUsedSize).toBeLessThan(50 * 1024 * 1024); // 50MB
|
|
2207
|
-
});
|
|
2208
|
-
});
|
|
2209
|
-
`
|
|
2210
|
-
};
|
|
2211
|
-
return templates[type];
|
|
2212
|
-
}
|
|
2213
|
-
function toCamelCase(str) {
|
|
2214
|
-
return str.replace(/[-_\s]+(.)?/g, (_, c) => c ? c.toUpperCase() : "").replace(/^[A-Z]/, (c) => c.toLowerCase());
|
|
2215
|
-
}
|
|
2216
|
-
function toPascalCase(str) {
|
|
2217
|
-
const camel = toCamelCase(str);
|
|
2218
|
-
return camel.charAt(0).toUpperCase() + camel.slice(1);
|
|
2219
|
-
}
|
|
2220
|
-
|
|
2221
|
-
// src/services/global-config.ts
|
|
2222
|
-
import fs6 from "fs";
|
|
2223
|
-
import path6 from "path";
|
|
2224
|
-
import os from "os";
|
|
2225
|
-
var QAT_DIR = path6.join(os.homedir(), ".qat");
|
|
2226
|
-
var AI_CONFIG_PATH = path6.join(QAT_DIR, "ai.json");
|
|
2227
|
-
function loadGlobalAIConfig() {
|
|
2228
|
-
if (!fs6.existsSync(AI_CONFIG_PATH)) {
|
|
2229
|
-
return null;
|
|
2230
|
-
}
|
|
2231
|
-
try {
|
|
2232
|
-
const content = fs6.readFileSync(AI_CONFIG_PATH, "utf-8");
|
|
2233
|
-
const config = JSON.parse(content);
|
|
2234
|
-
if (!config.baseUrl || !config.model) {
|
|
2235
|
-
return null;
|
|
2236
|
-
}
|
|
2237
|
-
return config;
|
|
2238
|
-
} catch {
|
|
2239
|
-
return null;
|
|
2240
|
-
}
|
|
2241
|
-
}
|
|
2242
|
-
function saveGlobalAIConfig(config) {
|
|
2243
|
-
if (!fs6.existsSync(QAT_DIR)) {
|
|
2244
|
-
fs6.mkdirSync(QAT_DIR, { recursive: true });
|
|
2245
|
-
}
|
|
2246
|
-
fs6.writeFileSync(AI_CONFIG_PATH, JSON.stringify(config, null, 2), "utf-8");
|
|
2247
|
-
}
|
|
2248
|
-
function toAIConfig(globalConfig) {
|
|
2249
|
-
return {
|
|
2250
|
-
provider: globalConfig.provider || "openai",
|
|
2251
|
-
apiKey: globalConfig.apiKey || void 0,
|
|
2252
|
-
baseUrl: globalConfig.baseUrl,
|
|
2253
|
-
model: globalConfig.model
|
|
2254
|
-
};
|
|
2255
|
-
}
|
|
2256
|
-
function maskApiKey(apiKey) {
|
|
2257
|
-
if (!apiKey) return "(\u672A\u8BBE\u7F6E)";
|
|
2258
|
-
if (apiKey.length <= 8) return "****";
|
|
2259
|
-
return apiKey.slice(0, 4) + "****" + apiKey.slice(-4);
|
|
2260
|
-
}
|
|
2261
|
-
function getAIConfigPath() {
|
|
2262
|
-
return AI_CONFIG_PATH;
|
|
2263
|
-
}
|
|
2512
|
+
domInteractive: entry?.domInteractive ?? 0,
|
|
2513
|
+
};
|
|
2514
|
+
});
|
|
2264
2515
|
|
|
2265
|
-
//
|
|
2266
|
-
|
|
2267
|
-
unit: "tests/unit",
|
|
2268
|
-
component: "tests/component",
|
|
2269
|
-
e2e: "tests/e2e",
|
|
2270
|
-
api: "tests/api",
|
|
2271
|
-
visual: "tests/visual",
|
|
2272
|
-
performance: "tests/e2e"
|
|
2273
|
-
};
|
|
2274
|
-
function inferTestType(filePath) {
|
|
2275
|
-
const normalized = filePath.replace(/\\/g, "/").toLowerCase();
|
|
2276
|
-
if (/\/api\//.test(normalized) || /api\.(ts|js)$/.test(normalized)) {
|
|
2277
|
-
return "api";
|
|
2278
|
-
}
|
|
2279
|
-
if (normalized.endsWith(".vue")) {
|
|
2280
|
-
return "component";
|
|
2281
|
-
}
|
|
2282
|
-
if (/\/composables?\//.test(normalized) || /\/hooks?\//.test(normalized) || /^use[A-Z]/.test(path7.basename(filePath))) {
|
|
2283
|
-
return "unit";
|
|
2284
|
-
}
|
|
2285
|
-
if (/\/(utils|helpers|services|lib)\//.test(normalized)) {
|
|
2286
|
-
return "unit";
|
|
2287
|
-
}
|
|
2288
|
-
return "unit";
|
|
2289
|
-
}
|
|
2290
|
-
function registerInitCommand(program2) {
|
|
2291
|
-
program2.command("init").description("\u521D\u59CB\u5316\u6D4B\u8BD5\u9879\u76EE - \u68C0\u6D4B\u9879\u76EE\u3001\u751F\u6210\u914D\u7F6E\u3001\u81EA\u52A8\u521B\u5EFA\u6D4B\u8BD5\u7528\u4F8B").option("-f, --force", "\u5F3A\u5236\u8986\u76D6\u5DF2\u6709\u914D\u7F6E\u6587\u4EF6").action(async (options) => {
|
|
2292
|
-
try {
|
|
2293
|
-
await executeInit(options);
|
|
2294
|
-
} catch (error) {
|
|
2295
|
-
console.error(chalk3.red(`
|
|
2296
|
-
\u2717 ${error instanceof Error ? error.message : String(error)}
|
|
2297
|
-
`));
|
|
2298
|
-
process.exit(1);
|
|
2299
|
-
}
|
|
2516
|
+
// DOM \u5185\u5BB9\u52A0\u8F7D\u5E94\u5728 2s \u5185
|
|
2517
|
+
expect(performanceMetrics.domInteractive).toBeLessThan(2000);
|
|
2300
2518
|
});
|
|
2301
|
-
|
|
2302
|
-
async
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
type: "confirm",
|
|
2311
|
-
name: "proceed",
|
|
2312
|
-
message: "\u672A\u68C0\u6D4B\u5230 Vue \u9879\u76EE\uFF0C\u662F\u5426\u7EE7\u7EED\u521D\u59CB\u5316\uFF1F",
|
|
2313
|
-
default: false
|
|
2314
|
-
}
|
|
2315
|
-
]);
|
|
2316
|
-
if (!proceed) {
|
|
2317
|
-
console.log(chalk3.gray("\n \u5DF2\u53D6\u6D88\u521D\u59CB\u5316\n"));
|
|
2318
|
-
return;
|
|
2319
|
-
}
|
|
2320
|
-
}
|
|
2321
|
-
let globalAI = loadGlobalAIConfig();
|
|
2322
|
-
if (!globalAI) {
|
|
2323
|
-
console.log(chalk3.cyan(" AI \u6A21\u578B\u914D\u7F6E (\u9996\u6B21\u4F7F\u7528\u9700\u914D\u7F6E\uFF0C\u4E4B\u540E\u53EF\u901A\u8FC7 qat change \u4FEE\u6539)\n"));
|
|
2324
|
-
globalAI = await promptAIConfig();
|
|
2325
|
-
saveGlobalAIConfig(globalAI);
|
|
2326
|
-
console.log(chalk3.gray(` \u914D\u7F6E\u5DF2\u4FDD\u5B58\u81F3 ${getAIConfigPath()}
|
|
2327
|
-
`));
|
|
2328
|
-
} else {
|
|
2329
|
-
console.log(chalk3.green(` \u2713 \u5F53\u524D AI \u6A21\u578B: ${chalk3.white(globalAI.model)} @ ${chalk3.gray(globalAI.baseUrl)} (${maskApiKey(globalAI.apiKey)})
|
|
2330
|
-
`));
|
|
2331
|
-
}
|
|
2332
|
-
const aiConfig = toAIConfig(globalAI);
|
|
2333
|
-
if (aiConfig.apiKey || aiConfig.baseUrl) {
|
|
2334
|
-
const testSpinner = ora2(`\u6B63\u5728\u6D4B\u8BD5 AI \u8FDE\u901A\u6027 (${globalAI.model})...`).start();
|
|
2335
|
-
try {
|
|
2336
|
-
const result = await testAIConnection(aiConfig);
|
|
2337
|
-
if (result.ok) {
|
|
2338
|
-
testSpinner.succeed(`AI \u8FDE\u901A\u6B63\u5E38 ${chalk3.gray(`${globalAI.model} (${result.latencyMs}ms)`)}`);
|
|
2339
|
-
} else {
|
|
2340
|
-
testSpinner.fail(`AI \u8FDE\u901A\u5F02\u5E38: ${result.message}`);
|
|
2341
|
-
console.log(chalk3.yellow(" \u53EF\u8FD0\u884C qat change \u4FEE\u6539 AI \u914D\u7F6E\u3002"));
|
|
2342
|
-
}
|
|
2343
|
-
} catch (error) {
|
|
2344
|
-
testSpinner.fail(`AI \u8FDE\u901A\u6D4B\u8BD5\u5931\u8D25: ${error instanceof Error ? error.message : String(error)}`);
|
|
2345
|
-
}
|
|
2346
|
-
}
|
|
2347
|
-
const config = buildProjectConfig(projectInfo);
|
|
2348
|
-
let configPath;
|
|
2349
|
-
const existingConfigPath = path7.join(process.cwd(), "qat.config.js");
|
|
2350
|
-
const existingTsPath = path7.join(process.cwd(), "qat.config.ts");
|
|
2351
|
-
const configExists = fs7.existsSync(existingConfigPath) || fs7.existsSync(existingTsPath);
|
|
2352
|
-
if (configExists && !options.force) {
|
|
2353
|
-
const { overwrite } = await inquirer.prompt([
|
|
2354
|
-
{
|
|
2355
|
-
type: "confirm",
|
|
2356
|
-
name: "overwrite",
|
|
2357
|
-
message: "\u914D\u7F6E\u6587\u4EF6 qat.config.js \u5DF2\u5B58\u5728\uFF0C\u662F\u5426\u8986\u76D6\uFF1F",
|
|
2358
|
-
default: true
|
|
2359
|
-
}
|
|
2360
|
-
]);
|
|
2361
|
-
if (!overwrite) {
|
|
2362
|
-
console.log(chalk3.gray(" \u4FDD\u7559\u73B0\u6709\u914D\u7F6E\u6587\u4EF6\uFF0C\u7EE7\u7EED\u540E\u7EED\u6B65\u9AA4..."));
|
|
2363
|
-
configPath = existingConfigPath;
|
|
2364
|
-
} else {
|
|
2365
|
-
const fileSpinner = ora2("\u6B63\u5728\u8986\u76D6\u914D\u7F6E\u6587\u4EF6...").start();
|
|
2366
|
-
try {
|
|
2367
|
-
configPath = await writeConfigFile(process.cwd(), config, true);
|
|
2368
|
-
fileSpinner.succeed("\u914D\u7F6E\u6587\u4EF6\u5DF2\u8986\u76D6");
|
|
2369
|
-
} catch (error) {
|
|
2370
|
-
fileSpinner.fail("\u914D\u7F6E\u6587\u4EF6\u8986\u76D6\u5931\u8D25");
|
|
2371
|
-
throw error;
|
|
2372
|
-
}
|
|
2373
|
-
}
|
|
2374
|
-
} else {
|
|
2375
|
-
const fileSpinner = ora2("\u6B63\u5728\u751F\u6210\u914D\u7F6E\u6587\u4EF6...").start();
|
|
2376
|
-
try {
|
|
2377
|
-
configPath = await writeConfigFile(process.cwd(), config, options.force);
|
|
2378
|
-
fileSpinner.succeed("\u914D\u7F6E\u6587\u4EF6\u5DF2\u751F\u6210");
|
|
2379
|
-
} catch (error) {
|
|
2380
|
-
fileSpinner.fail("\u914D\u7F6E\u6587\u4EF6\u751F\u6210\u5931\u8D25");
|
|
2381
|
-
throw error;
|
|
2382
|
-
}
|
|
2383
|
-
}
|
|
2384
|
-
const dirSpinner = ora2("\u6B63\u5728\u521B\u5EFA\u6D4B\u8BD5\u76EE\u5F55...").start();
|
|
2385
|
-
const createdDirs = createTestDirectories(config);
|
|
2386
|
-
dirSpinner.succeed("\u6D4B\u8BD5\u76EE\u5F55\u5DF2\u521B\u5EFA");
|
|
2387
|
-
if (config.mock?.enabled !== false) {
|
|
2388
|
-
const mockDir = config.mock?.routesDir || DEFAULT_CONFIG.mock.routesDir;
|
|
2389
|
-
initMockRoutesDir(mockDir);
|
|
2390
|
-
const srcDir = config.project?.srcDir || "src";
|
|
2391
|
-
const apiCalls = scanAPICalls(srcDir);
|
|
2392
|
-
if (apiCalls.length > 0) {
|
|
2393
|
-
const mockRoutes = generateMockRoutesFromAPICalls(apiCalls);
|
|
2394
|
-
const mockFilePath = path7.join(process.cwd(), mockDir, "auto-generated.json");
|
|
2395
|
-
if (!fs7.existsSync(mockFilePath)) {
|
|
2396
|
-
fs7.writeFileSync(mockFilePath, JSON.stringify(mockRoutes, null, 2), "utf-8");
|
|
2397
|
-
console.log(chalk3.green(` \u81EA\u52A8\u53D1\u73B0 ${apiCalls.length} \u4E2A API \u63A5\u53E3\uFF0C\u5DF2\u751F\u6210 Mock \u8DEF\u7531`));
|
|
2398
|
-
}
|
|
2399
|
-
} else {
|
|
2400
|
-
console.log(chalk3.gray(" \u672A\u53D1\u73B0 API \u8C03\u7528\uFF0C\u5DF2\u751F\u6210\u793A\u4F8B Mock \u8DEF\u7531"));
|
|
2401
|
-
}
|
|
2402
|
-
}
|
|
2403
|
-
const useAI = isAIAvailable(aiConfig);
|
|
2404
|
-
const generatedFiles = await autoGenerateTests(config, projectInfo, aiConfig, useAI);
|
|
2405
|
-
displayResult(configPath, createdDirs, generatedFiles, projectInfo);
|
|
2406
|
-
}
|
|
2407
|
-
async function promptAIConfig() {
|
|
2408
|
-
const answers = await inquirer.prompt([
|
|
2409
|
-
{
|
|
2410
|
-
type: "input",
|
|
2411
|
-
name: "apiKey",
|
|
2412
|
-
message: "API Key (Ollama \u672C\u5730\u53EF\u7559\u7A7A):",
|
|
2413
|
-
default: ""
|
|
2414
|
-
},
|
|
2415
|
-
{
|
|
2416
|
-
type: "input",
|
|
2417
|
-
name: "baseUrl",
|
|
2418
|
-
message: "API Base URL:",
|
|
2419
|
-
default: "https://api.deepseek.com/v1",
|
|
2420
|
-
validate: (input) => {
|
|
2421
|
-
if (!input.trim()) return "Base URL \u4E0D\u80FD\u4E3A\u7A7A";
|
|
2422
|
-
if (!input.trim().startsWith("http")) return "URL \u5FC5\u987B\u4EE5 http(s):// \u5F00\u5934";
|
|
2423
|
-
return true;
|
|
2424
|
-
}
|
|
2425
|
-
},
|
|
2426
|
-
{
|
|
2427
|
-
type: "input",
|
|
2428
|
-
name: "model",
|
|
2429
|
-
message: "\u6A21\u578B\u540D\u79F0:",
|
|
2430
|
-
default: "deepseek-chat",
|
|
2431
|
-
validate: (input) => {
|
|
2432
|
-
if (!input.trim()) return "\u6A21\u578B\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A";
|
|
2433
|
-
return true;
|
|
2434
|
-
}
|
|
2435
|
-
}
|
|
2436
|
-
]);
|
|
2437
|
-
return {
|
|
2438
|
-
provider: "openai",
|
|
2439
|
-
apiKey: answers.apiKey?.trim() || "",
|
|
2440
|
-
baseUrl: answers.baseUrl?.trim() || "https://api.deepseek.com/v1",
|
|
2441
|
-
model: answers.model?.trim() || "deepseek-chat"
|
|
2442
|
-
};
|
|
2443
|
-
}
|
|
2444
|
-
function buildProjectConfig(projectInfo) {
|
|
2445
|
-
return {
|
|
2446
|
-
project: {
|
|
2447
|
-
framework: projectInfo.framework,
|
|
2448
|
-
uiLibrary: projectInfo.uiLibrary !== "none" ? projectInfo.uiLibrary : void 0,
|
|
2449
|
-
monorepo: projectInfo.monorepo !== "none" ? projectInfo.monorepo : void 0,
|
|
2450
|
-
vite: projectInfo.isVite,
|
|
2451
|
-
srcDir: projectInfo.srcDir,
|
|
2452
|
-
appDir: projectInfo.appDirs.length > 0 ? projectInfo.appDirs[0] : void 0
|
|
2453
|
-
},
|
|
2454
|
-
vitest: {
|
|
2455
|
-
enabled: true,
|
|
2456
|
-
coverage: true,
|
|
2457
|
-
globals: true,
|
|
2458
|
-
environment: "happy-dom"
|
|
2459
|
-
},
|
|
2460
|
-
playwright: {
|
|
2461
|
-
enabled: true,
|
|
2462
|
-
browsers: ["chromium"],
|
|
2463
|
-
baseURL: "http://localhost:5173",
|
|
2464
|
-
screenshot: "only-on-failure"
|
|
2465
|
-
},
|
|
2466
|
-
visual: {
|
|
2467
|
-
enabled: true,
|
|
2468
|
-
threshold: 0.1,
|
|
2469
|
-
baselineDir: "tests/visual/baseline",
|
|
2470
|
-
diffDir: "tests/visual/diff"
|
|
2471
|
-
},
|
|
2472
|
-
lighthouse: {
|
|
2473
|
-
enabled: true,
|
|
2474
|
-
urls: ["http://localhost:5173"],
|
|
2475
|
-
runs: 3,
|
|
2476
|
-
thresholds: {
|
|
2477
|
-
performance: 80,
|
|
2478
|
-
accessibility: 90
|
|
2479
|
-
}
|
|
2480
|
-
},
|
|
2481
|
-
mock: {
|
|
2482
|
-
enabled: true,
|
|
2483
|
-
port: 3456,
|
|
2484
|
-
routesDir: "tests/mock/routes"
|
|
2485
|
-
}
|
|
2519
|
+
|
|
2520
|
+
test('should not have memory leaks', async ({ page }) => {
|
|
2521
|
+
await page.goto('/');
|
|
2522
|
+
|
|
2523
|
+
const metrics = await page.metrics();
|
|
2524
|
+
expect(metrics.JSHeapUsedSize).toBeLessThan(50 * 1024 * 1024); // 50MB
|
|
2525
|
+
});
|
|
2526
|
+
});
|
|
2527
|
+
`
|
|
2486
2528
|
};
|
|
2529
|
+
return templates[type];
|
|
2487
2530
|
}
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
const components = discoverVueComponents(process.cwd(), srcDir);
|
|
2491
|
-
const utilities = discoverUtilityFiles(process.cwd(), srcDir);
|
|
2492
|
-
if (components.length === 0 && utilities.length === 0) {
|
|
2493
|
-
console.log(chalk3.yellow("\n \u672A\u53D1\u73B0\u53EF\u6D4B\u8BD5\u7684\u6E90\u7801\u6587\u4EF6\uFF0C\u8DF3\u8FC7\u6D4B\u8BD5\u7528\u4F8B\u751F\u6210\u3002"));
|
|
2494
|
-
return [];
|
|
2495
|
-
}
|
|
2496
|
-
const allTargets = [];
|
|
2497
|
-
for (const compPath of components.slice(0, 30)) {
|
|
2498
|
-
allTargets.push({ filePath: compPath, testType: inferTestType(compPath) });
|
|
2499
|
-
}
|
|
2500
|
-
for (const utilPath of utilities.slice(0, 30)) {
|
|
2501
|
-
allTargets.push({ filePath: utilPath, testType: inferTestType(utilPath) });
|
|
2502
|
-
}
|
|
2503
|
-
const selectedTargets = await selectTargetFiles(allTargets);
|
|
2504
|
-
if (selectedTargets.length === 0) {
|
|
2505
|
-
console.log(chalk3.gray("\n \u672A\u9009\u62E9\u4EFB\u4F55\u6587\u4EF6\uFF0C\u8DF3\u8FC7\u6D4B\u8BD5\u7528\u4F8B\u751F\u6210\u3002"));
|
|
2506
|
-
return [];
|
|
2507
|
-
}
|
|
2508
|
-
const total = selectedTargets.length;
|
|
2509
|
-
const genSpinner = ora2(`\u6B63\u5728\u751F\u6210\u6D4B\u8BD5\u7528\u4F8B [0/${total}] ...`).start();
|
|
2510
|
-
const generatedFiles = [];
|
|
2511
|
-
const typeCount = {};
|
|
2512
|
-
const reviewReport = [];
|
|
2513
|
-
let current = 0;
|
|
2514
|
-
let failed = 0;
|
|
2515
|
-
for (const { filePath, testType } of selectedTargets) {
|
|
2516
|
-
current++;
|
|
2517
|
-
const fileLabel = path7.basename(filePath);
|
|
2518
|
-
genSpinner.text = `\u6B63\u5728\u751F\u6210\u6D4B\u8BD5\u7528\u4F8B [${current}/${total}] ${chalk3.cyan(fileLabel)} ...`;
|
|
2519
|
-
try {
|
|
2520
|
-
const result = await generateTestForTarget(
|
|
2521
|
-
testType,
|
|
2522
|
-
filePath,
|
|
2523
|
-
config,
|
|
2524
|
-
projectInfo,
|
|
2525
|
-
aiConfig,
|
|
2526
|
-
useAI
|
|
2527
|
-
);
|
|
2528
|
-
if (result) {
|
|
2529
|
-
generatedFiles.push(result.filePath);
|
|
2530
|
-
typeCount[testType] = (typeCount[testType] || 0) + 1;
|
|
2531
|
-
if (result.reviewEntry) {
|
|
2532
|
-
reviewReport.push(result.reviewEntry);
|
|
2533
|
-
}
|
|
2534
|
-
}
|
|
2535
|
-
} catch {
|
|
2536
|
-
failed++;
|
|
2537
|
-
}
|
|
2538
|
-
}
|
|
2539
|
-
if (generatedFiles.length > 0) {
|
|
2540
|
-
const summary = Object.entries(typeCount).map(([type, count]) => `${count} ${type}`).join(", ");
|
|
2541
|
-
const approvedCount = reviewReport.filter((r) => r.approved).length;
|
|
2542
|
-
let msg = `\u5DF2\u751F\u6210 ${generatedFiles.length}/${total} \u4E2A\u6D4B\u8BD5\u7528\u4F8B (${summary}) \u2014 AI \u5BA1\u8BA1 ${approvedCount}/${reviewReport.length} \u901A\u8FC7`;
|
|
2543
|
-
if (failed > 0) msg += chalk3.yellow(` ${failed} \u4E2A\u5931\u8D25`);
|
|
2544
|
-
genSpinner.succeed(msg);
|
|
2545
|
-
} else {
|
|
2546
|
-
genSpinner.warn(`\u672A\u751F\u6210\u6D4B\u8BD5\u7528\u4F8B (${failed} \u4E2A\u5931\u8D25)`);
|
|
2547
|
-
}
|
|
2548
|
-
if (reviewReport.length > 0) {
|
|
2549
|
-
printReviewReport(reviewReport);
|
|
2550
|
-
}
|
|
2551
|
-
return generatedFiles;
|
|
2552
|
-
}
|
|
2553
|
-
async function selectTargetFiles(allTargets) {
|
|
2554
|
-
const testTypeLabels = {
|
|
2555
|
-
unit: chalk3.blue("[unit]"),
|
|
2556
|
-
component: chalk3.magenta("[comp]"),
|
|
2557
|
-
e2e: chalk3.green("[e2e]"),
|
|
2558
|
-
api: chalk3.yellow("[api]"),
|
|
2559
|
-
visual: chalk3.cyan("[visual]"),
|
|
2560
|
-
performance: chalk3.gray("[perf]")
|
|
2561
|
-
};
|
|
2562
|
-
const choices = allTargets.map(({ filePath, testType }) => ({
|
|
2563
|
-
name: `${testTypeLabels[testType]} ${filePath}`,
|
|
2564
|
-
value: filePath,
|
|
2565
|
-
short: filePath
|
|
2566
|
-
}));
|
|
2567
|
-
choices.push({
|
|
2568
|
-
name: chalk3.gray("\u270E \u624B\u52A8\u8F93\u5165\u6587\u4EF6/\u76EE\u5F55\u8DEF\u5F84"),
|
|
2569
|
-
value: "__manual__",
|
|
2570
|
-
short: "\u624B\u52A8\u8F93\u5165"
|
|
2571
|
-
});
|
|
2572
|
-
const { selected } = await inquirer.prompt([
|
|
2573
|
-
{
|
|
2574
|
-
type: "checkbox",
|
|
2575
|
-
name: "selected",
|
|
2576
|
-
message: "\u9009\u62E9\u8981\u751F\u6210\u6D4B\u8BD5\u7528\u4F8B\u7684\u6587\u4EF6 (\u7A7A\u683C\u9009\u62E9/\u53D6\u6D88\uFF0C\u56DE\u8F66\u786E\u8BA4):",
|
|
2577
|
-
choices,
|
|
2578
|
-
pageSize: 15
|
|
2579
|
-
}
|
|
2580
|
-
]);
|
|
2581
|
-
const manualPaths = [];
|
|
2582
|
-
if (selected.includes("__manual__")) {
|
|
2583
|
-
const { manualInput } = await inquirer.prompt([
|
|
2584
|
-
{
|
|
2585
|
-
type: "input",
|
|
2586
|
-
name: "manualInput",
|
|
2587
|
-
message: "\u8F93\u5165\u6587\u4EF6\u6216\u76EE\u5F55\u8DEF\u5F84\uFF08\u591A\u4E2A\u7528\u9017\u53F7\u5206\u9694\uFF09:",
|
|
2588
|
-
default: "",
|
|
2589
|
-
filter: (input) => input.split(",").map((s) => s.trim()).filter(Boolean)
|
|
2590
|
-
}
|
|
2591
|
-
]);
|
|
2592
|
-
for (const p of manualInput) {
|
|
2593
|
-
const resolved = path7.resolve(process.cwd(), p);
|
|
2594
|
-
if (fs7.existsSync(resolved)) {
|
|
2595
|
-
const stat = fs7.statSync(resolved);
|
|
2596
|
-
if (stat.isDirectory()) {
|
|
2597
|
-
const dirFiles = walkDirForTestableFiles(resolved);
|
|
2598
|
-
manualPaths.push(...dirFiles);
|
|
2599
|
-
} else if (stat.isFile()) {
|
|
2600
|
-
manualPaths.push(p.replace(/\\/g, "/"));
|
|
2601
|
-
}
|
|
2602
|
-
} else {
|
|
2603
|
-
console.log(chalk3.yellow(` \u8DEF\u5F84\u4E0D\u5B58\u5728\uFF0C\u5DF2\u8DF3\u8FC7: ${p}`));
|
|
2604
|
-
}
|
|
2605
|
-
}
|
|
2606
|
-
}
|
|
2607
|
-
const selectedPaths = selected.filter((s) => s !== "__manual__");
|
|
2608
|
-
const selectedSet = new Set(selectedPaths);
|
|
2609
|
-
const result = allTargets.filter((t) => selectedSet.has(t.filePath));
|
|
2610
|
-
const existingPaths = new Set(result.map((t) => t.filePath));
|
|
2611
|
-
for (const mp of manualPaths) {
|
|
2612
|
-
if (!existingPaths.has(mp)) {
|
|
2613
|
-
result.push({ filePath: mp, testType: inferTestType(mp) });
|
|
2614
|
-
}
|
|
2615
|
-
}
|
|
2616
|
-
return result;
|
|
2531
|
+
function toCamelCase(str) {
|
|
2532
|
+
return str.replace(/[-_\s]+(.)?/g, (_, c) => c ? c.toUpperCase() : "").replace(/^[A-Z]/, (c) => c.toLowerCase());
|
|
2617
2533
|
}
|
|
2618
|
-
function
|
|
2619
|
-
const
|
|
2620
|
-
|
|
2621
|
-
const entries = fs7.readdirSync(dir, { withFileTypes: true });
|
|
2622
|
-
for (const entry of entries) {
|
|
2623
|
-
if (entry.name === "node_modules" || entry.name === "dist" || entry.name.startsWith(".")) continue;
|
|
2624
|
-
const fullPath = path7.join(dir, entry.name);
|
|
2625
|
-
if (entry.isDirectory()) {
|
|
2626
|
-
files.push(...walkDirForTestableFiles(fullPath));
|
|
2627
|
-
} else if (entry.isFile() && /\.(vue|ts|js)$/.test(entry.name)) {
|
|
2628
|
-
files.push(path7.relative(process.cwd(), fullPath).replace(/\\/g, "/"));
|
|
2629
|
-
}
|
|
2630
|
-
}
|
|
2631
|
-
} catch {
|
|
2632
|
-
}
|
|
2633
|
-
return files;
|
|
2534
|
+
function toPascalCase(str) {
|
|
2535
|
+
const camel = toCamelCase(str);
|
|
2536
|
+
return camel.charAt(0).toUpperCase() + camel.slice(1);
|
|
2634
2537
|
}
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2538
|
+
|
|
2539
|
+
// src/services/test-reviewer.ts
|
|
2540
|
+
import chalk3 from "chalk";
|
|
2541
|
+
import ora2 from "ora";
|
|
2542
|
+
var MAX_RETRIES = 3;
|
|
2543
|
+
var REVIEW_THRESHOLD = 0.6;
|
|
2544
|
+
async function generateWithReview(params) {
|
|
2545
|
+
const { testType, targetPath, sourceCode, analysis, aiConfig, framework, onAttempt } = params;
|
|
2546
|
+
const generatorProvider = createAIProvider(aiConfig);
|
|
2547
|
+
const reviewerProvider = createAIProvider(aiConfig);
|
|
2548
|
+
if (!generatorProvider.capabilities.generateTest) {
|
|
2549
|
+
throw new Error("\u5F53\u524D AI Provider \u4E0D\u652F\u6301\u6D4B\u8BD5\u751F\u6210");
|
|
2550
|
+
}
|
|
2551
|
+
let currentCode = "";
|
|
2552
|
+
let currentDescription = "";
|
|
2553
|
+
let currentConfidence = 0;
|
|
2554
|
+
let lastReview = null;
|
|
2555
|
+
let approved = false;
|
|
2556
|
+
let attempts = 0;
|
|
2557
|
+
for (let i = 0; i < MAX_RETRIES; i++) {
|
|
2558
|
+
attempts = i + 1;
|
|
2559
|
+
onAttempt?.(attempts, MAX_RETRIES);
|
|
2560
|
+
const generationPrompt = i === 0 ? void 0 : `\u4E0A\u6B21\u751F\u6210\u7684\u6D4B\u8BD5\u672A\u901A\u8FC7\u5BA1\u8BA1\uFF0C\u8BF7\u6839\u636E\u4EE5\u4E0B\u53CD\u9988\u91CD\u65B0\u751F\u6210\uFF1A
|
|
2561
|
+
\u5BA1\u8BA1\u8BC4\u5206: ${(lastReview.score * 100).toFixed(0)}%
|
|
2562
|
+
\u5BA1\u8BA1\u610F\u89C1: ${lastReview.feedback}
|
|
2563
|
+
\u5177\u4F53\u95EE\u9898:
|
|
2564
|
+
${lastReview.issues.map((issue) => `- ${issue}`).join("\n")}
|
|
2565
|
+
\u6539\u8FDB\u5EFA\u8BAE:
|
|
2566
|
+
${lastReview.suggestions.map((s) => `- ${s}`).join("\n")}
|
|
2567
|
+
|
|
2568
|
+
\u8BF7\u9488\u5BF9\u4EE5\u4E0A\u95EE\u9898\u91CD\u65B0\u751F\u6210\u66F4\u8D34\u5207\u3001\u66F4\u51C6\u786E\u7684\u6D4B\u8BD5\u7528\u4F8B\u3002`;
|
|
2569
|
+
const generateResponse = await generatorProvider.generateTest({
|
|
2570
|
+
type: testType,
|
|
2652
2571
|
target: targetPath,
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
globalStubs: projectInfo.componentTestSetup?.globalStubs,
|
|
2660
|
-
mountOptions: projectInfo.componentTestSetup?.mountOptions,
|
|
2661
|
-
exports: analysis.exports.map((e) => ({
|
|
2662
|
-
name: e.name,
|
|
2663
|
-
kind: e.kind,
|
|
2664
|
-
params: e.params,
|
|
2665
|
-
isAsync: e.isAsync,
|
|
2666
|
-
returnType: e.returnType
|
|
2667
|
-
})),
|
|
2668
|
-
props: analysis.vueAnalysis?.props.map((p) => ({
|
|
2669
|
-
name: p.name,
|
|
2670
|
-
type: p.type,
|
|
2671
|
-
required: p.required,
|
|
2672
|
-
defaultValue: p.defaultValue
|
|
2673
|
-
})),
|
|
2674
|
-
emits: analysis.vueAnalysis?.emits.map((e) => ({
|
|
2675
|
-
name: e.name,
|
|
2676
|
-
params: e.params
|
|
2677
|
-
})),
|
|
2678
|
-
methods: analysis.vueAnalysis?.methods,
|
|
2679
|
-
computed: analysis.vueAnalysis?.computed
|
|
2572
|
+
context: i === 0 ? sourceCode : `${sourceCode}
|
|
2573
|
+
|
|
2574
|
+
--- \u5BA1\u8BA1\u53CD\u9988 ---
|
|
2575
|
+
${generationPrompt}`,
|
|
2576
|
+
framework: framework || void 0,
|
|
2577
|
+
analysis
|
|
2680
2578
|
});
|
|
2579
|
+
currentCode = generateResponse.code;
|
|
2580
|
+
currentDescription = generateResponse.description;
|
|
2581
|
+
currentConfidence = generateResponse.confidence;
|
|
2582
|
+
const reviewRequest = {
|
|
2583
|
+
target: targetPath,
|
|
2584
|
+
sourceCode,
|
|
2585
|
+
analysis,
|
|
2586
|
+
testCode: currentCode,
|
|
2587
|
+
testType,
|
|
2588
|
+
generationDescription: currentDescription
|
|
2589
|
+
};
|
|
2590
|
+
lastReview = await reviewerProvider.reviewTest(reviewRequest);
|
|
2591
|
+
approved = lastReview.approved && lastReview.score >= REVIEW_THRESHOLD;
|
|
2592
|
+
if (approved) {
|
|
2593
|
+
break;
|
|
2594
|
+
}
|
|
2681
2595
|
}
|
|
2682
|
-
if (!fs7.existsSync(path7.join(process.cwd(), outputDir))) {
|
|
2683
|
-
fs7.mkdirSync(path7.join(process.cwd(), outputDir), { recursive: true });
|
|
2684
|
-
}
|
|
2685
|
-
fs7.writeFileSync(filePath, content, "utf-8");
|
|
2686
2596
|
return {
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
}
|
|
2697
|
-
const analysis = analyzeFile(target);
|
|
2698
|
-
const analysisSummary = analysis.exports.length > 0 || analysis.vueAnalysis ? {
|
|
2699
|
-
exports: analysis.exports.map((e) => ({
|
|
2700
|
-
name: e.name,
|
|
2701
|
-
kind: e.kind,
|
|
2702
|
-
params: e.params,
|
|
2703
|
-
isAsync: e.isAsync
|
|
2704
|
-
})),
|
|
2705
|
-
props: analysis.vueAnalysis?.props.map((p) => ({
|
|
2706
|
-
name: p.name,
|
|
2707
|
-
type: p.type,
|
|
2708
|
-
required: p.required
|
|
2709
|
-
})),
|
|
2710
|
-
emits: analysis.vueAnalysis?.emits.map((e) => ({
|
|
2711
|
-
name: e.name,
|
|
2712
|
-
params: e.params
|
|
2713
|
-
})),
|
|
2714
|
-
methods: analysis.vueAnalysis?.methods,
|
|
2715
|
-
computed: analysis.vueAnalysis?.computed
|
|
2716
|
-
} : void 0;
|
|
2717
|
-
const reviewResult = await generateWithReview({
|
|
2718
|
-
testType: type,
|
|
2719
|
-
targetPath: target,
|
|
2720
|
-
sourceCode,
|
|
2721
|
-
analysis: analysisSummary,
|
|
2722
|
-
aiConfig,
|
|
2723
|
-
framework: "vue"
|
|
2724
|
-
});
|
|
2725
|
-
const headerComment = [
|
|
2726
|
-
`// AI Generated Test - ${name}`,
|
|
2727
|
-
`// \u5BA1\u8BA1: ${reviewResult.approved ? "\u901A\u8FC7" : "\u672A\u901A\u8FC7"} (${(reviewResult.reviewScore * 100).toFixed(0)}%) \u2014 ${reviewResult.attempts}\u6B21\u5C1D\u8BD5`,
|
|
2728
|
-
reviewResult.reviewFeedback ? `// \u5BA1\u8BA1\u610F\u89C1: ${reviewResult.reviewFeedback}` : "",
|
|
2729
|
-
""
|
|
2730
|
-
].filter(Boolean).join("\n");
|
|
2731
|
-
const code = `${headerComment}
|
|
2732
|
-
${reviewResult.code}`;
|
|
2733
|
-
const reviewEntry = {
|
|
2734
|
-
target,
|
|
2735
|
-
testType: type,
|
|
2736
|
-
approved: reviewResult.approved,
|
|
2737
|
-
attempts: reviewResult.attempts,
|
|
2738
|
-
score: reviewResult.reviewScore,
|
|
2739
|
-
feedback: reviewResult.reviewFeedback,
|
|
2740
|
-
issues: reviewResult.approved ? [] : reviewResult.reviewIssues
|
|
2597
|
+
code: currentCode,
|
|
2598
|
+
description: currentDescription,
|
|
2599
|
+
confidence: currentConfidence,
|
|
2600
|
+
approved,
|
|
2601
|
+
attempts,
|
|
2602
|
+
reviewScore: lastReview?.score ?? 0,
|
|
2603
|
+
reviewFeedback: lastReview?.feedback ?? "",
|
|
2604
|
+
reviewIssues: lastReview?.issues ?? [],
|
|
2605
|
+
reviewSuggestions: lastReview?.suggestions ?? []
|
|
2741
2606
|
};
|
|
2742
|
-
return { code, reviewEntry };
|
|
2743
2607
|
}
|
|
2744
|
-
function
|
|
2745
|
-
|
|
2608
|
+
function printReviewReport(report) {
|
|
2609
|
+
if (report.length === 0) return;
|
|
2610
|
+
const approved = report.filter((r) => r.approved);
|
|
2611
|
+
const failed = report.filter((r) => !r.approved);
|
|
2612
|
+
console.log();
|
|
2613
|
+
console.log(chalk3.cyan(" \u5BA1\u8BA1\u62A5\u544A:"));
|
|
2746
2614
|
console.log(chalk3.gray(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
2747
|
-
const
|
|
2748
|
-
|
|
2749
|
-
["\u6846\u67B6", info.frameworkConfidence > 0.5 ? `${info.frameworkDisplayName} (\u7F6E\u4FE1\u5EA6 ${Math.round(info.frameworkConfidence * 100)}%)` : info.frameworkDisplayName],
|
|
2750
|
-
["Vue \u9879\u76EE", info.isVue ? `\u662F (v${info.vueVersion})` : "\u5426"],
|
|
2751
|
-
["UI \u7EC4\u4EF6\u5E93", info.uiLibrary !== "none" ? info.uiLibrary : "\u672A\u68C0\u6D4B\u5230"],
|
|
2752
|
-
["Vite \u6784\u5EFA", info.isVite ? "\u662F" : "\u5426"],
|
|
2753
|
-
["TypeScript", info.typescript ? "\u662F" : "\u5426"],
|
|
2754
|
-
["\u5305\u7BA1\u7406\u5668", info.packageManager],
|
|
2755
|
-
["Monorepo", info.monorepo !== "none" ? info.monorepo : "\u5426"],
|
|
2756
|
-
["\u6E90\u7801\u76EE\u5F55", info.srcDir]
|
|
2757
|
-
];
|
|
2758
|
-
if (info.appDirs.length > 0) {
|
|
2759
|
-
items.push(["\u5B50\u9879\u76EE", info.appDirs.join(", ")]);
|
|
2760
|
-
}
|
|
2761
|
-
if (info.testFrameworks.length > 0) {
|
|
2762
|
-
items.push(["\u5DF2\u6709\u6D4B\u8BD5\u6846\u67B6", info.testFrameworks.join(", ")]);
|
|
2763
|
-
}
|
|
2764
|
-
if (info.componentDirs.length > 0) {
|
|
2765
|
-
items.push(["\u7EC4\u4EF6\u76EE\u5F55", info.componentDirs.join(", ")]);
|
|
2766
|
-
}
|
|
2767
|
-
for (const [label, value] of items) {
|
|
2768
|
-
const displayValue = value === true ? chalk3.green("\u2713") : value === false ? chalk3.red("\u2717") : String(value);
|
|
2769
|
-
console.log(` ${chalk3.white(label.padEnd(12))} ${displayValue}`);
|
|
2615
|
+
for (const entry of approved) {
|
|
2616
|
+
console.log(` ${chalk3.green("\u2713")} ${chalk3.gray(entry.target)} ${chalk3.green(`(${(entry.score * 100).toFixed(0)}%`)}${entry.attempts > 1 ? chalk3.yellow(` ${entry.attempts}\u6B21\u5C1D\u8BD5`) : ""})`);
|
|
2770
2617
|
}
|
|
2771
|
-
|
|
2772
|
-
}
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
"tests": true,
|
|
2777
|
-
"tests/unit": config.vitest?.enabled !== false,
|
|
2778
|
-
"tests/component": config.vitest?.enabled !== false,
|
|
2779
|
-
"tests/e2e": config.playwright?.enabled !== false,
|
|
2780
|
-
"tests/api": config.mock?.enabled !== false,
|
|
2781
|
-
"tests/visual": config.visual?.enabled !== false,
|
|
2782
|
-
"tests/visual/baseline": config.visual?.enabled !== false,
|
|
2783
|
-
"tests/visual/diff": config.visual?.enabled !== false,
|
|
2784
|
-
"tests/mock": config.mock?.enabled !== false,
|
|
2785
|
-
"tests/mock/routes": config.mock?.enabled !== false
|
|
2786
|
-
};
|
|
2787
|
-
for (const [dir, shouldCreate] of Object.entries(dirMap)) {
|
|
2788
|
-
if (shouldCreate) {
|
|
2789
|
-
const fullPath = path7.join(process.cwd(), dir);
|
|
2790
|
-
if (!fs7.existsSync(fullPath)) {
|
|
2791
|
-
fs7.mkdirSync(fullPath, { recursive: true });
|
|
2792
|
-
dirs.push(dir);
|
|
2618
|
+
for (const entry of failed) {
|
|
2619
|
+
console.log(` ${chalk3.red("\u2717")} ${chalk3.gray(entry.target)} ${chalk3.red(`(${(entry.score * 100).toFixed(0)}%)`)}`);
|
|
2620
|
+
if (entry.issues.length > 0) {
|
|
2621
|
+
for (const issue of entry.issues.slice(0, 3)) {
|
|
2622
|
+
console.log(chalk3.gray(` - ${issue}`));
|
|
2793
2623
|
}
|
|
2794
2624
|
}
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
}
|
|
2798
|
-
function displayResult(configPath, createdDirs, generatedFiles, projectInfo) {
|
|
2799
|
-
const relativeConfig = path7.relative(process.cwd(), configPath);
|
|
2800
|
-
console.log(chalk3.green("\n \u2713 \u9879\u76EE\u521D\u59CB\u5316\u5B8C\u6210!\n"));
|
|
2801
|
-
console.log(chalk3.white(" \u5DF2\u751F\u6210\u914D\u7F6E:"));
|
|
2802
|
-
console.log(chalk3.gray(` ${relativeConfig}`));
|
|
2803
|
-
console.log();
|
|
2804
|
-
if (generatedFiles.length > 0) {
|
|
2805
|
-
console.log(chalk3.white(" \u5DF2\u751F\u6210\u6D4B\u8BD5\u7528\u4F8B:"));
|
|
2806
|
-
for (const file of generatedFiles) {
|
|
2807
|
-
const typeLabel = file.includes("/unit/") ? chalk3.blue("[unit]") : file.includes("/component/") ? chalk3.magenta("[comp]") : file.includes("/api/") ? chalk3.yellow("[api]") : chalk3.gray("[other]");
|
|
2808
|
-
console.log(` ${typeLabel} ${chalk3.gray(file)}`);
|
|
2625
|
+
if (entry.feedback) {
|
|
2626
|
+
console.log(chalk3.gray(` \u53CD\u9988: ${entry.feedback}`));
|
|
2809
2627
|
}
|
|
2810
|
-
console.log();
|
|
2811
|
-
}
|
|
2812
|
-
console.log(chalk3.cyan(" \u4E0B\u4E00\u6B65:"));
|
|
2813
|
-
if (generatedFiles.length > 0) {
|
|
2814
|
-
console.log(chalk3.gray(" 1. qat run \u6267\u884C\u6D4B\u8BD5"));
|
|
2815
|
-
console.log(chalk3.gray(" 2. qat create \u6DFB\u52A0\u66F4\u591A\u6D4B\u8BD5\u7528\u4F8B"));
|
|
2816
|
-
console.log(chalk3.gray(" 3. qat status \u67E5\u770B AI \u6A21\u578B\u72B6\u6001"));
|
|
2817
|
-
} else {
|
|
2818
|
-
console.log(chalk3.gray(" 1. qat change \u914D\u7F6E AI \u6A21\u578B"));
|
|
2819
|
-
console.log(chalk3.gray(" 2. qat create \u521B\u5EFA\u6D4B\u8BD5\u7528\u4F8B"));
|
|
2820
|
-
}
|
|
2821
|
-
if (projectInfo.testFrameworks.length === 0) {
|
|
2822
|
-
console.log();
|
|
2823
|
-
console.log(chalk3.yellow(" \u26A0 \u672A\u68C0\u6D4B\u5230\u6D4B\u8BD5\u6846\u67B6\u4F9D\u8D56\uFF0C\u5EFA\u8BAE\u8FD0\u884C:"));
|
|
2824
|
-
console.log(chalk3.gray(` qat setup`));
|
|
2825
2628
|
}
|
|
2826
2629
|
console.log();
|
|
2827
2630
|
}
|
|
2828
2631
|
|
|
2829
2632
|
// src/commands/create.ts
|
|
2830
|
-
import chalk4 from "chalk";
|
|
2831
|
-
import inquirer2 from "inquirer";
|
|
2832
|
-
import ora3 from "ora";
|
|
2833
|
-
import fs8 from "fs";
|
|
2834
|
-
import path8 from "path";
|
|
2835
2633
|
var TEST_TYPE_LABELS = {
|
|
2836
2634
|
unit: "\u5355\u5143\u6D4B\u8BD5",
|
|
2837
2635
|
component: "\u7EC4\u4EF6\u6D4B\u8BD5",
|
|
@@ -3169,18 +2967,29 @@ function displayCreateResult(createdFiles, skippedCount, usedAI) {
|
|
|
3169
2967
|
import chalk5 from "chalk";
|
|
3170
2968
|
import inquirer3 from "inquirer";
|
|
3171
2969
|
import ora4 from "ora";
|
|
3172
|
-
import
|
|
3173
|
-
import
|
|
2970
|
+
import fs11 from "fs";
|
|
2971
|
+
import path12 from "path";
|
|
3174
2972
|
|
|
3175
2973
|
// src/runners/vitest-runner.ts
|
|
3176
2974
|
import { execFile } from "child_process";
|
|
3177
2975
|
import path9 from "path";
|
|
2976
|
+
import fs9 from "fs";
|
|
2977
|
+
import os2 from "os";
|
|
2978
|
+
var isVerbose = () => process.env.QAT_VERBOSE === "true";
|
|
2979
|
+
function debug(label, ...args) {
|
|
2980
|
+
if (isVerbose()) {
|
|
2981
|
+
console.log(`\x1B[90m [debug:${label}]\x1B[0m`, ...args);
|
|
2982
|
+
}
|
|
2983
|
+
}
|
|
3178
2984
|
async function runVitest(options) {
|
|
3179
2985
|
const startTime = Date.now();
|
|
3180
2986
|
const args = buildVitestArgs(options);
|
|
2987
|
+
debug("vitest", "\u547D\u4EE4\u53C2\u6570:", args.join(" "));
|
|
3181
2988
|
try {
|
|
3182
2989
|
const result = await execVitest(args);
|
|
3183
2990
|
const endTime = Date.now();
|
|
2991
|
+
debug("vitest", `\u89E3\u6790\u7ED3\u679C: ${result.suites.length} \u4E2A\u5957\u4EF6, ${result.suites.reduce((s, su) => s + su.tests.length, 0)} \u4E2A\u7528\u4F8B`);
|
|
2992
|
+
debug("vitest", "\u89E3\u6790\u65B9\u5F0F:", result.parseMethod);
|
|
3184
2993
|
return {
|
|
3185
2994
|
type: options.type,
|
|
3186
2995
|
status: result.success ? "passed" : "failed",
|
|
@@ -3223,14 +3032,14 @@ function buildVitestArgs(options) {
|
|
|
3223
3032
|
if (options.files && options.files.length > 0) {
|
|
3224
3033
|
args.push(...options.files);
|
|
3225
3034
|
} else {
|
|
3226
|
-
const
|
|
3227
|
-
unit:
|
|
3228
|
-
component:
|
|
3229
|
-
api:
|
|
3035
|
+
const pathMap = {
|
|
3036
|
+
unit: "tests/unit/**/*.test.ts",
|
|
3037
|
+
component: "tests/component/**/*.test.ts",
|
|
3038
|
+
api: "tests/api/**/*.test.ts"
|
|
3230
3039
|
};
|
|
3231
|
-
const
|
|
3232
|
-
if (
|
|
3233
|
-
args.push(
|
|
3040
|
+
const testPattern = pathMap[options.type];
|
|
3041
|
+
if (testPattern) {
|
|
3042
|
+
args.push(testPattern);
|
|
3234
3043
|
}
|
|
3235
3044
|
}
|
|
3236
3045
|
if (options.coverage) {
|
|
@@ -3245,12 +3054,11 @@ function buildVitestArgs(options) {
|
|
|
3245
3054
|
return args;
|
|
3246
3055
|
}
|
|
3247
3056
|
async function execVitest(args) {
|
|
3248
|
-
const os2 = await import("os");
|
|
3249
|
-
const fs15 = await import("fs");
|
|
3250
3057
|
const tmpFile = path9.join(os2.tmpdir(), `qat-vitest-result-${Date.now()}.json`);
|
|
3251
3058
|
const argsWithOutput = [...args, "--outputFile", tmpFile];
|
|
3252
3059
|
return new Promise((resolve, reject) => {
|
|
3253
3060
|
const npx = process.platform === "win32" ? "npx.cmd" : "npx";
|
|
3061
|
+
debug("vitest", "\u6267\u884C\u547D\u4EE4:", npx, argsWithOutput.join(" "));
|
|
3254
3062
|
const child = execFile(npx, argsWithOutput, {
|
|
3255
3063
|
cwd: process.cwd(),
|
|
3256
3064
|
env: { ...process.env, FORCE_COLOR: "0", NO_COLOR: "1" },
|
|
@@ -3258,85 +3066,113 @@ async function execVitest(args) {
|
|
|
3258
3066
|
shell: true
|
|
3259
3067
|
}, (error, stdout, stderr) => {
|
|
3260
3068
|
const rawOutput = stdout || stderr || "";
|
|
3069
|
+
const exitCode = error && "code" in error ? error.code : 0;
|
|
3070
|
+
debug("vitest", `\u9000\u51FA\u7801: ${exitCode}`);
|
|
3071
|
+
debug("vitest", `stdout \u957F\u5EA6: ${stdout?.length || 0}, stderr \u957F\u5EA6: ${stderr?.length || 0}`);
|
|
3261
3072
|
let jsonResult = null;
|
|
3262
3073
|
try {
|
|
3263
|
-
if (
|
|
3264
|
-
jsonResult =
|
|
3265
|
-
|
|
3074
|
+
if (fs9.existsSync(tmpFile)) {
|
|
3075
|
+
jsonResult = fs9.readFileSync(tmpFile, "utf-8");
|
|
3076
|
+
debug("vitest", `\u4ECE\u4E34\u65F6\u6587\u4EF6\u8BFB\u53D6\u5230 JSON (${jsonResult.length} \u5B57\u7B26)`);
|
|
3077
|
+
debug("vitest", "JSON \u524D 500 \u5B57\u7B26:", jsonResult.substring(0, 500));
|
|
3078
|
+
fs9.unlinkSync(tmpFile);
|
|
3079
|
+
} else {
|
|
3080
|
+
debug("vitest", "\u4E34\u65F6\u6587\u4EF6\u4E0D\u5B58\u5728:", tmpFile);
|
|
3266
3081
|
}
|
|
3267
|
-
} catch {
|
|
3082
|
+
} catch (e) {
|
|
3083
|
+
debug("vitest", "\u4E34\u65F6\u6587\u4EF6\u8BFB\u53D6\u5931\u8D25:", e instanceof Error ? e.message : String(e));
|
|
3268
3084
|
}
|
|
3269
3085
|
if (jsonResult) {
|
|
3270
3086
|
try {
|
|
3271
|
-
const parsed =
|
|
3272
|
-
|
|
3087
|
+
const parsed = parseVitestJSON(jsonResult);
|
|
3088
|
+
debug("vitest", "\u4ECE\u4E34\u65F6\u6587\u4EF6\u89E3\u6790\u6210\u529F:", parsed.suites.length, "\u4E2A\u5957\u4EF6");
|
|
3089
|
+
resolve({ ...parsed, rawOutput, parseMethod: "outputFile-JSON" });
|
|
3273
3090
|
return;
|
|
3274
|
-
} catch {
|
|
3091
|
+
} catch (e) {
|
|
3092
|
+
debug("vitest", "\u4E34\u65F6\u6587\u4EF6 JSON \u89E3\u6790\u5931\u8D25:", e instanceof Error ? e.message : String(e));
|
|
3275
3093
|
}
|
|
3276
3094
|
}
|
|
3095
|
+
debug("vitest", "\u5C1D\u8BD5\u4ECE stdout \u63D0\u53D6 JSON...");
|
|
3277
3096
|
try {
|
|
3278
|
-
const parsed =
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3097
|
+
const parsed = parseFromStdout(rawOutput);
|
|
3098
|
+
debug("vitest", "\u4ECE stdout \u89E3\u6790\u6210\u529F:", parsed.suites.length, "\u4E2A\u5957\u4EF6");
|
|
3099
|
+
resolve({ ...parsed, rawOutput, parseMethod: "stdout-JSON" });
|
|
3100
|
+
return;
|
|
3101
|
+
} catch (e) {
|
|
3102
|
+
debug("vitest", "stdout JSON \u89E3\u6790\u5931\u8D25:", e instanceof Error ? e.message : String(e));
|
|
3103
|
+
}
|
|
3104
|
+
debug("vitest", "\u5C1D\u8BD5\u4ECE\u6587\u672C\u8F93\u51FA\u89E3\u6790...");
|
|
3105
|
+
if (rawOutput) {
|
|
3106
|
+
const parsed = parseVitestTextOutput(rawOutput, !!error);
|
|
3107
|
+
debug("vitest", "\u6587\u672C\u89E3\u6790\u7ED3\u679C:", parsed.suites.length, "\u4E2A\u5957\u4EF6");
|
|
3108
|
+
resolve({ ...parsed, rawOutput, parseMethod: "text-fallback" });
|
|
3109
|
+
return;
|
|
3110
|
+
}
|
|
3111
|
+
if (error && error.message.includes("ENOENT")) {
|
|
3112
|
+
reject(new Error("\u672A\u627E\u5230 vitest\uFF0C\u8BF7\u786E\u4FDD\u5DF2\u5B89\u88C5: npm install -D vitest"));
|
|
3113
|
+
return;
|
|
3288
3114
|
}
|
|
3115
|
+
debug("vitest", "\u6240\u6709\u89E3\u6790\u65B9\u5F0F\u5747\u5931\u8D25\uFF0C\u8FD4\u56DE\u7A7A\u7ED3\u679C");
|
|
3116
|
+
resolve({ success: !error, suites: [], rawOutput, parseMethod: "none" });
|
|
3289
3117
|
});
|
|
3290
3118
|
child.on("error", (err) => {
|
|
3291
3119
|
try {
|
|
3292
|
-
|
|
3120
|
+
fs9.unlinkSync(tmpFile);
|
|
3293
3121
|
} catch {
|
|
3294
3122
|
}
|
|
3295
3123
|
reject(new Error(`Vitest \u6267\u884C\u5931\u8D25: ${err.message}`));
|
|
3296
3124
|
});
|
|
3297
3125
|
});
|
|
3298
3126
|
}
|
|
3299
|
-
function
|
|
3127
|
+
function parseVitestJSON(jsonStr) {
|
|
3300
3128
|
const data = JSON.parse(jsonStr);
|
|
3301
3129
|
const suites = [];
|
|
3130
|
+
debug("vitest-json", "JSON \u9876\u5C42\u5B57\u6BB5:", Object.keys(data).join(", "));
|
|
3302
3131
|
if (data.testResults && Array.isArray(data.testResults)) {
|
|
3132
|
+
debug("vitest-json", `testResults \u6570\u91CF: ${data.testResults.length}`);
|
|
3303
3133
|
for (const fileResult of data.testResults) {
|
|
3134
|
+
const suiteTests = parseTestResults(fileResult);
|
|
3135
|
+
suites.push({
|
|
3136
|
+
name: path9.basename(fileResult.name || "unknown"),
|
|
3137
|
+
file: fileResult.name || "unknown",
|
|
3138
|
+
type: "unit",
|
|
3139
|
+
status: mapVitestStatus(fileResult.status),
|
|
3140
|
+
duration: fileResult.duration || 0,
|
|
3141
|
+
tests: suiteTests
|
|
3142
|
+
});
|
|
3143
|
+
}
|
|
3144
|
+
}
|
|
3145
|
+
if (suites.length === 0 && data.numTotalTests !== void 0) {
|
|
3146
|
+
debug("vitest-json", `\u4F7F\u7528\u6C47\u603B\u683C\u5F0F: total=${data.numTotalTests}, passed=${data.numPassedTests}`);
|
|
3147
|
+
suites.push({
|
|
3148
|
+
name: "Vitest Results",
|
|
3149
|
+
file: "unknown",
|
|
3150
|
+
type: "unit",
|
|
3151
|
+
status: data.numFailedTests > 0 ? "failed" : "passed",
|
|
3152
|
+
duration: 0,
|
|
3153
|
+
tests: buildTestsFromSummary(data)
|
|
3154
|
+
});
|
|
3155
|
+
}
|
|
3156
|
+
if (suites.length === 0 && data.suites && Array.isArray(data.suites)) {
|
|
3157
|
+
debug("vitest-json", `suites \u6570\u91CF: ${data.suites.length}`);
|
|
3158
|
+
for (const suiteData of data.suites) {
|
|
3304
3159
|
const suiteTests = [];
|
|
3305
|
-
const
|
|
3306
|
-
for (const assertion of assertions) {
|
|
3160
|
+
for (const test of suiteData.tests || []) {
|
|
3307
3161
|
suiteTests.push({
|
|
3308
|
-
name:
|
|
3309
|
-
file:
|
|
3310
|
-
status: mapVitestStatus(
|
|
3311
|
-
duration:
|
|
3312
|
-
error:
|
|
3162
|
+
name: test.name || test.title || "unknown",
|
|
3163
|
+
file: test.file || suiteData.file || "unknown",
|
|
3164
|
+
status: mapVitestStatus(test.status || test.result?.status),
|
|
3165
|
+
duration: test.duration || test.result?.duration || 0,
|
|
3166
|
+
error: test.result?.errors?.[0] ? { message: test.result.errors[0].message || String(test.result.errors[0]) } : void 0,
|
|
3313
3167
|
retries: 0
|
|
3314
3168
|
});
|
|
3315
3169
|
}
|
|
3316
|
-
if (suiteTests.length === 0 && fileResult.numPassingTests !== void 0) {
|
|
3317
|
-
const counts = [
|
|
3318
|
-
{ n: fileResult.numPassingTests || 0, s: "passed" },
|
|
3319
|
-
{ n: fileResult.numFailingTests || 0, s: "failed" },
|
|
3320
|
-
{ n: fileResult.numPendingTests || 0, s: "skipped" }
|
|
3321
|
-
];
|
|
3322
|
-
for (const { n, s } of counts) {
|
|
3323
|
-
for (let i = 0; i < n; i++) {
|
|
3324
|
-
suiteTests.push({
|
|
3325
|
-
name: `${s} test ${i + 1}`,
|
|
3326
|
-
file: fileResult.name || "unknown",
|
|
3327
|
-
status: s,
|
|
3328
|
-
duration: 0,
|
|
3329
|
-
retries: 0
|
|
3330
|
-
});
|
|
3331
|
-
}
|
|
3332
|
-
}
|
|
3333
|
-
}
|
|
3334
3170
|
suites.push({
|
|
3335
|
-
name:
|
|
3336
|
-
file:
|
|
3171
|
+
name: suiteData.name || "unknown",
|
|
3172
|
+
file: suiteData.file || "unknown",
|
|
3337
3173
|
type: "unit",
|
|
3338
|
-
status:
|
|
3339
|
-
duration:
|
|
3174
|
+
status: suiteTests.some((t) => t.status === "failed") ? "failed" : "passed",
|
|
3175
|
+
duration: 0,
|
|
3340
3176
|
tests: suiteTests
|
|
3341
3177
|
});
|
|
3342
3178
|
}
|
|
@@ -3345,58 +3181,101 @@ function parseVitestJSONResult(jsonStr) {
|
|
|
3345
3181
|
if (data.coverageMap) {
|
|
3346
3182
|
coverage = extractCoverage(data.coverageMap);
|
|
3347
3183
|
}
|
|
3348
|
-
|
|
3184
|
+
if (!coverage && data.coverage && typeof data.coverage === "object") {
|
|
3185
|
+
const cov = data.coverage;
|
|
3186
|
+
const totals = cov.totals || cov;
|
|
3187
|
+
const getVal = (key) => {
|
|
3188
|
+
const v = totals[key];
|
|
3189
|
+
return typeof v === "number" ? v : typeof v === "object" && v !== null && "pct" in v ? v.pct / 100 : 0;
|
|
3190
|
+
};
|
|
3191
|
+
coverage = {
|
|
3192
|
+
lines: getVal("lines"),
|
|
3193
|
+
statements: getVal("statements"),
|
|
3194
|
+
functions: getVal("functions"),
|
|
3195
|
+
branches: getVal("branches")
|
|
3196
|
+
};
|
|
3197
|
+
}
|
|
3198
|
+
const success = data.success !== false ? data.numFailedTests !== void 0 ? data.numFailedTests === 0 : suites.every((s) => s.status !== "failed") : false;
|
|
3349
3199
|
return { success, suites, coverage };
|
|
3350
3200
|
}
|
|
3351
|
-
function
|
|
3352
|
-
const
|
|
3353
|
-
|
|
3354
|
-
|
|
3201
|
+
function parseTestResults(fileResult) {
|
|
3202
|
+
const tests = [];
|
|
3203
|
+
const assertions = fileResult.assertionResults || fileResult.tests || [];
|
|
3204
|
+
for (const assertion of assertions) {
|
|
3205
|
+
tests.push({
|
|
3206
|
+
name: assertion.title || assertion.fullName || assertion.name || "unknown",
|
|
3207
|
+
file: fileResult.name || "unknown",
|
|
3208
|
+
status: mapVitestStatus(assertion.status),
|
|
3209
|
+
duration: assertion.duration || 0,
|
|
3210
|
+
error: assertion.failureMessages?.length ? { message: assertion.failureMessages[0] } : assertion.failureMessage ? { message: assertion.failureMessage } : void 0,
|
|
3211
|
+
retries: 0
|
|
3212
|
+
});
|
|
3355
3213
|
}
|
|
3356
|
-
|
|
3214
|
+
if (tests.length === 0 && fileResult.numPassingTests !== void 0) {
|
|
3215
|
+
tests.push(...buildTestsFromSummary(fileResult));
|
|
3216
|
+
}
|
|
3217
|
+
return tests;
|
|
3218
|
+
}
|
|
3219
|
+
function buildTestsFromSummary(data) {
|
|
3220
|
+
const tests = [];
|
|
3221
|
+
const counts = [
|
|
3222
|
+
[data.numPassedTests || 0, "passed"],
|
|
3223
|
+
[data.numFailedTests || 0, "failed"],
|
|
3224
|
+
[data.numPendingTests || 0, "skipped"]
|
|
3225
|
+
];
|
|
3226
|
+
for (const [n, s] of counts) {
|
|
3227
|
+
for (let i = 0; i < n; i++) {
|
|
3228
|
+
tests.push({
|
|
3229
|
+
name: `${s} test ${i + 1}`,
|
|
3230
|
+
file: data.name || "unknown",
|
|
3231
|
+
status: s,
|
|
3232
|
+
duration: 0,
|
|
3233
|
+
retries: 0
|
|
3234
|
+
});
|
|
3235
|
+
}
|
|
3236
|
+
}
|
|
3237
|
+
return tests;
|
|
3238
|
+
}
|
|
3239
|
+
function parseFromStdout(output) {
|
|
3240
|
+
const jsonMatch = output.match(/\{[\s\S]*"testResults"[\s\S]*\}/);
|
|
3241
|
+
if (jsonMatch) {
|
|
3357
3242
|
const data = JSON.parse(jsonMatch[0]);
|
|
3358
3243
|
const suites = [];
|
|
3359
3244
|
if (data.testResults && Array.isArray(data.testResults)) {
|
|
3360
3245
|
for (const fileResult of data.testResults) {
|
|
3361
|
-
|
|
3362
|
-
name: path9.basename(fileResult.name ||
|
|
3246
|
+
suites.push({
|
|
3247
|
+
name: path9.basename(fileResult.name || "unknown"),
|
|
3363
3248
|
file: fileResult.name || "unknown",
|
|
3364
3249
|
type: "unit",
|
|
3365
3250
|
status: mapVitestStatus(fileResult.status),
|
|
3366
3251
|
duration: fileResult.duration || 0,
|
|
3367
|
-
tests: (fileResult
|
|
3368
|
-
|
|
3369
|
-
file: fileResult.name || "unknown",
|
|
3370
|
-
status: mapVitestStatus(assertion.status),
|
|
3371
|
-
duration: assertion.duration || 0,
|
|
3372
|
-
error: assertion.failureMessages?.length ? { message: assertion.failureMessages[0] } : void 0,
|
|
3373
|
-
retries: 0
|
|
3374
|
-
}))
|
|
3375
|
-
};
|
|
3376
|
-
suites.push(suite);
|
|
3252
|
+
tests: parseTestResults(fileResult)
|
|
3253
|
+
});
|
|
3377
3254
|
}
|
|
3378
3255
|
}
|
|
3379
|
-
let coverage;
|
|
3380
|
-
if (data.coverageMap) {
|
|
3381
|
-
coverage = extractCoverage(data.coverageMap);
|
|
3382
|
-
}
|
|
3383
3256
|
const success = data.success !== false && suites.every((s) => s.status !== "failed");
|
|
3257
|
+
let coverage;
|
|
3258
|
+
if (data.coverageMap) coverage = extractCoverage(data.coverageMap);
|
|
3384
3259
|
return { success, suites, coverage };
|
|
3385
|
-
} catch {
|
|
3386
|
-
return parseVitestTextOutput(output, false);
|
|
3387
3260
|
}
|
|
3261
|
+
const anyJsonMatch = output.match(/\{[\s\S]*"numTotalTests"[\s\S]*\}/);
|
|
3262
|
+
if (anyJsonMatch) {
|
|
3263
|
+
return parseVitestJSON(anyJsonMatch[0]);
|
|
3264
|
+
}
|
|
3265
|
+
throw new Error("stdout \u4E2D\u672A\u627E\u5230\u6709\u6548 JSON");
|
|
3388
3266
|
}
|
|
3389
3267
|
function parseVitestTextOutput(output, hasError) {
|
|
3390
3268
|
const suites = [];
|
|
3269
|
+
debug("vitest-text", "\u6587\u672C\u8F93\u51FA\u524D 1000 \u5B57\u7B26:", output.substring(0, 1e3));
|
|
3270
|
+
const suiteRegex = /[✓✗×✕]\s+(.+\.test\.(ts|js)|.+\.spec\.(ts|js))\s*\((\d+)[^)]*\)/i;
|
|
3271
|
+
const lines = output.split("\n");
|
|
3391
3272
|
let totalPassed = 0;
|
|
3392
3273
|
let totalFailed = 0;
|
|
3393
|
-
const suiteRegex = /[✓✗×]\s+(.+\.test\.ts|.+\.spec\.ts)\s*\((\d+)\s+test/i;
|
|
3394
|
-
const lines = output.split("\n");
|
|
3395
3274
|
for (const line of lines) {
|
|
3396
3275
|
const match = line.match(suiteRegex);
|
|
3397
3276
|
if (match) {
|
|
3398
3277
|
const file = match[1];
|
|
3399
|
-
const testCount = parseInt(match[
|
|
3278
|
+
const testCount = parseInt(match[4], 10);
|
|
3400
3279
|
const isPassed = line.includes("\u2713");
|
|
3401
3280
|
if (isPassed) totalPassed += testCount;
|
|
3402
3281
|
else totalFailed += testCount;
|
|
@@ -3408,23 +3287,47 @@ function parseVitestTextOutput(output, hasError) {
|
|
|
3408
3287
|
duration: 0,
|
|
3409
3288
|
tests: Array.from({ length: testCount }, (_, i) => ({
|
|
3410
3289
|
name: `test ${i + 1}`,
|
|
3411
|
-
file,
|
|
3412
|
-
status: isPassed ? "passed" : "failed",
|
|
3290
|
+
file,
|
|
3291
|
+
status: isPassed ? "passed" : "failed",
|
|
3292
|
+
duration: 0,
|
|
3293
|
+
retries: 0
|
|
3294
|
+
}))
|
|
3295
|
+
});
|
|
3296
|
+
}
|
|
3297
|
+
}
|
|
3298
|
+
if (suites.length === 0) {
|
|
3299
|
+
const summaryMatch = output.match(/Tests\s+(\d+)\s+(passed|failed)/i);
|
|
3300
|
+
if (summaryMatch) {
|
|
3301
|
+
const count = parseInt(summaryMatch[1], 10);
|
|
3302
|
+
const status = summaryMatch[2].toLowerCase() === "passed" ? "passed" : "failed";
|
|
3303
|
+
suites.push({
|
|
3304
|
+
name: "Vitest Summary",
|
|
3305
|
+
file: "unknown",
|
|
3306
|
+
type: "unit",
|
|
3307
|
+
status,
|
|
3308
|
+
duration: 0,
|
|
3309
|
+
tests: Array.from({ length: count }, (_, i) => ({
|
|
3310
|
+
name: `test ${i + 1}`,
|
|
3311
|
+
file: "unknown",
|
|
3312
|
+
status,
|
|
3413
3313
|
duration: 0,
|
|
3414
3314
|
retries: 0
|
|
3415
3315
|
}))
|
|
3416
3316
|
});
|
|
3417
3317
|
}
|
|
3418
3318
|
}
|
|
3319
|
+
debug("vitest-text", `\u89E3\u6790\u5230 ${suites.length} \u4E2A\u5957\u4EF6, ${totalPassed} \u901A\u8FC7, ${totalFailed} \u5931\u8D25`);
|
|
3419
3320
|
return {
|
|
3420
3321
|
success: !hasError || totalFailed === 0,
|
|
3421
3322
|
suites
|
|
3422
3323
|
};
|
|
3423
3324
|
}
|
|
3424
3325
|
function mapVitestStatus(status) {
|
|
3326
|
+
if (!status) return "pending";
|
|
3425
3327
|
switch (status) {
|
|
3426
3328
|
case "passed":
|
|
3427
3329
|
case "pass":
|
|
3330
|
+
case "done":
|
|
3428
3331
|
return "passed";
|
|
3429
3332
|
case "failed":
|
|
3430
3333
|
case "fail":
|
|
@@ -3432,6 +3335,7 @@ function mapVitestStatus(status) {
|
|
|
3432
3335
|
case "skipped":
|
|
3433
3336
|
case "skip":
|
|
3434
3337
|
case "pending":
|
|
3338
|
+
case "todo":
|
|
3435
3339
|
return "skipped";
|
|
3436
3340
|
default:
|
|
3437
3341
|
return "pending";
|
|
@@ -3910,9 +3814,226 @@ function calculateAverageMetrics(results) {
|
|
|
3910
3814
|
};
|
|
3911
3815
|
}
|
|
3912
3816
|
|
|
3817
|
+
// src/services/reporter.ts
|
|
3818
|
+
import fs10 from "fs";
|
|
3819
|
+
import path11 from "path";
|
|
3820
|
+
function aggregateResults(results) {
|
|
3821
|
+
const summary = {
|
|
3822
|
+
total: 0,
|
|
3823
|
+
passed: 0,
|
|
3824
|
+
failed: 0,
|
|
3825
|
+
skipped: 0,
|
|
3826
|
+
pending: 0
|
|
3827
|
+
};
|
|
3828
|
+
const byType = {};
|
|
3829
|
+
let coverage;
|
|
3830
|
+
for (const result of results) {
|
|
3831
|
+
const typeKey = result.type;
|
|
3832
|
+
if (!byType[typeKey]) {
|
|
3833
|
+
byType[typeKey] = { total: 0, passed: 0, failed: 0, skipped: 0 };
|
|
3834
|
+
}
|
|
3835
|
+
for (const suite of result.suites) {
|
|
3836
|
+
for (const test of suite.tests) {
|
|
3837
|
+
summary.total++;
|
|
3838
|
+
byType[typeKey].total++;
|
|
3839
|
+
if (test.status === "passed") {
|
|
3840
|
+
summary.passed++;
|
|
3841
|
+
byType[typeKey].passed++;
|
|
3842
|
+
} else if (test.status === "failed") {
|
|
3843
|
+
summary.failed++;
|
|
3844
|
+
byType[typeKey].failed++;
|
|
3845
|
+
} else if (test.status === "skipped") {
|
|
3846
|
+
summary.skipped++;
|
|
3847
|
+
byType[typeKey].skipped++;
|
|
3848
|
+
} else {
|
|
3849
|
+
summary.pending++;
|
|
3850
|
+
}
|
|
3851
|
+
}
|
|
3852
|
+
}
|
|
3853
|
+
if (result.coverage) {
|
|
3854
|
+
if (!coverage) {
|
|
3855
|
+
coverage = { ...result.coverage };
|
|
3856
|
+
} else {
|
|
3857
|
+
coverage.lines = Math.max(coverage.lines, result.coverage.lines);
|
|
3858
|
+
coverage.statements = Math.max(coverage.statements, result.coverage.statements);
|
|
3859
|
+
coverage.functions = Math.max(coverage.functions, result.coverage.functions);
|
|
3860
|
+
coverage.branches = Math.max(coverage.branches, result.coverage.branches);
|
|
3861
|
+
}
|
|
3862
|
+
}
|
|
3863
|
+
}
|
|
3864
|
+
const totalDuration = results.reduce((sum, r) => sum + r.duration, 0);
|
|
3865
|
+
return {
|
|
3866
|
+
timestamp: Date.now(),
|
|
3867
|
+
duration: totalDuration,
|
|
3868
|
+
results,
|
|
3869
|
+
summary,
|
|
3870
|
+
byType,
|
|
3871
|
+
coverage: coverage?.lines ? coverage : void 0
|
|
3872
|
+
};
|
|
3873
|
+
}
|
|
3874
|
+
function formatDuration(ms) {
|
|
3875
|
+
if (ms < 1e3) return `${ms}ms`;
|
|
3876
|
+
if (ms < 6e4) return `${(ms / 1e3).toFixed(2)}s`;
|
|
3877
|
+
const minutes = Math.floor(ms / 6e4);
|
|
3878
|
+
const seconds = (ms % 6e4 / 1e3).toFixed(0);
|
|
3879
|
+
return `${minutes}m ${seconds}s`;
|
|
3880
|
+
}
|
|
3881
|
+
function formatTimestamp(ts) {
|
|
3882
|
+
return new Date(ts).toLocaleString("zh-CN", {
|
|
3883
|
+
year: "numeric",
|
|
3884
|
+
month: "2-digit",
|
|
3885
|
+
day: "2-digit",
|
|
3886
|
+
hour: "2-digit",
|
|
3887
|
+
minute: "2-digit",
|
|
3888
|
+
second: "2-digit"
|
|
3889
|
+
});
|
|
3890
|
+
}
|
|
3891
|
+
function pct(value) {
|
|
3892
|
+
return `${(value * 100).toFixed(1)}%`;
|
|
3893
|
+
}
|
|
3894
|
+
function renderCoverageMD(coverage) {
|
|
3895
|
+
return `
|
|
3896
|
+
### \u8986\u76D6\u7387
|
|
3897
|
+
|
|
3898
|
+
| \u6307\u6807 | \u8986\u76D6\u7387 | \u8FDB\u5EA6 |
|
|
3899
|
+
|------|--------|------|
|
|
3900
|
+
| \u8BED\u53E5 (Statements) | ${pct(coverage.statements)} | ${renderProgressBar(coverage.statements)} |
|
|
3901
|
+
| \u5206\u652F (Branches) | ${pct(coverage.branches)} | ${renderProgressBar(coverage.branches)} |
|
|
3902
|
+
| \u51FD\u6570 (Functions) | ${pct(coverage.functions)} | ${renderProgressBar(coverage.functions)} |
|
|
3903
|
+
| \u884C (Lines) | ${pct(coverage.lines)} | ${renderProgressBar(coverage.lines)} |
|
|
3904
|
+
`;
|
|
3905
|
+
}
|
|
3906
|
+
function renderProgressBar(value) {
|
|
3907
|
+
const filled = Math.round(value * 10);
|
|
3908
|
+
const empty = 10 - filled;
|
|
3909
|
+
return `${"\u2588".repeat(filled)}${"\u2591".repeat(empty)}`;
|
|
3910
|
+
}
|
|
3911
|
+
var TYPE_LABELS = {
|
|
3912
|
+
unit: "\u5355\u5143\u6D4B\u8BD5",
|
|
3913
|
+
component: "\u7EC4\u4EF6\u6D4B\u8BD5",
|
|
3914
|
+
e2e: "E2E \u6D4B\u8BD5",
|
|
3915
|
+
api: "API \u6D4B\u8BD5",
|
|
3916
|
+
visual: "\u89C6\u89C9\u56DE\u5F52\u6D4B\u8BD5",
|
|
3917
|
+
performance: "\u6027\u80FD\u6D4B\u8BD5"
|
|
3918
|
+
};
|
|
3919
|
+
function generateMDReport(data) {
|
|
3920
|
+
const passRate = data.summary.total > 0 ? (data.summary.passed / data.summary.total * 100).toFixed(1) : "0";
|
|
3921
|
+
const rateIcon = parseFloat(passRate) >= 80 ? "\u2705" : parseFloat(passRate) >= 50 ? "\u26A0\uFE0F" : "\u274C";
|
|
3922
|
+
const lines = [];
|
|
3923
|
+
lines.push(`# QAT \u6D4B\u8BD5\u62A5\u544A`);
|
|
3924
|
+
lines.push("");
|
|
3925
|
+
lines.push(`> \u751F\u6210\u65F6\u95F4: ${formatTimestamp(data.timestamp)} | \u603B\u8017\u65F6: ${formatDuration(data.duration)}`);
|
|
3926
|
+
lines.push("");
|
|
3927
|
+
lines.push(`## \u603B\u89C8`);
|
|
3928
|
+
lines.push("");
|
|
3929
|
+
lines.push(`| \u6307\u6807 | \u6570\u503C |`);
|
|
3930
|
+
lines.push(`|------|------|`);
|
|
3931
|
+
lines.push(`| \u901A\u8FC7\u7387 | ${rateIcon} **${passRate}%** |`);
|
|
3932
|
+
lines.push(`| \u603B\u7528\u4F8B | ${data.summary.total} |`);
|
|
3933
|
+
lines.push(`| \u2705 \u901A\u8FC7 | ${data.summary.passed} |`);
|
|
3934
|
+
if (data.summary.failed > 0) lines.push(`| \u274C \u5931\u8D25 | ${data.summary.failed} |`);
|
|
3935
|
+
if (data.summary.skipped > 0) lines.push(`| \u23ED\uFE0F \u8DF3\u8FC7 | ${data.summary.skipped} |`);
|
|
3936
|
+
if (data.summary.pending > 0) lines.push(`| \u23F3 \u5F85\u5B9A | ${data.summary.pending} |`);
|
|
3937
|
+
lines.push(`| \u23F1\uFE0F \u8017\u65F6 | ${formatDuration(data.duration)} |`);
|
|
3938
|
+
lines.push("");
|
|
3939
|
+
if (Object.keys(data.byType).length > 0) {
|
|
3940
|
+
lines.push(`## \u6309\u7C7B\u578B\u7EDF\u8BA1`);
|
|
3941
|
+
lines.push("");
|
|
3942
|
+
lines.push(`| \u7C7B\u578B | \u901A\u8FC7 | \u5931\u8D25 | \u8DF3\u8FC7 | \u603B\u8BA1 | \u901A\u8FC7\u7387 |`);
|
|
3943
|
+
lines.push(`|------|------|------|------|------|--------|`);
|
|
3944
|
+
for (const [type, stats] of Object.entries(data.byType)) {
|
|
3945
|
+
const label = TYPE_LABELS[type] || type;
|
|
3946
|
+
const rate = stats.total > 0 ? (stats.passed / stats.total * 100).toFixed(0) + "%" : "-";
|
|
3947
|
+
lines.push(`| ${label} | ${stats.passed} | ${stats.failed} | ${stats.skipped} | ${stats.total} | ${rate} |`);
|
|
3948
|
+
}
|
|
3949
|
+
lines.push("");
|
|
3950
|
+
}
|
|
3951
|
+
if (data.coverage) {
|
|
3952
|
+
lines.push(renderCoverageMD(data.coverage));
|
|
3953
|
+
lines.push("");
|
|
3954
|
+
}
|
|
3955
|
+
lines.push(`## \u6D4B\u8BD5\u8BE6\u60C5`);
|
|
3956
|
+
lines.push("");
|
|
3957
|
+
for (const result of data.results) {
|
|
3958
|
+
const typeLabel = TYPE_LABELS[result.type] || result.type;
|
|
3959
|
+
const statusIcon = result.status === "passed" ? "\u2705" : result.status === "failed" ? "\u274C" : "\u26A0\uFE0F";
|
|
3960
|
+
lines.push(`### ${statusIcon} ${typeLabel}`);
|
|
3961
|
+
lines.push("");
|
|
3962
|
+
if (result.suites.length === 0) {
|
|
3963
|
+
lines.push(`*\u65E0\u6D4B\u8BD5\u7ED3\u679C*`);
|
|
3964
|
+
lines.push("");
|
|
3965
|
+
continue;
|
|
3966
|
+
}
|
|
3967
|
+
for (const suite of result.suites) {
|
|
3968
|
+
const suiteIcon = suite.status === "passed" ? "\u2705" : suite.status === "failed" ? "\u274C" : "\u26A0\uFE0F";
|
|
3969
|
+
lines.push(`#### ${suiteIcon} ${suite.name}`);
|
|
3970
|
+
lines.push("");
|
|
3971
|
+
lines.push(`- \u6587\u4EF6: \`${suite.file}\``);
|
|
3972
|
+
lines.push(`- \u8017\u65F6: ${formatDuration(suite.duration)}`);
|
|
3973
|
+
lines.push("");
|
|
3974
|
+
if (suite.tests.length > 0) {
|
|
3975
|
+
lines.push(`| \u72B6\u6001 | \u6D4B\u8BD5\u540D\u79F0 | \u8017\u65F6 |`);
|
|
3976
|
+
lines.push(`|------|----------|------|`);
|
|
3977
|
+
for (const test of suite.tests) {
|
|
3978
|
+
const testIcon = test.status === "passed" ? "\u2705" : test.status === "failed" ? "\u274C" : test.status === "skipped" ? "\u23ED\uFE0F" : "\u23F3";
|
|
3979
|
+
const name = test.error ? `**${test.name}**` : test.name;
|
|
3980
|
+
lines.push(`| ${testIcon} | ${name} | ${formatDuration(test.duration)} |`);
|
|
3981
|
+
}
|
|
3982
|
+
lines.push("");
|
|
3983
|
+
}
|
|
3984
|
+
const failedTests = suite.tests.filter((t) => t.status === "failed" && t.error);
|
|
3985
|
+
if (failedTests.length > 0) {
|
|
3986
|
+
lines.push(`<details>`);
|
|
3987
|
+
lines.push(`<summary>\u274C \u5931\u8D25\u8BE6\u60C5 (${failedTests.length})</summary>`);
|
|
3988
|
+
lines.push("");
|
|
3989
|
+
for (const test of failedTests) {
|
|
3990
|
+
lines.push(`**${test.name}**`);
|
|
3991
|
+
lines.push("```");
|
|
3992
|
+
lines.push(test.error.message);
|
|
3993
|
+
if (test.error.stack) {
|
|
3994
|
+
lines.push(test.error.stack);
|
|
3995
|
+
}
|
|
3996
|
+
if (test.error.expected && test.error.actual) {
|
|
3997
|
+
lines.push(`Expected: ${test.error.expected}`);
|
|
3998
|
+
lines.push(`Actual: ${test.error.actual}`);
|
|
3999
|
+
}
|
|
4000
|
+
lines.push("```");
|
|
4001
|
+
lines.push("");
|
|
4002
|
+
}
|
|
4003
|
+
lines.push(`</details>`);
|
|
4004
|
+
lines.push("");
|
|
4005
|
+
}
|
|
4006
|
+
}
|
|
4007
|
+
}
|
|
4008
|
+
lines.push("---");
|
|
4009
|
+
lines.push("");
|
|
4010
|
+
lines.push(`*\u7531 QAT \u81EA\u52A8\u5316\u6D4B\u8BD5\u5DE5\u5177\u751F\u6210 | ${formatTimestamp(data.timestamp)}*`);
|
|
4011
|
+
return lines.join("\n");
|
|
4012
|
+
}
|
|
4013
|
+
function writeReportToDisk(data, outputDir) {
|
|
4014
|
+
const md = generateMDReport(data);
|
|
4015
|
+
const dir = path11.resolve(outputDir);
|
|
4016
|
+
if (!fs10.existsSync(dir)) {
|
|
4017
|
+
fs10.mkdirSync(dir, { recursive: true });
|
|
4018
|
+
}
|
|
4019
|
+
const mdPath = path11.join(dir, "report.md");
|
|
4020
|
+
fs10.writeFileSync(mdPath, md, "utf-8");
|
|
4021
|
+
const jsonPath = path11.join(dir, "report.json");
|
|
4022
|
+
fs10.writeFileSync(jsonPath, JSON.stringify(data, null, 2), "utf-8");
|
|
4023
|
+
return mdPath;
|
|
4024
|
+
}
|
|
4025
|
+
|
|
3913
4026
|
// src/commands/run.ts
|
|
3914
4027
|
var RESULTS_DIR = ".qat-results";
|
|
3915
4028
|
var SERVER_REQUIRED_TYPES = ["e2e", "visual", "performance"];
|
|
4029
|
+
var TYPE_DEPENDENCIES = {
|
|
4030
|
+
unit: { pkg: "vitest", runner: "Vitest", installCmd: "npm install -D vitest" },
|
|
4031
|
+
component: { pkg: "vitest", runner: "Vitest", installCmd: "npm install -D vitest @vue/test-utils happy-dom" },
|
|
4032
|
+
api: { pkg: "vitest", runner: "Vitest", installCmd: "npm install -D vitest" },
|
|
4033
|
+
e2e: { pkg: "@playwright/test", runner: "Playwright", installCmd: "npm install -D @playwright/test && npx playwright install" },
|
|
4034
|
+
visual: { pkg: "@playwright/test", runner: "Playwright", installCmd: "npm install -D @playwright/test && npx playwright install" },
|
|
4035
|
+
performance: { pkg: "lighthouse", runner: "Lighthouse", installCmd: "npm install -D lighthouse" }
|
|
4036
|
+
};
|
|
3916
4037
|
var TYPE_RUNNERS = {
|
|
3917
4038
|
unit: "Vitest",
|
|
3918
4039
|
component: "Vitest",
|
|
@@ -3921,7 +4042,7 @@ var TYPE_RUNNERS = {
|
|
|
3921
4042
|
visual: "Playwright",
|
|
3922
4043
|
performance: "Lighthouse"
|
|
3923
4044
|
};
|
|
3924
|
-
var
|
|
4045
|
+
var TYPE_LABELS2 = {
|
|
3925
4046
|
unit: "\u5355\u5143\u6D4B\u8BD5",
|
|
3926
4047
|
component: "\u7EC4\u4EF6\u6D4B\u8BD5",
|
|
3927
4048
|
e2e: "E2E\u6D4B\u8BD5",
|
|
@@ -3942,7 +4063,7 @@ function registerRunCommand(program2) {
|
|
|
3942
4063
|
});
|
|
3943
4064
|
}
|
|
3944
4065
|
async function executeRun(options) {
|
|
3945
|
-
const { type, file,
|
|
4066
|
+
const { type, file, watch, parallel } = options;
|
|
3946
4067
|
const config = await loadConfig(options.config);
|
|
3947
4068
|
const globalAI = loadGlobalAIConfig();
|
|
3948
4069
|
if (globalAI) {
|
|
@@ -3957,6 +4078,61 @@ async function executeRun(options) {
|
|
|
3957
4078
|
console.log(chalk5.yellow("\n \u6CA1\u6709\u53EF\u8FD0\u884C\u7684\u6D4B\u8BD5\u7C7B\u578B\uFF08\u8BF7\u5728 qat.config.ts \u4E2D\u542F\u7528\uFF09\n"));
|
|
3958
4079
|
return;
|
|
3959
4080
|
}
|
|
4081
|
+
const missingDeps = checkTestDependencies(typesToRun);
|
|
4082
|
+
if (missingDeps.length > 0) {
|
|
4083
|
+
console.log(chalk5.yellow("\n \u26A0 \u4EE5\u4E0B\u6D4B\u8BD5\u6846\u67B6\u4F9D\u8D56\u672A\u5B89\u88C5:\n"));
|
|
4084
|
+
for (const dep of missingDeps) {
|
|
4085
|
+
console.log(chalk5.white(` ${dep.runner} (${dep.pkg})`));
|
|
4086
|
+
console.log(chalk5.gray(` \u5B89\u88C5\u547D\u4EE4: ${chalk5.cyan(dep.installCmd)}`));
|
|
4087
|
+
}
|
|
4088
|
+
console.log();
|
|
4089
|
+
const { action } = await inquirer3.prompt([
|
|
4090
|
+
{
|
|
4091
|
+
type: "list",
|
|
4092
|
+
name: "action",
|
|
4093
|
+
message: "\u5982\u4F55\u5904\u7406\uFF1F",
|
|
4094
|
+
choices: [
|
|
4095
|
+
{ name: "\u81EA\u52A8\u5B89\u88C5\u7F3A\u5931\u4F9D\u8D56", value: "install" },
|
|
4096
|
+
{ name: "\u8DF3\u8FC7\u672A\u5B89\u88C5\u7684\u6D4B\u8BD5\u7C7B\u578B", value: "skip" },
|
|
4097
|
+
{ name: "\u53D6\u6D88\u8FD0\u884C", value: "cancel" }
|
|
4098
|
+
],
|
|
4099
|
+
default: "install"
|
|
4100
|
+
}
|
|
4101
|
+
]);
|
|
4102
|
+
if (action === "cancel") {
|
|
4103
|
+
console.log(chalk5.gray("\n \u5DF2\u53D6\u6D88\u8FD0\u884C\n"));
|
|
4104
|
+
return;
|
|
4105
|
+
}
|
|
4106
|
+
if (action === "install") {
|
|
4107
|
+
const installed = await installTestDependencies(missingDeps);
|
|
4108
|
+
if (!installed) {
|
|
4109
|
+
console.log(chalk5.yellow(" \u90E8\u5206\u4F9D\u8D56\u5B89\u88C5\u5931\u8D25\uFF0C\u5C06\u8DF3\u8FC7\u5BF9\u5E94\u7684\u6D4B\u8BD5\u7C7B\u578B"));
|
|
4110
|
+
}
|
|
4111
|
+
const stillMissing = checkTestDependencies(typesToRun);
|
|
4112
|
+
if (stillMissing.length > 0) {
|
|
4113
|
+
const missingRunners = new Set(stillMissing.map((d) => d.pkg));
|
|
4114
|
+
typesToRun = typesToRun.filter((t) => {
|
|
4115
|
+
const dep = TYPE_DEPENDENCIES[t];
|
|
4116
|
+
return !dep || !missingRunners.has(dep.pkg);
|
|
4117
|
+
});
|
|
4118
|
+
if (typesToRun.length === 0) {
|
|
4119
|
+
console.log(chalk5.yellow("\n \u6CA1\u6709\u53EF\u8FD0\u884C\u7684\u6D4B\u8BD5\u7C7B\u578B\uFF08\u4F9D\u8D56\u672A\u5B89\u88C5\uFF09\n"));
|
|
4120
|
+
return;
|
|
4121
|
+
}
|
|
4122
|
+
}
|
|
4123
|
+
}
|
|
4124
|
+
if (action === "skip") {
|
|
4125
|
+
const missingRunners = new Set(missingDeps.map((d) => d.pkg));
|
|
4126
|
+
typesToRun = typesToRun.filter((t) => {
|
|
4127
|
+
const dep = TYPE_DEPENDENCIES[t];
|
|
4128
|
+
return !dep || !missingRunners.has(dep.pkg);
|
|
4129
|
+
});
|
|
4130
|
+
if (typesToRun.length === 0) {
|
|
4131
|
+
console.log(chalk5.yellow("\n \u6CA1\u6709\u53EF\u8FD0\u884C\u7684\u6D4B\u8BD5\u7C7B\u578B\n"));
|
|
4132
|
+
return;
|
|
4133
|
+
}
|
|
4134
|
+
}
|
|
4135
|
+
}
|
|
3960
4136
|
const serverNeededTypes = typesToRun.filter((t) => SERVER_REQUIRED_TYPES.includes(t));
|
|
3961
4137
|
if (serverNeededTypes.length > 0) {
|
|
3962
4138
|
const serverOk = await checkDevServer(config);
|
|
@@ -3965,7 +4141,7 @@ async function executeRun(options) {
|
|
|
3965
4141
|
{
|
|
3966
4142
|
type: "list",
|
|
3967
4143
|
name: "action",
|
|
3968
|
-
message: `\u8FD0\u884C ${serverNeededTypes.map((t) =>
|
|
4144
|
+
message: `\u8FD0\u884C ${serverNeededTypes.map((t) => TYPE_LABELS2[t]).join("\u3001")} \u9700\u8981\u9879\u76EE\u670D\u52A1\u8FD0\u884C\uFF0C\u5982\u4F55\u5904\u7406\uFF1F`,
|
|
3969
4145
|
choices: [
|
|
3970
4146
|
{ name: "\u81EA\u52A8\u542F\u52A8 dev server \u5E76\u8FD0\u884C", value: "start" },
|
|
3971
4147
|
{ name: "\u8DF3\u8FC7\u8FD9\u4E9B\u6D4B\u8BD5\u7C7B\u578B\uFF0C\u4EC5\u8FD0\u884C\u5176\u4ED6\u6D4B\u8BD5", value: "skip" },
|
|
@@ -4046,7 +4222,7 @@ async function executeRun(options) {
|
|
|
4046
4222
|
spinner.stop();
|
|
4047
4223
|
} else {
|
|
4048
4224
|
for (const testType of typesToRun) {
|
|
4049
|
-
const label =
|
|
4225
|
+
const label = TYPE_LABELS2[testType] || testType;
|
|
4050
4226
|
const spinner = ora4(`\u6B63\u5728\u8FD0\u884C ${label}...`).start();
|
|
4051
4227
|
try {
|
|
4052
4228
|
const result = await runTestType(testType, config, options);
|
|
@@ -4058,14 +4234,25 @@ async function executeRun(options) {
|
|
|
4058
4234
|
}
|
|
4059
4235
|
}
|
|
4060
4236
|
}
|
|
4061
|
-
|
|
4237
|
+
displayJestStyleResults(results);
|
|
4062
4238
|
saveRunResults(results);
|
|
4239
|
+
const reportData = aggregateResults(results);
|
|
4240
|
+
const outputDir = config.report.outputDir || "qat-report";
|
|
4241
|
+
const reportPath = writeReportToDisk(reportData, outputDir);
|
|
4242
|
+
const relativePath = path12.relative(process.cwd(), reportPath);
|
|
4243
|
+
console.log(chalk5.gray(`
|
|
4244
|
+
\u62A5\u544A\u5DF2\u751F\u6210: ${chalk5.cyan(relativePath)}`));
|
|
4245
|
+
console.log();
|
|
4246
|
+
const hasFailures = results.some((r) => r.status === "failed");
|
|
4247
|
+
if (hasFailures && isAIAvailable(config.ai)) {
|
|
4248
|
+
await aiAnalyzeFailures(results, config.ai);
|
|
4249
|
+
}
|
|
4063
4250
|
}
|
|
4064
4251
|
function printDryRunCommands(types, options, config) {
|
|
4065
4252
|
console.log();
|
|
4066
4253
|
console.log(chalk5.cyan(" \u53EF\u6267\u884C\u7684\u6D4B\u8BD5\u547D\u4EE4:\n"));
|
|
4067
4254
|
for (const testType of types) {
|
|
4068
|
-
const label =
|
|
4255
|
+
const label = TYPE_LABELS2[testType];
|
|
4069
4256
|
const runner = TYPE_RUNNERS[testType];
|
|
4070
4257
|
let cmd;
|
|
4071
4258
|
switch (runner) {
|
|
@@ -4078,7 +4265,7 @@ function printDryRunCommands(types, options, config) {
|
|
|
4078
4265
|
api: "tests/api"
|
|
4079
4266
|
};
|
|
4080
4267
|
const dir = dirMap[testType];
|
|
4081
|
-
if (dir) cmd += `
|
|
4268
|
+
if (dir) cmd += ` '${dir}/**/*.test.ts'`;
|
|
4082
4269
|
if (config.vitest.coverage) cmd += " --coverage";
|
|
4083
4270
|
}
|
|
4084
4271
|
break;
|
|
@@ -4133,7 +4320,7 @@ async function determineTypesToRun(type, file, config) {
|
|
|
4133
4320
|
name: "selectedTypes",
|
|
4134
4321
|
message: "\u9009\u62E9\u8981\u8FD0\u884C\u7684\u6D4B\u8BD5\u7C7B\u578B (\u7A7A\u683C\u9009\u62E9/\u53D6\u6D88\uFF0C\u56DE\u8F66\u786E\u8BA4):",
|
|
4135
4322
|
choices: enabledTypes.map((t) => ({
|
|
4136
|
-
name: `${
|
|
4323
|
+
name: `${TYPE_LABELS2[t]} (${chalk5.gray(TYPE_RUNNERS[t])})`,
|
|
4137
4324
|
value: t,
|
|
4138
4325
|
checked: true
|
|
4139
4326
|
})),
|
|
@@ -4192,7 +4379,7 @@ async function runTestType(testType, config, options) {
|
|
|
4192
4379
|
throw new Error(`\u672A\u77E5\u7684\u8FD0\u884C\u5668: ${runner}`);
|
|
4193
4380
|
}
|
|
4194
4381
|
}
|
|
4195
|
-
async function runWatchMode(
|
|
4382
|
+
async function runWatchMode(_config, options) {
|
|
4196
4383
|
console.log(chalk5.cyan(" \u76D1\u542C\u6A21\u5F0F\u5DF2\u542F\u52A8 (Ctrl+C \u9000\u51FA)\n"));
|
|
4197
4384
|
const { spawn } = await import("child_process");
|
|
4198
4385
|
const args = ["vitest", "--watch"];
|
|
@@ -4208,7 +4395,7 @@ async function runWatchMode(config, options) {
|
|
|
4208
4395
|
};
|
|
4209
4396
|
const dirs = types.map((t) => dirMap[t]).filter(Boolean);
|
|
4210
4397
|
if (dirs.length > 0) {
|
|
4211
|
-
args.push(
|
|
4398
|
+
args.push(...dirs.map((d) => `${d}/**/*.test.ts`));
|
|
4212
4399
|
}
|
|
4213
4400
|
}
|
|
4214
4401
|
const child = spawn("npx", args, {
|
|
@@ -4267,53 +4454,82 @@ function suiteStatusToSpinner(status) {
|
|
|
4267
4454
|
return "warn";
|
|
4268
4455
|
}
|
|
4269
4456
|
}
|
|
4270
|
-
|
|
4271
|
-
|
|
4457
|
+
function displayJestStyleResults(results) {
|
|
4458
|
+
console.log();
|
|
4459
|
+
for (const result of results) {
|
|
4460
|
+
const typeLabel = TYPE_LABELS2[result.type] || result.type;
|
|
4461
|
+
if (result.suites.length === 0) {
|
|
4462
|
+
console.log(chalk5.gray(` ${typeLabel}: \u65E0\u6D4B\u8BD5\u7ED3\u679C`));
|
|
4463
|
+
continue;
|
|
4464
|
+
}
|
|
4465
|
+
for (const suite of result.suites) {
|
|
4466
|
+
if (suite.tests.length === 0) continue;
|
|
4467
|
+
const suiteIcon = suite.status === "passed" ? chalk5.green("PASS") : chalk5.red("FAIL");
|
|
4468
|
+
console.log(` ${suiteIcon} ${chalk5.white(suite.name)} ${chalk5.gray(`(${formatDuration2(suite.duration)})`)}`);
|
|
4469
|
+
for (const test of suite.tests) {
|
|
4470
|
+
const icon = test.status === "passed" ? chalk5.green(" \u2713") : test.status === "failed" ? chalk5.red(" \u2715") : chalk5.yellow(" \u25CB");
|
|
4471
|
+
const name = test.status === "failed" ? chalk5.red(test.name) : test.name;
|
|
4472
|
+
console.log(` ${icon} ${name} ${chalk5.gray(formatDuration2(test.duration))}`);
|
|
4473
|
+
}
|
|
4474
|
+
}
|
|
4475
|
+
console.log();
|
|
4476
|
+
}
|
|
4272
4477
|
let totalPassed = 0;
|
|
4273
4478
|
let totalFailed = 0;
|
|
4274
4479
|
let totalSkipped = 0;
|
|
4275
4480
|
let totalDuration = 0;
|
|
4481
|
+
const typeStats = {};
|
|
4276
4482
|
for (const result of results) {
|
|
4277
4483
|
totalDuration += result.duration;
|
|
4484
|
+
if (!typeStats[result.type]) {
|
|
4485
|
+
typeStats[result.type] = { passed: 0, failed: 0, skipped: 0, duration: 0 };
|
|
4486
|
+
}
|
|
4487
|
+
typeStats[result.type].duration += result.duration;
|
|
4278
4488
|
for (const suite of result.suites) {
|
|
4279
|
-
totalSuites++;
|
|
4280
4489
|
for (const test of suite.tests) {
|
|
4281
|
-
if (test.status === "passed")
|
|
4282
|
-
|
|
4283
|
-
|
|
4490
|
+
if (test.status === "passed") {
|
|
4491
|
+
totalPassed++;
|
|
4492
|
+
typeStats[result.type].passed++;
|
|
4493
|
+
} else if (test.status === "failed") {
|
|
4494
|
+
totalFailed++;
|
|
4495
|
+
typeStats[result.type].failed++;
|
|
4496
|
+
} else if (test.status === "skipped") {
|
|
4497
|
+
totalSkipped++;
|
|
4498
|
+
typeStats[result.type].skipped++;
|
|
4499
|
+
}
|
|
4284
4500
|
}
|
|
4285
|
-
}
|
|
4286
|
-
}
|
|
4287
|
-
const total = totalPassed + totalFailed + totalSkipped;
|
|
4288
|
-
console.log();
|
|
4289
|
-
console.log(chalk5.cyan(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
4290
|
-
if (totalFailed > 0) {
|
|
4291
|
-
console.log(chalk5.red(" \u2717 \u6D4B\u8BD5\u5931\u8D25"));
|
|
4292
|
-
} else if (total === 0) {
|
|
4293
|
-
console.log(chalk5.yellow(" \u26A0 \u6CA1\u6709\u53D1\u73B0\u6D4B\u8BD5\u7528\u4F8B"));
|
|
4294
|
-
} else {
|
|
4295
|
-
console.log(chalk5.green(" \u2713 \u5168\u90E8\u901A\u8FC7"));
|
|
4501
|
+
}
|
|
4296
4502
|
}
|
|
4503
|
+
const total = totalPassed + totalFailed + totalSkipped;
|
|
4504
|
+
console.log(chalk5.cyan(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
4505
|
+
console.log(chalk5.white(" \u6D4B\u8BD5\u7ED3\u679C\u6C47\u603B"));
|
|
4506
|
+
console.log(chalk5.cyan(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
4297
4507
|
console.log();
|
|
4298
|
-
console.log(` ${chalk5.white("\
|
|
4299
|
-
|
|
4300
|
-
|
|
4301
|
-
|
|
4302
|
-
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
console.log();
|
|
4306
|
-
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
|
|
4508
|
+
console.log(` ${chalk5.white("\u7C7B\u578B".padEnd(14))} ${chalk5.white("\u901A\u8FC7".padStart(6))} ${chalk5.white("\u5931\u8D25".padStart(6))} ${chalk5.white("\u8DF3\u8FC7".padStart(6))} ${chalk5.white("\u603B\u8BA1".padStart(6))} ${chalk5.white("\u901A\u8FC7\u7387".padStart(8))} ${chalk5.white("\u8017\u65F6".padStart(8))}`);
|
|
4509
|
+
console.log(` ${"\u2500".repeat(14)} ${"\u2500".repeat(6)} ${"\u2500".repeat(6)} ${"\u2500".repeat(6)} ${"\u2500".repeat(6)} ${"\u2500".repeat(8)} ${"\u2500".repeat(8)}`);
|
|
4510
|
+
for (const [type, stats] of Object.entries(typeStats)) {
|
|
4511
|
+
const label = (TYPE_LABELS2[type] || type).padEnd(14);
|
|
4512
|
+
const typeTotal = stats.passed + stats.failed + stats.skipped;
|
|
4513
|
+
const rate = typeTotal > 0 ? (stats.passed / typeTotal * 100).toFixed(0) + "%" : "-";
|
|
4514
|
+
const rateColored = typeTotal > 0 && stats.passed === typeTotal ? chalk5.green(rate.padStart(8)) : stats.failed > 0 ? chalk5.red(rate.padStart(8)) : rate.padStart(8);
|
|
4515
|
+
console.log(` ${label} ${String(stats.passed).padStart(6)} ${String(stats.failed).padStart(6)} ${String(stats.skipped).padStart(6)} ${String(typeTotal).padStart(6)} ${rateColored} ${formatDuration2(stats.duration).padStart(8)}`);
|
|
4516
|
+
}
|
|
4517
|
+
console.log(` ${"\u2500".repeat(14)} ${"\u2500".repeat(6)} ${"\u2500".repeat(6)} ${"\u2500".repeat(6)} ${"\u2500".repeat(6)} ${"\u2500".repeat(8)} ${"\u2500".repeat(8)}`);
|
|
4518
|
+
const totalRate = total > 0 ? (totalPassed / total * 100).toFixed(0) + "%" : "-";
|
|
4519
|
+
const totalRateColored = total > 0 && totalFailed === 0 ? chalk5.green(totalRate.padStart(8)) : totalFailed > 0 ? chalk5.red(totalRate.padStart(8)) : totalRate.padStart(8);
|
|
4520
|
+
console.log(` ${"\u603B\u8BA1".padEnd(14)} ${String(totalPassed).padStart(6)} ${String(totalFailed).padStart(6)} ${String(totalSkipped).padStart(6)} ${String(total).padStart(6)} ${totalRateColored} ${formatDuration2(totalDuration).padStart(8)}`);
|
|
4521
|
+
console.log();
|
|
4522
|
+
for (const result of results) {
|
|
4523
|
+
if (result.coverage) {
|
|
4524
|
+
const c = result.coverage;
|
|
4525
|
+
console.log(chalk5.cyan(" \u8986\u76D6\u7387:"));
|
|
4526
|
+
console.log(` \u8BED\u53E5: ${coverageColor(c.statements)} \u5206\u652F: ${coverageColor(c.branches)} \u51FD\u6570: ${coverageColor(c.functions)} \u884C: ${coverageColor(c.lines)}`);
|
|
4527
|
+
console.log();
|
|
4311
4528
|
}
|
|
4312
4529
|
}
|
|
4313
4530
|
for (const result of results) {
|
|
4314
4531
|
if (result.type === "performance" && result.performance) {
|
|
4315
4532
|
const p = result.performance;
|
|
4316
|
-
console.log();
|
|
4317
4533
|
console.log(chalk5.cyan(" \u6027\u80FD\u6307\u6807:"));
|
|
4318
4534
|
console.log(` Performance: ${scoreColor(p.performance)} ${p.performance}/100`);
|
|
4319
4535
|
console.log(` Accessibility: ${scoreColor(p.accessibility)} ${p.accessibility}/100`);
|
|
@@ -4322,6 +4538,7 @@ async function displaySummary(results, config) {
|
|
|
4322
4538
|
if (p.lcp !== void 0) console.log(` LCP: ${p.lcp.toFixed(0)}ms`);
|
|
4323
4539
|
if (p.fid !== void 0) console.log(` FID: ${p.fid.toFixed(0)}ms`);
|
|
4324
4540
|
if (p.cls !== void 0) console.log(` CLS: ${p.cls.toFixed(3)}`);
|
|
4541
|
+
console.log();
|
|
4325
4542
|
}
|
|
4326
4543
|
}
|
|
4327
4544
|
const failedTests = results.flatMap(
|
|
@@ -4330,23 +4547,31 @@ async function displaySummary(results, config) {
|
|
|
4330
4547
|
)
|
|
4331
4548
|
);
|
|
4332
4549
|
if (failedTests.length > 0) {
|
|
4333
|
-
console.log();
|
|
4334
4550
|
console.log(chalk5.red(" \u5931\u8D25\u8BE6\u60C5:"));
|
|
4335
4551
|
for (const { suite, test } of failedTests) {
|
|
4336
|
-
console.log(chalk5.red(` \
|
|
4552
|
+
console.log(chalk5.red(` \u2715 ${suite.name} > ${test.name}`));
|
|
4337
4553
|
if (test.error?.message) {
|
|
4338
4554
|
console.log(chalk5.gray(` ${test.error.message.split("\n")[0]}`));
|
|
4339
4555
|
}
|
|
4340
4556
|
}
|
|
4557
|
+
console.log();
|
|
4341
4558
|
}
|
|
4342
|
-
console.log(chalk5.cyan(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
4343
|
-
console.log();
|
|
4344
4559
|
if (totalFailed > 0) {
|
|
4560
|
+
console.log(chalk5.red(` Tests: ${totalFailed} failed, ${totalPassed} passed, ${total} total`));
|
|
4345
4561
|
process.exitCode = 1;
|
|
4562
|
+
} else if (total === 0) {
|
|
4563
|
+
console.log(chalk5.yellow(" \u6CA1\u6709\u53D1\u73B0\u6D4B\u8BD5\u7528\u4F8B"));
|
|
4564
|
+
} else {
|
|
4565
|
+
console.log(chalk5.green(` Tests: ${totalPassed} passed, ${total} total`));
|
|
4346
4566
|
}
|
|
4347
|
-
|
|
4348
|
-
|
|
4349
|
-
|
|
4567
|
+
console.log(chalk5.gray(` Time: ${formatDuration2(totalDuration)}`));
|
|
4568
|
+
console.log();
|
|
4569
|
+
}
|
|
4570
|
+
function coverageColor(value) {
|
|
4571
|
+
const pct3 = `${(value * 100).toFixed(1)}%`;
|
|
4572
|
+
if (value >= 0.8) return chalk5.green(pct3);
|
|
4573
|
+
if (value >= 0.5) return chalk5.yellow(pct3);
|
|
4574
|
+
return chalk5.red(pct3);
|
|
4350
4575
|
}
|
|
4351
4576
|
async function aiAnalyzeFailures(results, aiConfig) {
|
|
4352
4577
|
const failedTests = results.flatMap(
|
|
@@ -4375,7 +4600,7 @@ async function aiAnalyzeFailures(results, aiConfig) {
|
|
|
4375
4600
|
}
|
|
4376
4601
|
}
|
|
4377
4602
|
}
|
|
4378
|
-
function
|
|
4603
|
+
function formatDuration2(ms) {
|
|
4379
4604
|
if (ms < 1e3) return `${ms}ms`;
|
|
4380
4605
|
if (ms < 6e4) return `${(ms / 1e3).toFixed(1)}s`;
|
|
4381
4606
|
const min = Math.floor(ms / 6e4);
|
|
@@ -4401,8 +4626,8 @@ async function checkDevServer(config) {
|
|
|
4401
4626
|
}
|
|
4402
4627
|
async function startDevServer(config) {
|
|
4403
4628
|
const { spawn } = await import("child_process");
|
|
4404
|
-
const hasPnpm =
|
|
4405
|
-
const hasYarn =
|
|
4629
|
+
const hasPnpm = fs11.existsSync(path12.join(process.cwd(), "pnpm-lock.yaml"));
|
|
4630
|
+
const hasYarn = fs11.existsSync(path12.join(process.cwd(), "yarn.lock"));
|
|
4406
4631
|
const pkgCmd = hasPnpm ? "pnpm" : hasYarn ? "yarn" : "npm";
|
|
4407
4632
|
console.log(chalk5.cyan(` \u6B63\u5728\u542F\u52A8 dev server (${pkgCmd} run dev) ...`));
|
|
4408
4633
|
const child = spawn(pkgCmd, ["run", "dev"], {
|
|
@@ -4431,27 +4656,99 @@ async function startDevServer(config) {
|
|
|
4431
4656
|
}
|
|
4432
4657
|
function saveRunResults(results) {
|
|
4433
4658
|
if (results.length === 0) return;
|
|
4434
|
-
const resultsPath =
|
|
4435
|
-
if (!
|
|
4436
|
-
|
|
4659
|
+
const resultsPath = path12.join(process.cwd(), RESULTS_DIR);
|
|
4660
|
+
if (!fs11.existsSync(resultsPath)) {
|
|
4661
|
+
fs11.mkdirSync(resultsPath, { recursive: true });
|
|
4437
4662
|
}
|
|
4438
4663
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
4439
4664
|
const fileName = `result-${timestamp}.json`;
|
|
4440
|
-
const filePath =
|
|
4665
|
+
const filePath = path12.join(resultsPath, fileName);
|
|
4441
4666
|
const data = {
|
|
4442
4667
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4443
4668
|
results
|
|
4444
4669
|
};
|
|
4445
|
-
|
|
4670
|
+
fs11.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf-8");
|
|
4446
4671
|
try {
|
|
4447
|
-
const files =
|
|
4672
|
+
const files = fs11.readdirSync(resultsPath).filter((f) => f.startsWith("result-") && f.endsWith(".json")).sort();
|
|
4448
4673
|
while (files.length > 20) {
|
|
4449
4674
|
const oldest = files.shift();
|
|
4450
|
-
|
|
4675
|
+
fs11.unlinkSync(path12.join(resultsPath, oldest));
|
|
4451
4676
|
}
|
|
4452
4677
|
} catch {
|
|
4453
4678
|
}
|
|
4454
4679
|
}
|
|
4680
|
+
function checkTestDependencies(types) {
|
|
4681
|
+
const pkgPath = path12.join(process.cwd(), "package.json");
|
|
4682
|
+
let allDeps = {};
|
|
4683
|
+
if (fs11.existsSync(pkgPath)) {
|
|
4684
|
+
try {
|
|
4685
|
+
const pkg = JSON.parse(fs11.readFileSync(pkgPath, "utf-8"));
|
|
4686
|
+
allDeps = {
|
|
4687
|
+
...pkg.dependencies,
|
|
4688
|
+
...pkg.devDependencies
|
|
4689
|
+
};
|
|
4690
|
+
} catch {
|
|
4691
|
+
}
|
|
4692
|
+
}
|
|
4693
|
+
const missingDeps = [];
|
|
4694
|
+
const checkedPkgs = /* @__PURE__ */ new Set();
|
|
4695
|
+
for (const testType of types) {
|
|
4696
|
+
const dep = TYPE_DEPENDENCIES[testType];
|
|
4697
|
+
if (dep && !checkedPkgs.has(dep.pkg)) {
|
|
4698
|
+
checkedPkgs.add(dep.pkg);
|
|
4699
|
+
if (!allDeps[dep.pkg]) {
|
|
4700
|
+
missingDeps.push(dep);
|
|
4701
|
+
}
|
|
4702
|
+
}
|
|
4703
|
+
}
|
|
4704
|
+
return missingDeps;
|
|
4705
|
+
}
|
|
4706
|
+
async function installTestDependencies(missingDeps) {
|
|
4707
|
+
const hasPnpm = fs11.existsSync(path12.join(process.cwd(), "pnpm-lock.yaml"));
|
|
4708
|
+
const hasYarn = fs11.existsSync(path12.join(process.cwd(), "yarn.lock"));
|
|
4709
|
+
const pkgManager = hasPnpm ? "pnpm" : hasYarn ? "yarn" : "npm";
|
|
4710
|
+
const { execFile: execFile5 } = await import("child_process");
|
|
4711
|
+
let allSuccess = true;
|
|
4712
|
+
for (const dep of missingDeps) {
|
|
4713
|
+
const spinner = ora4(`\u6B63\u5728\u5B89\u88C5 ${dep.pkg}...`).start();
|
|
4714
|
+
const isPlaywright = dep.pkg === "@playwright/test";
|
|
4715
|
+
const installArgs = hasPnpm ? ["add", "-D", dep.pkg] : hasYarn ? ["add", "-D", dep.pkg] : ["install", "-D", dep.pkg];
|
|
4716
|
+
try {
|
|
4717
|
+
await new Promise((resolve, reject) => {
|
|
4718
|
+
const cmd = pkgManager === "npm" ? "npm" : pkgManager;
|
|
4719
|
+
const child = execFile5(cmd, installArgs, {
|
|
4720
|
+
cwd: process.cwd(),
|
|
4721
|
+
shell: true,
|
|
4722
|
+
stdio: "pipe"
|
|
4723
|
+
}, (error) => {
|
|
4724
|
+
if (error) reject(error);
|
|
4725
|
+
else resolve();
|
|
4726
|
+
});
|
|
4727
|
+
child.on("error", reject);
|
|
4728
|
+
});
|
|
4729
|
+
if (isPlaywright) {
|
|
4730
|
+
spinner.text = `\u6B63\u5728\u5B89\u88C5 Playwright \u6D4F\u89C8\u5668...`;
|
|
4731
|
+
await new Promise((resolve, reject) => {
|
|
4732
|
+
const child = execFile5("npx", ["playwright", "install"], {
|
|
4733
|
+
cwd: process.cwd(),
|
|
4734
|
+
shell: true,
|
|
4735
|
+
stdio: "pipe"
|
|
4736
|
+
}, (error) => {
|
|
4737
|
+
if (error) reject(error);
|
|
4738
|
+
else resolve();
|
|
4739
|
+
});
|
|
4740
|
+
child.on("error", reject);
|
|
4741
|
+
});
|
|
4742
|
+
}
|
|
4743
|
+
spinner.succeed(`${dep.pkg} \u5B89\u88C5\u6210\u529F`);
|
|
4744
|
+
} catch (error) {
|
|
4745
|
+
spinner.fail(`${dep.pkg} \u5B89\u88C5\u5931\u8D25`);
|
|
4746
|
+
console.log(chalk5.gray(` \u53EF\u624B\u52A8\u5B89\u88C5: ${chalk5.cyan(dep.installCmd)}`));
|
|
4747
|
+
allSuccess = false;
|
|
4748
|
+
}
|
|
4749
|
+
}
|
|
4750
|
+
return allSuccess;
|
|
4751
|
+
}
|
|
4455
4752
|
|
|
4456
4753
|
// src/commands/mock.ts
|
|
4457
4754
|
import chalk6 from "chalk";
|
|
@@ -4575,219 +4872,8 @@ function showStatus() {
|
|
|
4575
4872
|
// src/commands/report.ts
|
|
4576
4873
|
import chalk7 from "chalk";
|
|
4577
4874
|
import ora6 from "ora";
|
|
4578
|
-
import
|
|
4875
|
+
import fs12 from "fs";
|
|
4579
4876
|
import path13 from "path";
|
|
4580
|
-
|
|
4581
|
-
// src/services/reporter.ts
|
|
4582
|
-
import fs10 from "fs";
|
|
4583
|
-
import path12 from "path";
|
|
4584
|
-
function aggregateResults(results) {
|
|
4585
|
-
const summary = {
|
|
4586
|
-
total: 0,
|
|
4587
|
-
passed: 0,
|
|
4588
|
-
failed: 0,
|
|
4589
|
-
skipped: 0,
|
|
4590
|
-
pending: 0
|
|
4591
|
-
};
|
|
4592
|
-
const byType = {};
|
|
4593
|
-
let coverage;
|
|
4594
|
-
for (const result of results) {
|
|
4595
|
-
const typeKey = result.type;
|
|
4596
|
-
if (!byType[typeKey]) {
|
|
4597
|
-
byType[typeKey] = { total: 0, passed: 0, failed: 0, skipped: 0 };
|
|
4598
|
-
}
|
|
4599
|
-
for (const suite of result.suites) {
|
|
4600
|
-
for (const test of suite.tests) {
|
|
4601
|
-
summary.total++;
|
|
4602
|
-
byType[typeKey].total++;
|
|
4603
|
-
if (test.status === "passed") {
|
|
4604
|
-
summary.passed++;
|
|
4605
|
-
byType[typeKey].passed++;
|
|
4606
|
-
} else if (test.status === "failed") {
|
|
4607
|
-
summary.failed++;
|
|
4608
|
-
byType[typeKey].failed++;
|
|
4609
|
-
} else if (test.status === "skipped") {
|
|
4610
|
-
summary.skipped++;
|
|
4611
|
-
byType[typeKey].skipped++;
|
|
4612
|
-
} else {
|
|
4613
|
-
summary.pending++;
|
|
4614
|
-
}
|
|
4615
|
-
}
|
|
4616
|
-
}
|
|
4617
|
-
if (result.coverage) {
|
|
4618
|
-
if (!coverage) {
|
|
4619
|
-
coverage = { ...result.coverage };
|
|
4620
|
-
} else {
|
|
4621
|
-
coverage.lines = Math.max(coverage.lines, result.coverage.lines);
|
|
4622
|
-
coverage.statements = Math.max(coverage.statements, result.coverage.statements);
|
|
4623
|
-
coverage.functions = Math.max(coverage.functions, result.coverage.functions);
|
|
4624
|
-
coverage.branches = Math.max(coverage.branches, result.coverage.branches);
|
|
4625
|
-
}
|
|
4626
|
-
}
|
|
4627
|
-
}
|
|
4628
|
-
const totalDuration = results.reduce((sum, r) => sum + r.duration, 0);
|
|
4629
|
-
return {
|
|
4630
|
-
timestamp: Date.now(),
|
|
4631
|
-
duration: totalDuration,
|
|
4632
|
-
results,
|
|
4633
|
-
summary,
|
|
4634
|
-
byType,
|
|
4635
|
-
coverage: coverage?.lines ? coverage : void 0
|
|
4636
|
-
};
|
|
4637
|
-
}
|
|
4638
|
-
function formatDuration2(ms) {
|
|
4639
|
-
if (ms < 1e3) return `${ms}ms`;
|
|
4640
|
-
if (ms < 6e4) return `${(ms / 1e3).toFixed(2)}s`;
|
|
4641
|
-
const minutes = Math.floor(ms / 6e4);
|
|
4642
|
-
const seconds = (ms % 6e4 / 1e3).toFixed(0);
|
|
4643
|
-
return `${minutes}m ${seconds}s`;
|
|
4644
|
-
}
|
|
4645
|
-
function formatTimestamp(ts) {
|
|
4646
|
-
return new Date(ts).toLocaleString("zh-CN", {
|
|
4647
|
-
year: "numeric",
|
|
4648
|
-
month: "2-digit",
|
|
4649
|
-
day: "2-digit",
|
|
4650
|
-
hour: "2-digit",
|
|
4651
|
-
minute: "2-digit",
|
|
4652
|
-
second: "2-digit"
|
|
4653
|
-
});
|
|
4654
|
-
}
|
|
4655
|
-
function pct(value) {
|
|
4656
|
-
return `${(value * 100).toFixed(1)}%`;
|
|
4657
|
-
}
|
|
4658
|
-
function renderCoverageMD(coverage) {
|
|
4659
|
-
return `
|
|
4660
|
-
### \u8986\u76D6\u7387
|
|
4661
|
-
|
|
4662
|
-
| \u6307\u6807 | \u8986\u76D6\u7387 | \u8FDB\u5EA6 |
|
|
4663
|
-
|------|--------|------|
|
|
4664
|
-
| \u8BED\u53E5 (Statements) | ${pct(coverage.statements)} | ${renderProgressBar(coverage.statements)} |
|
|
4665
|
-
| \u5206\u652F (Branches) | ${pct(coverage.branches)} | ${renderProgressBar(coverage.branches)} |
|
|
4666
|
-
| \u51FD\u6570 (Functions) | ${pct(coverage.functions)} | ${renderProgressBar(coverage.functions)} |
|
|
4667
|
-
| \u884C (Lines) | ${pct(coverage.lines)} | ${renderProgressBar(coverage.lines)} |
|
|
4668
|
-
`;
|
|
4669
|
-
}
|
|
4670
|
-
function renderProgressBar(value) {
|
|
4671
|
-
const filled = Math.round(value * 10);
|
|
4672
|
-
const empty = 10 - filled;
|
|
4673
|
-
return `${"\u2588".repeat(filled)}${"\u2591".repeat(empty)}`;
|
|
4674
|
-
}
|
|
4675
|
-
var TYPE_LABELS2 = {
|
|
4676
|
-
unit: "\u5355\u5143\u6D4B\u8BD5",
|
|
4677
|
-
component: "\u7EC4\u4EF6\u6D4B\u8BD5",
|
|
4678
|
-
e2e: "E2E \u6D4B\u8BD5",
|
|
4679
|
-
api: "API \u6D4B\u8BD5",
|
|
4680
|
-
visual: "\u89C6\u89C9\u56DE\u5F52\u6D4B\u8BD5",
|
|
4681
|
-
performance: "\u6027\u80FD\u6D4B\u8BD5"
|
|
4682
|
-
};
|
|
4683
|
-
function generateMDReport(data) {
|
|
4684
|
-
const passRate = data.summary.total > 0 ? (data.summary.passed / data.summary.total * 100).toFixed(1) : "0";
|
|
4685
|
-
const rateIcon = parseFloat(passRate) >= 80 ? "\u2705" : parseFloat(passRate) >= 50 ? "\u26A0\uFE0F" : "\u274C";
|
|
4686
|
-
const lines = [];
|
|
4687
|
-
lines.push(`# QAT \u6D4B\u8BD5\u62A5\u544A`);
|
|
4688
|
-
lines.push("");
|
|
4689
|
-
lines.push(`> \u751F\u6210\u65F6\u95F4: ${formatTimestamp(data.timestamp)} | \u603B\u8017\u65F6: ${formatDuration2(data.duration)}`);
|
|
4690
|
-
lines.push("");
|
|
4691
|
-
lines.push(`## \u603B\u89C8`);
|
|
4692
|
-
lines.push("");
|
|
4693
|
-
lines.push(`| \u6307\u6807 | \u6570\u503C |`);
|
|
4694
|
-
lines.push(`|------|------|`);
|
|
4695
|
-
lines.push(`| \u901A\u8FC7\u7387 | ${rateIcon} **${passRate}%** |`);
|
|
4696
|
-
lines.push(`| \u603B\u7528\u4F8B | ${data.summary.total} |`);
|
|
4697
|
-
lines.push(`| \u2705 \u901A\u8FC7 | ${data.summary.passed} |`);
|
|
4698
|
-
if (data.summary.failed > 0) lines.push(`| \u274C \u5931\u8D25 | ${data.summary.failed} |`);
|
|
4699
|
-
if (data.summary.skipped > 0) lines.push(`| \u23ED\uFE0F \u8DF3\u8FC7 | ${data.summary.skipped} |`);
|
|
4700
|
-
if (data.summary.pending > 0) lines.push(`| \u23F3 \u5F85\u5B9A | ${data.summary.pending} |`);
|
|
4701
|
-
lines.push(`| \u23F1\uFE0F \u8017\u65F6 | ${formatDuration2(data.duration)} |`);
|
|
4702
|
-
lines.push("");
|
|
4703
|
-
if (Object.keys(data.byType).length > 0) {
|
|
4704
|
-
lines.push(`## \u6309\u7C7B\u578B\u7EDF\u8BA1`);
|
|
4705
|
-
lines.push("");
|
|
4706
|
-
lines.push(`| \u7C7B\u578B | \u901A\u8FC7 | \u5931\u8D25 | \u8DF3\u8FC7 | \u603B\u8BA1 | \u901A\u8FC7\u7387 |`);
|
|
4707
|
-
lines.push(`|------|------|------|------|------|--------|`);
|
|
4708
|
-
for (const [type, stats] of Object.entries(data.byType)) {
|
|
4709
|
-
const label = TYPE_LABELS2[type] || type;
|
|
4710
|
-
const rate = stats.total > 0 ? (stats.passed / stats.total * 100).toFixed(0) + "%" : "-";
|
|
4711
|
-
lines.push(`| ${label} | ${stats.passed} | ${stats.failed} | ${stats.skipped} | ${stats.total} | ${rate} |`);
|
|
4712
|
-
}
|
|
4713
|
-
lines.push("");
|
|
4714
|
-
}
|
|
4715
|
-
if (data.coverage) {
|
|
4716
|
-
lines.push(renderCoverageMD(data.coverage));
|
|
4717
|
-
lines.push("");
|
|
4718
|
-
}
|
|
4719
|
-
lines.push(`## \u6D4B\u8BD5\u8BE6\u60C5`);
|
|
4720
|
-
lines.push("");
|
|
4721
|
-
for (const result of data.results) {
|
|
4722
|
-
const typeLabel = TYPE_LABELS2[result.type] || result.type;
|
|
4723
|
-
const statusIcon = result.status === "passed" ? "\u2705" : result.status === "failed" ? "\u274C" : "\u26A0\uFE0F";
|
|
4724
|
-
lines.push(`### ${statusIcon} ${typeLabel}`);
|
|
4725
|
-
lines.push("");
|
|
4726
|
-
if (result.suites.length === 0) {
|
|
4727
|
-
lines.push(`*\u65E0\u6D4B\u8BD5\u7ED3\u679C*`);
|
|
4728
|
-
lines.push("");
|
|
4729
|
-
continue;
|
|
4730
|
-
}
|
|
4731
|
-
for (const suite of result.suites) {
|
|
4732
|
-
const suiteIcon = suite.status === "passed" ? "\u2705" : suite.status === "failed" ? "\u274C" : "\u26A0\uFE0F";
|
|
4733
|
-
lines.push(`#### ${suiteIcon} ${suite.name}`);
|
|
4734
|
-
lines.push("");
|
|
4735
|
-
lines.push(`- \u6587\u4EF6: \`${suite.file}\``);
|
|
4736
|
-
lines.push(`- \u8017\u65F6: ${formatDuration2(suite.duration)}`);
|
|
4737
|
-
lines.push("");
|
|
4738
|
-
if (suite.tests.length > 0) {
|
|
4739
|
-
lines.push(`| \u72B6\u6001 | \u6D4B\u8BD5\u540D\u79F0 | \u8017\u65F6 |`);
|
|
4740
|
-
lines.push(`|------|----------|------|`);
|
|
4741
|
-
for (const test of suite.tests) {
|
|
4742
|
-
const testIcon = test.status === "passed" ? "\u2705" : test.status === "failed" ? "\u274C" : test.status === "skipped" ? "\u23ED\uFE0F" : "\u23F3";
|
|
4743
|
-
const name = test.error ? `**${test.name}**` : test.name;
|
|
4744
|
-
lines.push(`| ${testIcon} | ${name} | ${formatDuration2(test.duration)} |`);
|
|
4745
|
-
}
|
|
4746
|
-
lines.push("");
|
|
4747
|
-
}
|
|
4748
|
-
const failedTests = suite.tests.filter((t) => t.status === "failed" && t.error);
|
|
4749
|
-
if (failedTests.length > 0) {
|
|
4750
|
-
lines.push(`<details>`);
|
|
4751
|
-
lines.push(`<summary>\u274C \u5931\u8D25\u8BE6\u60C5 (${failedTests.length})</summary>`);
|
|
4752
|
-
lines.push("");
|
|
4753
|
-
for (const test of failedTests) {
|
|
4754
|
-
lines.push(`**${test.name}**`);
|
|
4755
|
-
lines.push("```");
|
|
4756
|
-
lines.push(test.error.message);
|
|
4757
|
-
if (test.error.stack) {
|
|
4758
|
-
lines.push(test.error.stack);
|
|
4759
|
-
}
|
|
4760
|
-
if (test.error.expected && test.error.actual) {
|
|
4761
|
-
lines.push(`Expected: ${test.error.expected}`);
|
|
4762
|
-
lines.push(`Actual: ${test.error.actual}`);
|
|
4763
|
-
}
|
|
4764
|
-
lines.push("```");
|
|
4765
|
-
lines.push("");
|
|
4766
|
-
}
|
|
4767
|
-
lines.push(`</details>`);
|
|
4768
|
-
lines.push("");
|
|
4769
|
-
}
|
|
4770
|
-
}
|
|
4771
|
-
}
|
|
4772
|
-
lines.push("---");
|
|
4773
|
-
lines.push("");
|
|
4774
|
-
lines.push(`*\u7531 QAT \u81EA\u52A8\u5316\u6D4B\u8BD5\u5DE5\u5177\u751F\u6210 | ${formatTimestamp(data.timestamp)}*`);
|
|
4775
|
-
return lines.join("\n");
|
|
4776
|
-
}
|
|
4777
|
-
function writeReportToDisk(data, outputDir) {
|
|
4778
|
-
const md = generateMDReport(data);
|
|
4779
|
-
const dir = path12.resolve(outputDir);
|
|
4780
|
-
if (!fs10.existsSync(dir)) {
|
|
4781
|
-
fs10.mkdirSync(dir, { recursive: true });
|
|
4782
|
-
}
|
|
4783
|
-
const mdPath = path12.join(dir, "report.md");
|
|
4784
|
-
fs10.writeFileSync(mdPath, md, "utf-8");
|
|
4785
|
-
const jsonPath = path12.join(dir, "report.json");
|
|
4786
|
-
fs10.writeFileSync(jsonPath, JSON.stringify(data, null, 2), "utf-8");
|
|
4787
|
-
return mdPath;
|
|
4788
|
-
}
|
|
4789
|
-
|
|
4790
|
-
// src/commands/report.ts
|
|
4791
4877
|
var RESULTS_DIR2 = ".qat-results";
|
|
4792
4878
|
function registerReportCommand(program2) {
|
|
4793
4879
|
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) => {
|
|
@@ -4824,14 +4910,14 @@ async function executeReport(options) {
|
|
|
4824
4910
|
function collectResults() {
|
|
4825
4911
|
const results = [];
|
|
4826
4912
|
const resultsPath = path13.join(process.cwd(), RESULTS_DIR2);
|
|
4827
|
-
if (!
|
|
4913
|
+
if (!fs12.existsSync(resultsPath)) {
|
|
4828
4914
|
return results;
|
|
4829
4915
|
}
|
|
4830
|
-
const files =
|
|
4916
|
+
const files = fs12.readdirSync(resultsPath).filter((f) => f.endsWith(".json")).sort().reverse();
|
|
4831
4917
|
if (files.length > 0) {
|
|
4832
4918
|
const latestFile = path13.join(resultsPath, files[0]);
|
|
4833
4919
|
try {
|
|
4834
|
-
const data = JSON.parse(
|
|
4920
|
+
const data = JSON.parse(fs12.readFileSync(latestFile, "utf-8"));
|
|
4835
4921
|
if (Array.isArray(data)) {
|
|
4836
4922
|
results.push(...data);
|
|
4837
4923
|
} else if (data.results) {
|
|
@@ -4844,17 +4930,17 @@ function collectResults() {
|
|
|
4844
4930
|
}
|
|
4845
4931
|
function saveResultToHistory(reportData) {
|
|
4846
4932
|
const resultsPath = path13.join(process.cwd(), RESULTS_DIR2);
|
|
4847
|
-
if (!
|
|
4848
|
-
|
|
4933
|
+
if (!fs12.existsSync(resultsPath)) {
|
|
4934
|
+
fs12.mkdirSync(resultsPath, { recursive: true });
|
|
4849
4935
|
}
|
|
4850
4936
|
const timestamp = new Date(reportData.timestamp).toISOString().replace(/[:.]/g, "-");
|
|
4851
4937
|
const fileName = `result-${timestamp}.json`;
|
|
4852
4938
|
const filePath = path13.join(resultsPath, fileName);
|
|
4853
|
-
|
|
4854
|
-
const files =
|
|
4939
|
+
fs12.writeFileSync(filePath, JSON.stringify(reportData, null, 2), "utf-8");
|
|
4940
|
+
const files = fs12.readdirSync(resultsPath).filter((f) => f.startsWith("result-") && f.endsWith(".json")).sort();
|
|
4855
4941
|
while (files.length > 20) {
|
|
4856
4942
|
const oldest = files.shift();
|
|
4857
|
-
|
|
4943
|
+
fs12.unlinkSync(path13.join(resultsPath, oldest));
|
|
4858
4944
|
}
|
|
4859
4945
|
}
|
|
4860
4946
|
function displayReportResult(reportPath, data) {
|
|
@@ -4915,19 +5001,19 @@ import chalk8 from "chalk";
|
|
|
4915
5001
|
import ora7 from "ora";
|
|
4916
5002
|
|
|
4917
5003
|
// src/services/visual.ts
|
|
4918
|
-
import
|
|
5004
|
+
import fs13 from "fs";
|
|
4919
5005
|
import path14 from "path";
|
|
4920
5006
|
import pixelmatch from "pixelmatch";
|
|
4921
5007
|
import { PNG } from "pngjs";
|
|
4922
5008
|
function compareImages(baselinePath, currentPath, diffOutputPath, threshold = 0.1) {
|
|
4923
|
-
if (!
|
|
5009
|
+
if (!fs13.existsSync(baselinePath)) {
|
|
4924
5010
|
throw new Error(`\u57FA\u7EBF\u56FE\u7247\u4E0D\u5B58\u5728: ${baselinePath}`);
|
|
4925
5011
|
}
|
|
4926
|
-
if (!
|
|
5012
|
+
if (!fs13.existsSync(currentPath)) {
|
|
4927
5013
|
throw new Error(`\u5F53\u524D\u56FE\u7247\u4E0D\u5B58\u5728: ${currentPath}`);
|
|
4928
5014
|
}
|
|
4929
|
-
const baseline = PNG.sync.read(
|
|
4930
|
-
const current = PNG.sync.read(
|
|
5015
|
+
const baseline = PNG.sync.read(fs13.readFileSync(baselinePath));
|
|
5016
|
+
const current = PNG.sync.read(fs13.readFileSync(currentPath));
|
|
4931
5017
|
if (baseline.width !== current.width || baseline.height !== current.height) {
|
|
4932
5018
|
return {
|
|
4933
5019
|
passed: false,
|
|
@@ -4956,10 +5042,10 @@ function compareImages(baselinePath, currentPath, diffOutputPath, threshold = 0.
|
|
|
4956
5042
|
let diffPath;
|
|
4957
5043
|
if (diffPixels > 0) {
|
|
4958
5044
|
const diffDir = path14.dirname(diffOutputPath);
|
|
4959
|
-
if (!
|
|
4960
|
-
|
|
5045
|
+
if (!fs13.existsSync(diffDir)) {
|
|
5046
|
+
fs13.mkdirSync(diffDir, { recursive: true });
|
|
4961
5047
|
}
|
|
4962
|
-
|
|
5048
|
+
fs13.writeFileSync(diffOutputPath, PNG.sync.write(diff));
|
|
4963
5049
|
diffPath = diffOutputPath;
|
|
4964
5050
|
}
|
|
4965
5051
|
return {
|
|
@@ -4973,69 +5059,69 @@ function compareImages(baselinePath, currentPath, diffOutputPath, threshold = 0.
|
|
|
4973
5059
|
};
|
|
4974
5060
|
}
|
|
4975
5061
|
function createBaseline(currentPath, baselinePath) {
|
|
4976
|
-
if (!
|
|
5062
|
+
if (!fs13.existsSync(currentPath)) {
|
|
4977
5063
|
throw new Error(`\u5F53\u524D\u622A\u56FE\u4E0D\u5B58\u5728: ${currentPath}`);
|
|
4978
5064
|
}
|
|
4979
5065
|
const baselineDir = path14.dirname(baselinePath);
|
|
4980
|
-
if (!
|
|
4981
|
-
|
|
5066
|
+
if (!fs13.existsSync(baselineDir)) {
|
|
5067
|
+
fs13.mkdirSync(baselineDir, { recursive: true });
|
|
4982
5068
|
}
|
|
4983
|
-
|
|
5069
|
+
fs13.copyFileSync(currentPath, baselinePath);
|
|
4984
5070
|
return baselinePath;
|
|
4985
5071
|
}
|
|
4986
5072
|
function updateAllBaselines(currentDir, baselineDir) {
|
|
4987
5073
|
const updated = [];
|
|
4988
|
-
if (!
|
|
5074
|
+
if (!fs13.existsSync(currentDir)) {
|
|
4989
5075
|
return updated;
|
|
4990
5076
|
}
|
|
4991
|
-
const files =
|
|
5077
|
+
const files = fs13.readdirSync(currentDir).filter((f) => f.endsWith(".png"));
|
|
4992
5078
|
for (const file of files) {
|
|
4993
5079
|
const currentPath = path14.join(currentDir, file);
|
|
4994
5080
|
const baselinePath = path14.join(baselineDir, file);
|
|
4995
5081
|
const baselineDirAbs = path14.dirname(baselinePath);
|
|
4996
|
-
if (!
|
|
4997
|
-
|
|
5082
|
+
if (!fs13.existsSync(baselineDirAbs)) {
|
|
5083
|
+
fs13.mkdirSync(baselineDirAbs, { recursive: true });
|
|
4998
5084
|
}
|
|
4999
|
-
|
|
5085
|
+
fs13.copyFileSync(currentPath, baselinePath);
|
|
5000
5086
|
updated.push(file);
|
|
5001
5087
|
}
|
|
5002
5088
|
return updated;
|
|
5003
5089
|
}
|
|
5004
5090
|
function cleanBaselines(baselineDir) {
|
|
5005
|
-
if (!
|
|
5091
|
+
if (!fs13.existsSync(baselineDir)) {
|
|
5006
5092
|
return 0;
|
|
5007
5093
|
}
|
|
5008
|
-
const files =
|
|
5094
|
+
const files = fs13.readdirSync(baselineDir).filter((f) => f.endsWith(".png"));
|
|
5009
5095
|
let count = 0;
|
|
5010
5096
|
for (const file of files) {
|
|
5011
|
-
|
|
5097
|
+
fs13.unlinkSync(path14.join(baselineDir, file));
|
|
5012
5098
|
count++;
|
|
5013
5099
|
}
|
|
5014
5100
|
return count;
|
|
5015
5101
|
}
|
|
5016
5102
|
function cleanDiffs(diffDir) {
|
|
5017
|
-
if (!
|
|
5103
|
+
if (!fs13.existsSync(diffDir)) {
|
|
5018
5104
|
return 0;
|
|
5019
5105
|
}
|
|
5020
|
-
const files =
|
|
5106
|
+
const files = fs13.readdirSync(diffDir).filter((f) => f.endsWith(".png"));
|
|
5021
5107
|
let count = 0;
|
|
5022
5108
|
for (const file of files) {
|
|
5023
|
-
|
|
5109
|
+
fs13.unlinkSync(path14.join(diffDir, file));
|
|
5024
5110
|
count++;
|
|
5025
5111
|
}
|
|
5026
5112
|
return count;
|
|
5027
5113
|
}
|
|
5028
5114
|
function compareDirectories(baselineDir, currentDir, diffDir, threshold = 0.1) {
|
|
5029
5115
|
const results = [];
|
|
5030
|
-
if (!
|
|
5116
|
+
if (!fs13.existsSync(currentDir)) {
|
|
5031
5117
|
return results;
|
|
5032
5118
|
}
|
|
5033
|
-
const currentFiles =
|
|
5119
|
+
const currentFiles = fs13.readdirSync(currentDir).filter((f) => f.endsWith(".png"));
|
|
5034
5120
|
for (const file of currentFiles) {
|
|
5035
5121
|
const currentPath = path14.join(currentDir, file);
|
|
5036
5122
|
const baselinePath = path14.join(baselineDir, file);
|
|
5037
5123
|
const diffPath = path14.join(diffDir, file);
|
|
5038
|
-
if (!
|
|
5124
|
+
if (!fs13.existsSync(baselinePath)) {
|
|
5039
5125
|
createBaseline(currentPath, baselinePath);
|
|
5040
5126
|
results.push({
|
|
5041
5127
|
passed: true,
|
|
@@ -5067,7 +5153,7 @@ function compareDirectories(baselineDir, currentDir, diffDir, threshold = 0.1) {
|
|
|
5067
5153
|
}
|
|
5068
5154
|
|
|
5069
5155
|
// src/commands/visual.ts
|
|
5070
|
-
import
|
|
5156
|
+
import fs14 from "fs";
|
|
5071
5157
|
import path15 from "path";
|
|
5072
5158
|
function registerVisualCommand(program2) {
|
|
5073
5159
|
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) => {
|
|
@@ -5148,7 +5234,7 @@ function findCurrentScreenshotsDir(baselineDir) {
|
|
|
5148
5234
|
path15.join(process.cwd(), baselineDir, "..", "current")
|
|
5149
5235
|
];
|
|
5150
5236
|
for (const dir of possibleDirs) {
|
|
5151
|
-
if (
|
|
5237
|
+
if (fs14.existsSync(dir)) {
|
|
5152
5238
|
const pngs = findPngFiles(dir);
|
|
5153
5239
|
if (pngs.length > 0) return dir;
|
|
5154
5240
|
}
|
|
@@ -5158,8 +5244,8 @@ function findCurrentScreenshotsDir(baselineDir) {
|
|
|
5158
5244
|
function findPngFiles(dir) {
|
|
5159
5245
|
const files = [];
|
|
5160
5246
|
function walk(d) {
|
|
5161
|
-
if (!
|
|
5162
|
-
const entries =
|
|
5247
|
+
if (!fs14.existsSync(d)) return;
|
|
5248
|
+
const entries = fs14.readdirSync(d, { withFileTypes: true });
|
|
5163
5249
|
for (const entry of entries) {
|
|
5164
5250
|
const fullPath = path15.join(d, entry.name);
|
|
5165
5251
|
if (entry.isDirectory() && entry.name !== "node_modules") {
|
|
@@ -5257,7 +5343,7 @@ import chalk9 from "chalk";
|
|
|
5257
5343
|
import inquirer4 from "inquirer";
|
|
5258
5344
|
import ora8 from "ora";
|
|
5259
5345
|
import { execFile as execFile4 } from "child_process";
|
|
5260
|
-
import
|
|
5346
|
+
import fs15 from "fs";
|
|
5261
5347
|
import path16 from "path";
|
|
5262
5348
|
var DEPENDENCY_GROUPS = [
|
|
5263
5349
|
{
|
|
@@ -5292,7 +5378,7 @@ function registerSetupCommand(program2) {
|
|
|
5292
5378
|
async function executeSetup(options) {
|
|
5293
5379
|
console.log(chalk9.cyan("\n QAT \u4F9D\u8D56\u5B89\u88C5\u5668\n"));
|
|
5294
5380
|
const projectInfo = detectProject();
|
|
5295
|
-
if (!
|
|
5381
|
+
if (!fs15.existsSync(path16.join(process.cwd(), "package.json"))) {
|
|
5296
5382
|
throw new Error("\u672A\u627E\u5230 package.json\uFF0C\u8BF7\u5728\u9879\u76EE\u6839\u76EE\u5F55\u6267\u884C\u6B64\u547D\u4EE4");
|
|
5297
5383
|
}
|
|
5298
5384
|
if (projectInfo.frameworkConfidence > 0) {
|
|
@@ -5327,7 +5413,7 @@ async function executeSetup(options) {
|
|
|
5327
5413
|
]);
|
|
5328
5414
|
if (chooseDir !== "root") {
|
|
5329
5415
|
installDir = path16.join(process.cwd(), chooseDir);
|
|
5330
|
-
if (!
|
|
5416
|
+
if (!fs15.existsSync(path16.join(installDir, "package.json"))) {
|
|
5331
5417
|
throw new Error(`${chooseDir} \u4E0B\u6CA1\u6709 package.json`);
|
|
5332
5418
|
}
|
|
5333
5419
|
}
|
|
@@ -5647,7 +5733,7 @@ async function executeChange(_options) {
|
|
|
5647
5733
|
}
|
|
5648
5734
|
|
|
5649
5735
|
// src/cli.ts
|
|
5650
|
-
var VERSION = "0.3.
|
|
5736
|
+
var VERSION = "0.3.03";
|
|
5651
5737
|
function printLogo() {
|
|
5652
5738
|
const logo = `
|
|
5653
5739
|
${chalk12.bold.cyan(" ___ _ _ _ _ _____ _ _ ")}
|