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
535 lines
19 KiB
TypeScript
535 lines
19 KiB
TypeScript
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 = '<script>alert("XSS")</script>';
|
|
|
|
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 = '"><img src=x onerror=alert(1)>';
|
|
|
|
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());
|
|
});
|
|
});
|