refactor: convert frontend from submodule to true monorepo

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>
This commit is contained in:
William Valentin
2025-11-01 11:01:06 -07:00
parent 223dbb14b7
commit 2df5a303ed
38 changed files with 25312 additions and 3 deletions

View File

@@ -0,0 +1,228 @@
import React from 'react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { BrowserRouter } from 'react-router-dom';
import App from '../App';
import AuthProvider from '../context/AuthContext';
// Mock react-toastify to avoid toast errors
jest.mock('react-toastify', () => ({
toast: {
success: jest.fn(),
error: jest.fn(),
info: jest.fn(),
},
ToastContainer: () => null,
}));
// Mock Leaflet map components to avoid rendering issues
jest.mock('react-leaflet', () => ({
MapContainer: ({ children }) => <div data-testid="map-container">{children}</div>,
TileLayer: () => <div>TileLayer</div>,
Marker: () => <div>Marker</div>,
Popup: ({ children }) => <div>{children}</div>,
useMap: () => ({
flyTo: jest.fn(),
}),
}));
jest.mock('react-leaflet-cluster', () => ({
default: ({ children }) => <div>{children}</div>,
}));
// Mock Socket.IO
jest.mock('socket.io-client', () => {
return jest.fn(() => ({
on: jest.fn(),
emit: jest.fn(),
off: jest.fn(),
disconnect: jest.fn(),
}));
});
describe('Authentication Flow Integration Tests', () => {
beforeEach(() => {
localStorage.clear();
});
const renderApp = () => {
return render(
<BrowserRouter>
<AuthProvider>
<App />
</AuthProvider>
</BrowserRouter>
);
};
describe('Registration Flow', () => {
it('should allow user to register and access protected routes', async () => {
renderApp();
// Should be on the login page initially (or map if not authenticated)
await waitFor(() => {
expect(screen.queryByRole('status')).not.toBeInTheDocument();
});
// Navigate to register page if there's a link
const registerLinks = screen.queryAllByText(/register/i);
if (registerLinks.length > 0) {
fireEvent.click(registerLinks[0]);
}
// Fill out registration form
await waitFor(() => {
const nameInput = screen.queryByPlaceholderText(/name/i);
if (nameInput) {
fireEvent.change(nameInput, { target: { value: 'Test User' } });
const emailInput = screen.getByPlaceholderText(/email/i);
fireEvent.change(emailInput, { target: { value: 'test@example.com' } });
const passwordInput = screen.getByPlaceholderText(/^password$/i);
fireEvent.change(passwordInput, { target: { value: 'password123' } });
const confirmPasswordInput = screen.getByPlaceholderText(/confirm password/i);
fireEvent.change(confirmPasswordInput, { target: { value: 'password123' } });
const submitButton = screen.getByRole('button', { name: /register/i });
fireEvent.click(submitButton);
}
});
});
});
describe('Login Flow', () => {
it('should allow user to login and access protected routes', async () => {
renderApp();
// Wait for initial loading
await waitFor(() => {
expect(screen.queryByRole('status')).not.toBeInTheDocument();
}, { timeout: 3000 });
// Look for login form
const emailInput = screen.queryByPlaceholderText(/email/i);
const passwordInput = screen.queryByPlaceholderText(/password/i);
if (emailInput && passwordInput) {
// Fill out login form
fireEvent.change(emailInput, { target: { value: 'test@example.com' } });
fireEvent.change(passwordInput, { target: { value: 'password123' } });
const loginButton = screen.getByRole('button', { name: /login/i });
fireEvent.click(loginButton);
// Wait for login to complete
await waitFor(() => {
// After successful login, should redirect or show authenticated content
expect(localStorage.getItem('token')).toBeDefined();
}, { timeout: 3000 });
}
});
it('should show error with invalid credentials', async () => {
renderApp();
await waitFor(() => {
expect(screen.queryByRole('status')).not.toBeInTheDocument();
});
const emailInput = screen.queryByPlaceholderText(/email/i);
const passwordInput = screen.queryByPlaceholderText(/password/i);
if (emailInput && passwordInput) {
fireEvent.change(emailInput, { target: { value: 'wrong@example.com' } });
fireEvent.change(passwordInput, { target: { value: 'wrongpassword' } });
const loginButton = screen.getByRole('button', { name: /login/i });
fireEvent.click(loginButton);
// Wait for error handling
await waitFor(() => {
expect(localStorage.getItem('token')).toBeNull();
});
}
});
});
describe('Protected Routes', () => {
it('should redirect unauthenticated users from protected routes', async () => {
renderApp();
await waitFor(() => {
expect(screen.queryByRole('status')).not.toBeInTheDocument();
});
// Unauthenticated users should not have access to certain features
// This would depend on your routing configuration
expect(localStorage.getItem('token')).toBeNull();
});
});
describe('Logout Flow', () => {
it('should logout user and clear authentication state', async () => {
// Set a mock token
localStorage.setItem('token', 'mock-jwt-token');
renderApp();
await waitFor(() => {
expect(screen.queryByRole('status')).not.toBeInTheDocument();
});
// Look for logout button/link
const logoutButtons = screen.queryAllByText(/logout/i);
if (logoutButtons.length > 0) {
fireEvent.click(logoutButtons[0]);
await waitFor(() => {
expect(localStorage.getItem('token')).toBeNull();
});
}
});
});
describe('Token Persistence', () => {
it('should load user from token on app mount', async () => {
// Set a valid token
localStorage.setItem('token', 'mock-jwt-token');
renderApp();
// Should attempt to load user
await waitFor(() => {
expect(screen.queryByRole('status')).not.toBeInTheDocument();
});
});
it('should handle invalid token on app mount', async () => {
// Set an invalid token
localStorage.setItem('token', 'invalid-token');
renderApp();
await waitFor(() => {
expect(screen.queryByRole('status')).not.toBeInTheDocument();
});
// Token should be cleared if invalid
// This depends on the error handling in AuthContext
});
});
describe('Session Management', () => {
it('should maintain authentication across page navigation', async () => {
localStorage.setItem('token', 'mock-jwt-token');
renderApp();
await waitFor(() => {
expect(screen.queryByRole('status')).not.toBeInTheDocument();
});
// Token should persist
expect(localStorage.getItem('token')).toBe('mock-jwt-token');
});
});
});