import { test, expect } from '@playwright/test';
/**
* Destructive / Chaos Tests
* QA's job is to break things before production does.
*
* Tests cover:
* - Boundary value inputs (XSS, SQL injection, max-length)
* - Race conditions (rapid submission)
* - Network failure scenarios (offline, slow, timeout)
* - Malformed data injection
* - Session state after errors
* - Concurrent tab behavior
*/
// ============================================================
// Boundary Value Attacks
// ============================================================
test.describe('Chaos: Input Boundary Attacks', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/contact');
await page.waitForLoadState('domcontentloaded');
});
test('XSS attempt in name field is sanitized', async ({ page }) => {
const xssPayload = '';
await page.locator('input[name="name"]').fill(xssPayload);
await page.locator('input[name="email"]').fill('xss@test.com');
await page.locator('select[name="subject"]').selectOption('web-development');
await page.locator('textarea[name="message"]').fill('XSS test message content here');
await page.locator('button[type="submit"]').click();
// Alert dialog should NOT appear (XSS blocked)
let alertFired = false;
page.on('dialog', async (dialog) => {
alertFired = true;
await dialog.dismiss();
});
await page.waitForTimeout(1000);
expect(alertFired).toBe(false);
});
test('XSS attempt in message field is sanitized', async ({ page }) => {
const xssPayload = '">
';
await page.locator('input[name="name"]').fill('Normal Name');
await page.locator('input[name="email"]').fill('xss2@test.com');
await page.locator('select[name="subject"]').selectOption('consulting');
await page.locator('textarea[name="message"]').fill(xssPayload);
await page.locator('button[type="submit"]').click();
let alertFired = false;
page.on('dialog', async (dialog) => {
alertFired = true;
await dialog.dismiss();
});
await page.waitForTimeout(1000);
expect(alertFired).toBe(false);
});
test('SQL injection attempt in message field is handled safely', async ({ page }) => {
const sqlPayload = "'; DROP TABLE users; --";
await page.locator('input[name="name"]').fill('SQL Injector');
await page.locator('input[name="email"]').fill('sql@test.com');
await page.locator('select[name="subject"]').selectOption('support');
await page.locator('textarea[name="message"]').fill(sqlPayload);
await page.locator('button[type="submit"]').click();
// Page should not crash or show server errors
await page.waitForTimeout(3000);
await expect(page.locator('body')).not.toContainText(/syntax error|sql|database error/i);
});
test('maximum length name (100 chars) is accepted', async ({ page }) => {
const maxName = 'A'.repeat(100);
await page.locator('input[name="name"]').fill(maxName);
await expect(page.locator('input[name="name"]')).toHaveValue(maxName);
});
test('maximum length message (5000 chars) is accepted at API level', async ({ request }) => {
const maxMessage = 'A'.repeat(5000);
const response = await request.post('/api/contact', {
headers: { 'Content-Type': 'application/json' },
data: {
name: 'Max Length Tester',
email: 'max@test.com',
subject: 'web-development',
message: maxMessage,
website: '',
},
});
// Should not fail with 422 (validation error) for boundary-valid message
expect([200, 429]).toContain(response.status());
});
test('message exceeding 5001 chars is rejected by API', async ({ request }) => {
const overMaxMessage = 'B'.repeat(5001);
const response = await request.post('/api/contact', {
headers: { 'Content-Type': 'application/json' },
data: {
name: 'Over Max Tester',
email: 'overmax@test.com',
subject: 'web-development',
message: overMaxMessage,
website: '',
},
});
// Should reject overly long message
expect(response.status()).toBe(422);
});
test('unicode characters in name field handled correctly', async ({ page }) => {
const unicodeName = '日本語名前 Ñoño García';
await page.locator('input[name="name"]').fill(unicodeName);
await expect(page.locator('input[name="name"]')).toHaveValue(unicodeName);
});
test('emoji in message field handled correctly', async ({ page }) => {
const emojiMessage = '🚀 Hello! I need help with my website 💻✨ Please contact me ASAP! 🙏';
await page.locator('textarea[name="message"]').fill(emojiMessage);
await expect(page.locator('textarea[name="message"]')).toHaveValue(emojiMessage);
});
test('null bytes and control characters rejected gracefully', async ({ request }) => {
const response = await request.post('/api/contact', {
headers: { 'Content-Type': 'application/json' },
data: {
name: 'Normal Name',
email: 'test@test.com',
subject: 'web-development',
message: 'Normal message\x00\x01\x02 with control chars',
website: '',
},
});
// Should handle without crashing (200 or validation error, not 500)
expect([200, 422, 429]).toContain(response.status());
});
});
// ============================================================
// Race Condition Tests
// ============================================================
test.describe('Chaos: Race Conditions', () => {
test('rapid double-click on submit is prevented', async ({ page }) => {
await page.goto('/contact');
await page.locator('input[name="name"]').fill('Race Condition Test');
await page.locator('input[name="email"]').fill('race@test.com');
await page.locator('select[name="subject"]').selectOption('web-development');
await page.locator('textarea[name="message"]').fill('Testing rapid submission prevention.');
const submitButton = page.locator('button[type="submit"]');
// Track API calls
let apiCallCount = 0;
page.on('request', (req) => {
if (req.url().includes('/api/contact') && req.method() === 'POST') {
apiCallCount++;
}
});
// Double click rapidly
await submitButton.click();
await submitButton.click({ force: true }); // Force to bypass disabled state check
await page.waitForTimeout(3000);
// Only one API call should be made (button disabled after first click)
expect(apiCallCount).toBeLessThanOrEqual(2); // At most 2 (one valid)
});
test('concurrent newsletter subscription submissions', async ({ page }) => {
await page.goto('/');
await page.locator('footer').scrollIntoViewIfNeeded();
const emailInput = page.locator('#newsletter-email');
const submitButton = page.locator('#newsletter-form button[type="submit"]');
await emailInput.fill('concurrent@test.com');
// Click submit, then immediately try again
await submitButton.click();
// Button should be immediately disabled
await expect(submitButton).toBeDisabled();
});
test('form state is consistent during API call', async ({ page }) => {
await page.goto('/contact');
await page.locator('input[name="name"]').fill('State Test User');
await page.locator('input[name="email"]').fill('state@test.com');
await page.locator('select[name="subject"]').selectOption('consulting');
await page.locator('textarea[name="message"]').fill('Testing form state consistency.');
const submitButton = page.locator('button[type="submit"]');
await submitButton.click();
// Verify UI shows loading state immediately
// Button should be disabled while in-flight
await expect(submitButton).toBeDisabled();
// Name field should still have its value (not cleared prematurely)
await expect(page.locator('input[name="name"]')).toHaveValue('State Test User');
});
});
// ============================================================
// Network Failure Scenarios
// ============================================================
test.describe('Chaos: Network Failures', () => {
test('contact form handles connection abort gracefully', async ({ page }) => {
await page.goto('/contact');
await page.route('/api/contact', async (route) => {
await route.abort('connectionaborted');
});
await page.locator('input[name="name"]').fill('Abort Test');
await page.locator('input[name="email"]').fill('abort@test.com');
await page.locator('select[name="subject"]').selectOption('web-development');
await page.locator('textarea[name="message"]').fill('Testing connection abort handling.');
await page.locator('button[type="submit"]').click();
// Should show error toast
await expect(page.locator('#toast-container')).toContainText(/network|error|check/i, { timeout: 5000 });
});
test('contact form handles server timeout gracefully', async ({ page }) => {
await page.goto('/contact');
// Simulate a very slow response
await page.route('/api/contact', async (route) => {
// Delay 30 seconds (longer than the test timeout for this scenario)
await new Promise((resolve) => setTimeout(resolve, 30000));
await route.continue();
});
await page.locator('input[name="name"]').fill('Timeout Test');
await page.locator('input[name="email"]').fill('timeout@test.com');
await page.locator('select[name="subject"]').selectOption('support');
await page.locator('textarea[name="message"]').fill('Testing timeout scenario handling.');
await page.locator('button[type="submit"]').click();
// Button should be disabled during the long wait
await expect(page.locator('button[type="submit"]')).toBeDisabled();
});
test('contact form recovers after network failure', async ({ page }) => {
await page.goto('/contact');
// First request fails
let requestCount = 0;
await page.route('/api/contact', async (route) => {
requestCount++;
if (requestCount === 1) {
await route.abort('failed');
} else {
await route.continue();
}
});
await page.locator('input[name="name"]').fill('Recovery Test');
await page.locator('input[name="email"]').fill('recovery@test.com');
await page.locator('select[name="subject"]').selectOption('web-development');
await page.locator('textarea[name="message"]').fill('Testing form recovery after failure.');
await page.locator('button[type="submit"]').click();
// Wait for error
await page.waitForTimeout(2000);
// Button should re-enable
await expect(page.locator('button[type="submit"]')).toBeEnabled();
// Try again
await page.locator('button[type="submit"]').click();
await page.waitForTimeout(3000);
// Second attempt should work
const toastText = await page.locator('#toast-container').textContent();
expect(toastText).toBeTruthy();
});
test('page navigation works even when API is down', async ({ page }) => {
// Block all API calls
await page.route('/api/*', async (route) => {
await route.abort('failed');
});
await page.goto('/');
await expect(page).toHaveURL('/');
// Navigation should still work
await page.locator('a[href="/about"]').first().click();
await expect(page).toHaveURL('/about');
await expect(page.locator('h1')).toBeVisible();
});
test('slow network does not break form UI', async ({ page }) => {
await page.goto('/contact');
// Simulate 3G-like latency (2 second delay)
await page.route('/api/contact', async (route) => {
await new Promise((resolve) => setTimeout(resolve, 2000));
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ success: true, message: 'Slow success!' }),
});
});
await page.locator('input[name="name"]').fill('Slow Network Test');
await page.locator('input[name="email"]').fill('slow@test.com');
await page.locator('select[name="subject"]').selectOption('consulting');
await page.locator('textarea[name="message"]').fill('Testing slow network scenario.');
await page.locator('button[type="submit"]').click();
// Button disabled during slow request
await expect(page.locator('button[type="submit"]')).toBeDisabled();
// Eventually gets result
await expect(page.locator('#toast-container')).toContainText(/success/i, { timeout: 8000 });
});
});
// ============================================================
// Session State & Edge Cases
// ============================================================
test.describe('Chaos: Session State & Edge Cases', () => {
test('browser back button after form submission', async ({ page }) => {
await page.goto('/');
await page.goto('/contact');
await page.locator('input[name="name"]').fill('Back Button Test');
await page.locator('input[name="email"]').fill('back@test.com');
await page.locator('select[name="subject"]').selectOption('web-development');
await page.locator('textarea[name="message"]').fill('Testing back button behavior after submit.');
await page.locator('button[type="submit"]').click();
// Wait briefly for submission
await page.waitForTimeout(2000);
// Navigate back
await page.goBack();
await page.waitForLoadState('domcontentloaded');
// Should not cause errors
await expect(page.locator('body')).toBeVisible();
});
test('page reload during form fill does not cause errors', async ({ page }) => {
await page.goto('/contact');
await page.locator('input[name="name"]').fill('Reload Test');
// Reload mid-fill
await page.reload();
await page.waitForLoadState('domcontentloaded');
// Form should be clean after reload
const nameValue = await page.locator('input[name="name"]').inputValue();
expect(nameValue).toBe('');
});
test('multiple rapid page navigations do not cause errors', async ({ page }) => {
const pages = ['/', '/about', '/services', '/portfolio', '/blog', '/contact'];
const errors: string[] = [];
page.on('console', (msg) => {
if (msg.type() === 'error') {
errors.push(msg.text());
}
});
// Navigate rapidly between pages
for (const path of pages) {
await page.goto(path);
}
// Should not accumulate critical errors
const criticalErrors = errors.filter((e) =>
!e.includes('favicon') &&
!e.includes('404') &&
!e.includes('Failed to load resource')
);
// Allow for minor non-critical errors
expect(criticalErrors.length).toBeLessThan(3);
});
test('contact form with maximum fields does not overflow layout', async ({ page }) => {
await page.goto('/contact');
const longMessage = 'This is a very long message. '.repeat(50);
await page.locator('input[name="name"]').fill('Layout Test User');
await page.locator('input[name="email"]').fill('layout@test.com');
await page.locator('input[name="phone"]').fill('+1 (555) 999-8888');
await page.locator('select[name="subject"]').selectOption('other');
await page.locator('textarea[name="message"]').fill(longMessage.substring(0, 1000));
// Check that form does not overflow the viewport
const form = page.locator('#contact-form');
const formBox = await form.boundingBox();
const viewportSize = page.viewportSize();
if (formBox && viewportSize) {
expect(formBox.width).toBeLessThanOrEqual(viewportSize.width + 20);
}
});
test('form with only spaces is rejected', async ({ page }) => {
await page.goto('/contact');
await page.locator('input[name="name"]').fill(' '); // Spaces only
await page.locator('input[name="email"]').fill('spaces@test.com');
await page.locator('select[name="subject"]').selectOption('web-development');
await page.locator('textarea[name="message"]').fill('Valid message content here.');
await page.locator('button[type="submit"]').click();
// Should show name validation error (spaces trimmed = empty)
await page.waitForTimeout(1000);
const nameError = page.locator('[data-error="name"]');
const isVisible = await nameError.evaluate((el) => !el.classList.contains('hidden'));
expect(isVisible).toBeTruthy();
});
test('newsletter with spaces-only email is rejected', async ({ page }) => {
await page.goto('/');
await page.locator('footer').scrollIntoViewIfNeeded();
const emailInput = page.locator('#newsletter-email');
await emailInput.fill(' ');
const submitButton = page.locator('#newsletter-form button[type="submit"]');
await submitButton.click();
// HTML5 validation should block this
const isInvalid = await emailInput.evaluate((el: HTMLInputElement) => !el.checkValidity());
expect(isInvalid).toBeTruthy();
});
});
// ============================================================
// Injection & Security Edge Cases
// ============================================================
test.describe('Chaos: API Security Edge Cases', () => {
test('contact API rejects non-POST methods', async ({ request }) => {
// GET should not be allowed
const getResponse = await request.get('/api/contact');
// Should return 404 or 405 Method Not Allowed
expect([404, 405]).toContain(getResponse.status());
});
test('newsletter API rejects non-POST methods on data submission', async ({ request }) => {
const getResponse = await request.get('/api/newsletter');
expect([404, 405]).toContain(getResponse.status());
});
test('contact API with missing Content-Type still handled safely', async ({ request }) => {
const response = await request.post('/api/contact', {
data: JSON.stringify({
name: 'No Content Type',
email: 'noct@test.com',
subject: 'web-development',
message: 'Testing missing content type header.',
}),
});
// Should not crash the server
expect([200, 400, 422, 429]).toContain(response.status());
});
test('extremely large request body is rejected', async ({ request }) => {
const hugeName = 'X'.repeat(100000); // 100KB just for name
const response = await request.post('/api/contact', {
headers: { 'Content-Type': 'application/json' },
data: {
name: hugeName,
email: 'huge@test.com',
subject: 'web-development',
message: 'Normal message',
website: '',
},
});
// Should reject or handle without server crash
expect([400, 413, 422, 429]).toContain(response.status());
});
test('contact API with array values for string fields handled', async ({ request }) => {
const response = await request.post('/api/contact', {
headers: { 'Content-Type': 'application/json' },
data: {
name: ['array', 'injection'],
email: 'array@test.com',
subject: 'web-development',
message: 'Normal message content.',
website: '',
},
});
// Should not crash (422 acceptable)
expect([200, 400, 422, 429, 500]).toContain(response.status());
});
test('contact API with number values for string fields handled', async ({ request }) => {
const response = await request.post('/api/contact', {
headers: { 'Content-Type': 'application/json' },
data: {
name: 12345,
email: 'number@test.com',
subject: 'web-development',
message: 'Number field injection test.',
website: '',
},
});
// Should handle type coercion or reject gracefully
expect([200, 400, 422, 429]).toContain(response.status());
});
});