test(web): increase coverage for files in folder plugin-page and model-provider-page (#32377)

This commit is contained in:
akashseth-ifp
2026-02-24 18:27:47 +05:30
committed by GitHub
parent a1991c51e4
commit bcd5dd0f81
11 changed files with 1210 additions and 0 deletions

View File

@@ -0,0 +1,17 @@
import { fireEvent, render, screen } from '@testing-library/react'
import AddModelButton from './add-model-button'
describe('AddModelButton', () => {
it('should render button with text', () => {
render(<AddModelButton onClick={vi.fn()} />)
expect(screen.getByText('common.modelProvider.addModel')).toBeInTheDocument()
})
it('should call onClick when clicked', () => {
const handleClick = vi.fn()
render(<AddModelButton onClick={handleClick} />)
const button = screen.getByText('common.modelProvider.addModel')
fireEvent.click(button)
expect(handleClick).toHaveBeenCalledTimes(1)
})
})

View File

@@ -0,0 +1,33 @@
import { render } from '@testing-library/react'
import CooldownTimer from './cooldown-timer'
describe('CooldownTimer', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('should render timer when secondsRemaining is positive', () => {
const { container } = render(<CooldownTimer secondsRemaining={10} />)
expect(container.firstChild).toBeInTheDocument()
})
it('should not render when secondsRemaining is zero', () => {
const { container } = render(<CooldownTimer secondsRemaining={0} />)
expect(container.firstChild).toBeNull()
})
it('should not render when secondsRemaining is undefined', () => {
const { container } = render(<CooldownTimer />)
expect(container.firstChild).toBeNull()
})
it('should call onFinish after countdown completes', () => {
vi.useFakeTimers()
const onFinish = vi.fn()
render(<CooldownTimer secondsRemaining={1} onFinish={onFinish} />)
vi.advanceTimersByTime(2000)
expect(onFinish).toHaveBeenCalled()
vi.useRealTimers()
})
})

View File

@@ -0,0 +1,145 @@
import type { ModelProvider } from '../declarations'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { changeModelProviderPriority } from '@/service/common'
import { ConfigurationMethodEnum } from '../declarations'
import CredentialPanel from './credential-panel'
const mockEventEmitter = { emit: vi.fn() }
const mockNotify = vi.fn()
const mockUpdateModelList = vi.fn()
const mockUpdateModelProviders = vi.fn()
const mockCredentialStatus = {
hasCredential: true,
authorized: true,
authRemoved: false,
current_credential_name: 'test-credential',
notAllowedToUse: false,
}
vi.mock('@/config', async (importOriginal) => {
const actual = await importOriginal<typeof import('@/config')>()
return {
...actual,
IS_CLOUD_EDITION: true,
}
})
vi.mock('@/app/components/base/toast', () => ({
useToastContext: () => ({
notify: mockNotify,
}),
}))
vi.mock('@/context/event-emitter', () => ({
useEventEmitterContextContext: () => ({
eventEmitter: mockEventEmitter,
}),
}))
vi.mock('@/service/common', () => ({
changeModelProviderPriority: vi.fn(),
}))
vi.mock('@/app/components/header/account-setting/model-provider-page/model-auth', () => ({
ConfigProvider: () => <div data-testid="config-provider" />,
}))
vi.mock('@/app/components/header/account-setting/model-provider-page/model-auth/hooks', () => ({
useCredentialStatus: () => mockCredentialStatus,
}))
vi.mock('../hooks', () => ({
useUpdateModelList: () => mockUpdateModelList,
useUpdateModelProviders: () => mockUpdateModelProviders,
}))
vi.mock('./priority-selector', () => ({
default: ({ value, onSelect }: { value: string, onSelect: (key: string) => void }) => (
<button data-testid="priority-selector" onClick={() => onSelect('custom')}>
Priority Selector
{' '}
{value}
</button>
),
}))
vi.mock('./priority-use-tip', () => ({
default: () => <div data-testid="priority-use-tip">Priority Tip</div>,
}))
vi.mock('@/app/components/header/indicator', () => ({
default: ({ color }: { color: string }) => <div data-testid="indicator">{color}</div>,
}))
describe('CredentialPanel', () => {
const mockProvider: ModelProvider = {
provider: 'test-provider',
provider_credential_schema: true,
custom_configuration: { status: 'active' },
system_configuration: { enabled: true },
preferred_provider_type: 'system',
configurate_methods: [ConfigurationMethodEnum.predefinedModel],
supported_model_types: ['gpt-4'],
} as unknown as ModelProvider
beforeEach(() => {
vi.clearAllMocks()
Object.assign(mockCredentialStatus, {
hasCredential: true,
authorized: true,
authRemoved: false,
current_credential_name: 'test-credential',
notAllowedToUse: false,
})
})
it('should show credential name and configuration actions', () => {
render(<CredentialPanel provider={mockProvider} />)
expect(screen.getByText('test-credential')).toBeInTheDocument()
expect(screen.getByTestId('config-provider')).toBeInTheDocument()
expect(screen.getByTestId('priority-selector')).toBeInTheDocument()
})
it('should show unauthorized status label when credential is missing', () => {
mockCredentialStatus.hasCredential = false
render(<CredentialPanel provider={mockProvider} />)
expect(screen.getByText(/modelProvider\.auth\.unAuthorized/)).toBeInTheDocument()
})
it('should show removed credential label and priority tip for custom preference', () => {
mockCredentialStatus.authorized = false
mockCredentialStatus.authRemoved = true
render(<CredentialPanel provider={{ ...mockProvider, preferred_provider_type: 'custom' } as ModelProvider} />)
expect(screen.getByText(/modelProvider\.auth\.authRemoved/)).toBeInTheDocument()
expect(screen.getByTestId('priority-use-tip')).toBeInTheDocument()
})
it('should change priority and refresh related data after success', async () => {
const mockChangePriority = changeModelProviderPriority as ReturnType<typeof vi.fn>
mockChangePriority.mockResolvedValue({ result: 'success' })
render(<CredentialPanel provider={mockProvider} />)
fireEvent.click(screen.getByTestId('priority-selector'))
await waitFor(() => {
expect(mockChangePriority).toHaveBeenCalled()
expect(mockNotify).toHaveBeenCalled()
expect(mockUpdateModelProviders).toHaveBeenCalled()
expect(mockUpdateModelList).toHaveBeenCalledWith('gpt-4')
expect(mockEventEmitter.emit).toHaveBeenCalled()
})
})
it('should render standalone priority selector without provider schema', () => {
const providerNoSchema = {
...mockProvider,
provider_credential_schema: null,
} as unknown as ModelProvider
render(<CredentialPanel provider={providerNoSchema} />)
expect(screen.getByTestId('priority-selector')).toBeInTheDocument()
expect(screen.queryByTestId('config-provider')).not.toBeInTheDocument()
})
})

