feat(tests): add integration tests for explore app list, installed apps, and sidebar lifecycle flows (#32248)

Co-authored-by: CodingOnStar <hanxujiang@dify.com>
This commit is contained in:
Coding On Star
2026-02-12 10:29:03 +08:00
committed by GitHub
parent b65678bd4c
commit 3fd1eea4d7
27 changed files with 1186 additions and 550 deletions

View File

@ -1,7 +1,7 @@
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'
import { BannerItem } from '../banner-item'
const mockScrollTo = vi.fn()
const mockSlideNodes = vi.fn()
@ -16,17 +16,6 @@ vi.mock('@/app/components/base/carousel', () => ({
}),
}))
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',
@ -40,14 +29,11 @@ const createMockBanner = (overrides: Partial<Banner> = {}): Banner => ({
...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']>) {
@ -59,7 +45,6 @@ class MockResizeObserver {
}
unobserve() {
// No-op
}
}
@ -72,7 +57,6 @@ describe('BannerItem', () => {
vi.stubGlobal('ResizeObserver', MockResizeObserver)
// Mock window.innerWidth for responsive tests
Object.defineProperty(window, 'innerWidth', {
writable: true,
configurable: true,
@ -147,7 +131,7 @@ describe('BannerItem', () => {
/>,
)
expect(screen.getByText('View More')).toBeInTheDocument()
expect(screen.getByText('explore.banner.viewMore')).toBeInTheDocument()
})
})
@ -257,7 +241,6 @@ describe('BannerItem', () => {
/>,
)
// Component should render without issues
expect(screen.getByText('Test Banner Title')).toBeInTheDocument()
})
@ -271,7 +254,6 @@ describe('BannerItem', () => {
/>,
)
// Component should render with isPaused
expect(screen.getByText('Test Banner Title')).toBeInTheDocument()
})
})
@ -320,7 +302,6 @@ describe('BannerItem', () => {
})
it('sets maxWidth when window width is below breakpoint', () => {
// Set window width below RESPONSIVE_BREAKPOINT (1200)
Object.defineProperty(window, 'innerWidth', {
writable: true,
configurable: true,
@ -335,12 +316,10 @@ describe('BannerItem', () => {
/>,
)
// 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,
@ -355,8 +334,7 @@ describe('BannerItem', () => {
/>,
)
// The component should render even with responsive mode
expect(screen.getByText('View More')).toBeInTheDocument()
expect(screen.getByText('explore.banner.viewMore')).toBeInTheDocument()
})
})
@ -432,8 +410,6 @@ describe('BannerItem', () => {
/>,
)
// 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)
})

View File

@ -3,7 +3,7 @@ import type { Banner as BannerType } from '@/models/app'
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
import { act } from 'react'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import Banner from './banner'
import Banner from '../banner'
const mockUseGetBanners = vi.fn()
@ -53,7 +53,7 @@ vi.mock('@/app/components/base/carousel', () => ({
}),
}))
vi.mock('./banner-item', () => ({
vi.mock('../banner-item', () => ({
BannerItem: ({ banner, autoplayDelay, isPaused }: {
banner: BannerType
autoplayDelay: number
@ -105,7 +105,6 @@ describe('Banner', () => {
render(<Banner />)
// Loading component renders a spinner
const loadingWrapper = document.querySelector('[style*="min-height"]')
expect(loadingWrapper).toBeInTheDocument()
})
@ -266,7 +265,6 @@ describe('Banner', () => {
const carousel = screen.getByTestId('carousel')
// Enter and then leave
fireEvent.mouseEnter(carousel)
fireEvent.mouseLeave(carousel)
@ -285,7 +283,6 @@ describe('Banner', () => {
render(<Banner />)
// Trigger resize event
act(() => {
window.dispatchEvent(new Event('resize'))
})
@ -303,12 +300,10 @@ describe('Banner', () => {
render(<Banner />)
// Trigger resize event
act(() => {
window.dispatchEvent(new Event('resize'))
})
// Wait for debounce delay (50ms)
act(() => {
vi.advanceTimersByTime(50)
})
@ -326,31 +321,25 @@ describe('Banner', () => {
render(<Banner />)
// Trigger first resize event
act(() => {
window.dispatchEvent(new Event('resize'))
})
// Wait partial time
act(() => {
vi.advanceTimersByTime(30)
})
// Trigger second resize event
act(() => {
window.dispatchEvent(new Event('resize'))
})
// Wait another 30ms (total 60ms from second resize but only 30ms after)
act(() => {
vi.advanceTimersByTime(30)
})
// Should still be paused (debounce resets)
let bannerItem = screen.getByTestId('banner-item')
expect(bannerItem).toHaveAttribute('data-is-paused', 'true')
// Wait remaining time
act(() => {
vi.advanceTimersByTime(20)
})
@ -388,7 +377,6 @@ describe('Banner', () => {
const { unmount } = render(<Banner />)
// Trigger resize to create timer
act(() => {
window.dispatchEvent(new Event('resize'))
})
@ -462,10 +450,8 @@ describe('Banner', () => {
const { rerender } = render(<Banner />)
// Re-render with same props
rerender(<Banner />)
// Component should still be present (memo doesn't break rendering)
expect(screen.getByTestId('carousel')).toBeInTheDocument()
})
})

View File

@ -1,7 +1,7 @@
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
import { act } from 'react'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { IndicatorButton } from './indicator-button'
import { IndicatorButton } from '../indicator-button'
describe('IndicatorButton', () => {
beforeEach(() => {
@ -164,7 +164,6 @@ describe('IndicatorButton', () => {
/>,
)
// Check for conic-gradient style which indicates progress indicator
const progressIndicator = container.querySelector('[style*="conic-gradient"]')
expect(progressIndicator).not.toBeInTheDocument()
})
@ -221,10 +220,8 @@ describe('IndicatorButton', () => {
/>,
)
// Initially no progress indicator
expect(container.querySelector('[style*="conic-gradient"]')).not.toBeInTheDocument()
// Rerender with isNextSlide=true
rerender(
<IndicatorButton
index={1}
@ -237,7 +234,6 @@ describe('IndicatorButton', () => {
/>,
)
// Now progress indicator should be visible
expect(container.querySelector('[style*="conic-gradient"]')).toBeInTheDocument()
})
@ -255,11 +251,9 @@ describe('IndicatorButton', () => {
/>,
)
// Progress indicator should be present
const progressIndicator = container.querySelector('[style*="conic-gradient"]')
expect(progressIndicator).toBeInTheDocument()
// Rerender with new resetKey - this should reset the progress animation
rerender(
<IndicatorButton
index={1}
@ -273,7 +267,6 @@ describe('IndicatorButton', () => {
)
const newProgressIndicator = container.querySelector('[style*="conic-gradient"]')
// The progress indicator should still be present after reset
expect(newProgressIndicator).toBeInTheDocument()
})
@ -293,8 +286,6 @@ describe('IndicatorButton', () => {
/>,
)
// The component should still render but animation should be paused
// requestAnimationFrame might still be called for polling but progress won't update
expect(screen.getByRole('button')).toBeInTheDocument()
mockRequestAnimationFrame.mockRestore()
})
@ -315,7 +306,6 @@ describe('IndicatorButton', () => {
/>,
)
// Trigger animation frame
act(() => {
vi.advanceTimersToNextTimer()
})
@ -342,12 +332,10 @@ describe('IndicatorButton', () => {
/>,
)
// Trigger animation frame
act(() => {
vi.advanceTimersToNextTimer()
})
// Change isNextSlide to false - this should cancel the animation frame
rerender(
<IndicatorButton
index={1}
@ -368,7 +356,6 @@ describe('IndicatorButton', () => {
const mockOnClick = vi.fn()
const mockRequestAnimationFrame = vi.spyOn(window, 'requestAnimationFrame')
// Mock document.hidden to be true
Object.defineProperty(document, 'hidden', {
writable: true,
configurable: true,
@ -387,10 +374,8 @@ describe('IndicatorButton', () => {
/>,
)
// Component should still render
expect(screen.getByRole('button')).toBeInTheDocument()
// Reset document.hidden
Object.defineProperty(document, 'hidden', {
writable: true,
configurable: true,
@ -415,7 +400,6 @@ describe('IndicatorButton', () => {
/>,
)
// Progress indicator should be visible (animation running)
expect(container.querySelector('[style*="conic-gradient"]')).toBeInTheDocument()
})
})