/** * 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; 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( ); 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( ); 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( ); 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( ); 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( ); 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( ); 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( ); 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( ); 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( ); 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( ); 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( ); 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( ); 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( ); 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( ); 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( ); 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( ); 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( ); 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( ); 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( ); 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( ); 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( ); 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( ); 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'); }); }); }); });