mirror of https://github.com/langgenius/dify.git
test: Add comprehensive test suite for Chip component (#30119)
Signed-off-by: SherlockShemol <shemol@163.com> Co-authored-by: crazywoola <100913391+crazywoola@users.noreply.github.com>
This commit is contained in:
parent
a26b2d74d4
commit
29e7e822d7
|
|
@ -0,0 +1,394 @@
|
|||
import type { Item } from './index'
|
||||
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
|
||||
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>> = {}) => {
|
||||
return 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('[data-state]')
|
||||
}
|
||||
|
||||
// Helper function to open dropdown panel
|
||||
const openPanel = (container: HTMLElement) => {
|
||||
const trigger = getTrigger(container)
|
||||
if (trigger)
|
||||
fireEvent.click(trigger)
|
||||
}
|
||||
|
||||
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.getByText('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.getByText('Archived')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show left icon by default', () => {
|
||||
const { container } = renderChip()
|
||||
|
||||
// The filter icon should be visible
|
||||
const svg = container.querySelector('svg')
|
||||
expect(svg).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should hide left icon when showLeftIcon is false', () => {
|
||||
renderChip({ showLeftIcon: false })
|
||||
|
||||
// When showLeftIcon is false, there should be no filter icon before the text
|
||||
const textElement = screen.getByText('All Items')
|
||||
const parent = textElement.closest('div[data-state]')
|
||||
const icons = parent?.querySelectorAll('svg')
|
||||
|
||||
// Should only have the arrow icon, not the filter icon
|
||||
expect(icons?.length).toBe(1)
|
||||
})
|
||||
|
||||
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', () => {
|
||||
const customPanelClass = 'custom-panel-class'
|
||||
|
||||
const { container } = renderChip({ panelClassName: customPanelClass })
|
||||
openPanel(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', () => {
|
||||
const { container } = renderChip()
|
||||
|
||||
// Initially closed - check data-state attribute
|
||||
const trigger = getTrigger(container)
|
||||
expect(trigger).toHaveAttribute('data-state', 'closed')
|
||||
|
||||
// Open panel
|
||||
openPanel(container)
|
||||
expect(trigger).toHaveAttribute('data-state', 'open')
|
||||
// Panel items should be visible
|
||||
expect(screen.getAllByText('All Items').length).toBeGreaterThan(1)
|
||||
|
||||
// Close panel
|
||||
if (trigger)
|
||||
fireEvent.click(trigger)
|
||||
expect(trigger).toHaveAttribute('data-state', 'closed')
|
||||
})
|
||||
|
||||
it('should close panel after selecting an item', () => {
|
||||
const { container } = renderChip()
|
||||
|
||||
openPanel(container)
|
||||
const trigger = getTrigger(container)
|
||||
expect(trigger).toHaveAttribute('data-state', 'open')
|
||||
|
||||
// Click on an item in the dropdown panel
|
||||
const activeItems = screen.getAllByText('Active')
|
||||
// The second one should be in the dropdown
|
||||
fireEvent.click(activeItems[activeItems.length - 1])
|
||||
|
||||
expect(trigger).toHaveAttribute('data-state', 'closed')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Event Handlers', () => {
|
||||
it('should call onSelect with correct item when item is clicked', () => {
|
||||
const { container } = renderChip()
|
||||
|
||||
openPanel(container)
|
||||
// Get all "Active" texts and click the one in the dropdown (should be the last one)
|
||||
const activeItems = screen.getAllByText('Active')
|
||||
fireEvent.click(activeItems[activeItems.length - 1])
|
||||
|
||||
expect(onSelect).toHaveBeenCalledTimes(1)
|
||||
expect(onSelect).toHaveBeenCalledWith(items[1])
|
||||
})
|
||||
|
||||
it('should call onClear when clear button is clicked', () => {
|
||||
const { container } = renderChip({ value: 'active' })
|
||||
|
||||
// Find the close icon (last SVG in the trigger) and click its parent
|
||||
const trigger = getTrigger(container)
|
||||
const svgs = trigger?.querySelectorAll('svg')
|
||||
// The close icon should be the last SVG element
|
||||
const closeIcon = svgs?.[svgs.length - 1]
|
||||
const clearButton = closeIcon?.parentElement
|
||||
|
||||
expect(clearButton).toBeInTheDocument()
|
||||
if (clearButton)
|
||||
fireEvent.click(clearButton)
|
||||
|
||||
expect(onClear).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should stop event propagation when clear button is clicked', () => {
|
||||
const { container } = renderChip({ value: 'active' })
|
||||
|
||||
const trigger = getTrigger(container)
|
||||
expect(trigger).toHaveAttribute('data-state', 'closed')
|
||||
|
||||
// Find the close icon (last SVG) and click its parent
|
||||
const svgs = trigger?.querySelectorAll('svg')
|
||||
const closeIcon = svgs?.[svgs.length - 1]
|
||||
const clearButton = closeIcon?.parentElement
|
||||
|
||||
if (clearButton)
|
||||
fireEvent.click(clearButton)
|
||||
|
||||
// Panel should remain closed
|
||||
expect(trigger).toHaveAttribute('data-state', 'closed')
|
||||
expect(onClear).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should handle multiple rapid clicks on trigger', () => {
|
||||
const { container } = renderChip()
|
||||
|
||||
const trigger = getTrigger(container)
|
||||
|
||||
// Click 1: open
|
||||
if (trigger)
|
||||
fireEvent.click(trigger)
|
||||
expect(trigger).toHaveAttribute('data-state', 'open')
|
||||
|
||||
// Click 2: close
|
||||
if (trigger)
|
||||
fireEvent.click(trigger)
|
||||
expect(trigger).toHaveAttribute('data-state', 'closed')
|
||||
|
||||
// Click 3: open again
|
||||
if (trigger)
|
||||
fireEvent.click(trigger)
|
||||
expect(trigger).toHaveAttribute('data-state', 'open')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Conditional Rendering', () => {
|
||||
it('should show arrow down icon when no value is selected', () => {
|
||||
const { container } = renderChip({ value: '' })
|
||||
|
||||
// Should have SVG icons (filter icon and arrow down icon)
|
||||
const svgs = container.querySelectorAll('svg')
|
||||
expect(svgs.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('should show clear button when value is selected', () => {
|
||||
const { container } = renderChip({ value: 'active' })
|
||||
|
||||
// When value is selected, there should be an icon (the close icon)
|
||||
const svgs = container.querySelectorAll('svg')
|
||||
expect(svgs.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('should not show clear button when no value is selected', () => {
|
||||
const { container } = renderChip({ value: '' })
|
||||
|
||||
const trigger = getTrigger(container)
|
||||
|
||||
// When value is empty, the trigger should only have 2 SVGs (filter icon + arrow)
|
||||
// When value is selected, it would have 2 SVGs (filter icon + close icon)
|
||||
const svgs = trigger?.querySelectorAll('svg')
|
||||
// Arrow icon should be present, close icon should not
|
||||
expect(svgs?.length).toBe(2)
|
||||
|
||||
// Verify onClear hasn't been called
|
||||
expect(onClear).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should show dropdown content only when panel is open', () => {
|
||||
const { container } = renderChip()
|
||||
|
||||
const trigger = getTrigger(container)
|
||||
|
||||
// Closed by default
|
||||
expect(trigger).toHaveAttribute('data-state', 'closed')
|
||||
|
||||
openPanel(container)
|
||||
expect(trigger).toHaveAttribute('data-state', 'open')
|
||||
// Items should be duplicated (once in trigger, once in panel)
|
||||
expect(screen.getAllByText('All Items').length).toBeGreaterThan(1)
|
||||
})
|
||||
|
||||
it('should show check icon on selected item in dropdown', () => {
|
||||
const { container } = renderChip({ value: 'active' })
|
||||
|
||||
openPanel(container)
|
||||
|
||||
// Find the dropdown panel items
|
||||
const allActiveTexts = screen.getAllByText('Active')
|
||||
// The dropdown item should be the last one
|
||||
const dropdownItem = allActiveTexts[allActiveTexts.length - 1]
|
||||
const parentContainer = dropdownItem.parentElement
|
||||
|
||||
// The check icon should be a sibling within the parent
|
||||
const checkIcon = parentContainer?.querySelector('svg')
|
||||
expect(checkIcon).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render all items in dropdown when open', () => {
|
||||
const { container } = renderChip()
|
||||
|
||||
openPanel(container)
|
||||
|
||||
// Each item should appear at least twice (once in potential selected state, once in dropdown)
|
||||
// Use getAllByText to handle multiple occurrences
|
||||
expect(screen.getAllByText('All Items').length).toBeGreaterThan(0)
|
||||
expect(screen.getAllByText('Active').length).toBeGreaterThan(0)
|
||||
expect(screen.getAllByText('Archived').length).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle empty items array', () => {
|
||||
const { container } = renderChip({ items: [], value: '' })
|
||||
|
||||
// Trigger should still render
|
||||
const trigger = container.querySelector('[data-state]')
|
||||
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()
|
||||
})
|
||||
|
||||
it('should allow selecting already selected item', () => {
|
||||
const { container } = renderChip({ value: 'active' })
|
||||
|
||||
openPanel(container)
|
||||
|
||||
// Click on the already selected item in the dropdown
|
||||
const activeItems = screen.getAllByText('Active')
|
||||
fireEvent.click(activeItems[activeItems.length - 1])
|
||||
|
||||
expect(onSelect).toHaveBeenCalledTimes(1)
|
||||
expect(onSelect).toHaveBeenCalledWith(items[1])
|
||||
})
|
||||
|
||||
it('should handle numeric values', () => {
|
||||
const numericItems: Item[] = [
|
||||
{ value: 1, name: 'First' },
|
||||
{ value: 2, name: 'Second' },
|
||||
{ value: 3, name: 'Third' },
|
||||
]
|
||||
|
||||
const { container } = renderChip({ value: 2, items: numericItems })
|
||||
|
||||
expect(screen.getByText('Second')).toBeInTheDocument()
|
||||
|
||||
// Open panel and select Third
|
||||
openPanel(container)
|
||||
|
||||
const thirdItems = screen.getAllByText('Third')
|
||||
fireEvent.click(thirdItems[thirdItems.length - 1])
|
||||
|
||||
expect(onSelect).toHaveBeenCalledWith(numericItems[2])
|
||||
})
|
||||
|
||||
it('should handle items with additional properties', () => {
|
||||
const itemsWithExtra: Item[] = [
|
||||
{ value: 'a', name: 'Item A', customProp: 'extra1' },
|
||||
{ value: 'b', name: 'Item B', customProp: 'extra2' },
|
||||
]
|
||||
|
||||
const { container } = renderChip({ value: 'a', items: itemsWithExtra })
|
||||
|
||||
expect(screen.getByText('Item A')).toBeInTheDocument()
|
||||
|
||||
// Open panel and select Item B
|
||||
openPanel(container)
|
||||
|
||||
const itemBs = screen.getAllByText('Item B')
|
||||
fireEvent.click(itemBs[itemBs.length - 1])
|
||||
|
||||
expect(onSelect).toHaveBeenCalledWith(itemsWithExtra[1])
|
||||
})
|
||||
})
|
||||
})
|
||||
Loading…
Reference in New Issue