View File

@@ -0,0 +1,137 @@
import type { ModelItem, ModelProvider } from '../declarations'
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
import { fetchModelProviderModelList } from '@/service/common'
import { ConfigurationMethodEnum } from '../declarations'
import ProviderAddedCard from './index'
let mockIsCurrentWorkspaceManager = true
type SubscriptionPayload = { type?: string, payload?: string } | unknown
let subscriptionHandler: ((value: SubscriptionPayload) => void) | undefined
const mockEventEmitter: { useSubscription: unknown, emit: unknown } = {
useSubscription: vi.fn((handler: (value: SubscriptionPayload) => void) => {
subscriptionHandler = handler
}),
emit: vi.fn(),
}
vi.mock('@/service/common', () => ({
fetchModelProviderModelList: vi.fn(),
}))
vi.mock('@/context/app-context', () => ({
useAppContext: () => ({
isCurrentWorkspaceManager: mockIsCurrentWorkspaceManager,
}),
}))
vi.mock('@/context/event-emitter', () => ({
useEventEmitterContextContext: () => ({
eventEmitter: mockEventEmitter,
}),
}))
vi.mock('./credential-panel', () => ({
default: () => <div data-testid="credential-panel" />,
}))
vi.mock('./model-list', () => ({
default: ({ onCollapse, onChange }: { onCollapse: () => void, onChange: (provider: string) => void }) => (
<div data-testid="model-list">
<button type="button" onClick={onCollapse}>collapse list</button>
<button type="button" onClick={() => onChange('langgenius/openai/openai')}>refresh list</button>
</div>
),
}))
vi.mock('../provider-icon', () => ({
default: () => <div data-testid="provider-icon" />,
}))
vi.mock('../model-badge', () => ({
default: ({ children }: { children: string }) => <div data-testid="model-badge">{children}</div>,
}))
vi.mock('@/app/components/header/account-setting/model-provider-page/model-auth', () => ({
AddCustomModel: () => <div data-testid="add-custom-model" />,
ManageCustomModelCredentials: () => <div data-testid="manage-custom-model" />,
}))
describe('ProviderAddedCard', () => {
const mockProvider = {
provider: 'langgenius/openai/openai',
configurate_methods: ['predefinedModel'],
system_configuration: { enabled: true },
supported_model_types: ['llm'],
} as unknown as ModelProvider
beforeEach(() => {
vi.clearAllMocks()
mockIsCurrentWorkspaceManager = true
subscriptionHandler = undefined
})
it('should render provider added card component', () => {
const { container } = render(<ProviderAddedCard provider={mockProvider} />)
expect(container.firstChild).toBeInTheDocument()
})
it('should open and refresh model list from user actions', async () => {
vi.mocked(fetchModelProviderModelList).mockResolvedValue({ data: [{ model: 'gpt-4' }] } as unknown as { data: ModelItem[] })
render(<ProviderAddedCard provider={mockProvider} />)
const showModelsBtn = screen.getAllByText('common.modelProvider.showModels')[1]
fireEvent.click(showModelsBtn)
await screen.findByTestId('model-list')
expect(fetchModelProviderModelList).toHaveBeenCalledWith(`/workspaces/current/model-providers/${mockProvider.provider}/models`)
fireEvent.click(screen.getByRole('button', { name: 'refresh list' }))
await waitFor(() => {
expect(fetchModelProviderModelList).toHaveBeenCalledTimes(2)
})
fireEvent.click(screen.getByRole('button', { name: 'collapse list' }))
expect(screen.getAllByText(/common\.modelProvider\.showModelsNum:\{"num":1\}/).length).toBeGreaterThan(0)
})
it('should render configure tip when provider is not in quota list and not configured', () => {
const providerWithoutQuota = {
...mockProvider,
provider: 'custom/provider',
} as unknown as ModelProvider
render(<ProviderAddedCard provider={providerWithoutQuota} notConfigured />)
expect(screen.getByText('common.modelProvider.configureTip')).toBeInTheDocument()
})
it('should refresh model list on matching event subscription', async () => {
vi.mocked(fetchModelProviderModelList).mockResolvedValue({ data: [{ model: 'gpt-4' }] } as unknown as { data: ModelItem[] })
render(<ProviderAddedCard provider={mockProvider} notConfigured />)
expect(subscriptionHandler).toBeTruthy()
await act(async () => {
subscriptionHandler?.({
type: 'UPDATE_MODEL_PROVIDER_CUSTOM_MODEL_LIST',
payload: mockProvider.provider,
})
})
await waitFor(() => {
expect(fetchModelProviderModelList).toHaveBeenCalledTimes(1)
})
})
it('should render custom model actions only for workspace managers', () => {
const customConfigProvider = {
...mockProvider,
configurate_methods: [ConfigurationMethodEnum.customizableModel],
} as unknown as ModelProvider
const { rerender } = render(<ProviderAddedCard provider={customConfigProvider} />)
expect(screen.getByTestId('manage-custom-model')).toBeInTheDocument()
expect(screen.getByTestId('add-custom-model')).toBeInTheDocument()
mockIsCurrentWorkspaceManager = false
rerender(<ProviderAddedCard provider={customConfigProvider} />)
expect(screen.queryByTestId('manage-custom-model')).not.toBeInTheDocument()
})
})

