✨ Features: - Add comprehensive database service documentation - Create detailed module README with usage examples - Expand main documentation index with database links - Add component test support to Jest configuration 🔧 Improvements: - Fix AvatarDropdown test failures (dark mode classes and rapid clicking) - Update documentation version to 2.1 - Include migration guide and troubleshooting sections - Add performance considerations and security notes 📚 Documentation: - Complete API reference with code examples - Architecture overview with Strategy pattern explanation - Environment configuration and strategy selection guide - Best practices and development guidelines - Comprehensive refactoring summary 🧪 Testing: - All 292 tests passing across all modules - Component tests now properly integrated with Jest - Fixed TypeScript compatibility issues in tests - Verified database service functionality in all environments 📋 Summary: - Removed deprecated CouchDB service files - Consolidated database operations under unified service - Enhanced documentation structure and content - Improved test coverage and reliability - Maintained backward compatibility where possible
478 lines
15 KiB
TypeScript
478 lines
15 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';
|
|
|
|
// Mock user data
|
|
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: 'hashed-password',
|
|
};
|
|
|
|
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', () => {
|
|
render(<AvatarDropdown 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', () => {
|
|
render(
|
|
<AvatarDropdown 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: '' };
|
|
render(
|
|
<AvatarDropdown user={userWithEmptyName} onLogout={mockOnLogout} />
|
|
);
|
|
|
|
const button = screen.getByRole('button', { name: /user menu/i });
|
|
expect(button).toHaveTextContent('?');
|
|
});
|
|
|
|
test('should not render dropdown menu initially', () => {
|
|
render(<AvatarDropdown user={mockRegularUser} onLogout={mockOnLogout} />);
|
|
|
|
expect(screen.queryByText('Signed in as')).not.toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe('dropdown functionality', () => {
|
|
test('should open dropdown when avatar button is clicked', () => {
|
|
render(<AvatarDropdown 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', () => {
|
|
render(<AvatarDropdown user={mockRegularUser} onLogout={mockOnLogout} />);
|
|
|
|
const button = screen.getByRole('button', { name: /user menu/i });
|
|
|
|
// Open dropdown
|
|
fireEvent.click(button);
|
|
expect(screen.getByText('Signed in as')).toBeInTheDocument();
|
|
|
|
// Close dropdown
|
|
fireEvent.click(button);
|
|
expect(screen.queryByText('Signed in as')).not.toBeInTheDocument();
|
|
});
|
|
|
|
test('should close dropdown when clicking outside', async () => {
|
|
render(
|
|
<div>
|
|
<AvatarDropdown user={mockRegularUser} onLogout={mockOnLogout} />
|
|
<div data-testid='outside'>Outside element</div>
|
|
</div>
|
|
);
|
|
|
|
const button = screen.getByRole('button', { name: /user menu/i });
|
|
const outside = screen.getByTestId('outside');
|
|
|
|
// Open dropdown
|
|
fireEvent.click(button);
|
|
expect(screen.getByText('Signed in as')).toBeInTheDocument();
|
|
|
|
// Click outside
|
|
fireEvent.mouseDown(outside);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.queryByText('Signed in as')).not.toBeInTheDocument();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('user information display', () => {
|
|
test('should display username in dropdown', () => {
|
|
render(<AvatarDropdown 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', () => {
|
|
render(<AvatarDropdown 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', () => {
|
|
render(<AvatarDropdown 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',
|
|
};
|
|
|
|
render(
|
|
<AvatarDropdown 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', () => {
|
|
render(<AvatarDropdown 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', () => {
|
|
render(<AvatarDropdown 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', () => {
|
|
render(<AvatarDropdown user={mockRegularUser} onLogout={mockOnLogout} />);
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: /user menu/i }));
|
|
expect(screen.getByText('Logout')).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe('admin functionality', () => {
|
|
test('should display admin interface button for admin users when onAdmin provided', () => {
|
|
render(
|
|
<AvatarDropdown
|
|
user={mockAdminUser}
|
|
onLogout={mockOnLogout}
|
|
onAdmin={mockOnAdmin}
|
|
/>
|
|
);
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: /user menu/i }));
|
|
expect(screen.getByText('Admin Interface')).toBeInTheDocument();
|
|
});
|
|
|
|
test('should not display admin interface button for regular users', () => {
|
|
render(
|
|
<AvatarDropdown
|
|
user={mockRegularUser}
|
|
onLogout={mockOnLogout}
|
|
onAdmin={mockOnAdmin}
|
|
/>
|
|
);
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: /user menu/i }));
|
|
expect(screen.queryByText('Admin Interface')).not.toBeInTheDocument();
|
|
});
|
|
|
|
test('should not display admin interface button when onAdmin not provided', () => {
|
|
render(<AvatarDropdown user={mockAdminUser} onLogout={mockOnLogout} />);
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: /user menu/i }));
|
|
expect(screen.queryByText('Admin Interface')).not.toBeInTheDocument();
|
|
});
|
|
|
|
test('should call onAdmin when admin interface button is clicked', () => {
|
|
render(
|
|
<AvatarDropdown
|
|
user={mockAdminUser}
|
|
onLogout={mockOnLogout}
|
|
onAdmin={mockOnAdmin}
|
|
/>
|
|
);
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: /user menu/i }));
|
|
fireEvent.click(screen.getByText('Admin Interface'));
|
|
|
|
expect(mockOnAdmin).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
test('should close dropdown after admin interface is clicked', () => {
|
|
render(
|
|
<AvatarDropdown
|
|
user={mockAdminUser}
|
|
onLogout={mockOnLogout}
|
|
onAdmin={mockOnAdmin}
|
|
/>
|
|
);
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: /user menu/i }));
|
|
fireEvent.click(screen.getByText('Admin Interface'));
|
|
|
|
expect(screen.queryByText('Signed in as')).not.toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe('change password functionality', () => {
|
|
test('should display change password button for users with password when onChangePassword provided', () => {
|
|
render(
|
|
<AvatarDropdown
|
|
user={mockUserWithPassword}
|
|
onLogout={mockOnLogout}
|
|
onChangePassword={mockOnChangePassword}
|
|
/>
|
|
);
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: /user menu/i }));
|
|
expect(screen.getByText('Change Password')).toBeInTheDocument();
|
|
});
|
|
|
|
test('should not display change password button for users without password', () => {
|
|
render(
|
|
<AvatarDropdown
|
|
user={mockRegularUser}
|
|
onLogout={mockOnLogout}
|
|
onChangePassword={mockOnChangePassword}
|
|
/>
|
|
);
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: /user menu/i }));
|
|
expect(screen.queryByText('Change Password')).not.toBeInTheDocument();
|
|
});
|
|
|
|
test('should not display change password button when onChangePassword not provided', () => {
|
|
render(
|
|
<AvatarDropdown user={mockUserWithPassword} onLogout={mockOnLogout} />
|
|
);
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: /user menu/i }));
|
|
expect(screen.queryByText('Change Password')).not.toBeInTheDocument();
|
|
});
|
|
|
|
test('should call onChangePassword when change password button is clicked', () => {
|
|
render(
|
|
<AvatarDropdown
|
|
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);
|
|
});
|
|
|
|
test('should close dropdown after change password is clicked', () => {
|
|
render(
|
|
<AvatarDropdown
|
|
user={mockUserWithPassword}
|
|
onLogout={mockOnLogout}
|
|
onChangePassword={mockOnChangePassword}
|
|
/>
|
|
);
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: /user menu/i }));
|
|
fireEvent.click(screen.getByText('Change Password'));
|
|
|
|
expect(screen.queryByText('Signed in as')).not.toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe('getInitials function', () => {
|
|
test('should return first character uppercase for regular names', () => {
|
|
const userWithLowercase = { ...mockRegularUser, username: 'john doe' };
|
|
render(
|
|
<AvatarDropdown user={userWithLowercase} onLogout={mockOnLogout} />
|
|
);
|
|
|
|
const button = screen.getByRole('button', { name: /user menu/i });
|
|
expect(button).toHaveTextContent('J');
|
|
});
|
|
|
|
test('should return question mark for empty string', () => {
|
|
const userWithEmptyName = { ...mockRegularUser, username: '' };
|
|
render(
|
|
<AvatarDropdown user={userWithEmptyName} onLogout={mockOnLogout} />
|
|
);
|
|
|
|
const button = screen.getByRole('button', { name: /user menu/i });
|
|
expect(button).toHaveTextContent('?');
|
|
});
|
|
|
|
test('should handle single character names', () => {
|
|
const userWithSingleChar = { ...mockRegularUser, username: 'x' };
|
|
render(
|
|
<AvatarDropdown user={userWithSingleChar} onLogout={mockOnLogout} />
|
|
);
|
|
|
|
const button = screen.getByRole('button', { name: /user menu/i });
|
|
expect(button).toHaveTextContent('X');
|
|
});
|
|
|
|
test('should handle special characters', () => {
|
|
const userWithSpecialChar = { ...mockRegularUser, username: '@john' };
|
|
render(
|
|
<AvatarDropdown user={userWithSpecialChar} onLogout={mockOnLogout} />
|
|
);
|
|
|
|
const button = screen.getByRole('button', { name: /user menu/i });
|
|
expect(button).toHaveTextContent('@');
|
|
});
|
|
});
|
|
|
|
describe('accessibility', () => {
|
|
test('should have proper aria-label for avatar button', () => {
|
|
render(<AvatarDropdown user={mockRegularUser} onLogout={mockOnLogout} />);
|
|
|
|
const button = screen.getByRole('button', { name: /user menu/i });
|
|
expect(button).toHaveAttribute('aria-label', 'User menu');
|
|
});
|
|
|
|
test('should be keyboard accessible', () => {
|
|
render(<AvatarDropdown user={mockRegularUser} onLogout={mockOnLogout} />);
|
|
|
|
const button = screen.getByRole('button', { name: /user menu/i });
|
|
button.focus();
|
|
expect(button).toHaveFocus();
|
|
});
|
|
|
|
test('should have proper focus styles', () => {
|
|
render(<AvatarDropdown user={mockRegularUser} onLogout={mockOnLogout} />);
|
|
|
|
const button = screen.getByRole('button', { name: /user menu/i });
|
|
expect(button).toHaveClass('focus:outline-none', 'focus:ring-2');
|
|
});
|
|
});
|
|
|
|
describe('styling and theming', () => {
|
|
test('should apply dark mode classes', () => {
|
|
render(<AvatarDropdown user={mockRegularUser} onLogout={mockOnLogout} />);
|
|
|
|
const button = screen.getByRole('button', { name: /user menu/i });
|
|
expect(button).toHaveClass('dark:bg-slate-700', 'dark:text-slate-300');
|
|
|
|
fireEvent.click(button);
|
|
const dropdown = screen
|
|
.getByText('Signed in as')
|
|
.closest('div')?.parentElement;
|
|
expect(dropdown).toHaveClass(
|
|
'dark:bg-slate-800',
|
|
'dark:border-slate-700'
|
|
);
|
|
});
|
|
|
|
test('should apply hover styles to menu items', () => {
|
|
render(<AvatarDropdown user={mockRegularUser} onLogout={mockOnLogout} />);
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: /user menu/i }));
|
|
const logoutButton = screen.getByText('Logout');
|
|
expect(logoutButton).toHaveClass(
|
|
'hover:bg-slate-100',
|
|
'dark:hover:bg-slate-700'
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('edge cases', () => {
|
|
test('should handle clicking outside when dropdown is closed', async () => {
|
|
render(
|
|
<div>
|
|
<AvatarDropdown user={mockRegularUser} onLogout={mockOnLogout} />
|
|
<div data-testid='outside'>Outside element</div>
|
|
</div>
|
|
);
|
|
|
|
const outside = screen.getByTestId('outside');
|
|
fireEvent.mouseDown(outside);
|
|
|
|
// Should not throw any errors
|
|
expect(screen.queryByText('Signed in as')).not.toBeInTheDocument();
|
|
});
|
|
|
|
test('should handle rapid clicking', () => {
|
|
render(<AvatarDropdown user={mockRegularUser} onLogout={mockOnLogout} />);
|
|
|
|
const button = screen.getByRole('button', { name: /user menu/i });
|
|
|
|
// Rapid clicks - odd number should end up open
|
|
fireEvent.click(button);
|
|
fireEvent.click(button);
|
|
fireEvent.click(button);
|
|
|
|
// Should end up open (3 clicks = open)
|
|
expect(screen.getByText('Signed in as')).toBeInTheDocument();
|
|
});
|
|
|
|
test('should cleanup event listeners on unmount', () => {
|
|
const removeEventListenerSpy = jest.spyOn(
|
|
document,
|
|
'removeEventListener'
|
|
);
|
|
|
|
const { unmount } = render(
|
|
<AvatarDropdown user={mockRegularUser} onLogout={mockOnLogout} />
|
|
);
|
|
|
|
unmount();
|
|
|
|
expect(removeEventListenerSpy).toHaveBeenCalledWith(
|
|
'mousedown',
|
|
expect.any(Function)
|
|
);
|
|
|
|
removeEventListenerSpy.mockRestore();
|
|
});
|
|
});
|
|
});
|