chore: add some tests case code (#29927)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Coding On Star <447357187@qq.com>
This commit is contained in:
368
web/app/components/tools/marketplace/index.spec.tsx
Normal file
368
web/app/components/tools/marketplace/index.spec.tsx
Normal file
@@ -0,0 +1,368 @@
|
||||
import React from 'react'
|
||||
import { act, render, renderHook, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import Marketplace from './index'
|
||||
import { useMarketplace } from './hooks'
|
||||
import { PluginCategoryEnum } from '@/app/components/plugins/types'
|
||||
import { getMarketplaceListCondition } from '@/app/components/plugins/marketplace/utils'
|
||||
import { SCROLL_BOTTOM_THRESHOLD } from '@/app/components/plugins/marketplace/constants'
|
||||
import type { Collection } from '@/app/components/tools/types'
|
||||
import { CollectionType } from '@/app/components/tools/types'
|
||||
import type { Plugin } from '@/app/components/plugins/types'
|
||||
|
||||
const listRenderSpy = jest.fn()
|
||||
jest.mock('@/app/components/plugins/marketplace/list', () => ({
|
||||
__esModule: true,
|
||||
default: (props: {
|
||||
marketplaceCollections: unknown[]
|
||||
marketplaceCollectionPluginsMap: Record<string, unknown[]>
|
||||
plugins?: unknown[]
|
||||
showInstallButton?: boolean
|
||||
locale: string
|
||||
}) => {
|
||||
listRenderSpy(props)
|
||||
return <div data-testid="marketplace-list" />
|
||||
},
|
||||
}))
|
||||
|
||||
const mockUseMarketplaceCollectionsAndPlugins = jest.fn()
|
||||
const mockUseMarketplacePlugins = jest.fn()
|
||||
jest.mock('@/app/components/plugins/marketplace/hooks', () => ({
|
||||
useMarketplaceCollectionsAndPlugins: (...args: unknown[]) => mockUseMarketplaceCollectionsAndPlugins(...args),
|
||||
useMarketplacePlugins: (...args: unknown[]) => mockUseMarketplacePlugins(...args),
|
||||
}))
|
||||
|
||||
const mockUseAllToolProviders = jest.fn()
|
||||
jest.mock('@/service/use-tools', () => ({
|
||||
useAllToolProviders: (...args: unknown[]) => mockUseAllToolProviders(...args),
|
||||
}))
|
||||
|
||||
jest.mock('@/utils/var', () => ({
|
||||
__esModule: true,
|
||||
getMarketplaceUrl: jest.fn(() => 'https://marketplace.test/market'),
|
||||
}))
|
||||
|
||||
jest.mock('@/i18n-config', () => ({
|
||||
getLocaleOnClient: () => 'en',
|
||||
}))
|
||||
|
||||
jest.mock('next-themes', () => ({
|
||||
useTheme: () => ({ theme: 'light' }),
|
||||
}))
|
||||
|
||||
const { getMarketplaceUrl: mockGetMarketplaceUrl } = jest.requireMock('@/utils/var') as {
|
||||
getMarketplaceUrl: jest.Mock
|
||||
}
|
||||
|
||||
const createToolProvider = (overrides: Partial<Collection> = {}): Collection => ({
|
||||
id: 'provider-1',
|
||||
name: 'Provider 1',
|
||||
author: 'Author',
|
||||
description: { en_US: 'desc', zh_Hans: '描述' },
|
||||
icon: 'icon',
|
||||
label: { en_US: 'label', zh_Hans: '标签' },
|
||||
type: CollectionType.custom,
|
||||
team_credentials: {},
|
||||
is_team_authorization: false,
|
||||
allow_delete: false,
|
||||
labels: [],
|
||||
...overrides,
|
||||
})
|
||||
|
||||
const createPlugin = (overrides: Partial<Plugin> = {}): Plugin => ({
|
||||
type: 'plugin',
|
||||
org: 'org',
|
||||
author: 'author',
|
||||
name: 'Plugin One',
|
||||
plugin_id: 'plugin-1',
|
||||
version: '1.0.0',
|
||||
latest_version: '1.0.0',
|
||||
latest_package_identifier: 'plugin-1@1.0.0',
|
||||
icon: 'icon',
|
||||
verified: true,
|
||||
label: { en_US: 'Plugin One' },
|
||||
brief: { en_US: 'Brief' },
|
||||
description: { en_US: 'Plugin description' },
|
||||
introduction: 'Intro',
|
||||
repository: 'https://example.com',
|
||||
category: PluginCategoryEnum.tool,
|
||||
install_count: 0,
|
||||
endpoint: { settings: [] },
|
||||
tags: [{ name: 'tag' }],
|
||||
badges: [],
|
||||
verification: { authorized_category: 'community' },
|
||||
from: 'marketplace',
|
||||
...overrides,
|
||||
})
|
||||
|
||||
const createMarketplaceContext = (overrides: Partial<ReturnType<typeof useMarketplace>> = {}) => ({
|
||||
isLoading: false,
|
||||
marketplaceCollections: [],
|
||||
marketplaceCollectionPluginsMap: {},
|
||||
plugins: [],
|
||||
handleScroll: jest.fn(),
|
||||
page: 1,
|
||||
...overrides,
|
||||
})
|
||||
|
||||
describe('Marketplace', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
// Rendering the marketplace panel based on loading and visibility state.
|
||||
describe('Rendering', () => {
|
||||
it('should show loading indicator when loading first page', () => {
|
||||
// Arrange
|
||||
const marketplaceContext = createMarketplaceContext({ isLoading: true, page: 1 })
|
||||
render(
|
||||
<Marketplace
|
||||
searchPluginText=""
|
||||
filterPluginTags={[]}
|
||||
isMarketplaceArrowVisible={false}
|
||||
showMarketplacePanel={jest.fn()}
|
||||
marketplaceContext={marketplaceContext}
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert
|
||||
expect(document.querySelector('svg.spin-animation')).toBeInTheDocument()
|
||||
expect(screen.queryByTestId('marketplace-list')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render list when not loading', () => {
|
||||
// Arrange
|
||||
const marketplaceContext = createMarketplaceContext({
|
||||
isLoading: false,
|
||||
plugins: [createPlugin()],
|
||||
})
|
||||
render(
|
||||
<Marketplace
|
||||
searchPluginText=""
|
||||
filterPluginTags={[]}
|
||||
isMarketplaceArrowVisible={false}
|
||||
showMarketplacePanel={jest.fn()}
|
||||
marketplaceContext={marketplaceContext}
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('marketplace-list')).toBeInTheDocument()
|
||||
expect(listRenderSpy).toHaveBeenCalledWith(expect.objectContaining({
|
||||
showInstallButton: true,
|
||||
locale: 'en',
|
||||
}))
|
||||
})
|
||||
})
|
||||
|
||||
// Prop-driven UI output such as links and action triggers.
|
||||
describe('Props', () => {
|
||||
it('should build marketplace link and trigger panel when arrow is clicked', async () => {
|
||||
const user = userEvent.setup()
|
||||
// Arrange
|
||||
const marketplaceContext = createMarketplaceContext()
|
||||
const showMarketplacePanel = jest.fn()
|
||||
const { container } = render(
|
||||
<Marketplace
|
||||
searchPluginText="vector"
|
||||
filterPluginTags={['tag-a', 'tag-b']}
|
||||
isMarketplaceArrowVisible
|
||||
showMarketplacePanel={showMarketplacePanel}
|
||||
marketplaceContext={marketplaceContext}
|
||||
/>,
|
||||
)
|
||||
|
||||
// Act
|
||||
const arrowIcon = container.querySelector('svg.cursor-pointer')
|
||||
expect(arrowIcon).toBeTruthy()
|
||||
await user.click(arrowIcon as SVGElement)
|
||||
|
||||
// Assert
|
||||
expect(showMarketplacePanel).toHaveBeenCalledTimes(1)
|
||||
expect(mockGetMarketplaceUrl).toHaveBeenCalledWith('', {
|
||||
language: 'en',
|
||||
q: 'vector',
|
||||
tags: 'tag-a,tag-b',
|
||||
theme: 'light',
|
||||
})
|
||||
const marketplaceLink = screen.getByRole('link', { name: /plugin.marketplace.difyMarketplace/i })
|
||||
expect(marketplaceLink).toHaveAttribute('href', 'https://marketplace.test/market')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('useMarketplace', () => {
|
||||
const mockQueryMarketplaceCollectionsAndPlugins = jest.fn()
|
||||
const mockQueryPlugins = jest.fn()
|
||||
const mockQueryPluginsWithDebounced = jest.fn()
|
||||
const mockResetPlugins = jest.fn()
|
||||
const mockFetchNextPage = jest.fn()
|
||||
|
||||
const setupHookMocks = (overrides?: {
|
||||
isLoading?: boolean
|
||||
isPluginsLoading?: boolean
|
||||
pluginsPage?: number
|
||||
hasNextPage?: boolean
|
||||
plugins?: Plugin[] | undefined
|
||||
}) => {
|
||||
mockUseMarketplaceCollectionsAndPlugins.mockReturnValue({
|
||||
isLoading: overrides?.isLoading ?? false,
|
||||
marketplaceCollections: [],
|
||||
marketplaceCollectionPluginsMap: {},
|
||||
queryMarketplaceCollectionsAndPlugins: mockQueryMarketplaceCollectionsAndPlugins,
|
||||
})
|
||||
mockUseMarketplacePlugins.mockReturnValue({
|
||||
plugins: overrides?.plugins,
|
||||
resetPlugins: mockResetPlugins,
|
||||
queryPlugins: mockQueryPlugins,
|
||||
queryPluginsWithDebounced: mockQueryPluginsWithDebounced,
|
||||
isLoading: overrides?.isPluginsLoading ?? false,
|
||||
fetchNextPage: mockFetchNextPage,
|
||||
hasNextPage: overrides?.hasNextPage ?? false,
|
||||
page: overrides?.pluginsPage,
|
||||
})
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
mockUseAllToolProviders.mockReturnValue({
|
||||
data: [],
|
||||
isSuccess: true,
|
||||
})
|
||||
setupHookMocks()
|
||||
})
|
||||
|
||||
// Query behavior driven by search filters and provider exclusions.
|
||||
describe('Queries', () => {
|
||||
it('should query plugins with debounce when search text is provided', async () => {
|
||||
// Arrange
|
||||
mockUseAllToolProviders.mockReturnValue({
|
||||
data: [
|
||||
createToolProvider({ plugin_id: 'plugin-a' }),
|
||||
createToolProvider({ plugin_id: undefined }),
|
||||
],
|
||||
isSuccess: true,
|
||||
})
|
||||
|
||||
// Act
|
||||
renderHook(() => useMarketplace('alpha', []))
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(mockQueryPluginsWithDebounced).toHaveBeenCalledWith({
|
||||
category: PluginCategoryEnum.tool,
|
||||
query: 'alpha',
|
||||
tags: [],
|
||||
exclude: ['plugin-a'],
|
||||
type: 'plugin',
|
||||
})
|
||||
})
|
||||
expect(mockQueryMarketplaceCollectionsAndPlugins).not.toHaveBeenCalled()
|
||||
expect(mockResetPlugins).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should query plugins immediately when only tags are provided', async () => {
|
||||
// Arrange
|
||||
mockUseAllToolProviders.mockReturnValue({
|
||||
data: [createToolProvider({ plugin_id: 'plugin-b' })],
|
||||
isSuccess: true,
|
||||
})
|
||||
|
||||
// Act
|
||||
renderHook(() => useMarketplace('', ['tag-1']))
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(mockQueryPlugins).toHaveBeenCalledWith({
|
||||
category: PluginCategoryEnum.tool,
|
||||
query: '',
|
||||
tags: ['tag-1'],
|
||||
exclude: ['plugin-b'],
|
||||
type: 'plugin',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('should query collections and reset plugins when no filters are provided', async () => {
|
||||
// Arrange
|
||||
mockUseAllToolProviders.mockReturnValue({
|
||||
data: [createToolProvider({ plugin_id: 'plugin-c' })],
|
||||
isSuccess: true,
|
||||
})
|
||||
|
||||
// Act
|
||||
renderHook(() => useMarketplace('', []))
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(mockQueryMarketplaceCollectionsAndPlugins).toHaveBeenCalledWith({
|
||||
category: PluginCategoryEnum.tool,
|
||||
condition: getMarketplaceListCondition(PluginCategoryEnum.tool),
|
||||
exclude: ['plugin-c'],
|
||||
type: 'plugin',
|
||||
})
|
||||
})
|
||||
expect(mockResetPlugins).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
// State derived from hook inputs and loading signals.
|
||||
describe('State', () => {
|
||||
it('should expose combined loading state and fallback page value', () => {
|
||||
// Arrange
|
||||
setupHookMocks({ isLoading: true, isPluginsLoading: false, pluginsPage: undefined })
|
||||
|
||||
// Act
|
||||
const { result } = renderHook(() => useMarketplace('', []))
|
||||
|
||||
// Assert
|
||||
expect(result.current.isLoading).toBe(true)
|
||||
expect(result.current.page).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
// Scroll handling that triggers pagination when appropriate.
|
||||
describe('Scroll', () => {
|
||||
it('should fetch next page when scrolling near bottom with filters', () => {
|
||||
// Arrange
|
||||
setupHookMocks({ hasNextPage: true })
|
||||
const { result } = renderHook(() => useMarketplace('search', []))
|
||||
const event = {
|
||||
target: {
|
||||
scrollTop: 100,
|
||||
scrollHeight: 200,
|
||||
clientHeight: 100 + SCROLL_BOTTOM_THRESHOLD,
|
||||
},
|
||||
} as unknown as Event
|
||||
|
||||
// Act
|
||||
act(() => {
|
||||
result.current.handleScroll(event)
|
||||
})
|
||||
|
||||
// Assert
|
||||
expect(mockFetchNextPage).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should not fetch next page when no filters are applied', () => {
|
||||
// Arrange
|
||||
setupHookMocks({ hasNextPage: true })
|
||||
const { result } = renderHook(() => useMarketplace('', []))
|
||||
const event = {
|
||||
target: {
|
||||
scrollTop: 100,
|
||||
scrollHeight: 200,
|
||||
clientHeight: 100 + SCROLL_BOTTOM_THRESHOLD,
|
||||
},
|
||||
} as unknown as Event
|
||||
|
||||
// Act
|
||||
act(() => {
|
||||
result.current.handleScroll(event)
|
||||
})
|
||||
|
||||
// Assert
|
||||
expect(mockFetchNextPage).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user