Add tests for auth and theme contexts
This commit is contained in:
parent
cb9ccc9792
commit
b8599b4d08
2 changed files with 220 additions and 0 deletions
125
marketplace/src/context/AuthContext.test.tsx
Normal file
125
marketplace/src/context/AuthContext.test.tsx
Normal file
|
|
@ -0,0 +1,125 @@
|
||||||
|
import { render, screen, waitFor } from '@testing-library/react'
|
||||||
|
import userEvent from '@testing-library/user-event'
|
||||||
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||||
|
import { AuthProvider, useAuth } from './AuthContext'
|
||||||
|
|
||||||
|
const navigateMock = vi.fn()
|
||||||
|
const setTokenMock = vi.fn()
|
||||||
|
const logoutMock = vi.fn().mockResolvedValue(undefined)
|
||||||
|
|
||||||
|
vi.mock('react-router-dom', async () => {
|
||||||
|
const actual = await vi.importActual<typeof import('react-router-dom')>('react-router-dom')
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
useNavigate: () => navigateMock
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
vi.mock('../services/apiClient', () => ({
|
||||||
|
apiClient: {
|
||||||
|
setToken: setTokenMock
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('../services/auth', () => ({
|
||||||
|
authService: {
|
||||||
|
logout: logoutMock
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
function AuthConsumer() {
|
||||||
|
const { user, loading, login, logout, setUser } = useAuth()
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div data-testid="loading">{loading ? 'loading' : 'ready'}</div>
|
||||||
|
<div data-testid="username">{user?.name ?? 'guest'}</div>
|
||||||
|
<button onClick={() => login('token-123', 'admin', 'Admin User', '1')}>login admin</button>
|
||||||
|
<button onClick={() => login('token-456', 'delivery', 'Delivery User', '2')}>login delivery</button>
|
||||||
|
<button
|
||||||
|
onClick={() =>
|
||||||
|
setUser({
|
||||||
|
token: 'token-999',
|
||||||
|
role: 'seller',
|
||||||
|
name: 'Seller User',
|
||||||
|
id: '3',
|
||||||
|
companyId: 'company-1'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
set user
|
||||||
|
</button>
|
||||||
|
<button onClick={logout}>logout</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('AuthProvider', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
localStorage.clear()
|
||||||
|
navigateMock.mockReset()
|
||||||
|
setTokenMock.mockReset()
|
||||||
|
logoutMock.mockClear()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
localStorage.clear()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('hydrates user from localStorage and sets token', async () => {
|
||||||
|
const persisted = { token: 'persisted-token', role: 'admin', name: 'Persisted', id: '10' }
|
||||||
|
localStorage.setItem('mp-auth-user', JSON.stringify(persisted))
|
||||||
|
|
||||||
|
render(
|
||||||
|
<AuthProvider>
|
||||||
|
<AuthConsumer />
|
||||||
|
</AuthProvider>
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(screen.getByTestId('username')).toHaveTextContent('Persisted')
|
||||||
|
expect(setTokenMock).toHaveBeenCalledWith('persisted-token')
|
||||||
|
await waitFor(() => expect(screen.getByTestId('loading')).toHaveTextContent('ready'))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('logs in and navigates based on role', async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
render(
|
||||||
|
<AuthProvider>
|
||||||
|
<AuthConsumer />
|
||||||
|
</AuthProvider>
|
||||||
|
)
|
||||||
|
|
||||||
|
await user.click(screen.getByRole('button', { name: /login admin/i }))
|
||||||
|
|
||||||
|
expect(navigateMock).toHaveBeenCalledWith('/dashboard', { replace: true })
|
||||||
|
expect(localStorage.getItem('mp-auth-user')).toContain('Admin User')
|
||||||
|
|
||||||
|
await user.click(screen.getByRole('button', { name: /login delivery/i }))
|
||||||
|
expect(navigateMock).toHaveBeenCalledWith('/entregas', { replace: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('clears session on logout', async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
render(
|
||||||
|
<AuthProvider>
|
||||||
|
<AuthConsumer />
|
||||||
|
</AuthProvider>
|
||||||
|
)
|
||||||
|
|
||||||
|
await user.click(screen.getByRole('button', { name: /set user/i }))
|
||||||
|
expect(localStorage.getItem('mp-auth-user')).toContain('Seller User')
|
||||||
|
|
||||||
|
await user.click(screen.getByRole('button', { name: /logout/i }))
|
||||||
|
|
||||||
|
expect(logoutMock).toHaveBeenCalledTimes(1)
|
||||||
|
await waitFor(() => expect(localStorage.getItem('mp-auth-user'))).toBeNull()
|
||||||
|
expect(navigateMock).toHaveBeenCalledWith('/login', { replace: true })
|
||||||
|
expect(setTokenMock).toHaveBeenCalledWith(null)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('throws when useAuth is used outside provider', () => {
|
||||||
|
const ConsoleError = console.error
|
||||||
|
console.error = vi.fn()
|
||||||
|
expect(() => render(<AuthConsumer />)).toThrow('useAuth must be used within AuthProvider')
|
||||||
|
console.error = ConsoleError
|
||||||
|
})
|
||||||
|
})
|
||||||
95
marketplace/src/context/ThemeContext.test.tsx
Normal file
95
marketplace/src/context/ThemeContext.test.tsx
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
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
|
||||||
|
})
|
||||||
|
})
|
||||||
Loading…
Reference in a new issue