ctrlshiftleft
Version:
AI-powered toolkit for embedding QA and security testing into development workflows
160 lines (131 loc) • 5.05 kB
text/typescript
import fs from 'fs/promises';
import path from 'path';
import { glob } from 'glob';
import { LLMService } from './llmService';
import { TestTemplate } from '../types/testTypes';
import { ensureDirectoryExists } from '../utils/fileUtils';
import { generatePlaywrightTest } from '../templates/playwrightTemplate';
import { generateSeleniumTest } from '../templates/seleniumTemplate';
interface TestGeneratorOptions {
format: string;
timeout: number;
}
interface GenerationResult {
testCount: number;
files: string[];
}
export class TestGenerator {
private options: TestGeneratorOptions;
private llmService: LLMService;
constructor(options: TestGeneratorOptions) {
this.options = {
format: options.format || 'playwright',
timeout: options.timeout || 60
};
this.llmService = new LLMService();
}
/**
* Generates end-to-end tests from source code
* @param sourcePath Path to source file or directory
* @param outputDir Output directory for generated tests
* @returns Generation result containing test count and file paths
*/
async generateTests(sourcePath: string, outputDir: string): Promise<GenerationResult> {
// Ensure output directory exists
await ensureDirectoryExists(outputDir);
// Get all source files if sourcePath is a directory
const sourceFiles = await this.getSourceFiles(sourcePath);
const result: GenerationResult = {
testCount: 0,
files: []
};
// Generate tests for each source file
for (const sourceFile of sourceFiles) {
const sourceCode = await fs.readFile(sourceFile, 'utf8');
const relativeSourcePath = path.relative(process.cwd(), sourceFile);
// Skip files that don't need tests (like test files themselves)
if (this.shouldSkipFile(sourceFile)) {
continue;
}
// Analyze source code with LLM to extract test scenarios
const testScenarios = await this.llmService.extractTestScenarios(sourceCode, relativeSourcePath);
if (testScenarios.length === 0) {
continue;
}
// Generate test file path
const testFileName = await this.getTestFileName(sourceFile, outputDir);
// Generate test template based on format
const testTemplate = this.generateTestTemplate(testScenarios, relativeSourcePath);
// Write test file
await fs.writeFile(testFileName, testTemplate.content, 'utf8');
result.testCount += testScenarios.length;
result.files.push(path.relative(process.cwd(), testFileName));
}
return result;
}
/**
* Get all source files from a path (file or directory)
*/
private async getSourceFiles(sourcePath: string): Promise<string[]> {
const stats = await fs.stat(sourcePath);
if (stats.isFile()) {
return [sourcePath];
}
if (stats.isDirectory()) {
// Find all JS/TS files but exclude test files and node_modules
return glob(`${sourcePath}/**/*.{js,jsx,ts,tsx}`, {
ignore: [
'**/node_modules/**',
'**/*.test.{js,jsx,ts,tsx}',
'**/*.spec.{js,jsx,ts,tsx}',
'**/test/**',
'**/tests/**',
'**/dist/**',
'**/build/**'
]
});
}
return [];
}
/**
* Determine if a file should be skipped for test generation
*/
private shouldSkipFile(filePath: string): boolean {
const filename = path.basename(filePath).toLowerCase();
return filename.includes('.test.') ||
filename.includes('.spec.') ||
filename.endsWith('.d.ts');
}
/**
* Generate test file name based on source file
*/
private async getTestFileName(sourceFile: string, outputDir: string): Promise<string> {
// Check if outputDir is actually a file path rather than a directory
if (path.extname(outputDir) !== '') {
// If outputDir has a file extension, use it directly as the test file
await ensureDirectoryExists(path.dirname(outputDir));
return outputDir;
}
const sourceFileName = path.basename(sourceFile);
const sourceDir = path.dirname(sourceFile);
// Keep subdirectory structure relative to source root
const relativeDir = path.relative(process.cwd(), sourceDir);
const targetDir = path.join(outputDir, relativeDir);
// Replace extension with .test.ts or .test.js
let testFileName = sourceFileName.replace(/\.(js|jsx|ts|tsx)$/, '');
testFileName = `${testFileName}.test.${this.options.format === 'playwright' ? 'ts' : 'js'}`;
// Create subdirectory if needed
await ensureDirectoryExists(targetDir);
return path.join(targetDir, testFileName);
}
/**
* Generate test template based on format
*/
private generateTestTemplate(testScenarios: any[], sourcePath: string): TestTemplate {
if (this.options.format === 'playwright') {
return generatePlaywrightTest(testScenarios, sourcePath);
} else {
return generateSeleniumTest(testScenarios, sourcePath);
}
}
}