← back to Handbag Auth Nextjs

mobile-test.js

471 lines

const puppeteer = require('puppeteer');
const fs = require('fs').promises;
const path = require('path');

const SITE_URL = 'http://45.61.58.125:7991';
const VIEWPORTS = [
  { name: 'iPhone-SE', width: 375, height: 667 },
  { name: 'iPhone-12-Pro', width: 390, height: 844 },
  { name: 'iPhone-14-Pro-Max', width: 430, height: 932 },
  { name: 'iPad-Mini', width: 768, height: 1024 }
];

const PAGES = [
  { name: 'home', path: '/' },
  { name: 'camera', path: '/camera' },
  { name: 'price-tracker', path: '/price-tracker' }
];

class MobileUITester {
  constructor() {
    this.browser = null;
    this.issues = [];
    this.testResults = {
      responsiveness: [],
      navigation: [],
      forms: [],
      performance: [],
      typography: [],
      interactions: []
    };
  }

  async init() {
    this.browser = await puppeteer.launch({
      headless: true,
      args: ['--no-sandbox', '--disable-setuid-sandbox']
    });

    // Create screenshots directory
    await fs.mkdir('mobile-screenshots', { recursive: true });
  }

  async testPage(page, viewport, pageName) {
    console.log(`Testing ${pageName} at ${viewport.width}x${viewport.height}...`);

    await page.setViewport({
      width: viewport.width,
      height: viewport.height,
      isMobile: true,
      hasTouch: true,
      deviceScaleFactor: 2
    });

    await page.goto(`${SITE_URL}${pageName}`, {
      waitUntil: 'networkidle2',
      timeout: 30000
    });

    // Wait for content to load
    await page.waitForTimeout(2000);

    // Take screenshot
    const screenshotName = `${viewport.name}-${pageName.replace('/', '') || 'home'}.png`;
    await page.screenshot({
      path: path.join('mobile-screenshots', screenshotName),
      fullPage: true
    });

    // Test results object
    const testResult = {
      viewport: viewport.name,
      page: pageName,
      issues: []
    };

    // 1. Check for horizontal overflow
    const hasHorizontalScroll = await page.evaluate(() => {
      return document.documentElement.scrollWidth > document.documentElement.clientWidth;
    });
    if (hasHorizontalScroll) {
      testResult.issues.push('Horizontal scroll detected - content overflows viewport');
    }

    // 2. Check text readability
    const smallTextElements = await page.evaluate(() => {
      const elements = document.querySelectorAll('*');
      const smallText = [];
      elements.forEach(el => {
        const styles = window.getComputedStyle(el);
        const fontSize = parseInt(styles.fontSize);
        if (fontSize > 0 && fontSize < 14 && el.textContent.trim()) {
          smallText.push({
            text: el.textContent.substring(0, 50),
            size: fontSize,
            selector: el.className || el.tagName
          });
        }
      });
      return smallText.slice(0, 5); // Return first 5 issues
    });

    if (smallTextElements.length > 0) {
      testResult.issues.push(`Small text found (< 14px): ${smallTextElements.length} elements`);
    }

    // 3. Check input font sizes (iOS zoom prevention)
    const inputFontSizes = await page.evaluate(() => {
      const inputs = document.querySelectorAll('input, textarea, select');
      const smallInputs = [];
      inputs.forEach(input => {
        const fontSize = parseInt(window.getComputedStyle(input).fontSize);
        if (fontSize < 16) {
          smallInputs.push({
            type: input.type || input.tagName,
            fontSize: fontSize,
            id: input.id || input.className
          });
        }
      });
      return smallInputs;
    });

    if (inputFontSizes.length > 0) {
      testResult.issues.push(`Input elements with font-size < 16px (causes iOS zoom): ${inputFontSizes.length}`);
      this.testResults.typography.push({
        page: pageName,
        viewport: viewport.name,
        issue: 'Input font sizes < 16px',
        details: inputFontSizes
      });
    }

    // 4. Check button/link touch target sizes
    const smallTouchTargets = await page.evaluate(() => {
      const clickables = document.querySelectorAll('button, a, [role="button"]');
      const smallTargets = [];
      clickables.forEach(el => {
        const rect = el.getBoundingClientRect();
        if (rect.width < 44 || rect.height < 44) {
          smallTargets.push({
            element: el.tagName,
            text: el.textContent?.substring(0, 30),
            width: Math.round(rect.width),
            height: Math.round(rect.height)
          });
        }
      });
      return smallTargets.slice(0, 5);
    });

    if (smallTouchTargets.length > 0) {
      testResult.issues.push(`Small touch targets (< 44px): ${smallTouchTargets.length} elements`);
      this.testResults.interactions.push({
        page: pageName,
        viewport: viewport.name,
        issue: 'Touch targets too small',
        details: smallTouchTargets
      });
    }

    // 5. Check images for proper sizing
    const imageIssues = await page.evaluate(() => {
      const images = document.querySelectorAll('img');
      const issues = [];
      images.forEach(img => {
        if (img.naturalWidth > 0) {
          const displayWidth = img.getBoundingClientRect().width;
          const viewportWidth = window.innerWidth;
          if (displayWidth > viewportWidth) {
            issues.push({
              src: img.src.substring(img.src.lastIndexOf('/') + 1),
              displayWidth: Math.round(displayWidth),
              viewportWidth: viewportWidth
            });
          }
        }
      });
      return issues;
    });

    if (imageIssues.length > 0) {
      testResult.issues.push(`Images overflowing viewport: ${imageIssues.length}`);
    }

    // 6. Check navigation menu
    if (pageName === '/') {
      const navTest = await this.testNavigation(page, viewport);
      if (navTest.issues.length > 0) {
        testResult.issues = [...testResult.issues, ...navTest.issues];
        this.testResults.navigation.push(navTest);
      }
    }

    // 7. Check form elements on specific pages
    if (pageName === '/price-tracker' || pageName === '/') {
      const formTest = await this.testForms(page, viewport, pageName);
      if (formTest.issues.length > 0) {
        testResult.issues = [...testResult.issues, ...formTest.issues];
        this.testResults.forms.push(formTest);
      }
    }

    // 8. Check grid/card layouts
    const cardLayoutIssues = await page.evaluate(() => {
      const cards = document.querySelectorAll('[class*="card"], [class*="grid"] > div');
      const issues = [];
      const viewportWidth = window.innerWidth;

      cards.forEach(card => {
        const rect = card.getBoundingClientRect();
        if (rect.right > viewportWidth) {
          issues.push('Card extends beyond viewport');
        }
      });

      // Check if grid is responsive
      const grids = document.querySelectorAll('[class*="grid"]');
      grids.forEach(grid => {
        const styles = window.getComputedStyle(grid);
        const columns = styles.gridTemplateColumns;
        if (columns && !columns.includes('repeat') && !columns.includes('minmax')) {
          issues.push('Grid may not be responsive - using fixed columns');
        }
      });

      return issues;
    });

    if (cardLayoutIssues.length > 0) {
      testResult.issues = [...testResult.issues, ...cardLayoutIssues];
    }

    this.testResults.responsiveness.push(testResult);
    return testResult;
  }

