qat-cli 0.2.98 → 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 +1455 -1414
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +440 -267
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +14 -6
- package/dist/index.d.ts +14 -6
- package/dist/index.js +440 -267
- 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}}
|
|
@@ -2066,772 +2311,252 @@ describe('{{name}}', () => {
|
|
|
2066
2311
|
{{/each}}
|
|
2067
2312
|
}{{/if}});
|
|
2068
2313
|
// TODO: verify computed property '{{this}}'
|
|
2069
|
-
// expect(wrapper.vm.{{this}}).toBeDefined();
|
|
2070
|
-
});
|
|
2071
|
-
|
|
2072
|
-
{{/each}}
|
|
2073
|
-
{{/if}}
|
|
2074
|
-
{{#if methods}}
|
|
2075
|
-
{{#each methods}}
|
|
2076
|
-
it('calls {{this}} method', async () => {
|
|
2077
|
-
const wrapper = mountComponent({{#if ../requiredProps}}{
|
|
2078
|
-
{{#each ../requiredProps}}
|
|
2079
|
-
{{name}}: {{propTestValue this}},
|
|
2080
|
-
{{/each}}
|
|
2081
|
-
}{{/if}});
|
|
2082
|
-
// TODO: test method '{{this}}'
|
|
2083
|
-
// await wrapper.vm.{{this}}();
|
|
2084
|
-
// expect(someCondition).toBe(true);
|
|
2085
|
-
});
|
|
2086
|
-
|
|
2087
|
-
{{/each}}
|
|
2088
|
-
{{/if}}
|
|
2089
|
-
{{else}}
|
|
2090
|
-
it('renders correctly', () => {
|
|
2091
|
-
const wrapper = mount({{pascalName}}{{#if mountOptions}}, {{mountOptions}}{{/if}});
|
|
2092
|
-
expect(wrapper.exists()).toBe(true);
|
|
2093
|
-
});
|
|
2094
|
-
|
|
2095
|
-
it('renders default slot content', () => {
|
|
2096
|
-
const wrapper = mount({{pascalName}}, {
|
|
2097
|
-
{{#if mountOptions}}
|
|
2098
|
-
...{{mountOptions}},
|
|
2099
|
-
{{/if}}
|
|
2100
|
-
slots: {
|
|
2101
|
-
default: 'Test Content',
|
|
2102
|
-
},
|
|
2103
|
-
});
|
|
2104
|
-
expect(wrapper.text()).toContain('Test Content');
|
|
2105
|
-
});
|
|
2106
|
-
|
|
2107
|
-
it('handles user interaction', async () => {
|
|
2108
|
-
const wrapper = mount({{pascalName}}{{#if mountOptions}}, {{mountOptions}}{{/if}});
|
|
2109
|
-
// TODO: add interaction test
|
|
2110
|
-
});
|
|
2111
|
-
{{/if}}
|
|
2112
|
-
});
|
|
2113
|
-
`,
|
|
2114
|
-
e2e: `import { test, expect } from '@playwright/test';
|
|
2115
|
-
|
|
2116
|
-
test.describe('{{name}}', () => {
|
|
2117
|
-
test.beforeEach(async ({ page }) => {
|
|
2118
|
-
await page.goto('/');
|
|
2119
|
-
});
|
|
2120
|
-
|
|
2121
|
-
test('should display page correctly', async ({ page }) => {
|
|
2122
|
-
// TODO: \u6DFB\u52A0E2E\u6D4B\u8BD5\u65AD\u8A00
|
|
2123
|
-
await expect(page).toHaveTitle(/.*/);
|
|
2124
|
-
});
|
|
2125
|
-
|
|
2126
|
-
test('should navigate between pages', async ({ page }) => {
|
|
2127
|
-
// TODO: \u6DFB\u52A0\u5BFC\u822A\u6D4B\u8BD5
|
|
2128
|
-
});
|
|
2129
|
-
|
|
2130
|
-
test('should be responsive', async ({ page }) => {
|
|
2131
|
-
await page.setViewportSize({ width: 375, height: 667 });
|
|
2132
|
-
// TODO: \u6DFB\u52A0\u79FB\u52A8\u7AEF\u65AD\u8A00
|
|
2133
|
-
});
|
|
2134
|
-
});
|
|
2135
|
-
`,
|
|
2136
|
-
api: `import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
2137
|
-
{{#if imports}}
|
|
2138
|
-
{{#each imports}}
|
|
2139
|
-
import {{this}};
|
|
2140
|
-
{{/each}}
|
|
2141
|
-
{{/if}}
|
|
2142
|
-
|
|
2143
|
-
describe('{{name}} API', () => {
|
|
2144
|
-
const baseUrl = process.env.API_BASE_URL || 'http://localhost:3456';
|
|
2145
|
-
|
|
2146
|
-
it('should return 200 for GET request', async () => {
|
|
2147
|
-
const response = await fetch(\`\${baseUrl}/api/{{camelName}}\`);
|
|
2148
|
-
expect(response.status).toBe(200);
|
|
2149
|
-
});
|
|
2150
|
-
|
|
2151
|
-
it('should return expected response format', async () => {
|
|
2152
|
-
const response = await fetch(\`\${baseUrl}/api/{{camelName}}\`);
|
|
2153
|
-
const data = await response.json();
|
|
2154
|
-
expect(data).toBeDefined();
|
|
2155
|
-
});
|
|
2156
|
-
|
|
2157
|
-
it('should handle error responses', async () => {
|
|
2158
|
-
const response = await fetch(\`\${baseUrl}/api/{{camelName}}/nonexistent\`, {
|
|
2159
|
-
method: 'GET',
|
|
2160
|
-
});
|
|
2161
|
-
expect(response.status).toBeGreaterThanOrEqual(400);
|
|
2162
|
-
});
|
|
2163
|
-
});
|
|
2164
|
-
`,
|
|
2165
|
-
visual: `import { test, expect } from '@playwright/test';
|
|
2166
|
-
|
|
2167
|
-
test.describe('{{name}} visual regression', () => {
|
|
2168
|
-
test.beforeEach(async ({ page }) => {
|
|
2169
|
-
await page.goto('/');
|
|
2170
|
-
});
|
|
2171
|
-
|
|
2172
|
-
test('should match baseline screenshot - desktop', async ({ page }) => {
|
|
2173
|
-
await expect(page).toHaveScreenshot('{{name}}-desktop.png');
|
|
2174
|
-
});
|
|
2175
|
-
|
|
2176
|
-
test('should match baseline screenshot - mobile', async ({ page }) => {
|
|
2177
|
-
await page.setViewportSize({ width: 375, height: 667 });
|
|
2178
|
-
await expect(page).toHaveScreenshot('{{name}}-mobile.png');
|
|
2179
|
-
});
|
|
2180
|
-
});
|
|
2181
|
-
`,
|
|
2182
|
-
performance: `import { test, expect } from '@playwright/test';
|
|
2183
|
-
|
|
2184
|
-
test.describe('{{name}} performance', () => {
|
|
2185
|
-
test('should meet Core Web Vitals thresholds', async ({ page }) => {
|
|
2186
|
-
await page.goto('/');
|
|
2187
|
-
|
|
2188
|
-
// \u6D4B\u91CF\u9875\u9762\u52A0\u8F7D\u6027\u80FD
|
|
2189
|
-
const performanceMetrics = await page.evaluate(() => {
|
|
2190
|
-
const [entry] = performance.getEntriesByType('navigation') as PerformanceNavigationTiming[];
|
|
2191
|
-
return {
|
|
2192
|
-
domContentLoaded: entry?.domContentLoadedEventEnd - entry?.domContentLoadedEventStart ?? 0,
|
|
2193
|
-
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);
|
|
2314
|
+
// expect(wrapper.vm.{{this}}).toBeDefined();
|
|
2200
2315
|
});
|
|
2201
2316
|
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2317
|
+
{{/each}}
|
|
2318
|
+
{{/if}}
|
|
2319
|
+
{{#if methods}}
|
|
2320
|
+
{{#each methods}}
|
|
2321
|
+
it('calls {{this}} method', async () => {
|
|
2322
|
+
const wrapper = mountComponent({{#if ../requiredProps}}{
|
|
2323
|
+
{{#each ../requiredProps}}
|
|
2324
|
+
{{name}}: {{propTestValue this}},
|
|
2325
|
+
{{/each}}
|
|
2326
|
+
}{{/if}});
|
|
2327
|
+
// TODO: test method '{{this}}'
|
|
2328
|
+
// await wrapper.vm.{{this}}();
|
|
2329
|
+
// expect(someCondition).toBe(true);
|
|
2207
2330
|
});
|
|
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
|
-
}
|
|
2264
2331
|
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
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
|
-
}
|
|
2332
|
+
{{/each}}
|
|
2333
|
+
{{/if}}
|
|
2334
|
+
{{else}}
|
|
2335
|
+
it('renders correctly', () => {
|
|
2336
|
+
const wrapper = mount({{pascalName}}{{#if mountOptions}}, {{mountOptions}}{{/if}});
|
|
2337
|
+
expect(wrapper.exists()).toBe(true);
|
|
2300
2338
|
});
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
}
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
}
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
}
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
const
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
}
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
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
|
-
}
|
|
2339
|
+
|
|
2340
|
+
it('renders default slot content', () => {
|
|
2341
|
+
const wrapper = mount({{pascalName}}, {
|
|
2342
|
+
{{#if mountOptions}}
|
|
2343
|
+
...{{mountOptions}},
|
|
2344
|
+
{{/if}}
|
|
2345
|
+
slots: {
|
|
2346
|
+
default: 'Test Content',
|
|
2347
|
+
},
|
|
2348
|
+
});
|
|
2349
|
+
expect(wrapper.text()).toContain('Test Content');
|
|
2350
|
+
});
|
|
2351
|
+
|
|
2352
|
+
it('handles user interaction', async () => {
|
|
2353
|
+
const wrapper = mount({{pascalName}}{{#if mountOptions}}, {{mountOptions}}{{/if}});
|
|
2354
|
+
// TODO: add interaction test
|
|
2355
|
+
});
|
|
2356
|
+
{{/if}}
|
|
2357
|
+
});
|
|
2358
|
+
`,
|
|
2359
|
+
e2e: `import { test, expect } from '@playwright/test';
|
|
2360
|
+
|
|
2361
|
+
test.describe('{{name}}', () => {
|
|
2362
|
+
test.beforeEach(async ({ page }) => {
|
|
2363
|
+
await page.goto('/');
|
|
2364
|
+
});
|
|
2365
|
+
|
|
2366
|
+
test('should display page correctly', async ({ page }) => {
|
|
2367
|
+
// TODO: \u6DFB\u52A0E2E\u6D4B\u8BD5\u65AD\u8A00
|
|
2368
|
+
await expect(page).toHaveTitle(/.*/);
|
|
2369
|
+
});
|
|
2370
|
+
|
|
2371
|
+
test('should navigate between pages', async ({ page }) => {
|
|
2372
|
+
// TODO: \u6DFB\u52A0\u5BFC\u822A\u6D4B\u8BD5
|
|
2373
|
+
});
|
|
2374
|
+
|
|
2375
|
+
test('should be responsive', async ({ page }) => {
|
|
2376
|
+
await page.setViewportSize({ width: 375, height: 667 });
|
|
2377
|
+
// TODO: \u6DFB\u52A0\u79FB\u52A8\u7AEF\u65AD\u8A00
|
|
2378
|
+
});
|
|
2379
|
+
});
|
|
2380
|
+
`,
|
|
2381
|
+
api: `import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
2382
|
+
{{#if imports}}
|
|
2383
|
+
{{#each imports}}
|
|
2384
|
+
import {{this}};
|
|
2385
|
+
{{/each}}
|
|
2386
|
+
{{/if}}
|
|
2387
|
+
|
|
2388
|
+
describe('{{name}} API', () => {
|
|
2389
|
+
const baseUrl = process.env.API_BASE_URL || 'http://localhost:3456';
|
|
2390
|
+
|
|
2391
|
+
it('should return 200 for GET request', async () => {
|
|
2392
|
+
const response = await fetch(\`\${baseUrl}/api/{{camelName}}\`);
|
|
2393
|
+
expect(response.status).toBe(200);
|
|
2394
|
+
});
|
|
2395
|
+
|
|
2396
|
+
it('should return expected response format', async () => {
|
|
2397
|
+
const response = await fetch(\`\${baseUrl}/api/{{camelName}}\`);
|
|
2398
|
+
const data = await response.json();
|
|
2399
|
+
expect(data).toBeDefined();
|
|
2400
|
+
});
|
|
2401
|
+
|
|
2402
|
+
it('should handle error responses', async () => {
|
|
2403
|
+
const response = await fetch(\`\${baseUrl}/api/{{camelName}}/nonexistent\`, {
|
|
2404
|
+
method: 'GET',
|
|
2405
|
+
});
|
|
2406
|
+
expect(response.status).toBeGreaterThanOrEqual(400);
|
|
2407
|
+
});
|
|
2408
|
+
});
|
|
2409
|
+
`,
|
|
2410
|
+
visual: `import { test, expect } from '@playwright/test';
|
|
2411
|
+
|
|
2412
|
+
test.describe('{{name}} visual regression', () => {
|
|
2413
|
+
test.beforeEach(async ({ page }) => {
|
|
2414
|
+
await page.goto('/');
|
|
2415
|
+
});
|
|
2416
|
+
|
|
2417
|
+
test('should match baseline screenshot - desktop', async ({ page }) => {
|
|
2418
|
+
await expect(page).toHaveScreenshot('{{name}}-desktop.png');
|
|
2419
|
+
});
|
|
2420
|
+
|
|
2421
|
+
test('should match baseline screenshot - mobile', async ({ page }) => {
|
|
2422
|
+
await page.setViewportSize({ width: 375, height: 667 });
|
|
2423
|
+
await expect(page).toHaveScreenshot('{{name}}-mobile.png');
|
|
2424
|
+
});
|
|
2425
|
+
});
|
|
2426
|
+
`,
|
|
2427
|
+
performance: `import { test, expect } from '@playwright/test';
|
|
2428
|
+
|
|
2429
|
+
test.describe('{{name}} performance', () => {
|
|
2430
|
+
test('should meet Core Web Vitals thresholds', async ({ page }) => {
|
|
2431
|
+
await page.goto('/');
|
|
2432
|
+
|
|
2433
|
+
// \u6D4B\u91CF\u9875\u9762\u52A0\u8F7D\u6027\u80FD
|
|
2434
|
+
const performanceMetrics = await page.evaluate(() => {
|
|
2435
|
+
const [entry] = performance.getEntriesByType('navigation') as PerformanceNavigationTiming[];
|
|
2436
|
+
return {
|
|
2437
|
+
domContentLoaded: entry?.domContentLoadedEventEnd - entry?.domContentLoadedEventStart ?? 0,
|
|
2438
|
+
loadComplete: entry?.loadEventEnd - entry?.loadEventStart ?? 0,
|
|
2439
|
+
domInteractive: entry?.domInteractive ?? 0,
|
|
2440
|
+
};
|
|
2441
|
+
});
|
|
2442
|
+
|
|
2443
|
+
// DOM \u5185\u5BB9\u52A0\u8F7D\u5E94\u5728 2s \u5185
|
|
2444
|
+
expect(performanceMetrics.domInteractive).toBeLessThan(2000);
|
|
2445
|
+
});
|
|
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;
|
|
2458
|
+
function toCamelCase(str) {
|
|
2459
|
+
return str.replace(/[-_\s]+(.)?/g, (_, c) => c ? c.toUpperCase() : "").replace(/^[A-Z]/, (c) => c.toLowerCase());
|
|
2552
2460
|
}
|
|
2553
|
-
|
|
2554
|
-
const
|
|
2555
|
-
|
|
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;
|
|
2461
|
+
function toPascalCase(str) {
|
|
2462
|
+
const camel = toCamelCase(str);
|
|
2463
|
+
return camel.charAt(0).toUpperCase() + camel.slice(1);
|
|
2617
2464
|
}
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
}
|
|
2631
|
-
} catch {
|
|
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");
|
|
2632
2477
|
}
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
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
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
filePath: path7.relative(process.cwd(), filePath).replace(/\\/g, "/"),
|
|
2688
|
-
reviewEntry
|
|
2689
|
-
};
|
|
2690
|
-
}
|
|
2691
|
-
async function generateWithAIAndReview(type, name, target, aiConfig, projectInfo) {
|
|
2692
|
-
const fullPath = path7.resolve(process.cwd(), target);
|
|
2693
|
-
let sourceCode = "";
|
|
2694
|
-
if (fs7.existsSync(fullPath)) {
|
|
2695
|
-
sourceCode = fs7.readFileSync(fullPath, "utf-8");
|
|
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
|
|
2499
|
+
context: i === 0 ? sourceCode : `${sourceCode}
|
|
2500
|
+
|
|
2501
|
+
--- \u5BA1\u8BA1\u53CD\u9988 ---
|
|
2502
|
+
${generationPrompt}`,
|
|
2503
|
+
framework: framework || void 0,
|
|
2504
|
+
analysis
|
|
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
|
+
}
|
|
2522
|
+
}
|
|
2523
|
+
return {
|
|
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,83 +2981,228 @@ function buildVitestArgs(options) {
|
|
|
3245
2981
|
return args;
|
|
3246
2982
|
}
|
|
3247
2983
|
async function execVitest(args) {
|
|
2984
|
+
const tmpFile = path9.join(os2.tmpdir(), `qat-vitest-result-${Date.now()}.json`);
|
|
2985
|
+
const argsWithOutput = [...args, "--outputFile", tmpFile];
|
|
3248
2986
|
return new Promise((resolve, reject) => {
|
|
3249
2987
|
const npx = process.platform === "win32" ? "npx.cmd" : "npx";
|
|
3250
|
-
|
|
2988
|
+
debug("vitest", "\u6267\u884C\u547D\u4EE4:", npx, argsWithOutput.join(" "));
|
|
2989
|
+
const child = execFile(npx, argsWithOutput, {
|
|
3251
2990
|
cwd: process.cwd(),
|
|
3252
|
-
env: { ...process.env, FORCE_COLOR: "0" },
|
|
2991
|
+
env: { ...process.env, FORCE_COLOR: "0", NO_COLOR: "1" },
|
|
3253
2992
|
maxBuffer: 50 * 1024 * 1024,
|
|
3254
|
-
// 50MB
|
|
3255
2993
|
shell: true
|
|
3256
2994
|
}, (error, stdout, stderr) => {
|
|
3257
|
-
const
|
|
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}`);
|
|
2999
|
+
let jsonResult = null;
|
|
3258
3000
|
try {
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
} else if (error && error.message.includes("ENOENT")) {
|
|
3265
|
-
reject(new Error("\u672A\u627E\u5230 vitest\uFF0C\u8BF7\u786E\u4FDD\u5DF2\u5B89\u88C5: npm install -D vitest"));
|
|
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);
|
|
3266
3006
|
} else {
|
|
3267
|
-
|
|
3007
|
+
debug("vitest", "\u4E34\u65F6\u6587\u4EF6\u4E0D\u5B58\u5728:", tmpFile);
|
|
3008
|
+
}
|
|
3009
|
+
} catch (e) {
|
|
3010
|
+
debug("vitest", "\u4E34\u65F6\u6587\u4EF6\u8BFB\u53D6\u5931\u8D25:", e instanceof Error ? e.message : String(e));
|
|
3011
|
+
}
|
|
3012
|
+
if (jsonResult) {
|
|
3013
|
+
try {
|
|
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" });
|
|
3017
|
+
return;
|
|
3018
|
+
} catch (e) {
|
|
3019
|
+
debug("vitest", "\u4E34\u65F6\u6587\u4EF6 JSON \u89E3\u6790\u5931\u8D25:", e instanceof Error ? e.message : String(e));
|
|
3268
3020
|
}
|
|
3269
3021
|
}
|
|
3022
|
+
debug("vitest", "\u5C1D\u8BD5\u4ECE stdout \u63D0\u53D6 JSON...");
|
|
3023
|
+
try {
|
|
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;
|
|
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" });
|
|
3270
3044
|
});
|
|
3271
3045
|
child.on("error", (err) => {
|
|
3046
|
+
try {
|
|
3047
|
+
fs9.unlinkSync(tmpFile);
|
|
3048
|
+
} catch {
|
|
3049
|
+
}
|
|
3272
3050
|
reject(new Error(`Vitest \u6267\u884C\u5931\u8D25: ${err.message}`));
|
|
3273
3051
|
});
|
|
3274
3052
|
});
|
|
3275
3053
|
}
|
|
3276
|
-
function
|
|
3277
|
-
const
|
|
3278
|
-
|
|
3279
|
-
|
|
3054
|
+
function parseVitestJSON(jsonStr) {
|
|
3055
|
+
const data = JSON.parse(jsonStr);
|
|
3056
|
+
const suites = [];
|
|
3057
|
+
debug("vitest-json", "JSON \u9876\u5C42\u5B57\u6BB5:", Object.keys(data).join(", "));
|
|
3058
|
+
if (data.testResults && Array.isArray(data.testResults)) {
|
|
3059
|
+
debug("vitest-json", `testResults \u6570\u91CF: ${data.testResults.length}`);
|
|
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
|
+
}
|
|
3280
3071
|
}
|
|
3281
|
-
|
|
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) {
|
|
3086
|
+
const suiteTests = [];
|
|
3087
|
+
for (const test of suiteData.tests || []) {
|
|
3088
|
+
suiteTests.push({
|
|
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,
|
|
3094
|
+
retries: 0
|
|
3095
|
+
});
|
|
3096
|
+
}
|
|
3097
|
+
suites.push({
|
|
3098
|
+
name: suiteData.name || "unknown",
|
|
3099
|
+
file: suiteData.file || "unknown",
|
|
3100
|
+
type: "unit",
|
|
3101
|
+
status: suiteTests.some((t) => t.status === "failed") ? "failed" : "passed",
|
|
3102
|
+
duration: 0,
|
|
3103
|
+
tests: suiteTests
|
|
3104
|
+
});
|
|
3105
|
+
}
|
|
3106
|
+
}
|
|
3107
|
+
let coverage;
|
|
3108
|
+
if (data.coverageMap) {
|
|
3109
|
+
coverage = extractCoverage(data.coverageMap);
|
|
3110
|
+
}
|
|
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;
|
|
3126
|
+
return { success, suites, coverage };
|
|
3127
|
+
}
|
|
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
|
+
});
|
|
3140
|
+
}
|
|
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) {
|
|
3282
3169
|
const data = JSON.parse(jsonMatch[0]);
|
|
3283
3170
|
const suites = [];
|
|
3284
3171
|
if (data.testResults && Array.isArray(data.testResults)) {
|
|
3285
3172
|
for (const fileResult of data.testResults) {
|
|
3286
|
-
|
|
3287
|
-
name: path9.basename(fileResult.name ||
|
|
3173
|
+
suites.push({
|
|
3174
|
+
name: path9.basename(fileResult.name || "unknown"),
|
|
3288
3175
|
file: fileResult.name || "unknown",
|
|
3289
3176
|
type: "unit",
|
|
3290
3177
|
status: mapVitestStatus(fileResult.status),
|
|
3291
3178
|
duration: fileResult.duration || 0,
|
|
3292
|
-
tests: (fileResult
|
|
3293
|
-
|
|
3294
|
-
file: fileResult.name || "unknown",
|
|
3295
|
-
status: mapVitestStatus(assertion.status),
|
|
3296
|
-
duration: assertion.duration || 0,
|
|
3297
|
-
error: assertion.failureMessages?.length ? { message: assertion.failureMessages[0] } : void 0,
|
|
3298
|
-
retries: 0
|
|
3299
|
-
}))
|
|
3300
|
-
};
|
|
3301
|
-
suites.push(suite);
|
|
3179
|
+
tests: parseTestResults(fileResult)
|
|
3180
|
+
});
|
|
3302
3181
|
}
|
|
3303
3182
|
}
|
|
3304
|
-
let coverage;
|
|
3305
|
-
if (data.coverageMap) {
|
|
3306
|
-
coverage = extractCoverage(data.coverageMap);
|
|
3307
|
-
}
|
|
3308
3183
|
const success = data.success !== false && suites.every((s) => s.status !== "failed");
|
|
3184
|
+
let coverage;
|
|
3185
|
+
if (data.coverageMap) coverage = extractCoverage(data.coverageMap);
|
|
3309
3186
|
return { success, suites, coverage };
|
|
3310
|
-
} catch {
|
|
3311
|
-
return parseVitestTextOutput(output, false);
|
|
3312
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");
|
|
3313
3193
|
}
|
|
3314
3194
|
function parseVitestTextOutput(output, hasError) {
|
|
3315
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");
|
|
3316
3199
|
let totalPassed = 0;
|
|
3317
3200
|
let totalFailed = 0;
|
|
3318
|
-
const suiteRegex = /[✓✗×]\s+(.+\.test\.ts|.+\.spec\.ts)\s*\((\d+)\s+test/i;
|
|
3319
|
-
const lines = output.split("\n");
|
|
3320
3201
|
for (const line of lines) {
|
|
3321
3202
|
const match = line.match(suiteRegex);
|
|
3322
3203
|
if (match) {
|
|
3323
3204
|
const file = match[1];
|
|
3324
|
-
const testCount = parseInt(match[
|
|
3205
|
+
const testCount = parseInt(match[4], 10);
|
|
3325
3206
|
const isPassed = line.includes("\u2713");
|
|
3326
3207
|
if (isPassed) totalPassed += testCount;
|
|
3327
3208
|
else totalFailed += testCount;
|
|
@@ -3341,15 +3222,39 @@ function parseVitestTextOutput(output, hasError) {
|
|
|
3341
3222
|
});
|
|
3342
3223
|
}
|
|
3343
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,
|
|
3240
|
+
duration: 0,
|
|
3241
|
+
retries: 0
|
|
3242
|
+
}))
|
|
3243
|
+
});
|
|
3244
|
+
}
|
|
3245
|
+
}
|
|
3246
|
+
debug("vitest-text", `\u89E3\u6790\u5230 ${suites.length} \u4E2A\u5957\u4EF6, ${totalPassed} \u901A\u8FC7, ${totalFailed} \u5931\u8D25`);
|
|
3344
3247
|
return {
|
|
3345
3248
|
success: !hasError || totalFailed === 0,
|
|
3346
3249
|
suites
|
|
3347
3250
|
};
|
|
3348
3251
|
}
|
|
3349
3252
|
function mapVitestStatus(status) {
|
|
3253
|
+
if (!status) return "pending";
|
|
3350
3254
|
switch (status) {
|
|
3351
3255
|
case "passed":
|
|
3352
3256
|
case "pass":
|
|
3257
|
+
case "done":
|
|
3353
3258
|
return "passed";
|
|
3354
3259
|
case "failed":
|
|
3355
3260
|
case "fail":
|
|
@@ -3357,6 +3262,7 @@ function mapVitestStatus(status) {
|
|
|
3357
3262
|
case "skipped":
|
|
3358
3263
|
case "skip":
|
|
3359
3264
|
case "pending":
|
|
3265
|
+
case "todo":
|
|
3360
3266
|
return "skipped";
|
|
3361
3267
|
default:
|
|
3362
3268
|
return "pending";
|
|
@@ -3801,43 +3707,260 @@ function makeTestResult(name, value, threshold, isLowerBetter = false) {
|
|
|
3801
3707
|
}
|
|
3802
3708
|
}
|
|
3803
3709
|
return {
|
|
3804
|
-
name,
|
|
3805
|
-
file: "lighthouse",
|
|
3806
|
-
status,
|
|
3807
|
-
duration: 0,
|
|
3808
|
-
error,
|
|
3809
|
-
retries: 0
|
|
3710
|
+
name,
|
|
3711
|
+
file: "lighthouse",
|
|
3712
|
+
status,
|
|
3713
|
+
duration: 0,
|
|
3714
|
+
error,
|
|
3715
|
+
retries: 0
|
|
3716
|
+
};
|
|
3717
|
+
}
|
|
3718
|
+
function calculateAverageScores(results) {
|
|
3719
|
+
const valid = results.filter((r) => !r.error);
|
|
3720
|
+
if (valid.length === 0) {
|
|
3721
|
+
return { performance: 0, accessibility: 0, bestPractices: 0, seo: 0 };
|
|
3722
|
+
}
|
|
3723
|
+
return {
|
|
3724
|
+
performance: Math.round(valid.reduce((s, r) => s + r.scores.performance, 0) / valid.length),
|
|
3725
|
+
accessibility: Math.round(valid.reduce((s, r) => s + r.scores.accessibility, 0) / valid.length),
|
|
3726
|
+
bestPractices: Math.round(valid.reduce((s, r) => s + r.scores.bestPractices, 0) / valid.length),
|
|
3727
|
+
seo: Math.round(valid.reduce((s, r) => s + r.scores.seo, 0) / valid.length)
|
|
3728
|
+
};
|
|
3729
|
+
}
|
|
3730
|
+
function calculateAverageMetrics(results) {
|
|
3731
|
+
const valid = results.filter((r) => !r.error);
|
|
3732
|
+
const scores = calculateAverageScores(results);
|
|
3733
|
+
const lcpValues = valid.map((r) => r.metrics.lcp).filter((v) => v !== void 0);
|
|
3734
|
+
const fidValues = valid.map((r) => r.metrics.fid).filter((v) => v !== void 0);
|
|
3735
|
+
const clsValues = valid.map((r) => r.metrics.cls).filter((v) => v !== void 0);
|
|
3736
|
+
return {
|
|
3737
|
+
...scores,
|
|
3738
|
+
lcp: lcpValues.length > 0 ? lcpValues.reduce((s, v) => s + v, 0) / lcpValues.length : void 0,
|
|
3739
|
+
fid: fidValues.length > 0 ? fidValues.reduce((s, v) => s + v, 0) / fidValues.length : void 0,
|
|
3740
|
+
cls: clsValues.length > 0 ? clsValues.reduce((s, v) => s + v, 0) / clsValues.length : void 0
|
|
3741
|
+
};
|
|
3742
|
+
}
|
|
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
|
|
3810
3799
|
};
|
|
3811
3800
|
}
|
|
3812
|
-
function
|
|
3813
|
-
|
|
3814
|
-
if (
|
|
3815
|
-
|
|
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
|
+
}
|
|
3816
3934
|
}
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
seo: Math.round(valid.reduce((s, r) => s + r.scores.seo, 0) / valid.length)
|
|
3822
|
-
};
|
|
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");
|
|
3823
3939
|
}
|
|
3824
|
-
function
|
|
3825
|
-
const
|
|
3826
|
-
const
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
};
|
|
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;
|
|
3836
3951
|
}
|
|
3837
3952
|
|
|
3838
3953
|
// src/commands/run.ts
|
|
3839
3954
|
var RESULTS_DIR = ".qat-results";
|
|
3840
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
|
+
};
|
|
3841
3964
|
var TYPE_RUNNERS = {
|
|
3842
3965
|
unit: "Vitest",
|
|
3843
3966
|
component: "Vitest",
|
|
@@ -3846,7 +3969,7 @@ var TYPE_RUNNERS = {
|
|
|
3846
3969
|
visual: "Playwright",
|
|
3847
3970
|
performance: "Lighthouse"
|
|
3848
3971
|
};
|
|
3849
|
-
var
|
|
3972
|
+
var TYPE_LABELS2 = {
|
|
3850
3973
|
unit: "\u5355\u5143\u6D4B\u8BD5",
|
|
3851
3974
|
component: "\u7EC4\u4EF6\u6D4B\u8BD5",
|
|
3852
3975
|
e2e: "E2E\u6D4B\u8BD5",
|
|
@@ -3882,6 +4005,61 @@ async function executeRun(options) {
|
|
|
3882
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"));
|
|
3883
4006
|
return;
|
|
3884
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
|
+
}
|
|
3885
4063
|
const serverNeededTypes = typesToRun.filter((t) => SERVER_REQUIRED_TYPES.includes(t));
|
|
3886
4064
|
if (serverNeededTypes.length > 0) {
|
|
3887
4065
|
const serverOk = await checkDevServer(config);
|
|
@@ -3890,7 +4068,7 @@ async function executeRun(options) {
|
|
|
3890
4068
|
{
|
|
3891
4069
|
type: "list",
|
|
3892
4070
|
name: "action",
|
|
3893
|
-
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`,
|
|
3894
4072
|
choices: [
|
|
3895
4073
|
{ name: "\u81EA\u52A8\u542F\u52A8 dev server \u5E76\u8FD0\u884C", value: "start" },
|
|
3896
4074
|
{ name: "\u8DF3\u8FC7\u8FD9\u4E9B\u6D4B\u8BD5\u7C7B\u578B\uFF0C\u4EC5\u8FD0\u884C\u5176\u4ED6\u6D4B\u8BD5", value: "skip" },
|
|
@@ -3971,7 +4149,7 @@ async function executeRun(options) {
|
|
|
3971
4149
|
spinner.stop();
|
|
3972
4150
|
} else {
|
|
3973
4151
|
for (const testType of typesToRun) {
|
|
3974
|
-
const label =
|
|
4152
|
+
const label = TYPE_LABELS2[testType] || testType;
|
|
3975
4153
|
const spinner = ora4(`\u6B63\u5728\u8FD0\u884C ${label}...`).start();
|
|
3976
4154
|
try {
|
|
3977
4155
|
const result = await runTestType(testType, config, options);
|
|
@@ -3983,14 +4161,25 @@ async function executeRun(options) {
|
|
|
3983
4161
|
}
|
|
3984
4162
|
}
|
|
3985
4163
|
}
|
|
3986
|
-
|
|
4164
|
+
displayJestStyleResults(results);
|
|
3987
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
|
+
}
|
|
3988
4177
|
}
|
|
3989
4178
|
function printDryRunCommands(types, options, config) {
|
|
3990
4179
|
console.log();
|
|
3991
4180
|
console.log(chalk5.cyan(" \u53EF\u6267\u884C\u7684\u6D4B\u8BD5\u547D\u4EE4:\n"));
|
|
3992
4181
|
for (const testType of types) {
|
|
3993
|
-
const label =
|
|
4182
|
+
const label = TYPE_LABELS2[testType];
|
|
3994
4183
|
const runner = TYPE_RUNNERS[testType];
|
|
3995
4184
|
let cmd;
|
|
3996
4185
|
switch (runner) {
|
|
@@ -4058,7 +4247,7 @@ async function determineTypesToRun(type, file, config) {
|
|
|
4058
4247
|
name: "selectedTypes",
|
|
4059
4248
|
message: "\u9009\u62E9\u8981\u8FD0\u884C\u7684\u6D4B\u8BD5\u7C7B\u578B (\u7A7A\u683C\u9009\u62E9/\u53D6\u6D88\uFF0C\u56DE\u8F66\u786E\u8BA4):",
|
|
4060
4249
|
choices: enabledTypes.map((t) => ({
|
|
4061
|
-
name: `${
|
|
4250
|
+
name: `${TYPE_LABELS2[t]} (${chalk5.gray(TYPE_RUNNERS[t])})`,
|
|
4062
4251
|
value: t,
|
|
4063
4252
|
checked: true
|
|
4064
4253
|
})),
|
|
@@ -4192,53 +4381,82 @@ function suiteStatusToSpinner(status) {
|
|
|
4192
4381
|
return "warn";
|
|
4193
4382
|
}
|
|
4194
4383
|
}
|
|
4195
|
-
|
|
4196
|
-
|
|
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
|
+
}
|
|
4197
4404
|
let totalPassed = 0;
|
|
4198
4405
|
let totalFailed = 0;
|
|
4199
4406
|
let totalSkipped = 0;
|
|
4200
4407
|
let totalDuration = 0;
|
|
4408
|
+
const typeStats = {};
|
|
4201
4409
|
for (const result of results) {
|
|
4202
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;
|
|
4203
4415
|
for (const suite of result.suites) {
|
|
4204
|
-
totalSuites++;
|
|
4205
4416
|
for (const test of suite.tests) {
|
|
4206
|
-
if (test.status === "passed")
|
|
4207
|
-
|
|
4208
|
-
|
|
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
|
+
}
|
|
4209
4427
|
}
|
|
4210
4428
|
}
|
|
4211
4429
|
}
|
|
4212
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"));
|
|
4213
4434
|
console.log();
|
|
4214
|
-
console.log(chalk5.
|
|
4215
|
-
|
|
4216
|
-
|
|
4217
|
-
|
|
4218
|
-
|
|
4219
|
-
|
|
4220
|
-
|
|
4221
|
-
}
|
|
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)}`);
|
|
4222
4448
|
console.log();
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
|
|
4227
|
-
|
|
4228
|
-
|
|
4229
|
-
if (results.length > 1) {
|
|
4230
|
-
console.log();
|
|
4231
|
-
for (const result of results) {
|
|
4232
|
-
const label = TYPE_LABELS[result.type] || result.type;
|
|
4233
|
-
const icon = result.status === "passed" ? chalk5.green("\u2713") : result.status === "failed" ? chalk5.red("\u2717") : chalk5.yellow("\u26A0");
|
|
4234
|
-
const testCount = result.suites.reduce((sum, s) => sum + s.tests.length, 0);
|
|
4235
|
-
console.log(` ${icon} ${label} (${testCount} tests, ${formatDuration(result.duration)})`);
|
|
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();
|
|
4236
4455
|
}
|
|
4237
4456
|
}
|
|
4238
4457
|
for (const result of results) {
|
|
4239
4458
|
if (result.type === "performance" && result.performance) {
|
|
4240
4459
|
const p = result.performance;
|
|
4241
|
-
console.log();
|
|
4242
4460
|
console.log(chalk5.cyan(" \u6027\u80FD\u6307\u6807:"));
|
|
4243
4461
|
console.log(` Performance: ${scoreColor(p.performance)} ${p.performance}/100`);
|
|
4244
4462
|
console.log(` Accessibility: ${scoreColor(p.accessibility)} ${p.accessibility}/100`);
|
|
@@ -4247,6 +4465,7 @@ async function displaySummary(results, config) {
|
|
|
4247
4465
|
if (p.lcp !== void 0) console.log(` LCP: ${p.lcp.toFixed(0)}ms`);
|
|
4248
4466
|
if (p.fid !== void 0) console.log(` FID: ${p.fid.toFixed(0)}ms`);
|
|
4249
4467
|
if (p.cls !== void 0) console.log(` CLS: ${p.cls.toFixed(3)}`);
|
|
4468
|
+
console.log();
|
|
4250
4469
|
}
|
|
4251
4470
|
}
|
|
4252
4471
|
const failedTests = results.flatMap(
|
|
@@ -4255,23 +4474,31 @@ async function displaySummary(results, config) {
|
|
|
4255
4474
|
)
|
|
4256
4475
|
);
|
|
4257
4476
|
if (failedTests.length > 0) {
|
|
4258
|
-
console.log();
|
|
4259
4477
|
console.log(chalk5.red(" \u5931\u8D25\u8BE6\u60C5:"));
|
|
4260
4478
|
for (const { suite, test } of failedTests) {
|
|
4261
|
-
console.log(chalk5.red(` \
|
|
4479
|
+
console.log(chalk5.red(` \u2715 ${suite.name} > ${test.name}`));
|
|
4262
4480
|
if (test.error?.message) {
|
|
4263
4481
|
console.log(chalk5.gray(` ${test.error.message.split("\n")[0]}`));
|
|
4264
4482
|
}
|
|
4265
4483
|
}
|
|
4484
|
+
console.log();
|
|
4266
4485
|
}
|
|
4267
|
-
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"));
|
|
4268
|
-
console.log();
|
|
4269
4486
|
if (totalFailed > 0) {
|
|
4487
|
+
console.log(chalk5.red(` Tests: ${totalFailed} failed, ${totalPassed} passed, ${total} total`));
|
|
4270
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`));
|
|
4271
4493
|
}
|
|
4272
|
-
|
|
4273
|
-
|
|
4274
|
-
|
|
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);
|
|
4275
4502
|
}
|
|
4276
4503
|
async function aiAnalyzeFailures(results, aiConfig) {
|
|
4277
4504
|
const failedTests = results.flatMap(
|
|
@@ -4300,7 +4527,7 @@ async function aiAnalyzeFailures(results, aiConfig) {
|
|
|
4300
4527
|
}
|
|
4301
4528
|
}
|
|
4302
4529
|
}
|
|
4303
|
-
function
|
|
4530
|
+
function formatDuration2(ms) {
|
|
4304
4531
|
if (ms < 1e3) return `${ms}ms`;
|
|
4305
4532
|
if (ms < 6e4) return `${(ms / 1e3).toFixed(1)}s`;
|
|
4306
4533
|
const min = Math.floor(ms / 6e4);
|
|
@@ -4326,8 +4553,8 @@ async function checkDevServer(config) {
|
|
|
4326
4553
|
}
|
|
4327
4554
|
async function startDevServer(config) {
|
|
4328
4555
|
const { spawn } = await import("child_process");
|
|
4329
|
-
const hasPnpm =
|
|
4330
|
-
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"));
|
|
4331
4558
|
const pkgCmd = hasPnpm ? "pnpm" : hasYarn ? "yarn" : "npm";
|
|
4332
4559
|
console.log(chalk5.cyan(` \u6B63\u5728\u542F\u52A8 dev server (${pkgCmd} run dev) ...`));
|
|
4333
4560
|
const child = spawn(pkgCmd, ["run", "dev"], {
|
|
@@ -4356,27 +4583,99 @@ async function startDevServer(config) {
|
|
|
4356
4583
|
}
|
|
4357
4584
|
function saveRunResults(results) {
|
|
4358
4585
|
if (results.length === 0) return;
|
|
4359
|
-
const resultsPath =
|
|
4360
|
-
if (!
|
|
4361
|
-
|
|
4586
|
+
const resultsPath = path12.join(process.cwd(), RESULTS_DIR);
|
|
4587
|
+
if (!fs11.existsSync(resultsPath)) {
|
|
4588
|
+
fs11.mkdirSync(resultsPath, { recursive: true });
|
|
4362
4589
|
}
|
|
4363
4590
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
4364
4591
|
const fileName = `result-${timestamp}.json`;
|
|
4365
|
-
const filePath =
|
|
4592
|
+
const filePath = path12.join(resultsPath, fileName);
|
|
4366
4593
|
const data = {
|
|
4367
4594
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4368
4595
|
results
|
|
4369
4596
|
};
|
|
4370
|
-
|
|
4597
|
+
fs11.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf-8");
|
|
4371
4598
|
try {
|
|
4372
|
-
const files =
|
|
4599
|
+
const files = fs11.readdirSync(resultsPath).filter((f) => f.startsWith("result-") && f.endsWith(".json")).sort();
|
|
4373
4600
|
while (files.length > 20) {
|
|
4374
4601
|
const oldest = files.shift();
|
|
4375
|
-
|
|
4602
|
+
fs11.unlinkSync(path12.join(resultsPath, oldest));
|
|
4376
4603
|
}
|
|
4377
4604
|
} catch {
|
|
4378
4605
|
}
|
|
4379
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
|
+
}
|
|
4380
4679
|
|
|
4381
4680
|
// src/commands/mock.ts
|
|
4382
4681
|
import chalk6 from "chalk";
|
|
@@ -4500,276 +4799,8 @@ function showStatus() {
|
|
|
4500
4799
|
// src/commands/report.ts
|
|
4501
4800
|
import chalk7 from "chalk";
|
|
4502
4801
|
import ora6 from "ora";
|
|
4503
|
-
import
|
|
4802
|
+
import fs12 from "fs";
|
|
4504
4803
|
import path13 from "path";
|
|
4505
|
-
|
|
4506
|
-
// src/services/reporter.ts
|
|
4507
|
-
import fs10 from "fs";
|
|
4508
|
-
import path12 from "path";
|
|
4509
|
-
function aggregateResults(results) {
|
|
4510
|
-
const summary = {
|
|
4511
|
-
total: 0,
|
|
4512
|
-
passed: 0,
|
|
4513
|
-
failed: 0,
|
|
4514
|
-
skipped: 0,
|
|
4515
|
-
pending: 0
|
|
4516
|
-
};
|
|
4517
|
-
const byType = {};
|
|
4518
|
-
for (const result of results) {
|
|
4519
|
-
const typeKey = result.type;
|
|
4520
|
-
if (!byType[typeKey]) {
|
|
4521
|
-
byType[typeKey] = { total: 0, passed: 0, failed: 0, skipped: 0 };
|
|
4522
|
-
}
|
|
4523
|
-
for (const suite of result.suites) {
|
|
4524
|
-
for (const test of suite.tests) {
|
|
4525
|
-
summary.total++;
|
|
4526
|
-
byType[typeKey].total++;
|
|
4527
|
-
if (test.status === "passed") {
|
|
4528
|
-
summary.passed++;
|
|
4529
|
-
byType[typeKey].passed++;
|
|
4530
|
-
} else if (test.status === "failed") {
|
|
4531
|
-
summary.failed++;
|
|
4532
|
-
byType[typeKey].failed++;
|
|
4533
|
-
} else if (test.status === "skipped") {
|
|
4534
|
-
summary.skipped++;
|
|
4535
|
-
byType[typeKey].skipped++;
|
|
4536
|
-
} else {
|
|
4537
|
-
summary.pending++;
|
|
4538
|
-
}
|
|
4539
|
-
}
|
|
4540
|
-
}
|
|
4541
|
-
}
|
|
4542
|
-
const totalDuration = results.reduce((sum, r) => sum + r.duration, 0);
|
|
4543
|
-
return {
|
|
4544
|
-
timestamp: Date.now(),
|
|
4545
|
-
duration: totalDuration,
|
|
4546
|
-
results,
|
|
4547
|
-
summary,
|
|
4548
|
-
byType
|
|
4549
|
-
};
|
|
4550
|
-
}
|
|
4551
|
-
function formatDuration2(ms) {
|
|
4552
|
-
if (ms < 1e3) return `${ms}ms`;
|
|
4553
|
-
if (ms < 6e4) return `${(ms / 1e3).toFixed(2)}s`;
|
|
4554
|
-
const minutes = Math.floor(ms / 6e4);
|
|
4555
|
-
const seconds = (ms % 6e4 / 1e3).toFixed(0);
|
|
4556
|
-
return `${minutes}m ${seconds}s`;
|
|
4557
|
-
}
|
|
4558
|
-
function formatTimestamp(ts) {
|
|
4559
|
-
return new Date(ts).toLocaleString("zh-CN", {
|
|
4560
|
-
year: "numeric",
|
|
4561
|
-
month: "2-digit",
|
|
4562
|
-
day: "2-digit",
|
|
4563
|
-
hour: "2-digit",
|
|
4564
|
-
minute: "2-digit",
|
|
4565
|
-
second: "2-digit"
|
|
4566
|
-
});
|
|
4567
|
-
}
|
|
4568
|
-
function renderSuiteHTML(suite) {
|
|
4569
|
-
const statusClass = suite.status === "passed" ? "passed" : suite.status === "failed" ? "failed" : "skipped";
|
|
4570
|
-
const testsHTML = suite.tests.map((test) => {
|
|
4571
|
-
const testStatusClass = test.status;
|
|
4572
|
-
const errorHTML = test.error ? `<div class="error-message"><strong>${escapeHTML(test.error.message)}</strong>${test.error.stack ? `
|
|
4573
|
-
${escapeHTML(test.error.stack)}` : ""}${test.error.expected && test.error.actual ? `
|
|
4574
|
-
|
|
4575
|
-
Expected: ${escapeHTML(test.error.expected)}
|
|
4576
|
-
Actual: ${escapeHTML(test.error.actual)}` : ""}</div>` : "";
|
|
4577
|
-
return `<div class="test-item">
|
|
4578
|
-
<span class="status-dot ${testStatusClass}"></span>
|
|
4579
|
-
<span class="test-name">${escapeHTML(test.name)}</span>
|
|
4580
|
-
<span class="duration">${formatDuration2(test.duration)}</span>
|
|
4581
|
-
${test.retries > 0 ? `<span class="retries">\u91CD\u8BD5 ${test.retries} \u6B21</span>` : ""}
|
|
4582
|
-
</div>
|
|
4583
|
-
${errorHTML}`;
|
|
4584
|
-
}).join("");
|
|
4585
|
-
return `<div class="suite">
|
|
4586
|
-
<div class="suite-header" onclick="this.parentElement.classList.toggle('collapsed')">
|
|
4587
|
-
<div>
|
|
4588
|
-
<span class="status-dot ${statusClass}"></span>
|
|
4589
|
-
<strong>${escapeHTML(suite.name)}</strong>
|
|
4590
|
-
<span class="suite-file">${escapeHTML(suite.file)}</span>
|
|
4591
|
-
</div>
|
|
4592
|
-
<div>
|
|
4593
|
-
<span class="duration">${formatDuration2(suite.duration)}</span>
|
|
4594
|
-
<span class="toggle-icon">\u25BC</span>
|
|
4595
|
-
</div>
|
|
4596
|
-
</div>
|
|
4597
|
-
<div class="suite-body">${testsHTML}</div>
|
|
4598
|
-
</div>`;
|
|
4599
|
-
}
|
|
4600
|
-
function escapeHTML(str) {
|
|
4601
|
-
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
4602
|
-
}
|
|
4603
|
-
function generateHTMLReport(data) {
|
|
4604
|
-
const passRate = data.summary.total > 0 ? (data.summary.passed / data.summary.total * 100).toFixed(1) : "0";
|
|
4605
|
-
const suitesHTML = data.results.flatMap((r) => r.suites).map(renderSuiteHTML).join("\n");
|
|
4606
|
-
const byTypeHTML = Object.entries(data.byType).map(([type, stats]) => {
|
|
4607
|
-
const rate = stats.total > 0 ? (stats.passed / stats.total * 100).toFixed(0) : "0";
|
|
4608
|
-
return `<div class="type-card">
|
|
4609
|
-
<div class="type-name">${type}</div>
|
|
4610
|
-
<div class="type-stats">
|
|
4611
|
-
<span class="passed">${stats.passed} \u901A\u8FC7</span>
|
|
4612
|
-
<span class="failed">${stats.failed} \u5931\u8D25</span>
|
|
4613
|
-
<span class="skipped">${stats.skipped} \u8DF3\u8FC7</span>
|
|
4614
|
-
</div>
|
|
4615
|
-
<div class="type-rate">${rate}%</div>
|
|
4616
|
-
</div>`;
|
|
4617
|
-
}).join("\n");
|
|
4618
|
-
return `<!DOCTYPE html>
|
|
4619
|
-
<html lang="zh-CN">
|
|
4620
|
-
<head>
|
|
4621
|
-
<meta charset="UTF-8">
|
|
4622
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
4623
|
-
<title>QAT \u6D4B\u8BD5\u62A5\u544A - ${formatTimestamp(data.timestamp)}</title>
|
|
4624
|
-
<style>
|
|
4625
|
-
:root {
|
|
4626
|
-
--color-passed: #22c55e;
|
|
4627
|
-
--color-failed: #ef4444;
|
|
4628
|
-
--color-skipped: #f59e0b;
|
|
4629
|
-
--color-info: #3b82f6;
|
|
4630
|
-
--bg-primary: #ffffff;
|
|
4631
|
-
--bg-secondary: #f8fafc;
|
|
4632
|
-
--text-primary: #1e293b;
|
|
4633
|
-
--text-secondary: #64748b;
|
|
4634
|
-
--border-color: #e2e8f0;
|
|
4635
|
-
}
|
|
4636
|
-
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
4637
|
-
body {
|
|
4638
|
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
4639
|
-
background: var(--bg-secondary);
|
|
4640
|
-
color: var(--text-primary);
|
|
4641
|
-
line-height: 1.6;
|
|
4642
|
-
}
|
|
4643
|
-
.container { max-width: 1200px; margin: 0 auto; padding: 20px; }
|
|
4644
|
-
header {
|
|
4645
|
-
background: white;
|
|
4646
|
-
border-bottom: 1px solid var(--border-color);
|
|
4647
|
-
padding: 20px;
|
|
4648
|
-
margin-bottom: 20px;
|
|
4649
|
-
border-radius: 8px;
|
|
4650
|
-
}
|
|
4651
|
-
header h1 { font-size: 24px; font-weight: 600; }
|
|
4652
|
-
header .meta { color: var(--text-secondary); font-size: 14px; margin-top: 4px; }
|
|
4653
|
-
.summary { display: flex; gap: 16px; margin: 20px 0; flex-wrap: wrap; }
|
|
4654
|
-
.card {
|
|
4655
|
-
padding: 16px 24px; border-radius: 8px; color: white;
|
|
4656
|
-
font-weight: 600; min-width: 120px; text-align: center;
|
|
4657
|
-
}
|
|
4658
|
-
.card.passed { background: var(--color-passed); }
|
|
4659
|
-
.card.failed { background: var(--color-failed); }
|
|
4660
|
-
.card.skipped { background: var(--color-skipped); }
|
|
4661
|
-
.card.total { background: var(--color-info); }
|
|
4662
|
-
.card .card-value { font-size: 28px; }
|
|
4663
|
-
.card .card-label { font-size: 13px; opacity: 0.9; }
|
|
4664
|
-
.pass-rate {
|
|
4665
|
-
font-size: 48px; font-weight: 700; text-align: center; margin: 20px 0;
|
|
4666
|
-
color: ${parseFloat(passRate) >= 80 ? "var(--color-passed)" : parseFloat(passRate) >= 50 ? "var(--color-skipped)" : "var(--color-failed)"};
|
|
4667
|
-
}
|
|
4668
|
-
.pass-rate-label { text-align: center; color: var(--text-secondary); margin-bottom: 20px; }
|
|
4669
|
-
.by-type { display: flex; gap: 12px; flex-wrap: wrap; margin: 20px 0; }
|
|
4670
|
-
.type-card {
|
|
4671
|
-
background: white; border: 1px solid var(--border-color); border-radius: 8px;
|
|
4672
|
-
padding: 12px 16px; min-width: 180px;
|
|
4673
|
-
}
|
|
4674
|
-
.type-name { font-weight: 600; text-transform: capitalize; margin-bottom: 4px; }
|
|
4675
|
-
.type-stats { font-size: 13px; }
|
|
4676
|
-
.type-stats span { margin-right: 8px; }
|
|
4677
|
-
.type-stats .passed { color: var(--color-passed); }
|
|
4678
|
-
.type-stats .failed { color: var(--color-failed); }
|
|
4679
|
-
.type-stats .skipped { color: var(--color-skipped); }
|
|
4680
|
-
.type-rate { font-size: 20px; font-weight: 700; margin-top: 4px; }
|
|
4681
|
-
.suite {
|
|
4682
|
-
background: white; border: 1px solid var(--border-color);
|
|
4683
|
-
border-radius: 8px; margin-bottom: 12px; overflow: hidden;
|
|
4684
|
-
}
|
|
4685
|
-
.suite-header {
|
|
4686
|
-
padding: 12px 16px; display: flex; justify-content: space-between;
|
|
4687
|
-
align-items: center; cursor: pointer; user-select: none;
|
|
4688
|
-
}
|
|
4689
|
-
.suite-header:hover { background: var(--bg-secondary); }
|
|
4690
|
-
.suite-header div { display: flex; align-items: center; gap: 8px; }
|
|
4691
|
-
.suite.collapsed .suite-body { display: none; }
|
|
4692
|
-
.suite-file { color: var(--text-secondary); font-size: 12px; }
|
|
4693
|
-
.toggle-icon { font-size: 12px; color: var(--text-secondary); transition: transform 0.2s; }
|
|
4694
|
-
.suite.collapsed .toggle-icon { transform: rotate(-90deg); }
|
|
4695
|
-
.suite-body { border-top: 1px solid var(--border-color); padding: 8px 16px; }
|
|
4696
|
-
.test-item {
|
|
4697
|
-
padding: 8px 0; display: flex; align-items: center; gap: 8px;
|
|
4698
|
-
}
|
|
4699
|
-
.test-item + .test-item { border-top: 1px solid var(--border-color); }
|
|
4700
|
-
.status-dot {
|
|
4701
|
-
width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0;
|
|
4702
|
-
}
|
|
4703
|
-
.status-dot.passed { background: var(--color-passed); }
|
|
4704
|
-
.status-dot.failed { background: var(--color-failed); }
|
|
4705
|
-
.status-dot.skipped { background: var(--color-skipped); }
|
|
4706
|
-
.status-dot.pending { background: var(--text-secondary); }
|
|
4707
|
-
.test-name { flex: 1; }
|
|
4708
|
-
.duration { color: var(--text-secondary); font-size: 13px; }
|
|
4709
|
-
.retries { color: var(--color-skipped); font-size: 12px; }
|
|
4710
|
-
.error-message {
|
|
4711
|
-
background: #fef2f2; color: #991b1b; padding: 12px;
|
|
4712
|
-
border-radius: 4px; font-family: 'Fira Code', monospace;
|
|
4713
|
-
font-size: 13px; margin: 8px 0 8px 16px; white-space: pre-wrap;
|
|
4714
|
-
word-break: break-all;
|
|
4715
|
-
}
|
|
4716
|
-
footer {
|
|
4717
|
-
text-align: center; padding: 20px; color: var(--text-secondary);
|
|
4718
|
-
font-size: 13px; margin-top: 40px;
|
|
4719
|
-
}
|
|
4720
|
-
</style>
|
|
4721
|
-
</head>
|
|
4722
|
-
<body>
|
|
4723
|
-
<div class="container">
|
|
4724
|
-
<header>
|
|
4725
|
-
<h1>QAT \u6D4B\u8BD5\u62A5\u544A</h1>
|
|
4726
|
-
<div class="meta">\u751F\u6210\u65F6\u95F4: ${formatTimestamp(data.timestamp)} | \u603B\u8017\u65F6: ${formatDuration2(data.duration)}</div>
|
|
4727
|
-
</header>
|
|
4728
|
-
|
|
4729
|
-
<div class="pass-rate">${passRate}%</div>
|
|
4730
|
-
<div class="pass-rate-label">\u6D4B\u8BD5\u901A\u8FC7\u7387</div>
|
|
4731
|
-
|
|
4732
|
-
<div class="summary">
|
|
4733
|
-
<div class="card total">
|
|
4734
|
-
<div class="card-value">${data.summary.total}</div>
|
|
4735
|
-
<div class="card-label">\u603B\u8BA1</div>
|
|
4736
|
-
</div>
|
|
4737
|
-
<div class="card passed">
|
|
4738
|
-
<div class="card-value">${data.summary.passed}</div>
|
|
4739
|
-
<div class="card-label">\u901A\u8FC7</div>
|
|
4740
|
-
</div>
|
|
4741
|
-
<div class="card failed">
|
|
4742
|
-
<div class="card-value">${data.summary.failed}</div>
|
|
4743
|
-
<div class="card-label">\u5931\u8D25</div>
|
|
4744
|
-
</div>
|
|
4745
|
-
<div class="card skipped">
|
|
4746
|
-
<div class="card-value">${data.summary.skipped}</div>
|
|
4747
|
-
<div class="card-label">\u8DF3\u8FC7</div>
|
|
4748
|
-
</div>
|
|
4749
|
-
</div>
|
|
4750
|
-
|
|
4751
|
-
${Object.keys(data.byType).length > 0 ? `<h2 style="margin-top:30px;margin-bottom:10px;">\u6309\u7C7B\u578B\u7EDF\u8BA1</h2><div class="by-type">${byTypeHTML}</div>` : ""}
|
|
4752
|
-
|
|
4753
|
-
<h2 style="margin-top:30px;margin-bottom:10px;">\u6D4B\u8BD5\u8BE6\u60C5</h2>
|
|
4754
|
-
${suitesHTML || '<p style="color:var(--text-secondary)">\u6682\u65E0\u6D4B\u8BD5\u7ED3\u679C</p>'}
|
|
4755
|
-
|
|
4756
|
-
<footer>\u7531 QAT \u81EA\u52A8\u5316\u6D4B\u8BD5\u5DE5\u5177\u751F\u6210</footer>
|
|
4757
|
-
</div>
|
|
4758
|
-
</body>
|
|
4759
|
-
</html>`;
|
|
4760
|
-
}
|
|
4761
|
-
function writeReportToDisk(data, outputDir) {
|
|
4762
|
-
const html = generateHTMLReport(data);
|
|
4763
|
-
const dir = path12.resolve(outputDir);
|
|
4764
|
-
if (!fs10.existsSync(dir)) {
|
|
4765
|
-
fs10.mkdirSync(dir, { recursive: true });
|
|
4766
|
-
}
|
|
4767
|
-
const indexPath = path12.join(dir, "index.html");
|
|
4768
|
-
fs10.writeFileSync(indexPath, html, "utf-8");
|
|
4769
|
-
return indexPath;
|
|
4770
|
-
}
|
|
4771
|
-
|
|
4772
|
-
// src/commands/report.ts
|
|
4773
4804
|
var RESULTS_DIR2 = ".qat-results";
|
|
4774
4805
|
function registerReportCommand(program2) {
|
|
4775
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) => {
|
|
@@ -4793,7 +4824,7 @@ async function executeReport(options) {
|
|
|
4793
4824
|
console.log(chalk7.gray("\n \u63D0\u793A: \u5148\u8FD0\u884C qat run \u751F\u6210\u6D4B\u8BD5\u7ED3\u679C\n"));
|
|
4794
4825
|
return;
|
|
4795
4826
|
}
|
|
4796
|
-
spinner.text = "\u6B63\u5728\u751F\
|
|
4827
|
+
spinner.text = "\u6B63\u5728\u751F\u6210\u6D4B\u8BD5\u62A5\u544A...";
|
|
4797
4828
|
const reportData = aggregateResults(results);
|
|
4798
4829
|
const reportPath = writeReportToDisk(reportData, outputDir);
|
|
4799
4830
|
saveResultToHistory(reportData);
|
|
@@ -4806,14 +4837,14 @@ async function executeReport(options) {
|
|
|
4806
4837
|
function collectResults() {
|
|
4807
4838
|
const results = [];
|
|
4808
4839
|
const resultsPath = path13.join(process.cwd(), RESULTS_DIR2);
|
|
4809
|
-
if (!
|
|
4840
|
+
if (!fs12.existsSync(resultsPath)) {
|
|
4810
4841
|
return results;
|
|
4811
4842
|
}
|
|
4812
|
-
const files =
|
|
4843
|
+
const files = fs12.readdirSync(resultsPath).filter((f) => f.endsWith(".json")).sort().reverse();
|
|
4813
4844
|
if (files.length > 0) {
|
|
4814
4845
|
const latestFile = path13.join(resultsPath, files[0]);
|
|
4815
4846
|
try {
|
|
4816
|
-
const data = JSON.parse(
|
|
4847
|
+
const data = JSON.parse(fs12.readFileSync(latestFile, "utf-8"));
|
|
4817
4848
|
if (Array.isArray(data)) {
|
|
4818
4849
|
results.push(...data);
|
|
4819
4850
|
} else if (data.results) {
|
|
@@ -4826,44 +4857,54 @@ function collectResults() {
|
|
|
4826
4857
|
}
|
|
4827
4858
|
function saveResultToHistory(reportData) {
|
|
4828
4859
|
const resultsPath = path13.join(process.cwd(), RESULTS_DIR2);
|
|
4829
|
-
if (!
|
|
4830
|
-
|
|
4860
|
+
if (!fs12.existsSync(resultsPath)) {
|
|
4861
|
+
fs12.mkdirSync(resultsPath, { recursive: true });
|
|
4831
4862
|
}
|
|
4832
4863
|
const timestamp = new Date(reportData.timestamp).toISOString().replace(/[:.]/g, "-");
|
|
4833
4864
|
const fileName = `result-${timestamp}.json`;
|
|
4834
4865
|
const filePath = path13.join(resultsPath, fileName);
|
|
4835
|
-
|
|
4836
|
-
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();
|
|
4837
4868
|
while (files.length > 20) {
|
|
4838
4869
|
const oldest = files.shift();
|
|
4839
|
-
|
|
4870
|
+
fs12.unlinkSync(path13.join(resultsPath, oldest));
|
|
4840
4871
|
}
|
|
4841
4872
|
}
|
|
4842
4873
|
function displayReportResult(reportPath, data) {
|
|
4843
4874
|
const relativePath = path13.relative(process.cwd(), reportPath);
|
|
4875
|
+
const passRate = data.summary.total > 0 ? (data.summary.passed / data.summary.total * 100).toFixed(1) : "0";
|
|
4844
4876
|
console.log();
|
|
4845
4877
|
console.log(chalk7.green(" \u2713 \u6D4B\u8BD5\u62A5\u544A\u5DF2\u751F\u6210"));
|
|
4846
4878
|
console.log();
|
|
4847
4879
|
console.log(chalk7.white(" \u62A5\u544A\u8DEF\u5F84:"), chalk7.cyan(relativePath));
|
|
4880
|
+
console.log(chalk7.white(" \u901A\u8FC7\u7387: "), parseFloat(passRate) >= 80 ? chalk7.green(`${passRate}%`) : parseFloat(passRate) >= 50 ? chalk7.yellow(`${passRate}%`) : chalk7.red(`${passRate}%`));
|
|
4848
4881
|
console.log(chalk7.white(" \u6D4B\u8BD5\u7528\u4F8B:"), `${data.summary.total} total`);
|
|
4849
|
-
console.log(chalk7.white(" \u901A\u8FC7:"), chalk7.green(String(data.summary.passed)));
|
|
4882
|
+
console.log(chalk7.white(" \u2705 \u901A\u8FC7: "), chalk7.green(String(data.summary.passed)));
|
|
4850
4883
|
if (data.summary.failed > 0) {
|
|
4851
|
-
console.log(chalk7.white(" \u5931\u8D25:"), chalk7.red(String(data.summary.failed)));
|
|
4884
|
+
console.log(chalk7.white(" \u274C \u5931\u8D25: "), chalk7.red(String(data.summary.failed)));
|
|
4852
4885
|
}
|
|
4853
4886
|
if (data.summary.skipped > 0) {
|
|
4854
|
-
console.log(chalk7.white(" \u8DF3\u8FC7:"), chalk7.yellow(String(data.summary.skipped)));
|
|
4887
|
+
console.log(chalk7.white(" \u23ED\uFE0F \u8DF3\u8FC7: "), chalk7.yellow(String(data.summary.skipped)));
|
|
4855
4888
|
}
|
|
4856
4889
|
if (Object.keys(data.byType).length > 0) {
|
|
4857
4890
|
console.log();
|
|
4858
4891
|
console.log(chalk7.white(" \u6309\u7C7B\u578B:"));
|
|
4859
4892
|
for (const [type, stats] of Object.entries(data.byType)) {
|
|
4860
4893
|
const rate = stats.total > 0 ? (stats.passed / stats.total * 100).toFixed(0) : "0";
|
|
4861
|
-
const icon = stats.failed > 0 ? chalk7.red("\
|
|
4894
|
+
const icon = stats.failed > 0 ? chalk7.red("\u274C") : chalk7.green("\u2705");
|
|
4862
4895
|
console.log(` ${icon} ${type}: ${stats.passed}/${stats.total} (${rate}%)`);
|
|
4863
4896
|
}
|
|
4864
4897
|
}
|
|
4898
|
+
if (data.coverage) {
|
|
4899
|
+
console.log();
|
|
4900
|
+
console.log(chalk7.white(" \u8986\u76D6\u7387:"));
|
|
4901
|
+
console.log(` \u8BED\u53E5: ${chalk7.cyan(pct2(data.coverage.statements))} \u5206\u652F: ${chalk7.cyan(pct2(data.coverage.branches))} \u51FD\u6570: ${chalk7.cyan(pct2(data.coverage.functions))} \u884C: ${chalk7.cyan(pct2(data.coverage.lines))}`);
|
|
4902
|
+
}
|
|
4865
4903
|
console.log();
|
|
4866
4904
|
}
|
|
4905
|
+
function pct2(value) {
|
|
4906
|
+
return `${(value * 100).toFixed(1)}%`;
|
|
4907
|
+
}
|
|
4867
4908
|
async function openReport(reportPath) {
|
|
4868
4909
|
const { exec } = await import("child_process");
|
|
4869
4910
|
const platform = process.platform;
|
|
@@ -4887,19 +4928,19 @@ import chalk8 from "chalk";
|
|
|
4887
4928
|
import ora7 from "ora";
|
|
4888
4929
|
|
|
4889
4930
|
// src/services/visual.ts
|
|
4890
|
-
import
|
|
4931
|
+
import fs13 from "fs";
|
|
4891
4932
|
import path14 from "path";
|
|
4892
4933
|
import pixelmatch from "pixelmatch";
|
|
4893
4934
|
import { PNG } from "pngjs";
|
|
4894
4935
|
function compareImages(baselinePath, currentPath, diffOutputPath, threshold = 0.1) {
|
|
4895
|
-
if (!
|
|
4936
|
+
if (!fs13.existsSync(baselinePath)) {
|
|
4896
4937
|
throw new Error(`\u57FA\u7EBF\u56FE\u7247\u4E0D\u5B58\u5728: ${baselinePath}`);
|
|
4897
4938
|
}
|
|
4898
|
-
if (!
|
|
4939
|
+
if (!fs13.existsSync(currentPath)) {
|
|
4899
4940
|
throw new Error(`\u5F53\u524D\u56FE\u7247\u4E0D\u5B58\u5728: ${currentPath}`);
|
|
4900
4941
|
}
|
|
4901
|
-
const baseline = PNG.sync.read(
|
|
4902
|
-
const current = PNG.sync.read(
|
|
4942
|
+
const baseline = PNG.sync.read(fs13.readFileSync(baselinePath));
|
|
4943
|
+
const current = PNG.sync.read(fs13.readFileSync(currentPath));
|
|
4903
4944
|
if (baseline.width !== current.width || baseline.height !== current.height) {
|
|
4904
4945
|
return {
|
|
4905
4946
|
passed: false,
|
|
@@ -4928,10 +4969,10 @@ function compareImages(baselinePath, currentPath, diffOutputPath, threshold = 0.
|
|
|
4928
4969
|
let diffPath;
|
|
4929
4970
|
if (diffPixels > 0) {
|
|
4930
4971
|
const diffDir = path14.dirname(diffOutputPath);
|
|
4931
|
-
if (!
|
|
4932
|
-
|
|
4972
|
+
if (!fs13.existsSync(diffDir)) {
|
|
4973
|
+
fs13.mkdirSync(diffDir, { recursive: true });
|
|
4933
4974
|
}
|
|
4934
|
-
|
|
4975
|
+
fs13.writeFileSync(diffOutputPath, PNG.sync.write(diff));
|
|
4935
4976
|
diffPath = diffOutputPath;
|
|
4936
4977
|
}
|
|
4937
4978
|
return {
|
|
@@ -4945,69 +4986,69 @@ function compareImages(baselinePath, currentPath, diffOutputPath, threshold = 0.
|
|
|
4945
4986
|
};
|
|
4946
4987
|
}
|
|
4947
4988
|
function createBaseline(currentPath, baselinePath) {
|
|
4948
|
-
if (!
|
|
4989
|
+
if (!fs13.existsSync(currentPath)) {
|
|
4949
4990
|
throw new Error(`\u5F53\u524D\u622A\u56FE\u4E0D\u5B58\u5728: ${currentPath}`);
|
|
4950
4991
|
}
|
|
4951
4992
|
const baselineDir = path14.dirname(baselinePath);
|
|
4952
|
-
if (!
|
|
4953
|
-
|
|
4993
|
+
if (!fs13.existsSync(baselineDir)) {
|
|
4994
|
+
fs13.mkdirSync(baselineDir, { recursive: true });
|
|
4954
4995
|
}
|
|
4955
|
-
|
|
4996
|
+
fs13.copyFileSync(currentPath, baselinePath);
|
|
4956
4997
|
return baselinePath;
|
|
4957
4998
|
}
|
|
4958
4999
|
function updateAllBaselines(currentDir, baselineDir) {
|
|
4959
5000
|
const updated = [];
|
|
4960
|
-
if (!
|
|
5001
|
+
if (!fs13.existsSync(currentDir)) {
|
|
4961
5002
|
return updated;
|
|
4962
5003
|
}
|
|
4963
|
-
const files =
|
|
5004
|
+
const files = fs13.readdirSync(currentDir).filter((f) => f.endsWith(".png"));
|
|
4964
5005
|
for (const file of files) {
|
|
4965
5006
|
const currentPath = path14.join(currentDir, file);
|
|
4966
5007
|
const baselinePath = path14.join(baselineDir, file);
|
|
4967
5008
|
const baselineDirAbs = path14.dirname(baselinePath);
|
|
4968
|
-
if (!
|
|
4969
|
-
|
|
5009
|
+
if (!fs13.existsSync(baselineDirAbs)) {
|
|
5010
|
+
fs13.mkdirSync(baselineDirAbs, { recursive: true });
|
|
4970
5011
|
}
|
|
4971
|
-
|
|
5012
|
+
fs13.copyFileSync(currentPath, baselinePath);
|
|
4972
5013
|
updated.push(file);
|
|
4973
5014
|
}
|
|
4974
5015
|
return updated;
|
|
4975
5016
|
}
|
|
4976
5017
|
function cleanBaselines(baselineDir) {
|
|
4977
|
-
if (!
|
|
5018
|
+
if (!fs13.existsSync(baselineDir)) {
|
|
4978
5019
|
return 0;
|
|
4979
5020
|
}
|
|
4980
|
-
const files =
|
|
5021
|
+
const files = fs13.readdirSync(baselineDir).filter((f) => f.endsWith(".png"));
|
|
4981
5022
|
let count = 0;
|
|
4982
5023
|
for (const file of files) {
|
|
4983
|
-
|
|
5024
|
+
fs13.unlinkSync(path14.join(baselineDir, file));
|
|
4984
5025
|
count++;
|
|
4985
5026
|
}
|
|
4986
5027
|
return count;
|
|
4987
5028
|
}
|
|
4988
5029
|
function cleanDiffs(diffDir) {
|
|
4989
|
-
if (!
|
|
5030
|
+
if (!fs13.existsSync(diffDir)) {
|
|
4990
5031
|
return 0;
|
|
4991
5032
|
}
|
|
4992
|
-
const files =
|
|
5033
|
+
const files = fs13.readdirSync(diffDir).filter((f) => f.endsWith(".png"));
|
|
4993
5034
|
let count = 0;
|
|
4994
5035
|
for (const file of files) {
|
|
4995
|
-
|
|
5036
|
+
fs13.unlinkSync(path14.join(diffDir, file));
|
|
4996
5037
|
count++;
|
|
4997
5038
|
}
|
|
4998
5039
|
return count;
|
|
4999
5040
|
}
|
|
5000
5041
|
function compareDirectories(baselineDir, currentDir, diffDir, threshold = 0.1) {
|
|
5001
5042
|
const results = [];
|
|
5002
|
-
if (!
|
|
5043
|
+
if (!fs13.existsSync(currentDir)) {
|
|
5003
5044
|
return results;
|
|
5004
5045
|
}
|
|
5005
|
-
const currentFiles =
|
|
5046
|
+
const currentFiles = fs13.readdirSync(currentDir).filter((f) => f.endsWith(".png"));
|
|
5006
5047
|
for (const file of currentFiles) {
|
|
5007
5048
|
const currentPath = path14.join(currentDir, file);
|
|
5008
5049
|
const baselinePath = path14.join(baselineDir, file);
|
|
5009
5050
|
const diffPath = path14.join(diffDir, file);
|
|
5010
|
-
if (!
|
|
5051
|
+
if (!fs13.existsSync(baselinePath)) {
|
|
5011
5052
|
createBaseline(currentPath, baselinePath);
|
|
5012
5053
|
results.push({
|
|
5013
5054
|
passed: true,
|
|
@@ -5039,7 +5080,7 @@ function compareDirectories(baselineDir, currentDir, diffDir, threshold = 0.1) {
|
|
|
5039
5080
|
}
|
|
5040
5081
|
|
|
5041
5082
|
// src/commands/visual.ts
|
|
5042
|
-
import
|
|
5083
|
+
import fs14 from "fs";
|
|
5043
5084
|
import path15 from "path";
|
|
5044
5085
|
function registerVisualCommand(program2) {
|
|
5045
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) => {
|
|
@@ -5120,7 +5161,7 @@ function findCurrentScreenshotsDir(baselineDir) {
|
|
|
5120
5161
|
path15.join(process.cwd(), baselineDir, "..", "current")
|
|
5121
5162
|
];
|
|
5122
5163
|
for (const dir of possibleDirs) {
|
|
5123
|
-
if (
|
|
5164
|
+
if (fs14.existsSync(dir)) {
|
|
5124
5165
|
const pngs = findPngFiles(dir);
|
|
5125
5166
|
if (pngs.length > 0) return dir;
|
|
5126
5167
|
}
|
|
@@ -5130,8 +5171,8 @@ function findCurrentScreenshotsDir(baselineDir) {
|
|
|
5130
5171
|
function findPngFiles(dir) {
|
|
5131
5172
|
const files = [];
|
|
5132
5173
|
function walk(d) {
|
|
5133
|
-
if (!
|
|
5134
|
-
const entries =
|
|
5174
|
+
if (!fs14.existsSync(d)) return;
|
|
5175
|
+
const entries = fs14.readdirSync(d, { withFileTypes: true });
|
|
5135
5176
|
for (const entry of entries) {
|
|
5136
5177
|
const fullPath = path15.join(d, entry.name);
|
|
5137
5178
|
if (entry.isDirectory() && entry.name !== "node_modules") {
|
|
@@ -5229,7 +5270,7 @@ import chalk9 from "chalk";
|
|
|
5229
5270
|
import inquirer4 from "inquirer";
|
|
5230
5271
|
import ora8 from "ora";
|
|
5231
5272
|
import { execFile as execFile4 } from "child_process";
|
|
5232
|
-
import
|
|
5273
|
+
import fs15 from "fs";
|
|
5233
5274
|
import path16 from "path";
|
|
5234
5275
|
var DEPENDENCY_GROUPS = [
|
|
5235
5276
|
{
|
|
@@ -5264,7 +5305,7 @@ function registerSetupCommand(program2) {
|
|
|
5264
5305
|
async function executeSetup(options) {
|
|
5265
5306
|
console.log(chalk9.cyan("\n QAT \u4F9D\u8D56\u5B89\u88C5\u5668\n"));
|
|
5266
5307
|
const projectInfo = detectProject();
|
|
5267
|
-
if (!
|
|
5308
|
+
if (!fs15.existsSync(path16.join(process.cwd(), "package.json"))) {
|
|
5268
5309
|
throw new Error("\u672A\u627E\u5230 package.json\uFF0C\u8BF7\u5728\u9879\u76EE\u6839\u76EE\u5F55\u6267\u884C\u6B64\u547D\u4EE4");
|
|
5269
5310
|
}
|
|
5270
5311
|
if (projectInfo.frameworkConfidence > 0) {
|
|
@@ -5299,7 +5340,7 @@ async function executeSetup(options) {
|
|
|
5299
5340
|
]);
|
|
5300
5341
|
if (chooseDir !== "root") {
|
|
5301
5342
|
installDir = path16.join(process.cwd(), chooseDir);
|
|
5302
|
-
if (!
|
|
5343
|
+
if (!fs15.existsSync(path16.join(installDir, "package.json"))) {
|
|
5303
5344
|
throw new Error(`${chooseDir} \u4E0B\u6CA1\u6709 package.json`);
|
|
5304
5345
|
}
|
|
5305
5346
|
}
|
|
@@ -5619,7 +5660,7 @@ async function executeChange(_options) {
|
|
|
5619
5660
|
}
|
|
5620
5661
|
|
|
5621
5662
|
// src/cli.ts
|
|
5622
|
-
var VERSION = "0.
|
|
5663
|
+
var VERSION = "0.3.02";
|
|
5623
5664
|
function printLogo() {
|
|
5624
5665
|
const logo = `
|
|
5625
5666
|
${chalk12.bold.cyan(" ___ _ _ _ _ _____ _ _ ")}
|