Files
syndarix/frontend/tests/components/admin/users/UserListTable.test.tsx
Felipe Cardoso 0b192ce030 Update e2e tests and mocks for locale-based routing
- Adjusted assertions and navigation tests to include `/en` locale prefix for consistency.
- Updated next-intl and components-i18n mocks to support locale handling in tests.
- Renamed "Components" link and related references to "Design System" in homepage tests.
- Disabled typing delay in debounce test for improved test reliability.
2025-11-19 01:31:35 +01:00

438 lines
14 KiB
TypeScript

/**
* Tests for UserListTable Component
* Verifies rendering, search, filtering, pagination, and user interactions
*/
import { render, screen, waitFor, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { UserListTable } from '@/components/admin/users/UserListTable';
import type { User, PaginationMeta } from '@/lib/api/hooks/useAdmin';
// Mock UserActionMenu component
jest.mock('@/components/admin/users/UserActionMenu', () => ({
UserActionMenu: ({ user, isCurrentUser }: any) => (
<button data-testid={`action-menu-${user.id}`}>Actions {isCurrentUser && '(current)'}</button>
),
}));
describe('UserListTable', () => {
const mockUsers: User[] = [
{
id: '1',
email: 'user1@example.com',
first_name: 'Alice',
last_name: 'Smith',
is_active: true,
is_superuser: false,
created_at: '2025-01-01T00:00:00Z',
},
{
id: '2',
email: 'user2@example.com',
first_name: 'Bob',
last_name: null,
is_active: false,
is_superuser: true,
created_at: '2025-01-02T00:00:00Z',
},
];
const mockPagination: PaginationMeta = {
total: 2,
page: 1,
page_size: 20,
total_pages: 1,
has_next: false,
has_prev: false,
};
const defaultProps = {
users: mockUsers,
pagination: mockPagination,
isLoading: false,
selectedUsers: [],
onSelectUser: jest.fn(),
onSelectAll: jest.fn(),
onPageChange: jest.fn(),
onSearch: jest.fn(),
onFilterActive: jest.fn(),
onFilterSuperuser: jest.fn(),
onEditUser: jest.fn(),
currentUserId: undefined,
};
beforeEach(() => {
jest.clearAllMocks();
});
describe('Rendering', () => {
it('renders table with column headers', () => {
render(<UserListTable {...defaultProps} />);
expect(screen.getByText('Name')).toBeInTheDocument();
expect(screen.getByText('Email')).toBeInTheDocument();
expect(screen.getByText('Status')).toBeInTheDocument();
expect(screen.getByText('Superuser')).toBeInTheDocument();
expect(screen.getByText('Created')).toBeInTheDocument();
const actionsHeaders = screen.getAllByText('Actions');
expect(actionsHeaders.length).toBeGreaterThan(0);
});
it('renders user data in table rows', () => {
render(<UserListTable {...defaultProps} />);
expect(screen.getByText('Alice Smith')).toBeInTheDocument();
expect(screen.getByText('user1@example.com')).toBeInTheDocument();
expect(screen.getByText('Bob')).toBeInTheDocument();
expect(screen.getByText('user2@example.com')).toBeInTheDocument();
});
it('renders status badges correctly', () => {
render(<UserListTable {...defaultProps} />);
expect(screen.getByText('Active')).toBeInTheDocument();
expect(screen.getByText('Inactive')).toBeInTheDocument();
});
it('renders superuser icons correctly', () => {
render(<UserListTable {...defaultProps} />);
const yesIcons = screen.getAllByLabelText('Yes');
const noIcons = screen.getAllByLabelText('No');
expect(yesIcons).toHaveLength(1); // Bob is superuser
expect(noIcons).toHaveLength(1); // Alice is not superuser
});
it('formats dates correctly', () => {
render(<UserListTable {...defaultProps} />);
expect(screen.getByText('Jan 1, 2025')).toBeInTheDocument();
expect(screen.getByText('Jan 2, 2025')).toBeInTheDocument();
});
it('shows "You" badge for current user', () => {
render(<UserListTable {...defaultProps} currentUserId="1" />);
expect(screen.getByText('You')).toBeInTheDocument();
});
});
describe('Loading State', () => {
it('renders skeleton loaders when loading', () => {
render(<UserListTable {...defaultProps} isLoading={true} users={[]} />);
const skeletons = screen.getAllByRole('row').slice(1); // Exclude header row
expect(skeletons).toHaveLength(5); // 5 skeleton rows
});
it('does not render user data when loading', () => {
render(<UserListTable {...defaultProps} isLoading={true} />);
expect(screen.queryByText('Alice Smith')).not.toBeInTheDocument();
});
});
describe('Empty State', () => {
it('shows empty message when no users', () => {
render(
<UserListTable {...defaultProps} users={[]} pagination={{ ...mockPagination, total: 0 }} />
);
expect(screen.getByText('No users found. Try adjusting your filters.')).toBeInTheDocument();
});
it('does not render pagination when no users', () => {
render(
<UserListTable {...defaultProps} users={[]} pagination={{ ...mockPagination, total: 0 }} />
);
expect(screen.queryByText(/Showing/)).not.toBeInTheDocument();
});
});
describe('Search Functionality', () => {
it('renders search input', () => {
render(<UserListTable {...defaultProps} />);
const searchInput = screen.getByPlaceholderText('Search by name or email...');
expect(searchInput).toBeInTheDocument();
});
it('calls onSearch after debounce delay', async () => {
const user = userEvent.setup({ delay: null }); // Disable typing delay for debounce test
render(<UserListTable {...defaultProps} />);
const searchInput = screen.getByPlaceholderText('Search by name or email...');
await user.type(searchInput, 'alice');
// Should not call immediately (debounce is 300ms)
expect(defaultProps.onSearch).not.toHaveBeenCalled();
// Should call after debounce (300ms)
await waitFor(
() => {
expect(defaultProps.onSearch).toHaveBeenCalledWith('alice');
},
{ timeout: 500 }
);
});
it('updates search input value', async () => {
const user = userEvent.setup();
render(<UserListTable {...defaultProps} />);
const searchInput = screen.getByPlaceholderText(
'Search by name or email...'
) as HTMLInputElement;
await user.type(searchInput, 'test');
expect(searchInput.value).toBe('test');
});
});
describe('Filter Functionality', () => {
it('renders status filter dropdown', () => {
render(<UserListTable {...defaultProps} />);
expect(screen.getByText('All Status')).toBeInTheDocument();
});
it('renders user type filter dropdown', () => {
render(<UserListTable {...defaultProps} />);
// Find "All Users" in the filter dropdown (not the heading)
const selectTriggers = screen.getAllByRole('combobox');
const userTypeFilter = selectTriggers.find(
(trigger) => within(trigger).queryByText('All Users') !== null
);
expect(userTypeFilter).toBeInTheDocument();
});
// Note: Select component interaction tests are better suited for E2E tests
// Unit tests verify that the filters render correctly with proper callbacks
});
describe('Selection Functionality', () => {
it('renders select all checkbox', () => {
render(<UserListTable {...defaultProps} />);
const selectAllCheckbox = screen.getByLabelText('Select all users');
expect(selectAllCheckbox).toBeInTheDocument();
});
it('calls onSelectAll when select all checkbox is clicked', async () => {
const user = userEvent.setup();
render(<UserListTable {...defaultProps} />);
const selectAllCheckbox = screen.getByLabelText('Select all users');
await user.click(selectAllCheckbox);
expect(defaultProps.onSelectAll).toHaveBeenCalledWith(true);
});
it('renders individual user checkboxes', () => {
render(<UserListTable {...defaultProps} />);
expect(screen.getByLabelText('Select Alice Smith')).toBeInTheDocument();
expect(screen.getByLabelText('Select Bob')).toBeInTheDocument();
});
it('calls onSelectUser when individual checkbox is clicked', async () => {
const user = userEvent.setup();
render(<UserListTable {...defaultProps} />);
const userCheckbox = screen.getByLabelText('Select Alice Smith');
await user.click(userCheckbox);
expect(defaultProps.onSelectUser).toHaveBeenCalledWith('1');
});
it('checks individual checkbox when user is selected', () => {
render(<UserListTable {...defaultProps} selectedUsers={['1']} />);
const userCheckbox = screen.getByLabelText('Select Alice Smith');
expect(userCheckbox).toHaveAttribute('data-state', 'checked');
});
it('checks select all checkbox when all users are selected', () => {
render(<UserListTable {...defaultProps} selectedUsers={['1', '2']} />);
const selectAllCheckbox = screen.getByLabelText('Select all users');
expect(selectAllCheckbox).toHaveAttribute('data-state', 'checked');
});
it('disables checkbox for current user', () => {
render(<UserListTable {...defaultProps} currentUserId="1" />);
const currentUserCheckbox = screen.getByLabelText('Select Alice Smith');
expect(currentUserCheckbox).toBeDisabled();
});
it('disables select all checkbox when loading', () => {
render(<UserListTable {...defaultProps} isLoading={true} users={[]} />);
const selectAllCheckbox = screen.getByLabelText('Select all users');
expect(selectAllCheckbox).toBeDisabled();
});
it('disables select all checkbox when no users', () => {
render(
<UserListTable {...defaultProps} users={[]} pagination={{ ...mockPagination, total: 0 }} />
);
const selectAllCheckbox = screen.getByLabelText('Select all users');
expect(selectAllCheckbox).toBeDisabled();
});
});
describe('Pagination', () => {
const paginatedProps = {
...defaultProps,
pagination: {
total: 100,
page: 2,
page_size: 20,
total_pages: 5,
has_next: true,
has_prev: true,
},
};
it('renders pagination info', () => {
render(<UserListTable {...paginatedProps} />);
expect(screen.getByText(/Showing 21 to 40 of 100 users/)).toBeInTheDocument();
});
it('renders previous button', () => {
render(<UserListTable {...paginatedProps} />);
expect(screen.getByText('Previous')).toBeInTheDocument();
});
it('renders next button', () => {
render(<UserListTable {...paginatedProps} />);
expect(screen.getByText('Next')).toBeInTheDocument();
});
it('renders page number buttons', () => {
render(<UserListTable {...paginatedProps} />);
expect(screen.getByRole('button', { name: '1' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '2' })).toBeInTheDocument();
});
it('highlights current page button', () => {
render(<UserListTable {...paginatedProps} />);
const currentPageButton = screen.getByRole('button', { name: '2' });
expect(currentPageButton.className).toContain('bg-primary');
});
it('calls onPageChange when previous button is clicked', async () => {
const user = userEvent.setup();
render(<UserListTable {...paginatedProps} />);
const previousButton = screen.getByText('Previous');
await user.click(previousButton);
expect(defaultProps.onPageChange).toHaveBeenCalledWith(1);
});
it('calls onPageChange when next button is clicked', async () => {
const user = userEvent.setup();
render(<UserListTable {...paginatedProps} />);
const nextButton = screen.getByText('Next');
await user.click(nextButton);
expect(defaultProps.onPageChange).toHaveBeenCalledWith(3);
});
it('calls onPageChange when page number is clicked', async () => {
const user = userEvent.setup();
render(<UserListTable {...paginatedProps} />);
const pageButton = screen.getByRole('button', { name: '3' });
await user.click(pageButton);
expect(defaultProps.onPageChange).toHaveBeenCalledWith(3);
});
it('disables previous button on first page', () => {
render(
<UserListTable
{...paginatedProps}
pagination={{ ...paginatedProps.pagination, page: 1, has_prev: false }}
/>
);
const previousButton = screen.getByText('Previous');
expect(previousButton).toBeDisabled();
});
it('disables next button on last page', () => {
render(
<UserListTable
{...paginatedProps}
pagination={{ ...paginatedProps.pagination, page: 5, has_next: false }}
/>
);
const nextButton = screen.getByText('Next');
expect(nextButton).toBeDisabled();
});
it('shows ellipsis for skipped pages', () => {
render(
<UserListTable
{...paginatedProps}
pagination={{
...paginatedProps.pagination,
total_pages: 10,
page: 5,
}}
/>
);
const ellipses = screen.getAllByText('...');
expect(ellipses.length).toBeGreaterThan(0);
});
it('does not render pagination when loading', () => {
render(<UserListTable {...paginatedProps} isLoading={true} />);
expect(screen.queryByText(/Showing/)).not.toBeInTheDocument();
});
it('does not render pagination when no users', () => {
render(
<UserListTable {...defaultProps} users={[]} pagination={{ ...mockPagination, total: 0 }} />
);
expect(screen.queryByText(/Showing/)).not.toBeInTheDocument();
});
});
describe('User Actions', () => {
it('renders action menu for each user', () => {
render(<UserListTable {...defaultProps} />);
expect(screen.getByTestId('action-menu-1')).toBeInTheDocument();
expect(screen.getByTestId('action-menu-2')).toBeInTheDocument();
});
it('passes correct props to UserActionMenu', () => {
render(<UserListTable {...defaultProps} currentUserId="1" />);
expect(screen.getByText('Actions (current)')).toBeInTheDocument();
});
});
});