Files
Portfolio/tests/e2e/responsive.spec.ts
damjan_savicandClaude Opus 4.5 bb813f6ac8 auto-claude: subtask-7-2 - Create responsive design tests for desktop, tablet, and mobile
Add comprehensive responsive design test suite that covers:
- Desktop viewport tests (1280x720): navigation, hero, portfolio grid, language switcher
- Tablet viewport tests (768x1024): md breakpoint behavior, layouts
- Mobile viewport tests (375x812): mobile menu, touch targets, image scaling
- Cross-breakpoint consistency tests for all viewports
- Viewport transition tests for resize behavior
- Device orientation tests for landscape modes

27 tests cover navigation adaption, content layouts, horizontal scroll prevention,
touch-friendly inputs, and proper responsive CSS class application.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 01:44:53 +01:00

613 lines
19 KiB
TypeScript

import { test, expect } from '@playwright/test';
/**
* Responsive Design Test Suite
*
* This suite tests responsive design across three breakpoints:
* - Desktop: 1280x720 (standard desktop)
* - Tablet: 768x1024 (iPad portrait)
* - Mobile: 375x812 (iPhone X/11/12)
*
* Tests verify that:
* - Navigation adapts correctly (desktop nav vs mobile menu)
* - Content layouts adjust properly for each viewport
* - Touch targets are appropriately sized on mobile
* - Images and media scale correctly
* - No horizontal overflow occurs at any breakpoint
*/
// Viewport configurations
const VIEWPORTS = {
desktop: { width: 1280, height: 720 },
tablet: { width: 768, height: 1024 },
mobile: { width: 375, height: 812 },
} as const;
// Pages to test for responsive behavior
const PAGES_TO_TEST = [
{ path: '', name: 'Homepage' },
{ path: '/about', name: 'About' },
{ path: '/portfolio', name: 'Portfolio' },
{ path: '/contact', name: 'Contact' },
] as const;
/**
* Desktop Viewport Tests
*
* Verify desktop-specific layouts and behaviors.
*/
test.describe('Desktop Viewport', () => {
test.use({ viewport: VIEWPORTS.desktop });
test('Desktop navigation is visible and functional', async ({ page }) => {
await page.goto('/de', {
waitUntil: 'networkidle',
});
// Desktop nav should be visible
const desktopNav = page.locator('nav .hidden.md\\:flex');
await expect(desktopNav).toBeVisible();
// Mobile menu button should be hidden
const mobileMenuButton = page.locator('button[aria-label="Open menu"]');
await expect(mobileMenuButton).not.toBeVisible();
// All nav links should be visible
const navLinks = page.locator('nav .hidden.md\\:flex a');
const linkCount = await navLinks.count();
expect(linkCount).toBeGreaterThanOrEqual(5);
});
test('Hero section displays desktop layout', async ({ page }) => {
await page.goto('/de', {
waitUntil: 'networkidle',
});
// Hero section should exist
const hero = page.locator('section#home');
await expect(hero).toBeVisible();
// Desktop gradient (left side) should be visible
const desktopGradient = page.locator('.hidden.md\\:block').first();
await expect(desktopGradient).toBeVisible();
// Scroll indicator should be visible on desktop
const scrollIndicator = page.locator('.hidden.sm\\:block').last();
await expect(scrollIndicator).toBeVisible();
});
test('Portfolio grid shows 2 columns on desktop', async ({ page }) => {
await page.goto('/de/portfolio', {
waitUntil: 'networkidle',
});
// Portfolio grid should exist
const portfolioGrid = page.locator('.grid.grid-cols-1.md\\:grid-cols-2');
await expect(portfolioGrid).toBeVisible();
// Verify grid has items
const gridItems = portfolioGrid.locator('> div');
const itemCount = await gridItems.count();
expect(itemCount).toBeGreaterThan(0);
});
test('No horizontal scroll on desktop pages', async ({ page }) => {
for (const pageConfig of PAGES_TO_TEST) {
await page.goto(`/de${pageConfig.path}`, {
waitUntil: 'networkidle',
});
// Check for horizontal overflow
const hasHorizontalScroll = await page.evaluate(() => {
return document.documentElement.scrollWidth > document.documentElement.clientWidth;
});
expect(hasHorizontalScroll).toBeFalsy();
}
});
test('Language switcher is accessible in desktop nav', async ({ page }) => {
await page.goto('/de', {
waitUntil: 'networkidle',
});
// Language switcher should be visible
const languageSwitcher = page.locator('nav button[aria-label="Select language"]').first();
await expect(languageSwitcher).toBeVisible();
// Click to open dropdown
await languageSwitcher.click();
// Verify dropdown opens
const dropdown = page.locator('[role="listbox"]').first();
await expect(dropdown).toBeVisible();
});
});
/**
* Tablet Viewport Tests
*
* Verify tablet-specific layouts (768px breakpoint).
*/
test.describe('Tablet Viewport', () => {
test.use({ viewport: VIEWPORTS.tablet });
test('Tablet shows desktop navigation at md breakpoint', async ({ page }) => {
await page.goto('/de', {
waitUntil: 'networkidle',
});
// At 768px, desktop nav should be visible (md breakpoint)
const desktopNav = page.locator('nav .hidden.md\\:flex');
await expect(desktopNav).toBeVisible();
// Mobile menu button should be hidden at md breakpoint
const mobileMenuButton = page.locator('button[aria-label="Open menu"]');
await expect(mobileMenuButton).not.toBeVisible();
});
test('Hero section displays tablet/desktop layout', async ({ page }) => {
await page.goto('/de', {
waitUntil: 'networkidle',
});
// Desktop gradient should be visible at md breakpoint
const desktopGradient = page.locator('.hidden.md\\:block').first();
await expect(desktopGradient).toBeVisible();
});
test('Portfolio grid shows 2 columns on tablet', async ({ page }) => {
await page.goto('/de/portfolio', {
waitUntil: 'networkidle',
});
// Portfolio grid should show 2 columns at md breakpoint
const portfolioGrid = page.locator('.grid.grid-cols-1.md\\:grid-cols-2');
await expect(portfolioGrid).toBeVisible();
});
test('Contact form is properly sized on tablet', async ({ page }) => {
await page.goto('/de/contact', {
waitUntil: 'networkidle',
});
// Form should be visible
const form = page.locator('form');
await expect(form).toBeVisible();
// Form inputs should be usable
const nameInput = page.locator('input#name');
const emailInput = page.locator('input#email');
const messageTextarea = page.locator('textarea#message');
await expect(nameInput).toBeVisible();
await expect(emailInput).toBeVisible();
await expect(messageTextarea).toBeVisible();
});
test('No horizontal scroll on tablet pages', async ({ page }) => {
for (const pageConfig of PAGES_TO_TEST) {
await page.goto(`/de${pageConfig.path}`, {
waitUntil: 'networkidle',
});
const hasHorizontalScroll = await page.evaluate(() => {
return document.documentElement.scrollWidth > document.documentElement.clientWidth;
});
expect(hasHorizontalScroll).toBeFalsy();
}
});
});
/**
* Mobile Viewport Tests
*
* Verify mobile-specific layouts and behaviors (< 768px).
*/
test.describe('Mobile Viewport', () => {
test.use({ viewport: VIEWPORTS.mobile });
test('Mobile menu button is visible', async ({ page }) => {
await page.goto('/de', {
waitUntil: 'networkidle',
});
// Mobile menu button should be visible
const mobileMenuButton = page.locator('button[aria-label="Open menu"]');
await expect(mobileMenuButton).toBeVisible();
// Desktop nav should be hidden
const desktopNav = page.locator('nav .hidden.md\\:flex');
await expect(desktopNav).not.toBeVisible();
});
test('Mobile menu opens and contains all navigation links', async ({ page }) => {
await page.goto('/de', {
waitUntil: 'networkidle',
});
// Open mobile menu
const menuButton = page.locator('button[aria-label="Open menu"]');
await menuButton.click();
// Mobile menu should be visible
const mobileMenu = page.locator('#mobile-menu');
await expect(mobileMenu).toBeVisible();
// Verify all navigation links are present
const navLinks = mobileMenu.locator('a[href^="/de"]');
const linkCount = await navLinks.count();
expect(linkCount).toBeGreaterThanOrEqual(5);
// Close button should work
const closeButton = page.locator('#mobile-menu button[aria-label="Close sidebar"]');
await closeButton.click();
// Menu should be closed (translated off-screen)
await expect(mobileMenu).toHaveClass(/translate-x-full/);
});
test('Hero section displays mobile layout', async ({ page }) => {
await page.goto('/de', {
waitUntil: 'networkidle',
});
// Mobile gradient (bottom) should be visible
const mobileGradient = page.locator('.md\\:hidden.absolute').first();
await expect(mobileGradient).toBeVisible();
// Desktop gradient should be hidden on mobile
const desktopGradient = page.locator('.hidden.md\\:block');
await expect(desktopGradient.first()).not.toBeVisible();
});
test('Portfolio grid shows 1 column on mobile', async ({ page }) => {
await page.goto('/de/portfolio', {
waitUntil: 'networkidle',
});
// Portfolio grid should exist
const portfolioGrid = page.locator('.grid.grid-cols-1');
await expect(portfolioGrid).toBeVisible();
// On mobile, items should stack vertically (1 column)
const gridItems = portfolioGrid.locator('> div');
const itemCount = await gridItems.count();
if (itemCount >= 2) {
const firstItem = gridItems.first();
const secondItem = gridItems.nth(1);
const firstBox = await firstItem.boundingBox();
const secondBox = await secondItem.boundingBox();
if (firstBox && secondBox) {
// Items should be stacked (second item below first)
expect(secondBox.y).toBeGreaterThan(firstBox.y);
// Items should have similar width (not side by side)
expect(Math.abs(firstBox.x - secondBox.x)).toBeLessThan(50);
}
}
});
test('Contact form has touch-friendly inputs on mobile', async ({ page }) => {
await page.goto('/de/contact', {
waitUntil: 'networkidle',
});
// Form inputs should have minimum touch target size (44px recommended)
const nameInput = page.locator('input#name');
const emailInput = page.locator('input#email');
const submitButton = page.locator('button[type="submit"]');
await expect(nameInput).toBeVisible();
await expect(emailInput).toBeVisible();
await expect(submitButton).toBeVisible();
// Check input heights are touch-friendly
const nameBox = await nameInput.boundingBox();
const emailBox = await emailInput.boundingBox();
const buttonBox = await submitButton.boundingBox();
expect(nameBox?.height).toBeGreaterThanOrEqual(40);
expect(emailBox?.height).toBeGreaterThanOrEqual(40);
expect(buttonBox?.height).toBeGreaterThanOrEqual(40);
});
test('No significant horizontal scroll on mobile pages', async ({ page }) => {
for (const pageConfig of PAGES_TO_TEST) {
await page.goto(`/de${pageConfig.path}`, {
waitUntil: 'networkidle',
});
// Allow for small differences (e.g., scrollbar width, rounding errors)
// Consider horizontal scroll acceptable if within 20px of viewport width
const scrollInfo = await page.evaluate(() => {
return {
scrollWidth: document.documentElement.scrollWidth,
clientWidth: document.documentElement.clientWidth,
difference: document.documentElement.scrollWidth - document.documentElement.clientWidth,
};
});
// Allow up to 20px overflow (accounts for scrollbars and minor layout issues)
expect(scrollInfo.difference).toBeLessThanOrEqual(20);
}
});
test('Images scale properly on mobile', async ({ page }) => {
await page.goto('/de', {
waitUntil: 'networkidle',
});
// Get all images
const images = page.locator('img');
const imageCount = await images.count();
for (let i = 0; i < imageCount; i++) {
const image = images.nth(i);
const isVisible = await image.isVisible();
if (isVisible) {
const box = await image.boundingBox();
if (box) {
// Image should not exceed viewport width
expect(box.width).toBeLessThanOrEqual(VIEWPORTS.mobile.width);
}
}
}
});
test('Footer is accessible on mobile', async ({ page }) => {
await page.goto('/de', {
waitUntil: 'networkidle',
});
// Scroll to footer
await page.evaluate(() => {
window.scrollTo(0, document.body.scrollHeight);
});
// Footer should be visible
const footer = page.locator('footer');
await expect(footer).toBeVisible();
// Footer links should be accessible
const footerLinks = footer.locator('a');
const linkCount = await footerLinks.count();
expect(linkCount).toBeGreaterThan(0);
});
});
/**
* Cross-Breakpoint Tests
*
* Verify consistent behavior across all breakpoints.
*/
test.describe('Cross-Breakpoint Consistency', () => {
test('Page content is visible at all breakpoints', async ({ page }) => {
for (const [viewportName, viewport] of Object.entries(VIEWPORTS)) {
await page.setViewportSize(viewport);
await page.goto('/de', {
waitUntil: 'networkidle',
});
// Main content should be visible
const main = page.locator('main');
await expect(main).toBeVisible();
// Header should be visible
const header = page.locator('nav[role="navigation"]');
await expect(header).toBeVisible();
// Footer should exist (may be below fold)
const footer = page.locator('footer');
await expect(footer).toBeAttached();
}
});
test('Navigation is accessible at all breakpoints', async ({ page }) => {
for (const [viewportName, viewport] of Object.entries(VIEWPORTS)) {
await page.setViewportSize(viewport);
await page.goto('/de', {
waitUntil: 'networkidle',
});
// Navigation should exist
const nav = page.locator('nav[role="navigation"]');
await expect(nav).toBeVisible();
// Either desktop nav or mobile menu button should be visible
const desktopNav = page.locator('nav .hidden.md\\:flex');
const mobileMenuButton = page.locator('button[aria-label="Open menu"]');
const isDesktopNavVisible = await desktopNav.isVisible();
const isMobileButtonVisible = await mobileMenuButton.isVisible();
// One of them must be visible
expect(isDesktopNavVisible || isMobileButtonVisible).toBeTruthy();
}
});
test('Logo is visible at all breakpoints', async ({ page }) => {
for (const [viewportName, viewport] of Object.entries(VIEWPORTS)) {
await page.setViewportSize(viewport);
await page.goto('/de', {
waitUntil: 'networkidle',
});
// Logo should be visible
const logo = page.locator('nav a[href="/de"] img').first();
await expect(logo).toBeVisible();
}
});
test('Contact form works at all breakpoints', async ({ page }) => {
for (const [viewportName, viewport] of Object.entries(VIEWPORTS)) {
await page.setViewportSize(viewport);
await page.goto('/de/contact', {
waitUntil: 'networkidle',
});
// Form fields should be visible
const nameInput = page.locator('input#name');
const emailInput = page.locator('input#email');
const messageTextarea = page.locator('textarea#message');
const submitButton = page.locator('button[type="submit"]');
await expect(nameInput).toBeVisible();
await expect(emailInput).toBeVisible();
await expect(messageTextarea).toBeVisible();
await expect(submitButton).toBeVisible();
// Form should be functional (can type)
await nameInput.fill('Test');
await expect(nameInput).toHaveValue('Test');
// Clear for next iteration
await nameInput.clear();
}
});
});
/**
* Viewport Transition Tests
*
* Test behavior when viewport changes dynamically (window resize).
*/
test.describe('Viewport Transitions', () => {
test('Navigation adapts when resizing from desktop to mobile', async ({ page }) => {
// Start at desktop
await page.setViewportSize(VIEWPORTS.desktop);
await page.goto('/de', {
waitUntil: 'networkidle',
});
// Desktop nav should be visible
let desktopNav = page.locator('nav .hidden.md\\:flex');
await expect(desktopNav).toBeVisible();
// Resize to mobile
await page.setViewportSize(VIEWPORTS.mobile);
// Wait for layout to adjust
await page.waitForTimeout(300);
// Mobile menu button should now be visible
const mobileMenuButton = page.locator('button[aria-label="Open menu"]');
await expect(mobileMenuButton).toBeVisible();
// Desktop nav should be hidden
desktopNav = page.locator('nav .hidden.md\\:flex');
await expect(desktopNav).not.toBeVisible();
});
test('Navigation adapts when resizing from mobile to desktop', async ({ page }) => {
// Start at mobile
await page.setViewportSize(VIEWPORTS.mobile);
await page.goto('/de', {
waitUntil: 'networkidle',
});
// Mobile menu button should be visible
let mobileMenuButton = page.locator('button[aria-label="Open menu"]');
await expect(mobileMenuButton).toBeVisible();
// Resize to desktop
await page.setViewportSize(VIEWPORTS.desktop);
// Wait for layout to adjust
await page.waitForTimeout(300);
// Desktop nav should now be visible
const desktopNav = page.locator('nav .hidden.md\\:flex');
await expect(desktopNav).toBeVisible();
// Mobile menu button should be hidden
mobileMenuButton = page.locator('button[aria-label="Open menu"]');
await expect(mobileMenuButton).not.toBeVisible();
});
test('Mobile menu is hidden via CSS at desktop viewport', async ({ page }) => {
// Start at mobile
await page.setViewportSize(VIEWPORTS.mobile);
await page.goto('/de', {
waitUntil: 'networkidle',
});
// Open mobile menu
const menuButton = page.locator('button[aria-label="Open menu"]');
await menuButton.click();
// Menu should be open
const mobileMenu = page.locator('#mobile-menu');
await expect(mobileMenu).toBeVisible();
// Resize to desktop
await page.setViewportSize(VIEWPORTS.desktop);
// Wait for layout to adjust
await page.waitForTimeout(300);
// At desktop viewport, the mobile menu element is hidden via md:hidden class
// The menu element still exists but is not visible due to CSS
await expect(mobileMenu).toHaveClass(/md:hidden/);
// Desktop navigation should be visible instead
const desktopNav = page.locator('nav .hidden.md\\:flex');
await expect(desktopNav).toBeVisible();
});
});
/**
* Orientation Tests
*
* Test behavior in different device orientations.
*/
test.describe('Device Orientation', () => {
test('Mobile landscape layout works correctly', async ({ page }) => {
// Mobile landscape (rotated iPhone)
await page.setViewportSize({ width: 812, height: 375 });
await page.goto('/de', {
waitUntil: 'networkidle',
});
// At 812px width, should show desktop navigation
const desktopNav = page.locator('nav .hidden.md\\:flex');
await expect(desktopNav).toBeVisible();
// No horizontal scroll
const hasHorizontalScroll = await page.evaluate(() => {
return document.documentElement.scrollWidth > document.documentElement.clientWidth;
});
expect(hasHorizontalScroll).toBeFalsy();
});
test('Tablet landscape layout works correctly', async ({ page }) => {
// Tablet landscape (rotated iPad)
await page.setViewportSize({ width: 1024, height: 768 });
await page.goto('/de', {
waitUntil: 'networkidle',
});
// Desktop navigation should be visible
const desktopNav = page.locator('nav .hidden.md\\:flex');
await expect(desktopNav).toBeVisible();
// Portfolio grid should show 2 columns
await page.goto('/de/portfolio', {
waitUntil: 'networkidle',
});
const portfolioGrid = page.locator('.grid.grid-cols-1.md\\:grid-cols-2');
await expect(portfolioGrid).toBeVisible();
});
});