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 { verifyEmail, resendVerificationEmail } from '../services/authService'; // Mock dependencies vi.mock('../services/authService', () => ({ verifyEmail: vi.fn(), resendVerificationEmail: 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(verifyEmail).mockResolvedValue(undefined); const wrapperWithToken = ({ children }: { children: React.ReactNode }) => ( {children} ); render(, { wrapper: wrapperWithToken }); // Should show verifying state initially await waitFor(() => { expect(screen.getByText("Vérification de l'email")).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(verifyEmail).mockResolvedValue(undefined); const wrapperWithToken = ({ children }: { children: React.ReactNode }) => ( {children} ); render(, { wrapper: wrapperWithToken }); // Wait for verification to complete await waitFor( () => { expect(verifyEmail).toHaveBeenCalledWith('abc123'); }, { timeout: 3000 }, ); }); it('should display success message after successful verification', async () => { vi.mocked(verifyEmail).mockResolvedValue(undefined); const wrapperWithToken = ({ children }: { children: React.ReactNode }) => ( {children} ); render(, { wrapper: wrapperWithToken }); // Wait for verifyEmail to be called await waitFor( () => { expect(verifyEmail).toHaveBeenCalled(); }, { timeout: 2000 }, ); // 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(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.useFakeTimers(); vi.mocked(verifyEmail).mockResolvedValue(undefined); const wrapperWithToken = ({ children }: { children: React.ReactNode }) => ( {children} ); render(, { wrapper: wrapperWithToken }); // Wait for success await waitFor(() => { expect(screen.getByText('Email vérifié')).toBeInTheDocument(); }); // Fast-forward the timer (3 seconds) await act(async () => { vi.advanceTimersByTime(3000); }); await waitFor(() => { expect(mockNavigate).toHaveBeenCalledWith('/login', { replace: true }); }); vi.useRealTimers(); }); it('should display retry button when verification fails', async () => { const error = { message: 'Verification failed' }; vi.mocked(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(verifyEmail) .mockRejectedValueOnce(error) .mockResolvedValueOnce(undefined); 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(verifyEmail).toHaveBeenCalledTimes(2); }); }); it('should display resend email button', async () => { const error = { message: 'Verification failed' }; vi.mocked(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 resendVerificationEmail when resend button is clicked', async () => { const user = userEvent.setup(); const error = { message: 'Verification failed' }; vi.mocked(verifyEmail).mockRejectedValue(error); vi.mocked(resendVerificationEmail).mockResolvedValue(undefined); 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 resendVerificationEmail await waitFor( () => { expect(resendVerificationEmail).toHaveBeenCalledWith( 'test@example.com', ); }, { timeout: 3000 }, ); }); it('should set cooldown after resending email', async () => { const user = userEvent.setup(); const error = { message: 'Verification failed' }; vi.mocked(verifyEmail).mockRejectedValue(error); vi.mocked(resendVerificationEmail).mockResolvedValue(undefined); 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 resendVerificationEmail was called await waitFor( () => { expect(resendVerificationEmail).toHaveBeenCalledWith( '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(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); }); // The component should handle the missing email gracefully // resendVerificationEmail should NOT be called since email is missing // Wait a bit for async operations await new Promise((resolve) => setTimeout(resolve, 200)); expect(resendVerificationEmail).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'); }); });