fix: restore zoom level persistence on page load
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()')
This commit is contained in:
@@ -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
|
- ✅ **Browser Print** - Alternative print-friendly layout for manual PDF creation
|
||||||
- ✅ **HTMX Dynamic Updates** - Smooth UX without heavy JavaScript
|
- ✅ **HTMX Dynamic Updates** - Smooth UX without heavy JavaScript
|
||||||
- ✅ **Paper Design** - Professional CV on elegant white paper with gray background
|
- ✅ **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
|
- ✅ **Responsive** - Mobile, tablet, and desktop friendly
|
||||||
- ✅ **JSON-Based Content** - Easy to update without touching code
|
- ✅ **JSON-Based Content** - Easy to update without touching code
|
||||||
- ✅ **AI Development Section** - Showcases modern AI-assisted development skills
|
- ✅ **AI Development Section** - Showcases modern AI-assisted development skills
|
||||||
|
|||||||
@@ -7,10 +7,12 @@
|
|||||||
* These are thin JavaScript wrappers that delegate to Hyperscript functions.
|
* These are thin JavaScript wrappers that delegate to Hyperscript functions.
|
||||||
*
|
*
|
||||||
* Why wrappers are needed:
|
* Why wrappers are needed:
|
||||||
* - Hyperscript `call` command requires functions in global JavaScript scope
|
* - Hyperscript's `call` command within `_=""` attributes requires functions in global JS scope
|
||||||
* - Hyperscript `def` functions are NOT automatically exposed to window
|
* - While hyperscript docs state "global hyperscript functions can be called from JavaScript",
|
||||||
* - Templates use `_="on mouseenter call syncPdfHover(true)"`
|
* the reverse (JS calling hyperscript via `call` in attributes) requires window exposure
|
||||||
* - This syntax expects a JavaScript function, not a hyperscript def
|
* - 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:
|
* Implementation in Hyperscript:
|
||||||
* - toggleCVLength() → static/hyperscript/toggles._hs
|
* - toggleCVLength() → static/hyperscript/toggles._hs
|
||||||
@@ -20,7 +22,7 @@
|
|||||||
* - syncPrintHover() → static/hyperscript/hover-sync._hs
|
* - syncPrintHover() → static/hyperscript/hover-sync._hs
|
||||||
* - highlightZoomControl() → 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()')
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
end
|
end
|
||||||
set savedZoom to localStorage.getItem('cv-zoom')
|
set savedZoom to localStorage.getItem('cv-zoom')
|
||||||
if savedZoom
|
if savedZoom
|
||||||
set my value to savedZoom
|
set #zoom-slider's value to savedZoom
|
||||||
send input to #zoom-slider
|
send input to #zoom-slider
|
||||||
end
|
end
|
||||||
-- Check visibility preference: show only if explicitly enabled or first visit
|
-- Check visibility preference: show only if explicitly enabled or first visit
|
||||||
|
|||||||
Executable
+224
@@ -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();
|
||||||
Reference in New Issue
Block a user