qat-cli 0.3.1 → 0.3.3
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 +1319 -1233
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +308 -129
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +128 -120
- package/dist/index.d.ts +128 -120
- package/dist/index.js +307 -128
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -955,12 +955,36 @@ Handlebars.registerHelper("propTestValue", (prop) => {
|
|
|
955
955
|
};
|
|
956
956
|
return map[prop.type] || "'test-value'";
|
|
957
957
|
});
|
|
958
|
+
function resolveImportPath(testType, targetPath) {
|
|
959
|
+
if (!targetPath) return targetPath;
|
|
960
|
+
const testDirMap = {
|
|
961
|
+
unit: "tests/unit",
|
|
962
|
+
component: "tests/component",
|
|
963
|
+
e2e: "tests/e2e",
|
|
964
|
+
api: "tests/api",
|
|
965
|
+
visual: "tests/visual",
|
|
966
|
+
performance: "tests/e2e"
|
|
967
|
+
};
|
|
968
|
+
const testDir = testDirMap[testType];
|
|
969
|
+
if (!testDir) return targetPath;
|
|
970
|
+
if (targetPath.startsWith("../") || targetPath.startsWith("./../")) {
|
|
971
|
+
return targetPath;
|
|
972
|
+
}
|
|
973
|
+
const depth = testDir.split("/").length;
|
|
974
|
+
const prefix = "../".repeat(depth);
|
|
975
|
+
let cleanPath = targetPath.replace(/^\.\//, "");
|
|
976
|
+
if (!cleanPath.endsWith(".vue")) {
|
|
977
|
+
cleanPath = cleanPath.replace(/\.(ts|js|tsx|jsx)$/, "");
|
|
978
|
+
}
|
|
979
|
+
return `${prefix}${cleanPath}`;
|
|
980
|
+
}
|
|
958
981
|
function registerTemplate(type, templateContent) {
|
|
959
982
|
customTemplates.set(type, templateContent);
|
|
960
983
|
}
|
|
961
984
|
function renderTemplate(type, context) {
|
|
962
985
|
const templateContent = loadTemplate(type);
|
|
963
986
|
const template = Handlebars.compile(templateContent);
|
|
987
|
+
const resolvedTarget = resolveImportPath(type, context.target);
|
|
964
988
|
const fullContext = {
|
|
965
989
|
vueVersion: 3,
|
|
966
990
|
typescript: true,
|
|
@@ -981,6 +1005,8 @@ function renderTemplate(type, context) {
|
|
|
981
1005
|
requiredProps: [],
|
|
982
1006
|
optionalProps: [],
|
|
983
1007
|
...context,
|
|
1008
|
+
// 使用计算后的正确路径
|
|
1009
|
+
target: resolvedTarget,
|
|
984
1010
|
framework: context.framework || "vue",
|
|
985
1011
|
camelName: context.camelName || toCamelCase(context.name),
|
|
986
1012
|
pascalName: context.pascalName || toPascalCase(context.name)
|
|
@@ -1623,12 +1649,23 @@ function generateHTMLReport(data) {
|
|
|
1623
1649
|
// src/runners/vitest-runner.ts
|
|
1624
1650
|
import { execFile } from "child_process";
|
|
1625
1651
|
import path5 from "path";
|
|
1652
|
+
import fs6 from "fs";
|
|
1653
|
+
import os from "os";
|
|
1654
|
+
var isVerbose = () => process.env.QAT_VERBOSE === "true";
|
|
1655
|
+
function debug(label, ...args) {
|
|
1656
|
+
if (isVerbose()) {
|
|
1657
|
+
console.log(`\x1B[90m [debug:${label}]\x1B[0m`, ...args);
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1626
1660
|
async function runVitest(options) {
|
|
1627
1661
|
const startTime = Date.now();
|
|
1628
1662
|
const args = buildVitestArgs(options);
|
|
1663
|
+
debug("vitest", "\u547D\u4EE4\u53C2\u6570:", args.join(" "));
|
|
1629
1664
|
try {
|
|
1630
1665
|
const result = await execVitest(args);
|
|
1631
1666
|
const endTime = Date.now();
|
|
1667
|
+
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`);
|
|
1668
|
+
debug("vitest", "\u89E3\u6790\u65B9\u5F0F:", result.parseMethod);
|
|
1632
1669
|
return {
|
|
1633
1670
|
type: options.type,
|
|
1634
1671
|
status: result.success ? "passed" : "failed",
|
|
@@ -1671,14 +1708,14 @@ function buildVitestArgs(options) {
|
|
|
1671
1708
|
if (options.files && options.files.length > 0) {
|
|
1672
1709
|
args.push(...options.files);
|
|
1673
1710
|
} else {
|
|
1674
|
-
const
|
|
1675
|
-
unit:
|
|
1676
|
-
component:
|
|
1677
|
-
api:
|
|
1711
|
+
const pathMap = {
|
|
1712
|
+
unit: "tests/unit/**/*.test.ts",
|
|
1713
|
+
component: "tests/component/**/*.test.ts",
|
|
1714
|
+
api: "tests/api/**/*.test.ts"
|
|
1678
1715
|
};
|
|
1679
|
-
const
|
|
1680
|
-
if (
|
|
1681
|
-
args.push(
|
|
1716
|
+
const testPattern = pathMap[options.type];
|
|
1717
|
+
if (testPattern) {
|
|
1718
|
+
args.push(testPattern);
|
|
1682
1719
|
}
|
|
1683
1720
|
}
|
|
1684
1721
|
if (options.coverage) {
|
|
@@ -1693,12 +1730,11 @@ function buildVitestArgs(options) {
|
|
|
1693
1730
|
return args;
|
|
1694
1731
|
}
|
|
1695
1732
|
async function execVitest(args) {
|
|
1696
|
-
const os = await import("os");
|
|
1697
|
-
const fs9 = await import("fs");
|
|
1698
1733
|
const tmpFile = path5.join(os.tmpdir(), `qat-vitest-result-${Date.now()}.json`);
|
|
1699
1734
|
const argsWithOutput = [...args, "--outputFile", tmpFile];
|
|
1700
1735
|
return new Promise((resolve, reject) => {
|
|
1701
1736
|
const npx = process.platform === "win32" ? "npx.cmd" : "npx";
|
|
1737
|
+
debug("vitest", "\u6267\u884C\u547D\u4EE4:", npx, argsWithOutput.join(" "));
|
|
1702
1738
|
const child = execFile(npx, argsWithOutput, {
|
|
1703
1739
|
cwd: process.cwd(),
|
|
1704
1740
|
env: { ...process.env, FORCE_COLOR: "0", NO_COLOR: "1" },
|
|
@@ -1706,85 +1742,113 @@ async function execVitest(args) {
|
|
|
1706
1742
|
shell: true
|
|
1707
1743
|
}, (error, stdout, stderr) => {
|
|
1708
1744
|
const rawOutput = stdout || stderr || "";
|
|
1745
|
+
const exitCode = error && "code" in error ? error.code : 0;
|
|
1746
|
+
debug("vitest", `\u9000\u51FA\u7801: ${exitCode}`);
|
|
1747
|
+
debug("vitest", `stdout \u957F\u5EA6: ${stdout?.length || 0}, stderr \u957F\u5EA6: ${stderr?.length || 0}`);
|
|
1709
1748
|
let jsonResult = null;
|
|
1710
1749
|
try {
|
|
1711
|
-
if (
|
|
1712
|
-
jsonResult =
|
|
1713
|
-
|
|
1750
|
+
if (fs6.existsSync(tmpFile)) {
|
|
1751
|
+
jsonResult = fs6.readFileSync(tmpFile, "utf-8");
|
|
1752
|
+
debug("vitest", `\u4ECE\u4E34\u65F6\u6587\u4EF6\u8BFB\u53D6\u5230 JSON (${jsonResult.length} \u5B57\u7B26)`);
|
|
1753
|
+
debug("vitest", "JSON \u524D 500 \u5B57\u7B26:", jsonResult.substring(0, 500));
|
|
1754
|
+
fs6.unlinkSync(tmpFile);
|
|
1755
|
+
} else {
|
|
1756
|
+
debug("vitest", "\u4E34\u65F6\u6587\u4EF6\u4E0D\u5B58\u5728:", tmpFile);
|
|
1714
1757
|
}
|
|
1715
|
-
} catch {
|
|
1758
|
+
} catch (e) {
|
|
1759
|
+
debug("vitest", "\u4E34\u65F6\u6587\u4EF6\u8BFB\u53D6\u5931\u8D25:", e instanceof Error ? e.message : String(e));
|
|
1716
1760
|
}
|
|
1717
1761
|
if (jsonResult) {
|
|
1718
1762
|
try {
|
|
1719
|
-
const parsed =
|
|
1720
|
-
|
|
1763
|
+
const parsed = parseVitestJSON(jsonResult);
|
|
1764
|
+
debug("vitest", "\u4ECE\u4E34\u65F6\u6587\u4EF6\u89E3\u6790\u6210\u529F:", parsed.suites.length, "\u4E2A\u5957\u4EF6");
|
|
1765
|
+
resolve({ ...parsed, rawOutput, parseMethod: "outputFile-JSON" });
|
|
1721
1766
|
return;
|
|
1722
|
-
} catch {
|
|
1767
|
+
} catch (e) {
|
|
1768
|
+
debug("vitest", "\u4E34\u65F6\u6587\u4EF6 JSON \u89E3\u6790\u5931\u8D25:", e instanceof Error ? e.message : String(e));
|
|
1723
1769
|
}
|
|
1724
1770
|
}
|
|
1771
|
+
debug("vitest", "\u5C1D\u8BD5\u4ECE stdout \u63D0\u53D6 JSON...");
|
|
1725
1772
|
try {
|
|
1726
|
-
const parsed =
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1773
|
+
const parsed = parseFromStdout(rawOutput);
|
|
1774
|
+
debug("vitest", "\u4ECE stdout \u89E3\u6790\u6210\u529F:", parsed.suites.length, "\u4E2A\u5957\u4EF6");
|
|
1775
|
+
resolve({ ...parsed, rawOutput, parseMethod: "stdout-JSON" });
|
|
1776
|
+
return;
|
|
1777
|
+
} catch (e) {
|
|
1778
|
+
debug("vitest", "stdout JSON \u89E3\u6790\u5931\u8D25:", e instanceof Error ? e.message : String(e));
|
|
1779
|
+
}
|
|
1780
|
+
debug("vitest", "\u5C1D\u8BD5\u4ECE\u6587\u672C\u8F93\u51FA\u89E3\u6790...");
|
|
1781
|
+
if (rawOutput) {
|
|
1782
|
+
const parsed = parseVitestTextOutput(rawOutput, !!error);
|
|
1783
|
+
debug("vitest", "\u6587\u672C\u89E3\u6790\u7ED3\u679C:", parsed.suites.length, "\u4E2A\u5957\u4EF6");
|
|
1784
|
+
resolve({ ...parsed, rawOutput, parseMethod: "text-fallback" });
|
|
1785
|
+
return;
|
|
1736
1786
|
}
|
|
1787
|
+
if (error && error.message.includes("ENOENT")) {
|
|
1788
|
+
reject(new Error("\u672A\u627E\u5230 vitest\uFF0C\u8BF7\u786E\u4FDD\u5DF2\u5B89\u88C5: npm install -D vitest"));
|
|
1789
|
+
return;
|
|
1790
|
+
}
|
|
1791
|
+
debug("vitest", "\u6240\u6709\u89E3\u6790\u65B9\u5F0F\u5747\u5931\u8D25\uFF0C\u8FD4\u56DE\u7A7A\u7ED3\u679C");
|
|
1792
|
+
resolve({ success: !error, suites: [], rawOutput, parseMethod: "none" });
|
|
1737
1793
|
});
|
|
1738
1794
|
child.on("error", (err) => {
|
|
1739
1795
|
try {
|
|
1740
|
-
|
|
1796
|
+
fs6.unlinkSync(tmpFile);
|
|
1741
1797
|
} catch {
|
|
1742
1798
|
}
|
|
1743
1799
|
reject(new Error(`Vitest \u6267\u884C\u5931\u8D25: ${err.message}`));
|
|
1744
1800
|
});
|
|
1745
1801
|
});
|
|
1746
1802
|
}
|
|
1747
|
-
function
|
|
1803
|
+
function parseVitestJSON(jsonStr) {
|
|
1748
1804
|
const data = JSON.parse(jsonStr);
|
|
1749
1805
|
const suites = [];
|
|
1806
|
+
debug("vitest-json", "JSON \u9876\u5C42\u5B57\u6BB5:", Object.keys(data).join(", "));
|
|
1750
1807
|
if (data.testResults && Array.isArray(data.testResults)) {
|
|
1808
|
+
debug("vitest-json", `testResults \u6570\u91CF: ${data.testResults.length}`);
|
|
1751
1809
|
for (const fileResult of data.testResults) {
|
|
1810
|
+
const suiteTests = parseTestResults(fileResult);
|
|
1811
|
+
suites.push({
|
|
1812
|
+
name: path5.basename(fileResult.name || "unknown"),
|
|
1813
|
+
file: fileResult.name || "unknown",
|
|
1814
|
+
type: "unit",
|
|
1815
|
+
status: mapVitestStatus(fileResult.status),
|
|
1816
|
+
duration: fileResult.duration || 0,
|
|
1817
|
+
tests: suiteTests
|
|
1818
|
+
});
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
if (suites.length === 0 && data.numTotalTests !== void 0) {
|
|
1822
|
+
debug("vitest-json", `\u4F7F\u7528\u6C47\u603B\u683C\u5F0F: total=${data.numTotalTests}, passed=${data.numPassedTests}`);
|
|
1823
|
+
suites.push({
|
|
1824
|
+
name: "Vitest Results",
|
|
1825
|
+
file: "unknown",
|
|
1826
|
+
type: "unit",
|
|
1827
|
+
status: data.numFailedTests > 0 ? "failed" : "passed",
|
|
1828
|
+
duration: 0,
|
|
1829
|
+
tests: buildTestsFromSummary(data)
|
|
1830
|
+
});
|
|
1831
|
+
}
|
|
1832
|
+
if (suites.length === 0 && data.suites && Array.isArray(data.suites)) {
|
|
1833
|
+
debug("vitest-json", `suites \u6570\u91CF: ${data.suites.length}`);
|
|
1834
|
+
for (const suiteData of data.suites) {
|
|
1752
1835
|
const suiteTests = [];
|
|
1753
|
-
const
|
|
1754
|
-
for (const assertion of assertions) {
|
|
1836
|
+
for (const test of suiteData.tests || []) {
|
|
1755
1837
|
suiteTests.push({
|
|
1756
|
-
name:
|
|
1757
|
-
file:
|
|
1758
|
-
status: mapVitestStatus(
|
|
1759
|
-
duration:
|
|
1760
|
-
error:
|
|
1838
|
+
name: test.name || test.title || "unknown",
|
|
1839
|
+
file: test.file || suiteData.file || "unknown",
|
|
1840
|
+
status: mapVitestStatus(test.status || test.result?.status),
|
|
1841
|
+
duration: test.duration || test.result?.duration || 0,
|
|
1842
|
+
error: test.result?.errors?.[0] ? { message: test.result.errors[0].message || String(test.result.errors[0]) } : void 0,
|
|
1761
1843
|
retries: 0
|
|
1762
1844
|
});
|
|
1763
1845
|
}
|
|
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
1846
|
suites.push({
|
|
1783
|
-
name:
|
|
1784
|
-
file:
|
|
1847
|
+
name: suiteData.name || "unknown",
|
|
1848
|
+
file: suiteData.file || "unknown",
|
|
1785
1849
|
type: "unit",
|
|
1786
|
-
status:
|
|
1787
|
-
duration:
|
|
1850
|
+
status: suiteTests.some((t) => t.status === "failed") ? "failed" : "passed",
|
|
1851
|
+
duration: 0,
|
|
1788
1852
|
tests: suiteTests
|
|
1789
1853
|
});
|
|
1790
1854
|
}
|
|
@@ -1793,58 +1857,101 @@ function parseVitestJSONResult(jsonStr) {
|
|
|
1793
1857
|
if (data.coverageMap) {
|
|
1794
1858
|
coverage = extractCoverage(data.coverageMap);
|
|
1795
1859
|
}
|
|
1796
|
-
|
|
1860
|
+
if (!coverage && data.coverage && typeof data.coverage === "object") {
|
|
1861
|
+
const cov = data.coverage;
|
|
1862
|
+
const totals = cov.totals || cov;
|
|
1863
|
+
const getVal = (key) => {
|
|
1864
|
+
const v = totals[key];
|
|
1865
|
+
return typeof v === "number" ? v : typeof v === "object" && v !== null && "pct" in v ? v.pct / 100 : 0;
|
|
1866
|
+
};
|
|
1867
|
+
coverage = {
|
|
1868
|
+
lines: getVal("lines"),
|
|
1869
|
+
statements: getVal("statements"),
|
|
1870
|
+
functions: getVal("functions"),
|
|
1871
|
+
branches: getVal("branches")
|
|
1872
|
+
};
|
|
1873
|
+
}
|
|
1874
|
+
const success = data.success !== false ? data.numFailedTests !== void 0 ? data.numFailedTests === 0 : suites.every((s) => s.status !== "failed") : false;
|
|
1797
1875
|
return { success, suites, coverage };
|
|
1798
1876
|
}
|
|
1799
|
-
function
|
|
1800
|
-
const
|
|
1801
|
-
|
|
1802
|
-
|
|
1877
|
+
function parseTestResults(fileResult) {
|
|
1878
|
+
const tests = [];
|
|
1879
|
+
const assertions = fileResult.assertionResults || fileResult.tests || [];
|
|
1880
|
+
for (const assertion of assertions) {
|
|
1881
|
+
tests.push({
|
|
1882
|
+
name: assertion.title || assertion.fullName || assertion.name || "unknown",
|
|
1883
|
+
file: fileResult.name || "unknown",
|
|
1884
|
+
status: mapVitestStatus(assertion.status),
|
|
1885
|
+
duration: assertion.duration || 0,
|
|
1886
|
+
error: assertion.failureMessages?.length ? { message: assertion.failureMessages[0] } : assertion.failureMessage ? { message: assertion.failureMessage } : void 0,
|
|
1887
|
+
retries: 0
|
|
1888
|
+
});
|
|
1803
1889
|
}
|
|
1804
|
-
|
|
1890
|
+
if (tests.length === 0 && fileResult.numPassingTests !== void 0) {
|
|
1891
|
+
tests.push(...buildTestsFromSummary(fileResult));
|
|
1892
|
+
}
|
|
1893
|
+
return tests;
|
|
1894
|
+
}
|
|
1895
|
+
function buildTestsFromSummary(data) {
|
|
1896
|
+
const tests = [];
|
|
1897
|
+
const counts = [
|
|
1898
|
+
[data.numPassedTests || 0, "passed"],
|
|
1899
|
+
[data.numFailedTests || 0, "failed"],
|
|
1900
|
+
[data.numPendingTests || 0, "skipped"]
|
|
1901
|
+
];
|
|
1902
|
+
for (const [n, s] of counts) {
|
|
1903
|
+
for (let i = 0; i < n; i++) {
|
|
1904
|
+
tests.push({
|
|
1905
|
+
name: `${s} test ${i + 1}`,
|
|
1906
|
+
file: data.name || "unknown",
|
|
1907
|
+
status: s,
|
|
1908
|
+
duration: 0,
|
|
1909
|
+
retries: 0
|
|
1910
|
+
});
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
return tests;
|
|
1914
|
+
}
|
|
1915
|
+
function parseFromStdout(output) {
|
|
1916
|
+
const jsonMatch = output.match(/\{[\s\S]*"testResults"[\s\S]*\}/);
|
|
1917
|
+
if (jsonMatch) {
|
|
1805
1918
|
const data = JSON.parse(jsonMatch[0]);
|
|
1806
1919
|
const suites = [];
|
|
1807
1920
|
if (data.testResults && Array.isArray(data.testResults)) {
|
|
1808
1921
|
for (const fileResult of data.testResults) {
|
|
1809
|
-
|
|
1810
|
-
name: path5.basename(fileResult.name ||
|
|
1922
|
+
suites.push({
|
|
1923
|
+
name: path5.basename(fileResult.name || "unknown"),
|
|
1811
1924
|
file: fileResult.name || "unknown",
|
|
1812
1925
|
type: "unit",
|
|
1813
1926
|
status: mapVitestStatus(fileResult.status),
|
|
1814
1927
|
duration: fileResult.duration || 0,
|
|
1815
|
-
tests: (fileResult
|
|
1816
|
-
|
|
1817
|
-
file: fileResult.name || "unknown",
|
|
1818
|
-
status: mapVitestStatus(assertion.status),
|
|
1819
|
-
duration: assertion.duration || 0,
|
|
1820
|
-
error: assertion.failureMessages?.length ? { message: assertion.failureMessages[0] } : void 0,
|
|
1821
|
-
retries: 0
|
|
1822
|
-
}))
|
|
1823
|
-
};
|
|
1824
|
-
suites.push(suite);
|
|
1928
|
+
tests: parseTestResults(fileResult)
|
|
1929
|
+
});
|
|
1825
1930
|
}
|
|
1826
1931
|
}
|
|
1827
|
-
let coverage;
|
|
1828
|
-
if (data.coverageMap) {
|
|
1829
|
-
coverage = extractCoverage(data.coverageMap);
|
|
1830
|
-
}
|
|
1831
1932
|
const success = data.success !== false && suites.every((s) => s.status !== "failed");
|
|
1933
|
+
let coverage;
|
|
1934
|
+
if (data.coverageMap) coverage = extractCoverage(data.coverageMap);
|
|
1832
1935
|
return { success, suites, coverage };
|
|
1833
|
-
} catch {
|
|
1834
|
-
return parseVitestTextOutput(output, false);
|
|
1835
1936
|
}
|
|
1937
|
+
const anyJsonMatch = output.match(/\{[\s\S]*"numTotalTests"[\s\S]*\}/);
|
|
1938
|
+
if (anyJsonMatch) {
|
|
1939
|
+
return parseVitestJSON(anyJsonMatch[0]);
|
|
1940
|
+
}
|
|
1941
|
+
throw new Error("stdout \u4E2D\u672A\u627E\u5230\u6709\u6548 JSON");
|
|
1836
1942
|
}
|
|
1837
1943
|
function parseVitestTextOutput(output, hasError) {
|
|
1838
1944
|
const suites = [];
|
|
1945
|
+
debug("vitest-text", "\u6587\u672C\u8F93\u51FA\u524D 1000 \u5B57\u7B26:", output.substring(0, 1e3));
|
|
1946
|
+
const suiteRegex = /[✓✗×✕]\s+(.+\.test\.(ts|js)|.+\.spec\.(ts|js))\s*\((\d+)[^)]*\)/i;
|
|
1947
|
+
const lines = output.split("\n");
|
|
1839
1948
|
let totalPassed = 0;
|
|
1840
1949
|
let totalFailed = 0;
|
|
1841
|
-
const suiteRegex = /[✓✗×]\s+(.+\.test\.ts|.+\.spec\.ts)\s*\((\d+)\s+test/i;
|
|
1842
|
-
const lines = output.split("\n");
|
|
1843
1950
|
for (const line of lines) {
|
|
1844
1951
|
const match = line.match(suiteRegex);
|
|
1845
1952
|
if (match) {
|
|
1846
1953
|
const file = match[1];
|
|
1847
|
-
const testCount = parseInt(match[
|
|
1954
|
+
const testCount = parseInt(match[4], 10);
|
|
1848
1955
|
const isPassed = line.includes("\u2713");
|
|
1849
1956
|
if (isPassed) totalPassed += testCount;
|
|
1850
1957
|
else totalFailed += testCount;
|
|
@@ -1864,15 +1971,39 @@ function parseVitestTextOutput(output, hasError) {
|
|
|
1864
1971
|
});
|
|
1865
1972
|
}
|
|
1866
1973
|
}
|
|
1974
|
+
if (suites.length === 0) {
|
|
1975
|
+
const summaryMatch = output.match(/Tests\s+(\d+)\s+(passed|failed)/i);
|
|
1976
|
+
if (summaryMatch) {
|
|
1977
|
+
const count = parseInt(summaryMatch[1], 10);
|
|
1978
|
+
const status = summaryMatch[2].toLowerCase() === "passed" ? "passed" : "failed";
|
|
1979
|
+
suites.push({
|
|
1980
|
+
name: "Vitest Summary",
|
|
1981
|
+
file: "unknown",
|
|
1982
|
+
type: "unit",
|
|
1983
|
+
status,
|
|
1984
|
+
duration: 0,
|
|
1985
|
+
tests: Array.from({ length: count }, (_, i) => ({
|
|
1986
|
+
name: `test ${i + 1}`,
|
|
1987
|
+
file: "unknown",
|
|
1988
|
+
status,
|
|
1989
|
+
duration: 0,
|
|
1990
|
+
retries: 0
|
|
1991
|
+
}))
|
|
1992
|
+
});
|
|
1993
|
+
}
|
|
1994
|
+
}
|
|
1995
|
+
debug("vitest-text", `\u89E3\u6790\u5230 ${suites.length} \u4E2A\u5957\u4EF6, ${totalPassed} \u901A\u8FC7, ${totalFailed} \u5931\u8D25`);
|
|
1867
1996
|
return {
|
|
1868
1997
|
success: !hasError || totalFailed === 0,
|
|
1869
1998
|
suites
|
|
1870
1999
|
};
|
|
1871
2000
|
}
|
|
1872
2001
|
function mapVitestStatus(status) {
|
|
2002
|
+
if (!status) return "pending";
|
|
1873
2003
|
switch (status) {
|
|
1874
2004
|
case "passed":
|
|
1875
2005
|
case "pass":
|
|
2006
|
+
case "done":
|
|
1876
2007
|
return "passed";
|
|
1877
2008
|
case "failed":
|
|
1878
2009
|
case "fail":
|
|
@@ -1880,6 +2011,7 @@ function mapVitestStatus(status) {
|
|
|
1880
2011
|
case "skipped":
|
|
1881
2012
|
case "skip":
|
|
1882
2013
|
case "pending":
|
|
2014
|
+
case "todo":
|
|
1883
2015
|
return "skipped";
|
|
1884
2016
|
default:
|
|
1885
2017
|
return "pending";
|
|
@@ -2566,8 +2698,13 @@ ${error.actual ? `\u5B9E\u9645\u503C: ${error.actual}` : ""}`;
|
|
|
2566
2698
|
6. \u5982\u679C\u6709 props/emits \u4FE1\u606F\uFF0C\u52A1\u5FC5\u9488\u5BF9\u6BCF\u4E2A prop \u548C emit \u751F\u6210\u6D4B\u8BD5`;
|
|
2567
2699
|
}
|
|
2568
2700
|
buildGenerateTestUserPrompt(req) {
|
|
2701
|
+
const importPath = this.computeTestImportPath(req.type, req.target);
|
|
2569
2702
|
let prompt = `\u8BF7\u4E3A\u4EE5\u4E0B\u6587\u4EF6\u751F\u6210${req.type}\u6D4B\u8BD5\u4EE3\u7801:
|
|
2570
2703
|
\u76EE\u6807\u6587\u4EF6: ${req.target}
|
|
2704
|
+
\u6D4B\u8BD5\u6587\u4EF6\u5C06\u653E\u5728: ${this.getTestOutputDir(req.type)}/
|
|
2705
|
+
\u6B63\u786E\u7684 import \u8DEF\u5F84: ${importPath}
|
|
2706
|
+
|
|
2707
|
+
\u91CD\u8981\uFF1Aimport \u8BED\u53E5\u4E2D\u5FC5\u987B\u4F7F\u7528\u4E0A\u8FF0\u6B63\u786E\u7684\u76F8\u5BF9\u8DEF\u5F84 ${importPath}\uFF0C\u4E0D\u8981\u4F7F\u7528 ${req.target} \u6216\u5176\u4ED6\u8DEF\u5F84\uFF01
|
|
2571
2708
|
`;
|
|
2572
2709
|
if (req.analysis) {
|
|
2573
2710
|
prompt += "\n\u6E90\u7801\u5206\u6790\u7ED3\u679C:\n";
|
|
@@ -2617,6 +2754,46 @@ ${req.context}
|
|
|
2617
2754
|
}
|
|
2618
2755
|
return prompt;
|
|
2619
2756
|
}
|
|
2757
|
+
/**
|
|
2758
|
+
* 根据测试类型和源文件路径,计算从测试文件到源文件的正确相对导入路径
|
|
2759
|
+
*/
|
|
2760
|
+
computeTestImportPath(testType, targetPath) {
|
|
2761
|
+
if (!targetPath) return targetPath;
|
|
2762
|
+
const testDirMap = {
|
|
2763
|
+
unit: "tests/unit",
|
|
2764
|
+
component: "tests/component",
|
|
2765
|
+
e2e: "tests/e2e",
|
|
2766
|
+
api: "tests/api",
|
|
2767
|
+
visual: "tests/visual",
|
|
2768
|
+
performance: "tests/e2e"
|
|
2769
|
+
};
|
|
2770
|
+
const testDir = testDirMap[testType];
|
|
2771
|
+
if (!testDir) return targetPath;
|
|
2772
|
+
if (targetPath.startsWith("../") || targetPath.startsWith("./../")) {
|
|
2773
|
+
return targetPath;
|
|
2774
|
+
}
|
|
2775
|
+
const depth = testDir.split("/").length;
|
|
2776
|
+
const prefix = "../".repeat(depth);
|
|
2777
|
+
let cleanPath = targetPath.replace(/^\.\//, "");
|
|
2778
|
+
if (!cleanPath.endsWith(".vue")) {
|
|
2779
|
+
cleanPath = cleanPath.replace(/\.(ts|js|tsx|jsx)$/, "");
|
|
2780
|
+
}
|
|
2781
|
+
return `${prefix}${cleanPath}`;
|
|
2782
|
+
}
|
|
2783
|
+
/**
|
|
2784
|
+
* 获取测试输出目录
|
|
2785
|
+
*/
|
|
2786
|
+
getTestOutputDir(testType) {
|
|
2787
|
+
const dirMap = {
|
|
2788
|
+
unit: "tests/unit",
|
|
2789
|
+
component: "tests/component",
|
|
2790
|
+
e2e: "tests/e2e",
|
|
2791
|
+
api: "tests/api",
|
|
2792
|
+
visual: "tests/visual",
|
|
2793
|
+
performance: "tests/e2e"
|
|
2794
|
+
};
|
|
2795
|
+
return dirMap[testType] || "tests/unit";
|
|
2796
|
+
}
|
|
2620
2797
|
parseGenerateTestResponse(content) {
|
|
2621
2798
|
const codeBlockMatch = content.match(/```(?:typescript|ts|javascript|js)?\s*\n([\s\S]*?)```/);
|
|
2622
2799
|
const code = codeBlockMatch ? codeBlockMatch[1].trim() : content.replace(/^(?:```[\s\S]*?\n)?/, "").replace(/\n?```$/, "").trim();
|
|
@@ -2645,10 +2822,12 @@ ISSUES: \u95EE\u9898\u5217\u8868\uFF08\u6BCF\u884C\u4E00\u4E2A\uFF0C\u683C\u5F0F
|
|
|
2645
2822
|
SUGGESTIONS: \u6539\u8FDB\u5EFA\u8BAE\u5217\u8868\uFF08\u6BCF\u884C\u4E00\u4E2A\uFF0C\u683C\u5F0F "- \u5EFA\u8BAE\u63CF\u8FF0"\uFF09`;
|
|
2646
2823
|
}
|
|
2647
2824
|
buildReviewTestUserPrompt(req) {
|
|
2825
|
+
const importPath = this.computeTestImportPath(req.testType, req.target);
|
|
2648
2826
|
let prompt = `\u8BF7\u5BA1\u67E5\u4EE5\u4E0B\u6D4B\u8BD5\u7528\u4F8B\u662F\u5426\u4E0E\u6E90\u7801\u8D34\u5207\u4E14\u51C6\u786E\u3002
|
|
2649
2827
|
|
|
2650
2828
|
\u88AB\u6D4B\u6587\u4EF6: ${req.target}
|
|
2651
2829
|
\u6D4B\u8BD5\u7C7B\u578B: ${req.testType}
|
|
2830
|
+
\u6B63\u786E\u7684 import \u8DEF\u5F84\u5E94\u4E3A: ${importPath}\uFF08\u6D4B\u8BD5\u6587\u4EF6\u4F4D\u4E8E ${this.getTestOutputDir(req.testType)}/\uFF09
|
|
2652
2831
|
|
|
2653
2832
|
--- \u6E90\u7801\u5185\u5BB9 ---
|
|
2654
2833
|
\`\`\`typescript
|
|
@@ -2816,7 +2995,7 @@ async function testAIConnection(config) {
|
|
|
2816
2995
|
}
|
|
2817
2996
|
|
|
2818
2997
|
// src/services/mock-server.ts
|
|
2819
|
-
import
|
|
2998
|
+
import fs7 from "fs";
|
|
2820
2999
|
import path7 from "path";
|
|
2821
3000
|
var serverState = {
|
|
2822
3001
|
running: false,
|
|
@@ -2829,18 +3008,18 @@ function getMockServerState() {
|
|
|
2829
3008
|
}
|
|
2830
3009
|
async function loadMockRoutes(routesDir) {
|
|
2831
3010
|
const absDir = path7.resolve(process.cwd(), routesDir);
|
|
2832
|
-
if (!
|
|
3011
|
+
if (!fs7.existsSync(absDir)) {
|
|
2833
3012
|
return [];
|
|
2834
3013
|
}
|
|
2835
3014
|
const routes = [];
|
|
2836
|
-
const entries =
|
|
3015
|
+
const entries = fs7.readdirSync(absDir, { withFileTypes: true });
|
|
2837
3016
|
for (const entry of entries) {
|
|
2838
3017
|
if (!entry.isFile()) continue;
|
|
2839
3018
|
const filePath = path7.join(absDir, entry.name);
|
|
2840
3019
|
const ext = path7.extname(entry.name);
|
|
2841
3020
|
try {
|
|
2842
3021
|
if (ext === ".json") {
|
|
2843
|
-
const content =
|
|
3022
|
+
const content = fs7.readFileSync(filePath, "utf-8");
|
|
2844
3023
|
const parsed = JSON.parse(content);
|
|
2845
3024
|
if (Array.isArray(parsed)) {
|
|
2846
3025
|
routes.push(...parsed);
|
|
@@ -3032,11 +3211,11 @@ function generateMockRouteTemplate(name) {
|
|
|
3032
3211
|
}
|
|
3033
3212
|
function initMockRoutesDir(routesDir) {
|
|
3034
3213
|
const absDir = path7.resolve(process.cwd(), routesDir);
|
|
3035
|
-
if (!
|
|
3036
|
-
|
|
3214
|
+
if (!fs7.existsSync(absDir)) {
|
|
3215
|
+
fs7.mkdirSync(absDir, { recursive: true });
|
|
3037
3216
|
}
|
|
3038
3217
|
const examplePath = path7.join(absDir, "example.json");
|
|
3039
|
-
if (!
|
|
3218
|
+
if (!fs7.existsSync(examplePath)) {
|
|
3040
3219
|
const exampleRoutes = [
|
|
3041
3220
|
{
|
|
3042
3221
|
method: "GET",
|
|
@@ -3060,24 +3239,24 @@ function initMockRoutesDir(routesDir) {
|
|
|
3060
3239
|
}
|
|
3061
3240
|
}
|
|
3062
3241
|
];
|
|
3063
|
-
|
|
3242
|
+
fs7.writeFileSync(examplePath, JSON.stringify(exampleRoutes, null, 2), "utf-8");
|
|
3064
3243
|
}
|
|
3065
3244
|
}
|
|
3066
3245
|
|
|
3067
3246
|
// src/services/visual.ts
|
|
3068
|
-
import
|
|
3247
|
+
import fs8 from "fs";
|
|
3069
3248
|
import path8 from "path";
|
|
3070
3249
|
import pixelmatch from "pixelmatch";
|
|
3071
3250
|
import { PNG } from "pngjs";
|
|
3072
3251
|
function compareImages(baselinePath, currentPath, diffOutputPath, threshold = 0.1) {
|
|
3073
|
-
if (!
|
|
3252
|
+
if (!fs8.existsSync(baselinePath)) {
|
|
3074
3253
|
throw new Error(`\u57FA\u7EBF\u56FE\u7247\u4E0D\u5B58\u5728: ${baselinePath}`);
|
|
3075
3254
|
}
|
|
3076
|
-
if (!
|
|
3255
|
+
if (!fs8.existsSync(currentPath)) {
|
|
3077
3256
|
throw new Error(`\u5F53\u524D\u56FE\u7247\u4E0D\u5B58\u5728: ${currentPath}`);
|
|
3078
3257
|
}
|
|
3079
|
-
const baseline = PNG.sync.read(
|
|
3080
|
-
const current = PNG.sync.read(
|
|
3258
|
+
const baseline = PNG.sync.read(fs8.readFileSync(baselinePath));
|
|
3259
|
+
const current = PNG.sync.read(fs8.readFileSync(currentPath));
|
|
3081
3260
|
if (baseline.width !== current.width || baseline.height !== current.height) {
|
|
3082
3261
|
return {
|
|
3083
3262
|
passed: false,
|
|
@@ -3106,10 +3285,10 @@ function compareImages(baselinePath, currentPath, diffOutputPath, threshold = 0.
|
|
|
3106
3285
|
let diffPath;
|
|
3107
3286
|
if (diffPixels > 0) {
|
|
3108
3287
|
const diffDir = path8.dirname(diffOutputPath);
|
|
3109
|
-
if (!
|
|
3110
|
-
|
|
3288
|
+
if (!fs8.existsSync(diffDir)) {
|
|
3289
|
+
fs8.mkdirSync(diffDir, { recursive: true });
|
|
3111
3290
|
}
|
|
3112
|
-
|
|
3291
|
+
fs8.writeFileSync(diffOutputPath, PNG.sync.write(diff));
|
|
3113
3292
|
diffPath = diffOutputPath;
|
|
3114
3293
|
}
|
|
3115
3294
|
return {
|
|
@@ -3123,14 +3302,14 @@ function compareImages(baselinePath, currentPath, diffOutputPath, threshold = 0.
|
|
|
3123
3302
|
};
|
|
3124
3303
|
}
|
|
3125
3304
|
function createBaseline(currentPath, baselinePath) {
|
|
3126
|
-
if (!
|
|
3305
|
+
if (!fs8.existsSync(currentPath)) {
|
|
3127
3306
|
throw new Error(`\u5F53\u524D\u622A\u56FE\u4E0D\u5B58\u5728: ${currentPath}`);
|
|
3128
3307
|
}
|
|
3129
3308
|
const baselineDir = path8.dirname(baselinePath);
|
|
3130
|
-
if (!
|
|
3131
|
-
|
|
3309
|
+
if (!fs8.existsSync(baselineDir)) {
|
|
3310
|
+
fs8.mkdirSync(baselineDir, { recursive: true });
|
|
3132
3311
|
}
|
|
3133
|
-
|
|
3312
|
+
fs8.copyFileSync(currentPath, baselinePath);
|
|
3134
3313
|
return baselinePath;
|
|
3135
3314
|
}
|
|
3136
3315
|
function updateBaseline(currentPath, baselinePath) {
|
|
@@ -3138,69 +3317,69 @@ function updateBaseline(currentPath, baselinePath) {
|
|
|
3138
3317
|
}
|
|
3139
3318
|
function updateAllBaselines(currentDir, baselineDir) {
|
|
3140
3319
|
const updated = [];
|
|
3141
|
-
if (!
|
|
3320
|
+
if (!fs8.existsSync(currentDir)) {
|
|
3142
3321
|
return updated;
|
|
3143
3322
|
}
|
|
3144
|
-
const files =
|
|
3323
|
+
const files = fs8.readdirSync(currentDir).filter((f) => f.endsWith(".png"));
|
|
3145
3324
|
for (const file of files) {
|
|
3146
3325
|
const currentPath = path8.join(currentDir, file);
|
|
3147
3326
|
const baselinePath = path8.join(baselineDir, file);
|
|
3148
3327
|
const baselineDirAbs = path8.dirname(baselinePath);
|
|
3149
|
-
if (!
|
|
3150
|
-
|
|
3328
|
+
if (!fs8.existsSync(baselineDirAbs)) {
|
|
3329
|
+
fs8.mkdirSync(baselineDirAbs, { recursive: true });
|
|
3151
3330
|
}
|
|
3152
|
-
|
|
3331
|
+
fs8.copyFileSync(currentPath, baselinePath);
|
|
3153
3332
|
updated.push(file);
|
|
3154
3333
|
}
|
|
3155
3334
|
return updated;
|
|
3156
3335
|
}
|
|
3157
3336
|
function cleanBaselines(baselineDir) {
|
|
3158
|
-
if (!
|
|
3337
|
+
if (!fs8.existsSync(baselineDir)) {
|
|
3159
3338
|
return 0;
|
|
3160
3339
|
}
|
|
3161
|
-
const files =
|
|
3340
|
+
const files = fs8.readdirSync(baselineDir).filter((f) => f.endsWith(".png"));
|
|
3162
3341
|
let count = 0;
|
|
3163
3342
|
for (const file of files) {
|
|
3164
|
-
|
|
3343
|
+
fs8.unlinkSync(path8.join(baselineDir, file));
|
|
3165
3344
|
count++;
|
|
3166
3345
|
}
|
|
3167
3346
|
return count;
|
|
3168
3347
|
}
|
|
3169
3348
|
function cleanDiffs(diffDir) {
|
|
3170
|
-
if (!
|
|
3349
|
+
if (!fs8.existsSync(diffDir)) {
|
|
3171
3350
|
return 0;
|
|
3172
3351
|
}
|
|
3173
|
-
const files =
|
|
3352
|
+
const files = fs8.readdirSync(diffDir).filter((f) => f.endsWith(".png"));
|
|
3174
3353
|
let count = 0;
|
|
3175
3354
|
for (const file of files) {
|
|
3176
|
-
|
|
3355
|
+
fs8.unlinkSync(path8.join(diffDir, file));
|
|
3177
3356
|
count++;
|
|
3178
3357
|
}
|
|
3179
3358
|
return count;
|
|
3180
3359
|
}
|
|
3181
3360
|
function listBaselines(baselineDir) {
|
|
3182
|
-
if (!
|
|
3361
|
+
if (!fs8.existsSync(baselineDir)) {
|
|
3183
3362
|
return [];
|
|
3184
3363
|
}
|
|
3185
|
-
return
|
|
3364
|
+
return fs8.readdirSync(baselineDir).filter((f) => f.endsWith(".png")).sort();
|
|
3186
3365
|
}
|
|
3187
3366
|
function listDiffs(diffDir) {
|
|
3188
|
-
if (!
|
|
3367
|
+
if (!fs8.existsSync(diffDir)) {
|
|
3189
3368
|
return [];
|
|
3190
3369
|
}
|
|
3191
|
-
return
|
|
3370
|
+
return fs8.readdirSync(diffDir).filter((f) => f.endsWith(".png")).sort();
|
|
3192
3371
|
}
|
|
3193
3372
|
function compareDirectories(baselineDir, currentDir, diffDir, threshold = 0.1) {
|
|
3194
3373
|
const results = [];
|
|
3195
|
-
if (!
|
|
3374
|
+
if (!fs8.existsSync(currentDir)) {
|
|
3196
3375
|
return results;
|
|
3197
3376
|
}
|
|
3198
|
-
const currentFiles =
|
|
3377
|
+
const currentFiles = fs8.readdirSync(currentDir).filter((f) => f.endsWith(".png"));
|
|
3199
3378
|
for (const file of currentFiles) {
|
|
3200
3379
|
const currentPath = path8.join(currentDir, file);
|
|
3201
3380
|
const baselinePath = path8.join(baselineDir, file);
|
|
3202
3381
|
const diffPath = path8.join(diffDir, file);
|
|
3203
|
-
if (!
|
|
3382
|
+
if (!fs8.existsSync(baselinePath)) {
|
|
3204
3383
|
createBaseline(currentPath, baselinePath);
|
|
3205
3384
|
results.push({
|
|
3206
3385
|
passed: true,
|
|
@@ -3232,14 +3411,14 @@ function compareDirectories(baselineDir, currentDir, diffDir, threshold = 0.1) {
|
|
|
3232
3411
|
}
|
|
3233
3412
|
|
|
3234
3413
|
// src/services/source-analyzer.ts
|
|
3235
|
-
import
|
|
3414
|
+
import fs9 from "fs";
|
|
3236
3415
|
import path9 from "path";
|
|
3237
3416
|
function analyzeFile(filePath) {
|
|
3238
3417
|
const absolutePath = path9.resolve(process.cwd(), filePath);
|
|
3239
|
-
if (!
|
|
3418
|
+
if (!fs9.existsSync(absolutePath)) {
|
|
3240
3419
|
return { filePath, exports: [], apiCalls: [] };
|
|
3241
3420
|
}
|
|
3242
|
-
const content =
|
|
3421
|
+
const content = fs9.readFileSync(absolutePath, "utf-8");
|
|
3243
3422
|
const ext = path9.extname(filePath);
|
|
3244
3423
|
const result = {
|
|
3245
3424
|
filePath,
|
|
@@ -3594,13 +3773,13 @@ function cleanTemplateUrl(url) {
|
|
|
3594
3773
|
}
|
|
3595
3774
|
function scanAPICalls(srcDir) {
|
|
3596
3775
|
const absDir = path9.resolve(process.cwd(), srcDir);
|
|
3597
|
-
if (!
|
|
3776
|
+
if (!fs9.existsSync(absDir)) {
|
|
3598
3777
|
return [];
|
|
3599
3778
|
}
|
|
3600
3779
|
const allCalls = [];
|
|
3601
3780
|
const seen = /* @__PURE__ */ new Set();
|
|
3602
3781
|
function walk(dir) {
|
|
3603
|
-
const entries =
|
|
3782
|
+
const entries = fs9.readdirSync(dir, { withFileTypes: true });
|
|
3604
3783
|
for (const entry of entries) {
|
|
3605
3784
|
if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === "dist") continue;
|
|
3606
3785
|
const fullPath = path9.join(dir, entry.name);
|
|
@@ -3608,7 +3787,7 @@ function scanAPICalls(srcDir) {
|
|
|
3608
3787
|
walk(fullPath);
|
|
3609
3788
|
} else if (entry.isFile() && /\.(ts|js|vue|mjs)$/.test(entry.name)) {
|
|
3610
3789
|
try {
|
|
3611
|
-
const content =
|
|
3790
|
+
const content = fs9.readFileSync(fullPath, "utf-8");
|
|
3612
3791
|
const calls = extractAPICalls(content, path9.relative(process.cwd(), fullPath));
|
|
3613
3792
|
for (const call of calls) {
|
|
3614
3793
|
const key = `${call.method}:${call.url}`;
|