mirror of
https://github.com/langgenius/dify.git
synced 2026-06-16 14:01:10 +08:00
376 lines
12 KiB
TypeScript
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])
|
|
})
|
|
})
|
|
})
|