import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { MemoryRouter, Routes, Route } from 'react-router-dom'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { LoginPage } from '../pages/LoginPage'; import { RegisterPage } from '../pages/RegisterPage'; import { ForgotPasswordPage } from '../pages/ForgotPasswordPage'; import { ResetPasswordPage } from '../pages/ResetPasswordPage'; import { VerifyEmailPage } from '../pages/VerifyEmailPage'; // Mock authStore vi.mock('@/features/auth/store/authStore', () => ({ useAuthStore: Object.assign( vi.fn(() => ({ isAuthenticated: false, isLoading: false, user: null, login: vi.fn(), register: vi.fn(), clearError: vi.fn(), error: null, checkAuthStatus: vi.fn(), })), { getState: () => ({ isAuthenticated: false, user: null, }), }, ), })); // Mock useLogin - returns react-query mutation shape vi.mock('../hooks/useLogin', () => ({ useLogin: () => ({ mutate: vi.fn(), isPending: false, error: null, isSuccess: false, }), })); // Mock useRegister vi.mock('../hooks/useRegister', () => ({ useRegister: () => ({ mutate: vi.fn(), isPending: false, error: null, isSuccess: false, }), })); // Mock useUsernameAvailability vi.mock('../hooks/useUsernameAvailability', () => ({ useUsernameAvailability: () => ({ available: true, checking: false }), })); // Mock usePasswordReset vi.mock('../hooks/usePasswordReset', () => ({ usePasswordReset: () => ({ handleRequestReset: vi.fn(), handleReset: vi.fn(), loading: false, error: null, success: false, }), })); // Mock authApi for VerifyEmailPage vi.mock('@/services/api/auth', () => ({ authApi: { verifyEmail: vi.fn().mockResolvedValue({ message: 'Email verified' }), resendVerification: vi.fn().mockResolvedValue({ message: 'Email sent' }), checkUsername: vi.fn().mockResolvedValue({ available: true, username: 'test' }), }, })); // Mock useFormValidation (used by LoginForm and RegisterForm) vi.mock('@/hooks/useFormValidation', () => ({ useFormValidation: () => ({ validate: vi.fn(), errors: [], isValidating: false, }), })); // Mock useToast vi.mock('@/hooks/useToast', () => ({ useToast: () => ({ toast: vi.fn(), }), })); const mockNavigate = vi.fn(); vi.mock('react-router-dom', async () => { const actual = await vi.importActual('react-router-dom'); return { ...actual, useNavigate: () => mockNavigate, }; }); // Helper function to render with router and QueryClient const renderWithRouter = ( initialEntries: string[], path: string, component: React.ReactElement, ) => { const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false } }, }); return render( , ); }; describe('Auth Pages Integration Tests', () => { beforeEach(() => { vi.clearAllMocks(); mockNavigate.mockClear(); localStorage.clear(); }); afterEach(() => { localStorage.clear(); }); describe('Navigation between pages', () => { it('should navigate from login to register page', () => { renderWithRouter(['/login'], '/login', ); // LoginPage footer link: "Don't have an account? Sign up" const registerLink = screen.getByText(/sign up/i); expect(registerLink).toBeInTheDocument(); expect(registerLink.closest('a')).toHaveAttribute('href', '/register'); }); it('should navigate from login to forgot password page', () => { renderWithRouter(['/login'], '/login', ); const forgotPasswordLink = screen.getByText(/forgot password/i); expect(forgotPasswordLink).toBeInTheDocument(); expect(forgotPasswordLink.closest('a')).toHaveAttribute( 'href', '/forgot-password', ); }); it('should navigate from register to login page', () => { renderWithRouter(['/register'], '/register', ); const loginLink = screen.getByText(/se connecter/i); expect(loginLink).toBeInTheDocument(); expect(loginLink.closest('a')).toHaveAttribute('href', '/login'); }); it('should navigate from forgot password to login page', () => { renderWithRouter( ['/forgot-password'], '/forgot-password', , ); const loginLink = screen.getByText('Retour à la connexion'); expect(loginLink).toBeInTheDocument(); expect(loginLink.closest('a')).toHaveAttribute('href', '/login'); }); }); describe('Page rendering', () => { it('should render login page correctly', () => { renderWithRouter(['/login'], '/login', ); expect(screen.getByText('Welcome Back')).toBeInTheDocument(); expect(screen.getByLabelText('Email')).toBeInTheDocument(); expect(screen.getByLabelText('Password')).toBeInTheDocument(); expect( screen.getByRole('button', { name: 'Sign In' }), ).toBeInTheDocument(); }); it('should render register page correctly', () => { renderWithRouter(['/register'], '/register', ); expect(screen.getByText('Inscription')).toBeInTheDocument(); expect(screen.getByLabelText('Email')).toBeInTheDocument(); expect(screen.getByLabelText("Nom d'utilisateur")).toBeInTheDocument(); expect(screen.getByLabelText('Mot de passe')).toBeInTheDocument(); expect( screen.getByLabelText('Confirmer le mot de passe'), ).toBeInTheDocument(); }); it('should render forgot password page correctly', () => { renderWithRouter( ['/forgot-password'], '/forgot-password', , ); expect(screen.getByText('Mot de passe oublié')).toBeInTheDocument(); expect(screen.getByLabelText('Email')).toBeInTheDocument(); expect( screen.getByRole('button', { name: 'Envoyer le lien de réinitialisation', }), ).toBeInTheDocument(); }); it('should render reset password page correctly when token is present', async () => { renderWithRouter( ['/reset-password?token=test-token'], '/reset-password', , ); await waitFor( () => { expect( screen.getByLabelText('Nouveau mot de passe'), ).toBeInTheDocument(); }, { timeout: 3000 }, ); expect( screen.getByLabelText('Confirmer le mot de passe'), ).toBeInTheDocument(); }); it('should render reset password page error when token is missing', () => { renderWithRouter( ['/reset-password'], '/reset-password', , ); expect( screen.getByText('Lien de réinitialisation invalide'), ).toBeInTheDocument(); }); it('should render verify email page correctly when token is present', async () => { renderWithRouter( ['/verify-email?token=test-token'], '/verify-email', , ); await waitFor( () => { // VerifyEmailPage shows title "Vérification de l'email" initially, then changes expect( screen.getByText("Vérification de l'email").closest('h1') || screen.getByText('Email vérifié').closest('h1'), ).toBeInTheDocument(); }, { timeout: 2000 }, ); }); it('should render verify email page error when token is missing', () => { renderWithRouter(['/verify-email'], '/verify-email', ); expect(screen.getByText("Vérification de l'email")).toBeInTheDocument(); expect( screen.getByText('Lien de vérification invalide ou manquant'), ).toBeInTheDocument(); }); }); describe('Form validation integration', () => { it('should prevent form submission with invalid data', async () => { const user = userEvent.setup(); renderWithRouter(['/login'], '/login', ); const submitButton = screen.getByRole('button', { name: 'Sign In' }); const emailInput = screen.getByLabelText('Email'); // Try to submit without filling form await user.click(submitButton); // Form should still be visible (validation prevented submission) await waitFor( () => { expect(emailInput).toBeInTheDocument(); }, { timeout: 2000 }, ); }); it('should prevent registration form submission with invalid data', async () => { const user = userEvent.setup(); renderWithRouter(['/register'], '/register', ); const submitButton = screen.getByRole('button', { name: /s'inscrire/i }); const emailInput = screen.getByLabelText('Email'); // Try to submit without filling form await user.click(submitButton); // Form should still be visible (validation prevented submission) await waitFor( () => { expect(emailInput).toBeInTheDocument(); expect(submitButton).toBeInTheDocument(); }, { timeout: 2000 }, ); }); }); describe('Redirections', () => { it('should show login page for unauthenticated users', () => { renderWithRouter(['/login'], '/login', ); // Login form should be visible for unauthenticated users expect(screen.getByLabelText('Email')).toBeInTheDocument(); }); it('should show register page for unauthenticated users', () => { renderWithRouter(['/register'], '/register', ); // Register form should be visible for unauthenticated users expect(screen.getByLabelText('Email')).toBeInTheDocument(); }); }); describe('Password reset flow integration', () => { it('should render forgot password form and allow navigation', () => { renderWithRouter( ['/forgot-password'], '/forgot-password', , ); expect(screen.getByText('Mot de passe oublié')).toBeInTheDocument(); expect(screen.getByLabelText('Email')).toBeInTheDocument(); // Check navigation link const loginLink = screen.getByText('Retour à la connexion'); expect(loginLink).toBeInTheDocument(); expect(loginLink.closest('a')).toHaveAttribute('href', '/login'); }); it('should render reset password form when token is provided', async () => { renderWithRouter( ['/reset-password?token=valid-token'], '/reset-password', , ); await waitFor( () => { expect( screen.getByLabelText('Nouveau mot de passe'), ).toBeInTheDocument(); }, { timeout: 3000 }, ); expect( screen.getByLabelText('Confirmer le mot de passe'), ).toBeInTheDocument(); }); }); describe('Email verification flow integration', () => { it('should render verify email page with token', async () => { renderWithRouter( ['/verify-email?token=valid-token'], '/verify-email', , ); await waitFor( () => { // Initially shows "Vérification de l'email" while verifying const heading = document.querySelector('h1'); expect(heading).toBeInTheDocument(); }, { timeout: 2000 }, ); }); it('should show error when token is missing', () => { renderWithRouter(['/verify-email'], '/verify-email', ); expect( screen.getByText('Lien de vérification invalide ou manquant'), ).toBeInTheDocument(); }); }); describe('Complete Authentication Flow', () => { it('should complete full login flow with form interaction', async () => { const user = userEvent.setup(); renderWithRouter(['/login'], '/login', ); const emailInput = screen.getByLabelText('Email'); const passwordInput = screen.getByLabelText('Password'); const submitButton = screen.getByRole('button', { name: 'Sign In' }); await user.type(emailInput, 'test@example.com'); await user.type(passwordInput, 'password123'); expect(emailInput).toHaveValue('test@example.com'); expect(passwordInput).toHaveValue('password123'); expect(submitButton).toBeInTheDocument(); }); it('should complete full registration flow with form interaction', async () => { const user = userEvent.setup(); renderWithRouter(['/register'], '/register', ); const emailInput = screen.getByLabelText('Email'); const usernameInput = screen.getByLabelText("Nom d'utilisateur"); const passwordInput = screen.getByLabelText('Mot de passe'); const confirmPasswordInput = screen.getByLabelText( 'Confirmer le mot de passe', ); await user.type(emailInput, 'newuser@example.com'); await user.type(usernameInput, 'newuser'); await user.type(passwordInput, 'password123'); await user.type(confirmPasswordInput, 'password123'); expect(emailInput).toHaveValue('newuser@example.com'); expect(usernameInput).toHaveValue('newuser'); expect(passwordInput).toHaveValue('password123'); expect(confirmPasswordInput).toHaveValue('password123'); }); it('should complete forgot password flow with form interaction', async () => { const user = userEvent.setup(); renderWithRouter( ['/forgot-password'], '/forgot-password', , ); const emailInput = screen.getByLabelText('Email'); const submitButton = screen.getByRole('button', { name: 'Envoyer le lien de réinitialisation', }); await user.type(emailInput, 'test@example.com'); expect(emailInput).toHaveValue('test@example.com'); expect(submitButton).toBeInTheDocument(); }); it('should complete reset password flow with form interaction', async () => { const user = userEvent.setup(); renderWithRouter( ['/reset-password?token=valid-token'], '/reset-password', , ); await waitFor( () => { expect( screen.getByLabelText('Nouveau mot de passe'), ).toBeInTheDocument(); }, { timeout: 3000 }, ); const newPasswordInput = screen.getByLabelText('Nouveau mot de passe'); const confirmPasswordInput = screen.getByLabelText( 'Confirmer le mot de passe', ); await user.type(newPasswordInput, 'newpassword123'); await user.type(confirmPasswordInput, 'newpassword123'); expect(newPasswordInput).toHaveValue('newpassword123'); expect(confirmPasswordInput).toHaveValue('newpassword123'); }); it('should show validation errors on login form submission with empty fields', async () => { const user = userEvent.setup(); renderWithRouter(['/login'], '/login', ); const submitButton = screen.getByRole('button', { name: 'Sign In' }); await user.click(submitButton); // Wait for validation errors await waitFor( () => { const emailInput = screen.getByLabelText('Email'); expect(emailInput).toBeInTheDocument(); }, { timeout: 2000 }, ); }); it('should show validation errors on registration form submission with empty fields', async () => { const user = userEvent.setup(); renderWithRouter(['/register'], '/register', ); const submitButton = screen.getByRole('button', { name: /s'inscrire/i }); await user.click(submitButton); // Wait for validation errors await waitFor( () => { expect(submitButton).toBeInTheDocument(); const emailInput = screen.getByLabelText('Email'); expect(emailInput).toBeInTheDocument(); }, { timeout: 2000 }, ); }); it('should navigate through auth pages correctly', () => { // Test navigation from login to register renderWithRouter(['/login'], '/login', ); const registerLink = screen.getByText(/sign up/i); expect(registerLink).toBeInTheDocument(); expect(registerLink.closest('a')).toHaveAttribute('href', '/register'); }); it('should navigate from register to login', () => { // Test navigation from register to login renderWithRouter(['/register'], '/register', ); const loginLink = screen.getByText(/se connecter/i); expect(loginLink).toBeInTheDocument(); expect(loginLink.closest('a')).toHaveAttribute('href', '/login'); }); it('should handle password mismatch validation in reset password flow', async () => { const user = userEvent.setup(); renderWithRouter( ['/reset-password?token=valid-token'], '/reset-password', , ); await waitFor( () => { expect( screen.getByLabelText('Nouveau mot de passe'), ).toBeInTheDocument(); }, { timeout: 3000 }, ); const newPasswordInput = screen.getByLabelText('Nouveau mot de passe'); const confirmPasswordInput = screen.getByLabelText( 'Confirmer le mot de passe', ); const submitButton = screen.getByRole('button', { name: /réinitialiser/i, }); await user.type(newPasswordInput, 'password123'); await user.type(confirmPasswordInput, 'differentpassword'); // Form should show validation error on submit await user.click(submitButton); // Wait for any validation error to appear await waitFor( () => { expect(newPasswordInput).toBeInTheDocument(); }, { timeout: 2000 }, ); }); it('should handle remember me checkbox interaction', async () => { const user = userEvent.setup(); renderWithRouter(['/login'], '/login', ); const emailInput = screen.getByLabelText('Email'); // LoginPage uses English "Remember me" const rememberMeCheckbox = screen.queryByLabelText(/remember/i) || screen.queryByRole('checkbox', { name: /remember/i }); await user.type(emailInput, 'remember@example.com'); if (rememberMeCheckbox) { await user.click(rememberMeCheckbox); } expect(emailInput).toHaveValue('remember@example.com'); }); it('should complete email verification flow', async () => { renderWithRouter( ['/verify-email?token=valid-token'], '/verify-email', , ); await waitFor( () => { const heading = document.querySelector('h1'); expect(heading).toBeInTheDocument(); }, { timeout: 2000 }, ); }); }); });