qat-cli 0.2.98 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  **Quick Auto Testing — 面向 Vue 项目,集成 Vitest & Playwright,AI 驱动覆盖测试全流程**
6
6
 
7
- [![npm version](https://img.shields.io/badge/version-0.2.98-blue.svg)](https://www.npmjs.com/package/qat-cli)
7
+ [![npm version](https://img.shields.io/badge/version-0.3.01-blue.svg)](https://www.npmjs.com/package/qat-cli)
8
8
  [![Node.js](https://img.shields.io/badge/node-%3E%3D18.0.0-green.svg)](https://nodejs.org/)
9
9
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
10
10
  [![TypeScript](https://img.shields.io/badge/TypeScript-5.6-blue.svg)](https://www.typescriptlang.org/)
package/dist/cli.js CHANGED
@@ -3245,34 +3245,109 @@ function buildVitestArgs(options) {
3245
3245
  return args;
3246
3246
  }
3247
3247
  async function execVitest(args) {
3248
+ const os2 = await import("os");
3249
+ const fs15 = await import("fs");
3250
+ const tmpFile = path9.join(os2.tmpdir(), `qat-vitest-result-${Date.now()}.json`);
3251
+ const argsWithOutput = [...args, "--outputFile", tmpFile];
3248
3252
  return new Promise((resolve, reject) => {
3249
3253
  const npx = process.platform === "win32" ? "npx.cmd" : "npx";
3250
- const child = execFile(npx, args, {
3254
+ const child = execFile(npx, argsWithOutput, {
3251
3255
  cwd: process.cwd(),
3252
- env: { ...process.env, FORCE_COLOR: "0" },
3256
+ env: { ...process.env, FORCE_COLOR: "0", NO_COLOR: "1" },
3253
3257
  maxBuffer: 50 * 1024 * 1024,
3254
- // 50MB
3255
3258
  shell: true
3256
3259
  }, (error, stdout, stderr) => {
3257
- const output = stdout || stderr || "";
3260
+ const rawOutput = stdout || stderr || "";
3261
+ let jsonResult = null;
3258
3262
  try {
3259
- const parsed = parseVitestJSONOutput(output);
3260
- resolve(parsed);
3263
+ if (fs15.existsSync(tmpFile)) {
3264
+ jsonResult = fs15.readFileSync(tmpFile, "utf-8");
3265
+ fs15.unlinkSync(tmpFile);
3266
+ }
3261
3267
  } catch {
3262
- if (output) {
3263
- resolve(parseVitestTextOutput(output, !!error));
3268
+ }
3269
+ if (jsonResult) {
3270
+ try {
3271
+ const parsed = parseVitestJSONResult(jsonResult);
3272
+ resolve({ ...parsed, rawOutput });
3273
+ return;
3274
+ } catch {
3275
+ }
3276
+ }
3277
+ try {
3278
+ const parsed = parseVitestJSONOutput(rawOutput);
3279
+ resolve({ ...parsed, rawOutput });
3280
+ } catch {
3281
+ if (rawOutput) {
3282
+ resolve({ ...parseVitestTextOutput(rawOutput, !!error), rawOutput });
3264
3283
  } else if (error && error.message.includes("ENOENT")) {
3265
3284
  reject(new Error("\u672A\u627E\u5230 vitest\uFF0C\u8BF7\u786E\u4FDD\u5DF2\u5B89\u88C5: npm install -D vitest"));
3266
3285
  } else {
3267
- resolve({ success: !error, suites: [] });
3286
+ resolve({ success: !error, suites: [], rawOutput });
3268
3287
  }
3269
3288
  }
3270
3289
  });
3271
3290
  child.on("error", (err) => {
3291
+ try {
3292
+ fs15.unlinkSync(tmpFile);
3293
+ } catch {
3294
+ }
3272
3295
  reject(new Error(`Vitest \u6267\u884C\u5931\u8D25: ${err.message}`));
3273
3296
  });
3274
3297
  });
3275
3298
  }
3299
+ function parseVitestJSONResult(jsonStr) {
3300
+ const data = JSON.parse(jsonStr);
3301
+ const suites = [];
3302
+ if (data.testResults && Array.isArray(data.testResults)) {
3303
+ for (const fileResult of data.testResults) {
3304
+ const suiteTests = [];
3305
+ const assertions = fileResult.assertionResults || fileResult.tests || [];
3306
+ for (const assertion of assertions) {
3307
+ suiteTests.push({
3308
+ name: assertion.title || assertion.fullName || assertion.name || "unknown",
3309
+ file: fileResult.name || "unknown",
3310
+ status: mapVitestStatus(assertion.status),
3311
+ duration: assertion.duration || 0,
3312
+ error: assertion.failureMessages?.length ? { message: assertion.failureMessages[0] } : assertion.failureMessage ? { message: assertion.failureMessage } : void 0,
3313
+ retries: 0
3314
+ });
3315
+ }
3316
+ if (suiteTests.length === 0 && fileResult.numPassingTests !== void 0) {
3317
+ const counts = [
3318
+ { n: fileResult.numPassingTests || 0, s: "passed" },
3319
+ { n: fileResult.numFailingTests || 0, s: "failed" },
3320
+ { n: fileResult.numPendingTests || 0, s: "skipped" }
3321
+ ];
3322
+ for (const { n, s } of counts) {
3323
+ for (let i = 0; i < n; i++) {
3324
+ suiteTests.push({
3325
+ name: `${s} test ${i + 1}`,
3326
+ file: fileResult.name || "unknown",
3327
+ status: s,
3328
+ duration: 0,
3329
+ retries: 0
3330
+ });
3331
+ }
3332
+ }
3333
+ }
3334
+ suites.push({
3335
+ name: path9.basename(fileResult.name || "unknown"),
3336
+ file: fileResult.name || "unknown",
3337
+ type: "unit",
3338
+ status: mapVitestStatus(fileResult.status),
3339
+ duration: fileResult.duration || 0,
3340
+ tests: suiteTests
3341
+ });
3342
+ }
3343
+ }
3344
+ let coverage;
3345
+ if (data.coverageMap) {
3346
+ coverage = extractCoverage(data.coverageMap);
3347
+ }
3348
+ const success = data.success !== false && data.numFailedTests === void 0 ? suites.every((s) => s.status !== "failed") : (data.numFailedTests || 0) === 0;
3349
+ return { success, suites, coverage };
3350
+ }
3276
3351
  function parseVitestJSONOutput(output) {
3277
3352
  const jsonMatch = output.match(/\{[\s\S]*"testResults"[\s\S]*\}/);
3278
3353
  if (!jsonMatch) {
@@ -4515,6 +4590,7 @@ function aggregateResults(results) {
4515
4590
  pending: 0
4516
4591
  };
4517
4592
  const byType = {};
4593
+ let coverage;
4518
4594
  for (const result of results) {
4519
4595
  const typeKey = result.type;
4520
4596
  if (!byType[typeKey]) {
@@ -4538,6 +4614,16 @@ function aggregateResults(results) {
4538
4614
  }
4539
4615
  }
4540
4616
  }
4617
+ if (result.coverage) {
4618
+ if (!coverage) {
4619
+ coverage = { ...result.coverage };
4620
+ } else {
4621
+ coverage.lines = Math.max(coverage.lines, result.coverage.lines);
4622
+ coverage.statements = Math.max(coverage.statements, result.coverage.statements);
4623
+ coverage.functions = Math.max(coverage.functions, result.coverage.functions);
4624
+ coverage.branches = Math.max(coverage.branches, result.coverage.branches);
4625
+ }
4626
+ }
4541
4627
  }
4542
4628
  const totalDuration = results.reduce((sum, r) => sum + r.duration, 0);
4543
4629
  return {
@@ -4545,7 +4631,8 @@ function aggregateResults(results) {
4545
4631
  duration: totalDuration,
4546
4632
  results,
4547
4633
  summary,
4548
- byType
4634
+ byType,
4635
+ coverage: coverage?.lines ? coverage : void 0
4549
4636
  };
4550
4637
  }
4551
4638
  function formatDuration2(ms) {
@@ -4565,208 +4652,139 @@ function formatTimestamp(ts) {
4565
4652
  second: "2-digit"
4566
4653
  });
4567
4654
  }
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 ? `
4655
+ function pct(value) {
4656
+ return `${(value * 100).toFixed(1)}%`;
4657
+ }
4658
+ function renderCoverageMD(coverage) {
4659
+ return `
4660
+ ### \u8986\u76D6\u7387
4574
4661
 
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) {
4662
+ | \u6307\u6807 | \u8986\u76D6\u7387 | \u8FDB\u5EA6 |
4663
+ |------|--------|------|
4664
+ | \u8BED\u53E5 (Statements) | ${pct(coverage.statements)} | ${renderProgressBar(coverage.statements)} |
4665
+ | \u5206\u652F (Branches) | ${pct(coverage.branches)} | ${renderProgressBar(coverage.branches)} |
4666
+ | \u51FD\u6570 (Functions) | ${pct(coverage.functions)} | ${renderProgressBar(coverage.functions)} |
4667
+ | \u884C (Lines) | ${pct(coverage.lines)} | ${renderProgressBar(coverage.lines)} |
4668
+ `;
4669
+ }
4670
+ function renderProgressBar(value) {
4671
+ const filled = Math.round(value * 10);
4672
+ const empty = 10 - filled;
4673
+ return `${"\u2588".repeat(filled)}${"\u2591".repeat(empty)}`;
4674
+ }
4675
+ var TYPE_LABELS2 = {
4676
+ unit: "\u5355\u5143\u6D4B\u8BD5",
4677
+ component: "\u7EC4\u4EF6\u6D4B\u8BD5",
4678
+ e2e: "E2E \u6D4B\u8BD5",
4679
+ api: "API \u6D4B\u8BD5",
4680
+ visual: "\u89C6\u89C9\u56DE\u5F52\u6D4B\u8BD5",
4681
+ performance: "\u6027\u80FD\u6D4B\u8BD5"
4682
+ };
4683
+ function generateMDReport(data) {
4604
4684
  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>`;
4685
+ const rateIcon = parseFloat(passRate) >= 80 ? "\u2705" : parseFloat(passRate) >= 50 ? "\u26A0\uFE0F" : "\u274C";
4686
+ const lines = [];
4687
+ lines.push(`# QAT \u6D4B\u8BD5\u62A5\u544A`);
4688
+ lines.push("");
4689
+ lines.push(`> \u751F\u6210\u65F6\u95F4: ${formatTimestamp(data.timestamp)} | \u603B\u8017\u65F6: ${formatDuration2(data.duration)}`);
4690
+ lines.push("");
4691
+ lines.push(`## \u603B\u89C8`);
4692
+ lines.push("");
4693
+ lines.push(`| \u6307\u6807 | \u6570\u503C |`);
4694
+ lines.push(`|------|------|`);
4695
+ lines.push(`| \u901A\u8FC7\u7387 | ${rateIcon} **${passRate}%** |`);
4696
+ lines.push(`| \u603B\u7528\u4F8B | ${data.summary.total} |`);
4697
+ lines.push(`| \u2705 \u901A\u8FC7 | ${data.summary.passed} |`);
4698
+ if (data.summary.failed > 0) lines.push(`| \u274C \u5931\u8D25 | ${data.summary.failed} |`);
4699
+ if (data.summary.skipped > 0) lines.push(`| \u23ED\uFE0F \u8DF3\u8FC7 | ${data.summary.skipped} |`);
4700
+ if (data.summary.pending > 0) lines.push(`| \u23F3 \u5F85\u5B9A | ${data.summary.pending} |`);
4701
+ lines.push(`| \u23F1\uFE0F \u8017\u65F6 | ${formatDuration2(data.duration)} |`);
4702
+ lines.push("");
4703
+ if (Object.keys(data.byType).length > 0) {
4704
+ lines.push(`## \u6309\u7C7B\u578B\u7EDF\u8BA1`);
4705
+ lines.push("");
4706
+ lines.push(`| \u7C7B\u578B | \u901A\u8FC7 | \u5931\u8D25 | \u8DF3\u8FC7 | \u603B\u8BA1 | \u901A\u8FC7\u7387 |`);
4707
+ lines.push(`|------|------|------|------|------|--------|`);
4708
+ for (const [type, stats] of Object.entries(data.byType)) {
4709
+ const label = TYPE_LABELS2[type] || type;
4710
+ const rate = stats.total > 0 ? (stats.passed / stats.total * 100).toFixed(0) + "%" : "-";
4711
+ lines.push(`| ${label} | ${stats.passed} | ${stats.failed} | ${stats.skipped} | ${stats.total} | ${rate} |`);
4712
+ }
4713
+ lines.push("");
4714
+ }
4715
+ if (data.coverage) {
4716
+ lines.push(renderCoverageMD(data.coverage));
4717
+ lines.push("");
4718
+ }
4719
+ lines.push(`## \u6D4B\u8BD5\u8BE6\u60C5`);
4720
+ lines.push("");
4721
+ for (const result of data.results) {
4722
+ const typeLabel = TYPE_LABELS2[result.type] || result.type;
4723
+ const statusIcon = result.status === "passed" ? "\u2705" : result.status === "failed" ? "\u274C" : "\u26A0\uFE0F";
4724
+ lines.push(`### ${statusIcon} ${typeLabel}`);
4725
+ lines.push("");
4726
+ if (result.suites.length === 0) {
4727
+ lines.push(`*\u65E0\u6D4B\u8BD5\u7ED3\u679C*`);
4728
+ lines.push("");
4729
+ continue;
4730
+ }
4731
+ for (const suite of result.suites) {
4732
+ const suiteIcon = suite.status === "passed" ? "\u2705" : suite.status === "failed" ? "\u274C" : "\u26A0\uFE0F";
4733
+ lines.push(`#### ${suiteIcon} ${suite.name}`);
4734
+ lines.push("");
4735
+ lines.push(`- \u6587\u4EF6: \`${suite.file}\``);
4736
+ lines.push(`- \u8017\u65F6: ${formatDuration2(suite.duration)}`);
4737
+ lines.push("");
4738
+ if (suite.tests.length > 0) {
4739
+ lines.push(`| \u72B6\u6001 | \u6D4B\u8BD5\u540D\u79F0 | \u8017\u65F6 |`);
4740
+ lines.push(`|------|----------|------|`);
4741
+ for (const test of suite.tests) {
4742
+ const testIcon = test.status === "passed" ? "\u2705" : test.status === "failed" ? "\u274C" : test.status === "skipped" ? "\u23ED\uFE0F" : "\u23F3";
4743
+ const name = test.error ? `**${test.name}**` : test.name;
4744
+ lines.push(`| ${testIcon} | ${name} | ${formatDuration2(test.duration)} |`);
4745
+ }
4746
+ lines.push("");
4747
+ }
4748
+ const failedTests = suite.tests.filter((t) => t.status === "failed" && t.error);
4749
+ if (failedTests.length > 0) {
4750
+ lines.push(`<details>`);
4751
+ lines.push(`<summary>\u274C \u5931\u8D25\u8BE6\u60C5 (${failedTests.length})</summary>`);
4752
+ lines.push("");
4753
+ for (const test of failedTests) {
4754
+ lines.push(`**${test.name}**`);
4755
+ lines.push("```");
4756
+ lines.push(test.error.message);
4757
+ if (test.error.stack) {
4758
+ lines.push(test.error.stack);
4759
+ }
4760
+ if (test.error.expected && test.error.actual) {
4761
+ lines.push(`Expected: ${test.error.expected}`);
4762
+ lines.push(`Actual: ${test.error.actual}`);
4763
+ }
4764
+ lines.push("```");
4765
+ lines.push("");
4766
+ }
4767
+ lines.push(`</details>`);
4768
+ lines.push("");
4769
+ }
4770
+ }
4771
+ }
4772
+ lines.push("---");
4773
+ lines.push("");
4774
+ lines.push(`*\u7531 QAT \u81EA\u52A8\u5316\u6D4B\u8BD5\u5DE5\u5177\u751F\u6210 | ${formatTimestamp(data.timestamp)}*`);
4775
+ return lines.join("\n");
4760
4776
  }
4761
4777
  function writeReportToDisk(data, outputDir) {
4762
- const html = generateHTMLReport(data);
4778
+ const md = generateMDReport(data);
4763
4779
  const dir = path12.resolve(outputDir);
4764
4780
  if (!fs10.existsSync(dir)) {
4765
4781
  fs10.mkdirSync(dir, { recursive: true });
4766
4782
  }
4767
- const indexPath = path12.join(dir, "index.html");
4768
- fs10.writeFileSync(indexPath, html, "utf-8");
4769
- return indexPath;
4783
+ const mdPath = path12.join(dir, "report.md");
4784
+ fs10.writeFileSync(mdPath, md, "utf-8");
4785
+ const jsonPath = path12.join(dir, "report.json");
4786
+ fs10.writeFileSync(jsonPath, JSON.stringify(data, null, 2), "utf-8");
4787
+ return mdPath;
4770
4788
  }
4771
4789
 
4772
4790
  // src/commands/report.ts
@@ -4793,7 +4811,7 @@ async function executeReport(options) {
4793
4811
  console.log(chalk7.gray("\n \u63D0\u793A: \u5148\u8FD0\u884C qat run \u751F\u6210\u6D4B\u8BD5\u7ED3\u679C\n"));
4794
4812
  return;
4795
4813
  }
4796
- spinner.text = "\u6B63\u5728\u751F\u6210HTML\u62A5\u544A...";
4814
+ spinner.text = "\u6B63\u5728\u751F\u6210\u6D4B\u8BD5\u62A5\u544A...";
4797
4815
  const reportData = aggregateResults(results);
4798
4816
  const reportPath = writeReportToDisk(reportData, outputDir);
4799
4817
  saveResultToHistory(reportData);
@@ -4841,29 +4859,39 @@ function saveResultToHistory(reportData) {
4841
4859
  }
4842
4860
  function displayReportResult(reportPath, data) {
4843
4861
  const relativePath = path13.relative(process.cwd(), reportPath);
4862
+ const passRate = data.summary.total > 0 ? (data.summary.passed / data.summary.total * 100).toFixed(1) : "0";
4844
4863
  console.log();
4845
4864
  console.log(chalk7.green(" \u2713 \u6D4B\u8BD5\u62A5\u544A\u5DF2\u751F\u6210"));
4846
4865
  console.log();
4847
4866
  console.log(chalk7.white(" \u62A5\u544A\u8DEF\u5F84:"), chalk7.cyan(relativePath));
4867
+ console.log(chalk7.white(" \u901A\u8FC7\u7387: "), parseFloat(passRate) >= 80 ? chalk7.green(`${passRate}%`) : parseFloat(passRate) >= 50 ? chalk7.yellow(`${passRate}%`) : chalk7.red(`${passRate}%`));
4848
4868
  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)));
4869
+ console.log(chalk7.white(" \u2705 \u901A\u8FC7: "), chalk7.green(String(data.summary.passed)));
4850
4870
  if (data.summary.failed > 0) {
4851
- console.log(chalk7.white(" \u5931\u8D25:"), chalk7.red(String(data.summary.failed)));
4871
+ console.log(chalk7.white(" \u274C \u5931\u8D25: "), chalk7.red(String(data.summary.failed)));
4852
4872
  }
4853
4873
  if (data.summary.skipped > 0) {
4854
- console.log(chalk7.white(" \u8DF3\u8FC7:"), chalk7.yellow(String(data.summary.skipped)));
4874
+ console.log(chalk7.white(" \u23ED\uFE0F \u8DF3\u8FC7: "), chalk7.yellow(String(data.summary.skipped)));
4855
4875
  }
4856
4876
  if (Object.keys(data.byType).length > 0) {
4857
4877
  console.log();
4858
4878
  console.log(chalk7.white(" \u6309\u7C7B\u578B:"));
4859
4879
  for (const [type, stats] of Object.entries(data.byType)) {
4860
4880
  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");
4881
+ const icon = stats.failed > 0 ? chalk7.red("\u274C") : chalk7.green("\u2705");
4862
4882
  console.log(` ${icon} ${type}: ${stats.passed}/${stats.total} (${rate}%)`);
4863
4883
  }
4864
4884
  }
4885
+ if (data.coverage) {
4886
+ console.log();
4887
+ console.log(chalk7.white(" \u8986\u76D6\u7387:"));
4888
+ 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))}`);
4889
+ }
4865
4890
  console.log();
4866
4891
  }
4892
+ function pct2(value) {
4893
+ return `${(value * 100).toFixed(1)}%`;
4894
+ }
4867
4895
  async function openReport(reportPath) {
4868
4896
  const { exec } = await import("child_process");
4869
4897
  const platform = process.platform;
@@ -5619,7 +5647,7 @@ async function executeChange(_options) {
5619
5647
  }
5620
5648
 
5621
5649
  // src/cli.ts
5622
- var VERSION = "0.2.98";
5650
+ var VERSION = "0.3.01";
5623
5651
  function printLogo() {
5624
5652
  const logo = `
5625
5653
  ${chalk12.bold.cyan(" ___ _ _ _ _ _____ _ _ ")}