Files
portfolio/e2e/hydration.spec.ts
Cursor Agent 63fc45488a test(e2e): click first visible interactive element
Avoid failing on mobile-only hidden buttons in desktop viewport.

Co-authored-by: dennis <dennis@konkol.net>
2026-01-14 22:00:39 +00:00

139 lines
4.3 KiB
TypeScript

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);
}
});
});