426 lines
14 KiB
TypeScript
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');
|
|
});
|
|
});
|
|
});
|