2eafb78954
Fixed two critical mobile view issues: 1. Extended CV Sidebar Accordion: - Updated sidebar.html to use native <details> element (was div with onclick) - Styled accordion header to match CV title badges dark theme (#303030) - Applied consistent styling: dark gray background, light text, uppercase, no spacing - Result: Sidebars now collapse/expand properly with native HTML functionality 2. PDF Download Modal Centering: - Added JavaScript-based centering for mobile viewports (≤768px) - Uses inline styles with !important flag to override browser defaults - Updated download button to call openPdfModal() function - Result: Modal is perfectly centered on mobile (0px offset) Technical notes: - Modal centering required setProperty() with 'important' flag - Accordion matches cv-title-badges-header style exactly - All tests passing: accordion toggle, modal centering Files modified: - templates/partials/cv/sidebar.html - static/css/05-responsive/_breakpoints.css - static/js/main.js - templates/partials/widgets/download-button.html Tests added: - tests/mjs/43-mobile-accordion-and-modal-test.mjs - tests/mjs/46-visual-accordion-style-test.mjs
223 lines
7.5 KiB
JavaScript
Executable File
223 lines
7.5 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
/**
|
|
* Test: Footer Hover Programmatic (JavaScript Events)
|
|
*
|
|
* Tests footer hover interaction by programmatically triggering mouseenter/mouseleave events
|
|
* instead of using Playwright's hover (which is blocked by overlapping buttons)
|
|
*/
|
|
|
|
import { chromium } from 'playwright';
|
|
|
|
const TEST_URL = 'http://localhost:1999';
|
|
const VIEWPORT_WIDTH = 375; // Mobile width
|
|
const VIEWPORT_HEIGHT = 812; // iPhone X height
|
|
|
|
async function testFooterHoverProgrammatic() {
|
|
console.log('🧪 Testing Footer Hover with Programmatic Events');
|
|
console.log('='.repeat(70));
|
|
|
|
const browser = await chromium.launch({ headless: true });
|
|
const context = await browser.newContext({
|
|
viewport: { width: VIEWPORT_WIDTH, height: VIEWPORT_HEIGHT },
|
|
deviceScaleFactor: 2,
|
|
});
|
|
const page = await context.newPage();
|
|
|
|
// Disable cache
|
|
await page.route('**/*', (route) => {
|
|
route.continue({
|
|
headers: {
|
|
...route.request().headers(),
|
|
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
|
},
|
|
});
|
|
});
|
|
|
|
try {
|
|
await page.goto(TEST_URL, { waitUntil: 'networkidle' });
|
|
console.log(`✅ Navigated to ${TEST_URL}`);
|
|
await page.waitForTimeout(1500);
|
|
|
|
// Scroll to footer
|
|
await page.evaluate(() => {
|
|
window.scrollTo(0, document.body.scrollHeight);
|
|
});
|
|
await page.waitForTimeout(500);
|
|
|
|
const buttons = [
|
|
'.download-btn',
|
|
'.print-friendly-btn',
|
|
'.shortcuts-btn',
|
|
'.color-theme-switcher',
|
|
'.info-button',
|
|
'.back-to-top'
|
|
];
|
|
|
|
let allTestsPassed = true;
|
|
|
|
console.log('\n📊 Test 1: Check if footer-buttons-interaction.js is loaded');
|
|
console.log('-'.repeat(70));
|
|
|
|
const jsLoaded = await page.evaluate(() => {
|
|
const scripts = Array.from(document.querySelectorAll('script'));
|
|
return scripts.some(script => script.src.includes('footer-buttons-interaction.js'));
|
|
});
|
|
|
|
if (jsLoaded) {
|
|
console.log('✅ footer-buttons-interaction.js is loaded in the page');
|
|
} else {
|
|
console.log('❌ footer-buttons-interaction.js NOT found in the page');
|
|
allTestsPassed = false;
|
|
}
|
|
|
|
console.log('\n📊 Test 2: Check if footer element exists');
|
|
console.log('-'.repeat(70));
|
|
|
|
const footerExists = await page.evaluate(() => {
|
|
const footer = document.querySelector('footer.no-print');
|
|
return footer !== null;
|
|
});
|
|
|
|
if (footerExists) {
|
|
console.log('✅ Footer element (footer.no-print) found');
|
|
} else {
|
|
console.log('❌ Footer element (footer.no-print) NOT found');
|
|
allTestsPassed = false;
|
|
}
|
|
|
|
console.log('\n📊 Test 3: Programmatically trigger footer mouseenter');
|
|
console.log('-'.repeat(70));
|
|
|
|
// Trigger mouseenter event on footer
|
|
const mouseEnterResult = await page.evaluate(() => {
|
|
const footer = document.querySelector('footer.no-print');
|
|
if (!footer) return { success: false, error: 'Footer not found' };
|
|
|
|
const event = new MouseEvent('mouseenter', {
|
|
bubbles: true,
|
|
cancelable: true,
|
|
view: window
|
|
});
|
|
footer.dispatchEvent(event);
|
|
|
|
// Wait for event handlers to execute AND CSS transitions to complete (300ms)
|
|
return new Promise(resolve => {
|
|
setTimeout(() => {
|
|
const buttons = document.querySelectorAll(
|
|
'.download-btn, .print-friendly-btn, .shortcuts-btn, ' +
|
|
'.info-button, .back-to-top, .color-theme-switcher'
|
|
);
|
|
|
|
const results = {};
|
|
buttons.forEach(btn => {
|
|
const className = btn.className.split(' ').find(c => c.includes('btn') || c.includes('switcher'));
|
|
results[className] = {
|
|
hasClass: btn.classList.contains('footer-hovered'),
|
|
opacity: window.getComputedStyle(btn).opacity,
|
|
pointerEvents: window.getComputedStyle(btn).pointerEvents
|
|
};
|
|
});
|
|
|
|
resolve({ success: true, buttons: results });
|
|
}, 500); // Wait 500ms for 300ms transition to complete
|
|
});
|
|
});
|
|
|
|
if (mouseEnterResult.success) {
|
|
console.log('✅ Mouseenter event dispatched successfully');
|
|
console.log('\n Button states after footer mouseenter:');
|
|
|
|
for (const [btnName, data] of Object.entries(mouseEnterResult.buttons)) {
|
|
const opacityCorrect = Math.abs(parseFloat(data.opacity) - 0.2) < 0.05;
|
|
const pointerEventsCorrect = data.pointerEvents === 'none';
|
|
|
|
if (data.hasClass && opacityCorrect && pointerEventsCorrect) {
|
|
console.log(` ✅ ${btnName}: class=${data.hasClass}, opacity=${data.opacity}, pointerEvents=${data.pointerEvents}`);
|
|
} else {
|
|
console.log(` ❌ ${btnName}: class=${data.hasClass}, opacity=${data.opacity}, pointerEvents=${data.pointerEvents}`);
|
|
console.log(` Expected: class=true, opacity=~0.2, pointerEvents=none`);
|
|
allTestsPassed = false;
|
|
}
|
|
}
|
|
} else {
|
|
console.log(`❌ Failed to dispatch mouseenter: ${mouseEnterResult.error}`);
|
|
allTestsPassed = false;
|
|
}
|
|
|
|
console.log('\n📊 Test 4: Programmatically trigger footer mouseleave');
|
|
console.log('-'.repeat(70));
|
|
|
|
// Trigger mouseleave event on footer
|
|
const mouseLeaveResult = await page.evaluate(() => {
|
|
const footer = document.querySelector('footer.no-print');
|
|
if (!footer) return { success: false, error: 'Footer not found' };
|
|
|
|
const event = new MouseEvent('mouseleave', {
|
|
bubbles: true,
|
|
cancelable: true,
|
|
view: window
|
|
});
|
|
footer.dispatchEvent(event);
|
|
|
|
// Wait a moment for event handlers to execute
|
|
return new Promise(resolve => {
|
|
setTimeout(() => {
|
|
const buttons = document.querySelectorAll(
|
|
'.download-btn, .print-friendly-btn, .shortcuts-btn, ' +
|
|
'.info-button, .back-to-top, .color-theme-switcher'
|
|
);
|
|
|
|
const results = {};
|
|
buttons.forEach(btn => {
|
|
const className = btn.className.split(' ').find(c => c.includes('btn') || c.includes('switcher'));
|
|
results[className] = {
|
|
hasClass: btn.classList.contains('footer-hovered')
|
|
};
|
|
});
|
|
|
|
resolve({ success: true, buttons: results });
|
|
}, 500); // Wait 500ms for 300ms transition to complete
|
|
});
|
|
});
|
|
|
|
if (mouseLeaveResult.success) {
|
|
console.log('✅ Mouseleave event dispatched successfully');
|
|
console.log('\n Button states after footer mouseleave:');
|
|
|
|
for (const [btnName, data] of Object.entries(mouseLeaveResult.buttons)) {
|
|
if (!data.hasClass) {
|
|
console.log(` ✅ ${btnName}: footer-hovered class removed`);
|
|
} else {
|
|
console.log(` ❌ ${btnName}: footer-hovered class still present`);
|
|
allTestsPassed = false;
|
|
}
|
|
}
|
|
} else {
|
|
console.log(`❌ Failed to dispatch mouseleave: ${mouseLeaveResult.error}`);
|
|
allTestsPassed = false;
|
|
}
|
|
|
|
console.log('-'.repeat(70));
|
|
|
|
if (allTestsPassed) {
|
|
console.log('\n✅ ALL TESTS PASSED!');
|
|
console.log(' • JavaScript file loaded correctly');
|
|
console.log(' • Footer element exists');
|
|
console.log(' • Buttons become transparent (0.2) on footer hover');
|
|
console.log(' • Buttons restore normal state on footer leave');
|
|
} else {
|
|
console.log('\n❌ SOME TESTS FAILED - Check output above');
|
|
}
|
|
|
|
await browser.close();
|
|
process.exit(allTestsPassed ? 0 : 1);
|
|
|
|
} catch (error) {
|
|
console.error('\n❌ Test error:', error);
|
|
await browser.close();
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
testFooterHoverProgrammatic();
|