6.8 KiB
Theme Testing Report
Agent: test-engineer
Date: 2026-03-21
Task: Test theme persistence and synchronization
File: tests/theme.spec.ts
Summary
Created a comprehensive theme test suite (tests/theme.spec.ts) covering all aspects of the dark/light theme system implemented by the frontend-specialist and security-auditor agents. The test file contains 10 test groups, ~55 individual tests.
Theme System Architecture (Observed)
| Component | Location | Role |
|---|---|---|
| Inline blocking script | BaseLayout.astro <head> |
Reads localStorage, applies html.dark before first paint (FOUC prevention) |
ThemeToggle.astro |
src/components/ThemeToggle.astro |
Click handler, ARIA sync, system pref listener |
| Design tokens | src/styles/design-tokens.css |
CSS variables for html.dark context |
| Dual meta tags | BaseLayout.astro |
<meta name="theme-color" media="(prefers-color-scheme: ...)" /> for browser chrome |
Test Coverage
1. localStorage Persistence (7 tests)
- Toggle to dark →
localStoragestores"dark" - Toggle to light →
localStoragestores"light" - Stored
"dark"applied on page load - Stored
"light"applied on page load - Preference persists across navigation (Home → About → Services)
- Toggle persists after navigate-away and return
- Multiple toggles correctly alternate and store final value
2. System Preference Detection (5 tests)
- No stored preference + dark OS → page loads dark
- No stored preference + light OS → page loads light
- Stored dark preference overrides light OS
- Stored light preference overrides dark OS
- System pref change triggers update when no stored preference
3. FOUC Prevention (5 tests)
- Dark class applied before first paint (inline blocking script)
- No body background flash —
html.darkset synchronously - Light mode: no dark class when
"light"is stored - Theme init script located in
<head>not<body> - No visible reflow on dark page reload
4. ARIA Accessibility & State Sync (9 tests)
- Toggle button has
role="switch" aria-checked="false"in light modearia-checked="true"in dark modearia-labelupdates after toggling (reflects current action)- Live region announces change on toggle
- Live region is
aria-live="polite"(non-interrupting) - All toggle instances (desktop + mobile) sync
aria-checked - Keyboard accessible via Space key
- Keyboard accessible via Enter key
5. theme-color Meta Tag Synchronization (4 tests)
- Dark mode: meta tags updated to
#0f172a - Light mode: meta tags updated to
#0891b2 - Toggling theme updates meta tags
- Both
theme-colormeta tags exist in document head
6. Visual CSS State (7 tests)
- Dark mode:
htmlelement hasdarkclass - Light mode:
htmlelement does NOT havedarkclass - Dark mode:
background-coloris visibly dark (brightness < 200) - Light mode:
background-coloris visibly light (brightness > 550) - Sun icon visible in dark mode (opacity: 1)
- Moon icon visible in light mode (opacity: 1)
- Reduced motion: icon transitions use
0sduration
7. JavaScript Disabled (4 tests)
- Page renders without JS (no crash, SSR content visible)
- Without JS: defaults to light mode (progressive enhancement)
- Without JS:
theme-colormeta tags still present (server-rendered) - Without JS:
<noscript>font fallback renders
8. Cross-Page Consistency (4 tests)
- Dark mode consistent on all 6 content pages (/)
- Theme toggle present on all content pages
- Theme state consistent in mobile nav
- Theme preserved when switching mobile ↔ desktop viewports
9. Browser Session Persistence (4 tests)
- Preference survives page refresh
- Preference survives navigation and browser Back button
- New page context starts fresh (no cross-session bleed)
localStorageaccessible from multiple tabs (same origin)
10. Visual Snapshots (8 tests)
- Dark + light screenshots for: home, about, services, contact
- Saved to
tests/screenshots/{browser}-{page}-{dark|light}.png
Key Implementation Findings
FOUC Prevention Strategy
Browser parses HTML
→ Encounters <script is:inline> in <head>
→ Reads localStorage.getItem('theme')
→ Calls document.documentElement.classList.add('dark') if needed
→ Continues parsing body
→ CSS loads, dark: variants already applied
No flash because the class is set synchronously before any CSS-in-JS or body rendering.
Multi-Instance Toggle Sync
The component uses querySelectorAll('[data-theme-toggle]') instead of querySelector — both desktop header and mobile nav toggles sync simultaneously.
System Preference Listener
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', ...) only fires when localStorage.getItem('theme') is null — stored user preference always takes precedence.
Progressive Enhancement
When JavaScript is disabled:
- The inline
<script is:inline>cannot run (it is JavaScript) - The OS-level
<meta name="theme-color" media="...">tags still work for browser chrome - Tailwind dark mode classes (
dark:) will not activate — page renders in light mode - All content remains visible (SSR-rendered HTML)
Test Design Decisions
| Decision | Rationale |
|---|---|
loadPageWithStoredTheme helper |
Simulates returning visitor with stored preference via navigate → set storage → reload |
| Brightness threshold for background color | More robust than exact hex matching across browsers |
waitForTimeout(150) after toggles |
Allows ARIA sync + meta tag updates to propagate |
| New context for session isolation tests | Playwright contexts don't share localStorage by default |
| Skip JS-disabled dark class assertion | Progressive enhancement — documented expected degraded behavior |
Test Execution
Run only theme tests:
npx playwright test tests/theme.spec.ts
Run on specific browser:
npx playwright test tests/theme.spec.ts --project=chromium
npx playwright test tests/theme.spec.ts --project=firefox
npx playwright test tests/theme.spec.ts --project=webkit
Run with UI:
npx playwright test tests/theme.spec.ts --ui
Browsers Covered
Per playwright.config.ts, tests run on:
- Chromium (Desktop Chrome)
- Firefox (Desktop Firefox)
- WebKit (Desktop Safari)
- Edge (msedge channel)
- Mobile Chrome (Pixel 5)
- Mobile Safari (iPhone 12)
- Tablet (iPad Pro 11)
Related Files
| File | Purpose |
|---|---|
src/components/ThemeToggle.astro |
Toggle component with click handler, ARIA sync |
src/layouts/BaseLayout.astro |
Inline blocking script, dual meta tags |
src/styles/design-tokens.css |
CSS custom properties for dark/light |
tests/theme.spec.ts |
This test suite |
.agents/security-auditor/ACCESSIBILITY_AUDIT.md |
Security auditor's a11y findings |