diff --git a/marketplace/src/context/AuthContext.test.tsx b/marketplace/src/context/AuthContext.test.tsx new file mode 100644 index 0000000..5df05ef --- /dev/null +++ b/marketplace/src/context/AuthContext.test.tsx @@ -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('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 ( +
+
{loading ? 'loading' : 'ready'}
+
{user?.name ?? 'guest'}
+ + + + +
+ ) +} + +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( + + + + ) + + 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( + + + + ) + + 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( + + + + ) + + 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()).toThrow('useAuth must be used within AuthProvider') + console.error = ConsoleError + }) +}) diff --git a/marketplace/src/context/ThemeContext.test.tsx b/marketplace/src/context/ThemeContext.test.tsx new file mode 100644 index 0000000..7eef17a --- /dev/null +++ b/marketplace/src/context/ThemeContext.test.tsx @@ -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 ( +
+
{theme}
+
{resolvedTheme}
+ + +
+ ) +} + +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( + + + + ) + + 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( + + + + ) + + 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( + + + + ) + + 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()).toThrow('useTheme must be used within ThemeProvider') + console.error = ConsoleError + }) +})