@oalacea/demon 1.0.0
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 +38 -0
- package/LICENSE +23 -0
- package/README.md +103 -0
- package/agents/deps-analyzer.js +366 -0
- package/agents/detector.js +570 -0
- package/agents/fix-engine.js +305 -0
- package/agents/perf-analyzer.js +294 -0
- package/agents/test-generator.js +387 -0
- package/agents/test-runner.js +318 -0
- package/bin/Dockerfile +65 -0
- package/bin/cli.js +455 -0
- package/lib/config.js +237 -0
- package/lib/docker.js +207 -0
- package/lib/reporter.js +297 -0
- package/package.json +34 -0
- package/prompts/DEPS_EFFICIENCY.md +558 -0
- package/prompts/E2E.md +491 -0
- package/prompts/EXECUTE.md +782 -0
- package/prompts/INTEGRATION_API.md +484 -0
- package/prompts/INTEGRATION_DB.md +425 -0
- package/prompts/PERF_API.md +433 -0
- package/prompts/PERF_DB.md +430 -0
- package/prompts/REMEDIATION.md +482 -0
- package/prompts/UNIT.md +260 -0
- package/scripts/dev.js +106 -0
- package/templates/README.md +22 -0
- package/templates/k6/load-test.js +54 -0
- package/templates/playwright/e2e.spec.ts +61 -0
- package/templates/vitest/api.test.ts +51 -0
- package/templates/vitest/component.test.ts +27 -0
- package/templates/vitest/hook.test.ts +36 -0
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Demon - 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
|
+
};
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Demon - Test Runner Agent
|
|
3
|
+
*
|
|
4
|
+
* Executes tests and parses results.
|
|
5
|
+
* Supports Vitest, Jest, and Playwright.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { execSync } = require('child_process');
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
|
|
12
|
+
// Configuration
|
|
13
|
+
const CONFIG = {
|
|
14
|
+
container: 'demon-tools',
|
|
15
|
+
docker: 'docker exec',
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Run tests inside the Docker container
|
|
20
|
+
*/
|
|
21
|
+
function runTests(command, options = {}) {
|
|
22
|
+
const dockerCmd = `${CONFIG.docker} ${CONFIG.container} ${command}`;
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const output = execSync(dockerCmd, {
|
|
26
|
+
encoding: 'utf-8',
|
|
27
|
+
stdio: options.silent ? 'pipe' : 'inherit',
|
|
28
|
+
timeout: options.timeout || 120000,
|
|
29
|
+
cwd: options.cwd || process.cwd(),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
success: true,
|
|
34
|
+
output,
|
|
35
|
+
exitCode: 0,
|
|
36
|
+
};
|
|
37
|
+
} catch (error) {
|
|
38
|
+
return {
|
|
39
|
+
success: false,
|
|
40
|
+
output: error.stdout || '',
|
|
41
|
+
error: error.stderr || error.message,
|
|
42
|
+
exitCode: error.status || 1,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Run unit tests
|
|
49
|
+
*/
|
|
50
|
+
function runUnitTests(testFile = null) {
|
|
51
|
+
const cmd = testFile
|
|
52
|
+
? `npm test -- ${testFile}`
|
|
53
|
+
: `npm test`;
|
|
54
|
+
|
|
55
|
+
return runTests(cmd);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Run integration tests
|
|
60
|
+
*/
|
|
61
|
+
function runIntegrationTests(testFile = null) {
|
|
62
|
+
const cmd = testFile
|
|
63
|
+
? `npm test -- ${testFile}`
|
|
64
|
+
: `npm test -- tests/integration`;
|
|
65
|
+
|
|
66
|
+
return runTests(cmd);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Run E2E tests
|
|
71
|
+
*/
|
|
72
|
+
function runE2ETests(testFile = null) {
|
|
73
|
+
const cmd = testFile
|
|
74
|
+
? `npx playwright test ${testFile}`
|
|
75
|
+
: `npx playwright test`;
|
|
76
|
+
|
|
77
|
+
return runTests(cmd);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Run performance tests
|
|
82
|
+
*/
|
|
83
|
+
function runPerformanceTests(testFile = null) {
|
|
84
|
+
const cmd = testFile
|
|
85
|
+
? `k6 run ${testFile}`
|
|
86
|
+
: `k6 run tests/performance`;
|
|
87
|
+
|
|
88
|
+
return runTests(cmd);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Parse Vitest/Jest output
|
|
93
|
+
*/
|
|
94
|
+
function parseTestOutput(output) {
|
|
95
|
+
const results = {
|
|
96
|
+
total: 0,
|
|
97
|
+
passed: 0,
|
|
98
|
+
failed: 0,
|
|
99
|
+
skipped: 0,
|
|
100
|
+
failures: [],
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// Parse Vitest output
|
|
104
|
+
const testFiles = output.match(/Test Files\s+\d+ passed/g);
|
|
105
|
+
if (testFiles) {
|
|
106
|
+
results.total += parseInt(testFiles[0].match(/\d+/)[0]);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const passed = output.match(/(\d+) passed/);
|
|
110
|
+
if (passed) {
|
|
111
|
+
results.passed += parseInt(passed[1]);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const failed = output.match(/(\d+) failed/);
|
|
115
|
+
if (failed) {
|
|
116
|
+
results.failed += parseInt(failed[1]);
|
|
117
|
+
// Parse failure details
|
|
118
|
+
const failOutput = output.match(/FAIL\s+(.+)/g);
|
|
119
|
+
if (failOutput) {
|
|
120
|
+
results.failures = failOutput.map((f) => {
|
|
121
|
+
const match = f.match(/FAIL\s+(.+?)\s+/);
|
|
122
|
+
return match ? match[1] : f;
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const skipped = output.match(/(\d+) skipped/);
|
|
128
|
+
if (skipped) {
|
|
129
|
+
results.skipped += parseInt(skipped[1]);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return results;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Parse Playwright output
|
|
137
|
+
*/
|
|
138
|
+
function parseE2EOutput(output) {
|
|
139
|
+
const results = {
|
|
140
|
+
total: 0,
|
|
141
|
+
passed: 0,
|
|
142
|
+
failed: 0,
|
|
143
|
+
skipped: 0,
|
|
144
|
+
failures: [],
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const passed = output.match(/passed\s+(\d+)/);
|
|
148
|
+
if (passed) results.passed += parseInt(passed[1]);
|
|
149
|
+
|
|
150
|
+
const failed = output.match(/failed\s+(\d+)/);
|
|
151
|
+
if (failed) {
|
|
152
|
+
results.failed += parseInt(failed[1]);
|
|
153
|
+
// Parse failure details
|
|
154
|
+
const failLines = output.match(/^.*?\[ERROR\].*$/gm);
|
|
155
|
+
if (failLines) {
|
|
156
|
+
results.failures = failLines;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const skipped = output.match(/skipped\s+(\d+)/);
|
|
161
|
+
if (skipped) results.skipped += parseInt(skipped[1]);
|
|
162
|
+
|
|
163
|
+
results.total = results.passed + results.failed + results.skipped;
|
|
164
|
+
|
|
165
|
+
return results;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Parse k6 output
|
|
170
|
+
*/
|
|
171
|
+
function parsePerfOutput(output) {
|
|
172
|
+
const results = {
|
|
173
|
+
requests: 0,
|
|
174
|
+
duration: 0,
|
|
175
|
+
avgResponseTime: 0,
|
|
176
|
+
p95: 0,
|
|
177
|
+
p99: 0,
|
|
178
|
+
rps: 0,
|
|
179
|
+
failureRate: 0,
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
// Parse k6 summary output
|
|
183
|
+
const checks = output.match(/checks.*?:\s+([\d.]+)%/);
|
|
184
|
+
if (checks) results.failureRate = 100 - parseFloat(checks[1]);
|
|
185
|
+
|
|
186
|
+
const dataReceived = output.match(/data_received\s+([\d.]+\s+\w+)/);
|
|
187
|
+
if (dataReceived) results.dataReceived = dataReceived[1];
|
|
188
|
+
|
|
189
|
+
const httpReqDuration = output.match(/http_req_duration.*?avg=\s*([\d.]+)ms.*?p\(95\)=\s*([\d.]+)ms/);
|
|
190
|
+
if (httpReqDuration) {
|
|
191
|
+
results.avgResponseTime = parseFloat(httpReqDuration[1]);
|
|
192
|
+
results.p95 = parseFloat(httpReqDuration[2]);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const httpReqs = output.match(/http_reqs\s+([\d.]+)/);
|
|
196
|
+
if (httpReqs) results.requests = parseInt(httpReqs[1]);
|
|
197
|
+
|
|
198
|
+
const iterationDuration = output.match(/iteration_duration.*?avg=\s*([\d.]+)/);
|
|
199
|
+
if (iterationDuration) results.duration = parseFloat(iterationDuration[1]);
|
|
200
|
+
|
|
201
|
+
return results;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Get test coverage
|
|
206
|
+
*/
|
|
207
|
+
function getCoverage() {
|
|
208
|
+
const result = runTests('npm test -- --coverage');
|
|
209
|
+
|
|
210
|
+
if (!result.success) {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const coverage = {
|
|
215
|
+
lines: 0,
|
|
216
|
+
functions: 0,
|
|
217
|
+
branches: 0,
|
|
218
|
+
statements: 0,
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
// Parse coverage output
|
|
222
|
+
const lines = result.output.match(/All files[^]*?(\d+)%/);
|
|
223
|
+
if (lines) coverage.lines = parseInt(lines[1]);
|
|
224
|
+
|
|
225
|
+
const functions = result.output.match(/functions[^]*?(\d+)%/);
|
|
226
|
+
if (functions) coverage.functions = parseInt(functions[1]);
|
|
227
|
+
|
|
228
|
+
const branches = result.output.match(/branches[^]*?(\d+)%/);
|
|
229
|
+
if (branches) coverage.branches = parseInt(branches[1]);
|
|
230
|
+
|
|
231
|
+
const statements = result.output.match(/statements[^]*?(\d+)%/);
|
|
232
|
+
if (statements) coverage.statements = parseInt(statements[1]);
|
|
233
|
+
|
|
234
|
+
return coverage;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Run all tests and return summary
|
|
239
|
+
*/
|
|
240
|
+
async function runAllTests() {
|
|
241
|
+
const summary = {
|
|
242
|
+
unit: null,
|
|
243
|
+
integration: null,
|
|
244
|
+
e2e: null,
|
|
245
|
+
performance: null,
|
|
246
|
+
coverage: null,
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
// Unit tests
|
|
250
|
+
const unitResult = runUnitTests();
|
|
251
|
+
summary.unit = {
|
|
252
|
+
...parseTestOutput(unitResult.output),
|
|
253
|
+
success: unitResult.success,
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
// Integration tests
|
|
257
|
+
const integrationResult = runIntegrationTests();
|
|
258
|
+
summary.integration = {
|
|
259
|
+
...parseTestOutput(integrationResult.output),
|
|
260
|
+
success: integrationResult.success,
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
// E2E tests
|
|
264
|
+
const e2eResult = runE2ETests();
|
|
265
|
+
summary.e2e = {
|
|
266
|
+
...parseE2EOutput(e2eResult.output),
|
|
267
|
+
success: e2eResult.success,
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
// Performance tests
|
|
271
|
+
const perfResult = runPerformanceTests();
|
|
272
|
+
summary.performance = {
|
|
273
|
+
...parsePerfOutput(perfResult.output),
|
|
274
|
+
success: perfResult.success,
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
// Coverage
|
|
278
|
+
summary.coverage = getCoverage();
|
|
279
|
+
|
|
280
|
+
return summary;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Run a single test file
|
|
285
|
+
*/
|
|
286
|
+
function runSingleTest(testPath) {
|
|
287
|
+
return runTests(`npm test -- ${testPath}`);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Watch tests
|
|
292
|
+
*/
|
|
293
|
+
function watchTests() {
|
|
294
|
+
return runTests('npm test -- --watch');
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Debug a failing test
|
|
299
|
+
*/
|
|
300
|
+
function debugTest(testPath) {
|
|
301
|
+
return runTests(`npm test -- ${testPath} --reporter=verbose --no-coverage`);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
module.exports = {
|
|
305
|
+
runTests,
|
|
306
|
+
runUnitTests,
|
|
307
|
+
runIntegrationTests,
|
|
308
|
+
runE2ETests,
|
|
309
|
+
runPerformanceTests,
|
|
310
|
+
parseTestOutput,
|
|
311
|
+
parseE2EOutput,
|
|
312
|
+
parsePerfOutput,
|
|
313
|
+
getCoverage,
|
|
314
|
+
runAllTests,
|
|
315
|
+
runSingleTest,
|
|
316
|
+
watchTests,
|
|
317
|
+
debugTest,
|
|
318
|
+
};
|