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();
+ });
+ });
+ });
+});