qat-cli 0.2.4 → 0.2.6
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 +135 -12
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
**Quick Auto Testing — 面向 Vue 项目,集成 Vitest & Playwright,AI 驱动覆盖测试全流程**
|
|
6
6
|
|
|
7
|
-
[](https://www.npmjs.com/package/qat-cli)
|
|
8
8
|
[](https://nodejs.org/)
|
|
9
9
|
[](https://opensource.org/licenses/MIT)
|
|
10
10
|
[](https://www.typescriptlang.org/)
|
package/dist/cli.js
CHANGED
|
@@ -2342,14 +2342,41 @@ async function executeInit(options) {
|
|
|
2342
2342
|
}
|
|
2343
2343
|
}
|
|
2344
2344
|
const config = buildProjectConfig(projectInfo);
|
|
2345
|
-
const fileSpinner = ora2("\u6B63\u5728\u751F\u6210\u914D\u7F6E\u6587\u4EF6...").start();
|
|
2346
2345
|
let configPath;
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2346
|
+
const existingConfigPath = path7.join(process.cwd(), "qat.config.js");
|
|
2347
|
+
const existingTsPath = path7.join(process.cwd(), "qat.config.ts");
|
|
2348
|
+
const configExists = fs7.existsSync(existingConfigPath) || fs7.existsSync(existingTsPath);
|
|
2349
|
+
if (configExists && !options.force) {
|
|
2350
|
+
const { overwrite } = await inquirer.prompt([
|
|
2351
|
+
{
|
|
2352
|
+
type: "confirm",
|
|
2353
|
+
name: "overwrite",
|
|
2354
|
+
message: "\u914D\u7F6E\u6587\u4EF6 qat.config.js \u5DF2\u5B58\u5728\uFF0C\u662F\u5426\u8986\u76D6\uFF1F",
|
|
2355
|
+
default: true
|
|
2356
|
+
}
|
|
2357
|
+
]);
|
|
2358
|
+
if (!overwrite) {
|
|
2359
|
+
console.log(chalk3.gray(" \u4FDD\u7559\u73B0\u6709\u914D\u7F6E\u6587\u4EF6\uFF0C\u7EE7\u7EED\u540E\u7EED\u6B65\u9AA4..."));
|
|
2360
|
+
configPath = existingConfigPath;
|
|
2361
|
+
} else {
|
|
2362
|
+
const fileSpinner = ora2("\u6B63\u5728\u8986\u76D6\u914D\u7F6E\u6587\u4EF6...").start();
|
|
2363
|
+
try {
|
|
2364
|
+
configPath = await writeConfigFile(process.cwd(), config, true);
|
|
2365
|
+
fileSpinner.succeed("\u914D\u7F6E\u6587\u4EF6\u5DF2\u8986\u76D6");
|
|
2366
|
+
} catch (error) {
|
|
2367
|
+
fileSpinner.fail("\u914D\u7F6E\u6587\u4EF6\u8986\u76D6\u5931\u8D25");
|
|
2368
|
+
throw error;
|
|
2369
|
+
}
|
|
2370
|
+
}
|
|
2371
|
+
} else {
|
|
2372
|
+
const fileSpinner = ora2("\u6B63\u5728\u751F\u6210\u914D\u7F6E\u6587\u4EF6...").start();
|
|
2373
|
+
try {
|
|
2374
|
+
configPath = await writeConfigFile(process.cwd(), config, options.force);
|
|
2375
|
+
fileSpinner.succeed("\u914D\u7F6E\u6587\u4EF6\u5DF2\u751F\u6210");
|
|
2376
|
+
} catch (error) {
|
|
2377
|
+
fileSpinner.fail("\u914D\u7F6E\u6587\u4EF6\u751F\u6210\u5931\u8D25");
|
|
2378
|
+
throw error;
|
|
2379
|
+
}
|
|
2353
2380
|
}
|
|
2354
2381
|
const dirSpinner = ora2("\u6B63\u5728\u521B\u5EFA\u6D4B\u8BD5\u76EE\u5F55...").start();
|
|
2355
2382
|
const createdDirs = createTestDirectories(config);
|
|
@@ -2470,11 +2497,22 @@ async function autoGenerateTests(config, projectInfo, aiConfig, useAI) {
|
|
|
2470
2497
|
for (const utilPath of utilities.slice(0, 30)) {
|
|
2471
2498
|
allTargets.push({ filePath: utilPath, testType: inferTestType(utilPath) });
|
|
2472
2499
|
}
|
|
2473
|
-
const
|
|
2500
|
+
const selectedTargets = await selectTargetFiles(allTargets);
|
|
2501
|
+
if (selectedTargets.length === 0) {
|
|
2502
|
+
console.log(chalk3.gray("\n \u672A\u9009\u62E9\u4EFB\u4F55\u6587\u4EF6\uFF0C\u8DF3\u8FC7\u6D4B\u8BD5\u7528\u4F8B\u751F\u6210\u3002"));
|
|
2503
|
+
return [];
|
|
2504
|
+
}
|
|
2505
|
+
const total = selectedTargets.length;
|
|
2506
|
+
const genSpinner = ora2(`\u6B63\u5728\u751F\u6210\u6D4B\u8BD5\u7528\u4F8B [0/${total}] ...`).start();
|
|
2474
2507
|
const generatedFiles = [];
|
|
2475
2508
|
const typeCount = {};
|
|
2476
2509
|
const reviewReport = [];
|
|
2477
|
-
|
|
2510
|
+
let current = 0;
|
|
2511
|
+
let failed = 0;
|
|
2512
|
+
for (const { filePath, testType } of selectedTargets) {
|
|
2513
|
+
current++;
|
|
2514
|
+
const fileLabel = path7.basename(filePath);
|
|
2515
|
+
genSpinner.text = `\u6B63\u5728\u751F\u6210\u6D4B\u8BD5\u7528\u4F8B [${current}/${total}] ${chalk3.cyan(fileLabel)} ...`;
|
|
2478
2516
|
try {
|
|
2479
2517
|
const result = await generateTestForTarget(
|
|
2480
2518
|
testType,
|
|
@@ -2492,20 +2530,105 @@ async function autoGenerateTests(config, projectInfo, aiConfig, useAI) {
|
|
|
2492
2530
|
}
|
|
2493
2531
|
}
|
|
2494
2532
|
} catch {
|
|
2533
|
+
failed++;
|
|
2495
2534
|
}
|
|
2496
2535
|
}
|
|
2497
2536
|
if (generatedFiles.length > 0) {
|
|
2498
2537
|
const summary = Object.entries(typeCount).map(([type, count]) => `${count} ${type}`).join(", ");
|
|
2499
2538
|
const approvedCount = reviewReport.filter((r) => r.approved).length;
|
|
2500
|
-
|
|
2539
|
+
let msg = `\u5DF2\u751F\u6210 ${generatedFiles.length}/${total} \u4E2A\u6D4B\u8BD5\u7528\u4F8B (${summary}) \u2014 AI \u5BA1\u8BA1 ${approvedCount}/${reviewReport.length} \u901A\u8FC7`;
|
|
2540
|
+
if (failed > 0) msg += chalk3.yellow(` ${failed} \u4E2A\u5931\u8D25`);
|
|
2541
|
+
genSpinner.succeed(msg);
|
|
2501
2542
|
} else {
|
|
2502
|
-
genSpinner.warn(
|
|
2543
|
+
genSpinner.warn(`\u672A\u751F\u6210\u6D4B\u8BD5\u7528\u4F8B (${failed} \u4E2A\u5931\u8D25)`);
|
|
2503
2544
|
}
|
|
2504
2545
|
if (reviewReport.length > 0) {
|
|
2505
2546
|
printReviewReport(reviewReport);
|
|
2506
2547
|
}
|
|
2507
2548
|
return generatedFiles;
|
|
2508
2549
|
}
|
|
2550
|
+
async function selectTargetFiles(allTargets) {
|
|
2551
|
+
const testTypeLabels = {
|
|
2552
|
+
unit: chalk3.blue("[unit]"),
|
|
2553
|
+
component: chalk3.magenta("[comp]"),
|
|
2554
|
+
e2e: chalk3.green("[e2e]"),
|
|
2555
|
+
api: chalk3.yellow("[api]"),
|
|
2556
|
+
visual: chalk3.cyan("[visual]"),
|
|
2557
|
+
performance: chalk3.gray("[perf]")
|
|
2558
|
+
};
|
|
2559
|
+
const choices = allTargets.map(({ filePath, testType }) => ({
|
|
2560
|
+
name: `${testTypeLabels[testType]} ${filePath}`,
|
|
2561
|
+
value: filePath,
|
|
2562
|
+
short: filePath
|
|
2563
|
+
}));
|
|
2564
|
+
choices.push({
|
|
2565
|
+
name: chalk3.gray("\u270E \u624B\u52A8\u8F93\u5165\u6587\u4EF6/\u76EE\u5F55\u8DEF\u5F84"),
|
|
2566
|
+
value: "__manual__",
|
|
2567
|
+
short: "\u624B\u52A8\u8F93\u5165"
|
|
2568
|
+
});
|
|
2569
|
+
const { selected } = await inquirer.prompt([
|
|
2570
|
+
{
|
|
2571
|
+
type: "checkbox",
|
|
2572
|
+
name: "selected",
|
|
2573
|
+
message: "\u9009\u62E9\u8981\u751F\u6210\u6D4B\u8BD5\u7528\u4F8B\u7684\u6587\u4EF6 (\u7A7A\u683C\u9009\u62E9/\u53D6\u6D88\uFF0C\u56DE\u8F66\u786E\u8BA4):",
|
|
2574
|
+
choices,
|
|
2575
|
+
pageSize: 15
|
|
2576
|
+
}
|
|
2577
|
+
]);
|
|
2578
|
+
const manualPaths = [];
|
|
2579
|
+
if (selected.includes("__manual__")) {
|
|
2580
|
+
const { manualInput } = await inquirer.prompt([
|
|
2581
|
+
{
|
|
2582
|
+
type: "input",
|
|
2583
|
+
name: "manualInput",
|
|
2584
|
+
message: "\u8F93\u5165\u6587\u4EF6\u6216\u76EE\u5F55\u8DEF\u5F84\uFF08\u591A\u4E2A\u7528\u9017\u53F7\u5206\u9694\uFF09:",
|
|
2585
|
+
default: "",
|
|
2586
|
+
filter: (input) => input.split(",").map((s) => s.trim()).filter(Boolean)
|
|
2587
|
+
}
|
|
2588
|
+
]);
|
|
2589
|
+
for (const p of manualInput) {
|
|
2590
|
+
const resolved = path7.resolve(process.cwd(), p);
|
|
2591
|
+
if (fs7.existsSync(resolved)) {
|
|
2592
|
+
const stat = fs7.statSync(resolved);
|
|
2593
|
+
if (stat.isDirectory()) {
|
|
2594
|
+
const dirFiles = walkDirForTestableFiles(resolved);
|
|
2595
|
+
manualPaths.push(...dirFiles);
|
|
2596
|
+
} else if (stat.isFile()) {
|
|
2597
|
+
manualPaths.push(p.replace(/\\/g, "/"));
|
|
2598
|
+
}
|
|
2599
|
+
} else {
|
|
2600
|
+
console.log(chalk3.yellow(` \u8DEF\u5F84\u4E0D\u5B58\u5728\uFF0C\u5DF2\u8DF3\u8FC7: ${p}`));
|
|
2601
|
+
}
|
|
2602
|
+
}
|
|
2603
|
+
}
|
|
2604
|
+
const selectedPaths = selected.filter((s) => s !== "__manual__");
|
|
2605
|
+
const selectedSet = new Set(selectedPaths);
|
|
2606
|
+
const result = allTargets.filter((t) => selectedSet.has(t.filePath));
|
|
2607
|
+
const existingPaths = new Set(result.map((t) => t.filePath));
|
|
2608
|
+
for (const mp of manualPaths) {
|
|
2609
|
+
if (!existingPaths.has(mp)) {
|
|
2610
|
+
result.push({ filePath: mp, testType: inferTestType(mp) });
|
|
2611
|
+
}
|
|
2612
|
+
}
|
|
2613
|
+
return result;
|
|
2614
|
+
}
|
|
2615
|
+
function walkDirForTestableFiles(dir) {
|
|
2616
|
+
const files = [];
|
|
2617
|
+
try {
|
|
2618
|
+
const entries = fs7.readdirSync(dir, { withFileTypes: true });
|
|
2619
|
+
for (const entry of entries) {
|
|
2620
|
+
if (entry.name === "node_modules" || entry.name === "dist" || entry.name.startsWith(".")) continue;
|
|
2621
|
+
const fullPath = path7.join(dir, entry.name);
|
|
2622
|
+
if (entry.isDirectory()) {
|
|
2623
|
+
files.push(...walkDirForTestableFiles(fullPath));
|
|
2624
|
+
} else if (entry.isFile() && /\.(vue|ts|js)$/.test(entry.name)) {
|
|
2625
|
+
files.push(path7.relative(process.cwd(), fullPath).replace(/\\/g, "/"));
|
|
2626
|
+
}
|
|
2627
|
+
}
|
|
2628
|
+
} catch {
|
|
2629
|
+
}
|
|
2630
|
+
return files;
|
|
2631
|
+
}
|
|
2509
2632
|
async function generateTestForTarget(testType, targetPath, config, projectInfo, aiConfig, useAI) {
|
|
2510
2633
|
const basename = path7.basename(targetPath, path7.extname(targetPath));
|
|
2511
2634
|
const name = basename.replace(/[^a-zA-Z0-9]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "") || `${testType}-test`;
|
|
@@ -5367,7 +5490,7 @@ async function executeChange(_options) {
|
|
|
5367
5490
|
}
|
|
5368
5491
|
|
|
5369
5492
|
// src/cli.ts
|
|
5370
|
-
var VERSION = "0.2.
|
|
5493
|
+
var VERSION = "0.2.6";
|
|
5371
5494
|
function printLogo() {
|
|
5372
5495
|
const logo = `
|
|
5373
5496
|
${chalk12.bold.cyan(" ___ _ _ _ _ _____ _ _ ")}
|