2025-12-03 21:56:50 +00:00
|
|
|
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 }) => (
|
|
|
|
|
<MemoryRouter>{children}</MemoryRouter>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
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 }) => (
|
|
|
|
|
<MemoryRouter initialEntries={['/reset-password?token=test-token']}>
|
|
|
|
|
{children}
|
|
|
|
|
</MemoryRouter>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
render(<ResetPasswordPage />, { wrapper: wrapperWithToken });
|
|
|
|
|
|
|
|
|
|
// Wait for the token to be extracted and form to render
|
|
|
|
|
await waitFor(() => {
|
|
|
|
|
expect(screen.getByLabelText('Nouveau mot de passe')).toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
|
2025-12-13 02:34:34 +00:00
|
|
|
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();
|
2025-12-03 21:56:50 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should display error message when token is missing', () => {
|
|
|
|
|
render(<ResetPasswordPage />, { wrapper });
|
|
|
|
|
|
2025-12-13 02:34:34 +00:00
|
|
|
expect(
|
|
|
|
|
screen.getByText('Lien de réinitialisation invalide'),
|
|
|
|
|
).toBeInTheDocument();
|
2025-12-03 21:56:50 +00:00
|
|
|
// Check for the subtitle which is unique
|
2025-12-13 02:34:34 +00:00
|
|
|
expect(
|
|
|
|
|
screen.getByText('Le lien de réinitialisation est invalide ou a expiré'),
|
|
|
|
|
).toBeInTheDocument();
|
2025-12-03 21:56:50 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should extract token from URL', async () => {
|
|
|
|
|
const wrapperWithToken = ({ children }: { children: React.ReactNode }) => (
|
|
|
|
|
<MemoryRouter initialEntries={['/reset-password?token=abc123']}>
|
|
|
|
|
{children}
|
|
|
|
|
</MemoryRouter>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
render(<ResetPasswordPage />, { wrapper: wrapperWithToken });
|
|
|
|
|
|
|
|
|
|
// Wait for the token to be extracted and form to render
|
|
|
|
|
// The useEffect will extract the token and update state
|
2025-12-13 02:34:34 +00:00
|
|
|
await waitFor(
|
|
|
|
|
() => {
|
|
|
|
|
// Check for the form inputs instead of the title (which appears twice)
|
|
|
|
|
expect(
|
|
|
|
|
screen.getByLabelText('Nouveau mot de passe'),
|
|
|
|
|
).toBeInTheDocument();
|
|
|
|
|
},
|
|
|
|
|
{ timeout: 3000 },
|
|
|
|
|
);
|
|
|
|
|
|
2025-12-03 21:56:50 +00:00
|
|
|
// Verify the form is rendered (not the error message)
|
2025-12-13 02:34:34 +00:00
|
|
|
expect(
|
|
|
|
|
screen.queryByText('Lien de réinitialisation invalide'),
|
|
|
|
|
).not.toBeInTheDocument();
|
|
|
|
|
expect(
|
|
|
|
|
screen.getByText('Entrez votre nouveau mot de passe'),
|
|
|
|
|
).toBeInTheDocument();
|
2025-12-03 21:56:50 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should validate required password field', async () => {
|
|
|
|
|
const wrapperWithToken = ({ children }: { children: React.ReactNode }) => (
|
|
|
|
|
<MemoryRouter initialEntries={['/reset-password?token=test-token']}>
|
|
|
|
|
{children}
|
|
|
|
|
</MemoryRouter>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
render(<ResetPasswordPage />, { wrapper: wrapperWithToken });
|
|
|
|
|
|
2025-12-13 02:34:34 +00:00
|
|
|
const form = screen
|
|
|
|
|
.getByRole('button', { name: 'Réinitialiser le mot de passe' })
|
|
|
|
|
.closest('form');
|
2025-12-03 21:56:50 +00:00
|
|
|
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 }) => (
|
|
|
|
|
<MemoryRouter initialEntries={['/reset-password?token=test-token']}>
|
|
|
|
|
{children}
|
|
|
|
|
</MemoryRouter>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
render(<ResetPasswordPage />, { wrapper: wrapperWithToken });
|
|
|
|
|
|
|
|
|
|
const passwordInput = screen.getByLabelText('Nouveau mot de passe');
|
|
|
|
|
await act(async () => {
|
|
|
|
|
await user.type(passwordInput, 'short');
|
|
|
|
|
await user.tab();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await waitFor(() => {
|
2025-12-13 02:34:34 +00:00
|
|
|
expect(
|
|
|
|
|
screen.getByText('Le mot de passe doit contenir au moins 8 caractères'),
|
|
|
|
|
).toBeInTheDocument();
|
2025-12-03 21:56:50 +00:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should validate password confirmation match', async () => {
|
|
|
|
|
const user = userEvent.setup();
|
|
|
|
|
const wrapperWithToken = ({ children }: { children: React.ReactNode }) => (
|
|
|
|
|
<MemoryRouter initialEntries={['/reset-password?token=test-token']}>
|
|
|
|
|
{children}
|
|
|
|
|
</MemoryRouter>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
render(<ResetPasswordPage />, { wrapper: wrapperWithToken });
|
|
|
|
|
|
|
|
|
|
const passwordInput = screen.getByLabelText('Nouveau mot de passe');
|
2025-12-13 02:34:34 +00:00
|
|
|
const confirmPasswordInput = screen.getByLabelText(
|
|
|
|
|
'Confirmer le mot de passe',
|
|
|
|
|
);
|
2025-12-03 21:56:50 +00:00
|
|
|
|
|
|
|
|
await act(async () => {
|
|
|
|
|
await user.type(passwordInput, 'password123');
|
|
|
|
|
await user.type(confirmPasswordInput, 'password456');
|
|
|
|
|
await user.tab();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await waitFor(() => {
|
2025-12-13 02:34:34 +00:00
|
|
|
expect(
|
|
|
|
|
screen.getByText('Les mots de passe ne correspondent pas'),
|
|
|
|
|
).toBeInTheDocument();
|
2025-12-03 21:56:50 +00:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should clear error when user starts typing', async () => {
|
|
|
|
|
const user = userEvent.setup();
|
|
|
|
|
const wrapperWithToken = ({ children }: { children: React.ReactNode }) => (
|
|
|
|
|
<MemoryRouter initialEntries={['/reset-password?token=test-token']}>
|
|
|
|
|
{children}
|
|
|
|
|
</MemoryRouter>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
render(<ResetPasswordPage />, { wrapper: wrapperWithToken });
|
|
|
|
|
|
|
|
|
|
const passwordInput = screen.getByLabelText('Nouveau mot de passe');
|
2025-12-13 02:34:34 +00:00
|
|
|
|
2025-12-03 21:56:50 +00:00
|
|
|
// 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 }) => (
|
|
|
|
|
<MemoryRouter initialEntries={['/reset-password?token=test-token-123']}>
|
|
|
|
|
{children}
|
|
|
|
|
</MemoryRouter>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
render(<ResetPasswordPage />, { wrapper: wrapperWithToken });
|
|
|
|
|
|
|
|
|
|
await act(async () => {
|
2025-12-13 02:34:34 +00:00
|
|
|
await user.type(
|
|
|
|
|
screen.getByLabelText('Nouveau mot de passe'),
|
|
|
|
|
'newpassword123',
|
|
|
|
|
);
|
|
|
|
|
await user.type(
|
|
|
|
|
screen.getByLabelText('Confirmer le mot de passe'),
|
|
|
|
|
'newpassword123',
|
|
|
|
|
);
|
2025-12-03 21:56:50 +00:00
|
|
|
});
|
|
|
|
|
|
2025-12-13 02:34:34 +00:00
|
|
|
const form = screen
|
|
|
|
|
.getByRole('button', { name: 'Réinitialiser le mot de passe' })
|
|
|
|
|
.closest('form');
|
2025-12-03 21:56:50 +00:00
|
|
|
await act(async () => {
|
|
|
|
|
if (form) {
|
2025-12-13 02:34:34 +00:00
|
|
|
await userEvent.click(
|
|
|
|
|
screen.getByRole('button', { name: 'Réinitialiser le mot de passe' }),
|
|
|
|
|
);
|
2025-12-03 21:56:50 +00:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
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 }) => (
|
|
|
|
|
<MemoryRouter initialEntries={['/reset-password?token=test-token']}>
|
|
|
|
|
{children}
|
|
|
|
|
</MemoryRouter>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
render(<ResetPasswordPage />, { 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 }) => (
|
|
|
|
|
<MemoryRouter initialEntries={['/reset-password?token=test-token']}>
|
|
|
|
|
{children}
|
|
|
|
|
</MemoryRouter>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
render(<ResetPasswordPage />, { wrapper: wrapperWithToken });
|
|
|
|
|
|
2026-01-07 09:32:53 +00:00
|
|
|
// Le texte "Chargement..." est dans un span avec aria-hidden, donc on cherche le bouton par son aria-busy
|
2026-01-13 18:47:57 +00:00
|
|
|
const submitButton = screen.getByRole('button', {
|
|
|
|
|
name: 'Chargement en cours',
|
|
|
|
|
});
|
2025-12-03 21:56:50 +00:00
|
|
|
expect(submitButton).toBeDisabled();
|
2026-01-07 09:32:53 +00:00
|
|
|
expect(submitButton).toHaveAttribute('aria-busy', 'true');
|
2025-12-03 21:56:50 +00:00
|
|
|
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 }) => (
|
|
|
|
|
<MemoryRouter initialEntries={['/reset-password?token=test-token']}>
|
|
|
|
|
{children}
|
|
|
|
|
</MemoryRouter>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
render(<ResetPasswordPage />, { wrapper: wrapperWithToken });
|
|
|
|
|
|
|
|
|
|
expect(screen.getByText('Mot de passe réinitialisé')).toBeInTheDocument();
|
2025-12-13 02:34:34 +00:00
|
|
|
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();
|
2025-12-03 21:56:50 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should redirect to login after successful reset', async () => {
|
|
|
|
|
vi.mocked(usePasswordReset).mockReturnValue({
|
|
|
|
|
...mockUsePasswordReset,
|
|
|
|
|
success: true,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const wrapperWithToken = ({ children }: { children: React.ReactNode }) => (
|
|
|
|
|
<MemoryRouter initialEntries={['/reset-password?token=test-token']}>
|
|
|
|
|
{children}
|
|
|
|
|
</MemoryRouter>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
render(<ResetPasswordPage />, { 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)
|
2025-12-13 02:34:34 +00:00
|
|
|
await waitFor(
|
|
|
|
|
() => {
|
|
|
|
|
expect(mockNavigate).toHaveBeenCalledWith('/login', { replace: true });
|
|
|
|
|
},
|
|
|
|
|
{ timeout: 4000 },
|
|
|
|
|
);
|
2025-12-03 21:56:50 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should display password strength indicator', () => {
|
|
|
|
|
const wrapperWithToken = ({ children }: { children: React.ReactNode }) => (
|
|
|
|
|
<MemoryRouter initialEntries={['/reset-password?token=test-token']}>
|
|
|
|
|
{children}
|
|
|
|
|
</MemoryRouter>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
render(<ResetPasswordPage />, { 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 }) => (
|
|
|
|
|
<MemoryRouter initialEntries={['/reset-password?token=test-token']}>
|
|
|
|
|
{children}
|
|
|
|
|
</MemoryRouter>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
render(<ResetPasswordPage />, { wrapper: wrapperWithToken });
|
|
|
|
|
|
|
|
|
|
const loginLink = screen.getByText('Retour à la connexion');
|
|
|
|
|
expect(loginLink).toBeInTheDocument();
|
|
|
|
|
expect(loginLink.closest('a')).toHaveAttribute('href', '/login');
|
|
|
|
|
});
|
|
|
|
|
});
|