veza/apps/web/e2e/tests/mobile.spec.ts

376 lines
14 KiB
TypeScript

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