75efeb1474
Fixes info modal positioning to be perfectly centered on mobile devices in all orientations. ISSUE: - Info modal was not centered on mobile viewports - User reported "pop-up of information in mobile it is not centered" - Modal positioning relied on inset:0 + margin:auto which doesn't work consistently on mobile devices FIX: - Added explicit mobile centering using transform translate(-50%, -50%) - Position: top: 50%, left: 50% with transform centering - Applied to all modals: info, keyboard shortcuts, and PDF download - Added mobile-specific fade-in animation preserving centering - Constrained modal to viewport with calc(100vw - 2rem) width/height Files modified: - static/css/04-interactive/_modals.css - Mobile centering for all modals - tests/mjs/58-modal-centering-test.mjs - Validation test Test results: ✅ Portrait (375×667): Perfect center - 0px offset ✅ Landscape (667×375): Perfect center - 0px offset ✅ Modal center matches viewport center exactly
152 lines
6.1 KiB
JavaScript
Executable File
152 lines
6.1 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
||
|
||
import { chromium } from 'playwright';
|
||
|
||
const MOBILE_VIEWPORT = { width: 375, height: 667 };
|
||
const LANDSCAPE_VIEWPORT = { width: 667, height: 375 };
|
||
|
||
(async () => {
|
||
const browser = await chromium.launch({ headless: true });
|
||
|
||
console.log('🧪 Testing Modal Centering on Mobile\n');
|
||
|
||
// TEST 1: Portrait Mode
|
||
console.log('📱 TEST 1: Portrait Mode (375×667)\n');
|
||
const portraitContext = await browser.newContext({
|
||
viewport: MOBILE_VIEWPORT,
|
||
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.0 Mobile/15E148 Safari/604.1',
|
||
hasTouch: true
|
||
});
|
||
const portraitPage = await portraitContext.newPage();
|
||
|
||
await portraitPage.goto('http://localhost:1999/?lang=en&view=extended');
|
||
await portraitPage.waitForLoadState('networkidle');
|
||
|
||
// Click info button to open modal
|
||
await portraitPage.click('.info-button');
|
||
await portraitPage.waitForTimeout(500); // Wait for animation
|
||
|
||
const portraitModal = await portraitPage.evaluate(() => {
|
||
const modal = document.querySelector('.info-modal');
|
||
if (!modal) return null;
|
||
|
||
const rect = modal.getBoundingClientRect();
|
||
const viewport = {
|
||
width: window.innerWidth,
|
||
height: window.innerHeight
|
||
};
|
||
|
||
const modalCenterX = rect.left + rect.width / 2;
|
||
const modalCenterY = rect.top + rect.height / 2;
|
||
const viewportCenterX = viewport.width / 2;
|
||
const viewportCenterY = viewport.height / 2;
|
||
|
||
return {
|
||
viewport,
|
||
modal: {
|
||
left: Math.round(rect.left),
|
||
top: Math.round(rect.top),
|
||
width: Math.round(rect.width),
|
||
height: Math.round(rect.height),
|
||
centerX: Math.round(modalCenterX),
|
||
centerY: Math.round(modalCenterY)
|
||
},
|
||
viewportCenter: {
|
||
x: Math.round(viewportCenterX),
|
||
y: Math.round(viewportCenterY)
|
||
},
|
||
offsetX: Math.round(Math.abs(modalCenterX - viewportCenterX)),
|
||
offsetY: Math.round(Math.abs(modalCenterY - viewportCenterY)),
|
||
isCentered: Math.abs(modalCenterX - viewportCenterX) < 5 && Math.abs(modalCenterY - viewportCenterY) < 5
|
||
};
|
||
});
|
||
|
||
console.log('Portrait Modal:');
|
||
console.log(` • Viewport: ${portraitModal.viewport.width}×${portraitModal.viewport.height}`);
|
||
console.log(` • Modal size: ${portraitModal.modal.width}×${portraitModal.modal.height}`);
|
||
console.log(` • Modal center: (${portraitModal.modal.centerX}, ${portraitModal.modal.centerY})`);
|
||
console.log(` • Viewport center: (${portraitModal.viewportCenter.x}, ${portraitModal.viewportCenter.y})`);
|
||
console.log(` • Offset: X=${portraitModal.offsetX}px, Y=${portraitModal.offsetY}px`);
|
||
console.log(` • Is centered: ${portraitModal.isCentered ? '✅' : '❌'}\n`);
|
||
|
||
await portraitPage.screenshot({
|
||
path: 'tests/screenshots/modal-portrait.png',
|
||
fullPage: true
|
||
});
|
||
|
||
await portraitContext.close();
|
||
|
||
// TEST 2: Landscape Mode
|
||
console.log('📱 TEST 2: Landscape Mode (667×375)\n');
|
||
const landscapeContext = await browser.newContext({
|
||
viewport: LANDSCAPE_VIEWPORT,
|
||
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.0 Mobile/15E148 Safari/604.1',
|
||
hasTouch: true
|
||
});
|
||
const landscapePage = await landscapeContext.newPage();
|
||
|
||
await landscapePage.goto('http://localhost:1999/?lang=en&view=extended');
|
||
await landscapePage.waitForLoadState('networkidle');
|
||
|
||
// Click info button to open modal
|
||
await landscapePage.click('.info-button');
|
||
await landscapePage.waitForTimeout(500); // Wait for animation
|
||
|
||
const landscapeModal = await landscapePage.evaluate(() => {
|
||
const modal = document.querySelector('.info-modal');
|
||
if (!modal) return null;
|
||
|
||
const rect = modal.getBoundingClientRect();
|
||
const viewport = {
|
||
width: window.innerWidth,
|
||
height: window.innerHeight
|
||
};
|
||
|
||
const modalCenterX = rect.left + rect.width / 2;
|
||
const modalCenterY = rect.top + rect.height / 2;
|
||
const viewportCenterX = viewport.width / 2;
|
||
const viewportCenterY = viewport.height / 2;
|
||
|
||
return {
|
||
viewport,
|
||
modal: {
|
||
left: Math.round(rect.left),
|
||
top: Math.round(rect.top),
|
||
width: Math.round(rect.width),
|
||
height: Math.round(rect.height),
|
||
centerX: Math.round(modalCenterX),
|
||
centerY: Math.round(modalCenterY)
|
||
},
|
||
viewportCenter: {
|
||
x: Math.round(viewportCenterX),
|
||
y: Math.round(viewportCenterY)
|
||
},
|
||
offsetX: Math.round(Math.abs(modalCenterX - viewportCenterX)),
|
||
offsetY: Math.round(Math.abs(modalCenterY - viewportCenterY)),
|
||
isCentered: Math.abs(modalCenterX - viewportCenterX) < 5 && Math.abs(modalCenterY - viewportCenterY) < 5
|
||
};
|
||
});
|
||
|
||
console.log('Landscape Modal:');
|
||
console.log(` • Viewport: ${landscapeModal.viewport.width}×${landscapeModal.viewport.height}`);
|
||
console.log(` • Modal size: ${landscapeModal.modal.width}×${landscapeModal.modal.height}`);
|
||
console.log(` • Modal center: (${landscapeModal.modal.centerX}, ${landscapeModal.modal.centerY})`);
|
||
console.log(` • Viewport center: (${landscapeModal.viewportCenter.x}, ${landscapeModal.viewportCenter.y})`);
|
||
console.log(` • Offset: X=${landscapeModal.offsetX}px, Y=${landscapeModal.offsetY}px`);
|
||
console.log(` • Is centered: ${landscapeModal.isCentered ? '✅' : '❌'}\n`);
|
||
|
||
await landscapePage.screenshot({
|
||
path: 'tests/screenshots/modal-landscape.png',
|
||
fullPage: true
|
||
});
|
||
|
||
await landscapeContext.close();
|
||
|
||
const allPassed = portraitModal.isCentered && landscapeModal.isCentered;
|
||
|
||
console.log(`${allPassed ? '✅' : '❌'} Tests ${allPassed ? 'PASSED' : 'FAILED'}\n`);
|
||
|
||
await browser.close();
|
||
process.exit(allPassed ? 0 : 1);
|
||
})();
|