* Revise portfolio: warm brown theme, elegant typography, optimized analytics tracking (#55) * Initial plan * Update color theme to warm brown and off-white, add elegant fonts, fix analytics tracking Co-authored-by: denshooter <44590296+denshooter@users.noreply.github.com> * Fix 404 page integration with warm theme, update admin console colors, fix font loading Co-authored-by: denshooter <44590296+denshooter@users.noreply.github.com> * Address code review feedback: fix navigation, add utils, improve tracking Co-authored-by: denshooter <44590296+denshooter@users.noreply.github.com> * Fix accessibility and memory leak issues from code review Co-authored-by: denshooter <44590296+denshooter@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: denshooter <44590296+denshooter@users.noreply.github.com> * chore: Code cleanup, add Sentry.io monitoring, and documentation (#56) * Initial plan * Remove unused code and clean up console statements Co-authored-by: denshooter <44590296+denshooter@users.noreply.github.com> * Remove unused components and fix type issues Co-authored-by: denshooter <44590296+denshooter@users.noreply.github.com> * Wrap console.warn in development check Co-authored-by: denshooter <44590296+denshooter@users.noreply.github.com> * Integrate Sentry.io monitoring and add text editing documentation Co-authored-by: denshooter <44590296+denshooter@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: denshooter <44590296+denshooter@users.noreply.github.com> * Initial plan * feat: Add Sentry configuration files and example pages - Add sentry.server.config.ts and sentry.edge.config.ts - Update instrumentation.ts with onRequestError export - Update instrumentation-client.ts with onRouterTransitionStart export - Update global-error.tsx to capture exceptions with Sentry - Create Sentry example page at app/sentry-example-page/page.tsx - Create Sentry example API route at app/api/sentry-example-api/route.ts Co-authored-by: denshooter <44590296+denshooter@users.noreply.github.com> * feat: Update middleware to allow Sentry example page and fix deprecated API - Update middleware to exclude /sentry-example-page from locale routing - Remove deprecated startTransaction API from Sentry example page - Use consistent DSN configuration with fallback values Co-authored-by: denshooter <44590296+denshooter@users.noreply.github.com> * refactor: Improve Sentry configuration with environment-based sampling - Add comments explaining DSN fallback values - Use environment-based tracesSampleRate (10% in production, 100% in dev) - Address code review feedback for production-safe configuration Co-authored-by: denshooter <44590296+denshooter@users.noreply.github.com> --------- Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
138 lines
4.2 KiB
TypeScript
138 lines
4.2 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);
|
|
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);
|
|
}
|
|
});
|
|
});
|