test(web): update tests for credential panel refactoring and new ModelAuthDropdown components

Rewrite credential-panel.spec.tsx to match the new discriminated union
state model and variant-driven rendering. Add new test files for
useCredentialPanelState hook, SystemQuotaCard Label enhancement,
and all ModelAuthDropdown sub-components.
This commit is contained in:
yyh
2026-03-05 08:41:17 +08:00
parent ab87ac333a
commit 970493fa85
8 changed files with 1025 additions and 93 deletions

View File

@ -2,74 +2,57 @@ import type { ModelProvider } from '../declarations'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { changeModelProviderPriority } from '@/service/common'
import { ConfigurationMethodEnum } from '../declarations'
import {
ConfigurationMethodEnum,
CustomConfigurationStatusEnum,
PreferredProviderTypeEnum,
} 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,
}
const mockTrialCredits = { credits: 100, isExhausted: false, isLoading: false, nextCreditResetDate: undefined }
vi.mock('@/config', async (importOriginal) => {
const actual = await importOriginal<typeof import('@/config')>()
return {
...actual,
IS_CLOUD_EDITION: true,
}
return { ...actual, IS_CLOUD_EDITION: true }
})
vi.mock('@/app/components/base/toast', () => ({
useToastContext: () => ({
notify: mockNotify,
}),
useToastContext: () => ({ notify: mockNotify }),
}))
vi.mock('@/context/event-emitter', () => ({
useEventEmitterContextContext: () => ({
eventEmitter: mockEventEmitter,
}),
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('./use-trial-credits', () => ({
useTrialCredits: () => mockTrialCredits,
}))
vi.mock('./model-auth-dropdown', () => ({
default: ({ state, onChangePriority }: { state: { variant: string, hasCredentials: boolean }, onChangePriority: (key: string) => void }) => (
<div data-testid="model-auth-dropdown" data-variant={state.variant}>
<button data-testid="change-priority-btn" onClick={() => onChangePriority('custom')}>
Change Priority
</button>
</div>
),
}))
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>,
default: ({ color }: { color: string }) => <div data-testid="indicator" data-color={color} />,
}))
const createTestQueryClient = () => new QueryClient({
@ -78,6 +61,22 @@ const createTestQueryClient = () => new QueryClient({
},
})
const createProvider = (overrides: Partial<ModelProvider> = {}): ModelProvider => ({
provider: 'test-provider',
provider_credential_schema: { credential_form_schemas: [] },
custom_configuration: {
status: CustomConfigurationStatusEnum.active,
current_credential_id: 'cred-1',
current_credential_name: 'test-credential',
available_credentials: [{ credential_id: 'cred-1', credential_name: 'test-credential' }],
},
system_configuration: { enabled: true, current_quota_type: 'trial', quota_configurations: [] },
preferred_provider_type: PreferredProviderTypeEnum.system,
configurate_methods: [ConfigurationMethodEnum.predefinedModel],
supported_model_types: ['llm'],
...overrides,
} as unknown as ModelProvider)
const renderWithQueryClient = (provider: ModelProvider) => {
const queryClient = createTestQueryClient()
return render(
@ -88,74 +87,146 @@ const renderWithQueryClient = (provider: ModelProvider) => {
}
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,
Object.assign(mockTrialCredits, { credits: 100, isExhausted: false, isLoading: false })
})
// Text label variants
describe('Text label variants', () => {
it('should show "AI credits in use" for credits-active variant', () => {
renderWithQueryClient(createProvider())
expect(screen.getByText(/aiCreditsInUse/)).toBeInTheDocument()
})
it('should show "Credits exhausted" for credits-exhausted variant', () => {
mockTrialCredits.isExhausted = true
mockTrialCredits.credits = 0
renderWithQueryClient(createProvider({
custom_configuration: {
status: CustomConfigurationStatusEnum.noConfigure,
available_credentials: [],
},
}))
expect(screen.getByText(/quotaExhausted/)).toBeInTheDocument()
})
it('should show "No available usage" for no-usage variant', () => {
mockTrialCredits.isExhausted = true
renderWithQueryClient(createProvider({
custom_configuration: {
status: CustomConfigurationStatusEnum.active,
current_credential_id: undefined,
current_credential_name: undefined,
available_credentials: [{ credential_id: 'cred-1' }],
},
}))
expect(screen.getByText(/noAvailableUsage/)).toBeInTheDocument()
})
it('should show "API key required" for api-required-add variant', () => {
renderWithQueryClient(createProvider({
preferred_provider_type: PreferredProviderTypeEnum.custom,
custom_configuration: {
status: CustomConfigurationStatusEnum.noConfigure,
available_credentials: [],
},
}))
expect(screen.getByText(/apiKeyRequired/)).toBeInTheDocument()
})
})
it('should show credential name and configuration actions', () => {
renderWithQueryClient(mockProvider)
// Status label variants (dot + credential name)
describe('Status label variants', () => {
it('should show green indicator and credential name for api-fallback', () => {
mockTrialCredits.isExhausted = true
expect(screen.getByText('test-credential')).toBeInTheDocument()
expect(screen.getByTestId('config-provider')).toBeInTheDocument()
expect(screen.getByTestId('priority-selector')).toBeInTheDocument()
})
renderWithQueryClient(createProvider())
it('should show unauthorized status label when credential is missing', () => {
mockCredentialStatus.hasCredential = false
renderWithQueryClient(mockProvider)
expect(screen.getByTestId('indicator')).toHaveAttribute('data-color', 'green')
expect(screen.getByText('test-credential')).toBeInTheDocument()
})
expect(screen.getByText(/modelProvider\.auth\.unAuthorized/)).toBeInTheDocument()
})
it('should show green indicator for api-active', () => {
renderWithQueryClient(createProvider({
preferred_provider_type: PreferredProviderTypeEnum.custom,
}))
it('should show removed credential label and priority tip for custom preference', () => {
mockCredentialStatus.authorized = false
mockCredentialStatus.authRemoved = true
renderWithQueryClient({ ...mockProvider, preferred_provider_type: 'custom' } as ModelProvider)
expect(screen.getByTestId('indicator')).toHaveAttribute('data-color', 'green')
})
expect(screen.getByText(/modelProvider\.auth\.authRemoved/)).toBeInTheDocument()
expect(screen.getByTestId('priority-use-tip')).toBeInTheDocument()
})
it('should show red indicator and "Unavailable" for api-unavailable', () => {
renderWithQueryClient(createProvider({
preferred_provider_type: PreferredProviderTypeEnum.custom,
custom_configuration: {
status: CustomConfigurationStatusEnum.active,
current_credential_id: undefined,
current_credential_name: undefined,
available_credentials: [{ credential_id: 'cred-1' }],
},
}))
it('should change priority and refresh related data after success', async () => {
const mockChangePriority = changeModelProviderPriority as ReturnType<typeof vi.fn>
mockChangePriority.mockResolvedValue({ result: 'success' })
renderWithQueryClient(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()
expect(screen.getByTestId('indicator')).toHaveAttribute('data-color', 'red')
expect(screen.getByText(/unavailable/i)).toBeInTheDocument()
})
})
it('should render standalone priority selector without provider schema', () => {
const providerNoSchema = {
...mockProvider,
provider_credential_schema: null,
} as unknown as ModelProvider
renderWithQueryClient(providerNoSchema)
expect(screen.getByTestId('priority-selector')).toBeInTheDocument()
expect(screen.queryByTestId('config-provider')).not.toBeInTheDocument()
// Destructive styling
describe('Destructive styling', () => {
it('should apply destructive container for credits-exhausted', () => {
mockTrialCredits.isExhausted = true
const { container } = renderWithQueryClient(createProvider({
custom_configuration: {
status: CustomConfigurationStatusEnum.noConfigure,
available_credentials: [],
},
}))
const card = container.querySelector('[class*="border-state-destructive"]')
expect(card).toBeTruthy()
})
it('should apply default container for credits-active', () => {
const { container } = renderWithQueryClient(createProvider())
const card = container.querySelector('[class*="bg-white"]')
expect(card).toBeTruthy()
})
})
// Priority change
describe('Priority change', () => {
it('should change priority and refresh data after success', async () => {
const mockChangePriority = changeModelProviderPriority as ReturnType<typeof vi.fn>
mockChangePriority.mockResolvedValue({ result: 'success' })
renderWithQueryClient(createProvider())
fireEvent.click(screen.getByTestId('change-priority-btn'))
await waitFor(() => {
expect(mockChangePriority).toHaveBeenCalled()
expect(mockNotify).toHaveBeenCalled()
expect(mockUpdateModelProviders).toHaveBeenCalled()
expect(mockUpdateModelList).toHaveBeenCalledWith('llm')
expect(mockEventEmitter.emit).toHaveBeenCalled()
})
})
})
// ModelAuthDropdown integration
describe('ModelAuthDropdown integration', () => {
it('should pass state variant to ModelAuthDropdown', () => {
renderWithQueryClient(createProvider())
expect(screen.getByTestId('model-auth-dropdown')).toHaveAttribute('data-variant', 'credits-active')
})
})
})

View File

@ -0,0 +1,142 @@
import type { Credential, ModelProvider } from '../../declarations'
import { fireEvent, render, screen } from '@testing-library/react'
import { CustomConfigurationStatusEnum, PreferredProviderTypeEnum } from '../../declarations'
import ApiKeySection from './api-key-section'
const createCredential = (overrides: Partial<Credential> = {}): Credential => ({
credential_id: 'cred-1',
credential_name: 'Test API Key',
...overrides,
})
const createProvider = (overrides: Partial<ModelProvider> = {}): ModelProvider => ({
provider: 'test-provider',
allow_custom_token: true,
custom_configuration: {
status: CustomConfigurationStatusEnum.active,
available_credentials: [],
},
system_configuration: { enabled: true, current_quota_type: 'trial', quota_configurations: [] },
preferred_provider_type: PreferredProviderTypeEnum.system,
...overrides,
} as unknown as ModelProvider)
describe('ApiKeySection', () => {
const handlers = {
onItemClick: vi.fn(),
onEdit: vi.fn(),
onDelete: vi.fn(),
onAdd: vi.fn(),
}
beforeEach(() => {
vi.clearAllMocks()
})
// Empty state
describe('Empty state (no credentials)', () => {
it('should show empty state message', () => {
render(
<ApiKeySection
provider={createProvider()}
credentials={[]}
selectedCredentialId={undefined}
{...handlers}
/>,
)
expect(screen.getByText(/noApiKeysTitle/)).toBeInTheDocument()
expect(screen.getByText(/noApiKeysDescription/)).toBeInTheDocument()
})
it('should show Add API Key button', () => {
render(
<ApiKeySection
provider={createProvider()}
credentials={[]}
selectedCredentialId={undefined}
{...handlers}
/>,
)
expect(screen.getByRole('button', { name: /addApiKey/ })).toBeInTheDocument()
})
it('should call onAdd when Add API Key is clicked', () => {
render(
<ApiKeySection
provider={createProvider()}
credentials={[]}
selectedCredentialId={undefined}
{...handlers}
/>,
)
fireEvent.click(screen.getByRole('button', { name: /addApiKey/ }))
expect(handlers.onAdd).toHaveBeenCalledTimes(1)
})
it('should hide Add API Key button when allow_custom_token is false', () => {
render(
<ApiKeySection
provider={createProvider({ allow_custom_token: false })}
credentials={[]}
selectedCredentialId={undefined}
{...handlers}
/>,
)
expect(screen.queryByRole('button', { name: /addApiKey/ })).not.toBeInTheDocument()
})
})
// With credentials
describe('With credentials', () => {
const credentials = [
createCredential({ credential_id: 'cred-1', credential_name: 'Key Alpha' }),
createCredential({ credential_id: 'cred-2', credential_name: 'Key Beta' }),
]
it('should render credential list with header', () => {
render(
<ApiKeySection
provider={createProvider()}
credentials={credentials}
selectedCredentialId="cred-1"
{...handlers}
/>,
)
expect(screen.getByText(/apiKeys/)).toBeInTheDocument()
expect(screen.getByText('Key Alpha')).toBeInTheDocument()
expect(screen.getByText('Key Beta')).toBeInTheDocument()
})
it('should show Add API Key button in footer', () => {
render(
<ApiKeySection
provider={createProvider()}
credentials={credentials}
selectedCredentialId="cred-1"
{...handlers}
/>,
)
expect(screen.getByRole('button', { name: /addApiKey/ })).toBeInTheDocument()
})
it('should hide Add API Key when allow_custom_token is false', () => {
render(
<ApiKeySection
provider={createProvider({ allow_custom_token: false })}
credentials={credentials}
selectedCredentialId="cred-1"
{...handlers}
/>,
)
expect(screen.queryByRole('button', { name: /addApiKey/ })).not.toBeInTheDocument()
})
})
})

View File

@ -0,0 +1,63 @@
import { render, screen } from '@testing-library/react'
import CreditsExhaustedAlert from './credits-exhausted-alert'
const mockTrialCredits = { credits: 0, isExhausted: true, isLoading: false, nextCreditResetDate: undefined }
vi.mock('../use-trial-credits', () => ({
useTrialCredits: () => mockTrialCredits,
}))
describe('CreditsExhaustedAlert', () => {
beforeEach(() => {
vi.clearAllMocks()
Object.assign(mockTrialCredits, { credits: 0 })
})
// Without API key fallback
describe('Without API key fallback', () => {
it('should show exhausted message', () => {
render(<CreditsExhaustedAlert hasApiKeyFallback={false} />)
expect(screen.getByText(/creditsExhaustedMessage/)).toBeInTheDocument()
})
it('should show description with upgrade link', () => {
render(<CreditsExhaustedAlert hasApiKeyFallback={false} />)
expect(screen.getByText(/creditsExhaustedDescription/)).toBeInTheDocument()
})
})
// With API key fallback
describe('With API key fallback', () => {
it('should show fallback message', () => {
render(<CreditsExhaustedAlert hasApiKeyFallback />)
expect(screen.getByText(/creditsExhaustedFallback(?!Description)/)).toBeInTheDocument()
})
it('should show fallback description', () => {
render(<CreditsExhaustedAlert hasApiKeyFallback />)
expect(screen.getByText(/creditsExhaustedFallbackDescription/)).toBeInTheDocument()
})
})
// Usage display
describe('Usage display', () => {
it('should show usage label', () => {
render(<CreditsExhaustedAlert hasApiKeyFallback={false} />)
expect(screen.getByText(/usageLabel/)).toBeInTheDocument()
})
it('should show usage amounts', () => {
mockTrialCredits.credits = 200
render(<CreditsExhaustedAlert hasApiKeyFallback={false} />)
expect(screen.getByText(/9,800/)).toBeInTheDocument()
expect(screen.getByText(/10,000/)).toBeInTheDocument()
})
})
})

View File

@ -0,0 +1,199 @@
import type { CredentialPanelState } from '../use-credential-panel-state'
import type { ModelProvider } from '../../declarations'
import { fireEvent, render, screen } from '@testing-library/react'
import { CustomConfigurationStatusEnum, PreferredProviderTypeEnum } from '../../declarations'
import DropdownContent from './dropdown-content'
const mockHandleOpenModal = vi.fn()
const mockHandleActiveCredential = vi.fn()
const mockOpenConfirmDelete = vi.fn()
const mockCloseConfirmDelete = vi.fn()
const mockHandleConfirmDelete = vi.fn()
let mockDeleteCredentialId: string | null = null
vi.mock('../use-trial-credits', () => ({
useTrialCredits: () => ({ credits: 0, isExhausted: true, isLoading: false }),
}))
vi.mock('../../model-auth/hooks', () => ({
useAuth: () => ({
openConfirmDelete: mockOpenConfirmDelete,
closeConfirmDelete: mockCloseConfirmDelete,
doingAction: false,
handleActiveCredential: mockHandleActiveCredential,
handleConfirmDelete: mockHandleConfirmDelete,
deleteCredentialId: mockDeleteCredentialId,
handleOpenModal: mockHandleOpenModal,
}),
}))
const createProvider = (overrides: Partial<ModelProvider> = {}): ModelProvider => ({
provider: 'test',
custom_configuration: {
status: CustomConfigurationStatusEnum.active,
current_credential_id: 'cred-1',
current_credential_name: 'My Key',
available_credentials: [
{ credential_id: 'cred-1', credential_name: 'My Key' },
{ credential_id: 'cred-2', credential_name: 'Other Key' },
],
},
system_configuration: { enabled: true, current_quota_type: 'trial', quota_configurations: [] },
preferred_provider_type: PreferredProviderTypeEnum.system,
configurate_methods: ['predefined-model'],
supported_model_types: ['llm'],
...overrides,
} as unknown as ModelProvider)
const createState = (overrides: Partial<CredentialPanelState> = {}): CredentialPanelState => ({
variant: 'api-active',
priority: 'apiKey',
supportsCredits: true,
showPrioritySwitcher: true,
hasCredentials: true,
isCreditsExhausted: false,
credentialName: 'My Key',
credits: 100,
...overrides,
})
describe('DropdownContent', () => {
const onChangePriority = vi.fn()
const onClose = vi.fn()
beforeEach(() => {
vi.clearAllMocks()
mockDeleteCredentialId = null
})
// Conditional sections rendering
describe('Conditional sections', () => {
it('should show UsagePrioritySection when showPrioritySwitcher is true', () => {
render(
<DropdownContent
provider={createProvider()}
state={createState({ showPrioritySwitcher: true })}
onChangePriority={onChangePriority}
onClose={onClose}
/>,
)
expect(screen.getByText(/usagePriority/)).toBeInTheDocument()
})
it('should hide UsagePrioritySection when showPrioritySwitcher is false', () => {
render(
<DropdownContent
provider={createProvider()}
state={createState({ showPrioritySwitcher: false })}
onChangePriority={onChangePriority}
onClose={onClose}
/>,
)
expect(screen.queryByText(/usagePriority/)).not.toBeInTheDocument()
})
it('should show CreditsExhaustedAlert when credits exhausted and supports credits', () => {
render(
<DropdownContent
provider={createProvider()}
state={createState({ isCreditsExhausted: true, supportsCredits: true })}
onChangePriority={onChangePriority}
onClose={onClose}
/>,
)
expect(screen.getAllByText(/creditsExhausted/).length).toBeGreaterThan(0)
})
it('should hide CreditsExhaustedAlert when credits not exhausted', () => {
render(
<DropdownContent
provider={createProvider()}
state={createState({ isCreditsExhausted: false })}
onChangePriority={onChangePriority}
onClose={onClose}
/>,
)
expect(screen.queryByText(/creditsExhausted/)).not.toBeInTheDocument()
})
})
// API key section
describe('API key section', () => {
it('should render credential items', () => {
render(
<DropdownContent
provider={createProvider()}
state={createState()}
onChangePriority={onChangePriority}
onClose={onClose}
/>,
)
expect(screen.getByText('My Key')).toBeInTheDocument()
expect(screen.getByText('Other Key')).toBeInTheDocument()
})
it('should show empty state when no credentials', () => {
render(
<DropdownContent
provider={createProvider({
custom_configuration: {
status: CustomConfigurationStatusEnum.noConfigure,
available_credentials: [],
},
})}
state={createState({ hasCredentials: false })}
onChangePriority={onChangePriority}
onClose={onClose}
/>,
)
expect(screen.getByText(/noApiKeysTitle/)).toBeInTheDocument()
})
})
// Add API Key action
describe('Add API Key', () => {
it('should call handleOpenModal and onClose when adding API key', () => {
render(
<DropdownContent
provider={createProvider({
custom_configuration: {
status: CustomConfigurationStatusEnum.noConfigure,
available_credentials: [],
},
})}
state={createState({ hasCredentials: false })}
onChangePriority={onChangePriority}
onClose={onClose}
/>,
)
fireEvent.click(screen.getByRole('button', { name: /addApiKey/ }))
expect(mockHandleOpenModal).toHaveBeenCalledWith()
expect(onClose).toHaveBeenCalled()
})
})
// Width constraint
describe('Layout', () => {
it('should have 320px width container', () => {
const { container } = render(
<DropdownContent
provider={createProvider()}
state={createState()}
onChangePriority={onChangePriority}
onClose={onClose}
/>,
)
const widthContainer = container.querySelector('.w-\\[320px\\]')
expect(widthContainer).toBeTruthy()
})
})
})

View File

@ -0,0 +1,99 @@
import type { CredentialPanelState } from '../use-credential-panel-state'
import type { ModelProvider } from '../../declarations'
import { render, screen } from '@testing-library/react'
import { CustomConfigurationStatusEnum, PreferredProviderTypeEnum } from '../../declarations'
import ModelAuthDropdown from './index'
vi.mock('../../model-auth/hooks', () => ({
useAuth: () => ({
openConfirmDelete: vi.fn(),
closeConfirmDelete: vi.fn(),
doingAction: false,
handleActiveCredential: vi.fn(),
handleConfirmDelete: vi.fn(),
deleteCredentialId: null,
handleOpenModal: vi.fn(),
}),
}))
const createProvider = (): ModelProvider => ({
provider: 'test',
custom_configuration: {
status: CustomConfigurationStatusEnum.active,
available_credentials: [],
},
system_configuration: { enabled: true, current_quota_type: 'trial', quota_configurations: [] },
preferred_provider_type: PreferredProviderTypeEnum.system,
} as unknown as ModelProvider)
const createState = (overrides: Partial<CredentialPanelState> = {}): CredentialPanelState => ({
variant: 'credits-active',
priority: 'credits',
supportsCredits: true,
showPrioritySwitcher: false,
hasCredentials: false,
isCreditsExhausted: false,
credentialName: undefined,
credits: 100,
...overrides,
})
describe('ModelAuthDropdown', () => {
const onChangePriority = vi.fn()
beforeEach(() => {
vi.clearAllMocks()
})
// Button text based on variant
describe('Button configuration', () => {
it('should show "Add API Key" when no credentials and non-accent variant', () => {
render(
<ModelAuthDropdown
provider={createProvider()}
state={createState({ hasCredentials: false, variant: 'credits-active' })}
onChangePriority={onChangePriority}
/>,
)
expect(screen.getByRole('button', { name: /addApiKey/ })).toBeInTheDocument()
})
it('should show "Configure" when has credentials and non-accent variant', () => {
render(
<ModelAuthDropdown
provider={createProvider()}
state={createState({ hasCredentials: true, variant: 'api-active' })}
onChangePriority={onChangePriority}
/>,
)
expect(screen.getByRole('button', { name: /config/ })).toBeInTheDocument()
})
it('should show "Add API Key" for api-required-add variant with accent style', () => {
render(
<ModelAuthDropdown
provider={createProvider()}
state={createState({ variant: 'api-required-add', hasCredentials: false })}
onChangePriority={onChangePriority}
/>,
)
const button = screen.getByRole('button', { name: /addApiKey/ })
expect(button).toBeInTheDocument()
})
it('should show "Configure" for api-required-configure variant with accent style', () => {
render(
<ModelAuthDropdown
provider={createProvider()}
state={createState({ variant: 'api-required-configure', hasCredentials: true })}
onChangePriority={onChangePriority}
/>,
)
expect(screen.getByRole('button', { name: /config/ })).toBeInTheDocument()
})
})
})

View File

@ -0,0 +1,66 @@
import { fireEvent, render, screen } from '@testing-library/react'
import { PreferredProviderTypeEnum } from '../../declarations'
import UsagePrioritySection from './usage-priority-section'
describe('UsagePrioritySection', () => {
const onSelect = vi.fn()
beforeEach(() => {
vi.clearAllMocks()
})
// Rendering
describe('Rendering', () => {
it('should render title and both option buttons', () => {
render(<UsagePrioritySection value="credits" onSelect={onSelect} />)
expect(screen.getByText(/usagePriority/)).toBeInTheDocument()
expect(screen.getAllByRole('button')).toHaveLength(2)
})
})
// Selection state
describe('Selection state', () => {
it('should highlight AI credits option when value is credits', () => {
render(<UsagePrioritySection value="credits" onSelect={onSelect} />)
const buttons = screen.getAllByRole('button')
expect(buttons[0].className).toContain('border-components-option-card-option-selected-border')
expect(buttons[1].className).not.toContain('border-components-option-card-option-selected-border')
})
it('should highlight API key option when value is apiKey', () => {
render(<UsagePrioritySection value="apiKey" onSelect={onSelect} />)
const buttons = screen.getAllByRole('button')
expect(buttons[0].className).not.toContain('border-components-option-card-option-selected-border')
expect(buttons[1].className).toContain('border-components-option-card-option-selected-border')
})
it('should highlight API key option when value is apiKeyOnly', () => {
render(<UsagePrioritySection value="apiKeyOnly" onSelect={onSelect} />)
const buttons = screen.getAllByRole('button')
expect(buttons[1].className).toContain('border-components-option-card-option-selected-border')
})
})
// User interactions
describe('User interactions', () => {
it('should call onSelect with system when clicking AI credits option', () => {
render(<UsagePrioritySection value="apiKey" onSelect={onSelect} />)
fireEvent.click(screen.getAllByRole('button')[0])
expect(onSelect).toHaveBeenCalledWith(PreferredProviderTypeEnum.system)
})
it('should call onSelect with custom when clicking API key option', () => {
render(<UsagePrioritySection value="credits" onSelect={onSelect} />)
fireEvent.click(screen.getAllByRole('button')[1])
expect(onSelect).toHaveBeenCalledWith(PreferredProviderTypeEnum.custom)
})
})
})

View File

@ -0,0 +1,89 @@
import { render, screen } from '@testing-library/react'
import SystemQuotaCard from './system-quota-card'
describe('SystemQuotaCard', () => {
// Renders container with children
describe('Rendering', () => {
it('should render children', () => {
render(
<SystemQuotaCard>
<span>content</span>
</SystemQuotaCard>,
)
expect(screen.getByText('content')).toBeInTheDocument()
})
it('should apply default variant styles', () => {
const { container } = render(
<SystemQuotaCard>
<span>test</span>
</SystemQuotaCard>,
)
const card = container.firstElementChild!
expect(card.className).toContain('bg-white')
})
it('should apply destructive variant styles', () => {
const { container } = render(
<SystemQuotaCard variant="destructive">
<span>test</span>
</SystemQuotaCard>,
)
const card = container.firstElementChild!
expect(card.className).toContain('border-state-destructive-border')
})
})
// Label sub-component
describe('Label', () => {
it('should apply default variant text color when no className provided', () => {
render(
<SystemQuotaCard>
<SystemQuotaCard.Label>Default label</SystemQuotaCard.Label>
</SystemQuotaCard>,
)
expect(screen.getByText('Default label').className).toContain('text-text-secondary')
})
it('should apply destructive variant text color when no className provided', () => {
render(
<SystemQuotaCard variant="destructive">
<SystemQuotaCard.Label>Error label</SystemQuotaCard.Label>
</SystemQuotaCard>,
)
expect(screen.getByText('Error label').className).toContain('text-text-destructive')
})
it('should override variant color with custom className', () => {
render(
<SystemQuotaCard variant="destructive">
<SystemQuotaCard.Label className="gap-1">Custom label</SystemQuotaCard.Label>
</SystemQuotaCard>,
)
const label = screen.getByText('Custom label')
expect(label.className).toContain('gap-1')
expect(label.className).not.toContain('text-text-destructive')
})
})
// Actions sub-component
describe('Actions', () => {
it('should render action children', () => {
render(
<SystemQuotaCard>
<SystemQuotaCard.Actions>
<button>Click me</button>
</SystemQuotaCard.Actions>
</SystemQuotaCard>,
)
expect(screen.getByRole('button', { name: /click me/i })).toBeInTheDocument()
})
})
})

View File

@ -0,0 +1,203 @@
import type { ModelProvider } from '../declarations'
import { renderHook } from '@testing-library/react'
import {
ConfigurationMethodEnum,
CustomConfigurationStatusEnum,
PreferredProviderTypeEnum,
} from '../declarations'
import { isDestructiveVariant, useCredentialPanelState } from './use-credential-panel-state'
const mockTrialCredits = { credits: 100, isExhausted: false, isLoading: false, nextCreditResetDate: undefined }
vi.mock('./use-trial-credits', () => ({
useTrialCredits: () => mockTrialCredits,
}))
vi.mock('@/config', async (importOriginal) => {
const actual = await importOriginal<typeof import('@/config')>()
return { ...actual, IS_CLOUD_EDITION: true }
})
const createProvider = (overrides: Partial<ModelProvider> = {}): ModelProvider => ({
provider: 'test-provider',
provider_credential_schema: { credential_form_schemas: [] },
custom_configuration: {
status: CustomConfigurationStatusEnum.active,
current_credential_id: 'cred-1',
current_credential_name: 'My Key',
available_credentials: [{ credential_id: 'cred-1', credential_name: 'My Key' }],
},
system_configuration: { enabled: true, current_quota_type: 'trial', quota_configurations: [] },
preferred_provider_type: PreferredProviderTypeEnum.system,
configurate_methods: [ConfigurationMethodEnum.predefinedModel],
supported_model_types: ['llm'],
...overrides,
} as unknown as ModelProvider)
describe('useCredentialPanelState', () => {
beforeEach(() => {
vi.clearAllMocks()
Object.assign(mockTrialCredits, { credits: 100, isExhausted: false, isLoading: false })
})
// Credits priority variants
describe('Credits priority variants', () => {
it('should return credits-active when credits available', () => {
const { result } = renderHook(() => useCredentialPanelState(createProvider()))
expect(result.current.variant).toBe('credits-active')
expect(result.current.priority).toBe('credits')
expect(result.current.supportsCredits).toBe(true)
})
it('should return api-fallback when credits exhausted but API key authorized', () => {
mockTrialCredits.isExhausted = true
mockTrialCredits.credits = 0
const { result } = renderHook(() => useCredentialPanelState(createProvider()))
expect(result.current.variant).toBe('api-fallback')
})
it('should return no-usage when credits exhausted and API key unauthorized', () => {
mockTrialCredits.isExhausted = true
const provider = createProvider({
custom_configuration: {
status: CustomConfigurationStatusEnum.active,
current_credential_id: undefined,
current_credential_name: undefined,
available_credentials: [{ credential_id: 'cred-1', credential_name: 'My Key' }],
},
})
const { result } = renderHook(() => useCredentialPanelState(provider))
expect(result.current.variant).toBe('no-usage')
})
it('should return credits-exhausted when credits exhausted and no credentials', () => {
mockTrialCredits.isExhausted = true
const provider = createProvider({
custom_configuration: {
status: CustomConfigurationStatusEnum.noConfigure,
available_credentials: [],
},
})
const { result } = renderHook(() => useCredentialPanelState(provider))
expect(result.current.variant).toBe('credits-exhausted')
})
})
// API key priority variants
describe('API key priority variants', () => {
it('should return api-active when API key authorized', () => {
const provider = createProvider({
preferred_provider_type: PreferredProviderTypeEnum.custom,
})
const { result } = renderHook(() => useCredentialPanelState(provider))
expect(result.current.variant).toBe('api-active')
expect(result.current.priority).toBe('apiKey')
})
it('should return api-unavailable when API key unauthorized', () => {
const provider = createProvider({
preferred_provider_type: PreferredProviderTypeEnum.custom,
custom_configuration: {
status: CustomConfigurationStatusEnum.active,
current_credential_id: undefined,
current_credential_name: undefined,
available_credentials: [{ credential_id: 'cred-1', credential_name: 'My Key' }],
},
})
const { result } = renderHook(() => useCredentialPanelState(provider))
expect(result.current.variant).toBe('api-unavailable')
})
it('should return api-required-add when no credentials exist', () => {
const provider = createProvider({
preferred_provider_type: PreferredProviderTypeEnum.custom,
custom_configuration: {
status: CustomConfigurationStatusEnum.noConfigure,
available_credentials: [],
},
})
const { result } = renderHook(() => useCredentialPanelState(provider))
expect(result.current.variant).toBe('api-required-add')
})
})
// apiKeyOnly priority
describe('apiKeyOnly priority (non-cloud / system disabled)', () => {
it('should return apiKeyOnly when system config disabled', () => {
const provider = createProvider({
system_configuration: { enabled: false, current_quota_type: 'trial', quota_configurations: [] },
})
const { result } = renderHook(() => useCredentialPanelState(provider))
expect(result.current.priority).toBe('apiKeyOnly')
expect(result.current.supportsCredits).toBe(false)
})
})
// Derived metadata
describe('Derived metadata', () => {
it('should show priority switcher when credits supported and custom config active', () => {
const provider = createProvider()
const { result } = renderHook(() => useCredentialPanelState(provider))
expect(result.current.showPrioritySwitcher).toBe(true)
})
it('should hide priority switcher when custom config not active', () => {
const provider = createProvider({
custom_configuration: {
status: CustomConfigurationStatusEnum.noConfigure,
available_credentials: [],
},
})
const { result } = renderHook(() => useCredentialPanelState(provider))
expect(result.current.showPrioritySwitcher).toBe(false)
})
it('should expose credential name from provider', () => {
const { result } = renderHook(() => useCredentialPanelState(createProvider()))
expect(result.current.credentialName).toBe('My Key')
})
it('should expose credits amount', () => {
mockTrialCredits.credits = 500
const { result } = renderHook(() => useCredentialPanelState(createProvider()))
expect(result.current.credits).toBe(500)
})
})
})
describe('isDestructiveVariant', () => {
it.each([
['credits-exhausted', true],
['no-usage', true],
['api-unavailable', true],
['credits-active', false],
['api-fallback', false],
['api-active', false],
['api-required-add', false],
['api-required-configure', false],
] as const)('should return %s for variant %s', (variant, expected) => {
expect(isDestructiveVariant(variant)).toBe(expected)
})
})