774 lines
27 KiB
JavaScript
Executable File
774 lines
27 KiB
JavaScript
Executable File
#!/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();
|