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:
228
frontend/src/__tests__/auth-flow.integration.test.js
Normal file
228
frontend/src/__tests__/auth-flow.integration.test.js
Normal 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user