import { test, expect } from '@playwright/test'; /** * Newsletter Subscription E2E Tests * Tests the newsletter form in the footer across all pages * - Happy path subscription * - Validation (empty, invalid email) * - Loading states * - Error handling (network, server) * - Rate limiting behavior * - Accessibility */ test.describe('Newsletter Subscription - Form Presence', () => { const pagesWithNewsletter = [ { path: '/', name: 'Homepage' }, { path: '/about', name: 'About' }, { path: '/services', name: 'Services' }, { path: '/blog', name: 'Blog' }, { path: '/contact', name: 'Contact' }, ]; for (const pg of pagesWithNewsletter) { test(`newsletter form is present on ${pg.name}`, async ({ page }) => { await page.goto(pg.path); await page.waitForLoadState('domcontentloaded'); const form = page.locator('#newsletter-form'); await expect(form).toBeVisible(); const emailInput = page.locator('#newsletter-email'); await expect(emailInput).toBeVisible(); const submitButton = form.locator('button[type="submit"]'); await expect(submitButton).toBeVisible(); }); } }); test.describe('Newsletter Subscription - Happy Path', () => { test.beforeEach(async ({ page }) => { await page.goto('/'); await page.waitForLoadState('networkidle'); // Scroll to footer where newsletter form is await page.locator('footer').scrollIntoViewIfNeeded(); }); test('valid email subscription shows success message', async ({ page }) => { const emailInput = page.locator('#newsletter-email'); const submitButton = page.locator('#newsletter-form button[type="submit"]'); const messageEl = page.locator('#newsletter-message'); await emailInput.fill('subscriber@example.com'); await submitButton.click(); // Message should appear await expect(messageEl).not.toHaveClass(/hidden/, { timeout: 10000 }); const messageText = await messageEl.textContent(); expect(messageText).toBeTruthy(); expect(messageText!.length).toBeGreaterThan(0); }); test('form resets after successful subscription', async ({ page }) => { const emailInput = page.locator('#newsletter-email'); const submitButton = page.locator('#newsletter-form button[type="submit"]'); await emailInput.fill('reset.test@example.com'); await submitButton.click(); // Wait for response await page.waitForTimeout(3000); // Check if message is shown (success or configured error) const messageEl = page.locator('#newsletter-message'); const isVisible = await messageEl.evaluate((el) => !el.classList.contains('hidden')); // If successful, form should be reset if (isVisible) { const messageText = await messageEl.textContent(); if (messageText?.toLowerCase().includes('subscrib') || messageText?.toLowerCase().includes('thanks')) { await expect(emailInput).toHaveValue(''); } } }); test('submit button shows loading state during submission', async ({ page }) => { const emailInput = page.locator('#newsletter-email'); const submitButton = page.locator('#newsletter-form button[type="submit"]'); await emailInput.fill('loading.test@example.com'); await submitButton.click(); // Button should be disabled during submission await expect(submitButton).toBeDisabled(); }); test('submit button re-enables after submission completes', async ({ page }) => { const emailInput = page.locator('#newsletter-email'); const submitButton = page.locator('#newsletter-form button[type="submit"]'); await emailInput.fill('reenable.test@example.com'); await submitButton.click(); // Wait for the submission to complete await page.waitForTimeout(4000); // Button should be re-enabled await expect(submitButton).toBeEnabled(); }); }); test.describe('Newsletter Subscription - Validation', () => { test.beforeEach(async ({ page }) => { await page.goto('/'); await page.locator('footer').scrollIntoViewIfNeeded(); }); test('email field is required - HTML5 validation', async ({ page }) => { const emailInput = page.locator('#newsletter-email'); const submitButton = page.locator('#newsletter-form button[type="submit"]'); // Submit without email await submitButton.click(); // HTML5 required validation should prevent submission const isInvalid = await emailInput.evaluate((el: HTMLInputElement) => !el.checkValidity()); expect(isInvalid).toBeTruthy(); }); test('email field rejects invalid email format - HTML5', async ({ page }) => { const emailInput = page.locator('#newsletter-email'); const submitButton = page.locator('#newsletter-form button[type="submit"]'); await emailInput.fill('not-an-email'); await submitButton.click(); // HTML5 type="email" should reject non-email const isInvalid = await emailInput.evaluate((el: HTMLInputElement) => !el.checkValidity()); expect(isInvalid).toBeTruthy(); }); test('email field accepts valid email format', async ({ page }) => { const emailInput = page.locator('#newsletter-email'); const validEmails = [ 'user@example.com', 'user.name+tag@example.co.uk', 'user123@subdomain.domain.org', ]; for (const email of validEmails) { await emailInput.fill(email); const isValid = await emailInput.evaluate((el: HTMLInputElement) => el.checkValidity()); expect(isValid).toBeTruthy(); } }); test('email field has correct input type', async ({ page }) => { const emailInput = page.locator('#newsletter-email'); const inputType = await emailInput.getAttribute('type'); expect(inputType).toBe('email'); }); test('email field has autocomplete attribute', async ({ page }) => { const emailInput = page.locator('#newsletter-email'); const autocomplete = await emailInput.getAttribute('autocomplete'); expect(autocomplete).toBe('email'); }); }); test.describe('Newsletter Subscription - API Integration', () => { test('newsletter API endpoint responds to valid subscription', async ({ request }) => { const response = await request.post('/api/newsletter', { headers: { 'Content-Type': 'application/json' }, data: { email: 'api.test@example.com' }, }); // Should be 200 (success) or 429 (rate limited in test env) expect([200, 429]).toContain(response.status()); if (response.status() === 200) { const body = await response.json() as { success: boolean; message?: string }; expect(body.success).toBe(true); expect(body.message).toBeTruthy(); } }); test('newsletter API rejects invalid email', async ({ request }) => { const response = await request.post('/api/newsletter', { headers: { 'Content-Type': 'application/json' }, data: { email: 'invalid-email' }, }); expect(response.status()).toBe(422); const body = await response.json() as { success: boolean; error: string }; expect(body.success).toBe(false); expect(body.error).toBeTruthy(); }); test('newsletter API rejects empty email', async ({ request }) => { const response = await request.post('/api/newsletter', { headers: { 'Content-Type': 'application/json' }, data: { email: '' }, }); expect(response.status()).toBe(422); const body = await response.json() as { success: boolean; error: string }; expect(body.success).toBe(false); }); test('newsletter API returns rate limit headers', async ({ request }) => { const response = await request.post('/api/newsletter', { headers: { 'Content-Type': 'application/json' }, data: { email: 'ratelimit.headers@example.com' }, }); const headers = response.headers(); expect(headers['x-ratelimit-limit']).toBeDefined(); expect(headers['x-ratelimit-remaining']).toBeDefined(); expect(headers['x-ratelimit-reset']).toBeDefined(); }); test('newsletter API accepts form-encoded data', async ({ request }) => { const response = await request.post('/api/newsletter', { headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, form: { email: 'formencoded@example.com' }, }); // Should be 200 or 429 (not 400 bad request) expect([200, 429]).toContain(response.status()); }); }); test.describe('Newsletter Subscription - Network Error Handling', () => { test('newsletter form shows error on network failure', async ({ page }) => { await page.goto('/'); await page.locator('footer').scrollIntoViewIfNeeded(); // Intercept the API call and make it fail await page.route('/api/newsletter', async (route) => { await route.abort('failed'); }); const emailInput = page.locator('#newsletter-email'); const submitButton = page.locator('#newsletter-form button[type="submit"]'); const messageEl = page.locator('#newsletter-message'); await emailInput.fill('network.error@example.com'); await submitButton.click(); // Should show error message await expect(messageEl).not.toHaveClass(/hidden/, { timeout: 5000 }); const messageText = await messageEl.textContent(); expect(messageText?.toLowerCase()).toMatch(/network|error|check/); }); test('newsletter form shows server error response', async ({ page }) => { await page.goto('/'); await page.locator('footer').scrollIntoViewIfNeeded(); // Intercept and return server error await page.route('/api/newsletter', async (route) => { await route.fulfill({ status: 500, contentType: 'application/json', body: JSON.stringify({ success: false, error: 'Server temporarily unavailable.' }), }); }); const emailInput = page.locator('#newsletter-email'); const submitButton = page.locator('#newsletter-form button[type="submit"]'); const messageEl = page.locator('#newsletter-message'); await emailInput.fill('server.error@example.com'); await submitButton.click(); // Should show error await expect(messageEl).not.toHaveClass(/hidden/, { timeout: 5000 }); const messageText = await messageEl.textContent(); expect(messageText).toBeTruthy(); }); test('newsletter button re-enables after network failure', async ({ page }) => { await page.goto('/'); await page.locator('footer').scrollIntoViewIfNeeded(); await page.route('/api/newsletter', async (route) => { await route.abort('failed'); }); const emailInput = page.locator('#newsletter-email'); const submitButton = page.locator('#newsletter-form button[type="submit"]'); await emailInput.fill('recovery@example.com'); await submitButton.click(); // Wait for error handling await page.waitForTimeout(3000); // Button should be re-enabled await expect(submitButton).toBeEnabled(); }); }); test.describe('Newsletter Subscription - Accessibility', () => { test('newsletter email field has accessible label', async ({ page }) => { await page.goto('/'); await page.locator('footer').scrollIntoViewIfNeeded(); const emailInput = page.locator('#newsletter-email'); // Check for label or placeholder const hasLabel = await page.locator('label[for="newsletter-email"]').count() > 0; const placeholder = await emailInput.getAttribute('placeholder'); const ariaLabel = await emailInput.getAttribute('aria-label'); expect(hasLabel || placeholder || ariaLabel).toBeTruthy(); }); test('newsletter submit button has accessible text', async ({ page }) => { await page.goto('/'); await page.locator('footer').scrollIntoViewIfNeeded(); const submitButton = page.locator('#newsletter-form button[type="submit"]'); const text = await submitButton.textContent(); const ariaLabel = await submitButton.getAttribute('aria-label'); expect((text && text.trim().length > 0) || ariaLabel).toBeTruthy(); }); test('newsletter form is keyboard accessible', async ({ page }) => { await page.goto('/'); // Tab to newsletter form const emailInput = page.locator('#newsletter-email'); await emailInput.scrollIntoViewIfNeeded(); await emailInput.focus(); // Should be focusable await expect(emailInput).toBeFocused(); // Tab to submit button await page.keyboard.press('Tab'); const submitButton = page.locator('#newsletter-form button[type="submit"]'); await expect(submitButton).toBeFocused(); }); }); test.describe('Newsletter Subscription - Mobile Experience', () => { test.use({ viewport: { width: 375, height: 667 } }); test('newsletter form is visible and usable on mobile', async ({ page }) => { await page.goto('/'); await page.waitForLoadState('networkidle'); const form = page.locator('#newsletter-form'); await form.scrollIntoViewIfNeeded(); await expect(form).toBeVisible(); const emailInput = page.locator('#newsletter-email'); await emailInput.tap(); await emailInput.fill('mobile.newsletter@example.com'); await expect(emailInput).toHaveValue('mobile.newsletter@example.com'); }); test('newsletter email input triggers email keyboard on mobile', async ({ page }) => { await page.goto('/'); const emailInput = page.locator('#newsletter-email'); const inputType = await emailInput.getAttribute('type'); expect(inputType).toBe('email'); // This triggers email keyboard }); test('newsletter form submit button is tappable on mobile', async ({ page }) => { await page.goto('/'); const submitButton = page.locator('#newsletter-form button[type="submit"]'); await submitButton.scrollIntoViewIfNeeded(); await expect(submitButton).toBeVisible(); // Check the button is large enough to tap (min 44x44 per WCAG) const box = await submitButton.boundingBox(); expect(box).toBeTruthy(); expect(box!.height).toBeGreaterThanOrEqual(40); // Slightly lenient }); });