qat-cli 0.3.5 → 0.3.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 CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  **Quick Auto Testing — 面向 Vue 项目,集成 Vitest & Playwright,AI 驱动覆盖测试全流程**
6
6
 
7
- [![npm version](https://img.shields.io/badge/version-0.3.05-blue.svg)](https://www.npmjs.com/package/qat-cli)
7
+ [![npm version](https://img.shields.io/badge/version-0.3.06-blue.svg)](https://www.npmjs.com/package/qat-cli)
8
8
  [![Node.js](https://img.shields.io/badge/node-%3E%3D18.0.0-green.svg)](https://nodejs.org/)
9
9
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
10
10
  [![TypeScript](https://img.shields.io/badge/TypeScript-5.6-blue.svg)](https://www.typescriptlang.org/)
package/dist/cli.js CHANGED
@@ -1156,6 +1156,8 @@ ${this.compressSourceCode(req.context, 3e3, importPathRewrites)}
1156
1156
  if (srcDirMatch) {
1157
1157
  variations.add(`@/${srcDirMatch[1]}`);
1158
1158
  variations.add(`@/${srcDirMatch[1].replace(/\.(ts|js|tsx|jsx)$/, "")}`);
1159
+ variations.add(`#/${srcDirMatch[1]}`);
1160
+ variations.add(`#/${srcDirMatch[1].replace(/\.(ts|js|tsx|jsx)$/, "")}`);
1159
1161
  }
1160
1162
  const pathParts = targetPath.replace(/^\.\//, "").split("/");
1161
1163
  const fileName = pathParts[pathParts.length - 1];
@@ -1890,7 +1892,7 @@ function extractImports(content) {
1890
1892
  function analyzeImportSignatures(imports, baseDir) {
1891
1893
  const signatures = [];
1892
1894
  for (const imp of imports) {
1893
- if (!imp.source.startsWith(".") && !imp.source.startsWith("@/") && !imp.source.startsWith("~")) {
1895
+ if (!imp.source.startsWith(".") && !imp.source.startsWith("@/") && !imp.source.startsWith("#/") && !imp.source.startsWith("~")) {
1894
1896
  continue;
1895
1897
  }
1896
1898
  const resolvedPath = resolveImportPath(imp.source, baseDir);
@@ -1932,8 +1934,8 @@ function analyzeImportSignatures(imports, baseDir) {
1932
1934
  }
1933
1935
  function resolveImportPath(importSource, baseDir) {
1934
1936
  let resolved;
1935
- if (importSource.startsWith("@/") || importSource.startsWith("~")) {
1936
- const relativePath = importSource.replace(/^[@~]\//, "src/");
1937
+ if (importSource.startsWith("@/") || importSource.startsWith("#/") || importSource.startsWith("~")) {
1938
+ const relativePath = importSource.replace(/^[@#~]\//, "src/");
1937
1939
  resolved = path4.resolve(process.cwd(), relativePath);
1938
1940
  } else if (importSource.startsWith(".")) {
1939
1941
  resolved = path4.resolve(baseDir, importSource);
@@ -2914,59 +2916,101 @@ async function executeCreate(options) {
2914
2916
  ]);
2915
2917
  useAI = ai;
2916
2918
  }
2919
+ const tasks = [];
2920
+ for (const testType of types) {
2921
+ for (const testTarget of targets) {
2922
+ tasks.push({ testType, target: testTarget });
2923
+ }
2924
+ }
2917
2925
  const createdFiles = [];
2918
2926
  let skippedCount = 0;
2919
2927
  const reviewReportEntries = [];
2920
- for (const testType of types) {
2921
- for (const testTarget of targets) {
2922
- const spinner = ora3(`\u6B63\u5728\u751F\u6210 ${TEST_TYPE_LABELS[testType]} - ${path9.basename(testTarget)}...`).start();
2923
- try {
2924
- let content;
2925
- if (useAI && testTarget) {
2926
- const aiResult = await generateWithAI(testType, name, testTarget, config, (text) => {
2927
- spinner.text = `${text}`;
2928
- });
2929
- content = aiResult.code;
2930
- if (aiResult.reviewEntry) {
2931
- reviewReportEntries.push(aiResult.reviewEntry);
2932
- }
2928
+ if (useAI && tasks.length > 0) {
2929
+ const spinner = ora3(`\u5E76\u884C\u751F\u6210 ${tasks.length} \u4E2A\u6D4B\u8BD5\u7528\u4F8B...`).start();
2930
+ let completed = 0;
2931
+ let approvedCount = 0;
2932
+ let failedCount = 0;
2933
+ const results = await Promise.allSettled(
2934
+ tasks.map(async (task) => {
2935
+ const result = await generateWithAI(task.testType, name, task.target, config, (text) => {
2936
+ completed++;
2937
+ spinner.text = `\u5E76\u884C\u751F\u6210\u4E2D (${completed}/${tasks.length}) ${text}`;
2938
+ });
2939
+ return { ...task, ...result };
2940
+ })
2941
+ );
2942
+ spinner.stop();
2943
+ for (let i = 0; i < results.length; i++) {
2944
+ const r = results[i];
2945
+ const task = tasks[i];
2946
+ if (r.status === "rejected") {
2947
+ console.log(chalk5.red(` \u2717 ${TEST_TYPE_LABELS[task.testType]} - ${path9.basename(task.target)}: ${r.reason instanceof Error ? r.reason.message : String(r.reason)}`));
2948
+ failedCount++;
2949
+ continue;
2950
+ }
2951
+ const { code, reviewEntry } = r.value;
2952
+ const outputDir = getTestOutputDir(task.testType);
2953
+ const filePath = path9.join(outputDir, `${name}.${task.testType === "e2e" ? "spec" : "test"}.ts`);
2954
+ if (fs8.existsSync(filePath)) {
2955
+ console.log(chalk5.yellow(` \u26A0 \u6D4B\u8BD5\u6587\u4EF6\u5DF2\u5B58\u5728: ${path9.relative(process.cwd(), filePath)}`));
2956
+ skippedCount++;
2957
+ continue;
2958
+ }
2959
+ if (!fs8.existsSync(outputDir)) {
2960
+ fs8.mkdirSync(outputDir, { recursive: true });
2961
+ }
2962
+ fs8.writeFileSync(filePath, code, "utf-8");
2963
+ createdFiles.push({ type: task.testType, filePath });
2964
+ if (reviewEntry) {
2965
+ reviewReportEntries.push(reviewEntry);
2966
+ if (reviewEntry.approved) {
2967
+ approvedCount++;
2933
2968
  } else {
2934
- const analysis = testTarget ? analyzeFile(testTarget) : void 0;
2935
- content = renderTemplate(testType, {
2936
- name,
2937
- target: testTarget || `./${config.project.srcDir}`,
2938
- framework: projectInfo.framework,
2939
- vueVersion: projectInfo.vueVersion,
2940
- typescript: projectInfo.typescript,
2941
- uiLibrary: projectInfo.uiLibrary,
2942
- extraImports: projectInfo.componentTestSetup?.extraImports,
2943
- globalPlugins: projectInfo.componentTestSetup?.globalPlugins,
2944
- globalStubs: projectInfo.componentTestSetup?.globalStubs,
2945
- mountOptions: projectInfo.componentTestSetup?.mountOptions,
2946
- // 注入源码分析结果
2947
- exports: analysis?.exports.map((e) => ({
2948
- name: e.name,
2949
- kind: e.kind,
2950
- params: e.params,
2951
- isAsync: e.isAsync,
2952
- returnType: e.returnType
2953
- })),
2954
- props: analysis?.vueAnalysis?.props.map((p) => ({
2955
- name: p.name,
2956
- type: p.type,
2957
- required: p.required,
2958
- defaultValue: p.defaultValue
2959
- })),
2960
- emits: analysis?.vueAnalysis?.emits.map((e) => ({
2961
- name: e.name,
2962
- params: e.params
2963
- })),
2964
- methods: analysis?.vueAnalysis?.methods,
2965
- computed: analysis?.vueAnalysis?.computed
2966
- });
2969
+ failedCount++;
2967
2970
  }
2968
- const outputDir = getTestOutputDir(testType);
2969
- const filePath = path9.join(outputDir, `${name}.${testType === "e2e" ? "spec" : "test"}.ts`);
2971
+ }
2972
+ }
2973
+ console.log(chalk5.cyan(`
2974
+ \u5E76\u884C\u751F\u6210\u5B8C\u6210: ${createdFiles.length} \u6210\u529F, ${skippedCount} \u8DF3\u8FC7, ${failedCount} \u5BA1\u8BA1\u672A\u901A\u8FC7`));
2975
+ } else {
2976
+ for (const task of tasks) {
2977
+ const spinner = ora3(`\u6B63\u5728\u751F\u6210 ${TEST_TYPE_LABELS[task.testType]} - ${path9.basename(task.target)}...`).start();
2978
+ try {
2979
+ const analysis = task.target ? analyzeFile(task.target) : void 0;
2980
+ const content = renderTemplate(task.testType, {
2981
+ name,
2982
+ target: task.target || `./${config.project.srcDir}`,
2983
+ framework: projectInfo.framework,
2984
+ vueVersion: projectInfo.vueVersion,
2985
+ typescript: projectInfo.typescript,
2986
+ uiLibrary: projectInfo.uiLibrary,
2987
+ extraImports: projectInfo.componentTestSetup?.extraImports,
2988
+ globalPlugins: projectInfo.componentTestSetup?.globalPlugins,
2989
+ globalStubs: projectInfo.componentTestSetup?.globalStubs,
2990
+ mountOptions: projectInfo.componentTestSetup?.mountOptions,
2991
+ // 注入源码分析结果
2992
+ exports: analysis?.exports.map((e) => ({
2993
+ name: e.name,
2994
+ kind: e.kind,
2995
+ params: e.params,
2996
+ isAsync: e.isAsync,
2997
+ returnType: e.returnType
2998
+ })),
2999
+ props: analysis?.vueAnalysis?.props.map((p) => ({
3000
+ name: p.name,
3001
+ type: p.type,
3002
+ required: p.required,
3003
+ defaultValue: p.defaultValue
3004
+ })),
3005
+ emits: analysis?.vueAnalysis?.emits.map((e) => ({
3006
+ name: e.name,
3007
+ params: e.params
3008
+ })),
3009
+ methods: analysis?.vueAnalysis?.methods,
3010
+ computed: analysis?.vueAnalysis?.computed
3011
+ });
3012
+ const outputDir = getTestOutputDir(task.testType);
3013
+ const filePath = path9.join(outputDir, `${name}.${task.testType === "e2e" ? "spec" : "test"}.ts`);
2970
3014
  if (fs8.existsSync(filePath)) {
2971
3015
  spinner.warn(`\u6D4B\u8BD5\u6587\u4EF6\u5DF2\u5B58\u5728: ${path9.relative(process.cwd(), filePath)}`);
2972
3016
  skippedCount++;
@@ -2976,10 +3020,10 @@ async function executeCreate(options) {
2976
3020
  fs8.mkdirSync(outputDir, { recursive: true });
2977
3021
  }
2978
3022
  fs8.writeFileSync(filePath, content, "utf-8");
2979
- spinner.succeed(`${TEST_TYPE_LABELS[testType]} - ${path9.basename(testTarget)}`);
2980
- createdFiles.push({ type: testType, filePath });
3023
+ spinner.succeed(`${TEST_TYPE_LABELS[task.testType]} - ${path9.basename(task.target)}`);
3024
+ createdFiles.push({ type: task.testType, filePath });
2981
3025
  } catch (error) {
2982
- spinner.fail(`${TEST_TYPE_LABELS[testType]} - ${path9.basename(testTarget)} \u521B\u5EFA\u5931\u8D25`);
3026
+ spinner.fail(`${TEST_TYPE_LABELS[task.testType]} - ${path9.basename(task.target)} \u521B\u5EFA\u5931\u8D25`);
2983
3027
  console.log(chalk5.gray(` ${error instanceof Error ? error.message : String(error)}`));
2984
3028
  }
2985
3029
  }
@@ -2988,6 +3032,34 @@ async function executeCreate(options) {
2988
3032
  if (reviewReportEntries.length > 0) {
2989
3033
  printReviewReport(reviewReportEntries);
2990
3034
  }
3035
+ const failedEntries = reviewReportEntries.filter((e) => !e.approved);
3036
+ if (failedEntries.length > 0 && useAI) {
3037
+ const { retry } = await inquirer2.prompt([
3038
+ {
3039
+ type: "confirm",
3040
+ name: "retry",
3041
+ message: `${failedEntries.length} \u4E2A\u6D4B\u8BD5\u7528\u4F8B\u5BA1\u8BA1\u672A\u901A\u8FC7\uFF0C\u662F\u5426\u91CD\u65B0\u751F\u6210\uFF1F`,
3042
+ default: false
3043
+ }
3044
+ ]);
3045
+ if (retry) {
3046
+ console.log(chalk5.cyan("\n \u91CD\u65B0\u751F\u6210\u672A\u901A\u8FC7\u7684\u6D4B\u8BD5\u7528\u4F8B...\n"));
3047
+ for (const entry of failedEntries) {
3048
+ const spinner = ora3(`\u91CD\u65B0\u751F\u6210 ${path9.basename(entry.target)}...`).start();
3049
+ try {
3050
+ const aiResult = await generateWithAI(entry.testType, name, entry.target, config, (text) => {
3051
+ spinner.text = text;
3052
+ });
3053
+ const outputDir = getTestOutputDir(entry.testType);
3054
+ const filePath = path9.join(outputDir, `${name}.${entry.testType === "e2e" ? "spec" : "test"}.ts`);
3055
+ fs8.writeFileSync(filePath, aiResult.code, "utf-8");
3056
+ spinner.succeed(`${path9.basename(entry.target)} \u5DF2\u91CD\u65B0\u751F\u6210${aiResult.reviewEntry?.approved ? chalk5.green(" (\u5BA1\u8BA1\u901A\u8FC7)") : chalk5.yellow(" (\u4ECD\u9700\u5BA1\u67E5)")}`);
3057
+ } catch (error) {
3058
+ spinner.fail(`${path9.basename(entry.target)} \u91CD\u65B0\u751F\u6210\u5931\u8D25`);
3059
+ }
3060
+ }
3061
+ }
3062
+ }
2991
3063
  }
2992
3064
  async function selectTargets(types, projectInfo, srcDir) {
2993
3065
  const allTargets = [];
@@ -5922,7 +5994,7 @@ async function executeChange(_options) {
5922
5994
  }
5923
5995
 
5924
5996
  // src/cli.ts
5925
- var VERSION = "0.3.05";
5997
+ var VERSION = "0.3.06";
5926
5998
  function printLogo() {
5927
5999
  const logo = `
5928
6000
  ${chalk13.bold.cyan(" ___ _ _ _ _ _____ _ _ ")}