View File

@@ -0,0 +1,130 @@
import type { ModelItem, ModelProvider } from '../declarations'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { disableModel, enableModel } from '@/service/common'
import { ModelStatusEnum } from '../declarations'
import ModelListItem from './model-list-item'
let mockModelLoadBalancingEnabled = false
vi.mock('@/context/app-context', () => ({
useAppContext: () => ({
isCurrentWorkspaceManager: true,
}),
}))
vi.mock('@/context/provider-context', () => ({
useProviderContext: () => ({
plan: { type: 'pro' },
}),
useProviderContextSelector: () => mockModelLoadBalancingEnabled,
}))
vi.mock('@/service/common', () => ({
enableModel: vi.fn(),
disableModel: vi.fn(),
}))
vi.mock('../hooks', () => ({
useUpdateModelList: () => vi.fn(),
}))
vi.mock('../model-icon', () => ({
default: () => <div data-testid="model-icon" />,
}))
vi.mock('../model-name', () => ({
default: ({ children }: { children: React.ReactNode }) => <div data-testid="model-name">{children}</div>,
}))
vi.mock('../model-auth', () => ({
ConfigModel: ({ onClick }: { onClick: () => void }) => (
<button type="button" onClick={onClick}>modify load balancing</button>
),
}))
describe('ModelListItem', () => {
const mockProvider = {
provider: 'test-provider',
} as unknown as ModelProvider
const mockModel = {
model: 'gpt-4',
model_type: 'llm',
fetch_from: 'system',
status: 'active',
deprecated: false,
load_balancing_enabled: false,
has_invalid_load_balancing_configs: false,
} as unknown as ModelItem
beforeEach(() => {
vi.clearAllMocks()
mockModelLoadBalancingEnabled = false
})
it('should render model item with icon and name', () => {
render(
<ModelListItem
model={mockModel}
provider={mockProvider}
isConfigurable={false}
/>,
)
expect(screen.getByTestId('model-icon')).toBeInTheDocument()
expect(screen.getByTestId('model-name')).toBeInTheDocument()
})
it('should disable an active model when switch is clicked', async () => {
const onChange = vi.fn()
render(
<ModelListItem
model={mockModel}
provider={mockProvider}
isConfigurable={false}
onChange={onChange}
/>,
)
fireEvent.click(screen.getByRole('switch'))
await waitFor(() => {
expect(disableModel).toHaveBeenCalled()
expect(onChange).toHaveBeenCalledWith('test-provider')
}, { timeout: 2000 })
})
it('should enable a disabled model when switch is clicked', async () => {
const onChange = vi.fn()
const disabledModel = { ...mockModel, status: ModelStatusEnum.disabled }
render(
<ModelListItem
model={disabledModel}
provider={mockProvider}
isConfigurable={false}
onChange={onChange}
/>,
)
fireEvent.click(screen.getByRole('switch'))
await waitFor(() => {
expect(enableModel).toHaveBeenCalled()
expect(onChange).toHaveBeenCalledWith('test-provider')
}, { timeout: 2000 })
})
it('should open load balancing config action when available', () => {
mockModelLoadBalancingEnabled = true
const onModifyLoadBalancing = vi.fn()
render(
<ModelListItem
model={mockModel}
provider={mockProvider}
isConfigurable={false}
onModifyLoadBalancing={onModifyLoadBalancing}
/>,
)
fireEvent.click(screen.getByRole('button', { name: 'modify load balancing' }))
expect(onModifyLoadBalancing).toHaveBeenCalledWith(mockModel)
})
})

