MeshiTrack/packages/web/src/components/__tests__/ThemeProvider.test.tsx

126 lines
3.5 KiB
TypeScript
Raw Normal View History

2026-05-14 19:59:42 +09:00
import { describe, it, expect, vi, beforeEach } from 'vitest';
2026-05-14 14:47:23 +09:00
import { render, screen, fireEvent, act } from '@testing-library/react';
import { ThemeProvider, useTheme } from '../ThemeProvider';
import { Providers } from '../Providers';
// Mock next-auth SessionProvider
vi.mock('next-auth/react', () => ({
SessionProvider: ({ children }: { children: React.ReactNode }) => <>{children}</>,
}));
function ThemeConsumer() {
const { theme, accent, setTheme, setAccent, toggleTheme } = useTheme();
return (
<div>
<span data-testid="theme">{theme}</span>
<span data-testid="accent">{accent}</span>
<button onClick={() => setTheme('dark')}>Set dark</button>
<button onClick={() => setAccent('cobalt')}>Set cobalt</button>
<button onClick={toggleTheme}>Toggle</button>
</div>
);
}
describe('ThemeProvider', () => {
beforeEach(() => {
localStorage.clear();
document.documentElement.removeAttribute('data-theme');
document.documentElement.style.cssText = '';
});
it('provides default light/sage theme', () => {
render(
<ThemeProvider>
<ThemeConsumer />
</ThemeProvider>,
);
expect(screen.getByTestId('theme').textContent).toBe('light');
expect(screen.getByTestId('accent').textContent).toBe('sage');
});
it('hydrates from localStorage', () => {
localStorage.setItem('mt-theme', 'dark');
localStorage.setItem('mt-accent', 'terracotta');
render(
<ThemeProvider>
<ThemeConsumer />
</ThemeProvider>,
);
expect(screen.getByTestId('theme').textContent).toBe('dark');
expect(screen.getByTestId('accent').textContent).toBe('terracotta');
});
it('setTheme updates theme and persists', () => {
render(
<ThemeProvider>
<ThemeConsumer />
</ThemeProvider>,
);
act(() => fireEvent.click(screen.getByText('Set dark')));
expect(screen.getByTestId('theme').textContent).toBe('dark');
expect(localStorage.getItem('mt-theme')).toBe('dark');
expect(document.documentElement.getAttribute('data-theme')).toBe('dark');
});
it('setAccent updates accent and applies CSS vars', () => {
render(
<ThemeProvider>
<ThemeConsumer />
</ThemeProvider>,
);
act(() => fireEvent.click(screen.getByText('Set cobalt')));
expect(screen.getByTestId('accent').textContent).toBe('cobalt');
expect(localStorage.getItem('mt-accent')).toBe('cobalt');
2026-05-19 10:13:40 +09:00
expect(document.documentElement.style.getPropertyValue('--brand')).toBe('#3b82f6');
2026-05-14 14:47:23 +09:00
});
it('toggleTheme flips light to dark', () => {
render(
<ThemeProvider>
<ThemeConsumer />
</ThemeProvider>,
);
act(() => fireEvent.click(screen.getByText('Toggle')));
expect(screen.getByTestId('theme').textContent).toBe('dark');
});
it('toggleTheme flips dark to light', () => {
localStorage.setItem('mt-theme', 'dark');
render(
<ThemeProvider>
<ThemeConsumer />
</ThemeProvider>,
);
act(() => fireEvent.click(screen.getByText('Toggle')));
expect(screen.getByTestId('theme').textContent).toBe('light');
});
it('useTheme throws outside provider', () => {
expect(() => render(<ThemeConsumer />)).toThrow('useTheme must be used inside ThemeProvider');
});
});
describe('Providers', () => {
it('renders children with session and theme providers', () => {
render(
<Providers>
<span>child content</span>
</Providers>,
);
expect(screen.getByText('child content')).toBeInTheDocument();
});
});