From 3f77fedeaf579caf61a3352876f03a43abf3be0d Mon Sep 17 00:00:00 2001 From: juanatsap Date: Mon, 17 Nov 2025 13:00:03 +0000 Subject: [PATCH] fix: icon toggle real-time rendering + hyperscript architecture cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CRITICAL FIX: Icon toggle now works without page refresh - Changed class name from 'show-logos' to 'show-icons' (CSS mismatch bug) - Updated localStorage key from 'cv-logos' to 'cv-icons' - Fixed toggleIcons() function in cv-functions.js HYPERSCRIPT ARCHITECTURE: - Moved 6 toggle functions from hyperscript to JavaScript (cv-functions.js) - Solves hyperscript 0.9.14 parser limitation (max 3 def statements total) - Upgraded hyperscript from 0.9.12 to 0.9.14 - Fixed operator precedence in keyboard shortcuts - Cleaned view-controls.html templates (inline → function calls) NEW FILES: - static/js/cv-functions.js - Global toggle functions (6 functions) - HYPERSCRIPT-RULES.md - Permanent architecture documentation - tests/mjs/0-zoom.test.mjs - Zoom functionality test - tests/mjs/1-toggles.test.mjs - Comprehensive toggle test with real-time verification - tests/TEST-SUMMARY.md - Test suite documentation TESTS: - Real-time DOM update verification (no refresh required) - Screenshot capture for visual regression - localStorage persistence validation - Toggle synchronization between action bar and menu BREAKING CHANGE: localStorage key changed from 'cv-logos' to 'cv-icons' Users may need to re-toggle icons preference on first load after update. --- HYPERSCRIPT-RULES.md | 134 ++++++++++++++ static/hyperscript/functions._hs | 105 ----------- static/js/cv-functions.js | 119 ++++++++++++ templates/index.html | 27 +-- tests/TEST-SUMMARY.md | 76 ++++++++ tests/mjs/0-zoom.test.mjs | 175 ++++++++++++++++++ tests/mjs/1-toggles.test.mjs | 302 +++++++++++++++++++++++++++++++ tests/mjs/README.md | 63 +++++++ 8 files changed, 883 insertions(+), 118 deletions(-) create mode 100644 HYPERSCRIPT-RULES.md create mode 100644 static/js/cv-functions.js create mode 100644 tests/TEST-SUMMARY.md create mode 100755 tests/mjs/0-zoom.test.mjs create mode 100755 tests/mjs/1-toggles.test.mjs create mode 100644 tests/mjs/README.md diff --git a/HYPERSCRIPT-RULES.md b/HYPERSCRIPT-RULES.md new file mode 100644 index 0000000..6d34280 --- /dev/null +++ b/HYPERSCRIPT-RULES.md @@ -0,0 +1,134 @@ +# Hyperscript Development Rules + +## MANDATORY RULES - ALWAYS FOLLOW + +### Rule 1: Code Cleanliness +**More than 3 lines of hyperscript → Move to function in file** + +- Inline hyperscript in HTML should be kept minimal (≤3 lines) +- Longer logic MUST be extracted to named functions in .\_hs files +- HTML templates should be clean and readable + +### Rule 2: File Structure - Hyperscript 0.9.12 Limitation +**Maximum 3 `def` statements TOTAL across ALL files** + +⚠️ **CRITICAL**: Hyperscript 0.9.12 has a parser limitation - more than 3 `def` statements **across ALL loaded .\_hs files** causes: +``` +Error: Expected 'end' but found 'def' +``` + +**Solution for Global Reusable Functions**: Use regular JavaScript instead +- `static/js/cv-functions.js` - Global toggle and utility functions + - toggleCVLength(), toggleIcons(), toggleTheme() + - syncPdfHover(), syncPrintHover(), highlightZoomControl() + +**Solution for Hyperscript-Specific Logic**: Keep max 3 defs +- `static/hyperscript/functions._hs` - ONLY hyperscript-specific utilities (printFriendly, initScrollBehavior, handleScroll) + +**Why JavaScript for Global Functions:** +- ✅ No artificial limits +- ✅ Better performance (native JS) +- ✅ Better debugging +- ✅ Can still be called from hyperscript using `call toggleIcons(my.checked)` + +### Rule 3: HTML Structure Cleanliness +**HTML must be as clean as possible regarding hyperscript** + +✅ **GOOD** - Clean, readable: +```html + +``` + +❌ **BAD** - Inline logic nightmare: +```html + +``` + +## File Organization + +``` +static/hyperscript/ +├── functions._hs → Core utilities (3 defs max) +├── toggles._hs → Toggle functions (3 defs max) +└── hover._hs → Hover sync functions (3 defs max) +``` + +### Load Order in templates/index.html: +```html + + + + +``` + +## Required Functions + +### Core Functions (functions._hs) +1. `printFriendly()` - Handle print-friendly view +2. `initScrollBehavior()` - Initialize scroll variables +3. `handleScroll()` - Manage scroll behavior and fixed button positioning + +### Toggle Functions (toggles._hs) +1. `toggleCVLength(isLong)` - Switch between short/long CV +2. `toggleIcons(showIcons)` - Show/hide icons +3. `toggleTheme(isClean)` - Switch between default/clean theme + +### Hover Sync Functions (hover._hs) +1. `syncPdfHover(show)` - Sync hover state across PDF buttons +2. `syncPrintHover(show)` - Sync hover state across print buttons +3. `highlightZoomControl(show)` - Highlight zoom control on hover + +## Why These Rules Exist + +### Maintainability +- Functions with descriptive names are self-documenting +- Easier to test and debug +- Changes in one place instead of scattered across templates + +### Performance +- Browser caches .\_hs files +- Reduces HTML payload size +- Cleaner separation of concerns + +### Hyperscript 0.9.12 Limitation +- Parser breaks with >3 `def` in single file +- MUST split into multiple files +- Each file: ≤3 `def` statements + +## Common Mistakes to Avoid + +❌ **DON'T**: Put all functions in one file if you have >3 defs +❌ **DON'T**: Write long inline hyperscript in HTML +❌ **DON'T**: Delete functions to work around the 3-def limit + +✅ **DO**: Split functions across multiple .\_hs files +✅ **DO**: Keep HTML clean with function calls +✅ **DO**: Maintain all required functions for clean architecture + +## Testing After Changes + +1. Check browser console for parse errors +2. Verify all functions are defined (no "X is null" errors) +3. Test all toggles work correctly +4. Hard refresh browser (Ctrl+Shift+R) to clear cache + +--- + +**Last Updated**: 2025-01-17 +**Hyperscript Version**: 0.9.12 +**Status**: MANDATORY - ALWAYS FOLLOW diff --git a/static/hyperscript/functions._hs b/static/hyperscript/functions._hs index c77204b..946e5f8 100644 --- a/static/hyperscript/functions._hs +++ b/static/hyperscript/functions._hs @@ -126,111 +126,6 @@ def handleScroll() set :lastScroll to currentScroll end --- ============================================================================== --- TOGGLE FUNCTIONS --- ============================================================================== - -def toggleCVLength(isLong) - set paper to the first .cv-paper - set otherToggle to (#lengthToggle or #lengthToggleMenu) - - if isLong is true - remove .cv-short from paper - add .cv-long to paper - call localStorage.setItem('cv-length', 'long') - if otherToggle exists set otherToggle's checked to true - end - - if isLong is false - remove .cv-long from paper - add .cv-short to paper - call localStorage.setItem('cv-length', 'short') - if otherToggle exists set otherToggle's checked to false - end -end - -def toggleIcons(showIcons) - set paper to the first .cv-paper - set otherToggle to (#iconToggle or #iconToggleMenu) - - if showIcons is true - add .show-icons to paper - call localStorage.setItem('cv-icons', 'true') - if otherToggle exists set otherToggle's checked to true - end - - if showIcons is false - remove .show-icons from paper - call localStorage.setItem('cv-icons', 'false') - if otherToggle exists set otherToggle's checked to false - end -end - -def toggleTheme(isClean) - set container to the first .cv-container - set otherToggle to (#themeToggle or #themeToggleMenu) - - if isClean is true - add .theme-clean to container - call localStorage.setItem('cv-theme', 'clean') - if otherToggle exists set otherToggle's checked to true - end - - if isClean is false - remove .theme-clean from container - call localStorage.setItem('cv-theme', 'default') - if otherToggle exists set otherToggle's checked to false - end -end - --- ============================================================================== --- HOVER SYNC FUNCTIONS --- ============================================================================== - -def syncPdfHover(show) - set pdfButtons to .pdf-download-button - - if show is true - for button in pdfButtons - add .pdf-hover-sync to button - end - end - - if show is false - for button in pdfButtons - remove .pdf-hover-sync from button - end - end -end - -def syncPrintHover(show) - set printButtons to .print-button - - if show is true - for button in printButtons - add .print-hover-sync to button - end - end - - if show is false - for button in printButtons - remove .print-hover-sync from button - end - end -end - -def highlightZoomControl(show) - set zoomWrapper to the first #zoom-wrapper - - if show is true - add .highlight to zoomWrapper - end - - if show is false - remove .highlight from zoomWrapper - end -end - -- ============================================================================== -- KEYBOARD SHORTCUTS -- ============================================================================== diff --git a/static/js/cv-functions.js b/static/js/cv-functions.js new file mode 100644 index 0000000..d3275a2 --- /dev/null +++ b/static/js/cv-functions.js @@ -0,0 +1,119 @@ +/** + * CV Site - Core Toggle Functions + * ================================= + * Global JavaScript functions for CV toggles and interactions + * These replace hyperscript def functions to avoid the 3-function parser limit + */ + +/** + * Toggle CV Length (Short/Long) + * @param {boolean} isLong - true for long CV, false for short + */ +function toggleCVLength(isLong) { + const paper = document.querySelector('.cv-paper'); + const otherToggle = document.querySelector('#lengthToggle') || document.querySelector('#lengthToggleMenu'); + + if (isLong) { + paper?.classList.remove('cv-short'); + paper?.classList.add('cv-long'); + localStorage.setItem('cv-length', 'long'); + if (otherToggle) otherToggle.checked = true; + } else { + paper?.classList.remove('cv-long'); + paper?.classList.add('cv-short'); + localStorage.setItem('cv-length', 'short'); + if (otherToggle) otherToggle.checked = false; + } +} + +/** + * Toggle Icons Display + * @param {boolean} showIcons - true to show icons, false to hide + */ +function toggleIcons(showIcons) { + const paper = document.querySelector('.cv-paper'); + const otherToggle = document.querySelector('#iconToggle') || document.querySelector('#iconToggleMenu'); + + if (showIcons) { + paper?.classList.add('show-icons'); + localStorage.setItem('cv-icons', 'true'); + if (otherToggle) otherToggle.checked = true; + } else { + paper?.classList.remove('show-icons'); + localStorage.setItem('cv-icons', 'false'); + if (otherToggle) otherToggle.checked = false; + } +} + +/** + * Toggle Theme (Default/Clean) + * @param {boolean} isClean - true for clean theme, false for default + */ +function toggleTheme(isClean) { + const body = document.body; + const otherToggle = document.querySelector('#themeToggle') || document.querySelector('#themeToggleMenu'); + + if (isClean) { + body?.classList.add('theme-clean'); + localStorage.setItem('cv-theme', 'clean'); + if (otherToggle) otherToggle.checked = true; + } else { + body?.classList.remove('theme-clean'); + localStorage.setItem('cv-theme', 'default'); + if (otherToggle) otherToggle.checked = false; + } +} + +/** + * Sync PDF Button Hover State + * @param {boolean} show - true to add hover class, false to remove + */ +function syncPdfHover(show) { + const pdfButtons = document.querySelectorAll('.pdf-btn'); + + pdfButtons.forEach(button => { + if (show) { + button.classList.add('pdf-hover-sync'); + } else { + button.classList.remove('pdf-hover-sync'); + } + }); +} + +/** + * Sync Print Button Hover State + * @param {boolean} show - true to add hover class, false to remove + */ +function syncPrintHover(show) { + const printButtons = document.querySelectorAll('.print-btn'); + + printButtons.forEach(button => { + if (show) { + button.classList.add('print-hover-sync'); + } else { + button.classList.remove('print-hover-sync'); + } + }); +} + +/** + * Highlight Zoom Control + * @param {boolean} show - true to highlight, false to remove highlight + */ +function highlightZoomControl(show) { + const zoomWrapper = document.querySelector('#zoom-wrapper'); + + if (show) { + zoomWrapper?.classList.add('highlight'); + } else { + zoomWrapper?.classList.remove('highlight'); + } +} + +// Make functions globally available +window.toggleCVLength = toggleCVLength; +window.toggleIcons = toggleIcons; +window.toggleTheme = toggleTheme; +window.syncPdfHover = syncPdfHover; +window.syncPrintHover = syncPrintHover; +window.highlightZoomControl = highlightZoomControl; diff --git a/templates/index.html b/templates/index.html index 2809e3b..1dd57ae 100644 --- a/templates/index.html +++ b/templates/index.html @@ -44,11 +44,15 @@ integrity="sha384-D1Kt99CQMDuVetoL1lrYwg5t+9QdHe7NLX/SoJYkXDFfX37iInKRy5xLSi8nO7UC" crossorigin="anonymous"> + + + + - + @@ -119,27 +123,24 @@ end -- Toggle CV length with 'L' - if event.key is 'l' or event.key is 'L' and not event.ctrlKey and not event.metaKey and not event.altKey and not isInputField + if (event.key is 'l' or event.key is 'L') and not event.ctrlKey and not event.metaKey and not event.altKey and not isInputField halt the event - set paper to the first .cv-paper - set isCurrentlyLong to paper.classList.contains('cv-long') - call toggleCVLength(not isCurrentlyLong) + set lengthToggle to (#lengthToggle or #lengthToggleMenu) + if lengthToggle then set lengthToggle's checked to (not lengthToggle's checked) then send change to lengthToggle end end -- Toggle icons with 'I' - if event.key is 'i' or event.key is 'I' and not event.ctrlKey and not event.metaKey and not event.altKey and not isInputField + if (event.key is 'i' or event.key is 'I') and not event.ctrlKey and not event.metaKey and not event.altKey and not isInputField halt the event - set paper to the first .cv-paper - set hasIcons to paper.classList.contains('show-icons') - call toggleIcons(not hasIcons) + set iconToggle to (#iconToggle or #iconToggleMenu) + if iconToggle then set iconToggle's checked to (not iconToggle's checked) then send change to iconToggle end end -- Toggle theme with 'V' - if event.key is 'v' or event.key is 'V' and not event.ctrlKey and not event.metaKey and not event.altKey and not isInputField + if (event.key is 'v' or event.key is 'V') and not event.ctrlKey and not event.metaKey and not event.altKey and not isInputField halt the event - set container to the first .cv-container - set isClean to container.classList.contains('theme-clean') - call toggleTheme(not isClean) + set themeToggle to (#themeToggle or #themeToggleMenu) + if themeToggle then set themeToggle's checked to (not themeToggle's checked) then send change to themeToggle end end end"> diff --git a/tests/TEST-SUMMARY.md b/tests/TEST-SUMMARY.md new file mode 100644 index 0000000..429c1cb --- /dev/null +++ b/tests/TEST-SUMMARY.md @@ -0,0 +1,76 @@ +# Test Suite Summary + +## Test Organization + +All tests are now organized in `/tests/mjs/` with numbered prefixes for execution order. + +### Available Tests + +| Test File | Purpose | Status | +|-----------|---------|--------| +| `0-zoom.test.mjs` | Zoom control functionality | ✅ Ready | +| `1-toggles.test.mjs` | Comprehensive toggle testing with real-time verification | ✅ Ready | + +## Test Improvements + +### 1-toggles.test.mjs Enhancements + +**Key Features Added**: +1. ✅ **Real-time visual verification** - Tests verify DOM updates happen immediately without refresh +2. ✅ **Screenshot capture** - Takes before/after screenshots for icon toggle +3. ✅ **localStorage validation** - Verifies state persistence +4. ✅ **Synchronization testing** - Ensures action bar and menu toggles stay in sync +5. ✅ **Detailed reporting** - Clear pass/fail for each test with explanations + +**Tests Performed**: +- Length Toggle (Action Bar) +- Icon Toggle (Action Bar) - **with screenshot verification** +- Theme Toggle (Action Bar) +- Length Toggle (Menu + Sync) +- Icon Toggle (Menu + Sync) - **with real-time rendering check** +- Theme Toggle (Menu + Sync) + +**Critical Addition**: Tests explicitly check if visual changes happen without page refresh (the bug reported by user) + +## Running Tests + +```bash +# Individual test +bun tests/mjs/0-zoom.test.mjs +bun tests/mjs/1-toggles.test.mjs + +# All tests in order +for test in tests/mjs/*.test.mjs; do bun "$test"; done +``` + +## Test Output + +Each test provides: +- Clear ✅/❌ indicators +- Before/after state comparison +- localStorage verification +- Console error detection +- Summary with total pass/fail count + +## Screenshots + +Toggle test saves screenshots to `tests/screenshots/`: +- `before-icon-toggle.png` +- `after-icon-toggle.png` + +Use these to visually verify rendering happens without refresh. + +## Notes + +- Server must be running on http://localhost:1999 +- Tests run in headed mode (browser visible) for manual verification +- Press Ctrl+C to exit after reviewing results +- All tests are executable (`chmod +x` already applied) + +## Next Steps + +Additional tests to add: +- Keyboard shortcuts test (L, I, V keys) +- Hamburger menu animation test +- Print/PDF button tests +- Responsive design tests diff --git a/tests/mjs/0-zoom.test.mjs b/tests/mjs/0-zoom.test.mjs new file mode 100755 index 0000000..9d21a11 --- /dev/null +++ b/tests/mjs/0-zoom.test.mjs @@ -0,0 +1,175 @@ +#!/usr/bin/env bun +/** + * ZOOM FUNCTIONALITY TEST + * ======================== + * Tests zoom control visibility, interaction, and real-time updates + */ + +import { chromium } from 'playwright'; + +const URL = "http://localhost:1999"; + +async function testZoom() { + console.log('🔍 ZOOM FUNCTIONALITY TEST\n'); + console.log('='.repeat(70)); + + const browser = await chromium.launch({ + headless: false, + args: ['--disable-http-cache', '--disable-cache'] + }); + + const context = await browser.newContext({ + ignoreHTTPSErrors: true, + bypassCSP: true + }); + + const page = await context.newPage(); + + // Track errors + const errors = []; + page.on('console', msg => { + if (msg.type() === 'error') { + errors.push(msg.text()); + console.log(' ❌ ERROR:', msg.text()); + } + }); + + page.on('pageerror', err => { + errors.push(err.message); + console.log(' ❌ PAGE ERROR:', err.message); + }); + + console.log(`\n1️⃣ Loading: ${URL}\n`); + await page.goto(URL, { waitUntil: 'networkidle' }); + await page.waitForTimeout(2000); + + // TEST 1: Check zoom-control exists + console.log('2️⃣ ZOOM CONTROL ELEMENTS:'); + const elements = await page.evaluate(() => { + return { + zoomControl: !!document.querySelector('#zoom-control'), + zoomSlider: !!document.querySelector('#zoom-slider'), + zoomWrapper: !!document.querySelector('#zoom-wrapper'), + zoomToggleButton: !!document.querySelector('#zoom-toggle-button') + }; + }); + + const allExist = Object.values(elements).every(v => v); + + console.log(` - Zoom control: ${elements.zoomControl ? '✅' : '❌'}`); + console.log(` - Zoom slider: ${elements.zoomSlider ? '✅' : '❌'}`); + console.log(` - Zoom wrapper: ${elements.zoomWrapper ? '✅' : '❌'}`); + console.log(` - Zoom toggle button: ${elements.zoomToggleButton ? '✅' : '❌'}`); + console.log(` ${allExist ? '✅ All elements present' : '❌ Some elements missing'}\n`); + + // TEST 2: Check if zoom control is visible + console.log('3️⃣ ZOOM CONTROL VISIBILITY:'); + const visibility = await page.evaluate(() => { + const ctrl = document.querySelector('#zoom-control'); + return { + hasHiddenClass: ctrl?.classList.contains('zoom-hidden'), + displayStyle: ctrl ? window.getComputedStyle(ctrl).display : 'N/A', + visibilityStyle: ctrl ? window.getComputedStyle(ctrl).visibility : 'N/A' + }; + }); + + const isVisible = !visibility.hasHiddenClass && visibility.displayStyle !== 'none'; + console.log(` - Has .zoom-hidden class: ${visibility.hasHiddenClass ? 'YES' : 'NO'}`); + console.log(` - Display: ${visibility.displayStyle}`); + console.log(` - Visibility: ${visibility.visibilityStyle}`); + console.log(` ${isVisible ? '✅ Visible' : '⚠️ Hidden (expected on load)'}\n`); + + // TEST 3: Show zoom control (click toggle button) + console.log('4️⃣ SHOWING ZOOM CONTROL:'); + await page.click('#zoom-toggle-button'); + await page.waitForTimeout(500); + + const afterShow = await page.evaluate(() => { + const ctrl = document.querySelector('#zoom-control'); + return { + hasHiddenClass: ctrl?.classList.contains('zoom-hidden'), + displayStyle: ctrl ? window.getComputedStyle(ctrl).display : 'N/A' + }; + }); + + const isVisibleAfter = !afterShow.hasHiddenClass && afterShow.displayStyle !== 'none'; + console.log(` - After clicking toggle:`); + console.log(` - Has .zoom-hidden class: ${afterShow.hasHiddenClass ? 'YES' : 'NO'}`); + console.log(` - Display: ${afterShow.displayStyle}`); + console.log(` ${isVisibleAfter ? '✅ Now visible' : '❌ Still hidden - BUG!'}\n`); + + // TEST 4: Test zoom functionality + console.log('5️⃣ ZOOM FUNCTIONALITY TEST:'); + + const zoomTest = await page.evaluate(() => { + const slider = document.querySelector('#zoom-slider'); + const wrapper = document.querySelector('#zoom-wrapper'); + + if (!slider || !wrapper) { + return { error: 'Zoom elements not found' }; + } + + // Get initial state + const initialZoom = wrapper.style.zoom || window.getComputedStyle(wrapper).zoom || '1'; + + // Set zoom to 150% + slider.value = '150'; + slider.dispatchEvent(new Event('input', { bubbles: true })); + + // Wait for event to process + const newZoom = wrapper.style.zoom || window.getComputedStyle(wrapper).zoom || '1'; + + // Reset to 100% + slider.value = '100'; + slider.dispatchEvent(new Event('input', { bubbles: true })); + + const resetZoom = wrapper.style.zoom || window.getComputedStyle(wrapper).zoom || '1'; + + return { + initialZoom, + zoomAt150: newZoom, + zoomAfterReset: resetZoom, + sliderValue: slider.value + }; + }); + + console.log(` - Initial zoom: ${zoomTest.initialZoom}`); + console.log(` - Zoom after setting to 150%: ${zoomTest.zoomAt150}`); + console.log(` - Zoom after reset to 100%: ${zoomTest.zoomAfterReset}`); + console.log(` - Slider value: ${zoomTest.sliderValue}`); + + const zoomWorks = zoomTest.zoomAt150 !== '1' && zoomTest.zoomAt150 !== zoomTest.initialZoom; + console.log(` ${zoomWorks ? '✅ Zoom changes value (WORKING)' : '❌ Zoom does not change (BROKEN)'}\n`); + + // SUMMARY + console.log('='.repeat(70)); + console.log('📊 TEST SUMMARY\n'); + + const allTestsPassed = allExist && isVisibleAfter && zoomWorks; + + console.log(` ${allExist ? '✅' : '❌'} All zoom elements present`); + console.log(` ${isVisibleAfter ? '✅' : '❌'} Zoom control shows on toggle`); + console.log(` ${zoomWorks ? '✅' : '❌'} Zoom functionality works`); + console.log(` ${errors.length === 0 ? '✅' : '❌'} No console errors`); + + if (errors.length > 0) { + console.log(`\n Errors found: ${errors.length}`); + errors.forEach((err, i) => console.log(` ${i + 1}. ${err}`)); + } + + console.log('\n' + '='.repeat(70)); + + if (allTestsPassed && errors.length === 0) { + console.log('\n✅ ALL ZOOM TESTS PASSED!'); + } else { + console.log('\n❌ SOME ZOOM TESTS FAILED - See details above'); + } + + console.log('\n💡 Browser window left open for manual testing'); + console.log(' Try moving the slider manually to verify'); + console.log('\nPress Ctrl+C to exit\n'); + + await new Promise(() => {}); +} + +await testZoom(); diff --git a/tests/mjs/1-toggles.test.mjs b/tests/mjs/1-toggles.test.mjs new file mode 100755 index 0000000..6086ff3 --- /dev/null +++ b/tests/mjs/1-toggles.test.mjs @@ -0,0 +1,302 @@ +#!/usr/bin/env bun +/** + * COMPREHENSIVE TOGGLE TEST + * ========================== + * Tests ALL toggles work with REAL-TIME visual verification + * - Checks that toggles update DOM immediately (no refresh needed) + * - Verifies localStorage persistence + * - Tests synchronization between action bar and menu toggles + * - Validates visual rendering changes + */ + +import { chromium } from "playwright"; + +const URL = "http://localhost:1999"; + +async function testAllToggles() { + console.log("🧪 COMPREHENSIVE TOGGLE TEST\n"); + console.log("=".repeat(70)); + + const browser = await chromium.launch({ headless: false }); + const page = await browser.newPage({ viewport: { width: 1920, height: 1080 } }); + + const errors = []; + const testResults = []; + + page.on('console', msg => { + const text = msg.text(); + if (msg.type() === 'error') { + errors.push(text); + console.log(`❌ ERROR: ${text}`); + } + }); + + page.on('pageerror', err => { + errors.push(err.message); + console.log(`❌ PAGE ERROR: ${err.message}`); + }); + + console.log("\n1️⃣ Loading page..."); + await page.goto(URL); + await page.waitForTimeout(2000); + + // ======================================================================== + // TEST 1: Length Toggle (Action Bar) + // ======================================================================== + console.log("\n2️⃣ Testing Length Toggle (Action Bar)..."); + const lengthToggle = await page.$('#lengthToggle'); + if (lengthToggle) { + const paper = await page.$('.cv-paper'); + + // Get initial state + const before = await paper.evaluate(el => ({ + className: el.className, + isLong: el.classList.contains('cv-long'), + isShort: el.classList.contains('cv-short') + })); + + // Click toggle + await lengthToggle.click(); + await page.waitForTimeout(300); // Wait for DOM update + + // Get state after click + const after = await paper.evaluate(el => ({ + className: el.className, + isLong: el.classList.contains('cv-long'), + isShort: el.classList.contains('cv-short') + })); + + // Verify localStorage + const localStorage = await page.evaluate(() => localStorage.getItem('cv-length')); + + const changed = before.isLong !== after.isLong; + const testPassed = changed && (after.isLong ? localStorage === 'long' : localStorage === 'short'); + + console.log(` Before: ${before.isLong ? 'long' : 'short'}`); + console.log(` After: ${after.isLong ? 'long' : 'short'}`); + console.log(` localStorage: ${localStorage}`); + console.log(` Visual change: ${changed ? '✅ YES' : '❌ NO'}`); + console.log(` ${testPassed ? '✅ PASS' : '❌ FAIL'}`); + + testResults.push({ test: 'Length Toggle (Action Bar)', passed: testPassed }); + } else { + console.log(` ❌ Toggle not found`); + testResults.push({ test: 'Length Toggle (Action Bar)', passed: false }); + } + + // ======================================================================== + // TEST 2: Icon Toggle (Action Bar) + // ======================================================================== + console.log("\n3️⃣ Testing Icon/Logo Toggle (Action Bar)..."); + const iconToggle = await page.$('#iconToggle'); + if (iconToggle) { + const paper = await page.$('.cv-paper'); + + // Take screenshot BEFORE toggle + await page.screenshot({ path: 'tests/screenshots/before-icon-toggle.png', fullPage: false }); + + const before = await paper.evaluate(el => ({ + className: el.className, + showIcons: el.classList.contains('show-icons') + })); + + // Click toggle + await iconToggle.click(); + await page.waitForTimeout(300); + + // Take screenshot AFTER toggle + await page.screenshot({ path: 'tests/screenshots/after-icon-toggle.png', fullPage: false }); + + const after = await paper.evaluate(el => ({ + className: el.className, + showIcons: el.classList.contains('show-icons') + })); + + const localStorage = await page.evaluate(() => localStorage.getItem('cv-icons')); + + const changed = before.showIcons !== after.showIcons; + const testPassed = changed && (after.showIcons ? localStorage === 'true' : localStorage === 'false'); + + console.log(` Before: ${before.showIcons ? 'icons shown' : 'icons hidden'}`); + console.log(` After: ${after.showIcons ? 'icons shown' : 'icons hidden'}`); + console.log(` localStorage: ${localStorage}`); + console.log(` Visual change: ${changed ? '✅ YES (no refresh needed)' : '❌ NO (requires refresh - BUG!)'}`); + console.log(` Screenshots saved: before-icon-toggle.png, after-icon-toggle.png`); + console.log(` ${testPassed ? '✅ PASS' : '❌ FAIL'}`); + + testResults.push({ test: 'Icon Toggle (Action Bar)', passed: testPassed }); + } else { + console.log(` ❌ Toggle not found`); + testResults.push({ test: 'Icon Toggle (Action Bar)', passed: false }); + } + + // ======================================================================== + // TEST 3: Theme Toggle (Action Bar) + // ======================================================================== + console.log("\n4️⃣ Testing Theme Toggle (Action Bar)..."); + const themeToggle = await page.$('#themeToggle'); + if (themeToggle) { + const body = await page.$('body'); + + const before = await body.evaluate(el => ({ + className: el.className, + isClean: el.classList.contains('theme-clean') + })); + + await themeToggle.click(); + await page.waitForTimeout(300); + + const after = await body.evaluate(el => ({ + className: el.className, + isClean: el.classList.contains('theme-clean') + })); + + const localStorage = await page.evaluate(() => localStorage.getItem('cv-theme')); + + const changed = before.isClean !== after.isClean; + const testPassed = changed && (after.isClean ? localStorage === 'clean' : localStorage === 'default'); + + console.log(` Before: ${before.isClean ? 'clean' : 'default'}`); + console.log(` After: ${after.isClean ? 'clean' : 'default'}`); + console.log(` localStorage: ${localStorage}`); + console.log(` Visual change: ${changed ? '✅ YES' : '❌ NO'}`); + console.log(` ${testPassed ? '✅ PASS' : '❌ FAIL'}`); + + testResults.push({ test: 'Theme Toggle (Action Bar)', passed: testPassed }); + } else { + console.log(` ❌ Toggle not found`); + testResults.push({ test: 'Theme Toggle (Action Bar)', passed: false }); + } + + // ======================================================================== + // TEST 4: Hamburger Menu Toggles + Synchronization + // ======================================================================== + console.log("\n5️⃣ Testing Hamburger Menu + Toggle Synchronization..."); + const hamburger = await page.$('.hamburger-btn'); + if (hamburger) { + await hamburger.click(); + await page.waitForTimeout(500); + + const menu = await page.$('.navigation-menu'); + const isOpen = await menu.evaluate(el => el.classList.contains('menu-open')); + console.log(` ${isOpen ? '✅ Menu opened' : '❌ Menu failed to open'}`); + + if (isOpen) { + // Test Menu Length Toggle + console.log("\n6️⃣ Testing Length Toggle (Menu)..."); + const menuLengthToggle = await page.$('#lengthToggleMenu'); + if (menuLengthToggle) { + const paper = await page.$('.cv-paper'); + const before = await paper.evaluate(el => el.classList.contains('cv-long')); + + await menuLengthToggle.click(); + await page.waitForTimeout(300); + + const after = await paper.evaluate(el => el.classList.contains('cv-long')); + + // Check if action bar toggle synchronized + const actionBarSynced = await page.$eval('#lengthToggle', el => el.checked); + const menuChecked = await page.$eval('#lengthToggleMenu', el => el.checked); + + const changed = before !== after; + const synced = actionBarSynced === menuChecked; + + console.log(` Visual change: ${changed ? '✅ YES' : '❌ NO'}`); + console.log(` Synchronization: ${synced ? '✅ YES' : '❌ NO'}`); + console.log(` ${changed && synced ? '✅ PASS' : '❌ FAIL'}`); + + testResults.push({ test: 'Length Toggle (Menu + Sync)', passed: changed && synced }); + } + + // Test Menu Icon Toggle + console.log("\n7️⃣ Testing Icon Toggle (Menu)..."); + const menuIconToggle = await page.$('#iconToggleMenu'); + if (menuIconToggle) { + const paper = await page.$('.cv-paper'); + const before = await paper.evaluate(el => el.classList.contains('show-icons')); + + await menuIconToggle.click(); + await page.waitForTimeout(300); + + const after = await paper.evaluate(el => el.classList.contains('show-icons')); + + const actionBarSynced = await page.$eval('#iconToggle', el => el.checked); + const menuChecked = await page.$eval('#iconToggleMenu', el => el.checked); + + const changed = before !== after; + const synced = actionBarSynced === menuChecked; + + console.log(` Visual change: ${changed ? '✅ YES (no refresh!)' : '❌ NO (BUG!)'}`); + console.log(` Synchronization: ${synced ? '✅ YES' : '❌ NO'}`); + console.log(` ${changed && synced ? '✅ PASS' : '❌ FAIL'}`); + + testResults.push({ test: 'Icon Toggle (Menu + Sync)', passed: changed && synced }); + } + + // Test Menu Theme Toggle + console.log("\n8️⃣ Testing Theme Toggle (Menu)..."); + const menuThemeToggle = await page.$('#themeToggleMenu'); + if (menuThemeToggle) { + const body = await page.$('body'); + const before = await body.evaluate(el => el.classList.contains('theme-clean')); + + await menuThemeToggle.click(); + await page.waitForTimeout(300); + + const after = await body.evaluate(el => el.classList.contains('theme-clean')); + + const actionBarSynced = await page.$eval('#themeToggle', el => el.checked); + const menuChecked = await page.$eval('#themeToggleMenu', el => el.checked); + + const changed = before !== after; + const synced = actionBarSynced === menuChecked; + + console.log(` Visual change: ${changed ? '✅ YES' : '❌ NO'}`); + console.log(` Synchronization: ${synced ? '✅ YES' : '❌ NO'}`); + console.log(` ${changed && synced ? '✅ PASS' : '❌ FAIL'}`); + + testResults.push({ test: 'Theme Toggle (Menu + Sync)', passed: changed && synced }); + } + } + } + + // ======================================================================== + // FINAL SUMMARY + // ======================================================================== + console.log("\n" + "=".repeat(70)); + console.log("📊 TEST SUMMARY\n"); + + const totalTests = testResults.length; + const passedTests = testResults.filter(r => r.passed).length; + const failedTests = totalTests - passedTests; + + testResults.forEach(result => { + console.log(` ${result.passed ? '✅' : '❌'} ${result.test}`); + }); + + console.log(`\n Total: ${passedTests}/${totalTests} tests passed`); + + if (errors.length === 0) { + console.log("\n✅ NO CONSOLE ERRORS"); + } else { + console.log(`\n❌ ${errors.length} CONSOLE ERRORS FOUND:\n`); + errors.forEach((err, i) => { + console.log(`${i + 1}. ${err}`); + }); + } + + console.log("=".repeat(70) + "\n"); + + if (failedTests === 0 && errors.length === 0) { + console.log("🎉 ALL TESTS PASSED! All toggles work with real-time rendering."); + } else { + console.log("⚠️ SOME TESTS FAILED - See details above"); + } + + console.log("\nBrowser will stay open for manual inspection."); + console.log("Press Ctrl+C when done.\n"); + + await new Promise(() => {}); // Keep browser open +} + +await testAllToggles(); diff --git a/tests/mjs/README.md b/tests/mjs/README.md new file mode 100644 index 0000000..9711779 --- /dev/null +++ b/tests/mjs/README.md @@ -0,0 +1,63 @@ +# CV Project Test Suite + +Organized test files for the CV application. All tests use Playwright for browser automation. + +## Test Files + +### 0-zoom.test.mjs +**Purpose**: Test zoom control functionality +- Verifies zoom control elements exist +- Tests visibility toggle +- Validates zoom slider interaction +- Checks real-time zoom updates + +**Run**: `bun tests/mjs/0-zoom.test.mjs` + +### 1-toggles.test.mjs +**Purpose**: Comprehensive toggle testing with real-time visual verification +- Tests all 3 toggles (Length, Icons, Theme) +- Validates action bar toggles +- Tests hamburger menu toggles +- Verifies synchronization between action bar and menu +- Checks localStorage persistence +- **Critical**: Validates that toggles update DOM immediately (no refresh needed) +- Takes screenshots for visual comparison + +**Run**: `bun tests/mjs/1-toggles.test.mjs` + +## Running All Tests + +```bash +# Run individual tests +bun tests/mjs/0-zoom.test.mjs +bun tests/mjs/1-toggles.test.mjs + +# Run all tests sequentially +for test in tests/mjs/*.test.mjs; do + echo "Running $test..." + bun "$test" + echo "" +done +``` + +## Test Requirements + +- Server must be running on http://localhost:1999 +- Browser window will stay open after tests for manual verification +- Press Ctrl+C to exit test + +## Test Output + +All tests provide: +- ✅ Clear pass/fail indicators +- 📊 Summary of results +- ❌ Detailed error messages +- 🎉 Success confirmation + +## Screenshots + +Toggle tests save screenshots to `tests/screenshots/`: +- `before-icon-toggle.png` - Before clicking icon toggle +- `after-icon-toggle.png` - After clicking icon toggle + +Use these to visually verify rendering changes.