View File

@@ -0,0 +1,108 @@
import type { ModelItem, ModelProvider } from '../declarations'
import { fireEvent, render, screen } from '@testing-library/react'
import ModelList from './model-list'
const mockSetShowModelLoadBalancingModal = vi.fn()
let mockIsCurrentWorkspaceManager = true
vi.mock('@/context/app-context', () => ({
useAppContext: () => ({
isCurrentWorkspaceManager: mockIsCurrentWorkspaceManager,
}),
}))
vi.mock('@/context/modal-context', () => ({
useModalContextSelector: (selector: (state: { setShowModelLoadBalancingModal: typeof mockSetShowModelLoadBalancingModal }) => unknown) =>
selector({ setShowModelLoadBalancingModal: mockSetShowModelLoadBalancingModal }),
}))
vi.mock('./model-list-item', () => ({
default: ({ model, onModifyLoadBalancing }: { model: ModelItem, onModifyLoadBalancing: (model: ModelItem) => void }) => (
<button type="button" onClick={() => onModifyLoadBalancing(model)}>
{model.model}
</button>
),
}))
vi.mock('@/app/components/header/account-setting/model-provider-page/model-auth', () => ({
ManageCustomModelCredentials: () => <div data-testid="manage-credentials" />,
AddCustomModel: () => <div data-testid="add-custom-model" />,
}))
describe('ModelList', () => {
const mockProvider = {
provider: 'test-provider',
configurate_methods: ['customizableModel'],
} as unknown as ModelProvider
const mockModels = [
{ model: 'gpt-4', model_type: 'llm', fetch_from: 'system' },
{ model: 'gpt-3.5', model_type: 'llm', fetch_from: 'system' },
] as unknown as ModelItem[]
const mockOnCollapse = vi.fn()
const mockOnChange = vi.fn()
beforeEach(() => {
vi.clearAllMocks()
mockIsCurrentWorkspaceManager = true
})
it('should render model count and model items', () => {
render(
<ModelList
provider={mockProvider}
models={mockModels}
onCollapse={mockOnCollapse}
onChange={mockOnChange}
/>,
)
expect(screen.getAllByText(/modelProvider\.modelsNum/).length).toBeGreaterThan(0)
expect(screen.getByRole('button', { name: 'gpt-4' })).toBeInTheDocument()
expect(screen.getByRole('button', { name: 'gpt-3.5' })).toBeInTheDocument()
})
it('should trigger collapse when collapsed label is clicked', () => {
render(
<ModelList
provider={mockProvider}
models={mockModels}
onCollapse={mockOnCollapse}
onChange={mockOnChange}
/>,
)
const countElements = screen.getAllByText(/modelProvider\.modelsNum/)
fireEvent.click(countElements[1])
expect(mockOnCollapse).toHaveBeenCalled()
})
it('should open load balancing modal for selected model', () => {
render(
<ModelList
provider={mockProvider}
models={mockModels}
onCollapse={mockOnCollapse}
onChange={mockOnChange}
/>,
)
fireEvent.click(screen.getByRole('button', { name: 'gpt-4' }))
expect(mockSetShowModelLoadBalancingModal).toHaveBeenCalled()
})
it('should hide custom model actions for non-manager', () => {
mockIsCurrentWorkspaceManager = false
render(
<ModelList
provider={mockProvider}
models={mockModels}
onCollapse={mockOnCollapse}
onChange={mockOnChange}
/>,
)
expect(screen.queryByTestId('manage-credentials')).not.toBeInTheDocument()
expect(screen.queryByTestId('add-custom-model')).not.toBeInTheDocument()
})
})

View File

