#!/usr/bin/env node /** * Mobile UI Diagnostic Screenshot Tool * * Purpose: Generate annotated screenshots for manual review (LOCAL ONLY - not for CI) * * Features: * - Takes screenshots at multiple mobile viewports * - Annotates problem areas visually * - Opens sheet menu and captures it * - Generates HTML report * * Usage: node test_mobile_diagnostic.js */ const puppeteer = require('puppeteer'); const AuthHelper = require('../auth_helper'); const path = require('path'); const fs = require('fs').promises; // Skip in CI if (process.env.CI) { console.log('āļø Skipping diagnostic tool in CI environment'); process.exit(0); } // Test devices const DEVICES = { 'iPhone_SE': { width: 375, height: 667, deviceScaleFactor: 2, isMobile: true, hasTouch: true }, 'iPhone_14_Pro': { width: 430, height: 932, deviceScaleFactor: 3, isMobile: true, hasTouch: true }, 'iPad_Mini': { width: 768, height: 1024, deviceScaleFactor: 2, isMobile: true, hasTouch: true } }; // All pages to test const PAGES = [ { path: '/', name: 'Research', requiresAuth: true }, { path: '/history/', name: 'History', requiresAuth: true }, { path: '/news/', name: 'News', requiresAuth: true }, { path: '/news/subscriptions', name: 'Subscriptions', requiresAuth: true }, { path: '/settings/', name: 'Settings', requiresAuth: true }, { path: '/metrics/', name: 'Metrics', requiresAuth: true }, { path: '/metrics/context-overflow', name: 'Metrics_Context', requiresAuth: true }, { path: '/metrics/costs', name: 'Metrics_Costs', requiresAuth: true }, { path: '/benchmark/', name: 'Benchmark', requiresAuth: true }, { path: '/settings/collections', name: 'Collections', requiresAuth: true }, { path: '/library/', name: 'Library', requiresAuth: true }, { path: '/auth/login', name: 'Login', requiresAuth: false }, { path: '/auth/register', name: 'Register', requiresAuth: false }, ]; class MobileDiagnosticTool { constructor() { this.baseUrl = process.env.TEST_BASE_URL || 'http://127.0.0.1:5000'; this.timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5); this.outputDir = path.join(__dirname, 'diagnostic-screenshots', this.timestamp); this.results = []; } async run() { console.log('š Mobile UI Diagnostic Tool'); console.log('=' .repeat(60)); console.log(`š Output: ${this.outputDir}`); console.log('=' .repeat(60)); await fs.mkdir(this.outputDir, { recursive: true }); let browser; try { browser = await puppeteer.launch({ headless: process.env.HEADLESS !== 'false', args: ['--no-sandbox', '--disable-setuid-sandbox'] }); for (const [deviceName, viewport] of Object.entries(DEVICES)) { await this.testDevice(browser, deviceName, viewport); } await this.generateReport(); console.log('\nā Diagnostic complete!'); console.log(`š View report: file://${path.join(this.outputDir, 'report.html')}`); } finally { if (browser) await browser.close(); } } async testDevice(browser, deviceName, viewport) { console.log(`\nš± Testing ${deviceName} (${viewport.width}x${viewport.height})`); console.log('ā'.repeat(50)); const deviceDir = path.join(this.outputDir, deviceName); await fs.mkdir(deviceDir, { recursive: true }); const page = await browser.newPage(); await page.setViewport(viewport); // Authenticate const authHelper = new AuthHelper(page, this.baseUrl); try { await authHelper.ensureAuthenticated(); console.log(' ā Authenticated'); } catch (error) { console.log(' ā ļø Authentication failed:', error.message); } for (const pageInfo of PAGES) { await this.testPage(page, deviceName, deviceDir, pageInfo); } // Test sheet menu await this.testSheetMenu(page, deviceName, deviceDir); await page.close(); } async testPage(page, deviceName, deviceDir, pageInfo) { console.log(` š ${pageInfo.name}...`); try { await page.goto(this.baseUrl + pageInfo.path, { waitUntil: 'domcontentloaded', timeout: 30000 }); // Wait for mobile nav to initialize await new Promise(resolve => setTimeout(resolve, 1000)); // Run diagnostic checks const diagnostics = await page.evaluate(() => { const issues = []; // Check for horizontal scroll if (document.body.scrollWidth > window.innerWidth) { issues.push({ type: 'horizontal-scroll', severity: 'error', message: `Horizontal scroll detected: ${document.body.scrollWidth}px > ${window.innerWidth}px` }); } // Check touch targets const smallTargets = []; document.querySelectorAll('button, a, input, select, textarea').forEach(el => { const rect = el.getBoundingClientRect(); if (rect.width > 0 && rect.height > 0) { if (rect.width < 44 || rect.height < 44) { smallTargets.push({ tag: el.tagName, text: el.textContent?.slice(0, 20) || el.placeholder || '', size: `${Math.round(rect.width)}x${Math.round(rect.height)}`, rect: { top: rect.top, left: rect.left, width: rect.width, height: rect.height } }); } } }); if (smallTargets.length > 0) { issues.push({ type: 'small-touch-targets', severity: 'warning', message: `${smallTargets.length} elements smaller than 44x44px`, details: smallTargets.slice(0, 5) }); } // Check small text const smallText = []; document.querySelectorAll('p, span, label, h1, h2, h3, h4, h5, h6, a, button').forEach(el => { const style = window.getComputedStyle(el); const fontSize = parseFloat(style.fontSize); if (fontSize > 0 && fontSize < 12 && el.textContent?.trim()) { smallText.push({ tag: el.tagName, text: el.textContent.slice(0, 30), fontSize: Math.round(fontSize) }); } }); if (smallText.length > 0) { issues.push({ type: 'small-text', severity: 'warning', message: `${smallText.length} elements with font size < 12px`, details: smallText.slice(0, 5) }); } // Check mobile nav const mobileNav = document.querySelector('.ldr-mobile-bottom-nav'); const mobileNavVisible = mobileNav && window.getComputedStyle(mobileNav).display !== 'none' && mobileNav.classList.contains('visible'); // Check if content is hidden behind nav const mainContent = document.querySelector('.ldr-main-content, .ldr-page.active, main'); let contentHiddenBehindNav = false; if (mainContent && mobileNav) { const contentRect = mainContent.getBoundingClientRect(); const navRect = mobileNav.getBoundingClientRect(); if (contentRect.bottom > navRect.top) { contentHiddenBehindNav = true; issues.push({ type: 'content-behind-nav', severity: 'error', message: 'Content may be hidden behind mobile navigation' }); } } // Check sidebar const sidebar = document.querySelector('.ldr-sidebar'); const sidebarVisible = sidebar && window.getComputedStyle(sidebar).display !== 'none'; if (sidebarVisible && window.innerWidth <= 768) { issues.push({ type: 'sidebar-visible-on-mobile', severity: 'error', message: 'Desktop sidebar is visible on mobile viewport' }); } return { url: window.location.href, title: document.title, viewport: { width: window.innerWidth, height: window.innerHeight }, mobileNav: { exists: !!mobileNav, visible: mobileNavVisible }, sidebar: { exists: !!sidebar, visible: sidebarVisible }, issues }; }); // Annotate issues on page if (diagnostics.issues.length > 0) { await this.annotateIssues(page, diagnostics.issues); } // Take screenshot const screenshotPath = path.join(deviceDir, `${pageInfo.name}.png`); await page.screenshot({ path: screenshotPath, fullPage: false }); // Store results this.results.push({ device: deviceName, page: pageInfo.name, path: pageInfo.path, screenshot: path.relative(this.outputDir, screenshotPath), diagnostics }); const issueCount = diagnostics.issues.length; const status = issueCount === 0 ? 'ā ' : issueCount > 2 ? 'ā' : 'ā ļø'; console.log(` ${status} ${issueCount} issues | Nav: ${diagnostics.mobileNav.visible ? 'ā ' : 'ā'}`); } catch (error) { console.log(` ā Error: ${error.message}`); this.results.push({ device: deviceName, page: pageInfo.name, path: pageInfo.path, error: error.message }); } } async annotateIssues(page, issues) { await page.evaluate((issueList) => { // Remove previous annotations document.querySelectorAll('.diagnostic-annotation').forEach(el => el.remove()); issueList.forEach(issue => { if (issue.type === 'small-touch-targets' && issue.details) { issue.details.forEach(target => { const annotation = document.createElement('div'); annotation.className = 'diagnostic-annotation'; annotation.style.cssText = ` position: fixed; top: ${target.rect.top}px; left: ${target.rect.left}px; width: ${target.rect.width}px; height: ${target.rect.height}px; border: 2px dashed orange; background: rgba(255, 165, 0, 0.2); pointer-events: none; z-index: 99999; `; document.body.appendChild(annotation); }); } }); }, issues); } async testSheetMenu(page, deviceName, deviceDir) { console.log(' š Testing sheet menu...'); try { // Navigate to a page with mobile nav await page.goto(this.baseUrl + '/news/', { waitUntil: 'domcontentloaded', timeout: 30000 }); await new Promise(resolve => setTimeout(resolve, 1000)); // Click More button to open sheet const moreBtn = await page.$('.ldr-mobile-nav-tab[data-tab-id="more"]'); if (moreBtn) { await moreBtn.click(); await new Promise(resolve => setTimeout(resolve, 500)); // Check if sheet opened const sheetOpen = await page.evaluate(() => { const sheet = document.querySelector('.ldr-mobile-sheet-menu'); return sheet && sheet.classList.contains('active'); }); if (sheetOpen) { // Check if Settings section is visible (the one that was getting cut off) const settingsVisible = await page.evaluate(() => { const sheet = document.querySelector('.ldr-mobile-sheet-content'); const settingsSection = Array.from(sheet?.querySelectorAll('.ldr-mobile-sheet-title') || []) .find(el => el.textContent.includes('Settings')); if (!settingsSection) return { found: false }; const rect = settingsSection.getBoundingClientRect(); const sheetRect = sheet.getBoundingClientRect(); return { found: true, visible: rect.top < sheetRect.bottom, position: { top: rect.top, sheetBottom: sheetRect.bottom } }; }); // Take screenshot of open sheet const screenshotPath = path.join(deviceDir, 'SheetMenu_Open.png'); await page.screenshot({ path: screenshotPath, fullPage: false }); this.results.push({ device: deviceName, page: 'SheetMenu', path: 'N/A', screenshot: path.relative(this.outputDir, screenshotPath), diagnostics: { sheetMenu: { opened: true, settingsVisible: settingsVisible.visible, settingsDetails: settingsVisible } } }); const status = settingsVisible.visible ? 'ā ' : 'ā'; console.log(` ${status} Settings section ${settingsVisible.visible ? 'visible' : 'CUT OFF!'}`); // Close sheet const overlay = await page.$('.ldr-mobile-sheet-overlay'); if (overlay) await overlay.click(); } else { console.log(' ā ļø Sheet menu did not open'); } } else { console.log(' ā ļø More button not found'); } } catch (error) { console.log(` ā Error testing sheet menu: ${error.message}`); } } async generateReport() { const reportPath = path.join(this.outputDir, 'report.html'); const html = `
Generated: ${new Date().toISOString()}
Total pages tested: ${this.results.length}
Pages with errors: ${this.results.filter(r => r.diagnostics?.issues?.some(i => i.severity === 'error')).length}
Pages with warnings: ${this.results.filter(r => r.diagnostics?.issues?.some(i => i.severity === 'warning')).length}