Files
cv-site/test-comprehensive.mjs
T
2025-11-17 08:34:50 +00:00

774 lines
27 KiB
JavaScript
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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();