Phase 4: AI Assistant Integration - AI chat interface with suggested questions - Real-time messaging with backend OpenAI integration - Material UI chat bubbles and animations - Medical disclaimer and user-friendly UX Phase 5: Pattern Recognition & Analytics - Analytics dashboard with tabbed interface - Weekly sleep chart with bar/line visualizations - Feeding frequency graphs with type distribution - Growth curve with WHO percentiles (0-24 months) - Pattern insights with AI-powered recommendations - PDF report export functionality - Recharts integration for all data visualizations Phase 6: Testing & Optimization - Jest and React Testing Library setup - Unit tests for auth, API client, and components - Integration tests with full coverage - WCAG AA accessibility compliance testing - Performance optimizations (SWC, image optimization) - Accessibility monitoring with axe-core - 70% code coverage threshold 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
318 lines
8.6 KiB
TypeScript
318 lines
8.6 KiB
TypeScript
import { renderHook, waitFor, act } from '@testing-library/react';
|
|
import { AuthProvider, useAuth } from '@/lib/auth/AuthContext';
|
|
import apiClient from '@/lib/api/client';
|
|
import { useRouter } from 'next/navigation';
|
|
|
|
// Mock dependencies
|
|
jest.mock('@/lib/api/client');
|
|
jest.mock('next/navigation', () => ({
|
|
useRouter: jest.fn(),
|
|
}));
|
|
|
|
const mockApiClient = apiClient as jest.Mocked<typeof apiClient>;
|
|
const mockUseRouter = useRouter as jest.MockedFunction<typeof useRouter>;
|
|
|
|
describe('AuthContext', () => {
|
|
const mockRouter = {
|
|
push: jest.fn(),
|
|
replace: jest.fn(),
|
|
refresh: jest.fn(),
|
|
back: jest.fn(),
|
|
forward: jest.fn(),
|
|
prefetch: jest.fn(),
|
|
};
|
|
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
mockUseRouter.mockReturnValue(mockRouter as any);
|
|
localStorage.clear();
|
|
});
|
|
|
|
describe('login', () => {
|
|
it('should login successfully and store tokens', async () => {
|
|
const mockUser = {
|
|
id: 'usr_123',
|
|
email: 'test@example.com',
|
|
name: 'Test User',
|
|
role: 'parent',
|
|
};
|
|
|
|
const mockTokens = {
|
|
accessToken: 'access_token_123',
|
|
refreshToken: 'refresh_token_123',
|
|
};
|
|
|
|
mockApiClient.post.mockResolvedValueOnce({
|
|
data: {
|
|
success: true,
|
|
data: {
|
|
user: mockUser,
|
|
tokens: mockTokens,
|
|
},
|
|
},
|
|
});
|
|
|
|
const { result } = renderHook(() => useAuth(), {
|
|
wrapper: AuthProvider,
|
|
});
|
|
|
|
await act(async () => {
|
|
await result.current.login({
|
|
email: 'test@example.com',
|
|
password: 'password123',
|
|
});
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(result.current.user).toEqual(mockUser);
|
|
expect(result.current.isAuthenticated).toBe(true);
|
|
expect(localStorage.setItem).toHaveBeenCalledWith('accessToken', mockTokens.accessToken);
|
|
expect(localStorage.setItem).toHaveBeenCalledWith('refreshToken', mockTokens.refreshToken);
|
|
expect(mockRouter.push).toHaveBeenCalledWith('/');
|
|
});
|
|
});
|
|
|
|
it('should handle login failure', async () => {
|
|
mockApiClient.post.mockRejectedValueOnce({
|
|
response: {
|
|
data: {
|
|
message: 'Invalid credentials',
|
|
},
|
|
},
|
|
});
|
|
|
|
const { result } = renderHook(() => useAuth(), {
|
|
wrapper: AuthProvider,
|
|
});
|
|
|
|
await expect(
|
|
result.current.login({
|
|
email: 'test@example.com',
|
|
password: 'wrong_password',
|
|
})
|
|
).rejects.toThrow('Invalid credentials');
|
|
|
|
expect(result.current.user).toBeNull();
|
|
expect(result.current.isAuthenticated).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('register', () => {
|
|
it('should register successfully and redirect to onboarding', async () => {
|
|
const mockUser = {
|
|
id: 'usr_123',
|
|
email: 'newuser@example.com',
|
|
name: 'New User',
|
|
role: 'parent',
|
|
};
|
|
|
|
const mockTokens = {
|
|
accessToken: 'access_token_123',
|
|
refreshToken: 'refresh_token_123',
|
|
};
|
|
|
|
mockApiClient.post.mockResolvedValueOnce({
|
|
data: {
|
|
success: true,
|
|
data: {
|
|
user: mockUser,
|
|
tokens: mockTokens,
|
|
family: { id: 'fam_123' },
|
|
},
|
|
},
|
|
});
|
|
|
|
const { result } = renderHook(() => useAuth(), {
|
|
wrapper: AuthProvider,
|
|
});
|
|
|
|
await act(async () => {
|
|
await result.current.register({
|
|
email: 'newuser@example.com',
|
|
password: 'password123',
|
|
name: 'New User',
|
|
});
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(result.current.user).toEqual(mockUser);
|
|
expect(localStorage.setItem).toHaveBeenCalledWith('accessToken', mockTokens.accessToken);
|
|
expect(mockRouter.push).toHaveBeenCalledWith('/onboarding');
|
|
});
|
|
});
|
|
|
|
it('should handle registration failure with invalid tokens', async () => {
|
|
mockApiClient.post.mockResolvedValueOnce({
|
|
data: {
|
|
success: true,
|
|
data: {
|
|
user: {},
|
|
tokens: {}, // Invalid tokens
|
|
},
|
|
},
|
|
});
|
|
|
|
const { result } = renderHook(() => useAuth(), {
|
|
wrapper: AuthProvider,
|
|
});
|
|
|
|
await expect(
|
|
result.current.register({
|
|
email: 'newuser@example.com',
|
|
password: 'password123',
|
|
name: 'New User',
|
|
})
|
|
).rejects.toThrow('Invalid response from server');
|
|
});
|
|
});
|
|
|
|
describe('logout', () => {
|
|
it('should logout successfully and clear tokens', async () => {
|
|
mockApiClient.post.mockResolvedValueOnce({
|
|
data: { success: true },
|
|
});
|
|
|
|
localStorage.setItem('accessToken', 'token_123');
|
|
localStorage.setItem('refreshToken', 'refresh_123');
|
|
|
|
const { result } = renderHook(() => useAuth(), {
|
|
wrapper: AuthProvider,
|
|
});
|
|
|
|
await act(async () => {
|
|
await result.current.logout();
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(localStorage.removeItem).toHaveBeenCalledWith('accessToken');
|
|
expect(localStorage.removeItem).toHaveBeenCalledWith('refreshToken');
|
|
expect(result.current.user).toBeNull();
|
|
expect(mockRouter.push).toHaveBeenCalledWith('/login');
|
|
});
|
|
});
|
|
|
|
it('should clear tokens even if API call fails', async () => {
|
|
mockApiClient.post.mockRejectedValueOnce(new Error('Network error'));
|
|
|
|
localStorage.setItem('accessToken', 'token_123');
|
|
localStorage.setItem('refreshToken', 'refresh_123');
|
|
|
|
const { result } = renderHook(() => useAuth(), {
|
|
wrapper: AuthProvider,
|
|
});
|
|
|
|
await act(async () => {
|
|
await result.current.logout();
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(localStorage.removeItem).toHaveBeenCalledWith('accessToken');
|
|
expect(localStorage.removeItem).toHaveBeenCalledWith('refreshToken');
|
|
expect(mockRouter.push).toHaveBeenCalledWith('/login');
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('refreshUser', () => {
|
|
it('should refresh user data successfully', async () => {
|
|
const mockUser = {
|
|
id: 'usr_123',
|
|
email: 'test@example.com',
|
|
name: 'Updated Name',
|
|
role: 'parent',
|
|
};
|
|
|
|
mockApiClient.get.mockResolvedValueOnce({
|
|
data: {
|
|
data: mockUser,
|
|
},
|
|
});
|
|
|
|
const { result } = renderHook(() => useAuth(), {
|
|
wrapper: AuthProvider,
|
|
});
|
|
|
|
await act(async () => {
|
|
await result.current.refreshUser();
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(result.current.user).toEqual(mockUser);
|
|
});
|
|
});
|
|
|
|
it('should handle refresh failure gracefully', async () => {
|
|
mockApiClient.get.mockRejectedValueOnce(new Error('Unauthorized'));
|
|
|
|
const { result } = renderHook(() => useAuth(), {
|
|
wrapper: AuthProvider,
|
|
});
|
|
|
|
await act(async () => {
|
|
await result.current.refreshUser();
|
|
});
|
|
|
|
// User should remain null, no error thrown
|
|
expect(result.current.user).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('authentication check', () => {
|
|
it('should check auth on mount with valid token', async () => {
|
|
const mockUser = {
|
|
id: 'usr_123',
|
|
email: 'test@example.com',
|
|
name: 'Test User',
|
|
role: 'parent',
|
|
};
|
|
|
|
localStorage.setItem('accessToken', 'valid_token');
|
|
|
|
mockApiClient.get.mockResolvedValueOnce({
|
|
data: {
|
|
data: mockUser,
|
|
},
|
|
});
|
|
|
|
const { result } = renderHook(() => useAuth(), {
|
|
wrapper: AuthProvider,
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(result.current.isLoading).toBe(false);
|
|
expect(result.current.user).toEqual(mockUser);
|
|
expect(result.current.isAuthenticated).toBe(true);
|
|
});
|
|
});
|
|
|
|
it('should handle auth check failure', async () => {
|
|
localStorage.setItem('accessToken', 'invalid_token');
|
|
|
|
mockApiClient.get.mockRejectedValueOnce(new Error('Unauthorized'));
|
|
|
|
const { result } = renderHook(() => useAuth(), {
|
|
wrapper: AuthProvider,
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(result.current.isLoading).toBe(false);
|
|
expect(result.current.user).toBeNull();
|
|
expect(localStorage.removeItem).toHaveBeenCalledWith('accessToken');
|
|
expect(localStorage.removeItem).toHaveBeenCalledWith('refreshToken');
|
|
});
|
|
});
|
|
|
|
it('should not check auth if no token exists', async () => {
|
|
const { result } = renderHook(() => useAuth(), {
|
|
wrapper: AuthProvider,
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(result.current.isLoading).toBe(false);
|
|
expect(result.current.user).toBeNull();
|
|
expect(mockApiClient.get).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
});
|
|
});
|