95 lines
3 KiB
TypeScript
95 lines
3 KiB
TypeScript
import { render, screen, waitFor } from '@testing-library/react'
|
|
import userEvent from '@testing-library/user-event'
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
import { ThemeProvider, useTheme } from './ThemeContext'
|
|
|
|
function ThemeConsumer() {
|
|
const { theme, resolvedTheme, setTheme, toggleTheme } = useTheme()
|
|
return (
|
|
<div>
|
|
<div data-testid="theme">{theme}</div>
|
|
<div data-testid="resolved">{resolvedTheme}</div>
|
|
<button onClick={() => setTheme('dark')}>set dark</button>
|
|
<button onClick={toggleTheme}>toggle</button>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
describe('ThemeProvider', () => {
|
|
beforeEach(() => {
|
|
localStorage.clear()
|
|
})
|
|
|
|
it('loads theme from localStorage and applies resolved theme', () => {
|
|
localStorage.setItem('mp-theme', 'dark')
|
|
const matchMediaMock = vi.fn().mockReturnValue({
|
|
matches: false,
|
|
addEventListener: vi.fn(),
|
|
removeEventListener: vi.fn()
|
|
})
|
|
vi.stubGlobal('matchMedia', matchMediaMock)
|
|
|
|
render(
|
|
<ThemeProvider>
|
|
<ThemeConsumer />
|
|
</ThemeProvider>
|
|
)
|
|
|
|
expect(screen.getByTestId('theme')).toHaveTextContent('dark')
|
|
expect(screen.getByTestId('resolved')).toHaveTextContent('dark')
|
|
expect(document.documentElement.classList.contains('dark')).toBe(true)
|
|
expect(document.documentElement.getAttribute('data-theme')).toBe('dark')
|
|
})
|
|
|
|
it('resolves system theme when set to system', async () => {
|
|
const matchMediaMock = vi.fn().mockReturnValue({
|
|
matches: true,
|
|
addEventListener: vi.fn(),
|
|
removeEventListener: vi.fn()
|
|
})
|
|
vi.stubGlobal('matchMedia', matchMediaMock)
|
|
|
|
render(
|
|
<ThemeProvider>
|
|
<ThemeConsumer />
|
|
</ThemeProvider>
|
|
)
|
|
|
|
expect(screen.getByTestId('theme')).toHaveTextContent('system')
|
|
expect(screen.getByTestId('resolved')).toHaveTextContent('dark')
|
|
expect(document.documentElement.classList.contains('dark')).toBe(true)
|
|
|
|
const user = userEvent.setup()
|
|
await user.click(screen.getByRole('button', { name: /set dark/i }))
|
|
expect(localStorage.getItem('mp-theme')).toBe('dark')
|
|
})
|
|
|
|
it('toggles between light and dark', async () => {
|
|
const matchMediaMock = vi.fn().mockReturnValue({
|
|
matches: false,
|
|
addEventListener: vi.fn(),
|
|
removeEventListener: vi.fn()
|
|
})
|
|
vi.stubGlobal('matchMedia', matchMediaMock)
|
|
|
|
render(
|
|
<ThemeProvider>
|
|
<ThemeConsumer />
|
|
</ThemeProvider>
|
|
)
|
|
|
|
const user = userEvent.setup()
|
|
expect(screen.getByTestId('resolved')).toHaveTextContent('light')
|
|
|
|
await user.click(screen.getByRole('button', { name: /toggle/i }))
|
|
await waitFor(() => expect(screen.getByTestId('resolved')).toHaveTextContent('dark'))
|
|
expect(document.documentElement.classList.contains('dark')).toBe(true)
|
|
})
|
|
|
|
it('throws when useTheme is used outside provider', () => {
|
|
const ConsoleError = console.error
|
|
console.error = vi.fn()
|
|
expect(() => render(<ThemeConsumer />)).toThrow('useTheme must be used within ThemeProvider')
|
|
console.error = ConsoleError
|
|
})
|
|
})
|