qat-cli 0.2.8 → 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 +1 -1
- package/dist/cli.js +238 -210
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +261 -194
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +14 -6
- package/dist/index.d.ts +14 -6
- package/dist/index.js +261 -194
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -596,7 +596,7 @@ declare function renderTemplate(type: TestType, context: TemplateContext): strin
|
|
|
596
596
|
declare function generateTestFile(type: TestType, context: TemplateContext, outputDir: string): string;
|
|
597
597
|
|
|
598
598
|
/**
|
|
599
|
-
* 报告聚合服务 -
|
|
599
|
+
* 报告聚合服务 - 收集各运行器结果,生成 Markdown 报告
|
|
600
600
|
*/
|
|
601
601
|
|
|
602
602
|
/** 报告数据 */
|
|
@@ -618,20 +618,28 @@ interface ReportData {
|
|
|
618
618
|
failed: number;
|
|
619
619
|
skipped: number;
|
|
620
620
|
}>;
|
|
621
|
+
/** 覆盖率汇总 */
|
|
622
|
+
coverage?: CoverageResult;
|
|
623
|
+
/** AI 审计报告 */
|
|
624
|
+
reviewSummary?: {
|
|
625
|
+
total: number;
|
|
626
|
+
approved: number;
|
|
627
|
+
avgScore: number;
|
|
628
|
+
};
|
|
621
629
|
}
|
|
622
630
|
/**
|
|
623
631
|
* 聚合多个测试运行结果
|
|
624
632
|
*/
|
|
625
633
|
declare function aggregateResults(results: TestRunResult[]): ReportData;
|
|
626
634
|
/**
|
|
627
|
-
*
|
|
628
|
-
*/
|
|
629
|
-
declare function generateHTMLReport(data: ReportData): string;
|
|
630
|
-
/**
|
|
631
|
-
* 将报告写入磁盘
|
|
635
|
+
* 将报告写入磁盘(Markdown 格式)
|
|
632
636
|
* @returns 报告文件路径
|
|
633
637
|
*/
|
|
634
638
|
declare function writeReportToDisk(data: ReportData, outputDir: string): string;
|
|
639
|
+
/**
|
|
640
|
+
* 生成 HTML 报告(保留兼容,从 Markdown 转换为简易 HTML)
|
|
641
|
+
*/
|
|
642
|
+
declare function generateHTMLReport(data: ReportData): string;
|
|
635
643
|
|
|
636
644
|
/**
|
|
637
645
|
* Vitest 运行器 - 通过子进程调用Vitest,解析输出收集结果
|
package/dist/index.d.ts
CHANGED
|
@@ -596,7 +596,7 @@ declare function renderTemplate(type: TestType, context: TemplateContext): strin
|
|
|
596
596
|
declare function generateTestFile(type: TestType, context: TemplateContext, outputDir: string): string;
|
|
597
597
|
|
|
598
598
|
/**
|
|
599
|
-
* 报告聚合服务 -
|
|
599
|
+
* 报告聚合服务 - 收集各运行器结果,生成 Markdown 报告
|
|
600
600
|
*/
|
|
601
601
|
|
|
602
602
|
/** 报告数据 */
|
|
@@ -618,20 +618,28 @@ interface ReportData {
|
|
|
618
618
|
failed: number;
|
|
619
619
|
skipped: number;
|
|
620
620
|
}>;
|
|
621
|
+
/** 覆盖率汇总 */
|
|
622
|
+
coverage?: CoverageResult;
|
|
623
|
+
/** AI 审计报告 */
|
|
624
|
+
reviewSummary?: {
|
|
625
|
+
total: number;
|
|
626
|
+
approved: number;
|
|
627
|
+
avgScore: number;
|
|
628
|
+
};
|
|
621
629
|
}
|
|
622
630
|
/**
|
|
623
631
|
* 聚合多个测试运行结果
|
|
624
632
|
*/
|
|
625
633
|
declare function aggregateResults(results: TestRunResult[]): ReportData;
|
|
626
634
|
/**
|
|
627
|
-
*
|
|
628
|
-
*/
|
|
629
|
-
declare function generateHTMLReport(data: ReportData): string;
|
|
630
|
-
/**
|
|
631
|
-
* 将报告写入磁盘
|
|
635
|
+
* 将报告写入磁盘(Markdown 格式)
|
|
632
636
|
* @returns 报告文件路径
|
|
633
637
|
*/
|
|
634
638
|
declare function writeReportToDisk(data: ReportData, outputDir: string): string;
|
|
639
|
+
/**
|
|
640
|
+
* 生成 HTML 报告(保留兼容,从 Markdown 转换为简易 HTML)
|
|
641
|
+
*/
|
|
642
|
+
declare function generateHTMLReport(data: ReportData): string;
|
|
635
643
|
|
|
636
644
|
/**
|
|
637
645
|
* Vitest 运行器 - 通过子进程调用Vitest,解析输出收集结果
|
package/dist/index.js
CHANGED
|
@@ -1374,6 +1374,7 @@ function aggregateResults(results) {
|
|
|
1374
1374
|
pending: 0
|
|
1375
1375
|
};
|
|
1376
1376
|
const byType = {};
|
|
1377
|
+
let coverage;
|
|
1377
1378
|
for (const result of results) {
|
|
1378
1379
|
const typeKey = result.type;
|
|
1379
1380
|
if (!byType[typeKey]) {
|
|
@@ -1397,6 +1398,16 @@ function aggregateResults(results) {
|
|
|
1397
1398
|
}
|
|
1398
1399
|
}
|
|
1399
1400
|
}
|
|
1401
|
+
if (result.coverage) {
|
|
1402
|
+
if (!coverage) {
|
|
1403
|
+
coverage = { ...result.coverage };
|
|
1404
|
+
} else {
|
|
1405
|
+
coverage.lines = Math.max(coverage.lines, result.coverage.lines);
|
|
1406
|
+
coverage.statements = Math.max(coverage.statements, result.coverage.statements);
|
|
1407
|
+
coverage.functions = Math.max(coverage.functions, result.coverage.functions);
|
|
1408
|
+
coverage.branches = Math.max(coverage.branches, result.coverage.branches);
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1400
1411
|
}
|
|
1401
1412
|
const totalDuration = results.reduce((sum, r) => sum + r.duration, 0);
|
|
1402
1413
|
return {
|
|
@@ -1404,7 +1415,8 @@ function aggregateResults(results) {
|
|
|
1404
1415
|
duration: totalDuration,
|
|
1405
1416
|
results,
|
|
1406
1417
|
summary,
|
|
1407
|
-
byType
|
|
1418
|
+
byType,
|
|
1419
|
+
coverage: coverage?.lines ? coverage : void 0
|
|
1408
1420
|
};
|
|
1409
1421
|
}
|
|
1410
1422
|
function formatDuration(ms) {
|
|
@@ -1424,56 +1436,149 @@ function formatTimestamp(ts) {
|
|
|
1424
1436
|
second: "2-digit"
|
|
1425
1437
|
});
|
|
1426
1438
|
}
|
|
1427
|
-
function
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1439
|
+
function pct(value) {
|
|
1440
|
+
return `${(value * 100).toFixed(1)}%`;
|
|
1441
|
+
}
|
|
1442
|
+
function renderCoverageMD(coverage) {
|
|
1443
|
+
return `
|
|
1444
|
+
### \u8986\u76D6\u7387
|
|
1433
1445
|
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1446
|
+
| \u6307\u6807 | \u8986\u76D6\u7387 | \u8FDB\u5EA6 |
|
|
1447
|
+
|------|--------|------|
|
|
1448
|
+
| \u8BED\u53E5 (Statements) | ${pct(coverage.statements)} | ${renderProgressBar(coverage.statements)} |
|
|
1449
|
+
| \u5206\u652F (Branches) | ${pct(coverage.branches)} | ${renderProgressBar(coverage.branches)} |
|
|
1450
|
+
| \u51FD\u6570 (Functions) | ${pct(coverage.functions)} | ${renderProgressBar(coverage.functions)} |
|
|
1451
|
+
| \u884C (Lines) | ${pct(coverage.lines)} | ${renderProgressBar(coverage.lines)} |
|
|
1452
|
+
`;
|
|
1453
|
+
}
|
|
1454
|
+
function renderProgressBar(value) {
|
|
1455
|
+
const filled = Math.round(value * 10);
|
|
1456
|
+
const empty = 10 - filled;
|
|
1457
|
+
return `${"\u2588".repeat(filled)}${"\u2591".repeat(empty)}`;
|
|
1458
|
+
}
|
|
1459
|
+
var TYPE_LABELS = {
|
|
1460
|
+
unit: "\u5355\u5143\u6D4B\u8BD5",
|
|
1461
|
+
component: "\u7EC4\u4EF6\u6D4B\u8BD5",
|
|
1462
|
+
e2e: "E2E \u6D4B\u8BD5",
|
|
1463
|
+
api: "API \u6D4B\u8BD5",
|
|
1464
|
+
visual: "\u89C6\u89C9\u56DE\u5F52\u6D4B\u8BD5",
|
|
1465
|
+
performance: "\u6027\u80FD\u6D4B\u8BD5"
|
|
1466
|
+
};
|
|
1467
|
+
function generateMDReport(data) {
|
|
1468
|
+
const passRate = data.summary.total > 0 ? (data.summary.passed / data.summary.total * 100).toFixed(1) : "0";
|
|
1469
|
+
const rateIcon = parseFloat(passRate) >= 80 ? "\u2705" : parseFloat(passRate) >= 50 ? "\u26A0\uFE0F" : "\u274C";
|
|
1470
|
+
const lines = [];
|
|
1471
|
+
lines.push(`# QAT \u6D4B\u8BD5\u62A5\u544A`);
|
|
1472
|
+
lines.push("");
|
|
1473
|
+
lines.push(`> \u751F\u6210\u65F6\u95F4: ${formatTimestamp(data.timestamp)} | \u603B\u8017\u65F6: ${formatDuration(data.duration)}`);
|
|
1474
|
+
lines.push("");
|
|
1475
|
+
lines.push(`## \u603B\u89C8`);
|
|
1476
|
+
lines.push("");
|
|
1477
|
+
lines.push(`| \u6307\u6807 | \u6570\u503C |`);
|
|
1478
|
+
lines.push(`|------|------|`);
|
|
1479
|
+
lines.push(`| \u901A\u8FC7\u7387 | ${rateIcon} **${passRate}%** |`);
|
|
1480
|
+
lines.push(`| \u603B\u7528\u4F8B | ${data.summary.total} |`);
|
|
1481
|
+
lines.push(`| \u2705 \u901A\u8FC7 | ${data.summary.passed} |`);
|
|
1482
|
+
if (data.summary.failed > 0) lines.push(`| \u274C \u5931\u8D25 | ${data.summary.failed} |`);
|
|
1483
|
+
if (data.summary.skipped > 0) lines.push(`| \u23ED\uFE0F \u8DF3\u8FC7 | ${data.summary.skipped} |`);
|
|
1484
|
+
if (data.summary.pending > 0) lines.push(`| \u23F3 \u5F85\u5B9A | ${data.summary.pending} |`);
|
|
1485
|
+
lines.push(`| \u23F1\uFE0F \u8017\u65F6 | ${formatDuration(data.duration)} |`);
|
|
1486
|
+
lines.push("");
|
|
1487
|
+
if (Object.keys(data.byType).length > 0) {
|
|
1488
|
+
lines.push(`## \u6309\u7C7B\u578B\u7EDF\u8BA1`);
|
|
1489
|
+
lines.push("");
|
|
1490
|
+
lines.push(`| \u7C7B\u578B | \u901A\u8FC7 | \u5931\u8D25 | \u8DF3\u8FC7 | \u603B\u8BA1 | \u901A\u8FC7\u7387 |`);
|
|
1491
|
+
lines.push(`|------|------|------|------|------|--------|`);
|
|
1492
|
+
for (const [type, stats] of Object.entries(data.byType)) {
|
|
1493
|
+
const label = TYPE_LABELS[type] || type;
|
|
1494
|
+
const rate = stats.total > 0 ? (stats.passed / stats.total * 100).toFixed(0) + "%" : "-";
|
|
1495
|
+
lines.push(`| ${label} | ${stats.passed} | ${stats.failed} | ${stats.skipped} | ${stats.total} | ${rate} |`);
|
|
1496
|
+
}
|
|
1497
|
+
lines.push("");
|
|
1498
|
+
}
|
|
1499
|
+
if (data.coverage) {
|
|
1500
|
+
lines.push(renderCoverageMD(data.coverage));
|
|
1501
|
+
lines.push("");
|
|
1502
|
+
}
|
|
1503
|
+
lines.push(`## \u6D4B\u8BD5\u8BE6\u60C5`);
|
|
1504
|
+
lines.push("");
|
|
1505
|
+
for (const result of data.results) {
|
|
1506
|
+
const typeLabel = TYPE_LABELS[result.type] || result.type;
|
|
1507
|
+
const statusIcon = result.status === "passed" ? "\u2705" : result.status === "failed" ? "\u274C" : "\u26A0\uFE0F";
|
|
1508
|
+
lines.push(`### ${statusIcon} ${typeLabel}`);
|
|
1509
|
+
lines.push("");
|
|
1510
|
+
if (result.suites.length === 0) {
|
|
1511
|
+
lines.push(`*\u65E0\u6D4B\u8BD5\u7ED3\u679C*`);
|
|
1512
|
+
lines.push("");
|
|
1513
|
+
continue;
|
|
1514
|
+
}
|
|
1515
|
+
for (const suite of result.suites) {
|
|
1516
|
+
const suiteIcon = suite.status === "passed" ? "\u2705" : suite.status === "failed" ? "\u274C" : "\u26A0\uFE0F";
|
|
1517
|
+
lines.push(`#### ${suiteIcon} ${suite.name}`);
|
|
1518
|
+
lines.push("");
|
|
1519
|
+
lines.push(`- \u6587\u4EF6: \`${suite.file}\``);
|
|
1520
|
+
lines.push(`- \u8017\u65F6: ${formatDuration(suite.duration)}`);
|
|
1521
|
+
lines.push("");
|
|
1522
|
+
if (suite.tests.length > 0) {
|
|
1523
|
+
lines.push(`| \u72B6\u6001 | \u6D4B\u8BD5\u540D\u79F0 | \u8017\u65F6 |`);
|
|
1524
|
+
lines.push(`|------|----------|------|`);
|
|
1525
|
+
for (const test of suite.tests) {
|
|
1526
|
+
const testIcon = test.status === "passed" ? "\u2705" : test.status === "failed" ? "\u274C" : test.status === "skipped" ? "\u23ED\uFE0F" : "\u23F3";
|
|
1527
|
+
const name = test.error ? `**${test.name}**` : test.name;
|
|
1528
|
+
lines.push(`| ${testIcon} | ${name} | ${formatDuration(test.duration)} |`);
|
|
1529
|
+
}
|
|
1530
|
+
lines.push("");
|
|
1531
|
+
}
|
|
1532
|
+
const failedTests = suite.tests.filter((t) => t.status === "failed" && t.error);
|
|
1533
|
+
if (failedTests.length > 0) {
|
|
1534
|
+
lines.push(`<details>`);
|
|
1535
|
+
lines.push(`<summary>\u274C \u5931\u8D25\u8BE6\u60C5 (${failedTests.length})</summary>`);
|
|
1536
|
+
lines.push("");
|
|
1537
|
+
for (const test of failedTests) {
|
|
1538
|
+
lines.push(`**${test.name}**`);
|
|
1539
|
+
lines.push("```");
|
|
1540
|
+
lines.push(test.error.message);
|
|
1541
|
+
if (test.error.stack) {
|
|
1542
|
+
lines.push(test.error.stack);
|
|
1543
|
+
}
|
|
1544
|
+
if (test.error.expected && test.error.actual) {
|
|
1545
|
+
lines.push(`Expected: ${test.error.expected}`);
|
|
1546
|
+
lines.push(`Actual: ${test.error.actual}`);
|
|
1547
|
+
}
|
|
1548
|
+
lines.push("```");
|
|
1549
|
+
lines.push("");
|
|
1550
|
+
}
|
|
1551
|
+
lines.push(`</details>`);
|
|
1552
|
+
lines.push("");
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
lines.push("---");
|
|
1557
|
+
lines.push("");
|
|
1558
|
+
lines.push(`*\u7531 QAT \u81EA\u52A8\u5316\u6D4B\u8BD5\u5DE5\u5177\u751F\u6210 | ${formatTimestamp(data.timestamp)}*`);
|
|
1559
|
+
return lines.join("\n");
|
|
1560
|
+
}
|
|
1561
|
+
function writeReportToDisk(data, outputDir) {
|
|
1562
|
+
const md = generateMDReport(data);
|
|
1563
|
+
const dir = path4.resolve(outputDir);
|
|
1564
|
+
if (!fs5.existsSync(dir)) {
|
|
1565
|
+
fs5.mkdirSync(dir, { recursive: true });
|
|
1566
|
+
}
|
|
1567
|
+
const mdPath = path4.join(dir, "report.md");
|
|
1568
|
+
fs5.writeFileSync(mdPath, md, "utf-8");
|
|
1569
|
+
const jsonPath = path4.join(dir, "report.json");
|
|
1570
|
+
fs5.writeFileSync(jsonPath, JSON.stringify(data, null, 2), "utf-8");
|
|
1571
|
+
return mdPath;
|
|
1461
1572
|
}
|
|
1462
1573
|
function generateHTMLReport(data) {
|
|
1463
1574
|
const passRate = data.summary.total > 0 ? (data.summary.passed / data.summary.total * 100).toFixed(1) : "0";
|
|
1464
|
-
const
|
|
1465
|
-
const
|
|
1466
|
-
const
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
<span class="failed">${stats.failed} \u5931\u8D25</span>
|
|
1472
|
-
<span class="skipped">${stats.skipped} \u8DF3\u8FC7</span>
|
|
1473
|
-
</div>
|
|
1474
|
-
<div class="type-rate">${rate}%</div>
|
|
1475
|
-
</div>`;
|
|
1476
|
-
}).join("\n");
|
|
1575
|
+
const md = generateMDReport(data);
|
|
1576
|
+
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) => {
|
|
1577
|
+
const cells = match.split("|").filter(Boolean).map((c) => c.trim());
|
|
1578
|
+
if (cells.every((c) => c.startsWith("-") || c === "")) return "";
|
|
1579
|
+
const tds = cells.map((c) => `<td>${c}</td>`).join("");
|
|
1580
|
+
return `<tr>${tds}</tr>`;
|
|
1581
|
+
}).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>');
|
|
1477
1582
|
return `<!DOCTYPE html>
|
|
1478
1583
|
<html lang="zh-CN">
|
|
1479
1584
|
<head>
|
|
@@ -1481,152 +1586,39 @@ function generateHTMLReport(data) {
|
|
|
1481
1586
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1482
1587
|
<title>QAT \u6D4B\u8BD5\u62A5\u544A - ${formatTimestamp(data.timestamp)}</title>
|
|
1483
1588
|
<style>
|
|
1484
|
-
:root {
|
|
1485
|
-
--color-passed: #22c55e;
|
|
1486
|
-
--color-failed: #ef4444;
|
|
1487
|
-
--color-skipped: #f59e0b;
|
|
1488
|
-
--color-info: #3b82f6;
|
|
1489
|
-
--bg-primary: #ffffff;
|
|
1490
|
-
--bg-secondary: #f8fafc;
|
|
1491
|
-
--text-primary: #1e293b;
|
|
1492
|
-
--text-secondary: #64748b;
|
|
1493
|
-
--border-color: #e2e8f0;
|
|
1494
|
-
}
|
|
1495
|
-
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
1496
1589
|
body {
|
|
1497
1590
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
1498
|
-
|
|
1499
|
-
color:
|
|
1500
|
-
|
|
1501
|
-
}
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
}
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
.
|
|
1520
|
-
.card.total { background: var(--color-info); }
|
|
1521
|
-
.card .card-value { font-size: 28px; }
|
|
1522
|
-
.card .card-label { font-size: 13px; opacity: 0.9; }
|
|
1523
|
-
.pass-rate {
|
|
1524
|
-
font-size: 48px; font-weight: 700; text-align: center; margin: 20px 0;
|
|
1525
|
-
color: ${parseFloat(passRate) >= 80 ? "var(--color-passed)" : parseFloat(passRate) >= 50 ? "var(--color-skipped)" : "var(--color-failed)"};
|
|
1526
|
-
}
|
|
1527
|
-
.pass-rate-label { text-align: center; color: var(--text-secondary); margin-bottom: 20px; }
|
|
1528
|
-
.by-type { display: flex; gap: 12px; flex-wrap: wrap; margin: 20px 0; }
|
|
1529
|
-
.type-card {
|
|
1530
|
-
background: white; border: 1px solid var(--border-color); border-radius: 8px;
|
|
1531
|
-
padding: 12px 16px; min-width: 180px;
|
|
1532
|
-
}
|
|
1533
|
-
.type-name { font-weight: 600; text-transform: capitalize; margin-bottom: 4px; }
|
|
1534
|
-
.type-stats { font-size: 13px; }
|
|
1535
|
-
.type-stats span { margin-right: 8px; }
|
|
1536
|
-
.type-stats .passed { color: var(--color-passed); }
|
|
1537
|
-
.type-stats .failed { color: var(--color-failed); }
|
|
1538
|
-
.type-stats .skipped { color: var(--color-skipped); }
|
|
1539
|
-
.type-rate { font-size: 20px; font-weight: 700; margin-top: 4px; }
|
|
1540
|
-
.suite {
|
|
1541
|
-
background: white; border: 1px solid var(--border-color);
|
|
1542
|
-
border-radius: 8px; margin-bottom: 12px; overflow: hidden;
|
|
1543
|
-
}
|
|
1544
|
-
.suite-header {
|
|
1545
|
-
padding: 12px 16px; display: flex; justify-content: space-between;
|
|
1546
|
-
align-items: center; cursor: pointer; user-select: none;
|
|
1547
|
-
}
|
|
1548
|
-
.suite-header:hover { background: var(--bg-secondary); }
|
|
1549
|
-
.suite-header div { display: flex; align-items: center; gap: 8px; }
|
|
1550
|
-
.suite.collapsed .suite-body { display: none; }
|
|
1551
|
-
.suite-file { color: var(--text-secondary); font-size: 12px; }
|
|
1552
|
-
.toggle-icon { font-size: 12px; color: var(--text-secondary); transition: transform 0.2s; }
|
|
1553
|
-
.suite.collapsed .toggle-icon { transform: rotate(-90deg); }
|
|
1554
|
-
.suite-body { border-top: 1px solid var(--border-color); padding: 8px 16px; }
|
|
1555
|
-
.test-item {
|
|
1556
|
-
padding: 8px 0; display: flex; align-items: center; gap: 8px;
|
|
1557
|
-
}
|
|
1558
|
-
.test-item + .test-item { border-top: 1px solid var(--border-color); }
|
|
1559
|
-
.status-dot {
|
|
1560
|
-
width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0;
|
|
1561
|
-
}
|
|
1562
|
-
.status-dot.passed { background: var(--color-passed); }
|
|
1563
|
-
.status-dot.failed { background: var(--color-failed); }
|
|
1564
|
-
.status-dot.skipped { background: var(--color-skipped); }
|
|
1565
|
-
.status-dot.pending { background: var(--text-secondary); }
|
|
1566
|
-
.test-name { flex: 1; }
|
|
1567
|
-
.duration { color: var(--text-secondary); font-size: 13px; }
|
|
1568
|
-
.retries { color: var(--color-skipped); font-size: 12px; }
|
|
1569
|
-
.error-message {
|
|
1570
|
-
background: #fef2f2; color: #991b1b; padding: 12px;
|
|
1571
|
-
border-radius: 4px; font-family: 'Fira Code', monospace;
|
|
1572
|
-
font-size: 13px; margin: 8px 0 8px 16px; white-space: pre-wrap;
|
|
1573
|
-
word-break: break-all;
|
|
1574
|
-
}
|
|
1575
|
-
footer {
|
|
1576
|
-
text-align: center; padding: 20px; color: var(--text-secondary);
|
|
1577
|
-
font-size: 13px; margin-top: 40px;
|
|
1578
|
-
}
|
|
1591
|
+
max-width: 960px; margin: 0 auto; padding: 20px;
|
|
1592
|
+
background: #f8fafc; color: #1e293b; line-height: 1.6;
|
|
1593
|
+
}
|
|
1594
|
+
h1 { border-bottom: 2px solid #3b82f6; padding-bottom: 8px; }
|
|
1595
|
+
h2 { margin-top: 24px; color: #1e40af; }
|
|
1596
|
+
h3 { margin-top: 16px; color: #334155; }
|
|
1597
|
+
h4 { margin-top: 12px; color: #475569; }
|
|
1598
|
+
blockquote { color: #64748b; border-left: 3px solid #cbd5e1; padding-left: 12px; margin: 8px 0; }
|
|
1599
|
+
table { border-collapse: collapse; width: 100%; margin: 12px 0; }
|
|
1600
|
+
td, th { border: 1px solid #e2e8f0; padding: 8px 12px; text-align: left; }
|
|
1601
|
+
tr:nth-child(even) { background: #f1f5f9; }
|
|
1602
|
+
code { background: #e2e8f0; padding: 2px 6px; border-radius: 3px; font-size: 0.9em; }
|
|
1603
|
+
pre { background: #1e293b; color: #e2e8f0; padding: 16px; border-radius: 8px; overflow-x: auto; }
|
|
1604
|
+
pre code { background: none; padding: 0; }
|
|
1605
|
+
hr { border: none; border-top: 1px solid #e2e8f0; margin: 24px 0; }
|
|
1606
|
+
details { margin: 8px 0; }
|
|
1607
|
+
summary { cursor: pointer; color: #ef4444; font-weight: 600; }
|
|
1608
|
+
strong { color: #1e293b; }
|
|
1609
|
+
.pass-rate { font-size: 48px; font-weight: 700; text-align: center; margin: 20px 0;
|
|
1610
|
+
color: ${parseFloat(passRate) >= 80 ? "#22c55e" : parseFloat(passRate) >= 50 ? "#f59e0b" : "#ef4444"};
|
|
1611
|
+
}
|
|
1612
|
+
.pass-rate-label { text-align: center; color: #64748b; margin-bottom: 20px; }
|
|
1579
1613
|
</style>
|
|
1580
1614
|
</head>
|
|
1581
1615
|
<body>
|
|
1582
|
-
<div class="
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
<div class="meta">\u751F\u6210\u65F6\u95F4: ${formatTimestamp(data.timestamp)} | \u603B\u8017\u65F6: ${formatDuration(data.duration)}</div>
|
|
1586
|
-
</header>
|
|
1587
|
-
|
|
1588
|
-
<div class="pass-rate">${passRate}%</div>
|
|
1589
|
-
<div class="pass-rate-label">\u6D4B\u8BD5\u901A\u8FC7\u7387</div>
|
|
1590
|
-
|
|
1591
|
-
<div class="summary">
|
|
1592
|
-
<div class="card total">
|
|
1593
|
-
<div class="card-value">${data.summary.total}</div>
|
|
1594
|
-
<div class="card-label">\u603B\u8BA1</div>
|
|
1595
|
-
</div>
|
|
1596
|
-
<div class="card passed">
|
|
1597
|
-
<div class="card-value">${data.summary.passed}</div>
|
|
1598
|
-
<div class="card-label">\u901A\u8FC7</div>
|
|
1599
|
-
</div>
|
|
1600
|
-
<div class="card failed">
|
|
1601
|
-
<div class="card-value">${data.summary.failed}</div>
|
|
1602
|
-
<div class="card-label">\u5931\u8D25</div>
|
|
1603
|
-
</div>
|
|
1604
|
-
<div class="card skipped">
|
|
1605
|
-
<div class="card-value">${data.summary.skipped}</div>
|
|
1606
|
-
<div class="card-label">\u8DF3\u8FC7</div>
|
|
1607
|
-
</div>
|
|
1608
|
-
</div>
|
|
1609
|
-
|
|
1610
|
-
${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>` : ""}
|
|
1611
|
-
|
|
1612
|
-
<h2 style="margin-top:30px;margin-bottom:10px;">\u6D4B\u8BD5\u8BE6\u60C5</h2>
|
|
1613
|
-
${suitesHTML || '<p style="color:var(--text-secondary)">\u6682\u65E0\u6D4B\u8BD5\u7ED3\u679C</p>'}
|
|
1614
|
-
|
|
1615
|
-
<footer>\u7531 QAT \u81EA\u52A8\u5316\u6D4B\u8BD5\u5DE5\u5177\u751F\u6210</footer>
|
|
1616
|
-
</div>
|
|
1616
|
+
<div class="pass-rate">${passRate}%</div>
|
|
1617
|
+
<div class="pass-rate-label">\u6D4B\u8BD5\u901A\u8FC7\u7387</div>
|
|
1618
|
+
${html}
|
|
1617
1619
|
</body>
|
|
1618
1620
|
</html>`;
|
|
1619
1621
|
}
|
|
1620
|
-
function writeReportToDisk(data, outputDir) {
|
|
1621
|
-
const html = generateHTMLReport(data);
|
|
1622
|
-
const dir = path4.resolve(outputDir);
|
|
1623
|
-
if (!fs5.existsSync(dir)) {
|
|
1624
|
-
fs5.mkdirSync(dir, { recursive: true });
|
|
1625
|
-
}
|
|
1626
|
-
const indexPath = path4.join(dir, "index.html");
|
|
1627
|
-
fs5.writeFileSync(indexPath, html, "utf-8");
|
|
1628
|
-
return indexPath;
|
|
1629
|
-
}
|
|
1630
1622
|
|
|
1631
1623
|
// src/runners/vitest-runner.ts
|
|
1632
1624
|
import { execFile } from "child_process";
|
|
@@ -1701,34 +1693,109 @@ function buildVitestArgs(options) {
|
|
|
1701
1693
|
return args;
|
|
1702
1694
|
}
|
|
1703
1695
|
async function execVitest(args) {
|
|
1696
|
+
const os = await import("os");
|
|
1697
|
+
const fs9 = await import("fs");
|
|
1698
|
+
const tmpFile = path5.join(os.tmpdir(), `qat-vitest-result-${Date.now()}.json`);
|
|
1699
|
+
const argsWithOutput = [...args, "--outputFile", tmpFile];
|
|
1704
1700
|
return new Promise((resolve, reject) => {
|
|
1705
1701
|
const npx = process.platform === "win32" ? "npx.cmd" : "npx";
|
|
1706
|
-
const child = execFile(npx,
|
|
1702
|
+
const child = execFile(npx, argsWithOutput, {
|
|
1707
1703
|
cwd: process.cwd(),
|
|
1708
|
-
env: { ...process.env, FORCE_COLOR: "0" },
|
|
1704
|
+
env: { ...process.env, FORCE_COLOR: "0", NO_COLOR: "1" },
|
|
1709
1705
|
maxBuffer: 50 * 1024 * 1024,
|
|
1710
|
-
// 50MB
|
|
1711
1706
|
shell: true
|
|
1712
1707
|
}, (error, stdout, stderr) => {
|
|
1713
|
-
const
|
|
1708
|
+
const rawOutput = stdout || stderr || "";
|
|
1709
|
+
let jsonResult = null;
|
|
1714
1710
|
try {
|
|
1715
|
-
|
|
1716
|
-
|
|
1711
|
+
if (fs9.existsSync(tmpFile)) {
|
|
1712
|
+
jsonResult = fs9.readFileSync(tmpFile, "utf-8");
|
|
1713
|
+
fs9.unlinkSync(tmpFile);
|
|
1714
|
+
}
|
|
1717
1715
|
} catch {
|
|
1718
|
-
|
|
1719
|
-
|
|
1716
|
+
}
|
|
1717
|
+
if (jsonResult) {
|
|
1718
|
+
try {
|
|
1719
|
+
const parsed = parseVitestJSONResult(jsonResult);
|
|
1720
|
+
resolve({ ...parsed, rawOutput });
|
|
1721
|
+
return;
|
|
1722
|
+
} catch {
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
try {
|
|
1726
|
+
const parsed = parseVitestJSONOutput(rawOutput);
|
|
1727
|
+
resolve({ ...parsed, rawOutput });
|
|
1728
|
+
} catch {
|
|
1729
|
+
if (rawOutput) {
|
|
1730
|
+
resolve({ ...parseVitestTextOutput(rawOutput, !!error), rawOutput });
|
|
1720
1731
|
} else if (error && error.message.includes("ENOENT")) {
|
|
1721
1732
|
reject(new Error("\u672A\u627E\u5230 vitest\uFF0C\u8BF7\u786E\u4FDD\u5DF2\u5B89\u88C5: npm install -D vitest"));
|
|
1722
1733
|
} else {
|
|
1723
|
-
resolve({ success: !error, suites: [] });
|
|
1734
|
+
resolve({ success: !error, suites: [], rawOutput });
|
|
1724
1735
|
}
|
|
1725
1736
|
}
|
|
1726
1737
|
});
|
|
1727
1738
|
child.on("error", (err) => {
|
|
1739
|
+
try {
|
|
1740
|
+
fs9.unlinkSync(tmpFile);
|
|
1741
|
+
} catch {
|
|
1742
|
+
}
|
|
1728
1743
|
reject(new Error(`Vitest \u6267\u884C\u5931\u8D25: ${err.message}`));
|
|
1729
1744
|
});
|
|
1730
1745
|
});
|
|
1731
1746
|
}
|
|
1747
|
+
function parseVitestJSONResult(jsonStr) {
|
|
1748
|
+
const data = JSON.parse(jsonStr);
|
|
1749
|
+
const suites = [];
|
|
1750
|
+
if (data.testResults && Array.isArray(data.testResults)) {
|
|
1751
|
+
for (const fileResult of data.testResults) {
|
|
1752
|
+
const suiteTests = [];
|
|
1753
|
+
const assertions = fileResult.assertionResults || fileResult.tests || [];
|
|
1754
|
+
for (const assertion of assertions) {
|
|
1755
|
+
suiteTests.push({
|
|
1756
|
+
name: assertion.title || assertion.fullName || assertion.name || "unknown",
|
|
1757
|
+
file: fileResult.name || "unknown",
|
|
1758
|
+
status: mapVitestStatus(assertion.status),
|
|
1759
|
+
duration: assertion.duration || 0,
|
|
1760
|
+
error: assertion.failureMessages?.length ? { message: assertion.failureMessages[0] } : assertion.failureMessage ? { message: assertion.failureMessage } : void 0,
|
|
1761
|
+
retries: 0
|
|
1762
|
+
});
|
|
1763
|
+
}
|
|
1764
|
+
if (suiteTests.length === 0 && fileResult.numPassingTests !== void 0) {
|
|
1765
|
+
const counts = [
|
|
1766
|
+
{ n: fileResult.numPassingTests || 0, s: "passed" },
|
|
1767
|
+
{ n: fileResult.numFailingTests || 0, s: "failed" },
|
|
1768
|
+
{ n: fileResult.numPendingTests || 0, s: "skipped" }
|
|
1769
|
+
];
|
|
1770
|
+
for (const { n, s } of counts) {
|
|
1771
|
+
for (let i = 0; i < n; i++) {
|
|
1772
|
+
suiteTests.push({
|
|
1773
|
+
name: `${s} test ${i + 1}`,
|
|
1774
|
+
file: fileResult.name || "unknown",
|
|
1775
|
+
status: s,
|
|
1776
|
+
duration: 0,
|
|
1777
|
+
retries: 0
|
|
1778
|
+
});
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
suites.push({
|
|
1783
|
+
name: path5.basename(fileResult.name || "unknown"),
|
|
1784
|
+
file: fileResult.name || "unknown",
|
|
1785
|
+
type: "unit",
|
|
1786
|
+
status: mapVitestStatus(fileResult.status),
|
|
1787
|
+
duration: fileResult.duration || 0,
|
|
1788
|
+
tests: suiteTests
|
|
1789
|
+
});
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
let coverage;
|
|
1793
|
+
if (data.coverageMap) {
|
|
1794
|
+
coverage = extractCoverage(data.coverageMap);
|
|
1795
|
+
}
|
|
1796
|
+
const success = data.success !== false && data.numFailedTests === void 0 ? suites.every((s) => s.status !== "failed") : (data.numFailedTests || 0) === 0;
|
|
1797
|
+
return { success, suites, coverage };
|
|
1798
|
+
}
|
|
1732
1799
|
function parseVitestJSONOutput(output) {
|
|
1733
1800
|
const jsonMatch = output.match(/\{[\s\S]*"testResults"[\s\S]*\}/);
|
|
1734
1801
|
if (!jsonMatch) {
|