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