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/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 chalk3 from "chalk";
14
+ import chalk2 from "chalk";
15
15
  import inquirer from "inquirer";
16
- import ora2 from "ora";
17
- import fs7 from "fs";
18
- import path7 from "path";
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/template.ts
1700
+ // src/services/global-config.ts
1794
1701
  import fs5 from "fs";
1795
1702
  import path5 from "path";
1796
- import { fileURLToPath } from "url";
1797
- import Handlebars from "handlebars";
1798
- var __filename = fileURLToPath(import.meta.url);
1799
- var __dirname = path5.dirname(__filename);
1800
- var TEMPLATE_MAP = {
1801
- unit: "unit-test.hbs",
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
- if (fullContext.emits && fullContext.emits.length > 0) {
1882
- fullContext.isVueComponent = true;
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 loadTemplate(type) {
1887
- const custom = customTemplates.get(type);
1888
- if (custom) return custom;
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
- return getBuiltinTemplate(type);
1725
+ fs5.writeFileSync(AI_CONFIG_PATH, JSON.stringify(config, null, 2), "utf-8");
1896
1726
  }
1897
- function getTemplateDir() {
1898
- const projectTemplates = path5.join(process.cwd(), "templates");
1899
- if (fs5.existsSync(projectTemplates)) {
1900
- return projectTemplates;
1901
- }
1902
- return path5.join(__dirname, "..", "templates");
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
- describe('{{name}}', () => {
1919
- {{#if hasAnalysis}}
1920
- {{#each functionExports}}
1921
- describe('{{name}}()', () => {
1922
- {{#if isAsync}}
1923
- it('should resolve successfully', async () => {
1924
- const result = await {{name}}({{#each params}}{{#unless @first}}, {{/unless}}{{this}}: undefined{{/each}});
1925
- expect(result).toBeDefined();
1926
- });
1927
- {{else}}
1928
- it('should return a value', () => {
1929
- const result = {{name}}({{#each params}}{{#unless @first}}, {{/unless}}undefined as any{{/each}});
1930
- expect(result).toBeDefined();
1931
- });
1932
- {{/if}}
1933
- {{#if returnType}}
1934
- it('should return correct type', () => {
1935
- {{#if isAsync}}
1936
- const result = await {{name}}({{#each params}}{{#unless @first}}, {{/unless}}undefined as any{{/each}});
1937
- {{else}}
1938
- const result = {{name}}({{#each params}}{{#unless @first}}, {{/unless}}undefined as any{{/each}});
1939
- {{/if}}
1940
- expect(result).toBeDefined();
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
- test('should not have memory leaks', async ({ page }) => {
2203
- await page.goto('/');
2204
-
2205
- const metrics = await page.metrics();
2206
- expect(metrics.JSHeapUsedSize).toBeLessThan(50 * 1024 * 1024); // 50MB
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
- // src/commands/init.ts
2266
- var TEST_TYPE_DIR = {
2267
- unit: "tests/unit",
2268
- component: "tests/component",
2269
- e2e: "tests/e2e",
2270
- api: "tests/api",
2271
- visual: "tests/visual",
2272
- performance: "tests/e2e"
2273
- };
2274
- function inferTestType(filePath) {
2275
- const normalized = filePath.replace(/\\/g, "/").toLowerCase();
2276
- if (/\/api\//.test(normalized) || /api\.(ts|js)$/.test(normalized)) {
2277
- return "api";
2278
- }
2279
- if (normalized.endsWith(".vue")) {
2280
- return "component";
2281
- }
2282
- if (/\/composables?\//.test(normalized) || /\/hooks?\//.test(normalized) || /^use[A-Z]/.test(path7.basename(filePath))) {
2283
- return "unit";
2284
- }
2285
- if (/\/(utils|helpers|services|lib)\//.test(normalized)) {
2286
- return "unit";
2287
- }
2288
- return "unit";
2289
- }
2290
- function registerInitCommand(program2) {
2291
- program2.command("init").description("\u521D\u59CB\u5316\u6D4B\u8BD5\u9879\u76EE - \u68C0\u6D4B\u9879\u76EE\u3001\u751F\u6210\u914D\u7F6E\u3001\u81EA\u52A8\u521B\u5EFA\u6D4B\u8BD5\u7528\u4F8B").option("-f, --force", "\u5F3A\u5236\u8986\u76D6\u5DF2\u6709\u914D\u7F6E\u6587\u4EF6").action(async (options) => {
2292
- try {
2293
- await executeInit(options);
2294
- } catch (error) {
2295
- console.error(chalk3.red(`
2296
- \u2717 ${error instanceof Error ? error.message : String(error)}
2297
- `));
2298
- process.exit(1);
2299
- }
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
- async function executeInit(options) {
2303
- const spinner = ora2("\u6B63\u5728\u68C0\u6D4B\u9879\u76EE\u7ED3\u6784...").start();
2304
- const projectInfo = detectProject();
2305
- spinner.stop();
2306
- displayProjectInfo(projectInfo);
2307
- if (!projectInfo.isVue) {
2308
- const { proceed } = await inquirer.prompt([
2309
- {
2310
- type: "confirm",
2311
- name: "proceed",
2312
- message: "\u672A\u68C0\u6D4B\u5230 Vue \u9879\u76EE\uFF0C\u662F\u5426\u7EE7\u7EED\u521D\u59CB\u5316\uFF1F",
2313
- default: false
2314
- }
2315
- ]);
2316
- if (!proceed) {
2317
- console.log(chalk3.gray("\n \u5DF2\u53D6\u6D88\u521D\u59CB\u5316\n"));
2318
- return;
2319
- }
2320
- }
2321
- let globalAI = loadGlobalAIConfig();
2322
- if (!globalAI) {
2323
- console.log(chalk3.cyan(" AI \u6A21\u578B\u914D\u7F6E (\u9996\u6B21\u4F7F\u7528\u9700\u914D\u7F6E\uFF0C\u4E4B\u540E\u53EF\u901A\u8FC7 qat change \u4FEE\u6539)\n"));
2324
- globalAI = await promptAIConfig();
2325
- saveGlobalAIConfig(globalAI);
2326
- console.log(chalk3.gray(` \u914D\u7F6E\u5DF2\u4FDD\u5B58\u81F3 ${getAIConfigPath()}
2327
- `));
2328
- } else {
2329
- console.log(chalk3.green(` \u2713 \u5F53\u524D AI \u6A21\u578B: ${chalk3.white(globalAI.model)} @ ${chalk3.gray(globalAI.baseUrl)} (${maskApiKey(globalAI.apiKey)})
2330
- `));
2331
- }
2332
- const aiConfig = toAIConfig(globalAI);
2333
- if (aiConfig.apiKey || aiConfig.baseUrl) {
2334
- const testSpinner = ora2(`\u6B63\u5728\u6D4B\u8BD5 AI \u8FDE\u901A\u6027 (${globalAI.model})...`).start();
2335
- try {
2336
- const result = await testAIConnection(aiConfig);
2337
- if (result.ok) {
2338
- testSpinner.succeed(`AI \u8FDE\u901A\u6B63\u5E38 ${chalk3.gray(`${globalAI.model} (${result.latencyMs}ms)`)}`);
2339
- } else {
2340
- testSpinner.fail(`AI \u8FDE\u901A\u5F02\u5E38: ${result.message}`);
2341
- console.log(chalk3.yellow(" \u53EF\u8FD0\u884C qat change \u4FEE\u6539 AI \u914D\u7F6E\u3002"));
2342
- }
2343
- } catch (error) {
2344
- testSpinner.fail(`AI \u8FDE\u901A\u6D4B\u8BD5\u5931\u8D25: ${error instanceof Error ? error.message : String(error)}`);
2345
- }
2346
- }
2347
- const config = buildProjectConfig(projectInfo);
2348
- let configPath;
2349
- const existingConfigPath = path7.join(process.cwd(), "qat.config.js");
2350
- const existingTsPath = path7.join(process.cwd(), "qat.config.ts");
2351
- const configExists = fs7.existsSync(existingConfigPath) || fs7.existsSync(existingTsPath);
2352
- if (configExists && !options.force) {
2353
- const { overwrite } = await inquirer.prompt([
2354
- {
2355
- type: "confirm",
2356
- name: "overwrite",
2357
- message: "\u914D\u7F6E\u6587\u4EF6 qat.config.js \u5DF2\u5B58\u5728\uFF0C\u662F\u5426\u8986\u76D6\uFF1F",
2358
- default: true
2359
- }
2360
- ]);
2361
- if (!overwrite) {
2362
- console.log(chalk3.gray(" \u4FDD\u7559\u73B0\u6709\u914D\u7F6E\u6587\u4EF6\uFF0C\u7EE7\u7EED\u540E\u7EED\u6B65\u9AA4..."));
2363
- configPath = existingConfigPath;
2364
- } else {
2365
- const fileSpinner = ora2("\u6B63\u5728\u8986\u76D6\u914D\u7F6E\u6587\u4EF6...").start();
2366
- try {
2367
- configPath = await writeConfigFile(process.cwd(), config, true);
2368
- fileSpinner.succeed("\u914D\u7F6E\u6587\u4EF6\u5DF2\u8986\u76D6");
2369
- } catch (error) {
2370
- fileSpinner.fail("\u914D\u7F6E\u6587\u4EF6\u8986\u76D6\u5931\u8D25");
2371
- throw error;
2372
- }
2373
- }
2374
- } else {
2375
- const fileSpinner = ora2("\u6B63\u5728\u751F\u6210\u914D\u7F6E\u6587\u4EF6...").start();
2376
- try {
2377
- configPath = await writeConfigFile(process.cwd(), config, options.force);
2378
- fileSpinner.succeed("\u914D\u7F6E\u6587\u4EF6\u5DF2\u751F\u6210");
2379
- } catch (error) {
2380
- fileSpinner.fail("\u914D\u7F6E\u6587\u4EF6\u751F\u6210\u5931\u8D25");
2381
- throw error;
2382
- }
2383
- }
2384
- const dirSpinner = ora2("\u6B63\u5728\u521B\u5EFA\u6D4B\u8BD5\u76EE\u5F55...").start();
2385
- const createdDirs = createTestDirectories(config);
2386
- dirSpinner.succeed("\u6D4B\u8BD5\u76EE\u5F55\u5DF2\u521B\u5EFA");
2387
- if (config.mock?.enabled !== false) {
2388
- const mockDir = config.mock?.routesDir || DEFAULT_CONFIG.mock.routesDir;
2389
- initMockRoutesDir(mockDir);
2390
- const srcDir = config.project?.srcDir || "src";
2391
- const apiCalls = scanAPICalls(srcDir);
2392
- if (apiCalls.length > 0) {
2393
- const mockRoutes = generateMockRoutesFromAPICalls(apiCalls);
2394
- const mockFilePath = path7.join(process.cwd(), mockDir, "auto-generated.json");
2395
- if (!fs7.existsSync(mockFilePath)) {
2396
- fs7.writeFileSync(mockFilePath, JSON.stringify(mockRoutes, null, 2), "utf-8");
2397
- console.log(chalk3.green(` \u81EA\u52A8\u53D1\u73B0 ${apiCalls.length} \u4E2A API \u63A5\u53E3\uFF0C\u5DF2\u751F\u6210 Mock \u8DEF\u7531`));
2398
- }
2399
- } else {
2400
- console.log(chalk3.gray(" \u672A\u53D1\u73B0 API \u8C03\u7528\uFF0C\u5DF2\u751F\u6210\u793A\u4F8B Mock \u8DEF\u7531"));
2401
- }
2402
- }
2403
- const useAI = isAIAvailable(aiConfig);
2404
- const generatedFiles = await autoGenerateTests(config, projectInfo, aiConfig, useAI);
2405
- displayResult(configPath, createdDirs, generatedFiles, projectInfo);
2406
- }
2407
- async function promptAIConfig() {
2408
- const answers = await inquirer.prompt([
2409
- {
2410
- type: "input",
2411
- name: "apiKey",
2412
- message: "API Key (Ollama \u672C\u5730\u53EF\u7559\u7A7A):",
2413
- default: ""
2414
- },
2415
- {
2416
- type: "input",
2417
- name: "baseUrl",
2418
- message: "API Base URL:",
2419
- default: "https://api.deepseek.com/v1",
2420
- validate: (input) => {
2421
- if (!input.trim()) return "Base URL \u4E0D\u80FD\u4E3A\u7A7A";
2422
- if (!input.trim().startsWith("http")) return "URL \u5FC5\u987B\u4EE5 http(s):// \u5F00\u5934";
2423
- return true;
2424
- }
2425
- },
2426
- {
2427
- type: "input",
2428
- name: "model",
2429
- message: "\u6A21\u578B\u540D\u79F0:",
2430
- default: "deepseek-chat",
2431
- validate: (input) => {
2432
- if (!input.trim()) return "\u6A21\u578B\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A";
2433
- return true;
2434
- }
2435
- }
2436
- ]);
2437
- return {
2438
- provider: "openai",
2439
- apiKey: answers.apiKey?.trim() || "",
2440
- baseUrl: answers.baseUrl?.trim() || "https://api.deepseek.com/v1",
2441
- model: answers.model?.trim() || "deepseek-chat"
2442
- };
2443
- }
2444
- function buildProjectConfig(projectInfo) {
2445
- return {
2446
- project: {
2447
- framework: projectInfo.framework,
2448
- uiLibrary: projectInfo.uiLibrary !== "none" ? projectInfo.uiLibrary : void 0,
2449
- monorepo: projectInfo.monorepo !== "none" ? projectInfo.monorepo : void 0,
2450
- vite: projectInfo.isVite,
2451
- srcDir: projectInfo.srcDir,
2452
- appDir: projectInfo.appDirs.length > 0 ? projectInfo.appDirs[0] : void 0
2453
- },
2454
- vitest: {
2455
- enabled: true,
2456
- coverage: true,
2457
- globals: true,
2458
- environment: "happy-dom"
2459
- },
2460
- playwright: {
2461
- enabled: true,
2462
- browsers: ["chromium"],
2463
- baseURL: "http://localhost:5173",
2464
- screenshot: "only-on-failure"
2465
- },
2466
- visual: {
2467
- enabled: true,
2468
- threshold: 0.1,
2469
- baselineDir: "tests/visual/baseline",
2470
- diffDir: "tests/visual/diff"
2471
- },
2472
- lighthouse: {
2473
- enabled: true,
2474
- urls: ["http://localhost:5173"],
2475
- runs: 3,
2476
- thresholds: {
2477
- performance: 80,
2478
- accessibility: 90
2479
- }
2480
- },
2481
- mock: {
2482
- enabled: true,
2483
- port: 3456,
2484
- routesDir: "tests/mock/routes"
2485
- }
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
- async function autoGenerateTests(config, projectInfo, aiConfig, useAI) {
2489
- const srcDir = config.project?.srcDir || "src";
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
- async function selectTargetFiles(allTargets) {
2554
- const testTypeLabels = {
2555
- unit: chalk3.blue("[unit]"),
2556
- component: chalk3.magenta("[comp]"),
2557
- e2e: chalk3.green("[e2e]"),
2558
- api: chalk3.yellow("[api]"),
2559
- visual: chalk3.cyan("[visual]"),
2560
- performance: chalk3.gray("[perf]")
2561
- };
2562
- const choices = allTargets.map(({ filePath, testType }) => ({
2563
- name: `${testTypeLabels[testType]} ${filePath}`,
2564
- value: filePath,
2565
- short: filePath
2566
- }));
2567
- choices.push({
2568
- name: chalk3.gray("\u270E \u624B\u52A8\u8F93\u5165\u6587\u4EF6/\u76EE\u5F55\u8DEF\u5F84"),
2569
- value: "__manual__",
2570
- short: "\u624B\u52A8\u8F93\u5165"
2571
- });
2572
- const { selected } = await inquirer.prompt([
2573
- {
2574
- type: "checkbox",
2575
- name: "selected",
2576
- message: "\u9009\u62E9\u8981\u751F\u6210\u6D4B\u8BD5\u7528\u4F8B\u7684\u6587\u4EF6 (\u7A7A\u683C\u9009\u62E9/\u53D6\u6D88\uFF0C\u56DE\u8F66\u786E\u8BA4):",
2577
- choices,
2578
- pageSize: 15
2579
- }
2580
- ]);
2581
- const manualPaths = [];
2582
- if (selected.includes("__manual__")) {
2583
- const { manualInput } = await inquirer.prompt([
2584
- {
2585
- type: "input",
2586
- name: "manualInput",
2587
- message: "\u8F93\u5165\u6587\u4EF6\u6216\u76EE\u5F55\u8DEF\u5F84\uFF08\u591A\u4E2A\u7528\u9017\u53F7\u5206\u9694\uFF09:",
2588
- default: "",
2589
- filter: (input) => input.split(",").map((s) => s.trim()).filter(Boolean)
2590
- }
2591
- ]);
2592
- for (const p of manualInput) {
2593
- const resolved = path7.resolve(process.cwd(), p);
2594
- if (fs7.existsSync(resolved)) {
2595
- const stat = fs7.statSync(resolved);
2596
- if (stat.isDirectory()) {
2597
- const dirFiles = walkDirForTestableFiles(resolved);
2598
- manualPaths.push(...dirFiles);
2599
- } else if (stat.isFile()) {
2600
- manualPaths.push(p.replace(/\\/g, "/"));
2601
- }
2602
- } else {
2603
- console.log(chalk3.yellow(` \u8DEF\u5F84\u4E0D\u5B58\u5728\uFF0C\u5DF2\u8DF3\u8FC7: ${p}`));
2604
- }
2605
- }
2606
- }
2607
- const selectedPaths = selected.filter((s) => s !== "__manual__");
2608
- const selectedSet = new Set(selectedPaths);
2609
- const result = allTargets.filter((t) => selectedSet.has(t.filePath));
2610
- const existingPaths = new Set(result.map((t) => t.filePath));
2611
- for (const mp of manualPaths) {
2612
- if (!existingPaths.has(mp)) {
2613
- result.push({ filePath: mp, testType: inferTestType(mp) });
2614
- }
2615
- }
2616
- return result;
2461
+ function toPascalCase(str) {
2462
+ const camel = toCamelCase(str);
2463
+ return camel.charAt(0).toUpperCase() + camel.slice(1);
2617
2464
  }
2618
- function walkDirForTestableFiles(dir) {
2619
- const files = [];
2620
- try {
2621
- const entries = fs7.readdirSync(dir, { withFileTypes: true });
2622
- for (const entry of entries) {
2623
- if (entry.name === "node_modules" || entry.name === "dist" || entry.name.startsWith(".")) continue;
2624
- const fullPath = path7.join(dir, entry.name);
2625
- if (entry.isDirectory()) {
2626
- files.push(...walkDirForTestableFiles(fullPath));
2627
- } else if (entry.isFile() && /\.(vue|ts|js)$/.test(entry.name)) {
2628
- files.push(path7.relative(process.cwd(), fullPath).replace(/\\/g, "/"));
2629
- }
2630
- }
2631
- } catch {
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
- return files;
2634
- }
2635
- async function generateTestForTarget(testType, targetPath, config, projectInfo, aiConfig, useAI) {
2636
- const basename = path7.basename(targetPath, path7.extname(targetPath));
2637
- const name = basename.replace(/[^a-zA-Z0-9]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "") || `${testType}-test`;
2638
- const outputDir = TEST_TYPE_DIR[testType];
2639
- const fileName = `${name}.${testType === "e2e" ? "spec" : "test"}.ts`;
2640
- const filePath = path7.join(process.cwd(), outputDir, fileName);
2641
- if (fs7.existsSync(filePath)) return null;
2642
- let content;
2643
- let reviewEntry;
2644
- if (useAI) {
2645
- const result = await generateWithAIAndReview(testType, name, targetPath, aiConfig, projectInfo);
2646
- content = result.code;
2647
- reviewEntry = result.reviewEntry;
2648
- } else {
2649
- const analysis = analyzeFile(targetPath);
2650
- content = renderTemplate(testType, {
2651
- name,
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
- framework: projectInfo.framework,
2654
- vueVersion: projectInfo.vueVersion,
2655
- typescript: projectInfo.typescript,
2656
- uiLibrary: projectInfo.uiLibrary,
2657
- extraImports: projectInfo.componentTestSetup?.extraImports,
2658
- globalPlugins: projectInfo.componentTestSetup?.globalPlugins,
2659
- globalStubs: projectInfo.componentTestSetup?.globalStubs,
2660
- mountOptions: projectInfo.componentTestSetup?.mountOptions,
2661
- exports: analysis.exports.map((e) => ({
2662
- name: e.name,
2663
- kind: e.kind,
2664
- params: e.params,
2665
- isAsync: e.isAsync,
2666
- returnType: e.returnType
2667
- })),
2668
- props: analysis.vueAnalysis?.props.map((p) => ({
2669
- name: p.name,
2670
- type: p.type,
2671
- required: p.required,
2672
- defaultValue: p.defaultValue
2673
- })),
2674
- emits: analysis.vueAnalysis?.emits.map((e) => ({
2675
- name: e.name,
2676
- params: e.params
2677
- })),
2678
- methods: analysis.vueAnalysis?.methods,
2679
- computed: analysis.vueAnalysis?.computed
2680
- });
2681
- }
2682
- if (!fs7.existsSync(path7.join(process.cwd(), outputDir))) {
2683
- fs7.mkdirSync(path7.join(process.cwd(), outputDir), { recursive: true });
2684
- }
2685
- fs7.writeFileSync(filePath, content, "utf-8");
2686
- return {
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 displayProjectInfo(info) {
2745
- console.log(chalk3.cyan("\n \u9879\u76EE\u68C0\u6D4B\u7ED3\u679C:"));
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 items = [
2748
- ["\u9879\u76EE\u540D\u79F0", info.name],
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
- console.log();
2772
- }
2773
- function createTestDirectories(config) {
2774
- const dirs = [];
2775
- const dirMap = {
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
- return dirs;
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 fs9 from "fs";
3173
- import path11 from "path";
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
- const child = execFile(npx, args, {
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 output = stdout || stderr || "";
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
- const parsed = parseVitestJSONOutput(output);
3260
- resolve(parsed);
3261
- } catch {
3262
- if (output) {
3263
- resolve(parseVitestTextOutput(output, !!error));
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
- resolve({ success: !error, suites: [] });
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 parseVitestJSONOutput(output) {
3277
- const jsonMatch = output.match(/\{[\s\S]*"testResults"[\s\S]*\}/);
3278
- if (!jsonMatch) {
3279
- return parseVitestTextOutput(output, false);
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
- try {
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
- const suite = {
3287
- name: path9.basename(fileResult.name || fileResult.assertionResults?.[0]?.ancestorTitles?.[0] || "unknown"),
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.assertionResults || []).map((assertion) => ({
3293
- name: assertion.title || assertion.fullName || "unknown",
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[2], 10);
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 calculateAverageScores(results) {
3813
- const valid = results.filter((r) => !r.error);
3814
- if (valid.length === 0) {
3815
- return { performance: 0, accessibility: 0, bestPractices: 0, seo: 0 };
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
- return {
3818
- performance: Math.round(valid.reduce((s, r) => s + r.scores.performance, 0) / valid.length),
3819
- accessibility: Math.round(valid.reduce((s, r) => s + r.scores.accessibility, 0) / valid.length),
3820
- bestPractices: Math.round(valid.reduce((s, r) => s + r.scores.bestPractices, 0) / valid.length),
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 calculateAverageMetrics(results) {
3825
- const valid = results.filter((r) => !r.error);
3826
- const scores = calculateAverageScores(results);
3827
- const lcpValues = valid.map((r) => r.metrics.lcp).filter((v) => v !== void 0);
3828
- const fidValues = valid.map((r) => r.metrics.fid).filter((v) => v !== void 0);
3829
- const clsValues = valid.map((r) => r.metrics.cls).filter((v) => v !== void 0);
3830
- return {
3831
- ...scores,
3832
- lcp: lcpValues.length > 0 ? lcpValues.reduce((s, v) => s + v, 0) / lcpValues.length : void 0,
3833
- fid: fidValues.length > 0 ? fidValues.reduce((s, v) => s + v, 0) / fidValues.length : void 0,
3834
- cls: clsValues.length > 0 ? clsValues.reduce((s, v) => s + v, 0) / clsValues.length : void 0
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 TYPE_LABELS = {
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) => TYPE_LABELS[t]).join("\u3001")} \u9700\u8981\u9879\u76EE\u670D\u52A1\u8FD0\u884C\uFF0C\u5982\u4F55\u5904\u7406\uFF1F`,
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 = TYPE_LABELS[testType] || testType;
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
- displaySummary(results, config);
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 = TYPE_LABELS[testType];
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: `${TYPE_LABELS[t]} (${chalk5.gray(TYPE_RUNNERS[t])})`,
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
- async function displaySummary(results, config) {
4196
- let totalSuites = 0;
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") totalPassed++;
4207
- else if (test.status === "failed") totalFailed++;
4208
- else if (test.status === "skipped") totalSkipped++;
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.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"));
4215
- if (totalFailed > 0) {
4216
- console.log(chalk5.red(" \u2717 \u6D4B\u8BD5\u5931\u8D25"));
4217
- } else if (total === 0) {
4218
- console.log(chalk5.yellow(" \u26A0 \u6CA1\u6709\u53D1\u73B0\u6D4B\u8BD5\u7528\u4F8B"));
4219
- } else {
4220
- console.log(chalk5.green(" \u2713 \u5168\u90E8\u901A\u8FC7"));
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
- console.log(` ${chalk5.white("\u6D4B\u8BD5\u7528\u4F8B:")} ${total} total`);
4224
- if (totalPassed > 0) console.log(` ${chalk5.green(" \u901A\u8FC7:")} ${totalPassed}`);
4225
- if (totalFailed > 0) console.log(` ${chalk5.red(" \u5931\u8D25:")} ${totalFailed}`);
4226
- if (totalSkipped > 0) console.log(` ${chalk5.yellow(" \u8DF3\u8FC7:")} ${totalSkipped}`);
4227
- console.log(` ${chalk5.gray(" \u5957\u4EF6:")} ${totalSuites}`);
4228
- console.log(` ${chalk5.gray(" \u8017\u65F6:")} ${formatDuration(totalDuration)}`);
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(` \u2717 ${suite.name} > ${test.name}`));
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
- if (totalFailed > 0 && isAIAvailable(config.ai)) {
4273
- await aiAnalyzeFailures(results, config.ai);
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 formatDuration(ms) {
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 = fs9.existsSync(path11.join(process.cwd(), "pnpm-lock.yaml"));
4330
- const hasYarn = fs9.existsSync(path11.join(process.cwd(), "yarn.lock"));
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 = path11.join(process.cwd(), RESULTS_DIR);
4360
- if (!fs9.existsSync(resultsPath)) {
4361
- fs9.mkdirSync(resultsPath, { recursive: true });
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 = path11.join(resultsPath, fileName);
4592
+ const filePath = path12.join(resultsPath, fileName);
4366
4593
  const data = {
4367
4594
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
4368
4595
  results
4369
4596
  };
4370
- fs9.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf-8");
4597
+ fs11.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf-8");
4371
4598
  try {
4372
- const files = fs9.readdirSync(resultsPath).filter((f) => f.startsWith("result-") && f.endsWith(".json")).sort();
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
- fs9.unlinkSync(path11.join(resultsPath, oldest));
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 fs11 from "fs";
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
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\u6210HTML\u62A5\u544A...";
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 (!fs11.existsSync(resultsPath)) {
4840
+ if (!fs12.existsSync(resultsPath)) {
4810
4841
  return results;
4811
4842
  }
4812
- const files = fs11.readdirSync(resultsPath).filter((f) => f.endsWith(".json")).sort().reverse();
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(fs11.readFileSync(latestFile, "utf-8"));
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 (!fs11.existsSync(resultsPath)) {
4830
- fs11.mkdirSync(resultsPath, { recursive: true });
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
- fs11.writeFileSync(filePath, JSON.stringify(reportData, null, 2), "utf-8");
4836
- const files = fs11.readdirSync(resultsPath).filter((f) => f.startsWith("result-") && f.endsWith(".json")).sort();
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
- fs11.unlinkSync(path13.join(resultsPath, oldest));
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("\u2717") : chalk7.green("\u2713");
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 fs12 from "fs";
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 (!fs12.existsSync(baselinePath)) {
4936
+ if (!fs13.existsSync(baselinePath)) {
4896
4937
  throw new Error(`\u57FA\u7EBF\u56FE\u7247\u4E0D\u5B58\u5728: ${baselinePath}`);
4897
4938
  }
4898
- if (!fs12.existsSync(currentPath)) {
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(fs12.readFileSync(baselinePath));
4902
- const current = PNG.sync.read(fs12.readFileSync(currentPath));
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 (!fs12.existsSync(diffDir)) {
4932
- fs12.mkdirSync(diffDir, { recursive: true });
4972
+ if (!fs13.existsSync(diffDir)) {
4973
+ fs13.mkdirSync(diffDir, { recursive: true });
4933
4974
  }
4934
- fs12.writeFileSync(diffOutputPath, PNG.sync.write(diff));
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 (!fs12.existsSync(currentPath)) {
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 (!fs12.existsSync(baselineDir)) {
4953
- fs12.mkdirSync(baselineDir, { recursive: true });
4993
+ if (!fs13.existsSync(baselineDir)) {
4994
+ fs13.mkdirSync(baselineDir, { recursive: true });
4954
4995
  }
4955
- fs12.copyFileSync(currentPath, baselinePath);
4996
+ fs13.copyFileSync(currentPath, baselinePath);
4956
4997
  return baselinePath;
4957
4998
  }
4958
4999
  function updateAllBaselines(currentDir, baselineDir) {
4959
5000
  const updated = [];
4960
- if (!fs12.existsSync(currentDir)) {
5001
+ if (!fs13.existsSync(currentDir)) {
4961
5002
  return updated;
4962
5003
  }
4963
- const files = fs12.readdirSync(currentDir).filter((f) => f.endsWith(".png"));
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 (!fs12.existsSync(baselineDirAbs)) {
4969
- fs12.mkdirSync(baselineDirAbs, { recursive: true });
5009
+ if (!fs13.existsSync(baselineDirAbs)) {
5010
+ fs13.mkdirSync(baselineDirAbs, { recursive: true });
4970
5011
  }
4971
- fs12.copyFileSync(currentPath, baselinePath);
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 (!fs12.existsSync(baselineDir)) {
5018
+ if (!fs13.existsSync(baselineDir)) {
4978
5019
  return 0;
4979
5020
  }
4980
- const files = fs12.readdirSync(baselineDir).filter((f) => f.endsWith(".png"));
5021
+ const files = fs13.readdirSync(baselineDir).filter((f) => f.endsWith(".png"));
4981
5022
  let count = 0;
4982
5023
  for (const file of files) {
4983
- fs12.unlinkSync(path14.join(baselineDir, file));
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 (!fs12.existsSync(diffDir)) {
5030
+ if (!fs13.existsSync(diffDir)) {
4990
5031
  return 0;
4991
5032
  }
4992
- const files = fs12.readdirSync(diffDir).filter((f) => f.endsWith(".png"));
5033
+ const files = fs13.readdirSync(diffDir).filter((f) => f.endsWith(".png"));
4993
5034
  let count = 0;
4994
5035
  for (const file of files) {
4995
- fs12.unlinkSync(path14.join(diffDir, file));
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 (!fs12.existsSync(currentDir)) {
5043
+ if (!fs13.existsSync(currentDir)) {
5003
5044
  return results;
5004
5045
  }
5005
- const currentFiles = fs12.readdirSync(currentDir).filter((f) => f.endsWith(".png"));
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 (!fs12.existsSync(baselinePath)) {
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 fs13 from "fs";
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 (fs13.existsSync(dir)) {
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 (!fs13.existsSync(d)) return;
5134
- const entries = fs13.readdirSync(d, { withFileTypes: true });
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 fs14 from "fs";
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 (!fs14.existsSync(path16.join(process.cwd(), "package.json"))) {
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 (!fs14.existsSync(path16.join(installDir, "package.json"))) {
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.2.98";
5663
+ var VERSION = "0.3.02";
5623
5664
  function printLogo() {
5624
5665
  const logo = `
5625
5666
  ${chalk12.bold.cyan(" ___ _ _ _ _ _____ _ _ ")}