veza/apps/web/src/features/auth/pages/ResetPasswordPage.test.tsx

380 lines
12 KiB
TypeScript

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();
});
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(<ResetPasswordPage />, { 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 }) => (
<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
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 }) => (
<MemoryRouter initialEntries={['/reset-password?token=test-token']}>
{children}
</MemoryRouter>
);
render(<ResetPasswordPage />, { 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 }) => (
<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(() => {
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 }) => (
<MemoryRouter initialEntries={['/reset-password?token=test-token']}>
{children}
</MemoryRouter>
);
render(<ResetPasswordPage />, { 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 }) => (
<MemoryRouter initialEntries={['/reset-password?token=test-token']}>
{children}
</MemoryRouter>
);
render(<ResetPasswordPage />, { 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 }) => (
<MemoryRouter initialEntries={['/reset-password?token=test-token-123']}>
{children}
</MemoryRouter>
);
render(<ResetPasswordPage />, { 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 }) => (
<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 });
// 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 }) => (
<MemoryRouter initialEntries={['/reset-password?token=test-token']}>
{children}
</MemoryRouter>
);
render(<ResetPasswordPage />, { 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 }) => (
<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)
await waitFor(
() => {
expect(mockNavigate).toHaveBeenCalledWith('/login', { replace: true });
},
{ timeout: 4000 },
);
});
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');
});
});