dify/web/app/components/base/tab-slider/__tests__/index.spec.tsx

148 lines
5.3 KiB
TypeScript

import { cleanup, fireEvent, render, screen } from '@testing-library/react'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { useInstalledPluginList } from '@/service/use-plugins'
import TabSlider from '../index'
// Mock the service hook
vi.mock('@/service/use-plugins', () => ({
useInstalledPluginList: vi.fn(),
}))
const mockOptions = [
{ value: 'all', text: 'All' },
{ value: 'plugins', text: 'Plugins' },
{ value: 'settings', text: 'Settings' },
]
describe('TabSlider Component', () => {
const onChangeMock = vi.fn()
beforeEach(() => {
vi.clearAllMocks()
vi.mocked(useInstalledPluginList).mockReturnValue({
data: { total: 0 },
isLoading: false,
} as ReturnType<typeof useInstalledPluginList>)
})
afterEach(() => {
cleanup()
})
// Helper to inject layout values into JSDOM
const setElementLayout = (id: string, left: number, width: number) => {
const el = document.getElementById(id)
if (el) {
Object.defineProperty(el, 'offsetLeft', { configurable: true, value: left })
Object.defineProperty(el, 'offsetWidth', { configurable: true, value: width })
}
}
it('renders all options correctly', () => {
render(<TabSlider value="all" options={mockOptions} onChange={onChangeMock} />)
mockOptions.forEach((option) => {
expect(screen.getByText(option.text as string)).toBeInTheDocument()
})
})
it('calls onChange when a new tab is clicked', () => {
render(<TabSlider value="all" options={mockOptions} onChange={onChangeMock} />)
const pluginTab = screen.getByTestId('tab-item-plugins')
fireEvent.click(pluginTab)
expect(onChangeMock).toHaveBeenCalledWith('plugins')
})
it('applies the correct active classes to the selected tab', () => {
render(<TabSlider value="plugins" options={mockOptions} onChange={onChangeMock} />)
const activeTab = screen.getByTestId('tab-item-plugins')
expect(activeTab).toHaveClass('text-text-primary')
const inactiveTab = screen.getByTestId('tab-item-all')
expect(inactiveTab).toHaveClass('text-text-tertiary')
})
it('renders the Badge when plugins exist and value is "plugins"', () => {
vi.mocked(useInstalledPluginList).mockReturnValue({
data: { total: 5 },
isLoading: false,
} as ReturnType<typeof useInstalledPluginList>)
render(<TabSlider value="all" options={mockOptions} onChange={onChangeMock} />)
expect(screen.getByText('5')).toBeInTheDocument()
})
it('supports functional itemClassName based on active state', () => {
render(
<TabSlider
value="all"
options={mockOptions}
onChange={onChangeMock}
itemClassName={active => (active ? 'is-active-custom' : 'is-inactive-custom')}
/>,
)
expect(screen.getByTestId('tab-item-all')).toHaveClass('is-active-custom')
expect(screen.getByTestId('tab-item-settings')).toHaveClass('is-inactive-custom')
})
it('updates slider styles based on element dimensions', () => {
// 1. Initial Render
const { rerender } = render(
<TabSlider value="all" options={mockOptions} onChange={onChangeMock} />,
)
// 2. Mock layout properties for the elements now that they are in the DOM
setElementLayout('tab-0', 0, 100)
setElementLayout('tab-1', 120, 80)
// 3. Rerender with the same or new value to trigger the useEffect
// This forces updateSliderStyle to run while the mocked values exist
rerender(<TabSlider value="plugins" options={mockOptions} onChange={onChangeMock} />)
const slider = screen.getByTestId('tab-slider-bg')
// Assert the transform matches the "tab-1" (plugins) layout we mocked
expect(slider.style.transform).toBe('translateX(120px)')
expect(slider.style.width).toBe('80px')
})
it('does not call onChange when clicking the already active tab', () => {
render(<TabSlider value="all" options={mockOptions} onChange={onChangeMock} />)
const activeTab = screen.getByTestId('tab-item-all')
fireEvent.click(activeTab)
expect(onChangeMock).not.toHaveBeenCalled()
})
it('handles invalid value gracefully', () => {
const { container, rerender } = render(<TabSlider value="invalid" options={mockOptions} onChange={onChangeMock} />)
const activeTabs = container.querySelectorAll('.text-text-primary')
expect(activeTabs.length).toBe(0)
// Changing to a valid value should work
rerender(<TabSlider value="all" options={mockOptions} onChange={onChangeMock} />)
expect(screen.getByTestId('tab-item-all')).toHaveClass('text-text-primary')
})
it('supports string itemClassName', () => {
render(
<TabSlider
value="all"
options={mockOptions}
onChange={onChangeMock}
itemClassName="custom-static-class"
/>,
)
expect(screen.getByTestId('tab-item-all')).toHaveClass('custom-static-class')
expect(screen.getByTestId('tab-item-settings')).toHaveClass('custom-static-class')
})
it('handles missing pluginList data gracefully', () => {
vi.mocked(useInstalledPluginList).mockReturnValue({
data: undefined as unknown as { total: number },
isLoading: false,
} as ReturnType<typeof useInstalledPluginList>)
render(<TabSlider value="plugins" options={mockOptions} onChange={onChangeMock} />)
expect(screen.queryByRole('status')).not.toBeInTheDocument() // Badge shouldn't render
})
})