← 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);