fix: Complete mobile UX overhaul - horizontal scroll, landscape mode, and centering
Fixes three critical mobile issues across Android and iPhone: 1. HORIZONTAL SCROLL ELIMINATION (CRITICAL) - Added overflow-x: hidden to html and body globally - Landscape: Aggressive max-width: 100vw on all containers - Fixed .cv-page, .cv-container overflow issues - Prevented scale transform from causing overflow 2. LANDSCAPE MODE COMPLETE FIX - Single column layout enforced (grid-template-columns: 1fr) - Photo visible and sized appropriately (max-width: 120px) - Hamburger menu visible and accessible - Action bar simplified (center controls hidden) - Language selector compact - Smaller buttons (40px) with recalculated positions - Typography reduced for better fit 3. BUTTON CENTERING (VERIFIED WORKING) - Confirmed perfect centering in portrait mode - Android: 290px bar centered at viewport center (188px) - iPhone: Identical centering behavior - Landscape: 240px bar for 5 buttons (40px each) Files modified: - static/css/01-foundation/_reset.css - Global overflow-x fix - static/css/05-responsive/_breakpoints.css - Comprehensive landscape overhaul - tests/mjs/54-landscape-mode-test.mjs - Landscape validation (Android + iPhone) - tests/mjs/55-button-centering-test.mjs - Button centering validation - tests/mjs/56-landscape-debug-test.mjs - Media query debugging tool - tests/mjs/57-horizontal-scroll-debug.mjs - Scroll width debugging tool Test results: ✅ Portrait: Buttons perfectly centered (0px offset) ✅ Landscape: Single column, no horizontal scroll ✅ Hamburger visible and accessible in landscape ✅ Photo visible in all orientations ✅ Android + iPhone parity confirmed
This commit is contained in:
@@ -25,13 +25,16 @@ body {
|
||||
background-image: var(--page-bg-pattern);
|
||||
background-size: 40px 40px; /* For dark theme diagonal grid */
|
||||
background-attachment: fixed;
|
||||
overflow-x: auto;
|
||||
max-width: 100vw; /* Prevent horizontal overflow */
|
||||
overflow-x: hidden; /* NO horizontal scroll on mobile */
|
||||
}
|
||||
|
||||
/* Smooth scrolling */
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
scroll-padding-top: 70px; /* Account for fixed header */
|
||||
max-width: 100vw; /* Prevent horizontal overflow */
|
||||
overflow-x: hidden; /* NO horizontal scroll */
|
||||
}
|
||||
|
||||
/* Ensure Iconify icons display properly */
|
||||
|
||||
@@ -697,15 +697,83 @@
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Landscape Orientation Fixes
|
||||
Landscape Orientation Fixes (Mobile Devices)
|
||||
======================================== */
|
||||
|
||||
@media (max-width: 915px) and (orientation: landscape) {
|
||||
/* CRITICAL: Prevent horizontal scroll in landscape */
|
||||
* {
|
||||
max-width: 100vw !important;
|
||||
}
|
||||
|
||||
html, body {
|
||||
width: 100vw !important;
|
||||
max-width: 100vw !important;
|
||||
overflow-x: hidden !important;
|
||||
}
|
||||
|
||||
.cv-container {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
overflow-x: hidden !important;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.cv-page {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
margin: 0 !important;
|
||||
transform: scale(1) !important; /* Reset zoom that might cause overflow */
|
||||
box-shadow: none !important; /* Remove shadow that might cause overflow */
|
||||
}
|
||||
|
||||
.page-content {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
overflow-x: hidden !important;
|
||||
}
|
||||
|
||||
/* Hide elements that might cause overflow */
|
||||
.action-bar, .cv-header, .cv-sidebar, .cv-main {
|
||||
max-width: 100% !important;
|
||||
overflow-x: hidden !important;
|
||||
}
|
||||
|
||||
/* Force single column layout in landscape mobile */
|
||||
.cv-page .page-1 .page-content,
|
||||
.cv-page .page-2 .page-content,
|
||||
.page-1 .page-content,
|
||||
.page-2 .page-content {
|
||||
grid-template-columns: 1fr !important;
|
||||
grid-template-rows: auto auto;
|
||||
grid-template-rows: auto auto !important;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
/* Stack elements vertically in landscape: sidebar -> main */
|
||||
.page-1 .cv-sidebar-left {
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
order: 1;
|
||||
}
|
||||
|
||||
.page-1 .cv-main {
|
||||
grid-column: 1;
|
||||
grid-row: 2;
|
||||
order: 2;
|
||||
}
|
||||
|
||||
/* Stack elements vertically in landscape: main -> sidebar */
|
||||
.page-2 .cv-main {
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
order: 1;
|
||||
}
|
||||
|
||||
.page-2 .cv-sidebar-right {
|
||||
grid-column: 1;
|
||||
grid-row: 2;
|
||||
order: 2;
|
||||
}
|
||||
|
||||
/* Reduce header size in landscape */
|
||||
@@ -714,28 +782,120 @@
|
||||
}
|
||||
|
||||
.cv-name {
|
||||
font-size: 1.2rem !important;
|
||||
font-size: 1.4rem !important;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.years-experience {
|
||||
font-size: 0.9em !important;
|
||||
font-size: 1em !important;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Reduce photo size in landscape to ~50% width but keep visible */
|
||||
.cv-header-content {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
/* Reduce photo size in landscape to 50% */
|
||||
.cv-photo {
|
||||
width: 50% !important;
|
||||
position: static !important;
|
||||
width: auto !important;
|
||||
height: auto !important;
|
||||
max-width: 80px !important;
|
||||
max-width: 120px !important;
|
||||
margin: 0.5rem auto !important;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Compact action bar */
|
||||
.cv-photo img {
|
||||
width: 100% !important;
|
||||
height: auto !important;
|
||||
}
|
||||
|
||||
/* Compact action bar - keep hamburger menu visible */
|
||||
.action-bar {
|
||||
padding: 0.5rem 1rem !important;
|
||||
padding: 0.5rem 0.75rem !important;
|
||||
}
|
||||
|
||||
/* Simplify action bar for landscape */
|
||||
.action-bar-content {
|
||||
grid-template-columns: 1fr !important;
|
||||
gap: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Hide center controls on landscape mobile (moved to hamburger menu) */
|
||||
.view-controls-center {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Hide action buttons on landscape (available in hamburger menu) */
|
||||
.action-buttons-right {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Keep site title visible and compact */
|
||||
.site-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding: 0 0.5rem;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.site-title-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.site-title-text {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 0.95rem !important;
|
||||
}
|
||||
|
||||
/* Language selector compact */
|
||||
.language-selector {
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
margin-left: auto;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.language-selector .selector-btn {
|
||||
padding: 0.3rem 0.6rem !important;
|
||||
font-size: 0.85rem !important;
|
||||
min-width: 35px !important;
|
||||
}
|
||||
|
||||
/* Hide year from title in landscape mobile */
|
||||
.site-title-year {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Show mobile icon in title */
|
||||
.site-logo-link {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.site-icon-mobile {
|
||||
display: inline-flex !important;
|
||||
}
|
||||
|
||||
/* Reduce sidebar padding */
|
||||
.cv-sidebar {
|
||||
padding: 1rem !important;
|
||||
padding: 0.75rem !important;
|
||||
}
|
||||
|
||||
/* Compact accordion headers in landscape */
|
||||
.sidebar-accordion summary.sidebar-accordion-header {
|
||||
padding: 6px 12px !important;
|
||||
font-size: 0.8em !important;
|
||||
}
|
||||
|
||||
/* Compact sections */
|
||||
@@ -749,34 +909,97 @@
|
||||
.project-item,
|
||||
.course-item {
|
||||
margin-bottom: 1rem !important;
|
||||
padding-bottom: 1rem !important;
|
||||
}
|
||||
|
||||
/* Make hamburger menu more accessible in landscape */
|
||||
.hamburger-menu {
|
||||
position: fixed !important;
|
||||
top: 10px !important;
|
||||
left: 10px !important;
|
||||
/* Reduce font sizes for landscape */
|
||||
.experience-period,
|
||||
.experience-location,
|
||||
.position {
|
||||
font-size: 0.85rem !important;
|
||||
}
|
||||
|
||||
.short-desc,
|
||||
.responsibilities li {
|
||||
font-size: 0.8rem !important;
|
||||
}
|
||||
|
||||
/* Make hamburger button accessible in landscape */
|
||||
.hamburger-btn {
|
||||
display: flex !important; /* Force visible */
|
||||
position: relative !important;
|
||||
z-index: 1001 !important;
|
||||
opacity: 1 !important;
|
||||
visibility: visible !important;
|
||||
}
|
||||
|
||||
/* Adjust fixed buttons for landscape */
|
||||
.fixed-btn {
|
||||
bottom: 1rem !important;
|
||||
/* Adjust bottom button bar for landscape - smaller buttons */
|
||||
.download-btn,
|
||||
.print-friendly-btn,
|
||||
.shortcuts-btn,
|
||||
.info-button,
|
||||
.back-to-top,
|
||||
.color-theme-switcher {
|
||||
width: 40px !important;
|
||||
height: 40px !important;
|
||||
bottom: 1rem !important;
|
||||
}
|
||||
|
||||
.back-to-top-btn {
|
||||
right: 1rem !important;
|
||||
/* Recalculate button positions for smaller 40px buttons */
|
||||
/* 6 buttons: 6 * 40px + 5 * 10px = 290px total */
|
||||
.download-btn {
|
||||
left: calc(50% - 145px) !important;
|
||||
}
|
||||
|
||||
.print-friendly-btn {
|
||||
left: calc(50% - 95px) !important;
|
||||
}
|
||||
|
||||
.shortcuts-btn {
|
||||
left: 1rem !important;
|
||||
bottom: 5rem !important;
|
||||
left: calc(50% - 45px) !important;
|
||||
}
|
||||
|
||||
.zoom-toggle-btn {
|
||||
left: 1rem !important;
|
||||
bottom: 9rem !important;
|
||||
.color-theme-switcher {
|
||||
left: calc(50% + 5px) !important;
|
||||
}
|
||||
|
||||
.info-button {
|
||||
left: calc(50% + 55px) !important;
|
||||
}
|
||||
|
||||
.back-to-top {
|
||||
left: calc(50% + 105px) !important;
|
||||
}
|
||||
|
||||
/* Real mobile devices in landscape: 5 buttons (no shortcuts) */
|
||||
/* 5 buttons: 5 * 40px + 4 * 10px = 240px total */
|
||||
.is-mobile-device .download-btn {
|
||||
left: calc(50% - 120px) !important;
|
||||
}
|
||||
|
||||
.is-mobile-device .print-friendly-btn {
|
||||
left: calc(50% - 70px) !important;
|
||||
}
|
||||
|
||||
.is-mobile-device .color-theme-switcher {
|
||||
left: calc(50% - 20px) !important;
|
||||
}
|
||||
|
||||
.is-mobile-device .info-button {
|
||||
left: calc(50% + 30px) !important;
|
||||
}
|
||||
|
||||
.is-mobile-device .back-to-top {
|
||||
left: calc(50% + 80px) !important;
|
||||
}
|
||||
|
||||
/* Reduce blur bar height for landscape */
|
||||
.fixed-buttons-backdrop {
|
||||
height: 70px !important;
|
||||
}
|
||||
|
||||
/* Add bottom padding to footer for landscape */
|
||||
footer.no-print {
|
||||
padding-bottom: 90px !important;
|
||||
}
|
||||
}
|
||||
|
||||
Executable
+117
@@ -0,0 +1,117 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { chromium } from 'playwright';
|
||||
|
||||
const LANDSCAPE_VIEWPORT = { width: 667, height: 375 }; // iPhone SE landscape
|
||||
|
||||
(async () => {
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
|
||||
console.log('🧪 Testing Landscape Mode Layout\n');
|
||||
|
||||
// TEST 1: Android Landscape
|
||||
console.log('📱 TEST 1: Android Landscape (Pixel 5)\n');
|
||||
const androidContext = await browser.newContext({
|
||||
viewport: LANDSCAPE_VIEWPORT,
|
||||
userAgent: 'Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.91 Mobile Safari/537.36',
|
||||
hasTouch: true
|
||||
});
|
||||
const androidPage = await androidContext.newPage();
|
||||
|
||||
await androidPage.goto('http://localhost:1999/?lang=en&view=extended');
|
||||
await androidPage.waitForLoadState('networkidle');
|
||||
|
||||
const androidLayout = await androidPage.evaluate(() => {
|
||||
const page1 = document.querySelector('.page-1 .page-content');
|
||||
const sidebar = document.querySelector('.cv-sidebar-left');
|
||||
const hamburger = document.querySelector('.hamburger-btn'); // Correct class name
|
||||
const photo = document.querySelector('.cv-photo');
|
||||
const buttons = document.querySelector('.download-btn');
|
||||
|
||||
const gridCols = window.getComputedStyle(page1).gridTemplateColumns;
|
||||
// Single column if it's just one value (e.g., "667px" not "300px 667px")
|
||||
const singleColumn = !gridCols.includes(' ');
|
||||
|
||||
return {
|
||||
gridColumns: gridCols,
|
||||
singleColumn: singleColumn,
|
||||
hamburgerVisible: hamburger ? window.getComputedStyle(hamburger).display !== 'none' : false,
|
||||
photoMaxWidth: photo ? window.getComputedStyle(photo).maxWidth : 'N/A',
|
||||
photoVisible: photo ? window.getComputedStyle(photo).display !== 'none' : false,
|
||||
buttonWidth: buttons ? window.getComputedStyle(buttons).width : 'N/A',
|
||||
hasHorizontalScroll: document.body.scrollWidth > window.innerWidth
|
||||
};
|
||||
});
|
||||
|
||||
console.log('Android Landscape Layout:');
|
||||
console.log(` • Grid columns: ${androidLayout.gridColumns}`);
|
||||
console.log(` • Single column: ${androidLayout.singleColumn ? '✅' : '❌'}`);
|
||||
console.log(` • Hamburger menu visible: ${androidLayout.hamburgerVisible ? '✅' : '❌'}`);
|
||||
console.log(` • Photo max-width: ${androidLayout.photoMaxWidth}`);
|
||||
console.log(` • Photo visible: ${androidLayout.photoVisible ? '✅' : '❌'}`);
|
||||
console.log(` • Button width: ${androidLayout.buttonWidth}`);
|
||||
console.log(` • Has horizontal scroll: ${androidLayout.hasHorizontalScroll ? '❌ FAIL' : '✅'}\n`);
|
||||
|
||||
await androidPage.screenshot({
|
||||
path: 'tests/screenshots/landscape-android.png',
|
||||
fullPage: true
|
||||
});
|
||||
|
||||
await androidContext.close();
|
||||
|
||||
// TEST 2: iPhone Landscape
|
||||
console.log('📱 TEST 2: iPhone Landscape (iPhone 12)\n');
|
||||
const iphoneContext = await browser.newContext({
|
||||
viewport: { width: 844, height: 390 }, // iPhone 12 landscape
|
||||
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 iphonePage = await iphoneContext.newPage();
|
||||
|
||||
await iphonePage.goto('http://localhost:1999/?lang=en&view=extended');
|
||||
await iphonePage.waitForLoadState('networkidle');
|
||||
|
||||
const iphoneLayout = await iphonePage.evaluate(() => {
|
||||
const page1 = document.querySelector('.page-1 .page-content');
|
||||
const photo = document.querySelector('.cv-photo');
|
||||
const buttons = document.querySelector('.download-btn');
|
||||
|
||||
const gridCols = window.getComputedStyle(page1).gridTemplateColumns;
|
||||
// Single column if it's just one value (e.g., "844px" not "300px 844px")
|
||||
const singleColumn = !gridCols.includes(' ');
|
||||
|
||||
return {
|
||||
gridColumns: gridCols,
|
||||
singleColumn: singleColumn,
|
||||
photoMaxWidth: photo ? window.getComputedStyle(photo).maxWidth : 'N/A',
|
||||
photoVisible: photo ? window.getComputedStyle(photo).display !== 'none' : false,
|
||||
buttonWidth: buttons ? window.getComputedStyle(buttons).width : 'N/A',
|
||||
hasHorizontalScroll: document.body.scrollWidth > window.innerWidth
|
||||
};
|
||||
});
|
||||
|
||||
console.log('iPhone Landscape Layout:');
|
||||
console.log(` • Grid columns: ${iphoneLayout.gridColumns}`);
|
||||
console.log(` • Single column: ${iphoneLayout.singleColumn ? '✅' : '❌'}`);
|
||||
console.log(` • Photo max-width: ${iphoneLayout.photoMaxWidth}`);
|
||||
console.log(` • Photo visible: ${iphoneLayout.photoVisible ? '✅' : '❌'}`);
|
||||
console.log(` • Button width: ${iphoneLayout.buttonWidth}`);
|
||||
console.log(` • Has horizontal scroll: ${iphoneLayout.hasHorizontalScroll ? '❌ FAIL' : '✅'}\n`);
|
||||
|
||||
await iphonePage.screenshot({
|
||||
path: 'tests/screenshots/landscape-iphone.png',
|
||||
fullPage: true
|
||||
});
|
||||
|
||||
await iphoneContext.close();
|
||||
|
||||
const allPassed = androidLayout.singleColumn && !androidLayout.hasHorizontalScroll &&
|
||||
androidLayout.hamburgerVisible && androidLayout.photoVisible &&
|
||||
iphoneLayout.singleColumn && !iphoneLayout.hasHorizontalScroll &&
|
||||
iphoneLayout.photoVisible;
|
||||
|
||||
console.log(`${allPassed ? '✅' : '❌'} Tests ${allPassed ? 'PASSED' : 'FAILED'}\n`);
|
||||
|
||||
await browser.close();
|
||||
process.exit(allPassed ? 0 : 1);
|
||||
})();
|
||||
Executable
+197
@@ -0,0 +1,197 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { chromium } from 'playwright';
|
||||
|
||||
const MOBILE_VIEWPORT = { width: 375, height: 667 };
|
||||
|
||||
(async () => {
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
|
||||
console.log('🧪 Testing Button Centering on Mobile\n');
|
||||
|
||||
// TEST 1: Android Portrait
|
||||
console.log('📱 TEST 1: Android Portrait (Pixel 5)\n');
|
||||
const androidContext = await browser.newContext({
|
||||
viewport: MOBILE_VIEWPORT,
|
||||
userAgent: 'Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.91 Mobile Safari/537.36',
|
||||
hasTouch: true
|
||||
});
|
||||
const androidPage = await androidContext.newPage();
|
||||
|
||||
await androidPage.goto('http://localhost:1999/?lang=en&view=extended');
|
||||
await androidPage.waitForLoadState('networkidle');
|
||||
|
||||
const androidButtons = await androidPage.evaluate(() => {
|
||||
const viewport = {
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight
|
||||
};
|
||||
|
||||
const buttons = {
|
||||
download: document.querySelector('.download-btn'),
|
||||
print: document.querySelector('.print-friendly-btn'),
|
||||
theme: document.querySelector('.color-theme-switcher'),
|
||||
info: document.querySelector('.info-button'),
|
||||
backToTop: document.querySelector('.back-to-top'),
|
||||
shortcuts: document.querySelector('.shortcuts-btn')
|
||||
};
|
||||
|
||||
const positions = {};
|
||||
let leftmostButton = Infinity;
|
||||
let rightmostButton = 0;
|
||||
|
||||
for (const [name, btn] of Object.entries(buttons)) {
|
||||
if (btn) {
|
||||
const rect = btn.getBoundingClientRect();
|
||||
const computed = window.getComputedStyle(btn);
|
||||
const visible = computed.display !== 'none';
|
||||
|
||||
if (visible) {
|
||||
positions[name] = {
|
||||
left: Math.round(rect.left),
|
||||
right: Math.round(rect.right),
|
||||
centerX: Math.round(rect.left + rect.width / 2),
|
||||
visible: true
|
||||
};
|
||||
leftmostButton = Math.min(leftmostButton, rect.left);
|
||||
rightmostButton = Math.max(rightmostButton, rect.right);
|
||||
} else {
|
||||
positions[name] = { visible: false };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const buttonBarWidth = rightmostButton - leftmostButton;
|
||||
const buttonBarCenterX = leftmostButton + buttonBarWidth / 2;
|
||||
const viewportCenterX = viewport.width / 2;
|
||||
const centeringOffset = Math.abs(buttonBarCenterX - viewportCenterX);
|
||||
|
||||
return {
|
||||
viewport,
|
||||
positions,
|
||||
buttonBarWidth: Math.round(buttonBarWidth),
|
||||
buttonBarCenterX: Math.round(buttonBarCenterX),
|
||||
viewportCenterX: Math.round(viewportCenterX),
|
||||
centeringOffset: Math.round(centeringOffset),
|
||||
isCentered: centeringOffset < 10 // Within 10px is acceptable
|
||||
};
|
||||
});
|
||||
|
||||
console.log('Android Button Positions:');
|
||||
Object.entries(androidButtons.positions).forEach(([name, pos]) => {
|
||||
if (pos.visible) {
|
||||
console.log(` • ${name}: ${pos.left}px to ${pos.right}px (center: ${pos.centerX}px)`);
|
||||
} else {
|
||||
console.log(` • ${name}: HIDDEN`);
|
||||
}
|
||||
});
|
||||
console.log(`\nButton Bar Analysis:`);
|
||||
console.log(` • Button bar width: ${androidButtons.buttonBarWidth}px`);
|
||||
console.log(` • Button bar center: ${androidButtons.buttonBarCenterX}px`);
|
||||
console.log(` • Viewport center: ${androidButtons.viewportCenterX}px`);
|
||||
console.log(` • Centering offset: ${androidButtons.centeringOffset}px`);
|
||||
console.log(` • Is centered: ${androidButtons.isCentered ? '✅' : '❌'}\n`);
|
||||
|
||||
await androidPage.screenshot({
|
||||
path: 'tests/screenshots/buttons-android-portrait.png',
|
||||
fullPage: true
|
||||
});
|
||||
|
||||
await androidContext.close();
|
||||
|
||||
// TEST 2: iPhone Portrait
|
||||
console.log('📱 TEST 2: iPhone Portrait (iPhone 12)\n');
|
||||
const iphoneContext = 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 iphonePage = await iphoneContext.newPage();
|
||||
|
||||
await iphonePage.goto('http://localhost:1999/?lang=en&view=extended');
|
||||
await iphonePage.waitForLoadState('networkidle');
|
||||
|
||||
const iphoneButtons = await iphonePage.evaluate(() => {
|
||||
const viewport = {
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight
|
||||
};
|
||||
|
||||
const buttons = {
|
||||
download: document.querySelector('.download-btn'),
|
||||
print: document.querySelector('.print-friendly-btn'),
|
||||
theme: document.querySelector('.color-theme-switcher'),
|
||||
info: document.querySelector('.info-button'),
|
||||
backToTop: document.querySelector('.back-to-top')
|
||||
};
|
||||
|
||||
const positions = {};
|
||||
let leftmostButton = Infinity;
|
||||
let rightmostButton = 0;
|
||||
|
||||
for (const [name, btn] of Object.entries(buttons)) {
|
||||
if (btn) {
|
||||
const rect = btn.getBoundingClientRect();
|
||||
const computed = window.getComputedStyle(btn);
|
||||
const visible = computed.display !== 'none';
|
||||
|
||||
if (visible) {
|
||||
positions[name] = {
|
||||
left: Math.round(rect.left),
|
||||
right: Math.round(rect.right),
|
||||
centerX: Math.round(rect.left + rect.width / 2),
|
||||
visible: true
|
||||
};
|
||||
leftmostButton = Math.min(leftmostButton, rect.left);
|
||||
rightmostButton = Math.max(rightmostButton, rect.right);
|
||||
} else {
|
||||
positions[name] = { visible: false };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const buttonBarWidth = rightmostButton - leftmostButton;
|
||||
const buttonBarCenterX = leftmostButton + buttonBarWidth / 2;
|
||||
const viewportCenterX = viewport.width / 2;
|
||||
const centeringOffset = Math.abs(buttonBarCenterX - viewportCenterX);
|
||||
|
||||
return {
|
||||
viewport,
|
||||
positions,
|
||||
buttonBarWidth: Math.round(buttonBarWidth),
|
||||
buttonBarCenterX: Math.round(buttonBarCenterX),
|
||||
viewportCenterX: Math.round(viewportCenterX),
|
||||
centeringOffset: Math.round(centeringOffset),
|
||||
isCentered: centeringOffset < 10
|
||||
};
|
||||
});
|
||||
|
||||
console.log('iPhone Button Positions:');
|
||||
Object.entries(iphoneButtons.positions).forEach(([name, pos]) => {
|
||||
if (pos.visible) {
|
||||
console.log(` • ${name}: ${pos.left}px to ${pos.right}px (center: ${pos.centerX}px)`);
|
||||
} else {
|
||||
console.log(` • ${name}: HIDDEN`);
|
||||
}
|
||||
});
|
||||
console.log(`\nButton Bar Analysis:`);
|
||||
console.log(` • Button bar width: ${iphoneButtons.buttonBarWidth}px`);
|
||||
console.log(` • Button bar center: ${iphoneButtons.buttonBarCenterX}px`);
|
||||
console.log(` • Viewport center: ${iphoneButtons.viewportCenterX}px`);
|
||||
console.log(` • Centering offset: ${iphoneButtons.centeringOffset}px`);
|
||||
console.log(` • Is centered: ${iphoneButtons.isCentered ? '✅' : '❌'}\n`);
|
||||
|
||||
await iphonePage.screenshot({
|
||||
path: 'tests/screenshots/buttons-iphone-portrait.png',
|
||||
fullPage: true
|
||||
});
|
||||
|
||||
await iphoneContext.close();
|
||||
|
||||
const allPassed = androidButtons.isCentered && iphoneButtons.isCentered;
|
||||
|
||||
console.log(`${allPassed ? '✅' : '❌'} Tests ${allPassed ? 'PASSED' : 'FAILED'}\n`);
|
||||
|
||||
await browser.close();
|
||||
process.exit(allPassed ? 0 : 1);
|
||||
})();
|
||||
Executable
+64
@@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { chromium } from 'playwright';
|
||||
|
||||
const LANDSCAPE_VIEWPORT = { width: 667, height: 375 };
|
||||
|
||||
(async () => {
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const context = await browser.newContext({
|
||||
viewport: LANDSCAPE_VIEWPORT,
|
||||
userAgent: 'Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.91 Mobile Safari/537.36',
|
||||
hasTouch: true
|
||||
});
|
||||
const page = await context.newPage();
|
||||
|
||||
await page.goto('http://localhost:1999/?lang=en&view=extended');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const debug = await page.evaluate(() => {
|
||||
const page1 = document.querySelector('.page-1 .page-content');
|
||||
const computed = window.getComputedStyle(page1);
|
||||
|
||||
// Check media query matches
|
||||
const landscapeMatch = window.matchMedia('(max-width: 915px) and (orientation: landscape)').matches;
|
||||
const maxWidth915 = window.matchMedia('(max-width: 915px)').matches;
|
||||
const orientationLandscape = window.matchMedia('(orientation: landscape)').matches;
|
||||
const maxWidth768 = window.matchMedia('(max-width: 768px)').matches;
|
||||
|
||||
return {
|
||||
viewport: {
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight,
|
||||
ratio: window.innerWidth / window.innerHeight
|
||||
},
|
||||
mediaQueries: {
|
||||
'max-width: 915px': maxWidth915,
|
||||
'orientation: landscape': orientationLandscape,
|
||||
'max-width: 915px AND landscape': landscapeMatch,
|
||||
'max-width: 768px': maxWidth768
|
||||
},
|
||||
gridColumns: computed.gridTemplateColumns,
|
||||
bodyOverflow: window.getComputedStyle(document.body).overflowX,
|
||||
htmlOverflow: window.getComputedStyle(document.documentElement).overflowX
|
||||
};
|
||||
});
|
||||
|
||||
console.log('Landscape Debug Info:\n');
|
||||
console.log('Viewport:');
|
||||
console.log(` • Width: ${debug.viewport.width}px`);
|
||||
console.log(` • Height: ${debug.viewport.height}px`);
|
||||
console.log(` • Ratio: ${debug.viewport.ratio.toFixed(2)} (${debug.viewport.ratio > 1 ? 'LANDSCAPE' : 'PORTRAIT'})\n`);
|
||||
|
||||
console.log('Media Query Matches:');
|
||||
Object.entries(debug.mediaQueries).forEach(([query, matches]) => {
|
||||
console.log(` • ${query}: ${matches ? '✅ YES' : '❌ NO'}`);
|
||||
});
|
||||
|
||||
console.log(`\nComputed Styles:`);
|
||||
console.log(` • Grid columns: ${debug.gridColumns}`);
|
||||
console.log(` • Body overflow-x: ${debug.bodyOverflow}`);
|
||||
console.log(` • HTML overflow-x: ${debug.htmlOverflow}\n`);
|
||||
|
||||
await browser.close();
|
||||
})();
|
||||
Executable
+70
@@ -0,0 +1,70 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { chromium } from 'playwright';
|
||||
|
||||
const LANDSCAPE_VIEWPORT = { width: 667, height: 375 };
|
||||
|
||||
(async () => {
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const context = await browser.newContext({
|
||||
viewport: LANDSCAPE_VIEWPORT,
|
||||
userAgent: 'Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.91 Mobile Safari/537.36',
|
||||
hasTouch: true
|
||||
});
|
||||
const page = await context.newPage();
|
||||
|
||||
await page.goto('http://localhost:1999/?lang=en&view=extended');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const debug = await page.evaluate(() => {
|
||||
// Find all elements wider than viewport
|
||||
const viewportWidth = window.innerWidth;
|
||||
const wideElements = [];
|
||||
|
||||
document.querySelectorAll('*').forEach((el) => {
|
||||
const rect = el.getBoundingClientRect();
|
||||
if (rect.width > viewportWidth) {
|
||||
const computed = window.getComputedStyle(el);
|
||||
wideElements.push({
|
||||
tag: el.tagName.toLowerCase(),
|
||||
id: el.id || '(no id)',
|
||||
classes: el.className || '(no classes)',
|
||||
width: Math.round(rect.width),
|
||||
computedWidth: computed.width,
|
||||
maxWidth: computed.maxWidth,
|
||||
overflow: computed.overflowX
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Sort by width descending
|
||||
wideElements.sort((a, b) => b.width - a.width);
|
||||
|
||||
return {
|
||||
viewportWidth,
|
||||
bodyScrollWidth: document.body.scrollWidth,
|
||||
documentScrollWidth: document.documentElement.scrollWidth,
|
||||
hasHorizontalScroll: document.body.scrollWidth > viewportWidth,
|
||||
wideElements: wideElements.slice(0, 10) // Top 10 widest
|
||||
};
|
||||
});
|
||||
|
||||
console.log(`Horizontal Scroll Debug:\n`);
|
||||
console.log(`Viewport: ${debug.viewportWidth}px`);
|
||||
console.log(`Body scroll width: ${debug.bodyScrollWidth}px`);
|
||||
console.log(`Document scroll width: ${debug.documentScrollWidth}px`);
|
||||
console.log(`Has horizontal scroll: ${debug.hasHorizontalScroll ? '❌ YES' : '✅ NO'}\n`);
|
||||
|
||||
if (debug.wideElements.length > 0) {
|
||||
console.log(`Elements wider than viewport:\n`);
|
||||
debug.wideElements.forEach((el, i) => {
|
||||
console.log(`${i + 1}. <${el.tag}> #${el.id} .${el.classes}`);
|
||||
console.log(` Width: ${el.width}px | Max-width: ${el.maxWidth} | Overflow: ${el.overflow}`);
|
||||
console.log(` Computed width: ${el.computedWidth}\n`);
|
||||
});
|
||||
} else {
|
||||
console.log('No elements wider than viewport found\n');
|
||||
}
|
||||
|
||||
await browser.close();
|
||||
})();
|
||||
Reference in New Issue
Block a user