diff --git a/src/components/ui/ThemeToggle.test.tsx b/src/components/ui/ThemeToggle.test.tsx new file mode 100644 index 0000000..f86985f --- /dev/null +++ b/src/components/ui/ThemeToggle.test.tsx @@ -0,0 +1,71 @@ +// @vitest-environment jsdom +import { describe, it, expect, beforeEach } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { ThemeToggle } from './ThemeToggle'; + +describe('ThemeToggle', () => { + beforeEach(() => { + // Reset DOM and localStorage before each test + document.documentElement.classList.remove('dark'); + localStorage.clear(); + }); + + it('renders three buttons: Light, Dark, System', () => { + render(); + expect(screen.getByRole('button', { name: /light/i })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /dark/i })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /system/i })).toBeInTheDocument(); + }); + + it('default state is System (aria-pressed="true" on System button) when localStorage is empty', () => { + render(); + const systemButton = screen.getByRole('button', { name: /system/i }); + const lightButton = screen.getByRole('button', { name: /light/i }); + const darkButton = screen.getByRole('button', { name: /dark/i }); + expect(systemButton).toHaveAttribute('aria-pressed', 'true'); + expect(lightButton).toHaveAttribute('aria-pressed', 'false'); + expect(darkButton).toHaveAttribute('aria-pressed', 'false'); + }); + + it('clicking Dark adds .dark class to document.documentElement', async () => { + const user = userEvent.setup(); + render(); + const darkButton = screen.getByRole('button', { name: /dark/i }); + await user.click(darkButton); + expect(document.documentElement.classList.contains('dark')).toBe(true); + }); + + it('clicking Light removes .dark class from document.documentElement', async () => { + const user = userEvent.setup(); + // Start with dark class applied + document.documentElement.classList.add('dark'); + render(); + const lightButton = screen.getByRole('button', { name: /light/i }); + await user.click(lightButton); + expect(document.documentElement.classList.contains('dark')).toBe(false); + }); + + it('clicking Dark writes "dark" to localStorage under key "r2b-theme"', async () => { + const user = userEvent.setup(); + render(); + const darkButton = screen.getByRole('button', { name: /dark/i }); + await user.click(darkButton); + expect(localStorage.getItem('r2b-theme')).toBe('dark'); + }); + + it('active button has aria-pressed="true", others have aria-pressed="false"', async () => { + const user = userEvent.setup(); + render(); + const darkButton = screen.getByRole('button', { name: /dark/i }); + await user.click(darkButton); + expect(darkButton).toHaveAttribute('aria-pressed', 'true'); + expect(screen.getByRole('button', { name: /light/i })).toHaveAttribute('aria-pressed', 'false'); + expect(screen.getByRole('button', { name: /system/i })).toHaveAttribute('aria-pressed', 'false'); + }); + + it('component group has aria-label="Theme"', () => { + render(); + expect(screen.getByRole('group', { name: 'Theme' })).toBeInTheDocument(); + }); +});