mirror of
https://github.com/langgenius/dify.git
synced 2026-02-24 18:05:11 +00:00
test(web): increase coverage for files in folder plugin-page and model-provider-page (#32377)
This commit is contained in:
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user