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:
juanatsap
2025-11-30 09:29:35 +00:00
parent 60c1b5ac2b
commit eb92f64e93
18 changed files with 874 additions and 183 deletions
@@ -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();
+57 -25
View File
@@ -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();