#!/usr/bin/env bun /** * MOBILE RESPONSIVE TEST * ======================= * Tests mobile viewport rendering and interactions * - Mobile viewport sizing (375px, 768px, 1024px) * - Touch interactions * - Mobile menu functionality * - Responsive layout breakpoints * - Text readability at small sizes */ import { chromium } from 'playwright'; const URL = "http://localhost:1999"; // Common mobile viewports const VIEWPORTS = { mobile: { width: 375, height: 667 }, // iPhone SE tablet: { width: 768, height: 1024 }, // iPad desktop: { width: 1920, height: 1080 } // Desktop baseline }; async function testMobileResponsive() { console.log('šŸ“± MOBILE RESPONSIVE TEST\n'); console.log('='.repeat(70)); const browser = await chromium.launch({ headless: true }); const errors = []; const testResults = []; // ======================================================================== // TEST 1: Mobile viewport (375px) // ======================================================================== console.log("\n1ļøāƒ£ Testing Mobile Viewport (375px)..."); const mobilePage = await browser.newPage({ viewport: VIEWPORTS.mobile }); mobilePage.on('console', msg => { if (msg.type() === 'error') { errors.push(msg.text()); console.log(`āŒ ERROR: ${msg.text()}`); } }); await mobilePage.goto(URL); await mobilePage.waitForTimeout(2000); const mobileTest = await mobilePage.evaluate(() => { const paper = document.querySelector('.cv-paper'); const body = document.body; const hamburger = document.querySelector('.hamburger-btn'); // Check for horizontal overflow const hasHorizontalScroll = document.documentElement.scrollWidth > window.innerWidth; // Check if text is readable (not too small) const paragraphs = Array.from(document.querySelectorAll('p, li')); const fontSizes = paragraphs.map(p => { const size = parseFloat(window.getComputedStyle(p).fontSize); return size; }); const minFontSize = Math.min(...fontSizes); // Check if hamburger menu exists (mobile navigation) const hasHamburger = !!hamburger; const hamburgerVisible = hasHamburger ? window.getComputedStyle(hamburger).display !== 'none' : false; return { width: window.innerWidth, height: window.innerHeight, hasHorizontalScroll, minFontSize, hasHamburger, hamburgerVisible, paperWidth: paper ? paper.offsetWidth : 0 }; }); console.log(` Viewport: ${mobileTest.width}x${mobileTest.height}`); console.log(` Horizontal scroll: ${mobileTest.hasHorizontalScroll ? 'āŒ YES (BAD)' : 'āœ… NO (GOOD)'}`); console.log(` Min font size: ${mobileTest.minFontSize.toFixed(1)}px ${mobileTest.minFontSize >= 14 ? 'āœ…' : 'āš ļø'}`); console.log(` Hamburger menu: ${mobileTest.hasHamburger ? 'āœ… Present' : 'āš ļø Not found'}`); console.log(` Hamburger visible: ${mobileTest.hamburgerVisible ? 'āœ… YES' : 'āš ļø NO'}`); const mobileViewportPassed = !mobileTest.hasHorizontalScroll && mobileTest.minFontSize >= 14; console.log(` ${mobileViewportPassed ? 'āœ… PASS' : 'āŒ FAIL'} - Mobile viewport`); testResults.push({ test: 'Mobile Viewport (375px)', passed: mobileViewportPassed }); // ======================================================================== // TEST 2: Hamburger Menu Click Toggle (Mobile) // ======================================================================== console.log("\n2ļøāƒ£ Testing Hamburger Menu Click Toggle..."); const hamburger = await mobilePage.$('.hamburger-btn'); if (hamburger) { // Click hamburger to open menu (simulates mobile tap) await hamburger.click(); await mobilePage.waitForTimeout(300); const menuOpenTest = await mobilePage.evaluate(() => { const menu = document.querySelector('.navigation-menu'); if (!menu) return { found: false }; const hasMenuOpen = menu.classList.contains('menu-open'); const computedStyle = window.getComputedStyle(menu); const isVisible = computedStyle.opacity === '1' && computedStyle.maxHeight !== '0px'; return { found: true, hasMenuOpen, isVisible, maxHeight: computedStyle.maxHeight }; }); console.log(` Menu found: ${menuOpenTest.found ? 'āœ…' : 'āŒ'}`); console.log(` Has menu-open class: ${menuOpenTest.hasMenuOpen ? 'āœ…' : 'āŒ'}`); console.log(` Menu visible: ${menuOpenTest.isVisible ? 'āœ…' : 'āŒ'}`); const openPassed = menuOpenTest.found && menuOpenTest.hasMenuOpen; console.log(` ${openPassed ? 'āœ… PASS' : 'āŒ FAIL'} - Menu opens on click`); testResults.push({ test: 'Menu Opens on Click', passed: openPassed }); // TEST 2B: Click again to close await hamburger.click(); await mobilePage.waitForTimeout(300); const menuCloseTest = await mobilePage.evaluate(() => { const menu = document.querySelector('.navigation-menu'); return { hasMenuOpen: menu?.classList.contains('menu-open') ?? false }; }); const closePassed = !menuCloseTest.hasMenuOpen; console.log(` Menu closes on second click: ${closePassed ? 'āœ…' : 'āŒ'}`); testResults.push({ test: 'Menu Closes on Click', passed: closePassed }); // TEST 2C: Click outside to close await hamburger.click(); // Open again await mobilePage.waitForTimeout(300); await mobilePage.click('body', { position: { x: 300, y: 400 } }); // Click outside await mobilePage.waitForTimeout(300); const outsideCloseTest = await mobilePage.evaluate(() => { const menu = document.querySelector('.navigation-menu'); return { hasMenuOpen: menu?.classList.contains('menu-open') ?? false }; }); const outsidePassed = !outsideCloseTest.hasMenuOpen; console.log(` Menu closes on outside click: ${outsidePassed ? 'āœ…' : 'āŒ'}`); testResults.push({ test: 'Menu Closes on Outside Click', passed: outsidePassed }); } else { console.log(` āš ļø SKIP - Hamburger menu not found`); testResults.push({ test: 'Menu Opens on Click', passed: false }); testResults.push({ test: 'Menu Closes on Click', passed: false }); testResults.push({ test: 'Menu Closes on Outside Click', passed: false }); } // ======================================================================== // TEST 3: Tablet viewport (768px) // ======================================================================== console.log("\n3ļøāƒ£ Testing Tablet Viewport (768px)..."); const tabletPage = await browser.newPage({ viewport: VIEWPORTS.tablet }); await tabletPage.goto(URL); await tabletPage.waitForTimeout(2000); const tabletTest = await tabletPage.evaluate(() => { const hasHorizontalScroll = document.documentElement.scrollWidth > window.innerWidth; const paper = document.querySelector('.cv-paper'); const actionBar = document.querySelector('.action-bar, .cv-controls'); return { width: window.innerWidth, hasHorizontalScroll, paperWidth: paper ? paper.offsetWidth : 0, hasActionBar: !!actionBar, actionBarVisible: actionBar ? window.getComputedStyle(actionBar).display !== 'none' : false }; }); console.log(` Viewport: ${tabletTest.width}px`); console.log(` Horizontal scroll: ${tabletTest.hasHorizontalScroll ? 'āŒ YES' : 'āœ… NO'}`); console.log(` Action bar: ${tabletTest.hasActionBar ? 'āœ… Present' : 'āš ļø Not found'}`); const tabletViewportPassed = !tabletTest.hasHorizontalScroll; console.log(` ${tabletViewportPassed ? 'āœ… PASS' : 'āŒ FAIL'} - Tablet viewport`); testResults.push({ test: 'Tablet Viewport (768px)', passed: tabletViewportPassed }); await tabletPage.close(); // ======================================================================== // TEST 4: Responsive breakpoints // ======================================================================== console.log("\n4ļøāƒ£ Testing Responsive Breakpoints..."); const breakpoints = [ { name: 'Small Mobile', width: 320 }, { name: 'Mobile', width: 375 }, { name: 'Large Mobile', width: 414 }, { name: 'Small Tablet', width: 600 }, { name: 'Tablet', width: 768 }, { name: 'Large Tablet', width: 1024 }, { name: 'Desktop', width: 1920 } ]; const breakpointResults = []; for (const bp of breakpoints) { await mobilePage.setViewportSize({ width: bp.width, height: 800 }); await mobilePage.waitForTimeout(200); const result = await mobilePage.evaluate(() => { return { hasHorizontalScroll: document.documentElement.scrollWidth > window.innerWidth, bodyWidth: document.body.offsetWidth }; }); const passed = !result.hasHorizontalScroll; breakpointResults.push({ name: bp.name, width: bp.width, passed }); console.log(` ${bp.name} (${bp.width}px): ${passed ? 'āœ…' : 'āŒ'}`); } const allBreakpointsPassed = breakpointResults.every(r => r.passed); console.log(` ${allBreakpointsPassed ? 'āœ… PASS' : 'āŒ FAIL'} - All breakpoints`); testResults.push({ test: 'Responsive Breakpoints', passed: allBreakpointsPassed }); // ======================================================================== // TEST 5: Mobile-specific features // ======================================================================== console.log("\n5ļøāƒ£ Testing Mobile-Specific Features..."); await mobilePage.setViewportSize(VIEWPORTS.mobile); await mobilePage.waitForTimeout(500); const mobileFeatures = await mobilePage.evaluate(() => { // Check viewport meta tag const viewportMeta = document.querySelector('meta[name="viewport"]'); const hasViewportMeta = !!viewportMeta; const viewportContent = viewportMeta?.getAttribute('content') || ''; // Check for touch-friendly button sizes (minimum 44x44px) const buttons = Array.from(document.querySelectorAll('button, a[role="button"], .btn, input[type="checkbox"]')); const buttonSizes = buttons.map(btn => { const rect = btn.getBoundingClientRect(); return { width: rect.width, height: rect.height }; }); const tooSmallButtons = buttonSizes.filter(s => s.width < 44 || s.height < 44).length; // Check for text overflow const hasTextOverflow = Array.from(document.querySelectorAll('*')).some(el => { return el.scrollWidth > el.clientWidth && window.getComputedStyle(el).overflow === 'visible'; }); return { hasViewportMeta, viewportContent, totalButtons: buttons.length, tooSmallButtons, hasTextOverflow }; }); console.log(` Viewport meta tag: ${mobileFeatures.hasViewportMeta ? 'āœ…' : 'āŒ'}`); console.log(` Content: "${mobileFeatures.viewportContent}"`); console.log(` Touch-friendly buttons: ${mobileFeatures.totalButtons - mobileFeatures.tooSmallButtons}/${mobileFeatures.totalButtons}`); console.log(` Too small (<44px): ${mobileFeatures.tooSmallButtons} ${mobileFeatures.tooSmallButtons === 0 ? 'āœ…' : 'āš ļø'}`); console.log(` Text overflow: ${mobileFeatures.hasTextOverflow ? 'āš ļø YES' : 'āœ… NO'}`); const mobileFeaturesPass = mobileFeatures.hasViewportMeta && !mobileFeatures.hasTextOverflow; console.log(` ${mobileFeaturesPass ? 'āœ… PASS' : 'āŒ FAIL'} - Mobile features`); testResults.push({ test: 'Mobile Features', passed: mobileFeaturesPass }); await mobilePage.close(); // ======================================================================== // 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`); } console.log("=".repeat(70) + "\n"); if (failedTests === 0) { console.log("šŸŽ‰ MOBILE RESPONSIVE VALIDATED!"); } else { console.log("āš ļø SOME TESTS FAILED - See details above"); } await browser.close(); process.exit(failedTests === 0 ? 0 : 1); } await testMobileResponsive();