qat-cli 0.2.98 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/cli.js +1455 -1414
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +440 -267
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +14 -6
- package/dist/index.d.ts +14 -6
- package/dist/index.js +440 -267
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/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,162 +1586,60 @@ 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";
|
|
1633
1625
|
import path5 from "path";
|
|
1626
|
+
import fs6 from "fs";
|
|
1627
|
+
import os from "os";
|
|
1628
|
+
var isVerbose = () => process.env.QAT_VERBOSE === "true";
|
|
1629
|
+
function debug(label, ...args) {
|
|
1630
|
+
if (isVerbose()) {
|
|
1631
|
+
console.log(`\x1B[90m [debug:${label}]\x1B[0m`, ...args);
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
1634
|
async function runVitest(options) {
|
|
1635
1635
|
const startTime = Date.now();
|
|
1636
1636
|
const args = buildVitestArgs(options);
|
|
1637
|
+
debug("vitest", "\u547D\u4EE4\u53C2\u6570:", args.join(" "));
|
|
1637
1638
|
try {
|
|
1638
1639
|
const result = await execVitest(args);
|
|
1639
1640
|
const endTime = Date.now();
|
|
1641
|
+
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`);
|
|
1642
|
+
debug("vitest", "\u89E3\u6790\u65B9\u5F0F:", result.parseMethod);
|
|
1640
1643
|
return {
|
|
1641
1644
|
type: options.type,
|
|
1642
1645
|
status: result.success ? "passed" : "failed",
|
|
@@ -1701,83 +1704,228 @@ function buildVitestArgs(options) {
|
|
|
1701
1704
|
return args;
|
|
1702
1705
|
}
|
|
1703
1706
|
async function execVitest(args) {
|
|
1707
|
+
const tmpFile = path5.join(os.tmpdir(), `qat-vitest-result-${Date.now()}.json`);
|
|
1708
|
+
const argsWithOutput = [...args, "--outputFile", tmpFile];
|
|
1704
1709
|
return new Promise((resolve, reject) => {
|
|
1705
1710
|
const npx = process.platform === "win32" ? "npx.cmd" : "npx";
|
|
1706
|
-
|
|
1711
|
+
debug("vitest", "\u6267\u884C\u547D\u4EE4:", npx, argsWithOutput.join(" "));
|
|
1712
|
+
const child = execFile(npx, argsWithOutput, {
|
|
1707
1713
|
cwd: process.cwd(),
|
|
1708
|
-
env: { ...process.env, FORCE_COLOR: "0" },
|
|
1714
|
+
env: { ...process.env, FORCE_COLOR: "0", NO_COLOR: "1" },
|
|
1709
1715
|
maxBuffer: 50 * 1024 * 1024,
|
|
1710
|
-
// 50MB
|
|
1711
1716
|
shell: true
|
|
1712
1717
|
}, (error, stdout, stderr) => {
|
|
1713
|
-
const
|
|
1718
|
+
const rawOutput = stdout || stderr || "";
|
|
1719
|
+
const exitCode = error && "code" in error ? error.code : 0;
|
|
1720
|
+
debug("vitest", `\u9000\u51FA\u7801: ${exitCode}`);
|
|
1721
|
+
debug("vitest", `stdout \u957F\u5EA6: ${stdout?.length || 0}, stderr \u957F\u5EA6: ${stderr?.length || 0}`);
|
|
1722
|
+
let jsonResult = null;
|
|
1714
1723
|
try {
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
} else if (error && error.message.includes("ENOENT")) {
|
|
1721
|
-
reject(new Error("\u672A\u627E\u5230 vitest\uFF0C\u8BF7\u786E\u4FDD\u5DF2\u5B89\u88C5: npm install -D vitest"));
|
|
1724
|
+
if (fs6.existsSync(tmpFile)) {
|
|
1725
|
+
jsonResult = fs6.readFileSync(tmpFile, "utf-8");
|
|
1726
|
+
debug("vitest", `\u4ECE\u4E34\u65F6\u6587\u4EF6\u8BFB\u53D6\u5230 JSON (${jsonResult.length} \u5B57\u7B26)`);
|
|
1727
|
+
debug("vitest", "JSON \u524D 500 \u5B57\u7B26:", jsonResult.substring(0, 500));
|
|
1728
|
+
fs6.unlinkSync(tmpFile);
|
|
1722
1729
|
} else {
|
|
1723
|
-
|
|
1730
|
+
debug("vitest", "\u4E34\u65F6\u6587\u4EF6\u4E0D\u5B58\u5728:", tmpFile);
|
|
1724
1731
|
}
|
|
1732
|
+
} catch (e) {
|
|
1733
|
+
debug("vitest", "\u4E34\u65F6\u6587\u4EF6\u8BFB\u53D6\u5931\u8D25:", e instanceof Error ? e.message : String(e));
|
|
1725
1734
|
}
|
|
1735
|
+
if (jsonResult) {
|
|
1736
|
+
try {
|
|
1737
|
+
const parsed = parseVitestJSON(jsonResult);
|
|
1738
|
+
debug("vitest", "\u4ECE\u4E34\u65F6\u6587\u4EF6\u89E3\u6790\u6210\u529F:", parsed.suites.length, "\u4E2A\u5957\u4EF6");
|
|
1739
|
+
resolve({ ...parsed, rawOutput, parseMethod: "outputFile-JSON" });
|
|
1740
|
+
return;
|
|
1741
|
+
} catch (e) {
|
|
1742
|
+
debug("vitest", "\u4E34\u65F6\u6587\u4EF6 JSON \u89E3\u6790\u5931\u8D25:", e instanceof Error ? e.message : String(e));
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
debug("vitest", "\u5C1D\u8BD5\u4ECE stdout \u63D0\u53D6 JSON...");
|
|
1746
|
+
try {
|
|
1747
|
+
const parsed = parseFromStdout(rawOutput);
|
|
1748
|
+
debug("vitest", "\u4ECE stdout \u89E3\u6790\u6210\u529F:", parsed.suites.length, "\u4E2A\u5957\u4EF6");
|
|
1749
|
+
resolve({ ...parsed, rawOutput, parseMethod: "stdout-JSON" });
|
|
1750
|
+
return;
|
|
1751
|
+
} catch (e) {
|
|
1752
|
+
debug("vitest", "stdout JSON \u89E3\u6790\u5931\u8D25:", e instanceof Error ? e.message : String(e));
|
|
1753
|
+
}
|
|
1754
|
+
debug("vitest", "\u5C1D\u8BD5\u4ECE\u6587\u672C\u8F93\u51FA\u89E3\u6790...");
|
|
1755
|
+
if (rawOutput) {
|
|
1756
|
+
const parsed = parseVitestTextOutput(rawOutput, !!error);
|
|
1757
|
+
debug("vitest", "\u6587\u672C\u89E3\u6790\u7ED3\u679C:", parsed.suites.length, "\u4E2A\u5957\u4EF6");
|
|
1758
|
+
resolve({ ...parsed, rawOutput, parseMethod: "text-fallback" });
|
|
1759
|
+
return;
|
|
1760
|
+
}
|
|
1761
|
+
if (error && error.message.includes("ENOENT")) {
|
|
1762
|
+
reject(new Error("\u672A\u627E\u5230 vitest\uFF0C\u8BF7\u786E\u4FDD\u5DF2\u5B89\u88C5: npm install -D vitest"));
|
|
1763
|
+
return;
|
|
1764
|
+
}
|
|
1765
|
+
debug("vitest", "\u6240\u6709\u89E3\u6790\u65B9\u5F0F\u5747\u5931\u8D25\uFF0C\u8FD4\u56DE\u7A7A\u7ED3\u679C");
|
|
1766
|
+
resolve({ success: !error, suites: [], rawOutput, parseMethod: "none" });
|
|
1726
1767
|
});
|
|
1727
1768
|
child.on("error", (err) => {
|
|
1769
|
+
try {
|
|
1770
|
+
fs6.unlinkSync(tmpFile);
|
|
1771
|
+
} catch {
|
|
1772
|
+
}
|
|
1728
1773
|
reject(new Error(`Vitest \u6267\u884C\u5931\u8D25: ${err.message}`));
|
|
1729
1774
|
});
|
|
1730
1775
|
});
|
|
1731
1776
|
}
|
|
1732
|
-
function
|
|
1733
|
-
const
|
|
1734
|
-
|
|
1735
|
-
|
|
1777
|
+
function parseVitestJSON(jsonStr) {
|
|
1778
|
+
const data = JSON.parse(jsonStr);
|
|
1779
|
+
const suites = [];
|
|
1780
|
+
debug("vitest-json", "JSON \u9876\u5C42\u5B57\u6BB5:", Object.keys(data).join(", "));
|
|
1781
|
+
if (data.testResults && Array.isArray(data.testResults)) {
|
|
1782
|
+
debug("vitest-json", `testResults \u6570\u91CF: ${data.testResults.length}`);
|
|
1783
|
+
for (const fileResult of data.testResults) {
|
|
1784
|
+
const suiteTests = parseTestResults(fileResult);
|
|
1785
|
+
suites.push({
|
|
1786
|
+
name: path5.basename(fileResult.name || "unknown"),
|
|
1787
|
+
file: fileResult.name || "unknown",
|
|
1788
|
+
type: "unit",
|
|
1789
|
+
status: mapVitestStatus(fileResult.status),
|
|
1790
|
+
duration: fileResult.duration || 0,
|
|
1791
|
+
tests: suiteTests
|
|
1792
|
+
});
|
|
1793
|
+
}
|
|
1736
1794
|
}
|
|
1737
|
-
|
|
1795
|
+
if (suites.length === 0 && data.numTotalTests !== void 0) {
|
|
1796
|
+
debug("vitest-json", `\u4F7F\u7528\u6C47\u603B\u683C\u5F0F: total=${data.numTotalTests}, passed=${data.numPassedTests}`);
|
|
1797
|
+
suites.push({
|
|
1798
|
+
name: "Vitest Results",
|
|
1799
|
+
file: "unknown",
|
|
1800
|
+
type: "unit",
|
|
1801
|
+
status: data.numFailedTests > 0 ? "failed" : "passed",
|
|
1802
|
+
duration: 0,
|
|
1803
|
+
tests: buildTestsFromSummary(data)
|
|
1804
|
+
});
|
|
1805
|
+
}
|
|
1806
|
+
if (suites.length === 0 && data.suites && Array.isArray(data.suites)) {
|
|
1807
|
+
debug("vitest-json", `suites \u6570\u91CF: ${data.suites.length}`);
|
|
1808
|
+
for (const suiteData of data.suites) {
|
|
1809
|
+
const suiteTests = [];
|
|
1810
|
+
for (const test of suiteData.tests || []) {
|
|
1811
|
+
suiteTests.push({
|
|
1812
|
+
name: test.name || test.title || "unknown",
|
|
1813
|
+
file: test.file || suiteData.file || "unknown",
|
|
1814
|
+
status: mapVitestStatus(test.status || test.result?.status),
|
|
1815
|
+
duration: test.duration || test.result?.duration || 0,
|
|
1816
|
+
error: test.result?.errors?.[0] ? { message: test.result.errors[0].message || String(test.result.errors[0]) } : void 0,
|
|
1817
|
+
retries: 0
|
|
1818
|
+
});
|
|
1819
|
+
}
|
|
1820
|
+
suites.push({
|
|
1821
|
+
name: suiteData.name || "unknown",
|
|
1822
|
+
file: suiteData.file || "unknown",
|
|
1823
|
+
type: "unit",
|
|
1824
|
+
status: suiteTests.some((t) => t.status === "failed") ? "failed" : "passed",
|
|
1825
|
+
duration: 0,
|
|
1826
|
+
tests: suiteTests
|
|
1827
|
+
});
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
let coverage;
|
|
1831
|
+
if (data.coverageMap) {
|
|
1832
|
+
coverage = extractCoverage(data.coverageMap);
|
|
1833
|
+
}
|
|
1834
|
+
if (!coverage && data.coverage && typeof data.coverage === "object") {
|
|
1835
|
+
const cov = data.coverage;
|
|
1836
|
+
const totals = cov.totals || cov;
|
|
1837
|
+
const getVal = (key) => {
|
|
1838
|
+
const v = totals[key];
|
|
1839
|
+
return typeof v === "number" ? v : typeof v === "object" && v !== null && "pct" in v ? v.pct / 100 : 0;
|
|
1840
|
+
};
|
|
1841
|
+
coverage = {
|
|
1842
|
+
lines: getVal("lines"),
|
|
1843
|
+
statements: getVal("statements"),
|
|
1844
|
+
functions: getVal("functions"),
|
|
1845
|
+
branches: getVal("branches")
|
|
1846
|
+
};
|
|
1847
|
+
}
|
|
1848
|
+
const success = data.success !== false ? data.numFailedTests !== void 0 ? data.numFailedTests === 0 : suites.every((s) => s.status !== "failed") : false;
|
|
1849
|
+
return { success, suites, coverage };
|
|
1850
|
+
}
|
|
1851
|
+
function parseTestResults(fileResult) {
|
|
1852
|
+
const tests = [];
|
|
1853
|
+
const assertions = fileResult.assertionResults || fileResult.tests || [];
|
|
1854
|
+
for (const assertion of assertions) {
|
|
1855
|
+
tests.push({
|
|
1856
|
+
name: assertion.title || assertion.fullName || assertion.name || "unknown",
|
|
1857
|
+
file: fileResult.name || "unknown",
|
|
1858
|
+
status: mapVitestStatus(assertion.status),
|
|
1859
|
+
duration: assertion.duration || 0,
|
|
1860
|
+
error: assertion.failureMessages?.length ? { message: assertion.failureMessages[0] } : assertion.failureMessage ? { message: assertion.failureMessage } : void 0,
|
|
1861
|
+
retries: 0
|
|
1862
|
+
});
|
|
1863
|
+
}
|
|
1864
|
+
if (tests.length === 0 && fileResult.numPassingTests !== void 0) {
|
|
1865
|
+
tests.push(...buildTestsFromSummary(fileResult));
|
|
1866
|
+
}
|
|
1867
|
+
return tests;
|
|
1868
|
+
}
|
|
1869
|
+
function buildTestsFromSummary(data) {
|
|
1870
|
+
const tests = [];
|
|
1871
|
+
const counts = [
|
|
1872
|
+
[data.numPassedTests || 0, "passed"],
|
|
1873
|
+
[data.numFailedTests || 0, "failed"],
|
|
1874
|
+
[data.numPendingTests || 0, "skipped"]
|
|
1875
|
+
];
|
|
1876
|
+
for (const [n, s] of counts) {
|
|
1877
|
+
for (let i = 0; i < n; i++) {
|
|
1878
|
+
tests.push({
|
|
1879
|
+
name: `${s} test ${i + 1}`,
|
|
1880
|
+
file: data.name || "unknown",
|
|
1881
|
+
status: s,
|
|
1882
|
+
duration: 0,
|
|
1883
|
+
retries: 0
|
|
1884
|
+
});
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
return tests;
|
|
1888
|
+
}
|
|
1889
|
+
function parseFromStdout(output) {
|
|
1890
|
+
const jsonMatch = output.match(/\{[\s\S]*"testResults"[\s\S]*\}/);
|
|
1891
|
+
if (jsonMatch) {
|
|
1738
1892
|
const data = JSON.parse(jsonMatch[0]);
|
|
1739
1893
|
const suites = [];
|
|
1740
1894
|
if (data.testResults && Array.isArray(data.testResults)) {
|
|
1741
1895
|
for (const fileResult of data.testResults) {
|
|
1742
|
-
|
|
1743
|
-
name: path5.basename(fileResult.name ||
|
|
1896
|
+
suites.push({
|
|
1897
|
+
name: path5.basename(fileResult.name || "unknown"),
|
|
1744
1898
|
file: fileResult.name || "unknown",
|
|
1745
1899
|
type: "unit",
|
|
1746
1900
|
status: mapVitestStatus(fileResult.status),
|
|
1747
1901
|
duration: fileResult.duration || 0,
|
|
1748
|
-
tests: (fileResult
|
|
1749
|
-
|
|
1750
|
-
file: fileResult.name || "unknown",
|
|
1751
|
-
status: mapVitestStatus(assertion.status),
|
|
1752
|
-
duration: assertion.duration || 0,
|
|
1753
|
-
error: assertion.failureMessages?.length ? { message: assertion.failureMessages[0] } : void 0,
|
|
1754
|
-
retries: 0
|
|
1755
|
-
}))
|
|
1756
|
-
};
|
|
1757
|
-
suites.push(suite);
|
|
1902
|
+
tests: parseTestResults(fileResult)
|
|
1903
|
+
});
|
|
1758
1904
|
}
|
|
1759
1905
|
}
|
|
1760
|
-
let coverage;
|
|
1761
|
-
if (data.coverageMap) {
|
|
1762
|
-
coverage = extractCoverage(data.coverageMap);
|
|
1763
|
-
}
|
|
1764
1906
|
const success = data.success !== false && suites.every((s) => s.status !== "failed");
|
|
1907
|
+
let coverage;
|
|
1908
|
+
if (data.coverageMap) coverage = extractCoverage(data.coverageMap);
|
|
1765
1909
|
return { success, suites, coverage };
|
|
1766
|
-
} catch {
|
|
1767
|
-
return parseVitestTextOutput(output, false);
|
|
1768
1910
|
}
|
|
1911
|
+
const anyJsonMatch = output.match(/\{[\s\S]*"numTotalTests"[\s\S]*\}/);
|
|
1912
|
+
if (anyJsonMatch) {
|
|
1913
|
+
return parseVitestJSON(anyJsonMatch[0]);
|
|
1914
|
+
}
|
|
1915
|
+
throw new Error("stdout \u4E2D\u672A\u627E\u5230\u6709\u6548 JSON");
|
|
1769
1916
|
}
|
|
1770
1917
|
function parseVitestTextOutput(output, hasError) {
|
|
1771
1918
|
const suites = [];
|
|
1919
|
+
debug("vitest-text", "\u6587\u672C\u8F93\u51FA\u524D 1000 \u5B57\u7B26:", output.substring(0, 1e3));
|
|
1920
|
+
const suiteRegex = /[✓✗×✕]\s+(.+\.test\.(ts|js)|.+\.spec\.(ts|js))\s*\((\d+)[^)]*\)/i;
|
|
1921
|
+
const lines = output.split("\n");
|
|
1772
1922
|
let totalPassed = 0;
|
|
1773
1923
|
let totalFailed = 0;
|
|
1774
|
-
const suiteRegex = /[✓✗×]\s+(.+\.test\.ts|.+\.spec\.ts)\s*\((\d+)\s+test/i;
|
|
1775
|
-
const lines = output.split("\n");
|
|
1776
1924
|
for (const line of lines) {
|
|
1777
1925
|
const match = line.match(suiteRegex);
|
|
1778
1926
|
if (match) {
|
|
1779
1927
|
const file = match[1];
|
|
1780
|
-
const testCount = parseInt(match[
|
|
1928
|
+
const testCount = parseInt(match[4], 10);
|
|
1781
1929
|
const isPassed = line.includes("\u2713");
|
|
1782
1930
|
if (isPassed) totalPassed += testCount;
|
|
1783
1931
|
else totalFailed += testCount;
|
|
@@ -1797,15 +1945,39 @@ function parseVitestTextOutput(output, hasError) {
|
|
|
1797
1945
|
});
|
|
1798
1946
|
}
|
|
1799
1947
|
}
|
|
1948
|
+
if (suites.length === 0) {
|
|
1949
|
+
const summaryMatch = output.match(/Tests\s+(\d+)\s+(passed|failed)/i);
|
|
1950
|
+
if (summaryMatch) {
|
|
1951
|
+
const count = parseInt(summaryMatch[1], 10);
|
|
1952
|
+
const status = summaryMatch[2].toLowerCase() === "passed" ? "passed" : "failed";
|
|
1953
|
+
suites.push({
|
|
1954
|
+
name: "Vitest Summary",
|
|
1955
|
+
file: "unknown",
|
|
1956
|
+
type: "unit",
|
|
1957
|
+
status,
|
|
1958
|
+
duration: 0,
|
|
1959
|
+
tests: Array.from({ length: count }, (_, i) => ({
|
|
1960
|
+
name: `test ${i + 1}`,
|
|
1961
|
+
file: "unknown",
|
|
1962
|
+
status,
|
|
1963
|
+
duration: 0,
|
|
1964
|
+
retries: 0
|
|
1965
|
+
}))
|
|
1966
|
+
});
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
debug("vitest-text", `\u89E3\u6790\u5230 ${suites.length} \u4E2A\u5957\u4EF6, ${totalPassed} \u901A\u8FC7, ${totalFailed} \u5931\u8D25`);
|
|
1800
1970
|
return {
|
|
1801
1971
|
success: !hasError || totalFailed === 0,
|
|
1802
1972
|
suites
|
|
1803
1973
|
};
|
|
1804
1974
|
}
|
|
1805
1975
|
function mapVitestStatus(status) {
|
|
1976
|
+
if (!status) return "pending";
|
|
1806
1977
|
switch (status) {
|
|
1807
1978
|
case "passed":
|
|
1808
1979
|
case "pass":
|
|
1980
|
+
case "done":
|
|
1809
1981
|
return "passed";
|
|
1810
1982
|
case "failed":
|
|
1811
1983
|
case "fail":
|
|
@@ -1813,6 +1985,7 @@ function mapVitestStatus(status) {
|
|
|
1813
1985
|
case "skipped":
|
|
1814
1986
|
case "skip":
|
|
1815
1987
|
case "pending":
|
|
1988
|
+
case "todo":
|
|
1816
1989
|
return "skipped";
|
|
1817
1990
|
default:
|
|
1818
1991
|
return "pending";
|
|
@@ -2749,7 +2922,7 @@ async function testAIConnection(config) {
|
|
|
2749
2922
|
}
|
|
2750
2923
|
|
|
2751
2924
|
// src/services/mock-server.ts
|
|
2752
|
-
import
|
|
2925
|
+
import fs7 from "fs";
|
|
2753
2926
|
import path7 from "path";
|
|
2754
2927
|
var serverState = {
|
|
2755
2928
|
running: false,
|
|
@@ -2762,18 +2935,18 @@ function getMockServerState() {
|
|
|
2762
2935
|
}
|
|
2763
2936
|
async function loadMockRoutes(routesDir) {
|
|
2764
2937
|
const absDir = path7.resolve(process.cwd(), routesDir);
|
|
2765
|
-
if (!
|
|
2938
|
+
if (!fs7.existsSync(absDir)) {
|
|
2766
2939
|
return [];
|
|
2767
2940
|
}
|
|
2768
2941
|
const routes = [];
|
|
2769
|
-
const entries =
|
|
2942
|
+
const entries = fs7.readdirSync(absDir, { withFileTypes: true });
|
|
2770
2943
|
for (const entry of entries) {
|
|
2771
2944
|
if (!entry.isFile()) continue;
|
|
2772
2945
|
const filePath = path7.join(absDir, entry.name);
|
|
2773
2946
|
const ext = path7.extname(entry.name);
|
|
2774
2947
|
try {
|
|
2775
2948
|
if (ext === ".json") {
|
|
2776
|
-
const content =
|
|
2949
|
+
const content = fs7.readFileSync(filePath, "utf-8");
|
|
2777
2950
|
const parsed = JSON.parse(content);
|
|
2778
2951
|
if (Array.isArray(parsed)) {
|
|
2779
2952
|
routes.push(...parsed);
|
|
@@ -2965,11 +3138,11 @@ function generateMockRouteTemplate(name) {
|
|
|
2965
3138
|
}
|
|
2966
3139
|
function initMockRoutesDir(routesDir) {
|
|
2967
3140
|
const absDir = path7.resolve(process.cwd(), routesDir);
|
|
2968
|
-
if (!
|
|
2969
|
-
|
|
3141
|
+
if (!fs7.existsSync(absDir)) {
|
|
3142
|
+
fs7.mkdirSync(absDir, { recursive: true });
|
|
2970
3143
|
}
|
|
2971
3144
|
const examplePath = path7.join(absDir, "example.json");
|
|
2972
|
-
if (!
|
|
3145
|
+
if (!fs7.existsSync(examplePath)) {
|
|
2973
3146
|
const exampleRoutes = [
|
|
2974
3147
|
{
|
|
2975
3148
|
method: "GET",
|
|
@@ -2993,24 +3166,24 @@ function initMockRoutesDir(routesDir) {
|
|
|
2993
3166
|
}
|
|
2994
3167
|
}
|
|
2995
3168
|
];
|
|
2996
|
-
|
|
3169
|
+
fs7.writeFileSync(examplePath, JSON.stringify(exampleRoutes, null, 2), "utf-8");
|
|
2997
3170
|
}
|
|
2998
3171
|
}
|
|
2999
3172
|
|
|
3000
3173
|
// src/services/visual.ts
|
|
3001
|
-
import
|
|
3174
|
+
import fs8 from "fs";
|
|
3002
3175
|
import path8 from "path";
|
|
3003
3176
|
import pixelmatch from "pixelmatch";
|
|
3004
3177
|
import { PNG } from "pngjs";
|
|
3005
3178
|
function compareImages(baselinePath, currentPath, diffOutputPath, threshold = 0.1) {
|
|
3006
|
-
if (!
|
|
3179
|
+
if (!fs8.existsSync(baselinePath)) {
|
|
3007
3180
|
throw new Error(`\u57FA\u7EBF\u56FE\u7247\u4E0D\u5B58\u5728: ${baselinePath}`);
|
|
3008
3181
|
}
|
|
3009
|
-
if (!
|
|
3182
|
+
if (!fs8.existsSync(currentPath)) {
|
|
3010
3183
|
throw new Error(`\u5F53\u524D\u56FE\u7247\u4E0D\u5B58\u5728: ${currentPath}`);
|
|
3011
3184
|
}
|
|
3012
|
-
const baseline = PNG.sync.read(
|
|
3013
|
-
const current = PNG.sync.read(
|
|
3185
|
+
const baseline = PNG.sync.read(fs8.readFileSync(baselinePath));
|
|
3186
|
+
const current = PNG.sync.read(fs8.readFileSync(currentPath));
|
|
3014
3187
|
if (baseline.width !== current.width || baseline.height !== current.height) {
|
|
3015
3188
|
return {
|
|
3016
3189
|
passed: false,
|
|
@@ -3039,10 +3212,10 @@ function compareImages(baselinePath, currentPath, diffOutputPath, threshold = 0.
|
|
|
3039
3212
|
let diffPath;
|
|
3040
3213
|
if (diffPixels > 0) {
|
|
3041
3214
|
const diffDir = path8.dirname(diffOutputPath);
|
|
3042
|
-
if (!
|
|
3043
|
-
|
|
3215
|
+
if (!fs8.existsSync(diffDir)) {
|
|
3216
|
+
fs8.mkdirSync(diffDir, { recursive: true });
|
|
3044
3217
|
}
|
|
3045
|
-
|
|
3218
|
+
fs8.writeFileSync(diffOutputPath, PNG.sync.write(diff));
|
|
3046
3219
|
diffPath = diffOutputPath;
|
|
3047
3220
|
}
|
|
3048
3221
|
return {
|
|
@@ -3056,14 +3229,14 @@ function compareImages(baselinePath, currentPath, diffOutputPath, threshold = 0.
|
|
|
3056
3229
|
};
|
|
3057
3230
|
}
|
|
3058
3231
|
function createBaseline(currentPath, baselinePath) {
|
|
3059
|
-
if (!
|
|
3232
|
+
if (!fs8.existsSync(currentPath)) {
|
|
3060
3233
|
throw new Error(`\u5F53\u524D\u622A\u56FE\u4E0D\u5B58\u5728: ${currentPath}`);
|
|
3061
3234
|
}
|
|
3062
3235
|
const baselineDir = path8.dirname(baselinePath);
|
|
3063
|
-
if (!
|
|
3064
|
-
|
|
3236
|
+
if (!fs8.existsSync(baselineDir)) {
|
|
3237
|
+
fs8.mkdirSync(baselineDir, { recursive: true });
|
|
3065
3238
|
}
|
|
3066
|
-
|
|
3239
|
+
fs8.copyFileSync(currentPath, baselinePath);
|
|
3067
3240
|
return baselinePath;
|
|
3068
3241
|
}
|
|
3069
3242
|
function updateBaseline(currentPath, baselinePath) {
|
|
@@ -3071,69 +3244,69 @@ function updateBaseline(currentPath, baselinePath) {
|
|
|
3071
3244
|
}
|
|
3072
3245
|
function updateAllBaselines(currentDir, baselineDir) {
|
|
3073
3246
|
const updated = [];
|
|
3074
|
-
if (!
|
|
3247
|
+
if (!fs8.existsSync(currentDir)) {
|
|
3075
3248
|
return updated;
|
|
3076
3249
|
}
|
|
3077
|
-
const files =
|
|
3250
|
+
const files = fs8.readdirSync(currentDir).filter((f) => f.endsWith(".png"));
|
|
3078
3251
|
for (const file of files) {
|
|
3079
3252
|
const currentPath = path8.join(currentDir, file);
|
|
3080
3253
|
const baselinePath = path8.join(baselineDir, file);
|
|
3081
3254
|
const baselineDirAbs = path8.dirname(baselinePath);
|
|
3082
|
-
if (!
|
|
3083
|
-
|
|
3255
|
+
if (!fs8.existsSync(baselineDirAbs)) {
|
|
3256
|
+
fs8.mkdirSync(baselineDirAbs, { recursive: true });
|
|
3084
3257
|
}
|
|
3085
|
-
|
|
3258
|
+
fs8.copyFileSync(currentPath, baselinePath);
|
|
3086
3259
|
updated.push(file);
|
|
3087
3260
|
}
|
|
3088
3261
|
return updated;
|
|
3089
3262
|
}
|
|
3090
3263
|
function cleanBaselines(baselineDir) {
|
|
3091
|
-
if (!
|
|
3264
|
+
if (!fs8.existsSync(baselineDir)) {
|
|
3092
3265
|
return 0;
|
|
3093
3266
|
}
|
|
3094
|
-
const files =
|
|
3267
|
+
const files = fs8.readdirSync(baselineDir).filter((f) => f.endsWith(".png"));
|
|
3095
3268
|
let count = 0;
|
|
3096
3269
|
for (const file of files) {
|
|
3097
|
-
|
|
3270
|
+
fs8.unlinkSync(path8.join(baselineDir, file));
|
|
3098
3271
|
count++;
|
|
3099
3272
|
}
|
|
3100
3273
|
return count;
|
|
3101
3274
|
}
|
|
3102
3275
|
function cleanDiffs(diffDir) {
|
|
3103
|
-
if (!
|
|
3276
|
+
if (!fs8.existsSync(diffDir)) {
|
|
3104
3277
|
return 0;
|
|
3105
3278
|
}
|
|
3106
|
-
const files =
|
|
3279
|
+
const files = fs8.readdirSync(diffDir).filter((f) => f.endsWith(".png"));
|
|
3107
3280
|
let count = 0;
|
|
3108
3281
|
for (const file of files) {
|
|
3109
|
-
|
|
3282
|
+
fs8.unlinkSync(path8.join(diffDir, file));
|
|
3110
3283
|
count++;
|
|
3111
3284
|
}
|
|
3112
3285
|
return count;
|
|
3113
3286
|
}
|
|
3114
3287
|
function listBaselines(baselineDir) {
|
|
3115
|
-
if (!
|
|
3288
|
+
if (!fs8.existsSync(baselineDir)) {
|
|
3116
3289
|
return [];
|
|
3117
3290
|
}
|
|
3118
|
-
return
|
|
3291
|
+
return fs8.readdirSync(baselineDir).filter((f) => f.endsWith(".png")).sort();
|
|
3119
3292
|
}
|
|
3120
3293
|
function listDiffs(diffDir) {
|
|
3121
|
-
if (!
|
|
3294
|
+
if (!fs8.existsSync(diffDir)) {
|
|
3122
3295
|
return [];
|
|
3123
3296
|
}
|
|
3124
|
-
return
|
|
3297
|
+
return fs8.readdirSync(diffDir).filter((f) => f.endsWith(".png")).sort();
|
|
3125
3298
|
}
|
|
3126
3299
|
function compareDirectories(baselineDir, currentDir, diffDir, threshold = 0.1) {
|
|
3127
3300
|
const results = [];
|
|
3128
|
-
if (!
|
|
3301
|
+
if (!fs8.existsSync(currentDir)) {
|
|
3129
3302
|
return results;
|
|
3130
3303
|
}
|
|
3131
|
-
const currentFiles =
|
|
3304
|
+
const currentFiles = fs8.readdirSync(currentDir).filter((f) => f.endsWith(".png"));
|
|
3132
3305
|
for (const file of currentFiles) {
|
|
3133
3306
|
const currentPath = path8.join(currentDir, file);
|
|
3134
3307
|
const baselinePath = path8.join(baselineDir, file);
|
|
3135
3308
|
const diffPath = path8.join(diffDir, file);
|
|
3136
|
-
if (!
|
|
3309
|
+
if (!fs8.existsSync(baselinePath)) {
|
|
3137
3310
|
createBaseline(currentPath, baselinePath);
|
|
3138
3311
|
results.push({
|
|
3139
3312
|
passed: true,
|
|
@@ -3165,14 +3338,14 @@ function compareDirectories(baselineDir, currentDir, diffDir, threshold = 0.1) {
|
|
|
3165
3338
|
}
|
|
3166
3339
|
|
|
3167
3340
|
// src/services/source-analyzer.ts
|
|
3168
|
-
import
|
|
3341
|
+
import fs9 from "fs";
|
|
3169
3342
|
import path9 from "path";
|
|
3170
3343
|
function analyzeFile(filePath) {
|
|
3171
3344
|
const absolutePath = path9.resolve(process.cwd(), filePath);
|
|
3172
|
-
if (!
|
|
3345
|
+
if (!fs9.existsSync(absolutePath)) {
|
|
3173
3346
|
return { filePath, exports: [], apiCalls: [] };
|
|
3174
3347
|
}
|
|
3175
|
-
const content =
|
|
3348
|
+
const content = fs9.readFileSync(absolutePath, "utf-8");
|
|
3176
3349
|
const ext = path9.extname(filePath);
|
|
3177
3350
|
const result = {
|
|
3178
3351
|
filePath,
|
|
@@ -3527,13 +3700,13 @@ function cleanTemplateUrl(url) {
|
|
|
3527
3700
|
}
|
|
3528
3701
|
function scanAPICalls(srcDir) {
|
|
3529
3702
|
const absDir = path9.resolve(process.cwd(), srcDir);
|
|
3530
|
-
if (!
|
|
3703
|
+
if (!fs9.existsSync(absDir)) {
|
|
3531
3704
|
return [];
|
|
3532
3705
|
}
|
|
3533
3706
|
const allCalls = [];
|
|
3534
3707
|
const seen = /* @__PURE__ */ new Set();
|
|
3535
3708
|
function walk(dir) {
|
|
3536
|
-
const entries =
|
|
3709
|
+
const entries = fs9.readdirSync(dir, { withFileTypes: true });
|
|
3537
3710
|
for (const entry of entries) {
|
|
3538
3711
|
if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === "dist") continue;
|
|
3539
3712
|
const fullPath = path9.join(dir, entry.name);
|
|
@@ -3541,7 +3714,7 @@ function scanAPICalls(srcDir) {
|
|
|
3541
3714
|
walk(fullPath);
|
|
3542
3715
|
} else if (entry.isFile() && /\.(ts|js|vue|mjs)$/.test(entry.name)) {
|
|
3543
3716
|
try {
|
|
3544
|
-
const content =
|
|
3717
|
+
const content = fs9.readFileSync(fullPath, "utf-8");
|
|
3545
3718
|
const calls = extractAPICalls(content, path9.relative(process.cwd(), fullPath));
|
|
3546
3719
|
for (const call of calls) {
|
|
3547
3720
|
const key = `${call.method}:${call.url}`;
|