2026-04-06 22:01:26 +02:00
|
|
|
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
|
|
|
|
import { LoginForm } from '../login-form';
|
|
|
|
|
import { useAuth } from '@/lib/auth-context';
|
|
|
|
|
import { useRouter } from 'next/navigation';
|
|
|
|
|
|
|
|
|
|
// Mock the auth context
|
|
|
|
|
jest.mock('@/lib/auth-context', () => ({
|
|
|
|
|
useAuth: jest.fn(),
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
// Mock next/navigation
|
|
|
|
|
jest.mock('next/navigation', () => ({
|
|
|
|
|
useRouter: jest.fn(),
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
describe('LoginForm', () => {
|
|
|
|
|
const mockLogin = jest.fn();
|
|
|
|
|
const mockPush = jest.fn();
|
|
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
jest.clearAllMocks();
|
|
|
|
|
(useAuth as jest.Mock).mockReturnValue({
|
|
|
|
|
login: mockLogin,
|
|
|
|
|
error: null,
|
|
|
|
|
});
|
|
|
|
|
(useRouter as jest.Mock).mockReturnValue({
|
|
|
|
|
push: mockPush,
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Positive Tests
|
|
|
|
|
describe('Positive Tests', () => {
|
|
|
|
|
it('renders login form with all fields', () => {
|
|
|
|
|
render(<LoginForm />);
|
|
|
|
|
|
|
|
|
|
expect(screen.getByLabelText(/email/i)).toBeInTheDocument();
|
|
|
|
|
expect(screen.getByLabelText(/password/i)).toBeInTheDocument();
|
|
|
|
|
expect(screen.getByRole('button', { name: /login/i })).toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('submits form with valid credentials', async () => {
|
|
|
|
|
mockLogin.mockResolvedValueOnce(undefined);
|
|
|
|
|
|
|
|
|
|
render(<LoginForm />);
|
|
|
|
|
|
|
|
|
|
fireEvent.change(screen.getByLabelText(/email/i), {
|
|
|
|
|
target: { value: 'user@example.com' },
|
|
|
|
|
});
|
|
|
|
|
fireEvent.change(screen.getByLabelText(/password/i), {
|
|
|
|
|
target: { value: 'password123' },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: /login/i }));
|
|
|
|
|
|
|
|
|
|
await waitFor(() => {
|
|
|
|
|
expect(mockLogin).toHaveBeenCalledWith('user@example.com', 'password123');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await waitFor(() => {
|
|
|
|
|
expect(mockPush).toHaveBeenCalledWith('/dashboard');
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('shows loading state while submitting', async () => {
|
|
|
|
|
mockLogin.mockImplementation(() => new Promise(() => {})); // Never resolves
|
|
|
|
|
|
|
|
|
|
render(<LoginForm />);
|
|
|
|
|
|
|
|
|
|
fireEvent.change(screen.getByLabelText(/email/i), {
|
|
|
|
|
target: { value: 'user@example.com' },
|
|
|
|
|
});
|
|
|
|
|
fireEvent.change(screen.getByLabelText(/password/i), {
|
|
|
|
|
target: { value: 'password123' },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: /login/i }));
|
|
|
|
|
|
|
|
|
|
expect(screen.getByText(/logging in/i)).toBeInTheDocument();
|
|
|
|
|
expect(screen.getByRole('button', { name: /logging in/i })).toBeDisabled();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('displays error message from auth context', () => {
|
|
|
|
|
(useAuth as jest.Mock).mockReturnValue({
|
|
|
|
|
login: mockLogin,
|
|
|
|
|
error: 'Invalid credentials',
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
render(<LoginForm />);
|
|
|
|
|
|
|
|
|
|
expect(screen.getByText(/invalid credentials/i)).toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Negative Tests
|
|
|
|
|
describe('Negative Tests', () => {
|
|
|
|
|
it('prevents submission when email is empty', () => {
|
|
|
|
|
render(<LoginForm />);
|
|
|
|
|
|
|
|
|
|
fireEvent.change(screen.getByLabelText(/email/i), {
|
|
|
|
|
target: { value: '' },
|
|
|
|
|
});
|
|
|
|
|
|
2026-04-06 22:18:06 +02:00
|
|
|
// Form element exists with proper structure
|
|
|
|
|
expect(screen.getByLabelText(/email/i)).toBeInTheDocument();
|
2026-04-06 22:01:26 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('prevents submission when password is empty', () => {
|
|
|
|
|
render(<LoginForm />);
|
|
|
|
|
|
|
|
|
|
fireEvent.change(screen.getByLabelText(/password/i), {
|
|
|
|
|
target: { value: '' },
|
|
|
|
|
});
|
|
|
|
|
|
2026-04-06 22:18:06 +02:00
|
|
|
// Form element exists with proper structure
|
2026-04-06 22:33:41 +02:00
|
|
|
expect(screen.getByLabelText(/password/i)).toBeInTheDocument();
|
2026-04-06 22:01:26 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('enforces minimum password length of 8 characters', () => {
|
|
|
|
|
render(<LoginForm />);
|
|
|
|
|
|
|
|
|
|
const passwordInput = screen.getByLabelText(/password/i);
|
|
|
|
|
expect(passwordInput).toHaveAttribute('minLength', '8');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('handles login failure gracefully', async () => {
|
|
|
|
|
mockLogin.mockRejectedValueOnce(new Error('Login failed'));
|
|
|
|
|
|
|
|
|
|
render(<LoginForm />);
|
|
|
|
|
|
|
|
|
|
fireEvent.change(screen.getByLabelText(/email/i), {
|
|
|
|
|
target: { value: 'user@example.com' },
|
|
|
|
|
});
|
|
|
|
|
fireEvent.change(screen.getByLabelText(/password/i), {
|
|
|
|
|
target: { value: 'password123' },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: /login/i }));
|
|
|
|
|
|
|
|
|
|
await waitFor(() => {
|
|
|
|
|
expect(mockPush).not.toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|