Files
cv-site/test-verification.mjs
T

579 lines
20 KiB
JavaScript
Raw Normal View History

2025-11-16 10:11:58 +00:00
#!/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();