/** * Theme E2E Tests * * End-to-end tests for dark mode/theme switching functionality * Tests the actual UI interaction and CSS application */ import { test, expect } from '@playwright/test'; test.describe('Theme Switching E2E', () => { test.beforeEach(async ({ page }) => { // Clear localStorage before each test await page.goto('/'); await page.evaluate(() => localStorage.clear()); await page.reload(); }); test('should display theme toggle button', async ({ page }) => { await page.goto('/'); const themeToggle = page.locator('#theme-toggle'); await expect(themeToggle).toBeVisible(); }); test('should start with light theme by default', async ({ page }) => { await page.goto('/'); const theme = await page.evaluate(() => document.documentElement.getAttribute('data-theme') ); expect(theme).toBe('light'); }); test('should toggle to dark theme when button clicked', async ({ page }) => { await page.goto('/'); // Click theme toggle button await page.click('#theme-toggle'); // Verify theme changed to dark const theme = await page.evaluate(() => document.documentElement.getAttribute('data-theme') ); expect(theme).toBe('dark'); }); test('should toggle back to light theme on second click', async ({ page }) => { await page.goto('/'); // Click twice await page.click('#theme-toggle'); await page.click('#theme-toggle'); // Should be back to light const theme = await page.evaluate(() => document.documentElement.getAttribute('data-theme') ); expect(theme).toBe('light'); }); test('should update icon when toggling theme', async ({ page }) => { await page.goto('/'); // Light theme should show moon icon let iconClass = await page.locator('#theme-toggle i').getAttribute('class'); expect(iconClass).toContain('fa-moon'); // Click to dark theme await page.click('#theme-toggle'); // Dark theme should show sun icon iconClass = await page.locator('#theme-toggle i').getAttribute('class'); expect(iconClass).toContain('fa-sun'); }); test('should persist theme in localStorage', async ({ page }) => { await page.goto('/'); // Toggle to dark await page.click('#theme-toggle'); // Check localStorage const savedTheme = await page.evaluate(() => localStorage.getItem('theme')); expect(savedTheme).toBe('dark'); }); test('should load saved theme on page reload', async ({ page }) => { await page.goto('/'); // Toggle to dark await page.click('#theme-toggle'); // Reload page await page.reload(); // Should still be dark const theme = await page.evaluate(() => document.documentElement.getAttribute('data-theme') ); expect(theme).toBe('dark'); }); test('should maintain theme across navigation', async ({ page }) => { await page.goto('/'); // Toggle to dark await page.click('#theme-toggle'); // Navigate away and back (if there are other pages) // For now, just reload as a proxy await page.reload(); await page.waitForLoadState('networkidle'); // Should still be dark const theme = await page.evaluate(() => document.documentElement.getAttribute('data-theme') ); expect(theme).toBe('dark'); }); }); test.describe('Theme CSS Application', () => { test.beforeEach(async ({ page }) => { await page.goto('/'); await page.evaluate(() => localStorage.clear()); await page.reload(); }); test('should apply dark theme styles to body', async ({ page }) => { await page.goto('/'); // Toggle to dark theme await page.click('#theme-toggle'); // Wait for theme to apply await page.waitForTimeout(100); // Verify data-theme attribute is set (which CSS uses) const theme = await page.evaluate(() => document.documentElement.getAttribute('data-theme') ); expect(theme).toBe('dark'); }); test('should affect all page elements when theme changes', async ({ page }) => { await page.goto('/'); await page.waitForLoadState('networkidle'); // Get background color in light theme const lightBg = await page.evaluate(() => window.getComputedStyle(document.body).backgroundColor ); // Toggle to dark theme await page.click('#theme-toggle'); await page.waitForTimeout(100); // Get background color in dark theme const darkBg = await page.evaluate(() => window.getComputedStyle(document.body).backgroundColor ); // Colors should be different expect(lightBg).not.toBe(darkBg); }); }); test.describe('Theme Accessibility', () => { test('should have accessible theme toggle button', async ({ page }) => { await page.goto('/'); const button = page.locator('#theme-toggle'); // Button should be keyboard accessible await button.focus(); const isFocused = await button.evaluate((el) => el === document.activeElement); expect(isFocused).toBe(true); }); test('should toggle theme with Enter key', async ({ page }) => { await page.goto('/'); // Focus the button await page.locator('#theme-toggle').focus(); // Press Enter await page.keyboard.press('Enter'); // Theme should change const theme = await page.evaluate(() => document.documentElement.getAttribute('data-theme') ); expect(theme).toBe('dark'); }); test('should have proper contrast in both themes', async ({ page }) => { await page.goto('/'); await page.waitForLoadState('networkidle'); // This is a basic test - proper contrast testing would require // more sophisticated color analysis tools // Light theme let bodyBg = await page.evaluate(() => window.getComputedStyle(document.body).backgroundColor ); expect(bodyBg).toBeTruthy(); // Dark theme await page.click('#theme-toggle'); await page.waitForTimeout(100); bodyBg = await page.evaluate(() => window.getComputedStyle(document.body).backgroundColor ); expect(bodyBg).toBeTruthy(); }); }); test.describe('Theme Performance', () => { test('should toggle theme quickly without lag', async ({ page }) => { await page.goto('/'); const startTime = Date.now(); // Toggle 10 times for (let i = 0; i < 10; i++) { await page.click('#theme-toggle'); } const endTime = Date.now(); const duration = endTime - startTime; // Should complete in reasonable time (< 2 seconds for 10 toggles) expect(duration).toBeLessThan(2000); }); test('should not cause memory leaks with repeated toggles', async ({ page }) => { await page.goto('/'); // Get initial memory (if available) const initialMemory = await page.evaluate(() => { if (performance.memory) { return performance.memory.usedJSHeapSize; } return null; }); // Toggle many times for (let i = 0; i < 50; i++) { await page.click('#theme-toggle'); } // Get final memory const finalMemory = await page.evaluate(() => { if (performance.memory) { return performance.memory.usedJSHeapSize; } return null; }); if (initialMemory && finalMemory) { // Memory shouldn't grow excessively (allow 5MB growth) const memoryGrowth = finalMemory - initialMemory; expect(memoryGrowth).toBeLessThan(5 * 1024 * 1024); } }); }); test.describe('Theme Edge Cases', () => { test('should handle rapid clicks gracefully', async ({ page }) => { await page.goto('/'); // Click very rapidly const button = page.locator('#theme-toggle'); await button.click({ clickCount: 5, delay: 10 }); // Should still have a valid theme const theme = await page.evaluate(() => document.documentElement.getAttribute('data-theme') ); expect(['light', 'dark']).toContain(theme); }); test('should work when localStorage is disabled', async ({ page, context }) => { // Some browsers/modes disable localStorage // This test verifies graceful degradation await page.goto('/'); // Attempt to toggle (might fail silently) await page.click('#theme-toggle'); // Page should still function const isVisible = await page.locator('body').isVisible(); expect(isVisible).toBe(true); }); test('should handle missing theme icon element', async ({ page }) => { await page.goto('/'); // Remove the icon element await page.evaluate(() => { const icon = document.querySelector('#theme-toggle i'); if (icon) icon.remove(); }); // Toggle should still work (just without icon update) await page.click('#theme-toggle'); const theme = await page.evaluate(() => document.documentElement.getAttribute('data-theme') ); expect(theme).toBe('dark'); }); }); test.describe('Theme Integration with Other Features', () => { test('should maintain theme when opening modals', async ({ page }) => { await page.goto('/'); // Set dark theme await page.click('#theme-toggle'); // Try to open a modal (if exists) const settingsButton = page.locator('#settings-button'); if (await settingsButton.isVisible()) { await settingsButton.click(); await page.waitForTimeout(500); // Theme should still be dark const theme = await page.evaluate(() => document.documentElement.getAttribute('data-theme') ); expect(theme).toBe('dark'); } }); test('should apply theme to dynamically loaded content', async ({ page }) => { await page.goto('/'); // Set dark theme await page.click('#theme-toggle'); // Verify the data-theme attribute is on root // Any dynamically loaded content should inherit this const hasTheme = await page.evaluate(() => document.documentElement.hasAttribute('data-theme') ); expect(hasTheme).toBe(true); }); });