Deploy to Production / Build & Verify (push) Failing after 5m56s
Ping Search Engines / Notify Search Engines (push) Successful in 2s
Deploy to Production / Pre-Deploy Tests (push) Has been skipped
Deploy to Production / Deploy to Railway (push) Has been skipped
Deploy to Production / Deploy to Render (push) Has been skipped
Deploy to Production / Deploy to VPS (PM2) (push) Has been skipped
Deploy to Production / Deploy to Fly.io (push) Has been skipped
Deploy to Production / Post-Deploy Verification (push) Has been skipped
Deploy to Production / Notify on Failure (push) Successful in 2s
E2E Test Suite / Critical User Journeys (push) Has been skipped
E2E Test Suite / API Integration Tests (push) Has been skipped
E2E Test Suite / Smoke Tests (P0) (push) Failing after 11m26s
E2E Test Suite / Form Interaction Tests (push) Failing after 11m42s
E2E Test Suite / Destructive & Chaos Tests (push) Failing after 12m2s
E2E Test Suite / Cross-Browser Regression (chromium) (push) Failing after 16m14s
E2E Test Suite / Cross-Browser Regression (webkit) (push) Failing after 17m45s
E2E Test Suite / Cross-Browser Regression (firefox) (push) Failing after 25m23s
E2E Test Suite / Security Header Tests (push) Failing after 7m55s
E2E Test Suite / Test Report Summary (push) Failing after 20s
E2E Test Suite / Mobile Device Tests (push) Failing after 2h49m9s
Uptime Monitor / Health & Response Time (push) Failing after 2s
Uptime Monitor / SSL Certificate (push) Successful in 2s
Uptime Monitor / Send Alerts (push) Failing after 3s
Uptime Monitor / Record Uptime Success (push) Has been skipped
387 lines
14 KiB
TypeScript
387 lines
14 KiB
TypeScript
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
|
|
});
|
|
});
|