@@ -0,0 +1,191 @@
import type {
Credential,
CustomModelCredential,
ModelCredential,
ModelLoadBalancingConfig,
ModelProvider,
} from '../declarations'
import { fireEvent, render, screen } from '@testing-library/react'
import { useState } from 'react'
import { ConfigurationMethodEnum } from '../declarations'
import ModelLoadBalancingConfigs from './model-load-balancing-configs'
let mockModelLoadBalancingEnabled = true
vi.mock('@/config', () => ({
IS_CE_EDITION: false,
}))
vi.mock('@/context/provider-context', () => ({
useProviderContextSelector: () => mockModelLoadBalancingEnabled,
}))
vi.mock('./cooldown-timer', () => ({
default: ({ secondsRemaining, onFinish }: { secondsRemaining?: number, onFinish?: () => void }) => (
<button type="button" onClick={onFinish}>
{secondsRemaining}
s
</button>
),
}))
vi.mock('@/app/components/header/account-setting/model-provider-page/model-auth', () => ({
AddCredentialInLoadBalancing: ({ onSelectCredential, onUpdate, onRemove }: {
onSelectCredential: (credential: Credential) => void
onUpdate?: (payload?: unknown, formValues?: Record<string, unknown>) => void
onRemove?: (credentialId: string) => void
}) => (
<div>
<button
type="button"
onClick={() => onSelectCredential({ credential_id: 'cred-2', credential_name: 'Key 2' } as Credential)}
>
add credential
</button>
<button
type="button"
onClick={() => onUpdate?.({ credential: { credential_id: 'cred-2' } }, { __authorization_name__: 'Key 2' })}
>
trigger update
</button>
<button
type="button"
onClick={() => onRemove?.('cred-2')}
>
trigger remove
</button>
</div>
),
}))
vi.mock('@/app/components/billing/upgrade-btn', () => ({
default: () => <div>upgrade</div>,
}))
describe('ModelLoadBalancingConfigs', () => {
const mockProvider = {
provider: 'test-provider',
} as unknown as ModelProvider
const mockModelCredential = {
available_credentials: [
{
credential_id: 'cred-1',
credential_name: 'Key 1',
not_allowed_to_use: false,
},
{
credential_id: 'cred-2',
credential_name: 'Key 2',
not_allowed_to_use: false,
},
],
} as unknown as ModelCredential
const createDraftConfig = (enabled = true): ModelLoadBalancingConfig => ({
enabled,
configs: [
{
id: 'cfg-1',
credential_id: 'cred-1',
enabled: true,
name: 'Key 1',
},
],
} as ModelLoadBalancingConfig)
const StatefulHarness = ({
initialConfig,
withSwitch = false,
onUpdate,
onRemove,
}: {
initialConfig: ModelLoadBalancingConfig
withSwitch?: boolean
onUpdate?: (payload?: unknown, formValues?: Record<string, unknown>) => void
onRemove?: (credentialId: string) => void
}) => {
const [draftConfig, setDraftConfig] = useState<ModelLoadBalancingConfig | undefined>(initialConfig)
return (
<ModelLoadBalancingConfigs
draftConfig={draftConfig}
setDraftConfig={setDraftConfig}
provider={mockProvider}
configurationMethod={ConfigurationMethodEnum.predefinedModel}
modelCredential={mockModelCredential}
model={{ model: 'gpt-4', model_type: 'llm' } as CustomModelCredential}
withSwitch={withSwitch}
onUpdate={onUpdate}
onRemove={onRemove}
/>
)
}
beforeEach(() => {
vi.clearAllMocks()
mockModelLoadBalancingEnabled = true
})
it('should render nothing when draft config is missing', () => {
const { container } = render(
<ModelLoadBalancingConfigs
draftConfig={undefined}
setDraftConfig={vi.fn()}
provider={mockProvider}
configurationMethod={ConfigurationMethodEnum.predefinedModel}
modelCredential={mockModelCredential}
model={{ model: 'gpt-4', model_type: 'llm' } as CustomModelCredential}
/>,
)
expect(container.firstChild).toBeNull()
})
it('should show current configs and low key warning when enabled', () => {
render(<StatefulHarness initialConfig={createDraftConfig(true)} />)
expect(screen.getAllByText(/modelProvider\.loadBalancing/).length).toBeGreaterThan(0)
expect(screen.getByText('Key 1')).toBeInTheDocument()
expect(screen.getByText(/modelProvider\.loadBalancingLeastKeyWarning/)).toBeInTheDocument()
})
it('should enable load balancing by clicking the panel when disabled', () => {
render(<StatefulHarness initialConfig={createDraftConfig(false)} />)
fireEvent.click(screen.getAllByText(/modelProvider\.loadBalancing/)[0])
expect(screen.getByText('Key 1')).toBeInTheDocument()
})
it('should add and remove credentials from the visible list', () => {
const onUpdate = vi.fn()
const onRemove = vi.fn()
const draftConfig = {
enabled: true,
configs: [
{ id: 'cfg-1', credential_id: 'cred-1', enabled: true, name: 'Key 1', in_cooldown: true, ttl: 30 },
{ id: 'cfg-2', credential_id: 'cred-2', enabled: true, name: '__inherit__' },
],
} as unknown as ModelLoadBalancingConfig
render(<StatefulHarness initialConfig={draftConfig} withSwitch onUpdate={onUpdate} onRemove={onRemove} />)
fireEvent.click(screen.getByRole('button', { name: '30s' }))
fireEvent.click(screen.getByRole('button', { name: 'add credential' }))
expect(screen.getByText('Key 2')).toBeInTheDocument()
fireEvent.click(screen.getByRole('button', { name: 'trigger update' }))
expect(onUpdate).toHaveBeenCalled()
fireEvent.click(screen.getByRole('button', { name: 'trigger remove' }))
expect(onRemove).toHaveBeenCalledWith('cred-2')
expect(screen.queryByText('Key 2')).not.toBeInTheDocument()
fireEvent.click(screen.getAllByRole('switch')[0])
})
it('should show upgrade prompt when feature is unavailable', () => {
mockModelLoadBalancingEnabled = false
render(<StatefulHarness initialConfig={createDraftConfig(true)} withSwitch />)
expect(screen.getByText(/modelProvider\.upgradeForLoadBalancing/)).toBeInTheDocument()
expect(screen.getByText('upgrade')).toBeInTheDocument()
})
})

