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');
});
});
});