mirror of
https://github.com/langgenius/dify.git
synced 2026-05-04 01:18:05 +08:00
test: add tests for some base components (#32479)
This commit is contained in:
124
web/app/components/base/select/custom.spec.tsx
Normal file
124
web/app/components/base/select/custom.spec.tsx
Normal file
@ -0,0 +1,124 @@
|
||||
import type { Option } from './custom'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import CustomSelect from './custom'
|
||||
|
||||
const options: Option[] = [
|
||||
{ label: 'First option', value: 'first' },
|
||||
{ label: 'Second option', value: 'second' },
|
||||
]
|
||||
|
||||
describe('CustomSelect', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
// Rendering behavior and value fallback.
|
||||
describe('Rendering', () => {
|
||||
it('should show the placeholder when value is undefined or not found', () => {
|
||||
const { rerender } = render(
|
||||
<CustomSelect options={options} />,
|
||||
)
|
||||
|
||||
expect(screen.getByTitle(/select/i)).toBeInTheDocument()
|
||||
|
||||
rerender(
|
||||
<CustomSelect options={options} value="missing" />,
|
||||
)
|
||||
|
||||
expect(screen.getByTitle(/select/i)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// User interactions for opening and selecting options.
|
||||
describe('User Interactions', () => {
|
||||
it('should call onChange and close the popup when an option is selected', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onChange = vi.fn()
|
||||
|
||||
render(
|
||||
<CustomSelect options={options} onChange={onChange} />,
|
||||
)
|
||||
|
||||
await user.click(screen.getByTitle(/select/i))
|
||||
expect(screen.getByTitle('Second option')).toBeInTheDocument()
|
||||
|
||||
await user.click(screen.getByTitle('Second option'))
|
||||
expect(onChange).toHaveBeenCalledWith('second')
|
||||
expect(screen.queryByTitle('Second option')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// Controlled container props behavior.
|
||||
describe('Container Props', () => {
|
||||
it('should delegate open-state changes through containerProps.onOpenChange', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onOpenChange = vi.fn()
|
||||
|
||||
render(
|
||||
<CustomSelect
|
||||
options={options}
|
||||
containerProps={{ open: true, onOpenChange }}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByTitle('First option')).toBeInTheDocument()
|
||||
|
||||
await user.click(screen.getByTitle(/select/i))
|
||||
expect(onOpenChange).toHaveBeenCalledWith(false)
|
||||
})
|
||||
})
|
||||
|
||||
// Custom rendering hooks for trigger and options.
|
||||
describe('Custom Renderers', () => {
|
||||
it('should render CustomTrigger and CustomOption with selected state', async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
render(
|
||||
<CustomSelect
|
||||
options={options}
|
||||
value="first"
|
||||
CustomTrigger={(option, open) => <div>{`${option?.label ?? 'none'}-${open ? 'open' : 'closed'}`}</div>}
|
||||
CustomOption={(option, selected) => <div>{`${option.label}-${selected ? 'selected' : 'idle'}`}</div>}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByText('First option-closed')).toBeInTheDocument()
|
||||
|
||||
await user.click(screen.getByText('First option-closed'))
|
||||
|
||||
expect(screen.getByText('First option-open')).toBeInTheDocument()
|
||||
expect(screen.getByText('First option-selected')).toBeInTheDocument()
|
||||
expect(screen.getByText('Second option-idle')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// Class-based customization props.
|
||||
describe('Style Props', () => {
|
||||
it('should apply trigger and popup class names from props', async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
render(
|
||||
<CustomSelect
|
||||
options={options}
|
||||
triggerProps={{ className: 'trigger-class' }}
|
||||
popupProps={{
|
||||
wrapperClassName: 'wrapper-class',
|
||||
className: 'popup-class',
|
||||
itemClassName: 'item-class',
|
||||
}}
|
||||
/>,
|
||||
)
|
||||
|
||||
const triggerLabel = screen.getByTitle(/select/i)
|
||||
const trigger = triggerLabel.parentElement
|
||||
expect(trigger).toHaveClass('trigger-class')
|
||||
|
||||
await user.click(triggerLabel)
|
||||
|
||||
expect(document.querySelector('.wrapper-class')).toBeInTheDocument()
|
||||
expect(document.querySelector('.popup-class')).toBeInTheDocument()
|
||||
expect(document.querySelectorAll('.item-class')).toHaveLength(options.length)
|
||||
})
|
||||
})
|
||||
})
|
||||
216
web/app/components/base/select/index.spec.tsx
Normal file
216
web/app/components/base/select/index.spec.tsx
Normal file
@ -0,0 +1,216 @@
|
||||
import type { Item } from './index'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import Select, { PortalSelect, SimpleSelect } from './index'
|
||||
|
||||
const items: Item[] = [
|
||||
{ value: 'apple', name: 'Apple' },
|
||||
{ value: 'banana', name: 'Banana' },
|
||||
{ value: 'citrus', name: 'Citrus' },
|
||||
]
|
||||
|
||||
describe('Select', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
// Rendering and edge behavior for default select.
|
||||
describe('Rendering', () => {
|
||||
it('should show the default selected item when defaultValue matches an item', () => {
|
||||
render(
|
||||
<Select
|
||||
items={items}
|
||||
defaultValue="banana"
|
||||
allowSearch={false}
|
||||
onSelect={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByTitle('Banana')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// User interactions for default select.
|
||||
describe('User Interactions', () => {
|
||||
it('should call onSelect when choosing an option from default select', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onSelect = vi.fn()
|
||||
|
||||
render(
|
||||
<Select
|
||||
items={items}
|
||||
defaultValue="banana"
|
||||
allowSearch={false}
|
||||
onSelect={onSelect}
|
||||
/>,
|
||||
)
|
||||
|
||||
await user.click(screen.getByTitle('Banana'))
|
||||
await user.click(screen.getByText('Citrus'))
|
||||
|
||||
expect(onSelect).toHaveBeenCalledWith(expect.objectContaining({
|
||||
value: 'citrus',
|
||||
name: 'Citrus',
|
||||
}))
|
||||
})
|
||||
|
||||
it('should not open or select when default select is disabled', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onSelect = vi.fn()
|
||||
|
||||
render(
|
||||
<Select
|
||||
items={items}
|
||||
defaultValue="banana"
|
||||
allowSearch={false}
|
||||
disabled={true}
|
||||
onSelect={onSelect}
|
||||
/>,
|
||||
)
|
||||
|
||||
await user.click(screen.getByTitle('Banana'))
|
||||
|
||||
expect(screen.queryByText('Citrus')).not.toBeInTheDocument()
|
||||
expect(onSelect).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('SimpleSelect', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
// Rendering and placeholder fallback behavior.
|
||||
describe('Rendering', () => {
|
||||
it('should render i18n placeholder when no selection exists', () => {
|
||||
render(
|
||||
<SimpleSelect
|
||||
items={items}
|
||||
defaultValue="missing"
|
||||
onSelect={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByText(/select/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render custom placeholder when provided', () => {
|
||||
render(
|
||||
<SimpleSelect
|
||||
items={items}
|
||||
defaultValue="missing"
|
||||
placeholder="Pick one"
|
||||
onSelect={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByText('Pick one')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// User interactions and callback behavior.
|
||||
describe('User Interactions', () => {
|
||||
it('should call onSelect and update display when an option is chosen', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onSelect = vi.fn()
|
||||
|
||||
render(
|
||||
<SimpleSelect
|
||||
items={items}
|
||||
defaultValue="missing"
|
||||
onSelect={onSelect}
|
||||
/>,
|
||||
)
|
||||
|
||||
await user.click(screen.getByRole('button'))
|
||||
await user.click(screen.getByText('Apple'))
|
||||
|
||||
expect(onSelect).toHaveBeenCalledWith(expect.objectContaining({
|
||||
value: 'apple',
|
||||
name: 'Apple',
|
||||
}))
|
||||
expect(screen.getByText('Apple')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should pass open state into renderTrigger', async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
render(
|
||||
<SimpleSelect
|
||||
items={items}
|
||||
defaultValue="missing"
|
||||
onSelect={vi.fn()}
|
||||
renderTrigger={(selected, open) => (
|
||||
<span>{`${selected?.name ?? 'none'}-${open ? 'open' : 'closed'}`}</span>
|
||||
)}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByText('none-closed')).toBeInTheDocument()
|
||||
await user.click(screen.getByText('none-closed'))
|
||||
expect(screen.getByText('none-open')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('PortalSelect', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
// Rendering for edge case when value is empty.
|
||||
describe('Rendering', () => {
|
||||
it('should show placeholder when value is empty', () => {
|
||||
render(
|
||||
<PortalSelect
|
||||
value=""
|
||||
items={items}
|
||||
onSelect={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByText(/select/i)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// Interaction and readonly behavior.
|
||||
describe('User Interactions', () => {
|
||||
it('should call onSelect when choosing an option from portal dropdown', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onSelect = vi.fn()
|
||||
|
||||
render(
|
||||
<PortalSelect
|
||||
value=""
|
||||
items={items}
|
||||
onSelect={onSelect}
|
||||
/>,
|
||||
)
|
||||
|
||||
await user.click(screen.getByText(/select/i))
|
||||
await user.click(screen.getByText('Citrus'))
|
||||
|
||||
expect(onSelect).toHaveBeenCalledWith(expect.objectContaining({
|
||||
value: 'citrus',
|
||||
name: 'Citrus',
|
||||
}))
|
||||
})
|
||||
|
||||
it('should not open the portal dropdown when readonly is true', async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
render(
|
||||
<PortalSelect
|
||||
value=""
|
||||
items={items}
|
||||
readonly={true}
|
||||
onSelect={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
await user.click(screen.getByText(/select/i))
|
||||
expect(screen.queryByTitle('Citrus')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
116
web/app/components/base/select/locale-signin.spec.tsx
Normal file
116
web/app/components/base/select/locale-signin.spec.tsx
Normal file
@ -0,0 +1,116 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import LocaleSigninSelect from './locale-signin'
|
||||
|
||||
const localeItems = [
|
||||
{ value: 'en-US', name: 'English (US)' },
|
||||
{ value: 'zh-Hans', name: '简体中文' },
|
||||
{ value: 'ja-JP', name: '日本語' },
|
||||
]
|
||||
|
||||
describe('LocaleSigninSelect', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
// Rendering behavior for selected value and fallback state.
|
||||
describe('Rendering', () => {
|
||||
it('should render selected locale name when value matches an item', () => {
|
||||
render(
|
||||
<LocaleSigninSelect
|
||||
items={localeItems}
|
||||
value="en-US"
|
||||
onChange={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByRole('button', { name: /english \(us\)/i })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render trigger without selected label when value is not found', () => {
|
||||
render(
|
||||
<LocaleSigninSelect
|
||||
items={localeItems}
|
||||
value="missing"
|
||||
onChange={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
const trigger = screen.getByRole('button')
|
||||
expect(trigger).toBeInTheDocument()
|
||||
expect(trigger).not.toHaveTextContent('English (US)')
|
||||
})
|
||||
})
|
||||
|
||||
// Menu interactions and callback behavior.
|
||||
describe('User Interactions', () => {
|
||||
it('should call onChange with selected locale value when clicking an option', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onChange = vi.fn()
|
||||
|
||||
render(
|
||||
<LocaleSigninSelect
|
||||
items={localeItems}
|
||||
value="en-US"
|
||||
onChange={onChange}
|
||||
/>,
|
||||
)
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /english \(us\)/i }))
|
||||
await user.click(screen.getByRole('menuitem', { name: '日本語' }))
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith('ja-JP')
|
||||
})
|
||||
|
||||
it('should render all locale options when menu is opened', async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
render(
|
||||
<LocaleSigninSelect
|
||||
items={localeItems}
|
||||
value="en-US"
|
||||
onChange={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /english \(us\)/i }))
|
||||
|
||||
expect(screen.getByRole('menuitem', { name: 'English (US)' })).toBeInTheDocument()
|
||||
expect(screen.getByRole('menuitem', { name: '简体中文' })).toBeInTheDocument()
|
||||
expect(screen.getByRole('menuitem', { name: '日本語' })).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// Edge behavior for missing callback and empty data.
|
||||
describe('Edge Cases', () => {
|
||||
it('should not throw when onChange is undefined and option is selected', async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
render(
|
||||
<LocaleSigninSelect
|
||||
items={localeItems}
|
||||
value="en-US"
|
||||
/>,
|
||||
)
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /english \(us\)/i }))
|
||||
await user.click(screen.getByRole('menuitem', { name: '简体中文' }))
|
||||
// No assertion needed — test verifies no exception is thrown during selection without onChange.
|
||||
})
|
||||
|
||||
it('should render no options when items are empty', async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
render(
|
||||
<LocaleSigninSelect
|
||||
items={[]}
|
||||
value="en-US"
|
||||
onChange={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
await user.click(screen.getByRole('button'))
|
||||
expect(screen.queryAllByRole('menuitem')).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
115
web/app/components/base/select/locale.spec.tsx
Normal file
115
web/app/components/base/select/locale.spec.tsx
Normal file
@ -0,0 +1,115 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import LocaleSelect from './locale'
|
||||
|
||||
const localeItems = [
|
||||
{ value: 'en-US', name: 'English (US)' },
|
||||
{ value: 'zh-Hans', name: '简体中文' },
|
||||
{ value: 'ja-JP', name: '日本語' },
|
||||
]
|
||||
|
||||
describe('LocaleSelect', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
// Rendering behavior for selected value and fallback state.
|
||||
describe('Rendering', () => {
|
||||
it('should render selected locale name when value matches an item', () => {
|
||||
render(
|
||||
<LocaleSelect
|
||||
items={localeItems}
|
||||
value="en-US"
|
||||
onChange={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByRole('button', { name: /english \(us\)/i })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render trigger without selected label when value is not found', () => {
|
||||
render(
|
||||
<LocaleSelect
|
||||
items={localeItems}
|
||||
value="missing"
|
||||
onChange={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
const trigger = screen.getByRole('button')
|
||||
expect(trigger).toBeInTheDocument()
|
||||
expect(trigger).not.toHaveTextContent('English (US)')
|
||||
})
|
||||
})
|
||||
|
||||
// Menu interactions and callback behavior.
|
||||
describe('User Interactions', () => {
|
||||
it('should call onChange with selected locale value when clicking an option', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onChange = vi.fn()
|
||||
|
||||
render(
|
||||
<LocaleSelect
|
||||
items={localeItems}
|
||||
value="en-US"
|
||||
onChange={onChange}
|
||||
/>,
|
||||
)
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /english \(us\)/i }))
|
||||
await user.click(screen.getByRole('menuitem', { name: '日本語' }))
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith('ja-JP')
|
||||
})
|
||||
|
||||
it('should render all locale options when menu is opened', async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
render(
|
||||
<LocaleSelect
|
||||
items={localeItems}
|
||||
value="en-US"
|
||||
onChange={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /english \(us\)/i }))
|
||||
|
||||
expect(screen.getByRole('menuitem', { name: 'English (US)' })).toBeInTheDocument()
|
||||
expect(screen.getByRole('menuitem', { name: '简体中文' })).toBeInTheDocument()
|
||||
expect(screen.getByRole('menuitem', { name: '日本語' })).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// Edge behavior for missing callback and empty data.
|
||||
describe('Edge Cases', () => {
|
||||
it('should not throw when onChange is undefined and option is selected', async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
render(
|
||||
<LocaleSelect
|
||||
items={localeItems}
|
||||
value="en-US"
|
||||
/>,
|
||||
)
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /english \(us\)/i }))
|
||||
await user.click(screen.getByRole('menuitem', { name: '简体中文' }))
|
||||
})
|
||||
|
||||
it('should render no options when items are empty', async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
render(
|
||||
<LocaleSelect
|
||||
items={[]}
|
||||
value="en-US"
|
||||
onChange={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
await user.click(screen.getByRole('button'))
|
||||
expect(screen.queryAllByRole('menuitem')).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
175
web/app/components/base/select/pure.spec.tsx
Normal file
175
web/app/components/base/select/pure.spec.tsx
Normal file
@ -0,0 +1,175 @@
|
||||
import type { Option } from './pure'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import PureSelect from './pure'
|
||||
|
||||
const options: Option[] = [
|
||||
{ label: 'Apple', value: 'apple' },
|
||||
{ label: 'Banana', value: 'banana' },
|
||||
{ label: 'Citrus', value: 'citrus' },
|
||||
]
|
||||
|
||||
describe('PureSelect', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
// Rendering and placeholder behavior in single/multiple modes.
|
||||
describe('Rendering', () => {
|
||||
it('should render i18n placeholder when single value is empty', () => {
|
||||
render(<PureSelect options={options} />)
|
||||
expect(screen.getByTitle(/select/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render custom placeholder when provided', () => {
|
||||
render(<PureSelect options={options} placeholder="Choose value" />)
|
||||
expect(screen.getByTitle('Choose value')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render selected option label in single mode', () => {
|
||||
render(<PureSelect options={options} value="banana" />)
|
||||
expect(screen.getByTitle('Banana')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render selected count text in multiple mode', () => {
|
||||
render(<PureSelect options={options} multiple={true} value={['apple', 'banana']} />)
|
||||
expect(screen.getByText(/selected/i)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// Interaction behavior in single and multiple selection modes.
|
||||
describe('User Interactions', () => {
|
||||
it('should call onChange and close popup when selecting an option in single mode', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onChange = vi.fn()
|
||||
|
||||
render(<PureSelect options={options} onChange={onChange} />)
|
||||
|
||||
await user.click(screen.getByTitle(/select/i))
|
||||
expect(screen.getByTitle('Banana')).toBeInTheDocument()
|
||||
|
||||
await user.click(screen.getByTitle('Banana'))
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith('banana')
|
||||
expect(screen.queryByTitle('Citrus')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should append a new value in multiple mode when clicking an unselected option', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onChange = vi.fn()
|
||||
|
||||
render(
|
||||
<PureSelect
|
||||
options={options}
|
||||
multiple={true}
|
||||
value={['apple']}
|
||||
onChange={onChange}
|
||||
/>,
|
||||
)
|
||||
|
||||
await user.click(screen.getByText(/common\.dynamicSelect\.selected/i))
|
||||
await user.click(screen.getAllByTitle('Banana')[0])
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith(['apple', 'banana'])
|
||||
})
|
||||
|
||||
it('should remove an existing value in multiple mode when clicking a selected option', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onChange = vi.fn()
|
||||
|
||||
render(
|
||||
<PureSelect
|
||||
options={options}
|
||||
multiple={true}
|
||||
value={['apple', 'banana']}
|
||||
onChange={onChange}
|
||||
/>,
|
||||
)
|
||||
|
||||
await user.click(screen.getByText(/common\.dynamicSelect\.selected/i))
|
||||
await user.click(screen.getAllByTitle('Apple')[0])
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith(['banana'])
|
||||
})
|
||||
})
|
||||
|
||||
// Controlled open state and disabled behavior.
|
||||
describe('Container And Disabled Props', () => {
|
||||
it('should call containerProps.onOpenChange when trigger is clicked in controlled mode', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onOpenChange = vi.fn()
|
||||
|
||||
render(
|
||||
<PureSelect
|
||||
options={options}
|
||||
containerProps={{ open: true, onOpenChange }}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByTitle('Apple')).toBeInTheDocument()
|
||||
await user.click(screen.getByTitle(/select/i))
|
||||
|
||||
expect(onOpenChange).toHaveBeenCalledWith(false)
|
||||
})
|
||||
|
||||
it('should not open popup when disabled', async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
render(
|
||||
<PureSelect
|
||||
options={options}
|
||||
disabled={true}
|
||||
/>,
|
||||
)
|
||||
|
||||
await user.click(screen.getByTitle(/select/i))
|
||||
expect(screen.queryByTitle('Apple')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should ignore option clicks when disabled even if popup is open', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onChange = vi.fn()
|
||||
|
||||
render(
|
||||
<PureSelect
|
||||
options={options}
|
||||
disabled={true}
|
||||
onChange={onChange}
|
||||
containerProps={{ open: true }}
|
||||
/>,
|
||||
)
|
||||
|
||||
await user.click(screen.getAllByTitle('Apple')[0])
|
||||
expect(onChange).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
// Style and popup customization props.
|
||||
describe('Style Props', () => {
|
||||
it('should apply trigger and popup class names and render popup title', () => {
|
||||
render(
|
||||
<PureSelect
|
||||
options={options}
|
||||
triggerProps={{ className: 'trigger-class' }}
|
||||
popupProps={{
|
||||
wrapperClassName: 'wrapper-class',
|
||||
className: 'popup-class',
|
||||
itemClassName: 'item-class',
|
||||
title: 'Available options',
|
||||
titleClassName: 'title-class',
|
||||
}}
|
||||
containerProps={{ open: true }}
|
||||
/>,
|
||||
)
|
||||
|
||||
const triggerLabel = screen.getByTitle(/select/i)
|
||||
const trigger = triggerLabel.parentElement
|
||||
|
||||
expect(trigger).toHaveClass('trigger-class')
|
||||
expect(document.querySelector('.wrapper-class')).toBeInTheDocument()
|
||||
expect(document.querySelector('.popup-class')).toBeInTheDocument()
|
||||
expect(document.querySelectorAll('.item-class')).toHaveLength(options.length)
|
||||
expect(screen.getByText('Available options')).toHaveClass('title-class')
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user