#!/usr/bin/env node /** * COMPREHENSIVE VERIFICATION TEST SUITE * Tests both HTMX indicators and shortcuts button visibility fixes */ import { chromium } from 'playwright'; const BASE_URL = 'http://localhost:1999'; const RESULTS = { passed: [], failed: [], warnings: [] }; function log(status, message) { const timestamp = new Date().toLocaleTimeString(); const icons = { pass: '✅', fail: '❌', warn: '⚠️', info: 'ℹ️' }; console.log(`[${timestamp}] ${icons[status] || icons.info} ${message}`); if (status === 'pass') RESULTS.passed.push(message); if (status === 'fail') RESULTS.failed.push(message); if (status === 'warn') RESULTS.warnings.push(message); } function measureTime(start) { return `${Date.now() - start}ms`; } async function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } async function test1_HTMXLoadingIndicators(page) { log('info', '═══════════════════════════════════════════════════════'); log('info', 'TEST 1: HTMX Loading Indicators (Feature 003)'); log('info', '═══════════════════════════════════════════════════════'); try { // Navigate to page await page.goto(BASE_URL); await page.waitForLoadState('networkidle'); log('pass', 'Page loaded successfully'); // Test 1.1: Verify indicator elements exist log('info', 'Test 1.1: Checking indicator elements exist...'); const enIndicator = page.locator('#lang-indicator-en'); const esIndicator = page.locator('#lang-indicator-es'); await enIndicator.waitFor({ state: 'attached', timeout: 5000 }); await esIndicator.waitFor({ state: 'attached', timeout: 5000 }); log('pass', 'Both language indicators found in DOM'); // Test 1.2: Verify initial opacity is 0 (hidden) log('info', 'Test 1.2: Checking initial indicator opacity...'); const enInitialOpacity = await enIndicator.evaluate(el => window.getComputedStyle(el).opacity ); const esInitialOpacity = await esIndicator.evaluate(el => window.getComputedStyle(el).opacity ); if (enInitialOpacity === '0' && esInitialOpacity === '0') { log('pass', `Indicators hidden initially (opacity: ${enInitialOpacity})`); } else { log('fail', `Indicators should be hidden (EN: ${enInitialOpacity}, ES: ${esInitialOpacity})`); } // Test 1.3: Click EN button and verify indicator appears log('info', 'Test 1.3: Testing EN button loading indicator...'); // Get the ES button (since we're on EN by default) const esButton = page.locator('button.selector-btn[data-short="ES"]'); await esButton.waitFor({ state: 'visible' }); // Set up monitoring for opacity changes const opacityPromise = page.evaluate(() => { return new Promise(resolve => { const indicator = document.querySelector('#lang-indicator-es'); let maxOpacity = 0; let opacityChanges = []; const observer = new MutationObserver(() => { const currentOpacity = parseFloat(window.getComputedStyle(indicator).opacity); opacityChanges.push(currentOpacity); maxOpacity = Math.max(maxOpacity, currentOpacity); }); observer.observe(indicator.parentElement, { attributes: true, attributeFilter: ['class'], subtree: true }); // Check opacity every 10ms for 2 seconds let checks = 0; const interval = setInterval(() => { const currentOpacity = parseFloat(window.getComputedStyle(indicator).opacity); opacityChanges.push(currentOpacity); maxOpacity = Math.max(maxOpacity, currentOpacity); checks++; if (checks > 200) { // 2 seconds clearInterval(interval); observer.disconnect(); resolve({ maxOpacity, opacityChanges: opacityChanges.filter(o => o > 0) }); } }, 10); }); }); // Click the button const clickTime = Date.now(); await esButton.click(); // Wait for the opacity monitoring to complete const opacityData = await opacityPromise; const responseTime = measureTime(clickTime); log('info', `Request completed in ${responseTime}`); log('info', `Max indicator opacity: ${opacityData.maxOpacity}`); log('info', `Opacity changes detected: ${opacityData.opacityChanges.length}`); if (opacityData.maxOpacity >= 0.9) { log('pass', `Indicator became visible (max opacity: ${opacityData.maxOpacity})`); } else if (opacityData.maxOpacity > 0) { log('warn', `Indicator partially visible but not fully (max: ${opacityData.maxOpacity})`); } else { log('warn', 'Indicator not visible on fast request (expected on localhost - will verify with throttled test)'); } // Test 1.4: Verify indicator faded out after request await sleep(500); const finalOpacity = await esIndicator.evaluate(el => window.getComputedStyle(el).opacity ); if (finalOpacity === '0') { log('pass', `Indicator hidden after request (opacity: ${finalOpacity})`); } else { log('warn', `Indicator may not have faded out (opacity: ${finalOpacity})`); } // Test 1.5: Take screenshot during loading log('info', 'Test 1.5: Capturing screenshot during loading...'); // Click back to EN to trigger another loading state await sleep(500); const enButton = page.locator('button.selector-btn[data-short="EN"]'); // Start click and immediately capture const screenshotPromise = page.screenshot({ path: '/Users/txeo/Git/yo/cv/test-screenshots/htmx-indicator-loading.png', fullPage: false }); await enButton.click(); await screenshotPromise; log('pass', 'Screenshot captured: test-screenshots/htmx-indicator-loading.png'); // Test 1.6: Network throttling test log('info', 'Test 1.6: Testing with slow 3G network...'); // Slow 3G preset - only delay the specific endpoint let requestIntercepted = false; await page.route('**/switch-language**', async route => { if (!requestIntercepted) { requestIntercepted = true; await sleep(800); // Simulate 800ms delay } await route.continue(); }); await sleep(500); const slowClickTime = Date.now(); // Click and monitor const slowOpacityPromise = page.evaluate(() => { return new Promise(resolve => { const indicator = document.querySelector('#lang-indicator-en'); let maxOpacity = 0; const interval = setInterval(() => { const opacity = parseFloat(window.getComputedStyle(indicator).opacity); maxOpacity = Math.max(maxOpacity, opacity); }, 10); setTimeout(() => { clearInterval(interval); resolve(maxOpacity); }, 1000); }); }); await enButton.click(); const slowOpacity = await slowOpacityPromise; await page.waitForLoadState('networkidle'); const slowResponseTime = measureTime(slowClickTime); log('info', `Slow request completed in ${slowResponseTime}`); log('info', `Mid-request opacity: ${slowOpacity}`); if (slowOpacity >= 0.9) { log('pass', `Indicator visible during slow request (opacity: ${slowOpacity})`); } else { log('fail', `Indicator not visible during slow request (opacity: ${slowOpacity})`); } // Unroute to restore normal speed await page.unroute('**/switch-language**'); } catch (error) { log('fail', `Test 1 error: ${error.message}`); console.error(error); } } async function test2_ShortcutsButtonVisibility(page) { log('info', '═══════════════════════════════════════════════════════'); log('info', 'TEST 2: Shortcuts Button Visibility (Feature 001)'); log('info', '═══════════════════════════════════════════════════════'); try { // Ensure we're on the page await page.goto(BASE_URL); await page.waitForLoadState('networkidle'); // Test 2.1: Verify button exists and is visible log('info', 'Test 2.1: Checking shortcuts button exists...'); const shortcutsBtn = page.locator('.shortcuts-btn'); await shortcutsBtn.waitFor({ state: 'visible', timeout: 5000 }); log('pass', 'Shortcuts button found and visible'); // Test 2.2: Measure initial opacity log('info', 'Test 2.2: Measuring button opacity...'); const opacity = await shortcutsBtn.evaluate(el => window.getComputedStyle(el).opacity ); const opacityNum = parseFloat(opacity); log('info', `Button opacity: ${opacity}`); if (opacityNum === 0.6) { log('pass', `Button opacity is exactly 0.6 as expected`); } else if (opacityNum >= 0.5 && opacityNum <= 0.7) { log('warn', `Button opacity close to target (${opacity} vs 0.6)`); } else { log('fail', `Button opacity incorrect (${opacity}, expected 0.6)`); } // Test 2.3: Verify button is actually visible to users log('info', 'Test 2.3: Verifying visual discoverability...'); const boundingBox = await shortcutsBtn.boundingBox(); if (boundingBox) { log('pass', `Button has dimensions: ${boundingBox.width}x${boundingBox.height}px`); log('info', `Position: (${boundingBox.x}, ${boundingBox.y})`); } else { log('fail', 'Button has no bounding box (may not be rendered)'); } // Test 2.4: Test hover state log('info', 'Test 2.4: Testing hover state...'); await shortcutsBtn.hover(); await sleep(500); // Wait for transition const hoverOpacity = await shortcutsBtn.evaluate(el => window.getComputedStyle(el).opacity ); if (parseFloat(hoverOpacity) === 1.0) { log('pass', `Hover opacity is 1.0 (full visibility)`); } else { log('warn', `Hover opacity: ${hoverOpacity} (expected 1.0)`); } // Test 2.5: Take screenshot log('info', 'Test 2.5: Capturing button screenshot...'); await page.screenshot({ path: '/Users/txeo/Git/yo/cv/test-screenshots/shortcuts-button-visible.png', fullPage: false }); log('pass', 'Screenshot captured: test-screenshots/shortcuts-button-visible.png'); // Test 2.6: Verify functionality log('info', 'Test 2.6: Testing button functionality...'); await shortcutsBtn.click(); await sleep(300); // Check if modal opened const modal = page.locator('.shortcuts-modal, [id*="shortcut"], [class*="modal"]'); const modalVisible = await modal.isVisible().catch(() => false); if (modalVisible) { log('pass', 'Shortcuts modal opened successfully'); // Test ESC to close await page.keyboard.press('Escape'); await sleep(300); const modalClosed = await modal.isVisible().catch(() => false); if (!modalClosed) { log('pass', 'Modal closes with ESC key'); } else { log('warn', 'Modal may not close with ESC'); } } else { log('fail', 'Modal did not open on button click'); } // Test 2.7: Check info button consistency log('info', 'Test 2.7: Verifying info button has same opacity...'); const infoBtn = page.locator('.info-button'); const infoBtnExists = await infoBtn.count(); if (infoBtnExists > 0) { const infoOpacity = await infoBtn.evaluate(el => window.getComputedStyle(el).opacity ); if (parseFloat(infoOpacity) === 0.6) { log('pass', `Info button also has opacity 0.6 (consistency maintained)`); } else { log('warn', `Info button opacity: ${infoOpacity} (expected 0.6)`); } } else { log('info', 'Info button not found (may not be on this page)'); } } catch (error) { log('fail', `Test 2 error: ${error.message}`); console.error(error); } } async function test3_RegressionTests(page) { log('info', '═══════════════════════════════════════════════════════'); log('info', 'TEST 3: Regression Testing (Ensure Nothing Broke)'); log('info', '═══════════════════════════════════════════════════════'); try { await page.goto(BASE_URL); await page.waitForLoadState('networkidle'); // Test 3.1: Skeleton loader still works log('info', 'Test 3.1: Verifying skeleton loader animation...'); const skeletonExists = await page.locator('#skeleton-loader').count(); if (skeletonExists > 0) { log('pass', 'Skeleton loader element found'); // Trigger language switch const esButton = page.locator('button.selector-btn[data-short="ES"]'); const skeletonActivated = await page.evaluate(() => { return new Promise(resolve => { const skeleton = document.querySelector('#skeleton-loader'); const observer = new MutationObserver((mutations) => { for (const mutation of mutations) { if (mutation.target.classList.contains('active')) { observer.disconnect(); resolve(true); } } }); observer.observe(skeleton, { attributes: true, attributeFilter: ['class'] }); setTimeout(() => { observer.disconnect(); resolve(false); }, 2000); }); }); await esButton.click(); await page.waitForLoadState('networkidle'); if (skeletonActivated) { log('pass', 'Skeleton loader activated during language switch'); } else { log('warn', 'Skeleton loader may not be activating'); } } else { log('info', 'Skeleton loader not found (may not be used)'); } // Test 3.2: No console errors log('info', 'Test 3.2: Checking for console errors...'); const errors = []; page.on('console', msg => { if (msg.type() === 'error') { errors.push(msg.text()); } }); await page.reload(); await sleep(1000); if (errors.length === 0) { log('pass', 'No console errors detected'); } else { log('fail', `Console errors found: ${errors.join(', ')}`); } // Test 3.3: No layout shifts log('info', 'Test 3.3: Measuring Cumulative Layout Shift...'); const cls = await page.evaluate(() => { return new Promise(resolve => { let clsValue = 0; try { const observer = new PerformanceObserver((list) => { for (const entry of list.getEntries()) { if (!entry.hadRecentInput) { clsValue += entry.value; } } }); observer.observe({ type: 'layout-shift', buffered: true }); setTimeout(() => { observer.disconnect(); resolve(clsValue); }, 2000); } catch (e) { resolve(0); } }); }); log('info', `CLS Score: ${cls.toFixed(3)}`); if (cls < 0.1) { log('pass', 'Excellent CLS score (< 0.1)'); } else if (cls < 0.25) { log('warn', `CLS needs improvement (${cls.toFixed(3)})`); } else { log('fail', `Poor CLS score (${cls.toFixed(3)})`); } // Test 3.4: Page load performance log('info', 'Test 3.4: Measuring page load performance...'); const perfMetrics = await page.evaluate(() => { const perf = performance.getEntriesByType('navigation')[0]; return { loadTime: perf.loadEventEnd - perf.fetchStart, domContentLoaded: perf.domContentLoadedEventEnd - perf.fetchStart, firstPaint: performance.getEntriesByType('paint')[0]?.startTime || 0 }; }); log('info', `Load time: ${perfMetrics.loadTime.toFixed(0)}ms`); log('info', `DOMContentLoaded: ${perfMetrics.domContentLoaded.toFixed(0)}ms`); log('info', `First Paint: ${perfMetrics.firstPaint.toFixed(0)}ms`); if (perfMetrics.loadTime < 3000) { log('pass', 'Page loads in under 3 seconds'); } else { log('warn', `Page load time: ${perfMetrics.loadTime.toFixed(0)}ms`); } } catch (error) { log('fail', `Test 3 error: ${error.message}`); console.error(error); } } async function generateReport() { log('info', '═══════════════════════════════════════════════════════'); log('info', 'FINAL TEST REPORT'); log('info', '═══════════════════════════════════════════════════════'); console.log('\n📊 SUMMARY:'); console.log(` ✅ Passed: ${RESULTS.passed.length}`); console.log(` ❌ Failed: ${RESULTS.failed.length}`); console.log(` ⚠️ Warnings: ${RESULTS.warnings.length}`); if (RESULTS.failed.length > 0) { console.log('\n❌ FAILURES:'); RESULTS.failed.forEach((msg, i) => console.log(` ${i + 1}. ${msg}`)); } if (RESULTS.warnings.length > 0) { console.log('\n⚠️ WARNINGS:'); RESULTS.warnings.forEach((msg, i) => console.log(` ${i + 1}. ${msg}`)); } console.log('\n📈 FEATURE GRADES:'); // Feature 003: HTMX Indicators const indicatorTests = RESULTS.passed.filter(m => m.includes('indicator') || m.includes('Indicator') ).length; const indicatorFails = RESULTS.failed.filter(m => m.includes('indicator') || m.includes('Indicator') ).length; let feature003Grade = 'F'; if (indicatorFails === 0 && indicatorTests >= 5) feature003Grade = 'A'; else if (indicatorFails === 0 && indicatorTests >= 3) feature003Grade = 'B'; else if (indicatorFails <= 1) feature003Grade = 'C'; else if (indicatorFails <= 2) feature003Grade = 'D'; console.log(` Feature 003 (HTMX Indicators): ${feature003Grade} (${indicatorTests} tests passed, ${indicatorFails} failed)`); // Feature 001: Shortcuts Button const buttonTests = RESULTS.passed.filter(m => m.includes('Button') || m.includes('button') || m.includes('opacity') ).length; const buttonFails = RESULTS.failed.filter(m => m.includes('Button') || m.includes('button') || m.includes('opacity') ).length; let feature001Grade = 'A-'; if (buttonFails === 0 && buttonTests >= 6) feature001Grade = 'A'; else if (buttonFails === 0 && buttonTests >= 4) feature001Grade = 'A-'; else if (buttonFails <= 1) feature001Grade = 'B+'; else if (buttonFails <= 2) feature001Grade = 'B'; console.log(` Feature 001 (Shortcuts Button): ${feature001Grade} (${buttonTests} tests passed, ${buttonFails} failed)`); console.log('\n📸 SCREENSHOTS:'); console.log(' - test-screenshots/htmx-indicator-loading.png'); console.log(' - test-screenshots/shortcuts-button-visible.png'); const overallSuccess = RESULTS.failed.length === 0; console.log(`\n${overallSuccess ? '✅ ALL TESTS PASSED' : '❌ SOME TESTS FAILED'}\n`); return overallSuccess; } async function main() { console.log('🧪 COMPREHENSIVE VERIFICATION TEST SUITE'); console.log('Testing HTMX Indicators + Shortcuts Button Fixes\n'); // Create screenshots directory const { mkdir } = await import('fs/promises'); await mkdir('/Users/txeo/Git/yo/cv/test-screenshots', { recursive: true }); const browser = await chromium.launch({ headless: true, args: ['--disable-dev-shm-usage'] }); const context = await browser.newContext({ viewport: { width: 1920, height: 1080 }, userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36' }); const page = await context.newPage(); try { // Run all test suites await test1_HTMXLoadingIndicators(page); await test2_ShortcutsButtonVisibility(page); await test3_RegressionTests(page); // Generate report const success = await generateReport(); await browser.close(); process.exit(success ? 0 : 1); } catch (error) { console.error('Fatal error:', error); await browser.close(); process.exit(1); } } main();