test: add tests for some base components (#32479)

This commit is contained in:
Saumya Talwani
2026-02-25 13:38:03 +05:30
committed by GitHub
parent 34b6fc92d7
commit 6f2c101e3c
26 changed files with 3577 additions and 78 deletions

View 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)
})
})
})

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

View 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)
})
})
})

View 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)
})
})
})

View 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')
})
})
})