2df5a303ed
Convert frontend from Git submodule to a regular monorepo directory for simplified development workflow. Changes: - Remove frontend submodule tracking (mode 160000 gitlink) - Add all frontend source files directly to main repository - Remove frontend/.git directory - Update CLAUDE.md to clarify true monorepo structure - Update Frontend Architecture documentation (React Router v6, Socket.IO, Leaflet, ErrorBoundary) Benefits of Monorepo: - Single git clone for entire project - Unified commit history - Simpler CI/CD pipeline - Easier for new developers - No submodule sync issues - Atomic commits across frontend and backend Frontend Files Added: - All React components (MapView, ErrorBoundary, TaskList, SocialFeed, etc.) - Context providers (AuthContext, SocketContext) - Complete test suite with MSW - Dependencies and configuration files Branch Cleanup: - Using 'main' as default branch (develop deleted) - Frontend no longer has separate Git history 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
259 lines
7.9 KiB
JavaScript
259 lines
7.9 KiB
JavaScript
import React from 'react';
|
|
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
|
import { BrowserRouter } from 'react-router-dom';
|
|
import Login from '../Login';
|
|
import { AuthContext } from '../../context/AuthContext';
|
|
|
|
// Mock useNavigate
|
|
const mockedNavigate = jest.fn();
|
|
jest.mock('react-router-dom', () => ({
|
|
...jest.requireActual('react-router-dom'),
|
|
Navigate: ({ to }) => {
|
|
mockedNavigate(to);
|
|
return null;
|
|
},
|
|
}));
|
|
|
|
describe('Login Component', () => {
|
|
const mockLogin = jest.fn();
|
|
|
|
const mockAuthContext = {
|
|
auth: {
|
|
isAuthenticated: false,
|
|
loading: false,
|
|
user: null,
|
|
},
|
|
login: mockLogin,
|
|
};
|
|
|
|
const renderLogin = (contextValue = mockAuthContext) => {
|
|
return render(
|
|
<BrowserRouter>
|
|
<AuthContext.Provider value={contextValue}>
|
|
<Login />
|
|
</AuthContext.Provider>
|
|
</BrowserRouter>
|
|
);
|
|
};
|
|
|
|
beforeEach(() => {
|
|
mockLogin.mockClear();
|
|
mockedNavigate.mockClear();
|
|
});
|
|
|
|
describe('Rendering', () => {
|
|
it('should render login form', () => {
|
|
renderLogin();
|
|
|
|
expect(screen.getByRole('heading', { name: /login/i })).toBeInTheDocument();
|
|
expect(screen.getByPlaceholderText(/email/i)).toBeInTheDocument();
|
|
expect(screen.getByPlaceholderText(/password/i)).toBeInTheDocument();
|
|
expect(screen.getByRole('button', { name: /login/i })).toBeInTheDocument();
|
|
});
|
|
|
|
it('should render email input field', () => {
|
|
renderLogin();
|
|
|
|
const emailInput = screen.getByPlaceholderText(/email/i);
|
|
expect(emailInput).toBeInTheDocument();
|
|
expect(emailInput).toHaveAttribute('type', 'email');
|
|
expect(emailInput).toHaveAttribute('required');
|
|
});
|
|
|
|
it('should render password input field', () => {
|
|
renderLogin();
|
|
|
|
const passwordInput = screen.getByPlaceholderText(/password/i);
|
|
expect(passwordInput).toBeInTheDocument();
|
|
expect(passwordInput).toHaveAttribute('type', 'password');
|
|
expect(passwordInput).toHaveAttribute('required');
|
|
});
|
|
});
|
|
|
|
describe('Form Validation', () => {
|
|
it('should update email field on change', () => {
|
|
renderLogin();
|
|
|
|
const emailInput = screen.getByPlaceholderText(/email/i);
|
|
fireEvent.change(emailInput, { target: { value: 'test@example.com' } });
|
|
|
|
expect(emailInput).toHaveValue('test@example.com');
|
|
});
|
|
|
|
it('should update password field on change', () => {
|
|
renderLogin();
|
|
|
|
const passwordInput = screen.getByPlaceholderText(/password/i);
|
|
fireEvent.change(passwordInput, { target: { value: 'password123' } });
|
|
|
|
expect(passwordInput).toHaveValue('password123');
|
|
});
|
|
|
|
it('should have required fields', () => {
|
|
renderLogin();
|
|
|
|
const emailInput = screen.getByPlaceholderText(/email/i);
|
|
const passwordInput = screen.getByPlaceholderText(/password/i);
|
|
|
|
expect(emailInput).toBeRequired();
|
|
expect(passwordInput).toBeRequired();
|
|
});
|
|
});
|
|
|
|
describe('Form Submission', () => {
|
|
it('should call login function on form submit', async () => {
|
|
renderLogin();
|
|
|
|
const emailInput = screen.getByPlaceholderText(/email/i);
|
|
const passwordInput = screen.getByPlaceholderText(/password/i);
|
|
const submitButton = screen.getByRole('button', { name: /login/i });
|
|
|
|
fireEvent.change(emailInput, { target: { value: 'test@example.com' } });
|
|
fireEvent.change(passwordInput, { target: { value: 'password123' } });
|
|
fireEvent.click(submitButton);
|
|
|
|
await waitFor(() => {
|
|
expect(mockLogin).toHaveBeenCalledWith('test@example.com', 'password123');
|
|
});
|
|
});
|
|
|
|
it('should disable form fields during submission', async () => {
|
|
mockLogin.mockImplementation(() => new Promise(resolve => setTimeout(resolve, 100)));
|
|
|
|
renderLogin();
|
|
|
|
const emailInput = screen.getByPlaceholderText(/email/i);
|
|
const passwordInput = screen.getByPlaceholderText(/password/i);
|
|
const submitButton = screen.getByRole('button', { name: /login/i });
|
|
|
|
fireEvent.change(emailInput, { target: { value: 'test@example.com' } });
|
|
fireEvent.change(passwordInput, { target: { value: 'password123' } });
|
|
fireEvent.click(submitButton);
|
|
|
|
await waitFor(() => {
|
|
expect(emailInput).toBeDisabled();
|
|
expect(passwordInput).toBeDisabled();
|
|
expect(submitButton).toBeDisabled();
|
|
});
|
|
});
|
|
|
|
it('should show loading state during submission', async () => {
|
|
mockLogin.mockImplementation(() => new Promise(resolve => setTimeout(resolve, 100)));
|
|
|
|
renderLogin();
|
|
|
|
const emailInput = screen.getByPlaceholderText(/email/i);
|
|
const passwordInput = screen.getByPlaceholderText(/password/i);
|
|
const submitButton = screen.getByRole('button', { name: /login/i });
|
|
|
|
fireEvent.change(emailInput, { target: { value: 'test@example.com' } });
|
|
fireEvent.change(passwordInput, { target: { value: 'password123' } });
|
|
fireEvent.click(submitButton);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText(/logging in/i)).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it('should handle login errors gracefully', async () => {
|
|
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
|
|
mockLogin.mockRejectedValue(new Error('Login failed'));
|
|
|
|
renderLogin();
|
|
|
|
const emailInput = screen.getByPlaceholderText(/email/i);
|
|
const passwordInput = screen.getByPlaceholderText(/password/i);
|
|
const submitButton = screen.getByRole('button', { name: /login/i });
|
|
|
|
fireEvent.change(emailInput, { target: { value: 'test@example.com' } });
|
|
fireEvent.change(passwordInput, { target: { value: 'wrong' } });
|
|
fireEvent.click(submitButton);
|
|
|
|
await waitFor(() => {
|
|
expect(consoleErrorSpy).toHaveBeenCalled();
|
|
});
|
|
|
|
consoleErrorSpy.mockRestore();
|
|
});
|
|
});
|
|
|
|
describe('Authentication State', () => {
|
|
it('should redirect to /map when already authenticated', () => {
|
|
const authenticatedContext = {
|
|
auth: {
|
|
isAuthenticated: true,
|
|
loading: false,
|
|
user: { name: 'Test User' },
|
|
},
|
|
login: mockLogin,
|
|
};
|
|
|
|
renderLogin(authenticatedContext);
|
|
|
|
expect(mockedNavigate).toHaveBeenCalledWith('/map');
|
|
});
|
|
|
|
it('should show loading spinner when auth is loading', () => {
|
|
const loadingContext = {
|
|
auth: {
|
|
isAuthenticated: false,
|
|
loading: true,
|
|
user: null,
|
|
},
|
|
login: mockLogin,
|
|
};
|
|
|
|
renderLogin(loadingContext);
|
|
|
|
expect(screen.getByRole('status')).toBeInTheDocument();
|
|
expect(screen.getByText(/loading/i)).toBeInTheDocument();
|
|
});
|
|
|
|
it('should not show form when auth is loading', () => {
|
|
const loadingContext = {
|
|
auth: {
|
|
isAuthenticated: false,
|
|
loading: true,
|
|
user: null,
|
|
},
|
|
login: mockLogin,
|
|
};
|
|
|
|
renderLogin(loadingContext);
|
|
|
|
expect(screen.queryByPlaceholderText(/email/i)).not.toBeInTheDocument();
|
|
expect(screen.queryByPlaceholderText(/password/i)).not.toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe('Accessibility', () => {
|
|
it('should have accessible form elements', () => {
|
|
renderLogin();
|
|
|
|
const emailInput = screen.getByPlaceholderText(/email/i);
|
|
const passwordInput = screen.getByPlaceholderText(/password/i);
|
|
|
|
expect(emailInput).toHaveAttribute('name', 'email');
|
|
expect(passwordInput).toHaveAttribute('name', 'password');
|
|
});
|
|
|
|
it('should have accessible button', () => {
|
|
renderLogin();
|
|
|
|
const submitButton = screen.getByRole('button', { name: /login/i });
|
|
expect(submitButton).toHaveAttribute('type', 'submit');
|
|
});
|
|
});
|
|
|
|
describe('Empty Form Submission', () => {
|
|
it('should not submit with empty fields', () => {
|
|
renderLogin();
|
|
|
|
const submitButton = screen.getByRole('button', { name: /login/i });
|
|
fireEvent.click(submitButton);
|
|
|
|
expect(mockLogin).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
});
|