#!/usr/bin/env node /** * HVAC Event Creation Test Suite Runner * * Comprehensive test suite runner for all UI/UX enhanced functionality. * Runs tests in optimal order with proper setup and teardown. * * Usage: * node tests/test-suite-runner.js * node tests/test-suite-runner.js --suite=security * node tests/test-suite-runner.js --parallel=false * node tests/test-suite-runner.js --browser=chromium */ const { execSync, spawn } = require('child_process'); const path = require('path'); const fs = require('fs'); // Test suites in execution order const TEST_SUITES = { security: { file: 'test-event-creation-security.js', description: 'Security vulnerability tests (XSS, CSRF, file upload)', priority: 1, timeout: 60000, retries: 2 }, 'rich-text-editor': { file: 'test-rich-text-editor.js', description: 'Rich text editor functionality and validation', priority: 2, timeout: 45000, retries: 1 }, 'featured-image-upload': { file: 'test-featured-image-upload.js', description: 'Featured image upload with drag-and-drop', priority: 2, timeout: 60000, retries: 2 }, 'searchable-selectors': { file: 'test-searchable-selectors.js', description: 'Multi-select and searchable selector components', priority: 2, timeout: 45000, retries: 1 }, 'modal-forms': { file: 'test-modal-forms.js', description: 'Modal form creation and validation', priority: 3, timeout: 45000, retries: 1 }, 'toggle-controls': { file: 'test-toggle-controls.js', description: 'Toggle switch controls and state management', priority: 3, timeout: 30000, retries: 1 }, 'integration': { file: 'test-integration-comprehensive.js', description: 'End-to-end integration tests', priority: 4, timeout: 120000, retries: 2 } }; // Configuration const CONFIG = { browser: process.env.BROWSER || 'chromium', headless: process.env.HEADLESS !== 'false', baseUrl: process.env.BASE_URL || 'http://localhost:8080', parallel: process.argv.includes('--parallel=false') ? false : true, maxWorkers: process.env.MAX_WORKERS || '4', timeout: 120000, retries: 2 }; // Parse command line arguments const args = process.argv.slice(2); const suiteFilter = args.find(arg => arg.startsWith('--suite='))?.split('=')[1]; const browserArg = args.find(arg => arg.startsWith('--browser='))?.split('=')[1]; if (browserArg) CONFIG.browser = browserArg; class TestRunner { constructor() { this.results = { total: 0, passed: 0, failed: 0, skipped: 0, duration: 0, suiteResults: {} }; } async run() { console.log('๐ HVAC Event Creation Test Suite Runner'); console.log('=========================================='); console.log(`Browser: ${CONFIG.browser}`); console.log(`Headless: ${CONFIG.headless}`); console.log(`Base URL: ${CONFIG.baseUrl}`); console.log(`Parallel: ${CONFIG.parallel}`); console.log(''); // Validate environment await this.validateEnvironment(); // Setup test environment await this.setupTestEnvironment(); // Get test suites to run const suitesToRun = this.getSuitesToRun(); console.log(`๐ Running ${suitesToRun.length} test suites:`); suitesToRun.forEach(suite => { console.log(` ${suite.name}: ${suite.config.description}`); }); console.log(''); const startTime = Date.now(); try { if (CONFIG.parallel) { await this.runSuitesParallel(suitesToRun); } else { await this.runSuitesSequential(suitesToRun); } } catch (error) { console.error('โ Test suite runner failed:', error.message); process.exit(1); } this.results.duration = Date.now() - startTime; // Generate report await this.generateReport(); // Cleanup await this.cleanup(); // Exit with appropriate code process.exit(this.results.failed > 0 ? 1 : 0); } async validateEnvironment() { console.log('๐ Validating test environment...'); // Check if Playwright is installed try { execSync('npx playwright --version', { stdio: 'pipe' }); } catch (error) { console.error('โ Playwright not found. Please install with: npm install @playwright/test'); process.exit(1); } // Check if test server is running try { const response = await fetch(CONFIG.baseUrl); if (!response.ok) { throw new Error(`Server returned ${response.status}`); } } catch (error) { console.error(`โ Test server not accessible at ${CONFIG.baseUrl}`); console.log('๐ก Start the test server with: docker compose -f tests/docker-compose.test.yml up -d'); process.exit(1); } // Check test files exist const missingFiles = []; Object.values(TEST_SUITES).forEach(suite => { const filePath = path.join(__dirname, suite.file); if (!fs.existsSync(filePath)) { missingFiles.push(suite.file); } }); if (missingFiles.length > 0) { console.error('โ Missing test files:', missingFiles.join(', ')); process.exit(1); } console.log('โ Environment validation passed'); } async setupTestEnvironment() { console.log('๐ ๏ธ Setting up test environment...'); // Create test fixtures directory const fixturesDir = path.join(__dirname, 'fixtures', 'images'); if (!fs.existsSync(fixturesDir)) { fs.mkdirSync(fixturesDir, { recursive: true }); } // Create minimal test images await this.createTestFixtures(fixturesDir); // Verify test database state await this.verifyTestDatabase(); console.log('โ Test environment setup complete'); } async createTestFixtures(dir) { // Create minimal valid image files for testing const validJpeg = Buffer.from([ 0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00, 0x01, 0x01, 0x01, 0x00, 0x48, 0x00, 0x48, 0x00, 0x00, // ... minimal JPEG data 0xFF, 0xD9 ]); const testImages = { 'hvac-training.jpg': validJpeg, 'manual-j-training.jpg': validJpeg, 'valid-image.jpg': validJpeg }; Object.entries(testImages).forEach(([filename, data]) => { const filePath = path.join(dir, filename); if (!fs.existsSync(filePath)) { fs.writeFileSync(filePath, data); } }); } async verifyTestDatabase() { // Check if test data is available try { const response = await fetch(`${CONFIG.baseUrl}/wp-json/hvac/v1/test-data-status`); if (response.ok) { const data = await response.json(); if (!data.hasTestData) { console.warn('โ ๏ธ Test database may need seeding. Run: bin/seed-comprehensive-events.sh'); } } } catch (error) { console.warn('โ ๏ธ Could not verify test database status'); } } getSuitesToRun() { const suites = []; if (suiteFilter) { if (TEST_SUITES[suiteFilter]) { suites.push({ name: suiteFilter, config: TEST_SUITES[suiteFilter] }); } else { console.error(`โ Unknown test suite: ${suiteFilter}`); console.log('Available suites:', Object.keys(TEST_SUITES).join(', ')); process.exit(1); } } else { // Run all suites in priority order Object.entries(TEST_SUITES) .sort(([,a], [,b]) => a.priority - b.priority) .forEach(([name, config]) => { suites.push({ name, config }); }); } return suites; } async runSuitesParallel(suites) { console.log('โก Running test suites in parallel...\n'); const promises = suites.map(suite => this.runSuite(suite)); const results = await Promise.allSettled(promises); results.forEach((result, index) => { const suiteName = suites[index].name; if (result.status === 'fulfilled') { this.results.suiteResults[suiteName] = result.value; } else { this.results.suiteResults[suiteName] = { passed: 0, failed: 1, error: result.reason.message }; } }); } async runSuitesSequential(suites) { console.log('๐ Running test suites sequentially...\n'); for (const suite of suites) { try { const result = await this.runSuite(suite); this.results.suiteResults[suite.name] = result; } catch (error) { this.results.suiteResults[suite.name] = { passed: 0, failed: 1, error: error.message }; } } } async runSuite(suite) { console.log(`๐งช Running ${suite.name}...`); const playwrightArgs = [ 'test', path.join(__dirname, suite.config.file), `--project=${CONFIG.browser}`, `--timeout=${suite.config.timeout || CONFIG.timeout}`, `--retries=${suite.config.retries || CONFIG.retries}`, '--reporter=json' ]; if (CONFIG.headless) { playwrightArgs.push('--headed=false'); } return new Promise((resolve, reject) => { const process = spawn('npx', ['playwright', ...playwrightArgs], { stdio: ['ignore', 'pipe', 'pipe'], env: { ...process.env, BASE_URL: CONFIG.baseUrl } }); let stdout = ''; let stderr = ''; process.stdout.on('data', (data) => { stdout += data.toString(); }); process.stderr.on('data', (data) => { stderr += data.toString(); }); process.on('close', (code) => { try { // Parse Playwright JSON output const jsonOutput = stdout.split('\n') .find(line => line.trim().startsWith('{')) ?.trim(); if (jsonOutput) { const results = JSON.parse(jsonOutput); const suiteResult = { passed: results.stats?.passed || 0, failed: results.stats?.failed || 0, skipped: results.stats?.skipped || 0, duration: results.stats?.duration || 0, details: results.suites || [] }; if (code === 0) { console.log(`โ ${suite.name}: ${suiteResult.passed} passed, ${suiteResult.failed} failed`); } else { console.log(`โ ${suite.name}: ${suiteResult.passed} passed, ${suiteResult.failed} failed`); } resolve(suiteResult); } else { // Fallback parsing const passed = (stdout.match(/\d+ passed/g) || ['0'])[0].split(' ')[0]; const failed = (stdout.match(/\d+ failed/g) || ['0'])[0].split(' ')[0]; const result = { passed: parseInt(passed), failed: parseInt(failed), error: code !== 0 ? stderr : null }; if (code === 0) { console.log(`โ ${suite.name}: ${result.passed} passed`); } else { console.log(`โ ${suite.name}: ${result.passed} passed, ${result.failed} failed`); } resolve(result); } } catch (parseError) { reject(new Error(`Failed to parse test results: ${parseError.message}`)); } }); // Kill test after maximum timeout setTimeout(() => { process.kill(); reject(new Error(`Test suite ${suite.name} timed out`)); }, suite.config.timeout + 30000); }); } async generateReport() { console.log('\n๐ Test Results Summary'); console.log('========================'); // Aggregate results Object.values(this.results.suiteResults).forEach(result => { this.results.total += (result.passed || 0) + (result.failed || 0) + (result.skipped || 0); this.results.passed += result.passed || 0; this.results.failed += result.failed || 0; this.results.skipped += result.skipped || 0; }); // Suite-by-suite results Object.entries(this.results.suiteResults).forEach(([suiteName, result]) => { const status = (result.failed || 0) > 0 ? 'โ' : 'โ '; console.log(`${status} ${suiteName}: ${result.passed || 0} passed, ${result.failed || 0} failed`); if (result.error) { console.log(` Error: ${result.error}`); } }); console.log(''); console.log(`Total Tests: ${this.results.total}`); console.log(`Passed: ${this.results.passed} โ `); console.log(`Failed: ${this.results.failed} ${this.results.failed > 0 ? 'โ' : ''}`); console.log(`Skipped: ${this.results.skipped}`); console.log(`Duration: ${(this.results.duration / 1000).toFixed(2)}s`); const successRate = this.results.total > 0 ? ((this.results.passed / this.results.total) * 100).toFixed(1) : '0.0'; console.log(`Success Rate: ${successRate}%`); // Generate HTML report await this.generateHtmlReport(); console.log('\n๐ Detailed HTML report generated: tests/reports/test-results.html'); } async generateHtmlReport() { const reportsDir = path.join(__dirname, 'reports'); if (!fs.existsSync(reportsDir)) { fs.mkdirSync(reportsDir, { recursive: true }); } const htmlContent = this.generateHtmlContent(); fs.writeFileSync(path.join(reportsDir, 'test-results.html'), htmlContent); } generateHtmlContent() { const timestamp = new Date().toISOString(); const successRate = this.results.total > 0 ? ((this.results.passed / this.results.total) * 100).toFixed(1) : '0.0'; return `
Generated: ${timestamp}
Environment: ${CONFIG.baseUrl} (${CONFIG.browser})
Total Tests
Passed
Failed
Success Rate
Description: ${TEST_SUITES[suiteName]?.description || 'Test suite'}
Results: ${result.passed || 0} passed, ${result.failed || 0} failed, ${result.skipped || 0} skipped
${result.duration ? `Duration: ${(result.duration / 1000).toFixed(2)}s
` : ''} ${result.error ? `HVAC Community Events Plugin - UI/UX Enhancement Test Suite