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()
+ })
+})