Files
syndarix/frontend/tests/components/admin/users/UserActionMenu.test.tsx
Felipe Cardoso b2f3ec8f25 Refactor ESLint configuration and update test rules for clarity and consistency
- Consolidated and modularized `eslint.config.mjs` with defined rules for source, test, E2E, and scripts.
- Improved test and E2E rules with relaxed settings for flexibility and enhanced mocking.
- Standardized variable naming and removed redundant imports in unit and E2E tests.
- Updated error handling and comments to align with modern TypeScript best practices (e.g., `@ts-expect-error`).
2025-11-10 10:57:43 +01:00

601 lines
16 KiB
TypeScript

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