#!/usr/bin/env node /** * Comprehensive Mobile UI Test * Tests all UI elements for mobile compatibility, not just navigation */ const puppeteer = require('puppeteer'); const AuthHelper = require('../auth_helper'); const fs = require('fs').promises; const path = require('path'); class MobileUITester { constructor() { this.baseUrl = process.env.TEST_BASE_URL || 'http://127.0.0.1:5000'; this.results = { passed: [], failed: [], warnings: [] }; } async test() { let browser; try { browser = await puppeteer.launch({ headless: process.env.HEADLESS !== 'false', args: ['--no-sandbox', '--disable-setuid-sandbox'] }); const devices = [ { name: 'iPhone_SE', width: 375, height: 667 }, { name: 'iPhone_14_Pro', width: 430, height: 932 }, { name: 'iPad_Mini', width: 768, height: 1024 } ]; for (const device of devices) { await this.testDevice(browser, device); } this.printResults(); // Exit with error if any tests failed process.exit(this.results.failed.length > 0 ? 1 : 0); } catch (error) { console.error('Test failed:', error); process.exit(1); } finally { if (browser) await browser.close(); } } async testDevice(browser, device) { console.log(`\nšŸ“± Testing ${device.name} (${device.width}x${device.height})`); console.log('═'.repeat(50)); const page = await browser.newPage(); await page.setViewport({ width: device.width, height: device.height, isMobile: true, hasTouch: true }); // Authenticate const authHelper = new AuthHelper(page, this.baseUrl); await authHelper.ensureAuthenticated(); // Test pages const pages = [ { path: '/', name: 'Research', tests: ['topBar', 'inputFields', 'buttons', 'modals'] }, { path: '/history/', name: 'History', tests: ['topBar', 'searchBar', 'list'] }, { path: '/news/', name: 'News', tests: ['topBar', 'filters', 'cards'] }, { path: '/settings/', name: 'Settings', tests: ['topBar', 'tabs', 'forms', 'scrolling'] }, { path: '/metrics/', name: 'Metrics', tests: ['topBar', 'charts', 'tables'] }, { path: '/benchmark/', name: 'Benchmark', tests: ['topBar', 'forms', 'buttons'] } ]; for (const pageInfo of pages) { console.log(`\n šŸ“„ ${pageInfo.name}`); await page.goto(this.baseUrl + pageInfo.path, { waitUntil: 'domcontentloaded', timeout: 30000 }); // Run UI tests const results = await page.evaluate(() => { const tests = {}; // 1. Check duplicate navigation bars tests.duplicateNavBars = { oldNav: !!document.querySelector('.mobile-tab-bar'), newNav: !!document.querySelector('.ldr-mobile-bottom-nav'), hasDuplicates: false }; tests.duplicateNavBars.hasDuplicates = tests.duplicateNavBars.oldNav && tests.duplicateNavBars.newNav; // 2. Check top bar elements const topBar = document.querySelector('.top-bar'); if (topBar) { const topBarRect = topBar.getBoundingClientRect(); const userInfo = document.querySelector('.user-info'); const logoutBtn = document.querySelector('.logout-btn'); tests.topBar = { exists: true, overflowing: topBarRect.width < topBar.scrollWidth, userInfoVisible: userInfo ? window.getComputedStyle(userInfo).display !== 'none' : false, logoutVisible: logoutBtn ? window.getComputedStyle(logoutBtn).display !== 'none' : false, height: topBarRect.height, isClipped: false }; // Check if elements are clipped if (userInfo) { const userRect = userInfo.getBoundingClientRect(); tests.topBar.isClipped = userRect.right > window.innerWidth; } } else { tests.topBar = { exists: false }; } // 3. Check input fields accessibility const inputs = document.querySelectorAll('input, textarea, select'); tests.inputs = { total: inputs.length, accessible: 0, overlapped: [], tooSmall: [] }; inputs.forEach(input => { const rect = input.getBoundingClientRect(); const style = window.getComputedStyle(input); // Check if visible and accessible if (rect.width > 0 && rect.height > 0 && style.display !== 'none' && style.visibility !== 'hidden') { tests.inputs.accessible++; // Check if too small for touch (minimum 44x44px for iOS) if (rect.height < 44) { tests.inputs.tooSmall.push({ type: input.tagName, height: rect.height, id: input.id || input.name }); } } // Check for overlaps with navigation const navBar = document.querySelector('.ldr-mobile-bottom-nav, .mobile-tab-bar'); if (navBar) { const navRect = navBar.getBoundingClientRect(); if (rect.bottom > navRect.top && rect.top < navRect.bottom) { tests.inputs.overlapped.push({ type: input.tagName, id: input.id || input.name }); } } }); // 4. Check buttons and touch targets const buttons = document.querySelectorAll('button, .btn, a.btn'); tests.buttons = { total: buttons.length, tooSmall: [], overlapped: [] }; buttons.forEach(button => { const rect = button.getBoundingClientRect(); if (rect.height < 44 || rect.width < 44) { tests.buttons.tooSmall.push({ text: button.textContent.trim().substring(0, 20), size: `${rect.width}x${rect.height}` }); } }); // 5. Check horizontal scrolling (should not exist) tests.horizontalScroll = { bodyWidth: document.body.scrollWidth, windowWidth: window.innerWidth, hasScroll: document.body.scrollWidth > window.innerWidth }; // 6. Check content padding for nav bars const mainContent = document.querySelector('.main-content, .page, main'); if (mainContent) { const contentRect = mainContent.getBoundingClientRect(); const navBar = document.querySelector('.ldr-mobile-bottom-nav'); tests.contentPadding = { hasBottomPadding: false, bottomSpace: 0 }; if (navBar) { const navRect = navBar.getBoundingClientRect(); tests.contentPadding.bottomSpace = window.innerHeight - contentRect.bottom; tests.contentPadding.hasBottomPadding = tests.contentPadding.bottomSpace >= navRect.height; } } // 7. Check tab navigation (Settings page) const tabs = document.querySelectorAll('.nav-tabs, .tab-navigation, [role="tablist"]'); if (tabs.length > 0) { const tabContainer = tabs[0]; const tabRect = tabContainer.getBoundingClientRect(); tests.tabs = { exists: true, overflowing: tabContainer.scrollWidth > tabRect.width, scrollable: window.getComputedStyle(tabContainer).overflowX === 'auto' || window.getComputedStyle(tabContainer).overflowX === 'scroll' }; } // 8. Check modals and overlays const modals = document.querySelectorAll('.modal, .sheet-menu, .overlay'); tests.modals = { visible: [], zIndexIssues: [] }; modals.forEach(modal => { const style = window.getComputedStyle(modal); if (style.display !== 'none' && style.visibility !== 'hidden') { tests.modals.visible.push(modal.className); // Check z-index const zIndex = parseInt(style.zIndex, 10) || 0; if (zIndex < 1000) { tests.modals.zIndexIssues.push({ class: modal.className, zIndex }); } } }); // 9. Check text readability const textElements = document.querySelectorAll('p, span, label, h1, h2, h3, h4, h5, h6'); tests.text = { tooSmall: [], truncated: [] }; textElements.forEach(elem => { const style = window.getComputedStyle(elem); const fontSize = parseFloat(style.fontSize); if (fontSize < 12) { tests.text.tooSmall.push({ tag: elem.tagName, fontSize, text: elem.textContent.substring(0, 30) }); } // Check for text overflow if (elem.scrollWidth > elem.clientWidth) { tests.text.truncated.push({ tag: elem.tagName, text: elem.textContent.substring(0, 30) }); } }); return tests; }); // Analyze results this.analyzeResults(device.name, pageInfo.name, results); // Take screenshot for review const screenshotDir = './mobile-ui-tests'; await fs.mkdir(screenshotDir, { recursive: true }); const screenshotPath = path.join(screenshotDir, `${device.name}_${pageInfo.name}.png`); await page.screenshot({ path: screenshotPath, fullPage: false }); } await page.close(); } analyzeResults(device, pageName, results) { const context = `${device} - ${pageName}`; // Check for critical issues if (results.duplicateNavBars?.hasDuplicates) { this.results.failed.push(`${context}: Duplicate navigation bars (both old and new)`); } if (results.topBar?.isClipped) { this.results.failed.push(`${context}: Top bar elements are clipped/cut off`); } if (results.inputs?.overlapped?.length > 0) { this.results.failed.push(`${context}: ${results.inputs.overlapped.length} input(s) overlapped by navigation`); } if (results.horizontalScroll?.hasScroll) { this.results.failed.push(`${context}: Horizontal scroll detected (${results.horizontalScroll.bodyWidth}px > ${results.horizontalScroll.windowWidth}px)`); } if (results.contentPadding && !results.contentPadding.hasBottomPadding) { this.results.warnings.push(`${context}: Content may be hidden behind navigation bar`); } if (results.tabs?.overflowing && !results.tabs?.scrollable) { this.results.failed.push(`${context}: Tab navigation overflowing and not scrollable`); } // Check for warnings if (results.inputs?.tooSmall?.length > 0) { this.results.warnings.push(`${context}: ${results.inputs.tooSmall.length} input(s) below 44px touch target`); } if (results.buttons?.tooSmall?.length > 0) { this.results.warnings.push(`${context}: ${results.buttons.tooSmall.length} button(s) below 44px touch target`); } if (results.text?.tooSmall?.length > 0) { this.results.warnings.push(`${context}: ${results.text.tooSmall.length} text element(s) below 12px`); } if (results.text?.truncated?.length > 0) { this.results.warnings.push(`${context}: ${results.text.truncated.length} text element(s) truncated`); } // Log success if no issues if (!this.hasIssues(results)) { this.results.passed.push(`${context}: All UI elements properly displayed`); } } hasIssues(results) { return results.duplicateNavBars?.hasDuplicates || results.topBar?.isClipped || results.inputs?.overlapped?.length > 0 || results.horizontalScroll?.hasScroll || (results.tabs?.overflowing && !results.tabs?.scrollable); } printResults() { console.log('\n' + '═'.repeat(60)); console.log('šŸ“Š MOBILE UI TEST RESULTS'); console.log('═'.repeat(60)); if (this.results.passed.length > 0) { console.log('\nāœ… PASSED:'); this.results.passed.forEach(p => console.log(` ${p}`)); } if (this.results.failed.length > 0) { console.log('\nāŒ FAILED:'); this.results.failed.forEach(f => console.log(` ${f}`)); } if (this.results.warnings.length > 0) { console.log('\nāš ļø WARNINGS:'); this.results.warnings.forEach(w => console.log(` ${w}`)); } console.log('\n' + '═'.repeat(60)); console.log(`Total: ${this.results.passed.length} passed, ${this.results.failed.length} failed, ${this.results.warnings.length} warnings`); if (this.results.failed.length === 0) { console.log('āœ… All critical UI tests passed!'); } else { console.log('āŒ Critical UI issues found - see failures above'); } } } // Run tests if (require.main === module) { const tester = new MobileUITester(); tester.test(); } module.exports = MobileUITester;