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
|
||||
- ✅ **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
|
||||
|
||||
@@ -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()')
|
||||
*/
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
|
||||
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