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:
@@ -157,9 +157,9 @@
|
||||
<div class="cv-container">
|
||||
{{template "cv-content.html" .}}
|
||||
</div>
|
||||
</div> <!-- End zoom-wrapper -->
|
||||
|
||||
{{template "page-footer" .}}
|
||||
</div> <!-- End zoom-wrapper -->
|
||||
|
||||
{{template "error-toast" .}}
|
||||
{{template "back-to-top" .}}
|
||||
|
||||
@@ -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();
|
||||
Reference in New Issue
Block a user