Some checks are pending
HVAC Plugin CI/CD Pipeline / Security Analysis (push) Waiting to run
HVAC Plugin CI/CD Pipeline / Code Quality & Standards (push) Waiting to run
HVAC Plugin CI/CD Pipeline / Unit Tests (push) Waiting to run
HVAC Plugin CI/CD Pipeline / Integration Tests (push) Waiting to run
HVAC Plugin CI/CD Pipeline / Deploy to Staging (push) Blocked by required conditions
HVAC Plugin CI/CD Pipeline / Deploy to Production (push) Blocked by required conditions
HVAC Plugin CI/CD Pipeline / Notification (push) Blocked by required conditions
Security Monitoring & Compliance / Dependency Vulnerability Scan (push) Waiting to run
Security Monitoring & Compliance / Secrets & Credential Scan (push) Waiting to run
Security Monitoring & Compliance / WordPress Security Analysis (push) Waiting to run
Security Monitoring & Compliance / Static Code Security Analysis (push) Waiting to run
Security Monitoring & Compliance / Security Compliance Validation (push) Waiting to run
Security Monitoring & Compliance / Security Summary Report (push) Blocked by required conditions
Security Monitoring & Compliance / Security Team Notification (push) Blocked by required conditions
- Add 90+ test files including E2E, unit, and integration tests - Implement Page Object Model (POM) architecture - Add Docker testing environment with comprehensive services - Include modernized test framework with error recovery - Add specialized test suites for master trainer and trainer workflows - Update .gitignore to properly track test infrastructure 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
465 lines
No EOL
15 KiB
JavaScript
465 lines
No EOL
15 KiB
JavaScript
#!/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 `<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<title>HVAC Testing Framework - Test Results</title>
|
|
<style>
|
|
body { font-family: Arial, sans-serif; margin: 20px; }
|
|
.header { background: #f5f5f5; padding: 20px; border-radius: 8px; margin-bottom: 20px; }
|
|
.summary { display: flex; gap: 20px; margin: 20px 0; }
|
|
.metric { background: #e8f4fd; padding: 15px; border-radius: 5px; text-align: center; }
|
|
.metric.passed { background: #d4edda; }
|
|
.metric.failed { background: #f8d7da; }
|
|
.suite { border: 1px solid #ddd; margin: 10px 0; padding: 15px; border-radius: 5px; }
|
|
.suite.passed { border-left: 5px solid #28a745; }
|
|
.suite.failed { border-left: 5px solid #dc3545; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="header">
|
|
<h1>🧪 HVAC Testing Framework - Test Results</h1>
|
|
<p><strong>Environment:</strong> ${environment}</p>
|
|
<p><strong>Timestamp:</strong> ${timestamp}</p>
|
|
<p><strong>Pass Rate:</strong> ${summary.passRate}%</p>
|
|
</div>
|
|
|
|
<div class="summary">
|
|
<div class="metric passed">
|
|
<h3>${results.passed}</h3>
|
|
<p>Passed</p>
|
|
</div>
|
|
<div class="metric failed">
|
|
<h3>${results.failed}</h3>
|
|
<p>Failed</p>
|
|
</div>
|
|
<div class="metric">
|
|
<h3>${results.skipped}</h3>
|
|
<p>Skipped</p>
|
|
</div>
|
|
<div class="metric">
|
|
<h3>${results.total}</h3>
|
|
<p>Total</p>
|
|
</div>
|
|
</div>
|
|
|
|
<h2>📋 Test Suites</h2>
|
|
${results.suites.map(suite => `
|
|
<div class="suite ${suite.failed === 0 ? 'passed' : 'failed'}">
|
|
<h3>${suite.name}</h3>
|
|
<p><strong>Path:</strong> ${suite.path}</p>
|
|
<p><strong>Results:</strong> ${suite.passed} passed, ${suite.failed} failed, ${suite.skipped} skipped</p>
|
|
<p><strong>Duration:</strong> ${suite.duration}ms</p>
|
|
${suite.error ? `<p><strong>Error:</strong> ${suite.error}</p>` : ''}
|
|
</div>
|
|
`).join('')}
|
|
|
|
<footer style="margin-top: 40px; padding-top: 20px; border-top: 1px solid #ddd; text-align: center; color: #666;">
|
|
<p>Generated by HVAC Testing Framework 2.0</p>
|
|
</footer>
|
|
</body>
|
|
</html>`;
|
|
}
|
|
|
|
/**
|
|
* Clean up test environment
|
|
*/
|
|
async cleanup() {
|
|
console.log('🧹 Cleaning up...');
|
|
|
|
// Clean up temporary files if needed
|
|
// Keep evidence for analysis
|
|
|
|
console.log('✅ Cleanup complete');
|
|
}
|
|
|
|
/**
|
|
* Print final test summary
|
|
*/
|
|
printSummary() {
|
|
console.log('─'.repeat(60));
|
|
console.log('📊 TEST SUMMARY');
|
|
console.log('─'.repeat(60));
|
|
console.log(`Environment: ${this.config.environment}`);
|
|
console.log(`Total Tests: ${this.results.total}`);
|
|
console.log(`✅ Passed: ${this.results.passed}`);
|
|
console.log(`❌ Failed: ${this.results.failed}`);
|
|
console.log(`⏭️ Skipped: ${this.results.skipped}`);
|
|
|
|
if (this.results.total > 0) {
|
|
const passRate = ((this.results.passed / this.results.total) * 100).toFixed(2);
|
|
console.log(`📈 Pass Rate: ${passRate}%`);
|
|
}
|
|
|
|
console.log('─'.repeat(60));
|
|
|
|
if (this.results.failed === 0) {
|
|
console.log('🎉 ALL TESTS PASSED!');
|
|
} else {
|
|
console.log('💥 SOME TESTS FAILED');
|
|
console.log('📄 Check reports in evidence/reports/ for details');
|
|
}
|
|
}
|
|
}
|
|
|
|
// Run the test runner if this file is executed directly
|
|
if (require.main === module) {
|
|
const runner = new TestRunner();
|
|
runner.run().catch(error => {
|
|
console.error('Fatal error:', error);
|
|
process.exit(1);
|
|
});
|
|
}
|
|
|
|
module.exports = TestRunner; |