#!/usr/bin/env bun /** * BUTTON POSITIONING & RESPONSIVE LAYOUT TEST * ============================================== * Tests button positioning across different viewport sizes * - Desktop: Vertical layout on left side * - Wide Mobile (483-900px): Horizontal layout at bottom + back-to-top on right * - Narrow Mobile (<483px): Horizontal layout + back-to-top moved up on right * - Visibility: Zoom button hidden in mobile */ import { chromium } from 'playwright'; const URL = "http://localhost:1999"; async function testButtonPositioning() { console.log('šŸŽÆ BUTTON POSITIONING & RESPONSIVE TEST\n'); console.log('='.repeat(70)); const browser = await chromium.launch({ headless: false }); const errors = []; const testResults = []; try { // ======================================================================== // TEST 1: Desktop Layout (>900px) - Vertical on Left Side // ======================================================================== console.log("\n1ļøāƒ£ Testing Desktop Layout (1920x1080)..."); const desktopPage = await browser.newPage({ viewport: { width: 1920, height: 1080 } }); await desktopPage.goto(URL); await desktopPage.waitForTimeout(1000); const desktopLayout = await desktopPage.evaluate(() => { const buttons = { download: document.querySelector('.download-btn'), print: document.querySelector('.print-friendly-btn'), shortcuts: document.querySelector('.shortcuts-btn'), info: document.querySelector('.info-button'), backToTop: document.querySelector('.back-to-top'), zoom: document.querySelector('.zoom-toggle-btn') }; // Get computed styles const getPosition = (el) => { if (!el) return null; const style = window.getComputedStyle(el); const rect = el.getBoundingClientRect(); return { display: style.display, position: style.position, left: style.left, right: style.right, bottom: style.bottom, top: rect.top, leftPx: rect.left, visible: style.display !== 'none' && style.visibility !== 'hidden' }; }; // Check if buttons are vertically stacked (left side) const positions = {}; for (const [key, button] of Object.entries(buttons)) { positions[key] = getPosition(button); } // Check LEFT side buttons (download, print, shortcuts, info) const leftSideButtons = ['download', 'print', 'shortcuts', 'info']; const leftButtonsOnLeft = leftSideButtons.every(key => { const pos = positions[key]; return pos && pos.left !== 'auto' && parseFloat(pos.left) < 100; // Left side positioning }); // Check back-to-top is on RIGHT side (as intended) const backToTopOnRight = positions.backToTop && positions.backToTop.right !== 'auto' && parseFloat(positions.backToTop.right) < 100; // Check vertical stacking (different bottom values for left side buttons) const bottomValues = leftSideButtons.map(key => parseFloat(positions[key]?.bottom || '0')); const isVertical = new Set(bottomValues).size === leftSideButtons.length; // All different return { positions, leftButtonsOnLeft, backToTopOnRight, isVertical, zoomVisible: positions.zoom?.visible || false }; }); console.log(` Left side buttons on left: ${desktopLayout.leftButtonsOnLeft ? 'āœ…' : 'āŒ'}`); console.log(` Back-to-top on right side: ${desktopLayout.backToTopOnRight ? 'āœ…' : 'āŒ'}`); console.log(` Vertical layout (stacked): ${desktopLayout.isVertical ? 'āœ…' : 'āŒ'}`); console.log(` Zoom button visible: ${desktopLayout.zoomVisible ? 'āœ…' : 'āŒ'}`); const desktopPassed = desktopLayout.leftButtonsOnLeft && desktopLayout.backToTopOnRight && desktopLayout.isVertical && desktopLayout.zoomVisible; console.log(` ${desktopPassed ? 'āœ… PASS' : 'āŒ FAIL'} - Desktop vertical layout`); testResults.push({ test: 'Desktop Layout (>900px)', passed: desktopPassed }); await desktopPage.close(); // ======================================================================== // TEST 2: Wide Mobile Layout (483-900px) - Horizontal + Back-to-top Right // ======================================================================== console.log("\n2ļøāƒ£ Testing Wide Mobile Layout (768x1024)..."); const wideMobilePage = await browser.newPage({ viewport: { width: 768, height: 1024 } }); await wideMobilePage.goto(URL); await wideMobilePage.waitForTimeout(1000); const wideMobileLayout = await wideMobilePage.evaluate(() => { const buttons = { download: document.querySelector('.download-btn'), print: document.querySelector('.print-friendly-btn'), shortcuts: document.querySelector('.shortcuts-btn'), info: document.querySelector('.info-button'), backToTop: document.querySelector('.back-to-top'), zoom: document.querySelector('.zoom-toggle-btn') }; const getPosition = (el) => { if (!el) return null; const style = window.getComputedStyle(el); const rect = el.getBoundingClientRect(); return { display: style.display, left: style.left, right: style.right, bottom: style.bottom, bottomPx: window.innerHeight - rect.bottom, leftPx: rect.left, rightPx: window.innerWidth - rect.right, visible: style.display !== 'none' && style.visibility !== 'hidden' }; }; const positions = {}; for (const [key, button] of Object.entries(buttons)) { positions[key] = getPosition(button); } // Check horizontal layout (all at same bottom, different left positions) const centerButtons = ['download', 'print', 'shortcuts', 'info']; const bottomValues = centerButtons.map(key => parseFloat(positions[key]?.bottom || '0')); const sameBottom = new Set(bottomValues).size === 1; // All same bottom // Check back-to-top on right side const backToTopRight = positions.backToTop && parseFloat(positions.backToTop.right) < 50 && // Right side parseFloat(positions.backToTop.right) > 0; // Check zoom hidden const zoomHidden = !positions.zoom?.visible; return { positions, sameBottom, backToTopRight, zoomHidden }; }); console.log(` Buttons at same bottom (horizontal): ${wideMobileLayout.sameBottom ? 'āœ…' : 'āŒ'}`); console.log(` Back-to-top on right side: ${wideMobileLayout.backToTopRight ? 'āœ…' : 'āŒ'}`); console.log(` Zoom button hidden: ${wideMobileLayout.zoomHidden ? 'āœ…' : 'āŒ'}`); const wideMobilePassed = wideMobileLayout.sameBottom && wideMobileLayout.backToTopRight && wideMobileLayout.zoomHidden; console.log(` ${wideMobilePassed ? 'āœ… PASS' : 'āŒ FAIL'} - Wide mobile layout`); testResults.push({ test: 'Wide Mobile Layout (483-900px)', passed: wideMobilePassed }); await wideMobilePage.close(); // ======================================================================== // TEST 3: Narrow Mobile Layout (<483px) - Back-to-top Moved Up // ======================================================================== console.log("\n3ļøāƒ£ Testing Narrow Mobile Layout (375x667)..."); const narrowMobilePage = await browser.newPage({ viewport: { width: 375, height: 667 } }); await narrowMobilePage.goto(URL); await narrowMobilePage.waitForTimeout(1000); const narrowMobileLayout = await narrowMobilePage.evaluate(() => { const buttons = { download: document.querySelector('.download-btn'), info: document.querySelector('.info-button'), backToTop: document.querySelector('.back-to-top') }; const getPosition = (el) => { if (!el) return null; const style = window.getComputedStyle(el); const rect = el.getBoundingClientRect(); return { left: style.left, right: style.right, bottom: style.bottom, bottomPx: parseFloat(style.bottom) }; }; const positions = {}; for (const [key, button] of Object.entries(buttons)) { positions[key] = getPosition(button); } // Check back-to-top is higher than other buttons const backToTopBottom = positions.backToTop?.bottomPx || 0; const infoBottom = positions.info?.bottomPx || 0; const backToTopHigher = backToTopBottom > infoBottom + 30; // At least 30px higher // Check back-to-top still on right const backToTopRight = positions.backToTop && parseFloat(positions.backToTop.right) < 50 && parseFloat(positions.backToTop.right) > 0; return { positions, backToTopHigher, backToTopRight, backToTopBottomPx: backToTopBottom, infoBottomPx: infoBottom }; }); console.log(` Back-to-top higher than info button: ${narrowMobileLayout.backToTopHigher ? 'āœ…' : 'āŒ'}`); console.log(` Back-to-top still on right side: ${narrowMobileLayout.backToTopRight ? 'āœ…' : 'āŒ'}`); console.log(` Info button bottom: ${narrowMobileLayout.infoBottomPx}px`); console.log(` Back-to-top bottom: ${narrowMobileLayout.backToTopBottomPx}px`); const narrowMobilePassed = narrowMobileLayout.backToTopHigher && narrowMobileLayout.backToTopRight; console.log(` ${narrowMobilePassed ? 'āœ… PASS' : 'āŒ FAIL'} - Narrow mobile layout`); testResults.push({ test: 'Narrow Mobile Layout (<483px)', passed: narrowMobilePassed }); await narrowMobilePage.close(); // ======================================================================== // TEST 4: Button Visibility & Accessibility // ======================================================================== console.log("\n4ļøāƒ£ Testing Button Visibility & Accessibility..."); const a11yPage = await browser.newPage({ viewport: { width: 1920, height: 1080 } }); await a11yPage.goto(URL); await a11yPage.waitForTimeout(1000); const a11yCheck = await a11yPage.evaluate(() => { const buttons = document.querySelectorAll('.download-btn, .print-friendly-btn, .shortcuts-btn, .info-button, .back-to-top, .zoom-toggle-btn'); const checks = { allButtonsPresent: buttons.length >= 6, allHaveAriaLabels: true, allClickable: true, buttonDetails: [] }; buttons.forEach(button => { const ariaLabel = button.getAttribute('aria-label') || button.getAttribute('title'); const style = window.getComputedStyle(button); const isVisible = style.display !== 'none' && style.visibility !== 'hidden'; const isClickable = style.pointerEvents !== 'none'; checks.buttonDetails.push({ class: button.className, hasAriaLabel: !!ariaLabel, visible: isVisible, clickable: isClickable }); // Only check clickability for visible buttons if (isVisible && !isClickable) { checks.allClickable = false; } }); return checks; }); console.log(` All buttons present: ${a11yCheck.allButtonsPresent ? 'āœ…' : 'āŒ'} (${a11yCheck.buttonDetails.length} buttons)`); console.log(` All clickable: ${a11yCheck.allClickable ? 'āœ…' : 'āŒ'}`); const a11yPassed = a11yCheck.allButtonsPresent && a11yCheck.allClickable; console.log(` ${a11yPassed ? 'āœ… PASS' : 'āŒ FAIL'} - Button visibility & accessibility`); testResults.push({ test: 'Button Visibility & Accessibility', passed: a11yPassed }); await a11yPage.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 ERRORS"); } else { console.log(`\nāš ļø ${errors.length} ERRORS`); } console.log("=".repeat(70) + "\n"); if (failedTests === 0) { console.log("šŸŽ‰ ALL BUTTON POSITIONING TESTS PASSED!"); } 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 } catch (error) { console.error('āŒ Test failed:', error); await browser.close(); } } await testButtonPositioning();