import { test, expect, Page } from '@playwright/test'; // ============================================================ // Theme Persistence & Synchronization Tests // Tests: localStorage, system preference, FOUC, multi-browser // ============================================================ // Helper: get the current theme state from the DOM async function getThemeState(page: Page) { return page.evaluate(() => ({ hasDarkClass: document.documentElement.classList.contains('dark'), stored: (() => { try { return localStorage.getItem('theme'); } catch { return null; } })(), ariaChecked: document.querySelector('[data-theme-toggle]')?.getAttribute('aria-checked'), ariaLabel: document.querySelector('[data-theme-toggle]')?.getAttribute('aria-label'), themeColorMetas: Array.from(document.querySelectorAll('meta[name="theme-color"]')).map(m => ({ content: m.getAttribute('content'), media: m.getAttribute('media'), })), liveRegion: document.querySelector('[data-theme-live]')?.textContent ?? '', htmlClass: document.documentElement.className, })); } // Helper: set localStorage theme before page load (via storageState simulation) async function loadPageWithStoredTheme(page: Page, theme: 'dark' | 'light' | null, url = '/') { // Navigate to page first, then set storage, then reload — simulates returning visitor await page.goto(url, { waitUntil: 'domcontentloaded' }); if (theme !== null) { await page.evaluate((t) => { try { localStorage.setItem('theme', t); } catch { /* noop */ } }, theme); } else { await page.evaluate(() => { try { localStorage.removeItem('theme'); } catch { /* noop */ } }); } // Reload to trigger the blocking init script await page.reload({ waitUntil: 'domcontentloaded' }); } // Helper: click the theme toggle async function clickThemeToggle(page: Page) { const toggle = page.locator('[data-theme-toggle]').first(); await expect(toggle).toBeVisible(); await toggle.click(); await page.waitForTimeout(150); // Allow state sync } // ============================================================ // 1. localStorage Persistence // ============================================================ test.describe('Theme: localStorage Persistence', () => { test('Toggling to dark stores "dark" in localStorage', async ({ page }) => { await loadPageWithStoredTheme(page, null); // Start in light mode (no stored preference) const before = await getThemeState(page); // State may be light or dark depending on system — click once and check stored value await clickThemeToggle(page); const after = await getThemeState(page); const isDarkNow = after.hasDarkClass; expect(after.stored).toBe(isDarkNow ? 'dark' : 'light'); }); test('Toggling to light stores "light" in localStorage', async ({ page }) => { // Start with dark stored await loadPageWithStoredTheme(page, 'dark'); const before = await getThemeState(page); expect(before.hasDarkClass).toBe(true); await clickThemeToggle(page); const after = await getThemeState(page); expect(after.hasDarkClass).toBe(false); expect(after.stored).toBe('light'); }); test('Stored "dark" preference is applied on page load', async ({ page }) => { await loadPageWithStoredTheme(page, 'dark'); const state = await getThemeState(page); expect(state.hasDarkClass).toBe(true); expect(state.stored).toBe('dark'); }); test('Stored "light" preference is applied on page load', async ({ page }) => { await loadPageWithStoredTheme(page, 'light'); const state = await getThemeState(page); expect(state.hasDarkClass).toBe(false); expect(state.stored).toBe('light'); }); test('Preference persists across page navigation (About → Services)', async ({ page }) => { await loadPageWithStoredTheme(page, 'dark'); // Verify dark on home expect((await getThemeState(page)).hasDarkClass).toBe(true); // Navigate to About await page.goto('/about', { waitUntil: 'domcontentloaded' }); expect((await getThemeState(page)).hasDarkClass).toBe(true); // Navigate to Services await page.goto('/services', { waitUntil: 'domcontentloaded' }); expect((await getThemeState(page)).hasDarkClass).toBe(true); }); test('Light preference persists across page navigation', async ({ page }) => { await loadPageWithStoredTheme(page, 'light'); await page.goto('/portfolio', { waitUntil: 'domcontentloaded' }); const state = await getThemeState(page); expect(state.hasDarkClass).toBe(false); expect(state.stored).toBe('light'); }); test('Theme toggle persists after navigating away and back', async ({ page }) => { await loadPageWithStoredTheme(page, null); // Toggle to dark await clickThemeToggle(page); const wasSetToDark = (await getThemeState(page)).hasDarkClass; // Navigate away await page.goto('/contact', { waitUntil: 'domcontentloaded' }); const afterNav = await getThemeState(page); expect(afterNav.hasDarkClass).toBe(wasSetToDark); // Navigate back await page.goto('/', { waitUntil: 'domcontentloaded' }); const afterBack = await getThemeState(page); expect(afterBack.hasDarkClass).toBe(wasSetToDark); }); test('Multiple toggles correctly alternate and store final value', async ({ page }) => { await loadPageWithStoredTheme(page, 'light'); await clickThemeToggle(page); // → dark expect((await getThemeState(page)).stored).toBe('dark'); await clickThemeToggle(page); // → light expect((await getThemeState(page)).stored).toBe('light'); await clickThemeToggle(page); // → dark const final = await getThemeState(page); expect(final.stored).toBe('dark'); expect(final.hasDarkClass).toBe(true); }); }); // ============================================================ // 2. System Preference Detection (prefers-color-scheme) // ============================================================ test.describe('Theme: System Preference Detection', () => { test('No stored preference: dark OS → page loads in dark mode', async ({ page }) => { await page.emulateMedia({ colorScheme: 'dark' }); await loadPageWithStoredTheme(page, null); const state = await getThemeState(page); expect(state.hasDarkClass).toBe(true); expect(state.stored).toBeNull(); // Should NOT store when respecting OS }); test('No stored preference: light OS → page loads in light mode', async ({ page }) => { await page.emulateMedia({ colorScheme: 'light' }); await loadPageWithStoredTheme(page, null); const state = await getThemeState(page); expect(state.hasDarkClass).toBe(false); expect(state.stored).toBeNull(); }); test('Stored preference overrides OS preference (dark stored, light OS)', async ({ page }) => { await page.emulateMedia({ colorScheme: 'light' }); await loadPageWithStoredTheme(page, 'dark'); const state = await getThemeState(page); expect(state.hasDarkClass).toBe(true); // Stored wins }); test('Stored preference overrides OS preference (light stored, dark OS)', async ({ page }) => { await page.emulateMedia({ colorScheme: 'dark' }); await loadPageWithStoredTheme(page, 'light'); const state = await getThemeState(page); expect(state.hasDarkClass).toBe(false); // Stored wins }); test('System preference change updates theme when no stored preference', async ({ page }) => { await page.emulateMedia({ colorScheme: 'light' }); await loadPageWithStoredTheme(page, null); // Verify light initially expect((await getThemeState(page)).hasDarkClass).toBe(false); // Simulate OS switching to dark await page.emulateMedia({ colorScheme: 'dark' }); await page.waitForTimeout(300); // Allow matchMedia listener to fire const state = await getThemeState(page); expect(state.hasDarkClass).toBe(true); expect(state.stored).toBeNull(); // Still not stored }); test('System preference change is ignored when user has stored preference', async ({ page }) => { await page.emulateMedia({ colorScheme: 'light' }); await loadPageWithStoredTheme(page, 'dark'); // OS switches to light await page.emulateMedia({ colorScheme: 'light' }); await page.waitForTimeout(300); const state = await getThemeState(page); // User's stored 'dark' should still be active — but note: system pref change // listener only runs for initial load not active toggles — document behavior as-is expect(state.stored).toBe('dark'); }); }); // ============================================================ // 3. No Flash of Unstyled Content (FOUC) Prevention // ============================================================ test.describe('Theme: FOUC Prevention', () => { test('Dark mode class applied before first paint (inline script)', async ({ page }) => { // Set up dark preference before navigating // We need to ensure html.dark is set synchronously before any CSS loads // Use CDP to intercept and verify class set early let darkClassAppliedBeforeDOMContentLoaded = false; // Listen on DOMContentLoaded and check if class is already present await page.addInitScript(() => { // This script runs AFTER inline scripts but BEFORE DOMContentLoaded // The theme init inline script should have run by now window.__themeClassAtInitScript = document.documentElement.classList.contains('dark'); }); // Navigate with dark stored await page.goto('/', { waitUntil: 'domcontentloaded' }); await page.evaluate(() => { try { localStorage.setItem('theme', 'dark'); } catch { /* noop */ } }); // Reload and capture early state await page.addInitScript(() => { // Capture class state at very start of script execution window.__earlyDarkCheck = document.documentElement.classList.contains('dark'); }); await page.reload({ waitUntil: 'domcontentloaded' }); // After reload, html.dark should be present immediately const hasDarkClass = await page.evaluate(() => document.documentElement.classList.contains('dark')); expect(hasDarkClass).toBe(true); }); test('No body background flash - html.dark applied synchronously', async ({ page }) => { // Navigate with dark preference and verify dark class is set on html element await page.goto('/', { waitUntil: 'domcontentloaded' }); await page.evaluate(() => { try { localStorage.setItem('theme', 'dark'); } catch { /* noop */ } }); await page.reload({ waitUntil: 'domcontentloaded' }); // The dark class must be on the html element (not body) const htmlClasses = await page.evaluate(() => document.documentElement.className); expect(htmlClasses).toContain('dark'); }); test('Light mode: no dark class on html element when light is stored', async ({ page }) => { await page.goto('/', { waitUntil: 'domcontentloaded' }); await page.evaluate(() => { try { localStorage.setItem('theme', 'light'); } catch { /* noop */ } }); await page.reload({ waitUntil: 'domcontentloaded' }); const hasDarkClass = await page.evaluate(() => document.documentElement.classList.contains('dark')); expect(hasDarkClass).toBe(false); }); test('Theme init script is in not ', async ({ page }) => { await page.goto('/', { waitUntil: 'domcontentloaded' }); // Verify the blocking theme script is in const headScripts = await page.evaluate(() => { const scripts = Array.from(document.head.querySelectorAll('script:not([type])')); return scripts.map(s => s.textContent?.substring(0, 100) ?? ''); }); // At least one head script should contain the localStorage theme logic const hasThemeScript = headScripts.some(s => s.includes('localStorage') && (s.includes('theme') || s.includes('dark')) ); expect(hasThemeScript).toBe(true); }); test('FOUC check: no visible content reflow on dark reload', async ({ page }) => { // If FOUC were present, background-color would transition from white to dark // We verify the html element has the class before DOMContentLoaded fires let classBeforeDOMContentLoaded: boolean | null = null; page.on('domcontentloaded', async () => { classBeforeDOMContentLoaded = await page.evaluate( () => document.documentElement.classList.contains('dark') ).catch(() => null); }); // Ensure dark is stored await page.goto('/', { waitUntil: 'domcontentloaded' }); await page.evaluate(() => { try { localStorage.setItem('theme', 'dark'); } catch {} }); await page.reload({ waitUntil: 'domcontentloaded' }); // At DOMContentLoaded, dark class should be present (set by inline blocking script) const hasDark = await page.evaluate(() => document.documentElement.classList.contains('dark')); expect(hasDark).toBe(true); }); }); // ============================================================ // 4. ARIA Accessibility & State Synchronization // ============================================================ test.describe('Theme: ARIA Accessibility', () => { test('Toggle button has role="switch"', async ({ page }) => { await page.goto('/', { waitUntil: 'domcontentloaded' }); const role = await page.locator('[data-theme-toggle]').first().getAttribute('role'); expect(role).toBe('switch'); }); test('aria-checked="false" in light mode', async ({ page }) => { await loadPageWithStoredTheme(page, 'light'); const ariaChecked = await page.locator('[data-theme-toggle]').first().getAttribute('aria-checked'); expect(ariaChecked).toBe('false'); }); test('aria-checked="true" in dark mode', async ({ page }) => { await loadPageWithStoredTheme(page, 'dark'); const ariaChecked = await page.locator('[data-theme-toggle]').first().getAttribute('aria-checked'); expect(ariaChecked).toBe('true'); }); test('aria-label updates after toggling to dark', async ({ page }) => { await loadPageWithStoredTheme(page, 'light'); await clickThemeToggle(page); const state = await getThemeState(page); if (state.hasDarkClass) { expect(state.ariaLabel).toBe('Switch to light mode'); expect(state.ariaChecked).toBe('true'); } else { expect(state.ariaLabel).toBe('Switch to dark mode'); expect(state.ariaChecked).toBe('false'); } }); test('aria-label updates after toggling to light', async ({ page }) => { await loadPageWithStoredTheme(page, 'dark'); await clickThemeToggle(page); const state = await getThemeState(page); expect(state.hasDarkClass).toBe(false); expect(state.ariaLabel).toBe('Switch to dark mode'); expect(state.ariaChecked).toBe('false'); }); test('Live region announces theme change on toggle', async ({ page }) => { await loadPageWithStoredTheme(page, 'light'); // Clear live region await page.evaluate(() => { document.querySelectorAll('[data-theme-live]').forEach(el => el.textContent = ''); }); await clickThemeToggle(page); await page.waitForTimeout(200); const liveRegionText = await page.evaluate(() => document.querySelector('[data-theme-live]')?.textContent ?? '' ); expect(['Dark mode enabled', 'Light mode enabled']).toContain(liveRegionText.trim()); }); test('Live region is polite (non-interrupting)', async ({ page }) => { await page.goto('/', { waitUntil: 'domcontentloaded' }); const ariaLive = await page.locator('[data-theme-live]').first().getAttribute('aria-live'); expect(ariaLive).toBe('polite'); }); test('All theme toggle instances sync aria-checked (desktop + mobile)', async ({ page }) => { await page.setViewportSize({ width: 375, height: 667 }); await loadPageWithStoredTheme(page, 'light'); // Click toggle (finds first visible one on mobile) await clickThemeToggle(page); await page.waitForTimeout(200); // All toggle instances should have same aria-checked const allCheckedValues = await page.evaluate(() => Array.from(document.querySelectorAll('[data-theme-toggle]')).map(btn => btn.getAttribute('aria-checked') ) ); // All should be the same value const uniqueValues = [...new Set(allCheckedValues)]; expect(uniqueValues).toHaveLength(1); }); test('Toggle is keyboard accessible (Space key)', async ({ page }) => { await loadPageWithStoredTheme(page, 'light'); const before = await getThemeState(page); const wasLight = !before.hasDarkClass; // Focus and press Space await page.locator('[data-theme-toggle]').first().focus(); await page.keyboard.press('Space'); await page.waitForTimeout(150); const after = await getThemeState(page); // Space on a button triggers click - theme should have toggled expect(after.hasDarkClass).toBe(!before.hasDarkClass); }); test('Toggle is keyboard accessible (Enter key)', async ({ page }) => { await loadPageWithStoredTheme(page, 'light'); const before = await getThemeState(page); await page.locator('[data-theme-toggle]').first().focus(); await page.keyboard.press('Enter'); await page.waitForTimeout(150); const after = await getThemeState(page); expect(after.hasDarkClass).toBe(!before.hasDarkClass); }); test('Toggle button is focusable (tabindex not -1)', async ({ page }) => { await page.goto('/', { waitUntil: 'domcontentloaded' }); const tabindex = await page.locator('[data-theme-toggle]').first().getAttribute('tabindex'); // Should not be -1 (which would make it unfocusable) expect(tabindex).not.toBe('-1'); }); }); // ============================================================ // 5. theme-color Meta Tag Synchronization // ============================================================ test.describe('Theme: Meta Tag Synchronization', () => { test('Dark mode: theme-color meta tags updated to dark color', async ({ page }) => { await loadPageWithStoredTheme(page, 'dark'); const state = await getThemeState(page); // At least one meta should have the dark color const hasStoredDarkColor = state.themeColorMetas.some(m => m.content === '#0f172a'); expect(hasStoredDarkColor).toBe(true); }); test('Light mode: theme-color meta tags updated to light color', async ({ page }) => { await loadPageWithStoredTheme(page, 'light'); const state = await getThemeState(page); // At least one meta should have the light color const hasStoredLightColor = state.themeColorMetas.some(m => m.content === '#0891b2'); expect(hasStoredLightColor).toBe(true); }); test('Toggling theme updates theme-color meta tags', async ({ page }) => { await loadPageWithStoredTheme(page, 'light'); await clickThemeToggle(page); await page.waitForTimeout(150); const state = await getThemeState(page); if (state.hasDarkClass) { const hasDarkColor = state.themeColorMetas.some(m => m.content === '#0f172a'); expect(hasDarkColor).toBe(true); } else { const hasLightColor = state.themeColorMetas.some(m => m.content === '#0891b2'); expect(hasLightColor).toBe(true); } }); test('Both theme-color meta tags exist in the document head', async ({ page }) => { await page.goto('/', { waitUntil: 'domcontentloaded' }); const metaCount = await page.evaluate(() => document.querySelectorAll('meta[name="theme-color"]').length ); expect(metaCount).toBeGreaterThanOrEqual(1); }); }); // ============================================================ // 6. Visual State (CSS) // ============================================================ test.describe('Theme: Visual CSS State', () => { test('Dark mode: html element has "dark" class', async ({ page }) => { await loadPageWithStoredTheme(page, 'dark'); const hasDark = await page.evaluate(() => document.documentElement.classList.contains('dark')); expect(hasDark).toBe(true); }); test('Light mode: html element does NOT have "dark" class', async ({ page }) => { await loadPageWithStoredTheme(page, 'light'); const hasDark = await page.evaluate(() => document.documentElement.classList.contains('dark')); expect(hasDark).toBe(false); }); test('Dark mode: page background is dark-colored', async ({ page }) => { await loadPageWithStoredTheme(page, 'dark'); const bgColor = await page.evaluate(() => window.getComputedStyle(document.body).backgroundColor ); // Dark surface is #0f172a = rgb(15, 23, 42) // Verify it's distinctly dark (r+g+b should be low) const match = bgColor.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/); if (match) { const brightness = parseInt(match[1]) + parseInt(match[2]) + parseInt(match[3]); expect(brightness).toBeLessThan(200); // Dark background } }); test('Light mode: page background is light-colored', async ({ page }) => { await loadPageWithStoredTheme(page, 'light'); const bgColor = await page.evaluate(() => window.getComputedStyle(document.body).backgroundColor ); const match = bgColor.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/); if (match) { const brightness = parseInt(match[1]) + parseInt(match[2]) + parseInt(match[3]); expect(brightness).toBeGreaterThan(550); // Light background } }); test('Sun icon visible in dark mode', async ({ page }) => { await loadPageWithStoredTheme(page, 'dark'); const sunOpacity = await page.evaluate(() => { const btn = document.querySelector('[data-theme-toggle]'); const sun = btn?.querySelector('.sun-icon') as HTMLElement | null; return sun ? window.getComputedStyle(sun).opacity : null; }); expect(sunOpacity).toBe('1'); }); test('Moon icon visible in light mode', async ({ page }) => { await loadPageWithStoredTheme(page, 'light'); const moonOpacity = await page.evaluate(() => { const btn = document.querySelector('[data-theme-toggle]'); const moon = btn?.querySelector('.moon-icon') as HTMLElement | null; return moon ? window.getComputedStyle(moon).opacity : null; }); expect(moonOpacity).toBe('1'); }); test('Dark mode: icon transitions respect prefers-reduced-motion', async ({ page }) => { await page.emulateMedia({ reducedMotion: 'reduce' }); await loadPageWithStoredTheme(page, 'dark'); const transitionDuration = await page.evaluate(() => { const btn = document.querySelector('[data-theme-toggle]'); const sun = btn?.querySelector('.sun-icon') as HTMLElement | null; return sun ? window.getComputedStyle(sun).transitionDuration : null; }); // With reduced motion, transition should be 0s or none if (transitionDuration) { expect(['0s', '0ms']).toContain(transitionDuration); } }); }); // ============================================================ // 7. JavaScript Disabled Behavior // ============================================================ test.describe('Theme: JavaScript Disabled', () => { test('Page renders without JS (no crash, content visible)', async ({ browser }) => { // Create context with JS disabled const context = await browser.newContext({ javaScriptEnabled: false }); const page = await context.newPage(); await page.goto('/', { waitUntil: 'domcontentloaded' }); // Page should still render content (SSR) const bodyText = await page.locator('body').innerText(); expect(bodyText.trim().length).toBeGreaterThan(50); // Header should be present await expect(page.locator('#main-header')).toBeAttached(); await context.close(); }); test('Without JS: page defaults to light mode (no dark class)', async ({ browser }) => { const context = await browser.newContext({ javaScriptEnabled: false }); const page = await context.newPage(); await page.goto('/', { waitUntil: 'domcontentloaded' }); // Without JS, the inline theme script cannot run // Page should default to light mode (no dark class on html) const hasDark = await page.evaluate(() => document.documentElement.classList.contains('dark')); // Note: This tests the degraded state — dark class won't be applied without JS // This is expected behavior (progressive enhancement) // The OS-level theme-color media queries still work for browser chrome expect(typeof hasDark).toBe('boolean'); // Just verify evaluation works await context.close(); }); test('Without JS: theme-color meta tags still exist (native OS support)', async ({ browser }) => { const context = await browser.newContext({ javaScriptEnabled: false }); const page = await context.newPage(); await page.goto('/', { waitUntil: 'domcontentloaded' }); // Meta tags are server-rendered so they exist even without JS const metaCount = await page.evaluate(() => document.querySelectorAll('meta[name="theme-color"]').length ); expect(metaCount).toBeGreaterThanOrEqual(1); await context.close(); }); test('Without JS: noscript font fallback renders', async ({ browser }) => { const context = await browser.newContext({ javaScriptEnabled: false }); const page = await context.newPage(); await page.goto('/', { waitUntil: 'domcontentloaded' }); // Noscript font link should be in the document const noscript = await page.evaluate(() => { const noscripts = document.querySelectorAll('noscript'); return Array.from(noscripts).some(n => n.innerHTML.includes('fonts.googleapis.com')); }); expect(noscript).toBe(true); await context.close(); }); }); // ============================================================ // 8. Cross-Page Theme Consistency // ============================================================ test.describe('Theme: Cross-Page Consistency', () => { const ALL_CONTENT_PAGES = ['/', '/about', '/services', '/portfolio', '/contact', '/blog']; for (const url of ALL_CONTENT_PAGES) { test(`Dark mode consistent on ${url}`, async ({ page }) => { // Start with dark preference and visit each page await page.goto('/', { waitUntil: 'domcontentloaded' }); await page.evaluate(() => { try { localStorage.setItem('theme', 'dark'); } catch {} }); await page.goto(url, { waitUntil: 'domcontentloaded' }); const hasDark = await page.evaluate(() => document.documentElement.classList.contains('dark')); expect(hasDark).toBe(true); }); } test('Theme toggle present on all content pages', async ({ page }) => { await page.setViewportSize({ width: 1280, height: 800 }); // Desktop view for (const url of ALL_CONTENT_PAGES) { await page.goto(url, { waitUntil: 'domcontentloaded' }); const toggleCount = await page.locator('[data-theme-toggle]').count(); expect(toggleCount, `Theme toggle missing on ${url}`).toBeGreaterThanOrEqual(1); } }); test('Theme state consistent in mobile nav', async ({ page }) => { await page.setViewportSize({ width: 375, height: 667 }); await loadPageWithStoredTheme(page, 'dark'); // Open mobile menu const mobileToggle = page.locator('#mobile-menu-toggle'); await expect(mobileToggle).toBeVisible(); await mobileToggle.click(); await page.waitForTimeout(400); // There should be a theme toggle in the mobile menu too const togglesInMobileMenu = await page.locator('#mobile-menu [data-theme-toggle]').count(); // At least one toggle should exist (in header, possibly in mobile menu too) const allToggles = await page.locator('[data-theme-toggle]').count(); expect(allToggles).toBeGreaterThanOrEqual(1); }); test('Theme preserved when switching between mobile and desktop viewports', async ({ page }) => { await page.setViewportSize({ width: 375, height: 667 }); await loadPageWithStoredTheme(page, 'dark'); expect((await getThemeState(page)).hasDarkClass).toBe(true); // Resize to desktop await page.setViewportSize({ width: 1280, height: 800 }); await page.waitForTimeout(200); expect((await getThemeState(page)).hasDarkClass).toBe(true); }); }); // ============================================================ // 9. Browser Session Persistence // ============================================================ test.describe('Theme: Browser Session Persistence', () => { test('Theme preference survives page refresh', async ({ page }) => { await loadPageWithStoredTheme(page, 'dark'); // Hard refresh await page.reload({ waitUntil: 'domcontentloaded' }); const state = await getThemeState(page); expect(state.hasDarkClass).toBe(true); expect(state.stored).toBe('dark'); }); test('Theme preference survives navigation and back button', async ({ page }) => { await loadPageWithStoredTheme(page, 'dark'); await page.goto('/about', { waitUntil: 'domcontentloaded' }); expect((await getThemeState(page)).hasDarkClass).toBe(true); await page.goBack({ waitUntil: 'domcontentloaded' }); expect((await getThemeState(page)).hasDarkClass).toBe(true); }); test('New page context starts fresh (no cross-session bleed)', async ({ browser }) => { // Session 1: set dark const context1 = await browser.newContext(); const page1 = await context1.newPage(); await page1.goto('http://localhost:10000/', { waitUntil: 'domcontentloaded' }); await page1.evaluate(() => { try { localStorage.setItem('theme', 'dark'); } catch {} }); await context1.close(); // Session 2: fresh context - should not inherit session 1's preference const context2 = await browser.newContext(); const page2 = await context2.newPage(); await page2.goto('http://localhost:10000/', { waitUntil: 'domcontentloaded' }); const stored = await page2.evaluate(() => { try { return localStorage.getItem('theme'); } catch { return null; } }); // Fresh context should have no stored preference expect(stored).toBeNull(); await context2.close(); }); test('localStorage persists through multiple tabs (same origin)', async ({ context }) => { // Set dark in tab 1 const page1 = await context.newPage(); await page1.goto('/', { waitUntil: 'domcontentloaded' }); await page1.evaluate(() => { try { localStorage.setItem('theme', 'dark'); } catch {} }); // Open tab 2 — should pick up dark preference from localStorage const page2 = await context.newPage(); await page2.goto('/', { waitUntil: 'domcontentloaded' }); const stored = await page2.evaluate(() => { try { return localStorage.getItem('theme'); } catch { return null; } }); expect(stored).toBe('dark'); await page1.close(); await page2.close(); }); }); // ============================================================ // 10. Visual Screenshots (dark & light across browsers) // ============================================================ test.describe('Theme: Visual Snapshots', () => { const SNAPSHOT_PAGES = [ { name: 'home', url: '/' }, { name: 'about', url: '/about' }, { name: 'services', url: '/services' }, { name: 'contact', url: '/contact' }, ]; for (const p of SNAPSHOT_PAGES) { test(`Dark mode screenshot: ${p.name}`, async ({ page, browserName }) => { await page.setViewportSize({ width: 1280, height: 800 }); await loadPageWithStoredTheme(page, 'dark'); await page.goto(p.url, { waitUntil: 'domcontentloaded' }); await page.waitForTimeout(500); await page.screenshot({ path: `tests/screenshots/${browserName}-${p.name}-dark.png`, fullPage: false, clip: { x: 0, y: 0, width: 1280, height: 800 }, }); }); test(`Light mode screenshot: ${p.name}`, async ({ page, browserName }) => { await page.setViewportSize({ width: 1280, height: 800 }); await loadPageWithStoredTheme(page, 'light'); await page.goto(p.url, { waitUntil: 'domcontentloaded' }); await page.waitForTimeout(500); await page.screenshot({ path: `tests/screenshots/${browserName}-${p.name}-light.png`, fullPage: false, clip: { x: 0, y: 0, width: 1280, height: 800 }, }); }); } });