dify/web/app/components/base/chip/__tests__/index.spec.tsx

376 lines
12 KiB
TypeScript

import type { Item } from '../index'
import { cleanup, render, screen, waitFor, within } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import * as React from 'react'
import Chip from '../index'
afterEach(cleanup)
// Test data factory
const createTestItems = (): Item[] => [
{ value: 'all', name: 'All Items' },
{ value: 'active', name: 'Active' },
{ value: 'archived', name: 'Archived' },
]
describe('Chip', () => {
// Shared test props
let items: Item[]
let onSelect: (item: Item) => void
let onClear: () => void
beforeEach(() => {
vi.clearAllMocks()
items = createTestItems()
onSelect = vi.fn()
onClear = vi.fn()
})
// Helper function to render Chip with default props
const renderChip = (props: Partial<React.ComponentProps<typeof Chip>> = {}) => {
const user = userEvent.setup()
return {
user,
...render(
<Chip
value="all"
items={items}
onSelect={onSelect}
onClear={onClear}
{...props}
/>,
),
}
}
// Helper function to get the trigger element
const getTrigger = (container: HTMLElement) => {
return container.querySelector('button[role="combobox"]') as HTMLElement | null
}
// Helper function to open dropdown panel
const openPanel = async (user: ReturnType<typeof userEvent.setup>, container: HTMLElement) => {
const trigger = getTrigger(container)
expect(trigger).toBeInTheDocument()
await user.click(trigger!)
return screen.findByRole('listbox')
}
const expectPanelClosed = async (trigger: HTMLElement | null) => {
await waitFor(() => {
expect(screen.queryByRole('listbox')).not.toBeInTheDocument()
expect(trigger).not.toHaveAttribute('data-popup-open')
})
}
describe('Rendering', () => {
it('should render without crashing', () => {
renderChip()
expect(screen.getByText('All Items'))!.toBeInTheDocument()
})
it('should display current selected item name', () => {
renderChip({ value: 'active' })
expect(screen.getByRole('combobox', { name: 'Active' }))!.toBeInTheDocument()
})
it('should display empty content when value does not match any item', () => {
const { container } = renderChip({ value: 'nonexistent' })
// When value doesn't match, no text should be displayed in trigger
const trigger = getTrigger(container)
// Check that there's no item name text (only icons should be present)
expect(trigger?.textContent?.trim()).toBeFalsy()
})
})
describe('Props', () => {
it('should update displayed item name when value prop changes', () => {
const { rerender } = renderChip({ value: 'all' })
expect(screen.getByText('All Items'))!.toBeInTheDocument()
rerender(
<Chip
value="archived"
items={items}
onSelect={onSelect}
onClear={onClear}
/>,
)
expect(screen.getByRole('combobox', { name: 'Archived' }))!.toBeInTheDocument()
})
it('should show left icon by default', () => {
const { container } = renderChip()
expect(container.querySelector('.i-ri-filter-3-line')).toBeInTheDocument()
})
it('should hide left icon when showLeftIcon is false', () => {
renderChip({ showLeftIcon: false, value: '' })
// When showLeftIcon is false, there should be no filter icon before the text
const trigger = getTrigger(document.body)
expect(trigger?.querySelector('.i-ri-filter-3-line')).not.toBeInTheDocument()
expect(trigger?.querySelector('.i-ri-arrow-down-s-line')).toBeInTheDocument()
})
it('should render custom left icon', () => {
const CustomIcon = () => <span data-testid="custom-icon"></span>
renderChip({ leftIcon: <CustomIcon /> })
expect(screen.getByTestId('custom-icon'))!.toBeInTheDocument()
})
it('should apply custom className to trigger', () => {
const customClass = 'custom-chip-class'
const { container } = renderChip({ className: customClass })
const chipElement = container.querySelector(`.${customClass}`)
expect(chipElement)!.toBeInTheDocument()
})
it('should apply custom panelClassName to dropdown panel', async () => {
const customPanelClass = 'custom-panel-class'
const { container, user } = renderChip({ panelClassName: customPanelClass })
await openPanel(user, container)
// Panel is rendered in a portal, so check document.body
const panel = document.body.querySelector(`.${customPanelClass}`)
expect(panel)!.toBeInTheDocument()
})
})
describe('State Management', () => {
it('should toggle dropdown panel on trigger click', async () => {
const { container, user } = renderChip()
const trigger = getTrigger(container)
expect(screen.queryByRole('listbox')).not.toBeInTheDocument()
expect(trigger).not.toHaveAttribute('data-popup-open')
const listbox = await openPanel(user, container)
expect(trigger).toHaveAttribute('data-popup-open')
expect(within(listbox).getByRole('option', { name: 'All Items' })).toBeInTheDocument()
if (trigger)
await user.click(trigger)
await expectPanelClosed(trigger)
})
it('should close panel after selecting an item', async () => {
const { container, user } = renderChip()
const listbox = await openPanel(user, container)
const trigger = getTrigger(container)
expect(trigger).toHaveAttribute('data-popup-open')
await user.click(within(listbox).getByRole('option', { name: 'Active' }))
await expectPanelClosed(trigger)
})
})
describe('Event Handlers', () => {
it('should call onSelect with correct item when item is clicked', async () => {
const { container, user } = renderChip()
const listbox = await openPanel(user, container)
await user.click(within(listbox).getByRole('option', { name: 'Active' }))
expect(onSelect).toHaveBeenCalledTimes(1)
expect(onSelect).toHaveBeenCalledWith(items[1])
})
it('should call onClear when clear button is clicked', async () => {
const { user } = renderChip({ value: 'active' })
const clearButton = screen.getByRole('button', { name: 'common.operation.clear' })
await user.click(clearButton)
expect(onClear).toHaveBeenCalledTimes(1)
})
it('should stop event propagation when clear button is clicked', async () => {
const { container, user } = renderChip({ value: 'active' })
const trigger = getTrigger(container)
expect(screen.queryByRole('listbox')).not.toBeInTheDocument()
expect(trigger).not.toHaveAttribute('data-popup-open')
const clearButton = screen.getByRole('button', { name: 'common.operation.clear' })
await user.click(clearButton)
expect(screen.queryByRole('listbox')).not.toBeInTheDocument()
expect(trigger).not.toHaveAttribute('data-popup-open')
expect(onClear).toHaveBeenCalledTimes(1)
})
it('should handle multiple rapid clicks on trigger', async () => {
const { container, user } = renderChip()
const trigger = getTrigger(container)
if (trigger)
await user.click(trigger)
expect(await screen.findByRole('listbox')).toBeInTheDocument()
expect(trigger).toHaveAttribute('data-popup-open')
if (trigger)
await user.click(trigger)
await expectPanelClosed(trigger)
if (trigger)
await user.click(trigger)
expect(await screen.findByRole('listbox')).toBeInTheDocument()
expect(trigger).toHaveAttribute('data-popup-open')
})
})
describe('Conditional Rendering', () => {
it('should show arrow down icon when no value is selected', () => {
const { container } = renderChip({ value: '' })
expect(container.querySelector('.i-ri-arrow-down-s-line')).toBeInTheDocument()
})
it('should show clear button when value is selected', () => {
const { container } = renderChip({ value: 'active' })
expect(container.querySelector('.i-ri-close-circle-fill')).toBeInTheDocument()
})
it('should not show clear button when no value is selected', () => {
const { container } = renderChip({ value: '' })
const trigger = getTrigger(container)
expect(trigger?.querySelector('.i-ri-filter-3-line')).toBeInTheDocument()
expect(trigger?.querySelector('.i-ri-arrow-down-s-line')).toBeInTheDocument()
expect(container.querySelector('.i-ri-close-circle-fill')).not.toBeInTheDocument()
// Verify onClear hasn't been called
expect(onClear).not.toHaveBeenCalled()
})
it('should show dropdown content only when panel is open', async () => {
const { container, user } = renderChip()
const trigger = getTrigger(container)
expect(screen.queryByRole('listbox')).not.toBeInTheDocument()
expect(trigger).not.toHaveAttribute('data-popup-open')
const listbox = await openPanel(user, container)
expect(trigger).toHaveAttribute('data-popup-open')
expect(within(listbox).getByRole('option', { name: 'All Items' })).toBeInTheDocument()
})
it('should show check icon on selected item in dropdown', async () => {
const { container, user } = renderChip({ value: 'active' })
const listbox = await openPanel(user, container)
expect(within(listbox).getByRole('option', { name: 'Active' })).toHaveAttribute('aria-selected', 'true')
})
it('should render all items in dropdown when open', async () => {
const { container, user } = renderChip()
const listbox = await openPanel(user, container)
expect(within(listbox).getByRole('option', { name: 'All Items' })).toBeInTheDocument()
expect(within(listbox).getByRole('option', { name: 'Active' })).toBeInTheDocument()
expect(within(listbox).getByRole('option', { name: 'Archived' })).toBeInTheDocument()
})
})
describe('Edge Cases', () => {
it('should handle empty items array', () => {
const { container } = renderChip({ items: [], value: '' })
// Trigger should still render
const trigger = getTrigger(container)
expect(trigger)!.toBeInTheDocument()
})
it('should handle value not in items list', () => {
const { container } = renderChip({ value: 'nonexistent' })
const trigger = getTrigger(container)
expect(trigger)!.toBeInTheDocument()
// The trigger should not display any item name text
expect(trigger?.textContent?.trim()).toBeFalsy()
expect(screen.queryByRole('button', { name: 'common.operation.clear' })).not.toBeInTheDocument()
})
it('should allow selecting already selected item', async () => {
const { container, user } = renderChip({ value: 'active' })
const listbox = await openPanel(user, container)
await user.click(within(listbox).getByRole('option', { name: 'Active' }))
expect(onSelect).toHaveBeenCalledTimes(1)
expect(onSelect).toHaveBeenCalledWith(items[1])
})
it('should handle numeric values', async () => {
const numericItems: Item[] = [
{ value: 1, name: 'First' },
{ value: 2, name: 'Second' },
{ value: 3, name: 'Third' },
]
const { container, user } = renderChip({ value: 2, items: numericItems })
expect(screen.getByText('Second'))!.toBeInTheDocument()
// Open panel and select Third
const listbox = await openPanel(user, container)
await user.click(within(listbox).getByRole('option', { name: 'Third' }))
expect(onSelect).toHaveBeenCalledWith(numericItems[2])
})
it('should treat numeric zero as a selected value', () => {
const numericItems: Item[] = [
{ value: 0, name: 'Zero' },
{ value: 1, name: 'One' },
]
renderChip({ value: 0, items: numericItems })
expect(screen.getByRole('combobox', { name: 'Zero' })).toBeInTheDocument()
expect(screen.getByRole('button', { name: 'common.operation.clear' })).toBeInTheDocument()
})
it('should handle items with additional properties', async () => {
const itemsWithExtra: Item[] = [
{ value: 'a', name: 'Item A', customProp: 'extra1' },
{ value: 'b', name: 'Item B', customProp: 'extra2' },
]
const { container, user } = renderChip({ value: 'a', items: itemsWithExtra })
expect(screen.getByText('Item A'))!.toBeInTheDocument()
// Open panel and select Item B
const listbox = await openPanel(user, container)
await user.click(within(listbox).getByRole('option', { name: 'Item B' }))
expect(onSelect).toHaveBeenCalledWith(itemsWithExtra[1])
})
})
})