openmatrix 0.2.19 → 0.2.20

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.
@@ -0,0 +1,17 @@
1
+ import type { TestScanResult, UncoveredSourceFile, GeneratedTestFile, GeneratedMockFile, TestGenerationResult, TestConfig } from '../types/index.js';
2
+ /**
3
+ * 生成测试文件
4
+ */
5
+ export declare function generateTestFiles(scanResult: TestScanResult, config: TestConfig, selectedSources: UncoveredSourceFile[]): TestGenerationResult;
6
+ /**
7
+ * 生成 E2E 测试文件
8
+ */
9
+ export declare function generateE2ETestFiles(scanResult: TestScanResult, config: TestConfig, uiComponents: UncoveredSourceFile[]): GeneratedTestFile[];
10
+ /**
11
+ * 写入生成的测试文件
12
+ */
13
+ export declare function writeGeneratedFiles(files: GeneratedTestFile[], mockFiles: GeneratedMockFile[], projectRoot: string): void;
14
+ /**
15
+ * 生成测试文件(简化接口)
16
+ */
17
+ export declare function generateTests(projectRoot: string, scanResult: TestScanResult, targetFiles?: string[]): TestGenerationResult;
@@ -0,0 +1,403 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.generateTestFiles = generateTestFiles;
37
+ exports.generateE2ETestFiles = generateE2ETestFiles;
38
+ exports.writeGeneratedFiles = writeGeneratedFiles;
39
+ exports.generateTests = generateTests;
40
+ // src/test/generator.ts
41
+ /**
42
+ * 测试用例生成器 - 基于上下文生成测试用例模板
43
+ *
44
+ * 功能:
45
+ * - 生成测试文件模板
46
+ * - 根据现有测试风格保持一致性
47
+ * - 支持多种测试框架
48
+ * - 生成 Mock 文件
49
+ *
50
+ * 模块依赖: test/context-analyzer → test/generator → skills/test
51
+ */
52
+ const fs = __importStar(require("fs"));
53
+ const path = __importStar(require("path"));
54
+ /**
55
+ * 生成测试文件
56
+ */
57
+ function generateTestFiles(scanResult, config, selectedSources) {
58
+ const templateConfig = {
59
+ framework: config.framework,
60
+ testStyle: scanResult.testStyle,
61
+ outputDir: config.outputDir || 'tests',
62
+ fileSuffix: scanResult.testStyle?.fileSuffix || '.test.ts'
63
+ };
64
+ const files = [];
65
+ const testCases = [];
66
+ const mockFiles = [];
67
+ for (const source of selectedSources) {
68
+ const testFile = generateSingleTestFile(source, templateConfig, scanResult.projectRoot);
69
+ files.push(testFile);
70
+ const cases = generateTestCaseDescriptions(source, config.testTypes);
71
+ testCases.push(...cases);
72
+ }
73
+ // 如果需要 Mock
74
+ if (config.includeMocks) {
75
+ const mocks = generateMockFiles(selectedSources, templateConfig, scanResult.projectRoot);
76
+ mockFiles.push(...mocks);
77
+ }
78
+ // 获取运行命令
79
+ const primaryFramework = scanResult.frameworks.find(f => f.isPrimary);
80
+ const runCommand = primaryFramework?.commands.test || 'npm test';
81
+ return {
82
+ timestamp: new Date().toISOString(),
83
+ config,
84
+ files,
85
+ testCases,
86
+ mockFiles,
87
+ statistics: {
88
+ fileCount: files.length,
89
+ testCaseCount: testCases.length,
90
+ unitTestCount: testCases.filter(c => c.type === 'unit').length,
91
+ integrationTestCount: testCases.filter(c => c.type === 'integration').length,
92
+ e2eTestCount: testCases.filter(c => c.type === 'e2e').length,
93
+ mockFileCount: mockFiles.length
94
+ },
95
+ runCommand,
96
+ notes: [
97
+ '测试文件已生成,请检查内容是否符合预期',
98
+ '运行测试前请确保安装了必要的依赖'
99
+ ]
100
+ };
101
+ }
102
+ /**
103
+ * 生成单个测试文件
104
+ */
105
+ function generateSingleTestFile(source, config, projectRoot) {
106
+ const testPath = determineTestPath(source.path, config);
107
+ const content = generateTestContent(source, config);
108
+ return {
109
+ path: testPath,
110
+ content,
111
+ type: config.testStyle?.usesTypeScript ? 'unit' : 'unit',
112
+ sourceFile: source.path,
113
+ testCaseIds: source.exports.map((exp, i) => `${source.path}-${exp}-${i}`),
114
+ overwrites: false
115
+ };
116
+ }
117
+ /**
118
+ * 确定测试文件路径
119
+ */
120
+ function determineTestPath(sourcePath, config) {
121
+ const sourceFileName = path.basename(sourcePath);
122
+ const sourceDir = path.dirname(sourcePath);
123
+ const ext = path.extname(sourceFileName);
124
+ let testFileName;
125
+ if (config.testStyle?.fileLocation === 'adjacent') {
126
+ // 同目录
127
+ testFileName = sourceFileName.replace(ext, config.fileSuffix.replace('.test', '.test').replace('.spec', '.spec'));
128
+ return path.join(sourceDir, testFileName);
129
+ }
130
+ else {
131
+ // 独立目录
132
+ testFileName = sourceFileName.replace(ext, config.fileSuffix);
133
+ return path.join(config.outputDir, sourceDir, testFileName);
134
+ }
135
+ }
136
+ /**
137
+ * 生成测试内容
138
+ */
139
+ function generateTestContent(source, config) {
140
+ const framework = config.framework;
141
+ const usesDescribeIt = config.testStyle?.namingConvention === 'describe-it';
142
+ const usesTypeScript = config.testStyle?.usesTypeScript ?? true;
143
+ // 导入语句
144
+ const importPath = source.path.replace(/\.(ts|tsx)$/, '').replace(/\.(js|jsx)$/, '');
145
+ const imports = generateImports(source.exports, importPath, usesTypeScript);
146
+ // 测试块
147
+ const testBlocks = source.exports.map(exp => generateTestBlock(exp, framework, usesDescribeIt, source.fileType));
148
+ // 组装内容
149
+ const content = `${imports}\n\n${testBlocks.join('\n\n')}\n`;
150
+ return content;
151
+ }
152
+ /**
153
+ * 生成导入语句
154
+ */
155
+ function generateImports(exports, importPath, usesTypeScript) {
156
+ if (exports.length === 0) {
157
+ return usesTypeScript
158
+ ? `import * as target from '../../${importPath}';`
159
+ : `const target = require('../../${importPath}');`;
160
+ }
161
+ return usesTypeScript
162
+ ? `import { ${exports.join(', ')} } from '../../${importPath}';`
163
+ : `const { ${exports.join(', ')} } = require('../../${importPath}');`;
164
+ }
165
+ /**
166
+ * 生成测试块
167
+ */
168
+ function generateTestBlock(exportName, framework, usesDescribeIt, fileType) {
169
+ const useDescribe = usesDescribeIt ?? true;
170
+ const describeOrTest = usesDescribeIt ? 'describe' : 'test';
171
+ if (useDescribe) {
172
+ return `describe('${getDescribeTitle(exportName, fileType)}', () => {
173
+ it('should work correctly', () => {
174
+ // Arrange
175
+ const input = {};
176
+
177
+ // Act
178
+ const result = ${exportName}(input);
179
+
180
+ // Assert
181
+ expect(result).toBeDefined();
182
+ });
183
+
184
+ it('should handle edge cases', () => {
185
+ // Arrange
186
+ const input = null;
187
+
188
+ // Act & Assert
189
+ expect(() => ${exportName}(input)).not.toThrow();
190
+ });
191
+ });`;
192
+ }
193
+ else {
194
+ return `test('${exportName}', () => {
195
+ expect(${exportName}).toBeDefined();
196
+ expect(${exportName}(null)).toBeDefined();
197
+ });`;
198
+ }
199
+ }
200
+ /**
201
+ * 获取 describe 标题
202
+ */
203
+ function getDescribeTitle(exportName, fileType) {
204
+ const typeLabels = {
205
+ component: 'Component',
206
+ service: 'Service',
207
+ util: 'Util',
208
+ module: 'Module',
209
+ class: 'Class',
210
+ function: 'Function'
211
+ };
212
+ const label = typeLabels[fileType] || 'Module';
213
+ return `${label}: ${exportName}`;
214
+ }
215
+ /**
216
+ * 生成测试用例描述
217
+ */
218
+ function generateTestCaseDescriptions(source, testTypes) {
219
+ const cases = [];
220
+ let caseId = 0;
221
+ for (const exportName of source.exports) {
222
+ for (const testType of testTypes) {
223
+ cases.push({
224
+ id: `${source.path}-${exportName}-${caseId++}`,
225
+ name: `${exportName} - ${testType} test`,
226
+ type: testType,
227
+ description: `Test ${exportName} from ${source.path}`,
228
+ filePath: source.path,
229
+ sourceFile: source.path,
230
+ target: exportName,
231
+ priority: 'P2',
232
+ steps: [
233
+ { step: 1, action: `Prepare test input for ${exportName}` },
234
+ { step: 2, action: `Call ${exportName} with prepared input` },
235
+ { step: 3, action: 'Verify result matches expected output' }
236
+ ],
237
+ expectedResults: ['Function returns expected value', 'No errors thrown']
238
+ });
239
+ }
240
+ }
241
+ return cases;
242
+ }
243
+ /**
244
+ * 生成 Mock 文件
245
+ */
246
+ function generateMockFiles(sources, config, projectRoot) {
247
+ const mocks = [];
248
+ const mockDir = path.join(config.outputDir, '__mocks__');
249
+ for (const source of sources) {
250
+ // 为有外部依赖的文件生成 Mock
251
+ if (source.fileType === 'service' || source.fileType === 'module') {
252
+ const mockPath = path.join(mockDir, path.basename(source.path).replace(/\.(ts|js)$/, '.mock.ts'));
253
+ const mockContent = generateMockContent(source, config);
254
+ mocks.push({
255
+ path: mockPath,
256
+ content: mockContent,
257
+ type: 'module',
258
+ description: `Mock for ${source.path}`
259
+ });
260
+ }
261
+ }
262
+ return mocks;
263
+ }
264
+ /**
265
+ * 生成 Mock 内容
266
+ */
267
+ function generateMockContent(source, config) {
268
+ const mockExports = source.exports.map(exp => {
269
+ return `export const ${exp} = vi.fn(() => ({
270
+ // Default mock implementation
271
+ data: null,
272
+ error: null
273
+ }));`;
274
+ });
275
+ return `// Mock for ${source.path}
276
+ import { vi } from 'vitest';
277
+
278
+ ${mockExports.join('\n\n')}
279
+
280
+ // Reset all mocks before each test
281
+ beforeEach(() => {
282
+ vi.clearAllMocks();
283
+ });
284
+ `;
285
+ }
286
+ /**
287
+ * 生成 E2E 测试文件
288
+ */
289
+ function generateE2ETestFiles(scanResult, config, uiComponents) {
290
+ const files = [];
291
+ if (!config.includeUI)
292
+ return files;
293
+ const e2eDir = 'tests/e2e';
294
+ for (const component of uiComponents) {
295
+ const testPath = path.join(e2eDir, `${component.exports[0] || 'component'}.spec.ts`);
296
+ const content = generateE2EContent(component, config.framework);
297
+ files.push({
298
+ path: testPath,
299
+ content,
300
+ type: 'e2e',
301
+ sourceFile: component.path,
302
+ testCaseIds: [`e2e-${component.path}`],
303
+ overwrites: false
304
+ });
305
+ }
306
+ return files;
307
+ }
308
+ /**
309
+ * 生成 E2E 测试内容
310
+ */
311
+ function generateE2EContent(component, framework) {
312
+ if (framework === 'playwright') {
313
+ return `// E2E test for ${component.path}
314
+ import { test, expect } from '@playwright/test';
315
+
316
+ test.describe('${component.exports[0] || 'Component'} E2E', () => {
317
+ test.beforeEach(async ({ page }) => {
318
+ await page.goto('/');
319
+ });
320
+
321
+ test('should render correctly', async ({ page }) => {
322
+ // Navigate to component page
323
+ await page.waitForSelector('[data-testid="${component.exports[0]?.toLowerCase() || 'component'}"]');
324
+
325
+ // Take screenshot
326
+ await page.screenshot({ path: 'screenshots/${component.exports[0]?.toLowerCase() || 'component'}.png' });
327
+ });
328
+
329
+ test('should handle user interaction', async ({ page }) => {
330
+ const element = await page.locator('[data-testid="${component.exports[0]?.toLowerCase() || 'component'}"]');
331
+ await element.click();
332
+
333
+ // Verify state change
334
+ await expect(element).toHaveAttribute('data-active', 'true');
335
+ });
336
+ });
337
+ `;
338
+ }
339
+ // Default Cypress style
340
+ return `// E2E test for ${component.path}
341
+ describe('${component.exports[0] || 'Component'} E2E', () => {
342
+ beforeEach(() => {
343
+ cy.visit('/');
344
+ });
345
+
346
+ it('should render correctly', () => {
347
+ cy.get('[data-testid="${component.exports[0]?.toLowerCase() || 'component'}"]')
348
+ .should('be.visible');
349
+
350
+ cy.screenshot('${component.exports[0]?.toLowerCase() || 'component'}');
351
+ });
352
+
353
+ it('should handle user interaction', () => {
354
+ cy.get('[data-testid="${component.exports[0]?.toLowerCase() || 'component'}"]')
355
+ .click()
356
+ .should('have.attr', 'data-active', 'true');
357
+ });
358
+ });
359
+ `;
360
+ }
361
+ /**
362
+ * 写入生成的测试文件
363
+ */
364
+ function writeGeneratedFiles(files, mockFiles, projectRoot) {
365
+ // 写入测试文件
366
+ for (const file of files) {
367
+ const fullPath = path.join(projectRoot, file.path);
368
+ const dir = path.dirname(fullPath);
369
+ if (!fs.existsSync(dir)) {
370
+ fs.mkdirSync(dir, { recursive: true });
371
+ }
372
+ if (file.overwrites || !fs.existsSync(fullPath)) {
373
+ fs.writeFileSync(fullPath, file.content, 'utf-8');
374
+ }
375
+ }
376
+ // 写入 Mock 文件
377
+ for (const mock of mockFiles) {
378
+ const fullPath = path.join(projectRoot, mock.path);
379
+ const dir = path.dirname(fullPath);
380
+ if (!fs.existsSync(dir)) {
381
+ fs.mkdirSync(dir, { recursive: true });
382
+ }
383
+ fs.writeFileSync(fullPath, mock.content, 'utf-8');
384
+ }
385
+ }
386
+ /**
387
+ * 生成测试文件(简化接口)
388
+ */
389
+ function generateTests(projectRoot, scanResult, targetFiles) {
390
+ // 确定要生成测试的源文件
391
+ const sources = targetFiles
392
+ ? scanResult.uncoveredSources.filter(s => targetFiles.includes(s.path))
393
+ : scanResult.uncoveredSources;
394
+ // 默认配置
395
+ const config = {
396
+ target: scanResult.target,
397
+ testTypes: ['unit'],
398
+ framework: scanResult.frameworks.find(f => f.isPrimary)?.framework || 'vitest',
399
+ includeUI: scanResult.isFrontend && scanResult.hasUIComponents,
400
+ includeMocks: true
401
+ };
402
+ return generateTestFiles(scanResult, config, sources);
403
+ }