mirror of
https://github.com/langgenius/dify.git
synced 2026-05-06 10:28:10 +08:00
318 lines
12 KiB
TypeScript
318 lines
12 KiB
TypeScript
import type { ComponentProps } from 'react'
|
|
import type { Credential, CredentialFormSchema, CustomModel, ModelProvider } from '../declarations'
|
|
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
|
import * as React from 'react'
|
|
import {
|
|
ConfigurationMethodEnum,
|
|
CurrentSystemQuotaTypeEnum,
|
|
CustomConfigurationStatusEnum,
|
|
ModelModalModeEnum,
|
|
ModelTypeEnum,
|
|
PreferredProviderTypeEnum,
|
|
QuotaUnitEnum,
|
|
} from '../declarations'
|
|
import ModelModal from './index'
|
|
|
|
type CredentialData = {
|
|
credentials: Record<string, unknown>
|
|
available_credentials: Credential[]
|
|
}
|
|
|
|
type ModelFormSchemas = {
|
|
formSchemas: CredentialFormSchema[]
|
|
formValues: Record<string, unknown>
|
|
modelNameAndTypeFormSchemas: CredentialFormSchema[]
|
|
modelNameAndTypeFormValues: Record<string, unknown>
|
|
}
|
|
|
|
const mockState = vi.hoisted(() => ({
|
|
isLoading: false,
|
|
credentialData: { credentials: {}, available_credentials: [] } as CredentialData,
|
|
doingAction: false,
|
|
deleteCredentialId: null as string | null,
|
|
isCurrentWorkspaceManager: true,
|
|
formSchemas: [] as CredentialFormSchema[],
|
|
formValues: {} as Record<string, unknown>,
|
|
modelNameAndTypeFormSchemas: [] as CredentialFormSchema[],
|
|
modelNameAndTypeFormValues: {} as Record<string, unknown>,
|
|
}))
|
|
|
|
const mockHandlers = vi.hoisted(() => ({
|
|
handleSaveCredential: vi.fn(),
|
|
handleConfirmDelete: vi.fn(),
|
|
closeConfirmDelete: vi.fn(),
|
|
openConfirmDelete: vi.fn(),
|
|
handleActiveCredential: vi.fn(),
|
|
}))
|
|
|
|
vi.mock('../model-auth/hooks', () => ({
|
|
useCredentialData: () => ({
|
|
isLoading: mockState.isLoading,
|
|
credentialData: mockState.credentialData,
|
|
}),
|
|
useAuth: () => ({
|
|
handleSaveCredential: mockHandlers.handleSaveCredential,
|
|
handleConfirmDelete: mockHandlers.handleConfirmDelete,
|
|
deleteCredentialId: mockState.deleteCredentialId,
|
|
closeConfirmDelete: mockHandlers.closeConfirmDelete,
|
|
openConfirmDelete: mockHandlers.openConfirmDelete,
|
|
doingAction: mockState.doingAction,
|
|
handleActiveCredential: mockHandlers.handleActiveCredential,
|
|
}),
|
|
useModelFormSchemas: (): ModelFormSchemas => ({
|
|
formSchemas: mockState.formSchemas,
|
|
formValues: mockState.formValues,
|
|
modelNameAndTypeFormSchemas: mockState.modelNameAndTypeFormSchemas,
|
|
modelNameAndTypeFormValues: mockState.modelNameAndTypeFormValues,
|
|
}),
|
|
}))
|
|
|
|
vi.mock('@/context/app-context', () => ({
|
|
useAppContext: () => ({ isCurrentWorkspaceManager: mockState.isCurrentWorkspaceManager }),
|
|
}))
|
|
|
|
vi.mock('@/hooks/use-i18n', () => ({
|
|
useRenderI18nObject: () => (value: { en_US: string }) => value.en_US,
|
|
}))
|
|
|
|
vi.mock('../hooks', () => ({
|
|
useLanguage: () => 'en_US',
|
|
}))
|
|
|
|
const createI18n = (text: string) => ({ en_US: text, zh_Hans: text })
|
|
|
|
const createProvider = (overrides?: Partial<ModelProvider>): ModelProvider => ({
|
|
provider: 'openai',
|
|
label: createI18n('OpenAI'),
|
|
help: {
|
|
title: createI18n('Help'),
|
|
url: createI18n('https://example.com'),
|
|
},
|
|
icon_small: createI18n('icon'),
|
|
supported_model_types: [ModelTypeEnum.textGeneration],
|
|
configurate_methods: [ConfigurationMethodEnum.predefinedModel],
|
|
provider_credential_schema: { credential_form_schemas: [] },
|
|
model_credential_schema: {
|
|
model: { label: createI18n('Model'), placeholder: createI18n('Model') },
|
|
credential_form_schemas: [],
|
|
},
|
|
preferred_provider_type: PreferredProviderTypeEnum.system,
|
|
custom_configuration: {
|
|
status: CustomConfigurationStatusEnum.active,
|
|
available_credentials: [],
|
|
custom_models: [],
|
|
can_added_models: [],
|
|
},
|
|
system_configuration: {
|
|
enabled: true,
|
|
current_quota_type: CurrentSystemQuotaTypeEnum.trial,
|
|
quota_configurations: [
|
|
{
|
|
quota_type: CurrentSystemQuotaTypeEnum.trial,
|
|
quota_unit: QuotaUnitEnum.times,
|
|
quota_limit: 0,
|
|
quota_used: 0,
|
|
last_used: 0,
|
|
is_valid: true,
|
|
},
|
|
],
|
|
},
|
|
allow_custom_token: true,
|
|
...overrides,
|
|
})
|
|
|
|
const renderModal = (overrides?: Partial<ComponentProps<typeof ModelModal>>) => {
|
|
const provider = createProvider()
|
|
const props = {
|
|
provider,
|
|
configurateMethod: ConfigurationMethodEnum.predefinedModel,
|
|
onCancel: vi.fn(),
|
|
onSave: vi.fn(),
|
|
onRemove: vi.fn(),
|
|
...overrides,
|
|
}
|
|
render(<ModelModal {...props} />)
|
|
return props
|
|
}
|
|
|
|
const mockFormRef1 = {
|
|
getFormValues: vi.fn(),
|
|
getForm: vi.fn(() => ({ setFieldValue: vi.fn() })),
|
|
}
|
|
|
|
const mockFormRef2 = {
|
|
getFormValues: vi.fn(),
|
|
getForm: vi.fn(() => ({ setFieldValue: vi.fn() })),
|
|
}
|
|
|
|
vi.mock('@/app/components/base/form/form-scenarios/auth', () => ({
|
|
default: React.forwardRef((props: { formSchemas: Record<string, unknown>[], onChange?: (f: string, v: string) => void }, ref: React.ForwardedRef<unknown>) => {
|
|
React.useImperativeHandle(ref, () => {
|
|
// Return the mock depending on schemas passed (hacky but works for refs)
|
|
if (props.formSchemas.length > 0 && props.formSchemas[0].name === '__model_name')
|
|
return mockFormRef1
|
|
return mockFormRef2
|
|
})
|
|
return (
|
|
<div data-testid="auth-form" onClick={() => props.onChange?.('test-field', 'val')}>
|
|
AuthForm Mock (
|
|
{props.formSchemas.length}
|
|
{' '}
|
|
fields)
|
|
</div>
|
|
)
|
|
}),
|
|
}))
|
|
|
|
vi.mock('../model-auth', () => ({
|
|
CredentialSelector: ({ onSelect }: { onSelect: (val: unknown) => void }) => (
|
|
<button onClick={() => onSelect({ addNewCredential: true })} data-testid="credential-selector">
|
|
Select Credential
|
|
</button>
|
|
),
|
|
useAuth: vi.fn(),
|
|
useCredentialData: vi.fn(),
|
|
useModelFormSchemas: vi.fn(),
|
|
}))
|
|
|
|
describe('ModelModal', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
mockState.isLoading = false
|
|
mockState.credentialData = { credentials: {}, available_credentials: [] }
|
|
mockState.doingAction = false
|
|
mockState.deleteCredentialId = null
|
|
mockState.isCurrentWorkspaceManager = true
|
|
mockState.formSchemas = []
|
|
mockState.formValues = {}
|
|
mockState.modelNameAndTypeFormSchemas = []
|
|
mockState.modelNameAndTypeFormValues = {}
|
|
|
|
// reset form refs
|
|
mockFormRef1.getFormValues.mockReturnValue({ isCheckValidated: true, values: { __model_name: 'test', __model_type: ModelTypeEnum.textGeneration } })
|
|
mockFormRef2.getFormValues.mockReturnValue({ isCheckValidated: true, values: { __authorization_name__: 'test_auth', api_key: 'sk-test' } })
|
|
})
|
|
|
|
it('should render title and loading state for predefined credential modal', () => {
|
|
mockState.isLoading = true
|
|
renderModal()
|
|
expect(screen.getByText('common.modelProvider.auth.apiKeyModal.title')).toBeInTheDocument()
|
|
expect(screen.getByText('common.modelProvider.auth.apiKeyModal.desc')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render model credential title when mode is configModelCredential', () => {
|
|
renderModal({
|
|
mode: ModelModalModeEnum.configModelCredential,
|
|
model: { model: 'gpt-4', model_type: ModelTypeEnum.textGeneration },
|
|
})
|
|
expect(screen.getByText('common.modelProvider.auth.addModelCredential')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render edit credential title when credential exists', () => {
|
|
renderModal({
|
|
mode: ModelModalModeEnum.configModelCredential,
|
|
credential: { credential_id: '1' } as unknown as Credential,
|
|
})
|
|
expect(screen.getByText('common.modelProvider.auth.editModelCredential')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should change title to Add Model when mode is configCustomModel', () => {
|
|
mockState.modelNameAndTypeFormSchemas = [{ variable: '__model_name', type: 'text' } as unknown as CredentialFormSchema]
|
|
renderModal({ mode: ModelModalModeEnum.configCustomModel })
|
|
expect(screen.getByText('common.modelProvider.auth.addModel')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should validate and fail save if form is invalid in configCustomModel mode', async () => {
|
|
mockState.modelNameAndTypeFormSchemas = [{ variable: '__model_name', type: 'text' } as unknown as CredentialFormSchema]
|
|
mockFormRef1.getFormValues.mockReturnValue({ isCheckValidated: false, values: {} })
|
|
renderModal({ mode: ModelModalModeEnum.configCustomModel })
|
|
fireEvent.click(screen.getByRole('button', { name: 'common.operation.add' }))
|
|
expect(mockHandlers.handleSaveCredential).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('should validate and save new credential and model in configCustomModel mode', async () => {
|
|
mockState.modelNameAndTypeFormSchemas = [{ variable: '__model_name', type: 'text' } as unknown as CredentialFormSchema]
|
|
const props = renderModal({ mode: ModelModalModeEnum.configCustomModel })
|
|
fireEvent.click(screen.getByRole('button', { name: 'common.operation.add' }))
|
|
|
|
await waitFor(() => {
|
|
expect(mockHandlers.handleSaveCredential).toHaveBeenCalledWith({
|
|
credential_id: undefined,
|
|
credentials: { api_key: 'sk-test' },
|
|
name: 'test_auth',
|
|
model: 'test',
|
|
model_type: ModelTypeEnum.textGeneration,
|
|
})
|
|
expect(props.onSave).toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
it('should save credential only in standard configProviderCredential mode', async () => {
|
|
const { onSave } = renderModal({ mode: ModelModalModeEnum.configProviderCredential })
|
|
fireEvent.click(screen.getByRole('button', { name: 'common.operation.save' }))
|
|
|
|
await waitFor(() => {
|
|
expect(mockHandlers.handleSaveCredential).toHaveBeenCalledWith({
|
|
credential_id: undefined,
|
|
credentials: { api_key: 'sk-test' },
|
|
name: 'test_auth',
|
|
})
|
|
expect(onSave).toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
it('should save active credential and cancel when picking existing credential in addCustomModelToModelList mode', async () => {
|
|
renderModal({ mode: ModelModalModeEnum.addCustomModelToModelList, model: { model: 'm1', model_type: ModelTypeEnum.textGeneration } as unknown as CustomModel })
|
|
// By default selected is undefined so button clicks form
|
|
// Let's not click credential selector, so it evaluates without it. If selectedCredential is undefined, form validation is checked.
|
|
mockFormRef2.getFormValues.mockReturnValue({ isCheckValidated: false, values: {} })
|
|
fireEvent.click(screen.getByRole('button', { name: 'common.operation.add' }))
|
|
expect(mockHandlers.handleSaveCredential).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('should save active credential when picking existing credential in addCustomModelToModelList mode', async () => {
|
|
renderModal({ mode: ModelModalModeEnum.addCustomModelToModelList, model: { model: 'm2', model_type: ModelTypeEnum.textGeneration } as unknown as CustomModel })
|
|
|
|
// Select existing credential (addNewCredential: true simulates new but we can simulate false if we just hack the mocked state in the component, but it's internal.
|
|
// The credential selector sets selectedCredential.
|
|
fireEvent.click(screen.getByTestId('credential-selector')) // Sets addNewCredential = true internally, so it proceeds to form save
|
|
|
|
mockFormRef2.getFormValues.mockReturnValue({ isCheckValidated: true, values: { __authorization_name__: 'auth', api: 'key' } })
|
|
fireEvent.click(screen.getByRole('button', { name: 'common.operation.add' }))
|
|
|
|
await waitFor(() => {
|
|
expect(mockHandlers.handleSaveCredential).toHaveBeenCalledWith({
|
|
credential_id: undefined,
|
|
credentials: { api: 'key' },
|
|
name: 'auth',
|
|
model: 'm2',
|
|
model_type: ModelTypeEnum.textGeneration,
|
|
})
|
|
})
|
|
})
|
|
|
|
it('should open and confirm deletion of credential', () => {
|
|
mockState.credentialData = { credentials: { api_key: '123' }, available_credentials: [] }
|
|
mockState.formValues = { api_key: '123' } // To trigger isEditMode = true
|
|
const credential = { credential_id: 'c1' } as unknown as Credential
|
|
renderModal({ credential })
|
|
|
|
// Open Delete Confirm
|
|
fireEvent.click(screen.getByRole('button', { name: 'common.operation.remove' }))
|
|
expect(mockHandlers.openConfirmDelete).toHaveBeenCalledWith(credential, undefined)
|
|
|
|
// Simulate the dialog appearing and confirming
|
|
mockState.deleteCredentialId = 'c1'
|
|
renderModal({ credential }) // Re-render logic mock
|
|
fireEvent.click(screen.getAllByRole('button', { name: 'common.operation.confirm' })[0])
|
|
|
|
expect(mockHandlers.handleConfirmDelete).toHaveBeenCalled()
|
|
})
|
|
|
|
it('should bind escape key to cancel', () => {
|
|
const props = renderModal()
|
|
fireEvent.keyDown(document, { key: 'Escape' })
|
|
expect(props.onCancel).toHaveBeenCalled()
|
|
})
|
|
})
|