diff --git a/src/design-system/composites/MultiSelect/MultiSelect.test.tsx b/src/design-system/composites/MultiSelect/MultiSelect.test.tsx new file mode 100644 index 0000000..c1713ab --- /dev/null +++ b/src/design-system/composites/MultiSelect/MultiSelect.test.tsx @@ -0,0 +1,109 @@ +import { describe, it, expect, vi } from 'vitest' +import { render, screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { MultiSelect } from './MultiSelect' + +const OPTIONS = [ + { value: 'admin', label: 'ADMIN' }, + { value: 'editor', label: 'EDITOR' }, + { value: 'viewer', label: 'VIEWER' }, + { value: 'operator', label: 'OPERATOR' }, +] + +describe('MultiSelect', () => { + it('renders trigger with placeholder', () => { + render() + expect(screen.getByText('Select...')).toBeInTheDocument() + }) + + it('renders trigger with custom placeholder', () => { + render() + expect(screen.getByText('Add roles...')).toBeInTheDocument() + }) + + it('shows selected count on trigger', () => { + render() + expect(screen.getByText('2 selected')).toBeInTheDocument() + }) + + it('opens dropdown on trigger click', async () => { + const user = userEvent.setup() + render() + await user.click(screen.getByRole('combobox')) + expect(screen.getByText('ADMIN')).toBeInTheDocument() + expect(screen.getByText('EDITOR')).toBeInTheDocument() + }) + + it('shows checkboxes for pre-selected values', async () => { + const user = userEvent.setup() + render() + await user.click(screen.getByRole('combobox')) + const adminCheckbox = screen.getByRole('checkbox', { name: 'ADMIN' }) + expect(adminCheckbox).toBeChecked() + }) + + it('filters options by search text', async () => { + const user = userEvent.setup() + render() + await user.click(screen.getByRole('combobox')) + await user.type(screen.getByPlaceholderText('Search...'), 'adm') + expect(screen.getByText('ADMIN')).toBeInTheDocument() + expect(screen.queryByText('EDITOR')).not.toBeInTheDocument() + }) + + it('calls onChange with selected values on Apply', async () => { + const onChange = vi.fn() + const user = userEvent.setup() + render() + await user.click(screen.getByRole('combobox')) + await user.click(screen.getByRole('checkbox', { name: 'ADMIN' })) + await user.click(screen.getByRole('checkbox', { name: 'VIEWER' })) + await user.click(screen.getByRole('button', { name: /Apply/ })) + expect(onChange).toHaveBeenCalledWith(['admin', 'viewer']) + }) + + it('discards pending changes on Escape', async () => { + const onChange = vi.fn() + const user = userEvent.setup() + render() + await user.click(screen.getByRole('combobox')) + await user.click(screen.getByRole('checkbox', { name: 'ADMIN' })) + await user.keyboard('{Escape}') + expect(onChange).not.toHaveBeenCalled() + }) + + it('closes dropdown on outside click without applying', async () => { + const onChange = vi.fn() + const user = userEvent.setup() + render( +
+ + +
+ ) + await user.click(screen.getByRole('combobox')) + await user.click(screen.getByRole('checkbox', { name: 'ADMIN' })) + await user.click(screen.getByText('Outside')) + expect(onChange).not.toHaveBeenCalled() + }) + + it('disables trigger when disabled prop is set', () => { + render() + expect(screen.getByRole('combobox')).toHaveAttribute('aria-disabled', 'true') + }) + + it('hides search input when searchable is false', async () => { + const user = userEvent.setup() + render() + await user.click(screen.getByRole('combobox')) + expect(screen.queryByPlaceholderText('Search...')).not.toBeInTheDocument() + }) + + it('shows Apply button with count of pending changes', async () => { + const user = userEvent.setup() + render() + await user.click(screen.getByRole('combobox')) + await user.click(screen.getByRole('checkbox', { name: 'EDITOR' })) + expect(screen.getByRole('button', { name: /Apply \(2\)/ })).toBeInTheDocument() + }) +})