import { test, expect } from '@playwright/test'; /** * Hydration Tests * Ensures React hydration works correctly without errors */ test.describe('Hydration Tests', () => { test('No hydration errors in console', async ({ page }) => { const consoleErrors: string[] = []; const consoleWarnings: string[] = []; // Capture console messages page.on('console', (msg) => { const text = msg.text(); if (msg.type() === 'error') { consoleErrors.push(text); } else if (msg.type() === 'warning') { consoleWarnings.push(text); } }); // Navigate to home page. // Avoid `networkidle` because the app has background polling/analytics requests. await page.goto('/en', { waitUntil: 'domcontentloaded' }); // Give hydration a moment to run await page.waitForTimeout(1000); // Check for hydration errors const hydrationErrors = consoleErrors.filter(error => error.includes('Hydration') || error.includes('hydration') || error.includes('Text content does not match') || error.includes('Expected server HTML') ); expect(hydrationErrors.length).toBe(0); // Log warnings for review (but don't fail) if (consoleWarnings.length > 0) { console.log('Console warnings:', consoleWarnings); } }); test('No duplicate React key warnings', async ({ page }) => { const consoleWarnings: string[] = []; page.on('console', (msg) => { if (msg.type() === 'warning') { const text = msg.text(); if (text.includes('key') || text.includes('duplicate')) { consoleWarnings.push(text); } } }); await page.goto('/en', { waitUntil: 'domcontentloaded' }); await page.waitForTimeout(500); // Check for duplicate key warnings const keyWarnings = consoleWarnings.filter(warning => warning.includes('key') && warning.includes('duplicate') ); expect(keyWarnings.length).toBe(0); }); test('Client-side navigation works without hydration errors', async ({ page }) => { const consoleErrors: string[] = []; page.on('console', (msg) => { if (msg.type() === 'error') { consoleErrors.push(msg.text()); } }); await page.goto('/en', { waitUntil: 'domcontentloaded' }); await page.waitForTimeout(500); // Navigate to projects page via link const projectsLink = page.locator('a[href*="/projects"]').first(); if (await projectsLink.count() > 0) { await projectsLink.click(); await page.waitForLoadState('domcontentloaded'); await page.waitForTimeout(500); // Check for errors after navigation const hydrationErrors = consoleErrors.filter(error => error.includes('Hydration') || error.includes('hydration') ); expect(hydrationErrors.length).toBe(0); } }); test('Server and client HTML match', async ({ page }) => { await page.goto('/en'); // Get initial HTML const initialHTML = await page.content(); // Wait for React to hydrate (avoid networkidle due to background requests) await page.waitForTimeout(1000); // Get HTML after hydration const hydratedHTML = await page.content(); // Basic check: main structure should be similar // (exact match is hard due to dynamic content) expect(hydratedHTML.length).toBeGreaterThan(0); expect(initialHTML.length).toBeGreaterThan(0); }); test('Interactive elements work after hydration', async ({ page }) => { await page.goto('/en'); await page.waitForTimeout(1000); // Try to find and click interactive elements const buttons = page.locator('button, a[role="button"]'); const buttonCount = await buttons.count(); if (buttonCount > 0) { // Find a visible interactive element (desktop hides some mobile-only buttons) let clicked = false; for (let i = 0; i < Math.min(buttonCount, 25); i++) { const candidate = buttons.nth(i); // eslint-disable-next-line no-await-in-loop if (await candidate.isVisible()) { await candidate.click().catch(() => { // Some buttons might be disabled or covered, that's OK }); clicked = true; break; } } expect(clicked).toBe(true); } }); });