Refactor database services and add component tests
- Remove deprecated CouchDB service files - Update database test configurations - Add test files for components and auth modules - Update user context and admin interface - Remove migration script for unified config - Fix User interface properties in tests (use status instead of isActive)
This commit is contained in:
334
components/__tests__/example.component.test.tsx
Normal file
334
components/__tests__/example.component.test.tsx
Normal file
@@ -0,0 +1,334 @@
|
||||
import React from 'react';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
// Mock component for demonstration purposes
|
||||
// This would normally import an actual component
|
||||
const MockButton: React.FC<{
|
||||
onClick: () => void;
|
||||
children: React.ReactNode;
|
||||
disabled?: boolean;
|
||||
}> = ({ onClick, children, disabled = false }) => (
|
||||
<button
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
data-testid='mock-button'
|
||||
className={`btn ${disabled ? 'btn-disabled' : 'btn-primary'}`}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
|
||||
const MockMedicationCard: React.FC<{
|
||||
medication: {
|
||||
id: string;
|
||||
name: string;
|
||||
dosage: string;
|
||||
frequency: string;
|
||||
};
|
||||
onEdit: (id: string) => void;
|
||||
onDelete: (id: string) => void;
|
||||
}> = ({ medication, onEdit, onDelete }) => (
|
||||
<div data-testid='medication-card' className='medication-card'>
|
||||
<h3 data-testid='medication-name'>{medication.name}</h3>
|
||||
<p data-testid='medication-dosage'>Dosage: {medication.dosage}</p>
|
||||
<p data-testid='medication-frequency'>Frequency: {medication.frequency}</p>
|
||||
<div className='actions'>
|
||||
<MockButton onClick={() => onEdit(medication.id)}>Edit</MockButton>
|
||||
<MockButton onClick={() => onDelete(medication.id)}>Delete</MockButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
describe('Component Testing Examples', () => {
|
||||
describe('MockButton Component', () => {
|
||||
test('renders button with correct text', () => {
|
||||
const mockClick = jest.fn();
|
||||
render(<MockButton onClick={mockClick}>Click Me</MockButton>);
|
||||
|
||||
const button = screen.getByTestId('mock-button');
|
||||
expect(button).toBeInTheDocument();
|
||||
expect(button).toHaveTextContent('Click Me');
|
||||
});
|
||||
|
||||
test('calls onClick handler when clicked', () => {
|
||||
const mockClick = jest.fn();
|
||||
render(<MockButton onClick={mockClick}>Click Me</MockButton>);
|
||||
|
||||
const button = screen.getByTestId('mock-button');
|
||||
fireEvent.click(button);
|
||||
|
||||
expect(mockClick).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('applies disabled state correctly', () => {
|
||||
const mockClick = jest.fn();
|
||||
render(
|
||||
<MockButton onClick={mockClick} disabled>
|
||||
Disabled Button
|
||||
</MockButton>
|
||||
);
|
||||
|
||||
const button = screen.getByTestId('mock-button');
|
||||
expect(button).toBeDisabled();
|
||||
expect(button).toHaveClass('btn-disabled');
|
||||
|
||||
fireEvent.click(button);
|
||||
expect(mockClick).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('applies correct CSS classes', () => {
|
||||
const mockClick = jest.fn();
|
||||
const { rerender } = render(
|
||||
<MockButton onClick={mockClick}>Normal Button</MockButton>
|
||||
);
|
||||
|
||||
let button = screen.getByTestId('mock-button');
|
||||
expect(button).toHaveClass('btn', 'btn-primary');
|
||||
expect(button).not.toHaveClass('btn-disabled');
|
||||
|
||||
rerender(
|
||||
<MockButton onClick={mockClick} disabled>
|
||||
Disabled Button
|
||||
</MockButton>
|
||||
);
|
||||
|
||||
button = screen.getByTestId('mock-button');
|
||||
expect(button).toHaveClass('btn', 'btn-disabled');
|
||||
expect(button).not.toHaveClass('btn-primary');
|
||||
});
|
||||
});
|
||||
|
||||
describe('MockMedicationCard Component', () => {
|
||||
const mockMedication = {
|
||||
id: 'med-123',
|
||||
name: 'Aspirin',
|
||||
dosage: '100mg',
|
||||
frequency: 'Daily',
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
medication: mockMedication,
|
||||
onEdit: jest.fn(),
|
||||
onDelete: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('renders medication information correctly', () => {
|
||||
render(<MockMedicationCard {...defaultProps} />);
|
||||
|
||||
expect(screen.getByTestId('medication-card')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('medication-name')).toHaveTextContent(
|
||||
'Aspirin'
|
||||
);
|
||||
expect(screen.getByTestId('medication-dosage')).toHaveTextContent(
|
||||
'Dosage: 100mg'
|
||||
);
|
||||
expect(screen.getByTestId('medication-frequency')).toHaveTextContent(
|
||||
'Frequency: Daily'
|
||||
);
|
||||
});
|
||||
|
||||
test('renders action buttons', () => {
|
||||
render(<MockMedicationCard {...defaultProps} />);
|
||||
|
||||
const editButton = screen.getByText('Edit');
|
||||
const deleteButton = screen.getByText('Delete');
|
||||
|
||||
expect(editButton).toBeInTheDocument();
|
||||
expect(deleteButton).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('calls onEdit with correct medication ID when edit button is clicked', () => {
|
||||
const mockOnEdit = jest.fn();
|
||||
render(<MockMedicationCard {...defaultProps} onEdit={mockOnEdit} />);
|
||||
|
||||
const editButton = screen.getByText('Edit');
|
||||
fireEvent.click(editButton);
|
||||
|
||||
expect(mockOnEdit).toHaveBeenCalledTimes(1);
|
||||
expect(mockOnEdit).toHaveBeenCalledWith('med-123');
|
||||
});
|
||||
|
||||
test('calls onDelete with correct medication ID when delete button is clicked', () => {
|
||||
const mockOnDelete = jest.fn();
|
||||
render(<MockMedicationCard {...defaultProps} onDelete={mockOnDelete} />);
|
||||
|
||||
const deleteButton = screen.getByText('Delete');
|
||||
fireEvent.click(deleteButton);
|
||||
|
||||
expect(mockOnDelete).toHaveBeenCalledTimes(1);
|
||||
expect(mockOnDelete).toHaveBeenCalledWith('med-123');
|
||||
});
|
||||
|
||||
test('handles different medication data', () => {
|
||||
const differentMedication = {
|
||||
id: 'med-456',
|
||||
name: 'Vitamin D',
|
||||
dosage: '1000 IU',
|
||||
frequency: 'Weekly',
|
||||
};
|
||||
|
||||
render(
|
||||
<MockMedicationCard
|
||||
{...defaultProps}
|
||||
medication={differentMedication}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('medication-name')).toHaveTextContent(
|
||||
'Vitamin D'
|
||||
);
|
||||
expect(screen.getByTestId('medication-dosage')).toHaveTextContent(
|
||||
'Dosage: 1000 IU'
|
||||
);
|
||||
expect(screen.getByTestId('medication-frequency')).toHaveTextContent(
|
||||
'Frequency: Weekly'
|
||||
);
|
||||
});
|
||||
|
||||
test('maintains proper component structure', () => {
|
||||
render(<MockMedicationCard {...defaultProps} />);
|
||||
|
||||
const card = screen.getByTestId('medication-card');
|
||||
expect(card).toHaveClass('medication-card');
|
||||
|
||||
const actionsContainer = card.querySelector('.actions');
|
||||
expect(actionsContainer).toBeInTheDocument();
|
||||
|
||||
// Verify buttons are within the actions container
|
||||
const buttons = actionsContainer?.querySelectorAll('button');
|
||||
expect(buttons).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Component Integration Tests', () => {
|
||||
test('multiple components work together', () => {
|
||||
const medications = [
|
||||
{
|
||||
id: 'med-1',
|
||||
name: 'Aspirin',
|
||||
dosage: '100mg',
|
||||
frequency: 'Daily',
|
||||
},
|
||||
{
|
||||
id: 'med-2',
|
||||
name: 'Vitamin C',
|
||||
dosage: '500mg',
|
||||
frequency: 'Twice daily',
|
||||
},
|
||||
];
|
||||
|
||||
const MockMedicationList: React.FC<{
|
||||
medications: typeof medications;
|
||||
onEdit: (id: string) => void;
|
||||
onDelete: (id: string) => void;
|
||||
}> = ({ medications, onEdit, onDelete }) => (
|
||||
<div data-testid='medication-list'>
|
||||
{medications.map(med => (
|
||||
<MockMedicationCard
|
||||
key={med.id}
|
||||
medication={med}
|
||||
onEdit={onEdit}
|
||||
onDelete={onDelete}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
const mockOnEdit = jest.fn();
|
||||
const mockOnDelete = jest.fn();
|
||||
|
||||
render(
|
||||
<MockMedicationList
|
||||
medications={medications}
|
||||
onEdit={mockOnEdit}
|
||||
onDelete={mockOnDelete}
|
||||
/>
|
||||
);
|
||||
|
||||
// Verify all medications are rendered
|
||||
expect(screen.getAllByTestId('medication-card')).toHaveLength(2);
|
||||
expect(screen.getByText('Aspirin')).toBeInTheDocument();
|
||||
expect(screen.getByText('Vitamin C')).toBeInTheDocument();
|
||||
|
||||
// Test interaction with first medication
|
||||
const editButtons = screen.getAllByText('Edit');
|
||||
fireEvent.click(editButtons[0]);
|
||||
expect(mockOnEdit).toHaveBeenCalledWith('med-1');
|
||||
|
||||
// Test interaction with second medication
|
||||
const deleteButtons = screen.getAllByText('Delete');
|
||||
fireEvent.click(deleteButtons[1]);
|
||||
expect(mockOnDelete).toHaveBeenCalledWith('med-2');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Accessibility Tests', () => {
|
||||
test('buttons have proper accessibility attributes', () => {
|
||||
const mockClick = jest.fn();
|
||||
render(<MockButton onClick={mockClick}>Accessible Button</MockButton>);
|
||||
|
||||
const button = screen.getByRole('button');
|
||||
expect(button).toBeInTheDocument();
|
||||
expect(button).toHaveAccessibleName('Accessible Button');
|
||||
});
|
||||
|
||||
test('medication card structure supports screen readers', () => {
|
||||
render(
|
||||
<MockMedicationCard
|
||||
{...{
|
||||
medication: {
|
||||
id: 'med-123',
|
||||
name: 'Test Medicine',
|
||||
dosage: '50mg',
|
||||
frequency: 'As needed',
|
||||
},
|
||||
onEdit: jest.fn(),
|
||||
onDelete: jest.fn(),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
// Check that important information is properly structured
|
||||
const heading = screen.getByRole('heading', { level: 3 });
|
||||
expect(heading).toHaveTextContent('Test Medicine');
|
||||
|
||||
const editButton = screen.getByRole('button', { name: 'Edit' });
|
||||
const deleteButton = screen.getByRole('button', { name: 'Delete' });
|
||||
|
||||
expect(editButton).toBeInTheDocument();
|
||||
expect(deleteButton).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error Handling', () => {
|
||||
test('components handle missing props gracefully', () => {
|
||||
// Test with minimal props
|
||||
const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {
|
||||
// Mock implementation for testing
|
||||
});
|
||||
|
||||
render(
|
||||
<MockMedicationCard
|
||||
medication={{
|
||||
id: '',
|
||||
name: '',
|
||||
dosage: '',
|
||||
frequency: '',
|
||||
}}
|
||||
onEdit={jest.fn()}
|
||||
onDelete={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
// Component should still render even with empty data
|
||||
expect(screen.getByTestId('medication-card')).toBeInTheDocument();
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { User, UserRole } from '../../types';
|
||||
import { AccountStatus } from '../../services/auth/auth.constants';
|
||||
import { dbService } from '../../services/couchdb.factory';
|
||||
import { databaseService } from '../../services/database';
|
||||
import { useUser } from '../../contexts/UserContext';
|
||||
|
||||
interface AdminInterfaceProps {
|
||||
@@ -22,8 +22,8 @@ const AdminInterface: React.FC<AdminInterfaceProps> = ({ onClose }) => {
|
||||
|
||||
const loadUsers = async () => {
|
||||
try {
|
||||
const allUsers = await dbService.getAllUsers();
|
||||
setUsers(allUsers);
|
||||
const users = await databaseService.getAllUsers();
|
||||
setUsers(users);
|
||||
} catch (error) {
|
||||
setError('Failed to load users');
|
||||
console.error('Error loading users:', error);
|
||||
@@ -34,7 +34,7 @@ const AdminInterface: React.FC<AdminInterfaceProps> = ({ onClose }) => {
|
||||
|
||||
const handleSuspendUser = async (userId: string) => {
|
||||
try {
|
||||
await dbService.suspendUser(userId);
|
||||
await databaseService.suspendUser(userId);
|
||||
await loadUsers();
|
||||
} catch (error) {
|
||||
setError('Failed to suspend user');
|
||||
@@ -44,7 +44,7 @@ const AdminInterface: React.FC<AdminInterfaceProps> = ({ onClose }) => {
|
||||
|
||||
const handleActivateUser = async (userId: string) => {
|
||||
try {
|
||||
await dbService.activateUser(userId);
|
||||
await databaseService.activateUser(userId);
|
||||
await loadUsers();
|
||||
} catch (error) {
|
||||
setError('Failed to activate user');
|
||||
@@ -62,7 +62,7 @@ const AdminInterface: React.FC<AdminInterfaceProps> = ({ onClose }) => {
|
||||
}
|
||||
|
||||
try {
|
||||
await dbService.deleteUser(userId);
|
||||
await databaseService.deleteUser(userId);
|
||||
await loadUsers();
|
||||
} catch (error) {
|
||||
setError('Failed to delete user');
|
||||
@@ -77,7 +77,7 @@ const AdminInterface: React.FC<AdminInterfaceProps> = ({ onClose }) => {
|
||||
}
|
||||
|
||||
try {
|
||||
await dbService.changeUserPassword(userId, newPassword);
|
||||
await databaseService.changeUserPassword(userId, newPassword);
|
||||
setNewPassword('');
|
||||
setSelectedUser(null);
|
||||
setError('');
|
||||
|
||||
475
components/auth/__tests__/AvatarDropdown.test.tsx
Normal file
475
components/auth/__tests__/AvatarDropdown.test.tsx
Normal file
@@ -0,0 +1,475 @@
|
||||
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');
|
||||
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
|
||||
fireEvent.click(button);
|
||||
fireEvent.click(button);
|
||||
fireEvent.click(button);
|
||||
|
||||
// Should end up closed
|
||||
expect(screen.queryByText('Signed in as')).not.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();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user