Files
rentall-app/frontend/src/__tests__/components/AuthModal.test.tsx
2025-12-20 14:59:09 -05:00

426 lines
14 KiB
TypeScript

/**
* AuthModal Component Tests
*
* Tests for the AuthModal component including login, signup,
* form validation, and modal behavior.
*/
import React from 'react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import AuthModal from '../../components/AuthModal';
// Mock the auth context
const mockLogin = jest.fn();
const mockRegister = jest.fn();
jest.mock('../../contexts/AuthContext', () => ({
...jest.requireActual('../../contexts/AuthContext'),
useAuth: () => ({
login: mockLogin,
register: mockRegister,
user: null,
loading: false,
}),
}));
// Mock child components
jest.mock('../../components/PasswordStrengthMeter', () => {
return function MockPasswordStrengthMeter({ password }: { password: string }) {
return <div data-testid="password-strength-meter">Strength: {password.length > 8 ? 'Strong' : 'Weak'}</div>;
};
});
jest.mock('../../components/PasswordInput', () => {
return function MockPasswordInput({
id,
label,
value,
onChange,
required
}: {
id: string;
label: string;
value: string;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
required?: boolean;
}) {
return (
<div className="mb-3">
<label htmlFor={id} className="form-label">{label}</label>
<input
id={id}
type="password"
className="form-control"
value={value}
onChange={onChange}
required={required}
data-testid="password-input"
/>
</div>
);
};
});
jest.mock('../../components/ForgotPasswordModal', () => {
return function MockForgotPasswordModal({
show,
onHide,
onBackToLogin
}: {
show: boolean;
onHide: () => void;
onBackToLogin: () => void;
}) {
if (!show) return null;
return (
<div data-testid="forgot-password-modal">
<button onClick={onBackToLogin} data-testid="back-to-login">Back to Login</button>
<button onClick={onHide}>Close</button>
</div>
);
};
});
jest.mock('../../components/VerificationCodeModal', () => {
return function MockVerificationCodeModal({
show,
onHide,
email,
onVerified
}: {
show: boolean;
onHide: () => void;
email: string;
onVerified: () => void;
}) {
if (!show) return null;
return (
<div data-testid="verification-modal">
<p>Verify email: {email}</p>
<button onClick={onVerified} data-testid="verify-button">Verify</button>
<button onClick={onHide}>Close</button>
</div>
);
};
});
describe('AuthModal', () => {
const defaultProps = {
show: true,
onHide: jest.fn(),
};
beforeEach(() => {
jest.clearAllMocks();
});
// Helper to get email input (it's a textbox with type email)
const getEmailInput = () => screen.getByRole('textbox', { hidden: false });
// Helper to get inputs by their preceding label text
const getInputByLabelText = (container: HTMLElement, labelText: string) => {
const label = Array.from(container.querySelectorAll('label')).find(
l => l.textContent === labelText
);
if (!label) throw new Error(`Label "${labelText}" not found`);
// Get the next sibling input or the input inside the same parent
const parent = label.parentElement;
return parent?.querySelector('input') as HTMLInputElement;
};
describe('Rendering', () => {
it('should render login form by default', () => {
const { container } = render(<AuthModal {...defaultProps} />);
expect(screen.getByText('Welcome to CommunityRentals.App')).toBeInTheDocument();
expect(getInputByLabelText(container, 'Email')).toBeInTheDocument();
expect(screen.getByTestId('password-input')).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Log in' })).toBeInTheDocument();
});
it('should render signup form when initialMode is signup', () => {
const { container } = render(<AuthModal {...defaultProps} initialMode="signup" />);
expect(getInputByLabelText(container, 'First Name')).toBeInTheDocument();
expect(getInputByLabelText(container, 'Last Name')).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Sign up' })).toBeInTheDocument();
expect(screen.getByTestId('password-strength-meter')).toBeInTheDocument();
});
it('should not render when show is false', () => {
render(<AuthModal {...defaultProps} show={false} />);
expect(screen.queryByText('Welcome to CommunityRentals.App')).not.toBeInTheDocument();
});
it('should render Google login button', () => {
render(<AuthModal {...defaultProps} />);
expect(screen.getByRole('button', { name: /continue with google/i })).toBeInTheDocument();
});
it('should render forgot password link in login mode', () => {
render(<AuthModal {...defaultProps} />);
expect(screen.getByText('Forgot password?')).toBeInTheDocument();
});
it('should not render forgot password link in signup mode', () => {
render(<AuthModal {...defaultProps} initialMode="signup" />);
expect(screen.queryByText('Forgot password?')).not.toBeInTheDocument();
});
});
describe('Mode Switching', () => {
it('should switch from login to signup mode', async () => {
const { container } = render(<AuthModal {...defaultProps} />);
// Initially in login mode
expect(screen.getByRole('button', { name: 'Log in' })).toBeInTheDocument();
// Click "Sign up" link
fireEvent.click(screen.getByText('Sign up'));
// Should now be in signup mode
expect(screen.getByRole('button', { name: 'Sign up' })).toBeInTheDocument();
expect(getInputByLabelText(container, 'First Name')).toBeInTheDocument();
});
it('should switch from signup to login mode', async () => {
render(<AuthModal {...defaultProps} initialMode="signup" />);
// Initially in signup mode
expect(screen.getByRole('button', { name: 'Sign up' })).toBeInTheDocument();
// Click "Log in" link
fireEvent.click(screen.getByText('Log in'));
// Should now be in login mode
expect(screen.getByRole('button', { name: 'Log in' })).toBeInTheDocument();
expect(screen.queryByText('First Name')).not.toBeInTheDocument();
});
});
describe('Login Form Submission', () => {
it('should call login with email and password', async () => {
mockLogin.mockResolvedValue({});
const { container } = render(<AuthModal {...defaultProps} />);
// Fill in the form
const emailInput = getInputByLabelText(container, 'Email');
await userEvent.type(emailInput, 'test@example.com');
await userEvent.type(screen.getByTestId('password-input'), 'password123');
// Submit the form
fireEvent.click(screen.getByRole('button', { name: 'Log in' }));
await waitFor(() => {
expect(mockLogin).toHaveBeenCalledWith('test@example.com', 'password123');
});
});
it('should call onHide after successful login', async () => {
mockLogin.mockResolvedValue({});
const { container } = render(<AuthModal {...defaultProps} />);
const emailInput = getInputByLabelText(container, 'Email');
await userEvent.type(emailInput, 'test@example.com');
await userEvent.type(screen.getByTestId('password-input'), 'password123');
fireEvent.click(screen.getByRole('button', { name: 'Log in' }));
await waitFor(() => {
expect(defaultProps.onHide).toHaveBeenCalled();
});
});
it('should display error message on login failure', async () => {
mockLogin.mockRejectedValue({
response: { data: { error: 'Invalid credentials' } },
});
const { container } = render(<AuthModal {...defaultProps} />);
const emailInput = getInputByLabelText(container, 'Email');
await userEvent.type(emailInput, 'test@example.com');
await userEvent.type(screen.getByTestId('password-input'), 'wrongpassword');
fireEvent.click(screen.getByRole('button', { name: 'Log in' }));
await waitFor(() => {
expect(screen.getByText('Invalid credentials')).toBeInTheDocument();
});
});
it('should show loading state during login', async () => {
// Make login take some time
mockLogin.mockImplementation(() => new Promise(resolve => setTimeout(resolve, 100)));
const { container } = render(<AuthModal {...defaultProps} />);
const emailInput = getInputByLabelText(container, 'Email');
await userEvent.type(emailInput, 'test@example.com');
await userEvent.type(screen.getByTestId('password-input'), 'password123');
fireEvent.click(screen.getByRole('button', { name: 'Log in' }));
expect(screen.getByRole('button', { name: 'Loading...' })).toBeInTheDocument();
});
});
describe('Signup Form Submission', () => {
it('should call register with user data', async () => {
mockRegister.mockResolvedValue({});
const { container } = render(<AuthModal {...defaultProps} initialMode="signup" />);
await userEvent.type(getInputByLabelText(container, 'First Name'), 'John');
await userEvent.type(getInputByLabelText(container, 'Last Name'), 'Doe');
await userEvent.type(getInputByLabelText(container, 'Email'), 'john@example.com');
await userEvent.type(screen.getByTestId('password-input'), 'StrongPass123!');
fireEvent.click(screen.getByRole('button', { name: 'Sign up' }));
await waitFor(() => {
expect(mockRegister).toHaveBeenCalledWith({
email: 'john@example.com',
password: 'StrongPass123!',
firstName: 'John',
lastName: 'Doe',
username: 'john', // Generated from email
});
});
});
it('should show verification modal after successful signup', async () => {
mockRegister.mockResolvedValue({});
const { container } = render(<AuthModal {...defaultProps} initialMode="signup" />);
await userEvent.type(getInputByLabelText(container, 'First Name'), 'John');
await userEvent.type(getInputByLabelText(container, 'Last Name'), 'Doe');
await userEvent.type(getInputByLabelText(container, 'Email'), 'john@example.com');
await userEvent.type(screen.getByTestId('password-input'), 'StrongPass123!');
fireEvent.click(screen.getByRole('button', { name: 'Sign up' }));
await waitFor(() => {
expect(screen.getByTestId('verification-modal')).toBeInTheDocument();
expect(screen.getByText('Verify email: john@example.com')).toBeInTheDocument();
});
});
it('should display error message on signup failure', async () => {
mockRegister.mockRejectedValue({
response: { data: { error: 'Email already exists' } },
});
const { container } = render(<AuthModal {...defaultProps} initialMode="signup" />);
await userEvent.type(getInputByLabelText(container, 'First Name'), 'John');
await userEvent.type(getInputByLabelText(container, 'Last Name'), 'Doe');
await userEvent.type(getInputByLabelText(container, 'Email'), 'existing@example.com');
await userEvent.type(screen.getByTestId('password-input'), 'StrongPass123!');
fireEvent.click(screen.getByRole('button', { name: 'Sign up' }));
await waitFor(() => {
expect(screen.getByText('Email already exists')).toBeInTheDocument();
});
});
});
describe('Forgot Password', () => {
it('should show forgot password modal when link is clicked', async () => {
render(<AuthModal {...defaultProps} />);
fireEvent.click(screen.getByText('Forgot password?'));
expect(screen.getByTestId('forgot-password-modal')).toBeInTheDocument();
});
it('should hide forgot password modal and show login when back is clicked', async () => {
render(<AuthModal {...defaultProps} />);
// Open forgot password modal
fireEvent.click(screen.getByText('Forgot password?'));
expect(screen.getByTestId('forgot-password-modal')).toBeInTheDocument();
// Click back to login
fireEvent.click(screen.getByTestId('back-to-login'));
// Should show login form again
await waitFor(() => {
expect(screen.queryByTestId('forgot-password-modal')).not.toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Log in' })).toBeInTheDocument();
});
});
});
describe('Modal Close', () => {
it('should call onHide when close button is clicked', async () => {
render(<AuthModal {...defaultProps} />);
// Click close button (btn-close class)
const closeButton = document.querySelector('.btn-close') as HTMLButtonElement;
fireEvent.click(closeButton);
expect(defaultProps.onHide).toHaveBeenCalled();
});
});
describe('Google OAuth', () => {
it('should redirect to Google OAuth when Google button is clicked', () => {
// Mock window.location
const originalLocation = window.location;
delete (window as any).location;
window.location = { ...originalLocation, href: '' } as Location;
render(<AuthModal {...defaultProps} />);
fireEvent.click(screen.getByRole('button', { name: /continue with google/i }));
// Check that window.location.href was set to Google OAuth URL
expect(window.location.href).toContain('accounts.google.com');
// Restore
window.location = originalLocation;
});
});
describe('Accessibility', () => {
it('should have password label associated with input', () => {
render(<AuthModal {...defaultProps} initialMode="signup" />);
// Password input has proper htmlFor through the mock
expect(screen.getByLabelText('Password')).toBeInTheDocument();
});
it('should display error in an alert role', async () => {
mockLogin.mockRejectedValue({
response: { data: { error: 'Test error' } },
});
const { container } = render(<AuthModal {...defaultProps} />);
const emailInput = getInputByLabelText(container, 'Email');
await userEvent.type(emailInput, 'test@example.com');
await userEvent.type(screen.getByTestId('password-input'), 'password');
fireEvent.click(screen.getByRole('button', { name: 'Log in' }));
await waitFor(() => {
expect(screen.getByRole('alert')).toBeInTheDocument();
});
});
});
describe('Terms and Privacy Links', () => {
it('should display terms and privacy links', () => {
render(<AuthModal {...defaultProps} />);
expect(screen.getByText('Terms of Service')).toHaveAttribute('href', '/terms');
expect(screen.getByText('Privacy Policy')).toHaveAttribute('href', '/privacy');
});
});
});