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
JavaScript
"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