/** * Bug Fix Verification Tests * Targeted regression tests for 5 fixed bugs. * These tests would have caught the original issues and will catch regressions. * * BUG-1: Duplicate footer on Portfolio page * BUG-2: Blog page theme styling issues * BUG-3: Read Blog post 500 error * BUG-4: CORS issues on Contact/Newsletter forms * BUG-5: Header text black in dark mode */ import { test, expect } from '@playwright/test'; // ============================================================ // BUG-1: Duplicate footer on Portfolio page // ============================================================ test.describe('BUG-1: Portfolio page has exactly one footer', () => { test('Portfolio page renders exactly one footer element', async ({ page }) => { await page.goto('/portfolio', { waitUntil: 'domcontentloaded' }); const footers = page.locator('footer'); await expect(footers).toHaveCount(1); }); test('Portfolio page footer is visible', async ({ page }) => { await page.goto('/portfolio', { waitUntil: 'domcontentloaded' }); await expect(page.locator('footer')).toBeVisible(); }); test('Portfolio page has one header and one footer (no duplicates)', async ({ page }) => { await page.goto('/portfolio', { waitUntil: 'domcontentloaded' }); // Exactly one header await expect(page.locator('#main-header')).toHaveCount(1); // Exactly one footer await expect(page.locator('footer')).toHaveCount(1); }); test('Other pages also have exactly one footer (sanity check)', async ({ page }) => { const pages = ['/', '/about', '/services', '/contact', '/blog']; for (const url of pages) { await page.goto(url, { waitUntil: 'domcontentloaded' }); const count = await page.locator('footer').count(); expect(count, `${url} should have exactly 1 footer, found ${count}`).toBe(1); } }); }); // ============================================================ // BUG-2: Blog page theme styling issues // ============================================================ test.describe('BUG-2: Blog page theme styling', () => { test('Blog index page loads without layout errors', async ({ page }) => { const consoleErrors: string[] = []; page.on('console', (msg) => { if (msg.type() === 'error') consoleErrors.push(msg.text()); }); const response = await page.goto('/blog', { waitUntil: 'domcontentloaded' }); expect(response?.status()).toBe(200); const critical = consoleErrors.filter( (e) => !e.includes('favicon') && !e.includes('livereload') && !e.includes('manifest') ); expect(critical).toHaveLength(0); }); test('Blog page applies dark mode classes when dark mode is active', async ({ page }) => { await page.goto('/blog', { waitUntil: 'domcontentloaded' }); // Force dark mode await page.evaluate(() => { document.documentElement.classList.add('dark'); localStorage.setItem('theme', 'dark'); }); await page.waitForTimeout(200); // Verify dark class is applied to html element const hasDarkClass = await page.evaluate(() => document.documentElement.classList.contains('dark') ); expect(hasDarkClass).toBe(true); }); test('Blog page hero section background is not transparent in dark mode', async ({ page }) => { await page.goto('/blog', { waitUntil: 'domcontentloaded' }); await page.evaluate(() => { document.documentElement.classList.add('dark'); }); await page.waitForTimeout(300); // Hero section should have a visible background (secondary-900 dark gradient) const heroSection = page.locator('section').first(); await expect(heroSection).toBeVisible(); const bgColor = await heroSection.evaluate((el) => window.getComputedStyle(el).backgroundColor ); // Should NOT be pure white (rgb(255, 255, 255)) in dark mode // The actual color depends on Tailwind's secondary-900, but it should be dark expect(bgColor).not.toBe('rgba(0, 0, 0, 0)'); }); test('Blog h1 is visible in both light and dark mode', async ({ page }) => { await page.goto('/blog', { waitUntil: 'domcontentloaded' }); await expect(page.locator('h1')).toBeVisible(); // Switch to dark mode await page.evaluate(() => document.documentElement.classList.add('dark')); await page.waitForTimeout(200); // H1 still visible await expect(page.locator('h1')).toBeVisible(); }); }); // ============================================================ // BUG-3: Read Blog post 500 error // ============================================================ test.describe('BUG-3: Blog post route never returns 500', () => { test('Blog index page never returns 500', async ({ page }) => { const response = await page.goto('/blog', { waitUntil: 'domcontentloaded' }); expect(response?.status()).not.toBe(500); expect(response?.status()).toBe(200); }); test('Blog post with invalid slug redirects (never 500)', async ({ page }) => { // A non-existent slug should return 404 redirect, not 500 const response = await page.goto('/blog/this-post-definitely-does-not-exist-xyz', { waitUntil: 'domcontentloaded', }); // Should redirect to /404 (200 for custom error page) or return actual 404 // Critical: MUST NOT be 500 const status = response?.status(); expect(status).not.toBe(500); }); test('Blog post with empty slug path redirects to /blog (never 500)', async ({ page }) => { // The slug guard redirects empty slug to /blog const response = await page.goto('/blog/', { waitUntil: 'domcontentloaded' }); expect(response?.status()).not.toBe(500); }); test('First blog post (if exists) loads successfully without 500', async ({ page }) => { await page.goto('/blog', { waitUntil: 'domcontentloaded' }); const firstPostLink = page.locator('a[href^="/blog/"]').first(); const hasPost = (await firstPostLink.count()) > 0; if (!hasPost) { test.skip(); // No posts in content collection — skip gracefully return; } const postHref = await firstPostLink.getAttribute('href'); if (!postHref) { test.skip(); return; } const response = await page.goto(postHref, { waitUntil: 'domcontentloaded' }); // Critical assertion: never a 500 expect(response?.status()).not.toBe(500); expect(response?.status()).toBe(200); // Has rendered content (markdown rendered to HTML) await expect(page.locator('h1')).toBeVisible(); const bodyText = await page.locator('body').innerText(); expect(bodyText.trim().length).toBeGreaterThan(100); }); test('Blog post SSR renders with BaseLayout (header + footer present)', async ({ page }) => { await page.goto('/blog', { waitUntil: 'domcontentloaded' }); const firstPostLink = page.locator('a[href^="/blog/"]').first(); if ((await firstPostLink.count()) === 0) { test.skip(); return; } await firstPostLink.click(); await page.waitForLoadState('domcontentloaded'); // BaseLayout provides these — presence confirms SSR completed without crash await expect(page.locator('#main-header')).toBeAttached(); await expect(page.locator('footer')).toBeAttached(); }); }); // ============================================================ // BUG-4: CORS issues on Contact & Newsletter forms // ============================================================ test.describe('BUG-4: CORS headers on API endpoints', () => { test('Contact API OPTIONS preflight returns 204 with CORS headers', async ({ page }) => { // Use fetch to send OPTIONS preflight const result = await page.evaluate(async () => { const res = await fetch('/api/contact', { method: 'OPTIONS', headers: { Origin: 'http://localhost:10000', 'Access-Control-Request-Method': 'POST', 'Access-Control-Request-Headers': 'Content-Type', }, }); return { status: res.status, allowOrigin: res.headers.get('access-control-allow-origin'), allowMethods: res.headers.get('access-control-allow-methods'), allowHeaders: res.headers.get('access-control-allow-headers'), }; }); expect(result.status).toBe(204); expect(result.allowOrigin).toBeTruthy(); expect(result.allowMethods).toContain('POST'); }); test('Newsletter API OPTIONS preflight returns 204 with CORS headers', async ({ page }) => { const result = await page.evaluate(async () => { const res = await fetch('/api/newsletter', { method: 'OPTIONS', headers: { Origin: 'http://localhost:10000', 'Access-Control-Request-Method': 'POST', 'Access-Control-Request-Headers': 'Content-Type', }, }); return { status: res.status, allowOrigin: res.headers.get('access-control-allow-origin'), allowMethods: res.headers.get('access-control-allow-methods'), }; }); expect(result.status).toBe(204); expect(result.allowOrigin).toBeTruthy(); expect(result.allowMethods).toContain('POST'); }); test('Contact API POST returns CORS headers (not missing)', async ({ page }) => { const result = await page.evaluate(async () => { const res = await fetch('/api/contact', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: '', email: '', subject: '', message: '' }), }); return { status: res.status, allowOrigin: res.headers.get('access-control-allow-origin'), contentType: res.headers.get('content-type'), }; }); // Status is validation error (422) or rate limit — but CORS headers must be present expect(result.allowOrigin).toBeTruthy(); expect(result.contentType).toContain('application/json'); // Critical: must NOT be a CORS error (which would give status 0 or throw) expect(result.status).toBeGreaterThan(0); }); test('Newsletter API POST returns CORS headers with invalid email', async ({ page }) => { const result = await page.evaluate(async () => { const res = await fetch('/api/newsletter', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: 'not-valid-email' }), }); return { status: res.status, allowOrigin: res.headers.get('access-control-allow-origin'), }; }); expect(result.status).toBe(422); // Validation error expect(result.allowOrigin).toBeTruthy(); // CORS header present }); test('Contact form page submits without JS CORS error', async ({ page }) => { const corsErrors: string[] = []; // Catch CORS-related console errors page.on('console', (msg) => { if ( msg.type() === 'error' && (msg.text().includes('CORS') || msg.text().includes('Access-Control') || msg.text().includes('cross-origin')) ) { corsErrors.push(msg.text()); } }); await page.goto('/contact', { waitUntil: 'domcontentloaded' }); // Attempt a form submission with invalid data (will get 422, but no CORS error) await page.fill('#name', 'Test'); await page.fill('#email', 'test@example.com'); await page.selectOption('#subject', 'web-development'); await page.fill('#message', 'This is a test submission for CORS verification'); await page.locator('#contact-form button[type="submit"]').click(); await page.waitForTimeout(1500); expect(corsErrors, 'CORS errors detected on form submission').toHaveLength(0); }); test('Newsletter form in footer submits without CORS error', async ({ page }) => { const corsErrors: string[] = []; page.on('console', (msg) => { if ( msg.type() === 'error' && (msg.text().includes('CORS') || msg.text().includes('Access-Control')) ) { corsErrors.push(msg.text()); } }); await page.goto('/', { waitUntil: 'domcontentloaded' }); // Scroll to footer newsletter form await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight)); await page.waitForTimeout(300); const newsletterInput = page.locator('footer input[type="email"]'); if (await newsletterInput.count() > 0) { await newsletterInput.fill('test@example.com'); const submitBtn = page.locator('footer button[type="submit"]').first(); if (await submitBtn.count() > 0) { await submitBtn.click(); await page.waitForTimeout(1000); } } expect(corsErrors, 'CORS errors on newsletter form').toHaveLength(0); }); }); // ============================================================ // BUG-5: Header text black in dark mode // ============================================================ test.describe('BUG-5: Header text readable in dark mode', () => { test('WorkRoot logo text is white (not black) in dark mode', async ({ page }) => { await page.goto('/', { waitUntil: 'domcontentloaded' }); // Activate dark mode await page.evaluate(() => { document.documentElement.classList.add('dark'); localStorage.setItem('theme', 'dark'); }); await page.waitForTimeout(200); // Find the WorkRoot logo span in the desktop header const logoSpan = page.locator('#main-header span').filter({ hasText: 'WorkRoot' }).first(); await expect(logoSpan).toBeVisible(); const color = await logoSpan.evaluate((el) => window.getComputedStyle(el).color ); // In dark mode, should be white (#ffffff = rgb(255, 255, 255)) // Original broken state was rgb(30, 41, 59) = #1e293b (near black) expect(color).not.toBe('rgb(30, 41, 59)'); // NOT the old broken black color expect(color).toBe('rgb(255, 255, 255)'); // IS the fixed white color }); test('WorkRoot logo text is dark (not white) in light mode', async ({ page }) => { await page.goto('/', { waitUntil: 'domcontentloaded' }); // Ensure light mode await page.evaluate(() => { document.documentElement.classList.remove('dark'); localStorage.setItem('theme', 'light'); }); await page.waitForTimeout(200); const logoSpan = page.locator('#main-header span').filter({ hasText: 'WorkRoot' }).first(); await expect(logoSpan).toBeVisible(); const color = await logoSpan.evaluate((el) => window.getComputedStyle(el).color ); // In light mode, should be dark text (text-secondary = #1e293b) expect(color).toBe('rgb(30, 41, 59)'); // Dark color in light mode is correct }); test('Header logo is readable on all pages in dark mode', async ({ page }) => { const testPages = ['/', '/about', '/services', '/portfolio', '/blog', '/contact']; for (const url of testPages) { await page.goto(url, { waitUntil: 'domcontentloaded' }); await page.evaluate(() => { document.documentElement.classList.add('dark'); }); await page.waitForTimeout(150); const logoSpan = page.locator('#main-header span').filter({ hasText: 'WorkRoot' }).first(); await expect(logoSpan).toBeVisible(); const color = await logoSpan.evaluate((el) => window.getComputedStyle(el).color ); // Must NOT be the broken black color expect( color, `Logo text on ${url} in dark mode should be white, not black` ).not.toBe('rgb(30, 41, 59)'); } }); test('Mobile menu WorkRoot text is white in dark mode', async ({ page }) => { await page.setViewportSize({ width: 375, height: 667 }); await page.goto('/', { waitUntil: 'domcontentloaded' }); // Activate dark mode await page.evaluate(() => { document.documentElement.classList.add('dark'); }); // Open mobile menu await page.locator('#mobile-menu-toggle').click(); await page.waitForTimeout(400); // Mobile menu logo span const mobileLogoSpan = page.locator('#mobile-menu span').filter({ hasText: 'WorkRoot' }).first(); // May or may not have a WorkRoot span in mobile menu, check gracefully if ((await mobileLogoSpan.count()) > 0) { const color = await mobileLogoSpan.evaluate((el) => window.getComputedStyle(el).color ); expect(color).not.toBe('rgb(30, 41, 59)'); } }); test('Header contrast is WCAG AA compliant in dark mode', async ({ page }) => { await page.goto('/', { waitUntil: 'domcontentloaded' }); await page.evaluate(() => { document.documentElement.classList.add('dark'); }); await page.waitForTimeout(200); const header = page.locator('#main-header'); const logoSpan = page.locator('#main-header span').filter({ hasText: 'WorkRoot' }).first(); const headerBg = await header.evaluate((el) => window.getComputedStyle(el).backgroundColor ); const textColor = await logoSpan.evaluate((el) => window.getComputedStyle(el).color ); // Both colors are set — not transparent expect(headerBg).not.toBe('rgba(0, 0, 0, 0)'); expect(textColor).not.toBe('rgba(0, 0, 0, 0)'); // White text on dark bg = ~17.5:1 contrast ratio (WAY above 4.5:1 AA) // Just verify it's not the broken identical-to-background color expect(textColor).not.toBe(headerBg); }); }); // ============================================================ // Cross-cutting: All fixes together — full page smoke // ============================================================ test.describe('All Fixes: Full page smoke test', () => { test('Home page loads cleanly with correct layout', async ({ page }) => { const response = await page.goto('/', { waitUntil: 'domcontentloaded' }); expect(response?.status()).toBe(200); await expect(page.locator('#main-header')).toHaveCount(1); await expect(page.locator('footer')).toHaveCount(1); }); test('Portfolio page loads with single footer and working filters', async ({ page }) => { await page.goto('/portfolio', { waitUntil: 'domcontentloaded' }); await expect(page.locator('footer')).toHaveCount(1); await expect(page.locator('.project-card').first()).toBeVisible(); await page.locator('button[data-filter="web"]').click(); await page.waitForTimeout(400); expect(await page.locator('.project-card:not(.hidden)').count()).toBeGreaterThan(0); }); test('Blog page loads without 500 and with correct theme structure', async ({ page }) => { const response = await page.goto('/blog', { waitUntil: 'domcontentloaded' }); expect(response?.status()).not.toBe(500); expect(response?.status()).toBe(200); await expect(page.locator('h1')).toBeVisible(); await expect(page.locator('footer')).toHaveCount(1); }); test('Contact page has form with working API endpoint (no CORS error)', async ({ page }) => { const errors: string[] = []; page.on('console', (msg) => { if (msg.type() === 'error') errors.push(msg.text()); }); await page.goto('/contact', { waitUntil: 'domcontentloaded' }); await expect(page.locator('#contact-form')).toBeVisible(); // Verify OPTIONS preflight works (simulates browser CORS check) const preflightResult = await page.evaluate(async () => { try { const res = await fetch('/api/contact', { method: 'OPTIONS' }); return { ok: true, status: res.status }; } catch { return { ok: false, status: 0 }; } }); expect(preflightResult.ok).toBe(true); expect(preflightResult.status).toBe(204); const criticalErrors = errors.filter( (e) => !e.includes('favicon') && !e.includes('manifest') && !e.includes('livereload') ); expect(criticalErrors).toHaveLength(0); }); test('Dark mode toggle works and header stays readable', async ({ page }) => { await page.goto('/', { waitUntil: 'domcontentloaded' }); // Toggle dark mode via ThemeToggle button const themeToggle = page.locator('#theme-toggle').first(); if (await themeToggle.count() > 0) { await themeToggle.click(); await page.waitForTimeout(300); } else { // Force dark mode programmatically await page.evaluate(() => document.documentElement.classList.add('dark')); await page.waitForTimeout(200); } // Header is visible await expect(page.locator('#main-header')).toBeVisible(); // Logo text exists and is not black const logoSpan = page.locator('#main-header span').filter({ hasText: 'WorkRoot' }).first(); if (await logoSpan.count() > 0) { const color = await logoSpan.evaluate((el) => window.getComputedStyle(el).color); expect(color).not.toBe('rgb(30, 41, 59)'); } }); });