View File

@@ -0,0 +1,268 @@
import type { ModelItem, ModelProvider } from '../declarations'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { ConfigurationMethodEnum } from '../declarations'
import ModelLoadBalancingModal from './model-load-balancing-modal'
type CredentialData = {
load_balancing: {
enabled: boolean
configs: Array<{
id: string
credential_id: string
enabled: boolean
name: string
credentials: { api_key: string }
}>
}
current_credential_id: string
available_credentials: Array<{ credential_id: string, credential_name: string }>
current_credential_name: string
}
const mockNotify = vi.fn()
const mockMutateAsync = vi.fn()
const mockRefetch = vi.fn()
const mockHandleRefreshModel = vi.fn()
const mockHandleConfirmDelete = vi.fn()
const mockOpenConfirmDelete = vi.fn()
let mockDeleteModel: unknown = null
let mockCredentialData: CredentialData | undefined = {
load_balancing: {
enabled: true,
configs: [
{ id: 'cfg-1', credential_id: 'cred-1', enabled: true, name: 'Default', credentials: { api_key: 'same-key' } },
{ id: 'cfg-2', credential_id: 'cred-2', enabled: true, name: 'Backup', credentials: { api_key: 'backup-key' } },
],
},
current_credential_id: 'cred-1',
available_credentials: [
{ credential_id: 'cred-1', credential_name: 'Default' },
{ credential_id: 'cred-2', credential_name: 'Backup' },
],
current_credential_name: 'Default',
}
vi.mock('@/app/components/base/toast', () => ({
useToastContext: () => ({
notify: mockNotify,
}),
}))
vi.mock('@/service/use-models', () => ({
useGetModelCredential: () => ({
isLoading: false,
data: mockCredentialData,
refetch: mockRefetch,
}),
useUpdateModelLoadBalancingConfig: () => ({
mutateAsync: mockMutateAsync,
}),
}))
vi.mock('../model-auth/hooks/use-auth', () => ({
useAuth: () => ({
doingAction: false,
deleteModel: mockDeleteModel,
openConfirmDelete: mockOpenConfirmDelete,
closeConfirmDelete: vi.fn(),
handleConfirmDelete: mockHandleConfirmDelete,
}),
}))
vi.mock('../hooks', () => ({
useRefreshModel: () => ({ handleRefreshModel: mockHandleRefreshModel }),
}))
vi.mock('./model-load-balancing-configs', () => ({
default: ({ onUpdate, onRemove }: {
onUpdate?: (payload?: unknown, formValues?: Record<string, unknown>) => void
onRemove?: (credentialId: string) => void
}) => (
<div>
<button type="button" onClick={() => onUpdate?.(undefined, { __authorization_name__: 'New Key' })}>config add credential</button>
<button type="button" onClick={() => onUpdate?.({ credential: { credential_id: 'cred-1' } }, { __authorization_name__: 'Renamed Key' })}>config rename credential</button>
<button type="button" onClick={() => onRemove?.('cred-1')}>config remove</button>
</div>
),
}))
vi.mock('@/app/components/header/account-setting/model-provider-page/model-auth', () => ({
SwitchCredentialInLoadBalancing: ({ onUpdate }: { onUpdate: () => void }) => (
<button type="button" onClick={onUpdate}>switch credential</button>
),
}))
vi.mock('../model-icon', () => ({
default: () => <div>model-icon</div>,
}))
vi.mock('../model-name', () => ({
default: () => <div>model-name</div>,
}))
describe('ModelLoadBalancingModal', () => {
const mockProvider = {
provider: 'test-provider',
provider_credential_schema: {
credential_form_schemas: [{ type: 'secret-input', variable: 'api_key' }],
},
model_credential_schema: {
credential_form_schemas: [{ type: 'secret-input', variable: 'api_key' }],
},
} as unknown as ModelProvider
const mockModel = {
model: 'gpt-4',
model_type: 'llm',
fetch_from: 'predefined-model',
} as unknown as ModelItem
beforeEach(() => {
vi.clearAllMocks()
mockDeleteModel = null
mockCredentialData = {
load_balancing: {
enabled: true,
configs: [
{ id: 'cfg-1', credential_id: 'cred-1', enabled: true, name: 'Default', credentials: { api_key: 'same-key' } },
{ id: 'cfg-2', credential_id: 'cred-2', enabled: true, name: 'Backup', credentials: { api_key: 'backup-key' } },
],
},
current_credential_id: 'cred-1',
available_credentials: [
{ credential_id: 'cred-1', credential_name: 'Default' },
{ credential_id: 'cred-2', credential_name: 'Backup' },
],
current_credential_name: 'Default',
}
mockMutateAsync.mockResolvedValue({ result: 'success' })
mockRefetch.mockResolvedValue({ data: mockCredentialData })
})
it('should show loading area while draft config is not ready', () => {
mockCredentialData = undefined
render(
<ModelLoadBalancingModal
provider={mockProvider}
configurateMethod={ConfigurationMethodEnum.predefinedModel}
model={mockModel}
open
/>,
)
expect(screen.getByRole('status')).toBeInTheDocument()
})
it('should render predefined model content', () => {
render(
<ModelLoadBalancingModal
provider={mockProvider}
configurateMethod={ConfigurationMethodEnum.predefinedModel}
model={mockModel}
open
/>,
)
expect(screen.getByText(/modelProvider\.auth\.configLoadBalancing/)).toBeInTheDocument()
expect(screen.getByText(/modelProvider\.auth\.providerManaged$/)).toBeInTheDocument()
expect(screen.getByText(/operation\.save/)).toBeInTheDocument()
})
it('should render custom model actions and close when update has no credentials', async () => {
const onClose = vi.fn()
mockRefetch.mockResolvedValue({ data: { available_credentials: [] } })
render(
<ModelLoadBalancingModal
provider={mockProvider}
configurateMethod={ConfigurationMethodEnum.customizableModel}
model={mockModel}
open
onClose={onClose}
/>,
)
expect(screen.getByText(/modelProvider\.auth\.removeModel/)).toBeInTheDocument()
expect(screen.getByRole('button', { name: 'switch credential' })).toBeInTheDocument()
fireEvent.click(screen.getByRole('button', { name: 'config add credential' }))
await waitFor(() => {
expect(onClose).toHaveBeenCalled()
})
})
it('should save load balancing config and close modal', async () => {
const onSave = vi.fn()
const onClose = vi.fn()
render(
<ModelLoadBalancingModal
provider={mockProvider}
configurateMethod={ConfigurationMethodEnum.predefinedModel}
model={mockModel}
open
onSave={onSave}
onClose={onClose}
/>,
)
fireEvent.click(screen.getByRole('button', { name: 'config add credential' }))
fireEvent.click(screen.getByRole('button', { name: 'config rename credential' }))
fireEvent.click(screen.getByText(/operation\.save/))
await waitFor(() => {
expect(mockRefetch).toHaveBeenCalled()
expect(mockMutateAsync).toHaveBeenCalled()
const payload = mockMutateAsync.mock.calls[0][0] as { load_balancing: { configs: Array<{ credentials: { api_key: string } }> } }
expect(payload.load_balancing.configs[0].credentials.api_key).toBe('[__HIDDEN__]')
expect(mockNotify).toHaveBeenCalled()
expect(mockHandleRefreshModel).toHaveBeenCalled()
expect(onSave).toHaveBeenCalledWith('test-provider')
expect(onClose).toHaveBeenCalled()
})
})
it('should close modal when switching credential yields no available credentials', async () => {
const onClose = vi.fn()
mockRefetch.mockResolvedValue({ data: { available_credentials: [] } })
render(
<ModelLoadBalancingModal
provider={mockProvider}
configurateMethod={ConfigurationMethodEnum.customizableModel}
model={mockModel}
open
onClose={onClose}
/>,
)
fireEvent.click(screen.getByRole('button', { name: 'switch credential' }))
await waitFor(() => {
expect(onClose).toHaveBeenCalled()
})
})
it('should confirm model deletion and close modal', async () => {
const onClose = vi.fn()
mockDeleteModel = { model: 'gpt-4' }
render(
<ModelLoadBalancingModal
provider={mockProvider}
configurateMethod={ConfigurationMethodEnum.customizableModel}
model={mockModel}
open
onClose={onClose}
/>,
)
fireEvent.click(screen.getByText(/modelProvider\.auth\.removeModel/))
fireEvent.click(screen.getByRole('button', { name: 'common.operation.confirm' }))
await waitFor(() => {
expect(mockOpenConfirmDelete).toHaveBeenCalled()
expect(mockHandleConfirmDelete).toHaveBeenCalled()
expect(onClose).toHaveBeenCalled()
})
})
})

