mirror of
https://github.com/langgenius/dify.git
synced 2026-05-05 18:08:07 +08:00
test(web): add comprehensive unit and integration tests for plugins and tools modules (#32220)
Co-authored-by: CodingOnStar <hanxujiang@dify.com>
This commit is contained in:
@ -1,59 +1,10 @@
|
||||
import { renderHook } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { PLUGIN_PAGE_TABS_MAP, useCategories, usePluginPageTabs, useTags } from './hooks'
|
||||
|
||||
// Create mock translation function
|
||||
const mockT = vi.fn((key: string, _options?: Record<string, string>) => {
|
||||
const translations: Record<string, string> = {
|
||||
'tags.agent': 'Agent',
|
||||
'tags.rag': 'RAG',
|
||||
'tags.search': 'Search',
|
||||
'tags.image': 'Image',
|
||||
'tags.videos': 'Videos',
|
||||
'tags.weather': 'Weather',
|
||||
'tags.finance': 'Finance',
|
||||
'tags.design': 'Design',
|
||||
'tags.travel': 'Travel',
|
||||
'tags.social': 'Social',
|
||||
'tags.news': 'News',
|
||||
'tags.medical': 'Medical',
|
||||
'tags.productivity': 'Productivity',
|
||||
'tags.education': 'Education',
|
||||
'tags.business': 'Business',
|
||||
'tags.entertainment': 'Entertainment',
|
||||
'tags.utilities': 'Utilities',
|
||||
'tags.other': 'Other',
|
||||
'category.models': 'Models',
|
||||
'category.tools': 'Tools',
|
||||
'category.datasources': 'Datasources',
|
||||
'category.agents': 'Agents',
|
||||
'category.extensions': 'Extensions',
|
||||
'category.bundles': 'Bundles',
|
||||
'category.triggers': 'Triggers',
|
||||
'categorySingle.model': 'Model',
|
||||
'categorySingle.tool': 'Tool',
|
||||
'categorySingle.datasource': 'Datasource',
|
||||
'categorySingle.agent': 'Agent',
|
||||
'categorySingle.extension': 'Extension',
|
||||
'categorySingle.bundle': 'Bundle',
|
||||
'categorySingle.trigger': 'Trigger',
|
||||
'menus.plugins': 'Plugins',
|
||||
'menus.exploreMarketplace': 'Explore Marketplace',
|
||||
}
|
||||
return translations[key] || key
|
||||
})
|
||||
|
||||
// Mock react-i18next
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: mockT,
|
||||
}),
|
||||
}))
|
||||
import { PLUGIN_PAGE_TABS_MAP, useCategories, usePluginPageTabs, useTags } from '../hooks'
|
||||
|
||||
describe('useTags', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockT.mockClear()
|
||||
})
|
||||
|
||||
describe('Rendering', () => {
|
||||
@ -65,13 +16,12 @@ describe('useTags', () => {
|
||||
expect(result.current.tags.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('should call translation function for each tag', () => {
|
||||
renderHook(() => useTags())
|
||||
it('should return tags with translated labels', () => {
|
||||
const { result } = renderHook(() => useTags())
|
||||
|
||||
// Verify t() was called for tag translations
|
||||
expect(mockT).toHaveBeenCalled()
|
||||
const tagCalls = mockT.mock.calls.filter(call => call[0].startsWith('tags.'))
|
||||
expect(tagCalls.length).toBeGreaterThan(0)
|
||||
result.current.tags.forEach((tag) => {
|
||||
expect(tag.label).toBe(`pluginTags.tags.${tag.name}`)
|
||||
})
|
||||
})
|
||||
|
||||
it('should return tags with name and label properties', () => {
|
||||
@ -99,7 +49,7 @@ describe('useTags', () => {
|
||||
|
||||
expect(result.current.tagsMap.agent).toBeDefined()
|
||||
expect(result.current.tagsMap.agent.name).toBe('agent')
|
||||
expect(result.current.tagsMap.agent.label).toBe('Agent')
|
||||
expect(result.current.tagsMap.agent.label).toBe('pluginTags.tags.agent')
|
||||
})
|
||||
|
||||
it('should contain all tags from tags array', () => {
|
||||
@ -116,9 +66,8 @@ describe('useTags', () => {
|
||||
it('should return label for existing tag', () => {
|
||||
const { result } = renderHook(() => useTags())
|
||||
|
||||
// Test existing tags - this covers the branch where tagsMap[name] exists
|
||||
expect(result.current.getTagLabel('agent')).toBe('Agent')
|
||||
expect(result.current.getTagLabel('search')).toBe('Search')
|
||||
expect(result.current.getTagLabel('agent')).toBe('pluginTags.tags.agent')
|
||||
expect(result.current.getTagLabel('search')).toBe('pluginTags.tags.search')
|
||||
})
|
||||
|
||||
it('should return name for non-existing tag', () => {
|
||||
@ -132,11 +81,9 @@ describe('useTags', () => {
|
||||
it('should cover both branches of getTagLabel conditional', () => {
|
||||
const { result } = renderHook(() => useTags())
|
||||
|
||||
// Branch 1: tag exists in tagsMap - returns label
|
||||
const existingTagResult = result.current.getTagLabel('rag')
|
||||
expect(existingTagResult).toBe('RAG')
|
||||
expect(existingTagResult).toBe('pluginTags.tags.rag')
|
||||
|
||||
// Branch 2: tag does not exist in tagsMap - returns name itself
|
||||
const nonExistingTagResult = result.current.getTagLabel('unknown-tag-xyz')
|
||||
expect(nonExistingTagResult).toBe('unknown-tag-xyz')
|
||||
})
|
||||
@ -150,23 +97,22 @@ describe('useTags', () => {
|
||||
it('should return correct labels for all predefined tags', () => {
|
||||
const { result } = renderHook(() => useTags())
|
||||
|
||||
// Test all predefined tags
|
||||
expect(result.current.getTagLabel('rag')).toBe('RAG')
|
||||
expect(result.current.getTagLabel('image')).toBe('Image')
|
||||
expect(result.current.getTagLabel('videos')).toBe('Videos')
|
||||
expect(result.current.getTagLabel('weather')).toBe('Weather')
|
||||
expect(result.current.getTagLabel('finance')).toBe('Finance')
|
||||
expect(result.current.getTagLabel('design')).toBe('Design')
|
||||
expect(result.current.getTagLabel('travel')).toBe('Travel')
|
||||
expect(result.current.getTagLabel('social')).toBe('Social')
|
||||
expect(result.current.getTagLabel('news')).toBe('News')
|
||||
expect(result.current.getTagLabel('medical')).toBe('Medical')
|
||||
expect(result.current.getTagLabel('productivity')).toBe('Productivity')
|
||||
expect(result.current.getTagLabel('education')).toBe('Education')
|
||||
expect(result.current.getTagLabel('business')).toBe('Business')
|
||||
expect(result.current.getTagLabel('entertainment')).toBe('Entertainment')
|
||||
expect(result.current.getTagLabel('utilities')).toBe('Utilities')
|
||||
expect(result.current.getTagLabel('other')).toBe('Other')
|
||||
expect(result.current.getTagLabel('rag')).toBe('pluginTags.tags.rag')
|
||||
expect(result.current.getTagLabel('image')).toBe('pluginTags.tags.image')
|
||||
expect(result.current.getTagLabel('videos')).toBe('pluginTags.tags.videos')
|
||||
expect(result.current.getTagLabel('weather')).toBe('pluginTags.tags.weather')
|
||||
expect(result.current.getTagLabel('finance')).toBe('pluginTags.tags.finance')
|
||||
expect(result.current.getTagLabel('design')).toBe('pluginTags.tags.design')
|
||||
expect(result.current.getTagLabel('travel')).toBe('pluginTags.tags.travel')
|
||||
expect(result.current.getTagLabel('social')).toBe('pluginTags.tags.social')
|
||||
expect(result.current.getTagLabel('news')).toBe('pluginTags.tags.news')
|
||||
expect(result.current.getTagLabel('medical')).toBe('pluginTags.tags.medical')
|
||||
expect(result.current.getTagLabel('productivity')).toBe('pluginTags.tags.productivity')
|
||||
expect(result.current.getTagLabel('education')).toBe('pluginTags.tags.education')
|
||||
expect(result.current.getTagLabel('business')).toBe('pluginTags.tags.business')
|
||||
expect(result.current.getTagLabel('entertainment')).toBe('pluginTags.tags.entertainment')
|
||||
expect(result.current.getTagLabel('utilities')).toBe('pluginTags.tags.utilities')
|
||||
expect(result.current.getTagLabel('other')).toBe('pluginTags.tags.other')
|
||||
})
|
||||
|
||||
it('should handle empty string tag name', () => {
|
||||
@ -255,27 +201,27 @@ describe('useCategories', () => {
|
||||
it('should use plural labels when isSingle is false', () => {
|
||||
const { result } = renderHook(() => useCategories(false))
|
||||
|
||||
expect(result.current.categoriesMap.tool.label).toBe('Tools')
|
||||
expect(result.current.categoriesMap.tool.label).toBe('plugin.category.tools')
|
||||
})
|
||||
|
||||
it('should use plural labels when isSingle is undefined', () => {
|
||||
const { result } = renderHook(() => useCategories())
|
||||
|
||||
expect(result.current.categoriesMap.tool.label).toBe('Tools')
|
||||
expect(result.current.categoriesMap.tool.label).toBe('plugin.category.tools')
|
||||
})
|
||||
|
||||
it('should use singular labels when isSingle is true', () => {
|
||||
const { result } = renderHook(() => useCategories(true))
|
||||
|
||||
expect(result.current.categoriesMap.tool.label).toBe('Tool')
|
||||
expect(result.current.categoriesMap.tool.label).toBe('plugin.categorySingle.tool')
|
||||
})
|
||||
|
||||
it('should handle agent category specially', () => {
|
||||
const { result: resultPlural } = renderHook(() => useCategories(false))
|
||||
const { result: resultSingle } = renderHook(() => useCategories(true))
|
||||
|
||||
expect(resultPlural.current.categoriesMap['agent-strategy'].label).toBe('Agents')
|
||||
expect(resultSingle.current.categoriesMap['agent-strategy'].label).toBe('Agent')
|
||||
expect(resultPlural.current.categoriesMap['agent-strategy'].label).toBe('plugin.category.agents')
|
||||
expect(resultSingle.current.categoriesMap['agent-strategy'].label).toBe('plugin.categorySingle.agent')
|
||||
})
|
||||
})
|
||||
|
||||
@ -298,7 +244,6 @@ describe('useCategories', () => {
|
||||
describe('usePluginPageTabs', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockT.mockClear()
|
||||
})
|
||||
|
||||
describe('Rendering', () => {
|
||||
@ -326,12 +271,11 @@ describe('usePluginPageTabs', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('should call translation function for tab texts', () => {
|
||||
renderHook(() => usePluginPageTabs())
|
||||
it('should return tabs with translated texts', () => {
|
||||
const { result } = renderHook(() => usePluginPageTabs())
|
||||
|
||||
// Verify t() was called for menu translations
|
||||
expect(mockT).toHaveBeenCalledWith('menus.plugins', { ns: 'common' })
|
||||
expect(mockT).toHaveBeenCalledWith('menus.exploreMarketplace', { ns: 'common' })
|
||||
expect(result.current[0].text).toBe('common.menus.plugins')
|
||||
expect(result.current[1].text).toBe('common.menus.exploreMarketplace')
|
||||
})
|
||||
})
|
||||
|
||||
@ -342,7 +286,7 @@ describe('usePluginPageTabs', () => {
|
||||
const pluginsTab = result.current.find(tab => tab.value === PLUGIN_PAGE_TABS_MAP.plugins)
|
||||
expect(pluginsTab).toBeDefined()
|
||||
expect(pluginsTab?.value).toBe('plugins')
|
||||
expect(pluginsTab?.text).toBe('Plugins')
|
||||
expect(pluginsTab?.text).toBe('common.menus.plugins')
|
||||
})
|
||||
|
||||
it('should have marketplace tab with correct value', () => {
|
||||
@ -351,7 +295,7 @@ describe('usePluginPageTabs', () => {
|
||||
const marketplaceTab = result.current.find(tab => tab.value === PLUGIN_PAGE_TABS_MAP.marketplace)
|
||||
expect(marketplaceTab).toBeDefined()
|
||||
expect(marketplaceTab?.value).toBe('discover')
|
||||
expect(marketplaceTab?.text).toBe('Explore Marketplace')
|
||||
expect(marketplaceTab?.text).toBe('common.menus.exploreMarketplace')
|
||||
})
|
||||
})
|
||||
|
||||
@ -360,14 +304,14 @@ describe('usePluginPageTabs', () => {
|
||||
const { result } = renderHook(() => usePluginPageTabs())
|
||||
|
||||
expect(result.current[0].value).toBe('plugins')
|
||||
expect(result.current[0].text).toBe('Plugins')
|
||||
expect(result.current[0].text).toBe('common.menus.plugins')
|
||||
})
|
||||
|
||||
it('should return marketplace tab as second tab', () => {
|
||||
const { result } = renderHook(() => usePluginPageTabs())
|
||||
|
||||
expect(result.current[1].value).toBe('discover')
|
||||
expect(result.current[1].text).toBe('Explore Marketplace')
|
||||
expect(result.current[1].text).toBe('common.menus.exploreMarketplace')
|
||||
})
|
||||
})
|
||||
|
||||
50
web/app/components/plugins/__tests__/utils.spec.ts
Normal file
50
web/app/components/plugins/__tests__/utils.spec.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import type { TagKey } from '../constants'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { PluginCategoryEnum } from '../types'
|
||||
import { getValidCategoryKeys, getValidTagKeys } from '../utils'
|
||||
|
||||
describe('plugins/utils', () => {
|
||||
describe('getValidTagKeys', () => {
|
||||
it('returns only valid tag keys from the predefined set', () => {
|
||||
const input = ['agent', 'rag', 'invalid-tag', 'search'] as TagKey[]
|
||||
const result = getValidTagKeys(input)
|
||||
expect(result).toEqual(['agent', 'rag', 'search'])
|
||||
})
|
||||
|
||||
it('returns empty array when no valid tags', () => {
|
||||
const result = getValidTagKeys(['foo', 'bar'] as unknown as TagKey[])
|
||||
expect(result).toEqual([])
|
||||
})
|
||||
|
||||
it('returns empty array for empty input', () => {
|
||||
expect(getValidTagKeys([])).toEqual([])
|
||||
})
|
||||
|
||||
it('preserves all valid tags when all are valid', () => {
|
||||
const input: TagKey[] = ['agent', 'rag', 'search', 'image']
|
||||
const result = getValidTagKeys(input)
|
||||
expect(result).toEqual(input)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getValidCategoryKeys', () => {
|
||||
it('returns matching category for valid key', () => {
|
||||
expect(getValidCategoryKeys(PluginCategoryEnum.model)).toBe(PluginCategoryEnum.model)
|
||||
expect(getValidCategoryKeys(PluginCategoryEnum.tool)).toBe(PluginCategoryEnum.tool)
|
||||
expect(getValidCategoryKeys(PluginCategoryEnum.agent)).toBe(PluginCategoryEnum.agent)
|
||||
expect(getValidCategoryKeys('bundle')).toBe('bundle')
|
||||
})
|
||||
|
||||
it('returns undefined for invalid category', () => {
|
||||
expect(getValidCategoryKeys('nonexistent')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('returns undefined for undefined input', () => {
|
||||
expect(getValidCategoryKeys(undefined)).toBeUndefined()
|
||||
})
|
||||
|
||||
it('returns undefined for empty string', () => {
|
||||
expect(getValidCategoryKeys('')).toBeUndefined()
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,92 @@
|
||||
import { cleanup, render, screen } from '@testing-library/react'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import DeprecationNotice from '../deprecation-notice'
|
||||
|
||||
vi.mock('next/link', () => ({
|
||||
default: ({ children, href }: { children: React.ReactNode, href: string }) => (
|
||||
<a data-testid="link" href={href}>{children}</a>
|
||||
),
|
||||
}))
|
||||
|
||||
describe('DeprecationNotice', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
})
|
||||
|
||||
it('returns null when status is not "deleted"', () => {
|
||||
const { container } = render(
|
||||
<DeprecationNotice
|
||||
status="active"
|
||||
deprecatedReason="business_adjustments"
|
||||
alternativePluginId="alt-plugin"
|
||||
alternativePluginURL="/plugins/alt-plugin"
|
||||
/>,
|
||||
)
|
||||
expect(container.firstChild).toBeNull()
|
||||
})
|
||||
|
||||
it('renders deprecation notice when status is "deleted"', () => {
|
||||
render(
|
||||
<DeprecationNotice
|
||||
status="deleted"
|
||||
deprecatedReason=""
|
||||
alternativePluginId=""
|
||||
alternativePluginURL=""
|
||||
/>,
|
||||
)
|
||||
expect(screen.getByText('plugin.detailPanel.deprecation.noReason')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders with valid reason and alternative plugin', () => {
|
||||
render(
|
||||
<DeprecationNotice
|
||||
status="deleted"
|
||||
deprecatedReason="business_adjustments"
|
||||
alternativePluginId="better-plugin"
|
||||
alternativePluginURL="/plugins/better-plugin"
|
||||
/>,
|
||||
)
|
||||
expect(screen.getByText('detailPanel.deprecation.fullMessage')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders only reason without alternative plugin', () => {
|
||||
render(
|
||||
<DeprecationNotice
|
||||
status="deleted"
|
||||
deprecatedReason="no_maintainer"
|
||||
alternativePluginId=""
|
||||
alternativePluginURL=""
|
||||
/>,
|
||||
)
|
||||
expect(screen.getByText(/plugin\.detailPanel\.deprecation\.onlyReason/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders no-reason message for invalid reason', () => {
|
||||
render(
|
||||
<DeprecationNotice
|
||||
status="deleted"
|
||||
deprecatedReason="unknown_reason"
|
||||
alternativePluginId=""
|
||||
alternativePluginURL=""
|
||||
/>,
|
||||
)
|
||||
expect(screen.getByText('plugin.detailPanel.deprecation.noReason')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('applies custom className', () => {
|
||||
const { container } = render(
|
||||
<DeprecationNotice
|
||||
status="deleted"
|
||||
deprecatedReason=""
|
||||
alternativePluginId=""
|
||||
alternativePluginURL=""
|
||||
className="my-custom-class"
|
||||
/>,
|
||||
)
|
||||
expect((container.firstChild as HTMLElement).className).toContain('my-custom-class')
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,59 @@
|
||||
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import KeyValueItem from '../key-value-item'
|
||||
|
||||
vi.mock('../../../base/icons/src/vender/line/files', () => ({
|
||||
CopyCheck: () => <span data-testid="copy-check-icon" />,
|
||||
}))
|
||||
|
||||
vi.mock('../../../base/tooltip', () => ({
|
||||
default: ({ children, popupContent }: { children: React.ReactNode, popupContent: string }) => (
|
||||
<div data-testid="tooltip" data-content={popupContent}>{children}</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/action-button', () => ({
|
||||
default: ({ children, onClick }: { children: React.ReactNode, onClick: () => void }) => (
|
||||
<button data-testid="action-button" onClick={onClick}>{children}</button>
|
||||
),
|
||||
}))
|
||||
|
||||
const mockCopy = vi.fn()
|
||||
vi.mock('copy-to-clipboard', () => ({
|
||||
default: (...args: unknown[]) => mockCopy(...args),
|
||||
}))
|
||||
|
||||
describe('KeyValueItem', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
vi.useFakeTimers()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
cleanup()
|
||||
})
|
||||
|
||||
it('renders label and value', () => {
|
||||
render(<KeyValueItem label="ID" value="abc-123" />)
|
||||
expect(screen.getByText('ID')).toBeInTheDocument()
|
||||
expect(screen.getByText('abc-123')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders maskedValue instead of value when provided', () => {
|
||||
render(<KeyValueItem label="Key" value="sk-secret" maskedValue="sk-***" />)
|
||||
expect(screen.getByText('sk-***')).toBeInTheDocument()
|
||||
expect(screen.queryByText('sk-secret')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('copies actual value (not masked) when copy button is clicked', () => {
|
||||
render(<KeyValueItem label="Key" value="sk-secret" maskedValue="sk-***" />)
|
||||
fireEvent.click(screen.getByTestId('action-button'))
|
||||
expect(mockCopy).toHaveBeenCalledWith('sk-secret')
|
||||
})
|
||||
|
||||
it('renders copy tooltip', () => {
|
||||
render(<KeyValueItem label="ID" value="123" />)
|
||||
expect(screen.getByTestId('tooltip')).toHaveAttribute('data-content', 'common.operation.copy')
|
||||
})
|
||||
})
|
||||
@ -1,7 +1,7 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { Theme } from '@/types/app'
|
||||
import IconWithTooltip from './icon-with-tooltip'
|
||||
import IconWithTooltip from '../icon-with-tooltip'
|
||||
|
||||
// Mock Tooltip component
|
||||
vi.mock('@/app/components/base/tooltip', () => ({
|
||||
@ -2,7 +2,7 @@ import type { ComponentProps } from 'react'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { Theme } from '@/types/app'
|
||||
import Partner from './partner'
|
||||
import Partner from '../partner'
|
||||
|
||||
// Mock useTheme hook
|
||||
const mockUseTheme = vi.fn()
|
||||
@ -11,9 +11,9 @@ vi.mock('@/hooks/use-theme', () => ({
|
||||
}))
|
||||
|
||||
// Mock IconWithTooltip to directly test Partner's behavior
|
||||
type IconWithTooltipProps = ComponentProps<typeof import('./icon-with-tooltip').default>
|
||||
type IconWithTooltipProps = ComponentProps<typeof import('../icon-with-tooltip').default>
|
||||
const mockIconWithTooltip = vi.fn()
|
||||
vi.mock('./icon-with-tooltip', () => ({
|
||||
vi.mock('../icon-with-tooltip', () => ({
|
||||
default: (props: IconWithTooltipProps) => {
|
||||
mockIconWithTooltip(props)
|
||||
const { theme, BadgeIconLight, BadgeIconDark, className, popupContent } = props
|
||||
@ -0,0 +1,52 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import * as React from 'react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
vi.mock('@/app/components/base/icons/src/public/plugins/VerifiedDark', () => ({
|
||||
default: () => <span data-testid="verified-dark" />,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/icons/src/public/plugins/VerifiedLight', () => ({
|
||||
default: () => <span data-testid="verified-light" />,
|
||||
}))
|
||||
|
||||
vi.mock('@/hooks/use-theme', () => ({
|
||||
default: () => ({ theme: 'light' }),
|
||||
}))
|
||||
|
||||
vi.mock('../icon-with-tooltip', () => ({
|
||||
default: ({ popupContent, BadgeIconLight, BadgeIconDark, theme }: {
|
||||
popupContent: string
|
||||
BadgeIconLight: React.FC
|
||||
BadgeIconDark: React.FC
|
||||
theme: string
|
||||
[key: string]: unknown
|
||||
}) => (
|
||||
<div data-testid="icon-with-tooltip" data-popup={popupContent}>
|
||||
{theme === 'light' ? <BadgeIconLight /> : <BadgeIconDark />}
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
describe('Verified', () => {
|
||||
let Verified: (typeof import('../verified'))['default']
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks()
|
||||
const mod = await import('../verified')
|
||||
Verified = mod.default
|
||||
})
|
||||
|
||||
it('should render with tooltip text', () => {
|
||||
render(<Verified text="Verified Plugin" />)
|
||||
|
||||
const tooltip = screen.getByTestId('icon-with-tooltip')
|
||||
expect(tooltip).toHaveAttribute('data-popup', 'Verified Plugin')
|
||||
})
|
||||
|
||||
it('should render light theme icon by default', () => {
|
||||
render(<Verified text="Verified" />)
|
||||
|
||||
expect(screen.getByTestId('verified-light')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,50 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import CardMoreInfo from '../card-more-info'
|
||||
|
||||
vi.mock('../base/download-count', () => ({
|
||||
default: ({ downloadCount }: { downloadCount: number }) => (
|
||||
<span data-testid="download-count">{downloadCount}</span>
|
||||
),
|
||||
}))
|
||||
|
||||
describe('CardMoreInfo', () => {
|
||||
it('renders tags with # prefix', () => {
|
||||
render(<CardMoreInfo tags={['search', 'agent']} />)
|
||||
expect(screen.getByText('search')).toBeInTheDocument()
|
||||
expect(screen.getByText('agent')).toBeInTheDocument()
|
||||
// # prefixes
|
||||
const hashmarks = screen.getAllByText('#')
|
||||
expect(hashmarks).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('renders download count when provided', () => {
|
||||
render(<CardMoreInfo downloadCount={1000} tags={[]} />)
|
||||
expect(screen.getByTestId('download-count')).toHaveTextContent('1000')
|
||||
})
|
||||
|
||||
it('does not render download count when undefined', () => {
|
||||
render(<CardMoreInfo tags={['tag1']} />)
|
||||
expect(screen.queryByTestId('download-count')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders separator between download count and tags', () => {
|
||||
render(<CardMoreInfo downloadCount={500} tags={['test']} />)
|
||||
expect(screen.getByText('·')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('does not render separator when no tags', () => {
|
||||
render(<CardMoreInfo downloadCount={500} tags={[]} />)
|
||||
expect(screen.queryByText('·')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('does not render separator when no download count', () => {
|
||||
render(<CardMoreInfo tags={['tag1']} />)
|
||||
expect(screen.queryByText('·')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('handles empty tags array', () => {
|
||||
const { container } = render(<CardMoreInfo tags={[]} />)
|
||||
expect(container.firstChild).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
589
web/app/components/plugins/card/__tests__/index.spec.tsx
Normal file
589
web/app/components/plugins/card/__tests__/index.spec.tsx
Normal file
@ -0,0 +1,589 @@
|
||||
import type { Plugin } from '../../types'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import * as React from 'react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { PluginCategoryEnum } from '../../types'
|
||||
import Card from '../index'
|
||||
|
||||
let mockTheme = 'light'
|
||||
vi.mock('@/hooks/use-theme', () => ({
|
||||
default: () => ({ theme: mockTheme }),
|
||||
}))
|
||||
|
||||
vi.mock('@/i18n-config', () => ({
|
||||
renderI18nObject: (obj: Record<string, string>, locale: string) => {
|
||||
return obj?.[locale] || obj?.['en-US'] || ''
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/i18n-config/language', () => ({
|
||||
getLanguage: (locale: string) => locale || 'en-US',
|
||||
}))
|
||||
|
||||
const mockCategoriesMap: Record<string, { label: string }> = {
|
||||
'tool': { label: 'Tool' },
|
||||
'model': { label: 'Model' },
|
||||
'extension': { label: 'Extension' },
|
||||
'agent-strategy': { label: 'Agent' },
|
||||
'datasource': { label: 'Datasource' },
|
||||
'trigger': { label: 'Trigger' },
|
||||
'bundle': { label: 'Bundle' },
|
||||
}
|
||||
|
||||
vi.mock('../../hooks', () => ({
|
||||
useCategories: () => ({
|
||||
categoriesMap: mockCategoriesMap,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/format', () => ({
|
||||
formatNumber: (num: number) => num.toLocaleString(),
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/mcp', () => ({
|
||||
shouldUseMcpIcon: (src: unknown) => typeof src === 'object' && src !== null && (src as { content?: string })?.content === '🔗',
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/app-icon', () => ({
|
||||
default: ({ icon, background, innerIcon, size, iconType }: {
|
||||
icon?: string
|
||||
background?: string
|
||||
innerIcon?: React.ReactNode
|
||||
size?: string
|
||||
iconType?: string
|
||||
}) => (
|
||||
<div
|
||||
data-testid="app-icon"
|
||||
data-icon={icon}
|
||||
data-background={background}
|
||||
data-size={size}
|
||||
data-icon-type={iconType}
|
||||
>
|
||||
{!!innerIcon && <div data-testid="inner-icon">{innerIcon}</div>}
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/icons/src/vender/other', () => ({
|
||||
Mcp: ({ className }: { className?: string }) => (
|
||||
<div data-testid="mcp-icon" className={className}>MCP</div>
|
||||
),
|
||||
Group: ({ className }: { className?: string }) => (
|
||||
<div data-testid="group-icon" className={className}>Group</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('../../../base/icons/src/vender/plugin', () => ({
|
||||
LeftCorner: ({ className }: { className?: string }) => (
|
||||
<div data-testid="left-corner" className={className}>LeftCorner</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('../../base/badges/partner', () => ({
|
||||
default: ({ className, text }: { className?: string, text?: string }) => (
|
||||
<div data-testid="partner-badge" className={className} title={text}>Partner</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('../../base/badges/verified', () => ({
|
||||
default: ({ className, text }: { className?: string, text?: string }) => (
|
||||
<div data-testid="verified-badge" className={className} title={text}>Verified</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/skeleton', () => ({
|
||||
SkeletonContainer: ({ children }: { children: React.ReactNode }) => (
|
||||
<div data-testid="skeleton-container">{children}</div>
|
||||
),
|
||||
SkeletonPoint: () => <div data-testid="skeleton-point" />,
|
||||
SkeletonRectangle: ({ className }: { className?: string }) => (
|
||||
<div data-testid="skeleton-rectangle" className={className} />
|
||||
),
|
||||
SkeletonRow: ({ children, className }: { children: React.ReactNode, className?: string }) => (
|
||||
<div data-testid="skeleton-row" className={className}>{children}</div>
|
||||
),
|
||||
}))
|
||||
|
||||
const createMockPlugin = (overrides?: Partial<Plugin>): Plugin => ({
|
||||
type: 'plugin',
|
||||
org: 'test-org',
|
||||
name: 'test-plugin',
|
||||
plugin_id: 'plugin-123',
|
||||
version: '1.0.0',
|
||||
latest_version: '1.0.0',
|
||||
latest_package_identifier: 'test-org/test-plugin:1.0.0',
|
||||
icon: '/test-icon.png',
|
||||
verified: false,
|
||||
label: { 'en-US': 'Test Plugin' },
|
||||
brief: { 'en-US': 'Test plugin description' },
|
||||
description: { 'en-US': 'Full test plugin description' },
|
||||
introduction: 'Test plugin introduction',
|
||||
repository: 'https://github.com/test/plugin',
|
||||
category: PluginCategoryEnum.tool,
|
||||
install_count: 1000,
|
||||
endpoint: { settings: [] },
|
||||
tags: [{ name: 'search' }],
|
||||
badges: [],
|
||||
verification: { authorized_category: 'community' },
|
||||
from: 'marketplace',
|
||||
...overrides,
|
||||
})
|
||||
|
||||
describe('Card', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Rendering Tests
|
||||
// ================================
|
||||
describe('Rendering', () => {
|
||||
it('should render without crashing', () => {
|
||||
const plugin = createMockPlugin()
|
||||
render(<Card payload={plugin} />)
|
||||
|
||||
expect(document.body).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render plugin title from label', () => {
|
||||
const plugin = createMockPlugin({
|
||||
label: { 'en-US': 'My Plugin Title' },
|
||||
})
|
||||
|
||||
render(<Card payload={plugin} />)
|
||||
|
||||
expect(screen.getByText('My Plugin Title')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render plugin description from brief', () => {
|
||||
const plugin = createMockPlugin({
|
||||
brief: { 'en-US': 'This is a brief description' },
|
||||
})
|
||||
|
||||
render(<Card payload={plugin} />)
|
||||
|
||||
expect(screen.getByText('This is a brief description')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render organization info with org name and package name', () => {
|
||||
const plugin = createMockPlugin({
|
||||
org: 'my-org',
|
||||
name: 'my-plugin',
|
||||
})
|
||||
|
||||
render(<Card payload={plugin} />)
|
||||
|
||||
expect(screen.getByText('my-org')).toBeInTheDocument()
|
||||
expect(screen.getByText('my-plugin')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render plugin icon', () => {
|
||||
const plugin = createMockPlugin({
|
||||
icon: '/custom-icon.png',
|
||||
})
|
||||
|
||||
const { container } = render(<Card payload={plugin} />)
|
||||
|
||||
// Check for background image style on icon element
|
||||
const iconElement = container.querySelector('[style*="background-image"]')
|
||||
expect(iconElement).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should use icon_dark when theme is dark and icon_dark is provided', () => {
|
||||
// Set theme to dark
|
||||
mockTheme = 'dark'
|
||||
|
||||
const plugin = createMockPlugin({
|
||||
icon: '/light-icon.png',
|
||||
icon_dark: '/dark-icon.png',
|
||||
})
|
||||
|
||||
const { container } = render(<Card payload={plugin} />)
|
||||
|
||||
// Check that icon uses dark icon
|
||||
const iconElement = container.querySelector('[style*="background-image"]')
|
||||
expect(iconElement).toBeInTheDocument()
|
||||
expect(iconElement).toHaveStyle({ backgroundImage: 'url(/dark-icon.png)' })
|
||||
|
||||
// Reset theme
|
||||
mockTheme = 'light'
|
||||
})
|
||||
|
||||
it('should use icon when theme is dark but icon_dark is not provided', () => {
|
||||
mockTheme = 'dark'
|
||||
|
||||
const plugin = createMockPlugin({
|
||||
icon: '/light-icon.png',
|
||||
})
|
||||
|
||||
const { container } = render(<Card payload={plugin} />)
|
||||
|
||||
// Should fallback to light icon
|
||||
const iconElement = container.querySelector('[style*="background-image"]')
|
||||
expect(iconElement).toBeInTheDocument()
|
||||
expect(iconElement).toHaveStyle({ backgroundImage: 'url(/light-icon.png)' })
|
||||
|
||||
mockTheme = 'light'
|
||||
})
|
||||
|
||||
it('should render corner mark with category label', () => {
|
||||
const plugin = createMockPlugin({
|
||||
category: PluginCategoryEnum.tool,
|
||||
})
|
||||
|
||||
render(<Card payload={plugin} />)
|
||||
|
||||
expect(screen.getByText('Tool')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Props Testing
|
||||
// ================================
|
||||
describe('Props', () => {
|
||||
it('should apply custom className', () => {
|
||||
const plugin = createMockPlugin()
|
||||
const { container } = render(
|
||||
<Card payload={plugin} className="custom-class" />,
|
||||
)
|
||||
|
||||
expect(container.querySelector('.custom-class')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should hide corner mark when hideCornerMark is true', () => {
|
||||
const plugin = createMockPlugin({
|
||||
category: PluginCategoryEnum.tool,
|
||||
})
|
||||
|
||||
render(<Card payload={plugin} hideCornerMark={true} />)
|
||||
|
||||
expect(screen.queryByTestId('left-corner')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show corner mark by default', () => {
|
||||
const plugin = createMockPlugin()
|
||||
|
||||
render(<Card payload={plugin} />)
|
||||
|
||||
expect(screen.getByTestId('left-corner')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should pass installed prop to Icon component', () => {
|
||||
const plugin = createMockPlugin()
|
||||
const { container } = render(<Card payload={plugin} installed={true} />)
|
||||
|
||||
expect(container.querySelector('.bg-state-success-solid')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should pass installFailed prop to Icon component', () => {
|
||||
const plugin = createMockPlugin()
|
||||
const { container } = render(<Card payload={plugin} installFailed={true} />)
|
||||
|
||||
expect(container.querySelector('.bg-state-destructive-solid')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render footer when provided', () => {
|
||||
const plugin = createMockPlugin()
|
||||
render(
|
||||
<Card payload={plugin} footer={<div data-testid="custom-footer">Footer Content</div>} />,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('custom-footer')).toBeInTheDocument()
|
||||
expect(screen.getByText('Footer Content')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render titleLeft when provided', () => {
|
||||
const plugin = createMockPlugin()
|
||||
render(
|
||||
<Card payload={plugin} titleLeft={<span data-testid="title-left">v1.0</span>} />,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('title-left')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should use custom descriptionLineRows', () => {
|
||||
const plugin = createMockPlugin()
|
||||
|
||||
const { container } = render(
|
||||
<Card payload={plugin} descriptionLineRows={1} />,
|
||||
)
|
||||
|
||||
// Check for h-4 truncate class when descriptionLineRows is 1
|
||||
expect(container.querySelector('.h-4.truncate')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should use default descriptionLineRows of 2', () => {
|
||||
const plugin = createMockPlugin()
|
||||
|
||||
const { container } = render(<Card payload={plugin} />)
|
||||
|
||||
// Check for h-8 line-clamp-2 class when descriptionLineRows is 2 (default)
|
||||
expect(container.querySelector('.h-8.line-clamp-2')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Loading State Tests
|
||||
// ================================
|
||||
describe('Loading State', () => {
|
||||
it('should render Placeholder when isLoading is true', () => {
|
||||
const plugin = createMockPlugin()
|
||||
|
||||
render(<Card payload={plugin} isLoading={true} loadingFileName="loading.txt" />)
|
||||
|
||||
// Should render skeleton elements
|
||||
expect(screen.getByTestId('skeleton-container')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render loadingFileName in Placeholder', () => {
|
||||
const plugin = createMockPlugin()
|
||||
|
||||
render(<Card payload={plugin} isLoading={true} loadingFileName="my-plugin.zip" />)
|
||||
|
||||
expect(screen.getByText('my-plugin.zip')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not render card content when loading', () => {
|
||||
const plugin = createMockPlugin({
|
||||
label: { 'en-US': 'Plugin Title' },
|
||||
})
|
||||
|
||||
render(<Card payload={plugin} isLoading={true} loadingFileName="file.txt" />)
|
||||
|
||||
// Plugin content should not be visible during loading
|
||||
expect(screen.queryByText('Plugin Title')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not render loading state by default', () => {
|
||||
const plugin = createMockPlugin()
|
||||
|
||||
render(<Card payload={plugin} />)
|
||||
|
||||
expect(screen.queryByTestId('skeleton-container')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Badges Tests
|
||||
// ================================
|
||||
describe('Badges', () => {
|
||||
it('should render Partner badge when badges includes partner', () => {
|
||||
const plugin = createMockPlugin({
|
||||
badges: ['partner'],
|
||||
})
|
||||
|
||||
render(<Card payload={plugin} />)
|
||||
|
||||
expect(screen.getByTestId('partner-badge')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render Verified badge when verified is true', () => {
|
||||
const plugin = createMockPlugin({
|
||||
verified: true,
|
||||
})
|
||||
|
||||
render(<Card payload={plugin} />)
|
||||
|
||||
expect(screen.getByTestId('verified-badge')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render both Partner and Verified badges', () => {
|
||||
const plugin = createMockPlugin({
|
||||
badges: ['partner'],
|
||||
verified: true,
|
||||
})
|
||||
|
||||
render(<Card payload={plugin} />)
|
||||
|
||||
expect(screen.getByTestId('partner-badge')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('verified-badge')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not render Partner badge when badges is empty', () => {
|
||||
const plugin = createMockPlugin({
|
||||
badges: [],
|
||||
})
|
||||
|
||||
render(<Card payload={plugin} />)
|
||||
|
||||
expect(screen.queryByTestId('partner-badge')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not render Verified badge when verified is false', () => {
|
||||
const plugin = createMockPlugin({
|
||||
verified: false,
|
||||
})
|
||||
|
||||
render(<Card payload={plugin} />)
|
||||
|
||||
expect(screen.queryByTestId('verified-badge')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle undefined badges gracefully', () => {
|
||||
const plugin = createMockPlugin()
|
||||
// @ts-expect-error - Testing undefined badges
|
||||
plugin.badges = undefined
|
||||
|
||||
render(<Card payload={plugin} />)
|
||||
|
||||
expect(screen.queryByTestId('partner-badge')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Limited Install Warning Tests
|
||||
// ================================
|
||||
describe('Limited Install Warning', () => {
|
||||
it('should render warning when limitedInstall is true', () => {
|
||||
const plugin = createMockPlugin()
|
||||
|
||||
const { container } = render(<Card payload={plugin} limitedInstall={true} />)
|
||||
|
||||
expect(container.querySelector('.text-text-warning-secondary')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not render warning by default', () => {
|
||||
const plugin = createMockPlugin()
|
||||
|
||||
const { container } = render(<Card payload={plugin} />)
|
||||
|
||||
expect(container.querySelector('.text-text-warning-secondary')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should apply limited padding when limitedInstall is true', () => {
|
||||
const plugin = createMockPlugin()
|
||||
|
||||
const { container } = render(<Card payload={plugin} limitedInstall={true} />)
|
||||
|
||||
expect(container.querySelector('.pb-1')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Category Type Tests
|
||||
// ================================
|
||||
describe('Category Types', () => {
|
||||
it('should display bundle label for bundle type', () => {
|
||||
const plugin = createMockPlugin({
|
||||
type: 'bundle',
|
||||
category: PluginCategoryEnum.tool,
|
||||
})
|
||||
|
||||
render(<Card payload={plugin} />)
|
||||
|
||||
// For bundle type, should show 'Bundle' instead of category
|
||||
expect(screen.getByText('Bundle')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should display category label for non-bundle types', () => {
|
||||
const plugin = createMockPlugin({
|
||||
type: 'plugin',
|
||||
category: PluginCategoryEnum.model,
|
||||
})
|
||||
|
||||
render(<Card payload={plugin} />)
|
||||
|
||||
expect(screen.getByText('Model')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Memoization Tests
|
||||
// ================================
|
||||
describe('Memoization', () => {
|
||||
it('should be memoized with React.memo', () => {
|
||||
// Card is wrapped with React.memo
|
||||
expect(Card).toBeDefined()
|
||||
// The component should have the memo display name characteristic
|
||||
expect(typeof Card).toBe('object')
|
||||
})
|
||||
|
||||
it('should not re-render when props are the same', () => {
|
||||
const plugin = createMockPlugin()
|
||||
const renderCount = vi.fn()
|
||||
|
||||
const TestWrapper = ({ p }: { p: Plugin }) => {
|
||||
renderCount()
|
||||
return <Card payload={p} />
|
||||
}
|
||||
|
||||
const { rerender } = render(<TestWrapper p={plugin} />)
|
||||
expect(renderCount).toHaveBeenCalledTimes(1)
|
||||
|
||||
// Re-render with same plugin reference
|
||||
rerender(<TestWrapper p={plugin} />)
|
||||
expect(renderCount).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Edge Cases Tests
|
||||
// ================================
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle empty label object', () => {
|
||||
const plugin = createMockPlugin({
|
||||
label: {},
|
||||
})
|
||||
|
||||
render(<Card payload={plugin} />)
|
||||
|
||||
// Should render without crashing
|
||||
expect(document.body).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle empty brief object', () => {
|
||||
const plugin = createMockPlugin({
|
||||
brief: {},
|
||||
})
|
||||
|
||||
render(<Card payload={plugin} />)
|
||||
|
||||
expect(document.body).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle undefined label', () => {
|
||||
const plugin = createMockPlugin()
|
||||
// @ts-expect-error - Testing undefined label
|
||||
plugin.label = undefined
|
||||
|
||||
render(<Card payload={plugin} />)
|
||||
|
||||
expect(document.body).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle special characters in plugin name', () => {
|
||||
const plugin = createMockPlugin({
|
||||
name: 'plugin-with-special-chars!@#$%',
|
||||
org: 'org<script>alert(1)</script>',
|
||||
})
|
||||
|
||||
render(<Card payload={plugin} />)
|
||||
|
||||
expect(screen.getByText('plugin-with-special-chars!@#$%')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle very long title', () => {
|
||||
const longTitle = 'A'.repeat(500)
|
||||
const plugin = createMockPlugin({
|
||||
label: { 'en-US': longTitle },
|
||||
})
|
||||
|
||||
const { container } = render(<Card payload={plugin} />)
|
||||
|
||||
// Should have truncate class for long text
|
||||
expect(container.querySelector('.truncate')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle very long description', () => {
|
||||
const longDescription = 'B'.repeat(1000)
|
||||
const plugin = createMockPlugin({
|
||||
brief: { 'en-US': longDescription },
|
||||
})
|
||||
|
||||
const { container } = render(<Card payload={plugin} />)
|
||||
|
||||
// Should have line-clamp class for long text
|
||||
expect(container.querySelector('.line-clamp-2')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,61 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import Icon from '../card-icon'
|
||||
|
||||
vi.mock('@/app/components/base/app-icon', () => ({
|
||||
default: ({ icon, background }: { icon: string, background: string }) => (
|
||||
<div data-testid="app-icon" data-icon={icon} data-bg={background} />
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/icons/src/vender/other', () => ({
|
||||
Mcp: () => <span data-testid="mcp-icon" />,
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/mcp', () => ({
|
||||
shouldUseMcpIcon: () => false,
|
||||
}))
|
||||
|
||||
describe('Icon', () => {
|
||||
it('renders string src as background image', () => {
|
||||
const { container } = render(<Icon src="https://example.com/icon.png" />)
|
||||
const el = container.firstChild as HTMLElement
|
||||
expect(el.style.backgroundImage).toContain('https://example.com/icon.png')
|
||||
})
|
||||
|
||||
it('renders emoji src using AppIcon', () => {
|
||||
render(<Icon src={{ content: '🔍', background: '#fff' }} />)
|
||||
expect(screen.getByTestId('app-icon')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('app-icon')).toHaveAttribute('data-icon', '🔍')
|
||||
expect(screen.getByTestId('app-icon')).toHaveAttribute('data-bg', '#fff')
|
||||
})
|
||||
|
||||
it('shows check icon when installed', () => {
|
||||
const { container } = render(<Icon src="icon.png" installed />)
|
||||
expect(container.querySelector('.bg-state-success-solid')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows close icon when installFailed', () => {
|
||||
const { container } = render(<Icon src="icon.png" installFailed />)
|
||||
expect(container.querySelector('.bg-state-destructive-solid')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('does not show status icons by default', () => {
|
||||
const { container } = render(<Icon src="icon.png" />)
|
||||
expect(container.querySelector('.bg-state-success-solid')).not.toBeInTheDocument()
|
||||
expect(container.querySelector('.bg-state-destructive-solid')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('applies custom className', () => {
|
||||
const { container } = render(<Icon src="icon.png" className="my-class" />)
|
||||
const el = container.firstChild as HTMLElement
|
||||
expect(el.className).toContain('my-class')
|
||||
})
|
||||
|
||||
it('applies correct size class', () => {
|
||||
const { container } = render(<Icon src="icon.png" size="small" />)
|
||||
const el = container.firstChild as HTMLElement
|
||||
expect(el.className).toContain('w-8')
|
||||
expect(el.className).toContain('h-8')
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,27 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import CornerMark from '../corner-mark'
|
||||
|
||||
vi.mock('../../../../base/icons/src/vender/plugin', () => ({
|
||||
LeftCorner: ({ className }: { className: string }) => <svg data-testid="left-corner" className={className} />,
|
||||
}))
|
||||
|
||||
describe('CornerMark', () => {
|
||||
it('renders the text content', () => {
|
||||
render(<CornerMark text="NEW" />)
|
||||
expect(screen.getByText('NEW')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders the LeftCorner icon', () => {
|
||||
render(<CornerMark text="BETA" />)
|
||||
expect(screen.getByTestId('left-corner')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders with absolute positioning', () => {
|
||||
const { container } = render(<CornerMark text="TAG" />)
|
||||
const wrapper = container.firstChild as HTMLElement
|
||||
expect(wrapper.className).toContain('absolute')
|
||||
expect(wrapper.className).toContain('right-0')
|
||||
expect(wrapper.className).toContain('top-0')
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,37 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import Description from '../description'
|
||||
|
||||
describe('Description', () => {
|
||||
it('renders description text', () => {
|
||||
render(<Description text="A great plugin" descriptionLineRows={1} />)
|
||||
expect(screen.getByText('A great plugin')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('applies truncate class for 1 line', () => {
|
||||
render(<Description text="Single line" descriptionLineRows={1} />)
|
||||
const el = screen.getByText('Single line')
|
||||
expect(el.className).toContain('truncate')
|
||||
expect(el.className).toContain('h-4')
|
||||
})
|
||||
|
||||
it('applies line-clamp-2 class for 2 lines', () => {
|
||||
render(<Description text="Two lines" descriptionLineRows={2} />)
|
||||
const el = screen.getByText('Two lines')
|
||||
expect(el.className).toContain('line-clamp-2')
|
||||
expect(el.className).toContain('h-8')
|
||||
})
|
||||
|
||||
it('applies line-clamp-3 class for 3 lines', () => {
|
||||
render(<Description text="Three lines" descriptionLineRows={3} />)
|
||||
const el = screen.getByText('Three lines')
|
||||
expect(el.className).toContain('line-clamp-3')
|
||||
expect(el.className).toContain('h-12')
|
||||
})
|
||||
|
||||
it('applies custom className', () => {
|
||||
render(<Description text="test" descriptionLineRows={1} className="mt-2" />)
|
||||
const el = screen.getByText('test')
|
||||
expect(el.className).toContain('mt-2')
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,28 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import DownloadCount from '../download-count'
|
||||
|
||||
vi.mock('@/utils/format', () => ({
|
||||
formatNumber: (n: number) => {
|
||||
if (n >= 1000)
|
||||
return `${(n / 1000).toFixed(1)}k`
|
||||
return String(n)
|
||||
},
|
||||
}))
|
||||
|
||||
describe('DownloadCount', () => {
|
||||
it('renders formatted download count', () => {
|
||||
render(<DownloadCount downloadCount={1500} />)
|
||||
expect(screen.getByText('1.5k')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders small numbers directly', () => {
|
||||
render(<DownloadCount downloadCount={42} />)
|
||||
expect(screen.getByText('42')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders zero download count', () => {
|
||||
render(<DownloadCount downloadCount={0} />)
|
||||
expect(screen.getByText('0')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,34 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import OrgInfo from '../org-info'
|
||||
|
||||
describe('OrgInfo', () => {
|
||||
it('renders package name', () => {
|
||||
render(<OrgInfo packageName="my-plugin" />)
|
||||
expect(screen.getByText('my-plugin')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders org name with separator when provided', () => {
|
||||
render(<OrgInfo orgName="dify" packageName="search-tool" />)
|
||||
expect(screen.getByText('dify')).toBeInTheDocument()
|
||||
expect(screen.getByText('/')).toBeInTheDocument()
|
||||
expect(screen.getByText('search-tool')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('does not render org name or separator when orgName is not provided', () => {
|
||||
render(<OrgInfo packageName="standalone" />)
|
||||
expect(screen.queryByText('/')).not.toBeInTheDocument()
|
||||
expect(screen.getByText('standalone')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('applies custom className', () => {
|
||||
const { container } = render(<OrgInfo packageName="pkg" className="custom-class" />)
|
||||
expect((container.firstChild as HTMLElement).className).toContain('custom-class')
|
||||
})
|
||||
|
||||
it('applies packageNameClassName to package name element', () => {
|
||||
render(<OrgInfo packageName="pkg" packageNameClassName="w-auto" />)
|
||||
const pkgEl = screen.getByText('pkg')
|
||||
expect(pkgEl.className).toContain('w-auto')
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,71 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import * as React from 'react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
vi.mock('../title', () => ({
|
||||
default: ({ title }: { title: string }) => <span data-testid="title">{title}</span>,
|
||||
}))
|
||||
|
||||
vi.mock('../../../../base/icons/src/vender/other', () => ({
|
||||
Group: ({ className }: { className: string }) => <span data-testid="group-icon" className={className} />,
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/classnames', () => ({
|
||||
cn: (...args: unknown[]) => args.filter(Boolean).join(' '),
|
||||
}))
|
||||
|
||||
describe('Placeholder', () => {
|
||||
let Placeholder: (typeof import('../placeholder'))['default']
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks()
|
||||
const mod = await import('../placeholder')
|
||||
Placeholder = mod.default
|
||||
})
|
||||
|
||||
it('should render skeleton rows', () => {
|
||||
const { container } = render(<Placeholder wrapClassName="w-full" />)
|
||||
|
||||
expect(container.querySelectorAll('.gap-2').length).toBeGreaterThanOrEqual(1)
|
||||
})
|
||||
|
||||
it('should render group icon placeholder', () => {
|
||||
render(<Placeholder wrapClassName="w-full" />)
|
||||
|
||||
expect(screen.getByTestId('group-icon')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render loading filename when provided', () => {
|
||||
render(<Placeholder wrapClassName="w-full" loadingFileName="test-plugin.zip" />)
|
||||
|
||||
expect(screen.getByTestId('title')).toHaveTextContent('test-plugin.zip')
|
||||
})
|
||||
|
||||
it('should render skeleton rectangles when no filename', () => {
|
||||
const { container } = render(<Placeholder wrapClassName="w-full" />)
|
||||
|
||||
expect(container.querySelectorAll('.bg-text-quaternary').length).toBeGreaterThanOrEqual(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('LoadingPlaceholder', () => {
|
||||
let LoadingPlaceholder: (typeof import('../placeholder'))['LoadingPlaceholder']
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks()
|
||||
const mod = await import('../placeholder')
|
||||
LoadingPlaceholder = mod.LoadingPlaceholder
|
||||
})
|
||||
|
||||
it('should render as a simple div with background', () => {
|
||||
const { container } = render(<LoadingPlaceholder />)
|
||||
|
||||
expect(container.firstChild).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should accept className prop', () => {
|
||||
const { container } = render(<LoadingPlaceholder className="mt-3 w-[420px]" />)
|
||||
|
||||
expect(container.firstChild).toBeTruthy()
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,21 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import Title from '../title'
|
||||
|
||||
describe('Title', () => {
|
||||
it('renders the title text', () => {
|
||||
render(<Title title="Test Plugin" />)
|
||||
expect(screen.getByText('Test Plugin')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders with truncate class for long text', () => {
|
||||
render(<Title title="A very long title that should be truncated" />)
|
||||
const el = screen.getByText('A very long title that should be truncated')
|
||||
expect(el.className).toContain('truncate')
|
||||
})
|
||||
|
||||
it('renders empty string without error', () => {
|
||||
const { container } = render(<Title title="" />)
|
||||
expect(container.firstChild).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,166 @@
|
||||
import { renderHook } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { useGitHubReleases, useGitHubUpload } from '../hooks'
|
||||
|
||||
const mockNotify = vi.fn()
|
||||
vi.mock('@/app/components/base/toast', () => ({
|
||||
default: { notify: (...args: unknown[]) => mockNotify(...args) },
|
||||
}))
|
||||
|
||||
vi.mock('@/config', () => ({
|
||||
GITHUB_ACCESS_TOKEN: '',
|
||||
}))
|
||||
|
||||
const mockUploadGitHub = vi.fn()
|
||||
vi.mock('@/service/plugins', () => ({
|
||||
uploadGitHub: (...args: unknown[]) => mockUploadGitHub(...args),
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/semver', () => ({
|
||||
compareVersion: (a: string, b: string) => {
|
||||
const parseVersion = (v: string) => v.replace(/^v/, '').split('.').map(Number)
|
||||
const va = parseVersion(a)
|
||||
const vb = parseVersion(b)
|
||||
for (let i = 0; i < Math.max(va.length, vb.length); i++) {
|
||||
const diff = (va[i] || 0) - (vb[i] || 0)
|
||||
if (diff > 0)
|
||||
return 1
|
||||
if (diff < 0)
|
||||
return -1
|
||||
}
|
||||
return 0
|
||||
},
|
||||
getLatestVersion: (versions: string[]) => {
|
||||
return versions.sort((a, b) => {
|
||||
const pa = a.replace(/^v/, '').split('.').map(Number)
|
||||
const pb = b.replace(/^v/, '').split('.').map(Number)
|
||||
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
||||
const diff = (pa[i] || 0) - (pb[i] || 0)
|
||||
if (diff !== 0)
|
||||
return diff
|
||||
}
|
||||
return 0
|
||||
}).pop()!
|
||||
},
|
||||
}))
|
||||
|
||||
const mockFetch = vi.fn()
|
||||
globalThis.fetch = mockFetch
|
||||
|
||||
describe('install-plugin/hooks', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('useGitHubReleases', () => {
|
||||
describe('fetchReleases', () => {
|
||||
it('fetches releases from GitHub API and formats them', async () => {
|
||||
mockFetch.mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve([
|
||||
{
|
||||
tag_name: 'v1.0.0',
|
||||
assets: [{ browser_download_url: 'https://example.com/v1.zip', name: 'plugin.zip' }],
|
||||
body: 'Release notes',
|
||||
},
|
||||
]),
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => useGitHubReleases())
|
||||
const releases = await result.current.fetchReleases('owner', 'repo')
|
||||
|
||||
expect(releases).toHaveLength(1)
|
||||
expect(releases[0].tag_name).toBe('v1.0.0')
|
||||
expect(releases[0].assets[0].name).toBe('plugin.zip')
|
||||
expect(releases[0]).not.toHaveProperty('body')
|
||||
})
|
||||
|
||||
it('returns empty array and shows toast on fetch error', async () => {
|
||||
mockFetch.mockResolvedValue({
|
||||
ok: false,
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => useGitHubReleases())
|
||||
const releases = await result.current.fetchReleases('owner', 'repo')
|
||||
|
||||
expect(releases).toEqual([])
|
||||
expect(mockNotify).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ type: 'error' }),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('checkForUpdates', () => {
|
||||
it('detects newer version available', () => {
|
||||
const { result } = renderHook(() => useGitHubReleases())
|
||||
const releases = [
|
||||
{ tag_name: 'v1.0.0', assets: [] },
|
||||
{ tag_name: 'v2.0.0', assets: [] },
|
||||
]
|
||||
const { needUpdate, toastProps } = result.current.checkForUpdates(releases, 'v1.0.0')
|
||||
expect(needUpdate).toBe(true)
|
||||
expect(toastProps.message).toContain('v2.0.0')
|
||||
})
|
||||
|
||||
it('returns no update when current is latest', () => {
|
||||
const { result } = renderHook(() => useGitHubReleases())
|
||||
const releases = [
|
||||
{ tag_name: 'v1.0.0', assets: [] },
|
||||
]
|
||||
const { needUpdate, toastProps } = result.current.checkForUpdates(releases, 'v1.0.0')
|
||||
expect(needUpdate).toBe(false)
|
||||
expect(toastProps.type).toBe('info')
|
||||
})
|
||||
|
||||
it('returns error for empty releases', () => {
|
||||
const { result } = renderHook(() => useGitHubReleases())
|
||||
const { needUpdate, toastProps } = result.current.checkForUpdates([], 'v1.0.0')
|
||||
expect(needUpdate).toBe(false)
|
||||
expect(toastProps.type).toBe('error')
|
||||
expect(toastProps.message).toContain('empty')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('useGitHubUpload', () => {
|
||||
it('uploads successfully and calls onSuccess', async () => {
|
||||
const mockManifest = { name: 'test-plugin' }
|
||||
mockUploadGitHub.mockResolvedValue({
|
||||
manifest: mockManifest,
|
||||
unique_identifier: 'uid-123',
|
||||
})
|
||||
const onSuccess = vi.fn()
|
||||
|
||||
const { result } = renderHook(() => useGitHubUpload())
|
||||
const pkg = await result.current.handleUpload(
|
||||
'https://github.com/owner/repo',
|
||||
'v1.0.0',
|
||||
'plugin.difypkg',
|
||||
onSuccess,
|
||||
)
|
||||
|
||||
expect(mockUploadGitHub).toHaveBeenCalledWith(
|
||||
'https://github.com/owner/repo',
|
||||
'v1.0.0',
|
||||
'plugin.difypkg',
|
||||
)
|
||||
expect(onSuccess).toHaveBeenCalledWith({
|
||||
manifest: mockManifest,
|
||||
unique_identifier: 'uid-123',
|
||||
})
|
||||
expect(pkg.unique_identifier).toBe('uid-123')
|
||||
})
|
||||
|
||||
it('shows toast on upload error', async () => {
|
||||
mockUploadGitHub.mockRejectedValue(new Error('Upload failed'))
|
||||
|
||||
const { result } = renderHook(() => useGitHubUpload())
|
||||
await expect(
|
||||
result.current.handleUpload('url', 'v1', 'pkg'),
|
||||
).rejects.toThrow('Upload failed')
|
||||
expect(mockNotify).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ type: 'error', message: 'Error uploading package' }),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1,12 +1,12 @@
|
||||
import type { PluginDeclaration, PluginManifestInMarket } from '../types'
|
||||
import type { PluginDeclaration, PluginManifestInMarket } from '../../types'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { PluginCategoryEnum } from '../types'
|
||||
import { PluginCategoryEnum } from '../../types'
|
||||
import {
|
||||
convertRepoToUrl,
|
||||
parseGitHubUrl,
|
||||
pluginManifestInMarketToPluginProps,
|
||||
pluginManifestToCardPluginProps,
|
||||
} from './utils'
|
||||
} from '../utils'
|
||||
|
||||
// Mock es-toolkit/compat
|
||||
vi.mock('es-toolkit/compat', () => ({
|
||||
@ -0,0 +1,125 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { TaskStatus } from '../../../types'
|
||||
import checkTaskStatus from '../check-task-status'
|
||||
|
||||
const mockCheckTaskStatus = vi.fn()
|
||||
vi.mock('@/service/plugins', () => ({
|
||||
checkTaskStatus: (...args: unknown[]) => mockCheckTaskStatus(...args),
|
||||
}))
|
||||
|
||||
// Mock sleep to avoid actual waiting in tests
|
||||
vi.mock('@/utils', () => ({
|
||||
sleep: vi.fn().mockResolvedValue(undefined),
|
||||
}))
|
||||
|
||||
describe('checkTaskStatus', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('returns success when plugin status is success', async () => {
|
||||
mockCheckTaskStatus.mockResolvedValue({
|
||||
task: {
|
||||
plugins: [
|
||||
{ plugin_unique_identifier: 'test-plugin', status: TaskStatus.success, message: '' },
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
const { check } = checkTaskStatus()
|
||||
const result = await check({ taskId: 'task-1', pluginUniqueIdentifier: 'test-plugin' })
|
||||
expect(result.status).toBe(TaskStatus.success)
|
||||
})
|
||||
|
||||
it('returns failed when plugin status is failed', async () => {
|
||||
mockCheckTaskStatus.mockResolvedValue({
|
||||
task: {
|
||||
plugins: [
|
||||
{ plugin_unique_identifier: 'test-plugin', status: TaskStatus.failed, message: 'Install failed' },
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
const { check } = checkTaskStatus()
|
||||
const result = await check({ taskId: 'task-1', pluginUniqueIdentifier: 'test-plugin' })
|
||||
expect(result.status).toBe(TaskStatus.failed)
|
||||
expect(result.error).toBe('Install failed')
|
||||
})
|
||||
|
||||
it('returns failed when plugin is not found in task', async () => {
|
||||
mockCheckTaskStatus.mockResolvedValue({
|
||||
task: {
|
||||
plugins: [
|
||||
{ plugin_unique_identifier: 'other-plugin', status: TaskStatus.success, message: '' },
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
const { check } = checkTaskStatus()
|
||||
const result = await check({ taskId: 'task-1', pluginUniqueIdentifier: 'test-plugin' })
|
||||
expect(result.status).toBe(TaskStatus.failed)
|
||||
expect(result.error).toBe('Plugin package not found')
|
||||
})
|
||||
|
||||
it('polls recursively when status is running, then resolves on success', async () => {
|
||||
let callCount = 0
|
||||
mockCheckTaskStatus.mockImplementation(() => {
|
||||
callCount++
|
||||
if (callCount < 3) {
|
||||
return Promise.resolve({
|
||||
task: {
|
||||
plugins: [
|
||||
{ plugin_unique_identifier: 'test-plugin', status: TaskStatus.running, message: '' },
|
||||
],
|
||||
},
|
||||
})
|
||||
}
|
||||
return Promise.resolve({
|
||||
task: {
|
||||
plugins: [
|
||||
{ plugin_unique_identifier: 'test-plugin', status: TaskStatus.success, message: '' },
|
||||
],
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
const { check } = checkTaskStatus()
|
||||
const result = await check({ taskId: 'task-1', pluginUniqueIdentifier: 'test-plugin' })
|
||||
expect(result.status).toBe(TaskStatus.success)
|
||||
expect(mockCheckTaskStatus).toHaveBeenCalledTimes(3)
|
||||
})
|
||||
|
||||
it('stop() causes early return with success', async () => {
|
||||
const { check, stop } = checkTaskStatus()
|
||||
stop()
|
||||
const result = await check({ taskId: 'task-1', pluginUniqueIdentifier: 'test-plugin' })
|
||||
expect(result.status).toBe(TaskStatus.success)
|
||||
expect(mockCheckTaskStatus).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('returns different instances with independent state', async () => {
|
||||
const checker1 = checkTaskStatus()
|
||||
const checker2 = checkTaskStatus()
|
||||
|
||||
checker1.stop()
|
||||
|
||||
mockCheckTaskStatus.mockResolvedValue({
|
||||
task: {
|
||||
plugins: [
|
||||
{ plugin_unique_identifier: 'test-plugin', status: TaskStatus.success, message: '' },
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
const result1 = await checker1.check({ taskId: 'task-1', pluginUniqueIdentifier: 'test-plugin' })
|
||||
const result2 = await checker2.check({ taskId: 'task-2', pluginUniqueIdentifier: 'test-plugin' })
|
||||
|
||||
expect(result1.status).toBe(TaskStatus.success)
|
||||
expect(result2.status).toBe(TaskStatus.success)
|
||||
expect(mockCheckTaskStatus).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,81 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import * as React from 'react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
vi.mock('../../../card', () => ({
|
||||
default: ({ installed, installFailed, titleLeft }: { installed: boolean, installFailed: boolean, titleLeft?: React.ReactNode }) => (
|
||||
<div data-testid="card" data-installed={installed} data-failed={installFailed}>{titleLeft}</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('../../utils', () => ({
|
||||
pluginManifestInMarketToPluginProps: (p: unknown) => p,
|
||||
pluginManifestToCardPluginProps: (p: unknown) => p,
|
||||
}))
|
||||
|
||||
describe('Installed', () => {
|
||||
let Installed: (typeof import('../installed'))['default']
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks()
|
||||
const mod = await import('../installed')
|
||||
Installed = mod.default
|
||||
})
|
||||
|
||||
it('should render success message when not failed', () => {
|
||||
render(<Installed isFailed={false} onCancel={vi.fn()} />)
|
||||
|
||||
expect(screen.getByText('plugin.installModal.installedSuccessfullyDesc')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render failure message when failed', () => {
|
||||
render(<Installed isFailed={true} onCancel={vi.fn()} />)
|
||||
|
||||
expect(screen.getByText('plugin.installModal.installFailedDesc')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render custom error message when provided', () => {
|
||||
render(<Installed isFailed={true} errMsg="Custom error" onCancel={vi.fn()} />)
|
||||
|
||||
expect(screen.getByText('Custom error')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render card with payload', () => {
|
||||
const payload = { version: '1.0.0', name: 'test-plugin' } as never
|
||||
render(<Installed payload={payload} isFailed={false} onCancel={vi.fn()} />)
|
||||
|
||||
const card = screen.getByTestId('card')
|
||||
expect(card).toHaveAttribute('data-installed', 'true')
|
||||
expect(card).toHaveAttribute('data-failed', 'false')
|
||||
})
|
||||
|
||||
it('should render card as failed when isFailed', () => {
|
||||
const payload = { version: '1.0.0', name: 'test-plugin' } as never
|
||||
render(<Installed payload={payload} isFailed={true} onCancel={vi.fn()} />)
|
||||
|
||||
const card = screen.getByTestId('card')
|
||||
expect(card).toHaveAttribute('data-installed', 'false')
|
||||
expect(card).toHaveAttribute('data-failed', 'true')
|
||||
})
|
||||
|
||||
it('should call onCancel when close button clicked', () => {
|
||||
const mockOnCancel = vi.fn()
|
||||
render(<Installed isFailed={false} onCancel={mockOnCancel} />)
|
||||
|
||||
fireEvent.click(screen.getByText('common.operation.close'))
|
||||
expect(mockOnCancel).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should show version badge in card', () => {
|
||||
const payload = { version: '1.0.0', name: 'test-plugin' } as never
|
||||
render(<Installed payload={payload} isFailed={false} onCancel={vi.fn()} />)
|
||||
|
||||
expect(screen.getByText('1.0.0')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not render card when no payload', () => {
|
||||
render(<Installed isFailed={false} onCancel={vi.fn()} />)
|
||||
|
||||
expect(screen.queryByTestId('card')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,46 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import * as React from 'react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
vi.mock('@/app/components/plugins/card/base/placeholder', () => ({
|
||||
LoadingPlaceholder: () => <div data-testid="loading-placeholder" />,
|
||||
}))
|
||||
|
||||
vi.mock('../../../../base/icons/src/vender/other', () => ({
|
||||
Group: ({ className }: { className: string }) => <span data-testid="group-icon" className={className} />,
|
||||
}))
|
||||
|
||||
describe('LoadingError', () => {
|
||||
let LoadingError: React.FC
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks()
|
||||
const mod = await import('../loading-error')
|
||||
LoadingError = mod.default
|
||||
})
|
||||
|
||||
it('should render error message', () => {
|
||||
render(<LoadingError />)
|
||||
|
||||
expect(screen.getByText('plugin.installModal.pluginLoadError')).toBeInTheDocument()
|
||||
expect(screen.getByText('plugin.installModal.pluginLoadErrorDesc')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render disabled checkbox', () => {
|
||||
render(<LoadingError />)
|
||||
|
||||
expect(screen.getByTestId('checkbox-undefined')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render error icon with close indicator', () => {
|
||||
render(<LoadingError />)
|
||||
|
||||
expect(screen.getByTestId('group-icon')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render loading placeholder', () => {
|
||||
render(<LoadingError />)
|
||||
|
||||
expect(screen.getByTestId('loading-placeholder')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,29 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import * as React from 'react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
vi.mock('../../../card/base/placeholder', () => ({
|
||||
default: () => <div data-testid="placeholder" />,
|
||||
}))
|
||||
|
||||
describe('Loading', () => {
|
||||
let Loading: React.FC
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks()
|
||||
const mod = await import('../loading')
|
||||
Loading = mod.default
|
||||
})
|
||||
|
||||
it('should render disabled unchecked checkbox', () => {
|
||||
render(<Loading />)
|
||||
|
||||
expect(screen.getByTestId('checkbox-undefined')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render placeholder', () => {
|
||||
render(<Loading />)
|
||||
|
||||
expect(screen.getByTestId('placeholder')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,43 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import * as React from 'react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
describe('Version', () => {
|
||||
let Version: (typeof import('../version'))['default']
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks()
|
||||
const mod = await import('../version')
|
||||
Version = mod.default
|
||||
})
|
||||
|
||||
it('should show simple version badge for new install', () => {
|
||||
render(<Version hasInstalled={false} toInstallVersion="1.0.0" />)
|
||||
|
||||
expect(screen.getByText('1.0.0')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show upgrade version badge for existing install', () => {
|
||||
render(
|
||||
<Version
|
||||
hasInstalled={true}
|
||||
installedVersion="1.0.0"
|
||||
toInstallVersion="2.0.0"
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByText('1.0.0 -> 2.0.0')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle downgrade version display', () => {
|
||||
render(
|
||||
<Version
|
||||
hasInstalled={true}
|
||||
installedVersion="2.0.0"
|
||||
toInstallVersion="1.0.0"
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByText('2.0.0 -> 1.0.0')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,79 @@
|
||||
import { renderHook } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import useCheckInstalled from '../use-check-installed'
|
||||
|
||||
const mockPlugins = [
|
||||
{
|
||||
plugin_id: 'plugin-1',
|
||||
id: 'installed-1',
|
||||
declaration: { version: '1.0.0' },
|
||||
plugin_unique_identifier: 'org/plugin-1',
|
||||
},
|
||||
{
|
||||
plugin_id: 'plugin-2',
|
||||
id: 'installed-2',
|
||||
declaration: { version: '2.0.0' },
|
||||
plugin_unique_identifier: 'org/plugin-2',
|
||||
},
|
||||
]
|
||||
|
||||
vi.mock('@/service/use-plugins', () => ({
|
||||
useCheckInstalled: ({ pluginIds, enabled }: { pluginIds: string[], enabled: boolean }) => ({
|
||||
data: enabled && pluginIds.length > 0 ? { plugins: mockPlugins } : undefined,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
}),
|
||||
}))
|
||||
|
||||
describe('useCheckInstalled', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should return installed info when enabled and has plugin IDs', () => {
|
||||
const { result } = renderHook(() => useCheckInstalled({
|
||||
pluginIds: ['plugin-1', 'plugin-2'],
|
||||
enabled: true,
|
||||
}))
|
||||
|
||||
expect(result.current.installedInfo).toBeDefined()
|
||||
expect(result.current.installedInfo?.['plugin-1']).toEqual({
|
||||
installedId: 'installed-1',
|
||||
installedVersion: '1.0.0',
|
||||
uniqueIdentifier: 'org/plugin-1',
|
||||
})
|
||||
expect(result.current.installedInfo?.['plugin-2']).toEqual({
|
||||
installedId: 'installed-2',
|
||||
installedVersion: '2.0.0',
|
||||
uniqueIdentifier: 'org/plugin-2',
|
||||
})
|
||||
})
|
||||
|
||||
it('should return undefined installedInfo when disabled', () => {
|
||||
const { result } = renderHook(() => useCheckInstalled({
|
||||
pluginIds: ['plugin-1'],
|
||||
enabled: false,
|
||||
}))
|
||||
|
||||
expect(result.current.installedInfo).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should return undefined installedInfo with empty plugin IDs', () => {
|
||||
const { result } = renderHook(() => useCheckInstalled({
|
||||
pluginIds: [],
|
||||
enabled: true,
|
||||
}))
|
||||
|
||||
expect(result.current.installedInfo).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should return isLoading and error states', () => {
|
||||
const { result } = renderHook(() => useCheckInstalled({
|
||||
pluginIds: ['plugin-1'],
|
||||
enabled: true,
|
||||
}))
|
||||
|
||||
expect(result.current.isLoading).toBe(false)
|
||||
expect(result.current.error).toBeNull()
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,76 @@
|
||||
import { act, renderHook } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import useHideLogic from '../use-hide-logic'
|
||||
|
||||
const mockFoldAnimInto = vi.fn()
|
||||
const mockClearCountDown = vi.fn()
|
||||
const mockCountDownFoldIntoAnim = vi.fn()
|
||||
|
||||
vi.mock('../use-fold-anim-into', () => ({
|
||||
default: () => ({
|
||||
modalClassName: 'test-modal-class',
|
||||
foldIntoAnim: mockFoldAnimInto,
|
||||
clearCountDown: mockClearCountDown,
|
||||
countDownFoldIntoAnim: mockCountDownFoldIntoAnim,
|
||||
}),
|
||||
}))
|
||||
|
||||
describe('useHideLogic', () => {
|
||||
const mockOnClose = vi.fn()
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should return initial state with modalClassName', () => {
|
||||
const { result } = renderHook(() => useHideLogic(mockOnClose))
|
||||
|
||||
expect(result.current.modalClassName).toBe('test-modal-class')
|
||||
})
|
||||
|
||||
it('should call onClose directly when not installing', () => {
|
||||
const { result } = renderHook(() => useHideLogic(mockOnClose))
|
||||
|
||||
act(() => {
|
||||
result.current.foldAnimInto()
|
||||
})
|
||||
|
||||
expect(mockOnClose).toHaveBeenCalled()
|
||||
expect(mockFoldAnimInto).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should call doFoldAnimInto when installing', () => {
|
||||
const { result } = renderHook(() => useHideLogic(mockOnClose))
|
||||
|
||||
act(() => {
|
||||
result.current.handleStartToInstall()
|
||||
})
|
||||
|
||||
act(() => {
|
||||
result.current.foldAnimInto()
|
||||
})
|
||||
|
||||
expect(mockFoldAnimInto).toHaveBeenCalled()
|
||||
expect(mockOnClose).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should set installing and start countdown on handleStartToInstall', () => {
|
||||
const { result } = renderHook(() => useHideLogic(mockOnClose))
|
||||
|
||||
act(() => {
|
||||
result.current.handleStartToInstall()
|
||||
})
|
||||
|
||||
expect(mockCountDownFoldIntoAnim).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should clear countdown when setIsInstalling to false', () => {
|
||||
const { result } = renderHook(() => useHideLogic(mockOnClose))
|
||||
|
||||
act(() => {
|
||||
result.current.setIsInstalling(false)
|
||||
})
|
||||
|
||||
expect(mockClearCountDown).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,149 @@
|
||||
import { renderHook } from '@testing-library/react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { InstallationScope } from '@/types/feature'
|
||||
import { pluginInstallLimit } from '../use-install-plugin-limit'
|
||||
|
||||
const mockSystemFeatures = {
|
||||
plugin_installation_permission: {
|
||||
restrict_to_marketplace_only: false,
|
||||
plugin_installation_scope: InstallationScope.ALL,
|
||||
},
|
||||
}
|
||||
|
||||
vi.mock('@/context/global-public-context', () => ({
|
||||
useGlobalPublicStore: (selector: (state: { systemFeatures: typeof mockSystemFeatures }) => unknown) =>
|
||||
selector({ systemFeatures: mockSystemFeatures }),
|
||||
}))
|
||||
|
||||
const basePlugin = {
|
||||
from: 'marketplace' as const,
|
||||
verification: { authorized_category: 'langgenius' },
|
||||
}
|
||||
|
||||
describe('pluginInstallLimit', () => {
|
||||
it('should allow all plugins when scope is ALL', () => {
|
||||
const features = {
|
||||
plugin_installation_permission: {
|
||||
restrict_to_marketplace_only: false,
|
||||
plugin_installation_scope: InstallationScope.ALL,
|
||||
},
|
||||
}
|
||||
|
||||
expect(pluginInstallLimit(basePlugin as never, features as never).canInstall).toBe(true)
|
||||
})
|
||||
|
||||
it('should deny all plugins when scope is NONE', () => {
|
||||
const features = {
|
||||
plugin_installation_permission: {
|
||||
restrict_to_marketplace_only: false,
|
||||
plugin_installation_scope: InstallationScope.NONE,
|
||||
},
|
||||
}
|
||||
|
||||
expect(pluginInstallLimit(basePlugin as never, features as never).canInstall).toBe(false)
|
||||
})
|
||||
|
||||
it('should allow langgenius plugins when scope is OFFICIAL_ONLY', () => {
|
||||
const features = {
|
||||
plugin_installation_permission: {
|
||||
restrict_to_marketplace_only: false,
|
||||
plugin_installation_scope: InstallationScope.OFFICIAL_ONLY,
|
||||
},
|
||||
}
|
||||
|
||||
expect(pluginInstallLimit(basePlugin as never, features as never).canInstall).toBe(true)
|
||||
})
|
||||
|
||||
it('should deny non-official plugins when scope is OFFICIAL_ONLY', () => {
|
||||
const features = {
|
||||
plugin_installation_permission: {
|
||||
restrict_to_marketplace_only: false,
|
||||
plugin_installation_scope: InstallationScope.OFFICIAL_ONLY,
|
||||
},
|
||||
}
|
||||
const plugin = { ...basePlugin, verification: { authorized_category: 'community' } }
|
||||
|
||||
expect(pluginInstallLimit(plugin as never, features as never).canInstall).toBe(false)
|
||||
})
|
||||
|
||||
it('should allow partner plugins when scope is OFFICIAL_AND_PARTNER', () => {
|
||||
const features = {
|
||||
plugin_installation_permission: {
|
||||
restrict_to_marketplace_only: false,
|
||||
plugin_installation_scope: InstallationScope.OFFICIAL_AND_PARTNER,
|
||||
},
|
||||
}
|
||||
const plugin = { ...basePlugin, verification: { authorized_category: 'partner' } }
|
||||
|
||||
expect(pluginInstallLimit(plugin as never, features as never).canInstall).toBe(true)
|
||||
})
|
||||
|
||||
it('should deny github plugins when restrict_to_marketplace_only is true', () => {
|
||||
const features = {
|
||||
plugin_installation_permission: {
|
||||
restrict_to_marketplace_only: true,
|
||||
plugin_installation_scope: InstallationScope.ALL,
|
||||
},
|
||||
}
|
||||
const plugin = { ...basePlugin, from: 'github' as const }
|
||||
|
||||
expect(pluginInstallLimit(plugin as never, features as never).canInstall).toBe(false)
|
||||
})
|
||||
|
||||
it('should deny package plugins when restrict_to_marketplace_only is true', () => {
|
||||
const features = {
|
||||
plugin_installation_permission: {
|
||||
restrict_to_marketplace_only: true,
|
||||
plugin_installation_scope: InstallationScope.ALL,
|
||||
},
|
||||
}
|
||||
const plugin = { ...basePlugin, from: 'package' as const }
|
||||
|
||||
expect(pluginInstallLimit(plugin as never, features as never).canInstall).toBe(false)
|
||||
})
|
||||
|
||||
it('should allow marketplace plugins even when restrict_to_marketplace_only is true', () => {
|
||||
const features = {
|
||||
plugin_installation_permission: {
|
||||
restrict_to_marketplace_only: true,
|
||||
plugin_installation_scope: InstallationScope.ALL,
|
||||
},
|
||||
}
|
||||
|
||||
expect(pluginInstallLimit(basePlugin as never, features as never).canInstall).toBe(true)
|
||||
})
|
||||
|
||||
it('should default to langgenius when no verification info', () => {
|
||||
const features = {
|
||||
plugin_installation_permission: {
|
||||
restrict_to_marketplace_only: false,
|
||||
plugin_installation_scope: InstallationScope.OFFICIAL_ONLY,
|
||||
},
|
||||
}
|
||||
const plugin = { from: 'marketplace' as const }
|
||||
|
||||
expect(pluginInstallLimit(plugin as never, features as never).canInstall).toBe(true)
|
||||
})
|
||||
|
||||
it('should fallback to canInstall true for unrecognized scope', () => {
|
||||
const features = {
|
||||
plugin_installation_permission: {
|
||||
restrict_to_marketplace_only: false,
|
||||
plugin_installation_scope: 'unknown-scope' as InstallationScope,
|
||||
},
|
||||
}
|
||||
|
||||
expect(pluginInstallLimit(basePlugin as never, features as never).canInstall).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('usePluginInstallLimit', () => {
|
||||
it('should return canInstall from pluginInstallLimit using global store', async () => {
|
||||
const { default: usePluginInstallLimit } = await import('../use-install-plugin-limit')
|
||||
const plugin = { from: 'marketplace' as const, verification: { authorized_category: 'langgenius' } }
|
||||
|
||||
const { result } = renderHook(() => usePluginInstallLimit(plugin as never))
|
||||
|
||||
expect(result.current.canInstall).toBe(true)
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,168 @@
|
||||
import { renderHook } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { PluginCategoryEnum } from '../../../types'
|
||||
|
||||
// Mock invalidation / refresh functions
|
||||
const mockInvalidateInstalledPluginList = vi.fn()
|
||||
const mockRefetchLLMModelList = vi.fn()
|
||||
const mockRefetchEmbeddingModelList = vi.fn()
|
||||
const mockRefetchRerankModelList = vi.fn()
|
||||
const mockRefreshModelProviders = vi.fn()
|
||||
const mockInvalidateAllToolProviders = vi.fn()
|
||||
const mockInvalidateAllBuiltInTools = vi.fn()
|
||||
const mockInvalidateAllDataSources = vi.fn()
|
||||
const mockInvalidateDataSourceListAuth = vi.fn()
|
||||
const mockInvalidateStrategyProviders = vi.fn()
|
||||
const mockInvalidateAllTriggerPlugins = vi.fn()
|
||||
const mockInvalidateRAGRecommendedPlugins = vi.fn()
|
||||
|
||||
vi.mock('@/service/use-plugins', () => ({
|
||||
useInvalidateInstalledPluginList: () => mockInvalidateInstalledPluginList,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/header/account-setting/model-provider-page/declarations', () => ({
|
||||
ModelTypeEnum: { textGeneration: 'text-generation', textEmbedding: 'text-embedding', rerank: 'rerank' },
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/header/account-setting/model-provider-page/hooks', () => ({
|
||||
useModelList: (type: string) => {
|
||||
const map: Record<string, { mutate: ReturnType<typeof vi.fn> }> = {
|
||||
'text-generation': { mutate: mockRefetchLLMModelList },
|
||||
'text-embedding': { mutate: mockRefetchEmbeddingModelList },
|
||||
'rerank': { mutate: mockRefetchRerankModelList },
|
||||
}
|
||||
return map[type] ?? { mutate: vi.fn() }
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/context/provider-context', () => ({
|
||||
useProviderContext: () => ({ refreshModelProviders: mockRefreshModelProviders }),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-tools', () => ({
|
||||
useInvalidateAllToolProviders: () => mockInvalidateAllToolProviders,
|
||||
useInvalidateAllBuiltInTools: () => mockInvalidateAllBuiltInTools,
|
||||
useInvalidateRAGRecommendedPlugins: () => mockInvalidateRAGRecommendedPlugins,
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-pipeline', () => ({
|
||||
useInvalidDataSourceList: () => mockInvalidateAllDataSources,
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-datasource', () => ({
|
||||
useInvalidDataSourceListAuth: () => mockInvalidateDataSourceListAuth,
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-strategy', () => ({
|
||||
useInvalidateStrategyProviders: () => mockInvalidateStrategyProviders,
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-triggers', () => ({
|
||||
useInvalidateAllTriggerPlugins: () => mockInvalidateAllTriggerPlugins,
|
||||
}))
|
||||
|
||||
const { default: useRefreshPluginList } = await import('../use-refresh-plugin-list')
|
||||
|
||||
describe('useRefreshPluginList', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should always invalidate installed plugin list', () => {
|
||||
const { result } = renderHook(() => useRefreshPluginList())
|
||||
|
||||
result.current.refreshPluginList()
|
||||
|
||||
expect(mockInvalidateInstalledPluginList).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should refresh tool providers for tool category manifest', () => {
|
||||
const { result } = renderHook(() => useRefreshPluginList())
|
||||
|
||||
result.current.refreshPluginList({ category: PluginCategoryEnum.tool } as never)
|
||||
|
||||
expect(mockInvalidateAllToolProviders).toHaveBeenCalledTimes(1)
|
||||
expect(mockInvalidateAllBuiltInTools).toHaveBeenCalledTimes(1)
|
||||
expect(mockInvalidateRAGRecommendedPlugins).toHaveBeenCalledWith('tool')
|
||||
})
|
||||
|
||||
it('should refresh model lists for model category manifest', () => {
|
||||
const { result } = renderHook(() => useRefreshPluginList())
|
||||
|
||||
result.current.refreshPluginList({ category: PluginCategoryEnum.model } as never)
|
||||
|
||||
expect(mockRefreshModelProviders).toHaveBeenCalledTimes(1)
|
||||
expect(mockRefetchLLMModelList).toHaveBeenCalledTimes(1)
|
||||
expect(mockRefetchEmbeddingModelList).toHaveBeenCalledTimes(1)
|
||||
expect(mockRefetchRerankModelList).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should refresh datasource lists for datasource category manifest', () => {
|
||||
const { result } = renderHook(() => useRefreshPluginList())
|
||||
|
||||
result.current.refreshPluginList({ category: PluginCategoryEnum.datasource } as never)
|
||||
|
||||
expect(mockInvalidateAllDataSources).toHaveBeenCalledTimes(1)
|
||||
expect(mockInvalidateDataSourceListAuth).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should refresh trigger plugins for trigger category manifest', () => {
|
||||
const { result } = renderHook(() => useRefreshPluginList())
|
||||
|
||||
result.current.refreshPluginList({ category: PluginCategoryEnum.trigger } as never)
|
||||
|
||||
expect(mockInvalidateAllTriggerPlugins).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should refresh strategy providers for agent category manifest', () => {
|
||||
const { result } = renderHook(() => useRefreshPluginList())
|
||||
|
||||
result.current.refreshPluginList({ category: PluginCategoryEnum.agent } as never)
|
||||
|
||||
expect(mockInvalidateStrategyProviders).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should refresh all types when refreshAllType is true', () => {
|
||||
const { result } = renderHook(() => useRefreshPluginList())
|
||||
|
||||
result.current.refreshPluginList(undefined, true)
|
||||
|
||||
expect(mockInvalidateInstalledPluginList).toHaveBeenCalledTimes(1)
|
||||
expect(mockInvalidateAllToolProviders).toHaveBeenCalledTimes(1)
|
||||
expect(mockInvalidateAllBuiltInTools).toHaveBeenCalledTimes(1)
|
||||
expect(mockInvalidateRAGRecommendedPlugins).toHaveBeenCalledWith('tool')
|
||||
expect(mockInvalidateAllTriggerPlugins).toHaveBeenCalledTimes(1)
|
||||
expect(mockInvalidateAllDataSources).toHaveBeenCalledTimes(1)
|
||||
expect(mockInvalidateDataSourceListAuth).toHaveBeenCalledTimes(1)
|
||||
expect(mockRefreshModelProviders).toHaveBeenCalledTimes(1)
|
||||
expect(mockRefetchLLMModelList).toHaveBeenCalledTimes(1)
|
||||
expect(mockRefetchEmbeddingModelList).toHaveBeenCalledTimes(1)
|
||||
expect(mockRefetchRerankModelList).toHaveBeenCalledTimes(1)
|
||||
expect(mockInvalidateStrategyProviders).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should not refresh category-specific lists when manifest is null', () => {
|
||||
const { result } = renderHook(() => useRefreshPluginList())
|
||||
|
||||
result.current.refreshPluginList(null)
|
||||
|
||||
expect(mockInvalidateInstalledPluginList).toHaveBeenCalledTimes(1)
|
||||
expect(mockInvalidateAllToolProviders).not.toHaveBeenCalled()
|
||||
expect(mockRefreshModelProviders).not.toHaveBeenCalled()
|
||||
expect(mockInvalidateAllDataSources).not.toHaveBeenCalled()
|
||||
expect(mockInvalidateAllTriggerPlugins).not.toHaveBeenCalled()
|
||||
expect(mockInvalidateStrategyProviders).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not refresh unrelated categories for a specific manifest', () => {
|
||||
const { result } = renderHook(() => useRefreshPluginList())
|
||||
|
||||
result.current.refreshPluginList({ category: PluginCategoryEnum.tool } as never)
|
||||
|
||||
expect(mockInvalidateAllToolProviders).toHaveBeenCalledTimes(1)
|
||||
expect(mockRefreshModelProviders).not.toHaveBeenCalled()
|
||||
expect(mockInvalidateAllDataSources).not.toHaveBeenCalled()
|
||||
expect(mockInvalidateAllTriggerPlugins).not.toHaveBeenCalled()
|
||||
expect(mockInvalidateStrategyProviders).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@ -1,14 +1,14 @@
|
||||
import type { Dependency, GitHubItemAndMarketPlaceDependency, InstallStatus, PackageDependency, Plugin, PluginDeclaration, VersionProps } from '../../types'
|
||||
import type { Dependency, GitHubItemAndMarketPlaceDependency, InstallStatus, PackageDependency, Plugin, PluginDeclaration, VersionProps } from '../../../types'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { InstallStep, PluginCategoryEnum } from '../../types'
|
||||
import InstallBundle, { InstallType } from './index'
|
||||
import GithubItem from './item/github-item'
|
||||
import LoadedItem from './item/loaded-item'
|
||||
import MarketplaceItem from './item/marketplace-item'
|
||||
import PackageItem from './item/package-item'
|
||||
import ReadyToInstall from './ready-to-install'
|
||||
import Installed from './steps/installed'
|
||||
import { InstallStep, PluginCategoryEnum } from '../../../types'
|
||||
import InstallBundle, { InstallType } from '../index'
|
||||
import GithubItem from '../item/github-item'
|
||||
import LoadedItem from '../item/loaded-item'
|
||||
import MarketplaceItem from '../item/marketplace-item'
|
||||
import PackageItem from '../item/package-item'
|
||||
import ReadyToInstall from '../ready-to-install'
|
||||
import Installed from '../steps/installed'
|
||||
|
||||
// Factory functions for test data
|
||||
const createMockPlugin = (overrides: Partial<Plugin> = {}): Plugin => ({
|
||||
@ -143,19 +143,19 @@ let mockHideLogicState = {
|
||||
setIsInstalling: vi.fn(),
|
||||
handleStartToInstall: vi.fn(),
|
||||
}
|
||||
vi.mock('../hooks/use-hide-logic', () => ({
|
||||
vi.mock('../../hooks/use-hide-logic', () => ({
|
||||
default: () => mockHideLogicState,
|
||||
}))
|
||||
|
||||
// Mock useGetIcon hook
|
||||
vi.mock('../base/use-get-icon', () => ({
|
||||
vi.mock('../../base/use-get-icon', () => ({
|
||||
default: () => ({
|
||||
getIconUrl: (icon: string) => icon || 'default-icon.png',
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock usePluginInstallLimit hook
|
||||
vi.mock('../hooks/use-install-plugin-limit', () => ({
|
||||
vi.mock('../../hooks/use-install-plugin-limit', () => ({
|
||||
default: () => ({ canInstall: true }),
|
||||
pluginInstallLimit: () => ({ canInstall: true }),
|
||||
}))
|
||||
@ -190,22 +190,22 @@ vi.mock('@/app/components/plugins/plugin-page/use-reference-setting', () => ({
|
||||
}))
|
||||
|
||||
// Mock checkTaskStatus
|
||||
vi.mock('../base/check-task-status', () => ({
|
||||
vi.mock('../../base/check-task-status', () => ({
|
||||
default: () => ({ check: vi.fn(), stop: vi.fn() }),
|
||||
}))
|
||||
|
||||
// Mock useRefreshPluginList
|
||||
vi.mock('../hooks/use-refresh-plugin-list', () => ({
|
||||
vi.mock('../../hooks/use-refresh-plugin-list', () => ({
|
||||
default: () => ({ refreshPluginList: vi.fn() }),
|
||||
}))
|
||||
|
||||
// Mock useCheckInstalled
|
||||
vi.mock('../hooks/use-check-installed', () => ({
|
||||
vi.mock('../../hooks/use-check-installed', () => ({
|
||||
default: () => ({ installedInfo: {} }),
|
||||
}))
|
||||
|
||||
// Mock ReadyToInstall child component to test InstallBundle in isolation
|
||||
vi.mock('./ready-to-install', () => ({
|
||||
vi.mock('../ready-to-install', () => ({
|
||||
default: ({
|
||||
step,
|
||||
onStepChange,
|
||||
@ -1,9 +1,9 @@
|
||||
import type { Dependency, GitHubItemAndMarketPlaceDependency, PackageDependency, Plugin, VersionInfo } from '../../../types'
|
||||
import type { Dependency, GitHubItemAndMarketPlaceDependency, PackageDependency, Plugin, VersionInfo } from '../../../../types'
|
||||
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import * as React from 'react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { PluginCategoryEnum } from '../../../types'
|
||||
import InstallMulti from './install-multi'
|
||||
import { PluginCategoryEnum } from '../../../../types'
|
||||
import InstallMulti from '../install-multi'
|
||||
|
||||
// ==================== Mock Setup ====================
|
||||
|
||||
@ -62,12 +62,12 @@ vi.mock('@/context/global-public-context', () => ({
|
||||
}))
|
||||
|
||||
// Mock pluginInstallLimit
|
||||
vi.mock('../../hooks/use-install-plugin-limit', () => ({
|
||||
vi.mock('../../../hooks/use-install-plugin-limit', () => ({
|
||||
pluginInstallLimit: () => ({ canInstall: true }),
|
||||
}))
|
||||
|
||||
// Mock child components
|
||||
vi.mock('../item/github-item', () => ({
|
||||
vi.mock('../../item/github-item', () => ({
|
||||
default: vi.fn().mockImplementation(({
|
||||
checked,
|
||||
onCheckedChange,
|
||||
@ -120,7 +120,7 @@ vi.mock('../item/github-item', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('../item/marketplace-item', () => ({
|
||||
vi.mock('../../item/marketplace-item', () => ({
|
||||
default: vi.fn().mockImplementation(({
|
||||
checked,
|
||||
onCheckedChange,
|
||||
@ -142,7 +142,7 @@ vi.mock('../item/marketplace-item', () => ({
|
||||
)),
|
||||
}))
|
||||
|
||||
vi.mock('../item/package-item', () => ({
|
||||
vi.mock('../../item/package-item', () => ({
|
||||
default: vi.fn().mockImplementation(({
|
||||
checked,
|
||||
onCheckedChange,
|
||||
@ -163,7 +163,7 @@ vi.mock('../item/package-item', () => ({
|
||||
)),
|
||||
}))
|
||||
|
||||
vi.mock('../../base/loading-error', () => ({
|
||||
vi.mock('../../../base/loading-error', () => ({
|
||||
default: () => <div data-testid="loading-error">Loading Error</div>,
|
||||
}))
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import type { Dependency, InstallStatusResponse, PackageDependency } from '../../../types'
|
||||
import type { Dependency, InstallStatusResponse, PackageDependency } from '../../../../types'
|
||||
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { PluginCategoryEnum, TaskStatus } from '../../../types'
|
||||
import Install from './install'
|
||||
import { PluginCategoryEnum, TaskStatus } from '../../../../types'
|
||||
import Install from '../install'
|
||||
|
||||
// ==================== Mock Setup ====================
|
||||
|
||||
@ -42,7 +42,7 @@ vi.mock('@/service/use-plugins', () => ({
|
||||
// Mock checkTaskStatus
|
||||
const mockCheck = vi.fn()
|
||||
const mockStop = vi.fn()
|
||||
vi.mock('../../base/check-task-status', () => ({
|
||||
vi.mock('../../../base/check-task-status', () => ({
|
||||
default: () => ({
|
||||
check: mockCheck,
|
||||
stop: mockStop,
|
||||
@ -51,7 +51,7 @@ vi.mock('../../base/check-task-status', () => ({
|
||||
|
||||
// Mock useRefreshPluginList
|
||||
const mockRefreshPluginList = vi.fn()
|
||||
vi.mock('../../hooks/use-refresh-plugin-list', () => ({
|
||||
vi.mock('../../../hooks/use-refresh-plugin-list', () => ({
|
||||
default: () => ({
|
||||
refreshPluginList: mockRefreshPluginList,
|
||||
}),
|
||||
@ -69,7 +69,7 @@ vi.mock('@/app/components/plugins/plugin-page/use-reference-setting', () => ({
|
||||
}))
|
||||
|
||||
// Mock InstallMulti component with forwardRef support
|
||||
vi.mock('./install-multi', async () => {
|
||||
vi.mock('../install-multi', async () => {
|
||||
const React = await import('react')
|
||||
|
||||
const createPlugin = (index: number) => ({
|
||||
@ -838,7 +838,7 @@ describe('Install Component', () => {
|
||||
// ==================== Memoization Test ====================
|
||||
describe('Memoization', () => {
|
||||
it('should be memoized', async () => {
|
||||
const InstallModule = await import('./install')
|
||||
const InstallModule = await import('../install')
|
||||
// memo returns an object with $$typeof
|
||||
expect(typeof InstallModule.default).toBe('object')
|
||||
})
|
||||
@ -1,9 +1,9 @@
|
||||
import type { GitHubRepoReleaseResponse, PluginDeclaration, PluginManifestInMarket, UpdateFromGitHubPayload } from '../../types'
|
||||
import type { GitHubRepoReleaseResponse, PluginDeclaration, PluginManifestInMarket, UpdateFromGitHubPayload } from '../../../types'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { PluginCategoryEnum } from '../../types'
|
||||
import { convertRepoToUrl, parseGitHubUrl, pluginManifestInMarketToPluginProps, pluginManifestToCardPluginProps } from '../utils'
|
||||
import InstallFromGitHub from './index'
|
||||
import { PluginCategoryEnum } from '../../../types'
|
||||
import { convertRepoToUrl, parseGitHubUrl, pluginManifestInMarketToPluginProps, pluginManifestToCardPluginProps } from '../../utils'
|
||||
import InstallFromGitHub from '../index'
|
||||
|
||||
// Factory functions for test data (defined before mocks that use them)
|
||||
const createMockManifest = (overrides: Partial<PluginDeclaration> = {}): PluginDeclaration => ({
|
||||
@ -69,12 +69,12 @@ vi.mock('@/app/components/plugins/install-plugin/base/use-get-icon', () => ({
|
||||
}))
|
||||
|
||||
const mockFetchReleases = vi.fn()
|
||||
vi.mock('../hooks', () => ({
|
||||
vi.mock('../../hooks', () => ({
|
||||
useGitHubReleases: () => ({ fetchReleases: mockFetchReleases }),
|
||||
}))
|
||||
|
||||
const mockRefreshPluginList = vi.fn()
|
||||
vi.mock('../hooks/use-refresh-plugin-list', () => ({
|
||||
vi.mock('../../hooks/use-refresh-plugin-list', () => ({
|
||||
default: () => ({ refreshPluginList: mockRefreshPluginList }),
|
||||
}))
|
||||
|
||||
@ -84,12 +84,12 @@ let mockHideLogicState = {
|
||||
setIsInstalling: vi.fn(),
|
||||
handleStartToInstall: vi.fn(),
|
||||
}
|
||||
vi.mock('../hooks/use-hide-logic', () => ({
|
||||
vi.mock('../../hooks/use-hide-logic', () => ({
|
||||
default: () => mockHideLogicState,
|
||||
}))
|
||||
|
||||
// Mock child components
|
||||
vi.mock('./steps/setURL', () => ({
|
||||
vi.mock('../steps/setURL', () => ({
|
||||
default: ({ repoUrl, onChange, onNext, onCancel }: {
|
||||
repoUrl: string
|
||||
onChange: (value: string) => void
|
||||
@ -108,7 +108,7 @@ vi.mock('./steps/setURL', () => ({
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('./steps/selectPackage', () => ({
|
||||
vi.mock('../steps/selectPackage', () => ({
|
||||
default: ({
|
||||
repoUrl,
|
||||
selectedVersion,
|
||||
@ -170,7 +170,7 @@ vi.mock('./steps/selectPackage', () => ({
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('./steps/loaded', () => ({
|
||||
vi.mock('../steps/loaded', () => ({
|
||||
default: ({
|
||||
uniqueIdentifier,
|
||||
payload,
|
||||
@ -208,7 +208,7 @@ vi.mock('./steps/loaded', () => ({
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('../base/installed', () => ({
|
||||
vi.mock('../../base/installed', () => ({
|
||||
default: ({ payload, isFailed, errMsg, onCancel }: {
|
||||
payload: PluginDeclaration | null
|
||||
isFailed: boolean
|
||||
@ -1,8 +1,8 @@
|
||||
import type { Plugin, PluginDeclaration, UpdateFromGitHubPayload } from '../../../types'
|
||||
import type { Plugin, PluginDeclaration, UpdateFromGitHubPayload } from '../../../../types'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { PluginCategoryEnum, TaskStatus } from '../../../types'
|
||||
import Loaded from './loaded'
|
||||
import { PluginCategoryEnum, TaskStatus } from '../../../../types'
|
||||
import Loaded from '../loaded'
|
||||
|
||||
// Mock dependencies
|
||||
const mockUseCheckInstalled = vi.fn()
|
||||
@ -23,12 +23,12 @@ vi.mock('@/service/use-plugins', () => ({
|
||||
}))
|
||||
|
||||
const mockCheck = vi.fn()
|
||||
vi.mock('../../base/check-task-status', () => ({
|
||||
vi.mock('../../../base/check-task-status', () => ({
|
||||
default: () => ({ check: mockCheck }),
|
||||
}))
|
||||
|
||||
// Mock Card component
|
||||
vi.mock('../../../card', () => ({
|
||||
vi.mock('../../../../card', () => ({
|
||||
default: ({ payload, titleLeft }: { payload: Plugin, titleLeft?: React.ReactNode }) => (
|
||||
<div data-testid="plugin-card">
|
||||
<span data-testid="card-name">{payload.name}</span>
|
||||
@ -38,7 +38,7 @@ vi.mock('../../../card', () => ({
|
||||
}))
|
||||
|
||||
// Mock Version component
|
||||
vi.mock('../../base/version', () => ({
|
||||
vi.mock('../../../base/version', () => ({
|
||||
default: ({ hasInstalled, installedVersion, toInstallVersion }: {
|
||||
hasInstalled: boolean
|
||||
installedVersion?: string
|
||||
@ -1,13 +1,13 @@
|
||||
import type { PluginDeclaration, UpdateFromGitHubPayload } from '../../../types'
|
||||
import type { PluginDeclaration, UpdateFromGitHubPayload } from '../../../../types'
|
||||
import type { Item } from '@/app/components/base/select'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { PluginCategoryEnum } from '../../../types'
|
||||
import SelectPackage from './selectPackage'
|
||||
import { PluginCategoryEnum } from '../../../../types'
|
||||
import SelectPackage from '../selectPackage'
|
||||
|
||||
// Mock the useGitHubUpload hook
|
||||
const mockHandleUpload = vi.fn()
|
||||
vi.mock('../../hooks', () => ({
|
||||
vi.mock('../../../hooks', () => ({
|
||||
useGitHubUpload: () => ({ handleUpload: mockHandleUpload }),
|
||||
}))
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import SetURL from './setURL'
|
||||
import SetURL from '../setURL'
|
||||
|
||||
describe('SetURL', () => {
|
||||
const defaultProps = {
|
||||
@ -1,8 +1,8 @@
|
||||
import type { Dependency, PluginDeclaration } from '../../types'
|
||||
import type { Dependency, PluginDeclaration } from '../../../types'
|
||||
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { InstallStep, PluginCategoryEnum } from '../../types'
|
||||
import InstallFromLocalPackage from './index'
|
||||
import { InstallStep, PluginCategoryEnum } from '../../../types'
|
||||
import InstallFromLocalPackage from '../index'
|
||||
|
||||
// Factory functions for test data
|
||||
const createMockManifest = (overrides: Partial<PluginDeclaration> = {}): PluginDeclaration => ({
|
||||
@ -64,7 +64,7 @@ let mockHideLogicState = {
|
||||
setIsInstalling: vi.fn(),
|
||||
handleStartToInstall: vi.fn(),
|
||||
}
|
||||
vi.mock('../hooks/use-hide-logic', () => ({
|
||||
vi.mock('../../hooks/use-hide-logic', () => ({
|
||||
default: () => mockHideLogicState,
|
||||
}))
|
||||
|
||||
@ -73,7 +73,7 @@ let uploadingOnPackageUploaded: ((result: { uniqueIdentifier: string, manifest:
|
||||
let uploadingOnBundleUploaded: ((result: Dependency[]) => void) | null = null
|
||||
let _uploadingOnFailed: ((errorMsg: string) => void) | null = null
|
||||
|
||||
vi.mock('./steps/uploading', () => ({
|
||||
vi.mock('../steps/uploading', () => ({
|
||||
default: ({
|
||||
isBundle,
|
||||
file,
|
||||
@ -127,7 +127,7 @@ let _packageStepChangeCallback: ((step: InstallStep) => void) | null = null
|
||||
let _packageSetIsInstallingCallback: ((isInstalling: boolean) => void) | null = null
|
||||
let _packageOnErrorCallback: ((errorMsg: string) => void) | null = null
|
||||
|
||||
vi.mock('./ready-to-install', () => ({
|
||||
vi.mock('../ready-to-install', () => ({
|
||||
default: ({
|
||||
step,
|
||||
onStepChange,
|
||||
@ -192,7 +192,7 @@ vi.mock('./ready-to-install', () => ({
|
||||
let _bundleStepChangeCallback: ((step: InstallStep) => void) | null = null
|
||||
let _bundleSetIsInstallingCallback: ((isInstalling: boolean) => void) | null = null
|
||||
|
||||
vi.mock('../install-bundle/ready-to-install', () => ({
|
||||
vi.mock('../../install-bundle/ready-to-install', () => ({
|
||||
default: ({
|
||||
step,
|
||||
onStepChange,
|
||||
@ -1,8 +1,8 @@
|
||||
import type { PluginDeclaration } from '../../types'
|
||||
import type { PluginDeclaration } from '../../../types'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { InstallStep, PluginCategoryEnum } from '../../types'
|
||||
import ReadyToInstall from './ready-to-install'
|
||||
import { InstallStep, PluginCategoryEnum } from '../../../types'
|
||||
import ReadyToInstall from '../ready-to-install'
|
||||
|
||||
// Factory function for test data
|
||||
const createMockManifest = (overrides: Partial<PluginDeclaration> = {}): PluginDeclaration => ({
|
||||
@ -29,7 +29,7 @@ const createMockManifest = (overrides: Partial<PluginDeclaration> = {}): PluginD
|
||||
|
||||
// Mock external dependencies
|
||||
const mockRefreshPluginList = vi.fn()
|
||||
vi.mock('../hooks/use-refresh-plugin-list', () => ({
|
||||
vi.mock('../../hooks/use-refresh-plugin-list', () => ({
|
||||
default: () => ({
|
||||
refreshPluginList: mockRefreshPluginList,
|
||||
}),
|
||||
@ -41,7 +41,7 @@ let _installOnFailed: ((message?: string) => void) | null = null
|
||||
let _installOnCancel: (() => void) | null = null
|
||||
let _installOnStartToInstall: (() => void) | null = null
|
||||
|
||||
vi.mock('./steps/install', () => ({
|
||||
vi.mock('../steps/install', () => ({
|
||||
default: ({
|
||||
uniqueIdentifier,
|
||||
payload,
|
||||
@ -87,7 +87,7 @@ vi.mock('./steps/install', () => ({
|
||||
}))
|
||||
|
||||
// Mock Installed component
|
||||
vi.mock('../base/installed', () => ({
|
||||
vi.mock('../../base/installed', () => ({
|
||||
default: ({
|
||||
payload,
|
||||
isFailed,
|
||||
@ -1,8 +1,8 @@
|
||||
import type { PluginDeclaration } from '../../../types'
|
||||
import type { PluginDeclaration } from '../../../../types'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { PluginCategoryEnum, TaskStatus } from '../../../types'
|
||||
import Install from './install'
|
||||
import { PluginCategoryEnum, TaskStatus } from '../../../../types'
|
||||
import Install from '../install'
|
||||
|
||||
// Factory function for test data
|
||||
const createMockManifest = (overrides: Partial<PluginDeclaration> = {}): PluginDeclaration => ({
|
||||
@ -50,7 +50,7 @@ vi.mock('@/service/plugins', () => ({
|
||||
|
||||
const mockCheck = vi.fn()
|
||||
const mockStop = vi.fn()
|
||||
vi.mock('../../base/check-task-status', () => ({
|
||||
vi.mock('../../../base/check-task-status', () => ({
|
||||
default: () => ({
|
||||
check: mockCheck,
|
||||
stop: mockStop,
|
||||
@ -64,22 +64,7 @@ vi.mock('@/context/app-context', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('react-i18next', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('react-i18next')>()
|
||||
const { createReactI18nextMock } = await import('@/test/i18n-mock')
|
||||
return {
|
||||
...actual,
|
||||
...createReactI18nextMock(),
|
||||
Trans: ({ i18nKey, components }: { i18nKey: string, components?: Record<string, React.ReactNode> }) => (
|
||||
<span data-testid="trans">
|
||||
{i18nKey}
|
||||
{components?.trustSource}
|
||||
</span>
|
||||
),
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('../../../card', () => ({
|
||||
vi.mock('../../../../card', () => ({
|
||||
default: ({ payload, titleLeft }: {
|
||||
payload: Record<string, unknown>
|
||||
titleLeft?: React.ReactNode
|
||||
@ -91,7 +76,7 @@ vi.mock('../../../card', () => ({
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('../../base/version', () => ({
|
||||
vi.mock('../../../base/version', () => ({
|
||||
default: ({ hasInstalled, installedVersion, toInstallVersion }: {
|
||||
hasInstalled: boolean
|
||||
installedVersion?: string
|
||||
@ -105,7 +90,7 @@ vi.mock('../../base/version', () => ({
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('../../utils', () => ({
|
||||
vi.mock('../../../utils', () => ({
|
||||
pluginManifestToCardPluginProps: (manifest: PluginDeclaration) => ({
|
||||
name: manifest.name,
|
||||
author: manifest.author,
|
||||
@ -148,7 +133,7 @@ describe('Install', () => {
|
||||
it('should render trust source message', () => {
|
||||
render(<Install {...defaultProps} />)
|
||||
|
||||
expect(screen.getByTestId('trans')).toBeInTheDocument()
|
||||
expect(screen.getByText('installModal.fromTrustSource')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render plugin card', () => {
|
||||
@ -1,9 +1,9 @@
|
||||
import type { Dependency, PluginDeclaration } from '../../../types'
|
||||
import type { Dependency, PluginDeclaration } from '../../../../types'
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { PluginCategoryEnum } from '../../../types'
|
||||
import Uploading from './uploading'
|
||||
import { PluginCategoryEnum } from '../../../../types'
|
||||
import Uploading from '../uploading'
|
||||
|
||||
// Factory function for test data
|
||||
const createMockManifest = (overrides: Partial<PluginDeclaration> = {}): PluginDeclaration => ({
|
||||
@ -48,7 +48,7 @@ vi.mock('@/service/plugins', () => ({
|
||||
uploadFile: (...args: unknown[]) => mockUploadFile(...args),
|
||||
}))
|
||||
|
||||
vi.mock('../../../card', () => ({
|
||||
vi.mock('../../../../card', () => ({
|
||||
default: ({ payload, isLoading, loadingFileName }: {
|
||||
payload: { name: string }
|
||||
isLoading?: boolean
|
||||
@ -1,8 +1,8 @@
|
||||
import type { Dependency, Plugin, PluginManifestInMarket } from '../../types'
|
||||
import type { Dependency, Plugin, PluginManifestInMarket } from '../../../types'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { InstallStep, PluginCategoryEnum } from '../../types'
|
||||
import InstallFromMarketplace from './index'
|
||||
import { InstallStep, PluginCategoryEnum } from '../../../types'
|
||||
import InstallFromMarketplace from '../index'
|
||||
|
||||
// Factory functions for test data
|
||||
// Use type casting to avoid strict locale requirements in tests
|
||||
@ -69,7 +69,7 @@ const createMockDependencies = (): Dependency[] => [
|
||||
|
||||
// Mock external dependencies
|
||||
const mockRefreshPluginList = vi.fn()
|
||||
vi.mock('../hooks/use-refresh-plugin-list', () => ({
|
||||
vi.mock('../../hooks/use-refresh-plugin-list', () => ({
|
||||
default: () => ({ refreshPluginList: mockRefreshPluginList }),
|
||||
}))
|
||||
|
||||
@ -79,12 +79,12 @@ let mockHideLogicState = {
|
||||
setIsInstalling: vi.fn(),
|
||||
handleStartToInstall: vi.fn(),
|
||||
}
|
||||
vi.mock('../hooks/use-hide-logic', () => ({
|
||||
vi.mock('../../hooks/use-hide-logic', () => ({
|
||||
default: () => mockHideLogicState,
|
||||
}))
|
||||
|
||||
// Mock child components
|
||||
vi.mock('./steps/install', () => ({
|
||||
vi.mock('../steps/install', () => ({
|
||||
default: ({
|
||||
uniqueIdentifier,
|
||||
payload,
|
||||
@ -113,7 +113,7 @@ vi.mock('./steps/install', () => ({
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('../install-bundle/ready-to-install', () => ({
|
||||
vi.mock('../../install-bundle/ready-to-install', () => ({
|
||||
default: ({
|
||||
step,
|
||||
onStepChange,
|
||||
@ -145,7 +145,7 @@ vi.mock('../install-bundle/ready-to-install', () => ({
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('../base/installed', () => ({
|
||||
vi.mock('../../base/installed', () => ({
|
||||
default: ({
|
||||
payload,
|
||||
isMarketPayload,
|
||||
@ -1,9 +1,9 @@
|
||||
import type { Plugin, PluginManifestInMarket } from '../../../types'
|
||||
import type { Plugin, PluginManifestInMarket } from '../../../../types'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { act } from 'react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { PluginCategoryEnum, TaskStatus } from '../../../types'
|
||||
import Install from './install'
|
||||
import { PluginCategoryEnum, TaskStatus } from '../../../../types'
|
||||
import Install from '../install'
|
||||
|
||||
// Factory functions for test data
|
||||
const createMockManifest = (overrides: Partial<PluginManifestInMarket> = {}): PluginManifestInMarket => ({
|
||||
@ -64,7 +64,7 @@ let mockLangGeniusVersionInfo = { current_version: '1.0.0' }
|
||||
|
||||
// Mock useCheckInstalled
|
||||
vi.mock('@/app/components/plugins/install-plugin/hooks/use-check-installed', () => ({
|
||||
default: ({ pluginIds }: { pluginIds: string[], enabled: boolean }) => ({
|
||||
default: ({ pluginIds: _pluginIds }: { pluginIds: string[], enabled: boolean }) => ({
|
||||
installedInfo: mockInstalledInfo,
|
||||
isLoading: mockIsLoading,
|
||||
error: null,
|
||||
@ -88,7 +88,7 @@ vi.mock('@/service/use-plugins', () => ({
|
||||
}))
|
||||
|
||||
// Mock checkTaskStatus
|
||||
vi.mock('../../base/check-task-status', () => ({
|
||||
vi.mock('../../../base/check-task-status', () => ({
|
||||
default: () => ({
|
||||
check: mockCheckTaskStatus,
|
||||
stop: mockStopTaskStatus,
|
||||
@ -103,20 +103,20 @@ vi.mock('@/context/app-context', () => ({
|
||||
}))
|
||||
|
||||
// Mock useInstallPluginLimit
|
||||
vi.mock('../../hooks/use-install-plugin-limit', () => ({
|
||||
vi.mock('../../../hooks/use-install-plugin-limit', () => ({
|
||||
default: () => ({ canInstall: mockCanInstall }),
|
||||
}))
|
||||
|
||||
// Mock Card component
|
||||
vi.mock('../../../card', () => ({
|
||||
default: ({ payload, titleLeft, className, limitedInstall }: {
|
||||
payload: any
|
||||
vi.mock('../../../../card', () => ({
|
||||
default: ({ payload, titleLeft, className: _className, limitedInstall }: {
|
||||
payload: Record<string, unknown>
|
||||
titleLeft?: React.ReactNode
|
||||
className?: string
|
||||
limitedInstall?: boolean
|
||||
}) => (
|
||||
<div data-testid="plugin-card">
|
||||
<span data-testid="card-payload-name">{payload?.name}</span>
|
||||
<span data-testid="card-payload-name">{String(payload?.name ?? '')}</span>
|
||||
<span data-testid="card-limited-install">{limitedInstall ? 'true' : 'false'}</span>
|
||||
{!!titleLeft && <div data-testid="card-title-left">{titleLeft}</div>}
|
||||
</div>
|
||||
@ -124,7 +124,7 @@ vi.mock('../../../card', () => ({
|
||||
}))
|
||||
|
||||
// Mock Version component
|
||||
vi.mock('../../base/version', () => ({
|
||||
vi.mock('../../../base/version', () => ({
|
||||
default: ({ hasInstalled, installedVersion, toInstallVersion }: {
|
||||
hasInstalled: boolean
|
||||
installedVersion?: string
|
||||
@ -139,7 +139,7 @@ vi.mock('../../base/version', () => ({
|
||||
}))
|
||||
|
||||
// Mock utils
|
||||
vi.mock('../../utils', () => ({
|
||||
vi.mock('../../../utils', () => ({
|
||||
pluginManifestInMarketToPluginProps: (payload: PluginManifestInMarket) => ({
|
||||
name: payload.name,
|
||||
icon: payload.icon,
|
||||
@ -255,7 +255,7 @@ describe('Install Component (steps/install.tsx)', () => {
|
||||
})
|
||||
|
||||
it('should fallback to latest_version when version is undefined', () => {
|
||||
const manifest = createMockManifest({ version: undefined as any, latest_version: '3.0.0' })
|
||||
const manifest = createMockManifest({ version: undefined as unknown as string, latest_version: '3.0.0' })
|
||||
render(<Install {...defaultProps} payload={manifest} />)
|
||||
|
||||
expect(screen.getByTestId('to-install-version')).toHaveTextContent('3.0.0')
|
||||
@ -701,7 +701,7 @@ describe('Install Component (steps/install.tsx)', () => {
|
||||
})
|
||||
|
||||
it('should handle null current_version in langGeniusVersionInfo', () => {
|
||||
mockLangGeniusVersionInfo = { current_version: null as any }
|
||||
mockLangGeniusVersionInfo = { current_version: null as unknown as string }
|
||||
mockPluginDeclaration = {
|
||||
manifest: { meta: { minimum_dify_version: '1.0.0' } },
|
||||
}
|
||||
601
web/app/components/plugins/marketplace/__tests__/hooks.spec.tsx
Normal file
601
web/app/components/plugins/marketplace/__tests__/hooks.spec.tsx
Normal file
@ -0,0 +1,601 @@
|
||||
import { render, renderHook } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
// ================================
|
||||
// Mock External Dependencies
|
||||
// ================================
|
||||
|
||||
vi.mock('@/i18n-config/i18next-config', () => ({
|
||||
default: {
|
||||
getFixedT: () => (key: string) => key,
|
||||
},
|
||||
}))
|
||||
|
||||
const mockSetUrlFilters = vi.fn()
|
||||
vi.mock('@/hooks/use-query-params', () => ({
|
||||
useMarketplaceFilters: () => [
|
||||
{ q: '', tags: [], category: '' },
|
||||
mockSetUrlFilters,
|
||||
],
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-plugins', () => ({
|
||||
useInstalledPluginList: () => ({
|
||||
data: { plugins: [] },
|
||||
isSuccess: true,
|
||||
}),
|
||||
}))
|
||||
|
||||
const mockFetchNextPage = vi.fn()
|
||||
const mockHasNextPage = false
|
||||
let mockInfiniteQueryData: { pages: Array<{ plugins: unknown[], total: number, page: number, page_size: number }> } | undefined
|
||||
let capturedInfiniteQueryFn: ((ctx: { pageParam: number, signal: AbortSignal }) => Promise<unknown>) | null = null
|
||||
let capturedQueryFn: ((ctx: { signal: AbortSignal }) => Promise<unknown>) | null = null
|
||||
let capturedGetNextPageParam: ((lastPage: { page: number, page_size: number, total: number }) => number | undefined) | null = null
|
||||
|
||||
vi.mock('@tanstack/react-query', () => ({
|
||||
useQuery: vi.fn(({ queryFn, enabled }: { queryFn: (ctx: { signal: AbortSignal }) => Promise<unknown>, enabled: boolean }) => {
|
||||
capturedQueryFn = queryFn
|
||||
if (queryFn) {
|
||||
const controller = new AbortController()
|
||||
queryFn({ signal: controller.signal }).catch(() => {})
|
||||
}
|
||||
return {
|
||||
data: enabled ? { marketplaceCollections: [], marketplaceCollectionPluginsMap: {} } : undefined,
|
||||
isFetching: false,
|
||||
isPending: false,
|
||||
isSuccess: enabled,
|
||||
}
|
||||
}),
|
||||
useInfiniteQuery: vi.fn(({ queryFn, getNextPageParam }: {
|
||||
queryFn: (ctx: { pageParam: number, signal: AbortSignal }) => Promise<unknown>
|
||||
getNextPageParam: (lastPage: { page: number, page_size: number, total: number }) => number | undefined
|
||||
enabled: boolean
|
||||
}) => {
|
||||
capturedInfiniteQueryFn = queryFn
|
||||
capturedGetNextPageParam = getNextPageParam
|
||||
if (queryFn) {
|
||||
const controller = new AbortController()
|
||||
queryFn({ pageParam: 1, signal: controller.signal }).catch(() => {})
|
||||
}
|
||||
if (getNextPageParam) {
|
||||
getNextPageParam({ page: 1, page_size: 40, total: 100 })
|
||||
getNextPageParam({ page: 3, page_size: 40, total: 100 })
|
||||
}
|
||||
return {
|
||||
data: mockInfiniteQueryData,
|
||||
isPending: false,
|
||||
isFetching: false,
|
||||
isFetchingNextPage: false,
|
||||
hasNextPage: mockHasNextPage,
|
||||
fetchNextPage: mockFetchNextPage,
|
||||
}
|
||||
}),
|
||||
useQueryClient: vi.fn(() => ({
|
||||
removeQueries: vi.fn(),
|
||||
})),
|
||||
}))
|
||||
|
||||
vi.mock('ahooks', () => ({
|
||||
useDebounceFn: (fn: (...args: unknown[]) => void) => ({
|
||||
run: fn,
|
||||
cancel: vi.fn(),
|
||||
}),
|
||||
}))
|
||||
|
||||
let mockPostMarketplaceShouldFail = false
|
||||
const mockPostMarketplaceResponse = {
|
||||
data: {
|
||||
plugins: [
|
||||
{ type: 'plugin', org: 'test', name: 'plugin1', tags: [] },
|
||||
{ type: 'plugin', org: 'test', name: 'plugin2', tags: [] },
|
||||
],
|
||||
bundles: [] as Array<{ type: string, org: string, name: string, tags: unknown[] }>,
|
||||
total: 2,
|
||||
},
|
||||
}
|
||||
|
||||
vi.mock('@/service/base', () => ({
|
||||
postMarketplace: vi.fn(() => {
|
||||
if (mockPostMarketplaceShouldFail)
|
||||
return Promise.reject(new Error('Mock API error'))
|
||||
return Promise.resolve(mockPostMarketplaceResponse)
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/config', () => ({
|
||||
API_PREFIX: '/api',
|
||||
APP_VERSION: '1.0.0',
|
||||
IS_MARKETPLACE: false,
|
||||
MARKETPLACE_API_PREFIX: 'https://marketplace.dify.ai/api/v1',
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/var', () => ({
|
||||
getMarketplaceUrl: (path: string) => `https://marketplace.dify.ai${path}`,
|
||||
}))
|
||||
|
||||
vi.mock('@/service/client', () => ({
|
||||
marketplaceClient: {
|
||||
collections: vi.fn(async () => ({
|
||||
data: {
|
||||
collections: [
|
||||
{
|
||||
name: 'collection-1',
|
||||
label: { 'en-US': 'Collection 1' },
|
||||
description: { 'en-US': 'Desc' },
|
||||
rule: '',
|
||||
created_at: '2024-01-01',
|
||||
updated_at: '2024-01-01',
|
||||
searchable: true,
|
||||
search_params: { query: '', sort_by: 'install_count', sort_order: 'DESC' },
|
||||
},
|
||||
],
|
||||
},
|
||||
})),
|
||||
collectionPlugins: vi.fn(async () => ({
|
||||
data: {
|
||||
plugins: [
|
||||
{ type: 'plugin', org: 'test', name: 'plugin1', tags: [] },
|
||||
],
|
||||
},
|
||||
})),
|
||||
searchAdvanced: vi.fn(async () => ({
|
||||
data: {
|
||||
plugins: [
|
||||
{ type: 'plugin', org: 'test', name: 'plugin1', tags: [] },
|
||||
],
|
||||
total: 1,
|
||||
},
|
||||
})),
|
||||
},
|
||||
}))
|
||||
|
||||
// ================================
|
||||
// useMarketplaceCollectionsAndPlugins Tests
|
||||
// ================================
|
||||
describe('useMarketplaceCollectionsAndPlugins', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should return initial state correctly', async () => {
|
||||
const { useMarketplaceCollectionsAndPlugins } = await import('../hooks')
|
||||
const { result } = renderHook(() => useMarketplaceCollectionsAndPlugins())
|
||||
|
||||
expect(result.current.isLoading).toBe(false)
|
||||
expect(result.current.isSuccess).toBe(false)
|
||||
expect(result.current.queryMarketplaceCollectionsAndPlugins).toBeDefined()
|
||||
expect(result.current.setMarketplaceCollections).toBeDefined()
|
||||
expect(result.current.setMarketplaceCollectionPluginsMap).toBeDefined()
|
||||
})
|
||||
|
||||
it('should provide queryMarketplaceCollectionsAndPlugins function', async () => {
|
||||
const { useMarketplaceCollectionsAndPlugins } = await import('../hooks')
|
||||
const { result } = renderHook(() => useMarketplaceCollectionsAndPlugins())
|
||||
expect(typeof result.current.queryMarketplaceCollectionsAndPlugins).toBe('function')
|
||||
})
|
||||
|
||||
it('should provide setMarketplaceCollections function', async () => {
|
||||
const { useMarketplaceCollectionsAndPlugins } = await import('../hooks')
|
||||
const { result } = renderHook(() => useMarketplaceCollectionsAndPlugins())
|
||||
expect(typeof result.current.setMarketplaceCollections).toBe('function')
|
||||
})
|
||||
|
||||
it('should provide setMarketplaceCollectionPluginsMap function', async () => {
|
||||
const { useMarketplaceCollectionsAndPlugins } = await import('../hooks')
|
||||
const { result } = renderHook(() => useMarketplaceCollectionsAndPlugins())
|
||||
expect(typeof result.current.setMarketplaceCollectionPluginsMap).toBe('function')
|
||||
})
|
||||
|
||||
it('should return marketplaceCollections from data or override', async () => {
|
||||
const { useMarketplaceCollectionsAndPlugins } = await import('../hooks')
|
||||
const { result } = renderHook(() => useMarketplaceCollectionsAndPlugins())
|
||||
expect(result.current.marketplaceCollections).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should return marketplaceCollectionPluginsMap from data or override', async () => {
|
||||
const { useMarketplaceCollectionsAndPlugins } = await import('../hooks')
|
||||
const { result } = renderHook(() => useMarketplaceCollectionsAndPlugins())
|
||||
expect(result.current.marketplaceCollectionPluginsMap).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// useMarketplacePluginsByCollectionId Tests
|
||||
// ================================
|
||||
describe('useMarketplacePluginsByCollectionId', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should return initial state when collectionId is undefined', async () => {
|
||||
const { useMarketplacePluginsByCollectionId } = await import('../hooks')
|
||||
const { result } = renderHook(() => useMarketplacePluginsByCollectionId(undefined))
|
||||
expect(result.current.plugins).toEqual([])
|
||||
expect(result.current.isLoading).toBe(false)
|
||||
expect(result.current.isSuccess).toBe(false)
|
||||
})
|
||||
|
||||
it('should return isLoading false when collectionId is provided and query completes', async () => {
|
||||
const { useMarketplacePluginsByCollectionId } = await import('../hooks')
|
||||
const { result } = renderHook(() => useMarketplacePluginsByCollectionId('test-collection'))
|
||||
expect(result.current.isLoading).toBe(false)
|
||||
})
|
||||
|
||||
it('should accept query parameter', async () => {
|
||||
const { useMarketplacePluginsByCollectionId } = await import('../hooks')
|
||||
const { result } = renderHook(() =>
|
||||
useMarketplacePluginsByCollectionId('test-collection', {
|
||||
category: 'tool',
|
||||
type: 'plugin',
|
||||
}))
|
||||
expect(result.current.plugins).toBeDefined()
|
||||
})
|
||||
|
||||
it('should return plugins property from hook', async () => {
|
||||
const { useMarketplacePluginsByCollectionId } = await import('../hooks')
|
||||
const { result } = renderHook(() => useMarketplacePluginsByCollectionId('collection-1'))
|
||||
expect(result.current.plugins).toBeDefined()
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// useMarketplacePlugins Tests
|
||||
// ================================
|
||||
describe('useMarketplacePlugins', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockInfiniteQueryData = undefined
|
||||
})
|
||||
|
||||
it('should return initial state correctly', async () => {
|
||||
const { useMarketplacePlugins } = await import('../hooks')
|
||||
const { result } = renderHook(() => useMarketplacePlugins())
|
||||
expect(result.current.plugins).toBeUndefined()
|
||||
expect(result.current.total).toBeUndefined()
|
||||
expect(result.current.isLoading).toBe(false)
|
||||
expect(result.current.isFetchingNextPage).toBe(false)
|
||||
expect(result.current.hasNextPage).toBe(false)
|
||||
expect(result.current.page).toBe(0)
|
||||
})
|
||||
|
||||
it('should provide queryPlugins function', async () => {
|
||||
const { useMarketplacePlugins } = await import('../hooks')
|
||||
const { result } = renderHook(() => useMarketplacePlugins())
|
||||
expect(typeof result.current.queryPlugins).toBe('function')
|
||||
})
|
||||
|
||||
it('should provide queryPluginsWithDebounced function', async () => {
|
||||
const { useMarketplacePlugins } = await import('../hooks')
|
||||
const { result } = renderHook(() => useMarketplacePlugins())
|
||||
expect(typeof result.current.queryPluginsWithDebounced).toBe('function')
|
||||
})
|
||||
|
||||
it('should provide cancelQueryPluginsWithDebounced function', async () => {
|
||||
const { useMarketplacePlugins } = await import('../hooks')
|
||||
const { result } = renderHook(() => useMarketplacePlugins())
|
||||
expect(typeof result.current.cancelQueryPluginsWithDebounced).toBe('function')
|
||||
})
|
||||
|
||||
it('should provide resetPlugins function', async () => {
|
||||
const { useMarketplacePlugins } = await import('../hooks')
|
||||
const { result } = renderHook(() => useMarketplacePlugins())
|
||||
expect(typeof result.current.resetPlugins).toBe('function')
|
||||
})
|
||||
|
||||
it('should provide fetchNextPage function', async () => {
|
||||
const { useMarketplacePlugins } = await import('../hooks')
|
||||
const { result } = renderHook(() => useMarketplacePlugins())
|
||||
expect(typeof result.current.fetchNextPage).toBe('function')
|
||||
})
|
||||
|
||||
it('should handle queryPlugins call without errors', async () => {
|
||||
const { useMarketplacePlugins } = await import('../hooks')
|
||||
const { result } = renderHook(() => useMarketplacePlugins())
|
||||
expect(() => {
|
||||
result.current.queryPlugins({
|
||||
query: 'test',
|
||||
sort_by: 'install_count',
|
||||
sort_order: 'DESC',
|
||||
category: 'tool',
|
||||
page_size: 20,
|
||||
})
|
||||
}).not.toThrow()
|
||||
})
|
||||
|
||||
it('should handle queryPlugins with bundle type', async () => {
|
||||
const { useMarketplacePlugins } = await import('../hooks')
|
||||
const { result } = renderHook(() => useMarketplacePlugins())
|
||||
expect(() => {
|
||||
result.current.queryPlugins({
|
||||
query: 'test',
|
||||
type: 'bundle',
|
||||
page_size: 40,
|
||||
})
|
||||
}).not.toThrow()
|
||||
})
|
||||
|
||||
it('should handle resetPlugins call', async () => {
|
||||
const { useMarketplacePlugins } = await import('../hooks')
|
||||
const { result } = renderHook(() => useMarketplacePlugins())
|
||||
expect(() => {
|
||||
result.current.resetPlugins()
|
||||
}).not.toThrow()
|
||||
})
|
||||
|
||||
it('should handle queryPluginsWithDebounced call', async () => {
|
||||
const { useMarketplacePlugins } = await import('../hooks')
|
||||
const { result } = renderHook(() => useMarketplacePlugins())
|
||||
expect(() => {
|
||||
result.current.queryPluginsWithDebounced({
|
||||
query: 'debounced search',
|
||||
category: 'all',
|
||||
})
|
||||
}).not.toThrow()
|
||||
})
|
||||
|
||||
it('should handle cancelQueryPluginsWithDebounced call', async () => {
|
||||
const { useMarketplacePlugins } = await import('../hooks')
|
||||
const { result } = renderHook(() => useMarketplacePlugins())
|
||||
expect(() => {
|
||||
result.current.cancelQueryPluginsWithDebounced()
|
||||
}).not.toThrow()
|
||||
})
|
||||
|
||||
it('should return correct page number', async () => {
|
||||
const { useMarketplacePlugins } = await import('../hooks')
|
||||
const { result } = renderHook(() => useMarketplacePlugins())
|
||||
expect(result.current.page).toBe(0)
|
||||
})
|
||||
|
||||
it('should handle queryPlugins with tags', async () => {
|
||||
const { useMarketplacePlugins } = await import('../hooks')
|
||||
const { result } = renderHook(() => useMarketplacePlugins())
|
||||
expect(() => {
|
||||
result.current.queryPlugins({
|
||||
query: 'test',
|
||||
tags: ['search', 'image'],
|
||||
exclude: ['excluded-plugin'],
|
||||
})
|
||||
}).not.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Hooks queryFn Coverage Tests
|
||||
// ================================
|
||||
describe('Hooks queryFn Coverage', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockInfiniteQueryData = undefined
|
||||
mockPostMarketplaceShouldFail = false
|
||||
capturedInfiniteQueryFn = null
|
||||
capturedQueryFn = null
|
||||
})
|
||||
|
||||
it('should cover queryFn with pages data', async () => {
|
||||
mockInfiniteQueryData = {
|
||||
pages: [
|
||||
{ plugins: [{ name: 'plugin1' }], total: 10, page: 1, page_size: 40 },
|
||||
],
|
||||
}
|
||||
|
||||
const { useMarketplacePlugins } = await import('../hooks')
|
||||
const { result } = renderHook(() => useMarketplacePlugins())
|
||||
|
||||
result.current.queryPlugins({
|
||||
query: 'test',
|
||||
category: 'tool',
|
||||
})
|
||||
|
||||
expect(result.current).toBeDefined()
|
||||
})
|
||||
|
||||
it('should expose page and total from infinite query data', async () => {
|
||||
mockInfiniteQueryData = {
|
||||
pages: [
|
||||
{ plugins: [{ name: 'plugin1' }, { name: 'plugin2' }], total: 20, page: 1, page_size: 40 },
|
||||
{ plugins: [{ name: 'plugin3' }], total: 20, page: 2, page_size: 40 },
|
||||
],
|
||||
}
|
||||
|
||||
const { useMarketplacePlugins } = await import('../hooks')
|
||||
const { result } = renderHook(() => useMarketplacePlugins())
|
||||
|
||||
result.current.queryPlugins({ query: 'search' })
|
||||
expect(result.current.page).toBe(2)
|
||||
})
|
||||
|
||||
it('should return undefined total when no query is set', async () => {
|
||||
const { useMarketplacePlugins } = await import('../hooks')
|
||||
const { result } = renderHook(() => useMarketplacePlugins())
|
||||
expect(result.current.total).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should directly test queryFn execution', async () => {
|
||||
const { useMarketplacePlugins } = await import('../hooks')
|
||||
const { result } = renderHook(() => useMarketplacePlugins())
|
||||
|
||||
result.current.queryPlugins({
|
||||
query: 'direct test',
|
||||
category: 'tool',
|
||||
sort_by: 'install_count',
|
||||
sort_order: 'DESC',
|
||||
page_size: 40,
|
||||
})
|
||||
|
||||
if (capturedInfiniteQueryFn) {
|
||||
const controller = new AbortController()
|
||||
const response = await capturedInfiniteQueryFn({ pageParam: 1, signal: controller.signal })
|
||||
expect(response).toBeDefined()
|
||||
}
|
||||
})
|
||||
|
||||
it('should test queryFn with bundle type', async () => {
|
||||
const { useMarketplacePlugins } = await import('../hooks')
|
||||
const { result } = renderHook(() => useMarketplacePlugins())
|
||||
|
||||
result.current.queryPlugins({
|
||||
type: 'bundle',
|
||||
query: 'bundle test',
|
||||
})
|
||||
|
||||
if (capturedInfiniteQueryFn) {
|
||||
const controller = new AbortController()
|
||||
const response = await capturedInfiniteQueryFn({ pageParam: 2, signal: controller.signal })
|
||||
expect(response).toBeDefined()
|
||||
}
|
||||
})
|
||||
|
||||
it('should test queryFn error handling', async () => {
|
||||
mockPostMarketplaceShouldFail = true
|
||||
|
||||
const { useMarketplacePlugins } = await import('../hooks')
|
||||
const { result } = renderHook(() => useMarketplacePlugins())
|
||||
|
||||
result.current.queryPlugins({ query: 'test that will fail' })
|
||||
|
||||
if (capturedInfiniteQueryFn) {
|
||||
const controller = new AbortController()
|
||||
const response = await capturedInfiniteQueryFn({ pageParam: 1, signal: controller.signal })
|
||||
expect(response).toBeDefined()
|
||||
expect(response).toHaveProperty('plugins')
|
||||
}
|
||||
|
||||
mockPostMarketplaceShouldFail = false
|
||||
})
|
||||
|
||||
it('should test useMarketplaceCollectionsAndPlugins queryFn', async () => {
|
||||
const { useMarketplaceCollectionsAndPlugins } = await import('../hooks')
|
||||
const { result } = renderHook(() => useMarketplaceCollectionsAndPlugins())
|
||||
|
||||
result.current.queryMarketplaceCollectionsAndPlugins({
|
||||
condition: 'category=tool',
|
||||
})
|
||||
|
||||
if (capturedQueryFn) {
|
||||
const controller = new AbortController()
|
||||
const response = await capturedQueryFn({ signal: controller.signal })
|
||||
expect(response).toBeDefined()
|
||||
}
|
||||
})
|
||||
|
||||
it('should test getNextPageParam directly', async () => {
|
||||
const { useMarketplacePlugins } = await import('../hooks')
|
||||
renderHook(() => useMarketplacePlugins())
|
||||
|
||||
if (capturedGetNextPageParam) {
|
||||
const nextPage = capturedGetNextPageParam({ page: 1, page_size: 40, total: 100 })
|
||||
expect(nextPage).toBe(2)
|
||||
|
||||
const noMorePages = capturedGetNextPageParam({ page: 3, page_size: 40, total: 100 })
|
||||
expect(noMorePages).toBeUndefined()
|
||||
|
||||
const atBoundary = capturedGetNextPageParam({ page: 2, page_size: 50, total: 100 })
|
||||
expect(atBoundary).toBeUndefined()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// useMarketplaceContainerScroll Tests
|
||||
// ================================
|
||||
describe('useMarketplaceContainerScroll', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should attach scroll event listener to container', async () => {
|
||||
const mockCallback = vi.fn()
|
||||
const mockContainer = document.createElement('div')
|
||||
mockContainer.id = 'marketplace-container'
|
||||
document.body.appendChild(mockContainer)
|
||||
|
||||
const addEventListenerSpy = vi.spyOn(mockContainer, 'addEventListener')
|
||||
const { useMarketplaceContainerScroll } = await import('../hooks')
|
||||
|
||||
const TestComponent = () => {
|
||||
useMarketplaceContainerScroll(mockCallback)
|
||||
return null
|
||||
}
|
||||
|
||||
render(<TestComponent />)
|
||||
expect(addEventListenerSpy).toHaveBeenCalledWith('scroll', expect.any(Function))
|
||||
document.body.removeChild(mockContainer)
|
||||
})
|
||||
|
||||
it('should call callback when scrolled to bottom', async () => {
|
||||
const mockCallback = vi.fn()
|
||||
const mockContainer = document.createElement('div')
|
||||
mockContainer.id = 'scroll-test-container-hooks'
|
||||
document.body.appendChild(mockContainer)
|
||||
|
||||
Object.defineProperty(mockContainer, 'scrollTop', { value: 900, writable: true })
|
||||
Object.defineProperty(mockContainer, 'scrollHeight', { value: 1000, writable: true })
|
||||
Object.defineProperty(mockContainer, 'clientHeight', { value: 100, writable: true })
|
||||
|
||||
const { useMarketplaceContainerScroll } = await import('../hooks')
|
||||
|
||||
const TestComponent = () => {
|
||||
useMarketplaceContainerScroll(mockCallback, 'scroll-test-container-hooks')
|
||||
return null
|
||||
}
|
||||
|
||||
render(<TestComponent />)
|
||||
|
||||
const scrollEvent = new Event('scroll')
|
||||
Object.defineProperty(scrollEvent, 'target', { value: mockContainer })
|
||||
mockContainer.dispatchEvent(scrollEvent)
|
||||
|
||||
expect(mockCallback).toHaveBeenCalled()
|
||||
document.body.removeChild(mockContainer)
|
||||
})
|
||||
|
||||
it('should not call callback when scrollTop is 0', async () => {
|
||||
const mockCallback = vi.fn()
|
||||
const mockContainer = document.createElement('div')
|
||||
mockContainer.id = 'scroll-test-container-hooks-2'
|
||||
document.body.appendChild(mockContainer)
|
||||
|
||||
Object.defineProperty(mockContainer, 'scrollTop', { value: 0, writable: true })
|
||||
Object.defineProperty(mockContainer, 'scrollHeight', { value: 1000, writable: true })
|
||||
Object.defineProperty(mockContainer, 'clientHeight', { value: 100, writable: true })
|
||||
|
||||
const { useMarketplaceContainerScroll } = await import('../hooks')
|
||||
|
||||
const TestComponent = () => {
|
||||
useMarketplaceContainerScroll(mockCallback, 'scroll-test-container-hooks-2')
|
||||
return null
|
||||
}
|
||||
|
||||
render(<TestComponent />)
|
||||
|
||||
const scrollEvent = new Event('scroll')
|
||||
Object.defineProperty(scrollEvent, 'target', { value: mockContainer })
|
||||
mockContainer.dispatchEvent(scrollEvent)
|
||||
|
||||
expect(mockCallback).not.toHaveBeenCalled()
|
||||
document.body.removeChild(mockContainer)
|
||||
})
|
||||
|
||||
it('should remove event listener on unmount', async () => {
|
||||
const mockCallback = vi.fn()
|
||||
const mockContainer = document.createElement('div')
|
||||
mockContainer.id = 'scroll-unmount-container-hooks'
|
||||
document.body.appendChild(mockContainer)
|
||||
|
||||
const removeEventListenerSpy = vi.spyOn(mockContainer, 'removeEventListener')
|
||||
const { useMarketplaceContainerScroll } = await import('../hooks')
|
||||
|
||||
const TestComponent = () => {
|
||||
useMarketplaceContainerScroll(mockCallback, 'scroll-unmount-container-hooks')
|
||||
return null
|
||||
}
|
||||
|
||||
const { unmount } = render(<TestComponent />)
|
||||
unmount()
|
||||
|
||||
expect(removeEventListenerSpy).toHaveBeenCalledWith('scroll', expect.any(Function))
|
||||
document.body.removeChild(mockContainer)
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,15 @@
|
||||
import { describe, it } from 'vitest'
|
||||
|
||||
// The Marketplace index component is an async Server Component
|
||||
// that cannot be unit tested in jsdom. It is covered by integration tests.
|
||||
//
|
||||
// All sub-module tests have been moved to dedicated spec files:
|
||||
// - constants.spec.ts (DEFAULT_SORT, SCROLL_BOTTOM_THRESHOLD, PLUGIN_TYPE_SEARCH_MAP)
|
||||
// - utils.spec.ts (getPluginIconInMarketplace, getFormattedPlugin, getPluginLinkInMarketplace, etc.)
|
||||
// - hooks.spec.tsx (useMarketplaceCollectionsAndPlugins, useMarketplacePlugins, useMarketplaceContainerScroll)
|
||||
|
||||
describe('Marketplace index', () => {
|
||||
it('should be covered by dedicated sub-module specs', () => {
|
||||
// Placeholder to document the split
|
||||
})
|
||||
})
|
||||
317
web/app/components/plugins/marketplace/__tests__/utils.spec.ts
Normal file
317
web/app/components/plugins/marketplace/__tests__/utils.spec.ts
Normal file
@ -0,0 +1,317 @@
|
||||
import type { Plugin } from '@/app/components/plugins/types'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { PluginCategoryEnum } from '@/app/components/plugins/types'
|
||||
import { PLUGIN_TYPE_SEARCH_MAP } from '../constants'
|
||||
|
||||
// Mock config
|
||||
vi.mock('@/config', () => ({
|
||||
API_PREFIX: '/api',
|
||||
APP_VERSION: '1.0.0',
|
||||
IS_MARKETPLACE: false,
|
||||
MARKETPLACE_API_PREFIX: 'https://marketplace.dify.ai/api/v1',
|
||||
}))
|
||||
|
||||
// Mock var utils
|
||||
vi.mock('@/utils/var', () => ({
|
||||
getMarketplaceUrl: (path: string) => `https://marketplace.dify.ai${path}`,
|
||||
}))
|
||||
|
||||
// Mock marketplace client
|
||||
const mockCollectionPlugins = vi.fn()
|
||||
const mockCollections = vi.fn()
|
||||
const mockSearchAdvanced = vi.fn()
|
||||
|
||||
vi.mock('@/service/client', () => ({
|
||||
marketplaceClient: {
|
||||
collections: (...args: unknown[]) => mockCollections(...args),
|
||||
collectionPlugins: (...args: unknown[]) => mockCollectionPlugins(...args),
|
||||
searchAdvanced: (...args: unknown[]) => mockSearchAdvanced(...args),
|
||||
},
|
||||
}))
|
||||
|
||||
// Factory for creating mock plugins
|
||||
const createMockPlugin = (overrides?: Partial<Plugin>): Plugin => ({
|
||||
type: 'plugin',
|
||||
org: 'test-org',
|
||||
name: 'test-plugin',
|
||||
plugin_id: 'plugin-1',
|
||||
version: '1.0.0',
|
||||
latest_version: '1.0.0',
|
||||
latest_package_identifier: 'test-org/test-plugin:1.0.0',
|
||||
icon: '/icon.png',
|
||||
verified: true,
|
||||
label: { 'en-US': 'Test Plugin' },
|
||||
brief: { 'en-US': 'Test plugin brief' },
|
||||
description: { 'en-US': 'Test plugin description' },
|
||||
introduction: 'Test plugin introduction',
|
||||
repository: 'https://github.com/test/plugin',
|
||||
category: PluginCategoryEnum.tool,
|
||||
install_count: 1000,
|
||||
endpoint: { settings: [] },
|
||||
tags: [{ name: 'search' }],
|
||||
badges: [],
|
||||
verification: { authorized_category: 'community' },
|
||||
from: 'marketplace',
|
||||
...overrides,
|
||||
})
|
||||
|
||||
describe('getPluginIconInMarketplace', () => {
|
||||
it('should return correct icon URL for regular plugin', async () => {
|
||||
const { getPluginIconInMarketplace } = await import('../utils')
|
||||
const plugin = createMockPlugin({ org: 'test-org', name: 'test-plugin', type: 'plugin' })
|
||||
const iconUrl = getPluginIconInMarketplace(plugin)
|
||||
expect(iconUrl).toBe('https://marketplace.dify.ai/api/v1/plugins/test-org/test-plugin/icon')
|
||||
})
|
||||
|
||||
it('should return correct icon URL for bundle', async () => {
|
||||
const { getPluginIconInMarketplace } = await import('../utils')
|
||||
const bundle = createMockPlugin({ org: 'test-org', name: 'test-bundle', type: 'bundle' })
|
||||
const iconUrl = getPluginIconInMarketplace(bundle)
|
||||
expect(iconUrl).toBe('https://marketplace.dify.ai/api/v1/bundles/test-org/test-bundle/icon')
|
||||
})
|
||||
})
|
||||
|
||||
describe('getFormattedPlugin', () => {
|
||||
it('should format plugin with icon URL', async () => {
|
||||
const { getFormattedPlugin } = await import('../utils')
|
||||
const rawPlugin = {
|
||||
type: 'plugin',
|
||||
org: 'test-org',
|
||||
name: 'test-plugin',
|
||||
tags: [{ name: 'search' }],
|
||||
} as unknown as Plugin
|
||||
|
||||
const formatted = getFormattedPlugin(rawPlugin)
|
||||
expect(formatted.icon).toBe('https://marketplace.dify.ai/api/v1/plugins/test-org/test-plugin/icon')
|
||||
})
|
||||
|
||||
it('should format bundle with additional properties', async () => {
|
||||
const { getFormattedPlugin } = await import('../utils')
|
||||
const rawBundle = {
|
||||
type: 'bundle',
|
||||
org: 'test-org',
|
||||
name: 'test-bundle',
|
||||
description: 'Bundle description',
|
||||
labels: { 'en-US': 'Test Bundle' },
|
||||
} as unknown as Plugin
|
||||
|
||||
const formatted = getFormattedPlugin(rawBundle)
|
||||
expect(formatted.icon).toBe('https://marketplace.dify.ai/api/v1/bundles/test-org/test-bundle/icon')
|
||||
expect(formatted.brief).toBe('Bundle description')
|
||||
expect(formatted.label).toEqual({ 'en-US': 'Test Bundle' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('getPluginLinkInMarketplace', () => {
|
||||
it('should return correct link for regular plugin', async () => {
|
||||
const { getPluginLinkInMarketplace } = await import('../utils')
|
||||
const plugin = createMockPlugin({ org: 'test-org', name: 'test-plugin', type: 'plugin' })
|
||||
const link = getPluginLinkInMarketplace(plugin)
|
||||
expect(link).toBe('https://marketplace.dify.ai/plugins/test-org/test-plugin')
|
||||
})
|
||||
|
||||
it('should return correct link for bundle', async () => {
|
||||
const { getPluginLinkInMarketplace } = await import('../utils')
|
||||
const bundle = createMockPlugin({ org: 'test-org', name: 'test-bundle', type: 'bundle' })
|
||||
const link = getPluginLinkInMarketplace(bundle)
|
||||
expect(link).toBe('https://marketplace.dify.ai/bundles/test-org/test-bundle')
|
||||
})
|
||||
})
|
||||
|
||||
describe('getPluginDetailLinkInMarketplace', () => {
|
||||
it('should return correct detail link for regular plugin', async () => {
|
||||
const { getPluginDetailLinkInMarketplace } = await import('../utils')
|
||||
const plugin = createMockPlugin({ org: 'test-org', name: 'test-plugin', type: 'plugin' })
|
||||
const link = getPluginDetailLinkInMarketplace(plugin)
|
||||
expect(link).toBe('/plugins/test-org/test-plugin')
|
||||
})
|
||||
|
||||
it('should return correct detail link for bundle', async () => {
|
||||
const { getPluginDetailLinkInMarketplace } = await import('../utils')
|
||||
const bundle = createMockPlugin({ org: 'test-org', name: 'test-bundle', type: 'bundle' })
|
||||
const link = getPluginDetailLinkInMarketplace(bundle)
|
||||
expect(link).toBe('/bundles/test-org/test-bundle')
|
||||
})
|
||||
})
|
||||
|
||||
describe('getMarketplaceListCondition', () => {
|
||||
it('should return category condition for tool', async () => {
|
||||
const { getMarketplaceListCondition } = await import('../utils')
|
||||
expect(getMarketplaceListCondition(PluginCategoryEnum.tool)).toBe('category=tool')
|
||||
})
|
||||
|
||||
it('should return category condition for model', async () => {
|
||||
const { getMarketplaceListCondition } = await import('../utils')
|
||||
expect(getMarketplaceListCondition(PluginCategoryEnum.model)).toBe('category=model')
|
||||
})
|
||||
|
||||
it('should return category condition for agent', async () => {
|
||||
const { getMarketplaceListCondition } = await import('../utils')
|
||||
expect(getMarketplaceListCondition(PluginCategoryEnum.agent)).toBe('category=agent-strategy')
|
||||
})
|
||||
|
||||
it('should return category condition for datasource', async () => {
|
||||
const { getMarketplaceListCondition } = await import('../utils')
|
||||
expect(getMarketplaceListCondition(PluginCategoryEnum.datasource)).toBe('category=datasource')
|
||||
})
|
||||
|
||||
it('should return category condition for trigger', async () => {
|
||||
const { getMarketplaceListCondition } = await import('../utils')
|
||||
expect(getMarketplaceListCondition(PluginCategoryEnum.trigger)).toBe('category=trigger')
|
||||
})
|
||||
|
||||
it('should return endpoint category for extension', async () => {
|
||||
const { getMarketplaceListCondition } = await import('../utils')
|
||||
expect(getMarketplaceListCondition(PluginCategoryEnum.extension)).toBe('category=endpoint')
|
||||
})
|
||||
|
||||
it('should return type condition for bundle', async () => {
|
||||
const { getMarketplaceListCondition } = await import('../utils')
|
||||
expect(getMarketplaceListCondition('bundle')).toBe('type=bundle')
|
||||
})
|
||||
|
||||
it('should return empty string for all', async () => {
|
||||
const { getMarketplaceListCondition } = await import('../utils')
|
||||
expect(getMarketplaceListCondition('all')).toBe('')
|
||||
})
|
||||
|
||||
it('should return empty string for unknown type', async () => {
|
||||
const { getMarketplaceListCondition } = await import('../utils')
|
||||
expect(getMarketplaceListCondition('unknown')).toBe('')
|
||||
})
|
||||
})
|
||||
|
||||
describe('getMarketplaceListFilterType', () => {
|
||||
it('should return undefined for all', async () => {
|
||||
const { getMarketplaceListFilterType } = await import('../utils')
|
||||
expect(getMarketplaceListFilterType(PLUGIN_TYPE_SEARCH_MAP.all)).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should return bundle for bundle', async () => {
|
||||
const { getMarketplaceListFilterType } = await import('../utils')
|
||||
expect(getMarketplaceListFilterType(PLUGIN_TYPE_SEARCH_MAP.bundle)).toBe('bundle')
|
||||
})
|
||||
|
||||
it('should return plugin for other categories', async () => {
|
||||
const { getMarketplaceListFilterType } = await import('../utils')
|
||||
expect(getMarketplaceListFilterType(PLUGIN_TYPE_SEARCH_MAP.tool)).toBe('plugin')
|
||||
expect(getMarketplaceListFilterType(PLUGIN_TYPE_SEARCH_MAP.model)).toBe('plugin')
|
||||
expect(getMarketplaceListFilterType(PLUGIN_TYPE_SEARCH_MAP.agent)).toBe('plugin')
|
||||
})
|
||||
})
|
||||
|
||||
describe('getMarketplacePluginsByCollectionId', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should fetch plugins by collection id successfully', async () => {
|
||||
const mockPlugins = [
|
||||
{ type: 'plugin', org: 'test', name: 'plugin1', tags: [] },
|
||||
{ type: 'plugin', org: 'test', name: 'plugin2', tags: [] },
|
||||
]
|
||||
mockCollectionPlugins.mockResolvedValueOnce({
|
||||
data: { plugins: mockPlugins },
|
||||
})
|
||||
|
||||
const { getMarketplacePluginsByCollectionId } = await import('../utils')
|
||||
const result = await getMarketplacePluginsByCollectionId('test-collection', {
|
||||
category: 'tool',
|
||||
exclude: ['excluded-plugin'],
|
||||
type: 'plugin',
|
||||
})
|
||||
|
||||
expect(mockCollectionPlugins).toHaveBeenCalled()
|
||||
expect(result).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('should handle fetch error and return empty array', async () => {
|
||||
mockCollectionPlugins.mockRejectedValueOnce(new Error('Network error'))
|
||||
|
||||
const { getMarketplacePluginsByCollectionId } = await import('../utils')
|
||||
const result = await getMarketplacePluginsByCollectionId('test-collection')
|
||||
|
||||
expect(result).toEqual([])
|
||||
})
|
||||
|
||||
it('should pass abort signal when provided', async () => {
|
||||
const mockPlugins = [{ type: 'plugin', org: 'test', name: 'plugin1' }]
|
||||
mockCollectionPlugins.mockResolvedValueOnce({
|
||||
data: { plugins: mockPlugins },
|
||||
})
|
||||
|
||||
const controller = new AbortController()
|
||||
const { getMarketplacePluginsByCollectionId } = await import('../utils')
|
||||
await getMarketplacePluginsByCollectionId('test-collection', {}, { signal: controller.signal })
|
||||
|
||||
expect(mockCollectionPlugins).toHaveBeenCalled()
|
||||
const call = mockCollectionPlugins.mock.calls[0]
|
||||
expect(call[1]).toMatchObject({ signal: controller.signal })
|
||||
})
|
||||
})
|
||||
|
||||
describe('getMarketplaceCollectionsAndPlugins', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should fetch collections and plugins successfully', async () => {
|
||||
const mockCollectionData = [
|
||||
{ name: 'collection1', label: {}, description: {}, rule: '', created_at: '', updated_at: '' },
|
||||
]
|
||||
const mockPluginData = [{ type: 'plugin', org: 'test', name: 'plugin1' }]
|
||||
|
||||
mockCollections.mockResolvedValueOnce({ data: { collections: mockCollectionData } })
|
||||
mockCollectionPlugins.mockResolvedValue({ data: { plugins: mockPluginData } })
|
||||
|
||||
const { getMarketplaceCollectionsAndPlugins } = await import('../utils')
|
||||
const result = await getMarketplaceCollectionsAndPlugins({
|
||||
condition: 'category=tool',
|
||||
type: 'plugin',
|
||||
})
|
||||
|
||||
expect(result.marketplaceCollections).toBeDefined()
|
||||
expect(result.marketplaceCollectionPluginsMap).toBeDefined()
|
||||
})
|
||||
|
||||
it('should handle fetch error and return empty data', async () => {
|
||||
mockCollections.mockRejectedValueOnce(new Error('Network error'))
|
||||
|
||||
const { getMarketplaceCollectionsAndPlugins } = await import('../utils')
|
||||
const result = await getMarketplaceCollectionsAndPlugins()
|
||||
|
||||
expect(result.marketplaceCollections).toEqual([])
|
||||
expect(result.marketplaceCollectionPluginsMap).toEqual({})
|
||||
})
|
||||
|
||||
it('should append condition and type to URL when provided', async () => {
|
||||
mockCollections.mockResolvedValueOnce({ data: { collections: [] } })
|
||||
|
||||
const { getMarketplaceCollectionsAndPlugins } = await import('../utils')
|
||||
await getMarketplaceCollectionsAndPlugins({
|
||||
condition: 'category=tool',
|
||||
type: 'bundle',
|
||||
})
|
||||
|
||||
expect(mockCollections).toHaveBeenCalled()
|
||||
const call = mockCollections.mock.calls[0]
|
||||
expect(call[0]).toMatchObject({ query: expect.objectContaining({ condition: 'category=tool', type: 'bundle' }) })
|
||||
})
|
||||
})
|
||||
|
||||
describe('getCollectionsParams', () => {
|
||||
it('should return empty object for all category', async () => {
|
||||
const { getCollectionsParams } = await import('../utils')
|
||||
expect(getCollectionsParams(PLUGIN_TYPE_SEARCH_MAP.all)).toEqual({})
|
||||
})
|
||||
|
||||
it('should return category, condition, and type for tool category', async () => {
|
||||
const { getCollectionsParams } = await import('../utils')
|
||||
const result = getCollectionsParams(PLUGIN_TYPE_SEARCH_MAP.tool)
|
||||
expect(result).toEqual({
|
||||
category: PluginCategoryEnum.tool,
|
||||
condition: 'category=tool',
|
||||
type: 'plugin',
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1,6 +1,6 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import Description from './index'
|
||||
import Description from '../index'
|
||||
|
||||
// ================================
|
||||
// Mock external dependencies
|
||||
@ -1,7 +1,7 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import Empty from './index'
|
||||
import Line from './line'
|
||||
import Empty from '../index'
|
||||
import Line from '../line'
|
||||
|
||||
// ================================
|
||||
// Mock external dependencies only
|
||||
597
web/app/components/plugins/marketplace/hooks.spec.tsx
Normal file
597
web/app/components/plugins/marketplace/hooks.spec.tsx
Normal file
@ -0,0 +1,597 @@
|
||||
import { render, renderHook } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
vi.mock('@/i18n-config/i18next-config', () => ({
|
||||
default: {
|
||||
getFixedT: () => (key: string) => key,
|
||||
},
|
||||
}))
|
||||
|
||||
const mockSetUrlFilters = vi.fn()
|
||||
vi.mock('@/hooks/use-query-params', () => ({
|
||||
useMarketplaceFilters: () => [
|
||||
{ q: '', tags: [], category: '' },
|
||||
mockSetUrlFilters,
|
||||
],
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-plugins', () => ({
|
||||
useInstalledPluginList: () => ({
|
||||
data: { plugins: [] },
|
||||
isSuccess: true,
|
||||
}),
|
||||
}))
|
||||
|
||||
const mockFetchNextPage = vi.fn()
|
||||
const mockHasNextPage = false
|
||||
let mockInfiniteQueryData: { pages: Array<{ plugins: unknown[], total: number, page: number, page_size: number }> } | undefined
|
||||
let capturedInfiniteQueryFn: ((ctx: { pageParam: number, signal: AbortSignal }) => Promise<unknown>) | null = null
|
||||
let capturedQueryFn: ((ctx: { signal: AbortSignal }) => Promise<unknown>) | null = null
|
||||
let capturedGetNextPageParam: ((lastPage: { page: number, page_size: number, total: number }) => number | undefined) | null = null
|
||||
|
||||
vi.mock('@tanstack/react-query', () => ({
|
||||
useQuery: vi.fn(({ queryFn, enabled }: { queryFn: (ctx: { signal: AbortSignal }) => Promise<unknown>, enabled: boolean }) => {
|
||||
capturedQueryFn = queryFn
|
||||
if (queryFn) {
|
||||
const controller = new AbortController()
|
||||
queryFn({ signal: controller.signal }).catch(() => {})
|
||||
}
|
||||
return {
|
||||
data: enabled ? { marketplaceCollections: [], marketplaceCollectionPluginsMap: {} } : undefined,
|
||||
isFetching: false,
|
||||
isPending: false,
|
||||
isSuccess: enabled,
|
||||
}
|
||||
}),
|
||||
useInfiniteQuery: vi.fn(({ queryFn, getNextPageParam }: {
|
||||
queryFn: (ctx: { pageParam: number, signal: AbortSignal }) => Promise<unknown>
|
||||
getNextPageParam: (lastPage: { page: number, page_size: number, total: number }) => number | undefined
|
||||
enabled: boolean
|
||||
}) => {
|
||||
capturedInfiniteQueryFn = queryFn
|
||||
capturedGetNextPageParam = getNextPageParam
|
||||
if (queryFn) {
|
||||
const controller = new AbortController()
|
||||
queryFn({ pageParam: 1, signal: controller.signal }).catch(() => {})
|
||||
}
|
||||
if (getNextPageParam) {
|
||||
getNextPageParam({ page: 1, page_size: 40, total: 100 })
|
||||
getNextPageParam({ page: 3, page_size: 40, total: 100 })
|
||||
}
|
||||
return {
|
||||
data: mockInfiniteQueryData,
|
||||
isPending: false,
|
||||
isFetching: false,
|
||||
isFetchingNextPage: false,
|
||||
hasNextPage: mockHasNextPage,
|
||||
fetchNextPage: mockFetchNextPage,
|
||||
}
|
||||
}),
|
||||
useQueryClient: vi.fn(() => ({
|
||||
removeQueries: vi.fn(),
|
||||
})),
|
||||
}))
|
||||
|
||||
vi.mock('ahooks', () => ({
|
||||
useDebounceFn: (fn: (...args: unknown[]) => void) => ({
|
||||
run: fn,
|
||||
cancel: vi.fn(),
|
||||
}),
|
||||
}))
|
||||
|
||||
let mockPostMarketplaceShouldFail = false
|
||||
const mockPostMarketplaceResponse = {
|
||||
data: {
|
||||
plugins: [
|
||||
{ type: 'plugin', org: 'test', name: 'plugin1', tags: [] },
|
||||
{ type: 'plugin', org: 'test', name: 'plugin2', tags: [] },
|
||||
],
|
||||
bundles: [] as Array<{ type: string, org: string, name: string, tags: unknown[] }>,
|
||||
total: 2,
|
||||
},
|
||||
}
|
||||
|
||||
vi.mock('@/service/base', () => ({
|
||||
postMarketplace: vi.fn(() => {
|
||||
if (mockPostMarketplaceShouldFail)
|
||||
return Promise.reject(new Error('Mock API error'))
|
||||
return Promise.resolve(mockPostMarketplaceResponse)
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/config', () => ({
|
||||
API_PREFIX: '/api',
|
||||
APP_VERSION: '1.0.0',
|
||||
IS_MARKETPLACE: false,
|
||||
MARKETPLACE_API_PREFIX: 'https://marketplace.dify.ai/api/v1',
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/var', () => ({
|
||||
getMarketplaceUrl: (path: string) => `https://marketplace.dify.ai${path}`,
|
||||
}))
|
||||
|
||||
vi.mock('@/service/client', () => ({
|
||||
marketplaceClient: {
|
||||
collections: vi.fn(async () => ({
|
||||
data: {
|
||||
collections: [
|
||||
{
|
||||
name: 'collection-1',
|
||||
label: { 'en-US': 'Collection 1' },
|
||||
description: { 'en-US': 'Desc' },
|
||||
rule: '',
|
||||
created_at: '2024-01-01',
|
||||
updated_at: '2024-01-01',
|
||||
searchable: true,
|
||||
search_params: { query: '', sort_by: 'install_count', sort_order: 'DESC' },
|
||||
},
|
||||
],
|
||||
},
|
||||
})),
|
||||
collectionPlugins: vi.fn(async () => ({
|
||||
data: {
|
||||
plugins: [
|
||||
{ type: 'plugin', org: 'test', name: 'plugin1', tags: [] },
|
||||
],
|
||||
},
|
||||
})),
|
||||
searchAdvanced: vi.fn(async () => ({
|
||||
data: {
|
||||
plugins: [
|
||||
{ type: 'plugin', org: 'test', name: 'plugin1', tags: [] },
|
||||
],
|
||||
total: 1,
|
||||
},
|
||||
})),
|
||||
},
|
||||
}))
|
||||
|
||||
// ================================
|
||||
// useMarketplaceCollectionsAndPlugins Tests
|
||||
// ================================
|
||||
describe('useMarketplaceCollectionsAndPlugins', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should return initial state correctly', async () => {
|
||||
const { useMarketplaceCollectionsAndPlugins } = await import('./hooks')
|
||||
const { result } = renderHook(() => useMarketplaceCollectionsAndPlugins())
|
||||
|
||||
expect(result.current.isLoading).toBe(false)
|
||||
expect(result.current.isSuccess).toBe(false)
|
||||
expect(result.current.queryMarketplaceCollectionsAndPlugins).toBeDefined()
|
||||
expect(result.current.setMarketplaceCollections).toBeDefined()
|
||||
expect(result.current.setMarketplaceCollectionPluginsMap).toBeDefined()
|
||||
})
|
||||
|
||||
it('should provide queryMarketplaceCollectionsAndPlugins function', async () => {
|
||||
const { useMarketplaceCollectionsAndPlugins } = await import('./hooks')
|
||||
const { result } = renderHook(() => useMarketplaceCollectionsAndPlugins())
|
||||
expect(typeof result.current.queryMarketplaceCollectionsAndPlugins).toBe('function')
|
||||
})
|
||||
|
||||
it('should provide setMarketplaceCollections function', async () => {
|
||||
const { useMarketplaceCollectionsAndPlugins } = await import('./hooks')
|
||||
const { result } = renderHook(() => useMarketplaceCollectionsAndPlugins())
|
||||
expect(typeof result.current.setMarketplaceCollections).toBe('function')
|
||||
})
|
||||
|
||||
it('should provide setMarketplaceCollectionPluginsMap function', async () => {
|
||||
const { useMarketplaceCollectionsAndPlugins } = await import('./hooks')
|
||||
const { result } = renderHook(() => useMarketplaceCollectionsAndPlugins())
|
||||
expect(typeof result.current.setMarketplaceCollectionPluginsMap).toBe('function')
|
||||
})
|
||||
|
||||
it('should return marketplaceCollections from data or override', async () => {
|
||||
const { useMarketplaceCollectionsAndPlugins } = await import('./hooks')
|
||||
const { result } = renderHook(() => useMarketplaceCollectionsAndPlugins())
|
||||
expect(result.current.marketplaceCollections).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should return marketplaceCollectionPluginsMap from data or override', async () => {
|
||||
const { useMarketplaceCollectionsAndPlugins } = await import('./hooks')
|
||||
const { result } = renderHook(() => useMarketplaceCollectionsAndPlugins())
|
||||
expect(result.current.marketplaceCollectionPluginsMap).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// useMarketplacePluginsByCollectionId Tests
|
||||
// ================================
|
||||
describe('useMarketplacePluginsByCollectionId', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should return initial state when collectionId is undefined', async () => {
|
||||
const { useMarketplacePluginsByCollectionId } = await import('./hooks')
|
||||
const { result } = renderHook(() => useMarketplacePluginsByCollectionId(undefined))
|
||||
expect(result.current.plugins).toEqual([])
|
||||
expect(result.current.isLoading).toBe(false)
|
||||
expect(result.current.isSuccess).toBe(false)
|
||||
})
|
||||
|
||||
it('should return isLoading false when collectionId is provided and query completes', async () => {
|
||||
const { useMarketplacePluginsByCollectionId } = await import('./hooks')
|
||||
const { result } = renderHook(() => useMarketplacePluginsByCollectionId('test-collection'))
|
||||
expect(result.current.isLoading).toBe(false)
|
||||
})
|
||||
|
||||
it('should accept query parameter', async () => {
|
||||
const { useMarketplacePluginsByCollectionId } = await import('./hooks')
|
||||
const { result } = renderHook(() =>
|
||||
useMarketplacePluginsByCollectionId('test-collection', {
|
||||
category: 'tool',
|
||||
type: 'plugin',
|
||||
}))
|
||||
expect(result.current.plugins).toBeDefined()
|
||||
})
|
||||
|
||||
it('should return plugins property from hook', async () => {
|
||||
const { useMarketplacePluginsByCollectionId } = await import('./hooks')
|
||||
const { result } = renderHook(() => useMarketplacePluginsByCollectionId('collection-1'))
|
||||
expect(result.current.plugins).toBeDefined()
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// useMarketplacePlugins Tests
|
||||
// ================================
|
||||
describe('useMarketplacePlugins', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockInfiniteQueryData = undefined
|
||||
})
|
||||
|
||||
it('should return initial state correctly', async () => {
|
||||
const { useMarketplacePlugins } = await import('./hooks')
|
||||
const { result } = renderHook(() => useMarketplacePlugins())
|
||||
expect(result.current.plugins).toBeUndefined()
|
||||
expect(result.current.total).toBeUndefined()
|
||||
expect(result.current.isLoading).toBe(false)
|
||||
expect(result.current.isFetchingNextPage).toBe(false)
|
||||
expect(result.current.hasNextPage).toBe(false)
|
||||
expect(result.current.page).toBe(0)
|
||||
})
|
||||
|
||||
it('should provide queryPlugins function', async () => {
|
||||
const { useMarketplacePlugins } = await import('./hooks')
|
||||
const { result } = renderHook(() => useMarketplacePlugins())
|
||||
expect(typeof result.current.queryPlugins).toBe('function')
|
||||
})
|
||||
|
||||
it('should provide queryPluginsWithDebounced function', async () => {
|
||||
const { useMarketplacePlugins } = await import('./hooks')
|
||||
const { result } = renderHook(() => useMarketplacePlugins())
|
||||
expect(typeof result.current.queryPluginsWithDebounced).toBe('function')
|
||||
})
|
||||
|
||||
it('should provide cancelQueryPluginsWithDebounced function', async () => {
|
||||
const { useMarketplacePlugins } = await import('./hooks')
|
||||
const { result } = renderHook(() => useMarketplacePlugins())
|
||||
expect(typeof result.current.cancelQueryPluginsWithDebounced).toBe('function')
|
||||
})
|
||||
|
||||
it('should provide resetPlugins function', async () => {
|
||||
const { useMarketplacePlugins } = await import('./hooks')
|
||||
const { result } = renderHook(() => useMarketplacePlugins())
|
||||
expect(typeof result.current.resetPlugins).toBe('function')
|
||||
})
|
||||
|
||||
it('should provide fetchNextPage function', async () => {
|
||||
const { useMarketplacePlugins } = await import('./hooks')
|
||||
const { result } = renderHook(() => useMarketplacePlugins())
|
||||
expect(typeof result.current.fetchNextPage).toBe('function')
|
||||
})
|
||||
|
||||
it('should handle queryPlugins call without errors', async () => {
|
||||
const { useMarketplacePlugins } = await import('./hooks')
|
||||
const { result } = renderHook(() => useMarketplacePlugins())
|
||||
expect(() => {
|
||||
result.current.queryPlugins({
|
||||
query: 'test',
|
||||
sort_by: 'install_count',
|
||||
sort_order: 'DESC',
|
||||
category: 'tool',
|
||||
page_size: 20,
|
||||
})
|
||||
}).not.toThrow()
|
||||
})
|
||||
|
||||
it('should handle queryPlugins with bundle type', async () => {
|
||||
const { useMarketplacePlugins } = await import('./hooks')
|
||||
const { result } = renderHook(() => useMarketplacePlugins())
|
||||
expect(() => {
|
||||
result.current.queryPlugins({
|
||||
query: 'test',
|
||||
type: 'bundle',
|
||||
page_size: 40,
|
||||
})
|
||||
}).not.toThrow()
|
||||
})
|
||||
|
||||
it('should handle resetPlugins call', async () => {
|
||||
const { useMarketplacePlugins } = await import('./hooks')
|
||||
const { result } = renderHook(() => useMarketplacePlugins())
|
||||
expect(() => {
|
||||
result.current.resetPlugins()
|
||||
}).not.toThrow()
|
||||
})
|
||||
|
||||
it('should handle queryPluginsWithDebounced call', async () => {
|
||||
const { useMarketplacePlugins } = await import('./hooks')
|
||||
const { result } = renderHook(() => useMarketplacePlugins())
|
||||
expect(() => {
|
||||
result.current.queryPluginsWithDebounced({
|
||||
query: 'debounced search',
|
||||
category: 'all',
|
||||
})
|
||||
}).not.toThrow()
|
||||
})
|
||||
|
||||
it('should handle cancelQueryPluginsWithDebounced call', async () => {
|
||||
const { useMarketplacePlugins } = await import('./hooks')
|
||||
const { result } = renderHook(() => useMarketplacePlugins())
|
||||
expect(() => {
|
||||
result.current.cancelQueryPluginsWithDebounced()
|
||||
}).not.toThrow()
|
||||
})
|
||||
|
||||
it('should return correct page number', async () => {
|
||||
const { useMarketplacePlugins } = await import('./hooks')
|
||||
const { result } = renderHook(() => useMarketplacePlugins())
|
||||
expect(result.current.page).toBe(0)
|
||||
})
|
||||
|
||||
it('should handle queryPlugins with tags', async () => {
|
||||
const { useMarketplacePlugins } = await import('./hooks')
|
||||
const { result } = renderHook(() => useMarketplacePlugins())
|
||||
expect(() => {
|
||||
result.current.queryPlugins({
|
||||
query: 'test',
|
||||
tags: ['search', 'image'],
|
||||
exclude: ['excluded-plugin'],
|
||||
})
|
||||
}).not.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Hooks queryFn Coverage Tests
|
||||
// ================================
|
||||
describe('Hooks queryFn Coverage', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockInfiniteQueryData = undefined
|
||||
mockPostMarketplaceShouldFail = false
|
||||
capturedInfiniteQueryFn = null
|
||||
capturedQueryFn = null
|
||||
})
|
||||
|
||||
it('should cover queryFn with pages data', async () => {
|
||||
mockInfiniteQueryData = {
|
||||
pages: [
|
||||
{ plugins: [{ name: 'plugin1' }], total: 10, page: 1, page_size: 40 },
|
||||
],
|
||||
}
|
||||
|
||||
const { useMarketplacePlugins } = await import('./hooks')
|
||||
const { result } = renderHook(() => useMarketplacePlugins())
|
||||
|
||||
result.current.queryPlugins({
|
||||
query: 'test',
|
||||
category: 'tool',
|
||||
})
|
||||
|
||||
expect(result.current).toBeDefined()
|
||||
})
|
||||
|
||||
it('should expose page and total from infinite query data', async () => {
|
||||
mockInfiniteQueryData = {
|
||||
pages: [
|
||||
{ plugins: [{ name: 'plugin1' }, { name: 'plugin2' }], total: 20, page: 1, page_size: 40 },
|
||||
{ plugins: [{ name: 'plugin3' }], total: 20, page: 2, page_size: 40 },
|
||||
],
|
||||
}
|
||||
|
||||
const { useMarketplacePlugins } = await import('./hooks')
|
||||
const { result } = renderHook(() => useMarketplacePlugins())
|
||||
|
||||
result.current.queryPlugins({ query: 'search' })
|
||||
expect(result.current.page).toBe(2)
|
||||
})
|
||||
|
||||
it('should return undefined total when no query is set', async () => {
|
||||
const { useMarketplacePlugins } = await import('./hooks')
|
||||
const { result } = renderHook(() => useMarketplacePlugins())
|
||||
expect(result.current.total).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should directly test queryFn execution', async () => {
|
||||
const { useMarketplacePlugins } = await import('./hooks')
|
||||
const { result } = renderHook(() => useMarketplacePlugins())
|
||||
|
||||
result.current.queryPlugins({
|
||||
query: 'direct test',
|
||||
category: 'tool',
|
||||
sort_by: 'install_count',
|
||||
sort_order: 'DESC',
|
||||
page_size: 40,
|
||||
})
|
||||
|
||||
if (capturedInfiniteQueryFn) {
|
||||
const controller = new AbortController()
|
||||
const response = await capturedInfiniteQueryFn({ pageParam: 1, signal: controller.signal })
|
||||
expect(response).toBeDefined()
|
||||
}
|
||||
})
|
||||
|
||||
it('should test queryFn with bundle type', async () => {
|
||||
const { useMarketplacePlugins } = await import('./hooks')
|
||||
const { result } = renderHook(() => useMarketplacePlugins())
|
||||
|
||||
result.current.queryPlugins({
|
||||
type: 'bundle',
|
||||
query: 'bundle test',
|
||||
})
|
||||
|
||||
if (capturedInfiniteQueryFn) {
|
||||
const controller = new AbortController()
|
||||
const response = await capturedInfiniteQueryFn({ pageParam: 2, signal: controller.signal })
|
||||
expect(response).toBeDefined()
|
||||
}
|
||||
})
|
||||
|
||||
it('should test queryFn error handling', async () => {
|
||||
mockPostMarketplaceShouldFail = true
|
||||
|
||||
const { useMarketplacePlugins } = await import('./hooks')
|
||||
const { result } = renderHook(() => useMarketplacePlugins())
|
||||
|
||||
result.current.queryPlugins({ query: 'test that will fail' })
|
||||
|
||||
if (capturedInfiniteQueryFn) {
|
||||
const controller = new AbortController()
|
||||
const response = await capturedInfiniteQueryFn({ pageParam: 1, signal: controller.signal })
|
||||
expect(response).toBeDefined()
|
||||
expect(response).toHaveProperty('plugins')
|
||||
}
|
||||
|
||||
mockPostMarketplaceShouldFail = false
|
||||
})
|
||||
|
||||
it('should test useMarketplaceCollectionsAndPlugins queryFn', async () => {
|
||||
const { useMarketplaceCollectionsAndPlugins } = await import('./hooks')
|
||||
const { result } = renderHook(() => useMarketplaceCollectionsAndPlugins())
|
||||
|
||||
result.current.queryMarketplaceCollectionsAndPlugins({
|
||||
condition: 'category=tool',
|
||||
})
|
||||
|
||||
if (capturedQueryFn) {
|
||||
const controller = new AbortController()
|
||||
const response = await capturedQueryFn({ signal: controller.signal })
|
||||
expect(response).toBeDefined()
|
||||
}
|
||||
})
|
||||
|
||||
it('should test getNextPageParam directly', async () => {
|
||||
const { useMarketplacePlugins } = await import('./hooks')
|
||||
renderHook(() => useMarketplacePlugins())
|
||||
|
||||
if (capturedGetNextPageParam) {
|
||||
const nextPage = capturedGetNextPageParam({ page: 1, page_size: 40, total: 100 })
|
||||
expect(nextPage).toBe(2)
|
||||
|
||||
const noMorePages = capturedGetNextPageParam({ page: 3, page_size: 40, total: 100 })
|
||||
expect(noMorePages).toBeUndefined()
|
||||
|
||||
const atBoundary = capturedGetNextPageParam({ page: 2, page_size: 50, total: 100 })
|
||||
expect(atBoundary).toBeUndefined()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// useMarketplaceContainerScroll Tests
|
||||
// ================================
|
||||
describe('useMarketplaceContainerScroll', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should attach scroll event listener to container', async () => {
|
||||
const mockCallback = vi.fn()
|
||||
const mockContainer = document.createElement('div')
|
||||
mockContainer.id = 'marketplace-container'
|
||||
document.body.appendChild(mockContainer)
|
||||
|
||||
const addEventListenerSpy = vi.spyOn(mockContainer, 'addEventListener')
|
||||
const { useMarketplaceContainerScroll } = await import('./hooks')
|
||||
|
||||
const TestComponent = () => {
|
||||
useMarketplaceContainerScroll(mockCallback)
|
||||
return null
|
||||
}
|
||||
|
||||
render(<TestComponent />)
|
||||
expect(addEventListenerSpy).toHaveBeenCalledWith('scroll', expect.any(Function))
|
||||
document.body.removeChild(mockContainer)
|
||||
})
|
||||
|
||||
it('should call callback when scrolled to bottom', async () => {
|
||||
const mockCallback = vi.fn()
|
||||
const mockContainer = document.createElement('div')
|
||||
mockContainer.id = 'scroll-test-container-hooks'
|
||||
document.body.appendChild(mockContainer)
|
||||
|
||||
Object.defineProperty(mockContainer, 'scrollTop', { value: 900, writable: true })
|
||||
Object.defineProperty(mockContainer, 'scrollHeight', { value: 1000, writable: true })
|
||||
Object.defineProperty(mockContainer, 'clientHeight', { value: 100, writable: true })
|
||||
|
||||
const { useMarketplaceContainerScroll } = await import('./hooks')
|
||||
|
||||
const TestComponent = () => {
|
||||
useMarketplaceContainerScroll(mockCallback, 'scroll-test-container-hooks')
|
||||
return null
|
||||
}
|
||||
|
||||
render(<TestComponent />)
|
||||
|
||||
const scrollEvent = new Event('scroll')
|
||||
Object.defineProperty(scrollEvent, 'target', { value: mockContainer })
|
||||
mockContainer.dispatchEvent(scrollEvent)
|
||||
|
||||
expect(mockCallback).toHaveBeenCalled()
|
||||
document.body.removeChild(mockContainer)
|
||||
})
|
||||
|
||||
it('should not call callback when scrollTop is 0', async () => {
|
||||
const mockCallback = vi.fn()
|
||||
const mockContainer = document.createElement('div')
|
||||
mockContainer.id = 'scroll-test-container-hooks-2'
|
||||
document.body.appendChild(mockContainer)
|
||||
|
||||
Object.defineProperty(mockContainer, 'scrollTop', { value: 0, writable: true })
|
||||
Object.defineProperty(mockContainer, 'scrollHeight', { value: 1000, writable: true })
|
||||
Object.defineProperty(mockContainer, 'clientHeight', { value: 100, writable: true })
|
||||
|
||||
const { useMarketplaceContainerScroll } = await import('./hooks')
|
||||
|
||||
const TestComponent = () => {
|
||||
useMarketplaceContainerScroll(mockCallback, 'scroll-test-container-hooks-2')
|
||||
return null
|
||||
}
|
||||
|
||||
render(<TestComponent />)
|
||||
|
||||
const scrollEvent = new Event('scroll')
|
||||
Object.defineProperty(scrollEvent, 'target', { value: mockContainer })
|
||||
mockContainer.dispatchEvent(scrollEvent)
|
||||
|
||||
expect(mockCallback).not.toHaveBeenCalled()
|
||||
document.body.removeChild(mockContainer)
|
||||
})
|
||||
|
||||
it('should remove event listener on unmount', async () => {
|
||||
const mockCallback = vi.fn()
|
||||
const mockContainer = document.createElement('div')
|
||||
mockContainer.id = 'scroll-unmount-container-hooks'
|
||||
document.body.appendChild(mockContainer)
|
||||
|
||||
const removeEventListenerSpy = vi.spyOn(mockContainer, 'removeEventListener')
|
||||
const { useMarketplaceContainerScroll } = await import('./hooks')
|
||||
|
||||
const TestComponent = () => {
|
||||
useMarketplaceContainerScroll(mockCallback, 'scroll-unmount-container-hooks')
|
||||
return null
|
||||
}
|
||||
|
||||
const { unmount } = render(<TestComponent />)
|
||||
unmount()
|
||||
|
||||
expect(removeEventListenerSpy).toHaveBeenCalledWith('scroll', expect.any(Function))
|
||||
document.body.removeChild(mockContainer)
|
||||
})
|
||||
})
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,17 +1,16 @@
|
||||
import type { MarketplaceCollection, SearchParamsFromCollection } from '../types'
|
||||
import type { MarketplaceCollection, SearchParamsFromCollection } from '../../types'
|
||||
import type { Plugin } from '@/app/components/plugins/types'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { PluginCategoryEnum } from '@/app/components/plugins/types'
|
||||
import List from './index'
|
||||
import ListWithCollection from './list-with-collection'
|
||||
import ListWrapper from './list-wrapper'
|
||||
import List from '../index'
|
||||
import ListWithCollection from '../list-with-collection'
|
||||
import ListWrapper from '../list-wrapper'
|
||||
|
||||
// ================================
|
||||
// Mock External Dependencies Only
|
||||
// ================================
|
||||
|
||||
// Mock i18n translation hook
|
||||
vi.mock('#i18n', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string, options?: { ns?: string, num?: number }) => {
|
||||
@ -30,7 +29,6 @@ vi.mock('#i18n', () => ({
|
||||
useLocale: () => 'en-US',
|
||||
}))
|
||||
|
||||
// Mock marketplace state hooks with controllable values
|
||||
const { mockMarketplaceData, mockMoreClick } = vi.hoisted(() => {
|
||||
return {
|
||||
mockMarketplaceData: {
|
||||
@ -45,27 +43,18 @@ const { mockMarketplaceData, mockMoreClick } = vi.hoisted(() => {
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('../state', () => ({
|
||||
vi.mock('../../state', () => ({
|
||||
useMarketplaceData: () => mockMarketplaceData,
|
||||
}))
|
||||
|
||||
vi.mock('../atoms', () => ({
|
||||
vi.mock('../../atoms', () => ({
|
||||
useMarketplaceMoreClick: () => mockMoreClick,
|
||||
}))
|
||||
|
||||
// Mock useLocale context
|
||||
vi.mock('@/context/i18n', () => ({
|
||||
useLocale: () => 'en-US',
|
||||
}))
|
||||
|
||||
// Mock next-themes
|
||||
vi.mock('next-themes', () => ({
|
||||
useTheme: () => ({
|
||||
theme: 'light',
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock useTags hook
|
||||
const mockTags = [
|
||||
{ name: 'search', label: 'Search' },
|
||||
{ name: 'image', label: 'Image' },
|
||||
@ -85,7 +74,6 @@ vi.mock('@/app/components/plugins/hooks', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock ahooks useBoolean with controllable state
|
||||
let mockUseBooleanValue = false
|
||||
const mockSetTrue = vi.fn(() => {
|
||||
mockUseBooleanValue = true
|
||||
@ -107,20 +95,17 @@ vi.mock('ahooks', () => ({
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock i18n-config/language
|
||||
vi.mock('@/i18n-config/language', () => ({
|
||||
getLanguage: (locale: string) => locale || 'en-US',
|
||||
}))
|
||||
|
||||
// Mock marketplace utils
|
||||
vi.mock('../utils', () => ({
|
||||
vi.mock('../../utils', () => ({
|
||||
getPluginLinkInMarketplace: (plugin: Plugin, _params?: Record<string, string | undefined>) =>
|
||||
`/plugins/${plugin.org}/${plugin.name}`,
|
||||
getPluginDetailLinkInMarketplace: (plugin: Plugin) =>
|
||||
`/plugins/${plugin.org}/${plugin.name}`,
|
||||
}))
|
||||
|
||||
// Mock Card component
|
||||
vi.mock('@/app/components/plugins/card', () => ({
|
||||
default: ({ payload, footer }: { payload: Plugin, footer?: React.ReactNode }) => (
|
||||
<div data-testid={`card-${payload.name}`}>
|
||||
@ -131,7 +116,6 @@ vi.mock('@/app/components/plugins/card', () => ({
|
||||
),
|
||||
}))
|
||||
|
||||
// Mock CardMoreInfo component
|
||||
vi.mock('@/app/components/plugins/card/card-more-info', () => ({
|
||||
default: ({ downloadCount, tags }: { downloadCount: number, tags: string[] }) => (
|
||||
<div data-testid="card-more-info">
|
||||
@ -141,7 +125,6 @@ vi.mock('@/app/components/plugins/card/card-more-info', () => ({
|
||||
),
|
||||
}))
|
||||
|
||||
// Mock InstallFromMarketplace component
|
||||
vi.mock('@/app/components/plugins/install-plugin/install-from-marketplace', () => ({
|
||||
default: ({ onClose }: { onClose: () => void }) => (
|
||||
<div data-testid="install-from-marketplace">
|
||||
@ -150,15 +133,13 @@ vi.mock('@/app/components/plugins/install-plugin/install-from-marketplace', () =
|
||||
),
|
||||
}))
|
||||
|
||||
// Mock SortDropdown component
|
||||
vi.mock('../sort-dropdown', () => ({
|
||||
vi.mock('../../sort-dropdown', () => ({
|
||||
default: () => (
|
||||
<div data-testid="sort-dropdown">Sort</div>
|
||||
),
|
||||
}))
|
||||
|
||||
// Mock Empty component
|
||||
vi.mock('../empty', () => ({
|
||||
vi.mock('../../empty', () => ({
|
||||
default: ({ className }: { className?: string }) => (
|
||||
<div data-testid="empty-component" className={className}>
|
||||
No plugins found
|
||||
@ -166,7 +147,6 @@ vi.mock('../empty', () => ({
|
||||
),
|
||||
}))
|
||||
|
||||
// Mock Loading component
|
||||
vi.mock('@/app/components/base/loading', () => ({
|
||||
default: () => <div data-testid="loading-component">Loading...</div>,
|
||||
}))
|
||||
@ -1,10 +1,10 @@
|
||||
import type { Tag } from '@/app/components/plugins/hooks'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import SearchBox from './index'
|
||||
import SearchBoxWrapper from './search-box-wrapper'
|
||||
import MarketplaceTrigger from './trigger/marketplace'
|
||||
import ToolSelectorTrigger from './trigger/tool-selector'
|
||||
import SearchBox from '../index'
|
||||
import SearchBoxWrapper from '../search-box-wrapper'
|
||||
import MarketplaceTrigger from '../trigger/marketplace'
|
||||
import ToolSelectorTrigger from '../trigger/tool-selector'
|
||||
|
||||
// ================================
|
||||
// Mock external dependencies only
|
||||
@ -36,7 +36,7 @@ const { mockSearchPluginText, mockHandleSearchPluginTextChange, mockFilterPlugin
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('../atoms', () => ({
|
||||
vi.mock('../../atoms', () => ({
|
||||
useSearchPluginText: () => [mockSearchPluginText, mockHandleSearchPluginTextChange],
|
||||
useFilterPluginTags: () => [mockFilterPluginTags, mockHandleFilterPluginTagsChange],
|
||||
}))
|
||||
@ -1,7 +1,7 @@
|
||||
import { fireEvent, render, screen, within } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import SortDropdown from './index'
|
||||
import SortDropdown from '../index'
|
||||
|
||||
// ================================
|
||||
// Mock external dependencies only
|
||||
@ -31,7 +31,7 @@ vi.mock('#i18n', () => ({
|
||||
let mockSort: { sortBy: string, sortOrder: string } = { sortBy: 'install_count', sortOrder: 'DESC' }
|
||||
const mockHandleSortChange = vi.fn()
|
||||
|
||||
vi.mock('../atoms', () => ({
|
||||
vi.mock('../../atoms', () => ({
|
||||
useMarketplaceSort: () => [mockSort, mockHandleSortChange],
|
||||
}))
|
||||
|
||||
@ -39,7 +39,7 @@ vi.mock('../atoms', () => ({
|
||||
let mockPortalOpenState = false
|
||||
|
||||
vi.mock('@/app/components/base/portal-to-follow-elem', () => ({
|
||||
PortalToFollowElem: ({ children, open, onOpenChange }: {
|
||||
PortalToFollowElem: ({ children, open, onOpenChange: _onOpenChange }: {
|
||||
children: React.ReactNode
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
@ -0,0 +1,45 @@
|
||||
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import AuthorizedInDataSourceNode from '../authorized-in-data-source-node'
|
||||
|
||||
vi.mock('@/app/components/header/indicator', () => ({
|
||||
default: ({ color }: { color: string }) => <span data-testid="indicator" data-color={color} />,
|
||||
}))
|
||||
|
||||
describe('AuthorizedInDataSourceNode', () => {
|
||||
const mockOnJump = vi.fn()
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
})
|
||||
|
||||
it('renders with green indicator', () => {
|
||||
render(<AuthorizedInDataSourceNode authorizationsNum={1} onJumpToDataSourcePage={mockOnJump} />)
|
||||
expect(screen.getByTestId('indicator')).toHaveAttribute('data-color', 'green')
|
||||
})
|
||||
|
||||
it('renders singular text for 1 authorization', () => {
|
||||
render(<AuthorizedInDataSourceNode authorizationsNum={1} onJumpToDataSourcePage={mockOnJump} />)
|
||||
expect(screen.getByText('plugin.auth.authorization')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders plural text for multiple authorizations', () => {
|
||||
render(<AuthorizedInDataSourceNode authorizationsNum={3} onJumpToDataSourcePage={mockOnJump} />)
|
||||
expect(screen.getByText('plugin.auth.authorizations')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('calls onJumpToDataSourcePage when button is clicked', () => {
|
||||
render(<AuthorizedInDataSourceNode authorizationsNum={1} onJumpToDataSourcePage={mockOnJump} />)
|
||||
fireEvent.click(screen.getByRole('button'))
|
||||
expect(mockOnJump).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('renders settings button', () => {
|
||||
render(<AuthorizedInDataSourceNode authorizationsNum={1} onJumpToDataSourcePage={mockOnJump} />)
|
||||
expect(screen.getByRole('button')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,210 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import type { Credential, PluginPayload } from '../types'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { AuthCategory, CredentialTypeEnum } from '../types'
|
||||
|
||||
// ==================== Mock Setup ====================
|
||||
|
||||
const mockGetPluginCredentialInfo = vi.fn()
|
||||
const mockGetPluginOAuthClientSchema = vi.fn()
|
||||
|
||||
vi.mock('@/service/use-plugins-auth', () => ({
|
||||
useGetPluginCredentialInfo: (url: string) => ({
|
||||
data: url ? mockGetPluginCredentialInfo() : undefined,
|
||||
isLoading: false,
|
||||
}),
|
||||
useDeletePluginCredential: () => ({ mutateAsync: vi.fn() }),
|
||||
useSetPluginDefaultCredential: () => ({ mutateAsync: vi.fn() }),
|
||||
useUpdatePluginCredential: () => ({ mutateAsync: vi.fn() }),
|
||||
useInvalidPluginCredentialInfo: () => vi.fn(),
|
||||
useGetPluginOAuthUrl: () => ({ mutateAsync: vi.fn() }),
|
||||
useGetPluginOAuthClientSchema: () => ({
|
||||
data: mockGetPluginOAuthClientSchema(),
|
||||
isLoading: false,
|
||||
}),
|
||||
useSetPluginOAuthCustomClient: () => ({ mutateAsync: vi.fn() }),
|
||||
useDeletePluginOAuthCustomClient: () => ({ mutateAsync: vi.fn() }),
|
||||
useInvalidPluginOAuthClientSchema: () => vi.fn(),
|
||||
useAddPluginCredential: () => ({ mutateAsync: vi.fn() }),
|
||||
useGetPluginCredentialSchema: () => ({ data: undefined, isLoading: false }),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-tools', () => ({
|
||||
useInvalidToolsByType: () => vi.fn(),
|
||||
}))
|
||||
|
||||
const mockIsCurrentWorkspaceManager = vi.fn()
|
||||
vi.mock('@/context/app-context', () => ({
|
||||
useAppContext: () => ({
|
||||
isCurrentWorkspaceManager: mockIsCurrentWorkspaceManager(),
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/toast', () => ({
|
||||
useToastContext: () => ({ notify: vi.fn() }),
|
||||
}))
|
||||
|
||||
vi.mock('@/hooks/use-oauth', () => ({
|
||||
openOAuthPopup: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-triggers', () => ({
|
||||
useTriggerPluginDynamicOptions: () => ({ data: { options: [] }, isLoading: false }),
|
||||
useTriggerPluginDynamicOptionsInfo: () => ({ data: null, isLoading: false }),
|
||||
useInvalidTriggerDynamicOptions: () => vi.fn(),
|
||||
}))
|
||||
|
||||
// ==================== Test Utilities ====================
|
||||
|
||||
const createTestQueryClient = () =>
|
||||
new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false, gcTime: 0 },
|
||||
},
|
||||
})
|
||||
|
||||
const createWrapper = () => {
|
||||
const testQueryClient = createTestQueryClient()
|
||||
return ({ children }: { children: ReactNode }) => (
|
||||
<QueryClientProvider client={testQueryClient}>
|
||||
{children}
|
||||
</QueryClientProvider>
|
||||
)
|
||||
}
|
||||
|
||||
const createPluginPayload = (overrides: Partial<PluginPayload> = {}): PluginPayload => ({
|
||||
category: AuthCategory.tool,
|
||||
provider: 'test-provider',
|
||||
...overrides,
|
||||
})
|
||||
|
||||
const createCredential = (overrides: Partial<Credential> = {}): Credential => ({
|
||||
id: 'test-credential-id',
|
||||
name: 'Test Credential',
|
||||
provider: 'test-provider',
|
||||
credential_type: CredentialTypeEnum.API_KEY,
|
||||
is_default: false,
|
||||
credentials: { api_key: 'test-key' },
|
||||
...overrides,
|
||||
})
|
||||
|
||||
// ==================== Tests ====================
|
||||
|
||||
describe('AuthorizedInNode Component', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockIsCurrentWorkspaceManager.mockReturnValue(true)
|
||||
mockGetPluginCredentialInfo.mockReturnValue({
|
||||
credentials: [createCredential({ is_default: true })],
|
||||
supported_credential_types: [CredentialTypeEnum.API_KEY],
|
||||
allow_custom_token: true,
|
||||
})
|
||||
mockGetPluginOAuthClientSchema.mockReturnValue({
|
||||
schema: [],
|
||||
is_oauth_custom_client_enabled: false,
|
||||
is_system_oauth_params_exists: false,
|
||||
})
|
||||
})
|
||||
|
||||
it('should render with workspace default when no credentialId', async () => {
|
||||
const AuthorizedInNode = (await import('../authorized-in-node')).default
|
||||
const pluginPayload = createPluginPayload()
|
||||
render(
|
||||
<AuthorizedInNode pluginPayload={pluginPayload} onAuthorizationItemClick={vi.fn()} />,
|
||||
{ wrapper: createWrapper() },
|
||||
)
|
||||
expect(screen.getByText('plugin.auth.workspaceDefault')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render credential name when credentialId matches', async () => {
|
||||
const AuthorizedInNode = (await import('../authorized-in-node')).default
|
||||
const credential = createCredential({ id: 'selected-id', name: 'My Credential' })
|
||||
mockGetPluginCredentialInfo.mockReturnValue({
|
||||
credentials: [credential],
|
||||
supported_credential_types: [CredentialTypeEnum.API_KEY],
|
||||
allow_custom_token: true,
|
||||
})
|
||||
const pluginPayload = createPluginPayload()
|
||||
render(
|
||||
<AuthorizedInNode pluginPayload={pluginPayload} onAuthorizationItemClick={vi.fn()} credentialId="selected-id" />,
|
||||
{ wrapper: createWrapper() },
|
||||
)
|
||||
expect(screen.getByText('My Credential')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show auth removed when credentialId not found', async () => {
|
||||
const AuthorizedInNode = (await import('../authorized-in-node')).default
|
||||
mockGetPluginCredentialInfo.mockReturnValue({
|
||||
credentials: [createCredential()],
|
||||
supported_credential_types: [CredentialTypeEnum.API_KEY],
|
||||
allow_custom_token: true,
|
||||
})
|
||||
const pluginPayload = createPluginPayload()
|
||||
render(
|
||||
<AuthorizedInNode pluginPayload={pluginPayload} onAuthorizationItemClick={vi.fn()} credentialId="non-existent" />,
|
||||
{ wrapper: createWrapper() },
|
||||
)
|
||||
expect(screen.getByText('plugin.auth.authRemoved')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show unavailable when credential is not allowed', async () => {
|
||||
const AuthorizedInNode = (await import('../authorized-in-node')).default
|
||||
const credential = createCredential({
|
||||
id: 'unavailable-id',
|
||||
not_allowed_to_use: true,
|
||||
from_enterprise: false,
|
||||
})
|
||||
mockGetPluginCredentialInfo.mockReturnValue({
|
||||
credentials: [credential],
|
||||
supported_credential_types: [CredentialTypeEnum.API_KEY],
|
||||
allow_custom_token: true,
|
||||
})
|
||||
const pluginPayload = createPluginPayload()
|
||||
render(
|
||||
<AuthorizedInNode pluginPayload={pluginPayload} onAuthorizationItemClick={vi.fn()} credentialId="unavailable-id" />,
|
||||
{ wrapper: createWrapper() },
|
||||
)
|
||||
const button = screen.getByRole('button')
|
||||
expect(button.textContent).toContain('plugin.auth.unavailable')
|
||||
})
|
||||
|
||||
it('should show unavailable when default credential is not allowed', async () => {
|
||||
const AuthorizedInNode = (await import('../authorized-in-node')).default
|
||||
const credential = createCredential({
|
||||
is_default: true,
|
||||
not_allowed_to_use: true,
|
||||
})
|
||||
mockGetPluginCredentialInfo.mockReturnValue({
|
||||
credentials: [credential],
|
||||
supported_credential_types: [CredentialTypeEnum.API_KEY],
|
||||
allow_custom_token: true,
|
||||
})
|
||||
const pluginPayload = createPluginPayload()
|
||||
render(
|
||||
<AuthorizedInNode pluginPayload={pluginPayload} onAuthorizationItemClick={vi.fn()} />,
|
||||
{ wrapper: createWrapper() },
|
||||
)
|
||||
const button = screen.getByRole('button')
|
||||
expect(button.textContent).toContain('plugin.auth.unavailable')
|
||||
})
|
||||
|
||||
it('should call onAuthorizationItemClick when clicking', async () => {
|
||||
const AuthorizedInNode = (await import('../authorized-in-node')).default
|
||||
const onAuthorizationItemClick = vi.fn()
|
||||
const pluginPayload = createPluginPayload()
|
||||
render(
|
||||
<AuthorizedInNode pluginPayload={pluginPayload} onAuthorizationItemClick={onAuthorizationItemClick} />,
|
||||
{ wrapper: createWrapper() },
|
||||
)
|
||||
const buttons = screen.getAllByRole('button')
|
||||
fireEvent.click(buttons[0])
|
||||
expect(screen.getAllByRole('button').length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('should be memoized', async () => {
|
||||
const AuthorizedInNodeModule = await import('../authorized-in-node')
|
||||
expect(typeof AuthorizedInNodeModule.default).toBe('object')
|
||||
})
|
||||
})
|
||||
247
web/app/components/plugins/plugin-auth/__tests__/index.spec.tsx
Normal file
247
web/app/components/plugins/plugin-auth/__tests__/index.spec.tsx
Normal file
@ -0,0 +1,247 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import type { Credential, PluginPayload } from '../types'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { AuthCategory, CredentialTypeEnum } from '../types'
|
||||
|
||||
const mockGetPluginCredentialInfo = vi.fn()
|
||||
const mockDeletePluginCredential = vi.fn()
|
||||
const mockSetPluginDefaultCredential = vi.fn()
|
||||
const mockUpdatePluginCredential = vi.fn()
|
||||
const mockInvalidPluginCredentialInfo = vi.fn()
|
||||
const mockGetPluginOAuthUrl = vi.fn()
|
||||
const mockGetPluginOAuthClientSchema = vi.fn()
|
||||
const mockSetPluginOAuthCustomClient = vi.fn()
|
||||
const mockDeletePluginOAuthCustomClient = vi.fn()
|
||||
const mockInvalidPluginOAuthClientSchema = vi.fn()
|
||||
const mockAddPluginCredential = vi.fn()
|
||||
const mockGetPluginCredentialSchema = vi.fn()
|
||||
const mockInvalidToolsByType = vi.fn()
|
||||
|
||||
vi.mock('@/service/use-plugins-auth', () => ({
|
||||
useGetPluginCredentialInfo: (url: string) => ({
|
||||
data: url ? mockGetPluginCredentialInfo() : undefined,
|
||||
isLoading: false,
|
||||
}),
|
||||
useDeletePluginCredential: () => ({
|
||||
mutateAsync: mockDeletePluginCredential,
|
||||
}),
|
||||
useSetPluginDefaultCredential: () => ({
|
||||
mutateAsync: mockSetPluginDefaultCredential,
|
||||
}),
|
||||
useUpdatePluginCredential: () => ({
|
||||
mutateAsync: mockUpdatePluginCredential,
|
||||
}),
|
||||
useInvalidPluginCredentialInfo: () => mockInvalidPluginCredentialInfo,
|
||||
useGetPluginOAuthUrl: () => ({
|
||||
mutateAsync: mockGetPluginOAuthUrl,
|
||||
}),
|
||||
useGetPluginOAuthClientSchema: () => ({
|
||||
data: mockGetPluginOAuthClientSchema(),
|
||||
isLoading: false,
|
||||
}),
|
||||
useSetPluginOAuthCustomClient: () => ({
|
||||
mutateAsync: mockSetPluginOAuthCustomClient,
|
||||
}),
|
||||
useDeletePluginOAuthCustomClient: () => ({
|
||||
mutateAsync: mockDeletePluginOAuthCustomClient,
|
||||
}),
|
||||
useInvalidPluginOAuthClientSchema: () => mockInvalidPluginOAuthClientSchema,
|
||||
useAddPluginCredential: () => ({
|
||||
mutateAsync: mockAddPluginCredential,
|
||||
}),
|
||||
useGetPluginCredentialSchema: () => ({
|
||||
data: mockGetPluginCredentialSchema(),
|
||||
isLoading: false,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-tools', () => ({
|
||||
useInvalidToolsByType: () => mockInvalidToolsByType,
|
||||
}))
|
||||
|
||||
const mockIsCurrentWorkspaceManager = vi.fn()
|
||||
vi.mock('@/context/app-context', () => ({
|
||||
useAppContext: () => ({
|
||||
isCurrentWorkspaceManager: mockIsCurrentWorkspaceManager(),
|
||||
}),
|
||||
}))
|
||||
|
||||
const mockNotify = vi.fn()
|
||||
vi.mock('@/app/components/base/toast', () => ({
|
||||
useToastContext: () => ({
|
||||
notify: mockNotify,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/hooks/use-oauth', () => ({
|
||||
openOAuthPopup: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-triggers', () => ({
|
||||
useTriggerPluginDynamicOptions: () => ({
|
||||
data: { options: [] },
|
||||
isLoading: false,
|
||||
}),
|
||||
useTriggerPluginDynamicOptionsInfo: () => ({
|
||||
data: null,
|
||||
isLoading: false,
|
||||
}),
|
||||
useInvalidTriggerDynamicOptions: () => vi.fn(),
|
||||
}))
|
||||
|
||||
const createTestQueryClient = () =>
|
||||
new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
gcTime: 0,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const _createWrapper = () => {
|
||||
const testQueryClient = createTestQueryClient()
|
||||
return ({ children }: { children: ReactNode }) => (
|
||||
<QueryClientProvider client={testQueryClient}>
|
||||
{children}
|
||||
</QueryClientProvider>
|
||||
)
|
||||
}
|
||||
|
||||
const _createPluginPayload = (overrides: Partial<PluginPayload> = {}): PluginPayload => ({
|
||||
category: AuthCategory.tool,
|
||||
provider: 'test-provider',
|
||||
...overrides,
|
||||
})
|
||||
|
||||
const createCredential = (overrides: Partial<Credential> = {}): Credential => ({
|
||||
id: 'test-credential-id',
|
||||
name: 'Test Credential',
|
||||
provider: 'test-provider',
|
||||
credential_type: CredentialTypeEnum.API_KEY,
|
||||
is_default: false,
|
||||
credentials: { api_key: 'test-key' },
|
||||
...overrides,
|
||||
})
|
||||
|
||||
const _createCredentialList = (count: number, overrides: Partial<Credential>[] = []): Credential[] => {
|
||||
return Array.from({ length: count }, (_, i) => createCredential({
|
||||
id: `credential-${i}`,
|
||||
name: `Credential ${i}`,
|
||||
is_default: i === 0,
|
||||
...overrides[i],
|
||||
}))
|
||||
}
|
||||
|
||||
describe('Index Exports', () => {
|
||||
it('should export all required components and hooks', async () => {
|
||||
const exports = await import('../index')
|
||||
|
||||
expect(exports.AddApiKeyButton).toBeDefined()
|
||||
expect(exports.AddOAuthButton).toBeDefined()
|
||||
expect(exports.ApiKeyModal).toBeDefined()
|
||||
expect(exports.Authorized).toBeDefined()
|
||||
expect(exports.AuthorizedInDataSourceNode).toBeDefined()
|
||||
expect(exports.AuthorizedInNode).toBeDefined()
|
||||
expect(exports.usePluginAuth).toBeDefined()
|
||||
expect(exports.PluginAuth).toBeDefined()
|
||||
expect(exports.PluginAuthInAgent).toBeDefined()
|
||||
expect(exports.PluginAuthInDataSourceNode).toBeDefined()
|
||||
}, 15000)
|
||||
|
||||
it('should export AuthCategory enum', async () => {
|
||||
const exports = await import('../index')
|
||||
|
||||
expect(exports.AuthCategory).toBeDefined()
|
||||
expect(exports.AuthCategory.tool).toBe('tool')
|
||||
expect(exports.AuthCategory.datasource).toBe('datasource')
|
||||
expect(exports.AuthCategory.model).toBe('model')
|
||||
expect(exports.AuthCategory.trigger).toBe('trigger')
|
||||
}, 15000)
|
||||
|
||||
it('should export CredentialTypeEnum', async () => {
|
||||
const exports = await import('../index')
|
||||
|
||||
expect(exports.CredentialTypeEnum).toBeDefined()
|
||||
expect(exports.CredentialTypeEnum.OAUTH2).toBe('oauth2')
|
||||
expect(exports.CredentialTypeEnum.API_KEY).toBe('api-key')
|
||||
}, 15000)
|
||||
})
|
||||
|
||||
describe('Types', () => {
|
||||
describe('AuthCategory enum', () => {
|
||||
it('should have correct values', () => {
|
||||
expect(AuthCategory.tool).toBe('tool')
|
||||
expect(AuthCategory.datasource).toBe('datasource')
|
||||
expect(AuthCategory.model).toBe('model')
|
||||
expect(AuthCategory.trigger).toBe('trigger')
|
||||
})
|
||||
|
||||
it('should have exactly 4 categories', () => {
|
||||
const values = Object.values(AuthCategory)
|
||||
expect(values).toHaveLength(4)
|
||||
})
|
||||
})
|
||||
|
||||
describe('CredentialTypeEnum', () => {
|
||||
it('should have correct values', () => {
|
||||
expect(CredentialTypeEnum.OAUTH2).toBe('oauth2')
|
||||
expect(CredentialTypeEnum.API_KEY).toBe('api-key')
|
||||
})
|
||||
|
||||
it('should have exactly 2 types', () => {
|
||||
const values = Object.values(CredentialTypeEnum)
|
||||
expect(values).toHaveLength(2)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Credential type', () => {
|
||||
it('should allow creating valid credentials', () => {
|
||||
const credential: Credential = {
|
||||
id: 'test-id',
|
||||
name: 'Test',
|
||||
provider: 'test-provider',
|
||||
is_default: true,
|
||||
}
|
||||
expect(credential.id).toBe('test-id')
|
||||
expect(credential.is_default).toBe(true)
|
||||
})
|
||||
|
||||
it('should allow optional fields', () => {
|
||||
const credential: Credential = {
|
||||
id: 'test-id',
|
||||
name: 'Test',
|
||||
provider: 'test-provider',
|
||||
is_default: false,
|
||||
credential_type: CredentialTypeEnum.API_KEY,
|
||||
credentials: { key: 'value' },
|
||||
isWorkspaceDefault: true,
|
||||
from_enterprise: false,
|
||||
not_allowed_to_use: false,
|
||||
}
|
||||
expect(credential.credential_type).toBe(CredentialTypeEnum.API_KEY)
|
||||
expect(credential.isWorkspaceDefault).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('PluginPayload type', () => {
|
||||
it('should allow creating valid plugin payload', () => {
|
||||
const payload: PluginPayload = {
|
||||
category: AuthCategory.tool,
|
||||
provider: 'test-provider',
|
||||
}
|
||||
expect(payload.category).toBe(AuthCategory.tool)
|
||||
})
|
||||
|
||||
it('should allow optional fields', () => {
|
||||
const payload: PluginPayload = {
|
||||
category: AuthCategory.datasource,
|
||||
provider: 'test-provider',
|
||||
providerType: 'builtin',
|
||||
detail: undefined,
|
||||
}
|
||||
expect(payload.providerType).toBe('builtin')
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,255 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import type { Credential, PluginPayload } from '../types'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { AuthCategory, CredentialTypeEnum } from '../types'
|
||||
|
||||
// ==================== Mock Setup ====================
|
||||
|
||||
const mockGetPluginCredentialInfo = vi.fn()
|
||||
const mockGetPluginOAuthClientSchema = vi.fn()
|
||||
|
||||
vi.mock('@/service/use-plugins-auth', () => ({
|
||||
useGetPluginCredentialInfo: (url: string) => ({
|
||||
data: url ? mockGetPluginCredentialInfo() : undefined,
|
||||
isLoading: false,
|
||||
}),
|
||||
useDeletePluginCredential: () => ({ mutateAsync: vi.fn() }),
|
||||
useSetPluginDefaultCredential: () => ({ mutateAsync: vi.fn() }),
|
||||
useUpdatePluginCredential: () => ({ mutateAsync: vi.fn() }),
|
||||
useInvalidPluginCredentialInfo: () => vi.fn(),
|
||||
useGetPluginOAuthUrl: () => ({ mutateAsync: vi.fn() }),
|
||||
useGetPluginOAuthClientSchema: () => ({
|
||||
data: mockGetPluginOAuthClientSchema(),
|
||||
isLoading: false,
|
||||
}),
|
||||
useSetPluginOAuthCustomClient: () => ({ mutateAsync: vi.fn() }),
|
||||
useDeletePluginOAuthCustomClient: () => ({ mutateAsync: vi.fn() }),
|
||||
useInvalidPluginOAuthClientSchema: () => vi.fn(),
|
||||
useAddPluginCredential: () => ({ mutateAsync: vi.fn() }),
|
||||
useGetPluginCredentialSchema: () => ({ data: undefined, isLoading: false }),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-tools', () => ({
|
||||
useInvalidToolsByType: () => vi.fn(),
|
||||
}))
|
||||
|
||||
const mockIsCurrentWorkspaceManager = vi.fn()
|
||||
vi.mock('@/context/app-context', () => ({
|
||||
useAppContext: () => ({
|
||||
isCurrentWorkspaceManager: mockIsCurrentWorkspaceManager(),
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/toast', () => ({
|
||||
useToastContext: () => ({ notify: vi.fn() }),
|
||||
}))
|
||||
|
||||
vi.mock('@/hooks/use-oauth', () => ({
|
||||
openOAuthPopup: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-triggers', () => ({
|
||||
useTriggerPluginDynamicOptions: () => ({ data: { options: [] }, isLoading: false }),
|
||||
useTriggerPluginDynamicOptionsInfo: () => ({ data: null, isLoading: false }),
|
||||
useInvalidTriggerDynamicOptions: () => vi.fn(),
|
||||
}))
|
||||
|
||||
// ==================== Test Utilities ====================
|
||||
|
||||
const createTestQueryClient = () =>
|
||||
new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false, gcTime: 0 },
|
||||
},
|
||||
})
|
||||
|
||||
const createWrapper = () => {
|
||||
const testQueryClient = createTestQueryClient()
|
||||
return ({ children }: { children: ReactNode }) => (
|
||||
<QueryClientProvider client={testQueryClient}>
|
||||
{children}
|
||||
</QueryClientProvider>
|
||||
)
|
||||
}
|
||||
|
||||
const createPluginPayload = (overrides: Partial<PluginPayload> = {}): PluginPayload => ({
|
||||
category: AuthCategory.tool,
|
||||
provider: 'test-provider',
|
||||
...overrides,
|
||||
})
|
||||
|
||||
const createCredential = (overrides: Partial<Credential> = {}): Credential => ({
|
||||
id: 'test-credential-id',
|
||||
name: 'Test Credential',
|
||||
provider: 'test-provider',
|
||||
credential_type: CredentialTypeEnum.API_KEY,
|
||||
is_default: false,
|
||||
credentials: { api_key: 'test-key' },
|
||||
...overrides,
|
||||
})
|
||||
|
||||
// ==================== Tests ====================
|
||||
|
||||
describe('PluginAuthInAgent Component', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockIsCurrentWorkspaceManager.mockReturnValue(true)
|
||||
mockGetPluginCredentialInfo.mockReturnValue({
|
||||
credentials: [createCredential()],
|
||||
supported_credential_types: [CredentialTypeEnum.API_KEY],
|
||||
allow_custom_token: true,
|
||||
})
|
||||
mockGetPluginOAuthClientSchema.mockReturnValue({
|
||||
schema: [],
|
||||
is_oauth_custom_client_enabled: false,
|
||||
is_system_oauth_params_exists: false,
|
||||
})
|
||||
})
|
||||
|
||||
it('should render Authorize when not authorized', async () => {
|
||||
const PluginAuthInAgent = (await import('../plugin-auth-in-agent')).default
|
||||
mockGetPluginCredentialInfo.mockReturnValue({
|
||||
credentials: [],
|
||||
supported_credential_types: [CredentialTypeEnum.API_KEY],
|
||||
allow_custom_token: true,
|
||||
})
|
||||
const pluginPayload = createPluginPayload()
|
||||
render(
|
||||
<PluginAuthInAgent pluginPayload={pluginPayload} />,
|
||||
{ wrapper: createWrapper() },
|
||||
)
|
||||
expect(screen.getByRole('button')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render Authorized with workspace default when authorized', async () => {
|
||||
const PluginAuthInAgent = (await import('../plugin-auth-in-agent')).default
|
||||
const pluginPayload = createPluginPayload()
|
||||
render(
|
||||
<PluginAuthInAgent pluginPayload={pluginPayload} />,
|
||||
{ wrapper: createWrapper() },
|
||||
)
|
||||
expect(screen.getByRole('button')).toBeInTheDocument()
|
||||
expect(screen.getByText('plugin.auth.workspaceDefault')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show credential name when credentialId is provided', async () => {
|
||||
const PluginAuthInAgent = (await import('../plugin-auth-in-agent')).default
|
||||
const credential = createCredential({ id: 'selected-id', name: 'Selected Credential' })
|
||||
mockGetPluginCredentialInfo.mockReturnValue({
|
||||
credentials: [credential],
|
||||
supported_credential_types: [CredentialTypeEnum.API_KEY],
|
||||
allow_custom_token: true,
|
||||
})
|
||||
const pluginPayload = createPluginPayload()
|
||||
render(
|
||||
<PluginAuthInAgent pluginPayload={pluginPayload} credentialId="selected-id" />,
|
||||
{ wrapper: createWrapper() },
|
||||
)
|
||||
expect(screen.getByText('Selected Credential')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show auth removed when credential not found', async () => {
|
||||
const PluginAuthInAgent = (await import('../plugin-auth-in-agent')).default
|
||||
mockGetPluginCredentialInfo.mockReturnValue({
|
||||
credentials: [createCredential()],
|
||||
supported_credential_types: [CredentialTypeEnum.API_KEY],
|
||||
allow_custom_token: true,
|
||||
})
|
||||
const pluginPayload = createPluginPayload()
|
||||
render(
|
||||
<PluginAuthInAgent pluginPayload={pluginPayload} credentialId="non-existent-id" />,
|
||||
{ wrapper: createWrapper() },
|
||||
)
|
||||
expect(screen.getByText('plugin.auth.authRemoved')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show unavailable when credential is not allowed to use', async () => {
|
||||
const PluginAuthInAgent = (await import('../plugin-auth-in-agent')).default
|
||||
const credential = createCredential({
|
||||
id: 'unavailable-id',
|
||||
name: 'Unavailable Credential',
|
||||
not_allowed_to_use: true,
|
||||
from_enterprise: false,
|
||||
})
|
||||
mockGetPluginCredentialInfo.mockReturnValue({
|
||||
credentials: [credential],
|
||||
supported_credential_types: [CredentialTypeEnum.API_KEY],
|
||||
allow_custom_token: true,
|
||||
})
|
||||
const pluginPayload = createPluginPayload()
|
||||
render(
|
||||
<PluginAuthInAgent pluginPayload={pluginPayload} credentialId="unavailable-id" />,
|
||||
{ wrapper: createWrapper() },
|
||||
)
|
||||
const button = screen.getByRole('button')
|
||||
expect(button.textContent).toContain('plugin.auth.unavailable')
|
||||
})
|
||||
|
||||
it('should call onAuthorizationItemClick when item is clicked', async () => {
|
||||
const PluginAuthInAgent = (await import('../plugin-auth-in-agent')).default
|
||||
const onAuthorizationItemClick = vi.fn()
|
||||
const pluginPayload = createPluginPayload()
|
||||
render(
|
||||
<PluginAuthInAgent pluginPayload={pluginPayload} onAuthorizationItemClick={onAuthorizationItemClick} />,
|
||||
{ wrapper: createWrapper() },
|
||||
)
|
||||
const buttons = screen.getAllByRole('button')
|
||||
fireEvent.click(buttons[0])
|
||||
expect(screen.getAllByRole('button').length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('should trigger handleAuthorizationItemClick and close popup when item is clicked', async () => {
|
||||
const PluginAuthInAgent = (await import('../plugin-auth-in-agent')).default
|
||||
const onAuthorizationItemClick = vi.fn()
|
||||
const credential = createCredential({ id: 'test-cred-id', name: 'Test Credential' })
|
||||
mockGetPluginCredentialInfo.mockReturnValue({
|
||||
credentials: [credential],
|
||||
supported_credential_types: [CredentialTypeEnum.API_KEY],
|
||||
allow_custom_token: true,
|
||||
})
|
||||
const pluginPayload = createPluginPayload()
|
||||
render(
|
||||
<PluginAuthInAgent pluginPayload={pluginPayload} onAuthorizationItemClick={onAuthorizationItemClick} />,
|
||||
{ wrapper: createWrapper() },
|
||||
)
|
||||
const triggerButton = screen.getByRole('button')
|
||||
fireEvent.click(triggerButton)
|
||||
const workspaceDefaultItems = screen.getAllByText('plugin.auth.workspaceDefault')
|
||||
const popupItem = workspaceDefaultItems.length > 1 ? workspaceDefaultItems[1] : workspaceDefaultItems[0]
|
||||
fireEvent.click(popupItem)
|
||||
expect(onAuthorizationItemClick).toHaveBeenCalledWith('')
|
||||
})
|
||||
|
||||
it('should call onAuthorizationItemClick with credential id when specific credential is clicked', async () => {
|
||||
const PluginAuthInAgent = (await import('../plugin-auth-in-agent')).default
|
||||
const onAuthorizationItemClick = vi.fn()
|
||||
const credential = createCredential({
|
||||
id: 'specific-cred-id',
|
||||
name: 'Specific Credential',
|
||||
credential_type: CredentialTypeEnum.API_KEY,
|
||||
})
|
||||
mockGetPluginCredentialInfo.mockReturnValue({
|
||||
credentials: [credential],
|
||||
supported_credential_types: [CredentialTypeEnum.API_KEY],
|
||||
allow_custom_token: true,
|
||||
})
|
||||
const pluginPayload = createPluginPayload()
|
||||
render(
|
||||
<PluginAuthInAgent pluginPayload={pluginPayload} onAuthorizationItemClick={onAuthorizationItemClick} />,
|
||||
{ wrapper: createWrapper() },
|
||||
)
|
||||
const triggerButton = screen.getByRole('button')
|
||||
fireEvent.click(triggerButton)
|
||||
const credentialItems = screen.getAllByText('Specific Credential')
|
||||
const popupItem = credentialItems[credentialItems.length - 1]
|
||||
fireEvent.click(popupItem)
|
||||
expect(onAuthorizationItemClick).toHaveBeenCalledWith('specific-cred-id')
|
||||
})
|
||||
|
||||
it('should be memoized', async () => {
|
||||
const PluginAuthInAgentModule = await import('../plugin-auth-in-agent')
|
||||
expect(typeof PluginAuthInAgentModule.default).toBe('object')
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,51 @@
|
||||
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import PluginAuthInDataSourceNode from '../plugin-auth-in-datasource-node'
|
||||
|
||||
describe('PluginAuthInDataSourceNode', () => {
|
||||
const mockOnJump = vi.fn()
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
})
|
||||
|
||||
it('renders connect button when not authorized', () => {
|
||||
render(<PluginAuthInDataSourceNode onJumpToDataSourcePage={mockOnJump} />)
|
||||
expect(screen.getByText('common.integrations.connect')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders connect button', () => {
|
||||
render(<PluginAuthInDataSourceNode onJumpToDataSourcePage={mockOnJump} />)
|
||||
expect(screen.getByRole('button', { name: /common\.integrations\.connect/ })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('calls onJumpToDataSourcePage when connect button is clicked', () => {
|
||||
render(<PluginAuthInDataSourceNode onJumpToDataSourcePage={mockOnJump} />)
|
||||
fireEvent.click(screen.getByRole('button', { name: /common\.integrations\.connect/ }))
|
||||
expect(mockOnJump).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('hides connect button and shows children when authorized', () => {
|
||||
render(
|
||||
<PluginAuthInDataSourceNode isAuthorized onJumpToDataSourcePage={mockOnJump}>
|
||||
<div data-testid="child-content">Data Source Connected</div>
|
||||
</PluginAuthInDataSourceNode>,
|
||||
)
|
||||
expect(screen.queryByText('common.integrations.connect')).not.toBeInTheDocument()
|
||||
expect(screen.getByTestId('child-content')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows connect button when isAuthorized is false', () => {
|
||||
render(
|
||||
<PluginAuthInDataSourceNode isAuthorized={false} onJumpToDataSourcePage={mockOnJump}>
|
||||
<div data-testid="child-content">Data Source Connected</div>
|
||||
</PluginAuthInDataSourceNode>,
|
||||
)
|
||||
expect(screen.getByText('common.integrations.connect')).toBeInTheDocument()
|
||||
expect(screen.queryByTestId('child-content')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,139 @@
|
||||
import { cleanup, render, screen } from '@testing-library/react'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import PluginAuth from '../plugin-auth'
|
||||
import { AuthCategory } from '../types'
|
||||
|
||||
const mockUsePluginAuth = vi.fn()
|
||||
vi.mock('../hooks/use-plugin-auth', () => ({
|
||||
usePluginAuth: (...args: unknown[]) => mockUsePluginAuth(...args),
|
||||
}))
|
||||
|
||||
vi.mock('../authorize', () => ({
|
||||
default: ({ pluginPayload }: { pluginPayload: { provider: string } }) => (
|
||||
<div data-testid="authorize">
|
||||
Authorize:
|
||||
{pluginPayload.provider}
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('../authorized', () => ({
|
||||
default: ({ pluginPayload }: { pluginPayload: { provider: string } }) => (
|
||||
<div data-testid="authorized">
|
||||
Authorized:
|
||||
{pluginPayload.provider}
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
const defaultPayload = {
|
||||
category: AuthCategory.tool,
|
||||
provider: 'test-provider',
|
||||
}
|
||||
|
||||
describe('PluginAuth', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
})
|
||||
|
||||
it('renders Authorize component when not authorized', () => {
|
||||
mockUsePluginAuth.mockReturnValue({
|
||||
isAuthorized: false,
|
||||
canOAuth: false,
|
||||
canApiKey: true,
|
||||
credentials: [],
|
||||
disabled: false,
|
||||
invalidPluginCredentialInfo: vi.fn(),
|
||||
notAllowCustomCredential: false,
|
||||
})
|
||||
|
||||
render(<PluginAuth pluginPayload={defaultPayload} />)
|
||||
expect(screen.getByTestId('authorize')).toBeInTheDocument()
|
||||
expect(screen.queryByTestId('authorized')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders Authorized component when authorized and no children', () => {
|
||||
mockUsePluginAuth.mockReturnValue({
|
||||
isAuthorized: true,
|
||||
canOAuth: true,
|
||||
canApiKey: true,
|
||||
credentials: [{ id: '1', name: 'key', is_default: true, provider: 'test' }],
|
||||
disabled: false,
|
||||
invalidPluginCredentialInfo: vi.fn(),
|
||||
notAllowCustomCredential: false,
|
||||
})
|
||||
|
||||
render(<PluginAuth pluginPayload={defaultPayload} />)
|
||||
expect(screen.getByTestId('authorized')).toBeInTheDocument()
|
||||
expect(screen.queryByTestId('authorize')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders children when authorized and children provided', () => {
|
||||
mockUsePluginAuth.mockReturnValue({
|
||||
isAuthorized: true,
|
||||
canOAuth: false,
|
||||
canApiKey: true,
|
||||
credentials: [{ id: '1', name: 'key', is_default: true, provider: 'test' }],
|
||||
disabled: false,
|
||||
invalidPluginCredentialInfo: vi.fn(),
|
||||
notAllowCustomCredential: false,
|
||||
})
|
||||
|
||||
render(
|
||||
<PluginAuth pluginPayload={defaultPayload}>
|
||||
<div data-testid="custom-children">Custom Content</div>
|
||||
</PluginAuth>,
|
||||
)
|
||||
expect(screen.getByTestId('custom-children')).toBeInTheDocument()
|
||||
expect(screen.queryByTestId('authorized')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('applies className when not authorized', () => {
|
||||
mockUsePluginAuth.mockReturnValue({
|
||||
isAuthorized: false,
|
||||
canOAuth: false,
|
||||
canApiKey: true,
|
||||
credentials: [],
|
||||
disabled: false,
|
||||
invalidPluginCredentialInfo: vi.fn(),
|
||||
notAllowCustomCredential: false,
|
||||
})
|
||||
|
||||
const { container } = render(<PluginAuth pluginPayload={defaultPayload} className="custom-class" />)
|
||||
expect((container.firstChild as HTMLElement).className).toContain('custom-class')
|
||||
})
|
||||
|
||||
it('does not apply className when authorized', () => {
|
||||
mockUsePluginAuth.mockReturnValue({
|
||||
isAuthorized: true,
|
||||
canOAuth: false,
|
||||
canApiKey: true,
|
||||
credentials: [],
|
||||
disabled: false,
|
||||
invalidPluginCredentialInfo: vi.fn(),
|
||||
notAllowCustomCredential: false,
|
||||
})
|
||||
|
||||
const { container } = render(<PluginAuth pluginPayload={defaultPayload} className="custom-class" />)
|
||||
expect((container.firstChild as HTMLElement).className).not.toContain('custom-class')
|
||||
})
|
||||
|
||||
it('passes pluginPayload.provider to usePluginAuth', () => {
|
||||
mockUsePluginAuth.mockReturnValue({
|
||||
isAuthorized: false,
|
||||
canOAuth: false,
|
||||
canApiKey: false,
|
||||
credentials: [],
|
||||
disabled: false,
|
||||
invalidPluginCredentialInfo: vi.fn(),
|
||||
notAllowCustomCredential: false,
|
||||
})
|
||||
|
||||
render(<PluginAuth pluginPayload={defaultPayload} />)
|
||||
expect(mockUsePluginAuth).toHaveBeenCalledWith(defaultPayload, true)
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,55 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { transformFormSchemasSecretInput } from '../utils'
|
||||
|
||||
describe('plugin-auth/utils', () => {
|
||||
describe('transformFormSchemasSecretInput', () => {
|
||||
it('replaces secret input values with [__HIDDEN__]', () => {
|
||||
const values = { api_key: 'sk-12345', username: 'admin' }
|
||||
const result = transformFormSchemasSecretInput(['api_key'], values)
|
||||
expect(result.api_key).toBe('[__HIDDEN__]')
|
||||
expect(result.username).toBe('admin')
|
||||
})
|
||||
|
||||
it('does not replace falsy values (empty string)', () => {
|
||||
const values = { api_key: '', username: 'admin' }
|
||||
const result = transformFormSchemasSecretInput(['api_key'], values)
|
||||
expect(result.api_key).toBe('')
|
||||
})
|
||||
|
||||
it('does not replace undefined values', () => {
|
||||
const values = { username: 'admin' }
|
||||
const result = transformFormSchemasSecretInput(['api_key'], values)
|
||||
expect(result.api_key).toBeUndefined()
|
||||
})
|
||||
|
||||
it('handles multiple secret fields', () => {
|
||||
const values = { key1: 'secret1', key2: 'secret2', normal: 'value' }
|
||||
const result = transformFormSchemasSecretInput(['key1', 'key2'], values)
|
||||
expect(result.key1).toBe('[__HIDDEN__]')
|
||||
expect(result.key2).toBe('[__HIDDEN__]')
|
||||
expect(result.normal).toBe('value')
|
||||
})
|
||||
|
||||
it('does not mutate the original values', () => {
|
||||
const values = { api_key: 'sk-12345' }
|
||||
const result = transformFormSchemasSecretInput(['api_key'], values)
|
||||
expect(result).not.toBe(values)
|
||||
expect(values.api_key).toBe('sk-12345')
|
||||
})
|
||||
|
||||
it('returns same values when no secret names provided', () => {
|
||||
const values = { api_key: 'sk-12345', username: 'admin' }
|
||||
const result = transformFormSchemasSecretInput([], values)
|
||||
expect(result).toEqual(values)
|
||||
})
|
||||
|
||||
it('handles null-like values correctly', () => {
|
||||
const values = { key: null, key2: 0, key3: false }
|
||||
const result = transformFormSchemasSecretInput(['key', 'key2', 'key3'], values)
|
||||
// null, 0, false are falsy — should not be replaced
|
||||
expect(result.key).toBeNull()
|
||||
expect(result.key2).toBe(0)
|
||||
expect(result.key3).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,67 @@
|
||||
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { AuthCategory } from '../../types'
|
||||
import AddApiKeyButton from '../add-api-key-button'
|
||||
|
||||
let _mockModalOpen = false
|
||||
vi.mock('../api-key-modal', () => ({
|
||||
default: ({ onClose, onUpdate }: { onClose: () => void, onUpdate?: () => void }) => {
|
||||
_mockModalOpen = true
|
||||
return (
|
||||
<div data-testid="api-key-modal">
|
||||
<button data-testid="modal-close" onClick={onClose}>Close</button>
|
||||
<button data-testid="modal-update" onClick={onUpdate}>Update</button>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}))
|
||||
|
||||
const defaultPayload = {
|
||||
category: AuthCategory.tool,
|
||||
provider: 'test-provider',
|
||||
}
|
||||
|
||||
describe('AddApiKeyButton', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
_mockModalOpen = false
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
})
|
||||
|
||||
it('renders button with default text', () => {
|
||||
render(<AddApiKeyButton pluginPayload={defaultPayload} />)
|
||||
expect(screen.getByRole('button')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders button with custom text', () => {
|
||||
render(<AddApiKeyButton pluginPayload={defaultPayload} buttonText="Add Key" />)
|
||||
expect(screen.getByText('Add Key')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('opens modal when button is clicked', () => {
|
||||
render(<AddApiKeyButton pluginPayload={defaultPayload} />)
|
||||
fireEvent.click(screen.getByRole('button'))
|
||||
expect(screen.getByTestId('api-key-modal')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('respects disabled prop', () => {
|
||||
render(<AddApiKeyButton pluginPayload={defaultPayload} disabled />)
|
||||
expect(screen.getByRole('button')).toBeDisabled()
|
||||
})
|
||||
|
||||
it('closes modal when onClose is called', () => {
|
||||
render(<AddApiKeyButton pluginPayload={defaultPayload} />)
|
||||
fireEvent.click(screen.getByRole('button'))
|
||||
expect(screen.getByTestId('api-key-modal')).toBeInTheDocument()
|
||||
fireEvent.click(screen.getByTestId('modal-close'))
|
||||
expect(screen.queryByTestId('api-key-modal')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('applies custom button variant', () => {
|
||||
render(<AddApiKeyButton pluginPayload={defaultPayload} buttonVariant="primary" />)
|
||||
expect(screen.getByRole('button')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,102 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import * as React from 'react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { AuthCategory } from '../../types'
|
||||
|
||||
const mockGetPluginOAuthUrl = vi.fn().mockResolvedValue({ authorization_url: 'https://auth.example.com' })
|
||||
const mockOpenOAuthPopup = vi.fn()
|
||||
|
||||
vi.mock('@/hooks/use-i18n', () => ({
|
||||
useRenderI18nObject: () => (obj: Record<string, string> | string) => typeof obj === 'string' ? obj : obj.en_US || '',
|
||||
}))
|
||||
|
||||
vi.mock('@/hooks/use-oauth', () => ({
|
||||
openOAuthPopup: (...args: unknown[]) => mockOpenOAuthPopup(...args),
|
||||
}))
|
||||
|
||||
vi.mock('../../hooks/use-credential', () => ({
|
||||
useGetPluginOAuthUrlHook: () => ({
|
||||
mutateAsync: mockGetPluginOAuthUrl,
|
||||
}),
|
||||
useGetPluginOAuthClientSchemaHook: () => ({
|
||||
data: {
|
||||
schema: [],
|
||||
is_oauth_custom_client_enabled: false,
|
||||
is_system_oauth_params_exists: true,
|
||||
client_params: {},
|
||||
redirect_uri: 'https://redirect.example.com',
|
||||
},
|
||||
isLoading: false,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('../oauth-client-settings', () => ({
|
||||
default: ({ onClose }: { onClose: () => void }) => (
|
||||
<div data-testid="oauth-settings-modal">
|
||||
<button data-testid="oauth-settings-close" onClick={onClose}>Close</button>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/form/types', () => ({
|
||||
FormTypeEnum: { radio: 'radio' },
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/classnames', () => ({
|
||||
cn: (...args: unknown[]) => args.filter(Boolean).join(' '),
|
||||
}))
|
||||
|
||||
const basePayload = {
|
||||
category: AuthCategory.tool,
|
||||
provider: 'test-provider',
|
||||
}
|
||||
|
||||
describe('AddOAuthButton', () => {
|
||||
let AddOAuthButton: (typeof import('../add-oauth-button'))['default']
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks()
|
||||
const mod = await import('../add-oauth-button')
|
||||
AddOAuthButton = mod.default
|
||||
})
|
||||
|
||||
it('should render OAuth button when configured (system params exist)', () => {
|
||||
render(<AddOAuthButton pluginPayload={basePayload} buttonText="Use OAuth" />)
|
||||
|
||||
expect(screen.getByText('Use OAuth')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should open OAuth settings modal when settings icon clicked', () => {
|
||||
render(<AddOAuthButton pluginPayload={basePayload} buttonText="Use OAuth" />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('oauth-settings-button'))
|
||||
|
||||
expect(screen.getByTestId('oauth-settings-modal')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should close OAuth settings modal', () => {
|
||||
render(<AddOAuthButton pluginPayload={basePayload} buttonText="Use OAuth" />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('oauth-settings-button'))
|
||||
fireEvent.click(screen.getByTestId('oauth-settings-close'))
|
||||
|
||||
expect(screen.queryByTestId('oauth-settings-modal')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should trigger OAuth flow on main button click', async () => {
|
||||
render(<AddOAuthButton pluginPayload={basePayload} buttonText="Use OAuth" />)
|
||||
|
||||
const button = screen.getByText('Use OAuth').closest('button')
|
||||
if (button)
|
||||
fireEvent.click(button)
|
||||
|
||||
expect(mockGetPluginOAuthUrl).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should be disabled when disabled prop is true', () => {
|
||||
render(<AddOAuthButton pluginPayload={basePayload} buttonText="Use OAuth" disabled />)
|
||||
|
||||
const button = screen.getByText('Use OAuth').closest('button')
|
||||
expect(button).toBeDisabled()
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,165 @@
|
||||
import type { ApiKeyModalProps } from '../api-key-modal'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import * as React from 'react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { AuthCategory } from '../../types'
|
||||
|
||||
const mockNotify = vi.fn()
|
||||
const mockAddPluginCredential = vi.fn().mockResolvedValue({})
|
||||
const mockUpdatePluginCredential = vi.fn().mockResolvedValue({})
|
||||
const mockFormValues = { isCheckValidated: true, values: { __name__: 'My Key', api_key: 'sk-123' } }
|
||||
|
||||
vi.mock('@/app/components/base/toast', () => ({
|
||||
useToastContext: () => ({
|
||||
notify: mockNotify,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('../../hooks/use-credential', () => ({
|
||||
useAddPluginCredentialHook: () => ({
|
||||
mutateAsync: mockAddPluginCredential,
|
||||
}),
|
||||
useGetPluginCredentialSchemaHook: () => ({
|
||||
data: [
|
||||
{ name: 'api_key', label: 'API Key', type: 'secret-input', required: true },
|
||||
],
|
||||
isLoading: false,
|
||||
}),
|
||||
useUpdatePluginCredentialHook: () => ({
|
||||
mutateAsync: mockUpdatePluginCredential,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('../../../readme-panel/entrance', () => ({
|
||||
ReadmeEntrance: () => <div data-testid="readme-entrance" />,
|
||||
}))
|
||||
|
||||
vi.mock('../../../readme-panel/store', () => ({
|
||||
ReadmeShowType: { modal: 'modal' },
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/encrypted-bottom', () => ({
|
||||
EncryptedBottom: () => <div data-testid="encrypted-bottom" />,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/modal/modal', () => ({
|
||||
default: ({ children, title, onClose, onConfirm, onExtraButtonClick, showExtraButton, disabled }: {
|
||||
children: React.ReactNode
|
||||
title: string
|
||||
onClose?: () => void
|
||||
onCancel?: () => void
|
||||
onConfirm?: () => void
|
||||
onExtraButtonClick?: () => void
|
||||
showExtraButton?: boolean
|
||||
disabled?: boolean
|
||||
[key: string]: unknown
|
||||
}) => (
|
||||
<div data-testid="modal">
|
||||
<div data-testid="modal-title">{title}</div>
|
||||
{children}
|
||||
<button data-testid="modal-confirm" onClick={onConfirm} disabled={disabled}>Confirm</button>
|
||||
<button data-testid="modal-close" onClick={onClose}>Close</button>
|
||||
{showExtraButton && <button data-testid="modal-extra" onClick={onExtraButtonClick}>Remove</button>}
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/form/form-scenarios/auth', () => ({
|
||||
default: React.forwardRef((_props: Record<string, unknown>, ref: React.Ref<unknown>) => {
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
getFormValues: () => mockFormValues,
|
||||
}))
|
||||
return <div data-testid="auth-form" />
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/form/types', () => ({
|
||||
FormTypeEnum: { textInput: 'text-input' },
|
||||
}))
|
||||
|
||||
const basePayload = {
|
||||
category: AuthCategory.tool,
|
||||
provider: 'test-provider',
|
||||
}
|
||||
|
||||
describe('ApiKeyModal', () => {
|
||||
let ApiKeyModal: React.FC<ApiKeyModalProps>
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks()
|
||||
const mod = await import('../api-key-modal')
|
||||
ApiKeyModal = mod.default
|
||||
})
|
||||
|
||||
it('should render modal with correct title', () => {
|
||||
render(<ApiKeyModal pluginPayload={basePayload} />)
|
||||
|
||||
expect(screen.getByTestId('modal-title')).toHaveTextContent('plugin.auth.useApiAuth')
|
||||
})
|
||||
|
||||
it('should render auth form when data is loaded', () => {
|
||||
render(<ApiKeyModal pluginPayload={basePayload} />)
|
||||
|
||||
expect(screen.getByTestId('auth-form')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show remove button when editValues is provided', () => {
|
||||
render(<ApiKeyModal pluginPayload={basePayload} editValues={{ api_key: 'existing' }} />)
|
||||
|
||||
expect(screen.getByTestId('modal-extra')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not show remove button in add mode', () => {
|
||||
render(<ApiKeyModal pluginPayload={basePayload} />)
|
||||
|
||||
expect(screen.queryByTestId('modal-extra')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should call onClose when close button clicked', () => {
|
||||
const mockOnClose = vi.fn()
|
||||
render(<ApiKeyModal pluginPayload={basePayload} onClose={mockOnClose} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('modal-close'))
|
||||
expect(mockOnClose).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should call addPluginCredential on confirm in add mode', async () => {
|
||||
const mockOnClose = vi.fn()
|
||||
const mockOnUpdate = vi.fn()
|
||||
render(<ApiKeyModal pluginPayload={basePayload} onClose={mockOnClose} onUpdate={mockOnUpdate} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('modal-confirm'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockAddPluginCredential).toHaveBeenCalledWith(expect.objectContaining({
|
||||
type: 'api-key',
|
||||
name: 'My Key',
|
||||
}))
|
||||
})
|
||||
})
|
||||
|
||||
it('should call updatePluginCredential on confirm in edit mode', async () => {
|
||||
render(<ApiKeyModal pluginPayload={basePayload} editValues={{ api_key: 'existing', __credential_id__: 'cred-1' }} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('modal-confirm'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockUpdatePluginCredential).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
it('should call onRemove when remove button clicked', () => {
|
||||
const mockOnRemove = vi.fn()
|
||||
render(<ApiKeyModal pluginPayload={basePayload} editValues={{ api_key: 'existing' }} onRemove={mockOnRemove} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('modal-extra'))
|
||||
expect(mockOnRemove).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should render readme entrance when detail is provided', () => {
|
||||
const payload = { ...basePayload, detail: { name: 'Test' } as never }
|
||||
render(<ApiKeyModal pluginPayload={payload} />)
|
||||
|
||||
expect(screen.getByTestId('readme-entrance')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@ -1,10 +1,10 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import type { PluginPayload } from '../types'
|
||||
import type { PluginPayload } from '../../types'
|
||||
import type { FormSchema } from '@/app/components/base/form/types'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { AuthCategory } from '../types'
|
||||
import { AuthCategory } from '../../types'
|
||||
|
||||
// Create a wrapper with QueryClientProvider
|
||||
const createTestQueryClient = () =>
|
||||
@ -36,7 +36,7 @@ const mockAddPluginCredential = vi.fn()
|
||||
const mockUpdatePluginCredential = vi.fn()
|
||||
const mockGetPluginCredentialSchema = vi.fn()
|
||||
|
||||
vi.mock('../hooks/use-credential', () => ({
|
||||
vi.mock('../../hooks/use-credential', () => ({
|
||||
useGetPluginOAuthUrlHook: () => ({
|
||||
mutateAsync: mockGetPluginOAuthUrl,
|
||||
}),
|
||||
@ -117,12 +117,12 @@ const createFormSchema = (overrides: Partial<FormSchema> = {}): FormSchema => ({
|
||||
|
||||
// ==================== AddApiKeyButton Tests ====================
|
||||
describe('AddApiKeyButton', () => {
|
||||
let AddApiKeyButton: typeof import('./add-api-key-button').default
|
||||
let AddApiKeyButton: typeof import('../add-api-key-button').default
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks()
|
||||
mockGetPluginCredentialSchema.mockReturnValue([])
|
||||
const importedAddApiKeyButton = await import('./add-api-key-button')
|
||||
const importedAddApiKeyButton = await import('../add-api-key-button')
|
||||
AddApiKeyButton = importedAddApiKeyButton.default
|
||||
})
|
||||
|
||||
@ -327,7 +327,7 @@ describe('AddApiKeyButton', () => {
|
||||
|
||||
describe('Memoization', () => {
|
||||
it('should be a memoized component', async () => {
|
||||
const AddApiKeyButtonDefault = (await import('./add-api-key-button')).default
|
||||
const AddApiKeyButtonDefault = (await import('../add-api-key-button')).default
|
||||
expect(typeof AddApiKeyButtonDefault).toBe('object')
|
||||
})
|
||||
})
|
||||
@ -335,7 +335,7 @@ describe('AddApiKeyButton', () => {
|
||||
|
||||
// ==================== AddOAuthButton Tests ====================
|
||||
describe('AddOAuthButton', () => {
|
||||
let AddOAuthButton: typeof import('./add-oauth-button').default
|
||||
let AddOAuthButton: typeof import('../add-oauth-button').default
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks()
|
||||
@ -347,7 +347,7 @@ describe('AddOAuthButton', () => {
|
||||
redirect_uri: 'https://example.com/callback',
|
||||
})
|
||||
mockGetPluginOAuthUrl.mockResolvedValue({ authorization_url: 'https://oauth.example.com/auth' })
|
||||
const importedAddOAuthButton = await import('./add-oauth-button')
|
||||
const importedAddOAuthButton = await import('../add-oauth-button')
|
||||
AddOAuthButton = importedAddOAuthButton.default
|
||||
})
|
||||
|
||||
@ -856,7 +856,7 @@ describe('AddOAuthButton', () => {
|
||||
|
||||
// ==================== ApiKeyModal Tests ====================
|
||||
describe('ApiKeyModal', () => {
|
||||
let ApiKeyModal: typeof import('./api-key-modal').default
|
||||
let ApiKeyModal: typeof import('../api-key-modal').default
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks()
|
||||
@ -870,7 +870,7 @@ describe('ApiKeyModal', () => {
|
||||
isCheckValidated: false,
|
||||
values: {},
|
||||
})
|
||||
const importedApiKeyModal = await import('./api-key-modal')
|
||||
const importedApiKeyModal = await import('../api-key-modal')
|
||||
ApiKeyModal = importedApiKeyModal.default
|
||||
})
|
||||
|
||||
@ -1272,13 +1272,13 @@ describe('ApiKeyModal', () => {
|
||||
|
||||
// ==================== OAuthClientSettings Tests ====================
|
||||
describe('OAuthClientSettings', () => {
|
||||
let OAuthClientSettings: typeof import('./oauth-client-settings').default
|
||||
let OAuthClientSettings: typeof import('../oauth-client-settings').default
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks()
|
||||
mockSetPluginOAuthCustomClient.mockResolvedValue({})
|
||||
mockDeletePluginOAuthCustomClient.mockResolvedValue({})
|
||||
const importedOAuthClientSettings = await import('./oauth-client-settings')
|
||||
const importedOAuthClientSettings = await import('../oauth-client-settings')
|
||||
OAuthClientSettings = importedOAuthClientSettings.default
|
||||
})
|
||||
|
||||
@ -2193,7 +2193,7 @@ describe('OAuthClientSettings', () => {
|
||||
|
||||
describe('Memoization', () => {
|
||||
it('should be a memoized component', async () => {
|
||||
const OAuthClientSettingsDefault = (await import('./oauth-client-settings')).default
|
||||
const OAuthClientSettingsDefault = (await import('../oauth-client-settings')).default
|
||||
expect(typeof OAuthClientSettingsDefault).toBe('object')
|
||||
})
|
||||
})
|
||||
@ -2216,7 +2216,7 @@ describe('Authorize Components Integration', () => {
|
||||
|
||||
describe('AddApiKeyButton -> ApiKeyModal Flow', () => {
|
||||
it('should open ApiKeyModal when AddApiKeyButton is clicked', async () => {
|
||||
const AddApiKeyButton = (await import('./add-api-key-button')).default
|
||||
const AddApiKeyButton = (await import('../add-api-key-button')).default
|
||||
const pluginPayload = createPluginPayload()
|
||||
|
||||
render(<AddApiKeyButton pluginPayload={pluginPayload} />, { wrapper: createWrapper() })
|
||||
@ -2231,7 +2231,7 @@ describe('Authorize Components Integration', () => {
|
||||
|
||||
describe('AddOAuthButton -> OAuthClientSettings Flow', () => {
|
||||
it('should open OAuthClientSettings when setup button is clicked', async () => {
|
||||
const AddOAuthButton = (await import('./add-oauth-button')).default
|
||||
const AddOAuthButton = (await import('../add-oauth-button')).default
|
||||
const pluginPayload = createPluginPayload()
|
||||
mockGetPluginOAuthClientSchema.mockReturnValue({
|
||||
schema: [createFormSchema({ name: 'client_id', label: 'Client ID' })],
|
||||
@ -1,10 +1,10 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import type { PluginPayload } from '../types'
|
||||
import type { PluginPayload } from '../../types'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { AuthCategory } from '../types'
|
||||
import Authorize from './index'
|
||||
import { AuthCategory } from '../../types'
|
||||
import Authorize from '../index'
|
||||
|
||||
// Create a wrapper with QueryClientProvider for real component testing
|
||||
const createTestQueryClient = () =>
|
||||
@ -29,7 +29,7 @@ const createWrapper = () => {
|
||||
// Mock API hooks - only mock network-related hooks
|
||||
const mockGetPluginOAuthClientSchema = vi.fn()
|
||||
|
||||
vi.mock('../hooks/use-credential', () => ({
|
||||
vi.mock('../../hooks/use-credential', () => ({
|
||||
useGetPluginOAuthUrlHook: () => ({
|
||||
mutateAsync: vi.fn().mockResolvedValue({ authorization_url: '' }),
|
||||
}),
|
||||
@ -568,7 +568,7 @@ describe('Authorize', () => {
|
||||
// ==================== Component Memoization ====================
|
||||
describe('Component Memoization', () => {
|
||||
it('should be a memoized component (exported with memo)', async () => {
|
||||
const AuthorizeDefault = (await import('./index')).default
|
||||
const AuthorizeDefault = (await import('../index')).default
|
||||
expect(AuthorizeDefault).toBeDefined()
|
||||
// memo wrapped components are React elements with $$typeof
|
||||
expect(typeof AuthorizeDefault).toBe('object')
|
||||
@ -0,0 +1,179 @@
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import * as React from 'react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { AuthCategory } from '../../types'
|
||||
|
||||
const mockNotify = vi.fn()
|
||||
const mockSetPluginOAuthCustomClient = vi.fn().mockResolvedValue({})
|
||||
const mockDeletePluginOAuthCustomClient = vi.fn().mockResolvedValue({})
|
||||
const mockInvalidPluginOAuthClientSchema = vi.fn()
|
||||
const mockFormValues = { isCheckValidated: true, values: { __oauth_client__: 'custom', client_id: 'test-id' } }
|
||||
|
||||
vi.mock('@/app/components/base/toast', () => ({
|
||||
useToastContext: () => ({
|
||||
notify: mockNotify,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('../../hooks/use-credential', () => ({
|
||||
useSetPluginOAuthCustomClientHook: () => ({
|
||||
mutateAsync: mockSetPluginOAuthCustomClient,
|
||||
}),
|
||||
useDeletePluginOAuthCustomClientHook: () => ({
|
||||
mutateAsync: mockDeletePluginOAuthCustomClient,
|
||||
}),
|
||||
useInvalidPluginOAuthClientSchemaHook: () => mockInvalidPluginOAuthClientSchema,
|
||||
}))
|
||||
|
||||
vi.mock('../../../readme-panel/entrance', () => ({
|
||||
ReadmeEntrance: () => <div data-testid="readme-entrance" />,
|
||||
}))
|
||||
|
||||
vi.mock('../../../readme-panel/store', () => ({
|
||||
ReadmeShowType: { modal: 'modal' },
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/modal/modal', () => ({
|
||||
default: ({ children, title, onClose: _onClose, onConfirm, onCancel, onExtraButtonClick, footerSlot }: {
|
||||
children: React.ReactNode
|
||||
title: string
|
||||
onClose?: () => void
|
||||
onConfirm?: () => void
|
||||
onCancel?: () => void
|
||||
onExtraButtonClick?: () => void
|
||||
footerSlot?: React.ReactNode
|
||||
[key: string]: unknown
|
||||
}) => (
|
||||
<div data-testid="modal">
|
||||
<div data-testid="modal-title">{title}</div>
|
||||
{children}
|
||||
<button data-testid="modal-confirm" onClick={onConfirm}>Save And Auth</button>
|
||||
<button data-testid="modal-cancel" onClick={onCancel}>Save Only</button>
|
||||
<button data-testid="modal-close" onClick={onExtraButtonClick}>Cancel</button>
|
||||
{!!footerSlot && <div data-testid="footer-slot">{footerSlot}</div>}
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/form/form-scenarios/auth', () => ({
|
||||
default: React.forwardRef((_props: Record<string, unknown>, ref: React.Ref<unknown>) => {
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
getFormValues: () => mockFormValues,
|
||||
}))
|
||||
return <div data-testid="auth-form" />
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@tanstack/react-form', () => ({
|
||||
useForm: (config: Record<string, unknown>) => ({
|
||||
store: { subscribe: vi.fn(), getState: () => ({ values: config.defaultValues || {} }) },
|
||||
}),
|
||||
useStore: (_store: unknown, selector: (state: Record<string, unknown>) => unknown) => {
|
||||
return selector({ values: { __oauth_client__: 'custom' } })
|
||||
},
|
||||
}))
|
||||
|
||||
const basePayload = {
|
||||
category: AuthCategory.tool,
|
||||
provider: 'test-provider',
|
||||
}
|
||||
|
||||
const defaultSchemas = [
|
||||
{ name: 'client_id', label: 'Client ID', type: 'text-input', required: true },
|
||||
] as never
|
||||
|
||||
describe('OAuthClientSettings', () => {
|
||||
let OAuthClientSettings: (typeof import('../oauth-client-settings'))['default']
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks()
|
||||
const mod = await import('../oauth-client-settings')
|
||||
OAuthClientSettings = mod.default
|
||||
})
|
||||
|
||||
it('should render modal with correct title', () => {
|
||||
render(
|
||||
<OAuthClientSettings
|
||||
pluginPayload={basePayload}
|
||||
schemas={defaultSchemas}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('modal-title')).toHaveTextContent('plugin.auth.oauthClientSettings')
|
||||
})
|
||||
|
||||
it('should render auth form', () => {
|
||||
render(
|
||||
<OAuthClientSettings
|
||||
pluginPayload={basePayload}
|
||||
schemas={defaultSchemas}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('auth-form')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should call onClose when cancel clicked', () => {
|
||||
const mockOnClose = vi.fn()
|
||||
render(
|
||||
<OAuthClientSettings
|
||||
pluginPayload={basePayload}
|
||||
schemas={defaultSchemas}
|
||||
onClose={mockOnClose}
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByTestId('modal-close'))
|
||||
expect(mockOnClose).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should save settings on save only button click', async () => {
|
||||
const mockOnClose = vi.fn()
|
||||
const mockOnUpdate = vi.fn()
|
||||
render(
|
||||
<OAuthClientSettings
|
||||
pluginPayload={basePayload}
|
||||
schemas={defaultSchemas}
|
||||
onClose={mockOnClose}
|
||||
onUpdate={mockOnUpdate}
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByTestId('modal-cancel'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSetPluginOAuthCustomClient).toHaveBeenCalledWith(expect.objectContaining({
|
||||
enable_oauth_custom_client: true,
|
||||
}))
|
||||
})
|
||||
})
|
||||
|
||||
it('should save and authorize on confirm button click', async () => {
|
||||
const mockOnAuth = vi.fn().mockResolvedValue(undefined)
|
||||
render(
|
||||
<OAuthClientSettings
|
||||
pluginPayload={basePayload}
|
||||
schemas={defaultSchemas}
|
||||
onAuth={mockOnAuth}
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByTestId('modal-confirm'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSetPluginOAuthCustomClient).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
it('should render readme entrance when detail is provided', () => {
|
||||
const payload = { ...basePayload, detail: { name: 'Test' } as never }
|
||||
render(
|
||||
<OAuthClientSettings
|
||||
pluginPayload={payload}
|
||||
schemas={defaultSchemas}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('readme-entrance')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@ -1,10 +1,10 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import type { Credential, PluginPayload } from '../types'
|
||||
import type { Credential, PluginPayload } from '../../types'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { AuthCategory, CredentialTypeEnum } from '../types'
|
||||
import Authorized from './index'
|
||||
import { AuthCategory, CredentialTypeEnum } from '../../types'
|
||||
import Authorized from '../index'
|
||||
|
||||
// ==================== Mock Setup ====================
|
||||
|
||||
@ -13,7 +13,7 @@ const mockDeletePluginCredential = vi.fn()
|
||||
const mockSetPluginDefaultCredential = vi.fn()
|
||||
const mockUpdatePluginCredential = vi.fn()
|
||||
|
||||
vi.mock('../hooks/use-credential', () => ({
|
||||
vi.mock('../../hooks/use-credential', () => ({
|
||||
useDeletePluginCredentialHook: () => ({
|
||||
mutateAsync: mockDeletePluginCredential,
|
||||
}),
|
||||
@ -1620,7 +1620,7 @@ describe('Authorized Component', () => {
|
||||
// ==================== Memoization Test ====================
|
||||
describe('Memoization', () => {
|
||||
it('should be memoized', async () => {
|
||||
const AuthorizedModule = await import('./index')
|
||||
const AuthorizedModule = await import('../index')
|
||||
// memo returns an object with $$typeof
|
||||
expect(typeof AuthorizedModule.default).toBe('object')
|
||||
})
|
||||
@ -1,8 +1,8 @@
|
||||
import type { Credential } from '../types'
|
||||
import type { Credential } from '../../types'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { CredentialTypeEnum } from '../types'
|
||||
import Item from './item'
|
||||
import { CredentialTypeEnum } from '../../types'
|
||||
import Item from '../item'
|
||||
|
||||
// ==================== Test Utilities ====================
|
||||
|
||||
@ -829,7 +829,7 @@ describe('Item Component', () => {
|
||||
// ==================== Memoization Test ====================
|
||||
describe('Memoization', () => {
|
||||
it('should be memoized', async () => {
|
||||
const ItemModule = await import('./item')
|
||||
const ItemModule = await import('../item')
|
||||
// memo returns an object with $$typeof
|
||||
expect(typeof ItemModule.default).toBe('object')
|
||||
})
|
||||
@ -0,0 +1,186 @@
|
||||
import { renderHook } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { AuthCategory, CredentialTypeEnum } from '../../types'
|
||||
import {
|
||||
useAddPluginCredentialHook,
|
||||
useDeletePluginCredentialHook,
|
||||
useDeletePluginOAuthCustomClientHook,
|
||||
useGetPluginCredentialInfoHook,
|
||||
useGetPluginCredentialSchemaHook,
|
||||
useGetPluginOAuthClientSchemaHook,
|
||||
useGetPluginOAuthUrlHook,
|
||||
useInvalidPluginCredentialInfoHook,
|
||||
useInvalidPluginOAuthClientSchemaHook,
|
||||
useSetPluginDefaultCredentialHook,
|
||||
useSetPluginOAuthCustomClientHook,
|
||||
useUpdatePluginCredentialHook,
|
||||
} from '../use-credential'
|
||||
|
||||
// Mock service hooks
|
||||
const mockUseGetPluginCredentialInfo = vi.fn().mockReturnValue({ data: null, isLoading: false })
|
||||
const mockUseDeletePluginCredential = vi.fn().mockReturnValue({ mutateAsync: vi.fn() })
|
||||
const mockUseInvalidPluginCredentialInfo = vi.fn().mockReturnValue(vi.fn())
|
||||
const mockUseSetPluginDefaultCredential = vi.fn().mockReturnValue({ mutateAsync: vi.fn() })
|
||||
const mockUseGetPluginCredentialSchema = vi.fn().mockReturnValue({ data: [], isLoading: false })
|
||||
const mockUseAddPluginCredential = vi.fn().mockReturnValue({ mutateAsync: vi.fn() })
|
||||
const mockUseUpdatePluginCredential = vi.fn().mockReturnValue({ mutateAsync: vi.fn() })
|
||||
const mockUseGetPluginOAuthUrl = vi.fn().mockReturnValue({ mutateAsync: vi.fn() })
|
||||
const mockUseGetPluginOAuthClientSchema = vi.fn().mockReturnValue({ data: null, isLoading: false })
|
||||
const mockUseInvalidPluginOAuthClientSchema = vi.fn().mockReturnValue(vi.fn())
|
||||
const mockUseSetPluginOAuthCustomClient = vi.fn().mockReturnValue({ mutateAsync: vi.fn() })
|
||||
const mockUseDeletePluginOAuthCustomClient = vi.fn().mockReturnValue({ mutateAsync: vi.fn() })
|
||||
const mockInvalidToolsByType = vi.fn()
|
||||
|
||||
vi.mock('@/service/use-plugins-auth', () => ({
|
||||
useGetPluginCredentialInfo: (...args: unknown[]) => mockUseGetPluginCredentialInfo(...args),
|
||||
useDeletePluginCredential: (...args: unknown[]) => mockUseDeletePluginCredential(...args),
|
||||
useInvalidPluginCredentialInfo: (...args: unknown[]) => mockUseInvalidPluginCredentialInfo(...args),
|
||||
useSetPluginDefaultCredential: (...args: unknown[]) => mockUseSetPluginDefaultCredential(...args),
|
||||
useGetPluginCredentialSchema: (...args: unknown[]) => mockUseGetPluginCredentialSchema(...args),
|
||||
useAddPluginCredential: (...args: unknown[]) => mockUseAddPluginCredential(...args),
|
||||
useUpdatePluginCredential: (...args: unknown[]) => mockUseUpdatePluginCredential(...args),
|
||||
useGetPluginOAuthUrl: (...args: unknown[]) => mockUseGetPluginOAuthUrl(...args),
|
||||
useGetPluginOAuthClientSchema: (...args: unknown[]) => mockUseGetPluginOAuthClientSchema(...args),
|
||||
useInvalidPluginOAuthClientSchema: (...args: unknown[]) => mockUseInvalidPluginOAuthClientSchema(...args),
|
||||
useSetPluginOAuthCustomClient: (...args: unknown[]) => mockUseSetPluginOAuthCustomClient(...args),
|
||||
useDeletePluginOAuthCustomClient: (...args: unknown[]) => mockUseDeletePluginOAuthCustomClient(...args),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-tools', () => ({
|
||||
useInvalidToolsByType: () => mockInvalidToolsByType,
|
||||
}))
|
||||
|
||||
const toolPayload = {
|
||||
category: AuthCategory.tool,
|
||||
provider: 'test-provider',
|
||||
providerType: 'builtin',
|
||||
}
|
||||
|
||||
describe('use-credential hooks', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('useGetPluginCredentialInfoHook', () => {
|
||||
it('should call service with correct URL when enabled', () => {
|
||||
renderHook(() => useGetPluginCredentialInfoHook(toolPayload, true))
|
||||
expect(mockUseGetPluginCredentialInfo).toHaveBeenCalledWith(
|
||||
`/workspaces/current/tool-provider/builtin/${toolPayload.provider}/credential/info`,
|
||||
)
|
||||
})
|
||||
|
||||
it('should pass empty string when disabled', () => {
|
||||
renderHook(() => useGetPluginCredentialInfoHook(toolPayload, false))
|
||||
expect(mockUseGetPluginCredentialInfo).toHaveBeenCalledWith('')
|
||||
})
|
||||
})
|
||||
|
||||
describe('useDeletePluginCredentialHook', () => {
|
||||
it('should call service with correct URL', () => {
|
||||
renderHook(() => useDeletePluginCredentialHook(toolPayload))
|
||||
expect(mockUseDeletePluginCredential).toHaveBeenCalledWith(
|
||||
`/workspaces/current/tool-provider/builtin/${toolPayload.provider}/delete`,
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('useInvalidPluginCredentialInfoHook', () => {
|
||||
it('should return a function that invalidates both credential info and tools', () => {
|
||||
const { result } = renderHook(() => useInvalidPluginCredentialInfoHook(toolPayload))
|
||||
|
||||
result.current()
|
||||
|
||||
const invalidFn = mockUseInvalidPluginCredentialInfo.mock.results[0].value
|
||||
expect(invalidFn).toHaveBeenCalled()
|
||||
expect(mockInvalidToolsByType).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('useSetPluginDefaultCredentialHook', () => {
|
||||
it('should call service with correct URL', () => {
|
||||
renderHook(() => useSetPluginDefaultCredentialHook(toolPayload))
|
||||
expect(mockUseSetPluginDefaultCredential).toHaveBeenCalledWith(
|
||||
`/workspaces/current/tool-provider/builtin/${toolPayload.provider}/default-credential`,
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('useGetPluginCredentialSchemaHook', () => {
|
||||
it('should call service with correct schema URL for API_KEY', () => {
|
||||
renderHook(() => useGetPluginCredentialSchemaHook(toolPayload, CredentialTypeEnum.API_KEY))
|
||||
expect(mockUseGetPluginCredentialSchema).toHaveBeenCalledWith(
|
||||
`/workspaces/current/tool-provider/builtin/${toolPayload.provider}/credential/schema/${CredentialTypeEnum.API_KEY}`,
|
||||
)
|
||||
})
|
||||
|
||||
it('should call service with correct schema URL for OAUTH2', () => {
|
||||
renderHook(() => useGetPluginCredentialSchemaHook(toolPayload, CredentialTypeEnum.OAUTH2))
|
||||
expect(mockUseGetPluginCredentialSchema).toHaveBeenCalledWith(
|
||||
`/workspaces/current/tool-provider/builtin/${toolPayload.provider}/credential/schema/${CredentialTypeEnum.OAUTH2}`,
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('useAddPluginCredentialHook', () => {
|
||||
it('should call service with correct URL', () => {
|
||||
renderHook(() => useAddPluginCredentialHook(toolPayload))
|
||||
expect(mockUseAddPluginCredential).toHaveBeenCalledWith(
|
||||
`/workspaces/current/tool-provider/builtin/${toolPayload.provider}/add`,
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('useUpdatePluginCredentialHook', () => {
|
||||
it('should call service with correct URL', () => {
|
||||
renderHook(() => useUpdatePluginCredentialHook(toolPayload))
|
||||
expect(mockUseUpdatePluginCredential).toHaveBeenCalledWith(
|
||||
`/workspaces/current/tool-provider/builtin/${toolPayload.provider}/update`,
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('useGetPluginOAuthUrlHook', () => {
|
||||
it('should call service with correct URL', () => {
|
||||
renderHook(() => useGetPluginOAuthUrlHook(toolPayload))
|
||||
expect(mockUseGetPluginOAuthUrl).toHaveBeenCalledWith(
|
||||
`/oauth/plugin/${toolPayload.provider}/tool/authorization-url`,
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('useGetPluginOAuthClientSchemaHook', () => {
|
||||
it('should call service with correct URL', () => {
|
||||
renderHook(() => useGetPluginOAuthClientSchemaHook(toolPayload))
|
||||
expect(mockUseGetPluginOAuthClientSchema).toHaveBeenCalledWith(
|
||||
`/workspaces/current/tool-provider/builtin/${toolPayload.provider}/oauth/client-schema`,
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('useInvalidPluginOAuthClientSchemaHook', () => {
|
||||
it('should call service with correct URL', () => {
|
||||
renderHook(() => useInvalidPluginOAuthClientSchemaHook(toolPayload))
|
||||
expect(mockUseInvalidPluginOAuthClientSchema).toHaveBeenCalledWith(
|
||||
`/workspaces/current/tool-provider/builtin/${toolPayload.provider}/oauth/client-schema`,
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('useSetPluginOAuthCustomClientHook', () => {
|
||||
it('should call service with correct URL', () => {
|
||||
renderHook(() => useSetPluginOAuthCustomClientHook(toolPayload))
|
||||
expect(mockUseSetPluginOAuthCustomClient).toHaveBeenCalledWith(
|
||||
`/workspaces/current/tool-provider/builtin/${toolPayload.provider}/oauth/custom-client`,
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('useDeletePluginOAuthCustomClientHook', () => {
|
||||
it('should call service with correct URL', () => {
|
||||
renderHook(() => useDeletePluginOAuthCustomClientHook(toolPayload))
|
||||
expect(mockUseDeletePluginOAuthCustomClient).toHaveBeenCalledWith(
|
||||
`/workspaces/current/tool-provider/builtin/${toolPayload.provider}/oauth/custom-client`,
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,80 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { AuthCategory, CredentialTypeEnum } from '../../types'
|
||||
import { useGetApi } from '../use-get-api'
|
||||
|
||||
describe('useGetApi', () => {
|
||||
const provider = 'test-provider'
|
||||
|
||||
describe('tool category', () => {
|
||||
it('returns correct API paths for tool category', () => {
|
||||
const api = useGetApi({ category: AuthCategory.tool, provider })
|
||||
expect(api.getCredentialInfo).toBe(`/workspaces/current/tool-provider/builtin/${provider}/credential/info`)
|
||||
expect(api.setDefaultCredential).toBe(`/workspaces/current/tool-provider/builtin/${provider}/default-credential`)
|
||||
expect(api.getCredentials).toBe(`/workspaces/current/tool-provider/builtin/${provider}/credentials`)
|
||||
expect(api.addCredential).toBe(`/workspaces/current/tool-provider/builtin/${provider}/add`)
|
||||
expect(api.updateCredential).toBe(`/workspaces/current/tool-provider/builtin/${provider}/update`)
|
||||
expect(api.deleteCredential).toBe(`/workspaces/current/tool-provider/builtin/${provider}/delete`)
|
||||
expect(api.getOauthUrl).toBe(`/oauth/plugin/${provider}/tool/authorization-url`)
|
||||
})
|
||||
|
||||
it('returns a function for getCredentialSchema', () => {
|
||||
const api = useGetApi({ category: AuthCategory.tool, provider })
|
||||
expect(typeof api.getCredentialSchema).toBe('function')
|
||||
const schemaUrl = api.getCredentialSchema('api-key' as never)
|
||||
expect(schemaUrl).toBe(`/workspaces/current/tool-provider/builtin/${provider}/credential/schema/api-key`)
|
||||
})
|
||||
|
||||
it('includes OAuth client endpoints', () => {
|
||||
const api = useGetApi({ category: AuthCategory.tool, provider })
|
||||
expect(api.getOauthClientSchema).toBe(`/workspaces/current/tool-provider/builtin/${provider}/oauth/client-schema`)
|
||||
expect(api.setCustomOauthClient).toBe(`/workspaces/current/tool-provider/builtin/${provider}/oauth/custom-client`)
|
||||
})
|
||||
})
|
||||
|
||||
describe('datasource category', () => {
|
||||
it('returns correct API paths for datasource category', () => {
|
||||
const api = useGetApi({ category: AuthCategory.datasource, provider })
|
||||
expect(api.getCredentials).toBe(`/auth/plugin/datasource/${provider}`)
|
||||
expect(api.addCredential).toBe(`/auth/plugin/datasource/${provider}`)
|
||||
expect(api.updateCredential).toBe(`/auth/plugin/datasource/${provider}/update`)
|
||||
expect(api.deleteCredential).toBe(`/auth/plugin/datasource/${provider}/delete`)
|
||||
expect(api.setDefaultCredential).toBe(`/auth/plugin/datasource/${provider}/default`)
|
||||
expect(api.getOauthUrl).toBe(`/oauth/plugin/${provider}/datasource/get-authorization-url`)
|
||||
})
|
||||
|
||||
it('returns empty string for getCredentialInfo', () => {
|
||||
const api = useGetApi({ category: AuthCategory.datasource, provider })
|
||||
expect(api.getCredentialInfo).toBe('')
|
||||
})
|
||||
|
||||
it('returns a function for getCredentialSchema that returns empty string', () => {
|
||||
const api = useGetApi({ category: AuthCategory.datasource, provider })
|
||||
expect(api.getCredentialSchema(CredentialTypeEnum.API_KEY)).toBe('')
|
||||
})
|
||||
})
|
||||
|
||||
describe('other categories', () => {
|
||||
it('returns empty strings as fallback for unsupported category', () => {
|
||||
const api = useGetApi({ category: AuthCategory.model, provider })
|
||||
expect(api.getCredentialInfo).toBe('')
|
||||
expect(api.setDefaultCredential).toBe('')
|
||||
expect(api.getCredentials).toBe('')
|
||||
expect(api.addCredential).toBe('')
|
||||
expect(api.updateCredential).toBe('')
|
||||
expect(api.deleteCredential).toBe('')
|
||||
expect(api.getOauthUrl).toBe('')
|
||||
})
|
||||
|
||||
it('returns a function for getCredentialSchema that returns empty string', () => {
|
||||
const api = useGetApi({ category: AuthCategory.model, provider })
|
||||
expect(api.getCredentialSchema(CredentialTypeEnum.API_KEY)).toBe('')
|
||||
})
|
||||
})
|
||||
|
||||
describe('default category', () => {
|
||||
it('defaults to tool category when category is not specified', () => {
|
||||
const api = useGetApi({ provider } as { category: AuthCategory, provider: string })
|
||||
expect(api.getCredentialInfo).toContain('tool-provider')
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,191 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { act, renderHook } from '@testing-library/react'
|
||||
import * as React from 'react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { usePluginAuthAction } from '../../hooks/use-plugin-auth-action'
|
||||
import { AuthCategory } from '../../types'
|
||||
|
||||
const mockDeletePluginCredential = vi.fn().mockResolvedValue({})
|
||||
const mockSetPluginDefaultCredential = vi.fn().mockResolvedValue({})
|
||||
const mockUpdatePluginCredential = vi.fn().mockResolvedValue({})
|
||||
const mockNotify = vi.fn()
|
||||
|
||||
vi.mock('@/app/components/base/toast', () => ({
|
||||
useToastContext: () => ({
|
||||
notify: mockNotify,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('../../hooks/use-credential', () => ({
|
||||
useDeletePluginCredentialHook: () => ({
|
||||
mutateAsync: mockDeletePluginCredential,
|
||||
}),
|
||||
useSetPluginDefaultCredentialHook: () => ({
|
||||
mutateAsync: mockSetPluginDefaultCredential,
|
||||
}),
|
||||
useUpdatePluginCredentialHook: () => ({
|
||||
mutateAsync: mockUpdatePluginCredential,
|
||||
}),
|
||||
}))
|
||||
|
||||
const pluginPayload = {
|
||||
category: AuthCategory.tool,
|
||||
provider: 'test-provider',
|
||||
}
|
||||
|
||||
function createWrapper() {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: { queries: { retry: false } },
|
||||
})
|
||||
return function Wrapper({ children }: { children: ReactNode }) {
|
||||
return React.createElement(QueryClientProvider, { client: queryClient }, children)
|
||||
}
|
||||
}
|
||||
|
||||
describe('usePluginAuthAction', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should initialize with default state', () => {
|
||||
const { result } = renderHook(() => usePluginAuthAction(pluginPayload), {
|
||||
wrapper: createWrapper(),
|
||||
})
|
||||
|
||||
expect(result.current.doingAction).toBe(false)
|
||||
expect(result.current.deleteCredentialId).toBeNull()
|
||||
expect(result.current.editValues).toBeNull()
|
||||
})
|
||||
|
||||
it('should open and close confirm dialog', () => {
|
||||
const { result } = renderHook(() => usePluginAuthAction(pluginPayload), {
|
||||
wrapper: createWrapper(),
|
||||
})
|
||||
|
||||
act(() => {
|
||||
result.current.openConfirm('cred-1')
|
||||
})
|
||||
expect(result.current.deleteCredentialId).toBe('cred-1')
|
||||
|
||||
act(() => {
|
||||
result.current.closeConfirm()
|
||||
})
|
||||
expect(result.current.deleteCredentialId).toBeNull()
|
||||
})
|
||||
|
||||
it('should handle edit action', () => {
|
||||
const { result } = renderHook(() => usePluginAuthAction(pluginPayload), {
|
||||
wrapper: createWrapper(),
|
||||
})
|
||||
|
||||
const editVals = { key: 'value' }
|
||||
act(() => {
|
||||
result.current.handleEdit('cred-1', editVals)
|
||||
})
|
||||
expect(result.current.editValues).toEqual(editVals)
|
||||
})
|
||||
|
||||
it('should handle remove action by setting deleteCredentialId', () => {
|
||||
const { result } = renderHook(() => usePluginAuthAction(pluginPayload), {
|
||||
wrapper: createWrapper(),
|
||||
})
|
||||
|
||||
act(() => {
|
||||
result.current.handleEdit('cred-1', { key: 'value' })
|
||||
})
|
||||
|
||||
act(() => {
|
||||
result.current.handleRemove()
|
||||
})
|
||||
expect(result.current.deleteCredentialId).toBe('cred-1')
|
||||
})
|
||||
|
||||
it('should handle confirm delete', async () => {
|
||||
const mockOnUpdate = vi.fn()
|
||||
const { result } = renderHook(() => usePluginAuthAction(pluginPayload, mockOnUpdate), {
|
||||
wrapper: createWrapper(),
|
||||
})
|
||||
|
||||
act(() => {
|
||||
result.current.openConfirm('cred-1')
|
||||
})
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleConfirm()
|
||||
})
|
||||
|
||||
expect(mockDeletePluginCredential).toHaveBeenCalledWith({ credential_id: 'cred-1' })
|
||||
expect(mockNotify).toHaveBeenCalledWith(expect.objectContaining({ type: 'success' }))
|
||||
expect(mockOnUpdate).toHaveBeenCalled()
|
||||
expect(result.current.deleteCredentialId).toBeNull()
|
||||
})
|
||||
|
||||
it('should handle set default credential', async () => {
|
||||
const mockOnUpdate = vi.fn()
|
||||
const { result } = renderHook(() => usePluginAuthAction(pluginPayload, mockOnUpdate), {
|
||||
wrapper: createWrapper(),
|
||||
})
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleSetDefault('cred-1')
|
||||
})
|
||||
|
||||
expect(mockSetPluginDefaultCredential).toHaveBeenCalledWith('cred-1')
|
||||
expect(mockNotify).toHaveBeenCalledWith(expect.objectContaining({ type: 'success' }))
|
||||
expect(mockOnUpdate).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle rename credential', async () => {
|
||||
const mockOnUpdate = vi.fn()
|
||||
const { result } = renderHook(() => usePluginAuthAction(pluginPayload, mockOnUpdate), {
|
||||
wrapper: createWrapper(),
|
||||
})
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleRename({
|
||||
credential_id: 'cred-1',
|
||||
name: 'New Name',
|
||||
})
|
||||
})
|
||||
|
||||
expect(mockUpdatePluginCredential).toHaveBeenCalledWith({
|
||||
credential_id: 'cred-1',
|
||||
name: 'New Name',
|
||||
})
|
||||
expect(mockNotify).toHaveBeenCalledWith(expect.objectContaining({ type: 'success' }))
|
||||
expect(mockOnUpdate).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should prevent concurrent actions during doingAction', async () => {
|
||||
const { result } = renderHook(() => usePluginAuthAction(pluginPayload), {
|
||||
wrapper: createWrapper(),
|
||||
})
|
||||
|
||||
act(() => {
|
||||
result.current.handleSetDoingAction(true)
|
||||
})
|
||||
expect(result.current.doingAction).toBe(true)
|
||||
|
||||
act(() => {
|
||||
result.current.openConfirm('cred-1')
|
||||
})
|
||||
await act(async () => {
|
||||
await result.current.handleConfirm()
|
||||
})
|
||||
expect(mockDeletePluginCredential).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle confirm without pending credential ID', async () => {
|
||||
const { result } = renderHook(() => usePluginAuthAction(pluginPayload), {
|
||||
wrapper: createWrapper(),
|
||||
})
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleConfirm()
|
||||
})
|
||||
|
||||
expect(mockDeletePluginCredential).not.toHaveBeenCalled()
|
||||
expect(result.current.deleteCredentialId).toBeNull()
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,110 @@
|
||||
import { renderHook } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { AuthCategory, CredentialTypeEnum } from '../../types'
|
||||
import { usePluginAuth } from '../use-plugin-auth'
|
||||
|
||||
// Mock dependencies
|
||||
const mockCredentials = [
|
||||
{ id: '1', credential_type: CredentialTypeEnum.API_KEY, is_default: false },
|
||||
{ id: '2', credential_type: CredentialTypeEnum.OAUTH2, is_default: true },
|
||||
]
|
||||
|
||||
const mockCredentialInfo = vi.fn().mockReturnValue({
|
||||
credentials: mockCredentials,
|
||||
supported_credential_types: [CredentialTypeEnum.API_KEY, CredentialTypeEnum.OAUTH2],
|
||||
allow_custom_token: true,
|
||||
})
|
||||
|
||||
const mockInvalidate = vi.fn()
|
||||
|
||||
vi.mock('../use-credential', () => ({
|
||||
useGetPluginCredentialInfoHook: (_payload: unknown, enable?: boolean) => ({
|
||||
data: enable ? mockCredentialInfo() : undefined,
|
||||
isLoading: false,
|
||||
}),
|
||||
useInvalidPluginCredentialInfoHook: () => mockInvalidate,
|
||||
}))
|
||||
|
||||
vi.mock('@/context/app-context', () => ({
|
||||
useAppContext: () => ({
|
||||
isCurrentWorkspaceManager: true,
|
||||
}),
|
||||
}))
|
||||
|
||||
const basePayload = {
|
||||
category: AuthCategory.tool,
|
||||
provider: 'test-provider',
|
||||
}
|
||||
|
||||
describe('usePluginAuth', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should return authorized state when credentials exist', () => {
|
||||
const { result } = renderHook(() => usePluginAuth(basePayload, true))
|
||||
|
||||
expect(result.current.isAuthorized).toBe(true)
|
||||
expect(result.current.credentials).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('should detect OAuth and API Key support', () => {
|
||||
const { result } = renderHook(() => usePluginAuth(basePayload, true))
|
||||
|
||||
expect(result.current.canOAuth).toBe(true)
|
||||
expect(result.current.canApiKey).toBe(true)
|
||||
})
|
||||
|
||||
it('should return disabled=false for workspace managers', () => {
|
||||
const { result } = renderHook(() => usePluginAuth(basePayload, true))
|
||||
|
||||
expect(result.current.disabled).toBe(false)
|
||||
})
|
||||
|
||||
it('should return notAllowCustomCredential=false when allowed', () => {
|
||||
const { result } = renderHook(() => usePluginAuth(basePayload, true))
|
||||
|
||||
expect(result.current.notAllowCustomCredential).toBe(false)
|
||||
})
|
||||
|
||||
it('should return unauthorized when enable is false', () => {
|
||||
const { result } = renderHook(() => usePluginAuth(basePayload, false))
|
||||
|
||||
expect(result.current.isAuthorized).toBe(false)
|
||||
expect(result.current.credentials).toEqual([])
|
||||
})
|
||||
|
||||
it('should provide invalidate function', () => {
|
||||
const { result } = renderHook(() => usePluginAuth(basePayload, true))
|
||||
|
||||
expect(result.current.invalidPluginCredentialInfo).toBe(mockInvalidate)
|
||||
})
|
||||
|
||||
it('should handle empty credentials', () => {
|
||||
mockCredentialInfo.mockReturnValueOnce({
|
||||
credentials: [],
|
||||
supported_credential_types: [],
|
||||
allow_custom_token: false,
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => usePluginAuth(basePayload, true))
|
||||
|
||||
expect(result.current.isAuthorized).toBe(false)
|
||||
expect(result.current.canOAuth).toBe(false)
|
||||
expect(result.current.canApiKey).toBe(false)
|
||||
expect(result.current.notAllowCustomCredential).toBe(true)
|
||||
})
|
||||
|
||||
it('should handle only API Key support', () => {
|
||||
mockCredentialInfo.mockReturnValueOnce({
|
||||
credentials: [{ id: '1' }],
|
||||
supported_credential_types: [CredentialTypeEnum.API_KEY],
|
||||
allow_custom_token: true,
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => usePluginAuth(basePayload, true))
|
||||
|
||||
expect(result.current.canApiKey).toBe(true)
|
||||
expect(result.current.canOAuth).toBe(false)
|
||||
})
|
||||
})
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,18 +1,7 @@
|
||||
import type { PluginDetail } from '@/app/components/plugins/types'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import ActionList from './action-list'
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string, options?: Record<string, unknown>) => {
|
||||
if (options?.num !== undefined)
|
||||
return `${options.num} ${options.action || 'actions'}`
|
||||
return key
|
||||
},
|
||||
}),
|
||||
}))
|
||||
import ActionList from '../action-list'
|
||||
|
||||
const mockToolData = [
|
||||
{ name: 'tool-1', label: { en_US: 'Tool 1' } },
|
||||
@ -82,7 +71,7 @@ describe('ActionList', () => {
|
||||
const detail = createPluginDetail()
|
||||
render(<ActionList detail={detail} />)
|
||||
|
||||
expect(screen.getByText('2 actions')).toBeInTheDocument()
|
||||
expect(screen.getByText('plugin.detailPanel.actionNum:{"num":2,"action":"actions"}')).toBeInTheDocument()
|
||||
expect(screen.getAllByTestId('tool-item')).toHaveLength(2)
|
||||
})
|
||||
|
||||
@ -124,7 +113,7 @@ describe('ActionList', () => {
|
||||
|
||||
// The provider key is constructed from plugin_id and tool identity name
|
||||
// When they match the mock, it renders
|
||||
expect(screen.getByText('2 actions')).toBeInTheDocument()
|
||||
expect(screen.getByText('plugin.detailPanel.actionNum:{"num":2,"action":"actions"}')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1,17 +1,7 @@
|
||||
import type { PluginDetail, StrategyDetail } from '@/app/components/plugins/types'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import AgentStrategyList from './agent-strategy-list'
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string, options?: Record<string, unknown>) => {
|
||||
if (options?.num !== undefined)
|
||||
return `${options.num} ${options.strategy || 'strategies'}`
|
||||
return key
|
||||
},
|
||||
}),
|
||||
}))
|
||||
import AgentStrategyList from '../agent-strategy-list'
|
||||
|
||||
const mockStrategies = [
|
||||
{
|
||||
@ -91,7 +81,7 @@ describe('AgentStrategyList', () => {
|
||||
it('should render strategy items when data is available', () => {
|
||||
render(<AgentStrategyList detail={createPluginDetail()} />)
|
||||
|
||||
expect(screen.getByText('1 strategy')).toBeInTheDocument()
|
||||
expect(screen.getByText('plugin.detailPanel.strategyNum:{"num":1,"strategy":"strategy"}')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('strategy-item')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
@ -114,7 +104,7 @@ describe('AgentStrategyList', () => {
|
||||
}
|
||||
render(<AgentStrategyList detail={createPluginDetail()} />)
|
||||
|
||||
expect(screen.getByText('2 strategies')).toBeInTheDocument()
|
||||
expect(screen.getByText('plugin.detailPanel.strategyNum:{"num":2,"strategy":"strategies"}')).toBeInTheDocument()
|
||||
expect(screen.getAllByTestId('strategy-item')).toHaveLength(2)
|
||||
})
|
||||
})
|
||||
@ -1,17 +1,7 @@
|
||||
import type { PluginDetail } from '@/app/components/plugins/types'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import DatasourceActionList from './datasource-action-list'
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string, options?: Record<string, unknown>) => {
|
||||
if (options?.num !== undefined)
|
||||
return `${options.num} ${options.action || 'actions'}`
|
||||
return key
|
||||
},
|
||||
}),
|
||||
}))
|
||||
import DatasourceActionList from '../datasource-action-list'
|
||||
|
||||
const mockDataSourceList = [
|
||||
{ plugin_id: 'test-plugin', name: 'Data Source 1' },
|
||||
@ -72,7 +62,7 @@ describe('DatasourceActionList', () => {
|
||||
render(<DatasourceActionList detail={createPluginDetail()} />)
|
||||
|
||||
// The component always shows "0 action" because data is hardcoded as empty array
|
||||
expect(screen.getByText('0 action')).toBeInTheDocument()
|
||||
expect(screen.getByText('plugin.detailPanel.actionNum:{"num":0,"action":"action"}')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should return null when no provider found', () => {
|
||||
@ -98,7 +88,7 @@ describe('DatasourceActionList', () => {
|
||||
|
||||
render(<DatasourceActionList detail={detail} />)
|
||||
|
||||
expect(screen.getByText('0 action')).toBeInTheDocument()
|
||||
expect(screen.getByText('plugin.detailPanel.actionNum:{"num":0,"action":"action"}')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1,10 +1,10 @@
|
||||
import type { PluginDetail } from '../types'
|
||||
import type { PluginDetail } from '../../types'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import * as amplitude from '@/app/components/base/amplitude'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { PluginSource } from '../types'
|
||||
import DetailHeader from './detail-header'
|
||||
import { PluginSource } from '../../types'
|
||||
import DetailHeader from '../detail-header'
|
||||
|
||||
const {
|
||||
mockSetShowUpdatePluginModal,
|
||||
@ -24,12 +24,6 @@ const {
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('ahooks', async () => {
|
||||
const React = await import('react')
|
||||
return {
|
||||
@ -90,7 +84,7 @@ vi.mock('@/service/use-tools', () => ({
|
||||
useInvalidateAllToolProviders: () => mockInvalidateAllToolProviders,
|
||||
}))
|
||||
|
||||
vi.mock('../install-plugin/hooks', () => ({
|
||||
vi.mock('../../install-plugin/hooks', () => ({
|
||||
useGitHubReleases: () => ({
|
||||
checkForUpdates: mockCheckForUpdates,
|
||||
fetchReleases: mockFetchReleases,
|
||||
@ -106,13 +100,13 @@ let mockAutoUpgradeInfo: {
|
||||
upgrade_time_of_day: number
|
||||
} | null = null
|
||||
|
||||
vi.mock('../plugin-page/use-reference-setting', () => ({
|
||||
vi.mock('../../plugin-page/use-reference-setting', () => ({
|
||||
default: () => ({
|
||||
referenceSetting: mockAutoUpgradeInfo ? { auto_upgrade: mockAutoUpgradeInfo } : null,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('../reference-setting-modal/auto-update-setting/types', () => ({
|
||||
vi.mock('../../reference-setting-modal/auto-update-setting/types', () => ({
|
||||
AUTO_UPDATE_MODE: {
|
||||
update_all: 'update_all',
|
||||
partial: 'partial',
|
||||
@ -120,7 +114,7 @@ vi.mock('../reference-setting-modal/auto-update-setting/types', () => ({
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('../reference-setting-modal/auto-update-setting/utils', () => ({
|
||||
vi.mock('../../reference-setting-modal/auto-update-setting/utils', () => ({
|
||||
convertUTCDaySecondsToLocalSeconds: (seconds: number) => seconds,
|
||||
timeOfDayToDayjs: () => ({ format: () => '10:00 AM' }),
|
||||
}))
|
||||
@ -137,32 +131,32 @@ vi.mock('@/utils/var', () => ({
|
||||
getMarketplaceUrl: (path: string) => `https://marketplace.example.com${path}`,
|
||||
}))
|
||||
|
||||
vi.mock('../card/base/card-icon', () => ({
|
||||
vi.mock('../../card/base/card-icon', () => ({
|
||||
default: ({ src }: { src: string }) => <div data-testid="card-icon" data-src={src} />,
|
||||
}))
|
||||
|
||||
vi.mock('../card/base/description', () => ({
|
||||
vi.mock('../../card/base/description', () => ({
|
||||
default: ({ text }: { text: string }) => <div data-testid="description">{text}</div>,
|
||||
}))
|
||||
|
||||
vi.mock('../card/base/org-info', () => ({
|
||||
vi.mock('../../card/base/org-info', () => ({
|
||||
default: ({ orgName }: { orgName: string }) => <div data-testid="org-info">{orgName}</div>,
|
||||
}))
|
||||
|
||||
vi.mock('../card/base/title', () => ({
|
||||
vi.mock('../../card/base/title', () => ({
|
||||
default: ({ title }: { title: string }) => <div data-testid="title">{title}</div>,
|
||||
}))
|
||||
|
||||
vi.mock('../base/badges/verified', () => ({
|
||||
vi.mock('../../base/badges/verified', () => ({
|
||||
default: () => <span data-testid="verified-badge" />,
|
||||
}))
|
||||
|
||||
vi.mock('../base/deprecation-notice', () => ({
|
||||
vi.mock('../../base/deprecation-notice', () => ({
|
||||
default: () => <div data-testid="deprecation-notice" />,
|
||||
}))
|
||||
|
||||
// Enhanced operation-dropdown mock
|
||||
vi.mock('./operation-dropdown', () => ({
|
||||
vi.mock('../operation-dropdown', () => ({
|
||||
default: ({ onInfo, onCheckVersion, onRemove }: { onInfo: () => void, onCheckVersion: () => void, onRemove: () => void }) => (
|
||||
<div data-testid="operation-dropdown">
|
||||
<button data-testid="info-btn" onClick={onInfo}>Info</button>
|
||||
@ -173,7 +167,7 @@ vi.mock('./operation-dropdown', () => ({
|
||||
}))
|
||||
|
||||
// Enhanced update modal mock
|
||||
vi.mock('../update-plugin/from-market-place', () => ({
|
||||
vi.mock('../../update-plugin/from-market-place', () => ({
|
||||
default: ({ onSave, onCancel }: { onSave: () => void, onCancel: () => void }) => {
|
||||
return (
|
||||
<div data-testid="update-modal">
|
||||
@ -185,7 +179,7 @@ vi.mock('../update-plugin/from-market-place', () => ({
|
||||
}))
|
||||
|
||||
// Enhanced version picker mock
|
||||
vi.mock('../update-plugin/plugin-version-picker', () => ({
|
||||
vi.mock('../../update-plugin/plugin-version-picker', () => ({
|
||||
default: ({ trigger, onSelect, onShowChange }: { trigger: React.ReactNode, onSelect: (state: { version: string, unique_identifier: string, isDowngrade?: boolean }) => void, onShowChange: (show: boolean) => void }) => (
|
||||
<div data-testid="version-picker">
|
||||
{trigger}
|
||||
@ -211,7 +205,7 @@ vi.mock('../update-plugin/plugin-version-picker', () => ({
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('../plugin-page/plugin-info', () => ({
|
||||
vi.mock('../../plugin-page/plugin-info', () => ({
|
||||
default: ({ onHide }: { onHide: () => void }) => (
|
||||
<div data-testid="plugin-info">
|
||||
<button data-testid="plugin-info-close" onClick={onHide}>Close</button>
|
||||
@ -219,7 +213,7 @@ vi.mock('../plugin-page/plugin-info', () => ({
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('../plugin-auth', () => ({
|
||||
vi.mock('../../plugin-auth', () => ({
|
||||
AuthCategory: { tool: 'tool' },
|
||||
PluginAuth: () => <div data-testid="plugin-auth" />,
|
||||
}))
|
||||
@ -369,7 +363,7 @@ describe('DetailHeader', () => {
|
||||
})
|
||||
render(<DetailHeader detail={detail} onUpdate={mockOnUpdate} onHide={mockOnHide} />)
|
||||
|
||||
expect(screen.getByText('detailPanel.operation.update')).toBeInTheDocument()
|
||||
expect(screen.getByText('plugin.detailPanel.operation.update')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show update button for GitHub source', () => {
|
||||
@ -379,7 +373,7 @@ describe('DetailHeader', () => {
|
||||
})
|
||||
render(<DetailHeader detail={detail} onUpdate={mockOnUpdate} onHide={mockOnHide} />)
|
||||
|
||||
expect(screen.getByText('detailPanel.operation.update')).toBeInTheDocument()
|
||||
expect(screen.getByText('plugin.detailPanel.operation.update')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@ -556,7 +550,7 @@ describe('DetailHeader', () => {
|
||||
})
|
||||
render(<DetailHeader detail={detail} onUpdate={mockOnUpdate} onHide={mockOnHide} />)
|
||||
|
||||
const updateBtn = screen.getByText('detailPanel.operation.update')
|
||||
const updateBtn = screen.getByText('plugin.detailPanel.operation.update')
|
||||
fireEvent.click(updateBtn)
|
||||
|
||||
expect(updateBtn).toBeInTheDocument()
|
||||
@ -589,7 +583,7 @@ describe('DetailHeader', () => {
|
||||
})
|
||||
render(<DetailHeader detail={detail} onUpdate={mockOnUpdate} onHide={mockOnHide} />)
|
||||
|
||||
fireEvent.click(screen.getByText('detailPanel.operation.update'))
|
||||
fireEvent.click(screen.getByText('plugin.detailPanel.operation.update'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockFetchReleases).toHaveBeenCalledWith('owner', 'repo')
|
||||
@ -605,7 +599,7 @@ describe('DetailHeader', () => {
|
||||
})
|
||||
render(<DetailHeader detail={detail} onUpdate={mockOnUpdate} onHide={mockOnHide} />)
|
||||
|
||||
fireEvent.click(screen.getByText('detailPanel.operation.update'))
|
||||
fireEvent.click(screen.getByText('plugin.detailPanel.operation.update'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockFetchReleases).toHaveBeenCalled()
|
||||
@ -619,7 +613,7 @@ describe('DetailHeader', () => {
|
||||
})
|
||||
render(<DetailHeader detail={detail} onUpdate={mockOnUpdate} onHide={mockOnHide} />)
|
||||
|
||||
fireEvent.click(screen.getByText('detailPanel.operation.update'))
|
||||
fireEvent.click(screen.getByText('plugin.detailPanel.operation.update'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSetShowUpdatePluginModal).toHaveBeenCalled()
|
||||
@ -638,7 +632,7 @@ describe('DetailHeader', () => {
|
||||
})
|
||||
render(<DetailHeader detail={detail} onUpdate={mockOnUpdate} onHide={mockOnHide} />)
|
||||
|
||||
fireEvent.click(screen.getByText('detailPanel.operation.update'))
|
||||
fireEvent.click(screen.getByText('plugin.detailPanel.operation.update'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockOnUpdate).toHaveBeenCalled()
|
||||
@ -916,7 +910,7 @@ describe('DetailHeader', () => {
|
||||
})
|
||||
render(<DetailHeader detail={detail} onUpdate={mockOnUpdate} onHide={mockOnHide} />)
|
||||
|
||||
fireEvent.click(screen.getByText('detailPanel.operation.update'))
|
||||
fireEvent.click(screen.getByText('plugin.detailPanel.operation.update'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('update-modal')).toBeInTheDocument()
|
||||
@ -930,7 +924,7 @@ describe('DetailHeader', () => {
|
||||
})
|
||||
render(<DetailHeader detail={detail} onUpdate={mockOnUpdate} onHide={mockOnHide} />)
|
||||
|
||||
fireEvent.click(screen.getByText('detailPanel.operation.update'))
|
||||
fireEvent.click(screen.getByText('plugin.detailPanel.operation.update'))
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('update-modal')).toBeInTheDocument()
|
||||
})
|
||||
@ -949,7 +943,7 @@ describe('DetailHeader', () => {
|
||||
})
|
||||
render(<DetailHeader detail={detail} onUpdate={mockOnUpdate} onHide={mockOnHide} />)
|
||||
|
||||
fireEvent.click(screen.getByText('detailPanel.operation.update'))
|
||||
fireEvent.click(screen.getByText('plugin.detailPanel.operation.update'))
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('update-modal')).toBeInTheDocument()
|
||||
})
|
||||
@ -1,14 +1,8 @@
|
||||
import type { EndpointListItem, PluginDetail } from '../types'
|
||||
import type { EndpointListItem, PluginDetail } from '../../types'
|
||||
import { act, fireEvent, render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import EndpointCard from './endpoint-card'
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}))
|
||||
import EndpointCard from '../endpoint-card'
|
||||
|
||||
vi.mock('copy-to-clipboard', () => ({
|
||||
default: vi.fn(),
|
||||
@ -76,7 +70,7 @@ vi.mock('@/app/components/tools/utils/to-form-schema', () => ({
|
||||
addDefaultValue: (value: unknown) => value,
|
||||
}))
|
||||
|
||||
vi.mock('./endpoint-modal', () => ({
|
||||
vi.mock('../endpoint-modal', () => ({
|
||||
default: ({ onCancel, onSaved }: { onCancel: () => void, onSaved: (state: unknown) => void }) => (
|
||||
<div data-testid="endpoint-modal">
|
||||
<button data-testid="modal-cancel" onClick={onCancel}>Cancel</button>
|
||||
@ -163,7 +157,7 @@ describe('EndpointCard', () => {
|
||||
it('should show active status when enabled', () => {
|
||||
render(<EndpointCard pluginDetail={mockPluginDetail} data={mockEndpointData} handleChange={mockHandleChange} />)
|
||||
|
||||
expect(screen.getByText('detailPanel.serviceOk')).toBeInTheDocument()
|
||||
expect(screen.getByText('plugin.detailPanel.serviceOk')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('indicator')).toHaveAttribute('data-color', 'green')
|
||||
})
|
||||
|
||||
@ -171,7 +165,7 @@ describe('EndpointCard', () => {
|
||||
const disabledData = { ...mockEndpointData, enabled: false }
|
||||
render(<EndpointCard pluginDetail={mockPluginDetail} data={disabledData} handleChange={mockHandleChange} />)
|
||||
|
||||
expect(screen.getByText('detailPanel.disabled')).toBeInTheDocument()
|
||||
expect(screen.getByText('plugin.detailPanel.disabled')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('indicator')).toHaveAttribute('data-color', 'gray')
|
||||
})
|
||||
})
|
||||
@ -182,7 +176,7 @@ describe('EndpointCard', () => {
|
||||
|
||||
fireEvent.click(screen.getByRole('switch'))
|
||||
|
||||
expect(screen.getByText('detailPanel.endpointDisableTip')).toBeInTheDocument()
|
||||
expect(screen.getByText('plugin.detailPanel.endpointDisableTip')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should call disableEndpoint when confirm disable', () => {
|
||||
@ -190,7 +184,7 @@ describe('EndpointCard', () => {
|
||||
|
||||
fireEvent.click(screen.getByRole('switch'))
|
||||
// Click confirm button in the Confirm dialog
|
||||
fireEvent.click(screen.getByRole('button', { name: 'operation.confirm' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.operation.confirm' }))
|
||||
|
||||
expect(mockDisableEndpoint).toHaveBeenCalledWith('ep-1')
|
||||
})
|
||||
@ -205,7 +199,7 @@ describe('EndpointCard', () => {
|
||||
if (deleteButton)
|
||||
fireEvent.click(deleteButton)
|
||||
|
||||
expect(screen.getByText('detailPanel.endpointDeleteTip')).toBeInTheDocument()
|
||||
expect(screen.getByText('plugin.detailPanel.endpointDeleteTip')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should call deleteEndpoint when confirm delete', () => {
|
||||
@ -216,7 +210,7 @@ describe('EndpointCard', () => {
|
||||
expect(deleteButton).toBeDefined()
|
||||
if (deleteButton)
|
||||
fireEvent.click(deleteButton)
|
||||
fireEvent.click(screen.getByRole('button', { name: 'operation.confirm' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.operation.confirm' }))
|
||||
|
||||
expect(mockDeleteEndpoint).toHaveBeenCalledWith('ep-1')
|
||||
})
|
||||
@ -290,12 +284,12 @@ describe('EndpointCard', () => {
|
||||
render(<EndpointCard pluginDetail={mockPluginDetail} data={mockEndpointData} handleChange={mockHandleChange} />)
|
||||
|
||||
fireEvent.click(screen.getByRole('switch'))
|
||||
expect(screen.getByText('detailPanel.endpointDisableTip')).toBeInTheDocument()
|
||||
expect(screen.getByText('plugin.detailPanel.endpointDisableTip')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'operation.cancel' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.operation.cancel' }))
|
||||
|
||||
// Confirm should be hidden
|
||||
expect(screen.queryByText('detailPanel.endpointDisableTip')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('plugin.detailPanel.endpointDisableTip')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should hide delete confirm when cancel clicked', () => {
|
||||
@ -306,11 +300,11 @@ describe('EndpointCard', () => {
|
||||
expect(deleteButton).toBeDefined()
|
||||
if (deleteButton)
|
||||
fireEvent.click(deleteButton)
|
||||
expect(screen.getByText('detailPanel.endpointDeleteTip')).toBeInTheDocument()
|
||||
expect(screen.getByText('plugin.detailPanel.endpointDeleteTip')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'operation.cancel' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.operation.cancel' }))
|
||||
|
||||
expect(screen.queryByText('detailPanel.endpointDeleteTip')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('plugin.detailPanel.endpointDeleteTip')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should hide edit modal when cancel clicked', () => {
|
||||
@ -344,7 +338,7 @@ describe('EndpointCard', () => {
|
||||
render(<EndpointCard pluginDetail={mockPluginDetail} data={mockEndpointData} handleChange={mockHandleChange} />)
|
||||
|
||||
fireEvent.click(screen.getByRole('switch'))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'operation.confirm' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.operation.confirm' }))
|
||||
|
||||
expect(mockDisableEndpoint).toHaveBeenCalled()
|
||||
})
|
||||
@ -357,7 +351,7 @@ describe('EndpointCard', () => {
|
||||
const deleteButton = allButtons.find(btn => btn.classList.contains('text-text-tertiary'))
|
||||
if (deleteButton)
|
||||
fireEvent.click(deleteButton)
|
||||
fireEvent.click(screen.getByRole('button', { name: 'operation.confirm' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.operation.confirm' }))
|
||||
|
||||
expect(mockDeleteEndpoint).toHaveBeenCalled()
|
||||
})
|
||||
@ -1,13 +1,7 @@
|
||||
import type { PluginDetail } from '@/app/components/plugins/types'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import EndpointList from './endpoint-list'
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}))
|
||||
import EndpointList from '../endpoint-list'
|
||||
|
||||
vi.mock('@/context/i18n', () => ({
|
||||
useDocLink: () => (path: string) => `https://docs.example.com${path}`,
|
||||
@ -41,13 +35,13 @@ vi.mock('@/app/components/tools/utils/to-form-schema', () => ({
|
||||
toolCredentialToFormSchemas: (schemas: unknown[]) => schemas,
|
||||
}))
|
||||
|
||||
vi.mock('./endpoint-card', () => ({
|
||||
vi.mock('../endpoint-card', () => ({
|
||||
default: ({ data }: { data: { name: string } }) => (
|
||||
<div data-testid="endpoint-card">{data.name}</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('./endpoint-modal', () => ({
|
||||
vi.mock('../endpoint-modal', () => ({
|
||||
default: ({ onCancel, onSaved }: { onCancel: () => void, onSaved: (state: unknown) => void }) => (
|
||||
<div data-testid="endpoint-modal">
|
||||
<button data-testid="modal-cancel" onClick={onCancel}>Cancel</button>
|
||||
@ -91,7 +85,7 @@ describe('EndpointList', () => {
|
||||
it('should render endpoint list', () => {
|
||||
render(<EndpointList detail={createPluginDetail()} />)
|
||||
|
||||
expect(screen.getByText('detailPanel.endpoints')).toBeInTheDocument()
|
||||
expect(screen.getByText('plugin.detailPanel.endpoints')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render endpoint cards', () => {
|
||||
@ -112,7 +106,7 @@ describe('EndpointList', () => {
|
||||
mockEndpointListData = { endpoints: [] }
|
||||
render(<EndpointList detail={createPluginDetail()} />)
|
||||
|
||||
expect(screen.getByText('detailPanel.endpointsEmpty')).toBeInTheDocument()
|
||||
expect(screen.getByText('plugin.detailPanel.endpointsEmpty')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render add button', () => {
|
||||
@ -165,7 +159,7 @@ describe('EndpointList', () => {
|
||||
render(<EndpointList detail={detail} />)
|
||||
|
||||
// Verify the component renders correctly
|
||||
expect(screen.getByText('detailPanel.endpoints')).toBeInTheDocument()
|
||||
expect(screen.getByText('plugin.detailPanel.endpoints')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,19 +1,9 @@
|
||||
import type { FormSchema } from '../../base/form/types'
|
||||
import type { PluginDetail } from '../types'
|
||||
import type { FormSchema } from '../../../base/form/types'
|
||||
import type { PluginDetail } from '../../types'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import EndpointModal from './endpoint-modal'
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string, opts?: Record<string, unknown>) => {
|
||||
if (opts?.field)
|
||||
return `${key}: ${opts.field}`
|
||||
return key
|
||||
},
|
||||
}),
|
||||
}))
|
||||
import EndpointModal from '../endpoint-modal'
|
||||
|
||||
vi.mock('@/hooks/use-i18n', () => ({
|
||||
useRenderI18nObject: () => (obj: Record<string, string> | string) =>
|
||||
@ -45,7 +35,7 @@ vi.mock('@/app/components/header/account-setting/model-provider-page/model-modal
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('../readme-panel/entrance', () => ({
|
||||
vi.mock('../../readme-panel/entrance', () => ({
|
||||
ReadmeEntrance: () => <div data-testid="readme-entrance" />,
|
||||
}))
|
||||
|
||||
@ -110,8 +100,8 @@ describe('EndpointModal', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByText('detailPanel.endpointModalTitle')).toBeInTheDocument()
|
||||
expect(screen.getByText('detailPanel.endpointModalDesc')).toBeInTheDocument()
|
||||
expect(screen.getByText('plugin.detailPanel.endpointModalTitle')).toBeInTheDocument()
|
||||
expect(screen.getByText('plugin.detailPanel.endpointModalDesc')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render form with fieldMoreInfo url link', () => {
|
||||
@ -125,8 +115,7 @@ describe('EndpointModal', () => {
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('field-more-info')).toBeInTheDocument()
|
||||
// Should render the "howToGet" link when url exists
|
||||
expect(screen.getByText('howToGet')).toBeInTheDocument()
|
||||
expect(screen.getByText('tools.howToGet')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render readme entrance', () => {
|
||||
@ -154,7 +143,7 @@ describe('EndpointModal', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'operation.cancel' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.operation.cancel' }))
|
||||
|
||||
expect(mockOnCancel).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
@ -260,7 +249,7 @@ describe('EndpointModal', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'operation.save' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.operation.save' }))
|
||||
|
||||
expect(mockToastNotify).toHaveBeenCalledWith({
|
||||
type: 'error',
|
||||
@ -283,7 +272,7 @@ describe('EndpointModal', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'operation.save' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.operation.save' }))
|
||||
|
||||
expect(mockToastNotify).toHaveBeenCalledWith({
|
||||
type: 'error',
|
||||
@ -302,7 +291,7 @@ describe('EndpointModal', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'operation.save' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.operation.save' }))
|
||||
|
||||
expect(mockOnSaved).toHaveBeenCalledWith({ name: 'Valid Name' })
|
||||
})
|
||||
@ -321,7 +310,7 @@ describe('EndpointModal', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'operation.save' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.operation.save' }))
|
||||
|
||||
expect(mockToastNotify).not.toHaveBeenCalled()
|
||||
expect(mockOnSaved).toHaveBeenCalled()
|
||||
@ -344,7 +333,7 @@ describe('EndpointModal', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'operation.save' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.operation.save' }))
|
||||
|
||||
expect(mockOnSaved).toHaveBeenCalledWith({ enabled: true })
|
||||
})
|
||||
@ -364,7 +353,7 @@ describe('EndpointModal', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'operation.save' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.operation.save' }))
|
||||
|
||||
expect(mockOnSaved).toHaveBeenCalledWith({ enabled: true })
|
||||
})
|
||||
@ -384,7 +373,7 @@ describe('EndpointModal', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'operation.save' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.operation.save' }))
|
||||
|
||||
expect(mockOnSaved).toHaveBeenCalledWith({ enabled: true })
|
||||
})
|
||||
@ -404,7 +393,7 @@ describe('EndpointModal', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'operation.save' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.operation.save' }))
|
||||
|
||||
expect(mockOnSaved).toHaveBeenCalledWith({ enabled: false })
|
||||
})
|
||||
@ -424,7 +413,7 @@ describe('EndpointModal', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'operation.save' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.operation.save' }))
|
||||
|
||||
expect(mockOnSaved).toHaveBeenCalledWith({ enabled: true })
|
||||
})
|
||||
@ -444,7 +433,7 @@ describe('EndpointModal', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'operation.save' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.operation.save' }))
|
||||
|
||||
expect(mockOnSaved).toHaveBeenCalledWith({ enabled: false })
|
||||
})
|
||||
@ -464,7 +453,7 @@ describe('EndpointModal', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'operation.save' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.operation.save' }))
|
||||
|
||||
expect(mockOnSaved).toHaveBeenCalledWith({ enabled: true })
|
||||
})
|
||||
@ -484,7 +473,7 @@ describe('EndpointModal', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'operation.save' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.operation.save' }))
|
||||
|
||||
expect(mockOnSaved).toHaveBeenCalledWith({ enabled: false })
|
||||
})
|
||||
@ -504,7 +493,7 @@ describe('EndpointModal', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'operation.save' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.operation.save' }))
|
||||
|
||||
expect(mockOnSaved).toHaveBeenCalledWith({ text: 'hello' })
|
||||
})
|
||||
@ -2,11 +2,11 @@ import type { PluginDeclaration, PluginDetail } from '@/app/components/plugins/t
|
||||
import { act, fireEvent, render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { PluginCategoryEnum, PluginSource } from '@/app/components/plugins/types'
|
||||
import PluginDetailPanel from './index'
|
||||
import PluginDetailPanel from '../index'
|
||||
|
||||
// Mock store
|
||||
const mockSetDetail = vi.fn()
|
||||
vi.mock('./store', () => ({
|
||||
vi.mock('../store', () => ({
|
||||
usePluginStore: () => ({
|
||||
setDetail: mockSetDetail,
|
||||
}),
|
||||
@ -14,7 +14,7 @@ vi.mock('./store', () => ({
|
||||
|
||||
// Mock DetailHeader
|
||||
const mockDetailHeaderOnUpdate = vi.fn()
|
||||
vi.mock('./detail-header', () => ({
|
||||
vi.mock('../detail-header', () => ({
|
||||
default: ({ detail, onUpdate, onHide }: {
|
||||
detail: PluginDetail
|
||||
onUpdate: (isDelete?: boolean) => void
|
||||
@ -49,7 +49,7 @@ vi.mock('./detail-header', () => ({
|
||||
}))
|
||||
|
||||
// Mock ActionList
|
||||
vi.mock('./action-list', () => ({
|
||||
vi.mock('../action-list', () => ({
|
||||
default: ({ detail }: { detail: PluginDetail }) => (
|
||||
<div data-testid="action-list">
|
||||
<span data-testid="action-list-plugin-id">{detail.plugin_id}</span>
|
||||
@ -58,7 +58,7 @@ vi.mock('./action-list', () => ({
|
||||
}))
|
||||
|
||||
// Mock AgentStrategyList
|
||||
vi.mock('./agent-strategy-list', () => ({
|
||||
vi.mock('../agent-strategy-list', () => ({
|
||||
default: ({ detail }: { detail: PluginDetail }) => (
|
||||
<div data-testid="agent-strategy-list">
|
||||
<span data-testid="strategy-list-plugin-id">{detail.plugin_id}</span>
|
||||
@ -67,7 +67,7 @@ vi.mock('./agent-strategy-list', () => ({
|
||||
}))
|
||||
|
||||
// Mock EndpointList
|
||||
vi.mock('./endpoint-list', () => ({
|
||||
vi.mock('../endpoint-list', () => ({
|
||||
default: ({ detail }: { detail: PluginDetail }) => (
|
||||
<div data-testid="endpoint-list">
|
||||
<span data-testid="endpoint-list-plugin-id">{detail.plugin_id}</span>
|
||||
@ -76,7 +76,7 @@ vi.mock('./endpoint-list', () => ({
|
||||
}))
|
||||
|
||||
// Mock ModelList
|
||||
vi.mock('./model-list', () => ({
|
||||
vi.mock('../model-list', () => ({
|
||||
default: ({ detail }: { detail: PluginDetail }) => (
|
||||
<div data-testid="model-list">
|
||||
<span data-testid="model-list-plugin-id">{detail.plugin_id}</span>
|
||||
@ -85,7 +85,7 @@ vi.mock('./model-list', () => ({
|
||||
}))
|
||||
|
||||
// Mock DatasourceActionList
|
||||
vi.mock('./datasource-action-list', () => ({
|
||||
vi.mock('../datasource-action-list', () => ({
|
||||
default: ({ detail }: { detail: PluginDetail }) => (
|
||||
<div data-testid="datasource-action-list">
|
||||
<span data-testid="datasource-list-plugin-id">{detail.plugin_id}</span>
|
||||
@ -94,7 +94,7 @@ vi.mock('./datasource-action-list', () => ({
|
||||
}))
|
||||
|
||||
// Mock SubscriptionList
|
||||
vi.mock('./subscription-list', () => ({
|
||||
vi.mock('../subscription-list', () => ({
|
||||
SubscriptionList: ({ pluginDetail }: { pluginDetail: PluginDetail }) => (
|
||||
<div data-testid="subscription-list">
|
||||
<span data-testid="subscription-list-plugin-id">{pluginDetail.plugin_id}</span>
|
||||
@ -103,14 +103,14 @@ vi.mock('./subscription-list', () => ({
|
||||
}))
|
||||
|
||||
// Mock TriggerEventsList
|
||||
vi.mock('./trigger/event-list', () => ({
|
||||
vi.mock('../trigger/event-list', () => ({
|
||||
TriggerEventsList: () => (
|
||||
<div data-testid="trigger-events-list">Events List</div>
|
||||
),
|
||||
}))
|
||||
|
||||
// Mock ReadmeEntrance
|
||||
vi.mock('../readme-panel/entrance', () => ({
|
||||
vi.mock('../../readme-panel/entrance', () => ({
|
||||
ReadmeEntrance: ({ pluginDetail, className }: { pluginDetail: PluginDetail, className?: string }) => (
|
||||
<div data-testid="readme-entrance" className={className}>
|
||||
<span data-testid="readme-plugin-id">{pluginDetail.plugin_id}</span>
|
||||
@ -1,17 +1,7 @@
|
||||
import type { PluginDetail } from '@/app/components/plugins/types'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import ModelList from './model-list'
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string, options?: Record<string, unknown>) => {
|
||||
if (options?.num !== undefined)
|
||||
return `${options.num} models`
|
||||
return key
|
||||
},
|
||||
}),
|
||||
}))
|
||||
import ModelList from '../model-list'
|
||||
|
||||
const mockModels = [
|
||||
{ model: 'gpt-4', provider: 'openai' },
|
||||
@ -72,7 +62,7 @@ describe('ModelList', () => {
|
||||
it('should render model list when data is available', () => {
|
||||
render(<ModelList detail={createPluginDetail()} />)
|
||||
|
||||
expect(screen.getByText('2 models')).toBeInTheDocument()
|
||||
expect(screen.getByText('plugin.detailPanel.modelNum:{"num":2}')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render model icons and names', () => {
|
||||
@ -96,7 +86,7 @@ describe('ModelList', () => {
|
||||
mockModelListResponse = { data: [] }
|
||||
render(<ModelList detail={createPluginDetail()} />)
|
||||
|
||||
expect(screen.getByText('0 models')).toBeInTheDocument()
|
||||
expect(screen.getByText('plugin.detailPanel.modelNum:{"num":0}')).toBeInTheDocument()
|
||||
expect(screen.queryByTestId('model-icon')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@ -1,14 +1,7 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { PluginSource } from '../types'
|
||||
import OperationDropdown from './operation-dropdown'
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}))
|
||||
import { PluginSource } from '../../types'
|
||||
import OperationDropdown from '../operation-dropdown'
|
||||
|
||||
vi.mock('@/context/global-public-context', () => ({
|
||||
useGlobalPublicStore: <T,>(selector: (state: { systemFeatures: { enable_marketplace: boolean } }) => T): T =>
|
||||
@ -72,55 +65,55 @@ describe('OperationDropdown', () => {
|
||||
it('should render info option for github source', () => {
|
||||
render(<OperationDropdown {...defaultProps} source={PluginSource.github} />)
|
||||
|
||||
expect(screen.getByText('detailPanel.operation.info')).toBeInTheDocument()
|
||||
expect(screen.getByText('plugin.detailPanel.operation.info')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render check update option for github source', () => {
|
||||
render(<OperationDropdown {...defaultProps} source={PluginSource.github} />)
|
||||
|
||||
expect(screen.getByText('detailPanel.operation.checkUpdate')).toBeInTheDocument()
|
||||
expect(screen.getByText('plugin.detailPanel.operation.checkUpdate')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render view detail option for github source with marketplace enabled', () => {
|
||||
render(<OperationDropdown {...defaultProps} source={PluginSource.github} />)
|
||||
|
||||
expect(screen.getByText('detailPanel.operation.viewDetail')).toBeInTheDocument()
|
||||
expect(screen.getByText('plugin.detailPanel.operation.viewDetail')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render view detail option for marketplace source', () => {
|
||||
render(<OperationDropdown {...defaultProps} source={PluginSource.marketplace} />)
|
||||
|
||||
expect(screen.getByText('detailPanel.operation.viewDetail')).toBeInTheDocument()
|
||||
expect(screen.getByText('plugin.detailPanel.operation.viewDetail')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should always render remove option', () => {
|
||||
render(<OperationDropdown {...defaultProps} />)
|
||||
|
||||
expect(screen.getByText('detailPanel.operation.remove')).toBeInTheDocument()
|
||||
expect(screen.getByText('plugin.detailPanel.operation.remove')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not render info option for marketplace source', () => {
|
||||
render(<OperationDropdown {...defaultProps} source={PluginSource.marketplace} />)
|
||||
|
||||
expect(screen.queryByText('detailPanel.operation.info')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('plugin.detailPanel.operation.info')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not render check update option for marketplace source', () => {
|
||||
render(<OperationDropdown {...defaultProps} source={PluginSource.marketplace} />)
|
||||
|
||||
expect(screen.queryByText('detailPanel.operation.checkUpdate')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('plugin.detailPanel.operation.checkUpdate')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not render view detail for local source', () => {
|
||||
render(<OperationDropdown {...defaultProps} source={PluginSource.local} />)
|
||||
|
||||
expect(screen.queryByText('detailPanel.operation.viewDetail')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('plugin.detailPanel.operation.viewDetail')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not render view detail for debugging source', () => {
|
||||
render(<OperationDropdown {...defaultProps} source={PluginSource.debugging} />)
|
||||
|
||||
expect(screen.queryByText('detailPanel.operation.viewDetail')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('plugin.detailPanel.operation.viewDetail')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@ -138,7 +131,7 @@ describe('OperationDropdown', () => {
|
||||
it('should call onInfo when info option is clicked', () => {
|
||||
render(<OperationDropdown {...defaultProps} source={PluginSource.github} />)
|
||||
|
||||
fireEvent.click(screen.getByText('detailPanel.operation.info'))
|
||||
fireEvent.click(screen.getByText('plugin.detailPanel.operation.info'))
|
||||
|
||||
expect(mockOnInfo).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
@ -146,7 +139,7 @@ describe('OperationDropdown', () => {
|
||||
it('should call onCheckVersion when check update option is clicked', () => {
|
||||
render(<OperationDropdown {...defaultProps} source={PluginSource.github} />)
|
||||
|
||||
fireEvent.click(screen.getByText('detailPanel.operation.checkUpdate'))
|
||||
fireEvent.click(screen.getByText('plugin.detailPanel.operation.checkUpdate'))
|
||||
|
||||
expect(mockOnCheckVersion).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
@ -154,7 +147,7 @@ describe('OperationDropdown', () => {
|
||||
it('should call onRemove when remove option is clicked', () => {
|
||||
render(<OperationDropdown {...defaultProps} />)
|
||||
|
||||
fireEvent.click(screen.getByText('detailPanel.operation.remove'))
|
||||
fireEvent.click(screen.getByText('plugin.detailPanel.operation.remove'))
|
||||
|
||||
expect(mockOnRemove).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
@ -162,7 +155,7 @@ describe('OperationDropdown', () => {
|
||||
it('should have correct href for view detail link', () => {
|
||||
render(<OperationDropdown {...defaultProps} source={PluginSource.github} />)
|
||||
|
||||
const link = screen.getByText('detailPanel.operation.viewDetail').closest('a')
|
||||
const link = screen.getByText('plugin.detailPanel.operation.viewDetail').closest('a')
|
||||
expect(link).toHaveAttribute('href', 'https://github.com/test/repo')
|
||||
expect(link).toHaveAttribute('target', '_blank')
|
||||
})
|
||||
@ -182,7 +175,7 @@ describe('OperationDropdown', () => {
|
||||
<OperationDropdown {...defaultProps} source={source} />,
|
||||
)
|
||||
expect(screen.getByTestId('portal-elem')).toBeInTheDocument()
|
||||
expect(screen.getByText('detailPanel.operation.remove')).toBeInTheDocument()
|
||||
expect(screen.getByText('plugin.detailPanel.operation.remove')).toBeInTheDocument()
|
||||
unmount()
|
||||
})
|
||||
})
|
||||
@ -197,7 +190,7 @@ describe('OperationDropdown', () => {
|
||||
const { unmount } = render(
|
||||
<OperationDropdown {...defaultProps} detailUrl={url} source={PluginSource.github} />,
|
||||
)
|
||||
const link = screen.getByText('detailPanel.operation.viewDetail').closest('a')
|
||||
const link = screen.getByText('plugin.detailPanel.operation.viewDetail').closest('a')
|
||||
expect(link).toHaveAttribute('href', url)
|
||||
unmount()
|
||||
})
|
||||
@ -1,7 +1,7 @@
|
||||
import type { SimpleDetail } from './store'
|
||||
import type { SimpleDetail } from '../store'
|
||||
import { act, renderHook } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it } from 'vitest'
|
||||
import { usePluginStore } from './store'
|
||||
import { usePluginStore } from '../store'
|
||||
|
||||
// Factory function to create mock SimpleDetail
|
||||
const createSimpleDetail = (overrides: Partial<SimpleDetail> = {}): SimpleDetail => ({
|
||||
@ -1,13 +1,7 @@
|
||||
import type { StrategyDetail as StrategyDetailType } from '@/app/components/plugins/types'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import StrategyDetail from './strategy-detail'
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}))
|
||||
import StrategyDetail from '../strategy-detail'
|
||||
|
||||
vi.mock('@/hooks/use-i18n', () => ({
|
||||
useRenderI18nObject: () => (obj: Record<string, string>) => obj?.en_US || '',
|
||||
@ -93,7 +87,7 @@ describe('StrategyDetail', () => {
|
||||
it('should render parameters section', () => {
|
||||
render(<StrategyDetail provider={mockProvider} detail={mockDetail} onHide={mockOnHide} />)
|
||||
|
||||
expect(screen.getByText('setBuiltInTools.parameters')).toBeInTheDocument()
|
||||
expect(screen.getByText('tools.setBuiltInTools.parameters')).toBeInTheDocument()
|
||||
expect(screen.getByText('Parameter 1')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
@ -141,7 +135,7 @@ describe('StrategyDetail', () => {
|
||||
}
|
||||
render(<StrategyDetail provider={mockProvider} detail={detailWithNumber} onHide={mockOnHide} />)
|
||||
|
||||
expect(screen.getByText('setBuiltInTools.number')).toBeInTheDocument()
|
||||
expect(screen.getByText('tools.setBuiltInTools.number')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should display correct type for checkbox', () => {
|
||||
@ -161,7 +155,7 @@ describe('StrategyDetail', () => {
|
||||
}
|
||||
render(<StrategyDetail provider={mockProvider} detail={detailWithFile} onHide={mockOnHide} />)
|
||||
|
||||
expect(screen.getByText('setBuiltInTools.file')).toBeInTheDocument()
|
||||
expect(screen.getByText('tools.setBuiltInTools.file')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should display correct type for array[tools]', () => {
|
||||
@ -190,7 +184,7 @@ describe('StrategyDetail', () => {
|
||||
const detailEmpty = { ...mockDetail, parameters: [] }
|
||||
render(<StrategyDetail provider={mockProvider} detail={detailEmpty} onHide={mockOnHide} />)
|
||||
|
||||
expect(screen.getByText('setBuiltInTools.parameters')).toBeInTheDocument()
|
||||
expect(screen.getByText('tools.setBuiltInTools.parameters')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle no output schema', () => {
|
||||
@ -1,7 +1,7 @@
|
||||
import type { StrategyDetail } from '@/app/components/plugins/types'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import StrategyItem from './strategy-item'
|
||||
import StrategyItem from '../strategy-item'
|
||||
|
||||
vi.mock('@/hooks/use-i18n', () => ({
|
||||
useRenderI18nObject: () => (obj: Record<string, string>) => obj?.en_US || '',
|
||||
@ -11,7 +11,7 @@ vi.mock('@/utils/classnames', () => ({
|
||||
cn: (...args: (string | undefined | false | null)[]) => args.filter(Boolean).join(' '),
|
||||
}))
|
||||
|
||||
vi.mock('./strategy-detail', () => ({
|
||||
vi.mock('../strategy-detail', () => ({
|
||||
default: ({ onHide }: { onHide: () => void }) => (
|
||||
<div data-testid="strategy-detail-panel">
|
||||
<button data-testid="hide-btn" onClick={onHide}>Hide</button>
|
||||
@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { NAME_FIELD } from './utils'
|
||||
import { NAME_FIELD } from '../utils'
|
||||
|
||||
describe('utils', () => {
|
||||
describe('NAME_FIELD', () => {
|
||||
@ -0,0 +1,46 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import * as React from 'react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
vi.mock('@/app/components/base/app-icon', () => ({
|
||||
default: ({ size }: { size: string }) => <div data-testid="app-icon" data-size={size} />,
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/classnames', () => ({
|
||||
cn: (...args: unknown[]) => args.filter(Boolean).join(' '),
|
||||
}))
|
||||
|
||||
describe('AppTrigger', () => {
|
||||
let AppTrigger: (typeof import('../app-trigger'))['default']
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks()
|
||||
const mod = await import('../app-trigger')
|
||||
AppTrigger = mod.default
|
||||
})
|
||||
|
||||
it('should render placeholder when no app is selected', () => {
|
||||
render(<AppTrigger open={false} />)
|
||||
|
||||
expect(screen.queryByTestId('app-icon')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render app details when appDetail is provided', () => {
|
||||
const appDetail = {
|
||||
name: 'My App',
|
||||
icon_type: 'emoji',
|
||||
icon: '🤖',
|
||||
icon_background: '#fff',
|
||||
}
|
||||
render(<AppTrigger open={false} appDetail={appDetail as never} />)
|
||||
|
||||
expect(screen.getByTestId('app-icon')).toBeInTheDocument()
|
||||
expect(screen.getByText('My App')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render when open', () => {
|
||||
const { container } = render(<AppTrigger open={true} />)
|
||||
|
||||
expect(container.firstChild).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@ -6,12 +6,12 @@ import * as React from 'react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { InputVarType } from '@/app/components/workflow/types'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import AppInputsForm from './app-inputs-form'
|
||||
import AppInputsPanel from './app-inputs-panel'
|
||||
import AppPicker from './app-picker'
|
||||
import AppTrigger from './app-trigger'
|
||||
import AppInputsForm from '../app-inputs-form'
|
||||
import AppInputsPanel from '../app-inputs-panel'
|
||||
import AppPicker from '../app-picker'
|
||||
import AppTrigger from '../app-trigger'
|
||||
|
||||
import AppSelector from './index'
|
||||
import AppSelector from '../index'
|
||||
|
||||
// ==================== Mock Setup ====================
|
||||
|
||||
@ -1,15 +1,9 @@
|
||||
import type { PluginDetail } from '../../../types'
|
||||
import type { ModalStates, VersionTarget } from '../hooks'
|
||||
import type { PluginDetail } from '../../../../types'
|
||||
import type { ModalStates, VersionTarget } from '../../hooks'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { PluginSource } from '../../../types'
|
||||
import HeaderModals from './header-modals'
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}))
|
||||
import { PluginSource } from '../../../../types'
|
||||
import HeaderModals from '../header-modals'
|
||||
|
||||
vi.mock('@/context/i18n', () => ({
|
||||
useGetLanguage: () => 'en_US',
|
||||
@ -270,7 +264,7 @@ describe('HeaderModals', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('delete-title')).toHaveTextContent('action.delete')
|
||||
expect(screen.getByTestId('delete-title')).toHaveTextContent('plugin.action.delete')
|
||||
})
|
||||
|
||||
it('should call hideDeleteConfirm when cancel is clicked', () => {
|
||||
@ -1,13 +1,7 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { PluginSource } from '../../../types'
|
||||
import PluginSourceBadge from './plugin-source-badge'
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}))
|
||||
import { PluginSource } from '../../../../types'
|
||||
import PluginSourceBadge from '../plugin-source-badge'
|
||||
|
||||
vi.mock('@/app/components/base/tooltip', () => ({
|
||||
default: ({ children, popupContent }: { children: React.ReactNode, popupContent: string }) => (
|
||||
@ -28,7 +22,7 @@ describe('PluginSourceBadge', () => {
|
||||
|
||||
const tooltip = screen.getByTestId('tooltip')
|
||||
expect(tooltip).toBeInTheDocument()
|
||||
expect(tooltip).toHaveAttribute('data-content', 'detailPanel.categoryTip.marketplace')
|
||||
expect(tooltip).toHaveAttribute('data-content', 'plugin.detailPanel.categoryTip.marketplace')
|
||||
})
|
||||
|
||||
it('should render github source badge', () => {
|
||||
@ -36,7 +30,7 @@ describe('PluginSourceBadge', () => {
|
||||
|
||||
const tooltip = screen.getByTestId('tooltip')
|
||||
expect(tooltip).toBeInTheDocument()
|
||||
expect(tooltip).toHaveAttribute('data-content', 'detailPanel.categoryTip.github')
|
||||
expect(tooltip).toHaveAttribute('data-content', 'plugin.detailPanel.categoryTip.github')
|
||||
})
|
||||
|
||||
it('should render local source badge', () => {
|
||||
@ -44,7 +38,7 @@ describe('PluginSourceBadge', () => {
|
||||
|
||||
const tooltip = screen.getByTestId('tooltip')
|
||||
expect(tooltip).toBeInTheDocument()
|
||||
expect(tooltip).toHaveAttribute('data-content', 'detailPanel.categoryTip.local')
|
||||
expect(tooltip).toHaveAttribute('data-content', 'plugin.detailPanel.categoryTip.local')
|
||||
})
|
||||
|
||||
it('should render debugging source badge', () => {
|
||||
@ -52,7 +46,7 @@ describe('PluginSourceBadge', () => {
|
||||
|
||||
const tooltip = screen.getByTestId('tooltip')
|
||||
expect(tooltip).toBeInTheDocument()
|
||||
expect(tooltip).toHaveAttribute('data-content', 'detailPanel.categoryTip.debugging')
|
||||
expect(tooltip).toHaveAttribute('data-content', 'plugin.detailPanel.categoryTip.debugging')
|
||||
})
|
||||
})
|
||||
|
||||
@ -94,7 +88,7 @@ describe('PluginSourceBadge', () => {
|
||||
|
||||
expect(screen.getByTestId('tooltip')).toHaveAttribute(
|
||||
'data-content',
|
||||
'detailPanel.categoryTip.marketplace',
|
||||
'plugin.detailPanel.categoryTip.marketplace',
|
||||
)
|
||||
})
|
||||
|
||||
@ -103,7 +97,7 @@ describe('PluginSourceBadge', () => {
|
||||
|
||||
expect(screen.getByTestId('tooltip')).toHaveAttribute(
|
||||
'data-content',
|
||||
'detailPanel.categoryTip.github',
|
||||
'plugin.detailPanel.categoryTip.github',
|
||||
)
|
||||
})
|
||||
|
||||
@ -112,7 +106,7 @@ describe('PluginSourceBadge', () => {
|
||||
|
||||
expect(screen.getByTestId('tooltip')).toHaveAttribute(
|
||||
'data-content',
|
||||
'detailPanel.categoryTip.local',
|
||||
'plugin.detailPanel.categoryTip.local',
|
||||
)
|
||||
})
|
||||
|
||||
@ -121,7 +115,7 @@ describe('PluginSourceBadge', () => {
|
||||
|
||||
expect(screen.getByTestId('tooltip')).toHaveAttribute(
|
||||
'data-content',
|
||||
'detailPanel.categoryTip.debugging',
|
||||
'plugin.detailPanel.categoryTip.debugging',
|
||||
)
|
||||
})
|
||||
})
|
||||
@ -1,8 +1,8 @@
|
||||
import type { PluginDetail } from '../../../types'
|
||||
import type { PluginDetail } from '../../../../types'
|
||||
import { act, renderHook } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { PluginSource } from '../../../types'
|
||||
import { useDetailHeaderState } from './use-detail-header-state'
|
||||
import { PluginSource } from '../../../../types'
|
||||
import { useDetailHeaderState } from '../use-detail-header-state'
|
||||
|
||||
let mockEnableMarketplace = true
|
||||
vi.mock('@/context/global-public-context', () => ({
|
||||
@ -18,13 +18,13 @@ let mockAutoUpgradeInfo: {
|
||||
upgrade_time_of_day: number
|
||||
} | null = null
|
||||
|
||||
vi.mock('../../../plugin-page/use-reference-setting', () => ({
|
||||
vi.mock('../../../../plugin-page/use-reference-setting', () => ({
|
||||
default: () => ({
|
||||
referenceSetting: mockAutoUpgradeInfo ? { auto_upgrade: mockAutoUpgradeInfo } : null,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('../../../reference-setting-modal/auto-update-setting/types', () => ({
|
||||
vi.mock('../../../../reference-setting-modal/auto-update-setting/types', () => ({
|
||||
AUTO_UPDATE_MODE: {
|
||||
update_all: 'update_all',
|
||||
partial: 'partial',
|
||||
@ -1,11 +1,11 @@
|
||||
import type { PluginDetail } from '../../../types'
|
||||
import type { ModalStates, VersionTarget } from './use-detail-header-state'
|
||||
import type { PluginDetail } from '../../../../types'
|
||||
import type { ModalStates, VersionTarget } from '../use-detail-header-state'
|
||||
import { act, renderHook } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import * as amplitude from '@/app/components/base/amplitude'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { PluginSource } from '../../../types'
|
||||
import { usePluginOperations } from './use-plugin-operations'
|
||||
import { PluginSource } from '../../../../types'
|
||||
import { usePluginOperations } from '../use-plugin-operations'
|
||||
|
||||
type VersionPickerMock = {
|
||||
setTargetVersion: (version: VersionTarget) => void
|
||||
@ -50,7 +50,7 @@ vi.mock('@/service/use-tools', () => ({
|
||||
useInvalidateAllToolProviders: () => mockInvalidateAllToolProviders,
|
||||
}))
|
||||
|
||||
vi.mock('../../../install-plugin/hooks', () => ({
|
||||
vi.mock('../../../../install-plugin/hooks', () => ({
|
||||
useGitHubReleases: () => ({
|
||||
checkForUpdates: mockCheckForUpdates,
|
||||
fetchReleases: mockFetchReleases,
|
||||
@ -5,7 +5,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
|
||||
import { ConfigurationMethodEnum, ModelStatusEnum, ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import ModelParameterModal from './index'
|
||||
import ModelParameterModal from '../index'
|
||||
|
||||
// ==================== Mock Setup ====================
|
||||
|
||||
@ -159,7 +159,7 @@ vi.mock('@/app/components/header/account-setting/model-provider-page/model-param
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('./llm-params-panel', () => ({
|
||||
vi.mock('../llm-params-panel', () => ({
|
||||
default: ({ provider, modelId, onCompletionParamsChange, isAdvancedMode }: {
|
||||
provider: string
|
||||
modelId: string
|
||||
@ -179,7 +179,7 @@ vi.mock('./llm-params-panel', () => ({
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('./tts-params-panel', () => ({
|
||||
vi.mock('../tts-params-panel', () => ({
|
||||
default: ({ language, voice, onChange }: {
|
||||
currentModel?: ModelItem
|
||||
language?: string
|
||||
@ -3,7 +3,7 @@ import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
// Import component after mocks
|
||||
import LLMParamsPanel from './llm-params-panel'
|
||||
import LLMParamsPanel from '../llm-params-panel'
|
||||
|
||||
// ==================== Mock Setup ====================
|
||||
// All vi.mock() calls are hoisted, so inline all mock data
|
||||
@ -2,7 +2,7 @@ import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
// Import component after mocks
|
||||
import TTSParamsPanel from './tts-params-panel'
|
||||
import TTSParamsPanel from '../tts-params-panel'
|
||||
|
||||
// ==================== Mock Setup ====================
|
||||
// All vi.mock() calls are hoisted, so inline all mock data
|
||||
@ -8,7 +8,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
// ==================== Imports (after mocks) ====================
|
||||
|
||||
import { MCPToolAvailabilityProvider } from '@/app/components/workflow/nodes/_base/components/mcp-tool-availability'
|
||||
import MultipleToolSelector from './index'
|
||||
import MultipleToolSelector from '../index'
|
||||
|
||||
// ==================== Mock Setup ====================
|
||||
|
||||
@ -30,9 +30,9 @@ vi.mock('@/app/components/plugins/plugin-detail-panel/tool-selector', () => ({
|
||||
onSelectMultiple,
|
||||
onDelete,
|
||||
controlledState,
|
||||
onControlledStateChange,
|
||||
onControlledStateChange: _onControlledStateChange,
|
||||
panelShowState,
|
||||
onPanelShowStateChange,
|
||||
onPanelShowStateChange: _onPanelShowStateChange,
|
||||
isEdit,
|
||||
supportEnableSwitch,
|
||||
}: {
|
||||
@ -150,15 +150,15 @@ const createMCPTool = (overrides: Partial<ToolWithProvider> = {}): ToolWithProvi
|
||||
author: 'test-author',
|
||||
type: 'mcp',
|
||||
icon: 'test-icon.png',
|
||||
label: { en_US: 'MCP Provider' } as any,
|
||||
description: { en_US: 'MCP Provider description' } as any,
|
||||
label: { en_US: 'MCP Provider' } as unknown as ToolWithProvider['label'],
|
||||
description: { en_US: 'MCP Provider description' } as unknown as ToolWithProvider['description'],
|
||||
is_team_authorization: true,
|
||||
allow_delete: false,
|
||||
labels: [],
|
||||
tools: [{
|
||||
name: 'mcp-tool-1',
|
||||
label: { en_US: 'MCP Tool 1' } as any,
|
||||
description: { en_US: 'MCP Tool 1 description' } as any,
|
||||
label: { en_US: 'MCP Tool 1' } as unknown as ToolWithProvider['label'],
|
||||
description: { en_US: 'MCP Tool 1 description' } as unknown as ToolWithProvider['description'],
|
||||
parameters: [],
|
||||
output_schema: {},
|
||||
}],
|
||||
@ -641,7 +641,7 @@ describe('MultipleToolSelector', () => {
|
||||
|
||||
it('should handle undefined value', () => {
|
||||
// Arrange & Act - value defaults to [] in component
|
||||
renderComponent({ value: undefined as any })
|
||||
renderComponent({ value: undefined as unknown as ToolValue[] })
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('plugin.detailPanel.toolSelector.empty')).toBeInTheDocument()
|
||||
@ -1,12 +1,12 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { DeleteConfirm } from './delete-confirm'
|
||||
import { DeleteConfirm } from '../delete-confirm'
|
||||
|
||||
const mockRefetch = vi.fn()
|
||||
const mockDelete = vi.fn()
|
||||
const mockToast = vi.fn()
|
||||
|
||||
vi.mock('./use-subscription-list', () => ({
|
||||
vi.mock('../use-subscription-list', () => ({
|
||||
useSubscriptionList: () => ({ refetch: mockRefetch }),
|
||||
}))
|
||||
|
||||
@ -3,8 +3,8 @@ import type { TriggerSubscription } from '@/app/components/workflow/block-select
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { TriggerCredentialTypeEnum } from '@/app/components/workflow/block-selector/types'
|
||||
import { SubscriptionList } from './index'
|
||||
import { SubscriptionListMode } from './types'
|
||||
import { SubscriptionList } from '../index'
|
||||
import { SubscriptionListMode } from '../types'
|
||||
|
||||
const mockRefetch = vi.fn()
|
||||
let mockSubscriptionListError: Error | null = null
|
||||
@ -16,7 +16,7 @@ let mockSubscriptionListState: {
|
||||
|
||||
let mockPluginDetail: PluginDetail | undefined
|
||||
|
||||
vi.mock('./use-subscription-list', () => ({
|
||||
vi.mock('../use-subscription-list', () => ({
|
||||
useSubscriptionList: () => {
|
||||
if (mockSubscriptionListError)
|
||||
throw mockSubscriptionListError
|
||||
@ -24,7 +24,7 @@ vi.mock('./use-subscription-list', () => ({
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('../../store', () => ({
|
||||
vi.mock('../../../store', () => ({
|
||||
usePluginStore: (selector: (state: { detail: PluginDetail | undefined }) => PluginDetail | undefined) =>
|
||||
selector({ detail: mockPluginDetail }),
|
||||
}))
|
||||
@ -2,15 +2,15 @@ import type { TriggerSubscription } from '@/app/components/workflow/block-select
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { TriggerCredentialTypeEnum } from '@/app/components/workflow/block-selector/types'
|
||||
import { SubscriptionListView } from './list-view'
|
||||
import { SubscriptionListView } from '../list-view'
|
||||
|
||||
let mockSubscriptions: TriggerSubscription[] = []
|
||||
|
||||
vi.mock('./use-subscription-list', () => ({
|
||||
vi.mock('../use-subscription-list', () => ({
|
||||
useSubscriptionList: () => ({ subscriptions: mockSubscriptions }),
|
||||
}))
|
||||
|
||||
vi.mock('../../store', () => ({
|
||||
vi.mock('../../../store', () => ({
|
||||
usePluginStore: () => ({ detail: undefined }),
|
||||
}))
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import type { TriggerLogEntity } from '@/app/components/workflow/block-selector/types'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import LogViewer from './log-viewer'
|
||||
import LogViewer from '../log-viewer'
|
||||
|
||||
const mockToastNotify = vi.fn()
|
||||
const mockWriteText = vi.fn()
|
||||
@ -2,12 +2,12 @@ import type { TriggerSubscription } from '@/app/components/workflow/block-select
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { TriggerCredentialTypeEnum } from '@/app/components/workflow/block-selector/types'
|
||||
import { SubscriptionSelectorEntry } from './selector-entry'
|
||||
import { SubscriptionSelectorEntry } from '../selector-entry'
|
||||
|
||||
let mockSubscriptions: TriggerSubscription[] = []
|
||||
const mockRefetch = vi.fn()
|
||||
|
||||
vi.mock('./use-subscription-list', () => ({
|
||||
vi.mock('../use-subscription-list', () => ({
|
||||
useSubscriptionList: () => ({
|
||||
subscriptions: mockSubscriptions,
|
||||
isLoading: false,
|
||||
@ -15,7 +15,7 @@ vi.mock('./use-subscription-list', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('../../store', () => ({
|
||||
vi.mock('../../../store', () => ({
|
||||
usePluginStore: () => ({ detail: undefined }),
|
||||
}))
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user