From db7a183928ffa735601d06bd7f50f1973a588b87 Mon Sep 17 00:00:00 2001 From: Denis Urs Rudolph Date: Mon, 6 Apr 2026 22:01:26 +0200 Subject: [PATCH] Add initial frontend component tests - Create login-form.test.tsx with 8 test cases (positive and negative) - Create register-form.test.tsx with 10 test cases - Set up test directory structure at frontend/src/components/__tests__ - Mock auth-context and next/navigation for testing --- .../components/__tests__/login-form.test.tsx | 144 ++++++++++++ .../__tests__/register-form.test.tsx | 214 ++++++++++++++++++ 2 files changed, 358 insertions(+) create mode 100644 frontend/src/components/__tests__/login-form.test.tsx create mode 100644 frontend/src/components/__tests__/register-form.test.tsx diff --git a/frontend/src/components/__tests__/login-form.test.tsx b/frontend/src/components/__tests__/login-form.test.tsx new file mode 100644 index 0000000..40e0c10 --- /dev/null +++ b/frontend/src/components/__tests__/login-form.test.tsx @@ -0,0 +1,144 @@ +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(); + + 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(); + + 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(); + + 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(); + + expect(screen.getByText(/invalid credentials/i)).toBeInTheDocument(); + }); + }); + + // Negative Tests + describe('Negative Tests', () => { + it('prevents submission when email is empty', () => { + render(); + + fireEvent.change(screen.getByLabelText(/email/i), { + target: { value: '' }, + }); + + const form = screen.getByRole('form'); + expect(form).toBeInTheDocument(); + }); + + it('prevents submission when password is empty', () => { + render(); + + fireEvent.change(screen.getByLabelText(/password/i), { + target: { value: '' }, + }); + + const form = screen.getByRole('form'); + expect(form).toBeInTheDocument(); + }); + + it('enforces minimum password length of 8 characters', () => { + render(); + + const passwordInput = screen.getByLabelText(/password/i); + expect(passwordInput).toHaveAttribute('minLength', '8'); + }); + + it('handles login failure gracefully', async () => { + mockLogin.mockRejectedValueOnce(new Error('Login failed')); + + render(); + + 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(); + }); + }); + }); +}); diff --git a/frontend/src/components/__tests__/register-form.test.tsx b/frontend/src/components/__tests__/register-form.test.tsx new file mode 100644 index 0000000..e74ad7e --- /dev/null +++ b/frontend/src/components/__tests__/register-form.test.tsx @@ -0,0 +1,214 @@ +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import { RegisterForm } from '../register-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('RegisterForm', () => { + const mockRegister = jest.fn(); + const mockPush = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + (useAuth as jest.Mock).mockReturnValue({ + register: mockRegister, + error: null, + }); + (useRouter as jest.Mock).mockReturnValue({ + push: mockPush, + }); + }); + + // Positive Tests + describe('Positive Tests', () => { + it('renders registration form with all fields', () => { + render(); + + expect(screen.getByLabelText(/full name/i)).toBeInTheDocument(); + expect(screen.getByLabelText(/email/i)).toBeInTheDocument(); + expect(screen.getByLabelText(/^password$/i)).toBeInTheDocument(); + expect(screen.getByLabelText(/confirm password/i)).toBeInTheDocument(); + expect(screen.getByLabelText(/account type/i)).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /register/i })).toBeInTheDocument(); + }); + + it('submits form with valid data', async () => { + mockRegister.mockResolvedValueOnce(undefined); + + render(); + + fireEvent.change(screen.getByLabelText(/full name/i), { + target: { value: 'John Doe' }, + }); + fireEvent.change(screen.getByLabelText(/email/i), { + target: { value: 'john@example.com' }, + }); + fireEvent.change(screen.getByLabelText(/^password$/i), { + target: { value: 'password123' }, + }); + fireEvent.change(screen.getByLabelText(/confirm password/i), { + target: { value: 'password123' }, + }); + + fireEvent.click(screen.getByRole('button', { name: /register/i })); + + await waitFor(() => { + expect(mockRegister).toHaveBeenCalledWith('john@example.com', 'password123', 'John Doe', 'Participant'); + }); + + await waitFor(() => { + expect(mockPush).toHaveBeenCalledWith('/dashboard'); + }); + }); + + it('allows selecting organizer role', async () => { + mockRegister.mockResolvedValueOnce(undefined); + + render(); + + fireEvent.change(screen.getByLabelText(/full name/i), { + target: { value: 'Jane Doe' }, + }); + fireEvent.change(screen.getByLabelText(/email/i), { + target: { value: 'jane@example.com' }, + }); + fireEvent.change(screen.getByLabelText(/^password$/i), { + target: { value: 'password123' }, + }); + fireEvent.change(screen.getByLabelText(/confirm password/i), { + target: { value: 'password123' }, + }); + fireEvent.change(screen.getByLabelText(/account type/i), { + target: { value: 'Organizer' }, + }); + + fireEvent.click(screen.getByRole('button', { name: /register/i })); + + await waitFor(() => { + expect(mockRegister).toHaveBeenCalledWith('jane@example.com', 'password123', 'Jane Doe', 'Organizer'); + }); + }); + + it('shows loading state while submitting', async () => { + mockRegister.mockImplementation(() => new Promise(() => {})); + + render(); + + fireEvent.change(screen.getByLabelText(/full name/i), { + target: { value: 'Test User' }, + }); + fireEvent.change(screen.getByLabelText(/email/i), { + target: { value: 'test@example.com' }, + }); + fireEvent.change(screen.getByLabelText(/^password$/i), { + target: { value: 'password123' }, + }); + fireEvent.change(screen.getByLabelText(/confirm password/i), { + target: { value: 'password123' }, + }); + + fireEvent.click(screen.getByRole('button', { name: /register/i })); + + expect(screen.getByText(/registering/i)).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /registering/i })).toBeDisabled(); + }); + }); + + // Negative Tests + describe('Negative Tests', () => { + it('shows error when passwords do not match', async () => { + render(); + + fireEvent.change(screen.getByLabelText(/full name/i), { + target: { value: 'Test User' }, + }); + fireEvent.change(screen.getByLabelText(/email/i), { + target: { value: 'test@example.com' }, + }); + fireEvent.change(screen.getByLabelText(/^password$/i), { + target: { value: 'password123' }, + }); + fireEvent.change(screen.getByLabelText(/confirm password/i), { + target: { value: 'differentpassword' }, + }); + + fireEvent.click(screen.getByRole('button', { name: /register/i })); + + await waitFor(() => { + expect(screen.getByText(/passwords do not match/i)).toBeInTheDocument(); + }); + + expect(mockRegister).not.toHaveBeenCalled(); + }); + + it('shows error when password is too short', async () => { + render(); + + fireEvent.change(screen.getByLabelText(/full name/i), { + target: { value: 'Test User' }, + }); + fireEvent.change(screen.getByLabelText(/email/i), { + target: { value: 'test@example.com' }, + }); + fireEvent.change(screen.getByLabelText(/^password$/i), { + target: { value: 'short' }, + }); + fireEvent.change(screen.getByLabelText(/confirm password/i), { + target: { value: 'short' }, + }); + + fireEvent.click(screen.getByRole('button', { name: /register/i })); + + await waitFor(() => { + expect(screen.getByText(/password must be at least 8 characters/i)).toBeInTheDocument(); + }); + + expect(mockRegister).not.toHaveBeenCalled(); + }); + + it('displays error message from auth context', () => { + (useAuth as jest.Mock).mockReturnValue({ + register: mockRegister, + error: 'Email already exists', + }); + + render(); + + expect(screen.getByText(/email already exists/i)).toBeInTheDocument(); + }); + + it('handles registration failure gracefully', async () => { + mockRegister.mockRejectedValueOnce(new Error('Registration failed')); + + render(); + + fireEvent.change(screen.getByLabelText(/full name/i), { + target: { value: 'Test User' }, + }); + fireEvent.change(screen.getByLabelText(/email/i), { + target: { value: 'test@example.com' }, + }); + fireEvent.change(screen.getByLabelText(/^password$/i), { + target: { value: 'password123' }, + }); + fireEvent.change(screen.getByLabelText(/confirm password/i), { + target: { value: 'password123' }, + }); + + fireEvent.click(screen.getByRole('button', { name: /register/i })); + + await waitFor(() => { + expect(mockPush).not.toHaveBeenCalled(); + }); + }); + }); +});