  async testNavigation(page, viewport) {
    const result = {
      page: 'navigation',
      viewport: viewport.name,
      issues: []
    };

    // Check for mobile menu button
    const hasMobileMenu = await page.evaluate(() => {
      const menuButtons = document.querySelectorAll('[aria-label*="menu" i], [class*="menu" i], button svg');
      return menuButtons.length > 0;
    });

    if (!hasMobileMenu && viewport.width < 768) {
      result.issues.push('No mobile menu button found');
    }

    // Test menu interaction if exists
    if (hasMobileMenu) {
      try {
        await page.click('button:has(svg), [aria-label*="menu" i]');
        await page.waitForTimeout(500);

        const menuVisible = await page.evaluate(() => {
          const menus = document.querySelectorAll('nav, [role="navigation"], [class*="menu"]');
          return Array.from(menus).some(menu => {
            const styles = window.getComputedStyle(menu);
            return styles.display !== 'none' && styles.visibility !== 'hidden';
          });
        });

        if (!menuVisible) {
          result.issues.push('Mobile menu does not open when clicked');
        }
      } catch (e) {
        result.issues.push('Could not interact with mobile menu');
      }
    }

    return result;
  }

  async testForms(page, viewport, pageName) {
    const result = {
      page: pageName,
      viewport: viewport.name,
      issues: []
    };

    // Check search bar
    const searchInputs = await page.evaluate(() => {
      const inputs = document.querySelectorAll('input[type="search"], input[placeholder*="search" i]');
      return inputs.length;
    });

    if (searchInputs > 0) {
      const searchTest = await page.evaluate(() => {
        const input = document.querySelector('input[type="search"], input[placeholder*="search" i]');
        const rect = input.getBoundingClientRect();
        const issues = [];

        if (rect.width < 200) {
          issues.push('Search input may be too narrow on mobile');
        }

        const fontSize = parseInt(window.getComputedStyle(input).fontSize);
        if (fontSize < 16) {
          issues.push('Search input font size < 16px (will cause iOS zoom)');
        }

        return issues;
      });

      result.issues = [...result.issues, ...searchTest];
    }

    // Check filter dropdowns
    const dropdowns = await page.evaluate(() => {
      const selects = document.querySelectorAll('select, [role="combobox"]');
      const issues = [];

      selects.forEach(select => {
        const fontSize = parseInt(window.getComputedStyle(select).fontSize);
        if (fontSize < 16) {
          issues.push('Dropdown font size < 16px');
        }
      });

      return issues;
    });

    if (dropdowns.length > 0) {
      result.issues = [...result.issues, ...dropdowns];
    }

    return result;
  }

