376 lines
14 KiB
TypeScript
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
|
|
}
|
|
});
|
|
});
|
|
});
|
|
|