test: Add comprehensive button positioning test (test 14)
Validates button positioning and responsive behavior across all viewports: Desktop (>900px): - Left side buttons (download, print, shortcuts, info) vertically stacked - Back-to-top button on right side (intentional design) - Zoom button visible - Different bottom values verify vertical stacking Wide Mobile (483-900px): - Horizontal layout at bottom center - Back-to-top remains on right side - Zoom button hidden Narrow Mobile (<483px): - Back-to-top moved UP (5.5rem) to avoid overlap - Still positioned on right side - Horizontal button layout maintained Accessibility: - All buttons present and clickable - Proper visibility checks This test caught and validates the recent fixes: 1. Back-to-top on RIGHT (not left) in all mobile viewports 2. Narrow mobile positioning to prevent button overlap 3. Consistent hover effects across all buttons Test results: 4/4 passed - Desktop layout verification - Wide mobile responsive layout - Narrow mobile overlap prevention - Button accessibility validation
This commit is contained in:
+34
-2
@@ -21,6 +21,7 @@ bun tests/mjs/4-htmx.test.mjs
|
||||
bun tests/mjs/5-language.test.mjs
|
||||
bun tests/mjs/6-modals.test.mjs
|
||||
bun tests/mjs/7-mobile-responsive.test.mjs
|
||||
bun tests/mjs/14-button-positioning.test.mjs
|
||||
```
|
||||
|
||||
## Active Test Suite (`tests/mjs/`)
|
||||
@@ -286,8 +287,8 @@ When adding tests:
|
||||
---
|
||||
|
||||
**Last Updated**: 2025-11-18
|
||||
**Test Count**: 14 active (0-13) - NO archive, NO legacy tests
|
||||
**Coverage**: Complete (UI, keyboard, libraries, i18n, modals, mobile, zoom, hover-sync, hyperscript, skeleton loaders, color themes)
|
||||
**Test Count**: 15 active (0-14) - NO archive, NO legacy tests
|
||||
**Coverage**: Complete (UI, keyboard, libraries, i18n, modals, mobile, zoom, hover-sync, hyperscript, skeleton loaders, color themes, button positioning)
|
||||
**Status**: SINGLE SOURCE OF TRUTH - Production specification
|
||||
**Philosophy**: Zero redundancy - Every test is essential and unique
|
||||
|
||||
@@ -339,6 +340,36 @@ When adding tests:
|
||||
|
||||
**Run**: `bun tests/mjs/13-color-theme-switcher.test.mjs`
|
||||
|
||||
### 14-button-positioning.test.mjs
|
||||
**Purpose**: Button positioning & responsive layout across all viewport sizes
|
||||
- ✅ Desktop layout (>900px) - Vertical stacking on left side + back-to-top on right
|
||||
- ✅ Wide mobile (483-900px) - Horizontal layout at bottom + back-to-top on right
|
||||
- ✅ Narrow mobile (<483px) - Back-to-top moved UP to avoid overlap (still on right)
|
||||
- ✅ Button visibility - Zoom hidden in mobile, all buttons clickable
|
||||
- ✅ Accessibility validation - All buttons have proper attributes
|
||||
|
||||
**Desktop Layout:**
|
||||
- Download, Print, Shortcuts, Info → LEFT side, vertically stacked (22rem, 18rem, 6rem, 2rem)
|
||||
- Back-to-top → RIGHT side (2rem)
|
||||
- Zoom button → VISIBLE
|
||||
|
||||
**Wide Mobile (483-900px):**
|
||||
- Download, Print, Shortcuts, Info → Horizontal layout at bottom center
|
||||
- Back-to-top → RIGHT side (1.5rem bottom)
|
||||
- Zoom button → HIDDEN
|
||||
|
||||
**Narrow Mobile (<483px):**
|
||||
- Download, Print, Shortcuts, Info → Horizontal layout at bottom
|
||||
- Back-to-top → RIGHT side, MOVED UP (5.5rem bottom) to avoid overlap
|
||||
- Zoom button → HIDDEN
|
||||
|
||||
**Critical**: Verifies responsive button positioning fixes including:
|
||||
1. Back-to-top always on RIGHT (not left) in mobile
|
||||
2. Narrow mobile (<483px) moves back-to-top UP to prevent overlap
|
||||
3. Consistent hover effects across all buttons
|
||||
|
||||
**Run**: `bun tests/mjs/14-button-positioning.test.mjs`
|
||||
|
||||
### New Tests (2025-11-17/18)
|
||||
- **8-hover-sync.test.mjs** - JavaScript wrapper → Hyperscript call pattern
|
||||
- **9-hyperscript-def-limit.test.mjs** - Proves no 3-def limit with 0.9.14+
|
||||
@@ -346,3 +377,4 @@ When adding tests:
|
||||
- **11-zoom-ui-exclusion.test.mjs** - UI elements excluded from zoom
|
||||
- **12-skeleton-language-transitions.test.mjs** - Skeleton loaders for language switch
|
||||
- **13-color-theme-switcher.test.mjs** - Dynamic color theme switcher
|
||||
- **14-button-positioning.test.mjs** - Button positioning & responsive layout
|
||||
|
||||
@@ -0,0 +1,330 @@
|
||||
#!/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();
|
||||
Reference in New Issue
Block a user