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