- Refactor AvatarDropdown tests to use helper function pattern - Add ResetPasswordPage test coverage for form validation and submission - Update auth integration tests to verify bcrypt password handling - Fix database service tests to expect hashed passwords - Add proper mock setup for password verification scenarios
323 lines
11 KiB
TypeScript
323 lines
11 KiB
TypeScript
import React from 'react';
|
|
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
|
import '@testing-library/jest-dom';
|
|
import AvatarDropdown from '../AvatarDropdown';
|
|
import { User, UserRole } from '../../../types';
|
|
import { AccountStatus } from '../../../services/auth/auth.constants';
|
|
|
|
const mockRegularUser: User = {
|
|
_id: '1',
|
|
_rev: '1-abc123',
|
|
username: 'John Doe',
|
|
email: 'john@example.com',
|
|
role: UserRole.USER,
|
|
status: AccountStatus.ACTIVE,
|
|
createdAt: new Date(),
|
|
};
|
|
|
|
const mockAdminUser: User = {
|
|
_id: '2',
|
|
_rev: '1-def456',
|
|
username: 'Admin User',
|
|
email: 'admin@example.com',
|
|
role: UserRole.ADMIN,
|
|
status: AccountStatus.ACTIVE,
|
|
createdAt: new Date(),
|
|
};
|
|
|
|
const mockUserWithAvatar: User = {
|
|
...mockRegularUser,
|
|
avatar: 'https://example.com/avatar.jpg',
|
|
};
|
|
|
|
const mockUserWithPassword: User = {
|
|
...mockRegularUser,
|
|
password: '$2b$12$examplehashforpassword',
|
|
};
|
|
|
|
type DropdownProps = Partial<React.ComponentProps<typeof AvatarDropdown>>;
|
|
|
|
const renderDropdown = (props: DropdownProps = {}) => {
|
|
const defaultProps: React.ComponentProps<typeof AvatarDropdown> = {
|
|
user: mockRegularUser,
|
|
onLogout: jest.fn(),
|
|
};
|
|
|
|
const merged = { ...defaultProps, ...props };
|
|
return render(React.createElement(AvatarDropdown, merged));
|
|
};
|
|
|
|
describe('AvatarDropdown', () => {
|
|
const mockOnLogout = jest.fn();
|
|
const mockOnAdmin = jest.fn();
|
|
const mockOnChangePassword = jest.fn();
|
|
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
});
|
|
|
|
describe('rendering', () => {
|
|
test('should render avatar button with user initials', () => {
|
|
renderDropdown({ user: mockRegularUser, onLogout: mockOnLogout });
|
|
|
|
const button = screen.getByRole('button', { name: /user menu/i });
|
|
expect(button).toBeInTheDocument();
|
|
expect(button).toHaveTextContent('J');
|
|
});
|
|
|
|
test('should render avatar image when user has avatar', () => {
|
|
renderDropdown({ user: mockUserWithAvatar, onLogout: mockOnLogout });
|
|
|
|
const avatar = screen.getByAltText('User avatar');
|
|
expect(avatar).toBeInTheDocument();
|
|
expect(avatar).toHaveAttribute('src', 'https://example.com/avatar.jpg');
|
|
});
|
|
|
|
test('should render fallback character for empty username', () => {
|
|
const userWithEmptyName = { ...mockRegularUser, username: '' };
|
|
renderDropdown({ user: userWithEmptyName, onLogout: mockOnLogout });
|
|
|
|
const button = screen.getByRole('button', { name: /user menu/i });
|
|
expect(button).toHaveTextContent('?');
|
|
});
|
|
|
|
test('should not render dropdown menu initially', () => {
|
|
renderDropdown({ user: mockRegularUser, onLogout: mockOnLogout });
|
|
|
|
expect(screen.queryByText('Signed in as')).not.toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe('dropdown functionality', () => {
|
|
test('should open dropdown when avatar button is clicked', () => {
|
|
renderDropdown({ user: mockRegularUser, onLogout: mockOnLogout });
|
|
|
|
const button = screen.getByRole('button', { name: /user menu/i });
|
|
fireEvent.click(button);
|
|
|
|
expect(screen.getByText('Signed in as')).toBeInTheDocument();
|
|
expect(screen.getByText('John Doe')).toBeInTheDocument();
|
|
});
|
|
|
|
test('should close dropdown when avatar button is clicked again', () => {
|
|
renderDropdown({ user: mockRegularUser, onLogout: mockOnLogout });
|
|
|
|
const button = screen.getByRole('button', { name: /user menu/i });
|
|
|
|
fireEvent.click(button);
|
|
expect(screen.getByText('Signed in as')).toBeInTheDocument();
|
|
|
|
fireEvent.click(button);
|
|
expect(screen.queryByText('Signed in as')).not.toBeInTheDocument();
|
|
});
|
|
|
|
test('should close dropdown when clicking outside', async () => {
|
|
render(
|
|
React.createElement(
|
|
'div',
|
|
null,
|
|
React.createElement(AvatarDropdown, {
|
|
user: mockRegularUser,
|
|
onLogout: mockOnLogout,
|
|
}),
|
|
React.createElement(
|
|
'div',
|
|
{ 'data-testid': 'outside' },
|
|
'Outside element'
|
|
)
|
|
)
|
|
);
|
|
|
|
const button = screen.getByRole('button', { name: /user menu/i });
|
|
const outside = screen.getByTestId('outside');
|
|
|
|
fireEvent.click(button);
|
|
expect(screen.getByText('Signed in as')).toBeInTheDocument();
|
|
|
|
fireEvent.mouseDown(outside);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.queryByText('Signed in as')).not.toBeInTheDocument();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('user information display', () => {
|
|
test('should display username in dropdown', () => {
|
|
renderDropdown({ user: mockRegularUser, onLogout: mockOnLogout });
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: /user menu/i }));
|
|
expect(screen.getByText('John Doe')).toBeInTheDocument();
|
|
});
|
|
|
|
test('should display administrator badge for admin users', () => {
|
|
renderDropdown({ user: mockAdminUser, onLogout: mockOnLogout });
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: /user menu/i }));
|
|
expect(screen.getByText('Administrator')).toBeInTheDocument();
|
|
});
|
|
|
|
test('should not display administrator badge for regular users', () => {
|
|
renderDropdown({ user: mockRegularUser, onLogout: mockOnLogout });
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: /user menu/i }));
|
|
expect(screen.queryByText('Administrator')).not.toBeInTheDocument();
|
|
});
|
|
|
|
test('should truncate long usernames', () => {
|
|
const userWithLongName = {
|
|
...mockRegularUser,
|
|
username: 'Very Long Username That Should Be Truncated',
|
|
};
|
|
|
|
renderDropdown({ user: userWithLongName, onLogout: mockOnLogout });
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: /user menu/i }));
|
|
const usernameElement = screen.getByText(
|
|
'Very Long Username That Should Be Truncated'
|
|
);
|
|
expect(usernameElement).toHaveClass('truncate');
|
|
});
|
|
});
|
|
|
|
describe('logout functionality', () => {
|
|
test('should call onLogout when logout button is clicked', () => {
|
|
renderDropdown({ user: mockRegularUser, onLogout: mockOnLogout });
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: /user menu/i }));
|
|
fireEvent.click(screen.getByText('Logout'));
|
|
|
|
expect(mockOnLogout).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
test('should close dropdown after logout is clicked', () => {
|
|
renderDropdown({ user: mockRegularUser, onLogout: mockOnLogout });
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: /user menu/i }));
|
|
fireEvent.click(screen.getByText('Logout'));
|
|
|
|
expect(screen.queryByText('Signed in as')).not.toBeInTheDocument();
|
|
});
|
|
|
|
test('should always display logout button', () => {
|
|
renderDropdown({ user: mockRegularUser, onLogout: mockOnLogout });
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: /user menu/i }));
|
|
expect(screen.getByText('Logout')).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe('admin functionality', () => {
|
|
test('should render Admin Interface button for admin users', () => {
|
|
renderDropdown({
|
|
user: mockAdminUser,
|
|
onLogout: mockOnLogout,
|
|
onAdmin: mockOnAdmin,
|
|
});
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: /user menu/i }));
|
|
const adminButton = screen.getByText('Admin Interface');
|
|
expect(adminButton).toBeInTheDocument();
|
|
|
|
fireEvent.click(adminButton);
|
|
expect(mockOnAdmin).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
test('should not render Admin Interface button for regular users', () => {
|
|
renderDropdown({ user: mockRegularUser, onLogout: mockOnLogout });
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: /user menu/i }));
|
|
expect(screen.queryByText('Admin Interface')).not.toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe('change password visibility', () => {
|
|
test('should show change password option when user has password', () => {
|
|
renderDropdown({
|
|
user: mockUserWithPassword,
|
|
onLogout: mockOnLogout,
|
|
onChangePassword: mockOnChangePassword,
|
|
});
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: /user menu/i }));
|
|
expect(screen.getByText('Change Password')).toBeInTheDocument();
|
|
});
|
|
|
|
test('should hide change password option when user has no password', () => {
|
|
renderDropdown({
|
|
user: mockRegularUser,
|
|
onLogout: mockOnLogout,
|
|
onChangePassword: mockOnChangePassword,
|
|
});
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: /user menu/i }));
|
|
expect(screen.queryByText('Change Password')).not.toBeInTheDocument();
|
|
});
|
|
|
|
test('should call onChangePassword when change password button clicked', () => {
|
|
renderDropdown({
|
|
user: mockUserWithPassword,
|
|
onLogout: mockOnLogout,
|
|
onChangePassword: mockOnChangePassword,
|
|
});
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: /user menu/i }));
|
|
fireEvent.click(screen.getByText('Change Password'));
|
|
expect(mockOnChangePassword).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|
|
|
|
describe('keyboard accessibility', () => {
|
|
test('should toggle dropdown with Enter key', () => {
|
|
renderDropdown({ user: mockRegularUser, onLogout: mockOnLogout });
|
|
|
|
const button = screen.getByRole('button', { name: /user menu/i });
|
|
fireEvent.keyDown(button, { key: 'Enter', code: 'Enter' });
|
|
fireEvent.keyUp(button, { key: 'Enter', code: 'Enter' });
|
|
expect(screen.getByText('Signed in as')).toBeInTheDocument();
|
|
});
|
|
|
|
test('should not toggle dropdown with unrelated key', () => {
|
|
renderDropdown({ user: mockRegularUser, onLogout: mockOnLogout });
|
|
|
|
const button = screen.getByRole('button', { name: /user menu/i });
|
|
fireEvent.keyDown(button, { key: 'Space', code: 'Space' });
|
|
expect(screen.queryByText('Signed in as')).not.toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe('user initials generation', () => {
|
|
test('should handle lowercase usernames', () => {
|
|
const userWithLowercase = { ...mockRegularUser, username: 'john' };
|
|
renderDropdown({ user: userWithLowercase, onLogout: mockOnLogout });
|
|
|
|
const button = screen.getByRole('button', { name: /user menu/i });
|
|
expect(button).toHaveTextContent('J');
|
|
});
|
|
|
|
test('should handle empty username gracefully', () => {
|
|
const userWithEmptyName = { ...mockRegularUser, username: '' };
|
|
renderDropdown({ user: userWithEmptyName, onLogout: mockOnLogout });
|
|
|
|
const button = screen.getByRole('button', { name: /user menu/i });
|
|
expect(button).toHaveTextContent('?');
|
|
});
|
|
|
|
test('should handle single character username', () => {
|
|
const userWithSingleChar = { ...mockRegularUser, username: 'a' };
|
|
renderDropdown({ user: userWithSingleChar, onLogout: mockOnLogout });
|
|
|
|
const button = screen.getByRole('button', { name: /user menu/i });
|
|
expect(button).toHaveTextContent('A');
|
|
});
|
|
|
|
test('should handle usernames with special characters', () => {
|
|
const userWithSpecialChar = { ...mockRegularUser, username: '!john' };
|
|
renderDropdown({ user: userWithSpecialChar, onLogout: mockOnLogout });
|
|
|
|
const button = screen.getByRole('button', { name: /user menu/i });
|
|
expect(button).toHaveTextContent('!');
|
|
});
|
|
});
|
|
});
|