test(web): increase test coverage for model-provider-page folder (#32374)

This commit is contained in:
akashseth-ifp 2026-02-24 15:58:12 +05:30 committed by GitHub
parent 84533cbfe0
commit ad3a195734
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 1617 additions and 112 deletions

View File

@ -1,10 +1,6 @@
import { fireEvent, render, screen } from '@testing-library/react'
import SyncButton from './sync-button'
vi.mock('ahooks', () => ({
useBoolean: () => [false, { setTrue: vi.fn(), setFalse: vi.fn() }],
}))
describe('SyncButton', () => {
describe('Rendering', () => {
it('should render without crashing', () => {

View File

@ -1,4 +1,4 @@
import { act, render, screen, waitFor } from '@testing-library/react'
import { render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { audioToText } from '@/service/share'
@ -8,7 +8,6 @@ const { mockState, MockRecorder } = vi.hoisted(() => {
const state = {
params: {} as Record<string, string>,
pathname: '/test',
rafCallback: undefined as (() => void) | undefined,
recorderInstances: [] as unknown[],
startOverride: null as (() => Promise<void>) | null,
analyseData: new Uint8Array(1024).fill(150) as Uint8Array,
@ -55,13 +54,6 @@ vi.mock('./utils', () => ({
convertToMp3: vi.fn(() => new Blob(['test'], { type: 'audio/mp3' })),
}))
vi.mock('ahooks', () => ({
useRafInterval: vi.fn((fn: () => void) => {
mockState.rafCallback = fn
return vi.fn()
}),
}))
describe('VoiceInput', () => {
const onConverted = vi.fn()
const onCancel = vi.fn()
@ -70,7 +62,6 @@ describe('VoiceInput', () => {
vi.clearAllMocks()
mockState.params = {}
mockState.pathname = '/test'
mockState.rafCallback = undefined
mockState.recorderInstances = []
mockState.startOverride = null
@ -101,21 +92,6 @@ describe('VoiceInput', () => {
expect(screen.getByTestId('voice-input-timer')).toHaveTextContent('00:00')
})
it('should increment timer via useRafInterval callback', async () => {
render(<VoiceInput onConverted={onConverted} onCancel={onCancel} />)
await screen.findByText('common.voiceInput.speaking')
act(() => {
mockState.rafCallback?.()
})
expect(screen.getByTestId('voice-input-timer')).toHaveTextContent('00:01')
act(() => {
mockState.rafCallback?.()
})
expect(screen.getByTestId('voice-input-timer')).toHaveTextContent('00:02')
})
it('should call onCancel when recording start fails', async () => {
mockState.startOverride = () => Promise.reject(new Error('Permission denied'))
@ -177,32 +153,6 @@ describe('VoiceInput', () => {
expect(onCancel).toHaveBeenCalled()
})
it('should automatically stop recording after 600 seconds', async () => {
vi.mocked(audioToText).mockResolvedValueOnce({ text: 'auto stopped' })
mockState.params = { token: 'abc' }
render(<VoiceInput onConverted={onConverted} onCancel={onCancel} />)
await screen.findByTestId('voice-input-stop')
for (let i = 0; i < 600; i++)
act(() => { mockState.rafCallback?.() })
await waitFor(() => {
expect(onConverted).toHaveBeenCalledWith('auto stopped')
})
})
it('should show red timer text after 500 seconds', async () => {
render(<VoiceInput onConverted={onConverted} onCancel={onCancel} />)
await screen.findByTestId('voice-input-stop')
for (let i = 0; i < 501; i++)
act(() => { mockState.rafCallback?.() })
const timer = screen.getByTestId('voice-input-timer')
expect(timer.className).toContain('text-[#F04438]')
})
it('should draw on canvas with low data values triggering v < 128 clamp', async () => {
mockState.analyseData = new Uint8Array(1024).fill(50)

View File

@ -1,8 +1,22 @@
import type { Mock } from 'vitest'
import { renderHook } from '@testing-library/react'
import type {
DefaultModelResponse,
Model,
} from './declarations'
import { act, renderHook } from '@testing-library/react'
import { useLocale } from '@/context/i18n'
import { useLanguage } from './hooks'
import {
ConfigurationMethodEnum,
ModelTypeEnum,
} from './declarations'
import {
useLanguage,
useModelList,
useProviderCredentialsAndLoadBalancing,
useSystemDefaultModelAndModelList,
} from './hooks'
// Mock dependencies
vi.mock('@tanstack/react-query', () => ({
useQuery: vi.fn(),
useQueryClient: vi.fn(() => ({
@ -10,17 +24,6 @@ vi.mock('@tanstack/react-query', () => ({
})),
}))
// mock use-context-selector
vi.mock('use-context-selector', () => ({
useContext: vi.fn(),
createContext: () => ({
Provider: ({ children }: any) => children,
Consumer: ({ children }: any) => children(null),
}),
useContextSelector: vi.fn(),
}))
// mock service/common functions
vi.mock('@/service/common', () => ({
fetchDefaultModal: vi.fn(),
fetchModelList: vi.fn(),
@ -30,63 +33,129 @@ vi.mock('@/service/common', () => ({
vi.mock('@/service/use-common', () => ({
commonQueryKeys: {
modelProviders: ['common', 'model-providers'],
modelList: (type: string) => ['model-list', type],
modelProviders: ['model-providers'],
defaultModel: (type: string) => ['default-model', type],
},
}))
// mock context hooks
vi.mock('@/context/i18n', () => ({
useLocale: vi.fn(() => 'en-US'),
}))
vi.mock('@/context/provider-context', () => ({
useProviderContext: vi.fn(),
}))
const { useQuery } = await import('@tanstack/react-query')
const { fetchModelList, fetchModelProviderCredentials } = await import('@/service/common')
vi.mock('@/context/modal-context', () => ({
useModalContextSelector: vi.fn(),
}))
vi.mock('@/context/event-emitter', () => ({
useEventEmitterContextContext: vi.fn(),
}))
// mock plugins
vi.mock('@/app/components/plugins/marketplace/hooks', () => ({
useMarketplacePlugins: vi.fn(),
}))
vi.mock('@/app/components/plugins/marketplace/utils', () => ({
getMarketplacePluginsByCollectionId: vi.fn(),
}))
vi.mock('./provider-added-card', () => ({
default: vi.fn(),
}))
afterAll(() => {
vi.resetModules()
vi.clearAllMocks()
})
describe('useLanguage', () => {
it('should replace hyphen with underscore in locale', () => {
;(useLocale as Mock).mockReturnValue('en-US')
const { result } = renderHook(() => useLanguage())
expect(result.current).toBe('en_US')
describe('hooks', () => {
afterEach(() => {
vi.clearAllMocks()
})
it('should return locale as is if no hyphen exists', () => {
;(useLocale as Mock).mockReturnValue('enUS')
describe('useLanguage', () => {
it('should replace hyphen with underscore in locale', () => {
;(useLocale as Mock).mockReturnValue('en-US')
const { result } = renderHook(() => useLanguage())
expect(result.current).toBe('en_US')
})
const { result } = renderHook(() => useLanguage())
expect(result.current).toBe('enUS')
it('should return locale as is if no hyphen exists', () => {
;(useLocale as Mock).mockReturnValue('enUS')
const { result } = renderHook(() => useLanguage())
expect(result.current).toBe('enUS')
})
})
it('should handle multiple hyphens', () => {
;(useLocale as Mock).mockReturnValue('zh-Hans-CN')
describe('useSystemDefaultModelAndModelList', () => {
it('should return default model state', () => {
const defaultModel = {
provider: {
provider: 'openai',
icon_small: { en_US: 'icon', zh_Hans: 'icon' },
},
model: 'gpt-3.5',
model_type: ModelTypeEnum.textGeneration,
} as unknown as DefaultModelResponse
const modelList = [{ provider: 'openai', models: [{ model: 'gpt-3.5' }] }] as unknown as Model[]
const { result } = renderHook(() => useSystemDefaultModelAndModelList(defaultModel, modelList))
const { result } = renderHook(() => useLanguage())
expect(result.current).toBe('zh_Hans-CN')
expect(result.current[0]).toEqual({ model: 'gpt-3.5', provider: 'openai' })
})
it('should update default model state', () => {
const defaultModel = {
provider: {
provider: 'openai',
icon_small: { en_US: 'icon', zh_Hans: 'icon' },
},
model: 'gpt-3.5',
model_type: ModelTypeEnum.textGeneration,
} as any
const modelList = [{ provider: 'openai', models: [{ model: 'gpt-3.5' }] }] as any
const { result } = renderHook(() => useSystemDefaultModelAndModelList(defaultModel, modelList))
const newModel = { model: 'gpt-4', provider: 'openai' }
act(() => {
result.current[1](newModel)
})
expect(result.current[0]).toEqual(newModel)
})
})
describe('useProviderCredentialsAndLoadBalancing', () => {
it('should fetch predefined credentials', async () => {
(useQuery as Mock).mockReturnValue({
data: { credentials: { key: 'value' }, load_balancing: { enabled: true } },
isPending: false,
})
const { result } = renderHook(() => useProviderCredentialsAndLoadBalancing(
'openai',
ConfigurationMethodEnum.predefinedModel,
true,
undefined,
'cred-id',
))
expect(result.current.credentials).toEqual({ key: 'value' })
expect(result.current.loadBalancing).toEqual({ enabled: true })
expect(fetchModelProviderCredentials).not.toHaveBeenCalled() // useQuery calls it, but we blocked it with mockReturnValue
})
it('should fetch custom credentials', () => {
(useQuery as Mock).mockReturnValue({
data: { credentials: { key: 'value' }, load_balancing: { enabled: true } },
isPending: false,
})
const { result } = renderHook(() => useProviderCredentialsAndLoadBalancing(
'openai',
ConfigurationMethodEnum.customizableModel,
true,
{ __model_name: 'gpt-4', __model_type: ModelTypeEnum.textGeneration },
'cred-id',
))
expect(result.current.credentials).toEqual({
key: 'value',
__model_name: 'gpt-4',
__model_type: ModelTypeEnum.textGeneration,
})
})
})
describe('useModelList', () => {
it('should fetch model list', () => {
(useQuery as Mock).mockReturnValue({
data: { data: [{ model: 'gpt-4' }] },
isPending: false,
refetch: vi.fn(),
})
const { result } = renderHook(() => useModelList(ModelTypeEnum.textGeneration))
expect(result.current.data).toEqual([{ model: 'gpt-4' }])
expect(fetchModelList).not.toHaveBeenCalled()
})
})
})

View File

@ -0,0 +1,199 @@
import { act, render, screen } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
import {
CurrentSystemQuotaTypeEnum,
CustomConfigurationStatusEnum,
QuotaUnitEnum,
} from './declarations'
import ModelProviderPage from './index'
vi.mock('@/context/app-context', () => ({
useAppContext: () => ({
mutateCurrentWorkspace: vi.fn(),
isValidatingCurrentWorkspace: false,
}),
}))
const mockGlobalState = {
systemFeatures: { enable_marketplace: true },
}
const mockQuotaConfig = {
quota_type: CurrentSystemQuotaTypeEnum.free,
quota_unit: QuotaUnitEnum.times,
quota_limit: 100,
quota_used: 1,
last_used: 0,
is_valid: true,
}
vi.mock('@/context/global-public-context', () => ({
useGlobalPublicStore: (selector: (s: { systemFeatures: { enable_marketplace: boolean } }) => unknown) => selector(mockGlobalState),
}))
const mockProviders = [
{
provider: 'openai',
label: { en_US: 'OpenAI' },
custom_configuration: { status: CustomConfigurationStatusEnum.active },
system_configuration: {
enabled: false,
current_quota_type: CurrentSystemQuotaTypeEnum.free,
quota_configurations: [mockQuotaConfig],
},
},
{
provider: 'anthropic',
label: { en_US: 'Anthropic' },
custom_configuration: { status: CustomConfigurationStatusEnum.noConfigure },
system_configuration: {
enabled: false,
current_quota_type: CurrentSystemQuotaTypeEnum.free,
quota_configurations: [mockQuotaConfig],
},
},
]
vi.mock('@/context/provider-context', () => ({
useProviderContext: () => ({
modelProviders: mockProviders,
}),
}))
const mockDefaultModelState = {
data: null,
isLoading: false,
}
vi.mock('./hooks', () => ({
useDefaultModel: () => mockDefaultModelState,
}))
vi.mock('./install-from-marketplace', () => ({
default: () => <div data-testid="install-from-marketplace" />,
}))
vi.mock('./provider-added-card', () => ({
default: ({ provider }: { provider: { provider: string } }) => <div data-testid="provider-card">{provider.provider}</div>,
}))
vi.mock('./provider-added-card/quota-panel', () => ({
default: () => <div data-testid="quota-panel" />,
}))
vi.mock('./system-model-selector', () => ({
default: () => <div data-testid="system-model-selector" />,
}))
describe('ModelProviderPage', () => {
beforeEach(() => {
vi.useFakeTimers()
vi.clearAllMocks()
mockGlobalState.systemFeatures.enable_marketplace = true
mockDefaultModelState.data = null
mockDefaultModelState.isLoading = false
mockProviders.splice(0, mockProviders.length, {
provider: 'openai',
label: { en_US: 'OpenAI' },
custom_configuration: { status: CustomConfigurationStatusEnum.active },
system_configuration: {
enabled: false,
current_quota_type: CurrentSystemQuotaTypeEnum.free,
quota_configurations: [mockQuotaConfig],
},
}, {
provider: 'anthropic',
label: { en_US: 'Anthropic' },
custom_configuration: { status: CustomConfigurationStatusEnum.noConfigure },
system_configuration: {
enabled: false,
current_quota_type: CurrentSystemQuotaTypeEnum.free,
quota_configurations: [mockQuotaConfig],
},
})
})
afterEach(() => {
vi.useRealTimers()
})
it('should render main elements', () => {
render(<ModelProviderPage searchText="" />)
expect(screen.getByText('common.modelProvider.models')).toBeInTheDocument()
expect(screen.getByTestId('system-model-selector')).toBeInTheDocument()
expect(screen.getByTestId('install-from-marketplace')).toBeInTheDocument()
})
it('should render configured and not configured providers sections', () => {
render(<ModelProviderPage searchText="" />)
expect(screen.getByText('openai')).toBeInTheDocument()
expect(screen.getByText('common.modelProvider.toBeConfigured')).toBeInTheDocument()
expect(screen.getByText('anthropic')).toBeInTheDocument()
})
it('should filter providers based on search text', () => {
render(<ModelProviderPage searchText="open" />)
act(() => {
vi.advanceTimersByTime(600)
})
expect(screen.getByText('openai')).toBeInTheDocument()
expect(screen.queryByText('anthropic')).not.toBeInTheDocument()
})
it('should show empty state if no configured providers match', () => {
render(<ModelProviderPage searchText="non-existent" />)
act(() => {
vi.advanceTimersByTime(600)
})
expect(screen.getByText('common.modelProvider.emptyProviderTitle')).toBeInTheDocument()
})
it('should hide marketplace section when marketplace feature is disabled', () => {
mockGlobalState.systemFeatures.enable_marketplace = false
render(<ModelProviderPage searchText="" />)
expect(screen.queryByTestId('install-from-marketplace')).not.toBeInTheDocument()
})
it('should prioritize fixed providers in visible order', () => {
mockProviders.splice(0, mockProviders.length, {
provider: 'zeta-provider',
label: { en_US: 'Zeta Provider' },
custom_configuration: { status: CustomConfigurationStatusEnum.active },
system_configuration: {
enabled: false,
current_quota_type: CurrentSystemQuotaTypeEnum.free,
quota_configurations: [mockQuotaConfig],
},
}, {
provider: 'langgenius/anthropic/anthropic',
label: { en_US: 'Anthropic Fixed' },
custom_configuration: { status: CustomConfigurationStatusEnum.active },
system_configuration: {
enabled: false,
current_quota_type: CurrentSystemQuotaTypeEnum.free,
quota_configurations: [mockQuotaConfig],
},
}, {
provider: 'langgenius/openai/openai',
label: { en_US: 'OpenAI Fixed' },
custom_configuration: { status: CustomConfigurationStatusEnum.noConfigure },
system_configuration: {
enabled: true,
current_quota_type: CurrentSystemQuotaTypeEnum.free,
quota_configurations: [mockQuotaConfig],
},
})
render(<ModelProviderPage searchText="" />)
const renderedProviders = screen.getAllByTestId('provider-card').map(item => item.textContent)
expect(renderedProviders).toEqual([
'langgenius/openai/openai',
'langgenius/anthropic/anthropic',
'zeta-provider',
])
expect(screen.queryByText('common.modelProvider.toBeConfigured')).not.toBeInTheDocument()
})
})

View File

@ -0,0 +1,109 @@
import type { Mock } from 'vitest'
import type { ModelProvider } from './declarations'
import { fireEvent, render, screen } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
import { useMarketplaceAllPlugins } from './hooks'
import InstallFromMarketplace from './install-from-marketplace'
// Mock dependencies
vi.mock('next/link', () => ({
default: ({ children, href }: { children: React.ReactNode, href: string }) => <a href={href}>{children}</a>,
}))
vi.mock('next-themes', () => ({
useTheme: () => ({ theme: 'light' }),
}))
vi.mock('@/app/components/base/divider', () => ({
default: () => <div data-testid="divider" />,
}))
vi.mock('@/app/components/base/loading', () => ({
default: () => <div data-testid="loading" />,
}))
vi.mock('@/app/components/plugins/marketplace/list', () => ({
default: ({ plugins, cardRender }: { plugins: { plugin_id: string, name: string, type?: string }[], cardRender: (plugin: { plugin_id: string, name: string, type?: string }) => React.ReactNode }) => (
<div data-testid="plugin-list">
{plugins.map(p => (
<div key={p.plugin_id} data-testid="plugin-item">
{cardRender(p)}
</div>
))}
</div>
),
}))
vi.mock('@/app/components/plugins/provider-card', () => ({
default: ({ payload }: { payload: { name: string } }) => <div>{payload.name}</div>,
}))
vi.mock('./hooks', () => ({
useMarketplaceAllPlugins: vi.fn(() => ({
plugins: [],
isLoading: false,
})),
}))
describe('InstallFromMarketplace', () => {
const mockProviders = [] as ModelProvider[]
beforeEach(() => {
vi.clearAllMocks()
})
it('should render expanded by default', () => {
render(<InstallFromMarketplace providers={mockProviders} searchText="" />)
expect(screen.getByText('common.modelProvider.installProvider')).toBeInTheDocument()
expect(screen.getByTestId('plugin-list')).toBeInTheDocument()
})
it('should collapse when clicked', () => {
render(<InstallFromMarketplace providers={mockProviders} searchText="" />)
fireEvent.click(screen.getByText('common.modelProvider.installProvider'))
expect(screen.queryByTestId('plugin-list')).not.toBeInTheDocument()
})
it('should show loading state', () => {
(useMarketplaceAllPlugins as unknown as Mock).mockReturnValue({
plugins: [],
isLoading: true,
})
render(<InstallFromMarketplace providers={mockProviders} searchText="" />)
// It's expanded by default, so loading should show immediately
expect(screen.getByTestId('loading')).toBeInTheDocument()
})
it('should list plugins', () => {
(useMarketplaceAllPlugins as unknown as Mock).mockReturnValue({
plugins: [{ plugin_id: '1', name: 'Plugin 1' }],
isLoading: false,
})
render(<InstallFromMarketplace providers={mockProviders} searchText="" />)
// Expanded by default
expect(screen.getByText('Plugin 1')).toBeInTheDocument()
})
it('should hide bundle plugins from the list', () => {
(useMarketplaceAllPlugins as unknown as Mock).mockReturnValue({
plugins: [
{ plugin_id: '1', name: 'Plugin 1', type: 'plugin' },
{ plugin_id: '2', name: 'Bundle 1', type: 'bundle' },
],
isLoading: false,
})
render(<InstallFromMarketplace providers={mockProviders} searchText="" />)
expect(screen.getByText('Plugin 1')).toBeInTheDocument()
expect(screen.queryByText('Bundle 1')).not.toBeInTheDocument()
})
it('should render discovery link', () => {
render(<InstallFromMarketplace providers={mockProviders} searchText="" />)
expect(screen.getByText('plugin.marketplace.difyMarketplace')).toHaveAttribute('href')
})
})

View File

@ -0,0 +1,61 @@
import { fireEvent, render, screen } from '@testing-library/react'
import DeprecatedModelTrigger from './deprecated-model-trigger'
vi.mock('../model-icon', () => ({
default: ({ modelName }: { modelName: string }) => <span>{modelName}</span>,
}))
const mockUseProviderContext = vi.hoisted(() => vi.fn())
vi.mock('@/context/provider-context', () => ({
useProviderContext: mockUseProviderContext,
}))
describe('DeprecatedModelTrigger', () => {
beforeEach(() => {
vi.clearAllMocks()
mockUseProviderContext.mockReturnValue({
modelProviders: [{ provider: 'someone-else' }, { provider: 'openai' }],
})
})
it('should render model name', () => {
render(<DeprecatedModelTrigger modelName="gpt-deprecated" providerName="openai" />)
expect(screen.getAllByText('gpt-deprecated').length).toBeGreaterThan(0)
})
it('should show deprecated tooltip when warn icon is hovered', async () => {
const { container } = render(
<DeprecatedModelTrigger
modelName="gpt-deprecated"
providerName="openai"
showWarnIcon
/>,
)
const tooltipTrigger = container.querySelector('[data-state]') as HTMLElement
fireEvent.mouseEnter(tooltipTrigger)
expect(await screen.findByText('common.modelProvider.deprecated')).toBeInTheDocument()
})
it('should render when provider is not found', () => {
mockUseProviderContext.mockReturnValue({
modelProviders: [{ provider: 'someone-else' }],
})
render(<DeprecatedModelTrigger modelName="gpt-deprecated" providerName="openai" />)
expect(screen.getAllByText('gpt-deprecated').length).toBeGreaterThan(0)
})
it('should not show deprecated tooltip when warn icon is disabled', async () => {
render(
<DeprecatedModelTrigger
modelName="gpt-deprecated"
providerName="openai"
showWarnIcon={false}
/>,
)
expect(screen.queryByText('common.modelProvider.deprecated')).not.toBeInTheDocument()
})
})

View File

@ -0,0 +1,13 @@
import { render, screen } from '@testing-library/react'
import EmptyTrigger from './empty-trigger'
describe('EmptyTrigger', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('should render configure model text', () => {
render(<EmptyTrigger open={false} />)
expect(screen.getByText('plugin.detailPanel.configureModel')).toBeInTheDocument()
})
})

View File

@ -0,0 +1,50 @@
import { fireEvent, render, screen } from '@testing-library/react'
import {
ModelFeatureEnum,
ModelFeatureTextEnum,
} from '../declarations'
import FeatureIcon from './feature-icon'
describe('FeatureIcon', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('should show feature label when showFeaturesLabel is true', () => {
render(
<>
<FeatureIcon feature={ModelFeatureEnum.vision} showFeaturesLabel />
<FeatureIcon feature={ModelFeatureEnum.document} showFeaturesLabel />
<FeatureIcon feature={ModelFeatureEnum.audio} showFeaturesLabel />
<FeatureIcon feature={ModelFeatureEnum.video} showFeaturesLabel />
</>,
)
expect(screen.getByText(ModelFeatureTextEnum.vision)).toBeInTheDocument()
expect(screen.getByText(ModelFeatureTextEnum.document)).toBeInTheDocument()
expect(screen.getByText(ModelFeatureTextEnum.audio)).toBeInTheDocument()
expect(screen.getByText(ModelFeatureTextEnum.video)).toBeInTheDocument()
})
it('should show tooltip content on hover when showFeaturesLabel is false', async () => {
const cases: Array<{ feature: ModelFeatureEnum, text: string }> = [
{ feature: ModelFeatureEnum.vision, text: ModelFeatureTextEnum.vision },
{ feature: ModelFeatureEnum.document, text: ModelFeatureTextEnum.document },
{ feature: ModelFeatureEnum.audio, text: ModelFeatureTextEnum.audio },
{ feature: ModelFeatureEnum.video, text: ModelFeatureTextEnum.video },
]
for (const { feature, text } of cases) {
const { container, unmount } = render(<FeatureIcon feature={feature} />)
fireEvent.mouseEnter(container.firstElementChild as HTMLElement)
expect(await screen.findByText(`common.modelProvider.featureSupported:{"feature":"${text}"}`))
.toBeInTheDocument()
unmount()
}
})
it('should render nothing for unsupported feature', () => {
const { container } = render(<FeatureIcon feature={ModelFeatureEnum.toolCall} />)
expect(container).toBeEmptyDOMElement()
})
})

View File

@ -0,0 +1,126 @@
import type { Model, ModelItem } from '../declarations'
import { fireEvent, render, screen } from '@testing-library/react'
import {
ConfigurationMethodEnum,
ModelStatusEnum,
ModelTypeEnum,
} from '../declarations'
import ModelSelector from './index'
vi.mock('./model-trigger', () => ({
default: () => <div>model-trigger</div>,
}))
vi.mock('./deprecated-model-trigger', () => ({
default: ({ modelName }: { modelName: string }) => <div>{`deprecated:${modelName}`}</div>,
}))
vi.mock('./empty-trigger', () => ({
default: () => <div>empty-trigger</div>,
}))
vi.mock('./popup', () => ({
default: ({ onHide, onSelect }: { onHide: () => void, onSelect: (provider: string, model: ModelItem) => void }) => (
<>
<button type="button" onClick={() => onSelect('openai', { model: 'gpt-4' } as ModelItem)}>
select
</button>
<button type="button" onClick={onHide}>
hide
</button>
</>
),
}))
const makeModelItem = (overrides: Partial<ModelItem> = {}): ModelItem => ({
model: 'gpt-4',
label: { en_US: 'GPT-4', zh_Hans: 'GPT-4' },
model_type: ModelTypeEnum.textGeneration,
fetch_from: ConfigurationMethodEnum.predefinedModel,
status: ModelStatusEnum.active,
model_properties: {},
load_balancing_enabled: false,
...overrides,
})
const makeModel = (overrides: Partial<Model> = {}): Model => ({
provider: 'openai',
icon_small: { en_US: '', zh_Hans: '' },
label: { en_US: 'OpenAI', zh_Hans: 'OpenAI' },
models: [makeModelItem()],
status: ModelStatusEnum.active,
...overrides,
})
describe('ModelSelector', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('should toggle popup and close it after selecting a model', () => {
render(<ModelSelector modelList={[makeModel()]} />)
fireEvent.click(screen.getByText('empty-trigger'))
expect(screen.getByText('select')).toBeInTheDocument()
fireEvent.click(screen.getByText('select'))
expect(screen.queryByText('select')).not.toBeInTheDocument()
})
it('should call onSelect when provided', () => {
const onSelect = vi.fn()
render(<ModelSelector modelList={[makeModel()]} onSelect={onSelect} />)
fireEvent.click(screen.getByText('empty-trigger'))
fireEvent.click(screen.getByText('select'))
expect(onSelect).toHaveBeenCalledWith({ provider: 'openai', model: 'gpt-4' })
})
it('should close popup when popup requests hide', () => {
render(<ModelSelector modelList={[makeModel()]} />)
fireEvent.click(screen.getByText('empty-trigger'))
expect(screen.getByText('hide')).toBeInTheDocument()
fireEvent.click(screen.getByText('hide'))
expect(screen.queryByText('hide')).not.toBeInTheDocument()
})
it('should not open popup when readonly', () => {
render(<ModelSelector modelList={[makeModel()]} readonly />)
fireEvent.click(screen.getByText('empty-trigger'))
expect(screen.queryByText('select')).not.toBeInTheDocument()
})
it('should render deprecated trigger when defaultModel is not in list', () => {
const { rerender } = render(
<ModelSelector
defaultModel={{ provider: 'openai', model: 'missing-model' }}
modelList={[makeModel()]}
/>,
)
expect(screen.getByText('deprecated:missing-model')).toBeInTheDocument()
rerender(
<ModelSelector
defaultModel={{ provider: '', model: '' }}
modelList={[makeModel()]}
/>,
)
expect(screen.getByText('deprecated:')).toBeInTheDocument()
})
it('should render model trigger when defaultModel matches', () => {
render(
<ModelSelector
defaultModel={{ provider: 'openai', model: 'gpt-4' }}
modelList={[makeModel()]}
/>,
)
expect(screen.getByText('model-trigger')).toBeInTheDocument()
})
})

View File

@ -0,0 +1,91 @@
import type { Model, ModelItem } from '../declarations'
import { fireEvent, render, screen } from '@testing-library/react'
import {
ConfigurationMethodEnum,
ModelStatusEnum,
ModelTypeEnum,
} from '../declarations'
import ModelTrigger from './model-trigger'
vi.mock('../hooks', async () => {
const actual = await vi.importActual<typeof import('../hooks')>('../hooks')
return {
...actual,
useLanguage: () => 'en_US',
}
})
vi.mock('../model-icon', () => ({
default: ({ modelName }: { modelName: string }) => <span>{modelName}</span>,
}))
vi.mock('../model-name', () => ({
default: ({ modelItem }: { modelItem: ModelItem }) => <span>{modelItem.label.en_US}</span>,
}))
const makeModelItem = (overrides: Partial<ModelItem> = {}): ModelItem => ({
model: 'gpt-4',
label: { en_US: 'GPT-4', zh_Hans: 'GPT-4' },
model_type: ModelTypeEnum.textGeneration,
fetch_from: ConfigurationMethodEnum.predefinedModel,
status: ModelStatusEnum.active,
model_properties: {},
load_balancing_enabled: false,
...overrides,
})
const makeModel = (overrides: Partial<Model> = {}): Model => ({
provider: 'openai',
icon_small: { en_US: '', zh_Hans: '' },
label: { en_US: 'OpenAI', zh_Hans: 'OpenAI' },
models: [makeModelItem()],
status: ModelStatusEnum.active,
...overrides,
})
describe('ModelTrigger', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('should show model name', () => {
render(
<ModelTrigger
open
provider={makeModel()}
model={makeModelItem()}
/>,
)
expect(screen.getByText('GPT-4')).toBeInTheDocument()
})
it('should show status tooltip content when model is not active', async () => {
const { container } = render(
<ModelTrigger
open={false}
provider={makeModel()}
model={makeModelItem({ status: ModelStatusEnum.noConfigure })}
/>,
)
const tooltipTrigger = container.querySelector('[data-state]') as HTMLElement
fireEvent.mouseEnter(tooltipTrigger)
expect(await screen.findByText('No Configure')).toBeInTheDocument()
})
it('should not show status icon when readonly', () => {
render(
<ModelTrigger
open={false}
provider={makeModel()}
model={makeModelItem({ status: ModelStatusEnum.noConfigure })}
readonly
/>,
)
expect(screen.getByText('GPT-4')).toBeInTheDocument()
expect(screen.queryByText('No Configure')).not.toBeInTheDocument()
})
})

View File

@ -0,0 +1,147 @@
import type { DefaultModel, Model, ModelItem } from '../declarations'
import { fireEvent, render, screen } from '@testing-library/react'
import {
ConfigurationMethodEnum,
ModelFeatureEnum,
ModelStatusEnum,
ModelTypeEnum,
} from '../declarations'
import PopupItem from './popup-item'
const mockUpdateModelList = vi.hoisted(() => vi.fn())
const mockUpdateModelProviders = vi.hoisted(() => vi.fn())
vi.mock('../hooks', async () => {
const actual = await vi.importActual<typeof import('../hooks')>('../hooks')
return {
...actual,
useLanguage: () => 'en_US',
useUpdateModelList: () => mockUpdateModelList,
useUpdateModelProviders: () => mockUpdateModelProviders,
}
})
vi.mock('../model-badge', () => ({
default: ({ children }: { children: React.ReactNode }) => <span>{children}</span>,
}))
vi.mock('../model-icon', () => ({
default: ({ modelName }: { modelName: string }) => <span>{modelName}</span>,
}))
vi.mock('../model-name', () => ({
default: ({ modelItem }: { modelItem: ModelItem }) => <span>{modelItem.label.en_US}</span>,
}))
const mockSetShowModelModal = vi.hoisted(() => vi.fn())
vi.mock('@/context/modal-context', () => ({
useModalContext: () => ({
setShowModelModal: mockSetShowModelModal,
}),
}))
const mockUseProviderContext = vi.hoisted(() => vi.fn())
vi.mock('@/context/provider-context', () => ({
useProviderContext: mockUseProviderContext,
}))
const makeModelItem = (overrides: Partial<ModelItem> = {}): ModelItem => ({
model: 'gpt-4',
label: { en_US: 'GPT-4', zh_Hans: 'GPT-4' },
model_type: ModelTypeEnum.textGeneration,
features: [ModelFeatureEnum.vision],
fetch_from: ConfigurationMethodEnum.predefinedModel,
status: ModelStatusEnum.active,
model_properties: { mode: 'chat', context_size: 4096 },
load_balancing_enabled: false,
...overrides,
})
const makeModel = (overrides: Partial<Model> = {}): Model => ({
provider: 'openai',
icon_small: { en_US: '', zh_Hans: '' },
label: { en_US: 'OpenAI', zh_Hans: 'OpenAI' },
models: [makeModelItem()],
status: ModelStatusEnum.active,
...overrides,
})
describe('PopupItem', () => {
beforeEach(() => {
vi.clearAllMocks()
mockUseProviderContext.mockReturnValue({
modelProviders: [{ provider: 'openai' }],
})
})
it('should call onSelect when clicking an active model', () => {
const onSelect = vi.fn()
render(<PopupItem model={makeModel()} onSelect={onSelect} />)
fireEvent.click(screen.getByText('GPT-4'))
expect(onSelect).toHaveBeenCalledWith('openai', expect.objectContaining({ model: 'gpt-4' }))
})
it('should not call onSelect when model is not active', () => {
const onSelect = vi.fn()
render(
<PopupItem
model={makeModel({ models: [makeModelItem({ status: ModelStatusEnum.disabled })] })}
onSelect={onSelect}
/>,
)
fireEvent.click(screen.getByText('GPT-4'))
expect(onSelect).not.toHaveBeenCalled()
})
it('should open model modal when clicking add on unconfigured model', () => {
const { rerender } = render(
<PopupItem
model={makeModel({ models: [makeModelItem({ status: ModelStatusEnum.noConfigure })] })}
onSelect={vi.fn()}
/>,
)
fireEvent.click(screen.getByText('COMMON.OPERATION.ADD'))
expect(mockSetShowModelModal).toHaveBeenCalled()
const call = mockSetShowModelModal.mock.calls[0][0] as { onSaveCallback?: () => void }
call.onSaveCallback?.()
expect(mockUpdateModelProviders).toHaveBeenCalled()
expect(mockUpdateModelList).toHaveBeenCalledWith(ModelTypeEnum.textGeneration)
rerender(
<PopupItem
model={makeModel({
models: [makeModelItem({ status: ModelStatusEnum.noConfigure, model_type: undefined as unknown as ModelTypeEnum })],
})}
onSelect={vi.fn()}
/>,
)
fireEvent.click(screen.getByText('COMMON.OPERATION.ADD'))
const call2 = mockSetShowModelModal.mock.calls.at(-1)?.[0] as { onSaveCallback?: () => void } | undefined
call2?.onSaveCallback?.()
expect(mockUpdateModelProviders).toHaveBeenCalled()
expect(mockUpdateModelList).toHaveBeenCalledTimes(1)
})
it('should show selected state when defaultModel matches', () => {
const defaultModel: DefaultModel = { provider: 'openai', model: 'gpt-4' }
render(
<PopupItem
defaultModel={defaultModel}
model={makeModel()}
onSelect={vi.fn()}
/>,
)
expect(screen.getByText('GPT-4')).toBeInTheDocument()
})
})

View File

@ -0,0 +1,199 @@
import type { Model, ModelItem } from '../declarations'
import { fireEvent, render, screen } from '@testing-library/react'
import {
ConfigurationMethodEnum,
ModelFeatureEnum,
ModelStatusEnum,
ModelTypeEnum,
} from '../declarations'
import Popup from './popup'
let mockLanguage = 'en_US'
const mockSetShowAccountSettingModal = vi.hoisted(() => vi.fn())
vi.mock('@/context/modal-context', () => ({
useModalContext: () => ({
setShowAccountSettingModal: mockSetShowAccountSettingModal,
}),
}))
const mockSupportFunctionCall = vi.hoisted(() => vi.fn())
vi.mock('@/utils/tool-call', () => ({
supportFunctionCall: mockSupportFunctionCall,
}))
const mockCloseActiveTooltip = vi.hoisted(() => vi.fn())
vi.mock('@/app/components/base/tooltip/TooltipManager', () => ({
tooltipManager: {
closeActiveTooltip: mockCloseActiveTooltip,
register: vi.fn(),
clear: vi.fn(),
},
}))
vi.mock('@/app/components/base/icons/src/vender/solid/general', () => ({
XCircle: ({ onClick }: { onClick?: () => void }) => (
<button type="button" aria-label="clear-search" onClick={onClick} />
),
}))
vi.mock('../hooks', async () => {
const actual = await vi.importActual<typeof import('../hooks')>('../hooks')
return {
...actual,
useLanguage: () => mockLanguage,
}
})
vi.mock('./popup-item', () => ({
default: ({ model }: { model: Model }) => <div>{model.provider}</div>,
}))
const makeModelItem = (overrides: Partial<ModelItem> = {}): ModelItem => ({
model: 'gpt-4',
label: { en_US: 'GPT-4', zh_Hans: 'GPT-4' },
model_type: ModelTypeEnum.textGeneration,
fetch_from: ConfigurationMethodEnum.predefinedModel,
status: ModelStatusEnum.active,
model_properties: {},
load_balancing_enabled: false,
...overrides,
})
const makeModel = (overrides: Partial<Model> = {}): Model => ({
provider: 'openai',
icon_small: { en_US: '', zh_Hans: '' },
label: { en_US: 'OpenAI', zh_Hans: 'OpenAI' },
models: [makeModelItem()],
status: ModelStatusEnum.active,
...overrides,
})
describe('Popup', () => {
beforeEach(() => {
vi.clearAllMocks()
mockLanguage = 'en_US'
mockSupportFunctionCall.mockReturnValue(true)
})
it('should filter models by search and allow clearing search', () => {
render(
<Popup
modelList={[makeModel()]}
onSelect={vi.fn()}
onHide={vi.fn()}
/>,
)
expect(screen.getByText('openai')).toBeInTheDocument()
const input = screen.getByPlaceholderText('datasetSettings.form.searchModel')
fireEvent.change(input, { target: { value: 'not-found' } })
expect(screen.getByText('No model found for “not-found”')).toBeInTheDocument()
fireEvent.click(screen.getByRole('button', { name: 'clear-search' }))
expect((input as HTMLInputElement).value).toBe('')
})
it('should filter by scope features including toolCall and non-toolCall checks', () => {
const modelList = [
makeModel({ models: [makeModelItem({ features: [ModelFeatureEnum.toolCall, ModelFeatureEnum.vision] })] }),
]
// When tool-call support is missing, it should be filtered out.
mockSupportFunctionCall.mockReturnValue(false)
const { unmount } = render(
<Popup
modelList={modelList}
onSelect={vi.fn()}
onHide={vi.fn()}
scopeFeatures={[ModelFeatureEnum.toolCall, ModelFeatureEnum.vision]}
/>,
)
expect(screen.getByText('No model found for “”')).toBeInTheDocument()
// When tool-call support exists, the non-toolCall feature check should also pass.
unmount()
mockSupportFunctionCall.mockReturnValue(true)
const { unmount: unmount2 } = render(
<Popup
modelList={modelList}
onSelect={vi.fn()}
onHide={vi.fn()}
scopeFeatures={[ModelFeatureEnum.toolCall, ModelFeatureEnum.vision]}
/>,
)
expect(screen.getByText('openai')).toBeInTheDocument()
unmount2()
const { unmount: unmount3 } = render(
<Popup
modelList={modelList}
onSelect={vi.fn()}
onHide={vi.fn()}
scopeFeatures={[ModelFeatureEnum.vision]}
/>,
)
expect(screen.getByText('openai')).toBeInTheDocument()
// When features are missing, non-toolCall feature checks should fail.
unmount3()
render(
<Popup
modelList={[makeModel({ models: [makeModelItem({ features: undefined })] })]}
onSelect={vi.fn()}
onHide={vi.fn()}
scopeFeatures={[ModelFeatureEnum.vision]}
/>,
)
expect(screen.getByText('No model found for “”')).toBeInTheDocument()
})
it('should match labels from other languages when current language key is missing', () => {
mockLanguage = 'fr_FR'
render(
<Popup
modelList={[makeModel()]}
onSelect={vi.fn()}
onHide={vi.fn()}
/>,
)
fireEvent.change(
screen.getByPlaceholderText('datasetSettings.form.searchModel'),
{ target: { value: 'gpt' } },
)
expect(screen.getByText('openai')).toBeInTheDocument()
})
it('should close tooltip on scroll', () => {
const { container } = render(
<Popup
modelList={[makeModel()]}
onSelect={vi.fn()}
onHide={vi.fn()}
/>,
)
fireEvent.scroll(container.firstElementChild as HTMLElement)
expect(mockCloseActiveTooltip).toHaveBeenCalled()
})
it('should open provider settings when clicking footer link', () => {
render(
<Popup
modelList={[makeModel()]}
onSelect={vi.fn()}
onHide={vi.fn()}
/>,
)
fireEvent.click(screen.getByText('common.model.settingsLink'))
expect(mockSetShowAccountSettingModal).toHaveBeenCalledWith({
payload: 'provider',
})
})
})

View File

@ -0,0 +1,97 @@
import type { ModelProvider } from '../declarations'
import { render, screen } from '@testing-library/react'
import useTheme from '@/hooks/use-theme'
import { Theme } from '@/types/app'
import { useLanguage } from '../hooks'
import ProviderIcon from './index'
type UseThemeReturnType = ReturnType<typeof useTheme>
vi.mock('@/app/components/base/icons/src/public/llm', () => ({
AnthropicDark: ({ className }: { className: string }) => <div data-testid="anthropic-dark" className={className} />,
AnthropicLight: ({ className }: { className: string }) => <div data-testid="anthropic-light" className={className} />,
}))
vi.mock('@/app/components/base/icons/src/vender/other', () => ({
Openai: ({ className }: { className: string }) => <div data-testid="openai-icon" className={className} />,
}))
vi.mock('@/i18n-config', () => ({
renderI18nObject: (obj: Record<string, string> | string, lang: string) => {
if (typeof obj === 'string')
return obj
return obj[lang] || obj.en_US || Object.values(obj)[0]
},
}))
vi.mock('@/hooks/use-theme', () => {
const mockFn = vi.fn(() => ({ theme: Theme.light }))
return { default: mockFn }
})
vi.mock('../hooks', () => ({
useLanguage: vi.fn(() => 'en_US'),
}))
const createProvider = (overrides: Partial<ModelProvider> = {}): ModelProvider => ({
provider: 'some/provider',
label: { en_US: 'Provider', zh_Hans: '提供商' },
help: { title: { en_US: 'Help', zh_Hans: '帮助' }, url: { en_US: 'https://example.com', zh_Hans: 'https://example.com' } },
icon_small: { en_US: 'https://example.com/icon.png', zh_Hans: 'https://example.com/icon.png' },
supported_model_types: [],
configurate_methods: [],
provider_credential_schema: { credential_form_schemas: [] },
model_credential_schema: { model: { label: { en_US: 'Model', zh_Hans: '模型' }, placeholder: { en_US: 'Select', zh_Hans: '选择' } }, credential_form_schemas: [] },
preferred_provider_type: undefined,
...overrides,
} as ModelProvider)
describe('ProviderIcon', () => {
beforeEach(() => {
vi.clearAllMocks()
const mockTheme = vi.mocked(useTheme)
const mockLang = vi.mocked(useLanguage)
mockTheme.mockReturnValue({ theme: Theme.light, themes: [], setTheme: vi.fn() } as UseThemeReturnType)
mockLang.mockReturnValue('en_US')
})
it('should render Anthropic icon based on theme', () => {
const mockTheme = vi.mocked(useTheme)
mockTheme.mockReturnValue({ theme: Theme.dark, themes: [], setTheme: vi.fn() } as UseThemeReturnType)
const provider = createProvider({ provider: 'langgenius/anthropic/anthropic' })
render(<ProviderIcon provider={provider} />)
expect(screen.getByTestId('anthropic-light')).toBeInTheDocument()
mockTheme.mockReturnValue({ theme: Theme.light, themes: [], setTheme: vi.fn() } as UseThemeReturnType)
render(<ProviderIcon provider={provider} />)
expect(screen.getByTestId('anthropic-dark')).toBeInTheDocument()
})
it('should render OpenAI icon', () => {
const provider = createProvider({ provider: 'langgenius/openai/openai' })
render(<ProviderIcon provider={provider} />)
expect(screen.getByTestId('openai-icon')).toBeInTheDocument()
})
it('should render generic provider with image and label', () => {
const provider = createProvider({ label: { en_US: 'Custom', zh_Hans: '自定义' } })
render(<ProviderIcon provider={provider} />)
const img = screen.getByAltText('provider-icon') as HTMLImageElement
expect(img.src).toBe('https://example.com/icon.png')
expect(screen.getByText('Custom')).toBeInTheDocument()
})
it('should use dark icon in dark theme for generic provider', () => {
const mockTheme = vi.mocked(useTheme)
mockTheme.mockReturnValue({ theme: Theme.dark, themes: [], setTheme: vi.fn() } as UseThemeReturnType)
const provider = createProvider({
icon_small_dark: { en_US: 'https://example.com/dark.png', zh_Hans: 'https://example.com/dark.png' },
})
render(<ProviderIcon provider={provider} />)
const img = screen.getByAltText('provider-icon') as HTMLImageElement
expect(img.src).toBe('https://example.com/dark.png')
})
})

View File

@ -0,0 +1,160 @@
import type { DefaultModelResponse } from '../declarations'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { vi } from 'vitest'
import { ModelTypeEnum } from '../declarations'
import SystemModel from './index'
vi.mock('react-i18next', async () => {
const { createReactI18nextMock } = await import('@/test/i18n-mock')
return createReactI18nextMock({
'modelProvider.systemModelSettings': 'System Model Settings',
'modelProvider.systemReasoningModel.key': 'System Reasoning Model',
'modelProvider.systemReasoningModel.tip': 'Reasoning model tip',
'modelProvider.embeddingModel.key': 'Embedding Model',
'modelProvider.embeddingModel.tip': 'Embedding model tip',
'modelProvider.rerankModel.key': 'Rerank Model',
'modelProvider.rerankModel.tip': 'Rerank model tip',
'modelProvider.speechToTextModel.key': 'Speech to Text Model',
'modelProvider.speechToTextModel.tip': 'Speech to text model tip',
'modelProvider.ttsModel.key': 'TTS Model',
'modelProvider.ttsModel.tip': 'TTS model tip',
'operation.cancel': 'Cancel',
'operation.save': 'Save',
'actionMsg.modifiedSuccessfully': 'Modified successfully',
})
})
const mockNotify = vi.hoisted(() => vi.fn())
const mockUpdateModelList = vi.hoisted(() => vi.fn())
const mockUpdateDefaultModel = vi.hoisted(() => vi.fn(() => Promise.resolve({ result: 'success' })))
let mockIsCurrentWorkspaceManager = true
vi.mock('@/context/app-context', () => ({
useAppContext: () => ({
isCurrentWorkspaceManager: mockIsCurrentWorkspaceManager,
}),
}))
vi.mock('@/context/provider-context', () => ({
useProviderContext: () => ({
textGenerationModelList: [],
}),
}))
vi.mock('@/app/components/base/toast', () => ({
useToastContext: () => ({
notify: mockNotify,
}),
}))
vi.mock('../hooks', () => ({
useModelList: () => ({
data: [],
}),
useSystemDefaultModelAndModelList: (defaultModel: DefaultModelResponse | undefined) => [
defaultModel || { model: '', provider: { provider: '', icon_small: { en_US: '', zh_Hans: '' } } },
vi.fn(),
],
useUpdateModelList: () => mockUpdateModelList,
}))
vi.mock('@/service/common', () => ({
updateDefaultModel: mockUpdateDefaultModel,
}))
vi.mock('../model-selector', () => ({
default: ({ onSelect }: { onSelect: (model: { model: string, provider: string }) => void }) => (
<button onClick={() => onSelect({ model: 'test', provider: 'test' })}>Mock Model Selector</button>
),
}))
const mockModel: DefaultModelResponse = {
model: 'gpt-4',
model_type: ModelTypeEnum.textGeneration,
provider: {
provider: 'openai',
icon_small: { en_US: '', zh_Hans: '' },
},
}
const defaultProps = {
textGenerationDefaultModel: mockModel,
embeddingsDefaultModel: undefined,
rerankDefaultModel: undefined,
speech2textDefaultModel: undefined,
ttsDefaultModel: undefined,
notConfigured: false,
isLoading: false,
}
describe('SystemModel', () => {
beforeEach(() => {
vi.clearAllMocks()
mockIsCurrentWorkspaceManager = true
})
it('should render settings button', () => {
render(<SystemModel {...defaultProps} />)
expect(screen.getByRole('button', { name: /system model settings/i })).toBeInTheDocument()
})
it('should open modal when button is clicked', async () => {
render(<SystemModel {...defaultProps} />)
const button = screen.getByRole('button', { name: /system model settings/i })
fireEvent.click(button)
await waitFor(() => {
expect(screen.getByText(/system reasoning model/i)).toBeInTheDocument()
})
})
it('should disable button when loading', () => {
render(<SystemModel {...defaultProps} isLoading />)
expect(screen.getByRole('button', { name: /system model settings/i })).toBeDisabled()
})
it('should close modal when cancel is clicked', async () => {
render(<SystemModel {...defaultProps} />)
fireEvent.click(screen.getByRole('button', { name: /system model settings/i }))
await waitFor(() => {
expect(screen.getByRole('button', { name: /cancel/i })).toBeInTheDocument()
})
fireEvent.click(screen.getByRole('button', { name: /cancel/i }))
await waitFor(() => {
expect(screen.queryByRole('button', { name: /cancel/i })).not.toBeInTheDocument()
})
})
it('should save selected models and show success feedback', async () => {
render(<SystemModel {...defaultProps} />)
fireEvent.click(screen.getByRole('button', { name: /system model settings/i }))
await waitFor(() => {
expect(screen.getByRole('button', { name: /save/i })).toBeInTheDocument()
})
const selectorButtons = screen.getAllByRole('button', { name: 'Mock Model Selector' })
selectorButtons.forEach(button => fireEvent.click(button))
fireEvent.click(screen.getByRole('button', { name: /save/i }))
await waitFor(() => {
expect(mockUpdateDefaultModel).toHaveBeenCalledTimes(1)
expect(mockNotify).toHaveBeenCalledWith({
type: 'success',
message: 'Modified successfully',
})
expect(mockUpdateModelList).toHaveBeenCalledTimes(5)
})
})
it('should disable save when user is not workspace manager', async () => {
mockIsCurrentWorkspaceManager = false
render(<SystemModel {...defaultProps} />)
fireEvent.click(screen.getByRole('button', { name: /system model settings/i }))
await waitFor(() => {
expect(screen.getByRole('button', { name: /save/i })).toBeDisabled()
})
})
})

View File

@ -0,0 +1,238 @@
import type { Mock } from 'vitest'
import { describe, expect, it, vi } from 'vitest'
import {
deleteModelProvider,
setModelProvider,
validateModelLoadBalancingCredentials,
validateModelProvider,
} from '@/service/common'
import { ValidatedStatus } from '../key-validator/declarations'
import {
ConfigurationMethodEnum,
FormTypeEnum,
ModelTypeEnum,
} from './declarations'
import {
genModelNameFormSchema,
genModelTypeFormSchema,
modelTypeFormat,
removeCredentials,
saveCredentials,
savePredefinedLoadBalancingConfig,
sizeFormat,
validateCredentials,
validateLoadBalancingCredentials,
} from './utils'
// Mock service/common functions
vi.mock('@/service/common', () => ({
deleteModelProvider: vi.fn(),
setModelProvider: vi.fn(),
validateModelLoadBalancingCredentials: vi.fn(),
validateModelProvider: vi.fn(),
}))
describe('utils', () => {
afterEach(() => {
vi.clearAllMocks()
})
describe('sizeFormat', () => {
it('should format size less than 1000', () => {
expect(sizeFormat(500)).toBe('500')
})
it('should format size greater than 1000', () => {
expect(sizeFormat(1500)).toBe('1K')
})
})
describe('modelTypeFormat', () => {
it('should format text embedding type', () => {
expect(modelTypeFormat(ModelTypeEnum.textEmbedding)).toBe('TEXT EMBEDDING')
})
it('should format other types', () => {
expect(modelTypeFormat(ModelTypeEnum.textGeneration)).toBe('LLM')
})
})
describe('validateCredentials', () => {
it('should validate predefined credentials successfully', async () => {
(validateModelProvider as unknown as Mock).mockResolvedValue({ result: 'success' })
const result = await validateCredentials(true, 'provider', { key: 'value' })
expect(result).toEqual({ status: ValidatedStatus.Success })
expect(validateModelProvider).toHaveBeenCalledWith({
url: '/workspaces/current/model-providers/provider/credentials/validate',
body: { credentials: { key: 'value' } },
})
})
it('should validate custom credentials successfully', async () => {
(validateModelProvider as unknown as Mock).mockResolvedValue({ result: 'success' })
const result = await validateCredentials(false, 'provider', {
__model_name: 'model',
__model_type: 'type',
key: 'value',
})
expect(result).toEqual({ status: ValidatedStatus.Success })
expect(validateModelProvider).toHaveBeenCalledWith({
url: '/workspaces/current/model-providers/provider/models/credentials/validate',
body: {
model: 'model',
model_type: 'type',
credentials: { key: 'value' },
},
})
})
it('should handle validation failure', async () => {
(validateModelProvider as unknown as Mock).mockResolvedValue({ result: 'error', error: 'failed' })
const result = await validateCredentials(true, 'provider', {})
expect(result).toEqual({ status: ValidatedStatus.Error, message: 'failed' })
})
it('should handle exception', async () => {
(validateModelProvider as unknown as Mock).mockRejectedValue(new Error('network error'))
const result = await validateCredentials(true, 'provider', {})
expect(result).toEqual({ status: ValidatedStatus.Error, message: 'network error' })
})
})
describe('validateLoadBalancingCredentials', () => {
it('should validate load balancing credentials successfully', async () => {
(validateModelLoadBalancingCredentials as unknown as Mock).mockResolvedValue({ result: 'success' })
const result = await validateLoadBalancingCredentials(true, 'provider', {
__model_name: 'model',
__model_type: 'type',
key: 'value',
})
expect(result).toEqual({ status: ValidatedStatus.Success })
expect(validateModelLoadBalancingCredentials).toHaveBeenCalledWith({
url: '/workspaces/current/model-providers/provider/models/load-balancing-configs/credentials-validate',
body: {
model: 'model',
model_type: 'type',
credentials: { key: 'value' },
},
})
})
it('should validate load balancing credentials successfully with id', async () => {
(validateModelLoadBalancingCredentials as unknown as Mock).mockResolvedValue({ result: 'success' })
const result = await validateLoadBalancingCredentials(true, 'provider', {
__model_name: 'model',
__model_type: 'type',
key: 'value',
}, 'id')
expect(result).toEqual({ status: ValidatedStatus.Success })
expect(validateModelLoadBalancingCredentials).toHaveBeenCalledWith({
url: '/workspaces/current/model-providers/provider/models/load-balancing-configs/id/credentials-validate',
body: {
model: 'model',
model_type: 'type',
credentials: { key: 'value' },
},
})
})
it('should handle validation failure', async () => {
(validateModelLoadBalancingCredentials as unknown as Mock).mockResolvedValue({ result: 'error', error: 'failed' })
const result = await validateLoadBalancingCredentials(true, 'provider', {})
expect(result).toEqual({ status: ValidatedStatus.Error, message: 'failed' })
})
})
describe('saveCredentials', () => {
it('should save predefined credentials', async () => {
await saveCredentials(true, 'provider', { __authorization_name__: 'name', key: 'value' })
expect(setModelProvider).toHaveBeenCalledWith({
url: '/workspaces/current/model-providers/provider/credentials',
body: {
config_from: ConfigurationMethodEnum.predefinedModel,
credentials: { key: 'value' },
load_balancing: undefined,
name: 'name',
},
})
})
it('should save custom credentials', async () => {
await saveCredentials(false, 'provider', {
__model_name: 'model',
__model_type: 'type',
key: 'value',
})
expect(setModelProvider).toHaveBeenCalledWith({
url: '/workspaces/current/model-providers/provider/models',
body: {
model: 'model',
model_type: 'type',
credentials: { key: 'value' },
load_balancing: undefined,
},
})
})
})
describe('savePredefinedLoadBalancingConfig', () => {
it('should save predefined load balancing config', async () => {
await savePredefinedLoadBalancingConfig('provider', {
__model_name: 'model',
__model_type: 'type',
key: 'value',
})
expect(setModelProvider).toHaveBeenCalledWith({
url: '/workspaces/current/model-providers/provider/models',
body: {
config_from: ConfigurationMethodEnum.predefinedModel,
model: 'model',
model_type: 'type',
credentials: { key: 'value' },
load_balancing: undefined,
},
})
})
})
describe('removeCredentials', () => {
it('should remove predefined credentials', async () => {
await removeCredentials(true, 'provider', {}, 'id')
expect(deleteModelProvider).toHaveBeenCalledWith({
url: '/workspaces/current/model-providers/provider/credentials',
body: { credential_id: 'id' },
})
})
it('should remove custom credentials', async () => {
await removeCredentials(false, 'provider', {
__model_name: 'model',
__model_type: 'type',
})
expect(deleteModelProvider).toHaveBeenCalledWith({
url: '/workspaces/current/model-providers/provider/models',
body: {
model: 'model',
model_type: 'type',
},
})
})
})
describe('genModelTypeFormSchema', () => {
it('should generate form schema', () => {
const schema = genModelTypeFormSchema([ModelTypeEnum.textGeneration])
expect(schema.type).toBe(FormTypeEnum.select)
expect(schema.variable).toBe('__model_type')
expect(schema.options[0].value).toBe(ModelTypeEnum.textGeneration)
})
})
describe('genModelNameFormSchema', () => {
it('should generate form schema', () => {
const schema = genModelNameFormSchema()
expect(schema.type).toBe(FormTypeEnum.textInput)
expect(schema.variable).toBe('__model_name')
expect(schema.required).toBe(true)
})
})
})