#!/usr/bin/env node /** * COMPREHENSIVE CV SITE TEST SUITE * * Tests ALL features systematically: * - Hyperscript functions (9 total) * - Toggle functionality (CV length, icons, theme) * - Hover sync (PDF, print, zoom) * - Zoom functionality * - Scroll behavior * - Fixed button positioning * - Error detection * * Usage: node test-comprehensive.mjs */ import { chromium } from 'playwright'; const BASE_URL = 'http://localhost:1999'; const RESULTS = { passed: [], failed: [], warnings: [], errors: [] }; // Utility functions function log(status, message, indent = 0) { const timestamp = new Date().toLocaleTimeString(); const icons = { pass: '✅', fail: '❌', warn: '⚠️', info: 'ℹ️', section: '📋', error: '🔴' }; const prefix = ' '.repeat(indent); console.log(`${prefix}[${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); if (status === 'error') RESULTS.errors.push(message); } async function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } // ============================================================================== // TEST 1: HYPERSCRIPT FUNCTIONS & ERROR DETECTION // ============================================================================== async function test1_HyperscriptFunctions(page) { log('section', '═══════════════════════════════════════════════════════'); log('section', 'TEST 1: Hyperscript Functions & Error Detection'); log('section', '═══════════════════════════════════════════════════════'); try { // Navigate with cache-busting const cacheBust = `?t=${Date.now()}`; await page.goto(`${BASE_URL}${cacheBust}`, { waitUntil: 'networkidle' }); log('pass', 'Page loaded successfully', 1); // Test 1.1: Check for hyperscript parse errors log('info', 'Test 1.1: Checking for hyperscript parse errors...', 1); const parseErrors = await page.evaluate(() => { const errors = []; // Check for hyperscript error markers in console window._hyperscriptParseErrors = window._hyperscriptParseErrors || []; return window._hyperscriptParseErrors; }); if (parseErrors.length === 0) { log('pass', 'No hyperscript parse errors detected', 2); } else { log('fail', `Hyperscript parse errors found: ${parseErrors.join(', ')}`, 2); } // Test 1.2: Verify all 9 hyperscript functions are defined log('info', 'Test 1.2: Verifying all 9 hyperscript functions exist...', 1); const functionsCheck = await page.evaluate(() => { const requiredFunctions = [ 'printFriendly', 'initScrollBehavior', 'handleScroll', 'toggleCVLength', 'toggleIcons', 'toggleTheme', 'syncPdfHover', 'syncPrintHover', 'highlightZoomControl' ]; const results = {}; for (const funcName of requiredFunctions) { // Check if function exists in hyperscript runtime try { const func = _hyperscript.evaluate(`${funcName}`); results[funcName] = typeof func === 'function'; } catch (e) { results[funcName] = false; } } return results; }); const allFunctionsExist = Object.values(functionsCheck).every(v => v === true); if (allFunctionsExist) { log('pass', 'All 9 hyperscript functions are defined', 2); } else { const missing = Object.entries(functionsCheck) .filter(([_, exists]) => !exists) .map(([name]) => name); log('fail', `Missing functions: ${missing.join(', ')}`, 2); } // Test 1.3: Track console errors and warnings log('info', 'Test 1.3: Setting up error tracking...', 1); page.on('console', msg => { if (msg.type() === 'error') { log('error', `Console error: ${msg.text()}`, 2); } }); page.on('pageerror', error => { log('error', `Page error: ${error.message}`, 2); }); log('pass', 'Error tracking enabled', 2); } catch (error) { log('fail', `Test 1 error: ${error.message}`, 1); } } // ============================================================================== // TEST 2: TOGGLE FUNCTIONALITY // ============================================================================== async function test2_ToggleFunctionality(page) { log('section', '═══════════════════════════════════════════════════════'); log('section', 'TEST 2: Toggle Functionality (CV Length, Icons, Theme)'); log('section', '═══════════════════════════════════════════════════════'); try { // Test 2.1: CV Length Toggle log('info', 'Test 2.1: Testing CV length toggle...', 1); const lengthToggle = page.locator('#lengthToggle'); await lengthToggle.waitFor({ state: 'visible', timeout: 5000 }); // Get initial state const initialLengthState = await page.evaluate(() => { const paper = document.querySelector('.cv-paper'); return { isLong: paper.classList.contains('cv-long'), isShort: paper.classList.contains('cv-short') }; }); log('info', `Initial state: ${initialLengthState.isLong ? 'long' : 'short'}`, 2); // Click toggle await lengthToggle.click(); await sleep(500); // Verify state changed const newLengthState = await page.evaluate(() => { const paper = document.querySelector('.cv-paper'); return { isLong: paper.classList.contains('cv-long'), isShort: paper.classList.contains('cv-short') }; }); if (newLengthState.isLong !== initialLengthState.isLong) { log('pass', `CV length toggled successfully (now ${newLengthState.isLong ? 'long' : 'short'})`, 2); } else { log('fail', 'CV length did not toggle', 2); } // Test 2.2: Icons Toggle log('info', 'Test 2.2: Testing icons toggle...', 1); const iconsToggle = page.locator('#iconToggle'); await iconsToggle.waitFor({ state: 'visible', timeout: 5000 }); const initialIconsState = await page.evaluate(() => { const container = document.querySelector('.cv-container'); return container.classList.contains('hide-icons'); }); log('info', `Initial state: icons ${initialIconsState ? 'hidden' : 'visible'}`, 2); await iconsToggle.click(); await sleep(500); const newIconsState = await page.evaluate(() => { const container = document.querySelector('.cv-container'); return container.classList.contains('hide-icons'); }); if (newIconsState !== initialIconsState) { log('pass', `Icons toggled successfully (now ${newIconsState ? 'hidden' : 'visible'})`, 2); } else { log('fail', 'Icons did not toggle', 2); } // Test 2.3: Theme Toggle log('info', 'Test 2.3: Testing theme toggle...', 1); const themeToggle = page.locator('#themeToggle'); await themeToggle.waitFor({ state: 'visible', timeout: 5000 }); const initialThemeState = await page.evaluate(() => { const container = document.querySelector('.cv-container'); return container.classList.contains('theme-clean'); }); log('info', `Initial state: ${initialThemeState ? 'clean' : 'default'} theme`, 2); await themeToggle.click(); await sleep(500); const newThemeState = await page.evaluate(() => { const container = document.querySelector('.cv-container'); return container.classList.contains('theme-clean'); }); if (newThemeState !== initialThemeState) { log('pass', `Theme toggled successfully (now ${newThemeState ? 'clean' : 'default'})`, 2); } else { log('fail', 'Theme did not toggle', 2); } // Test 2.4: Verify localStorage persistence log('info', 'Test 2.4: Verifying localStorage persistence...', 1); const localStorageData = await page.evaluate(() => { return { length: localStorage.getItem('cv-length'), icons: localStorage.getItem('cv-icons'), theme: localStorage.getItem('cv-theme') }; }); const hasStorage = localStorageData.length && localStorageData.icons && localStorageData.theme; if (hasStorage) { log('pass', `Preferences saved to localStorage: length=${localStorageData.length}, icons=${localStorageData.icons}, theme=${localStorageData.theme}`, 2); } else { log('warn', 'Some preferences not saved to localStorage', 2); } } catch (error) { log('fail', `Test 2 error: ${error.message}`, 1); } } // ============================================================================== // TEST 3: HOVER SYNC FUNCTIONALITY // ============================================================================== async function test3_HoverSync(page) { log('section', '═══════════════════════════════════════════════════════'); log('section', 'TEST 3: Hover Sync (PDF, Print, Zoom)'); log('section', '═══════════════════════════════════════════════════════'); try { // Test 3.1: PDF Download Hover Sync log('info', 'Test 3.1: Testing PDF download hover sync...', 1); const pdfButtons = await page.locator('.pdf-btn').all(); log('info', `Found ${pdfButtons.length} PDF download buttons`, 2); if (pdfButtons.length > 0) { // Hover over first button await pdfButtons[0].hover(); await sleep(300); // Check if all buttons have sync class const allSynced = await page.evaluate(() => { const buttons = Array.from(document.querySelectorAll('.pdf-btn')); return buttons.every(btn => btn.classList.contains('pdf-hover-sync')); }); if (allSynced) { log('pass', 'All PDF download buttons synced on hover', 2); } else { log('fail', 'PDF download buttons not syncing on hover', 2); } // Move away and check sync removed await page.mouse.move(0, 0); await sleep(300); const allUnsynced = await page.evaluate(() => { const buttons = Array.from(document.querySelectorAll('.pdf-btn')); return buttons.every(btn => !btn.classList.contains('pdf-hover-sync')); }); if (allUnsynced) { log('pass', 'PDF download hover sync removed correctly', 2); } else { log('warn', 'PDF download hover sync may not be clearing', 2); } } else { log('warn', 'No PDF download buttons found', 2); } // Test 3.2: Print Friendly Hover Sync log('info', 'Test 3.2: Testing print friendly hover sync...', 1); const printButtons = await page.locator('.print-button').all(); log('info', `Found ${printButtons.length} print buttons`, 2); if (printButtons.length > 0) { await printButtons[0].hover(); await sleep(300); const allSynced = await page.evaluate(() => { const buttons = Array.from(document.querySelectorAll('.print-button')); return buttons.every(btn => btn.classList.contains('print-hover-sync')); }); if (allSynced) { log('pass', 'All print buttons synced on hover', 2); } else { log('fail', 'Print buttons not syncing on hover', 2); } await page.mouse.move(0, 0); await sleep(300); } else { log('warn', 'No print buttons found', 2); } // Test 3.3: Zoom Control Highlight log('info', 'Test 3.3: Testing zoom control highlight...', 1); // Check if zoom control is visible or hidden const zoomControlVisible = await page.evaluate(() => { const control = document.querySelector('#zoom-control'); return control && window.getComputedStyle(control).display !== 'none'; }); if (!zoomControlVisible) { // Try to find and interact with show button const showZoomButton = page.locator('#show-zoom-menu-btn'); const showZoomExists = await showZoomButton.count(); if (showZoomExists > 0) { // The button exists but may be in the hamburger menu log('info', 'Show zoom button found in menu (may require menu interaction)', 2); const hasHighlight = await page.evaluate(() => { const wrapper = document.querySelector('#zoom-wrapper'); return wrapper.classList.contains('highlight'); }); if (hasHighlight) { log('pass', 'Zoom control highlighted on button hover', 2); } else { log('warn', 'Zoom control highlight test skipped (button in menu)', 2); } } else { log('info', 'Show zoom button not found', 2); } } else { log('info', 'Zoom control already visible (no need for show button)', 2); } } catch (error) { log('fail', `Test 3 error: ${error.message}`, 1); } } // ============================================================================== // TEST 4: ZOOM FUNCTIONALITY // ============================================================================== async function test4_ZoomFunctionality(page) { log('section', '═══════════════════════════════════════════════════════'); log('section', 'TEST 4: Zoom Functionality'); log('section', '═══════════════════════════════════════════════════════'); try { // Test 4.1: Zoom control visibility log('info', 'Test 4.1: Verifying zoom control exists...', 1); const zoomControl = page.locator('#zoom-control'); const zoomControlVisible = await zoomControl.isVisible().catch(() => false); if (zoomControlVisible) { log('pass', 'Zoom control is visible', 2); } else { log('warn', 'Zoom control not visible (may be hidden by default)', 2); } // Test 4.2: Zoom slider functionality log('info', 'Test 4.2: Testing zoom slider...', 1); const zoomSlider = page.locator('#zoom-slider'); const sliderExists = await zoomSlider.count(); if (sliderExists > 0) { // Get initial zoom const initialZoom = await page.evaluate(() => { const wrapper = document.querySelector('#zoom-wrapper'); const transform = window.getComputedStyle(wrapper).transform; return transform; }); log('info', `Initial zoom transform: ${initialZoom}`, 2); // Change slider value await zoomSlider.fill('120'); await sleep(500); // Check if zoom changed const newZoom = await page.evaluate(() => { const wrapper = document.querySelector('#zoom-wrapper'); const transform = window.getComputedStyle(wrapper).transform; return transform; }); log('info', `New zoom transform: ${newZoom}`, 2); if (newZoom !== initialZoom) { log('pass', 'Zoom slider changes zoom level', 2); } else { log('fail', 'Zoom slider not affecting zoom level', 2); } // Reset to 100% await zoomSlider.fill('100'); await sleep(500); } else { log('warn', 'Zoom slider not found', 2); } // Test 4.3: Zoom persistence log('info', 'Test 4.3: Testing zoom persistence...', 1); const zoomInStorage = await page.evaluate(() => { return localStorage.getItem('cv-zoom'); }); if (zoomInStorage) { log('pass', `Zoom level saved to localStorage: ${zoomInStorage}%`, 2); } else { log('warn', 'Zoom level not saved to localStorage', 2); } } catch (error) { log('fail', `Test 4 error: ${error.message}`, 1); } } // ============================================================================== // TEST 5: SCROLL BEHAVIOR // ============================================================================== async function test5_ScrollBehavior(page) { log('section', '═══════════════════════════════════════════════════════'); log('section', 'TEST 5: Scroll Behavior'); log('section', '═══════════════════════════════════════════════════════'); try { // Test 5.1: Scroll to trigger header hide log('info', 'Test 5.1: Testing header hide on scroll down...', 1); // Scroll down await page.evaluate(() => window.scrollTo(0, 500)); await sleep(500); const headerHidden = await page.evaluate(() => { const actionBar = document.querySelector('.action-bar'); return actionBar.classList.contains('header-hidden'); }); if (headerHidden) { log('pass', 'Header hidden after scrolling down', 2); } else { log('warn', 'Header not hiding on scroll down (may need more scroll)', 2); } // Test 5.2: Back to top button visibility log('info', 'Test 5.2: Testing back-to-top button visibility...', 1); const backToTopVisible = await page.locator('#back-to-top').isVisible(); if (backToTopVisible) { log('pass', 'Back-to-top button visible after scroll', 2); } else { log('fail', 'Back-to-top button not visible after scroll', 2); } // Test 5.3: Scroll up to show header log('info', 'Test 5.3: Testing header show on scroll up...', 1); await page.evaluate(() => window.scrollTo(0, 0)); await sleep(500); const headerShown = await page.evaluate(() => { const actionBar = document.querySelector('.action-bar'); return !actionBar.classList.contains('header-hidden'); }); if (headerShown) { log('pass', 'Header shown after scrolling to top', 2); } else { log('fail', 'Header still hidden after scrolling to top', 2); } // Test 5.4: Back to top button hidden at top log('info', 'Test 5.4: Testing back-to-top button hidden at top...', 1); const backToTopHidden = await page.locator('#back-to-top').isHidden(); if (backToTopHidden) { log('pass', 'Back-to-top button hidden at top of page', 2); } else { log('warn', 'Back-to-top button still visible at top', 2); } } catch (error) { log('fail', `Test 5 error: ${error.message}`, 1); } } // ============================================================================== // TEST 6: FIXED BUTTON POSITIONING // ============================================================================== async function test6_FixedButtonPositioning(page) { log('section', '═══════════════════════════════════════════════════════'); log('section', 'TEST 6: Fixed Button Positioning (At-Bottom)'); log('section', '═══════════════════════════════════════════════════════'); try { // Test 6.1: Scroll to bottom log('info', 'Test 6.1: Testing at-bottom class application...', 1); // Scroll to bottom await page.evaluate(() => { window.scrollTo(0, document.documentElement.scrollHeight); }); await sleep(800); // Check if at-bottom class is applied const buttonsAtBottom = await page.evaluate(() => { const backToTop = document.querySelector('#back-to-top'); const infoBtn = document.querySelector('#info-button'); const shortcutsBtn = document.querySelector('#shortcuts-button'); return { backToTop: backToTop?.classList.contains('at-bottom'), infoBtn: infoBtn?.classList.contains('at-bottom'), shortcutsBtn: shortcutsBtn?.classList.contains('at-bottom') }; }); const allAtBottom = Object.values(buttonsAtBottom).every(v => v === true); if (allAtBottom) { log('pass', 'All fixed buttons have at-bottom class', 2); } else { log('fail', `Some buttons missing at-bottom class: ${JSON.stringify(buttonsAtBottom)}`, 2); } // Test 6.2: Scroll up to remove at-bottom log('info', 'Test 6.2: Testing at-bottom class removal...', 1); await page.evaluate(() => window.scrollTo(0, 200)); await sleep(500); const buttonsNotAtBottom = await page.evaluate(() => { const backToTop = document.querySelector('#back-to-top'); const infoBtn = document.querySelector('#info-button'); const shortcutsBtn = document.querySelector('#shortcuts-button'); return { backToTop: !backToTop?.classList.contains('at-bottom'), infoBtn: !infoBtn?.classList.contains('at-bottom'), shortcutsBtn: !shortcutsBtn?.classList.contains('at-bottom') }; }); const allNotAtBottom = Object.values(buttonsNotAtBottom).every(v => v === true); if (allNotAtBottom) { log('pass', 'At-bottom class removed from all buttons', 2); } else { log('warn', 'Some buttons still have at-bottom class', 2); } } catch (error) { log('fail', `Test 6 error: ${error.message}`, 1); } } // ============================================================================== // TEST 7: KEYBOARD SHORTCUTS // ============================================================================== async function test7_KeyboardShortcuts(page) { log('section', '═══════════════════════════════════════════════════════'); log('section', 'TEST 7: Keyboard Shortcuts'); log('section', '═══════════════════════════════════════════════════════'); try { // Test 7.1: ? key opens shortcuts modal log('info', 'Test 7.1: Testing ? key to open shortcuts modal...', 1); await page.keyboard.press('?'); await sleep(500); const modalVisible = await page.evaluate(() => { const modal = document.querySelector('#shortcuts-modal'); return modal && modal.hasAttribute('open'); }); if (modalVisible) { log('pass', 'Shortcuts modal opens with ? key', 2); // Close it await page.keyboard.press('Escape'); await sleep(300); const modalClosed = await page.evaluate(() => { const modal = document.querySelector('#shortcuts-modal'); return !modal || !modal.hasAttribute('open'); }); if (modalClosed) { log('pass', 'Shortcuts modal closes with Escape key', 2); } else { log('warn', 'Shortcuts modal may not close with Escape', 2); } } else { log('fail', 'Shortcuts modal did not open with ? key', 2); } } catch (error) { log('fail', `Test 7 error: ${error.message}`, 1); } } // ============================================================================== // GENERATE FINAL REPORT // ============================================================================== function generateReport() { log('section', '═══════════════════════════════════════════════════════'); log('section', 'FINAL TEST REPORT'); log('section', '═══════════════════════════════════════════════════════'); console.log('\n📊 SUMMARY:'); console.log(` ✅ Passed: ${RESULTS.passed.length}`); console.log(` ❌ Failed: ${RESULTS.failed.length}`); console.log(` ⚠️ Warnings: ${RESULTS.warnings.length}`); console.log(` 🔴 Errors: ${RESULTS.errors.length}`); if (RESULTS.failed.length > 0) { console.log('\n❌ FAILURES:'); RESULTS.failed.forEach((msg, i) => console.log(` ${i + 1}. ${msg}`)); } if (RESULTS.errors.length > 0) { console.log('\n🔴 ERRORS:'); RESULTS.errors.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}`)); } // Feature grades console.log('\n📈 FEATURE GRADES:'); const categories = { 'Hyperscript Functions': ['hyperscript', 'function', 'parse'], 'Toggle Functionality': ['toggle', 'CV length', 'icons', 'theme', 'localStorage'], 'Hover Sync': ['hover', 'sync', 'PDF', 'print', 'zoom control'], 'Zoom Control': ['zoom', 'slider', 'transform'], 'Scroll Behavior': ['scroll', 'header', 'back-to-top'], 'Fixed Positioning': ['at-bottom', 'fixed button'], 'Keyboard Shortcuts': ['keyboard', 'modal', 'Escape'] }; for (const [category, keywords] of Object.entries(categories)) { const categoryPassed = RESULTS.passed.filter(msg => keywords.some(kw => msg.toLowerCase().includes(kw.toLowerCase())) ).length; const categoryFailed = RESULTS.failed.filter(msg => keywords.some(kw => msg.toLowerCase().includes(kw.toLowerCase())) ).length; let grade = 'F'; if (categoryFailed === 0 && categoryPassed >= 3) grade = 'A'; else if (categoryFailed === 0 && categoryPassed >= 2) grade = 'B'; else if (categoryFailed <= 1) grade = 'C'; else if (categoryFailed <= 2) grade = 'D'; console.log(` ${category}: ${grade} (${categoryPassed} passed, ${categoryFailed} failed)`); } const overallSuccess = RESULTS.failed.length === 0 && RESULTS.errors.length === 0; console.log(`\n${overallSuccess ? '✅ ALL TESTS PASSED!' : '❌ SOME TESTS FAILED'}\n`); return overallSuccess; } // ============================================================================== // MAIN EXECUTION // ============================================================================== async function main() { console.log('🧪 COMPREHENSIVE CV SITE TEST SUITE'); console.log('Testing ALL features systematically\n'); const browser = await chromium.launch({ headless: false, // Keep browser open for inspection 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_HyperscriptFunctions(page); await test2_ToggleFunctionality(page); await test3_HoverSync(page); await test4_ZoomFunctionality(page); await test5_ScrollBehavior(page); await test6_FixedButtonPositioning(page); await test7_KeyboardShortcuts(page); // Generate report const success = generateReport(); // Keep browser open for inspection console.log('\n⏸️ Browser will remain open for manual inspection...'); console.log('Press Ctrl+C to close when done.\n'); // Don't close browser automatically // await browser.close(); // process.exit(success ? 0 : 1); } catch (error) { console.error('Fatal error:', error); await browser.close(); process.exit(1); } } main();