424 lines
12 KiB
TypeScript
424 lines
12 KiB
TypeScript
|
|
import { render, screen } from '@testing-library/react';
|
||
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||
|
|
import userEvent from '@testing-library/user-event';
|
||
|
|
import { Select } from './select';
|
||
|
|
|
||
|
|
describe('Select Component', () => {
|
||
|
|
const mockOnChange = vi.fn();
|
||
|
|
const options = [
|
||
|
|
{ value: 'option1', label: 'Option 1' },
|
||
|
|
{ value: 'option2', label: 'Option 2' },
|
||
|
|
{ value: 'option3', label: 'Option 3' },
|
||
|
|
];
|
||
|
|
|
||
|
|
beforeEach(() => {
|
||
|
|
vi.clearAllMocks();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('renders select trigger correctly', () => {
|
||
|
|
render(
|
||
|
|
<Select
|
||
|
|
options={options}
|
||
|
|
onChange={mockOnChange}
|
||
|
|
placeholder="Select..."
|
||
|
|
/>,
|
||
|
|
);
|
||
|
|
|
||
|
|
expect(screen.getByText('Select...')).toBeInTheDocument();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('opens dropdown when trigger is clicked', async () => {
|
||
|
|
const user = userEvent.setup();
|
||
|
|
render(<Select options={options} onChange={mockOnChange} />);
|
||
|
|
|
||
|
|
const triggers = screen.getAllByRole('button');
|
||
|
|
const trigger =
|
||
|
|
triggers.find((btn) =>
|
||
|
|
btn.textContent?.includes('Select an option...'),
|
||
|
|
) || triggers[0];
|
||
|
|
await user.click(trigger);
|
||
|
|
|
||
|
|
await waitFor(() => {
|
||
|
|
expect(screen.getByText('Option 1')).toBeInTheDocument();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
it('displays selected value for single select', () => {
|
||
|
|
render(
|
||
|
|
<Select options={options} value="option1" onChange={mockOnChange} />,
|
||
|
|
);
|
||
|
|
|
||
|
|
expect(screen.getByText('Option 1')).toBeInTheDocument();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('displays selected count for multi-select', () => {
|
||
|
|
render(
|
||
|
|
<Select
|
||
|
|
options={options}
|
||
|
|
value={['option1', 'option2']}
|
||
|
|
onChange={mockOnChange}
|
||
|
|
multiple
|
||
|
|
/>,
|
||
|
|
);
|
||
|
|
|
||
|
|
expect(screen.getByText('2 selected')).toBeInTheDocument();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('calls onChange when option is selected in single mode', async () => {
|
||
|
|
const user = userEvent.setup();
|
||
|
|
render(<Select options={options} onChange={mockOnChange} />);
|
||
|
|
|
||
|
|
const triggers = screen.getAllByRole('button');
|
||
|
|
const trigger =
|
||
|
|
triggers.find((btn) =>
|
||
|
|
btn.textContent?.includes('Select an option...'),
|
||
|
|
) || triggers[0];
|
||
|
|
await user.click(trigger);
|
||
|
|
|
||
|
|
await waitFor(() => {
|
||
|
|
expect(screen.getByText('Option 1')).toBeInTheDocument();
|
||
|
|
});
|
||
|
|
|
||
|
|
const option1 = screen.getByText('Option 1');
|
||
|
|
await user.click(option1);
|
||
|
|
|
||
|
|
expect(mockOnChange).toHaveBeenCalledWith('option1');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('calls onChange when option is selected in multi mode', async () => {
|
||
|
|
const user = userEvent.setup();
|
||
|
|
render(<Select options={options} onChange={mockOnChange} multiple />);
|
||
|
|
|
||
|
|
const triggers = screen.getAllByRole('button');
|
||
|
|
const trigger =
|
||
|
|
triggers.find((btn) =>
|
||
|
|
btn.textContent?.includes('Select an option...'),
|
||
|
|
) || triggers[0];
|
||
|
|
await user.click(trigger);
|
||
|
|
|
||
|
|
await waitFor(() => {
|
||
|
|
expect(screen.getByText('Option 1')).toBeInTheDocument();
|
||
|
|
});
|
||
|
|
|
||
|
|
const option1 = screen.getByText('Option 1');
|
||
|
|
await user.click(option1);
|
||
|
|
|
||
|
|
expect(mockOnChange).toHaveBeenCalledWith(['option1']);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('toggles option selection in multi mode', async () => {
|
||
|
|
const user = userEvent.setup();
|
||
|
|
render(
|
||
|
|
<Select
|
||
|
|
options={options}
|
||
|
|
value={['option1']}
|
||
|
|
onChange={mockOnChange}
|
||
|
|
multiple
|
||
|
|
/>,
|
||
|
|
);
|
||
|
|
|
||
|
|
const triggers = screen.getAllByRole('button');
|
||
|
|
const trigger =
|
||
|
|
triggers.find((btn) => btn.textContent?.includes('1 selected')) ||
|
||
|
|
triggers[0];
|
||
|
|
await user.click(trigger);
|
||
|
|
|
||
|
|
await waitFor(() => {
|
||
|
|
expect(screen.getByText('Option 1')).toBeInTheDocument();
|
||
|
|
});
|
||
|
|
|
||
|
|
const option1 = screen.getByText('Option 1');
|
||
|
|
await user.click(option1);
|
||
|
|
|
||
|
|
expect(mockOnChange).toHaveBeenCalledWith([]);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('filters options when searchable is enabled', async () => {
|
||
|
|
const user = userEvent.setup();
|
||
|
|
render(<Select options={options} onChange={mockOnChange} searchable />);
|
||
|
|
|
||
|
|
const triggers = screen.getAllByRole('button');
|
||
|
|
const trigger =
|
||
|
|
triggers.find((btn) =>
|
||
|
|
btn.textContent?.includes('Select an option...'),
|
||
|
|
) || triggers[0];
|
||
|
|
await user.click(trigger);
|
||
|
|
|
||
|
|
await waitFor(() => {
|
||
|
|
expect(screen.getByPlaceholderText('Search...')).toBeInTheDocument();
|
||
|
|
});
|
||
|
|
|
||
|
|
// Attendre que les options soient rendues
|
||
|
|
await waitFor(() => {
|
||
|
|
expect(screen.getAllByRole('menuitem').length).toBeGreaterThan(0);
|
||
|
|
});
|
||
|
|
|
||
|
|
const searchInput = screen.getByPlaceholderText('Search...');
|
||
|
|
await user.type(searchInput, 'Option 1');
|
||
|
|
|
||
|
|
await waitFor(
|
||
|
|
() => {
|
||
|
|
const menuItems = screen.getAllByRole('menuitem');
|
||
|
|
const option1Item = menuItems.find((item) =>
|
||
|
|
item.textContent?.includes('Option 1'),
|
||
|
|
);
|
||
|
|
expect(option1Item).toBeInTheDocument();
|
||
|
|
const option2Item = menuItems.find((item) =>
|
||
|
|
item.textContent?.includes('Option 2'),
|
||
|
|
);
|
||
|
|
expect(option2Item).not.toBeInTheDocument();
|
||
|
|
},
|
||
|
|
{ timeout: 2000 },
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('displays grouped options', async () => {
|
||
|
|
const user = userEvent.setup();
|
||
|
|
const groupedOptions = [
|
||
|
|
{ value: 'opt1', label: 'Option 1', group: 'Group 1' },
|
||
|
|
{ value: 'opt2', label: 'Option 2', group: 'Group 1' },
|
||
|
|
{ value: 'opt3', label: 'Option 3', group: 'Group 2' },
|
||
|
|
];
|
||
|
|
|
||
|
|
render(<Select options={groupedOptions} onChange={mockOnChange} />);
|
||
|
|
|
||
|
|
const triggers = screen.getAllByRole('button');
|
||
|
|
const trigger =
|
||
|
|
triggers.find((btn) =>
|
||
|
|
btn.textContent?.includes('Select an option...'),
|
||
|
|
) || triggers[0];
|
||
|
|
await user.click(trigger);
|
||
|
|
|
||
|
|
await waitFor(() => {
|
||
|
|
// Attendre que les options soient rendues
|
||
|
|
expect(screen.getAllByRole('menuitem').length).toBeGreaterThan(0);
|
||
|
|
});
|
||
|
|
|
||
|
|
// Chercher les labels de groupe
|
||
|
|
const groupLabels = Array.from(document.querySelectorAll('.text-xs'));
|
||
|
|
const group1Label = groupLabels.find(
|
||
|
|
(el) => el.textContent?.trim() === 'GROUP 1',
|
||
|
|
);
|
||
|
|
const group2Label = groupLabels.find(
|
||
|
|
(el) => el.textContent?.trim() === 'GROUP 2',
|
||
|
|
);
|
||
|
|
expect(group1Label).toBeInTheDocument();
|
||
|
|
expect(group2Label).toBeInTheDocument();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('filters grouped options when searching', async () => {
|
||
|
|
const user = userEvent.setup();
|
||
|
|
const groupedOptions = [
|
||
|
|
{ value: 'opt1', label: 'Apple', group: 'Fruits' },
|
||
|
|
{ value: 'opt2', label: 'Banana', group: 'Fruits' },
|
||
|
|
{ value: 'opt3', label: 'Carrot', group: 'Vegetables' },
|
||
|
|
];
|
||
|
|
|
||
|
|
render(
|
||
|
|
<Select options={groupedOptions} onChange={mockOnChange} searchable />,
|
||
|
|
);
|
||
|
|
|
||
|
|
const triggers = screen.getAllByRole('button');
|
||
|
|
const trigger =
|
||
|
|
triggers.find((btn) =>
|
||
|
|
btn.textContent?.includes('Select an option...'),
|
||
|
|
) || triggers[0];
|
||
|
|
await user.click(trigger);
|
||
|
|
|
||
|
|
await waitFor(() => {
|
||
|
|
expect(screen.getByPlaceholderText('Search...')).toBeInTheDocument();
|
||
|
|
});
|
||
|
|
|
||
|
|
const searchInput = screen.getByPlaceholderText('Search...');
|
||
|
|
await user.type(searchInput, 'Apple');
|
||
|
|
|
||
|
|
expect(screen.getByText('Apple')).toBeInTheDocument();
|
||
|
|
expect(screen.queryByText('Banana')).not.toBeInTheDocument();
|
||
|
|
expect(screen.queryByText('Carrot')).not.toBeInTheDocument();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('shows checkmark for selected option in single mode', async () => {
|
||
|
|
const user = userEvent.setup();
|
||
|
|
render(
|
||
|
|
<Select options={options} value="option1" onChange={mockOnChange} />,
|
||
|
|
);
|
||
|
|
|
||
|
|
const triggers = screen.getAllByRole('button');
|
||
|
|
const trigger =
|
||
|
|
triggers.find(
|
||
|
|
(btn) =>
|
||
|
|
btn.textContent?.includes('Option 1') &&
|
||
|
|
!btn.textContent?.includes('truncate'),
|
||
|
|
) || triggers[0];
|
||
|
|
await user.click(trigger);
|
||
|
|
|
||
|
|
await waitFor(() => {
|
||
|
|
const menuItems = screen.getAllByRole('menuitem');
|
||
|
|
const option1Item = menuItems.find(
|
||
|
|
(item) => item.textContent?.trim() === 'Option 1',
|
||
|
|
);
|
||
|
|
expect(option1Item).toBeInTheDocument();
|
||
|
|
expect(option1Item?.querySelector('svg')).toBeInTheDocument();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
it('shows checkbox for multi-select mode', async () => {
|
||
|
|
const user = userEvent.setup();
|
||
|
|
render(
|
||
|
|
<Select
|
||
|
|
options={options}
|
||
|
|
value={['option1']}
|
||
|
|
onChange={mockOnChange}
|
||
|
|
multiple
|
||
|
|
/>,
|
||
|
|
);
|
||
|
|
|
||
|
|
const triggers = screen.getAllByRole('button');
|
||
|
|
const trigger =
|
||
|
|
triggers.find((btn) => btn.textContent?.includes('1 selected')) ||
|
||
|
|
triggers[0];
|
||
|
|
await user.click(trigger);
|
||
|
|
|
||
|
|
await waitFor(() => {
|
||
|
|
const option1 = screen.getByText('Option 1').closest('[role="menuitem"]');
|
||
|
|
const checkbox = option1?.querySelector('.border');
|
||
|
|
expect(checkbox).toBeInTheDocument();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
it('disables option when disabled prop is true', async () => {
|
||
|
|
const user = userEvent.setup();
|
||
|
|
const optionsWithDisabled = [
|
||
|
|
{ value: 'opt1', label: 'Option 1' },
|
||
|
|
{ value: 'opt2', label: 'Option 2', disabled: true },
|
||
|
|
];
|
||
|
|
|
||
|
|
render(<Select options={optionsWithDisabled} onChange={mockOnChange} />);
|
||
|
|
|
||
|
|
const triggers = screen.getAllByRole('button');
|
||
|
|
const trigger =
|
||
|
|
triggers.find((btn) =>
|
||
|
|
btn.textContent?.includes('Select an option...'),
|
||
|
|
) || triggers[0];
|
||
|
|
await user.click(trigger);
|
||
|
|
|
||
|
|
await waitFor(() => {
|
||
|
|
const option2 = screen.getByText('Option 2').closest('[role="menuitem"]');
|
||
|
|
expect(option2).toHaveClass('opacity-50');
|
||
|
|
expect(option2).toHaveClass('cursor-not-allowed');
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
it('clears selection when clear button is clicked', async () => {
|
||
|
|
const user = userEvent.setup();
|
||
|
|
render(
|
||
|
|
<Select options={options} value="option1" onChange={mockOnChange} />,
|
||
|
|
);
|
||
|
|
|
||
|
|
const triggers = screen.getAllByRole('button');
|
||
|
|
const trigger = triggers.find((btn) =>
|
||
|
|
btn.textContent?.includes('Option 1'),
|
||
|
|
);
|
||
|
|
const clearButton = trigger?.querySelector('svg');
|
||
|
|
expect(clearButton).toBeInTheDocument();
|
||
|
|
|
||
|
|
if (clearButton) {
|
||
|
|
await user.click(clearButton);
|
||
|
|
expect(mockOnChange).toHaveBeenCalledWith('');
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
it('clears all selections in multi mode', async () => {
|
||
|
|
const user = userEvent.setup();
|
||
|
|
render(
|
||
|
|
<Select
|
||
|
|
options={options}
|
||
|
|
value={['option1', 'option2']}
|
||
|
|
onChange={mockOnChange}
|
||
|
|
multiple
|
||
|
|
/>,
|
||
|
|
);
|
||
|
|
|
||
|
|
const triggers = screen.getAllByRole('button');
|
||
|
|
const trigger = triggers.find((btn) =>
|
||
|
|
btn.textContent?.includes('2 selected'),
|
||
|
|
);
|
||
|
|
const clearButton = trigger?.querySelector('svg');
|
||
|
|
expect(clearButton).toBeInTheDocument();
|
||
|
|
|
||
|
|
if (clearButton) {
|
||
|
|
await user.click(clearButton);
|
||
|
|
expect(mockOnChange).toHaveBeenCalledWith([]);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
it('displays "No options found" when search yields no results', async () => {
|
||
|
|
const user = userEvent.setup();
|
||
|
|
render(<Select options={options} onChange={mockOnChange} searchable />);
|
||
|
|
|
||
|
|
const triggers = screen.getAllByRole('button');
|
||
|
|
const trigger =
|
||
|
|
triggers.find((btn) =>
|
||
|
|
btn.textContent?.includes('Select an option...'),
|
||
|
|
) || triggers[0];
|
||
|
|
await user.click(trigger);
|
||
|
|
|
||
|
|
await waitFor(() => {
|
||
|
|
expect(screen.getByPlaceholderText('Search...')).toBeInTheDocument();
|
||
|
|
});
|
||
|
|
|
||
|
|
const searchInput = screen.getByPlaceholderText('Search...');
|
||
|
|
await user.type(searchInput, 'nonexistent');
|
||
|
|
|
||
|
|
await waitFor(() => {
|
||
|
|
expect(screen.getByText('No options found')).toBeInTheDocument();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
it('renders hidden input with name attribute', () => {
|
||
|
|
render(
|
||
|
|
<Select
|
||
|
|
options={options}
|
||
|
|
value="option1"
|
||
|
|
onChange={mockOnChange}
|
||
|
|
name="test-select"
|
||
|
|
/>,
|
||
|
|
);
|
||
|
|
|
||
|
|
const hiddenInput = document.querySelector(
|
||
|
|
'input[type="hidden"][name="test-select"]',
|
||
|
|
);
|
||
|
|
expect(hiddenInput).toBeInTheDocument();
|
||
|
|
expect(hiddenInput).toHaveValue('option1');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('renders hidden input with comma-separated values for multi-select', () => {
|
||
|
|
render(
|
||
|
|
<Select
|
||
|
|
options={options}
|
||
|
|
value={['option1', 'option2']}
|
||
|
|
onChange={mockOnChange}
|
||
|
|
multiple
|
||
|
|
name="test-select"
|
||
|
|
/>,
|
||
|
|
);
|
||
|
|
|
||
|
|
const hiddenInput = document.querySelector(
|
||
|
|
'input[type="hidden"][name="test-select"]',
|
||
|
|
);
|
||
|
|
expect(hiddenInput).toBeInTheDocument();
|
||
|
|
expect(hiddenInput).toHaveValue('option1,option2');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('disables select when disabled prop is true', () => {
|
||
|
|
render(<Select options={options} onChange={mockOnChange} disabled />);
|
||
|
|
|
||
|
|
// Le Button dans le trigger devrait être disabled
|
||
|
|
const buttons = document.querySelectorAll('button');
|
||
|
|
const selectButton = Array.from(buttons).find(
|
||
|
|
(btn) => btn.textContent?.includes('Select an option...') && btn.disabled,
|
||
|
|
);
|
||
|
|
expect(selectButton).toBeInTheDocument();
|
||
|
|
});
|
||
|
|
});
|