Add tests for BulkActionToolbar and UserFormDialog components, and comprehensive E2E tests for admin user management
- Added unit tests for `BulkActionToolbar` to verify visibility logic, button states, confirmation dialogs, and hook integration. - Implemented unit tests for `UserFormDialog` to ensure proper rendering, validation, and interaction. - Introduced end-to-end tests for admin user management functionality, including user list, creation, editing, search, filtering, pagination, and bulk actions. - Improved test coverage and reliability across admin user-related features.
This commit is contained in:
603
frontend/tests/components/admin/users/UserActionMenu.test.tsx
Normal file
603
frontend/tests/components/admin/users/UserActionMenu.test.tsx
Normal file
@@ -0,0 +1,603 @@
|
||||
/**
|
||||
* Tests for UserActionMenu Component
|
||||
* Verifies dropdown menu actions, confirmation dialogs, and user permissions
|
||||
*/
|
||||
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { UserActionMenu } from '@/components/admin/users/UserActionMenu';
|
||||
import {
|
||||
useActivateUser,
|
||||
useDeactivateUser,
|
||||
useDeleteUser,
|
||||
type User,
|
||||
} from '@/lib/api/hooks/useAdmin';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
// Mock dependencies
|
||||
jest.mock('@/lib/api/hooks/useAdmin', () => ({
|
||||
useActivateUser: jest.fn(),
|
||||
useDeactivateUser: jest.fn(),
|
||||
useDeleteUser: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('sonner', () => ({
|
||||
toast: {
|
||||
success: jest.fn(),
|
||||
error: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
const mockUseActivateUser = useActivateUser as jest.MockedFunction<
|
||||
typeof useActivateUser
|
||||
>;
|
||||
const mockUseDeactivateUser = useDeactivateUser as jest.MockedFunction<
|
||||
typeof useDeactivateUser
|
||||
>;
|
||||
const mockUseDeleteUser = useDeleteUser as jest.MockedFunction<typeof useDeleteUser>;
|
||||
|
||||
describe('UserActionMenu', () => {
|
||||
const mockUser: User = {
|
||||
id: '1',
|
||||
email: 'user@example.com',
|
||||
first_name: 'Test',
|
||||
last_name: 'User',
|
||||
is_active: true,
|
||||
is_superuser: false,
|
||||
created_at: '2025-01-01T00:00:00Z',
|
||||
};
|
||||
|
||||
const mockActivateMutate = jest.fn();
|
||||
const mockDeactivateMutate = jest.fn();
|
||||
const mockDeleteMutate = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
mockUseActivateUser.mockReturnValue({
|
||||
mutateAsync: mockActivateMutate,
|
||||
isPending: false,
|
||||
} as any);
|
||||
|
||||
mockUseDeactivateUser.mockReturnValue({
|
||||
mutateAsync: mockDeactivateMutate,
|
||||
isPending: false,
|
||||
} as any);
|
||||
|
||||
mockUseDeleteUser.mockReturnValue({
|
||||
mutateAsync: mockDeleteMutate,
|
||||
isPending: false,
|
||||
} as any);
|
||||
|
||||
mockActivateMutate.mockResolvedValue({});
|
||||
mockDeactivateMutate.mockResolvedValue({});
|
||||
mockDeleteMutate.mockResolvedValue({});
|
||||
});
|
||||
|
||||
describe('Menu Rendering', () => {
|
||||
it('renders menu trigger button', () => {
|
||||
render(
|
||||
<UserActionMenu
|
||||
user={mockUser}
|
||||
isCurrentUser={false}
|
||||
onEdit={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
const menuButton = screen.getByRole('button', {
|
||||
name: 'Actions for Test User',
|
||||
});
|
||||
expect(menuButton).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows menu items when opened', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<UserActionMenu
|
||||
user={mockUser}
|
||||
isCurrentUser={false}
|
||||
onEdit={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
const menuButton = screen.getByRole('button', {
|
||||
name: 'Actions for Test User',
|
||||
});
|
||||
await user.click(menuButton);
|
||||
|
||||
expect(screen.getByText('Edit User')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows deactivate option for active user', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<UserActionMenu
|
||||
user={mockUser}
|
||||
isCurrentUser={false}
|
||||
onEdit={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
const menuButton = screen.getByRole('button', {
|
||||
name: 'Actions for Test User',
|
||||
});
|
||||
await user.click(menuButton);
|
||||
|
||||
expect(screen.getByText('Deactivate')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Activate')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows activate option for inactive user', async () => {
|
||||
const user = userEvent.setup();
|
||||
const inactiveUser = { ...mockUser, is_active: false };
|
||||
|
||||
render(
|
||||
<UserActionMenu
|
||||
user={inactiveUser}
|
||||
isCurrentUser={false}
|
||||
onEdit={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
const menuButton = screen.getByRole('button', {
|
||||
name: 'Actions for Test User',
|
||||
});
|
||||
await user.click(menuButton);
|
||||
|
||||
expect(screen.getByText('Activate')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Deactivate')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows delete option', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<UserActionMenu
|
||||
user={mockUser}
|
||||
isCurrentUser={false}
|
||||
onEdit={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
const menuButton = screen.getByRole('button', {
|
||||
name: 'Actions for Test User',
|
||||
});
|
||||
await user.click(menuButton);
|
||||
|
||||
expect(screen.getByText('Delete User')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edit Action', () => {
|
||||
it('calls onEdit when edit is clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
const mockOnEdit = jest.fn();
|
||||
|
||||
render(
|
||||
<UserActionMenu
|
||||
user={mockUser}
|
||||
isCurrentUser={false}
|
||||
onEdit={mockOnEdit}
|
||||
/>
|
||||
);
|
||||
|
||||
const menuButton = screen.getByRole('button', {
|
||||
name: 'Actions for Test User',
|
||||
});
|
||||
await user.click(menuButton);
|
||||
|
||||
const editButton = screen.getByText('Edit User');
|
||||
await user.click(editButton);
|
||||
|
||||
expect(mockOnEdit).toHaveBeenCalledWith(mockUser);
|
||||
});
|
||||
|
||||
it('closes menu after edit is clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
const mockOnEdit = jest.fn();
|
||||
|
||||
render(
|
||||
<UserActionMenu
|
||||
user={mockUser}
|
||||
isCurrentUser={false}
|
||||
onEdit={mockOnEdit}
|
||||
/>
|
||||
);
|
||||
|
||||
const menuButton = screen.getByRole('button', {
|
||||
name: 'Actions for Test User',
|
||||
});
|
||||
await user.click(menuButton);
|
||||
|
||||
const editButton = screen.getByText('Edit User');
|
||||
await user.click(editButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('Edit User')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Activate Action', () => {
|
||||
it('activates user immediately without confirmation', async () => {
|
||||
const user = userEvent.setup();
|
||||
const inactiveUser = { ...mockUser, is_active: false };
|
||||
|
||||
render(
|
||||
<UserActionMenu
|
||||
user={inactiveUser}
|
||||
isCurrentUser={false}
|
||||
onEdit={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
const menuButton = screen.getByRole('button', {
|
||||
name: 'Actions for Test User',
|
||||
});
|
||||
await user.click(menuButton);
|
||||
|
||||
const activateButton = screen.getByText('Activate');
|
||||
await user.click(activateButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockActivateMutate).toHaveBeenCalledWith('1');
|
||||
});
|
||||
});
|
||||
|
||||
it('shows success toast on activation', async () => {
|
||||
const user = userEvent.setup();
|
||||
const inactiveUser = { ...mockUser, is_active: false };
|
||||
|
||||
render(
|
||||
<UserActionMenu
|
||||
user={inactiveUser}
|
||||
isCurrentUser={false}
|
||||
onEdit={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
const menuButton = screen.getByRole('button', {
|
||||
name: 'Actions for Test User',
|
||||
});
|
||||
await user.click(menuButton);
|
||||
|
||||
const activateButton = screen.getByText('Activate');
|
||||
await user.click(activateButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(toast.success).toHaveBeenCalledWith(
|
||||
'Test User has been activated successfully.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('shows error toast on activation failure', async () => {
|
||||
const user = userEvent.setup();
|
||||
const inactiveUser = { ...mockUser, is_active: false };
|
||||
|
||||
mockActivateMutate.mockRejectedValueOnce(new Error('Network error'));
|
||||
|
||||
render(
|
||||
<UserActionMenu
|
||||
user={inactiveUser}
|
||||
isCurrentUser={false}
|
||||
onEdit={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
const menuButton = screen.getByRole('button', {
|
||||
name: 'Actions for Test User',
|
||||
});
|
||||
await user.click(menuButton);
|
||||
|
||||
const activateButton = screen.getByText('Activate');
|
||||
await user.click(activateButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(toast.error).toHaveBeenCalledWith('Network error');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Deactivate Action', () => {
|
||||
it('shows confirmation dialog when deactivate is clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(
|
||||
<UserActionMenu
|
||||
user={mockUser}
|
||||
isCurrentUser={false}
|
||||
onEdit={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
const menuButton = screen.getByRole('button', {
|
||||
name: 'Actions for Test User',
|
||||
});
|
||||
await user.click(menuButton);
|
||||
|
||||
const deactivateButton = screen.getByText('Deactivate');
|
||||
await user.click(deactivateButton);
|
||||
|
||||
expect(screen.getByText('Deactivate User')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(
|
||||
/Are you sure you want to deactivate Test User\?/
|
||||
)
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows confirmation dialog when deactivate is clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(
|
||||
<UserActionMenu
|
||||
user={mockUser}
|
||||
isCurrentUser={false}
|
||||
onEdit={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
const menuButton = screen.getByRole('button', {
|
||||
name: 'Actions for Test User',
|
||||
});
|
||||
await user.click(menuButton);
|
||||
|
||||
const deactivateButton = screen.getByText('Deactivate');
|
||||
await user.click(deactivateButton);
|
||||
|
||||
// Verify dialog opens with correct content
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Deactivate User')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(/Are you sure you want to deactivate Test User\?/)
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('disables deactivate option for current user', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(
|
||||
<UserActionMenu
|
||||
user={mockUser}
|
||||
isCurrentUser={true}
|
||||
onEdit={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
const menuButton = screen.getByRole('button', {
|
||||
name: 'Actions for Test User',
|
||||
});
|
||||
await user.click(menuButton);
|
||||
|
||||
const deactivateButton = screen.getByText('Deactivate');
|
||||
// Radix UI disabled menu items use aria-disabled
|
||||
expect(deactivateButton).toHaveAttribute('aria-disabled', 'true');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Delete Action', () => {
|
||||
it('shows confirmation dialog when delete is clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(
|
||||
<UserActionMenu
|
||||
user={mockUser}
|
||||
isCurrentUser={false}
|
||||
onEdit={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
const menuButton = screen.getByRole('button', {
|
||||
name: 'Actions for Test User',
|
||||
});
|
||||
await user.click(menuButton);
|
||||
|
||||
const deleteButton = screen.getByText('Delete User');
|
||||
await user.click(deleteButton);
|
||||
|
||||
expect(screen.getByText('Delete User')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(/Are you sure you want to delete Test User\?/)
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(/This action cannot be undone\./)
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('deletes user when confirmed', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(
|
||||
<UserActionMenu
|
||||
user={mockUser}
|
||||
isCurrentUser={false}
|
||||
onEdit={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
const menuButton = screen.getByRole('button', {
|
||||
name: 'Actions for Test User',
|
||||
});
|
||||
await user.click(menuButton);
|
||||
|
||||
const deleteButton = screen.getByText('Delete User');
|
||||
await user.click(deleteButton);
|
||||
|
||||
const confirmButton = screen.getByRole('button', { name: 'Delete' });
|
||||
await user.click(confirmButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockDeleteMutate).toHaveBeenCalledWith('1');
|
||||
});
|
||||
});
|
||||
|
||||
it('cancels deletion when cancel is clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(
|
||||
<UserActionMenu
|
||||
user={mockUser}
|
||||
isCurrentUser={false}
|
||||
onEdit={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
const menuButton = screen.getByRole('button', {
|
||||
name: 'Actions for Test User',
|
||||
});
|
||||
await user.click(menuButton);
|
||||
|
||||
const deleteButton = screen.getByText('Delete User');
|
||||
await user.click(deleteButton);
|
||||
|
||||
const cancelButton = screen.getByRole('button', { name: 'Cancel' });
|
||||
await user.click(cancelButton);
|
||||
|
||||
expect(mockDeleteMutate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('shows success toast on deletion', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(
|
||||
<UserActionMenu
|
||||
user={mockUser}
|
||||
isCurrentUser={false}
|
||||
onEdit={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
const menuButton = screen.getByRole('button', {
|
||||
name: 'Actions for Test User',
|
||||
});
|
||||
await user.click(menuButton);
|
||||
|
||||
const deleteButton = screen.getByText('Delete User');
|
||||
await user.click(deleteButton);
|
||||
|
||||
const confirmButton = screen.getByRole('button', { name: 'Delete' });
|
||||
await user.click(confirmButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(toast.success).toHaveBeenCalledWith(
|
||||
'Test User has been deleted successfully.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('disables delete option for current user', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(
|
||||
<UserActionMenu
|
||||
user={mockUser}
|
||||
isCurrentUser={true}
|
||||
onEdit={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
const menuButton = screen.getByRole('button', {
|
||||
name: 'Actions for Test User',
|
||||
});
|
||||
await user.click(menuButton);
|
||||
|
||||
const deleteButton = screen.getByText('Delete User');
|
||||
// Radix UI disabled menu items use aria-disabled
|
||||
expect(deleteButton).toHaveAttribute('aria-disabled', 'true');
|
||||
});
|
||||
});
|
||||
|
||||
describe('User Name Display', () => {
|
||||
it('displays full name when last name is provided', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(
|
||||
<UserActionMenu
|
||||
user={mockUser}
|
||||
isCurrentUser={false}
|
||||
onEdit={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
const menuButton = screen.getByRole('button', {
|
||||
name: 'Actions for Test User',
|
||||
});
|
||||
expect(menuButton).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays first name only when last name is null', async () => {
|
||||
const user = userEvent.setup();
|
||||
const userWithoutLastName = { ...mockUser, last_name: null };
|
||||
|
||||
render(
|
||||
<UserActionMenu
|
||||
user={userWithoutLastName}
|
||||
isCurrentUser={false}
|
||||
onEdit={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
const menuButton = screen.getByRole('button', {
|
||||
name: 'Actions for Test',
|
||||
});
|
||||
expect(menuButton).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error Handling', () => {
|
||||
it('shows error toast with custom message on error', async () => {
|
||||
const user = userEvent.setup();
|
||||
const inactiveUser = { ...mockUser, is_active: false };
|
||||
|
||||
mockActivateMutate.mockRejectedValueOnce(new Error('Custom error'));
|
||||
|
||||
render(
|
||||
<UserActionMenu
|
||||
user={inactiveUser}
|
||||
isCurrentUser={false}
|
||||
onEdit={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
const menuButton = screen.getByRole('button', {
|
||||
name: 'Actions for Test User',
|
||||
});
|
||||
await user.click(menuButton);
|
||||
|
||||
const activateButton = screen.getByText('Activate');
|
||||
await user.click(activateButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(toast.error).toHaveBeenCalledWith('Custom error');
|
||||
});
|
||||
});
|
||||
|
||||
it('shows generic error message for non-Error objects', async () => {
|
||||
const user = userEvent.setup();
|
||||
const inactiveUser = { ...mockUser, is_active: false };
|
||||
|
||||
mockActivateMutate.mockRejectedValueOnce('String error');
|
||||
|
||||
render(
|
||||
<UserActionMenu
|
||||
user={inactiveUser}
|
||||
isCurrentUser={false}
|
||||
onEdit={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
const menuButton = screen.getByRole('button', {
|
||||
name: 'Actions for Test User',
|
||||
});
|
||||
await user.click(menuButton);
|
||||
|
||||
const activateButton = screen.getByText('Activate');
|
||||
await user.click(activateButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(toast.error).toHaveBeenCalledWith('Failed to activate user');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user