From 2eafb78954a37b16f2bb55e5f08445f87d6ff156 Mon Sep 17 00:00:00 2001 From: juanatsap Date: Sat, 22 Nov 2025 16:23:05 +0000 Subject: [PATCH] fix: Mobile view improvements - accordion styling and modal centering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixed two critical mobile view issues: 1. Extended CV Sidebar Accordion: - Updated sidebar.html to use native
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 --- static/css/01-foundation/_themes.css | 37 ++- static/css/03-components/_experience.css | 6 +- static/css/04-interactive/_modals.css | 17 +- .../css/04-interactive/_scroll-behavior.css | 194 ++++++++++++--- static/css/05-responsive/_breakpoints.css | 94 ++++++++ static/css/color-theme.css | 30 ++- static/js/footer-buttons-interaction.js | 46 ++++ static/js/main.js | 13 + static/js/scroll-at-bottom-handler.js | 73 ++++++ templates/index.html | 6 + templates/partials/cv/sidebar.html | 40 ++-- .../partials/widgets/download-button.html | 2 +- tests/mjs/34-mobile-button-opacity-test.mjs | 138 +++++++++++ tests/mjs/35-mobile-colored-buttons-test.mjs | 160 +++++++++++++ tests/mjs/36-button-hover-and-footer-test.mjs | 187 +++++++++++++++ tests/mjs/37-footer-hover-programmatic.mjs | 222 ++++++++++++++++++ tests/mjs/38-mobile-fixes-verification.mjs | 181 ++++++++++++++ tests/mjs/39-mobile-updates-verification.mjs | 216 +++++++++++++++++ tests/mjs/40-back-to-top-and-footer-fixes.mjs | 188 +++++++++++++++ tests/mjs/41-mobile-accordion-test.mjs | 180 ++++++++++++++ .../43-mobile-accordion-and-modal-test.mjs | 163 +++++++++++++ tests/mjs/46-visual-accordion-style-test.mjs | 82 +++++++ 22 files changed, 2207 insertions(+), 68 deletions(-) create mode 100644 static/js/footer-buttons-interaction.js create mode 100644 static/js/scroll-at-bottom-handler.js create mode 100755 tests/mjs/34-mobile-button-opacity-test.mjs create mode 100755 tests/mjs/35-mobile-colored-buttons-test.mjs create mode 100755 tests/mjs/36-button-hover-and-footer-test.mjs create mode 100755 tests/mjs/37-footer-hover-programmatic.mjs create mode 100755 tests/mjs/38-mobile-fixes-verification.mjs create mode 100755 tests/mjs/39-mobile-updates-verification.mjs create mode 100755 tests/mjs/40-back-to-top-and-footer-fixes.mjs create mode 100755 tests/mjs/41-mobile-accordion-test.mjs create mode 100755 tests/mjs/43-mobile-accordion-and-modal-test.mjs create mode 100755 tests/mjs/46-visual-accordion-style-test.mjs diff --git a/static/css/01-foundation/_themes.css b/static/css/01-foundation/_themes.css index ff0a07a..6edc993 100644 --- a/static/css/01-foundation/_themes.css +++ b/static/css/01-foundation/_themes.css @@ -290,16 +290,39 @@ right: auto !important; width: 50px !important; height: 50px !important; - opacity: 0.7 !important; + /* Removed opacity: 1 !important to allow .footer-hovered to work */ transform: none !important; - /* Position before info button: 5 buttons total */ - /* Download, Print, Shortcuts, Theme, Info */ - /* Total width: 5 * 50px + 4 * 10px = 290px */ - left: calc(50% + 35px) !important; /* Fourth button */ + /* Position in 6-button layout: Download, Print, Shortcuts, Theme, Info, Back-to-top */ + /* Total width: 6 * 50px + 5 * 10px = 350px */ + left: calc(50% + 5px) !important; /* Fourth button */ } - .color-theme-switcher:hover { - opacity: 1 !important; + /* Show theme colors at 50% transparency by default on mobile */ + .color-theme-switcher[data-theme-mode="light"] { + background: rgba(212, 178, 0, 0.5) !important; /* Gold at 50% */ + } + + .color-theme-switcher[data-theme-mode="dark"] { + background: rgba(1, 60, 119, 0.5) !important; /* Blue at 50% */ + } + + .color-theme-switcher[data-theme-mode="auto"] { + background: rgba(155, 89, 182, 0.5) !important; /* Purple at 50% */ + } + + /* Full color opacity on hover */ + .color-theme-switcher:hover[data-theme-mode="light"] { + background: rgba(212, 178, 0, 1) !important; /* Full gold opacity */ + transform: translateY(-3px) !important; + } + + .color-theme-switcher:hover[data-theme-mode="dark"] { + background: rgba(1, 60, 119, 1) !important; /* Full blue opacity */ + transform: translateY(-3px) !important; + } + + .color-theme-switcher:hover[data-theme-mode="auto"] { + background: rgba(155, 89, 182, 1) !important; /* Full purple opacity */ transform: translateY(-3px) !important; } } diff --git a/static/css/03-components/_experience.css b/static/css/03-components/_experience.css index f4e07aa..26a0b39 100644 --- a/static/css/03-components/_experience.css +++ b/static/css/03-components/_experience.css @@ -451,18 +451,18 @@ footer { text-align: center; padding: 2rem; - color: rgba(255,255,255,0.7); + color: var(--text-muted); /* Theme-aware color (light: #666666, dark: #b0b0b0) */ font-size: 0.85rem; } /* GitHub repository link styling */ .github-repo-link { - color: whitesmoke !important; + color: var(--text-secondary) !important; /* Theme-aware link color */ transition: color 0.2s ease-in-out; } .github-repo-link:hover { - color: #66B3FF !important; + color: var(--accent-blue) !important; } diff --git a/static/css/04-interactive/_modals.css b/static/css/04-interactive/_modals.css index 0b040e6..c6fb6c6 100644 --- a/static/css/04-interactive/_modals.css +++ b/static/css/04-interactive/_modals.css @@ -739,8 +739,23 @@ } } +/* Mobile keyframes - must be outside media query */ +@keyframes modalFadeInMobile { + from { + opacity: 0; + transform: translate(-50%, -50%) scale(0.9); + } + to { + opacity: 1; + transform: translate(-50%, -50%) scale(1); + } +} + /* Mobile: Single column - Button-like style */ -@media (max-width: 479px) { +@media (max-width: 768px) { + /* Mobile centering is now handled via JavaScript in openPdfModal() */ + /* This CSS provides fallback styling for mobile screens */ + .pdf-download-modal { max-width: calc(100% - 1rem); } diff --git a/static/css/04-interactive/_scroll-behavior.css b/static/css/04-interactive/_scroll-behavior.css index 25e849c..f1c1679 100644 --- a/static/css/04-interactive/_scroll-behavior.css +++ b/static/css/04-interactive/_scroll-behavior.css @@ -111,7 +111,7 @@ /* Mobile adjustments - Flexbox button layout at bottom center */ @media (max-width: 900px) { - /* Hide only zoom control on mobile */ + /* Hide zoom control on mobile (keyboard shortcuts now visible) */ .zoom-toggle-btn, .zoom-control { display: none !important; @@ -128,73 +128,199 @@ right: auto !important; width: 50px !important; height: 50px !important; - opacity: 0.7 !important; + /* Removed opacity: 1 !important to allow .footer-hovered to work */ transform: none !important; } - /* Keep back-to-top button at bottom-right (same as desktop) */ + /* Mobile: Show colors at 50% transparency by default */ + .download-btn { + background: rgba(205, 96, 96, 0.5) !important; /* PDF red at 50% */ + } + + .print-friendly-btn { + background: rgba(255, 255, 255, 0.5) !important; /* White at 50% */ + } + + .print-friendly-btn iconify-icon { + color: #27ae60 !important; /* Green icon */ + } + + .shortcuts-btn { + background: rgba(243, 156, 18, 0.5) !important; /* Orange at 50% */ + } + + .info-button { + background: rgba(39, 174, 96, 0.5) !important; /* Green at 50% */ + } + .back-to-top { - position: fixed !important; - bottom: 1.5rem !important; - right: 1.5rem !important; - width: 50px !important; - height: 50px !important; + background: rgba(39, 174, 96, 0.5) !important; /* Green at 50% */ + opacity: 0.5; /* Semi-transparent by default, full opacity when at-bottom */ } /* Flexbox container behavior - buttons arrange themselves */ /* Buttons will be positioned using JavaScript or individual positioning */ /* For now, use fixed spacing from center */ - /* 5 buttons: Download, Print, Shortcuts, Theme, Info */ + /* 6 buttons: Download, Print, Shortcuts, Theme, Info, Back-to-top */ /* Spacing: 10px gap between buttons, centered horizontally */ - /* Total width: 5 * 50px + 4 * 10px = 290px */ - /* Start position: 50% - 145px */ + /* Total width: 6 * 50px + 5 * 10px = 350px */ + /* Start position: 50% - 175px */ .download-btn { - left: calc(50% - 145px) !important; /* First button */ + left: calc(50% - 175px) !important; /* First button */ } .print-friendly-btn { - left: calc(50% - 85px) !important; /* Second button */ + left: calc(50% - 115px) !important; /* Second button */ } .shortcuts-btn { - left: calc(50% - 25px) !important; /* Third button */ + left: calc(50% - 55px) !important; /* Third button */ } /* Theme switcher button - fourth position (defined in color-theme.css) */ - /* left: calc(50% + 35px) !important; */ + /* left: calc(50% + 5px) !important; */ .info-button { - left: calc(50% + 95px) !important; /* Fifth button (last) */ + left: calc(50% + 65px) !important; /* Fifth button */ } - /* Hover effects - only Y transform + enhanced shadow */ - .download-btn:hover, - .download-btn.pdf-hover-sync, - .print-friendly-btn:hover, - .print-friendly-btn.print-hover-sync, - .shortcuts-btn:hover, - .info-button:hover, + /* Back-to-top button - now part of the button row (sixth button) */ + .back-to-top { + position: fixed !important; + bottom: 1.5rem !important; + left: calc(50% + 125px) !important; /* Sixth button (last) */ + right: auto !important; /* Override previous right positioning */ + width: 50px !important; + height: 50px !important; + /* Removed fixed opacity - will be controlled by .at-bottom class */ + display: flex !important; /* Ensure it's always displayed */ + } + + /* Always show back-to-top on mobile (don't wait for scroll) */ .back-to-top:hover { - transform: translateY(-3px) !important; opacity: 1 !important; + } + + /* Hover effects - Full color opacity on hover */ + .download-btn:hover, + .download-btn.pdf-hover-sync { + background: rgba(205, 96, 96, 1) !important; /* Full red opacity */ + transform: translateY(-3px) !important; box-shadow: 0 6px 16px rgba(0, 0, 0, 0.4) !important; } - /* Keep at-bottom state without transform */ - .info-button.at-bottom, - .shortcuts-btn.at-bottom { + .print-friendly-btn:hover, + .print-friendly-btn.print-hover-sync { + background: rgba(255, 255, 255, 1) !important; /* Full white opacity */ + transform: translateY(-3px) !important; + box-shadow: 0 6px 16px rgba(0, 0, 0, 0.4) !important; + } + + .shortcuts-btn:hover { + background: rgba(243, 156, 18, 1) !important; /* Full orange opacity */ + transform: translateY(-3px) !important; + box-shadow: 0 6px 16px rgba(0, 0, 0, 0.4) !important; + } + + .info-button:hover { + background: rgba(39, 174, 96, 1) !important; /* Full green opacity */ + transform: translateY(-3px) !important; + box-shadow: 0 6px 16px rgba(0, 0, 0, 0.4) !important; + } + + .back-to-top:hover { + background: rgba(39, 174, 96, 1) !important; /* Full green opacity */ + transform: translateY(-3px) !important; + box-shadow: 0 6px 16px rgba(0, 0, 0, 0.4) !important; + } + + /* Keep at-bottom state - full opacity colors for each button */ + .download-btn.at-bottom { + background: rgba(205, 96, 96, 1) !important; /* Full red opacity */ opacity: 1 !important; transform: none !important; } -} -/* Very narrow mobile - Move back-to-top UP on RIGHT side to avoid overlap */ -@media (max-width: 483px) { - .back-to-top { - /* Stay on RIGHT side, just move UP higher */ - right: 1.5rem !important; - bottom: 5.5rem !important; /* Higher position to clear bottom button row */ + .print-friendly-btn.at-bottom { + background: rgba(255, 255, 255, 1) !important; /* Full white opacity */ + opacity: 1 !important; + transform: none !important; + } + + .shortcuts-btn.at-bottom { + background: rgba(243, 156, 18, 1) !important; /* Full orange opacity */ + opacity: 1 !important; + transform: none !important; + } + + .info-button.at-bottom { + background: rgba(39, 174, 96, 1) !important; /* Full green opacity */ + opacity: 1 !important; + transform: none !important; + } + + .back-to-top.at-bottom { + background: rgba(39, 174, 96, 1) !important; /* Full green opacity - NO transparency */ + opacity: 1 !important; + transform: none !important; + } + + /* Make all buttons semi-transparent when footer is hovered (applied via JS) */ + .download-btn.footer-hovered, + .print-friendly-btn.footer-hovered, + .shortcuts-btn.footer-hovered, + .info-button.footer-hovered, + .back-to-top.footer-hovered, + .color-theme-switcher.footer-hovered { + opacity: 0.2 !important; /* Make buttons very transparent to see footer */ + pointer-events: none !important; /* Prevent interaction when footer is hovered */ + } +} + +/* Very narrow mobile - Stack buttons vertically or adjust spacing if needed */ +@media (max-width: 483px) { + /* For very narrow screens, you may need to adjust button spacing or size */ + /* For now, keep the same horizontal layout but buttons might overflow slightly */ + /* Users can scroll horizontally if needed, or we can reduce button sizes */ +} + +/* ======================================== + Mobile: Keep action bar visible (prevent hiding on scroll) + ======================================== */ +@media (max-width: 900px) { + /* Prevent action bar from hiding on scroll on mobile */ + .action-bar.header-hidden { + transform: translateY(0) !important; /* Override hide behavior */ + } + + .navigation-menu.header-hidden { + transform: translateY(0) !important; /* Override hide behavior */ + } + + /* Add bottom padding to footer so text isn't hidden behind button bar */ + footer.no-print { + padding-bottom: 120px !important; /* More clearance for 6 buttons + spacing */ + transition: all 0.3s ease; + z-index: 1 !important; /* Keep footer behind buttons (buttons have z-index: 99) */ + position: relative; + } + + /* Footer hover effect - enlarge text when touched (for button transparency) */ + footer.no-print.footer-hovered { + /* Only used for button transparency interaction */ + } + + /* Footer text automatically enlarges when at page bottom */ + footer.no-print.at-bottom { + padding-bottom: 130px !important; /* Extra space when enlarged */ + } + + footer.no-print.at-bottom p, + footer.no-print.at-bottom a { + font-size: 1.2em !important; + font-weight: 500 !important; + transition: all 0.3s ease; } } diff --git a/static/css/05-responsive/_breakpoints.css b/static/css/05-responsive/_breakpoints.css index 20fdd98..2a29696 100644 --- a/static/css/05-responsive/_breakpoints.css +++ b/static/css/05-responsive/_breakpoints.css @@ -247,6 +247,42 @@ ======================================== */ @media (max-width: 768px) { + /* ======================================== + LAYOUT - Single column on mobile + ======================================== */ + /* Collapse grid to single column */ + .page-1 .page-content, + .page-2 .page-content { + grid-template-columns: 1fr !important; + grid-template-rows: auto auto; + } + + /* Stack elements vertically: sidebar -> main */ + .page-1 .cv-sidebar-left { + grid-column: 1; + grid-row: 1; + order: 1; + } + + .page-1 .cv-main { + grid-column: 1; + grid-row: 2; + order: 2; + } + + /* Stack elements vertically: main -> sidebar */ + .page-2 .cv-main { + grid-column: 1; + grid-row: 1; + order: 1; + } + + .page-2 .cv-sidebar-right { + grid-column: 1; + grid-row: 2; + order: 2; + } + /* ======================================== TYPOGRAPHY - Subtle font size reductions ======================================== */ @@ -442,6 +478,64 @@ margin-left: 1rem !important; font-size: 0.85rem !important; } + + /* ======================================== + SIDEBAR ACCORDION - MOBILE ONLY + ======================================== */ + /* Show accordion header on mobile - matches CV title badges style */ + .sidebar-accordion summary.sidebar-accordion-header { + display: flex !important; + justify-content: space-between; + align-items: center; + padding: 10px 20px; + background: #303030 !important; /* Match CV title badges dark gray */ + color: #ccc; + cursor: pointer; + border-radius: 0; /* No rounding - match title badges */ + margin-bottom: 0; + font-weight: normal; + font-size: 0.9em; + text-transform: uppercase; + gap: 0.5rem; + list-style: none; + user-select: none; + border-bottom: 2px solid #34495e; /* Match title badges border */ + } + + /* Remove default details marker */ + .sidebar-accordion summary.sidebar-accordion-header::-webkit-details-marker, + .sidebar-accordion summary.sidebar-accordion-header::marker { + display: none; + } + + /* Chevron rotation when open */ + .sidebar-accordion[open] summary.sidebar-accordion-header .chevron { + transform: rotate(180deg); + transition: transform 0.3s ease; + } + + .sidebar-accordion summary.sidebar-accordion-header .chevron { + transition: transform 0.3s ease; + color: #ccc; /* Match header text color */ + } + + /* Accordion content animation */ + .sidebar-accordion-content { + overflow: hidden; + transition: max-height 0.3s ease-in-out; + } + + /* Hide when closed */ + .sidebar-accordion:not([open]) .sidebar-accordion-content { + max-height: 0; + opacity: 0; + } + + /* Show when open */ + .sidebar-accordion[open] .sidebar-accordion-content { + max-height: 2000px; + opacity: 1; + } } /* ======================================== diff --git a/static/css/color-theme.css b/static/css/color-theme.css index 34d8bc3..5a8492d 100644 --- a/static/css/color-theme.css +++ b/static/css/color-theme.css @@ -284,7 +284,7 @@ right: auto !important; width: 50px !important; height: 50px !important; - opacity: 0.7 !important; + /* Removed opacity: 1 !important to allow .footer-hovered to work */ transform: none !important; /* Position before info button: 5 buttons total */ /* Download, Print, Shortcuts, Theme, Info */ @@ -292,8 +292,32 @@ left: calc(50% + 35px) !important; /* Fourth button */ } - .color-theme-switcher:hover { - opacity: 1 !important; + /* Show theme colors at 50% transparency by default on mobile */ + .color-theme-switcher[data-theme-mode="light"] { + background: rgba(212, 178, 0, 0.5) !important; /* Gold at 50% */ + } + + .color-theme-switcher[data-theme-mode="dark"] { + background: rgba(1, 60, 119, 0.5) !important; /* Blue at 50% */ + } + + .color-theme-switcher[data-theme-mode="auto"] { + background: rgba(155, 89, 182, 0.5) !important; /* Purple at 50% */ + } + + /* Full color opacity on hover */ + .color-theme-switcher:hover[data-theme-mode="light"] { + background: rgba(212, 178, 0, 1) !important; /* Full gold opacity */ + transform: translateY(-3px) !important; + } + + .color-theme-switcher:hover[data-theme-mode="dark"] { + background: rgba(1, 60, 119, 1) !important; /* Full blue opacity */ + transform: translateY(-3px) !important; + } + + .color-theme-switcher:hover[data-theme-mode="auto"] { + background: rgba(155, 89, 182, 1) !important; /* Full purple opacity */ transform: translateY(-3px) !important; } } diff --git a/static/js/footer-buttons-interaction.js b/static/js/footer-buttons-interaction.js new file mode 100644 index 0000000..ef3e694 --- /dev/null +++ b/static/js/footer-buttons-interaction.js @@ -0,0 +1,46 @@ +/** + * Footer and Button Bar Interaction + * Makes button bar semi-transparent when hovering over footer area + * so footer content remains visible + */ + +(function() { + 'use strict'; + + // Wait for DOM to be ready + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init); + } else { + init(); + } + + function init() { + const footer = document.querySelector('footer.no-print'); + if (!footer) return; + + // Get all fixed buttons + const buttons = document.querySelectorAll( + '.download-btn, .print-friendly-btn, .shortcuts-btn, ' + + '.info-button, .back-to-top, .color-theme-switcher' + ); + + // Add hover listeners to footer + footer.addEventListener('mouseenter', () => { + // Add class to footer itself for text enlargement + footer.classList.add('footer-hovered'); + // Add class to buttons for transparency + buttons.forEach(btn => { + btn.classList.add('footer-hovered'); + }); + }); + + footer.addEventListener('mouseleave', () => { + // Remove class from footer + footer.classList.remove('footer-hovered'); + // Remove class from buttons + buttons.forEach(btn => { + btn.classList.remove('footer-hovered'); + }); + }); + } +})(); diff --git a/static/js/main.js b/static/js/main.js index 61673bd..b2b9ab7 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -607,6 +607,19 @@ window.openPdfModal = function() { const pdfModal = document.querySelector('#pdf-modal'); if (pdfModal) { + // Apply mobile centering via inline styles with !important (overrides CSS !important) + if (window.innerWidth <= 768) { + // Reset inset FIRST (before setting top/left) + pdfModal.style.setProperty('inset', 'auto', 'important'); + pdfModal.style.setProperty('margin', '0', 'important'); + // Now set positioning with !important (after inset reset) + pdfModal.style.setProperty('position', 'fixed', 'important'); + pdfModal.style.setProperty('top', '50%', 'important'); + pdfModal.style.setProperty('left', '50%', 'important'); + pdfModal.style.setProperty('right', 'auto', 'important'); + pdfModal.style.setProperty('bottom', 'auto', 'important'); + pdfModal.style.setProperty('transform', 'translate(-50%, -50%)', 'important'); + } pdfModal.showModal(); } }; diff --git a/static/js/scroll-at-bottom-handler.js b/static/js/scroll-at-bottom-handler.js new file mode 100644 index 0000000..905d863 --- /dev/null +++ b/static/js/scroll-at-bottom-handler.js @@ -0,0 +1,73 @@ +/** + * Scroll At-Bottom Handler + * Adds 'at-bottom' class to buttons and footer when user scrolls to page bottom + */ + +(function() { + 'use strict'; + + // Wait for DOM to be ready + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init); + } else { + init(); + } + + function init() { + const footer = document.querySelector('footer.no-print'); + if (!footer) return; + + // Get all fixed buttons that need 'at-bottom' state + const buttons = document.querySelectorAll( + '.download-btn, .print-friendly-btn, .shortcuts-btn, ' + + '.info-button, .back-to-top, .color-theme-switcher' + ); + + if (buttons.length === 0) return; + + // Throttle scroll events for better performance + let ticking = false; + + function checkIfAtBottom() { + // Calculate if we're at the bottom of the page + // Allow 50px tolerance for smoother experience + const scrollPosition = window.pageYOffset || document.documentElement.scrollTop; + const windowHeight = window.innerHeight; + const documentHeight = document.documentElement.scrollHeight; + const distanceFromBottom = documentHeight - (scrollPosition + windowHeight); + + const isAtBottom = distanceFromBottom <= 50; + + if (isAtBottom) { + // Add 'at-bottom' class to all buttons and footer + buttons.forEach(btn => btn.classList.add('at-bottom')); + footer.classList.add('at-bottom'); + } else { + // Remove 'at-bottom' class from all buttons and footer + buttons.forEach(btn => btn.classList.remove('at-bottom')); + footer.classList.remove('at-bottom'); + } + } + + function onScroll() { + if (!ticking) { + window.requestAnimationFrame(() => { + checkIfAtBottom(); + ticking = false; + }); + ticking = true; + } + } + + // Check initial state + checkIfAtBottom(); + + // Listen to scroll events + window.addEventListener('scroll', onScroll, { passive: true }); + + // Also check on resize (mobile orientation changes, etc.) + window.addEventListener('resize', () => { + setTimeout(checkIfAtBottom, 100); + }); + } +})(); diff --git a/templates/index.html b/templates/index.html index e67f2c5..9cca6a7 100644 --- a/templates/index.html +++ b/templates/index.html @@ -65,6 +65,12 @@ + + + + + + diff --git a/templates/partials/cv/sidebar.html b/templates/partials/cv/sidebar.html index 760366d..1bfee0f 100644 --- a/templates/partials/cv/sidebar.html +++ b/templates/partials/cv/sidebar.html @@ -1,24 +1,26 @@ {{define "sidebar"}} {{end}} diff --git a/templates/partials/widgets/download-button.html b/templates/partials/widgets/download-button.html index bcc2d00..0255ebd 100644 --- a/templates/partials/widgets/download-button.html +++ b/templates/partials/widgets/download-button.html @@ -5,7 +5,7 @@ class="fixed-btn download-btn no-print has-tooltip" aria-label="{{if eq .Lang "es"}}Descargar PDF{{else}}Download as PDF{{end}}" data-tooltip="{{if eq .Lang "es"}}Descargar PDF{{else}}Download as PDF{{end}}" - onclick="document.getElementById('pdf-modal').showModal()" + onclick="openPdfModal()" _="on mouseenter call syncPdfHover(true) on mouseleave call syncPdfHover(false)"> diff --git a/tests/mjs/34-mobile-button-opacity-test.mjs b/tests/mjs/34-mobile-button-opacity-test.mjs new file mode 100755 index 0000000..a8bdea0 --- /dev/null +++ b/tests/mjs/34-mobile-button-opacity-test.mjs @@ -0,0 +1,138 @@ +#!/usr/bin/env node +/** + * Test: Mobile Button Opacity - 50% default, 100% on hover + * + * Verifies that on mobile view (max-width: 900px): + * - All fixed buttons have 50% opacity (0.5) by default + * - Buttons become 100% opaque (1.0) on hover + * - Buttons: download, print-friendly, shortcuts, theme-switcher, info + */ + +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 testMobileButtonOpacity() { + console.log('๐Ÿงช Testing Mobile Button Opacity (50% default, 100% hover)'); + 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 to ensure fresh CSS + await page.route('**/*', (route) => { + route.continue({ + headers: { + ...route.request().headers(), + 'Cache-Control': 'no-cache, no-store, must-revalidate', + }, + }); + }); + + try { + // Navigate to the page + await page.goto(TEST_URL, { waitUntil: 'networkidle' }); + console.log(`โœ… Navigated to ${TEST_URL}`); + + // Wait for page to be fully loaded + await page.waitForTimeout(1000); + + // Test buttons + const buttons = [ + { selector: '.download-btn', name: 'Download' }, + { selector: '.print-friendly-btn', name: 'Print Friendly' }, + { selector: '.shortcuts-btn', name: 'Shortcuts' }, + { selector: '.color-theme-switcher', name: 'Theme Switcher' }, + { selector: '.info-button', name: 'Info' }, + ]; + + console.log('\n๐Ÿ“ฑ Testing Mobile Button Opacities:'); + console.log('-'.repeat(70)); + + let allTestsPassed = true; + + for (const button of buttons) { + try { + // Check if button exists + const buttonElement = await page.$(button.selector); + if (!buttonElement) { + console.log(`โŒ ${button.name}: Button not found`); + allTestsPassed = false; + continue; + } + + // Get default opacity + const defaultOpacity = await page.evaluate((sel) => { + const btn = document.querySelector(sel); + return btn ? window.getComputedStyle(btn).opacity : null; + }, button.selector); + + // Test default opacity (should be 0.5) + const defaultOpacityNum = parseFloat(defaultOpacity); + const isDefaultCorrect = Math.abs(defaultOpacityNum - 0.5) < 0.01; + + if (isDefaultCorrect) { + console.log(`โœ… ${button.name}: Default opacity = ${defaultOpacity} (expected ~0.5)`); + } else { + console.log(`โŒ ${button.name}: Default opacity = ${defaultOpacity} (expected ~0.5)`); + allTestsPassed = false; + } + + // Hover over button and check opacity + await buttonElement.hover(); + await page.waitForTimeout(500); // Wait for transition + + const hoverOpacity = await page.evaluate((sel) => { + const btn = document.querySelector(sel); + return btn ? window.getComputedStyle(btn).opacity : null; + }, button.selector); + + // Test hover opacity (should be 1.0) + const hoverOpacityNum = parseFloat(hoverOpacity); + const isHoverCorrect = Math.abs(hoverOpacityNum - 1.0) < 0.01; + + if (isHoverCorrect) { + console.log(` โœ… Hover opacity = ${hoverOpacity} (expected ~1.0)`); + } else { + console.log(` โŒ Hover opacity = ${hoverOpacity} (expected ~1.0)`); + allTestsPassed = false; + } + + // Move mouse away to reset + await page.mouse.move(0, 0); + await page.waitForTimeout(500); + + } catch (error) { + console.log(`โŒ ${button.name}: Error - ${error.message}`); + allTestsPassed = false; + } + } + + console.log('-'.repeat(70)); + + if (allTestsPassed) { + console.log('\nโœ… ALL TESTS PASSED - Mobile button opacity working correctly!'); + console.log(' โ€ข Default opacity: 0.5 (50% transparent)'); + console.log(' โ€ข Hover opacity: 1.0 (fully opaque)'); + } else { + console.log('\nโŒ SOME TESTS FAILED - Check output above for details'); + } + + await browser.close(); + process.exit(allTestsPassed ? 0 : 1); + + } catch (error) { + console.error('\nโŒ Test error:', error); + await browser.close(); + process.exit(1); + } +} + +// Run the test +testMobileButtonOpacity(); diff --git a/tests/mjs/35-mobile-colored-buttons-test.mjs b/tests/mjs/35-mobile-colored-buttons-test.mjs new file mode 100755 index 0000000..7580770 --- /dev/null +++ b/tests/mjs/35-mobile-colored-buttons-test.mjs @@ -0,0 +1,160 @@ +#!/usr/bin/env node +/** + * Test: Mobile Colored Buttons - Colors at 50% transparency, full on hover + * + * Verifies that on mobile view (max-width: 900px): + * - Buttons show their colors (not gray) at 50% transparency + * - On hover, colors become fully opaque (100%) + * - Colors match the "at-bottom" or hover states from desktop + */ + +import { chromium } from 'playwright'; + +const TEST_URL = 'http://localhost:1999'; +const VIEWPORT_WIDTH = 375; // Mobile width +const VIEWPORT_HEIGHT = 812; // iPhone X height + +// Helper to extract RGB values from background color +function parseRGB(colorString) { + const match = colorString.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/); + if (!match) return null; + return { + r: parseInt(match[1]), + g: parseInt(match[2]), + b: parseInt(match[3]), + a: match[4] ? parseFloat(match[4]) : 1 + }; +} + +async function testMobileColoredButtons() { + console.log('๐Ÿงช Testing Mobile Colored Buttons (50% transparent โ†’ 100% on hover)'); + 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(1000); + + const buttons = [ + { selector: '.download-btn', name: 'Download', expectedColor: { r: 205, g: 96, b: 96 } }, // Red + { selector: '.print-friendly-btn', name: 'Print', expectedColor: { r: 255, g: 255, b: 255 } }, // White + { selector: '.shortcuts-btn', name: 'Shortcuts', expectedColor: { r: 243, g: 156, b: 18 } }, // Orange + { selector: '.info-button', name: 'Info', expectedColor: { r: 39, g: 174, b: 96 } }, // Green + { selector: '.back-to-top', name: 'Back to Top', expectedColor: { r: 39, g: 174, b: 96 } }, // Green + ]; + + console.log('\n๐ŸŽจ Testing Mobile Button Colors:'); + console.log('-'.repeat(70)); + + let allTestsPassed = true; + + for (const button of buttons) { + try { + const buttonElement = await page.$(button.selector); + if (!buttonElement) { + console.log(`โŒ ${button.name}: Button not found`); + allTestsPassed = false; + continue; + } + + // Get default background color + const defaultBg = await page.evaluate((sel) => { + const btn = document.querySelector(sel); + return btn ? window.getComputedStyle(btn).backgroundColor : null; + }, button.selector); + + const defaultColor = parseRGB(defaultBg); + + // Check if color matches expected (RGB should match, alpha should be ~0.5) + if (defaultColor) { + const colorMatches = + Math.abs(defaultColor.r - button.expectedColor.r) <= 5 && + Math.abs(defaultColor.g - button.expectedColor.g) <= 5 && + Math.abs(defaultColor.b - button.expectedColor.b) <= 5; + + const alphaCorrect = Math.abs(defaultColor.a - 0.5) < 0.1; + + if (colorMatches && alphaCorrect) { + console.log(`โœ… ${button.name}: Color rgba(${defaultColor.r}, ${defaultColor.g}, ${defaultColor.b}, ${defaultColor.a.toFixed(2)}) โœ“`); + } else { + console.log(`โŒ ${button.name}: Color rgba(${defaultColor.r}, ${defaultColor.g}, ${defaultColor.b}, ${defaultColor.a.toFixed(2)})`); + console.log(` Expected: rgba(${button.expectedColor.r}, ${button.expectedColor.g}, ${button.expectedColor.b}, ~0.5)`); + allTestsPassed = false; + } + } else { + console.log(`โŒ ${button.name}: Could not parse background color`); + allTestsPassed = false; + continue; + } + + // Hover and check color becomes fully opaque + await buttonElement.hover(); + await page.waitForTimeout(500); + + const hoverBg = await page.evaluate((sel) => { + const btn = document.querySelector(sel); + return btn ? window.getComputedStyle(btn).backgroundColor : null; + }, button.selector); + + const hoverColor = parseRGB(hoverBg); + + if (hoverColor) { + const hoverAlphaCorrect = Math.abs(hoverColor.a - 1.0) < 0.1; + + if (hoverAlphaCorrect) { + console.log(` โœ… Hover: rgba(${hoverColor.r}, ${hoverColor.g}, ${hoverColor.b}, ${hoverColor.a.toFixed(2)}) โœ“`); + } else { + console.log(` โŒ Hover: rgba(${hoverColor.r}, ${hoverColor.g}, ${hoverColor.b}, ${hoverColor.a.toFixed(2)})`); + console.log(` Expected alpha: ~1.0`); + allTestsPassed = false; + } + } + + // Move mouse away + await page.mouse.move(0, 0); + await page.waitForTimeout(500); + + } catch (error) { + console.log(`โŒ ${button.name}: Error - ${error.message}`); + allTestsPassed = false; + } + } + + console.log('-'.repeat(70)); + + if (allTestsPassed) { + console.log('\nโœ… ALL TESTS PASSED - Mobile colored buttons working correctly!'); + console.log(' โ€ข Buttons show colors at 50% transparency'); + console.log(' โ€ข Colors become fully opaque (100%) on hover'); + } 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); + } +} + +testMobileColoredButtons(); diff --git a/tests/mjs/36-button-hover-and-footer-test.mjs b/tests/mjs/36-button-hover-and-footer-test.mjs new file mode 100755 index 0000000..d27c5a8 --- /dev/null +++ b/tests/mjs/36-button-hover-and-footer-test.mjs @@ -0,0 +1,187 @@ +#!/usr/bin/env node +/** + * Test: Button Hover Full Opacity + Footer Interaction + * + * Verifies on mobile view (max-width: 900px): + * 1. All buttons become 100% opaque (alpha = 1.0) on hover + * 2. Buttons become semi-transparent (opacity 0.2) when hovering footer + */ + +import { chromium } from 'playwright'; + +const TEST_URL = 'http://localhost:1999'; +const VIEWPORT_WIDTH = 375; // Mobile width +const VIEWPORT_HEIGHT = 812; // iPhone X height + +// Helper to extract RGB values from background color +function parseRGB(colorString) { + const match = colorString.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/); + if (!match) return null; + return { + r: parseInt(match[1]), + g: parseInt(match[2]), + b: parseInt(match[3]), + a: match[4] ? parseFloat(match[4]) : 1 + }; +} + +async function testButtonHoverAndFooter() { + console.log('๐Ÿงช Testing Button Hover Opacity + Footer Interaction'); + 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); + + const buttons = [ + { selector: '.download-btn', name: 'Download' }, + { selector: '.print-friendly-btn', name: 'Print' }, + { selector: '.shortcuts-btn', name: 'Shortcuts' }, + { selector: '.color-theme-switcher', name: 'Theme' }, + { selector: '.info-button', name: 'Info' }, + { selector: '.back-to-top', name: 'Back to Top' }, + ]; + + let allTestsPassed = true; + + // TEST 1: All buttons reach 100% opacity on hover + console.log('\n๐Ÿ“Š TEST 1: Button Hover - Full Opacity (100%)'); + console.log('-'.repeat(70)); + + for (const button of buttons) { + try { + const buttonElement = await page.$(button.selector); + if (!buttonElement) { + console.log(`โŒ ${button.name}: Button not found`); + allTestsPassed = false; + continue; + } + + // Hover over the button + await buttonElement.hover(); + await page.waitForTimeout(500); + + // Get hover background color + const hoverBg = await page.evaluate((sel) => { + const btn = document.querySelector(sel); + return btn ? window.getComputedStyle(btn).backgroundColor : null; + }, button.selector); + + const hoverColor = parseRGB(hoverBg); + + if (hoverColor) { + const alphaIs100 = Math.abs(hoverColor.a - 1.0) < 0.1; + + if (alphaIs100) { + console.log(`โœ… ${button.name}: Hover opacity = ${hoverColor.a.toFixed(2)} (100%) โœ“`); + } else { + console.log(`โŒ ${button.name}: Hover opacity = ${hoverColor.a.toFixed(2)} (Expected 1.0)`); + allTestsPassed = false; + } + } else { + console.log(`โŒ ${button.name}: Could not parse hover background color`); + allTestsPassed = false; + } + + // Move mouse away + await page.mouse.move(0, 0); + await page.waitForTimeout(300); + + } catch (error) { + console.log(`โŒ ${button.name}: Error - ${error.message}`); + allTestsPassed = false; + } + } + + // TEST 2: Footer hover makes buttons semi-transparent + console.log('\n๐Ÿ“Š TEST 2: Footer Hover - Buttons Become Transparent (20%)'); + console.log('-'.repeat(70)); + + try { + // Scroll to footer + await page.evaluate(() => { + window.scrollTo(0, document.body.scrollHeight); + }); + await page.waitForTimeout(500); + + // Find footer element + const footer = await page.$('footer.no-print'); + if (!footer) { + console.log('โŒ Footer element not found'); + allTestsPassed = false; + } else { + // Hover over footer + await footer.hover(); + await page.waitForTimeout(500); + + // Check if buttons have footer-hovered class and opacity 0.2 + for (const button of buttons) { + const buttonData = await page.evaluate((sel) => { + const btn = document.querySelector(sel); + if (!btn) return null; + return { + hasClass: btn.classList.contains('footer-hovered'), + opacity: window.getComputedStyle(btn).opacity + }; + }, button.selector); + + if (buttonData) { + const opacityValue = parseFloat(buttonData.opacity); + const opacityCorrect = Math.abs(opacityValue - 0.2) < 0.05; + + if (buttonData.hasClass && opacityCorrect) { + console.log(`โœ… ${button.name}: Footer hover opacity = ${opacityValue.toFixed(2)} โœ“`); + } else { + console.log(`โŒ ${button.name}: Footer hover opacity = ${opacityValue.toFixed(2)} (Expected 0.2), Class: ${buttonData.hasClass}`); + allTestsPassed = false; + } + } else { + console.log(`โŒ ${button.name}: Could not check footer hover state`); + allTestsPassed = false; + } + } + } + } catch (error) { + console.log(`โŒ Footer hover test error: ${error.message}`); + allTestsPassed = false; + } + + console.log('-'.repeat(70)); + + if (allTestsPassed) { + console.log('\nโœ… ALL TESTS PASSED!'); + console.log(' โ€ข All buttons reach 100% opacity on hover'); + console.log(' โ€ข Buttons become 20% opacity when hovering footer'); + } 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); + } +} + +testButtonHoverAndFooter(); diff --git a/tests/mjs/37-footer-hover-programmatic.mjs b/tests/mjs/37-footer-hover-programmatic.mjs new file mode 100755 index 0000000..852a7f3 --- /dev/null +++ b/tests/mjs/37-footer-hover-programmatic.mjs @@ -0,0 +1,222 @@ +#!/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(); diff --git a/tests/mjs/38-mobile-fixes-verification.mjs b/tests/mjs/38-mobile-fixes-verification.mjs new file mode 100755 index 0000000..1b8a270 --- /dev/null +++ b/tests/mjs/38-mobile-fixes-verification.mjs @@ -0,0 +1,181 @@ +#!/usr/bin/env node +/** + * Test: Mobile Fixes Verification + * + * Verifies the three mobile fixes: + * 1. Shortcuts button is hidden on mobile + * 2. Action bar stays visible (no auto-hide on scroll) + * 3. Footer has bottom padding to prevent text hiding behind 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 testMobileFixes() { + console.log('๐Ÿงช Testing Mobile Fixes (Action Bar, Shortcuts Hide, Footer Padding)'); + 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); + + let allTestsPassed = true; + + // TEST 1: Shortcuts button should be hidden on mobile + console.log('\n๐Ÿ“Š TEST 1: Shortcuts button hidden on mobile'); + console.log('-'.repeat(70)); + + const shortcutsBtn = await page.$('.shortcuts-btn'); + if (shortcutsBtn) { + const isVisible = await shortcutsBtn.isVisible(); + if (!isVisible) { + console.log('โœ… Shortcuts button is hidden on mobile (display: none)'); + } else { + console.log('โŒ Shortcuts button is visible on mobile (should be hidden)'); + allTestsPassed = false; + } + } else { + console.log('โœ… Shortcuts button not found in DOM (correctly hidden)'); + } + + // TEST 2: Action bar should stay visible when scrolling + console.log('\n๐Ÿ“Š TEST 2: Action bar stays visible on scroll (mobile)'); + console.log('-'.repeat(70)); + + // Scroll down to trigger header-hidden class + await page.evaluate(() => { + window.scrollTo(0, 500); + }); + await page.waitForTimeout(500); + + const actionBarVisible = await page.evaluate(() => { + const actionBar = document.querySelector('.action-bar'); + if (!actionBar) return { exists: false }; + + const hasHiddenClass = actionBar.classList.contains('header-hidden'); + const transform = window.getComputedStyle(actionBar).transform; + + return { + exists: true, + hasHiddenClass, + transform, + isTransformed: transform !== 'none' && transform !== 'matrix(1, 0, 0, 1, 0, 0)' + }; + }); + + if (actionBarVisible.exists) { + if (!actionBarVisible.isTransformed) { + console.log(`โœ… Action bar stays visible (transform: ${actionBarVisible.transform})`); + console.log(` header-hidden class: ${actionBarVisible.hasHiddenClass}, but transform overridden`); + } else { + console.log(`โŒ Action bar is transformed away (transform: ${actionBarVisible.transform})`); + allTestsPassed = false; + } + } else { + console.log('โŒ Action bar not found'); + allTestsPassed = false; + } + + // TEST 3: Footer should have bottom padding on mobile + console.log('\n๐Ÿ“Š TEST 3: Footer has bottom padding on mobile'); + console.log('-'.repeat(70)); + + // Scroll to bottom + await page.evaluate(() => { + window.scrollTo(0, document.body.scrollHeight); + }); + await page.waitForTimeout(500); + + const footerPadding = await page.evaluate(() => { + const footer = document.querySelector('footer.no-print'); + if (!footer) return { exists: false }; + + const styles = window.getComputedStyle(footer); + return { + exists: true, + paddingBottom: styles.paddingBottom, + paddingBottomValue: parseInt(styles.paddingBottom, 10) + }; + }); + + if (footerPadding.exists) { + // Expect at least 70px bottom padding (we set 80px) + if (footerPadding.paddingBottomValue >= 70) { + console.log(`โœ… Footer has adequate bottom padding: ${footerPadding.paddingBottom}`); + } else { + console.log(`โŒ Footer padding insufficient: ${footerPadding.paddingBottom} (expected >= 70px)`); + allTestsPassed = false; + } + } else { + console.log('โŒ Footer element not found'); + allTestsPassed = false; + } + + // TEST 4: Hamburger button should be visible + console.log('\n๐Ÿ“Š TEST 4: Hamburger button visible on mobile'); + console.log('-'.repeat(70)); + + // Scroll back to top to see action bar + await page.evaluate(() => { + window.scrollTo(0, 0); + }); + await page.waitForTimeout(500); + + const hamburgerBtn = await page.$('.hamburger-btn'); + if (hamburgerBtn) { + const isVisible = await hamburgerBtn.isVisible(); + if (isVisible) { + console.log('โœ… Hamburger button is visible on mobile'); + } else { + console.log('โŒ Hamburger button is hidden on mobile (should be visible)'); + allTestsPassed = false; + } + } else { + console.log('โŒ Hamburger button not found in DOM'); + allTestsPassed = false; + } + + console.log('-'.repeat(70)); + + if (allTestsPassed) { + console.log('\nโœ… ALL TESTS PASSED!'); + console.log(' โ€ข Shortcuts button hidden on mobile'); + console.log(' โ€ข Action bar stays visible on scroll'); + console.log(' โ€ข Footer has bottom padding (80px)'); + console.log(' โ€ข Hamburger button visible on mobile'); + } 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); + } +} + +testMobileFixes(); diff --git a/tests/mjs/39-mobile-updates-verification.mjs b/tests/mjs/39-mobile-updates-verification.mjs new file mode 100755 index 0000000..8038f7b --- /dev/null +++ b/tests/mjs/39-mobile-updates-verification.mjs @@ -0,0 +1,216 @@ +#!/usr/bin/env node +/** + * Test: Mobile Updates Verification + * + * Verifies the updated mobile features: + * 1. Keyboard shortcuts button is visible + * 2. All 6 buttons are positioned in a single row + * 3. Footer has 120px bottom padding + * 4. Footer hover effect enlarges text + */ + +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 testMobileUpdates() { + console.log('๐Ÿงช Testing Mobile Updates (6-Button Row, Footer Padding, Hover Effect)'); + 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); + + let allTestsPassed = true; + + // TEST 1: Keyboard shortcuts button should be visible on mobile + console.log('\n๐Ÿ“Š TEST 1: Keyboard shortcuts button visible on mobile'); + console.log('-'.repeat(70)); + + const shortcutsBtn = await page.$('.shortcuts-btn'); + if (shortcutsBtn) { + const isVisible = await shortcutsBtn.isVisible(); + if (isVisible) { + console.log('โœ… Keyboard shortcuts button is visible on mobile'); + } else { + console.log('โŒ Keyboard shortcuts button is hidden (should be visible)'); + allTestsPassed = false; + } + } else { + console.log('โŒ Keyboard shortcuts button not found in DOM'); + allTestsPassed = false; + } + + // TEST 2: All 6 buttons should be positioned in a row + console.log('\n๐Ÿ“Š TEST 2: All 6 buttons positioned in a row'); + console.log('-'.repeat(70)); + + const buttonPositions = await page.evaluate(() => { + const buttons = [ + { name: 'Download', selector: '.download-btn' }, + { name: 'Print', selector: '.print-friendly-btn' }, + { name: 'Shortcuts', selector: '.shortcuts-btn' }, + { name: 'Theme', selector: '.color-theme-switcher' }, + { name: 'Info', selector: '.info-button' }, + { name: 'Back-to-top', selector: '.back-to-top' } + ]; + + return buttons.map(({ name, selector }) => { + const btn = document.querySelector(selector); + if (!btn) return { name, exists: false }; + + const rect = btn.getBoundingClientRect(); + const styles = window.getComputedStyle(btn); + return { + name, + exists: true, + bottom: rect.bottom, + left: rect.left, + width: rect.width, + position: styles.position + }; + }); + }); + + // Check all buttons exist and are at same bottom position + const bottomPosition = buttonPositions[0]?.bottom; + let allButtonsAligned = true; + + for (const btn of buttonPositions) { + if (!btn.exists) { + console.log(`โŒ ${btn.name}: Button not found`); + allTestsPassed = false; + allButtonsAligned = false; + } else { + const bottomMatch = Math.abs(btn.bottom - bottomPosition) < 5; // 5px tolerance + if (bottomMatch) { + console.log(`โœ… ${btn.name}: Positioned at bottom=${btn.bottom.toFixed(0)}px, left=${btn.left.toFixed(0)}px`); + } else { + console.log(`โŒ ${btn.name}: Wrong bottom position (${btn.bottom.toFixed(0)}px, expected ~${bottomPosition.toFixed(0)}px)`); + allTestsPassed = false; + allButtonsAligned = false; + } + } + } + + if (allButtonsAligned && buttonPositions.every(b => b.exists)) { + console.log('โœ… All 6 buttons are aligned in a row'); + } + + // TEST 3: Footer should have 120px bottom padding + console.log('\n๐Ÿ“Š TEST 3: Footer has 120px bottom padding'); + console.log('-'.repeat(70)); + + await page.evaluate(() => { + window.scrollTo(0, document.body.scrollHeight); + }); + await page.waitForTimeout(500); + + const footerPadding = await page.evaluate(() => { + const footer = document.querySelector('footer.no-print'); + if (!footer) return { exists: false }; + + const styles = window.getComputedStyle(footer); + return { + exists: true, + paddingBottom: styles.paddingBottom, + paddingBottomValue: parseInt(styles.paddingBottom, 10) + }; + }); + + if (footerPadding.exists) { + if (footerPadding.paddingBottomValue >= 110) { + console.log(`โœ… Footer has adequate padding: ${footerPadding.paddingBottom}`); + } else { + console.log(`โŒ Footer padding insufficient: ${footerPadding.paddingBottom} (expected >= 110px)`); + allTestsPassed = false; + } + } else { + console.log('โŒ Footer element not found'); + allTestsPassed = false; + } + + // TEST 4: Footer hover effect should enlarge text + console.log('\n๐Ÿ“Š TEST 4: Footer hover effect enlarges text'); + console.log('-'.repeat(70)); + + // Get initial font size + const initialFontSize = await page.evaluate(() => { + const footer = document.querySelector('footer.no-print p'); + if (!footer) return null; + return parseFloat(window.getComputedStyle(footer).fontSize); + }); + + // Trigger footer hover by adding class + await page.evaluate(() => { + const footer = document.querySelector('footer.no-print'); + if (footer) { + footer.classList.add('footer-hovered'); + // Trigger transition + footer.offsetHeight; // Force reflow + } + }); + + await page.waitForTimeout(500); // Wait for CSS transition + + const hoveredFontSize = await page.evaluate(() => { + const footer = document.querySelector('footer.no-print p'); + if (!footer) return null; + return parseFloat(window.getComputedStyle(footer).fontSize); + }); + + if (initialFontSize && hoveredFontSize) { + if (hoveredFontSize > initialFontSize) { + console.log(`โœ… Footer text enlarged on hover: ${initialFontSize.toFixed(1)}px โ†’ ${hoveredFontSize.toFixed(1)}px`); + } else { + console.log(`โŒ Footer text not enlarged: ${initialFontSize.toFixed(1)}px โ†’ ${hoveredFontSize.toFixed(1)}px`); + allTestsPassed = false; + } + } else { + console.log('โŒ Could not measure footer font sizes'); + allTestsPassed = false; + } + + console.log('-'.repeat(70)); + + if (allTestsPassed) { + console.log('\nโœ… ALL TESTS PASSED!'); + console.log(' โ€ข Keyboard shortcuts button visible'); + console.log(' โ€ข All 6 buttons in a single row'); + console.log(' โ€ข Footer has 120px bottom padding'); + console.log(' โ€ข Footer hover effect enlarges text'); + } 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); + } +} + +testMobileUpdates(); diff --git a/tests/mjs/40-back-to-top-and-footer-fixes.mjs b/tests/mjs/40-back-to-top-and-footer-fixes.mjs new file mode 100755 index 0000000..7265673 --- /dev/null +++ b/tests/mjs/40-back-to-top-and-footer-fixes.mjs @@ -0,0 +1,188 @@ +#!/usr/bin/env node +/** + * Test: Back-to-Top Button and Footer Text Fixes + * + * Verifies: + * 1. Back-to-top button shows full opacity (no transparency) when at page bottom + * 2. Footer text enlarges when footer is hovered/touched + */ + +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 testBackToTopAndFooterFixes() { + console.log('๐Ÿงช Testing Back-to-Top Button Opacity & Footer Text Enlargement'); + 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); + + let allTestsPassed = true; + + // TEST 1: Back-to-top button should have full opacity when at bottom + console.log('\n๐Ÿ“Š TEST 1: Back-to-top button full opacity at page bottom'); + console.log('-'.repeat(70)); + + // Scroll to bottom + await page.evaluate(() => { + window.scrollTo(0, document.body.scrollHeight); + }); + await page.waitForTimeout(500); + + const backToTopAtBottom = await page.evaluate(() => { + const btn = document.querySelector('.back-to-top'); + if (!btn) return { exists: false }; + + const hasAtBottomClass = btn.classList.contains('at-bottom'); + const styles = window.getComputedStyle(btn); + const opacity = parseFloat(styles.opacity); + const backgroundColor = styles.backgroundColor; + + return { + exists: true, + hasAtBottomClass, + opacity, + backgroundColor + }; + }); + + if (backToTopAtBottom.exists) { + // Should have opacity: 1 (full opacity, no transparency) + if (backToTopAtBottom.opacity >= 0.95) { + console.log(`โœ… Back-to-top button has full opacity: ${backToTopAtBottom.opacity}`); + console.log(` Background: ${backToTopAtBottom.backgroundColor}`); + console.log(` Has .at-bottom class: ${backToTopAtBottom.hasAtBottomClass}`); + } else { + console.log(`โŒ Back-to-top button still has transparency: opacity=${backToTopAtBottom.opacity} (expected >= 0.95)`); + allTestsPassed = false; + } + } else { + console.log('โŒ Back-to-top button not found'); + allTestsPassed = false; + } + + // TEST 2: Footer text should enlarge when footer is hovered + console.log('\n๐Ÿ“Š TEST 2: Footer text enlarges on hover'); + console.log('-'.repeat(70)); + + // Get initial font size before hover + const initialFooterState = await page.evaluate(() => { + const footer = document.querySelector('footer.no-print'); + const footerP = document.querySelector('footer.no-print p'); + if (!footer || !footerP) return { exists: false }; + + const styles = window.getComputedStyle(footerP); + return { + exists: true, + fontSize: parseFloat(styles.fontSize), + footerHasClass: footer.classList.contains('footer-hovered') + }; + }); + + if (!initialFooterState.exists) { + console.log('โŒ Footer or footer paragraph not found'); + allTestsPassed = false; + } else { + console.log(` Initial font size: ${initialFooterState.fontSize.toFixed(1)}px`); + console.log(` Footer has .footer-hovered class: ${initialFooterState.footerHasClass}`); + + // Trigger footer hover by adding class via JavaScript + await page.evaluate(() => { + const footer = document.querySelector('footer.no-print'); + if (footer) { + // Dispatch mouseenter event + const event = new MouseEvent('mouseenter', { + bubbles: true, + cancelable: true, + view: window + }); + footer.dispatchEvent(event); + } + }); + + // Wait for CSS transition + await page.waitForTimeout(500); + + // Get font size after hover + const hoveredFooterState = await page.evaluate(() => { + const footer = document.querySelector('footer.no-print'); + const footerP = document.querySelector('footer.no-print p'); + if (!footer || !footerP) return { exists: false }; + + const styles = window.getComputedStyle(footerP); + return { + exists: true, + fontSize: parseFloat(styles.fontSize), + footerHasClass: footer.classList.contains('footer-hovered'), + computedFontSize: styles.fontSize + }; + }); + + if (hoveredFooterState.exists) { + console.log(` After hover font size: ${hoveredFooterState.fontSize.toFixed(1)}px`); + console.log(` Footer has .footer-hovered class: ${hoveredFooterState.footerHasClass}`); + + if (hoveredFooterState.footerHasClass) { + console.log('โœ… Footer received .footer-hovered class'); + } else { + console.log('โŒ Footer did NOT receive .footer-hovered class'); + allTestsPassed = false; + } + + // Check if font size increased (should be 1.2x larger) + if (hoveredFooterState.fontSize > initialFooterState.fontSize) { + const increase = ((hoveredFooterState.fontSize - initialFooterState.fontSize) / initialFooterState.fontSize) * 100; + console.log(`โœ… Footer text enlarged by ${increase.toFixed(1)}%`); + } else { + console.log(`โŒ Footer text did NOT enlarge (${initialFooterState.fontSize.toFixed(1)}px โ†’ ${hoveredFooterState.fontSize.toFixed(1)}px)`); + allTestsPassed = false; + } + } else { + console.log('โŒ Could not measure hovered footer state'); + allTestsPassed = false; + } + } + + console.log('-'.repeat(70)); + + if (allTestsPassed) { + console.log('\nโœ… ALL TESTS PASSED!'); + console.log(' โ€ข Back-to-top button has full opacity at page bottom'); + console.log(' โ€ข Footer text enlarges when hovered'); + } 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); + } +} + +testBackToTopAndFooterFixes(); diff --git a/tests/mjs/41-mobile-accordion-test.mjs b/tests/mjs/41-mobile-accordion-test.mjs new file mode 100755 index 0000000..5c5ef89 --- /dev/null +++ b/tests/mjs/41-mobile-accordion-test.mjs @@ -0,0 +1,180 @@ +#!/usr/bin/env bun + +/** + * Mobile Accordion Test + * ====================== + * Tests the sidebar accordion functionality on mobile devices in the extended CV view. + * + * What this test verifies: + * - Accordion headers are visible on mobile (hidden on desktop) + * - Accordion opens and closes when clicked + * - Content is properly shown/hidden + * - Chevron icon rotates correctly + * - Both left and right sidebars work + */ + +import { chromium } from 'playwright'; + +const BASE_URL = 'http://localhost:1999'; +const MOBILE_VIEWPORT = { width: 375, height: 667 }; // iPhone SE size + +async function testMobileAccordion() { + console.log('๐Ÿงช Starting Mobile Accordion Test...\n'); + + const browser = await chromium.launch({ headless: true }); + const context = await browser.newContext({ + viewport: MOBILE_VIEWPORT, + userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X) AppleWebKit/605.1.15' + }); + const page = await context.newPage(); + + let allTestsPassed = true; + + try { + // Navigate to page with extended CV (lang param and let localStorage handle length) + console.log('๐Ÿ“ฑ Loading page in mobile viewport (375x667)...'); + + // First, set localStorage for extended CV + await page.goto(BASE_URL); + await page.evaluate(() => { + localStorage.setItem('cv-length', 'long'); + }); + + // Reload with the extended CV setting + await page.goto(BASE_URL, { waitUntil: 'networkidle' }); + await page.waitForTimeout(1500); + + // Test Left Sidebar Accordion + console.log('\n๐Ÿ” Testing LEFT sidebar accordion...'); + + const leftAccordion = await page.locator('.cv-sidebar-left .sidebar-accordion').first(); + const leftHeader = await page.locator('.cv-sidebar-left .sidebar-accordion-header').first(); + const leftContent = await page.locator('.cv-sidebar-left .sidebar-accordion-content').first(); + const leftChevron = await page.locator('.cv-sidebar-left .sidebar-accordion-header .chevron').first(); + + // Check header is visible on mobile + const leftHeaderVisible = await leftHeader.isVisible(); + console.log(` โœ“ Left accordion header visible: ${leftHeaderVisible ? 'โœ… YES' : 'โŒ NO'}`); + if (!leftHeaderVisible) allTestsPassed = false; + + // Check if accordion is initially open + const leftInitiallyOpen = await leftAccordion.getAttribute('open'); + console.log(` โœ“ Left accordion initially open: ${leftInitiallyOpen !== null ? 'โœ… YES' : 'โŒ NO'}`); + + // Check content is visible when open + const leftContentVisible = await leftContent.isVisible(); + console.log(` โœ“ Left content visible when open: ${leftContentVisible ? 'โœ… YES' : 'โŒ NO'}`); + if (!leftContentVisible && leftInitiallyOpen) allTestsPassed = false; + + // Close the accordion + console.log('\n ๐Ÿ–ฑ๏ธ Clicking left accordion to close...'); + await leftHeader.click(); + await page.waitForTimeout(500); // Wait for animation + + // Check accordion is closed + const leftNowClosed = await leftAccordion.getAttribute('open'); + console.log(` โœ“ Left accordion closed: ${leftNowClosed === null ? 'โœ… YES' : 'โŒ NO'}`); + if (leftNowClosed !== null) allTestsPassed = false; + + // Check content is hidden when closed + const leftContentHidden = !(await leftContent.isVisible()); + console.log(` โœ“ Left content hidden when closed: ${leftContentHidden ? 'โœ… YES' : 'โŒ NO'}`); + if (!leftContentHidden) allTestsPassed = false; + + // Open the accordion again + console.log('\n ๐Ÿ–ฑ๏ธ Clicking left accordion to re-open...'); + await leftHeader.click(); + await page.waitForTimeout(500); // Wait for animation + + // Check accordion is open again + const leftReopened = await leftAccordion.getAttribute('open'); + console.log(` โœ“ Left accordion re-opened: ${leftReopened !== null ? 'โœ… YES' : 'โŒ NO'}`); + if (leftReopened === null) allTestsPassed = false; + + // Check content is visible again + const leftContentVisibleAgain = await leftContent.isVisible(); + console.log(` โœ“ Left content visible again: ${leftContentVisibleAgain ? 'โœ… YES' : 'โŒ NO'}`); + if (!leftContentVisibleAgain) allTestsPassed = false; + + // Test Right Sidebar Accordion + console.log('\n๐Ÿ” Testing RIGHT sidebar accordion...'); + + const rightAccordion = await page.locator('.cv-sidebar-right .sidebar-accordion').first(); + const rightHeader = await page.locator('.cv-sidebar-right .sidebar-accordion-header').first(); + const rightContent = await page.locator('.cv-sidebar-right .sidebar-accordion-content').first(); + const rightChevron = await page.locator('.cv-sidebar-right .sidebar-accordion-header .chevron').first(); + + // Check header is visible on mobile + const rightHeaderVisible = await rightHeader.isVisible(); + console.log(` โœ“ Right accordion header visible: ${rightHeaderVisible ? 'โœ… YES' : 'โŒ NO'}`); + if (!rightHeaderVisible) allTestsPassed = false; + + // Close and re-open right accordion + console.log('\n ๐Ÿ–ฑ๏ธ Clicking right accordion to close...'); + await rightHeader.click(); + await page.waitForTimeout(500); + + const rightClosed = await rightAccordion.getAttribute('open'); + console.log(` โœ“ Right accordion closed: ${rightClosed === null ? 'โœ… YES' : 'โŒ NO'}`); + if (rightClosed !== null) allTestsPassed = false; + + console.log('\n ๐Ÿ–ฑ๏ธ Clicking right accordion to re-open...'); + await rightHeader.click(); + await page.waitForTimeout(500); + + const rightReopened = await rightAccordion.getAttribute('open'); + console.log(` โœ“ Right accordion re-opened: ${rightReopened !== null ? 'โœ… YES' : 'โŒ NO'}`); + if (rightReopened === null) allTestsPassed = false; + + // Test styling + console.log('\n๐ŸŽจ Testing accordion styling...'); + + const leftHeaderStyles = await leftHeader.evaluate(el => { + const styles = window.getComputedStyle(el); + return { + display: styles.display, + background: styles.backgroundColor, + padding: styles.padding, + cursor: styles.cursor, + borderRadius: styles.borderRadius + }; + }); + + console.log(` โœ“ Header display: ${leftHeaderStyles.display === 'flex' ? 'โœ… flex' : 'โŒ ' + leftHeaderStyles.display}`); + console.log(` โœ“ Header background: ${leftHeaderStyles.background}`); + console.log(` โœ“ Header cursor: ${leftHeaderStyles.cursor === 'pointer' ? 'โœ… pointer' : 'โŒ ' + leftHeaderStyles.cursor}`); + + if (leftHeaderStyles.display !== 'flex' || leftHeaderStyles.cursor !== 'pointer') { + allTestsPassed = false; + } + + // Take screenshot + console.log('\n๐Ÿ“ธ Taking screenshot...'); + await page.screenshot({ + path: 'tests/screenshots/mobile-accordion-extended.png', + fullPage: true + }); + console.log(' Saved: tests/screenshots/mobile-accordion-extended.png'); + + } catch (error) { + console.error('\nโŒ Test failed with error:', error); + allTestsPassed = false; + } finally { + await browser.close(); + } + + // Final result + console.log('\n' + '='.repeat(50)); + if (allTestsPassed) { + console.log('โœ… ALL MOBILE ACCORDION TESTS PASSED!'); + console.log('='.repeat(50)); + process.exit(0); + } else { + console.log('โŒ SOME MOBILE ACCORDION TESTS FAILED'); + console.log('='.repeat(50)); + process.exit(1); + } +} + +// Run test +testMobileAccordion(); diff --git a/tests/mjs/43-mobile-accordion-and-modal-test.mjs b/tests/mjs/43-mobile-accordion-and-modal-test.mjs new file mode 100755 index 0000000..2a9e192 --- /dev/null +++ b/tests/mjs/43-mobile-accordion-and-modal-test.mjs @@ -0,0 +1,163 @@ +#!/usr/bin/env node + +import { chromium } from 'playwright'; + +const MOBILE_VIEWPORT = { width: 375, height: 667 }; // iPhone SE size + +(async () => { + const browser = await chromium.launch({ headless: true }); + const context = await browser.newContext({ viewport: MOBILE_VIEWPORT }); + const page = await context.newPage(); + + try { + console.log('๐Ÿงช Testing Mobile Accordion and Modal Centering\n'); + console.log(`๐Ÿ“ฑ Mobile Viewport: ${MOBILE_VIEWPORT.width}x${MOBILE_VIEWPORT.height}`); + + // Navigate to extended view + await page.goto('http://localhost:1999/?lang=en&view=extended'); + await page.waitForLoadState('networkidle'); + console.log('โœ… Extended view loaded\n'); + + // ===== TEST 1: SIDEBAR ACCORDION ===== + console.log('๐Ÿ” TEST 1: Sidebar Accordion Structure\n'); + + // Check for accordion elements + const accordionDetails = await page.locator('.sidebar-accordion').first(); + const accordionSummary = await page.locator('.sidebar-accordion-header').first(); + const accordionContent = await page.locator('.sidebar-accordion-content').first(); + + const detailsCount = await page.locator('.sidebar-accordion').count(); + const summaryCount = await page.locator('.sidebar-accordion-header').count(); + const contentCount = await page.locator('.sidebar-accordion-content').count(); + + console.log(`๐Ÿ“Š Accordion Structure:`); + console.log(` โ€ข Details elements: ${detailsCount}`); + console.log(` โ€ข Summary elements: ${summaryCount}`); + console.log(` โ€ข Content elements: ${contentCount}`); + + // Check if accordion is initially open + const isOpen = await accordionDetails.evaluate(el => el.hasAttribute('open')); + console.log(` โ€ข Initially open: ${isOpen ? 'โœ… YES' : 'โŒ NO'}`); + + // Check if summary is visible on mobile + const summaryDisplay = await accordionSummary.evaluate(el => { + const style = window.getComputedStyle(el); + return style.display; + }); + console.log(` โ€ข Summary display: ${summaryDisplay}`); + console.log(` โ€ข Summary visible: ${summaryDisplay !== 'none' ? 'โœ… YES' : 'โŒ NO'}`); + + // Test accordion toggle + if (detailsCount > 0) { + console.log('\n๐Ÿ–ฑ๏ธ Testing Accordion Toggle...'); + + // Click to close + await accordionSummary.click(); + await page.waitForTimeout(500); + + const isClosedAfterClick = await accordionDetails.evaluate(el => !el.hasAttribute('open')); + console.log(` โ€ข Closed after click: ${isClosedAfterClick ? 'โœ… YES' : 'โŒ NO'}`); + + // Check if content is hidden + const contentHidden = await accordionContent.evaluate(el => { + const style = window.getComputedStyle(el); + return style.maxHeight === '0px' || style.opacity === '0'; + }); + console.log(` โ€ข Content hidden: ${contentHidden ? 'โœ… YES' : 'โŒ NO'}`); + + // Click to reopen + await accordionSummary.click(); + await page.waitForTimeout(500); + + const isReopenedAfterClick = await accordionDetails.evaluate(el => el.hasAttribute('open')); + console.log(` โ€ข Reopened after click: ${isReopenedAfterClick ? 'โœ… YES' : 'โŒ NO'}`); + } + + // ===== TEST 2: MODAL CENTERING ===== + console.log('\n๐Ÿ” TEST 2: Modal Centering\n'); + + // Open the PDF modal + await page.click('#download-button'); + await page.waitForTimeout(500); + + const modal = await page.locator('#pdf-modal'); + const isModalOpen = await modal.evaluate(el => el.hasAttribute('open')); + console.log(`๐Ÿ“ฆ Modal Status:`); + console.log(` โ€ข Modal open: ${isModalOpen ? 'โœ… YES' : 'โŒ NO'}`); + + if (isModalOpen) { + // Get modal bounding box + const modalBox = await modal.boundingBox(); + + console.log(`\n๐Ÿ“ Modal Position:`); + console.log(` โ€ข X position: ${modalBox.x.toFixed(2)}px`); + console.log(` โ€ข Y position: ${modalBox.y.toFixed(2)}px`); + console.log(` โ€ข Width: ${modalBox.width.toFixed(2)}px`); + console.log(` โ€ข Height: ${modalBox.height.toFixed(2)}px`); + + // Calculate centers + const modalCenterX = modalBox.x + modalBox.width / 2; + const modalCenterY = modalBox.y + modalBox.height / 2; + const viewportCenterX = MOBILE_VIEWPORT.width / 2; + const viewportCenterY = MOBILE_VIEWPORT.height / 2; + + console.log(`\n๐ŸŽฏ Centering Analysis:`); + console.log(` โ€ข Modal center X: ${modalCenterX.toFixed(2)}px`); + console.log(` โ€ข Viewport center X: ${viewportCenterX.toFixed(2)}px`); + console.log(` โ€ข Modal center Y: ${modalCenterY.toFixed(2)}px`); + console.log(` โ€ข Viewport center Y: ${viewportCenterY.toFixed(2)}px`); + + // Check if centered (10px tolerance) + const horizontalOffset = Math.abs(modalCenterX - viewportCenterX); + const verticalOffset = Math.abs(modalCenterY - viewportCenterY); + const TOLERANCE = 10; + + console.log(`\n๐Ÿ“ Offset Analysis:`); + console.log(` โ€ข Horizontal offset: ${horizontalOffset.toFixed(2)}px`); + console.log(` โ€ข Vertical offset: ${verticalOffset.toFixed(2)}px`); + console.log(` โ€ข Tolerance: ${TOLERANCE}px`); + + const isHorizontallyCentered = horizontalOffset <= TOLERANCE; + const isVerticallyCentered = verticalOffset <= TOLERANCE; + + console.log(`\nโœ… Centering Results:`); + console.log(` โ€ข Horizontally centered: ${isHorizontallyCentered ? 'โœ… YES' : 'โŒ NO'}`); + console.log(` โ€ข Vertically centered: ${isVerticallyCentered ? 'โœ… YES' : 'โŒ NO'}`); + + // Get CSS properties + const cssProps = await modal.evaluate(el => { + const style = window.getComputedStyle(el); + return { + position: style.position, + top: style.top, + left: style.left, + transform: style.transform + }; + }); + + console.log(`\n๐ŸŽจ CSS Properties:`); + console.log(` โ€ข position: ${cssProps.position}`); + console.log(` โ€ข top: ${cssProps.top}`); + console.log(` โ€ข left: ${cssProps.left}`); + console.log(` โ€ข transform: ${cssProps.transform}`); + + // Overall test results + console.log(`\n๐Ÿ“Š OVERALL RESULTS:`); + if (isHorizontallyCentered && isVerticallyCentered) { + console.log('โœ… Modal is PROPERLY CENTERED on mobile!'); + } else { + console.log('โŒ Modal is NOT centered on mobile'); + console.log(` Needs adjustment: ${!isHorizontallyCentered ? 'horizontal' : ''} ${!isVerticallyCentered ? 'vertical' : ''}`); + } + } + + console.log('\nโœ… Test completed successfully!\n'); + + } catch (error) { + console.error('โŒ Test failed:', error.message); + console.error(error.stack); + process.exit(1); + } finally { + await browser.close(); + } +})(); diff --git a/tests/mjs/46-visual-accordion-style-test.mjs b/tests/mjs/46-visual-accordion-style-test.mjs new file mode 100755 index 0000000..5981dde --- /dev/null +++ b/tests/mjs/46-visual-accordion-style-test.mjs @@ -0,0 +1,82 @@ +#!/usr/bin/env node + +import { chromium } from 'playwright'; +import { writeFileSync } from 'fs'; + +const MOBILE_VIEWPORT = { width: 375, height: 667 }; + +(async () => { + const browser = await chromium.launch({ headless: true }); + const context = await browser.newContext({ viewport: MOBILE_VIEWPORT }); + const page = await context.newPage(); + + try { + console.log('๐ŸŽจ Visual Accordion Style Test\n'); + + await page.goto('http://localhost:1999/?lang=en&view=extended'); + await page.waitForLoadState('networkidle'); + + // Check accordion header styling + const accordionHeader = await page.locator('.sidebar-accordion-header').first(); + + const styles = await accordionHeader.evaluate(el => { + const computed = window.getComputedStyle(el); + return { + background: computed.backgroundColor, + color: computed.color, + padding: computed.padding, + borderRadius: computed.borderRadius, + marginBottom: computed.marginBottom, + borderBottom: computed.borderBottom, + fontSize: computed.fontSize, + fontWeight: computed.fontWeight, + textTransform: computed.textTransform + }; + }); + + console.log('๐Ÿ“Š Accordion Header Styles:'); + console.log(` โ€ข Background: ${styles.background}`); + console.log(` โ€ข Text Color: ${styles.color}`); + console.log(` โ€ข Padding: ${styles.padding}`); + console.log(` โ€ข Border Radius: ${styles.borderRadius}`); + console.log(` โ€ข Margin Bottom: ${styles.marginBottom}`); + console.log(` โ€ข Border Bottom: ${styles.borderBottom}`); + console.log(` โ€ข Font Size: ${styles.fontSize}`); + console.log(` โ€ข Font Weight: ${styles.fontWeight}`); + console.log(` โ€ข Text Transform: ${styles.textTransform}`); + + // Check if matches dark theme + const isDarkBackground = styles.background.includes('rgb(48, 48, 48)') || + styles.background.includes('#303030'); + const isLightText = styles.color.includes('rgb(204, 204, 204)') || + styles.color.includes('#ccc'); + const hasNoBorderRadius = styles.borderRadius === '0px'; + const hasNoMarginBottom = styles.marginBottom === '0px'; + const isUppercase = styles.textTransform === 'uppercase'; + + console.log('\nโœ… Style Verification:'); + console.log(` โ€ข Dark background (#303030): ${isDarkBackground ? 'โœ…' : 'โŒ'}`); + console.log(` โ€ข Light text (#ccc): ${isLightText ? 'โœ…' : 'โŒ'}`); + console.log(` โ€ข No border radius: ${hasNoBorderRadius ? 'โœ…' : 'โŒ'}`); + console.log(` โ€ข No margin bottom: ${hasNoMarginBottom ? 'โœ…' : 'โŒ'}`); + console.log(` โ€ข Uppercase text: ${isUppercase ? 'โœ…' : 'โŒ'}`); + + const allMatch = isDarkBackground && isLightText && hasNoBorderRadius && + hasNoMarginBottom && isUppercase; + + console.log(`\n${allMatch ? 'โœ…' : 'โŒ'} Accordion style ${allMatch ? 'MATCHES' : 'DOES NOT MATCH'} CV title badges style\n`); + + // Take screenshot for visual verification + await page.screenshot({ + path: 'tests/screenshots/accordion-mobile-styled.png', + fullPage: true + }); + console.log('๐Ÿ“ธ Screenshot saved to tests/screenshots/accordion-mobile-styled.png\n'); + + } catch (error) { + console.error('โŒ Test failed:', error.message); + process.exit(1); + } finally { + await browser.close(); + } +})();