mirror of
https://github.com/langgenius/dify.git
synced 2026-04-28 11:56:55 +08:00
Co-authored-by: CodingOnStar <hanxujiang@dify.com> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
484 lines
13 KiB
TypeScript
484 lines
13 KiB
TypeScript
import type { Banner } from '@/models/app'
|
|
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import { BannerItem } from './banner-item'
|
|
|
|
const mockScrollTo = vi.fn()
|
|
const mockSlideNodes = vi.fn()
|
|
|
|
vi.mock('@/app/components/base/carousel', () => ({
|
|
useCarousel: () => ({
|
|
api: {
|
|
scrollTo: mockScrollTo,
|
|
slideNodes: mockSlideNodes,
|
|
},
|
|
selectedIndex: 0,
|
|
}),
|
|
}))
|
|
|
|
vi.mock('react-i18next', () => ({
|
|
useTranslation: () => ({
|
|
t: (key: string) => {
|
|
const translations: Record<string, string> = {
|
|
'banner.viewMore': 'View More',
|
|
}
|
|
return translations[key] || key
|
|
},
|
|
}),
|
|
}))
|
|
|
|
const createMockBanner = (overrides: Partial<Banner> = {}): Banner => ({
|
|
id: 'banner-1',
|
|
status: 'enabled',
|
|
link: 'https://example.com',
|
|
content: {
|
|
'category': 'Featured',
|
|
'title': 'Test Banner Title',
|
|
'description': 'Test banner description text',
|
|
'img-src': 'https://example.com/image.png',
|
|
},
|
|
...overrides,
|
|
} as Banner)
|
|
|
|
// Mock ResizeObserver methods declared at module level and initialized
|
|
const mockResizeObserverObserve = vi.fn()
|
|
const mockResizeObserverDisconnect = vi.fn()
|
|
|
|
// Create mock class outside of describe block for proper hoisting
|
|
class MockResizeObserver {
|
|
constructor(_callback: ResizeObserverCallback) {
|
|
// Store callback if needed
|
|
}
|
|
|
|
observe(...args: Parameters<ResizeObserver['observe']>) {
|
|
mockResizeObserverObserve(...args)
|
|
}
|
|
|
|
disconnect() {
|
|
mockResizeObserverDisconnect()
|
|
}
|
|
|
|
unobserve() {
|
|
// No-op
|
|
}
|
|
}
|
|
|
|
describe('BannerItem', () => {
|
|
let mockWindowOpen: ReturnType<typeof vi.spyOn>
|
|
|
|
beforeEach(() => {
|
|
mockWindowOpen = vi.spyOn(window, 'open').mockImplementation(() => null)
|
|
mockSlideNodes.mockReturnValue([{}, {}, {}]) // 3 slides
|
|
|
|
vi.stubGlobal('ResizeObserver', MockResizeObserver)
|
|
|
|
// Mock window.innerWidth for responsive tests
|
|
Object.defineProperty(window, 'innerWidth', {
|
|
writable: true,
|
|
configurable: true,
|
|
value: 1400, // Above RESPONSIVE_BREAKPOINT (1200)
|
|
})
|
|
})
|
|
|
|
afterEach(() => {
|
|
cleanup()
|
|
vi.clearAllMocks()
|
|
vi.unstubAllGlobals()
|
|
mockWindowOpen.mockRestore()
|
|
})
|
|
|
|
describe('basic rendering', () => {
|
|
it('renders banner category', () => {
|
|
const banner = createMockBanner()
|
|
render(
|
|
<BannerItem
|
|
banner={banner}
|
|
autoplayDelay={5000}
|
|
/>,
|
|
)
|
|
|
|
expect(screen.getByText('Featured')).toBeInTheDocument()
|
|
})
|
|
|
|
it('renders banner title', () => {
|
|
const banner = createMockBanner()
|
|
render(
|
|
<BannerItem
|
|
banner={banner}
|
|
autoplayDelay={5000}
|
|
/>,
|
|
)
|
|
|
|
expect(screen.getByText('Test Banner Title')).toBeInTheDocument()
|
|
})
|
|
|
|
it('renders banner description', () => {
|
|
const banner = createMockBanner()
|
|
render(
|
|
<BannerItem
|
|
banner={banner}
|
|
autoplayDelay={5000}
|
|
/>,
|
|
)
|
|
|
|
expect(screen.getByText('Test banner description text')).toBeInTheDocument()
|
|
})
|
|
|
|
it('renders banner image with correct src and alt', () => {
|
|
const banner = createMockBanner()
|
|
render(
|
|
<BannerItem
|
|
banner={banner}
|
|
autoplayDelay={5000}
|
|
/>,
|
|
)
|
|
|
|
const image = screen.getByRole('img')
|
|
expect(image).toHaveAttribute('src', 'https://example.com/image.png')
|
|
expect(image).toHaveAttribute('alt', 'Test Banner Title')
|
|
})
|
|
|
|
it('renders view more text', () => {
|
|
const banner = createMockBanner()
|
|
render(
|
|
<BannerItem
|
|
banner={banner}
|
|
autoplayDelay={5000}
|
|
/>,
|
|
)
|
|
|
|
expect(screen.getByText('View More')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('click handling', () => {
|
|
it('opens banner link in new tab when clicked', () => {
|
|
const banner = createMockBanner({ link: 'https://test-link.com' })
|
|
render(
|
|
<BannerItem
|
|
banner={banner}
|
|
autoplayDelay={5000}
|
|
/>,
|
|
)
|
|
|
|
const bannerElement = screen.getByText('Test Banner Title').closest('div[class*="cursor-pointer"]')
|
|
fireEvent.click(bannerElement!)
|
|
|
|
expect(mockWindowOpen).toHaveBeenCalledWith(
|
|
'https://test-link.com',
|
|
'_blank',
|
|
'noopener,noreferrer',
|
|
)
|
|
})
|
|
|
|
it('does not open window when banner has no link', () => {
|
|
const banner = createMockBanner({ link: '' })
|
|
render(
|
|
<BannerItem
|
|
banner={banner}
|
|
autoplayDelay={5000}
|
|
/>,
|
|
)
|
|
|
|
const bannerElement = screen.getByText('Test Banner Title').closest('div[class*="cursor-pointer"]')
|
|
fireEvent.click(bannerElement!)
|
|
|
|
expect(mockWindowOpen).not.toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
describe('slide indicators', () => {
|
|
it('renders correct number of indicator buttons', () => {
|
|
mockSlideNodes.mockReturnValue([{}, {}, {}])
|
|
const banner = createMockBanner()
|
|
render(
|
|
<BannerItem
|
|
banner={banner}
|
|
autoplayDelay={5000}
|
|
/>,
|
|
)
|
|
|
|
const buttons = screen.getAllByRole('button')
|
|
expect(buttons).toHaveLength(3)
|
|
})
|
|
|
|
it('renders indicator buttons with correct numbers', () => {
|
|
mockSlideNodes.mockReturnValue([{}, {}, {}])
|
|
const banner = createMockBanner()
|
|
render(
|
|
<BannerItem
|
|
banner={banner}
|
|
autoplayDelay={5000}
|
|
/>,
|
|
)
|
|
|
|
expect(screen.getByText('01')).toBeInTheDocument()
|
|
expect(screen.getByText('02')).toBeInTheDocument()
|
|
expect(screen.getByText('03')).toBeInTheDocument()
|
|
})
|
|
|
|
it('calls scrollTo when indicator is clicked', () => {
|
|
mockSlideNodes.mockReturnValue([{}, {}, {}])
|
|
const banner = createMockBanner()
|
|
render(
|
|
<BannerItem
|
|
banner={banner}
|
|
autoplayDelay={5000}
|
|
/>,
|
|
)
|
|
|
|
const secondIndicator = screen.getByText('02').closest('button')
|
|
fireEvent.click(secondIndicator!)
|
|
|
|
expect(mockScrollTo).toHaveBeenCalledWith(1)
|
|
})
|
|
|
|
it('renders no indicators when no slides', () => {
|
|
mockSlideNodes.mockReturnValue([])
|
|
const banner = createMockBanner()
|
|
render(
|
|
<BannerItem
|
|
banner={banner}
|
|
autoplayDelay={5000}
|
|
/>,
|
|
)
|
|
|
|
expect(screen.queryByRole('button')).not.toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('isPaused prop', () => {
|
|
it('defaults isPaused to false', () => {
|
|
const banner = createMockBanner()
|
|
render(
|
|
<BannerItem
|
|
banner={banner}
|
|
autoplayDelay={5000}
|
|
/>,
|
|
)
|
|
|
|
// Component should render without issues
|
|
expect(screen.getByText('Test Banner Title')).toBeInTheDocument()
|
|
})
|
|
|
|
it('accepts isPaused prop', () => {
|
|
const banner = createMockBanner()
|
|
render(
|
|
<BannerItem
|
|
banner={banner}
|
|
autoplayDelay={5000}
|
|
isPaused={true}
|
|
/>,
|
|
)
|
|
|
|
// Component should render with isPaused
|
|
expect(screen.getByText('Test Banner Title')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('responsive behavior', () => {
|
|
it('sets up ResizeObserver on mount', () => {
|
|
const banner = createMockBanner()
|
|
render(
|
|
<BannerItem
|
|
banner={banner}
|
|
autoplayDelay={5000}
|
|
/>,
|
|
)
|
|
|
|
expect(mockResizeObserverObserve).toHaveBeenCalled()
|
|
})
|
|
|
|
it('adds resize event listener on mount', () => {
|
|
const addEventListenerSpy = vi.spyOn(window, 'addEventListener')
|
|
const banner = createMockBanner()
|
|
render(
|
|
<BannerItem
|
|
banner={banner}
|
|
autoplayDelay={5000}
|
|
/>,
|
|
)
|
|
|
|
expect(addEventListenerSpy).toHaveBeenCalledWith('resize', expect.any(Function))
|
|
addEventListenerSpy.mockRestore()
|
|
})
|
|
|
|
it('removes resize event listener on unmount', () => {
|
|
const removeEventListenerSpy = vi.spyOn(window, 'removeEventListener')
|
|
const banner = createMockBanner()
|
|
const { unmount } = render(
|
|
<BannerItem
|
|
banner={banner}
|
|
autoplayDelay={5000}
|
|
/>,
|
|
)
|
|
|
|
unmount()
|
|
|
|
expect(removeEventListenerSpy).toHaveBeenCalledWith('resize', expect.any(Function))
|
|
removeEventListenerSpy.mockRestore()
|
|
})
|
|
|
|
it('sets maxWidth when window width is below breakpoint', () => {
|
|
// Set window width below RESPONSIVE_BREAKPOINT (1200)
|
|
Object.defineProperty(window, 'innerWidth', {
|
|
writable: true,
|
|
configurable: true,
|
|
value: 1000,
|
|
})
|
|
|
|
const banner = createMockBanner()
|
|
render(
|
|
<BannerItem
|
|
banner={banner}
|
|
autoplayDelay={5000}
|
|
/>,
|
|
)
|
|
|
|
// Component should render and apply responsive styles
|
|
expect(screen.getByText('Test Banner Title')).toBeInTheDocument()
|
|
})
|
|
|
|
it('applies responsive styles when below breakpoint', () => {
|
|
// Set window width below RESPONSIVE_BREAKPOINT (1200)
|
|
Object.defineProperty(window, 'innerWidth', {
|
|
writable: true,
|
|
configurable: true,
|
|
value: 800,
|
|
})
|
|
|
|
const banner = createMockBanner()
|
|
render(
|
|
<BannerItem
|
|
banner={banner}
|
|
autoplayDelay={5000}
|
|
/>,
|
|
)
|
|
|
|
// The component should render even with responsive mode
|
|
expect(screen.getByText('View More')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('content variations', () => {
|
|
it('renders long category text', () => {
|
|
const banner = createMockBanner({
|
|
content: {
|
|
'category': 'Very Long Category Name',
|
|
'title': 'Title',
|
|
'description': 'Description',
|
|
'img-src': 'https://example.com/img.png',
|
|
},
|
|
} as Partial<Banner>)
|
|
render(
|
|
<BannerItem
|
|
banner={banner}
|
|
autoplayDelay={5000}
|
|
/>,
|
|
)
|
|
|
|
expect(screen.getByText('Very Long Category Name')).toBeInTheDocument()
|
|
})
|
|
|
|
it('renders long title with truncation class', () => {
|
|
const banner = createMockBanner({
|
|
content: {
|
|
'category': 'Category',
|
|
'title': 'A Very Long Title That Should Be Truncated Eventually',
|
|
'description': 'Description',
|
|
'img-src': 'https://example.com/img.png',
|
|
},
|
|
} as Partial<Banner>)
|
|
render(
|
|
<BannerItem
|
|
banner={banner}
|
|
autoplayDelay={5000}
|
|
/>,
|
|
)
|
|
|
|
const titleElement = screen.getByText('A Very Long Title That Should Be Truncated Eventually')
|
|
expect(titleElement).toHaveClass('line-clamp-2')
|
|
})
|
|
|
|
it('renders long description with truncation class', () => {
|
|
const banner = createMockBanner({
|
|
content: {
|
|
'category': 'Category',
|
|
'title': 'Title',
|
|
'description': 'A very long description that should be limited to a certain number of lines for proper display in the banner component.',
|
|
'img-src': 'https://example.com/img.png',
|
|
},
|
|
} as Partial<Banner>)
|
|
render(
|
|
<BannerItem
|
|
banner={banner}
|
|
autoplayDelay={5000}
|
|
/>,
|
|
)
|
|
|
|
const descriptionElement = screen.getByText(/A very long description/)
|
|
expect(descriptionElement).toHaveClass('line-clamp-4')
|
|
})
|
|
})
|
|
|
|
describe('slide calculation', () => {
|
|
it('calculates next index correctly for first slide', () => {
|
|
mockSlideNodes.mockReturnValue([{}, {}, {}])
|
|
const banner = createMockBanner()
|
|
render(
|
|
<BannerItem
|
|
banner={banner}
|
|
autoplayDelay={5000}
|
|
/>,
|
|
)
|
|
|
|
// With selectedIndex=0 and 3 slides, nextIndex should be 1
|
|
// The second indicator button should show the "next slide" state
|
|
const buttons = screen.getAllByRole('button')
|
|
expect(buttons).toHaveLength(3)
|
|
})
|
|
|
|
it('handles single slide case', () => {
|
|
mockSlideNodes.mockReturnValue([{}])
|
|
const banner = createMockBanner()
|
|
render(
|
|
<BannerItem
|
|
banner={banner}
|
|
autoplayDelay={5000}
|
|
/>,
|
|
)
|
|
|
|
const buttons = screen.getAllByRole('button')
|
|
expect(buttons).toHaveLength(1)
|
|
})
|
|
})
|
|
|
|
describe('wrapper styling', () => {
|
|
it('has cursor-pointer class', () => {
|
|
const banner = createMockBanner()
|
|
const { container } = render(
|
|
<BannerItem
|
|
banner={banner}
|
|
autoplayDelay={5000}
|
|
/>,
|
|
)
|
|
|
|
const wrapper = container.firstChild as HTMLElement
|
|
expect(wrapper).toHaveClass('cursor-pointer')
|
|
})
|
|
|
|
it('has rounded-2xl class', () => {
|
|
const banner = createMockBanner()
|
|
const { container } = render(
|
|
<BannerItem
|
|
banner={banner}
|
|
autoplayDelay={5000}
|
|
/>,
|
|
)
|
|
|
|
const wrapper = container.firstChild as HTMLElement
|
|
expect(wrapper).toHaveClass('rounded-2xl')
|
|
})
|
|
})
|
|
})
|