From 35a836adf3ddf2888c27a8245324572b7411eaaf Mon Sep 17 00:00:00 2001 From: juanatsap Date: Mon, 17 Nov 2025 16:56:01 +0000 Subject: [PATCH] fix: restore zoom level persistence on page load MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Zoom level persistence was broken because hyperscript was setting the container's value instead of the slider's value on page load. Changes: - Fix zoom-control.html line 10: set #zoom-slider's value (not 'my value') - Add comprehensive zoom persistence test (10-zoom-persistence.test.mjs) - Update cv-functions.js documentation to clarify hyperscript interop - Add zoom control feature to README Test results: 5/5 tests pass - Zoom saves to localStorage when changed ✅ - Zoom restores correctly on page reload ✅ - Reset to 100% works and persists ✅ Architecture note: - Hyperscript 'call' within _="" attributes requires global JS scope - JavaScript wrappers bridge window exposure to hyperscript evaluate() - Pattern: window.fn() → _hyperscript.evaluate('hyperscriptFn()') --- README.md | 1 + static/js/cv-functions.js | 12 +- templates/partials/widgets/zoom-control.html | 2 +- tests/mjs/10-zoom-persistence.test.mjs | 224 +++++++++++++++++++ 4 files changed, 233 insertions(+), 6 deletions(-) create mode 100755 tests/mjs/10-zoom-persistence.test.mjs diff --git a/README.md b/README.md index 21c14fa..5d95c96 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ A professional, bilingual CV site with server-side PDF generation, HTMX interact - ✅ **Browser Print** - Alternative print-friendly layout for manual PDF creation - ✅ **HTMX Dynamic Updates** - Smooth UX without heavy JavaScript - ✅ **Paper Design** - Professional CV on elegant white paper with gray background +- ✅ **Zoom Control** - Adjustable zoom (25%-175%) with persistence across sessions - ✅ **Responsive** - Mobile, tablet, and desktop friendly - ✅ **JSON-Based Content** - Easy to update without touching code - ✅ **AI Development Section** - Showcases modern AI-assisted development skills diff --git a/static/js/cv-functions.js b/static/js/cv-functions.js index a95799b..5205df0 100644 --- a/static/js/cv-functions.js +++ b/static/js/cv-functions.js @@ -7,10 +7,12 @@ * These are thin JavaScript wrappers that delegate to Hyperscript functions. * * Why wrappers are needed: - * - Hyperscript `call` command requires functions in global JavaScript scope - * - Hyperscript `def` functions are NOT automatically exposed to window - * - Templates use `_="on mouseenter call syncPdfHover(true)"` - * - This syntax expects a JavaScript function, not a hyperscript def + * - Hyperscript's `call` command within `_=""` attributes requires functions in global JS scope + * - While hyperscript docs state "global hyperscript functions can be called from JavaScript", + * the reverse (JS calling hyperscript via `call` in attributes) requires window exposure + * - Templates use `_="on mouseenter call syncPdfHover(true)"` syntax + * - Hyperscript `def` functions are accessible via _hyperscript.evaluate() but not window.functionName + * - These wrappers bridge the gap by exposing to window and delegating to hyperscript * * Implementation in Hyperscript: * - toggleCVLength() → static/hyperscript/toggles._hs @@ -20,7 +22,7 @@ * - syncPrintHover() → static/hyperscript/hover-sync._hs * - highlightZoomControl() → static/hyperscript/hover-sync._hs * - * These wrappers call the hyperscript implementations via _hyperscript API. + * Pattern: window.functionName() → _hyperscript.evaluate('hyperscriptFunction()') */ /** diff --git a/templates/partials/widgets/zoom-control.html b/templates/partials/widgets/zoom-control.html index 6e19ef9..92770af 100644 --- a/templates/partials/widgets/zoom-control.html +++ b/templates/partials/widgets/zoom-control.html @@ -7,7 +7,7 @@ end set savedZoom to localStorage.getItem('cv-zoom') if savedZoom - set my value to savedZoom + set #zoom-slider's value to savedZoom send input to #zoom-slider end -- Check visibility preference: show only if explicitly enabled or first visit diff --git a/tests/mjs/10-zoom-persistence.test.mjs b/tests/mjs/10-zoom-persistence.test.mjs new file mode 100755 index 0000000..4d9c822 --- /dev/null +++ b/tests/mjs/10-zoom-persistence.test.mjs @@ -0,0 +1,224 @@ +#!/usr/bin/env bun +/** + * ZOOM PERSISTENCE TEST + * ====================== + * Tests that zoom level is persisted to localStorage and restored on page load + * + * Test Flow: + * 1. Load page (desktop viewport >768px) + * 2. Show zoom control + * 3. Change zoom level to 150% + * 4. Verify localStorage updated + * 5. Reload page + * 6. Verify zoom level restored to 150% + * 7. Reset zoom to 100% + * 8. Reload page + * 9. Verify zoom level restored to 100% + */ + +import { chromium } from "playwright"; + +const URL = "http://localhost:1999"; + +async function testZoomPersistence() { + console.log("🧪 ZOOM PERSISTENCE TEST\n"); + console.log("=".repeat(70)); + + const browser = await chromium.launch({ headless: false }); + // Use desktop viewport (>768px) to enable zoom control + const page = await browser.newPage({ viewport: { width: 1400, 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}`); + }); + + // ======================================================================== + // TEST 1: Show Zoom Control and Verify Initial State + // ======================================================================== + console.log("\n1️⃣ Loading page and showing zoom control..."); + await page.goto(URL); + await page.waitForTimeout(2000); + + // Clear any existing zoom localStorage + await page.evaluate(() => { + localStorage.removeItem('cv-zoom'); + localStorage.setItem('cv-zoom-visible', 'true'); + }); + + // Reload to apply clean state + await page.reload(); + await page.waitForTimeout(2000); + + const zoomControl = await page.$('#zoom-control'); + const isVisible = await zoomControl.evaluate(el => !el.classList.contains('zoom-hidden')); + + console.log(` Zoom control visible: ${isVisible ? '✅ YES' : '❌ NO'}`); + testResults.push({ test: 'Zoom Control Visibility', passed: isVisible }); + + // ======================================================================== + // TEST 2: Change Zoom to 150% and Verify localStorage + // ======================================================================== + console.log("\n2️⃣ Setting zoom to 150%..."); + const slider = await page.$('#zoom-slider'); + + if (slider) { + // Get initial zoom + const initialZoom = await slider.evaluate(el => el.value); + console.log(` Initial zoom: ${initialZoom}%`); + + // Set zoom to 150% + await slider.evaluate(el => { + el.value = '150'; + el.dispatchEvent(new Event('input', { bubbles: true })); + }); + await page.waitForTimeout(500); + + // Verify localStorage + const storedZoom = await page.evaluate(() => localStorage.getItem('cv-zoom')); + const displayedZoom = await page.$eval('#zoom-value-current', el => el.textContent); + const sliderValue = await slider.evaluate(el => el.value); + + const test2Passed = storedZoom === '150' && displayedZoom === '150' && sliderValue === '150'; + + console.log(` Slider value: ${sliderValue}%`); + console.log(` Displayed zoom: ${displayedZoom}%`); + console.log(` localStorage: ${storedZoom}%`); + console.log(` ${test2Passed ? '✅ PASS' : '❌ FAIL'}`); + + testResults.push({ test: 'Zoom Change and localStorage Save', passed: test2Passed }); + } else { + console.log(` ❌ Zoom slider not found`); + testResults.push({ test: 'Zoom Change and localStorage Save', passed: false }); + } + + // ======================================================================== + // TEST 3: Reload Page and Verify Zoom Restored + // ======================================================================== + console.log("\n3️⃣ Reloading page to verify zoom persistence..."); + await page.reload(); + await page.waitForTimeout(2000); + + const restoredSlider = await page.$('#zoom-slider'); + if (restoredSlider) { + const restoredValue = await restoredSlider.evaluate(el => el.value); + const restoredDisplay = await page.$eval('#zoom-value-current', el => el.textContent); + const restoredLocalStorage = await page.evaluate(() => localStorage.getItem('cv-zoom')); + + const test3Passed = restoredValue === '150' && restoredDisplay === '150' && restoredLocalStorage === '150'; + + console.log(` Restored slider: ${restoredValue}%`); + console.log(` Restored display: ${restoredDisplay}%`); + console.log(` localStorage: ${restoredLocalStorage}%`); + console.log(` ${test3Passed ? '✅ PASS - Zoom persisted correctly!' : '❌ FAIL'}`); + + testResults.push({ test: 'Zoom Persistence After Reload', passed: test3Passed }); + } else { + console.log(` ❌ Zoom slider not found after reload`); + testResults.push({ test: 'Zoom Persistence After Reload', passed: false }); + } + + // ======================================================================== + // TEST 4: Reset Zoom and Verify Persistence + // ======================================================================== + console.log("\n4️⃣ Testing zoom reset to 100%..."); + const resetBtn = await page.$('#zoom-reset'); + + if (resetBtn) { + await resetBtn.click(); + await page.waitForTimeout(500); + + const resetValue = await page.$eval('#zoom-slider', el => el.value); + const resetDisplay = await page.$eval('#zoom-value-current', el => el.textContent); + const resetLocalStorage = await page.evaluate(() => localStorage.getItem('cv-zoom')); + + const test4Passed = resetValue === '100' && resetDisplay === '100' && resetLocalStorage === '100'; + + console.log(` Reset slider: ${resetValue}%`); + console.log(` Reset display: ${resetDisplay}%`); + console.log(` localStorage: ${resetLocalStorage}%`); + console.log(` ${test4Passed ? '✅ PASS' : '❌ FAIL'}`); + + testResults.push({ test: 'Zoom Reset to 100%', passed: test4Passed }); + } else { + console.log(` ❌ Reset button not found`); + testResults.push({ test: 'Zoom Reset to 100%', passed: false }); + } + + // ======================================================================== + // TEST 5: Reload and Verify 100% Persisted + // ======================================================================== + console.log("\n5️⃣ Reloading page to verify 100% zoom persisted..."); + await page.reload(); + await page.waitForTimeout(2000); + + const finalSlider = await page.$('#zoom-slider'); + if (finalSlider) { + const finalValue = await finalSlider.evaluate(el => el.value); + const finalDisplay = await page.$eval('#zoom-value-current', el => el.textContent); + const finalLocalStorage = await page.evaluate(() => localStorage.getItem('cv-zoom')); + + const test5Passed = finalValue === '100' && finalDisplay === '100' && finalLocalStorage === '100'; + + console.log(` Final slider: ${finalValue}%`); + console.log(` Final display: ${finalDisplay}%`); + console.log(` localStorage: ${finalLocalStorage}%`); + console.log(` ${test5Passed ? '✅ PASS' : '❌ FAIL'}`); + + testResults.push({ test: 'Reset Zoom Persistence After Reload', passed: test5Passed }); + } else { + console.log(` ❌ Zoom slider not found after reload`); + testResults.push({ test: 'Reset Zoom Persistence After Reload', passed: false }); + } + + // ======================================================================== + // 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! Zoom persistence works correctly."); + } 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 testZoomPersistence();