fix: Mobile hamburger menu and iPad sidebar visibility
Mobile fixes: - Add click toggle handler for hamburger menu (was hover-only) - Menu now opens/closes on tap and closes when clicking outside - Keep hover support for desktop iPad fixes: - Sidebar content now visible on touch devices (901-1280px) - Added (hover: hover) media query to prevent hide-on-hover on tablets Security improvements: - Replace exec.CommandContext with go-git library for git operations - Add path traversal and command injection prevention - Fix race condition in template hot reload - Add environment-based cookie Secure flag Code quality: - Add constants.go for magic numbers - Remove unused code (ParsePreferenceToggleRequest, DomainError) - Add FOUC prevention with inline critical CSS - Add Makefile dev/run/clean targets - Fix README git clone URL - Add doc/DECISIONS.md for architectural decisions Tests: - Add hamburger menu click toggle tests - Add iPad sidebar visibility tests - Update security tests for go-git implementation - Add cookie Secure flag tests
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
#!/usr/bin/env bun
|
||||
/**
|
||||
* IPAD SIDEBAR VISIBILITY TEST
|
||||
* ============================
|
||||
* Tests that sidebar content is visible on iPad/tablet (no hover support)
|
||||
* Bug: Sidebar content was hidden on 901-1280px screens, requiring hover to show
|
||||
* Fix: Added (hover: hover) media query condition
|
||||
*/
|
||||
|
||||
import { chromium } from 'playwright';
|
||||
|
||||
const URL = "http://localhost:1999";
|
||||
|
||||
// iPad viewport sizes
|
||||
const VIEWPORTS = {
|
||||
ipadPortrait: { width: 768, height: 1024 },
|
||||
ipadLandscape: { width: 1024, height: 768 },
|
||||
ipadProPortrait: { width: 1024, height: 1366 },
|
||||
ipadProLandscape: { width: 1366, height: 1024 },
|
||||
};
|
||||
|
||||
async function testIPadSidebarVisibility() {
|
||||
console.log('📱 IPAD SIDEBAR VISIBILITY TEST\n');
|
||||
console.log('='.repeat(70));
|
||||
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const testResults = [];
|
||||
|
||||
for (const [name, viewport] of Object.entries(VIEWPORTS)) {
|
||||
console.log(`\n📐 Testing ${name} (${viewport.width}x${viewport.height})...`);
|
||||
|
||||
// Create context WITHOUT hover support (simulates touch device)
|
||||
const context = await browser.newContext({
|
||||
viewport,
|
||||
hasTouch: true, // Simulate touch device
|
||||
});
|
||||
|
||||
const page = await context.newPage();
|
||||
await page.goto(URL);
|
||||
await page.waitForTimeout(1500);
|
||||
|
||||
// Check if sidebar content is visible
|
||||
const sidebarTest = await page.evaluate(() => {
|
||||
const sidebarContents = document.querySelectorAll('.sidebar-content');
|
||||
const results = [];
|
||||
|
||||
sidebarContents.forEach((content, index) => {
|
||||
const style = window.getComputedStyle(content);
|
||||
const isVisible = style.opacity !== '0' &&
|
||||
style.maxHeight !== '0px' &&
|
||||
style.display !== 'none' &&
|
||||
style.visibility !== 'hidden';
|
||||
const height = content.offsetHeight;
|
||||
|
||||
results.push({
|
||||
index,
|
||||
isVisible,
|
||||
opacity: style.opacity,
|
||||
maxHeight: style.maxHeight,
|
||||
height,
|
||||
display: style.display
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
totalSidebarContents: sidebarContents.length,
|
||||
visibleCount: results.filter(r => r.isVisible).length,
|
||||
details: results
|
||||
};
|
||||
});
|
||||
|
||||
const passed = sidebarTest.visibleCount === sidebarTest.totalSidebarContents;
|
||||
|
||||
console.log(` Total sidebar sections: ${sidebarTest.totalSidebarContents}`);
|
||||
console.log(` Visible sections: ${sidebarTest.visibleCount}`);
|
||||
console.log(` ${passed ? '✅ PASS' : '❌ FAIL'} - Sidebar content ${passed ? 'is' : 'NOT'} visible`);
|
||||
|
||||
if (!passed) {
|
||||
console.log(' Hidden sections:');
|
||||
sidebarTest.details.filter(d => !d.isVisible).forEach(d => {
|
||||
console.log(` - Section ${d.index}: opacity=${d.opacity}, maxHeight=${d.maxHeight}`);
|
||||
});
|
||||
}
|
||||
|
||||
testResults.push({ name, passed, ...sidebarTest });
|
||||
await context.close();
|
||||
}
|
||||
|
||||
// Summary
|
||||
console.log("\n" + "=".repeat(70));
|
||||
console.log("📊 TEST SUMMARY\n");
|
||||
|
||||
const passedCount = testResults.filter(r => r.passed).length;
|
||||
const totalCount = testResults.length;
|
||||
|
||||
testResults.forEach(result => {
|
||||
console.log(` ${result.passed ? '✅' : '❌'} ${result.name}`);
|
||||
});
|
||||
|
||||
console.log(`\n Total: ${passedCount}/${totalCount} tests passed`);
|
||||
console.log("=".repeat(70) + "\n");
|
||||
|
||||
await browser.close();
|
||||
|
||||
if (passedCount === totalCount) {
|
||||
console.log("🎉 IPAD SIDEBAR VISIBILITY VALIDATED!");
|
||||
process.exit(0);
|
||||
} else {
|
||||
console.log("⚠️ SOME TESTS FAILED - Sidebar content hidden on touch devices");
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
await testIPadSidebarVisibility();
|
||||
@@ -25,7 +25,7 @@ async function testMobileResponsive() {
|
||||
console.log('📱 MOBILE RESPONSIVE TEST\n');
|
||||
console.log('='.repeat(70));
|
||||
|
||||
const browser = await chromium.launch({ headless: false });
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const errors = [];
|
||||
const testResults = [];
|
||||
|
||||
@@ -88,43 +88,77 @@ async function testMobileResponsive() {
|
||||
testResults.push({ test: 'Mobile Viewport (375px)', passed: mobileViewportPassed });
|
||||
|
||||
// ========================================================================
|
||||
// TEST 2: Touch interactions (hamburger menu)
|
||||
// TEST 2: Hamburger Menu Click Toggle (Mobile)
|
||||
// ========================================================================
|
||||
console.log("\n2️⃣ Testing Touch Interactions...");
|
||||
console.log("\n2️⃣ Testing Hamburger Menu Click Toggle...");
|
||||
|
||||
const hamburger = await mobilePage.$('.hamburger-btn');
|
||||
if (hamburger) {
|
||||
// Tap hamburger to open menu
|
||||
await hamburger.tap();
|
||||
await mobilePage.waitForTimeout(500);
|
||||
// Click hamburger to open menu (simulates mobile tap)
|
||||
await hamburger.click();
|
||||
await mobilePage.waitForTimeout(300);
|
||||
|
||||
const menuTest = await mobilePage.evaluate(() => {
|
||||
const menuOpenTest = await mobilePage.evaluate(() => {
|
||||
const menu = document.querySelector('.navigation-menu');
|
||||
if (!menu) return { found: false };
|
||||
|
||||
const isOpen = menu.classList.contains('menu-open') ||
|
||||
window.getComputedStyle(menu).display !== 'none';
|
||||
const hasMenuOpen = menu.classList.contains('menu-open');
|
||||
const computedStyle = window.getComputedStyle(menu);
|
||||
const isVisible = computedStyle.opacity === '1' && computedStyle.maxHeight !== '0px';
|
||||
|
||||
return {
|
||||
found: true,
|
||||
isOpen,
|
||||
isVisible: menu.offsetHeight > 0
|
||||
hasMenuOpen,
|
||||
isVisible,
|
||||
maxHeight: computedStyle.maxHeight
|
||||
};
|
||||
});
|
||||
|
||||
console.log(` Menu found: ${menuTest.found ? '✅' : '❌'}`);
|
||||
console.log(` Menu opens on tap: ${menuTest.isOpen ? '✅' : '❌'}`);
|
||||
console.log(` ${menuTest.found && menuTest.isOpen ? '✅ PASS' : '❌ FAIL'} - Touch interactions`);
|
||||
testResults.push({ test: 'Touch Interactions', passed: menuTest.found && menuTest.isOpen });
|
||||
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 });
|
||||
|
||||
// Close menu
|
||||
if (menuTest.isOpen) {
|
||||
await hamburger.tap();
|
||||
await mobilePage.waitForTimeout(300);
|
||||
}
|
||||
} else {
|
||||
console.log(` ⚠️ SKIP - Hamburger menu not found`);
|
||||
testResults.push({ test: 'Touch Interactions', passed: true });
|
||||
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 });
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
@@ -274,10 +308,8 @@ async function testMobileResponsive() {
|
||||
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
|
||||
await browser.close();
|
||||
process.exit(failedTests === 0 ? 0 : 1);
|
||||
}
|
||||
|
||||
await testMobileResponsive();
|
||||
|
||||
Reference in New Issue
Block a user