  async testAllPages() {
    const page = await this.browser.newPage();

    // Set user agent to mobile
    await page.setUserAgent('Mozilla/5.0 (iPhone; CPU iPhone OS 14_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.1 Mobile/15E148 Safari/604.1');

    for (const viewport of VIEWPORTS) {
      for (const pageInfo of PAGES) {
        try {
          await this.testPage(page, viewport, pageInfo.path);
        } catch (error) {
          console.error(`Error testing ${pageInfo.name} at ${viewport.name}:`, error.message);
          this.issues.push({
            page: pageInfo.path,
            viewport: viewport.name,
            error: error.message
          });
        }
      }
    }

    await page.close();
  }

  async generateReport() {
    const report = {
      timestamp: new Date().toISOString(),
      siteUrl: SITE_URL,
      summary: {
        totalIssues: 0,
        criticalIssues: [],
        recommendations: []
      },
      detailedResults: this.testResults,
      errors: this.issues
    };

    // Count issues
    let totalIssues = 0;
    const criticalIssues = new Set();

    this.testResults.responsiveness.forEach(result => {
      totalIssues += result.issues.length;
      result.issues.forEach(issue => {
        if (issue.includes('Horizontal scroll') || issue.includes('overflow')) {
          criticalIssues.add('Horizontal scrolling/overflow issues detected');
        }
        if (issue.includes('Input elements with font-size < 16px')) {
          criticalIssues.add('Input font sizes will cause iOS zoom issues');
        }
        if (issue.includes('Small touch targets')) {
          criticalIssues.add('Touch targets too small for mobile interaction');
        }
      });
    });

    report.summary.totalIssues = totalIssues;
    report.summary.criticalIssues = Array.from(criticalIssues);

    // Generate recommendations
    if (criticalIssues.has('Input font sizes will cause iOS zoom issues')) {
      report.summary.recommendations.push('Set all input, textarea, and select elements to font-size: 16px minimum');
    }
    if (criticalIssues.has('Touch targets too small for mobile interaction')) {
      report.summary.recommendations.push('Ensure all interactive elements are at least 44x44px for comfortable touch interaction');
    }
    if (criticalIssues.has('Horizontal scrolling/overflow issues detected')) {
      report.summary.recommendations.push('Review layouts to prevent horizontal overflow, use max-width: 100% on containers');
    }

    // Save report
    await fs.writeFile(
      'mobile-test-report.json',
      JSON.stringify(report, null, 2)
    );

    return report;
  }

  async close() {
    if (this.browser) {
      await this.browser.close();
    }
  }
}

async function runMobileTests() {
  console.log('Starting Mobile UI/UX Testing...');
  console.log('================================');

  const tester = new MobileUITester();

  try {
    await tester.init();
    await tester.testAllPages();
    const report = await tester.generateReport();

    console.log('\n=== TEST RESULTS ===');
    console.log(`Total Issues Found: ${report.summary.totalIssues}`);

    if (report.summary.criticalIssues.length > 0) {
      console.log('\nCritical Issues:');
      report.summary.criticalIssues.forEach(issue => {
        console.log(`  - ${issue}`);
      });
    }

    if (report.summary.recommendations.length > 0) {
      console.log('\nRecommendations:');
      report.summary.recommendations.forEach(rec => {
        console.log(`  - ${rec}`);
      });
    }

    // Display detailed results
    console.log('\n=== DETAILED RESULTS BY PAGE ===');
    report.detailedResults.responsiveness.forEach(result => {
      if (result.issues.length > 0) {
        console.log(`\n${result.viewport} - ${result.page}:`);
        result.issues.forEach(issue => {
          console.log(`  ⚠️  ${issue}`);
        });
      }
    });

    console.log('\n✅ Testing complete! Screenshots saved in mobile-screenshots/');
    console.log('📊 Full report saved in mobile-test-report.json');

  } catch (error) {
    console.error('Testing failed:', error);
  } finally {
    await tester.close();
  }
}

// Run the tests
runMobileTests().catch(console.error);