331 lines
13 KiB
JavaScript
331 lines
13 KiB
JavaScript
|
|
#!/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();
|