#!/usr/bin/env node /** * Comprehensive test runner for Black Canyon Tickets React Rebuild * * This script orchestrates the execution of all test suites and generates * comprehensive reports with screenshots for QA validation. */ import { exec } from 'child_process'; import { promisify } from 'util'; import fs from 'fs'; import path from 'path'; const execAsync = promisify(exec); interface TestSuite { name: string; file: string; description: string; critical: boolean; } const TEST_SUITES: TestSuite[] = [ { name: 'Smoke Tests', file: 'smoke.spec.ts', description: 'Basic functionality and application health checks', critical: true }, { name: 'Authentication (Realistic)', file: 'auth-realistic.spec.ts', description: 'Login flows using current component selectors', critical: true }, { name: 'Authentication (Enhanced)', file: 'auth.spec.ts', description: 'Full auth suite requiring data-testid attributes', critical: false }, { name: 'Navigation', file: 'navigation.spec.ts', description: 'Sidebar navigation, mobile menu, breadcrumbs, and routing', critical: false }, { name: 'Theme Switching', file: 'theme.spec.ts', description: 'Light/dark theme transitions and persistence', critical: false }, { name: 'Responsive Design', file: 'responsive.spec.ts', description: 'Mobile, tablet, desktop layouts and touch interactions', critical: false }, { name: 'UI Components', file: 'components.spec.ts', description: 'Buttons, forms, cards, modals, and interactive elements', critical: false } ]; class TestRunner { private results: { [key: string]: any } = {}; private startTime: Date = new Date(); async run(options: { critical?: boolean; suite?: string; headed?: boolean } = {}) { console.log('๐Ÿš€ Starting Black Canyon Tickets QA Test Suite'); console.log('=' .repeat(60)); await this.ensureDirectories(); await this.clearPreviousResults(); const suitesToRun = this.filterSuites(options); const playwrightOptions = this.buildPlaywrightOptions(options); console.log(`๐Ÿ“‹ Running ${suitesToRun.length} test suite(s):`); suitesToRun.forEach(suite => { console.log(` ${suite.critical ? '๐Ÿ”ด' : '๐ŸŸก'} ${suite.name}: ${suite.description}`); }); console.log(''); for (const suite of suitesToRun) { await this.runTestSuite(suite, playwrightOptions); } await this.generateReport(); const duration = new Date().getTime() - this.startTime.getTime(); console.log(`\nโœ… Test suite completed in ${Math.round(duration / 1000)}s`); console.log(`๐Ÿ“Š View detailed report: ./playwright-report/index.html`); console.log(`๐Ÿ“ธ Screenshots saved to: ./screenshots/`); } private filterSuites(options: { critical?: boolean; suite?: string }): TestSuite[] { if (options.suite) { const suite = TEST_SUITES.find(s => s.name.toLowerCase().includes(options.suite!.toLowerCase())); return suite ? [suite] : []; } if (options.critical) { return TEST_SUITES.filter(s => s.critical); } return TEST_SUITES; } private buildPlaywrightOptions(options: { headed?: boolean }): string { const opts = []; if (options.headed) { opts.push('--headed'); } opts.push('--reporter=html,line'); return opts.join(' '); } private async ensureDirectories() { const dirs = ['screenshots', 'test-results', 'playwright-report']; for (const dir of dirs) { if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } } } private async clearPreviousResults() { // Clear previous screenshots const screenshotsDir = './screenshots'; if (fs.existsSync(screenshotsDir)) { const files = fs.readdirSync(screenshotsDir); for (const file of files) { if (file.endsWith('.png')) { fs.unlinkSync(path.join(screenshotsDir, file)); } } } } private async runTestSuite(suite: TestSuite, playwrightOptions: string) { console.log(`๐Ÿงช Running ${suite.name} tests...`); try { const command = `npx playwright test tests/${suite.file} ${playwrightOptions}`; const { stdout } = await execAsync(command); this.results[suite.name] = { success: true, output: stdout, error: null, suite }; console.log(` โœ… ${suite.name} - PASSED`); } catch (error: any) { this.results[suite.name] = { success: false, output: error.stdout || '', error: error.stderr || error.message, suite }; console.log(` โŒ ${suite.name} - FAILED`); if (suite.critical) { console.log(` ๐Ÿšจ CRITICAL TEST FAILED - Consider stopping deployment`); } } } private async generateReport() { const report = { timestamp: new Date().toISOString(), duration: new Date().getTime() - this.startTime.getTime(), results: this.results, summary: this.generateSummary() }; // Save JSON report fs.writeFileSync('./test-results/qa-report.json', JSON.stringify(report, null, 2)); // Generate markdown report const markdown = this.generateMarkdownReport(report); fs.writeFileSync('./test-results/qa-report.md', markdown); console.log('\n๐Ÿ“Š Test Summary:'); console.log(` Total Suites: ${Object.keys(this.results).length}`); console.log(` Passed: ${report.summary.passed}`); console.log(` Failed: ${report.summary.failed}`); console.log(` Critical Failed: ${report.summary.criticalFailed}`); } private generateSummary() { const total = Object.keys(this.results).length; const passed = Object.values(this.results).filter(r => r.success).length; const failed = total - passed; const criticalFailed = Object.values(this.results).filter(r => !r.success && r.suite.critical).length; return { total, passed, failed, criticalFailed }; } private generateMarkdownReport(report: any): string { const { summary } = report; let markdown = `# Black Canyon Tickets QA Report\n\n`; markdown += `**Generated:** ${new Date(report.timestamp).toLocaleString()}\n`; markdown += `**Duration:** ${Math.round(report.duration / 1000)}s\n\n`; markdown += `## Summary\n\n`; markdown += `- Total Test Suites: ${summary.total}\n`; markdown += `- โœ… Passed: ${summary.passed}\n`; markdown += `- โŒ Failed: ${summary.failed}\n`; markdown += `- ๐Ÿšจ Critical Failed: ${summary.criticalFailed}\n\n`; if (summary.criticalFailed > 0) { markdown += `> โš ๏ธ **WARNING:** Critical tests have failed. Consider reviewing before deployment.\n\n`; } markdown += `## Test Results\n\n`; for (const [name, result] of Object.entries(report.results)) { const r = result as any; const status = r.success ? 'โœ… PASSED' : 'โŒ FAILED'; const critical = r.suite.critical ? ' (CRITICAL)' : ''; markdown += `### ${name}${critical}\n\n`; markdown += `**Status:** ${status}\n`; markdown += `**Description:** ${r.suite.description}\n\n`; if (!r.success && r.error) { markdown += `**Error:**\n\`\`\`\n${r.error}\n\`\`\`\n\n`; } } markdown += `## Screenshots\n\n`; markdown += `All test screenshots are saved in the \`./screenshots/\` directory.\n`; markdown += `Screenshots are organized by test suite and include timestamps.\n\n`; markdown += `## Next Steps\n\n`; if (summary.criticalFailed > 0) { markdown += `1. Review failed critical tests immediately\n`; markdown += `2. Fix critical issues before deployment\n`; markdown += `3. Re-run critical tests to verify fixes\n`; } else if (summary.failed > 0) { markdown += `1. Review failed non-critical tests\n`; markdown += `2. Consider fixing for next iteration\n`; markdown += `3. Document known issues if acceptable\n`; } else { markdown += `1. All tests passed! ๐ŸŽ‰\n`; markdown += `2. Review screenshots for visual validation\n`; markdown += `3. Application is ready for deployment\n`; } return markdown; } } // CLI interface if (require.main === module) { const args = process.argv.slice(2); const options: any = {}; for (let i = 0; i < args.length; i++) { switch (args[i]) { case '--critical': options.critical = true; break; case '--suite': options.suite = args[++i]; break; case '--headed': options.headed = true; break; case '--help': console.log(` Black Canyon Tickets QA Test Runner Usage: npm run test:qa [options] Options: --critical Run only critical test suites --suite NAME Run specific test suite (auth, navigation, theme, responsive, components) --headed Run tests with visible browser windows --help Show this help message Examples: npm run test:qa # Run all tests npm run test:qa -- --critical # Run only critical tests npm run test:qa -- --suite auth # Run only authentication tests npm run test:qa -- --headed # Run with visible browser `); process.exit(0); } } const runner = new TestRunner(); runner.run(options).catch(error => { console.error('โŒ Test runner failed:', error); process.exit(1); }); } export default TestRunner;