View File

@@ -0,0 +1,29 @@
import { fireEvent, render, screen } from '@testing-library/react'
import PrioritySelector from './priority-selector'
describe('PrioritySelector', () => {
const mockOnSelect = vi.fn()
beforeEach(() => {
vi.clearAllMocks()
})
it('should render selector button', () => {
render(<PrioritySelector value="system" onSelect={mockOnSelect} />)
expect(screen.getByRole('button')).toBeInTheDocument()
})
it('should call onSelect when option clicked', () => {
render(<PrioritySelector value="system" onSelect={mockOnSelect} />)
fireEvent.click(screen.getByRole('button'))
const option = screen.getByText('common.modelProvider.apiKey')
fireEvent.click(option)
expect(mockOnSelect).toHaveBeenCalled()
})
it('should display priority use header in popover', () => {
render(<PrioritySelector value="custom" onSelect={mockOnSelect} />)
fireEvent.click(screen.getByRole('button'))
expect(screen.getByText('common.modelProvider.card.priorityUse')).toBeInTheDocument()
})
})

View File

@@ -0,0 +1,14 @@
import { render } from '@testing-library/react'
import PriorityUseTip from './priority-use-tip'
describe('PriorityUseTip', () => {
it('should render tooltip with icon content', () => {
const { container } = render(<PriorityUseTip />)
expect(container.querySelector('[data-state]')).toBeInTheDocument()
})
it('should render the component without crashing', () => {
const { container } = render(<PriorityUseTip />)
expect(container.firstChild).toBeInTheDocument()
})
})

