Files
cv-site/test-comprehensive.mjs
T

774 lines
27 KiB
JavaScript
Raw Normal View History

2025-11-17 08:34:50 +00:00
#!/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();