UNPKG

testgenius-ai

Version:

šŸš€ TestGenius AI - The Ultimate E2E Testing Framework for Everyone | No Coding Required • AI-Powered Automation • Beautiful Reports • Zero Complexity

658 lines (653 loc) • 28.9 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.TestRunner = void 0; const webdriverio_1 = require("webdriverio"); const fs_extra_1 = __importDefault(require("fs-extra")); const path_1 = __importDefault(require("path")); const chalk_1 = __importDefault(require("chalk")); const AITestExecutor_1 = require("./AITestExecutor"); const DynamicAITestExecutor_1 = require("./DynamicAITestExecutor"); const TestSessionManager_1 = require("./TestSessionManager"); const ConfigManager_1 = require("./ConfigManager"); const Logger_1 = require("./Logger"); const AllureReporter_1 = require("./AllureReporter"); const ReportGenerator_1 = require("./ReportGenerator"); const CleanupManager_1 = require("./CleanupManager"); class TestRunner { constructor(loggingOptions) { this.allureReporter = null; this.browser = null; this.isInitialized = false; this.config = null; this.configManager = new ConfigManager_1.ConfigManager(); this.sessionManager = new TestSessionManager_1.TestSessionManager(); this.reportGenerator = new ReportGenerator_1.ReportGenerator(); this.cleanupManager = new CleanupManager_1.CleanupManager(); // Initialize logger with provided options or defaults this.logger = new Logger_1.Logger(loggingOptions); } // Auto-setup method for QA-friendly initialization async autoSetup() { if (this.isInitialized) return; this.logger.info("šŸ”§ Auto-setting up TestGenius..."); try { // 1. Auto-create directories await this.createDirectories(); // 2. Auto-create minimal config if not exists await this.createMinimalConfig(); // 3. Auto-detect and validate dependencies await this.validateDependencies(); this.isInitialized = true; this.logger.success("āœ… TestGenius setup complete!"); this.logger.info("šŸ“ Created: tests/, reports/, screenshots/"); this.logger.info("āš™ļø Created: testgenius.config.js (if needed)"); } catch (error) { this.logger.error("āŒ Auto-setup failed:", error.message); throw error; } } async createDirectories() { const directories = ["tests", "reports", "screenshots"]; for (const dir of directories) { try { await fs_extra_1.default.ensureDir(dir); this.logger.debug(`āœ… Created directory: ${dir}/`); } catch (error) { this.logger.warn(`āš ļø Could not create ${dir}/ directory:`, error.message); } } } async createMinimalConfig() { const configPath = path_1.default.join(process.cwd(), "testgenius.config.js"); if (await fs_extra_1.default.pathExists(configPath)) { this.logger.debug("āœ… Config file already exists"); return; } const minimalConfig = `module.exports = { browser: 'chrome', headless: false, timeout: 10000, baseUrl: '${process.env.STAGING_BASE_URL || "https://the-internet.herokuapp.com"}', screenshotOnFailure: true, screenshotOnSuccess: false };`; try { await fs_extra_1.default.writeFile(configPath, minimalConfig); this.logger.info("āš™ļø Created minimal testgenius.config.js"); } catch (error) { this.logger.warn("āš ļø Could not create config file:", error.message); } } async validateDependencies() { try { // Check if WebDriverIO is available require("webdriverio"); this.logger.debug("āœ… WebDriverIO dependency found"); } catch (error) { this.logger.warn("āš ļø WebDriverIO not found. Please run: npm install webdriverio"); } // Check if browser drivers are available try { require("chromedriver"); this.logger.debug("āœ… ChromeDriver found"); } catch (error) { this.logger.info("šŸ’” ChromeDriver not found. Will use system browser."); } } async run(testId, options = {}) { // Auto-setup if not initialized if (!this.isInitialized) { await this.autoSetup(); } this.logger.info("šŸš€ Starting TestGenius Test Runner..."); try { const config = await this.configManager.loadConfig(); // Create a mutable copy of config for this run const runConfig = JSON.parse(JSON.stringify(config)); // Initialize Allure reporter if enabled if (options.allure || runConfig.reporting?.allure?.enabled) { // If CLI flag is used, ensure Allure is enabled in config for this run if (options.allure) { if (!runConfig.reporting) runConfig.reporting = {}; if (!runConfig.reporting.allure) runConfig.reporting.allure = {}; runConfig.reporting.allure.enabled = true; } this.allureReporter = new AllureReporter_1.AllureReporter(runConfig); this.logger.info("šŸ“Š Allure reporting enabled"); } const tests = await this.findTests(testId, options, runConfig); if (tests.length === 0) { this.logger.warn("āš ļø No tests found matching the criteria."); return { totalTests: 0, passed: 0, failed: 0, totalDuration: 0, successRate: 0, results: [], timestamp: new Date().toISOString(), }; } this.logger.info(`šŸ“‹ Found ${tests.length} test(s) to run`); // Run tests const results = []; let totalDuration = 0; let passedCount = 0; let failedCount = 0; // Initialize browser await this.initializeBrowser(options, runConfig); // Initialize AI executor with config and set browser // Use Dynamic AI Test Executor for 100% AI-driven execution this.aiExecutor = new DynamicAITestExecutor_1.DynamicAITestExecutor(runConfig); if (this.browser) { this.aiExecutor.setBrowser(this.browser); console.log(chalk_1.default.green("āœ… Browser set on Dynamic AI executor before running tests.")); } else { throw new Error("Browser not initialized"); } // Run each test for (const test of tests) { this.logger.testStart(test.id, test.name); this.logger.info(`šŸ“ Description: ${test.description}`); this.logger.info(`🌐 Site: ${test.site}`); this.logger.info(`šŸ·ļø Tags: ${test.tags?.join(", ") || "none"}`); this.logger.info(`⚔ Priority: ${test.priority}`); const startTime = Date.now(); try { // Execute test const result = await this.runSingleTest(test, options, runConfig); const duration = Date.now() - startTime; totalDuration += duration; if (result.success) { passedCount++; } else { failedCount++; } results.push(result); } catch (error) { this.logger.error("āŒ Test execution failed:", error.message); failedCount++; } } // Generate summary const summary = { totalTests: tests.length, passed: passedCount, failed: failedCount, totalDuration, successRate: (passedCount / tests.length) * 100, results, timestamp: new Date().toISOString(), }; // Print summary this.printSummary(summary); // Generate simple HTML report await this.generateSimpleReport(summary); // Generate Allure report if enabled if (this.allureReporter) { await this.allureReporter.generateAllureReport(results); // Serve Allure report live using npx allure serve await this.allureReporter.serveAllureLive(); } return summary; } catch (error) { this.logger.error("āŒ Test execution failed:", error.message); throw error; } finally { // Clean up browser if (this.browser) { await this.browser.deleteSession(); this.browser = null; } } } async findTests(testId, options, config) { // Use config.testsDir if set, otherwise default to ['tests', 'src/tests'] const testDirs = config.testsDir ? [config.testsDir] : ["tests", "src/tests"]; let tests = []; // Handle file-based execution if (options.file) { // Single file execution const filePath = path_1.default.resolve(options.file); tests = await this.loadTestsFromFile(filePath); this.logger.info(`šŸ“ Loading tests from single file: ${options.file}`); } else if (options.files && options.files.length > 0) { // Multiple files execution for (const file of options.files) { const filePath = path_1.default.resolve(file); const fileTests = await this.loadTestsFromFile(filePath); tests = tests.concat(fileTests); } this.logger.info(`šŸ“ Loading tests from ${options.files.length} file(s): ${options.files.join(", ")}`); } else { // Directory-based execution (existing logic) for (const testDir of testDirs) { const fullPath = path_1.default.join(process.cwd(), testDir); if (await fs_extra_1.default.pathExists(fullPath)) { try { const testFiles = await fs_extra_1.default.readdir(fullPath); for (const file of testFiles) { // Skip excluded files if (options.excludeFiles && options.excludeFiles.some((excludeFile) => file.includes(excludeFile) || file === excludeFile)) { this.logger.debug(`ā­ļø Skipping excluded file: ${file}`); continue; } if (file.endsWith(".js") || file.endsWith(".ts")) { const filePath = path_1.default.join(fullPath, file); const fileTests = await this.loadTestsFromFile(filePath); tests = tests.concat(fileTests); } } if (tests.length > 0) { this.logger.info(`šŸ“ Found tests in ${testDir}/ directory`); break; } } catch (error) { this.logger.debug(`āš ļø Could not read ${testDir}/ directory:`, error.message); } } } } // Filter by testId if provided (single test execution) if (testId && testId !== "*" && testId !== "all") { tests = tests.filter((test) => test.id === testId || test.name === testId); } // Filter by specific test IDs if provided if (options.testIds && options.testIds.length > 0) { tests = tests.filter((test) => options.testIds.includes(test.id)); this.logger.info(`šŸŽÆ Filtering to specific test IDs: ${options.testIds.join(", ")}`); } // Filter by tag if provided if (options.tag) { tests = tests.filter((test) => test.tags && test.tags.includes(options.tag)); this.logger.info(`šŸ·ļø Filtering by tag: ${options.tag}`); } // Filter by priority if provided if (options.priority) { tests = tests.filter((test) => test.priority === options.priority); this.logger.info(`⚔ Filtering by priority: ${options.priority}`); } return tests; } async loadTestsFromFile(filePath) { try { if (!(await fs_extra_1.default.pathExists(filePath))) { this.logger.warn(`āš ļø File not found: ${filePath}`); return []; } const testModule = require(filePath); const testDefinitions = testModule.default || testModule; if (Array.isArray(testDefinitions)) { this.logger.debug(`āœ… Loaded ${testDefinitions.length} tests from ${filePath}`); return testDefinitions; } else if (testDefinitions && typeof testDefinitions === "object") { // Handle both single test objects and modules with multiple exports if (testDefinitions.id) { this.logger.debug(`āœ… Loaded 1 test from ${filePath}`); return [testDefinitions]; } else { // Handle modules with multiple exports (like exports.TEST1, exports.TEST2) const exportedTests = Object.values(testDefinitions).filter((test) => test && typeof test === 'object' && test.id); this.logger.debug(`āœ… Loaded ${exportedTests.length} tests from ${filePath}`); return exportedTests; } } else { this.logger.warn(`āš ļø No valid test definitions found in ${filePath}`); return []; } } catch (error) { this.logger.warn(`āš ļø Could not load test file ${filePath}:`, error.message); return []; } } async runSingleTest(test, options, config) { // Store config for AI executor initialization this.config = config; // aiExecutor is already initialized in run() with browser set const sessionId = `session-${Date.now()}-${Math.random() .toString(36) .substr(2, 9)}`; const sessionDir = path_1.default.join(process.cwd(), "test-results", sessionId); await fs_extra_1.default.ensureDir(sessionDir); const session = { testId: test.id, sessionId, startTime: new Date(), status: "running", steps: [], screenshots: [], errors: [], }; let result; try { // Execute test with AI const executionResult = await this.aiExecutor.executeTest(test, options); const duration = new Date().getTime() - session.startTime.getTime(); result = { ...session, id: test.id, ...executionResult, status: executionResult.success ? "passed" : "failed", endTime: new Date(), duration, }; // Track cost for this test execution await this.aiExecutor.trackTestCost(test.id, sessionId, duration, executionResult.success); console.log(chalk_1.default.green(`āœ… Test ${test.id} completed successfully!`)); console.log(chalk_1.default.gray(`ā±ļø Duration: ${duration}ms`)); } catch (error) { console.error(chalk_1.default.red(`āŒ Test ${test.id} failed:`), error.message); const duration = new Date().getTime() - session.startTime.getTime(); result = { ...session, id: test.id, status: "failed", success: false, endTime: new Date(), duration, errors: [...session.errors, error.message], }; // Track cost even for failed tests await this.aiExecutor.trackTestCost(test.id, sessionId, duration, false); // Take error screenshot if (this.browser) { try { const screenshotPath = path_1.default.join(sessionDir, "error-screenshot.png"); await this.browser.saveScreenshot(screenshotPath); result.screenshots.push(screenshotPath); } catch (screenshotError) { console.error(chalk_1.default.yellow("āš ļø Failed to capture error screenshot:"), screenshotError.message); } } } finally { // Save session data await this.saveSessionData(sessionDir, result); console.log(chalk_1.default.gray(`šŸ“ Session saved: ${sessionDir}\n`)); } return result; } async initializeBrowser(options, config) { // Use default values if config is undefined or incomplete const defaultBrowser = config?.browser?.defaultBrowser || "chrome"; const defaultLogLevel = config?.webdriverio?.logLevel || "info"; const defaultTimeout = config?.webdriverio?.timeout || 30000; const defaultWaitforTimeout = config?.webdriverio?.waitforTimeout || 10000; const defaultConnectionRetryCount = config?.webdriverio?.connectionRetryCount || 3; const defaultConnectionRetryTimeout = config?.webdriverio?.connectionRetryTimeout || 120000; const capabilities = { browserName: options.browser || defaultBrowser, "goog:chromeOptions": { args: [ "--no-sandbox", "--disable-dev-shm-usage", "--disable-gpu", "--disable-web-security", "--disable-features=VizDisplayCompositor", "--disable-extensions", "--disable-plugins", "--disable-background-timer-throttling", "--disable-backgrounding-occluded-windows", "--disable-renderer-backgrounding", "--disable-field-trial-config", "--disable-ipc-flooding-protection", "--start-maximized", "--disable-blink-features=AutomationControlled", ], }, }; if (options.headless !== false) { capabilities["goog:chromeOptions"].args.push("--headless"); } // Use WebdriverIO's direct browser automation (no standalone server needed) const wdioOptions = { automationProtocol: "devtools", capabilities, logLevel: defaultLogLevel, timeout: defaultTimeout, waitforTimeout: defaultWaitforTimeout, connectionRetryCount: defaultConnectionRetryCount, connectionRetryTimeout: defaultConnectionRetryTimeout, }; console.log(chalk_1.default.blue("🌐 Initializing browser with direct automation...")); this.browser = await (0, webdriverio_1.remote)(wdioOptions); } async saveSessionData(sessionDir, result) { const sessionFile = path_1.default.join(sessionDir, "session.json"); await fs_extra_1.default.writeJson(sessionFile, result, { spaces: 2 }); } printSummary(summary) { console.log(chalk_1.default.blue("\nšŸ“Š Test Execution Summary")); console.log(chalk_1.default.gray("─".repeat(50))); console.log(chalk_1.default.white(`Total Tests: ${summary.totalTests}`)); console.log(chalk_1.default.green(`āœ… Passed: ${summary.passed}`)); console.log(chalk_1.default.red(`āŒ Failed: ${summary.failed}`)); console.log(chalk_1.default.blue(`ā±ļø Total Duration: ${summary.totalDuration}ms`)); console.log(chalk_1.default.yellow(`šŸ“ˆ Success Rate: ${summary.successRate.toFixed(1)}%`)); console.log(chalk_1.default.gray("─".repeat(50))); } async generateSimpleReport(summary) { try { const reportDir = "reports"; await fs_extra_1.default.ensureDir(reportDir); const timestamp = new Date().toISOString().replace(/[:.]/g, "-"); const reportFile = path_1.default.join(reportDir, `test-report-${timestamp}.html`); const htmlContent = this.generateHTMLReport(summary); await fs_extra_1.default.writeFile(reportFile, htmlContent); this.logger.success(`šŸ“Š HTML report generated: ${reportFile}`); this.logger.info("🌐 Open the report in your browser to view detailed results"); } catch (error) { this.logger.warn("āš ļø Could not generate HTML report:", error.message); } } generateHTMLReport(summary) { const passedColor = "#28a745"; const failedColor = "#dc3545"; const successRate = summary.successRate; const statusColor = successRate >= 80 ? passedColor : successRate >= 60 ? "#ffc107" : failedColor; return `<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>TestGenius Report - ${new Date().toLocaleDateString()}</title> <style> body { font-family: Arial, sans-serif; margin: 20px; background-color: #f8f9fa; } .header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 20px; border-radius: 10px; margin-bottom: 20px; } .summary { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin-bottom: 30px; } .card { background: white; padding: 20px; border-radius: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); text-align: center; } .card h3 { margin: 0 0 10px 0; color: #333; } .card .number { font-size: 2em; font-weight: bold; } .passed { color: ${passedColor}; } .failed { color: ${failedColor}; } .success-rate { color: ${statusColor}; } .test-results { background: white; border-radius: 10px; padding: 20px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); } .test-item { border-left: 4px solid #ddd; padding: 15px; margin: 10px 0; background: #f8f9fa; border-radius: 0 5px 5px 0; } .test-item.passed { border-left-color: ${passedColor}; } .test-item.failed { border-left-color: ${failedColor}; } .test-name { font-weight: bold; margin-bottom: 5px; } .test-duration { color: #666; font-size: 0.9em; } .test-error { color: ${failedColor}; margin-top: 10px; font-family: monospace; background: #f8f9fa; padding: 10px; border-radius: 5px; } .timestamp { color: #666; font-size: 0.9em; margin-top: 20px; text-align: center; } </style> </head> <body> <div class="header"> <h1>🧠 TestGenius AI Report</h1> <p>Test execution completed on ${new Date().toLocaleString()}</p> </div> <div class="summary"> <div class="card"> <h3>Total Tests</h3> <div class="number">${summary.totalTests}</div> </div> <div class="card"> <h3>Passed</h3> <div class="number passed">${summary.passed}</div> </div> <div class="card"> <h3>Failed</h3> <div class="number failed">${summary.failed}</div> </div> <div class="card"> <h3>Success Rate</h3> <div class="number success-rate">${summary.successRate.toFixed(1)}%</div> </div> <div class="card"> <h3>Duration</h3> <div class="number">${summary.totalDuration}ms</div> </div> </div> <div class="test-results"> <h2>Test Results</h2> ${summary.results .map((result) => ` <div class="test-item ${result.success ? "passed" : "failed"}"> <div class="test-name">${result.testId}</div> <div class="test-duration">Duration: ${result.duration}ms | Status: ${result.status}</div> ${result.errors && result.errors.length > 0 ? ` <div class="test-error"> <strong>Error:</strong> ${result.errors[0]} </div> ` : ""} </div> `) .join("")} </div> <div class="timestamp"> Generated by TestGenius AI on ${new Date().toLocaleString()} </div> </body> </html>`; } getLogger() { return this.logger; } updateLoggingConfig(config) { this.logger.updateConfig(config); } async getLogFiles() { return this.logger.getLogFiles(); } async clearLogs() { return this.logger.clearLogs(); } /** * šŸš€ Run auto-generated test with maximum AI automation */ async runAutoTest(testDefinition, options = {}) { this.logger.info("šŸš€ Running AI-powered auto test..."); try { // Initialize browser if not already done if (!this.browser) { await this.initializeBrowser(options, await this.configManager.loadConfig()); } // Initialize AI executor this.aiExecutor = new AITestExecutor_1.AITestExecutor(await this.configManager.loadConfig()); if (this.browser) { this.aiExecutor.setBrowser(this.browser); } // Run the test with enhanced AI execution const result = await this.aiExecutor.executeTest(testDefinition, options); const testResult = { id: testDefinition.id, testId: testDefinition.id, sessionId: `auto-${Date.now()}`, startTime: new Date(), endTime: new Date(), status: result.success ? 'passed' : 'failed', steps: result.steps, screenshots: result.screenshots, errors: result.errors || [], success: result.success, duration: Date.now() - Date.now(), // Will be calculated properly browser: options.browser || 'chrome' }; this.logger.success("āœ… Auto test completed"); return testResult; } catch (error) { this.logger.error("āŒ Auto test failed:", error.message); throw error; } } /** * šŸ’¾ Save auto-generated test for future use */ async saveAutoTest(testDefinition, result) { try { const testContent = this.generateAutoTestContent(testDefinition, result); const fileName = `${testDefinition.id}.js`; const filePath = path_1.default.join(process.cwd(), 'tests', fileName); await fs_extra_1.default.ensureDir(path_1.default.dirname(filePath)); await fs_extra_1.default.writeFile(filePath, testContent); this.logger.success(`āœ… Auto test saved to: ${filePath}`); } catch (error) { this.logger.error("āŒ Failed to save auto test:", error.message); throw error; } } /** * šŸ“ Generate test content from auto-generated test */ generateAutoTestContent(testDefinition, result) { return `// Auto-generated test by TestGenius AI // Generated on: ${new Date().toISOString()} module.exports = { id: '${testDefinition.id}', name: '${testDefinition.name}', description: '${testDefinition.description}', priority: '${testDefinition.priority}', tags: ${JSON.stringify(testDefinition.tags)}, site: '${testDefinition.site}', task: '${testDefinition.task}', testData: ${JSON.stringify(testDefinition.testData || {}, null, 2)}, // Execution results executionResults: { success: ${result.success}, duration: ${result.duration || 0}, steps: ${result.steps.length}, screenshots: ${result.screenshots.length}, errors: ${result.errors.length} } };`; } } exports.TestRunner = TestRunner; //# sourceMappingURL=TestRunner.js.map