import { describe, it, expect, vi, beforeEach } from 'vitest'; import { render, screen, waitFor, act } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { MemoryRouter } from 'react-router-dom'; import { ResetPasswordPage } from './ResetPasswordPage'; import { usePasswordReset } from '../hooks/usePasswordReset'; // Mock dependencies vi.mock('../hooks/usePasswordReset', () => ({ usePasswordReset: vi.fn(), })); const mockNavigate = vi.fn(); vi.mock('react-router-dom', async () => { const actual = await vi.importActual('react-router-dom'); return { ...actual, useNavigate: () => mockNavigate, }; }); const wrapper = ({ children }: { children: React.ReactNode }) => ( {children} ); describe('ResetPasswordPage', () => { const mockHandleReset = vi.fn(); const mockUsePasswordReset = { handleReset: mockHandleReset, loading: false, error: null, success: false, }; beforeEach(() => { vi.clearAllMocks(); vi.mocked(usePasswordReset).mockReturnValue(mockUsePasswordReset); mockNavigate.mockClear(); }); it('should render reset password form when token is present', async () => { const wrapperWithToken = ({ children }: { children: React.ReactNode }) => ( {children} ); render(, { wrapper: wrapperWithToken }); // Wait for the token to be extracted and form to render await waitFor(() => { expect(screen.getByLabelText('Nouveau mot de passe')).toBeInTheDocument(); }); expect( screen.getByText('Entrez votre nouveau mot de passe'), ).toBeInTheDocument(); expect( screen.getByLabelText('Confirmer le mot de passe'), ).toBeInTheDocument(); expect( screen.getByRole('button', { name: 'Réinitialiser le mot de passe' }), ).toBeInTheDocument(); }); it('should display error message when token is missing', () => { render(, { wrapper }); expect( screen.getByText('Lien de réinitialisation invalide'), ).toBeInTheDocument(); // Check for the subtitle which is unique expect( screen.getByText('Le lien de réinitialisation est invalide ou a expiré'), ).toBeInTheDocument(); }); it('should extract token from URL', async () => { const wrapperWithToken = ({ children }: { children: React.ReactNode }) => ( {children} ); render(, { wrapper: wrapperWithToken }); // Wait for the token to be extracted and form to render // The useEffect will extract the token and update state await waitFor( () => { // Check for the form inputs instead of the title (which appears twice) expect( screen.getByLabelText('Nouveau mot de passe'), ).toBeInTheDocument(); }, { timeout: 3000 }, ); // Verify the form is rendered (not the error message) expect( screen.queryByText('Lien de réinitialisation invalide'), ).not.toBeInTheDocument(); expect( screen.getByText('Entrez votre nouveau mot de passe'), ).toBeInTheDocument(); }); it('should validate required password field', async () => { const wrapperWithToken = ({ children }: { children: React.ReactNode }) => ( {children} ); render(, { wrapper: wrapperWithToken }); const form = screen .getByRole('button', { name: 'Réinitialiser le mot de passe' }) .closest('form'); expect(form).toBeInTheDocument(); // Try to submit with empty password await act(async () => { if (form) { form.requestSubmit(); } }); // Wait a bit for validation to run await new Promise((resolve) => setTimeout(resolve, 100)); // The validation should prevent handleReset from being called expect(mockHandleReset).not.toHaveBeenCalled(); }); it('should validate password minimum length', async () => { const user = userEvent.setup(); const wrapperWithToken = ({ children }: { children: React.ReactNode }) => ( {children} ); render(, { wrapper: wrapperWithToken }); const passwordInput = screen.getByLabelText('Nouveau mot de passe'); await act(async () => { await user.type(passwordInput, 'short'); await user.tab(); }); await waitFor(() => { expect( screen.getByText('Le mot de passe doit contenir au moins 8 caractères'), ).toBeInTheDocument(); }); }); it('should validate password confirmation match', async () => { const user = userEvent.setup(); const wrapperWithToken = ({ children }: { children: React.ReactNode }) => ( {children} ); render(, { wrapper: wrapperWithToken }); const passwordInput = screen.getByLabelText('Nouveau mot de passe'); const confirmPasswordInput = screen.getByLabelText( 'Confirmer le mot de passe', ); await act(async () => { await user.type(passwordInput, 'password123'); await user.type(confirmPasswordInput, 'password456'); await user.tab(); }); await waitFor(() => { expect( screen.getByText('Les mots de passe ne correspondent pas'), ).toBeInTheDocument(); }); }); it('should clear error when user starts typing', async () => { const user = userEvent.setup(); const wrapperWithToken = ({ children }: { children: React.ReactNode }) => ( {children} ); render(, { wrapper: wrapperWithToken }); const passwordInput = screen.getByLabelText('Nouveau mot de passe'); // Trigger validation error await act(async () => { await user.click(passwordInput); await user.tab(); }); await waitFor(() => { expect(screen.getByText('Mot de passe requis')).toBeInTheDocument(); }); // Clear error by typing await act(async () => { await user.type(passwordInput, 'password123'); }); await waitFor(() => { expect(screen.queryByText('Mot de passe requis')).not.toBeInTheDocument(); }); }); it('should call handleReset with form data on valid submission', async () => { const user = userEvent.setup(); mockHandleReset.mockResolvedValue(undefined); const wrapperWithToken = ({ children }: { children: React.ReactNode }) => ( {children} ); render(, { wrapper: wrapperWithToken }); await act(async () => { await user.type( screen.getByLabelText('Nouveau mot de passe'), 'newpassword123', ); await user.type( screen.getByLabelText('Confirmer le mot de passe'), 'newpassword123', ); }); const form = screen .getByRole('button', { name: 'Réinitialiser le mot de passe' }) .closest('form'); await act(async () => { if (form) { await userEvent.click( screen.getByRole('button', { name: 'Réinitialiser le mot de passe' }), ); } }); await waitFor(() => { expect(mockHandleReset).toHaveBeenCalledWith({ token: 'test-token-123', password: 'newpassword123', confirmPassword: 'newpassword123', }); }); }); it('should display error message when reset fails', () => { const error = new Error('Token invalide ou expiré'); vi.mocked(usePasswordReset).mockReturnValue({ ...mockUsePasswordReset, error, }); const wrapperWithToken = ({ children }: { children: React.ReactNode }) => ( {children} ); render(, { wrapper: wrapperWithToken }); expect(screen.getByText('Token invalide ou expiré')).toBeInTheDocument(); }); it('should show loading state on button', () => { vi.mocked(usePasswordReset).mockReturnValue({ ...mockUsePasswordReset, loading: true, }); const wrapperWithToken = ({ children }: { children: React.ReactNode }) => ( {children} ); render(, { wrapper: wrapperWithToken }); // Le texte "Chargement..." est dans un span avec aria-hidden, donc on cherche le bouton par son aria-busy const submitButton = screen.getByRole('button', { name: 'Chargement en cours', }); expect(submitButton).toBeDisabled(); expect(submitButton).toHaveAttribute('aria-busy', 'true'); expect(screen.getByText('Chargement...')).toBeInTheDocument(); }); it('should display success message after successful reset', () => { vi.mocked(usePasswordReset).mockReturnValue({ ...mockUsePasswordReset, success: true, }); const wrapperWithToken = ({ children }: { children: React.ReactNode }) => ( {children} ); render(, { wrapper: wrapperWithToken }); expect(screen.getByText('Mot de passe réinitialisé')).toBeInTheDocument(); expect( screen.getByText(/Votre mot de passe a été modifié avec succès/), ).toBeInTheDocument(); expect( screen.getByText(/Vous allez être redirigé vers la page de connexion/), ).toBeInTheDocument(); }); it('should redirect to login after successful reset', async () => { vi.mocked(usePasswordReset).mockReturnValue({ ...mockUsePasswordReset, success: true, }); const wrapperWithToken = ({ children }: { children: React.ReactNode }) => ( {children} ); render(, { wrapper: wrapperWithToken }); // Wait for success message to appear await waitFor(() => { expect(screen.getByText('Mot de passe réinitialisé')).toBeInTheDocument(); }); // Wait for the redirect timer (3 seconds) await waitFor( () => { expect(mockNavigate).toHaveBeenCalledWith('/login', { replace: true }); }, { timeout: 4000 }, ); }); it('should display password strength indicator', () => { const wrapperWithToken = ({ children }: { children: React.ReactNode }) => ( {children} ); render(, { wrapper: wrapperWithToken }); // PasswordStrengthIndicator should be present const passwordInput = screen.getByLabelText('Nouveau mot de passe'); expect(passwordInput).toBeInTheDocument(); }); it('should display footer links', () => { const wrapperWithToken = ({ children }: { children: React.ReactNode }) => ( {children} ); render(, { wrapper: wrapperWithToken }); const loginLink = screen.getByText('Retour à la connexion'); expect(loginLink).toBeInTheDocument(); expect(loginLink.closest('a')).toHaveAttribute('href', '/login'); }); });