fix: exclude footer from zoom by moving it outside zoom-wrapper

The footer was being zoomed along with CV content because it was
inside the #zoom-wrapper div. This fix moves the footer outside the
zoom-wrapper so it remains at normal size regardless of zoom level.

Changes:
- Move footer template outside zoom-wrapper in index.html
- Add UI exclusion test (11-zoom-ui-exclusion.test.mjs)

Test coverage:
- Verify footer, action bar, and menu are outside zoom-wrapper (DOM)
- Verify UI elements remain unchanged when zooming to 200%
- Verify CV content properly zooms while UI stays fixed

Before: Footer inside zoom-wrapper (zoomed with content)
After: Footer outside zoom-wrapper (stays normal size)
This commit is contained in:
juanatsap
2025-11-17 17:10:46 +00:00
parent de67c73048
commit 52e97f1411
2 changed files with 245 additions and 2 deletions
+2 -2
View File
@@ -157,10 +157,10 @@
<div class="cv-container">
{{template "cv-content.html" .}}
</div>
{{template "page-footer" .}}
</div> <!-- End zoom-wrapper -->
{{template "page-footer" .}}
{{template "error-toast" .}}
{{template "back-to-top" .}}
{{template "info-button" .}}
+243
View File
@@ -0,0 +1,243 @@
#!/usr/bin/env bun
/**
* ZOOM UI EXCLUSION TEST
* =======================
* Verifies that UI elements (footer, menu, buttons) are NOT affected by zoom
*
* Test Requirements:
* - Footer should NOT be inside #zoom-wrapper
* - Action bar should NOT be inside #zoom-wrapper
* - Hamburger menu should NOT be inside #zoom-wrapper
* - Fixed buttons should NOT be inside #zoom-wrapper
* - Only .cv-container content should be affected by zoom
*
* Test Flow:
* 1. Load page and show zoom control
* 2. Measure initial sizes of UI elements
* 3. Set zoom to 200%
* 4. Verify UI elements remain same size
* 5. Verify CV content has doubled in size
*/
import { chromium } from "playwright";
const URL = "http://localhost:1999";
async function testZoomUIExclusion() {
console.log("🧪 ZOOM UI EXCLUSION TEST\n");
console.log("=".repeat(70));
const browser = await chromium.launch({ headless: false });
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: Verify DOM Structure
// ========================================================================
console.log("\n1️⃣ Verifying DOM structure (UI outside zoom-wrapper)...");
await page.goto(URL);
await page.waitForTimeout(2000);
// Set zoom visible
await page.evaluate(() => localStorage.setItem('cv-zoom-visible', 'true'));
await page.reload();
await page.waitForTimeout(2000);
// Check that footer is NOT inside zoom-wrapper
const footerOutsideZoom = await page.evaluate(() => {
const footer = document.querySelector('.page-footer');
const zoomWrapper = document.querySelector('#zoom-wrapper');
if (!footer || !zoomWrapper) return false;
return !zoomWrapper.contains(footer);
});
// Check that action bar is NOT inside zoom-wrapper
const actionBarOutsideZoom = await page.evaluate(() => {
const actionBar = document.querySelector('.action-bar');
const zoomWrapper = document.querySelector('#zoom-wrapper');
if (!actionBar || !zoomWrapper) return false;
return !zoomWrapper.contains(actionBar);
});
// Check that hamburger menu is NOT inside zoom-wrapper
const menuOutsideZoom = await page.evaluate(() => {
const menu = document.querySelector('.navigation-menu');
const zoomWrapper = document.querySelector('#zoom-wrapper');
if (!menu || !zoomWrapper) return false;
return !zoomWrapper.contains(menu);
});
// Check that CV content IS inside zoom-wrapper
const cvInsideZoom = await page.evaluate(() => {
const cv = document.querySelector('.cv-container');
const zoomWrapper = document.querySelector('#zoom-wrapper');
if (!cv || !zoomWrapper) return false;
return zoomWrapper.contains(cv);
});
const test1Passed = footerOutsideZoom && actionBarOutsideZoom && menuOutsideZoom && cvInsideZoom;
console.log(` Footer outside zoom-wrapper: ${footerOutsideZoom ? '✅ YES' : '❌ NO'}`);
console.log(` Action bar outside zoom-wrapper: ${actionBarOutsideZoom ? '✅ YES' : '❌ NO'}`);
console.log(` Menu outside zoom-wrapper: ${menuOutsideZoom ? '✅ YES' : '❌ NO'}`);
console.log(` CV content inside zoom-wrapper: ${cvInsideZoom ? '✅ YES' : '❌ NO'}`);
console.log(` ${test1Passed ? '✅ PASS' : '❌ FAIL'}`);
testResults.push({ test: 'DOM Structure - UI Outside Zoom', passed: test1Passed });
// ========================================================================
// TEST 2: Measure UI Elements at 100% Zoom
// ========================================================================
console.log("\n2️⃣ Measuring UI elements at 100% zoom...");
const initialSizes = await page.evaluate(() => {
const footer = document.querySelector('.page-footer');
const actionBar = document.querySelector('.action-bar');
const cvPaper = document.querySelector('.cv-paper');
return {
footerHeight: footer ? footer.offsetHeight : 0,
actionBarHeight: actionBar ? actionBar.offsetHeight : 0,
cvWidth: cvPaper ? cvPaper.offsetWidth : 0,
};
});
console.log(` Footer height: ${initialSizes.footerHeight}px`);
console.log(` Action bar height: ${initialSizes.actionBarHeight}px`);
console.log(` CV width: ${initialSizes.cvWidth}px`);
// ========================================================================
// TEST 3: Set Zoom to 200% and Re-measure
// ========================================================================
console.log("\n3️⃣ Setting zoom to 200% and re-measuring...");
const slider = await page.$('#zoom-slider');
if (slider) {
await slider.evaluate(el => {
el.value = '200';
el.dispatchEvent(new Event('input', { bubbles: true }));
});
await page.waitForTimeout(500);
}
const zoomedSizes = await page.evaluate(() => {
const footer = document.querySelector('.page-footer');
const actionBar = document.querySelector('.action-bar');
const cvPaper = document.querySelector('.cv-paper');
return {
footerHeight: footer ? footer.offsetHeight : 0,
actionBarHeight: actionBar ? actionBar.offsetHeight : 0,
cvWidth: cvPaper ? cvPaper.offsetWidth : 0,
};
});
console.log(` Footer height: ${zoomedSizes.footerHeight}px`);
console.log(` Action bar height: ${zoomedSizes.actionBarHeight}px`);
console.log(` CV width: ${zoomedSizes.cvWidth}px`);
// ========================================================================
// TEST 4: Verify UI Elements Unchanged
// ========================================================================
console.log("\n4️⃣ Verifying UI elements unchanged at 200% zoom...");
const footerUnchanged = Math.abs(initialSizes.footerHeight - zoomedSizes.footerHeight) < 5;
const actionBarUnchanged = Math.abs(initialSizes.actionBarHeight - zoomedSizes.actionBarHeight) < 5;
// CV should be approximately doubled (with some tolerance)
const cvGrew = zoomedSizes.cvWidth > initialSizes.cvWidth * 1.8;
const test4Passed = footerUnchanged && actionBarUnchanged && cvGrew;
console.log(` Footer unchanged: ${footerUnchanged ? '✅ YES' : '❌ NO'} (${initialSizes.footerHeight}px → ${zoomedSizes.footerHeight}px)`);
console.log(` Action bar unchanged: ${actionBarUnchanged ? '✅ YES' : '❌ NO'} (${initialSizes.actionBarHeight}px → ${zoomedSizes.actionBarHeight}px)`);
console.log(` CV content grew: ${cvGrew ? '✅ YES' : '❌ NO'} (${initialSizes.cvWidth}px → ${zoomedSizes.cvWidth}px)`);
console.log(` ${test4Passed ? '✅ PASS' : '❌ FAIL'}`);
testResults.push({ test: 'UI Elements Unchanged at 200%', passed: test4Passed });
// ========================================================================
// TEST 5: Reset and Verify
// ========================================================================
console.log("\n5️⃣ Resetting zoom to 100%...");
const resetBtn = await page.$('#zoom-reset');
if (resetBtn) {
await resetBtn.click();
await page.waitForTimeout(500);
}
const resetSizes = await page.evaluate(() => {
const footer = document.querySelector('.page-footer');
const actionBar = document.querySelector('.action-bar');
const cvPaper = document.querySelector('.cv-paper');
return {
footerHeight: footer ? footer.offsetHeight : 0,
actionBarHeight: actionBar ? actionBar.offsetHeight : 0,
cvWidth: cvPaper ? cvPaper.offsetWidth : 0,
};
});
const resetPassed = Math.abs(resetSizes.cvWidth - initialSizes.cvWidth) < 10;
console.log(` CV width restored: ${resetPassed ? '✅ YES' : '❌ NO'} (${resetSizes.cvWidth}px vs ${initialSizes.cvWidth}px initial)`);
console.log(` ${resetPassed ? '✅ PASS' : '❌ FAIL'}`);
testResults.push({ test: 'Zoom Reset to 100%', passed: resetPassed });
// ========================================================================
// 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! UI elements properly excluded from zoom.");
} 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 testZoomUIExclusion();