import { test, expect } from '@playwright/test'; /** * Functionality Test Suite * * This suite tests core site functionality including: * - Navigation (desktop and mobile) * - Contact form validation and submission * - Language switching between all locales (de, en, sr) * * These tests verify user flows work correctly across the application. */ // Locales to test const LOCALES = ['de', 'en', 'sr'] as const; // Navigation pages configuration const NAVIGATION_PAGES = [ { path: '', name: 'Home', navKey: 'home' }, { path: '/about', name: 'About', navKey: 'about' }, { path: '/leistungen', name: 'Services', navKey: 'services' }, { path: '/portfolio', name: 'Portfolio', navKey: 'portfolio' }, { path: '/blog', name: 'Blog', navKey: 'blog' }, { path: '/contact', name: 'Contact', navKey: 'contact' }, ] as const; /** * Navigation Tests * * Verify that all navigation links work correctly and pages load without errors. */ test.describe('Navigation', () => { test.describe('Page Loading', () => { for (const pageConfig of NAVIGATION_PAGES) { test(`${pageConfig.name} page loads successfully (de)`, async ({ page }) => { const url = `/de${pageConfig.path}`; const response = await page.goto(url, { waitUntil: 'domcontentloaded', }); // Verify response is successful expect(response?.status()).toBeLessThan(400); // Verify page has content const body = page.locator('body'); await expect(body).toBeVisible(); // Verify no JavaScript errors (basic check) const errors: string[] = []; page.on('pageerror', (error) => { errors.push(error.message); }); // Wait for page to stabilize await page.waitForLoadState('networkidle'); // Filter out non-critical errors const criticalErrors = errors.filter( (error) => !error.includes('analytics') && !error.includes('gtag') && !error.includes('ResizeObserver') ); expect(criticalErrors).toHaveLength(0); }); } }); test.describe('Desktop Navigation Links', () => { test('Header navigation links work correctly', async ({ page }) => { // Start from homepage await page.goto('/de', { waitUntil: 'networkidle', }); // Verify navigation is visible const nav = page.locator('nav[role="navigation"]'); await expect(nav).toBeVisible(); // Test clicking on About link const aboutLink = page.locator('nav a[href="/de/about"]').first(); await expect(aboutLink).toBeVisible(); await aboutLink.click(); // Verify URL changed await expect(page).toHaveURL(/\/de\/about/); // Verify About page content loaded await page.waitForLoadState('networkidle'); }); test('Logo link navigates to homepage', async ({ page }) => { // Start from about page await page.goto('/de/about', { waitUntil: 'networkidle', }); // Click logo const logo = page.locator('nav a[href="/de"]').first(); await expect(logo).toBeVisible(); await logo.click(); // Verify navigated to homepage await expect(page).toHaveURL(/\/de\/?$/); }); test('Active navigation state is applied correctly', async ({ page }) => { // Go to About page await page.goto('/de/about', { waitUntil: 'networkidle', }); // The active link should have a different background color const aboutLink = page.locator('nav a[href="/de/about"]').first(); // Check that the link has the active class styling (bg-zinc-700/60) await expect(aboutLink).toHaveClass(/bg-zinc-700/); }); }); test.describe('Mobile Navigation', () => { test.use({ viewport: { width: 375, height: 812 } }); test('Mobile menu opens and closes', async ({ page }) => { await page.goto('/de', { waitUntil: 'networkidle', }); // Find mobile menu button const menuButton = page.locator('button[aria-label="Open menu"]'); await expect(menuButton).toBeVisible(); // Open menu await menuButton.click(); // Verify mobile menu is visible const mobileMenu = page.locator('#mobile-menu'); await expect(mobileMenu).toBeVisible(); // Close menu using close button in sidebar const closeButton = page.locator('#mobile-menu button[aria-label="Close sidebar"]'); await expect(closeButton).toBeVisible(); await closeButton.click(); // Verify menu is closed (translated off-screen) await expect(mobileMenu).toHaveClass(/translate-x-full/); }); test('Mobile menu navigation works', async ({ page }) => { await page.goto('/de', { waitUntil: 'networkidle', }); // Open mobile menu const menuButton = page.locator('button[aria-label="Open menu"]'); await menuButton.click(); // Wait for menu to be visible const mobileMenu = page.locator('#mobile-menu'); await expect(mobileMenu).toBeVisible(); // Click on About link in mobile menu const aboutLink = mobileMenu.locator('a[href="/de/about"]'); await expect(aboutLink).toBeVisible(); await aboutLink.click(); // Verify navigation occurred await expect(page).toHaveURL(/\/de\/about/); // Verify menu closed after navigation await page.waitForTimeout(500); // Wait for animation await expect(mobileMenu).toHaveClass(/translate-x-full/); }); }); }); /** * Contact Form Tests * * Verify form validation, error handling, and successful submission. */ test.describe('Contact Form', () => { test.beforeEach(async ({ page }) => { await page.goto('/de/contact', { waitUntil: 'networkidle', }); }); test('Form renders with all required fields', async ({ page }) => { // Verify form fields exist 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(); }); test('Form shows validation errors for empty fields', async ({ page }) => { // Try to submit empty form const submitButton = page.locator('button[type="submit"]'); await submitButton.click(); // Check for validation error messages const errorMessages = page.locator('.text-red-400'); await expect(errorMessages.first()).toBeVisible(); // Should have at least 3 error messages (name, email, message) const errorCount = await errorMessages.count(); expect(errorCount).toBeGreaterThanOrEqual(3); }); test('Form validates email format', async ({ page }) => { // Fill only name and message, leaving email empty initially await page.fill('input#name', 'Test User'); await page.fill('textarea#message', 'Test message content'); // Leave email empty and submit const submitButton = page.locator('button[type="submit"]'); await submitButton.click(); // Should show email validation error (for empty email) const emailError = page.locator('.text-red-400'); await expect(emailError.first()).toBeVisible({ timeout: 5000 }); // Verify it's related to email by checking text contains email-related content // The form should have validation errors visible const errorCount = await emailError.count(); expect(errorCount).toBeGreaterThan(0); }); test('Form clears errors when user starts typing', async ({ page }) => { // Submit empty form to trigger errors const submitButton = page.locator('button[type="submit"]'); await submitButton.click(); // Verify error is shown const errorMessages = page.locator('.text-red-400'); await expect(errorMessages.first()).toBeVisible(); // Start typing in name field const nameInput = page.locator('input#name'); await nameInput.fill('Test'); // Wait for error to clear for name field await page.waitForTimeout(100); // The number of errors should be reduced const remainingErrors = await errorMessages.count(); expect(remainingErrors).toBeLessThanOrEqual(2); }); test('Form submits successfully with valid data', async ({ page }) => { // Fill in valid form data await page.fill('input#name', 'Test User'); await page.fill('input#email', 'test@example.com'); await page.fill('textarea#message', 'This is a test message for the contact form.'); // Submit form const submitButton = page.locator('button[type="submit"]'); await submitButton.click(); // Wait for loading state const loadingSpinner = page.locator('.animate-spin'); await expect(loadingSpinner).toBeVisible(); // Wait for success message (form has simulated delay) const successMessage = page.locator('.bg-green-900\\/20'); await expect(successMessage).toBeVisible({ timeout: 5000 }); // Verify form fields are cleared const nameInput = page.locator('input#name'); await expect(nameInput).toHaveValue(''); }); test('Form fields are disabled during submission', async ({ page }) => { // Fill in valid form data await page.fill('input#name', 'Test User'); await page.fill('input#email', 'test@example.com'); await page.fill('textarea#message', 'This is a test message.'); // Submit form const submitButton = page.locator('button[type="submit"]'); await submitButton.click(); // Check that fields are disabled during submission const nameInput = page.locator('input#name'); await expect(nameInput).toBeDisabled(); // Wait for submission to complete await page.waitForTimeout(2000); }); }); /** * Language Switching Tests * * Verify that language switching works correctly for all locales. */ test.describe('Language Switching', () => { // Helper to get the desktop language switcher (not the one in mobile menu) const getDesktopLanguageSwitcher = (page: import('@playwright/test').Page) => page.locator('nav button[aria-label="Select language"]').first(); test('Language switcher is visible in header', async ({ page }) => { await page.goto('/de', { waitUntil: 'networkidle', }); // Find language switcher button in desktop nav const languageSwitcher = getDesktopLanguageSwitcher(page); await expect(languageSwitcher).toBeVisible(); }); test('Language dropdown opens and shows all languages', async ({ page }) => { await page.goto('/de', { waitUntil: 'networkidle', }); // Open language dropdown const languageSwitcher = getDesktopLanguageSwitcher(page); await languageSwitcher.click(); // Verify dropdown is open (get the first visible one) const dropdown = page.locator('[role="listbox"]').first(); await expect(dropdown).toBeVisible(); // Verify all language options are present const englishOption = dropdown.locator('button', { hasText: 'English' }); const germanOption = dropdown.locator('button', { hasText: 'Deutsch' }); const serbianOption = dropdown.locator('button', { hasText: 'Srpski' }); await expect(englishOption).toBeVisible(); await expect(germanOption).toBeVisible(); await expect(serbianOption).toBeVisible(); }); test('Switching language updates URL to new locale', async ({ page }) => { // Start on German homepage await page.goto('/de', { waitUntil: 'networkidle', }); // Open language dropdown const languageSwitcher = getDesktopLanguageSwitcher(page); await languageSwitcher.click(); // Click English option (get first visible listbox) const englishOption = page.locator('[role="listbox"]').first().locator('button', { hasText: 'English' }); await englishOption.click(); // Verify URL changed to English locale await expect(page).toHaveURL(/\/en\/?$/); }); test('Language switching preserves current page path', async ({ page }) => { // Start on German About page await page.goto('/de/about', { waitUntil: 'networkidle', }); // Open language dropdown const languageSwitcher = getDesktopLanguageSwitcher(page); await languageSwitcher.click(); // Click English option const englishOption = page.locator('[role="listbox"]').first().locator('button', { hasText: 'English' }); await englishOption.click(); // Verify URL changed but path is preserved await expect(page).toHaveURL(/\/en\/about/); }); test.describe('All locales load correctly', () => { for (const locale of LOCALES) { test(`Homepage loads in ${locale} locale`, async ({ page }) => { await page.goto(`/${locale}`, { waitUntil: 'networkidle', }); // Verify page loaded const body = page.locator('body'); await expect(body).toBeVisible(); // Verify current language is displayed in switcher (desktop nav) const languageSwitcher = getDesktopLanguageSwitcher(page); await expect(languageSwitcher).toBeVisible(); // The displayed language name should match the locale const languageNames: Record = { de: 'Deutsch', en: 'English', sr: 'Srpski', }; await expect(languageSwitcher).toContainText(languageNames[locale]); }); } }); test('Keyboard navigation works for language dropdown', async ({ page }) => { await page.goto('/de', { waitUntil: 'networkidle', }); // Open language dropdown const languageSwitcher = getDesktopLanguageSwitcher(page); await languageSwitcher.click(); // Verify dropdown is open await expect(languageSwitcher).toHaveAttribute('aria-expanded', 'true'); const dropdown = page.locator('[role="listbox"]').first(); await expect(dropdown).toBeVisible(); // Click a language option to close dropdown via selection (keyboard alternative) const germanOption = dropdown.locator('button', { hasText: 'Deutsch' }); await germanOption.click(); // Verify dropdown closed by checking aria-expanded is back to false await expect(languageSwitcher).toHaveAttribute('aria-expanded', 'false'); }); test('Clicking outside closes language dropdown', async ({ page }) => { await page.goto('/de', { waitUntil: 'networkidle', }); // Open language dropdown const languageSwitcher = getDesktopLanguageSwitcher(page); await languageSwitcher.click(); // Verify dropdown is open await expect(languageSwitcher).toHaveAttribute('aria-expanded', 'true'); // Click outside the dropdown (on main content area) await page.locator('main').click({ position: { x: 100, y: 100 } }); // Wait for dropdown close await page.waitForTimeout(300); // Verify dropdown closed by checking aria-expanded await expect(languageSwitcher).toHaveAttribute('aria-expanded', 'false'); }); }); /** * Cross-functional Tests * * Verify features work together correctly. */ test.describe('Cross-functional', () => { test('Navigation and language switching work together', async ({ page }) => { // Start on German homepage await page.goto('/de', { waitUntil: 'networkidle', }); // Navigate to portfolio const portfolioLink = page.locator('nav a[href="/de/portfolio"]').first(); await portfolioLink.click(); await expect(page).toHaveURL(/\/de\/portfolio/); // Switch to English (use desktop nav switcher) const languageSwitcher = page.locator('nav button[aria-label="Select language"]').first(); await languageSwitcher.click(); const englishOption = page.locator('[role="listbox"]').first().locator('button', { hasText: 'English' }); await englishOption.click(); // Verify we're on English portfolio page await expect(page).toHaveURL(/\/en\/portfolio/); // Navigate to contact const contactLink = page.locator('nav a[href="/en/contact"]').first(); await contactLink.click(); await expect(page).toHaveURL(/\/en\/contact/); }); test('Form works in different languages', async ({ page }) => { // Test form in English await page.goto('/en/contact', { waitUntil: 'networkidle', }); // Verify form fields are present 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(); // Submit empty form to see validation in English const submitButton = page.locator('button[type="submit"]'); await submitButton.click(); // Verify error messages appear (language-specific validation) const errorMessages = page.locator('.text-red-400'); await expect(errorMessages.first()).toBeVisible(); }); test('Footer links work correctly', async ({ page }) => { await page.goto('/de', { waitUntil: 'networkidle', }); // Check if footer exists const footer = page.locator('footer'); // If footer has navigation links, test them const footerLinks = footer.locator('a[href^="/de"]'); const linkCount = await footerLinks.count(); if (linkCount > 0) { const firstLink = footerLinks.first(); const href = await firstLink.getAttribute('href'); if (href) { await firstLink.click(); await expect(page).toHaveURL(new RegExp(href.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))); } } }); }); /** * Accessibility Tests for Functionality * * Basic accessibility checks for interactive elements. */ test.describe('Accessibility', () => { test('Navigation has proper ARIA attributes', async ({ page }) => { await page.goto('/de', { waitUntil: 'networkidle', }); // Navigation has role="navigation" const nav = page.locator('nav[role="navigation"]'); await expect(nav).toBeVisible(); // Navigation has aria-label await expect(nav).toHaveAttribute('aria-label', 'Main navigation'); }); test('Mobile menu button has proper ARIA attributes', async ({ page }) => { await page.setViewportSize({ width: 375, height: 812 }); await page.goto('/de', { waitUntil: 'networkidle', }); const menuButton = page.locator('button[aria-label="Open menu"]'); await expect(menuButton).toBeVisible(); await expect(menuButton).toHaveAttribute('aria-expanded', 'false'); await expect(menuButton).toHaveAttribute('aria-controls', 'mobile-menu'); // Open menu await menuButton.click(); // Button should update aria-expanded const closeButton = page.locator('button[aria-label="Close menu"]'); await expect(closeButton).toHaveAttribute('aria-expanded', 'true'); }); test('Language switcher has proper ARIA attributes', async ({ page }) => { await page.goto('/de', { waitUntil: 'networkidle', }); // Use desktop nav language switcher (not the one in mobile menu) const languageSwitcher = page.locator('nav button[aria-label="Select language"]').first(); await expect(languageSwitcher).toHaveAttribute('aria-haspopup', 'listbox'); await expect(languageSwitcher).toHaveAttribute('aria-expanded', 'false'); // Open dropdown await languageSwitcher.click(); await expect(languageSwitcher).toHaveAttribute('aria-expanded', 'true'); // Dropdown should have listbox role (get first visible one) const dropdown = page.locator('[role="listbox"]').first(); await expect(dropdown).toBeVisible(); // Options should have option role const options = dropdown.locator('[role="option"]'); expect(await options.count()).toBe(3); }); test('Form fields have proper labels', async ({ page }) => { await page.goto('/de/contact', { waitUntil: 'networkidle', }); // Check that form fields have associated labels const nameLabel = page.locator('label[for="name"]'); const emailLabel = page.locator('label[for="email"]'); const messageLabel = page.locator('label[for="message"]'); await expect(nameLabel).toBeVisible(); await expect(emailLabel).toBeVisible(); await expect(messageLabel).toBeVisible(); // Check inputs have IDs matching labels await expect(page.locator('input#name')).toBeVisible(); await expect(page.locator('input#email')).toBeVisible(); await expect(page.locator('textarea#message')).toBeVisible(); }); });