All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 19m58s
189 lines
7.0 KiB
TypeScript
189 lines
7.0 KiB
TypeScript
// src/pages/ResetPasswordPage.test.tsx
|
|
import React from 'react';
|
|
import { render, screen, fireEvent, waitFor, act } from '@testing-library/react';
|
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
import { MemoryRouter, Route, Routes } from 'react-router-dom';
|
|
import { ResetPasswordPage } from './ResetPasswordPage';
|
|
import * as apiClient from '../services/apiClient';
|
|
import { logger } from '../services/logger.client';
|
|
|
|
// The apiClient and logger are now mocked globally.
|
|
const mockedApiClient = vi.mocked(apiClient);
|
|
|
|
// The logger is mocked globally.
|
|
// Helper function to render the component within a router context
|
|
const renderWithRouter = (token: string) => {
|
|
return render(
|
|
<MemoryRouter initialEntries={[`/reset-password/${token}`]}>
|
|
<Routes>
|
|
<Route path="/reset-password/:token" element={<ResetPasswordPage />} />
|
|
<Route path="/" element={<div>Home Page</div>} />
|
|
</Routes>
|
|
</MemoryRouter>,
|
|
);
|
|
};
|
|
|
|
describe('ResetPasswordPage', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it('should render the form with password fields and a submit button', () => {
|
|
renderWithRouter('test-token-123');
|
|
expect(screen.getByRole('heading', { name: /set a new password/i })).toBeInTheDocument();
|
|
expect(screen.getByPlaceholderText('New Password')).toBeInTheDocument();
|
|
expect(screen.getByPlaceholderText('Confirm New Password')).toBeInTheDocument();
|
|
expect(screen.getByRole('button', { name: /reset password/i })).toBeInTheDocument();
|
|
});
|
|
|
|
it('should call resetPassword and show success message on valid submission', async () => {
|
|
const setTimeoutSpy = vi.spyOn(global, 'setTimeout');
|
|
mockedApiClient.resetPassword.mockResolvedValue(
|
|
new Response(JSON.stringify({ message: 'Password reset was successful!' })),
|
|
);
|
|
const token = 'valid-token';
|
|
renderWithRouter(token);
|
|
|
|
fireEvent.change(screen.getByPlaceholderText('New Password'), {
|
|
target: { value: 'newSecurePassword123' },
|
|
});
|
|
fireEvent.change(screen.getByPlaceholderText('Confirm New Password'), {
|
|
target: { value: 'newSecurePassword123' },
|
|
});
|
|
fireEvent.click(screen.getByRole('button', { name: /reset password/i }));
|
|
|
|
await waitFor(() => {
|
|
expect(mockedApiClient.resetPassword).toHaveBeenCalledWith(token, 'newSecurePassword123');
|
|
expect(screen.getByText(/password reset was successful!/i)).toBeInTheDocument();
|
|
expect(screen.getByText(/return to home/i)).toBeInTheDocument();
|
|
});
|
|
expect(logger.info).toHaveBeenCalledWith('Password has been successfully reset.');
|
|
|
|
// Check that form is cleared
|
|
expect(screen.queryByPlaceholderText('New Password')).not.toBeInTheDocument();
|
|
|
|
// Verify redirect timeout was set
|
|
expect(setTimeoutSpy).toHaveBeenCalledWith(expect.any(Function), 4000);
|
|
setTimeoutSpy.mockRestore();
|
|
});
|
|
|
|
it('should show an error message if passwords do not match', async () => {
|
|
renderWithRouter('test-token');
|
|
|
|
fireEvent.change(screen.getByPlaceholderText('New Password'), {
|
|
target: { value: 'passwordA' },
|
|
});
|
|
fireEvent.change(screen.getByPlaceholderText('Confirm New Password'), {
|
|
target: { value: 'passwordB' },
|
|
});
|
|
fireEvent.click(screen.getByRole('button', { name: /reset password/i }));
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Passwords do not match.')).toBeInTheDocument();
|
|
});
|
|
expect(mockedApiClient.resetPassword).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should show an error message if the API call fails', async () => {
|
|
mockedApiClient.resetPassword.mockRejectedValueOnce(new Error('Invalid or expired token.'));
|
|
renderWithRouter('invalid-token');
|
|
|
|
fireEvent.change(screen.getByPlaceholderText('New Password'), {
|
|
target: { value: 'newSecurePassword123' },
|
|
});
|
|
fireEvent.change(screen.getByPlaceholderText('Confirm New Password'), {
|
|
target: { value: 'newSecurePassword123' },
|
|
});
|
|
fireEvent.click(screen.getByRole('button', { name: /reset password/i }));
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Invalid or expired token.')).toBeInTheDocument();
|
|
});
|
|
expect(logger.error).toHaveBeenCalledWith(
|
|
{ err: expect.any(Error) },
|
|
'Failed to reset password.',
|
|
);
|
|
});
|
|
|
|
it('should show a loading spinner while submitting', async () => {
|
|
let resolvePromise: (value: Response) => void;
|
|
const mockPromise = new Promise<Response>((resolve) => {
|
|
resolvePromise = resolve;
|
|
});
|
|
mockedApiClient.resetPassword.mockReturnValue(mockPromise);
|
|
renderWithRouter('test-token');
|
|
|
|
fireEvent.change(screen.getByPlaceholderText('New Password'), {
|
|
target: { value: 'newSecurePassword123' },
|
|
});
|
|
fireEvent.change(screen.getByPlaceholderText('Confirm New Password'), {
|
|
target: { value: 'newSecurePassword123' },
|
|
});
|
|
fireEvent.click(screen.getByRole('button', { name: /reset password/i }));
|
|
|
|
// Expect button to be disabled and text to be gone (replaced by spinner)
|
|
// We use the accessible name which persists via aria-label even when text content is replaced
|
|
expect(screen.getByRole('button', { name: /reset password/i })).toBeDisabled();
|
|
expect(screen.queryByText('Reset Password')).not.toBeInTheDocument();
|
|
|
|
await act(async () => {
|
|
resolvePromise!(new Response(JSON.stringify({ message: 'Password reset was successful!' })));
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText(/password reset was successful!/i)).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it('should show an error if no token is provided', async () => {
|
|
render(
|
|
<MemoryRouter initialEntries={['/reset-password']}>
|
|
<Routes>
|
|
<Route path="/reset-password" element={<ResetPasswordPage />} />
|
|
</Routes>
|
|
</MemoryRouter>,
|
|
);
|
|
|
|
// Fill in required fields to trigger form submission
|
|
fireEvent.change(screen.getByPlaceholderText('New Password'), {
|
|
target: { value: 'password123' },
|
|
});
|
|
fireEvent.change(screen.getByPlaceholderText('Confirm New Password'), {
|
|
target: { value: 'password123' },
|
|
});
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: /reset password/i }));
|
|
|
|
await waitFor(() => {
|
|
expect(
|
|
screen.getByText('No reset token provided. Please use the link from your email.'),
|
|
).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it('should handle unknown errors', async () => {
|
|
mockedApiClient.resetPassword.mockRejectedValue('Unknown error');
|
|
renderWithRouter('test-token');
|
|
|
|
fireEvent.change(screen.getByPlaceholderText('New Password'), {
|
|
target: { value: 'newSecurePassword123' },
|
|
});
|
|
fireEvent.change(screen.getByPlaceholderText('Confirm New Password'), {
|
|
target: { value: 'newSecurePassword123' },
|
|
});
|
|
fireEvent.click(screen.getByRole('button', { name: /reset password/i }));
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('An unknown error occurred.')).toBeInTheDocument();
|
|
});
|
|
expect(logger.error).toHaveBeenCalledWith(
|
|
{ err: 'Unknown error' },
|
|
'Failed to reset password.',
|
|
);
|
|
});
|
|
});
|