View File

@@ -0,0 +1,138 @@
import type { ModelProvider } from '../declarations'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import QuotaPanel from './quota-panel'
let mockWorkspace = {
trial_credits: 100,
trial_credits_used: 30,
next_credit_reset_date: '2024-12-31',
}
let mockTrialModels: string[] = ['langgenius/openai/openai']
let mockPlugins = [{
plugin_id: 'langgenius/openai',
latest_package_identifier: 'openai@1.0.0',
}]
vi.mock('@/app/components/base/icons/src/public/llm', () => {
const Icon = ({ label }: { label: string }) => <span>{label}</span>
return {
OpenaiSmall: () => <Icon label="openai" />,
AnthropicShortLight: () => <Icon label="anthropic" />,
Gemini: () => <Icon label="gemini" />,
Grok: () => <Icon label="x" />,
Deepseek: () => <Icon label="deepseek" />,
Tongyi: () => <Icon label="tongyi" />,
}
})
vi.mock('@/context/app-context', () => ({
useAppContext: () => ({
currentWorkspace: mockWorkspace,
}),
}))
vi.mock('@/context/global-public-context', () => ({
useGlobalPublicStore: (selector: (state: { systemFeatures: { trial_models: string[] } }) => unknown) => selector({
systemFeatures: {
trial_models: mockTrialModels,
},
}),
}))
vi.mock('../hooks', () => ({
useMarketplaceAllPlugins: () => ({
plugins: mockPlugins,
}),
}))
vi.mock('@/hooks/use-timestamp', () => ({
default: () => ({
formatTime: () => '2024-12-31',
}),
}))
vi.mock('@/app/components/plugins/install-plugin/install-from-marketplace', () => ({
default: ({ onClose }: { onClose: () => void }) => (
<div>
<span>install modal</span>
<button type="button" onClick={onClose}>close install</button>
</div>
),
}))
describe('QuotaPanel', () => {
const mockProviders = [
{
provider: 'langgenius/openai/openai',
preferred_provider_type: 'custom',
custom_configuration: { available_credentials: [{ id: '1' }] },
},
] as unknown as ModelProvider[]
beforeEach(() => {
vi.clearAllMocks()
mockWorkspace = {
trial_credits: 100,
trial_credits_used: 30,
next_credit_reset_date: '2024-12-31',
}
mockTrialModels = ['langgenius/openai/openai']
mockPlugins = [{ plugin_id: 'langgenius/openai', latest_package_identifier: 'openai@1.0.0' }]
})
it('should render loading state', () => {
render(
<QuotaPanel
providers={mockProviders}
isLoading
/>,
)
expect(screen.getByRole('status')).toBeInTheDocument()
})
it('should show remaining credits and reset date', () => {
render(
<QuotaPanel
providers={mockProviders}
/>,
)
expect(screen.getByText(/modelProvider\.quota/)).toBeInTheDocument()
expect(screen.getByText('70')).toBeInTheDocument()
expect(screen.getByText(/modelProvider\.resetDate/)).toBeInTheDocument()
})
it('should floor credits at zero when usage is higher than quota', () => {
mockWorkspace = {
trial_credits: 10,
trial_credits_used: 999,
next_credit_reset_date: '',
}
render(<QuotaPanel providers={mockProviders} />)
expect(screen.getByText('0')).toBeInTheDocument()
expect(screen.queryByText(/modelProvider\.resetDate/)).not.toBeInTheDocument()
})
it('should open install modal when clicking an unsupported trial provider', () => {
render(<QuotaPanel providers={[]} />)
fireEvent.click(screen.getByText('openai'))
expect(screen.getByText('install modal')).toBeInTheDocument()
})
it('should close install modal when provider becomes installed', async () => {
const { rerender } = render(<QuotaPanel providers={[]} />)
fireEvent.click(screen.getByText('openai'))
expect(screen.getByText('install modal')).toBeInTheDocument()
rerender(<QuotaPanel providers={mockProviders} />)
await waitFor(() => {
expect(screen.queryByText('install modal')).not.toBeInTheDocument()
})
})
})