import { describe, it, expect, vi, beforeEach, afterEach } 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 { VerifyEmailPage } from './VerifyEmailPage'; import { authApi } from '@/services/api/auth'; // Mock authApi vi.mock('@/services/api/auth', () => ({ authApi: { verifyEmail: vi.fn(), resendVerification: 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('VerifyEmailPage', () => { beforeEach(() => { vi.clearAllMocks(); mockNavigate.mockClear(); localStorage.clear(); }); afterEach(() => { vi.clearAllTimers(); }); it('should render verifying state when token is present', async () => { vi.mocked(authApi.verifyEmail).mockResolvedValue({ message: 'Email verified', }); const wrapperWithToken = ({ children }: { children: React.ReactNode }) => ( {children} ); render(, { wrapper: wrapperWithToken }); // Should show verifying state initially await waitFor(() => { const heading = document.querySelector('h1'); expect(heading).toBeInTheDocument(); }); }); it('should display error message when token is missing', () => { render(, { wrapper }); expect(screen.getByText("Vérification de l'email")).toBeInTheDocument(); expect( screen.getByText('Lien de vérification invalide ou manquant'), ).toBeInTheDocument(); }); it('should extract token from URL and verify email', async () => { vi.mocked(authApi.verifyEmail).mockResolvedValue({ message: 'Email verified', }); const wrapperWithToken = ({ children }: { children: React.ReactNode }) => ( {children} ); render(, { wrapper: wrapperWithToken }); // Wait for verification to complete await waitFor( () => { expect(authApi.verifyEmail).toHaveBeenCalledWith({ token: 'abc123' }); }, { timeout: 3000 }, ); }); it('should display success message after successful verification', async () => { vi.mocked(authApi.verifyEmail).mockResolvedValue({ message: 'Email verified', }); const wrapperWithToken = ({ children }: { children: React.ReactNode }) => ( {children} ); render(, { wrapper: wrapperWithToken }); // Wait for success message await waitFor( () => { expect(screen.getByText('Email vérifié')).toBeInTheDocument(); }, { timeout: 2000 }, ); }); it('should display error message when verification fails', async () => { const error = { message: 'Token invalide ou expiré' }; vi.mocked(authApi.verifyEmail).mockRejectedValue(error); const wrapperWithToken = ({ children }: { children: React.ReactNode }) => ( {children} ); render(, { wrapper: wrapperWithToken }); // Wait for error message await waitFor( () => { expect( screen.getByText('Token invalide ou expiré'), ).toBeInTheDocument(); }, { timeout: 3000 }, ); }); it('should redirect to login after successful verification', async () => { vi.mocked(authApi.verifyEmail).mockResolvedValue({ message: 'Email verified', }); const wrapperWithToken = ({ children }: { children: React.ReactNode }) => ( {children} ); render(, { wrapper: wrapperWithToken }); // Wait for success await waitFor(() => { expect(screen.getByText('Email vérifié')).toBeInTheDocument(); }); // Wait for the redirect timer (3 seconds) await waitFor( () => { expect(mockNavigate).toHaveBeenCalledWith('/login', { replace: true }); }, { timeout: 4000 }, ); }); it('should display retry button when verification fails', async () => { const error = { message: 'Verification failed' }; vi.mocked(authApi.verifyEmail).mockRejectedValue(error); const wrapperWithToken = ({ children }: { children: React.ReactNode }) => ( {children} ); render(, { wrapper: wrapperWithToken }); // Wait for error state await waitFor( () => { expect(screen.getByText('Réessayer')).toBeInTheDocument(); }, { timeout: 3000 }, ); }); it('should call verifyEmail when retry button is clicked', async () => { const user = userEvent.setup(); const error = { message: 'Verification failed' }; vi.mocked(authApi.verifyEmail) .mockRejectedValueOnce(error) .mockResolvedValueOnce({ message: 'Email verified' }); const wrapperWithToken = ({ children }: { children: React.ReactNode }) => ( {children} ); render(, { wrapper: wrapperWithToken }); // Wait for error state await waitFor(() => { expect(screen.getByText('Réessayer')).toBeInTheDocument(); }); // Click retry button const retryButton = screen.getByText('Réessayer'); await act(async () => { await user.click(retryButton); }); // Should call verifyEmail again await waitFor(() => { expect(authApi.verifyEmail).toHaveBeenCalledTimes(2); }); }); it('should display resend email button', async () => { const error = { message: 'Verification failed' }; vi.mocked(authApi.verifyEmail).mockRejectedValue(error); const wrapperWithToken = ({ children }: { children: React.ReactNode }) => ( {children} ); render(, { wrapper: wrapperWithToken }); // Wait for error state await waitFor( () => { expect( screen.getByText(/Renvoyer l'email de vérification/), ).toBeInTheDocument(); }, { timeout: 3000 }, ); }); it('should call resendVerification when resend button is clicked', async () => { const user = userEvent.setup(); const error = { message: 'Verification failed' }; vi.mocked(authApi.verifyEmail).mockRejectedValue(error); vi.mocked(authApi.resendVerification).mockResolvedValue({ message: 'Email sent', }); localStorage.setItem('pendingVerificationEmail', 'test@example.com'); const wrapperWithToken = ({ children }: { children: React.ReactNode }) => ( {children} ); render(, { wrapper: wrapperWithToken }); // Wait for error state await waitFor( () => { expect( screen.getByText(/Renvoyer l'email de vérification/), ).toBeInTheDocument(); }, { timeout: 3000 }, ); // Click resend button const resendButton = screen.getByText(/Renvoyer l'email de vérification/); await act(async () => { await user.click(resendButton); }); // Should call resendVerification await waitFor( () => { expect(authApi.resendVerification).toHaveBeenCalledWith({ email: 'test@example.com', }); }, { timeout: 3000 }, ); }); it('should set cooldown after resending email', async () => { const user = userEvent.setup(); const error = { message: 'Verification failed' }; vi.mocked(authApi.verifyEmail).mockRejectedValue(error); vi.mocked(authApi.resendVerification).mockResolvedValue({ message: 'Email sent', }); localStorage.setItem('pendingVerificationEmail', 'test@example.com'); const wrapperWithToken = ({ children }: { children: React.ReactNode }) => ( {children} ); render(, { wrapper: wrapperWithToken }); // Wait for error state await waitFor( () => { expect( screen.getByText(/Renvoyer l'email de vérification/), ).toBeInTheDocument(); }, { timeout: 2000 }, ); // Click resend button const resendButton = screen.getByText(/Renvoyer l'email de vérification/); await act(async () => { await user.click(resendButton); }); // Verify that resendVerification was called await waitFor( () => { expect(authApi.resendVerification).toHaveBeenCalledWith({ email: 'test@example.com', }); }, { timeout: 2000 }, ); }); it('should handle resend when email is not found', async () => { const user = userEvent.setup(); const error = { message: 'Verification failed' }; vi.mocked(authApi.verifyEmail).mockRejectedValue(error); // No email in localStorage localStorage.clear(); const wrapperWithToken = ({ children }: { children: React.ReactNode }) => ( {children} ); render(, { wrapper: wrapperWithToken }); // Wait for error state await waitFor( () => { expect( screen.getByText(/Renvoyer l'email de vérification/), ).toBeInTheDocument(); }, { timeout: 2000 }, ); // Click resend button const resendButton = screen.getByText(/Renvoyer l'email de vérification/); await act(async () => { await user.click(resendButton); }); // resendVerification should NOT be called since email is missing await new Promise((resolve) => setTimeout(resolve, 200)); expect(authApi.resendVerification).not.toHaveBeenCalled(); }); it('should display footer links', () => { render(, { wrapper }); const loginLink = screen.getByText('Retour à la connexion'); expect(loginLink).toBeInTheDocument(); expect(loginLink.closest('a')).toHaveAttribute('href', '/login'); }); });