@oalacea/daemon 0.5.0 → 0.5.1
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/CHANGELOG.md +46 -38
- package/LICENSE +23 -23
- package/README.md +147 -141
- package/agents/deps-analyzer.js +366 -366
- package/agents/detector.js +570 -570
- package/agents/fix-engine.js +305 -305
- package/agents/lighthouse-scanner.js +405 -405
- package/agents/perf-analyzer.js +294 -294
- package/agents/perf-front-analyzer.js +229 -229
- package/agents/test-generator.js +387 -387
- package/agents/test-runner.js +318 -318
- package/bin/Dockerfile +75 -74
- package/bin/cli.js +449 -449
- package/lib/config.js +250 -250
- package/lib/docker.js +207 -207
- package/lib/reporter.js +297 -297
- package/package.json +34 -34
- package/prompts/DEPS_EFFICIENCY.md +558 -558
- package/prompts/E2E.md +491 -491
- package/prompts/EXECUTE.md +1060 -1060
- package/prompts/INTEGRATION_API.md +484 -484
- package/prompts/INTEGRATION_DB.md +425 -425
- package/prompts/PERF_API.md +433 -433
- package/prompts/PERF_DB.md +430 -430
- package/prompts/PERF_FRONT.md +357 -357
- package/prompts/REMEDIATION.md +482 -482
- package/prompts/UNIT.md +260 -260
- package/scripts/dev.js +106 -106
- package/templates/README.md +38 -38
- package/templates/k6/load-test.js +54 -54
- package/templates/playwright/e2e.spec.ts +61 -61
- package/templates/vitest/angular-component.test.ts +38 -38
- package/templates/vitest/api.test.ts +51 -51
- package/templates/vitest/component.test.ts +27 -27
- package/templates/vitest/hook.test.ts +36 -36
- package/templates/vitest/solid-component.test.ts +34 -34
- package/templates/vitest/svelte-component.test.ts +33 -33
- package/templates/vitest/vue-component.test.ts +39 -39
package/agents/test-generator.js
CHANGED
|
@@ -1,387 +1,387 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Daemon - Test Generator Agent
|
|
3
|
-
*
|
|
4
|
-
* Generates test files based on source code analysis.
|
|
5
|
-
* Supports unit, integration, and E2E test generation.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
const fs = require('fs');
|
|
9
|
-
const path = require('path');
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Generate a unit test for a component
|
|
13
|
-
*/
|
|
14
|
-
function generateComponentTest(componentPath, componentName, content) {
|
|
15
|
-
const hasProps = /interface Props|type Props|Props:/.test(content);
|
|
16
|
-
const hasOnClick = /onClick|handleClick/.test(content);
|
|
17
|
-
const hasDisabled = /disabled|isDisabled/.test(content);
|
|
18
|
-
const hasLoading = /loading|isLoading/.test(content);
|
|
19
|
-
const hasChildren = /children|{props\.children}/.test(content);
|
|
20
|
-
const hasError = /error|isError/.test(content);
|
|
21
|
-
const hasVariants = /variant=|variants/.test(content);
|
|
22
|
-
|
|
23
|
-
let imports = [
|
|
24
|
-
`import { render, screen } from '@testing-library/react';`,
|
|
25
|
-
`import { describe, it, expect, vi } from 'vitest';`,
|
|
26
|
-
`import { ${componentName} } from '@/components/${componentName}';`,
|
|
27
|
-
];
|
|
28
|
-
|
|
29
|
-
let tests = [];
|
|
30
|
-
|
|
31
|
-
// Basic render test
|
|
32
|
-
tests.push(`
|
|
33
|
-
it('should render', () => {
|
|
34
|
-
render(<${componentName} />);
|
|
35
|
-
expect(screen.getByRole('button')).toBeInTheDocument();
|
|
36
|
-
});`);
|
|
37
|
-
|
|
38
|
-
// Children test
|
|
39
|
-
if (hasChildren) {
|
|
40
|
-
tests.push(`
|
|
41
|
-
it('should render children', () => {
|
|
42
|
-
render(<${componentName}>Test content</${componentName}>);
|
|
43
|
-
expect(screen.getByText('Test content')).toBeInTheDocument();
|
|
44
|
-
});`);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Disabled test
|
|
48
|
-
if (hasDisabled) {
|
|
49
|
-
tests.push(`
|
|
50
|
-
it('should be disabled when disabled prop is true', () => {
|
|
51
|
-
render(<${componentName} disabled />);
|
|
52
|
-
expect(screen.getByRole('button')).toBeDisabled();
|
|
53
|
-
});`);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// Loading test
|
|
57
|
-
if (hasLoading) {
|
|
58
|
-
tests.push(`
|
|
59
|
-
it('should show loading state', () => {
|
|
60
|
-
render(<${componentName} loading />);
|
|
61
|
-
expect(screen.getByTestId(/spinner|loading/i)).toBeInTheDocument();
|
|
62
|
-
});`);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Error test
|
|
66
|
-
if (hasError) {
|
|
67
|
-
tests.push(`
|
|
68
|
-
it('should show error message', () => {
|
|
69
|
-
render(<${componentName} error="Something went wrong" />);
|
|
70
|
-
expect(screen.getByText('Something went wrong')).toBeInTheDocument();
|
|
71
|
-
});`);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// onClick test
|
|
75
|
-
if (hasOnClick) {
|
|
76
|
-
tests.push(`
|
|
77
|
-
it('should call onClick when clicked', async () => {
|
|
78
|
-
const handleClick = vi.fn();
|
|
79
|
-
render(<${componentName} onClick={handleClick} />);
|
|
80
|
-
await screen.getByRole('button').click();
|
|
81
|
-
expect(handleClick).toHaveBeenCalledTimes(1);
|
|
82
|
-
});`);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// Variant test
|
|
86
|
-
if (hasVariants) {
|
|
87
|
-
tests.push(`
|
|
88
|
-
it('should apply variant class', () => {
|
|
89
|
-
render(<${componentName} variant="danger" />);
|
|
90
|
-
expect(screen.getByRole('button')).toHaveClass('btn-danger');
|
|
91
|
-
});`);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
return `${imports.join('\n')}
|
|
95
|
-
|
|
96
|
-
describe('${componentName}', () => {${tests.join('\n')}
|
|
97
|
-
});
|
|
98
|
-
`;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Generate a unit test for a hook
|
|
103
|
-
*/
|
|
104
|
-
function generateHookTest(hookPath, hookName, content) {
|
|
105
|
-
const hasReturn = /return\s*{/.test(content);
|
|
106
|
-
const hasState = /useState|useReducer/.test(content);
|
|
107
|
-
const hasEffect = /useEffect/.test(content);
|
|
108
|
-
const hasCallback = /useCallback/.test(content);
|
|
109
|
-
const hasMemo = /useMemo/.test(content);
|
|
110
|
-
|
|
111
|
-
let imports = [
|
|
112
|
-
`import { renderHook, act, waitFor } from '@testing-library/react';`,
|
|
113
|
-
`import { describe, it, expect, vi } from 'vitest';`,
|
|
114
|
-
`import { ${hookName} } from '@/hooks/${hookName}';`,
|
|
115
|
-
];
|
|
116
|
-
|
|
117
|
-
let tests = [];
|
|
118
|
-
|
|
119
|
-
// Basic return test
|
|
120
|
-
tests.push(`
|
|
121
|
-
it('should return initial state', () => {
|
|
122
|
-
const { result } = renderHook(() => ${hookName}());
|
|
123
|
-
expect(result.current).toBeDefined();
|
|
124
|
-
});`);
|
|
125
|
-
|
|
126
|
-
// State update test
|
|
127
|
-
if (hasState) {
|
|
128
|
-
tests.push(`
|
|
129
|
-
it('should update state', async () => {
|
|
130
|
-
const { result } = renderHook(() => ${hookName}());
|
|
131
|
-
|
|
132
|
-
act(() => {
|
|
133
|
-
result.current.setValue('test');
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
await waitFor(() => {
|
|
137
|
-
expect(result.current.value).toBe('test');
|
|
138
|
-
});
|
|
139
|
-
});`);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// Cleanup test
|
|
143
|
-
if (hasEffect) {
|
|
144
|
-
tests.push(`
|
|
145
|
-
it('should cleanup on unmount', () => {
|
|
146
|
-
const cleanup = vi.fn();
|
|
147
|
-
const { unmount } = renderHook(() => ${hookName}({ cleanup }));
|
|
148
|
-
unmount();
|
|
149
|
-
expect(cleanup).toHaveBeenCalled();
|
|
150
|
-
});`);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
return `${imports.join('\n')}
|
|
154
|
-
|
|
155
|
-
describe('${hookName}', () => {${tests.join('\n')}
|
|
156
|
-
});
|
|
157
|
-
`;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* Generate a unit test for a utility function
|
|
162
|
-
*/
|
|
163
|
-
function generateUtilTest(utilPath, utilName, content) {
|
|
164
|
-
const hasParams = /function\s+\w+\s*\(([^)]+)\)|export\s+const\s+\w+\s*=\s*\(([^)]+)\)/.test(content);
|
|
165
|
-
const isAsync = /async\s+function|=>\s*async/.test(content);
|
|
166
|
-
|
|
167
|
-
let imports = [
|
|
168
|
-
`import { describe, it, expect } from 'vitest';`,
|
|
169
|
-
`import { ${utilName} } from '@/utils/${utilName}';`,
|
|
170
|
-
];
|
|
171
|
-
|
|
172
|
-
let tests = [];
|
|
173
|
-
|
|
174
|
-
if (isAsync) {
|
|
175
|
-
tests.push(`
|
|
176
|
-
it('should return result', async () => {
|
|
177
|
-
const result = await ${utilName}('test');
|
|
178
|
-
expect(result).toBeDefined();
|
|
179
|
-
});`);
|
|
180
|
-
} else {
|
|
181
|
-
tests.push(`
|
|
182
|
-
it('should return result', () => {
|
|
183
|
-
const result = ${utilName}('test');
|
|
184
|
-
expect(result).toBeDefined();
|
|
185
|
-
});`);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
return `${imports.join('\n')}
|
|
189
|
-
|
|
190
|
-
describe('${utilName}', () => {${tests.join('\n')}
|
|
191
|
-
});
|
|
192
|
-
`;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* Generate an integration test for an API route
|
|
197
|
-
*/
|
|
198
|
-
function generateApiTest(routePath, routeMethod, content) {
|
|
199
|
-
const hasValidation = /zod|validate|schema/.test(content);
|
|
200
|
-
const hasAuth = /auth|requireAuth|getSession/.test(content);
|
|
201
|
-
const hasDb = /prisma|db\./.test(content);
|
|
202
|
-
|
|
203
|
-
let imports = [
|
|
204
|
-
`import { describe, it, expect, beforeEach, afterEach } from 'vitest';`,
|
|
205
|
-
`import { POST, GET } from '@/app${routePath}/route';`,
|
|
206
|
-
];
|
|
207
|
-
|
|
208
|
-
if (hasDb) {
|
|
209
|
-
imports.push(`import { db } from '@test/db';`);
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
let tests = [];
|
|
213
|
-
|
|
214
|
-
// Success test
|
|
215
|
-
tests.push(`
|
|
216
|
-
it('should return 200 on success', async () => {
|
|
217
|
-
const request = new Request('http://localhost:3000${routePath}', {
|
|
218
|
-
method: '${routeMethod}',
|
|
219
|
-
body: JSON.stringify({ test: 'data' }),
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
const response = await ${routeMethod}(request);
|
|
223
|
-
expect(response.status).toBe(200);
|
|
224
|
-
});`);
|
|
225
|
-
|
|
226
|
-
// Validation test
|
|
227
|
-
if (hasValidation) {
|
|
228
|
-
tests.push(`
|
|
229
|
-
it('should return 400 for invalid data', async () => {
|
|
230
|
-
const request = new Request('http://localhost:3000${routePath}', {
|
|
231
|
-
method: '${routeMethod}',
|
|
232
|
-
body: JSON.stringify({ invalid: 'data' }),
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
const response = await ${routeMethod}(request);
|
|
236
|
-
expect(response.status).toBe(400);
|
|
237
|
-
});`);
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
// Auth test
|
|
241
|
-
if (hasAuth) {
|
|
242
|
-
tests.push(`
|
|
243
|
-
it('should return 401 without auth', async () => {
|
|
244
|
-
const request = new Request('http://localhost:3000${routePath}', {
|
|
245
|
-
method: '${routeMethod}',
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
const response = await ${routeMethod}(request);
|
|
249
|
-
expect(response.status).toBe(401);
|
|
250
|
-
});`);
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
// DB transaction test
|
|
254
|
-
if (hasDb) {
|
|
255
|
-
tests = [
|
|
256
|
-
`
|
|
257
|
-
beforeEach(async () => {
|
|
258
|
-
await db.begin();
|
|
259
|
-
});`,
|
|
260
|
-
`
|
|
261
|
-
afterEach(async () => {
|
|
262
|
-
await db.rollback();
|
|
263
|
-
});`,
|
|
264
|
-
...tests,
|
|
265
|
-
];
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
return `${imports.join('\n')}
|
|
269
|
-
|
|
270
|
-
describe('${routeMethod} ${routePath}', () => {${tests.join('\n')}
|
|
271
|
-
});
|
|
272
|
-
`;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
/**
|
|
276
|
-
* Generate an E2E test with Playwright
|
|
277
|
-
*/
|
|
278
|
-
function generateE2ETest(pageName, actions) {
|
|
279
|
-
let imports = [
|
|
280
|
-
`import { test, expect } from '@playwright/test';`,
|
|
281
|
-
];
|
|
282
|
-
|
|
283
|
-
let testContent = '';
|
|
284
|
-
|
|
285
|
-
if (actions.includes('login')) {
|
|
286
|
-
testContent += `
|
|
287
|
-
test.beforeEach(async ({ page }) => {
|
|
288
|
-
await page.goto('/login');
|
|
289
|
-
});`;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
testContent += `
|
|
293
|
-
|
|
294
|
-
test('should complete flow', async ({ page }) => {
|
|
295
|
-
await page.goto('/${pageName}');`;
|
|
296
|
-
|
|
297
|
-
if (actions.includes('fill')) {
|
|
298
|
-
testContent += `
|
|
299
|
-
await page.fill('input[name="email"]', 'test@example.com');`;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
if (actions.includes('click')) {
|
|
303
|
-
testContent += `
|
|
304
|
-
await page.click('button[type="submit"]');`;
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
if (actions.includes('navigate')) {
|
|
308
|
-
testContent += `
|
|
309
|
-
await expect(page).toHaveURL('/dashboard');`;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
testContent += `
|
|
313
|
-
});`;
|
|
314
|
-
|
|
315
|
-
return `${imports.join('\n')}
|
|
316
|
-
|
|
317
|
-
test.describe('${pageName}', () => {${testContent}
|
|
318
|
-
});
|
|
319
|
-
`;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
/**
|
|
323
|
-
* Main generator function
|
|
324
|
-
*/
|
|
325
|
-
function generateTest(type, filePath, options = {}) {
|
|
326
|
-
if (!fs.existsSync(filePath)) {
|
|
327
|
-
throw new Error(`File not found: ${filePath}`);
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
const content = fs.readFileSync(filePath, 'utf-8');
|
|
331
|
-
const filename = path.basename(filePath, path.extname(filePath));
|
|
332
|
-
|
|
333
|
-
switch (type) {
|
|
334
|
-
case 'component':
|
|
335
|
-
return generateComponentTest(filePath, filename, content);
|
|
336
|
-
case 'hook':
|
|
337
|
-
return generateHookTest(filePath, filename, content);
|
|
338
|
-
case 'util':
|
|
339
|
-
return generateUtilTest(filePath, filename, content);
|
|
340
|
-
case 'api':
|
|
341
|
-
return generateApiTest(options.route, options.method, content);
|
|
342
|
-
case 'e2e':
|
|
343
|
-
return generateE2ETest(filename, options.actions || []);
|
|
344
|
-
default:
|
|
345
|
-
throw new Error(`Unknown test type: ${type}`);
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
/**
|
|
350
|
-
* Find files without tests
|
|
351
|
-
*/
|
|
352
|
-
function findFilesWithoutTests(projectDir, pattern) {
|
|
353
|
-
const files = [];
|
|
354
|
-
|
|
355
|
-
function findInDir(dir) {
|
|
356
|
-
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
357
|
-
|
|
358
|
-
for (const entry of entries) {
|
|
359
|
-
const fullPath = path.join(dir, entry.name);
|
|
360
|
-
|
|
361
|
-
if (entry.isDirectory()) {
|
|
362
|
-
if (['node_modules', '.next', 'dist', 'build', '.git', 'coverage'].includes(entry.name)) {
|
|
363
|
-
continue;
|
|
364
|
-
}
|
|
365
|
-
findInDir(fullPath);
|
|
366
|
-
} else if (entry.isFile() && fullPath.match(pattern)) {
|
|
367
|
-
const testPath = fullPath.replace(/\.(tsx?)$/, '.test$1');
|
|
368
|
-
if (!fs.existsSync(testPath)) {
|
|
369
|
-
files.push(fullPath);
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
findInDir(projectDir);
|
|
376
|
-
return files;
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
module.exports = {
|
|
380
|
-
generateTest,
|
|
381
|
-
generateComponentTest,
|
|
382
|
-
generateHookTest,
|
|
383
|
-
generateUtilTest,
|
|
384
|
-
generateApiTest,
|
|
385
|
-
generateE2ETest,
|
|
386
|
-
findFilesWithoutTests,
|
|
387
|
-
};
|
|
1
|
+
/**
|
|
2
|
+
* Daemon - Test Generator Agent
|
|
3
|
+
*
|
|
4
|
+
* Generates test files based on source code analysis.
|
|
5
|
+
* Supports unit, integration, and E2E test generation.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Generate a unit test for a component
|
|
13
|
+
*/
|
|
14
|
+
function generateComponentTest(componentPath, componentName, content) {
|
|
15
|
+
const hasProps = /interface Props|type Props|Props:/.test(content);
|
|
16
|
+
const hasOnClick = /onClick|handleClick/.test(content);
|
|
17
|
+
const hasDisabled = /disabled|isDisabled/.test(content);
|
|
18
|
+
const hasLoading = /loading|isLoading/.test(content);
|
|
19
|
+
const hasChildren = /children|{props\.children}/.test(content);
|
|
20
|
+
const hasError = /error|isError/.test(content);
|
|
21
|
+
const hasVariants = /variant=|variants/.test(content);
|
|
22
|
+
|
|
23
|
+
let imports = [
|
|
24
|
+
`import { render, screen } from '@testing-library/react';`,
|
|
25
|
+
`import { describe, it, expect, vi } from 'vitest';`,
|
|
26
|
+
`import { ${componentName} } from '@/components/${componentName}';`,
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
let tests = [];
|
|
30
|
+
|
|
31
|
+
// Basic render test
|
|
32
|
+
tests.push(`
|
|
33
|
+
it('should render', () => {
|
|
34
|
+
render(<${componentName} />);
|
|
35
|
+
expect(screen.getByRole('button')).toBeInTheDocument();
|
|
36
|
+
});`);
|
|
37
|
+
|
|
38
|
+
// Children test
|
|
39
|
+
if (hasChildren) {
|
|
40
|
+
tests.push(`
|
|
41
|
+
it('should render children', () => {
|
|
42
|
+
render(<${componentName}>Test content</${componentName}>);
|
|
43
|
+
expect(screen.getByText('Test content')).toBeInTheDocument();
|
|
44
|
+
});`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Disabled test
|
|
48
|
+
if (hasDisabled) {
|
|
49
|
+
tests.push(`
|
|
50
|
+
it('should be disabled when disabled prop is true', () => {
|
|
51
|
+
render(<${componentName} disabled />);
|
|
52
|
+
expect(screen.getByRole('button')).toBeDisabled();
|
|
53
|
+
});`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Loading test
|
|
57
|
+
if (hasLoading) {
|
|
58
|
+
tests.push(`
|
|
59
|
+
it('should show loading state', () => {
|
|
60
|
+
render(<${componentName} loading />);
|
|
61
|
+
expect(screen.getByTestId(/spinner|loading/i)).toBeInTheDocument();
|
|
62
|
+
});`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Error test
|
|
66
|
+
if (hasError) {
|
|
67
|
+
tests.push(`
|
|
68
|
+
it('should show error message', () => {
|
|
69
|
+
render(<${componentName} error="Something went wrong" />);
|
|
70
|
+
expect(screen.getByText('Something went wrong')).toBeInTheDocument();
|
|
71
|
+
});`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// onClick test
|
|
75
|
+
if (hasOnClick) {
|
|
76
|
+
tests.push(`
|
|
77
|
+
it('should call onClick when clicked', async () => {
|
|
78
|
+
const handleClick = vi.fn();
|
|
79
|
+
render(<${componentName} onClick={handleClick} />);
|
|
80
|
+
await screen.getByRole('button').click();
|
|
81
|
+
expect(handleClick).toHaveBeenCalledTimes(1);
|
|
82
|
+
});`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Variant test
|
|
86
|
+
if (hasVariants) {
|
|
87
|
+
tests.push(`
|
|
88
|
+
it('should apply variant class', () => {
|
|
89
|
+
render(<${componentName} variant="danger" />);
|
|
90
|
+
expect(screen.getByRole('button')).toHaveClass('btn-danger');
|
|
91
|
+
});`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return `${imports.join('\n')}
|
|
95
|
+
|
|
96
|
+
describe('${componentName}', () => {${tests.join('\n')}
|
|
97
|
+
});
|
|
98
|
+
`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Generate a unit test for a hook
|
|
103
|
+
*/
|
|
104
|
+
function generateHookTest(hookPath, hookName, content) {
|
|
105
|
+
const hasReturn = /return\s*{/.test(content);
|
|
106
|
+
const hasState = /useState|useReducer/.test(content);
|
|
107
|
+
const hasEffect = /useEffect/.test(content);
|
|
108
|
+
const hasCallback = /useCallback/.test(content);
|
|
109
|
+
const hasMemo = /useMemo/.test(content);
|
|
110
|
+
|
|
111
|
+
let imports = [
|
|
112
|
+
`import { renderHook, act, waitFor } from '@testing-library/react';`,
|
|
113
|
+
`import { describe, it, expect, vi } from 'vitest';`,
|
|
114
|
+
`import { ${hookName} } from '@/hooks/${hookName}';`,
|
|
115
|
+
];
|
|
116
|
+
|
|
117
|
+
let tests = [];
|
|
118
|
+
|
|
119
|
+
// Basic return test
|
|
120
|
+
tests.push(`
|
|
121
|
+
it('should return initial state', () => {
|
|
122
|
+
const { result } = renderHook(() => ${hookName}());
|
|
123
|
+
expect(result.current).toBeDefined();
|
|
124
|
+
});`);
|
|
125
|
+
|
|
126
|
+
// State update test
|
|
127
|
+
if (hasState) {
|
|
128
|
+
tests.push(`
|
|
129
|
+
it('should update state', async () => {
|
|
130
|
+
const { result } = renderHook(() => ${hookName}());
|
|
131
|
+
|
|
132
|
+
act(() => {
|
|
133
|
+
result.current.setValue('test');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
await waitFor(() => {
|
|
137
|
+
expect(result.current.value).toBe('test');
|
|
138
|
+
});
|
|
139
|
+
});`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Cleanup test
|
|
143
|
+
if (hasEffect) {
|
|
144
|
+
tests.push(`
|
|
145
|
+
it('should cleanup on unmount', () => {
|
|
146
|
+
const cleanup = vi.fn();
|
|
147
|
+
const { unmount } = renderHook(() => ${hookName}({ cleanup }));
|
|
148
|
+
unmount();
|
|
149
|
+
expect(cleanup).toHaveBeenCalled();
|
|
150
|
+
});`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return `${imports.join('\n')}
|
|
154
|
+
|
|
155
|
+
describe('${hookName}', () => {${tests.join('\n')}
|
|
156
|
+
});
|
|
157
|
+
`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Generate a unit test for a utility function
|
|
162
|
+
*/
|
|
163
|
+
function generateUtilTest(utilPath, utilName, content) {
|
|
164
|
+
const hasParams = /function\s+\w+\s*\(([^)]+)\)|export\s+const\s+\w+\s*=\s*\(([^)]+)\)/.test(content);
|
|
165
|
+
const isAsync = /async\s+function|=>\s*async/.test(content);
|
|
166
|
+
|
|
167
|
+
let imports = [
|
|
168
|
+
`import { describe, it, expect } from 'vitest';`,
|
|
169
|
+
`import { ${utilName} } from '@/utils/${utilName}';`,
|
|
170
|
+
];
|
|
171
|
+
|
|
172
|
+
let tests = [];
|
|
173
|
+
|
|
174
|
+
if (isAsync) {
|
|
175
|
+
tests.push(`
|
|
176
|
+
it('should return result', async () => {
|
|
177
|
+
const result = await ${utilName}('test');
|
|
178
|
+
expect(result).toBeDefined();
|
|
179
|
+
});`);
|
|
180
|
+
} else {
|
|
181
|
+
tests.push(`
|
|
182
|
+
it('should return result', () => {
|
|
183
|
+
const result = ${utilName}('test');
|
|
184
|
+
expect(result).toBeDefined();
|
|
185
|
+
});`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return `${imports.join('\n')}
|
|
189
|
+
|
|
190
|
+
describe('${utilName}', () => {${tests.join('\n')}
|
|
191
|
+
});
|
|
192
|
+
`;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Generate an integration test for an API route
|
|
197
|
+
*/
|
|
198
|
+
function generateApiTest(routePath, routeMethod, content) {
|
|
199
|
+
const hasValidation = /zod|validate|schema/.test(content);
|
|
200
|
+
const hasAuth = /auth|requireAuth|getSession/.test(content);
|
|
201
|
+
const hasDb = /prisma|db\./.test(content);
|
|
202
|
+
|
|
203
|
+
let imports = [
|
|
204
|
+
`import { describe, it, expect, beforeEach, afterEach } from 'vitest';`,
|
|
205
|
+
`import { POST, GET } from '@/app${routePath}/route';`,
|
|
206
|
+
];
|
|
207
|
+
|
|
208
|
+
if (hasDb) {
|
|
209
|
+
imports.push(`import { db } from '@test/db';`);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
let tests = [];
|
|
213
|
+
|
|
214
|
+
// Success test
|
|
215
|
+
tests.push(`
|
|
216
|
+
it('should return 200 on success', async () => {
|
|
217
|
+
const request = new Request('http://localhost:3000${routePath}', {
|
|
218
|
+
method: '${routeMethod}',
|
|
219
|
+
body: JSON.stringify({ test: 'data' }),
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
const response = await ${routeMethod}(request);
|
|
223
|
+
expect(response.status).toBe(200);
|
|
224
|
+
});`);
|
|
225
|
+
|
|
226
|
+
// Validation test
|
|
227
|
+
if (hasValidation) {
|
|
228
|
+
tests.push(`
|
|
229
|
+
it('should return 400 for invalid data', async () => {
|
|
230
|
+
const request = new Request('http://localhost:3000${routePath}', {
|
|
231
|
+
method: '${routeMethod}',
|
|
232
|
+
body: JSON.stringify({ invalid: 'data' }),
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
const response = await ${routeMethod}(request);
|
|
236
|
+
expect(response.status).toBe(400);
|
|
237
|
+
});`);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Auth test
|
|
241
|
+
if (hasAuth) {
|
|
242
|
+
tests.push(`
|
|
243
|
+
it('should return 401 without auth', async () => {
|
|
244
|
+
const request = new Request('http://localhost:3000${routePath}', {
|
|
245
|
+
method: '${routeMethod}',
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
const response = await ${routeMethod}(request);
|
|
249
|
+
expect(response.status).toBe(401);
|
|
250
|
+
});`);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// DB transaction test
|
|
254
|
+
if (hasDb) {
|
|
255
|
+
tests = [
|
|
256
|
+
`
|
|
257
|
+
beforeEach(async () => {
|
|
258
|
+
await db.begin();
|
|
259
|
+
});`,
|
|
260
|
+
`
|
|
261
|
+
afterEach(async () => {
|
|
262
|
+
await db.rollback();
|
|
263
|
+
});`,
|
|
264
|
+
...tests,
|
|
265
|
+
];
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return `${imports.join('\n')}
|
|
269
|
+
|
|
270
|
+
describe('${routeMethod} ${routePath}', () => {${tests.join('\n')}
|
|
271
|
+
});
|
|
272
|
+
`;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Generate an E2E test with Playwright
|
|
277
|
+
*/
|
|
278
|
+
function generateE2ETest(pageName, actions) {
|
|
279
|
+
let imports = [
|
|
280
|
+
`import { test, expect } from '@playwright/test';`,
|
|
281
|
+
];
|
|
282
|
+
|
|
283
|
+
let testContent = '';
|
|
284
|
+
|
|
285
|
+
if (actions.includes('login')) {
|
|
286
|
+
testContent += `
|
|
287
|
+
test.beforeEach(async ({ page }) => {
|
|
288
|
+
await page.goto('/login');
|
|
289
|
+
});`;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
testContent += `
|
|
293
|
+
|
|
294
|
+
test('should complete flow', async ({ page }) => {
|
|
295
|
+
await page.goto('/${pageName}');`;
|
|
296
|
+
|
|
297
|
+
if (actions.includes('fill')) {
|
|
298
|
+
testContent += `
|
|
299
|
+
await page.fill('input[name="email"]', 'test@example.com');`;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (actions.includes('click')) {
|
|
303
|
+
testContent += `
|
|
304
|
+
await page.click('button[type="submit"]');`;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (actions.includes('navigate')) {
|
|
308
|
+
testContent += `
|
|
309
|
+
await expect(page).toHaveURL('/dashboard');`;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
testContent += `
|
|
313
|
+
});`;
|
|
314
|
+
|
|
315
|
+
return `${imports.join('\n')}
|
|
316
|
+
|
|
317
|
+
test.describe('${pageName}', () => {${testContent}
|
|
318
|
+
});
|
|
319
|
+
`;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Main generator function
|
|
324
|
+
*/
|
|
325
|
+
function generateTest(type, filePath, options = {}) {
|
|
326
|
+
if (!fs.existsSync(filePath)) {
|
|
327
|
+
throw new Error(`File not found: ${filePath}`);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
331
|
+
const filename = path.basename(filePath, path.extname(filePath));
|
|
332
|
+
|
|
333
|
+
switch (type) {
|
|
334
|
+
case 'component':
|
|
335
|
+
return generateComponentTest(filePath, filename, content);
|
|
336
|
+
case 'hook':
|
|
337
|
+
return generateHookTest(filePath, filename, content);
|
|
338
|
+
case 'util':
|
|
339
|
+
return generateUtilTest(filePath, filename, content);
|
|
340
|
+
case 'api':
|
|
341
|
+
return generateApiTest(options.route, options.method, content);
|
|
342
|
+
case 'e2e':
|
|
343
|
+
return generateE2ETest(filename, options.actions || []);
|
|
344
|
+
default:
|
|
345
|
+
throw new Error(`Unknown test type: ${type}`);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Find files without tests
|
|
351
|
+
*/
|
|
352
|
+
function findFilesWithoutTests(projectDir, pattern) {
|
|
353
|
+
const files = [];
|
|
354
|
+
|
|
355
|
+
function findInDir(dir) {
|
|
356
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
357
|
+
|
|
358
|
+
for (const entry of entries) {
|
|
359
|
+
const fullPath = path.join(dir, entry.name);
|
|
360
|
+
|
|
361
|
+
if (entry.isDirectory()) {
|
|
362
|
+
if (['node_modules', '.next', 'dist', 'build', '.git', 'coverage'].includes(entry.name)) {
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
findInDir(fullPath);
|
|
366
|
+
} else if (entry.isFile() && fullPath.match(pattern)) {
|
|
367
|
+
const testPath = fullPath.replace(/\.(tsx?)$/, '.test$1');
|
|
368
|
+
if (!fs.existsSync(testPath)) {
|
|
369
|
+
files.push(fullPath);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
findInDir(projectDir);
|
|
376
|
+
return files;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
module.exports = {
|
|
380
|
+
generateTest,
|
|
381
|
+
generateComponentTest,
|
|
382
|
+
generateHookTest,
|
|
383
|
+
generateUtilTest,
|
|
384
|
+
generateApiTest,
|
|
385
|
+
generateE2ETest,
|
|
386
|
+
findFilesWithoutTests,
|
|
387
|
+
};
|