UNPKG

ctrlshiftleft

Version:

AI-powered toolkit for embedding QA and security testing into development workflows

160 lines (131 loc) 5.05 kB
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); } } }