import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import { useRouter } from 'next/navigation'; import LoginPage from '../app/(auth)/login/page'; import api from '../lib/api'; // Mock Next.js router jest.mock('next/navigation', () => ({ useRouter: jest.fn(), useSearchParams: jest.fn(), })); // Mock API jest.mock('../lib/api', () => ({ __esModule: true, default: { getToken: jest.fn(), checkEmail: jest.fn(), requestOTP: jest.fn(), saveEmail: jest.fn(), }, })); describe('LoginPage', () => { const mockPush = jest.fn(); const mockReplace = jest.fn(); beforeEach(() => { jest.clearAllMocks(); (useRouter as jest.Mock).mockReturnValue({ push: mockPush, replace: mockReplace, }); (api.getToken as jest.Mock).mockResolvedValue(null); }); it('renders login form correctly', () => { render(); expect(screen.getByText('Welcome to WellNuo')).toBeInTheDocument(); expect(screen.getByLabelText('Email address')).toBeInTheDocument(); expect(screen.getByRole('button', { name: /get verification code/i })).toBeInTheDocument(); }); it('redirects to dashboard if already authenticated', async () => { (api.getToken as jest.Mock).mockResolvedValue('mock-token'); render(); await waitFor(() => { expect(mockReplace).toHaveBeenCalledWith('/dashboard'); }); }); it('shows error for empty email', async () => { render(); const form = screen.getByRole('button', { name: /get verification code/i }).closest('form'); fireEvent.submit(form!); await waitFor(() => { expect(screen.getByText('Email is required')).toBeInTheDocument(); }); }); it('shows error for invalid email format', async () => { render(); const emailInput = screen.getByLabelText('Email address'); const form = screen.getByRole('button', { name: /get verification code/i }).closest('form'); fireEvent.change(emailInput, { target: { value: 'invalid-email' } }); fireEvent.submit(form!); await waitFor(() => { expect(screen.getByText('Please enter a valid email address')).toBeInTheDocument(); }); }); it('validates correct email format', async () => { (api.checkEmail as jest.Mock).mockResolvedValue({ ok: true, data: { exists: true } }); (api.requestOTP as jest.Mock).mockResolvedValue({ ok: true }); render(); const emailInput = screen.getByLabelText('Email address'); const submitButton = screen.getByRole('button', { name: /get verification code/i }); fireEvent.change(emailInput, { target: { value: 'test@example.com' } }); fireEvent.click(submitButton); await waitFor(() => { expect(api.checkEmail).toHaveBeenCalledWith('test@example.com'); expect(api.requestOTP).toHaveBeenCalledWith('test@example.com'); }); }); it('navigates to verify-otp on successful OTP request', async () => { (api.checkEmail as jest.Mock).mockResolvedValue({ ok: true, data: { exists: true } }); (api.requestOTP as jest.Mock).mockResolvedValue({ ok: true }); (api.saveEmail as jest.Mock).mockResolvedValue(undefined); render(); const emailInput = screen.getByLabelText('Email address'); const form = screen.getByRole('button', { name: /get verification code/i }).closest('form'); fireEvent.change(emailInput, { target: { value: 'test@example.com' } }); fireEvent.submit(form!); await waitFor(() => { expect(mockPush).toHaveBeenCalledWith( expect.stringMatching(/\/verify-otp\?email=test(%40|@)example\.com/) ); }); }); it('shows loading state during OTP request', async () => { (api.checkEmail as jest.Mock).mockResolvedValue({ ok: true, data: { exists: true } }); (api.requestOTP as jest.Mock).mockImplementation(() => new Promise(() => {})); // Never resolves render(); const emailInput = screen.getByLabelText('Email address'); const submitButton = screen.getByRole('button', { name: /get verification code/i }); fireEvent.change(emailInput, { target: { value: 'test@example.com' } }); fireEvent.click(submitButton); await waitFor(() => { expect(screen.getByText('Sending code...')).toBeInTheDocument(); expect(submitButton).toBeDisabled(); }); }); it('shows error message on failed OTP request', async () => { (api.checkEmail as jest.Mock).mockResolvedValue({ ok: true, data: { exists: true } }); (api.requestOTP as jest.Mock).mockResolvedValue({ ok: false, error: { message: 'Failed to send OTP' }, }); render(); const emailInput = screen.getByLabelText('Email address'); const submitButton = screen.getByRole('button', { name: /get verification code/i }); fireEvent.change(emailInput, { target: { value: 'test@example.com' } }); fireEvent.click(submitButton); await waitFor(() => { expect(screen.getByText('Failed to send OTP')).toBeInTheDocument(); }); }); it('shows invite code field when toggled', () => { render(); const inviteToggle = screen.getByText('Have an invite code?'); fireEvent.click(inviteToggle); expect(screen.getByLabelText('Invite code (optional)')).toBeInTheDocument(); }); it('includes invite code in navigation params when provided', async () => { (api.checkEmail as jest.Mock).mockResolvedValue({ ok: true, data: { exists: false } }); (api.requestOTP as jest.Mock).mockResolvedValue({ ok: true }); (api.saveEmail as jest.Mock).mockResolvedValue(undefined); render(); const emailInput = screen.getByLabelText('Email address'); const inviteToggle = screen.getByText('Have an invite code?'); fireEvent.click(inviteToggle); const inviteInput = screen.getByLabelText('Invite code (optional)'); const submitButton = screen.getByRole('button', { name: /get verification code/i }); fireEvent.change(emailInput, { target: { value: 'new@example.com' } }); fireEvent.change(inviteInput, { target: { value: 'INVITE123' } }); fireEvent.click(submitButton); await waitFor(() => { expect(mockPush).toHaveBeenCalledWith( expect.stringContaining('inviteCode=INVITE123') ); }); }); it('handles network errors gracefully', async () => { (api.checkEmail as jest.Mock).mockRejectedValue(new Error('Network error')); render(); const emailInput = screen.getByLabelText('Email address'); const submitButton = screen.getByRole('button', { name: /get verification code/i }); fireEvent.change(emailInput, { target: { value: 'test@example.com' } }); fireEvent.click(submitButton); await waitFor(() => { expect(screen.getByText('Network error. Please check your connection.')).toBeInTheDocument(); }); }); it('trims and lowercases email before submission', async () => { (api.checkEmail as jest.Mock).mockResolvedValue({ ok: true, data: { exists: true } }); (api.requestOTP as jest.Mock).mockResolvedValue({ ok: true }); render(); const emailInput = screen.getByLabelText('Email address'); const submitButton = screen.getByRole('button', { name: /get verification code/i }); fireEvent.change(emailInput, { target: { value: ' TEST@EXAMPLE.COM ' } }); fireEvent.click(submitButton); await waitFor(() => { expect(api.checkEmail).toHaveBeenCalledWith('test@example.com'); expect(api.requestOTP).toHaveBeenCalledWith('test@example.com'); }); }); });