qat-cli 0.3.1 → 0.3.2
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 +1257 -1244
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +228 -122
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +227 -121
- 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";
|
|
@@ -1262,99 +1262,6 @@ async function testAIConnection(config) {
|
|
|
1262
1262
|
return provider.testConnection();
|
|
1263
1263
|
}
|
|
1264
1264
|
|
|
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
1265
|
// src/services/source-analyzer.ts
|
|
1359
1266
|
import fs4 from "fs";
|
|
1360
1267
|
import path4 from "path";
|
|
@@ -1790,154 +1697,492 @@ function generateMockRoutesFromAPICalls(apiCalls) {
|
|
|
1790
1697
|
return routes;
|
|
1791
1698
|
}
|
|
1792
1699
|
|
|
1793
|
-
// src/services/
|
|
1700
|
+
// src/services/global-config.ts
|
|
1794
1701
|
import fs5 from "fs";
|
|
1795
1702
|
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);
|
|
1703
|
+
import os from "os";
|
|
1704
|
+
var QAT_DIR = path5.join(os.homedir(), ".qat");
|
|
1705
|
+
var AI_CONFIG_PATH = path5.join(QAT_DIR, "ai.json");
|
|
1706
|
+
function loadGlobalAIConfig() {
|
|
1707
|
+
if (!fs5.existsSync(AI_CONFIG_PATH)) {
|
|
1708
|
+
return null;
|
|
1880
1709
|
}
|
|
1881
|
-
|
|
1882
|
-
|
|
1710
|
+
try {
|
|
1711
|
+
const content = fs5.readFileSync(AI_CONFIG_PATH, "utf-8");
|
|
1712
|
+
const config = JSON.parse(content);
|
|
1713
|
+
if (!config.baseUrl || !config.model) {
|
|
1714
|
+
return null;
|
|
1715
|
+
}
|
|
1716
|
+
return config;
|
|
1717
|
+
} catch {
|
|
1718
|
+
return null;
|
|
1883
1719
|
}
|
|
1884
|
-
return template(fullContext);
|
|
1885
1720
|
}
|
|
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");
|
|
1721
|
+
function saveGlobalAIConfig(config) {
|
|
1722
|
+
if (!fs5.existsSync(QAT_DIR)) {
|
|
1723
|
+
fs5.mkdirSync(QAT_DIR, { recursive: true });
|
|
1894
1724
|
}
|
|
1895
|
-
|
|
1725
|
+
fs5.writeFileSync(AI_CONFIG_PATH, JSON.stringify(config, null, 2), "utf-8");
|
|
1896
1726
|
}
|
|
1897
|
-
function
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1727
|
+
function toAIConfig(globalConfig) {
|
|
1728
|
+
return {
|
|
1729
|
+
provider: globalConfig.provider || "openai",
|
|
1730
|
+
apiKey: globalConfig.apiKey || void 0,
|
|
1731
|
+
baseUrl: globalConfig.baseUrl,
|
|
1732
|
+
model: globalConfig.model
|
|
1733
|
+
};
|
|
1734
|
+
}
|
|
1735
|
+
function maskApiKey(apiKey) {
|
|
1736
|
+
if (!apiKey) return "(\u672A\u8BBE\u7F6E)";
|
|
1737
|
+
if (apiKey.length <= 8) return "****";
|
|
1738
|
+
return apiKey.slice(0, 4) + "****" + apiKey.slice(-4);
|
|
1739
|
+
}
|
|
1740
|
+
function getAIConfigPath() {
|
|
1741
|
+
return AI_CONFIG_PATH;
|
|
1903
1742
|
}
|
|
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
1743
|
|
|
1918
|
-
|
|
1919
|
-
{
|
|
1920
|
-
{
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1744
|
+
// src/commands/init.ts
|
|
1745
|
+
function registerInitCommand(program2) {
|
|
1746
|
+
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) => {
|
|
1747
|
+
try {
|
|
1748
|
+
await executeInit(options);
|
|
1749
|
+
} catch (error) {
|
|
1750
|
+
console.error(chalk2.red(`
|
|
1751
|
+
\u2717 ${error instanceof Error ? error.message : String(error)}
|
|
1752
|
+
`));
|
|
1753
|
+
process.exit(1);
|
|
1754
|
+
}
|
|
1755
|
+
});
|
|
1756
|
+
}
|
|
1757
|
+
async function executeInit(options) {
|
|
1758
|
+
const spinner = ora("\u6B63\u5728\u68C0\u6D4B\u9879\u76EE\u7ED3\u6784...").start();
|
|
1759
|
+
const projectInfo = detectProject();
|
|
1760
|
+
spinner.stop();
|
|
1761
|
+
displayProjectInfo(projectInfo);
|
|
1762
|
+
if (!projectInfo.isVue) {
|
|
1763
|
+
const { proceed } = await inquirer.prompt([
|
|
1764
|
+
{
|
|
1765
|
+
type: "confirm",
|
|
1766
|
+
name: "proceed",
|
|
1767
|
+
message: "\u672A\u68C0\u6D4B\u5230 Vue \u9879\u76EE\uFF0C\u662F\u5426\u7EE7\u7EED\u521D\u59CB\u5316\uFF1F",
|
|
1768
|
+
default: false
|
|
1769
|
+
}
|
|
1770
|
+
]);
|
|
1771
|
+
if (!proceed) {
|
|
1772
|
+
console.log(chalk2.gray("\n \u5DF2\u53D6\u6D88\u521D\u59CB\u5316\n"));
|
|
1773
|
+
return;
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
let globalAI = loadGlobalAIConfig();
|
|
1777
|
+
if (!globalAI) {
|
|
1778
|
+
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"));
|
|
1779
|
+
globalAI = await promptAIConfig();
|
|
1780
|
+
saveGlobalAIConfig(globalAI);
|
|
1781
|
+
console.log(chalk2.gray(` \u914D\u7F6E\u5DF2\u4FDD\u5B58\u81F3 ${getAIConfigPath()}
|
|
1782
|
+
`));
|
|
1783
|
+
} else {
|
|
1784
|
+
console.log(chalk2.green(` \u2713 \u5F53\u524D AI \u6A21\u578B: ${chalk2.white(globalAI.model)} @ ${chalk2.gray(globalAI.baseUrl)} (${maskApiKey(globalAI.apiKey)})
|
|
1785
|
+
`));
|
|
1786
|
+
}
|
|
1787
|
+
const aiConfig = toAIConfig(globalAI);
|
|
1788
|
+
if (aiConfig.apiKey || aiConfig.baseUrl) {
|
|
1789
|
+
const testSpinner = ora(`\u6B63\u5728\u6D4B\u8BD5 AI \u8FDE\u901A\u6027 (${globalAI.model})...`).start();
|
|
1790
|
+
try {
|
|
1791
|
+
const result = await testAIConnection(aiConfig);
|
|
1792
|
+
if (result.ok) {
|
|
1793
|
+
testSpinner.succeed(`AI \u8FDE\u901A\u6B63\u5E38 ${chalk2.gray(`${globalAI.model} (${result.latencyMs}ms)`)}`);
|
|
1794
|
+
} else {
|
|
1795
|
+
testSpinner.fail(`AI \u8FDE\u901A\u5F02\u5E38: ${result.message}`);
|
|
1796
|
+
console.log(chalk2.yellow(" \u53EF\u8FD0\u884C qat change \u4FEE\u6539 AI \u914D\u7F6E\u3002"));
|
|
1797
|
+
}
|
|
1798
|
+
} catch (error) {
|
|
1799
|
+
testSpinner.fail(`AI \u8FDE\u901A\u6D4B\u8BD5\u5931\u8D25: ${error instanceof Error ? error.message : String(error)}`);
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
const config = buildProjectConfig(projectInfo);
|
|
1803
|
+
let configPath;
|
|
1804
|
+
const existingConfigPath = path6.join(process.cwd(), "qat.config.js");
|
|
1805
|
+
const existingTsPath = path6.join(process.cwd(), "qat.config.ts");
|
|
1806
|
+
const configExists = fs6.existsSync(existingConfigPath) || fs6.existsSync(existingTsPath);
|
|
1807
|
+
if (configExists && !options.force) {
|
|
1808
|
+
const { overwrite } = await inquirer.prompt([
|
|
1809
|
+
{
|
|
1810
|
+
type: "confirm",
|
|
1811
|
+
name: "overwrite",
|
|
1812
|
+
message: "\u914D\u7F6E\u6587\u4EF6 qat.config.js \u5DF2\u5B58\u5728\uFF0C\u662F\u5426\u8986\u76D6\uFF1F",
|
|
1813
|
+
default: true
|
|
1814
|
+
}
|
|
1815
|
+
]);
|
|
1816
|
+
if (!overwrite) {
|
|
1817
|
+
console.log(chalk2.gray(" \u4FDD\u7559\u73B0\u6709\u914D\u7F6E\u6587\u4EF6\uFF0C\u7EE7\u7EED\u540E\u7EED\u6B65\u9AA4..."));
|
|
1818
|
+
configPath = existingConfigPath;
|
|
1819
|
+
} else {
|
|
1820
|
+
const fileSpinner = ora("\u6B63\u5728\u8986\u76D6\u914D\u7F6E\u6587\u4EF6...").start();
|
|
1821
|
+
try {
|
|
1822
|
+
configPath = await writeConfigFile(process.cwd(), config, true);
|
|
1823
|
+
fileSpinner.succeed("\u914D\u7F6E\u6587\u4EF6\u5DF2\u8986\u76D6");
|
|
1824
|
+
} catch (error) {
|
|
1825
|
+
fileSpinner.fail("\u914D\u7F6E\u6587\u4EF6\u8986\u76D6\u5931\u8D25");
|
|
1826
|
+
throw error;
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
} else {
|
|
1830
|
+
const fileSpinner = ora("\u6B63\u5728\u751F\u6210\u914D\u7F6E\u6587\u4EF6...").start();
|
|
1831
|
+
try {
|
|
1832
|
+
configPath = await writeConfigFile(process.cwd(), config, options.force);
|
|
1833
|
+
fileSpinner.succeed("\u914D\u7F6E\u6587\u4EF6\u5DF2\u751F\u6210");
|
|
1834
|
+
} catch (error) {
|
|
1835
|
+
fileSpinner.fail("\u914D\u7F6E\u6587\u4EF6\u751F\u6210\u5931\u8D25");
|
|
1836
|
+
throw error;
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
const dirSpinner = ora("\u6B63\u5728\u521B\u5EFA\u6D4B\u8BD5\u76EE\u5F55...").start();
|
|
1840
|
+
const createdDirs = createTestDirectories(config);
|
|
1841
|
+
dirSpinner.succeed("\u6D4B\u8BD5\u76EE\u5F55\u5DF2\u521B\u5EFA");
|
|
1842
|
+
if (config.mock?.enabled !== false) {
|
|
1843
|
+
const mockDir = config.mock?.routesDir || DEFAULT_CONFIG.mock.routesDir;
|
|
1844
|
+
initMockRoutesDir(mockDir);
|
|
1845
|
+
const srcDir2 = config.project?.srcDir || "src";
|
|
1846
|
+
const apiCalls = scanAPICalls(srcDir2);
|
|
1847
|
+
if (apiCalls.length > 0) {
|
|
1848
|
+
const mockRoutes = generateMockRoutesFromAPICalls(apiCalls);
|
|
1849
|
+
const mockFilePath = path6.join(process.cwd(), mockDir, "auto-generated.json");
|
|
1850
|
+
if (!fs6.existsSync(mockFilePath)) {
|
|
1851
|
+
fs6.writeFileSync(mockFilePath, JSON.stringify(mockRoutes, null, 2), "utf-8");
|
|
1852
|
+
console.log(chalk2.green(` \u81EA\u52A8\u53D1\u73B0 ${apiCalls.length} \u4E2A API \u63A5\u53E3\uFF0C\u5DF2\u751F\u6210 Mock \u8DEF\u7531`));
|
|
1853
|
+
}
|
|
1854
|
+
} else {
|
|
1855
|
+
console.log(chalk2.gray(" \u672A\u53D1\u73B0 API \u8C03\u7528\uFF0C\u5DF2\u751F\u6210\u793A\u4F8B Mock \u8DEF\u7531"));
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
const srcDir = config.project?.srcDir || "src";
|
|
1859
|
+
const components = discoverVueComponents(process.cwd(), srcDir);
|
|
1860
|
+
const utilities = discoverUtilityFiles(process.cwd(), srcDir);
|
|
1861
|
+
const totalFiles = components.length + utilities.length;
|
|
1862
|
+
if (totalFiles > 0) {
|
|
1863
|
+
console.log(chalk2.cyan(`
|
|
1864
|
+
\u53D1\u73B0 ${totalFiles} \u4E2A\u53EF\u6D4B\u8BD5\u6587\u4EF6 (${components.length} \u7EC4\u4EF6, ${utilities.length} \u5DE5\u5177/\u670D\u52A1)`));
|
|
1865
|
+
}
|
|
1866
|
+
displayResult(configPath, createdDirs, totalFiles, projectInfo);
|
|
1867
|
+
}
|
|
1868
|
+
async function promptAIConfig() {
|
|
1869
|
+
const answers = await inquirer.prompt([
|
|
1870
|
+
{
|
|
1871
|
+
type: "input",
|
|
1872
|
+
name: "apiKey",
|
|
1873
|
+
message: "API Key (Ollama \u672C\u5730\u53EF\u7559\u7A7A):",
|
|
1874
|
+
default: ""
|
|
1875
|
+
},
|
|
1876
|
+
{
|
|
1877
|
+
type: "input",
|
|
1878
|
+
name: "baseUrl",
|
|
1879
|
+
message: "API Base URL:",
|
|
1880
|
+
default: "https://api.deepseek.com/v1",
|
|
1881
|
+
validate: (input) => {
|
|
1882
|
+
if (!input.trim()) return "Base URL \u4E0D\u80FD\u4E3A\u7A7A";
|
|
1883
|
+
if (!input.trim().startsWith("http")) return "URL \u5FC5\u987B\u4EE5 http(s):// \u5F00\u5934";
|
|
1884
|
+
return true;
|
|
1885
|
+
}
|
|
1886
|
+
},
|
|
1887
|
+
{
|
|
1888
|
+
type: "input",
|
|
1889
|
+
name: "model",
|
|
1890
|
+
message: "\u6A21\u578B\u540D\u79F0:",
|
|
1891
|
+
default: "deepseek-chat",
|
|
1892
|
+
validate: (input) => {
|
|
1893
|
+
if (!input.trim()) return "\u6A21\u578B\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A";
|
|
1894
|
+
return true;
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
]);
|
|
1898
|
+
return {
|
|
1899
|
+
provider: "openai",
|
|
1900
|
+
apiKey: answers.apiKey?.trim() || "",
|
|
1901
|
+
baseUrl: answers.baseUrl?.trim() || "https://api.deepseek.com/v1",
|
|
1902
|
+
model: answers.model?.trim() || "deepseek-chat"
|
|
1903
|
+
};
|
|
1904
|
+
}
|
|
1905
|
+
function buildProjectConfig(projectInfo) {
|
|
1906
|
+
return {
|
|
1907
|
+
project: {
|
|
1908
|
+
framework: projectInfo.framework,
|
|
1909
|
+
uiLibrary: projectInfo.uiLibrary !== "none" ? projectInfo.uiLibrary : void 0,
|
|
1910
|
+
monorepo: projectInfo.monorepo !== "none" ? projectInfo.monorepo : void 0,
|
|
1911
|
+
vite: projectInfo.isVite,
|
|
1912
|
+
srcDir: projectInfo.srcDir,
|
|
1913
|
+
appDir: projectInfo.appDirs.length > 0 ? projectInfo.appDirs[0] : void 0
|
|
1914
|
+
},
|
|
1915
|
+
vitest: {
|
|
1916
|
+
enabled: true,
|
|
1917
|
+
coverage: true,
|
|
1918
|
+
globals: true,
|
|
1919
|
+
environment: "happy-dom"
|
|
1920
|
+
},
|
|
1921
|
+
playwright: {
|
|
1922
|
+
enabled: true,
|
|
1923
|
+
browsers: ["chromium"],
|
|
1924
|
+
baseURL: "http://localhost:5173",
|
|
1925
|
+
screenshot: "only-on-failure"
|
|
1926
|
+
},
|
|
1927
|
+
visual: {
|
|
1928
|
+
enabled: true,
|
|
1929
|
+
threshold: 0.1,
|
|
1930
|
+
baselineDir: "tests/visual/baseline",
|
|
1931
|
+
diffDir: "tests/visual/diff"
|
|
1932
|
+
},
|
|
1933
|
+
lighthouse: {
|
|
1934
|
+
enabled: true,
|
|
1935
|
+
urls: ["http://localhost:5173"],
|
|
1936
|
+
runs: 3,
|
|
1937
|
+
thresholds: {
|
|
1938
|
+
performance: 80,
|
|
1939
|
+
accessibility: 90
|
|
1940
|
+
}
|
|
1941
|
+
},
|
|
1942
|
+
mock: {
|
|
1943
|
+
enabled: true,
|
|
1944
|
+
port: 3456,
|
|
1945
|
+
routesDir: "tests/mock/routes"
|
|
1946
|
+
}
|
|
1947
|
+
};
|
|
1948
|
+
}
|
|
1949
|
+
function displayProjectInfo(info) {
|
|
1950
|
+
console.log(chalk2.cyan("\n \u9879\u76EE\u68C0\u6D4B\u7ED3\u679C:"));
|
|
1951
|
+
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"));
|
|
1952
|
+
const items = [
|
|
1953
|
+
["\u9879\u76EE\u540D\u79F0", info.name],
|
|
1954
|
+
["\u6846\u67B6", info.frameworkConfidence > 0.5 ? `${info.frameworkDisplayName} (\u7F6E\u4FE1\u5EA6 ${Math.round(info.frameworkConfidence * 100)}%)` : info.frameworkDisplayName],
|
|
1955
|
+
["Vue \u9879\u76EE", info.isVue ? `\u662F (v${info.vueVersion})` : "\u5426"],
|
|
1956
|
+
["UI \u7EC4\u4EF6\u5E93", info.uiLibrary !== "none" ? info.uiLibrary : "\u672A\u68C0\u6D4B\u5230"],
|
|
1957
|
+
["Vite \u6784\u5EFA", info.isVite ? "\u662F" : "\u5426"],
|
|
1958
|
+
["TypeScript", info.typescript ? "\u662F" : "\u5426"],
|
|
1959
|
+
["\u5305\u7BA1\u7406\u5668", info.packageManager],
|
|
1960
|
+
["Monorepo", info.monorepo !== "none" ? info.monorepo : "\u5426"],
|
|
1961
|
+
["\u6E90\u7801\u76EE\u5F55", info.srcDir]
|
|
1962
|
+
];
|
|
1963
|
+
if (info.appDirs.length > 0) {
|
|
1964
|
+
items.push(["\u5B50\u9879\u76EE", info.appDirs.join(", ")]);
|
|
1965
|
+
}
|
|
1966
|
+
if (info.testFrameworks.length > 0) {
|
|
1967
|
+
items.push(["\u5DF2\u6709\u6D4B\u8BD5\u6846\u67B6", info.testFrameworks.join(", ")]);
|
|
1968
|
+
}
|
|
1969
|
+
if (info.componentDirs.length > 0) {
|
|
1970
|
+
items.push(["\u7EC4\u4EF6\u76EE\u5F55", info.componentDirs.join(", ")]);
|
|
1971
|
+
}
|
|
1972
|
+
for (const [label, value] of items) {
|
|
1973
|
+
const displayValue = value === true ? chalk2.green("\u2713") : value === false ? chalk2.red("\u2717") : String(value);
|
|
1974
|
+
console.log(` ${chalk2.white(label.padEnd(12))} ${displayValue}`);
|
|
1975
|
+
}
|
|
1976
|
+
console.log();
|
|
1977
|
+
}
|
|
1978
|
+
function createTestDirectories(config) {
|
|
1979
|
+
const dirs = [];
|
|
1980
|
+
const dirMap = {
|
|
1981
|
+
"tests": true,
|
|
1982
|
+
"tests/unit": config.vitest?.enabled !== false,
|
|
1983
|
+
"tests/component": config.vitest?.enabled !== false,
|
|
1984
|
+
"tests/e2e": config.playwright?.enabled !== false,
|
|
1985
|
+
"tests/api": config.mock?.enabled !== false,
|
|
1986
|
+
"tests/visual": config.visual?.enabled !== false,
|
|
1987
|
+
"tests/visual/baseline": config.visual?.enabled !== false,
|
|
1988
|
+
"tests/visual/diff": config.visual?.enabled !== false,
|
|
1989
|
+
"tests/mock": config.mock?.enabled !== false,
|
|
1990
|
+
"tests/mock/routes": config.mock?.enabled !== false
|
|
1991
|
+
};
|
|
1992
|
+
for (const [dir, shouldCreate] of Object.entries(dirMap)) {
|
|
1993
|
+
if (shouldCreate) {
|
|
1994
|
+
const fullPath = path6.join(process.cwd(), dir);
|
|
1995
|
+
if (!fs6.existsSync(fullPath)) {
|
|
1996
|
+
fs6.mkdirSync(fullPath, { recursive: true });
|
|
1997
|
+
dirs.push(dir);
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
}
|
|
2001
|
+
return dirs;
|
|
2002
|
+
}
|
|
2003
|
+
function displayResult(configPath, createdDirs, totalTestableFiles, projectInfo) {
|
|
2004
|
+
const relativeConfig = path6.relative(process.cwd(), configPath);
|
|
2005
|
+
console.log(chalk2.green("\n \u2713 \u9879\u76EE\u521D\u59CB\u5316\u5B8C\u6210!\n"));
|
|
2006
|
+
console.log(chalk2.white(" \u5DF2\u751F\u6210\u914D\u7F6E:"));
|
|
2007
|
+
console.log(chalk2.gray(` ${relativeConfig}`));
|
|
2008
|
+
console.log();
|
|
2009
|
+
if (createdDirs.length > 0) {
|
|
2010
|
+
console.log(chalk2.white(" \u5DF2\u521B\u5EFA\u76EE\u5F55:"));
|
|
2011
|
+
for (const dir of createdDirs) {
|
|
2012
|
+
console.log(chalk2.gray(` ${dir}/`));
|
|
2013
|
+
}
|
|
2014
|
+
console.log();
|
|
2015
|
+
}
|
|
2016
|
+
console.log(chalk2.cyan(" \u4E0B\u4E00\u6B65:"));
|
|
2017
|
+
if (totalTestableFiles > 0) {
|
|
2018
|
+
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)`));
|
|
2019
|
+
} else {
|
|
2020
|
+
console.log(chalk2.gray(" 1. qat create \u521B\u5EFA\u6D4B\u8BD5\u7528\u4F8B"));
|
|
2021
|
+
}
|
|
2022
|
+
console.log(chalk2.gray(" 2. qat run \u6267\u884C\u6D4B\u8BD5"));
|
|
2023
|
+
console.log(chalk2.gray(" 3. qat status \u67E5\u770B AI \u6A21\u578B\u72B6\u6001"));
|
|
2024
|
+
if (projectInfo.testFrameworks.length === 0) {
|
|
2025
|
+
console.log();
|
|
2026
|
+
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"));
|
|
2027
|
+
}
|
|
2028
|
+
console.log();
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
// src/commands/create.ts
|
|
2032
|
+
import chalk4 from "chalk";
|
|
2033
|
+
import inquirer2 from "inquirer";
|
|
2034
|
+
import ora3 from "ora";
|
|
2035
|
+
import fs8 from "fs";
|
|
2036
|
+
import path8 from "path";
|
|
2037
|
+
|
|
2038
|
+
// src/services/template.ts
|
|
2039
|
+
import fs7 from "fs";
|
|
2040
|
+
import path7 from "path";
|
|
2041
|
+
import { fileURLToPath } from "url";
|
|
2042
|
+
import Handlebars from "handlebars";
|
|
2043
|
+
var __filename = fileURLToPath(import.meta.url);
|
|
2044
|
+
var __dirname = path7.dirname(__filename);
|
|
2045
|
+
var TEMPLATE_MAP = {
|
|
2046
|
+
unit: "unit-test.hbs",
|
|
2047
|
+
component: "component-test.hbs",
|
|
2048
|
+
e2e: "e2e-test.hbs",
|
|
2049
|
+
api: "api-test.hbs",
|
|
2050
|
+
visual: "visual-test.hbs",
|
|
2051
|
+
performance: "performance-test.hbs"
|
|
2052
|
+
};
|
|
2053
|
+
var customTemplates = /* @__PURE__ */ new Map();
|
|
2054
|
+
Handlebars.registerHelper("eq", (a, b) => a === b);
|
|
2055
|
+
Handlebars.registerHelper("neq", (a, b) => a !== b);
|
|
2056
|
+
Handlebars.registerHelper("includes", (arr, val) => Array.isArray(arr) && arr.includes(val));
|
|
2057
|
+
Handlebars.registerHelper("join", (arr, sep) => Array.isArray(arr) ? arr.join(sep) : "");
|
|
2058
|
+
Handlebars.registerHelper("camelCase", (str) => toCamelCase(str));
|
|
2059
|
+
Handlebars.registerHelper("pascalCase", (str) => toPascalCase(str));
|
|
2060
|
+
Handlebars.registerHelper("length", (arr) => Array.isArray(arr) ? arr.length : 0);
|
|
2061
|
+
Handlebars.registerHelper("gt", (a, b) => Number(a) > Number(b));
|
|
2062
|
+
Handlebars.registerHelper("and", (...args) => {
|
|
2063
|
+
args.pop();
|
|
2064
|
+
return args.every(Boolean);
|
|
2065
|
+
});
|
|
2066
|
+
Handlebars.registerHelper("or", (...args) => {
|
|
2067
|
+
args.pop();
|
|
2068
|
+
return args.some(Boolean);
|
|
2069
|
+
});
|
|
2070
|
+
Handlebars.registerHelper("propDefaultValue", (prop) => {
|
|
2071
|
+
if (!prop.defaultValue) return "undefined";
|
|
2072
|
+
const map = { String: "''", Number: "0", Boolean: "false", Array: "[]", Object: "{}" };
|
|
2073
|
+
return map[prop.type] || prop.defaultValue;
|
|
2074
|
+
});
|
|
2075
|
+
Handlebars.registerHelper("propTestValue", (prop) => {
|
|
2076
|
+
const map = {
|
|
2077
|
+
String: "'test-value'",
|
|
2078
|
+
Number: "42",
|
|
2079
|
+
Boolean: "true",
|
|
2080
|
+
Array: "[1, 2, 3]",
|
|
2081
|
+
Object: '{ key: "value" }'
|
|
2082
|
+
};
|
|
2083
|
+
return map[prop.type] || "'test-value'";
|
|
2084
|
+
});
|
|
2085
|
+
function renderTemplate(type, context) {
|
|
2086
|
+
const templateContent = loadTemplate(type);
|
|
2087
|
+
const template = Handlebars.compile(templateContent);
|
|
2088
|
+
const fullContext = {
|
|
2089
|
+
vueVersion: 3,
|
|
2090
|
+
typescript: true,
|
|
2091
|
+
imports: [],
|
|
2092
|
+
extraImports: [],
|
|
2093
|
+
globalPlugins: [],
|
|
2094
|
+
globalStubs: [],
|
|
2095
|
+
mountOptions: "",
|
|
2096
|
+
hasAnalysis: false,
|
|
2097
|
+
exports: [],
|
|
2098
|
+
functionExports: [],
|
|
2099
|
+
valueExports: [],
|
|
2100
|
+
props: [],
|
|
2101
|
+
emits: [],
|
|
2102
|
+
methods: [],
|
|
2103
|
+
computed: [],
|
|
2104
|
+
isVueComponent: false,
|
|
2105
|
+
requiredProps: [],
|
|
2106
|
+
optionalProps: [],
|
|
2107
|
+
...context,
|
|
2108
|
+
framework: context.framework || "vue",
|
|
2109
|
+
camelName: context.camelName || toCamelCase(context.name),
|
|
2110
|
+
pascalName: context.pascalName || toPascalCase(context.name)
|
|
2111
|
+
};
|
|
2112
|
+
if (fullContext.exports && fullContext.exports.length > 0) {
|
|
2113
|
+
fullContext.hasAnalysis = true;
|
|
2114
|
+
fullContext.functionExports = fullContext.exports.filter(
|
|
2115
|
+
(e) => e.kind === "function" || e.kind === "default"
|
|
2116
|
+
);
|
|
2117
|
+
fullContext.valueExports = fullContext.exports.filter(
|
|
2118
|
+
(e) => e.kind !== "function" && e.kind !== "default" && e.kind !== "type"
|
|
2119
|
+
);
|
|
2120
|
+
}
|
|
2121
|
+
if (fullContext.props && fullContext.props.length > 0) {
|
|
2122
|
+
fullContext.isVueComponent = true;
|
|
2123
|
+
fullContext.requiredProps = fullContext.props.filter((p) => p.required);
|
|
2124
|
+
fullContext.optionalProps = fullContext.props.filter((p) => !p.required);
|
|
2125
|
+
}
|
|
2126
|
+
if (fullContext.emits && fullContext.emits.length > 0) {
|
|
2127
|
+
fullContext.isVueComponent = true;
|
|
2128
|
+
}
|
|
2129
|
+
return template(fullContext);
|
|
2130
|
+
}
|
|
2131
|
+
function loadTemplate(type) {
|
|
2132
|
+
const custom = customTemplates.get(type);
|
|
2133
|
+
if (custom) return custom;
|
|
2134
|
+
const templateDir = getTemplateDir();
|
|
2135
|
+
const templateFile = TEMPLATE_MAP[type];
|
|
2136
|
+
const templatePath = path7.join(templateDir, templateFile);
|
|
2137
|
+
if (fs7.existsSync(templatePath)) {
|
|
2138
|
+
return fs7.readFileSync(templatePath, "utf-8");
|
|
2139
|
+
}
|
|
2140
|
+
return getBuiltinTemplate(type);
|
|
2141
|
+
}
|
|
2142
|
+
function getTemplateDir() {
|
|
2143
|
+
const projectTemplates = path7.join(process.cwd(), "templates");
|
|
2144
|
+
if (fs7.existsSync(projectTemplates)) {
|
|
2145
|
+
return projectTemplates;
|
|
2146
|
+
}
|
|
2147
|
+
return path7.join(__dirname, "..", "templates");
|
|
2148
|
+
}
|
|
2149
|
+
function getBuiltinTemplate(type) {
|
|
2150
|
+
const templates = {
|
|
2151
|
+
unit: `import { describe, it, expect } from 'vitest';
|
|
2152
|
+
{{#if hasAnalysis}}
|
|
2153
|
+
{{#each valueExports}}
|
|
2154
|
+
import { {{name}} } from '{{../target}}';
|
|
2155
|
+
{{/each}}
|
|
2156
|
+
{{#each functionExports}}
|
|
2157
|
+
import { {{name}} } from '{{../target}}';
|
|
2158
|
+
{{/each}}
|
|
2159
|
+
{{else}}
|
|
2160
|
+
import { {{camelName}} } from '{{target}}';
|
|
2161
|
+
{{/if}}
|
|
2162
|
+
|
|
2163
|
+
describe('{{name}}', () => {
|
|
2164
|
+
{{#if hasAnalysis}}
|
|
2165
|
+
{{#each functionExports}}
|
|
2166
|
+
describe('{{name}}()', () => {
|
|
2167
|
+
{{#if isAsync}}
|
|
2168
|
+
it('should resolve successfully', async () => {
|
|
2169
|
+
const result = await {{name}}({{#each params}}{{#unless @first}}, {{/unless}}{{this}}: undefined{{/each}});
|
|
2170
|
+
expect(result).toBeDefined();
|
|
2171
|
+
});
|
|
2172
|
+
{{else}}
|
|
2173
|
+
it('should return a value', () => {
|
|
2174
|
+
const result = {{name}}({{#each params}}{{#unless @first}}, {{/unless}}undefined as any{{/each}});
|
|
2175
|
+
expect(result).toBeDefined();
|
|
2176
|
+
});
|
|
2177
|
+
{{/if}}
|
|
2178
|
+
{{#if returnType}}
|
|
2179
|
+
it('should return correct type', () => {
|
|
2180
|
+
{{#if isAsync}}
|
|
2181
|
+
const result = await {{name}}({{#each params}}{{#unless @first}}, {{/unless}}undefined as any{{/each}});
|
|
2182
|
+
{{else}}
|
|
2183
|
+
const result = {{name}}({{#each params}}{{#unless @first}}, {{/unless}}undefined as any{{/each}});
|
|
2184
|
+
{{/if}}
|
|
2185
|
+
expect(result).toBeDefined();
|
|
1941
2186
|
});
|
|
1942
2187
|
{{/if}}
|
|
1943
2188
|
{{#if params.length}}
|
|
@@ -2191,647 +2436,127 @@ test.describe('{{name}} performance', () => {
|
|
|
2191
2436
|
return {
|
|
2192
2437
|
domContentLoaded: entry?.domContentLoadedEventEnd - entry?.domContentLoadedEventStart ?? 0,
|
|
2193
2438
|
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
|
-
}
|
|
2439
|
+
domInteractive: entry?.domInteractive ?? 0,
|
|
2440
|
+
};
|
|
2441
|
+
});
|
|
2264
2442
|
|
|
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
|
-
}
|
|
2443
|
+
// DOM \u5185\u5BB9\u52A0\u8F7D\u5E94\u5728 2s \u5185
|
|
2444
|
+
expect(performanceMetrics.domInteractive).toBeLessThan(2000);
|
|
2300
2445
|
});
|
|
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
|
-
}
|
|
2446
|
+
|
|
2447
|
+
test('should not have memory leaks', async ({ page }) => {
|
|
2448
|
+
await page.goto('/');
|
|
2449
|
+
|
|
2450
|
+
const metrics = await page.metrics();
|
|
2451
|
+
expect(metrics.JSHeapUsedSize).toBeLessThan(50 * 1024 * 1024); // 50MB
|
|
2452
|
+
});
|
|
2453
|
+
});
|
|
2454
|
+
`
|
|
2486
2455
|
};
|
|
2456
|
+
return templates[type];
|
|
2487
2457
|
}
|
|
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;
|
|
2458
|
+
function toCamelCase(str) {
|
|
2459
|
+
return str.replace(/[-_\s]+(.)?/g, (_, c) => c ? c.toUpperCase() : "").replace(/^[A-Z]/, (c) => c.toLowerCase());
|
|
2617
2460
|
}
|
|
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;
|
|
2461
|
+
function toPascalCase(str) {
|
|
2462
|
+
const camel = toCamelCase(str);
|
|
2463
|
+
return camel.charAt(0).toUpperCase() + camel.slice(1);
|
|
2634
2464
|
}
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2465
|
+
|
|
2466
|
+
// src/services/test-reviewer.ts
|
|
2467
|
+
import chalk3 from "chalk";
|
|
2468
|
+
import ora2 from "ora";
|
|
2469
|
+
var MAX_RETRIES = 3;
|
|
2470
|
+
var REVIEW_THRESHOLD = 0.6;
|
|
2471
|
+
async function generateWithReview(params) {
|
|
2472
|
+
const { testType, targetPath, sourceCode, analysis, aiConfig, framework, onAttempt } = params;
|
|
2473
|
+
const generatorProvider = createAIProvider(aiConfig);
|
|
2474
|
+
const reviewerProvider = createAIProvider(aiConfig);
|
|
2475
|
+
if (!generatorProvider.capabilities.generateTest) {
|
|
2476
|
+
throw new Error("\u5F53\u524D AI Provider \u4E0D\u652F\u6301\u6D4B\u8BD5\u751F\u6210");
|
|
2477
|
+
}
|
|
2478
|
+
let currentCode = "";
|
|
2479
|
+
let currentDescription = "";
|
|
2480
|
+
let currentConfidence = 0;
|
|
2481
|
+
let lastReview = null;
|
|
2482
|
+
let approved = false;
|
|
2483
|
+
let attempts = 0;
|
|
2484
|
+
for (let i = 0; i < MAX_RETRIES; i++) {
|
|
2485
|
+
attempts = i + 1;
|
|
2486
|
+
onAttempt?.(attempts, MAX_RETRIES);
|
|
2487
|
+
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
|
|
2488
|
+
\u5BA1\u8BA1\u8BC4\u5206: ${(lastReview.score * 100).toFixed(0)}%
|
|
2489
|
+
\u5BA1\u8BA1\u610F\u89C1: ${lastReview.feedback}
|
|
2490
|
+
\u5177\u4F53\u95EE\u9898:
|
|
2491
|
+
${lastReview.issues.map((issue) => `- ${issue}`).join("\n")}
|
|
2492
|
+
\u6539\u8FDB\u5EFA\u8BAE:
|
|
2493
|
+
${lastReview.suggestions.map((s) => `- ${s}`).join("\n")}
|
|
2494
|
+
|
|
2495
|
+
\u8BF7\u9488\u5BF9\u4EE5\u4E0A\u95EE\u9898\u91CD\u65B0\u751F\u6210\u66F4\u8D34\u5207\u3001\u66F4\u51C6\u786E\u7684\u6D4B\u8BD5\u7528\u4F8B\u3002`;
|
|
2496
|
+
const generateResponse = await generatorProvider.generateTest({
|
|
2497
|
+
type: testType,
|
|
2652
2498
|
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
|
|
2499
|
+
context: i === 0 ? sourceCode : `${sourceCode}
|
|
2500
|
+
|
|
2501
|
+
--- \u5BA1\u8BA1\u53CD\u9988 ---
|
|
2502
|
+
${generationPrompt}`,
|
|
2503
|
+
framework: framework || void 0,
|
|
2504
|
+
analysis
|
|
2680
2505
|
});
|
|
2506
|
+
currentCode = generateResponse.code;
|
|
2507
|
+
currentDescription = generateResponse.description;
|
|
2508
|
+
currentConfidence = generateResponse.confidence;
|
|
2509
|
+
const reviewRequest = {
|
|
2510
|
+
target: targetPath,
|
|
2511
|
+
sourceCode,
|
|
2512
|
+
analysis,
|
|
2513
|
+
testCode: currentCode,
|
|
2514
|
+
testType,
|
|
2515
|
+
generationDescription: currentDescription
|
|
2516
|
+
};
|
|
2517
|
+
lastReview = await reviewerProvider.reviewTest(reviewRequest);
|
|
2518
|
+
approved = lastReview.approved && lastReview.score >= REVIEW_THRESHOLD;
|
|
2519
|
+
if (approved) {
|
|
2520
|
+
break;
|
|
2521
|
+
}
|
|
2681
2522
|
}
|
|
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
2523
|
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
|
|
2524
|
+
code: currentCode,
|
|
2525
|
+
description: currentDescription,
|
|
2526
|
+
confidence: currentConfidence,
|
|
2527
|
+
approved,
|
|
2528
|
+
attempts,
|
|
2529
|
+
reviewScore: lastReview?.score ?? 0,
|
|
2530
|
+
reviewFeedback: lastReview?.feedback ?? "",
|
|
2531
|
+
reviewIssues: lastReview?.issues ?? [],
|
|
2532
|
+
reviewSuggestions: lastReview?.suggestions ?? []
|
|
2741
2533
|
};
|
|
2742
|
-
return { code, reviewEntry };
|
|
2743
2534
|
}
|
|
2744
|
-
function
|
|
2745
|
-
|
|
2535
|
+
function printReviewReport(report) {
|
|
2536
|
+
if (report.length === 0) return;
|
|
2537
|
+
const approved = report.filter((r) => r.approved);
|
|
2538
|
+
const failed = report.filter((r) => !r.approved);
|
|
2539
|
+
console.log();
|
|
2540
|
+
console.log(chalk3.cyan(" \u5BA1\u8BA1\u62A5\u544A:"));
|
|
2746
2541
|
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}`);
|
|
2542
|
+
for (const entry of approved) {
|
|
2543
|
+
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
2544
|
}
|
|
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);
|
|
2545
|
+
for (const entry of failed) {
|
|
2546
|
+
console.log(` ${chalk3.red("\u2717")} ${chalk3.gray(entry.target)} ${chalk3.red(`(${(entry.score * 100).toFixed(0)}%)`)}`);
|
|
2547
|
+
if (entry.issues.length > 0) {
|
|
2548
|
+
for (const issue of entry.issues.slice(0, 3)) {
|
|
2549
|
+
console.log(chalk3.gray(` - ${issue}`));
|
|
2793
2550
|
}
|
|
2794
2551
|
}
|
|
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)}`);
|
|
2552
|
+
if (entry.feedback) {
|
|
2553
|
+
console.log(chalk3.gray(` \u53CD\u9988: ${entry.feedback}`));
|
|
2809
2554
|
}
|
|
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
2555
|
}
|
|
2826
2556
|
console.log();
|
|
2827
2557
|
}
|
|
2828
2558
|
|
|
2829
2559
|
// 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
2560
|
var TEST_TYPE_LABELS = {
|
|
2836
2561
|
unit: "\u5355\u5143\u6D4B\u8BD5",
|
|
2837
2562
|
component: "\u7EC4\u4EF6\u6D4B\u8BD5",
|
|
@@ -3169,18 +2894,29 @@ function displayCreateResult(createdFiles, skippedCount, usedAI) {
|
|
|
3169
2894
|
import chalk5 from "chalk";
|
|
3170
2895
|
import inquirer3 from "inquirer";
|
|
3171
2896
|
import ora4 from "ora";
|
|
3172
|
-
import
|
|
3173
|
-
import
|
|
2897
|
+
import fs11 from "fs";
|
|
2898
|
+
import path12 from "path";
|
|
3174
2899
|
|
|
3175
2900
|
// src/runners/vitest-runner.ts
|
|
3176
2901
|
import { execFile } from "child_process";
|
|
3177
2902
|
import path9 from "path";
|
|
2903
|
+
import fs9 from "fs";
|
|
2904
|
+
import os2 from "os";
|
|
2905
|
+
var isVerbose = () => process.env.QAT_VERBOSE === "true";
|
|
2906
|
+
function debug(label, ...args) {
|
|
2907
|
+
if (isVerbose()) {
|
|
2908
|
+
console.log(`\x1B[90m [debug:${label}]\x1B[0m`, ...args);
|
|
2909
|
+
}
|
|
2910
|
+
}
|
|
3178
2911
|
async function runVitest(options) {
|
|
3179
2912
|
const startTime = Date.now();
|
|
3180
2913
|
const args = buildVitestArgs(options);
|
|
2914
|
+
debug("vitest", "\u547D\u4EE4\u53C2\u6570:", args.join(" "));
|
|
3181
2915
|
try {
|
|
3182
2916
|
const result = await execVitest(args);
|
|
3183
2917
|
const endTime = Date.now();
|
|
2918
|
+
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`);
|
|
2919
|
+
debug("vitest", "\u89E3\u6790\u65B9\u5F0F:", result.parseMethod);
|
|
3184
2920
|
return {
|
|
3185
2921
|
type: options.type,
|
|
3186
2922
|
status: result.success ? "passed" : "failed",
|
|
@@ -3245,12 +2981,11 @@ function buildVitestArgs(options) {
|
|
|
3245
2981
|
return args;
|
|
3246
2982
|
}
|
|
3247
2983
|
async function execVitest(args) {
|
|
3248
|
-
const os2 = await import("os");
|
|
3249
|
-
const fs15 = await import("fs");
|
|
3250
2984
|
const tmpFile = path9.join(os2.tmpdir(), `qat-vitest-result-${Date.now()}.json`);
|
|
3251
2985
|
const argsWithOutput = [...args, "--outputFile", tmpFile];
|
|
3252
2986
|
return new Promise((resolve, reject) => {
|
|
3253
2987
|
const npx = process.platform === "win32" ? "npx.cmd" : "npx";
|
|
2988
|
+
debug("vitest", "\u6267\u884C\u547D\u4EE4:", npx, argsWithOutput.join(" "));
|
|
3254
2989
|
const child = execFile(npx, argsWithOutput, {
|
|
3255
2990
|
cwd: process.cwd(),
|
|
3256
2991
|
env: { ...process.env, FORCE_COLOR: "0", NO_COLOR: "1" },
|
|
@@ -3258,85 +2993,113 @@ async function execVitest(args) {
|
|
|
3258
2993
|
shell: true
|
|
3259
2994
|
}, (error, stdout, stderr) => {
|
|
3260
2995
|
const rawOutput = stdout || stderr || "";
|
|
2996
|
+
const exitCode = error && "code" in error ? error.code : 0;
|
|
2997
|
+
debug("vitest", `\u9000\u51FA\u7801: ${exitCode}`);
|
|
2998
|
+
debug("vitest", `stdout \u957F\u5EA6: ${stdout?.length || 0}, stderr \u957F\u5EA6: ${stderr?.length || 0}`);
|
|
3261
2999
|
let jsonResult = null;
|
|
3262
3000
|
try {
|
|
3263
|
-
if (
|
|
3264
|
-
jsonResult =
|
|
3265
|
-
|
|
3001
|
+
if (fs9.existsSync(tmpFile)) {
|
|
3002
|
+
jsonResult = fs9.readFileSync(tmpFile, "utf-8");
|
|
3003
|
+
debug("vitest", `\u4ECE\u4E34\u65F6\u6587\u4EF6\u8BFB\u53D6\u5230 JSON (${jsonResult.length} \u5B57\u7B26)`);
|
|
3004
|
+
debug("vitest", "JSON \u524D 500 \u5B57\u7B26:", jsonResult.substring(0, 500));
|
|
3005
|
+
fs9.unlinkSync(tmpFile);
|
|
3006
|
+
} else {
|
|
3007
|
+
debug("vitest", "\u4E34\u65F6\u6587\u4EF6\u4E0D\u5B58\u5728:", tmpFile);
|
|
3266
3008
|
}
|
|
3267
|
-
} catch {
|
|
3009
|
+
} catch (e) {
|
|
3010
|
+
debug("vitest", "\u4E34\u65F6\u6587\u4EF6\u8BFB\u53D6\u5931\u8D25:", e instanceof Error ? e.message : String(e));
|
|
3268
3011
|
}
|
|
3269
3012
|
if (jsonResult) {
|
|
3270
3013
|
try {
|
|
3271
|
-
const parsed =
|
|
3272
|
-
|
|
3014
|
+
const parsed = parseVitestJSON(jsonResult);
|
|
3015
|
+
debug("vitest", "\u4ECE\u4E34\u65F6\u6587\u4EF6\u89E3\u6790\u6210\u529F:", parsed.suites.length, "\u4E2A\u5957\u4EF6");
|
|
3016
|
+
resolve({ ...parsed, rawOutput, parseMethod: "outputFile-JSON" });
|
|
3273
3017
|
return;
|
|
3274
|
-
} catch {
|
|
3018
|
+
} catch (e) {
|
|
3019
|
+
debug("vitest", "\u4E34\u65F6\u6587\u4EF6 JSON \u89E3\u6790\u5931\u8D25:", e instanceof Error ? e.message : String(e));
|
|
3275
3020
|
}
|
|
3276
3021
|
}
|
|
3022
|
+
debug("vitest", "\u5C1D\u8BD5\u4ECE stdout \u63D0\u53D6 JSON...");
|
|
3277
3023
|
try {
|
|
3278
|
-
const parsed =
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3024
|
+
const parsed = parseFromStdout(rawOutput);
|
|
3025
|
+
debug("vitest", "\u4ECE stdout \u89E3\u6790\u6210\u529F:", parsed.suites.length, "\u4E2A\u5957\u4EF6");
|
|
3026
|
+
resolve({ ...parsed, rawOutput, parseMethod: "stdout-JSON" });
|
|
3027
|
+
return;
|
|
3028
|
+
} catch (e) {
|
|
3029
|
+
debug("vitest", "stdout JSON \u89E3\u6790\u5931\u8D25:", e instanceof Error ? e.message : String(e));
|
|
3030
|
+
}
|
|
3031
|
+
debug("vitest", "\u5C1D\u8BD5\u4ECE\u6587\u672C\u8F93\u51FA\u89E3\u6790...");
|
|
3032
|
+
if (rawOutput) {
|
|
3033
|
+
const parsed = parseVitestTextOutput(rawOutput, !!error);
|
|
3034
|
+
debug("vitest", "\u6587\u672C\u89E3\u6790\u7ED3\u679C:", parsed.suites.length, "\u4E2A\u5957\u4EF6");
|
|
3035
|
+
resolve({ ...parsed, rawOutput, parseMethod: "text-fallback" });
|
|
3036
|
+
return;
|
|
3037
|
+
}
|
|
3038
|
+
if (error && error.message.includes("ENOENT")) {
|
|
3039
|
+
reject(new Error("\u672A\u627E\u5230 vitest\uFF0C\u8BF7\u786E\u4FDD\u5DF2\u5B89\u88C5: npm install -D vitest"));
|
|
3040
|
+
return;
|
|
3288
3041
|
}
|
|
3042
|
+
debug("vitest", "\u6240\u6709\u89E3\u6790\u65B9\u5F0F\u5747\u5931\u8D25\uFF0C\u8FD4\u56DE\u7A7A\u7ED3\u679C");
|
|
3043
|
+
resolve({ success: !error, suites: [], rawOutput, parseMethod: "none" });
|
|
3289
3044
|
});
|
|
3290
3045
|
child.on("error", (err) => {
|
|
3291
3046
|
try {
|
|
3292
|
-
|
|
3047
|
+
fs9.unlinkSync(tmpFile);
|
|
3293
3048
|
} catch {
|
|
3294
3049
|
}
|
|
3295
3050
|
reject(new Error(`Vitest \u6267\u884C\u5931\u8D25: ${err.message}`));
|
|
3296
3051
|
});
|
|
3297
3052
|
});
|
|
3298
3053
|
}
|
|
3299
|
-
function
|
|
3054
|
+
function parseVitestJSON(jsonStr) {
|
|
3300
3055
|
const data = JSON.parse(jsonStr);
|
|
3301
3056
|
const suites = [];
|
|
3057
|
+
debug("vitest-json", "JSON \u9876\u5C42\u5B57\u6BB5:", Object.keys(data).join(", "));
|
|
3302
3058
|
if (data.testResults && Array.isArray(data.testResults)) {
|
|
3059
|
+
debug("vitest-json", `testResults \u6570\u91CF: ${data.testResults.length}`);
|
|
3303
3060
|
for (const fileResult of data.testResults) {
|
|
3061
|
+
const suiteTests = parseTestResults(fileResult);
|
|
3062
|
+
suites.push({
|
|
3063
|
+
name: path9.basename(fileResult.name || "unknown"),
|
|
3064
|
+
file: fileResult.name || "unknown",
|
|
3065
|
+
type: "unit",
|
|
3066
|
+
status: mapVitestStatus(fileResult.status),
|
|
3067
|
+
duration: fileResult.duration || 0,
|
|
3068
|
+
tests: suiteTests
|
|
3069
|
+
});
|
|
3070
|
+
}
|
|
3071
|
+
}
|
|
3072
|
+
if (suites.length === 0 && data.numTotalTests !== void 0) {
|
|
3073
|
+
debug("vitest-json", `\u4F7F\u7528\u6C47\u603B\u683C\u5F0F: total=${data.numTotalTests}, passed=${data.numPassedTests}`);
|
|
3074
|
+
suites.push({
|
|
3075
|
+
name: "Vitest Results",
|
|
3076
|
+
file: "unknown",
|
|
3077
|
+
type: "unit",
|
|
3078
|
+
status: data.numFailedTests > 0 ? "failed" : "passed",
|
|
3079
|
+
duration: 0,
|
|
3080
|
+
tests: buildTestsFromSummary(data)
|
|
3081
|
+
});
|
|
3082
|
+
}
|
|
3083
|
+
if (suites.length === 0 && data.suites && Array.isArray(data.suites)) {
|
|
3084
|
+
debug("vitest-json", `suites \u6570\u91CF: ${data.suites.length}`);
|
|
3085
|
+
for (const suiteData of data.suites) {
|
|
3304
3086
|
const suiteTests = [];
|
|
3305
|
-
const
|
|
3306
|
-
for (const assertion of assertions) {
|
|
3087
|
+
for (const test of suiteData.tests || []) {
|
|
3307
3088
|
suiteTests.push({
|
|
3308
|
-
name:
|
|
3309
|
-
file:
|
|
3310
|
-
status: mapVitestStatus(
|
|
3311
|
-
duration:
|
|
3312
|
-
error:
|
|
3089
|
+
name: test.name || test.title || "unknown",
|
|
3090
|
+
file: test.file || suiteData.file || "unknown",
|
|
3091
|
+
status: mapVitestStatus(test.status || test.result?.status),
|
|
3092
|
+
duration: test.duration || test.result?.duration || 0,
|
|
3093
|
+
error: test.result?.errors?.[0] ? { message: test.result.errors[0].message || String(test.result.errors[0]) } : void 0,
|
|
3313
3094
|
retries: 0
|
|
3314
3095
|
});
|
|
3315
3096
|
}
|
|
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
3097
|
suites.push({
|
|
3335
|
-
name:
|
|
3336
|
-
file:
|
|
3098
|
+
name: suiteData.name || "unknown",
|
|
3099
|
+
file: suiteData.file || "unknown",
|
|
3337
3100
|
type: "unit",
|
|
3338
|
-
status:
|
|
3339
|
-
duration:
|
|
3101
|
+
status: suiteTests.some((t) => t.status === "failed") ? "failed" : "passed",
|
|
3102
|
+
duration: 0,
|
|
3340
3103
|
tests: suiteTests
|
|
3341
3104
|
});
|
|
3342
3105
|
}
|
|
@@ -3345,58 +3108,101 @@ function parseVitestJSONResult(jsonStr) {
|
|
|
3345
3108
|
if (data.coverageMap) {
|
|
3346
3109
|
coverage = extractCoverage(data.coverageMap);
|
|
3347
3110
|
}
|
|
3348
|
-
|
|
3111
|
+
if (!coverage && data.coverage && typeof data.coverage === "object") {
|
|
3112
|
+
const cov = data.coverage;
|
|
3113
|
+
const totals = cov.totals || cov;
|
|
3114
|
+
const getVal = (key) => {
|
|
3115
|
+
const v = totals[key];
|
|
3116
|
+
return typeof v === "number" ? v : typeof v === "object" && v !== null && "pct" in v ? v.pct / 100 : 0;
|
|
3117
|
+
};
|
|
3118
|
+
coverage = {
|
|
3119
|
+
lines: getVal("lines"),
|
|
3120
|
+
statements: getVal("statements"),
|
|
3121
|
+
functions: getVal("functions"),
|
|
3122
|
+
branches: getVal("branches")
|
|
3123
|
+
};
|
|
3124
|
+
}
|
|
3125
|
+
const success = data.success !== false ? data.numFailedTests !== void 0 ? data.numFailedTests === 0 : suites.every((s) => s.status !== "failed") : false;
|
|
3349
3126
|
return { success, suites, coverage };
|
|
3350
3127
|
}
|
|
3351
|
-
function
|
|
3352
|
-
const
|
|
3353
|
-
|
|
3354
|
-
|
|
3128
|
+
function parseTestResults(fileResult) {
|
|
3129
|
+
const tests = [];
|
|
3130
|
+
const assertions = fileResult.assertionResults || fileResult.tests || [];
|
|
3131
|
+
for (const assertion of assertions) {
|
|
3132
|
+
tests.push({
|
|
3133
|
+
name: assertion.title || assertion.fullName || assertion.name || "unknown",
|
|
3134
|
+
file: fileResult.name || "unknown",
|
|
3135
|
+
status: mapVitestStatus(assertion.status),
|
|
3136
|
+
duration: assertion.duration || 0,
|
|
3137
|
+
error: assertion.failureMessages?.length ? { message: assertion.failureMessages[0] } : assertion.failureMessage ? { message: assertion.failureMessage } : void 0,
|
|
3138
|
+
retries: 0
|
|
3139
|
+
});
|
|
3355
3140
|
}
|
|
3356
|
-
|
|
3141
|
+
if (tests.length === 0 && fileResult.numPassingTests !== void 0) {
|
|
3142
|
+
tests.push(...buildTestsFromSummary(fileResult));
|
|
3143
|
+
}
|
|
3144
|
+
return tests;
|
|
3145
|
+
}
|
|
3146
|
+
function buildTestsFromSummary(data) {
|
|
3147
|
+
const tests = [];
|
|
3148
|
+
const counts = [
|
|
3149
|
+
[data.numPassedTests || 0, "passed"],
|
|
3150
|
+
[data.numFailedTests || 0, "failed"],
|
|
3151
|
+
[data.numPendingTests || 0, "skipped"]
|
|
3152
|
+
];
|
|
3153
|
+
for (const [n, s] of counts) {
|
|
3154
|
+
for (let i = 0; i < n; i++) {
|
|
3155
|
+
tests.push({
|
|
3156
|
+
name: `${s} test ${i + 1}`,
|
|
3157
|
+
file: data.name || "unknown",
|
|
3158
|
+
status: s,
|
|
3159
|
+
duration: 0,
|
|
3160
|
+
retries: 0
|
|
3161
|
+
});
|
|
3162
|
+
}
|
|
3163
|
+
}
|
|
3164
|
+
return tests;
|
|
3165
|
+
}
|
|
3166
|
+
function parseFromStdout(output) {
|
|
3167
|
+
const jsonMatch = output.match(/\{[\s\S]*"testResults"[\s\S]*\}/);
|
|
3168
|
+
if (jsonMatch) {
|
|
3357
3169
|
const data = JSON.parse(jsonMatch[0]);
|
|
3358
3170
|
const suites = [];
|
|
3359
3171
|
if (data.testResults && Array.isArray(data.testResults)) {
|
|
3360
3172
|
for (const fileResult of data.testResults) {
|
|
3361
|
-
|
|
3362
|
-
name: path9.basename(fileResult.name ||
|
|
3173
|
+
suites.push({
|
|
3174
|
+
name: path9.basename(fileResult.name || "unknown"),
|
|
3363
3175
|
file: fileResult.name || "unknown",
|
|
3364
3176
|
type: "unit",
|
|
3365
3177
|
status: mapVitestStatus(fileResult.status),
|
|
3366
3178
|
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);
|
|
3179
|
+
tests: parseTestResults(fileResult)
|
|
3180
|
+
});
|
|
3377
3181
|
}
|
|
3378
3182
|
}
|
|
3379
|
-
let coverage;
|
|
3380
|
-
if (data.coverageMap) {
|
|
3381
|
-
coverage = extractCoverage(data.coverageMap);
|
|
3382
|
-
}
|
|
3383
3183
|
const success = data.success !== false && suites.every((s) => s.status !== "failed");
|
|
3184
|
+
let coverage;
|
|
3185
|
+
if (data.coverageMap) coverage = extractCoverage(data.coverageMap);
|
|
3384
3186
|
return { success, suites, coverage };
|
|
3385
|
-
} catch {
|
|
3386
|
-
return parseVitestTextOutput(output, false);
|
|
3387
3187
|
}
|
|
3188
|
+
const anyJsonMatch = output.match(/\{[\s\S]*"numTotalTests"[\s\S]*\}/);
|
|
3189
|
+
if (anyJsonMatch) {
|
|
3190
|
+
return parseVitestJSON(anyJsonMatch[0]);
|
|
3191
|
+
}
|
|
3192
|
+
throw new Error("stdout \u4E2D\u672A\u627E\u5230\u6709\u6548 JSON");
|
|
3388
3193
|
}
|
|
3389
3194
|
function parseVitestTextOutput(output, hasError) {
|
|
3390
3195
|
const suites = [];
|
|
3196
|
+
debug("vitest-text", "\u6587\u672C\u8F93\u51FA\u524D 1000 \u5B57\u7B26:", output.substring(0, 1e3));
|
|
3197
|
+
const suiteRegex = /[✓✗×✕]\s+(.+\.test\.(ts|js)|.+\.spec\.(ts|js))\s*\((\d+)[^)]*\)/i;
|
|
3198
|
+
const lines = output.split("\n");
|
|
3391
3199
|
let totalPassed = 0;
|
|
3392
3200
|
let totalFailed = 0;
|
|
3393
|
-
const suiteRegex = /[✓✗×]\s+(.+\.test\.ts|.+\.spec\.ts)\s*\((\d+)\s+test/i;
|
|
3394
|
-
const lines = output.split("\n");
|
|
3395
3201
|
for (const line of lines) {
|
|
3396
3202
|
const match = line.match(suiteRegex);
|
|
3397
3203
|
if (match) {
|
|
3398
3204
|
const file = match[1];
|
|
3399
|
-
const testCount = parseInt(match[
|
|
3205
|
+
const testCount = parseInt(match[4], 10);
|
|
3400
3206
|
const isPassed = line.includes("\u2713");
|
|
3401
3207
|
if (isPassed) totalPassed += testCount;
|
|
3402
3208
|
else totalFailed += testCount;
|
|
@@ -3408,23 +3214,47 @@ function parseVitestTextOutput(output, hasError) {
|
|
|
3408
3214
|
duration: 0,
|
|
3409
3215
|
tests: Array.from({ length: testCount }, (_, i) => ({
|
|
3410
3216
|
name: `test ${i + 1}`,
|
|
3411
|
-
file,
|
|
3412
|
-
status: isPassed ? "passed" : "failed",
|
|
3217
|
+
file,
|
|
3218
|
+
status: isPassed ? "passed" : "failed",
|
|
3219
|
+
duration: 0,
|
|
3220
|
+
retries: 0
|
|
3221
|
+
}))
|
|
3222
|
+
});
|
|
3223
|
+
}
|
|
3224
|
+
}
|
|
3225
|
+
if (suites.length === 0) {
|
|
3226
|
+
const summaryMatch = output.match(/Tests\s+(\d+)\s+(passed|failed)/i);
|
|
3227
|
+
if (summaryMatch) {
|
|
3228
|
+
const count = parseInt(summaryMatch[1], 10);
|
|
3229
|
+
const status = summaryMatch[2].toLowerCase() === "passed" ? "passed" : "failed";
|
|
3230
|
+
suites.push({
|
|
3231
|
+
name: "Vitest Summary",
|
|
3232
|
+
file: "unknown",
|
|
3233
|
+
type: "unit",
|
|
3234
|
+
status,
|
|
3235
|
+
duration: 0,
|
|
3236
|
+
tests: Array.from({ length: count }, (_, i) => ({
|
|
3237
|
+
name: `test ${i + 1}`,
|
|
3238
|
+
file: "unknown",
|
|
3239
|
+
status,
|
|
3413
3240
|
duration: 0,
|
|
3414
3241
|
retries: 0
|
|
3415
3242
|
}))
|
|
3416
3243
|
});
|
|
3417
3244
|
}
|
|
3418
3245
|
}
|
|
3246
|
+
debug("vitest-text", `\u89E3\u6790\u5230 ${suites.length} \u4E2A\u5957\u4EF6, ${totalPassed} \u901A\u8FC7, ${totalFailed} \u5931\u8D25`);
|
|
3419
3247
|
return {
|
|
3420
3248
|
success: !hasError || totalFailed === 0,
|
|
3421
3249
|
suites
|
|
3422
3250
|
};
|
|
3423
3251
|
}
|
|
3424
3252
|
function mapVitestStatus(status) {
|
|
3253
|
+
if (!status) return "pending";
|
|
3425
3254
|
switch (status) {
|
|
3426
3255
|
case "passed":
|
|
3427
3256
|
case "pass":
|
|
3257
|
+
case "done":
|
|
3428
3258
|
return "passed";
|
|
3429
3259
|
case "failed":
|
|
3430
3260
|
case "fail":
|
|
@@ -3432,6 +3262,7 @@ function mapVitestStatus(status) {
|
|
|
3432
3262
|
case "skipped":
|
|
3433
3263
|
case "skip":
|
|
3434
3264
|
case "pending":
|
|
3265
|
+
case "todo":
|
|
3435
3266
|
return "skipped";
|
|
3436
3267
|
default:
|
|
3437
3268
|
return "pending";
|
|
@@ -3910,9 +3741,226 @@ function calculateAverageMetrics(results) {
|
|
|
3910
3741
|
};
|
|
3911
3742
|
}
|
|
3912
3743
|
|
|
3744
|
+
// src/services/reporter.ts
|
|
3745
|
+
import fs10 from "fs";
|
|
3746
|
+
import path11 from "path";
|
|
3747
|
+
function aggregateResults(results) {
|
|
3748
|
+
const summary = {
|
|
3749
|
+
total: 0,
|
|
3750
|
+
passed: 0,
|
|
3751
|
+
failed: 0,
|
|
3752
|
+
skipped: 0,
|
|
3753
|
+
pending: 0
|
|
3754
|
+
};
|
|
3755
|
+
const byType = {};
|
|
3756
|
+
let coverage;
|
|
3757
|
+
for (const result of results) {
|
|
3758
|
+
const typeKey = result.type;
|
|
3759
|
+
if (!byType[typeKey]) {
|
|
3760
|
+
byType[typeKey] = { total: 0, passed: 0, failed: 0, skipped: 0 };
|
|
3761
|
+
}
|
|
3762
|
+
for (const suite of result.suites) {
|
|
3763
|
+
for (const test of suite.tests) {
|
|
3764
|
+
summary.total++;
|
|
3765
|
+
byType[typeKey].total++;
|
|
3766
|
+
if (test.status === "passed") {
|
|
3767
|
+
summary.passed++;
|
|
3768
|
+
byType[typeKey].passed++;
|
|
3769
|
+
} else if (test.status === "failed") {
|
|
3770
|
+
summary.failed++;
|
|
3771
|
+
byType[typeKey].failed++;
|
|
3772
|
+
} else if (test.status === "skipped") {
|
|
3773
|
+
summary.skipped++;
|
|
3774
|
+
byType[typeKey].skipped++;
|
|
3775
|
+
} else {
|
|
3776
|
+
summary.pending++;
|
|
3777
|
+
}
|
|
3778
|
+
}
|
|
3779
|
+
}
|
|
3780
|
+
if (result.coverage) {
|
|
3781
|
+
if (!coverage) {
|
|
3782
|
+
coverage = { ...result.coverage };
|
|
3783
|
+
} else {
|
|
3784
|
+
coverage.lines = Math.max(coverage.lines, result.coverage.lines);
|
|
3785
|
+
coverage.statements = Math.max(coverage.statements, result.coverage.statements);
|
|
3786
|
+
coverage.functions = Math.max(coverage.functions, result.coverage.functions);
|
|
3787
|
+
coverage.branches = Math.max(coverage.branches, result.coverage.branches);
|
|
3788
|
+
}
|
|
3789
|
+
}
|
|
3790
|
+
}
|
|
3791
|
+
const totalDuration = results.reduce((sum, r) => sum + r.duration, 0);
|
|
3792
|
+
return {
|
|
3793
|
+
timestamp: Date.now(),
|
|
3794
|
+
duration: totalDuration,
|
|
3795
|
+
results,
|
|
3796
|
+
summary,
|
|
3797
|
+
byType,
|
|
3798
|
+
coverage: coverage?.lines ? coverage : void 0
|
|
3799
|
+
};
|
|
3800
|
+
}
|
|
3801
|
+
function formatDuration(ms) {
|
|
3802
|
+
if (ms < 1e3) return `${ms}ms`;
|
|
3803
|
+
if (ms < 6e4) return `${(ms / 1e3).toFixed(2)}s`;
|
|
3804
|
+
const minutes = Math.floor(ms / 6e4);
|
|
3805
|
+
const seconds = (ms % 6e4 / 1e3).toFixed(0);
|
|
3806
|
+
return `${minutes}m ${seconds}s`;
|
|
3807
|
+
}
|
|
3808
|
+
function formatTimestamp(ts) {
|
|
3809
|
+
return new Date(ts).toLocaleString("zh-CN", {
|
|
3810
|
+
year: "numeric",
|
|
3811
|
+
month: "2-digit",
|
|
3812
|
+
day: "2-digit",
|
|
3813
|
+
hour: "2-digit",
|
|
3814
|
+
minute: "2-digit",
|
|
3815
|
+
second: "2-digit"
|
|
3816
|
+
});
|
|
3817
|
+
}
|
|
3818
|
+
function pct(value) {
|
|
3819
|
+
return `${(value * 100).toFixed(1)}%`;
|
|
3820
|
+
}
|
|
3821
|
+
function renderCoverageMD(coverage) {
|
|
3822
|
+
return `
|
|
3823
|
+
### \u8986\u76D6\u7387
|
|
3824
|
+
|
|
3825
|
+
| \u6307\u6807 | \u8986\u76D6\u7387 | \u8FDB\u5EA6 |
|
|
3826
|
+
|------|--------|------|
|
|
3827
|
+
| \u8BED\u53E5 (Statements) | ${pct(coverage.statements)} | ${renderProgressBar(coverage.statements)} |
|
|
3828
|
+
| \u5206\u652F (Branches) | ${pct(coverage.branches)} | ${renderProgressBar(coverage.branches)} |
|
|
3829
|
+
| \u51FD\u6570 (Functions) | ${pct(coverage.functions)} | ${renderProgressBar(coverage.functions)} |
|
|
3830
|
+
| \u884C (Lines) | ${pct(coverage.lines)} | ${renderProgressBar(coverage.lines)} |
|
|
3831
|
+
`;
|
|
3832
|
+
}
|
|
3833
|
+
function renderProgressBar(value) {
|
|
3834
|
+
const filled = Math.round(value * 10);
|
|
3835
|
+
const empty = 10 - filled;
|
|
3836
|
+
return `${"\u2588".repeat(filled)}${"\u2591".repeat(empty)}`;
|
|
3837
|
+
}
|
|
3838
|
+
var TYPE_LABELS = {
|
|
3839
|
+
unit: "\u5355\u5143\u6D4B\u8BD5",
|
|
3840
|
+
component: "\u7EC4\u4EF6\u6D4B\u8BD5",
|
|
3841
|
+
e2e: "E2E \u6D4B\u8BD5",
|
|
3842
|
+
api: "API \u6D4B\u8BD5",
|
|
3843
|
+
visual: "\u89C6\u89C9\u56DE\u5F52\u6D4B\u8BD5",
|
|
3844
|
+
performance: "\u6027\u80FD\u6D4B\u8BD5"
|
|
3845
|
+
};
|
|
3846
|
+
function generateMDReport(data) {
|
|
3847
|
+
const passRate = data.summary.total > 0 ? (data.summary.passed / data.summary.total * 100).toFixed(1) : "0";
|
|
3848
|
+
const rateIcon = parseFloat(passRate) >= 80 ? "\u2705" : parseFloat(passRate) >= 50 ? "\u26A0\uFE0F" : "\u274C";
|
|
3849
|
+
const lines = [];
|
|
3850
|
+
lines.push(`# QAT \u6D4B\u8BD5\u62A5\u544A`);
|
|
3851
|
+
lines.push("");
|
|
3852
|
+
lines.push(`> \u751F\u6210\u65F6\u95F4: ${formatTimestamp(data.timestamp)} | \u603B\u8017\u65F6: ${formatDuration(data.duration)}`);
|
|
3853
|
+
lines.push("");
|
|
3854
|
+
lines.push(`## \u603B\u89C8`);
|
|
3855
|
+
lines.push("");
|
|
3856
|
+
lines.push(`| \u6307\u6807 | \u6570\u503C |`);
|
|
3857
|
+
lines.push(`|------|------|`);
|
|
3858
|
+
lines.push(`| \u901A\u8FC7\u7387 | ${rateIcon} **${passRate}%** |`);
|
|
3859
|
+
lines.push(`| \u603B\u7528\u4F8B | ${data.summary.total} |`);
|
|
3860
|
+
lines.push(`| \u2705 \u901A\u8FC7 | ${data.summary.passed} |`);
|
|
3861
|
+
if (data.summary.failed > 0) lines.push(`| \u274C \u5931\u8D25 | ${data.summary.failed} |`);
|
|
3862
|
+
if (data.summary.skipped > 0) lines.push(`| \u23ED\uFE0F \u8DF3\u8FC7 | ${data.summary.skipped} |`);
|
|
3863
|
+
if (data.summary.pending > 0) lines.push(`| \u23F3 \u5F85\u5B9A | ${data.summary.pending} |`);
|
|
3864
|
+
lines.push(`| \u23F1\uFE0F \u8017\u65F6 | ${formatDuration(data.duration)} |`);
|
|
3865
|
+
lines.push("");
|
|
3866
|
+
if (Object.keys(data.byType).length > 0) {
|
|
3867
|
+
lines.push(`## \u6309\u7C7B\u578B\u7EDF\u8BA1`);
|
|
3868
|
+
lines.push("");
|
|
3869
|
+
lines.push(`| \u7C7B\u578B | \u901A\u8FC7 | \u5931\u8D25 | \u8DF3\u8FC7 | \u603B\u8BA1 | \u901A\u8FC7\u7387 |`);
|
|
3870
|
+
lines.push(`|------|------|------|------|------|--------|`);
|
|
3871
|
+
for (const [type, stats] of Object.entries(data.byType)) {
|
|
3872
|
+
const label = TYPE_LABELS[type] || type;
|
|
3873
|
+
const rate = stats.total > 0 ? (stats.passed / stats.total * 100).toFixed(0) + "%" : "-";
|
|
3874
|
+
lines.push(`| ${label} | ${stats.passed} | ${stats.failed} | ${stats.skipped} | ${stats.total} | ${rate} |`);
|
|
3875
|
+
}
|
|
3876
|
+
lines.push("");
|
|
3877
|
+
}
|
|
3878
|
+
if (data.coverage) {
|
|
3879
|
+
lines.push(renderCoverageMD(data.coverage));
|
|
3880
|
+
lines.push("");
|
|
3881
|
+
}
|
|
3882
|
+
lines.push(`## \u6D4B\u8BD5\u8BE6\u60C5`);
|
|
3883
|
+
lines.push("");
|
|
3884
|
+
for (const result of data.results) {
|
|
3885
|
+
const typeLabel = TYPE_LABELS[result.type] || result.type;
|
|
3886
|
+
const statusIcon = result.status === "passed" ? "\u2705" : result.status === "failed" ? "\u274C" : "\u26A0\uFE0F";
|
|
3887
|
+
lines.push(`### ${statusIcon} ${typeLabel}`);
|
|
3888
|
+
lines.push("");
|
|
3889
|
+
if (result.suites.length === 0) {
|
|
3890
|
+
lines.push(`*\u65E0\u6D4B\u8BD5\u7ED3\u679C*`);
|
|
3891
|
+
lines.push("");
|
|
3892
|
+
continue;
|
|
3893
|
+
}
|
|
3894
|
+
for (const suite of result.suites) {
|
|
3895
|
+
const suiteIcon = suite.status === "passed" ? "\u2705" : suite.status === "failed" ? "\u274C" : "\u26A0\uFE0F";
|
|
3896
|
+
lines.push(`#### ${suiteIcon} ${suite.name}`);
|
|
3897
|
+
lines.push("");
|
|
3898
|
+
lines.push(`- \u6587\u4EF6: \`${suite.file}\``);
|
|
3899
|
+
lines.push(`- \u8017\u65F6: ${formatDuration(suite.duration)}`);
|
|
3900
|
+
lines.push("");
|
|
3901
|
+
if (suite.tests.length > 0) {
|
|
3902
|
+
lines.push(`| \u72B6\u6001 | \u6D4B\u8BD5\u540D\u79F0 | \u8017\u65F6 |`);
|
|
3903
|
+
lines.push(`|------|----------|------|`);
|
|
3904
|
+
for (const test of suite.tests) {
|
|
3905
|
+
const testIcon = test.status === "passed" ? "\u2705" : test.status === "failed" ? "\u274C" : test.status === "skipped" ? "\u23ED\uFE0F" : "\u23F3";
|
|
3906
|
+
const name = test.error ? `**${test.name}**` : test.name;
|
|
3907
|
+
lines.push(`| ${testIcon} | ${name} | ${formatDuration(test.duration)} |`);
|
|
3908
|
+
}
|
|
3909
|
+
lines.push("");
|
|
3910
|
+
}
|
|
3911
|
+
const failedTests = suite.tests.filter((t) => t.status === "failed" && t.error);
|
|
3912
|
+
if (failedTests.length > 0) {
|
|
3913
|
+
lines.push(`<details>`);
|
|
3914
|
+
lines.push(`<summary>\u274C \u5931\u8D25\u8BE6\u60C5 (${failedTests.length})</summary>`);
|
|
3915
|
+
lines.push("");
|
|
3916
|
+
for (const test of failedTests) {
|
|
3917
|
+
lines.push(`**${test.name}**`);
|
|
3918
|
+
lines.push("```");
|
|
3919
|
+
lines.push(test.error.message);
|
|
3920
|
+
if (test.error.stack) {
|
|
3921
|
+
lines.push(test.error.stack);
|
|
3922
|
+
}
|
|
3923
|
+
if (test.error.expected && test.error.actual) {
|
|
3924
|
+
lines.push(`Expected: ${test.error.expected}`);
|
|
3925
|
+
lines.push(`Actual: ${test.error.actual}`);
|
|
3926
|
+
}
|
|
3927
|
+
lines.push("```");
|
|
3928
|
+
lines.push("");
|
|
3929
|
+
}
|
|
3930
|
+
lines.push(`</details>`);
|
|
3931
|
+
lines.push("");
|
|
3932
|
+
}
|
|
3933
|
+
}
|
|
3934
|
+
}
|
|
3935
|
+
lines.push("---");
|
|
3936
|
+
lines.push("");
|
|
3937
|
+
lines.push(`*\u7531 QAT \u81EA\u52A8\u5316\u6D4B\u8BD5\u5DE5\u5177\u751F\u6210 | ${formatTimestamp(data.timestamp)}*`);
|
|
3938
|
+
return lines.join("\n");
|
|
3939
|
+
}
|
|
3940
|
+
function writeReportToDisk(data, outputDir) {
|
|
3941
|
+
const md = generateMDReport(data);
|
|
3942
|
+
const dir = path11.resolve(outputDir);
|
|
3943
|
+
if (!fs10.existsSync(dir)) {
|
|
3944
|
+
fs10.mkdirSync(dir, { recursive: true });
|
|
3945
|
+
}
|
|
3946
|
+
const mdPath = path11.join(dir, "report.md");
|
|
3947
|
+
fs10.writeFileSync(mdPath, md, "utf-8");
|
|
3948
|
+
const jsonPath = path11.join(dir, "report.json");
|
|
3949
|
+
fs10.writeFileSync(jsonPath, JSON.stringify(data, null, 2), "utf-8");
|
|
3950
|
+
return mdPath;
|
|
3951
|
+
}
|
|
3952
|
+
|
|
3913
3953
|
// src/commands/run.ts
|
|
3914
3954
|
var RESULTS_DIR = ".qat-results";
|
|
3915
3955
|
var SERVER_REQUIRED_TYPES = ["e2e", "visual", "performance"];
|
|
3956
|
+
var TYPE_DEPENDENCIES = {
|
|
3957
|
+
unit: { pkg: "vitest", runner: "Vitest", installCmd: "npm install -D vitest" },
|
|
3958
|
+
component: { pkg: "vitest", runner: "Vitest", installCmd: "npm install -D vitest @vue/test-utils happy-dom" },
|
|
3959
|
+
api: { pkg: "vitest", runner: "Vitest", installCmd: "npm install -D vitest" },
|
|
3960
|
+
e2e: { pkg: "@playwright/test", runner: "Playwright", installCmd: "npm install -D @playwright/test && npx playwright install" },
|
|
3961
|
+
visual: { pkg: "@playwright/test", runner: "Playwright", installCmd: "npm install -D @playwright/test && npx playwright install" },
|
|
3962
|
+
performance: { pkg: "lighthouse", runner: "Lighthouse", installCmd: "npm install -D lighthouse" }
|
|
3963
|
+
};
|
|
3916
3964
|
var TYPE_RUNNERS = {
|
|
3917
3965
|
unit: "Vitest",
|
|
3918
3966
|
component: "Vitest",
|
|
@@ -3921,7 +3969,7 @@ var TYPE_RUNNERS = {
|
|
|
3921
3969
|
visual: "Playwright",
|
|
3922
3970
|
performance: "Lighthouse"
|
|
3923
3971
|
};
|
|
3924
|
-
var
|
|
3972
|
+
var TYPE_LABELS2 = {
|
|
3925
3973
|
unit: "\u5355\u5143\u6D4B\u8BD5",
|
|
3926
3974
|
component: "\u7EC4\u4EF6\u6D4B\u8BD5",
|
|
3927
3975
|
e2e: "E2E\u6D4B\u8BD5",
|
|
@@ -3957,6 +4005,61 @@ async function executeRun(options) {
|
|
|
3957
4005
|
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
4006
|
return;
|
|
3959
4007
|
}
|
|
4008
|
+
const missingDeps = checkTestDependencies(typesToRun);
|
|
4009
|
+
if (missingDeps.length > 0) {
|
|
4010
|
+
console.log(chalk5.yellow("\n \u26A0 \u4EE5\u4E0B\u6D4B\u8BD5\u6846\u67B6\u4F9D\u8D56\u672A\u5B89\u88C5:\n"));
|
|
4011
|
+
for (const dep of missingDeps) {
|
|
4012
|
+
console.log(chalk5.white(` ${dep.runner} (${dep.pkg})`));
|
|
4013
|
+
console.log(chalk5.gray(` \u5B89\u88C5\u547D\u4EE4: ${chalk5.cyan(dep.installCmd)}`));
|
|
4014
|
+
}
|
|
4015
|
+
console.log();
|
|
4016
|
+
const { action } = await inquirer3.prompt([
|
|
4017
|
+
{
|
|
4018
|
+
type: "list",
|
|
4019
|
+
name: "action",
|
|
4020
|
+
message: "\u5982\u4F55\u5904\u7406\uFF1F",
|
|
4021
|
+
choices: [
|
|
4022
|
+
{ name: "\u81EA\u52A8\u5B89\u88C5\u7F3A\u5931\u4F9D\u8D56", value: "install" },
|
|
4023
|
+
{ name: "\u8DF3\u8FC7\u672A\u5B89\u88C5\u7684\u6D4B\u8BD5\u7C7B\u578B", value: "skip" },
|
|
4024
|
+
{ name: "\u53D6\u6D88\u8FD0\u884C", value: "cancel" }
|
|
4025
|
+
],
|
|
4026
|
+
default: "install"
|
|
4027
|
+
}
|
|
4028
|
+
]);
|
|
4029
|
+
if (action === "cancel") {
|
|
4030
|
+
console.log(chalk5.gray("\n \u5DF2\u53D6\u6D88\u8FD0\u884C\n"));
|
|
4031
|
+
return;
|
|
4032
|
+
}
|
|
4033
|
+
if (action === "install") {
|
|
4034
|
+
const installed = await installTestDependencies(missingDeps);
|
|
4035
|
+
if (!installed) {
|
|
4036
|
+
console.log(chalk5.yellow(" \u90E8\u5206\u4F9D\u8D56\u5B89\u88C5\u5931\u8D25\uFF0C\u5C06\u8DF3\u8FC7\u5BF9\u5E94\u7684\u6D4B\u8BD5\u7C7B\u578B"));
|
|
4037
|
+
}
|
|
4038
|
+
const stillMissing = checkTestDependencies(typesToRun);
|
|
4039
|
+
if (stillMissing.length > 0) {
|
|
4040
|
+
const missingRunners = new Set(stillMissing.map((d) => d.pkg));
|
|
4041
|
+
typesToRun = typesToRun.filter((t) => {
|
|
4042
|
+
const dep = TYPE_DEPENDENCIES[t];
|
|
4043
|
+
return !dep || !missingRunners.has(dep.pkg);
|
|
4044
|
+
});
|
|
4045
|
+
if (typesToRun.length === 0) {
|
|
4046
|
+
console.log(chalk5.yellow("\n \u6CA1\u6709\u53EF\u8FD0\u884C\u7684\u6D4B\u8BD5\u7C7B\u578B\uFF08\u4F9D\u8D56\u672A\u5B89\u88C5\uFF09\n"));
|
|
4047
|
+
return;
|
|
4048
|
+
}
|
|
4049
|
+
}
|
|
4050
|
+
}
|
|
4051
|
+
if (action === "skip") {
|
|
4052
|
+
const missingRunners = new Set(missingDeps.map((d) => d.pkg));
|
|
4053
|
+
typesToRun = typesToRun.filter((t) => {
|
|
4054
|
+
const dep = TYPE_DEPENDENCIES[t];
|
|
4055
|
+
return !dep || !missingRunners.has(dep.pkg);
|
|
4056
|
+
});
|
|
4057
|
+
if (typesToRun.length === 0) {
|
|
4058
|
+
console.log(chalk5.yellow("\n \u6CA1\u6709\u53EF\u8FD0\u884C\u7684\u6D4B\u8BD5\u7C7B\u578B\n"));
|
|
4059
|
+
return;
|
|
4060
|
+
}
|
|
4061
|
+
}
|
|
4062
|
+
}
|
|
3960
4063
|
const serverNeededTypes = typesToRun.filter((t) => SERVER_REQUIRED_TYPES.includes(t));
|
|
3961
4064
|
if (serverNeededTypes.length > 0) {
|
|
3962
4065
|
const serverOk = await checkDevServer(config);
|
|
@@ -3965,7 +4068,7 @@ async function executeRun(options) {
|
|
|
3965
4068
|
{
|
|
3966
4069
|
type: "list",
|
|
3967
4070
|
name: "action",
|
|
3968
|
-
message: `\u8FD0\u884C ${serverNeededTypes.map((t) =>
|
|
4071
|
+
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
4072
|
choices: [
|
|
3970
4073
|
{ name: "\u81EA\u52A8\u542F\u52A8 dev server \u5E76\u8FD0\u884C", value: "start" },
|
|
3971
4074
|
{ name: "\u8DF3\u8FC7\u8FD9\u4E9B\u6D4B\u8BD5\u7C7B\u578B\uFF0C\u4EC5\u8FD0\u884C\u5176\u4ED6\u6D4B\u8BD5", value: "skip" },
|
|
@@ -4046,7 +4149,7 @@ async function executeRun(options) {
|
|
|
4046
4149
|
spinner.stop();
|
|
4047
4150
|
} else {
|
|
4048
4151
|
for (const testType of typesToRun) {
|
|
4049
|
-
const label =
|
|
4152
|
+
const label = TYPE_LABELS2[testType] || testType;
|
|
4050
4153
|
const spinner = ora4(`\u6B63\u5728\u8FD0\u884C ${label}...`).start();
|
|
4051
4154
|
try {
|
|
4052
4155
|
const result = await runTestType(testType, config, options);
|
|
@@ -4058,14 +4161,25 @@ async function executeRun(options) {
|
|
|
4058
4161
|
}
|
|
4059
4162
|
}
|
|
4060
4163
|
}
|
|
4061
|
-
|
|
4164
|
+
displayJestStyleResults(results);
|
|
4062
4165
|
saveRunResults(results);
|
|
4166
|
+
const reportData = aggregateResults(results);
|
|
4167
|
+
const outputDir = config.report.outputDir || "qat-report";
|
|
4168
|
+
const reportPath = writeReportToDisk(reportData, outputDir);
|
|
4169
|
+
const relativePath = path12.relative(process.cwd(), reportPath);
|
|
4170
|
+
console.log(chalk5.gray(`
|
|
4171
|
+
\u62A5\u544A\u5DF2\u751F\u6210: ${chalk5.cyan(relativePath)}`));
|
|
4172
|
+
console.log();
|
|
4173
|
+
const hasFailures = results.some((r) => r.status === "failed");
|
|
4174
|
+
if (hasFailures && isAIAvailable(config.ai)) {
|
|
4175
|
+
await aiAnalyzeFailures(results, config.ai);
|
|
4176
|
+
}
|
|
4063
4177
|
}
|
|
4064
4178
|
function printDryRunCommands(types, options, config) {
|
|
4065
4179
|
console.log();
|
|
4066
4180
|
console.log(chalk5.cyan(" \u53EF\u6267\u884C\u7684\u6D4B\u8BD5\u547D\u4EE4:\n"));
|
|
4067
4181
|
for (const testType of types) {
|
|
4068
|
-
const label =
|
|
4182
|
+
const label = TYPE_LABELS2[testType];
|
|
4069
4183
|
const runner = TYPE_RUNNERS[testType];
|
|
4070
4184
|
let cmd;
|
|
4071
4185
|
switch (runner) {
|
|
@@ -4133,7 +4247,7 @@ async function determineTypesToRun(type, file, config) {
|
|
|
4133
4247
|
name: "selectedTypes",
|
|
4134
4248
|
message: "\u9009\u62E9\u8981\u8FD0\u884C\u7684\u6D4B\u8BD5\u7C7B\u578B (\u7A7A\u683C\u9009\u62E9/\u53D6\u6D88\uFF0C\u56DE\u8F66\u786E\u8BA4):",
|
|
4135
4249
|
choices: enabledTypes.map((t) => ({
|
|
4136
|
-
name: `${
|
|
4250
|
+
name: `${TYPE_LABELS2[t]} (${chalk5.gray(TYPE_RUNNERS[t])})`,
|
|
4137
4251
|
value: t,
|
|
4138
4252
|
checked: true
|
|
4139
4253
|
})),
|
|
@@ -4267,53 +4381,82 @@ function suiteStatusToSpinner(status) {
|
|
|
4267
4381
|
return "warn";
|
|
4268
4382
|
}
|
|
4269
4383
|
}
|
|
4270
|
-
|
|
4271
|
-
|
|
4384
|
+
function displayJestStyleResults(results) {
|
|
4385
|
+
console.log();
|
|
4386
|
+
for (const result of results) {
|
|
4387
|
+
const typeLabel = TYPE_LABELS2[result.type] || result.type;
|
|
4388
|
+
if (result.suites.length === 0) {
|
|
4389
|
+
console.log(chalk5.gray(` ${typeLabel}: \u65E0\u6D4B\u8BD5\u7ED3\u679C`));
|
|
4390
|
+
continue;
|
|
4391
|
+
}
|
|
4392
|
+
for (const suite of result.suites) {
|
|
4393
|
+
if (suite.tests.length === 0) continue;
|
|
4394
|
+
const suiteIcon = suite.status === "passed" ? chalk5.green("PASS") : chalk5.red("FAIL");
|
|
4395
|
+
console.log(` ${suiteIcon} ${chalk5.white(suite.name)} ${chalk5.gray(`(${formatDuration2(suite.duration)})`)}`);
|
|
4396
|
+
for (const test of suite.tests) {
|
|
4397
|
+
const icon = test.status === "passed" ? chalk5.green(" \u2713") : test.status === "failed" ? chalk5.red(" \u2715") : chalk5.yellow(" \u25CB");
|
|
4398
|
+
const name = test.status === "failed" ? chalk5.red(test.name) : test.name;
|
|
4399
|
+
console.log(` ${icon} ${name} ${chalk5.gray(formatDuration2(test.duration))}`);
|
|
4400
|
+
}
|
|
4401
|
+
}
|
|
4402
|
+
console.log();
|
|
4403
|
+
}
|
|
4272
4404
|
let totalPassed = 0;
|
|
4273
4405
|
let totalFailed = 0;
|
|
4274
4406
|
let totalSkipped = 0;
|
|
4275
4407
|
let totalDuration = 0;
|
|
4408
|
+
const typeStats = {};
|
|
4276
4409
|
for (const result of results) {
|
|
4277
4410
|
totalDuration += result.duration;
|
|
4411
|
+
if (!typeStats[result.type]) {
|
|
4412
|
+
typeStats[result.type] = { passed: 0, failed: 0, skipped: 0, duration: 0 };
|
|
4413
|
+
}
|
|
4414
|
+
typeStats[result.type].duration += result.duration;
|
|
4278
4415
|
for (const suite of result.suites) {
|
|
4279
|
-
totalSuites++;
|
|
4280
4416
|
for (const test of suite.tests) {
|
|
4281
|
-
if (test.status === "passed")
|
|
4282
|
-
|
|
4283
|
-
|
|
4417
|
+
if (test.status === "passed") {
|
|
4418
|
+
totalPassed++;
|
|
4419
|
+
typeStats[result.type].passed++;
|
|
4420
|
+
} else if (test.status === "failed") {
|
|
4421
|
+
totalFailed++;
|
|
4422
|
+
typeStats[result.type].failed++;
|
|
4423
|
+
} else if (test.status === "skipped") {
|
|
4424
|
+
totalSkipped++;
|
|
4425
|
+
typeStats[result.type].skipped++;
|
|
4426
|
+
}
|
|
4284
4427
|
}
|
|
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"));
|
|
4428
|
+
}
|
|
4296
4429
|
}
|
|
4430
|
+
const total = totalPassed + totalFailed + totalSkipped;
|
|
4431
|
+
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"));
|
|
4432
|
+
console.log(chalk5.white(" \u6D4B\u8BD5\u7ED3\u679C\u6C47\u603B"));
|
|
4433
|
+
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
4434
|
console.log();
|
|
4298
|
-
console.log(` ${chalk5.white("\
|
|
4299
|
-
|
|
4300
|
-
|
|
4301
|
-
|
|
4302
|
-
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
console.log();
|
|
4306
|
-
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
|
|
4435
|
+
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))}`);
|
|
4436
|
+
console.log(` ${"\u2500".repeat(14)} ${"\u2500".repeat(6)} ${"\u2500".repeat(6)} ${"\u2500".repeat(6)} ${"\u2500".repeat(6)} ${"\u2500".repeat(8)} ${"\u2500".repeat(8)}`);
|
|
4437
|
+
for (const [type, stats] of Object.entries(typeStats)) {
|
|
4438
|
+
const label = (TYPE_LABELS2[type] || type).padEnd(14);
|
|
4439
|
+
const typeTotal = stats.passed + stats.failed + stats.skipped;
|
|
4440
|
+
const rate = typeTotal > 0 ? (stats.passed / typeTotal * 100).toFixed(0) + "%" : "-";
|
|
4441
|
+
const rateColored = typeTotal > 0 && stats.passed === typeTotal ? chalk5.green(rate.padStart(8)) : stats.failed > 0 ? chalk5.red(rate.padStart(8)) : rate.padStart(8);
|
|
4442
|
+
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)}`);
|
|
4443
|
+
}
|
|
4444
|
+
console.log(` ${"\u2500".repeat(14)} ${"\u2500".repeat(6)} ${"\u2500".repeat(6)} ${"\u2500".repeat(6)} ${"\u2500".repeat(6)} ${"\u2500".repeat(8)} ${"\u2500".repeat(8)}`);
|
|
4445
|
+
const totalRate = total > 0 ? (totalPassed / total * 100).toFixed(0) + "%" : "-";
|
|
4446
|
+
const totalRateColored = total > 0 && totalFailed === 0 ? chalk5.green(totalRate.padStart(8)) : totalFailed > 0 ? chalk5.red(totalRate.padStart(8)) : totalRate.padStart(8);
|
|
4447
|
+
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)}`);
|
|
4448
|
+
console.log();
|
|
4449
|
+
for (const result of results) {
|
|
4450
|
+
if (result.coverage) {
|
|
4451
|
+
const c = result.coverage;
|
|
4452
|
+
console.log(chalk5.cyan(" \u8986\u76D6\u7387:"));
|
|
4453
|
+
console.log(` \u8BED\u53E5: ${coverageColor(c.statements)} \u5206\u652F: ${coverageColor(c.branches)} \u51FD\u6570: ${coverageColor(c.functions)} \u884C: ${coverageColor(c.lines)}`);
|
|
4454
|
+
console.log();
|
|
4311
4455
|
}
|
|
4312
4456
|
}
|
|
4313
4457
|
for (const result of results) {
|
|
4314
4458
|
if (result.type === "performance" && result.performance) {
|
|
4315
4459
|
const p = result.performance;
|
|
4316
|
-
console.log();
|
|
4317
4460
|
console.log(chalk5.cyan(" \u6027\u80FD\u6307\u6807:"));
|
|
4318
4461
|
console.log(` Performance: ${scoreColor(p.performance)} ${p.performance}/100`);
|
|
4319
4462
|
console.log(` Accessibility: ${scoreColor(p.accessibility)} ${p.accessibility}/100`);
|
|
@@ -4322,6 +4465,7 @@ async function displaySummary(results, config) {
|
|
|
4322
4465
|
if (p.lcp !== void 0) console.log(` LCP: ${p.lcp.toFixed(0)}ms`);
|
|
4323
4466
|
if (p.fid !== void 0) console.log(` FID: ${p.fid.toFixed(0)}ms`);
|
|
4324
4467
|
if (p.cls !== void 0) console.log(` CLS: ${p.cls.toFixed(3)}`);
|
|
4468
|
+
console.log();
|
|
4325
4469
|
}
|
|
4326
4470
|
}
|
|
4327
4471
|
const failedTests = results.flatMap(
|
|
@@ -4330,23 +4474,31 @@ async function displaySummary(results, config) {
|
|
|
4330
4474
|
)
|
|
4331
4475
|
);
|
|
4332
4476
|
if (failedTests.length > 0) {
|
|
4333
|
-
console.log();
|
|
4334
4477
|
console.log(chalk5.red(" \u5931\u8D25\u8BE6\u60C5:"));
|
|
4335
4478
|
for (const { suite, test } of failedTests) {
|
|
4336
|
-
console.log(chalk5.red(` \
|
|
4479
|
+
console.log(chalk5.red(` \u2715 ${suite.name} > ${test.name}`));
|
|
4337
4480
|
if (test.error?.message) {
|
|
4338
4481
|
console.log(chalk5.gray(` ${test.error.message.split("\n")[0]}`));
|
|
4339
4482
|
}
|
|
4340
4483
|
}
|
|
4484
|
+
console.log();
|
|
4341
4485
|
}
|
|
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
4486
|
if (totalFailed > 0) {
|
|
4487
|
+
console.log(chalk5.red(` Tests: ${totalFailed} failed, ${totalPassed} passed, ${total} total`));
|
|
4345
4488
|
process.exitCode = 1;
|
|
4489
|
+
} else if (total === 0) {
|
|
4490
|
+
console.log(chalk5.yellow(" \u6CA1\u6709\u53D1\u73B0\u6D4B\u8BD5\u7528\u4F8B"));
|
|
4491
|
+
} else {
|
|
4492
|
+
console.log(chalk5.green(` Tests: ${totalPassed} passed, ${total} total`));
|
|
4346
4493
|
}
|
|
4347
|
-
|
|
4348
|
-
|
|
4349
|
-
|
|
4494
|
+
console.log(chalk5.gray(` Time: ${formatDuration2(totalDuration)}`));
|
|
4495
|
+
console.log();
|
|
4496
|
+
}
|
|
4497
|
+
function coverageColor(value) {
|
|
4498
|
+
const pct3 = `${(value * 100).toFixed(1)}%`;
|
|
4499
|
+
if (value >= 0.8) return chalk5.green(pct3);
|
|
4500
|
+
if (value >= 0.5) return chalk5.yellow(pct3);
|
|
4501
|
+
return chalk5.red(pct3);
|
|
4350
4502
|
}
|
|
4351
4503
|
async function aiAnalyzeFailures(results, aiConfig) {
|
|
4352
4504
|
const failedTests = results.flatMap(
|
|
@@ -4375,7 +4527,7 @@ async function aiAnalyzeFailures(results, aiConfig) {
|
|
|
4375
4527
|
}
|
|
4376
4528
|
}
|
|
4377
4529
|
}
|
|
4378
|
-
function
|
|
4530
|
+
function formatDuration2(ms) {
|
|
4379
4531
|
if (ms < 1e3) return `${ms}ms`;
|
|
4380
4532
|
if (ms < 6e4) return `${(ms / 1e3).toFixed(1)}s`;
|
|
4381
4533
|
const min = Math.floor(ms / 6e4);
|
|
@@ -4401,8 +4553,8 @@ async function checkDevServer(config) {
|
|
|
4401
4553
|
}
|
|
4402
4554
|
async function startDevServer(config) {
|
|
4403
4555
|
const { spawn } = await import("child_process");
|
|
4404
|
-
const hasPnpm =
|
|
4405
|
-
const hasYarn =
|
|
4556
|
+
const hasPnpm = fs11.existsSync(path12.join(process.cwd(), "pnpm-lock.yaml"));
|
|
4557
|
+
const hasYarn = fs11.existsSync(path12.join(process.cwd(), "yarn.lock"));
|
|
4406
4558
|
const pkgCmd = hasPnpm ? "pnpm" : hasYarn ? "yarn" : "npm";
|
|
4407
4559
|
console.log(chalk5.cyan(` \u6B63\u5728\u542F\u52A8 dev server (${pkgCmd} run dev) ...`));
|
|
4408
4560
|
const child = spawn(pkgCmd, ["run", "dev"], {
|
|
@@ -4431,27 +4583,99 @@ async function startDevServer(config) {
|
|
|
4431
4583
|
}
|
|
4432
4584
|
function saveRunResults(results) {
|
|
4433
4585
|
if (results.length === 0) return;
|
|
4434
|
-
const resultsPath =
|
|
4435
|
-
if (!
|
|
4436
|
-
|
|
4586
|
+
const resultsPath = path12.join(process.cwd(), RESULTS_DIR);
|
|
4587
|
+
if (!fs11.existsSync(resultsPath)) {
|
|
4588
|
+
fs11.mkdirSync(resultsPath, { recursive: true });
|
|
4437
4589
|
}
|
|
4438
4590
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
4439
4591
|
const fileName = `result-${timestamp}.json`;
|
|
4440
|
-
const filePath =
|
|
4592
|
+
const filePath = path12.join(resultsPath, fileName);
|
|
4441
4593
|
const data = {
|
|
4442
4594
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4443
4595
|
results
|
|
4444
4596
|
};
|
|
4445
|
-
|
|
4597
|
+
fs11.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf-8");
|
|
4446
4598
|
try {
|
|
4447
|
-
const files =
|
|
4599
|
+
const files = fs11.readdirSync(resultsPath).filter((f) => f.startsWith("result-") && f.endsWith(".json")).sort();
|
|
4448
4600
|
while (files.length > 20) {
|
|
4449
4601
|
const oldest = files.shift();
|
|
4450
|
-
|
|
4602
|
+
fs11.unlinkSync(path12.join(resultsPath, oldest));
|
|
4451
4603
|
}
|
|
4452
4604
|
} catch {
|
|
4453
4605
|
}
|
|
4454
4606
|
}
|
|
4607
|
+
function checkTestDependencies(types) {
|
|
4608
|
+
const pkgPath = path12.join(process.cwd(), "package.json");
|
|
4609
|
+
let allDeps = {};
|
|
4610
|
+
if (fs11.existsSync(pkgPath)) {
|
|
4611
|
+
try {
|
|
4612
|
+
const pkg = JSON.parse(fs11.readFileSync(pkgPath, "utf-8"));
|
|
4613
|
+
allDeps = {
|
|
4614
|
+
...pkg.dependencies,
|
|
4615
|
+
...pkg.devDependencies
|
|
4616
|
+
};
|
|
4617
|
+
} catch {
|
|
4618
|
+
}
|
|
4619
|
+
}
|
|
4620
|
+
const missingDeps = [];
|
|
4621
|
+
const checkedPkgs = /* @__PURE__ */ new Set();
|
|
4622
|
+
for (const testType of types) {
|
|
4623
|
+
const dep = TYPE_DEPENDENCIES[testType];
|
|
4624
|
+
if (dep && !checkedPkgs.has(dep.pkg)) {
|
|
4625
|
+
checkedPkgs.add(dep.pkg);
|
|
4626
|
+
if (!allDeps[dep.pkg]) {
|
|
4627
|
+
missingDeps.push(dep);
|
|
4628
|
+
}
|
|
4629
|
+
}
|
|
4630
|
+
}
|
|
4631
|
+
return missingDeps;
|
|
4632
|
+
}
|
|
4633
|
+
async function installTestDependencies(missingDeps) {
|
|
4634
|
+
const hasPnpm = fs11.existsSync(path12.join(process.cwd(), "pnpm-lock.yaml"));
|
|
4635
|
+
const hasYarn = fs11.existsSync(path12.join(process.cwd(), "yarn.lock"));
|
|
4636
|
+
const pkgManager = hasPnpm ? "pnpm" : hasYarn ? "yarn" : "npm";
|
|
4637
|
+
const { execFile: execFile5 } = await import("child_process");
|
|
4638
|
+
let allSuccess = true;
|
|
4639
|
+
for (const dep of missingDeps) {
|
|
4640
|
+
const spinner = ora4(`\u6B63\u5728\u5B89\u88C5 ${dep.pkg}...`).start();
|
|
4641
|
+
const isPlaywright = dep.pkg === "@playwright/test";
|
|
4642
|
+
const installArgs = hasPnpm ? ["add", "-D", dep.pkg] : hasYarn ? ["add", "-D", dep.pkg] : ["install", "-D", dep.pkg];
|
|
4643
|
+
try {
|
|
4644
|
+
await new Promise((resolve, reject) => {
|
|
4645
|
+
const cmd = pkgManager === "npm" ? "npm" : pkgManager;
|
|
4646
|
+
const child = execFile5(cmd, installArgs, {
|
|
4647
|
+
cwd: process.cwd(),
|
|
4648
|
+
shell: true,
|
|
4649
|
+
stdio: "pipe"
|
|
4650
|
+
}, (error) => {
|
|
4651
|
+
if (error) reject(error);
|
|
4652
|
+
else resolve();
|
|
4653
|
+
});
|
|
4654
|
+
child.on("error", reject);
|
|
4655
|
+
});
|
|
4656
|
+
if (isPlaywright) {
|
|
4657
|
+
spinner.text = `\u6B63\u5728\u5B89\u88C5 Playwright \u6D4F\u89C8\u5668...`;
|
|
4658
|
+
await new Promise((resolve, reject) => {
|
|
4659
|
+
const child = execFile5("npx", ["playwright", "install"], {
|
|
4660
|
+
cwd: process.cwd(),
|
|
4661
|
+
shell: true,
|
|
4662
|
+
stdio: "pipe"
|
|
4663
|
+
}, (error) => {
|
|
4664
|
+
if (error) reject(error);
|
|
4665
|
+
else resolve();
|
|
4666
|
+
});
|
|
4667
|
+
child.on("error", reject);
|
|
4668
|
+
});
|
|
4669
|
+
}
|
|
4670
|
+
spinner.succeed(`${dep.pkg} \u5B89\u88C5\u6210\u529F`);
|
|
4671
|
+
} catch (error) {
|
|
4672
|
+
spinner.fail(`${dep.pkg} \u5B89\u88C5\u5931\u8D25`);
|
|
4673
|
+
console.log(chalk5.gray(` \u53EF\u624B\u52A8\u5B89\u88C5: ${chalk5.cyan(dep.installCmd)}`));
|
|
4674
|
+
allSuccess = false;
|
|
4675
|
+
}
|
|
4676
|
+
}
|
|
4677
|
+
return allSuccess;
|
|
4678
|
+
}
|
|
4455
4679
|
|
|
4456
4680
|
// src/commands/mock.ts
|
|
4457
4681
|
import chalk6 from "chalk";
|
|
@@ -4575,219 +4799,8 @@ function showStatus() {
|
|
|
4575
4799
|
// src/commands/report.ts
|
|
4576
4800
|
import chalk7 from "chalk";
|
|
4577
4801
|
import ora6 from "ora";
|
|
4578
|
-
import
|
|
4802
|
+
import fs12 from "fs";
|
|
4579
4803
|
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
4804
|
var RESULTS_DIR2 = ".qat-results";
|
|
4792
4805
|
function registerReportCommand(program2) {
|
|
4793
4806
|
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 +4837,14 @@ async function executeReport(options) {
|
|
|
4824
4837
|
function collectResults() {
|
|
4825
4838
|
const results = [];
|
|
4826
4839
|
const resultsPath = path13.join(process.cwd(), RESULTS_DIR2);
|
|
4827
|
-
if (!
|
|
4840
|
+
if (!fs12.existsSync(resultsPath)) {
|
|
4828
4841
|
return results;
|
|
4829
4842
|
}
|
|
4830
|
-
const files =
|
|
4843
|
+
const files = fs12.readdirSync(resultsPath).filter((f) => f.endsWith(".json")).sort().reverse();
|
|
4831
4844
|
if (files.length > 0) {
|
|
4832
4845
|
const latestFile = path13.join(resultsPath, files[0]);
|
|
4833
4846
|
try {
|
|
4834
|
-
const data = JSON.parse(
|
|
4847
|
+
const data = JSON.parse(fs12.readFileSync(latestFile, "utf-8"));
|
|
4835
4848
|
if (Array.isArray(data)) {
|
|
4836
4849
|
results.push(...data);
|
|
4837
4850
|
} else if (data.results) {
|
|
@@ -4844,17 +4857,17 @@ function collectResults() {
|
|
|
4844
4857
|
}
|
|
4845
4858
|
function saveResultToHistory(reportData) {
|
|
4846
4859
|
const resultsPath = path13.join(process.cwd(), RESULTS_DIR2);
|
|
4847
|
-
if (!
|
|
4848
|
-
|
|
4860
|
+
if (!fs12.existsSync(resultsPath)) {
|
|
4861
|
+
fs12.mkdirSync(resultsPath, { recursive: true });
|
|
4849
4862
|
}
|
|
4850
4863
|
const timestamp = new Date(reportData.timestamp).toISOString().replace(/[:.]/g, "-");
|
|
4851
4864
|
const fileName = `result-${timestamp}.json`;
|
|
4852
4865
|
const filePath = path13.join(resultsPath, fileName);
|
|
4853
|
-
|
|
4854
|
-
const files =
|
|
4866
|
+
fs12.writeFileSync(filePath, JSON.stringify(reportData, null, 2), "utf-8");
|
|
4867
|
+
const files = fs12.readdirSync(resultsPath).filter((f) => f.startsWith("result-") && f.endsWith(".json")).sort();
|
|
4855
4868
|
while (files.length > 20) {
|
|
4856
4869
|
const oldest = files.shift();
|
|
4857
|
-
|
|
4870
|
+
fs12.unlinkSync(path13.join(resultsPath, oldest));
|
|
4858
4871
|
}
|
|
4859
4872
|
}
|
|
4860
4873
|
function displayReportResult(reportPath, data) {
|
|
@@ -4915,19 +4928,19 @@ import chalk8 from "chalk";
|
|
|
4915
4928
|
import ora7 from "ora";
|
|
4916
4929
|
|
|
4917
4930
|
// src/services/visual.ts
|
|
4918
|
-
import
|
|
4931
|
+
import fs13 from "fs";
|
|
4919
4932
|
import path14 from "path";
|
|
4920
4933
|
import pixelmatch from "pixelmatch";
|
|
4921
4934
|
import { PNG } from "pngjs";
|
|
4922
4935
|
function compareImages(baselinePath, currentPath, diffOutputPath, threshold = 0.1) {
|
|
4923
|
-
if (!
|
|
4936
|
+
if (!fs13.existsSync(baselinePath)) {
|
|
4924
4937
|
throw new Error(`\u57FA\u7EBF\u56FE\u7247\u4E0D\u5B58\u5728: ${baselinePath}`);
|
|
4925
4938
|
}
|
|
4926
|
-
if (!
|
|
4939
|
+
if (!fs13.existsSync(currentPath)) {
|
|
4927
4940
|
throw new Error(`\u5F53\u524D\u56FE\u7247\u4E0D\u5B58\u5728: ${currentPath}`);
|
|
4928
4941
|
}
|
|
4929
|
-
const baseline = PNG.sync.read(
|
|
4930
|
-
const current = PNG.sync.read(
|
|
4942
|
+
const baseline = PNG.sync.read(fs13.readFileSync(baselinePath));
|
|
4943
|
+
const current = PNG.sync.read(fs13.readFileSync(currentPath));
|
|
4931
4944
|
if (baseline.width !== current.width || baseline.height !== current.height) {
|
|
4932
4945
|
return {
|
|
4933
4946
|
passed: false,
|
|
@@ -4956,10 +4969,10 @@ function compareImages(baselinePath, currentPath, diffOutputPath, threshold = 0.
|
|
|
4956
4969
|
let diffPath;
|
|
4957
4970
|
if (diffPixels > 0) {
|
|
4958
4971
|
const diffDir = path14.dirname(diffOutputPath);
|
|
4959
|
-
if (!
|
|
4960
|
-
|
|
4972
|
+
if (!fs13.existsSync(diffDir)) {
|
|
4973
|
+
fs13.mkdirSync(diffDir, { recursive: true });
|
|
4961
4974
|
}
|
|
4962
|
-
|
|
4975
|
+
fs13.writeFileSync(diffOutputPath, PNG.sync.write(diff));
|
|
4963
4976
|
diffPath = diffOutputPath;
|
|
4964
4977
|
}
|
|
4965
4978
|
return {
|
|
@@ -4973,69 +4986,69 @@ function compareImages(baselinePath, currentPath, diffOutputPath, threshold = 0.
|
|
|
4973
4986
|
};
|
|
4974
4987
|
}
|
|
4975
4988
|
function createBaseline(currentPath, baselinePath) {
|
|
4976
|
-
if (!
|
|
4989
|
+
if (!fs13.existsSync(currentPath)) {
|
|
4977
4990
|
throw new Error(`\u5F53\u524D\u622A\u56FE\u4E0D\u5B58\u5728: ${currentPath}`);
|
|
4978
4991
|
}
|
|
4979
4992
|
const baselineDir = path14.dirname(baselinePath);
|
|
4980
|
-
if (!
|
|
4981
|
-
|
|
4993
|
+
if (!fs13.existsSync(baselineDir)) {
|
|
4994
|
+
fs13.mkdirSync(baselineDir, { recursive: true });
|
|
4982
4995
|
}
|
|
4983
|
-
|
|
4996
|
+
fs13.copyFileSync(currentPath, baselinePath);
|
|
4984
4997
|
return baselinePath;
|
|
4985
4998
|
}
|
|
4986
4999
|
function updateAllBaselines(currentDir, baselineDir) {
|
|
4987
5000
|
const updated = [];
|
|
4988
|
-
if (!
|
|
5001
|
+
if (!fs13.existsSync(currentDir)) {
|
|
4989
5002
|
return updated;
|
|
4990
5003
|
}
|
|
4991
|
-
const files =
|
|
5004
|
+
const files = fs13.readdirSync(currentDir).filter((f) => f.endsWith(".png"));
|
|
4992
5005
|
for (const file of files) {
|
|
4993
5006
|
const currentPath = path14.join(currentDir, file);
|
|
4994
5007
|
const baselinePath = path14.join(baselineDir, file);
|
|
4995
5008
|
const baselineDirAbs = path14.dirname(baselinePath);
|
|
4996
|
-
if (!
|
|
4997
|
-
|
|
5009
|
+
if (!fs13.existsSync(baselineDirAbs)) {
|
|
5010
|
+
fs13.mkdirSync(baselineDirAbs, { recursive: true });
|
|
4998
5011
|
}
|
|
4999
|
-
|
|
5012
|
+
fs13.copyFileSync(currentPath, baselinePath);
|
|
5000
5013
|
updated.push(file);
|
|
5001
5014
|
}
|
|
5002
5015
|
return updated;
|
|
5003
5016
|
}
|
|
5004
5017
|
function cleanBaselines(baselineDir) {
|
|
5005
|
-
if (!
|
|
5018
|
+
if (!fs13.existsSync(baselineDir)) {
|
|
5006
5019
|
return 0;
|
|
5007
5020
|
}
|
|
5008
|
-
const files =
|
|
5021
|
+
const files = fs13.readdirSync(baselineDir).filter((f) => f.endsWith(".png"));
|
|
5009
5022
|
let count = 0;
|
|
5010
5023
|
for (const file of files) {
|
|
5011
|
-
|
|
5024
|
+
fs13.unlinkSync(path14.join(baselineDir, file));
|
|
5012
5025
|
count++;
|
|
5013
5026
|
}
|
|
5014
5027
|
return count;
|
|
5015
5028
|
}
|
|
5016
5029
|
function cleanDiffs(diffDir) {
|
|
5017
|
-
if (!
|
|
5030
|
+
if (!fs13.existsSync(diffDir)) {
|
|
5018
5031
|
return 0;
|
|
5019
5032
|
}
|
|
5020
|
-
const files =
|
|
5033
|
+
const files = fs13.readdirSync(diffDir).filter((f) => f.endsWith(".png"));
|
|
5021
5034
|
let count = 0;
|
|
5022
5035
|
for (const file of files) {
|
|
5023
|
-
|
|
5036
|
+
fs13.unlinkSync(path14.join(diffDir, file));
|
|
5024
5037
|
count++;
|
|
5025
5038
|
}
|
|
5026
5039
|
return count;
|
|
5027
5040
|
}
|
|
5028
5041
|
function compareDirectories(baselineDir, currentDir, diffDir, threshold = 0.1) {
|
|
5029
5042
|
const results = [];
|
|
5030
|
-
if (!
|
|
5043
|
+
if (!fs13.existsSync(currentDir)) {
|
|
5031
5044
|
return results;
|
|
5032
5045
|
}
|
|
5033
|
-
const currentFiles =
|
|
5046
|
+
const currentFiles = fs13.readdirSync(currentDir).filter((f) => f.endsWith(".png"));
|
|
5034
5047
|
for (const file of currentFiles) {
|
|
5035
5048
|
const currentPath = path14.join(currentDir, file);
|
|
5036
5049
|
const baselinePath = path14.join(baselineDir, file);
|
|
5037
5050
|
const diffPath = path14.join(diffDir, file);
|
|
5038
|
-
if (!
|
|
5051
|
+
if (!fs13.existsSync(baselinePath)) {
|
|
5039
5052
|
createBaseline(currentPath, baselinePath);
|
|
5040
5053
|
results.push({
|
|
5041
5054
|
passed: true,
|
|
@@ -5067,7 +5080,7 @@ function compareDirectories(baselineDir, currentDir, diffDir, threshold = 0.1) {
|
|
|
5067
5080
|
}
|
|
5068
5081
|
|
|
5069
5082
|
// src/commands/visual.ts
|
|
5070
|
-
import
|
|
5083
|
+
import fs14 from "fs";
|
|
5071
5084
|
import path15 from "path";
|
|
5072
5085
|
function registerVisualCommand(program2) {
|
|
5073
5086
|
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 +5161,7 @@ function findCurrentScreenshotsDir(baselineDir) {
|
|
|
5148
5161
|
path15.join(process.cwd(), baselineDir, "..", "current")
|
|
5149
5162
|
];
|
|
5150
5163
|
for (const dir of possibleDirs) {
|
|
5151
|
-
if (
|
|
5164
|
+
if (fs14.existsSync(dir)) {
|
|
5152
5165
|
const pngs = findPngFiles(dir);
|
|
5153
5166
|
if (pngs.length > 0) return dir;
|
|
5154
5167
|
}
|
|
@@ -5158,8 +5171,8 @@ function findCurrentScreenshotsDir(baselineDir) {
|
|
|
5158
5171
|
function findPngFiles(dir) {
|
|
5159
5172
|
const files = [];
|
|
5160
5173
|
function walk(d) {
|
|
5161
|
-
if (!
|
|
5162
|
-
const entries =
|
|
5174
|
+
if (!fs14.existsSync(d)) return;
|
|
5175
|
+
const entries = fs14.readdirSync(d, { withFileTypes: true });
|
|
5163
5176
|
for (const entry of entries) {
|
|
5164
5177
|
const fullPath = path15.join(d, entry.name);
|
|
5165
5178
|
if (entry.isDirectory() && entry.name !== "node_modules") {
|
|
@@ -5257,7 +5270,7 @@ import chalk9 from "chalk";
|
|
|
5257
5270
|
import inquirer4 from "inquirer";
|
|
5258
5271
|
import ora8 from "ora";
|
|
5259
5272
|
import { execFile as execFile4 } from "child_process";
|
|
5260
|
-
import
|
|
5273
|
+
import fs15 from "fs";
|
|
5261
5274
|
import path16 from "path";
|
|
5262
5275
|
var DEPENDENCY_GROUPS = [
|
|
5263
5276
|
{
|
|
@@ -5292,7 +5305,7 @@ function registerSetupCommand(program2) {
|
|
|
5292
5305
|
async function executeSetup(options) {
|
|
5293
5306
|
console.log(chalk9.cyan("\n QAT \u4F9D\u8D56\u5B89\u88C5\u5668\n"));
|
|
5294
5307
|
const projectInfo = detectProject();
|
|
5295
|
-
if (!
|
|
5308
|
+
if (!fs15.existsSync(path16.join(process.cwd(), "package.json"))) {
|
|
5296
5309
|
throw new Error("\u672A\u627E\u5230 package.json\uFF0C\u8BF7\u5728\u9879\u76EE\u6839\u76EE\u5F55\u6267\u884C\u6B64\u547D\u4EE4");
|
|
5297
5310
|
}
|
|
5298
5311
|
if (projectInfo.frameworkConfidence > 0) {
|
|
@@ -5327,7 +5340,7 @@ async function executeSetup(options) {
|
|
|
5327
5340
|
]);
|
|
5328
5341
|
if (chooseDir !== "root") {
|
|
5329
5342
|
installDir = path16.join(process.cwd(), chooseDir);
|
|
5330
|
-
if (!
|
|
5343
|
+
if (!fs15.existsSync(path16.join(installDir, "package.json"))) {
|
|
5331
5344
|
throw new Error(`${chooseDir} \u4E0B\u6CA1\u6709 package.json`);
|
|
5332
5345
|
}
|
|
5333
5346
|
}
|
|
@@ -5647,7 +5660,7 @@ async function executeChange(_options) {
|
|
|
5647
5660
|
}
|
|
5648
5661
|
|
|
5649
5662
|
// src/cli.ts
|
|
5650
|
-
var VERSION = "0.3.
|
|
5663
|
+
var VERSION = "0.3.02";
|
|
5651
5664
|
function printLogo() {
|
|
5652
5665
|
const logo = `
|
|
5653
5666
|
${chalk12.bold.cyan(" ___ _ _ _ _ _____ _ _ ")}
|