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/dist/index.cjs CHANGED
@@ -1466,6 +1466,7 @@ function aggregateResults(results) {
1466
1466
  pending: 0
1467
1467
  };
1468
1468
  const byType = {};
1469
+ let coverage;
1469
1470
  for (const result of results) {
1470
1471
  const typeKey = result.type;
1471
1472
  if (!byType[typeKey]) {
@@ -1489,6 +1490,16 @@ function aggregateResults(results) {
1489
1490
  }
1490
1491
  }
1491
1492
  }
1493
+ if (result.coverage) {
1494
+ if (!coverage) {
1495
+ coverage = { ...result.coverage };
1496
+ } else {
1497
+ coverage.lines = Math.max(coverage.lines, result.coverage.lines);
1498
+ coverage.statements = Math.max(coverage.statements, result.coverage.statements);
1499
+ coverage.functions = Math.max(coverage.functions, result.coverage.functions);
1500
+ coverage.branches = Math.max(coverage.branches, result.coverage.branches);
1501
+ }
1502
+ }
1492
1503
  }
1493
1504
  const totalDuration = results.reduce((sum, r) => sum + r.duration, 0);
1494
1505
  return {
@@ -1496,7 +1507,8 @@ function aggregateResults(results) {
1496
1507
  duration: totalDuration,
1497
1508
  results,
1498
1509
  summary,
1499
- byType
1510
+ byType,
1511
+ coverage: coverage?.lines ? coverage : void 0
1500
1512
  };
1501
1513
  }
1502
1514
  function formatDuration(ms) {
@@ -1516,56 +1528,149 @@ function formatTimestamp(ts) {
1516
1528
  second: "2-digit"
1517
1529
  });
1518
1530
  }
