import { test, expect } from '@playwright/test'; import { TEST_CONFIG } from './utils/test-helpers'; /** * Mobile Responsive Tests * * These tests verify that the application works correctly on various mobile device sizes. * Tests cover: * - Small phones (iPhone SE, small Android) * - Medium phones (iPhone 12/13, standard Android) * - Large phones (iPhone Pro Max, large Android) * - Small tablets (iPad Mini) * * To run mobile responsive tests: * - Run: npx playwright test mobile-responsive * - Run on specific device: npx playwright test mobile-responsive --project="iPhone 12" */ // Common mobile viewport sizes const MOBILE_VIEWPORTS = { 'iPhone SE': { width: 375, height: 667 }, // Small phone 'iPhone 12': { width: 390, height: 844 }, // Medium phone 'iPhone 14 Pro Max': { width: 430, height: 932 }, // Large phone 'Samsung Galaxy S21': { width: 360, height: 800 }, // Android medium 'Pixel 5': { width: 393, height: 851 }, // Android medium 'iPad Mini': { width: 768, height: 1024 }, // Small tablet }; test.describe('Mobile Responsive Tests', () => { // Use authenticated state for most tests test.use({ storageState: 'e2e/.auth/user.json' }); test.describe('Small Phone (iPhone SE - 375x667)', () => { test.beforeEach(async ({ page }) => { await page.setViewportSize(MOBILE_VIEWPORTS['iPhone SE']); }); test('dashboard should be usable on small phone', async ({ page }) => { await page.goto(`${TEST_CONFIG.FRONTEND_URL}/dashboard`); await page.waitForLoadState('networkidle'); // Check that main content is visible const main = page.locator('main, [role="main"]').first(); await expect(main).toBeVisible(); // Check that navigation is accessible (hamburger menu or similar) const navButton = page.locator('button[aria-label*="menu"], button[aria-label*="Menu"], [data-testid*="menu"]').first(); if (await navButton.count() > 0) { await expect(navButton).toBeVisible(); } // Verify no horizontal scrolling const bodyWidth = await page.evaluate(() => document.body.scrollWidth); const viewportWidth = MOBILE_VIEWPORTS['iPhone SE'].width; expect(bodyWidth).toBeLessThanOrEqual(viewportWidth + 10); // Allow small margin }); test('login page should be usable on small phone', async ({ page }) => { await page.context().clearCookies(); await page.goto(`${TEST_CONFIG.FRONTEND_URL}/login`); await page.waitForLoadState('networkidle'); // Check form elements are visible and accessible const emailInput = page.locator('input[type="email"], input[name="email"]').first(); const passwordInput = page.locator('input[type="password"], input[name="password"]').first(); const submitButton = page.locator('button[type="submit"]').first(); await expect(emailInput).toBeVisible(); await expect(passwordInput).toBeVisible(); await expect(submitButton).toBeVisible(); // Check that inputs are large enough to tap (min 44x44px recommended) const emailBox = await emailInput.boundingBox(); const passwordBox = await passwordInput.boundingBox(); const buttonBox = await submitButton.boundingBox(); if (emailBox) expect(emailBox.height).toBeGreaterThanOrEqual(40); if (passwordBox) expect(passwordBox.height).toBeGreaterThanOrEqual(40); if (buttonBox) expect(buttonBox.height).toBeGreaterThanOrEqual(40); }); test('profile page should be usable on small phone', async ({ page }) => { await page.goto(`${TEST_CONFIG.FRONTEND_URL}/profile`); await page.waitForLoadState('networkidle'); const main = page.locator('main, [role="main"]').first(); await expect(main).toBeVisible(); // Check that form elements are accessible const inputs = page.locator('input, textarea, select'); const inputCount = await inputs.count(); expect(inputCount).toBeGreaterThan(0); }); }); test.describe('Medium Phone (iPhone 12 - 390x844)', () => { test.beforeEach(async ({ page }) => { await page.setViewportSize(MOBILE_VIEWPORTS['iPhone 12']); }); test('dashboard should render correctly on medium phone', async ({ page }) => { await page.goto(`${TEST_CONFIG.FRONTEND_URL}/dashboard`); await page.waitForLoadState('networkidle'); const main = page.locator('main, [role="main"]').first(); await expect(main).toBeVisible(); // Take screenshot for visual verification await expect(page).toHaveScreenshot('dashboard-iphone12.png', { fullPage: true, maxDiffPixels: 200, }); }); test('navigation should work on medium phone', async ({ page }) => { await page.goto(`${TEST_CONFIG.FRONTEND_URL}/dashboard`); await page.waitForLoadState('networkidle'); // Try to navigate to profile const profileLink = page.locator('a[href="/profile"], a[href*="profile"]').first(); if (await profileLink.count() > 0) { await profileLink.click({ timeout: 5000 }); await page.waitForURL('**/profile', { timeout: 5000 }); expect(page.url()).toContain('/profile'); } }); test('tracks page should be usable on medium phone', async ({ page }) => { await page.goto(`${TEST_CONFIG.FRONTEND_URL}/tracks`); await page.waitForLoadState('networkidle'); const main = page.locator('main, [role="main"]').first(); await expect(main).toBeVisible(); // Check that content is scrollable if needed const isScrollable = await page.evaluate(() => { return document.documentElement.scrollHeight > window.innerHeight; }); // Should be able to scroll if content is long expect(typeof isScrollable).toBe('boolean'); }); }); test.describe('Large Phone (iPhone 14 Pro Max - 430x932)', () => { test.beforeEach(async ({ page }) => { await page.setViewportSize(MOBILE_VIEWPORTS['iPhone 14 Pro Max']); }); test('dashboard should utilize larger screen space', async ({ page }) => { await page.goto(`${TEST_CONFIG.FRONTEND_URL}/dashboard`); await page.waitForLoadState('networkidle'); const main = page.locator('main, [role="main"]').first(); await expect(main).toBeVisible(); // On larger phones, sidebar might be visible const sidebar = page.locator('aside').first(); const sidebarVisible = await sidebar.isVisible().catch(() => false); // Either sidebar is visible or hamburger menu is available if (!sidebarVisible) { const menuButton = page.locator('button[aria-label*="menu"], [data-testid*="menu"]').first(); const menuExists = await menuButton.count() > 0; expect(menuExists || sidebarVisible).toBe(true); } }); test('forms should be properly sized on large phone', async ({ page }) => { await page.goto(`${TEST_CONFIG.FRONTEND_URL}/profile`); await page.waitForLoadState('networkidle'); const inputs = page.locator('input, textarea'); const inputCount = await inputs.count(); if (inputCount > 0) { const firstInput = inputs.first(); const box = await firstInput.boundingBox(); if (box) { // Inputs should be wide enough but not too wide expect(box.width).toBeGreaterThan(200); expect(box.width).toBeLessThan(430); // Should not exceed viewport } } }); }); test.describe('Android Devices', () => { test('Samsung Galaxy S21 should render correctly', async ({ page }) => { await page.setViewportSize(MOBILE_VIEWPORTS['Samsung Galaxy S21']); await page.goto(`${TEST_CONFIG.FRONTEND_URL}/dashboard`); await page.waitForLoadState('networkidle'); const main = page.locator('main, [role="main"]').first(); await expect(main).toBeVisible(); }); test('Pixel 5 should render correctly', async ({ page }) => { await page.setViewportSize(MOBILE_VIEWPORTS['Pixel 5']); await page.goto(`${TEST_CONFIG.FRONTEND_URL}/dashboard`); await page.waitForLoadState('networkidle'); const main = page.locator('main, [role="main"]').first(); await expect(main).toBeVisible(); }); }); test.describe('Small Tablet (iPad Mini - 768x1024)', () => { test.beforeEach(async ({ page }) => { await page.setViewportSize(MOBILE_VIEWPORTS['iPad Mini']); }); test('dashboard should use tablet layout', async ({ page }) => { await page.goto(`${TEST_CONFIG.FRONTEND_URL}/dashboard`); await page.waitForLoadState('networkidle'); const main = page.locator('main, [role="main"]').first(); await expect(main).toBeVisible(); // On tablets, sidebar might be visible const sidebar = page.locator('aside').first(); const sidebarVisible = await sidebar.isVisible().catch(() => false); // Tablet should show more content expect(sidebarVisible || true).toBe(true); // Sidebar or main content should be visible }); test('forms should be properly sized on tablet', async ({ page }) => { await page.goto(`${TEST_CONFIG.FRONTEND_URL}/profile`); await page.waitForLoadState('networkidle'); const form = page.locator('form').first(); if (await form.count() > 0) { await expect(form).toBeVisible(); // Forms on tablet should be wider const formBox = await form.boundingBox(); if (formBox) { expect(formBox.width).toBeGreaterThan(400); } } }); }); test.describe('Touch Interactions', () => { test.beforeEach(async ({ page }) => { await page.setViewportSize(MOBILE_VIEWPORTS['iPhone 12']); }); test('buttons should be tappable', async ({ page }) => { await page.goto(`${TEST_CONFIG.FRONTEND_URL}/dashboard`); await page.waitForLoadState('networkidle'); const buttons = page.locator('button').first(); if (await buttons.count() > 0) { const buttonBox = await buttons.boundingBox(); if (buttonBox) { // Buttons should be at least 44x44px for easy tapping expect(buttonBox.width).toBeGreaterThanOrEqual(40); expect(buttonBox.height).toBeGreaterThanOrEqual(40); } } }); test('links should be tappable', async ({ page }) => { await page.goto(`${TEST_CONFIG.FRONTEND_URL}/dashboard`); await page.waitForLoadState('networkidle'); const links = page.locator('a').first(); if (await links.count() > 0) { const linkBox = await links.boundingBox(); if (linkBox) { // Links should have adequate touch target size expect(linkBox.height).toBeGreaterThanOrEqual(30); } } }); }); test.describe('Orientation Changes', () => { test('should handle portrait orientation', async ({ page }) => { await page.setViewportSize({ width: 375, height: 667 }); // Portrait await page.goto(`${TEST_CONFIG.FRONTEND_URL}/dashboard`); await page.waitForLoadState('networkidle'); const main = page.locator('main, [role="main"]').first(); await expect(main).toBeVisible(); }); test('should handle landscape orientation', async ({ page }) => { await page.setViewportSize({ width: 667, height: 375 }); // Landscape await page.goto(`${TEST_CONFIG.FRONTEND_URL}/dashboard`); await page.waitForLoadState('networkidle'); const main = page.locator('main, [role="main"]').first(); await expect(main).toBeVisible(); // In landscape, sidebar might be visible const sidebar = page.locator('aside').first(); const sidebarVisible = await sidebar.isVisible().catch(() => false); // Should work in both cases expect(sidebarVisible || true).toBe(true); }); }); test.describe('Responsive Breakpoints', () => { test('should adapt to different breakpoints', async ({ page }) => { const breakpoints = [ { width: 320, height: 568, name: 'Very Small' }, { width: 375, height: 667, name: 'Small' }, { width: 414, height: 896, name: 'Medium' }, { width: 768, height: 1024, name: 'Tablet' }, ]; for (const breakpoint of breakpoints) { await page.setViewportSize({ width: breakpoint.width, height: breakpoint.height }); await page.goto(`${TEST_CONFIG.FRONTEND_URL}/dashboard`); await page.waitForLoadState('networkidle'); const main = page.locator('main, [role="main"]').first(); await expect(main).toBeVisible(); // Verify no horizontal overflow const bodyWidth = await page.evaluate(() => document.body.scrollWidth); expect(bodyWidth).toBeLessThanOrEqual(breakpoint.width + 20); // Allow small margin console.log(`✅ ${breakpoint.name} (${breakpoint.width}x${breakpoint.height}) - OK`); } }); }); test.describe('Mobile-Specific Features', () => { test.beforeEach(async ({ page }) => { await page.setViewportSize(MOBILE_VIEWPORTS['iPhone 12']); }); test('should handle mobile viewport meta tag', async ({ page }) => { await page.goto(`${TEST_CONFIG.FRONTEND_URL}/dashboard`); const viewport = await page.locator('meta[name="viewport"]').getAttribute('content'); // Should have viewport meta tag for mobile expect(viewport).toBeTruthy(); }); test('should prevent zoom on input focus', async ({ page }) => { await page.goto(`${TEST_CONFIG.FRONTEND_URL}/login`); await page.waitForLoadState('networkidle'); const input = page.locator('input').first(); if (await input.count() > 0) { await input.focus(); // Check that font-size is at least 16px to prevent zoom on iOS const fontSize = await input.evaluate((el) => { return window.getComputedStyle(el).fontSize; }); const fontSizeNum = parseFloat(fontSize); expect(fontSizeNum).toBeGreaterThanOrEqual(14); // At least 14px to prevent zoom } }); }); });