#!/usr/bin/env node /** * Comprehensive Test Runner for HVAC Testing Framework 2.0 * Orchestrates test execution with proper setup, cleanup, and reporting */ const { spawn } = require('child_process'); const fs = require('fs').promises; const path = require('path'); class TestRunner { constructor() { this.config = { environment: process.env.TEST_ENVIRONMENT || 'staging', parallel: process.env.PARALLEL === 'true', headless: process.env.HEADLESS !== 'false', retries: parseInt(process.env.RETRIES) || 2, timeout: parseInt(process.env.TIMEOUT) || 60000 }; this.results = { total: 0, passed: 0, failed: 0, skipped: 0, duration: 0, suites: [] }; } /** * Main test runner entry point */ async run() { console.log('๐ HVAC Testing Framework 2.0 - Test Runner'); console.log(`Environment: ${this.config.environment}`); console.log(`Parallel: ${this.config.parallel}`); console.log(`Headless: ${this.config.headless}`); console.log('โ'.repeat(60)); try { await this.setupEnvironment(); await this.runTestSuites(); await this.generateReports(); await this.cleanup(); this.printSummary(); process.exit(this.results.failed > 0 ? 1 : 0); } catch (error) { console.error('โ Test Runner Failed:', error.message); process.exit(1); } } /** * Set up test environment */ async setupEnvironment() { console.log('๐ Setting up test environment...'); // Create evidence directories const evidenceDirs = [ 'evidence/screenshots', 'evidence/videos', 'evidence/reports', 'evidence/logs' ]; for (const dir of evidenceDirs) { await fs.mkdir(dir, { recursive: true }); } // Validate framework dependencies await this.validateFramework(); console.log('โ Environment setup complete'); } /** * Validate framework dependencies */ async validateFramework() { try { // Check if core framework files exist const coreFiles = [ 'framework/base/BasePage.js', 'framework/base/BaseTest.js', 'framework/browser/BrowserManager.js', 'framework/authentication/AuthManager.js' ]; for (const file of coreFiles) { const filePath = path.join(__dirname, '..', file); await fs.access(filePath); } console.log(' โ Framework core files validated'); } catch (error) { throw new Error(`Framework validation failed: ${error.message}`); } } /** * Run test suites based on arguments or configuration */ async runTestSuites() { const args = process.argv.slice(2); let suitesToRun = []; if (args.length === 0) { // Run all test suites suitesToRun = await this.discoverTestSuites(); } else { // Run specific suites suitesToRun = args.map(suite => this.resolveTestSuite(suite)); } console.log(`๐ Running ${suitesToRun.length} test suite(s):`); suitesToRun.forEach(suite => console.log(` - ${suite}`)); console.log('โ'.repeat(60)); for (const suite of suitesToRun) { await this.runSingleSuite(suite); } } /** * Discover all available test suites */ async discoverTestSuites() { const suites = []; const suitesDir = path.join(__dirname, '..', 'suites'); try { const entries = await fs.readdir(suitesDir, { withFileTypes: true }); for (const entry of entries) { if (entry.isDirectory()) { const suiteFiles = await fs.readdir(path.join(suitesDir, entry.name)); const testFiles = suiteFiles.filter(file => file.endsWith('.js') && (file.includes('test') || file.includes('Test')) ); for (const testFile of testFiles) { suites.push(`suites/${entry.name}/${testFile}`); } } } } catch (error) { console.warn(`Warning: Could not discover test suites: ${error.message}`); } return suites.length > 0 ? suites : ['suites/master-trainer/MasterTrainerE2E.modernized.js']; } /** * Resolve test suite path */ resolveTestSuite(suiteName) { // Handle different suite name formats if (suiteName.includes('/') && suiteName.endsWith('.js')) { return suiteName; // Already a file path } if (suiteName === 'master-trainer' || suiteName === 'mt') { return 'suites/master-trainer/MasterTrainerE2E.modernized.js'; } if (suiteName === 'security') { return 'suites/security/SecurityTests.js'; } if (suiteName === 'trainer') { return 'suites/trainer/TrainerTests.js'; } // Default: assume it's a file in suites directory return `suites/${suiteName}`; } /** * Run a single test suite */ async runSingleSuite(suitePath) { const fullPath = path.join(__dirname, '..', suitePath); const suiteName = path.basename(suitePath, '.js'); console.log(`๐งช Running test suite: ${suiteName}`); const startTime = Date.now(); let suiteResult = { name: suiteName, path: suitePath, passed: 0, failed: 0, skipped: 0, duration: 0, error: null }; try { // Check if file exists await fs.access(fullPath); // Execute the test suite const result = await this.executeTestFile(fullPath); suiteResult.passed = result.passed || 0; suiteResult.failed = result.failed || 0; suiteResult.skipped = result.skipped || 0; } catch (error) { console.error(` โ Suite execution failed: ${error.message}`); suiteResult.failed = 1; suiteResult.error = error.message; } suiteResult.duration = Date.now() - startTime; // Update overall results this.results.total += suiteResult.passed + suiteResult.failed + suiteResult.skipped; this.results.passed += suiteResult.passed; this.results.failed += suiteResult.failed; this.results.skipped += suiteResult.skipped; this.results.suites.push(suiteResult); // Print suite summary const status = suiteResult.failed === 0 ? 'โ ' : 'โ'; console.log(` ${status} ${suiteName}: ${suiteResult.passed} passed, ${suiteResult.failed} failed (${suiteResult.duration}ms)`); } /** * Execute a test file */ async executeTestFile(filePath) { return new Promise((resolve, reject) => { const env = { ...process.env, TEST_ENVIRONMENT: this.config.environment, HEADLESS: this.config.headless.toString(), PARALLEL: this.config.parallel.toString() }; const childProcess = spawn('node', [filePath], { env: env, stdio: 'pipe' }); let stdout = ''; let stderr = ''; childProcess.stdout.on('data', (data) => { const text = data.toString(); stdout += text; process.stdout.write(text); }); childProcess.stderr.on('data', (data) => { const text = data.toString(); stderr += text; process.stderr.write(text); }); childProcess.on('close', (code) => { if (code === 0) { // Parse results from stdout if available const results = this.parseTestResults(stdout); resolve(results); } else { reject(new Error(`Test suite exited with code ${code}: ${stderr}`)); } }); childProcess.on('error', (error) => { reject(new Error(`Failed to start test suite: ${error.message}`)); }); // Set timeout setTimeout(() => { childProcess.kill('SIGKILL'); reject(new Error(`Test suite timed out after ${this.config.timeout}ms`)); }, this.config.timeout); }); } /** * Parse test results from stdout */ parseTestResults(stdout) { const results = { passed: 0, failed: 0, skipped: 0 }; // Look for test completion patterns const passedMatches = stdout.match(/โ .*passed/gi) || []; const failedMatches = stdout.match(/โ.*failed/gi) || []; results.passed = passedMatches.length; results.failed = failedMatches.length; // If no explicit results found, assume success if no errors if (results.passed === 0 && results.failed === 0) { if (stdout.includes('completed successfully') || stdout.includes('All tests passed')) { results.passed = 1; } else if (stdout.includes('failed') || stdout.includes('error')) { results.failed = 1; } else { results.passed = 1; // Default to passed if no clear indication } } return results; } /** * Generate test reports */ async generateReports() { console.log('๐ Generating test reports...'); const reportData = { timestamp: new Date().toISOString(), environment: this.config.environment, configuration: this.config, results: this.results, summary: { totalTests: this.results.total, passRate: this.results.total > 0 ? ((this.results.passed / this.results.total) * 100).toFixed(2) : '0', duration: this.results.duration } }; // Generate JSON report const jsonReportPath = path.join(__dirname, '..', 'evidence', 'reports', 'test-results.json'); await fs.writeFile(jsonReportPath, JSON.stringify(reportData, null, 2)); // Generate HTML report (simple) const htmlReport = this.generateHTMLReport(reportData); const htmlReportPath = path.join(__dirname, '..', 'evidence', 'reports', 'test-results.html'); await fs.writeFile(htmlReportPath, htmlReport); console.log(` โ Reports generated:`); console.log(` ๐ JSON: ${jsonReportPath}`); console.log(` ๐ HTML: ${htmlReportPath}`); } /** * Generate simple HTML report */ generateHTMLReport(reportData) { const { results, summary, timestamp, environment } = reportData; return `
Environment: ${environment}
Timestamp: ${timestamp}
Pass Rate: ${summary.passRate}%
Passed
Failed
Skipped
Total
Path: ${suite.path}
Results: ${suite.passed} passed, ${suite.failed} failed, ${suite.skipped} skipped
Duration: ${suite.duration}ms
${suite.error ? `Error: ${suite.error}
` : ''}