1519
- function renderSuiteHTML(suite) {
1520
- const statusClass = suite.status === "passed" ? "passed" : suite.status === "failed" ? "failed" : "skipped";
1521
- const testsHTML = suite.tests.map((test) => {
1522
- const testStatusClass = test.status;
1523
- const errorHTML = test.error ? `<div class="error-message"><strong>${escapeHTML(test.error.message)}</strong>${test.error.stack ? `
1524
- ${escapeHTML(test.error.stack)}` : ""}${test.error.expected && test.error.actual ? `
1531
+ function pct(value) {
1532
+ return `${(value * 100).toFixed(1)}%`;
1533
+ }
1534
+ function renderCoverageMD(coverage) {
1535
+ return `
1536
+ ### \u8986\u76D6\u7387
1525
1537
 
1526
- Expected: ${escapeHTML(test.error.expected)}
1527
- Actual: ${escapeHTML(test.error.actual)}` : ""}</div>` : "";
1528
- return `<div class="test-item">
1529
- <span class="status-dot ${testStatusClass}"></span>
1530
- <span class="test-name">${escapeHTML(test.name)}</span>
1531
- <span class="duration">${formatDuration(test.duration)}</span>
1532
- ${test.retries > 0 ? `<span class="retries">\u91CD\u8BD5 ${test.retries} \u6B21</span>` : ""}
1533
- </div>
1534
- ${errorHTML}`;
1535
- }).join("");
1536
- return `<div class="suite">
1537
- <div class="suite-header" onclick="this.parentElement.classList.toggle('collapsed')">
1538
- <div>
1539
- <span class="status-dot ${statusClass}"></span>
1540
- <strong>${escapeHTML(suite.name)}</strong>
1541
- <span class="suite-file">${escapeHTML(suite.file)}</span>
1542
- </div>
1543
- <div>
1544
- <span class="duration">${formatDuration(suite.duration)}</span>
1545
- <span class="toggle-icon">\u25BC</span>
1546
- </div>
1547
- </div>
1548
- <div class="suite-body">${testsHTML}</div>
1549
- </div>`;
1550
- }
1551
- function escapeHTML(str) {
1552
- return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
1538
+ | \u6307\u6807 | \u8986\u76D6\u7387 | \u8FDB\u5EA6 |
1539
+ |------|--------|------|
1540
+ | \u8BED\u53E5 (Statements) | ${pct(coverage.statements)} | ${renderProgressBar(coverage.statements)} |
1541
+ | \u5206\u652F (Branches) | ${pct(coverage.branches)} | ${renderProgressBar(coverage.branches)} |
1542
+ | \u51FD\u6570 (Functions) | ${pct(coverage.functions)} | ${renderProgressBar(coverage.functions)} |
1543
+ | \u884C (Lines) | ${pct(coverage.lines)} | ${renderProgressBar(coverage.lines)} |
1544
+ `;
1545
+ }
1546
+ function renderProgressBar(value) {
1547
+ const filled = Math.round(value * 10);
1548
+ const empty = 10 - filled;
1549
+ return `${"\u2588".repeat(filled)}${"\u2591".repeat(empty)}`;
1550
+ }
1551
+ var TYPE_LABELS = {
1552
+ unit: "\u5355\u5143\u6D4B\u8BD5",
1553
+ component: "\u7EC4\u4EF6\u6D4B\u8BD5",
1554
+ e2e: "E2E \u6D4B\u8BD5",
1555
+ api: "API \u6D4B\u8BD5",
1556
+ visual: "\u89C6\u89C9\u56DE\u5F52\u6D4B\u8BD5",
1557
+ performance: "\u6027\u80FD\u6D4B\u8BD5"
1558
+ };
1559
+ function generateMDReport(data) {
1560
+ const passRate = data.summary.total > 0 ? (data.summary.passed / data.summary.total * 100).toFixed(1) : "0";
1561
+ const rateIcon = parseFloat(passRate) >= 80 ? "\u2705" : parseFloat(passRate) >= 50 ? "\u26A0\uFE0F" : "\u274C";
1562
+ const lines = [];
1563
+ lines.push(`# QAT \u6D4B\u8BD5\u62A5\u544A`);
1564
+ lines.push("");
1565
+ lines.push(`> \u751F\u6210\u65F6\u95F4: ${formatTimestamp(data.timestamp)} | \u603B\u8017\u65F6: ${formatDuration(data.duration)}`);
1566
+ lines.push("");
1567
+ lines.push(`## \u603B\u89C8`);
1568
+ lines.push("");
1569
+ lines.push(`| \u6307\u6807 | \u6570\u503C |`);
1570
+ lines.push(`|------|------|`);
1571
+ lines.push(`| \u901A\u8FC7\u7387 | ${rateIcon} **${passRate}%** |`);
1572
+ lines.push(`| \u603B\u7528\u4F8B | ${data.summary.total} |`);
1573
+ lines.push(`| \u2705 \u901A\u8FC7 | ${data.summary.passed} |`);
1574
+ if (data.summary.failed > 0) lines.push(`| \u274C \u5931\u8D25 | ${data.summary.failed} |`);
1575
+ if (data.summary.skipped > 0) lines.push(`| \u23ED\uFE0F \u8DF3\u8FC7 | ${data.summary.skipped} |`);
1576
+ if (data.summary.pending > 0) lines.push(`| \u23F3 \u5F85\u5B9A | ${data.summary.pending} |`);
1577
+ lines.push(`| \u23F1\uFE0F \u8017\u65F6 | ${formatDuration(data.duration)} |`);
1578
+ lines.push("");
1579
+ if (Object.keys(data.byType).length > 0) {
1580
+ lines.push(`## \u6309\u7C7B\u578B\u7EDF\u8BA1`);
1581
+ lines.push("");
1582
+ lines.push(`| \u7C7B\u578B | \u901A\u8FC7 | \u5931\u8D25 | \u8DF3\u8FC7 | \u603B\u8BA1 | \u901A\u8FC7\u7387 |`);
1583
+ lines.push(`|------|------|------|------|------|--------|`);
1584
+ for (const [type, stats] of Object.entries(data.byType)) {
1585
+ const label = TYPE_LABELS[type] || type;
1586
+ const rate = stats.total > 0 ? (stats.passed / stats.total * 100).toFixed(0) + "%" : "-";
1587
+ lines.push(`| ${label} | ${stats.passed} | ${stats.failed} | ${stats.skipped} | ${stats.total} | ${rate} |`);
1588
+ }
1589
+ lines.push("");
1590
+ }
1591
+ if (data.coverage) {
1592
+ lines.push(renderCoverageMD(data.coverage));
1593
+ lines.push("");
1594
+ }
1595
+ lines.push(`## \u6D4B\u8BD5\u8BE6\u60C5`);
1596
+ lines.push("");
1597
+ for (const result of data.results) {
1598
+ const typeLabel = TYPE_LABELS[result.type] || result.type;
1599
+ const statusIcon = result.status === "passed" ? "\u2705" : result.status === "failed" ? "\u274C" : "\u26A0\uFE0F";
1600
+ lines.push(`### ${statusIcon} ${typeLabel}`);
1601
+ lines.push("");
1602
+ if (result.suites.length === 0) {
1603
+ lines.push(`*\u65E0\u6D4B\u8BD5\u7ED3\u679C*`);
1604
+ lines.push("");
1605
+ continue;
1606
+ }
1607
+ for (const suite of result.suites) {
1608
+ const suiteIcon = suite.status === "passed" ? "\u2705" : suite.status === "failed" ? "\u274C" : "\u26A0\uFE0F";
1609
+ lines.push(`#### ${suiteIcon} ${suite.name}`);
1610
+ lines.push("");
1611
+ lines.push(`- \u6587\u4EF6: \`${suite.file}\``);
1612
+ lines.push(`- \u8017\u65F6: ${formatDuration(suite.duration)}`);
1613
+ lines.push("");
1614
+ if (suite.tests.length > 0) {
1615
+ lines.push(`| \u72B6\u6001 | \u6D4B\u8BD5\u540D\u79F0 | \u8017\u65F6 |`);
1616
+ lines.push(`|------|----------|------|`);
1617
+ for (const test of suite.tests) {
1618
+ const testIcon = test.status === "passed" ? "\u2705" : test.status === "failed" ? "\u274C" : test.status === "skipped" ? "\u23ED\uFE0F" : "\u23F3";
1619
+ const name = test.error ? `**${test.name}**` : test.name;
1620
+ lines.push(`| ${testIcon} | ${name} | ${formatDuration(test.duration)} |`);
1621
+ }
1622
+ lines.push("");
1623
+ }
1624
+ const failedTests = suite.tests.filter((t) => t.status === "failed" && t.error);
1625
+ if (failedTests.length > 0) {
1626
+ lines.push(`<details>`);
1627
+ lines.push(`<summary>\u274C \u5931\u8D25\u8BE6\u60C5 (${failedTests.length})</summary>`);
1628
+ lines.push("");
1629
+ for (const test of failedTests) {
1630
+ lines.push(`**${test.name}**`);
1631
+ lines.push("```");
1632
+ lines.push(test.error.message);
1633
+ if (test.error.stack) {
1634
+ lines.push(test.error.stack);
1635
+ }
1636
+ if (test.error.expected && test.error.actual) {
1637
+ lines.push(`Expected: ${test.error.expected}`);
1638
+ lines.push(`Actual: ${test.error.actual}`);
1639
+ }
1640
+ lines.push("```");
1641
+ lines.push("");
1642
+ }
1643
+ lines.push(`</details>`);
1644
+ lines.push("");
1645
+ }
1646
+ }
1647
+ }
1648
+ lines.push("---");
1649
+ lines.push("");
1650
+ lines.push(`*\u7531 QAT \u81EA\u52A8\u5316\u6D4B\u8BD5\u5DE5\u5177\u751F\u6210 | ${formatTimestamp(data.timestamp)}*`);
1651
+ return lines.join("\n");
1652
+ }
1653
+ function writeReportToDisk(data, outputDir) {
1654
+ const md = generateMDReport(data);
1655
+ const dir = import_node_path5.default.resolve(outputDir);
1656
+ if (!import_node_fs5.default.existsSync(dir)) {
1657
+ import_node_fs5.default.mkdirSync(dir, { recursive: true });
1658
+ }
1659
+ const mdPath = import_node_path5.default.join(dir, "report.md");
1660
+ import_node_fs5.default.writeFileSync(mdPath, md, "utf-8");
1661
+ const jsonPath = import_node_path5.default.join(dir, "report.json");
1662
+ import_node_fs5.default.writeFileSync(jsonPath, JSON.stringify(data, null, 2), "utf-8");
1663
+ return mdPath;
1553
1664
  }
1554
1665
  function generateHTMLReport(data) {
1555
1666
  const passRate = data.summary.total > 0 ? (data.summary.passed / data.summary.total * 100).toFixed(1) : "0";
1556
- const suitesHTML = data.results.flatMap((r) => r.suites).map(renderSuiteHTML).join("\n");
1557
- const byTypeHTML = Object.entries(data.byType).map(([type, stats]) => {
1558
- const rate = stats.total > 0 ? (stats.passed / stats.total * 100).toFixed(0) : "0";
1559
- return `<div class="type-card">
1560
- <div class="type-name">${type}</div>
1561
- <div class="type-stats">
1562
- <span class="passed">${stats.passed} \u901A\u8FC7</span>
1563
- <span class="failed">${stats.failed} \u5931\u8D25</span>
1564
- <span class="skipped">${stats.skipped} \u8DF3\u8FC7</span>
1565
- </div>
1566
- <div class="type-rate">${rate}%</div>
1567
- </div>`;
1568
- }).join("\n");
1667
+ const md = generateMDReport(data);
1668
+ const html = md.replace(/^### (.+)$/gm, "<h3>$1</h3>").replace(/^## (.+)$/gm, "<h2>$1</h2>").replace(/^# (.+)$/gm, "<h1>$1</h1>").replace(/^> (.+)$/gm, "<blockquote>$1</blockquote>").replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>").replace(/`([^`]+)`/g, "<code>$1</code>").replace(/^---$/gm, "<hr>").replace(/\|(.+)\|/g, (match) => {
1669
+ const cells = match.split("|").filter(Boolean).map((c) => c.trim());
1670
+ if (cells.every((c) => c.startsWith("-") || c === "")) return "";
1671
+ const tds = cells.map((c) => `<td>${c}</td>`).join("");
1672
+ return `<tr>${tds}</tr>`;
1673
+ }).replace(/<tr>/g, "<table><tr>").replace(/<\/tr>(?!\s*<table>)/g, "</tr></table>").replace(/<\/table>\s*<table>/g, "").replace(/^✅/gm, '<span style="color:#22c55e">\u2705</span>').replace(/^❌/gm, '<span style="color:#ef4444">\u274C</span>').replace(/^⚠️/gm, '<span style="color:#f59e0b">\u26A0\uFE0F</span>');
1569
1674
  return `<!DOCTYPE html>
1570
1675
  <html lang="zh-CN">
1571
1676
  <head>
@@ -1573,152 +1678,39 @@ function generateHTMLReport(data) {
1573
1678
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
1574
1679
  <title>QAT \u6D4B\u8BD5\u62A5\u544A - ${formatTimestamp(data.timestamp)}</title>
1575
1680
  <style>
1576
- :root {
1577
- --color-passed: #22c55e;
1578
- --color-failed: #ef4444;
1579
- --color-skipped: #f59e0b;
1580
- --color-info: #3b82f6;
1581
- --bg-primary: #ffffff;
1582
- --bg-secondary: #f8fafc;
1583
- --text-primary: #1e293b;
1584
- --text-secondary: #64748b;
1585
- --border-color: #e2e8f0;
1586
- }
1587
- * { margin: 0; padding: 0; box-sizing: border-box; }
1588
1681
  body {
1589
1682
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
1590
- background: var(--bg-secondary);
1591
- color: var(--text-primary);
1592
- line-height: 1.6;
1593
- }
1594
- .container { max-width: 1200px; margin: 0 auto; padding: 20px; }
1595
- header {
1596
- background: white;
1597
- border-bottom: 1px solid var(--border-color);
1598
- padding: 20px;
1599
- margin-bottom: 20px;
1600
- border-radius: 8px;
1601
- }
1602
- header h1 { font-size: 24px; font-weight: 600; }
1603
- header .meta { color: var(--text-secondary); font-size: 14px; margin-top: 4px; }
1604
- .summary { display: flex; gap: 16px; margin: 20px 0; flex-wrap: wrap; }
1605
- .card {
1606
- padding: 16px 24px; border-radius: 8px; color: white;
1607
- font-weight: 600; min-width: 120px; text-align: center;
1608
- }
1609
- .card.passed { background: var(--color-passed); }
1610
- .card.failed { background: var(--color-failed); }
1611
- .card.skipped { background: var(--color-skipped); }
1612
- .card.total { background: var(--color-info); }
1613
- .card .card-value { font-size: 28px; }
1614
- .card .card-label { font-size: 13px; opacity: 0.9; }
1615
- .pass-rate {
1616
- font-size: 48px; font-weight: 700; text-align: center; margin: 20px 0;
1617
- color: ${parseFloat(passRate) >= 80 ? "var(--color-passed)" : parseFloat(passRate) >= 50 ? "var(--color-skipped)" : "var(--color-failed)"};
1618
- }
1619
- .pass-rate-label { text-align: center; color: var(--text-secondary); margin-bottom: 20px; }
1620
- .by-type { display: flex; gap: 12px; flex-wrap: wrap; margin: 20px 0; }
1621
- .type-card {
1622
- background: white; border: 1px solid var(--border-color); border-radius: 8px;
1623
- padding: 12px 16px; min-width: 180px;
1624
- }
1625
- .type-name { font-weight: 600; text-transform: capitalize; margin-bottom: 4px; }
1626
- .type-stats { font-size: 13px; }
1627
- .type-stats span { margin-right: 8px; }
1628
- .type-stats .passed { color: var(--color-passed); }
1629
- .type-stats .failed { color: var(--color-failed); }
1630
- .type-stats .skipped { color: var(--color-skipped); }
1631
- .type-rate { font-size: 20px; font-weight: 700; margin-top: 4px; }
1632
- .suite {
1633
- background: white; border: 1px solid var(--border-color);
1634
- border-radius: 8px; margin-bottom: 12px; overflow: hidden;
1635
- }
1636
- .suite-header {
1637
- padding: 12px 16px; display: flex; justify-content: space-between;
1638
- align-items: center; cursor: pointer; user-select: none;
1639
- }
1640
- .suite-header:hover { background: var(--bg-secondary); }
1641
- .suite-header div { display: flex; align-items: center; gap: 8px; }
1642
- .suite.collapsed .suite-body { display: none; }
1643
- .suite-file { color: var(--text-secondary); font-size: 12px; }
1644
- .toggle-icon { font-size: 12px; color: var(--text-secondary); transition: transform 0.2s; }
1645
- .suite.collapsed .toggle-icon { transform: rotate(-90deg); }
1646
- .suite-body { border-top: 1px solid var(--border-color); padding: 8px 16px; }
1647
- .test-item {
1648
- padding: 8px 0; display: flex; align-items: center; gap: 8px;
1649
- }
1650
- .test-item + .test-item { border-top: 1px solid var(--border-color); }
1651
- .status-dot {
1652
- width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0;
1653
- }
1654
- .status-dot.passed { background: var(--color-passed); }
1655
- .status-dot.failed { background: var(--color-failed); }
1656
- .status-dot.skipped { background: var(--color-skipped); }
1657
- .status-dot.pending { background: var(--text-secondary); }
1658
- .test-name { flex: 1; }
1659
- .duration { color: var(--text-secondary); font-size: 13px; }
1660
- .retries { color: var(--color-skipped); font-size: 12px; }
1661
- .error-message {
1662
- background: #fef2f2; color: #991b1b; padding: 12px;
1663
- border-radius: 4px; font-family: 'Fira Code', monospace;
1664
- font-size: 13px; margin: 8px 0 8px 16px; white-space: pre-wrap;
1665
- word-break: break-all;
1666
- }
1667
- footer {
1668
- text-align: center; padding: 20px; color: var(--text-secondary);
1669
- font-size: 13px; margin-top: 40px;
1670
- }
1683
+ max-width: 960px; margin: 0 auto; padding: 20px;
1684
+ background: #f8fafc; color: #1e293b; line-height: 1.6;
1685
+ }
1686
+ h1 { border-bottom: 2px solid #3b82f6; padding-bottom: 8px; }
1687
+ h2 { margin-top: 24px; color: #1e40af; }
1688
+ h3 { margin-top: 16px; color: #334155; }
1689
+ h4 { margin-top: 12px; color: #475569; }
1690
+ blockquote { color: #64748b; border-left: 3px solid #cbd5e1; padding-left: 12px; margin: 8px 0; }
1691
+ table { border-collapse: collapse; width: 100%; margin: 12px 0; }
1692
+ td, th { border: 1px solid #e2e8f0; padding: 8px 12px; text-align: left; }
1693
+ tr:nth-child(even) { background: #f1f5f9; }
1694
+ code { background: #e2e8f0; padding: 2px 6px; border-radius: 3px; font-size: 0.9em; }
1695
+ pre { background: #1e293b; color: #e2e8f0; padding: 16px; border-radius: 8px; overflow-x: auto; }
1696
+ pre code { background: none; padding: 0; }
1697
+ hr { border: none; border-top: 1px solid #e2e8f0; margin: 24px 0; }
1698
+ details { margin: 8px 0; }
1699
+ summary { cursor: pointer; color: #ef4444; font-weight: 600; }
1700
+ strong { color: #1e293b; }
1701
+ .pass-rate { font-size: 48px; font-weight: 700; text-align: center; margin: 20px 0;
1702
+ color: ${parseFloat(passRate) >= 80 ? "#22c55e" : parseFloat(passRate) >= 50 ? "#f59e0b" : "#ef4444"};
1703
+ }
1704
+ .pass-rate-label { text-align: center; color: #64748b; margin-bottom: 20px; }
1671
1705
  </style>
1672
1706
  </head>
1673
1707
  <body>
1674
- <div class="container">
1675
- <header>
1676
- <h1>QAT \u6D4B\u8BD5\u62A5\u544A</h1>
1677
- <div class="meta">\u751F\u6210\u65F6\u95F4: ${formatTimestamp(data.timestamp)} | \u603B\u8017\u65F6: ${formatDuration(data.duration)}</div>
1678
- </header>
1679
-
1680
- <div class="pass-rate">${passRate}%</div>
1681
- <div class="pass-rate-label">\u6D4B\u8BD5\u901A\u8FC7\u7387</div>
1682
-
1683
- <div class="summary">
1684
- <div class="card total">
1685
- <div class="card-value">${data.summary.total}</div>
1686
- <div class="card-label">\u603B\u8BA1</div>
1687
- </div>
1688
- <div class="card passed">
1689
- <div class="card-value">${data.summary.passed}</div>
1690
- <div class="card-label">\u901A\u8FC7</div>
1691
- </div>
1692
- <div class="card failed">
1693
- <div class="card-value">${data.summary.failed}</div>
1694
- <div class="card-label">\u5931\u8D25</div>
1695
- </div>
1696
- <div class="card skipped">
1697
- <div class="card-value">${data.summary.skipped}</div>
1698
- <div class="card-label">\u8DF3\u8FC7</div>
1699
- </div>
1700
- </div>
1701
-
1702
- ${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>` : ""}
1703
-
1704
- <h2 style="margin-top:30px;margin-bottom:10px;">\u6D4B\u8BD5\u8BE6\u60C5</h2>
1705
- ${suitesHTML || '<p style="color:var(--text-secondary)">\u6682\u65E0\u6D4B\u8BD5\u7ED3\u679C</p>'}
1706
-
1707
- <footer>\u7531 QAT \u81EA\u52A8\u5316\u6D4B\u8BD5\u5DE5\u5177\u751F\u6210</footer>
1708
- </div>
1708
+ <div class="pass-rate">${passRate}%</div>
1709
+ <div class="pass-rate-label">\u6D4B\u8BD5\u901A\u8FC7\u7387</div>
1710
+ ${html}
1709
1711
  </body>
1710
1712
  </html>`;
1711
1713
  }
1712
- function writeReportToDisk(data, outputDir) {
1713
- const html = generateHTMLReport(data);
1714
- const dir = import_node_path5.default.resolve(outputDir);
1715
- if (!import_node_fs5.default.existsSync(dir)) {
1716
- import_node_fs5.default.mkdirSync(dir, { recursive: true });
1717
- }
1718
- const indexPath = import_node_path5.default.join(dir, "index.html");
1719
- import_node_fs5.default.writeFileSync(indexPath, html, "utf-8");
1720
- return indexPath;
1721
- }
1722
1714
 
1723
1715
  // src/runners/vitest-runner.ts
1724
1716
  var import_node_child_process = require("child_process");
@@ -1793,34 +1785,109 @@ function buildVitestArgs(options) {
1793
1785
  return args;
1794
1786
  }
1795
1787
  async function execVitest(args) {
1788
+ const os = await import("os");
1789
+ const fs9 = await import("fs");
1790
+ const tmpFile = import_node_path6.default.join(os.tmpdir(), `qat-vitest-result-${Date.now()}.json`);
1791
+ const argsWithOutput = [...args, "--outputFile", tmpFile];
1796
1792
  return new Promise((resolve, reject) => {
1797
1793
  const npx = process.platform === "win32" ? "npx.cmd" : "npx";
1798
- const child = (0, import_node_child_process.execFile)(npx, args, {
1794
+ const child = (0, import_node_child_process.execFile)(npx, argsWithOutput, {
1799
1795
  cwd: process.cwd(),
1800
- env: { ...process.env, FORCE_COLOR: "0" },
1796
+ env: { ...process.env, FORCE_COLOR: "0", NO_COLOR: "1" },
1801
1797
  maxBuffer: 50 * 1024 * 1024,
1802
- // 50MB
1803
1798
  shell: true
1804
1799
  }, (error, stdout, stderr) => {
1805
- const output = stdout || stderr || "";
1800
+ const rawOutput = stdout || stderr || "";
1801
+ let jsonResult = null;
1806
1802
  try {
1807
- const parsed = parseVitestJSONOutput(output);
1808
- resolve(parsed);
1803
+ if (fs9.existsSync(tmpFile)) {
1804
+ jsonResult = fs9.readFileSync(tmpFile, "utf-8");
1805
+ fs9.unlinkSync(tmpFile);
1806
+ }
1809
1807
  } catch {
1810
- if (output) {
1811
- resolve(parseVitestTextOutput(output, !!error));
1808
+ }
1809
+ if (jsonResult) {
1810
+ try {
1811
+ const parsed = parseVitestJSONResult(jsonResult);
1812
+ resolve({ ...parsed, rawOutput });
1813
+ return;
1814
+ } catch {
1815
+ }
1816
+ }
1817
+ try {
1818
+ const parsed = parseVitestJSONOutput(rawOutput);
1819
+ resolve({ ...parsed, rawOutput });
1820
+ } catch {
1821
+ if (rawOutput) {
1822
+ resolve({ ...parseVitestTextOutput(rawOutput, !!error), rawOutput });
1812
1823
  } else if (error && error.message.includes("ENOENT")) {
1813
1824
  reject(new Error("\u672A\u627E\u5230 vitest\uFF0C\u8BF7\u786E\u4FDD\u5DF2\u5B89\u88C5: npm install -D vitest"));
1814
1825
  } else {
1815
- resolve({ success: !error, suites: [] });
1826
+ resolve({ success: !error, suites: [], rawOutput });
1816
1827
  }
1817
1828
  }
1818
1829
  });
1819
1830
  child.on("error", (err) => {
1831
+ try {
1832
+ fs9.unlinkSync(tmpFile);
1833
+ } catch {
1834
+ }
1820
1835
  reject(new Error(`Vitest \u6267\u884C\u5931\u8D25: ${err.message}`));
1821
1836
  });
1822
1837
  });
1823
1838
  }
1839
+ function parseVitestJSONResult(jsonStr) {
1840
+ const data = JSON.parse(jsonStr);
1841
+ const suites = [];
1842
+ if (data.testResults && Array.isArray(data.testResults)) {
1843
+ for (const fileResult of data.testResults) {
1844
+ const suiteTests = [];
1845
+ const assertions = fileResult.assertionResults || fileResult.tests || [];
1846
+ for (const assertion of assertions) {
1847
+ suiteTests.push({
1848
+ name: assertion.title || assertion.fullName || assertion.name || "unknown",
1849
+ file: fileResult.name || "unknown",
1850
+ status: mapVitestStatus(assertion.status),
1851
+ duration: assertion.duration || 0,
1852
+ error: assertion.failureMessages?.length ? { message: assertion.failureMessages[0] } : assertion.failureMessage ? { message: assertion.failureMessage } : void 0,
1853
+ retries: 0
1854
+ });
1855
+ }
1856
+ if (suiteTests.length === 0 && fileResult.numPassingTests !== void 0) {
1857
+ const counts = [
1858
+ { n: fileResult.numPassingTests || 0, s: "passed" },
1859
+ { n: fileResult.numFailingTests || 0, s: "failed" },
1860
+ { n: fileResult.numPendingTests || 0, s: "skipped" }
1861
+ ];
1862
+ for (const { n, s } of counts) {
1863
+ for (let i = 0; i < n; i++) {
1864
+ suiteTests.push({
1865
+ name: `${s} test ${i + 1}`,
1866
+ file: fileResult.name || "unknown",
1867
+ status: s,
1868
+ duration: 0,
1869
+ retries: 0
1870
+ });
1871
+ }
1872
+ }
1873
+ }
1874
+ suites.push({
1875
+ name: import_node_path6.default.basename(fileResult.name || "unknown"),
1876
+ file: fileResult.name || "unknown",
1877
+ type: "unit",
1878
+ status: mapVitestStatus(fileResult.status),
1879
+ duration: fileResult.duration || 0,
1880
+ tests: suiteTests
1881
+ });
1882
+ }
1883
+ }
1884
+ let coverage;
1885
+ if (data.coverageMap) {
1886
+ coverage = extractCoverage(data.coverageMap);
1887
+ }
1888
+ const success = data.success !== false && data.numFailedTests === void 0 ? suites.every((s) => s.status !== "failed") : (data.numFailedTests || 0) === 0;
1889
+ return { success, suites, coverage };
1890
+ }
1824
1891
  function parseVitestJSONOutput(output) {
1825
1892
  const jsonMatch = output.match(/\{[\s\S]*"testResults"[\s\S]*\}/);
1826
1893
  if (!jsonMatch) {