mirror of https://github.com/langgenius/dify.git
chore: some test (#30148)
Co-authored-by: yyh <92089059+lyzno1@users.noreply.github.com>
This commit is contained in:
parent
08d5eee993
commit
0f3ffbee2c
|
|
@ -0,0 +1,136 @@
|
|||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import Apps from './index'
|
||||
|
||||
const mockUseExploreAppList = vi.fn()
|
||||
|
||||
vi.mock('ahooks', () => ({
|
||||
useDebounceFn: (fn: () => void) => ({
|
||||
run: () => setTimeout(fn, 0),
|
||||
cancel: vi.fn(),
|
||||
flush: () => fn(),
|
||||
}),
|
||||
}))
|
||||
vi.mock('@/context/app-context', () => ({
|
||||
useAppContext: () => ({ isCurrentWorkspaceEditor: true }),
|
||||
}))
|
||||
vi.mock('use-context-selector', async () => {
|
||||
const actual = await vi.importActual<typeof import('use-context-selector')>('use-context-selector')
|
||||
return {
|
||||
...actual,
|
||||
useContext: () => ({ hasEditPermission: true }),
|
||||
}
|
||||
})
|
||||
vi.mock('@/hooks/use-tab-searchparams', () => ({
|
||||
useTabSearchParams: () => ['Recommended', vi.fn()],
|
||||
}))
|
||||
vi.mock('@/service/use-explore', () => ({
|
||||
useExploreAppList: () => mockUseExploreAppList(),
|
||||
}))
|
||||
vi.mock('@/app/components/app/type-selector', () => ({
|
||||
__esModule: true,
|
||||
default: ({ value, onChange }: { value: AppModeEnum[], onChange: (value: AppModeEnum[]) => void }) => (
|
||||
<button data-testid="type-selector" onClick={() => onChange([...value, 'chat' as AppModeEnum])}>{value.join(',')}</button>
|
||||
),
|
||||
}))
|
||||
vi.mock('../app-card', () => ({
|
||||
__esModule: true,
|
||||
default: ({ app, onCreate }: { app: any, onCreate: () => void }) => (
|
||||
<div
|
||||
data-testid="app-card"
|
||||
data-name={app.app.name}
|
||||
onClick={onCreate}
|
||||
>
|
||||
{app.app.name}
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
vi.mock('@/app/components/explore/create-app-modal', () => ({
|
||||
__esModule: true,
|
||||
default: () => <div data-testid="create-from-template-modal" />,
|
||||
}))
|
||||
vi.mock('@/app/components/base/toast', () => ({
|
||||
default: { notify: vi.fn() },
|
||||
}))
|
||||
vi.mock('@/app/components/base/amplitude', () => ({
|
||||
trackEvent: vi.fn(),
|
||||
}))
|
||||
vi.mock('@/service/apps', () => ({
|
||||
importDSL: vi.fn().mockResolvedValue({ app_id: '1' }),
|
||||
}))
|
||||
vi.mock('@/service/explore', () => ({
|
||||
fetchAppDetail: vi.fn().mockResolvedValue({
|
||||
export_data: 'dsl',
|
||||
mode: 'chat',
|
||||
}),
|
||||
}))
|
||||
vi.mock('@/app/components/workflow/plugin-dependency/hooks', () => ({
|
||||
usePluginDependencies: () => ({
|
||||
handleCheckPluginDependencies: vi.fn(),
|
||||
}),
|
||||
}))
|
||||
vi.mock('@/utils/app-redirection', () => ({
|
||||
getRedirection: vi.fn(),
|
||||
}))
|
||||
vi.mock('next/navigation', () => ({
|
||||
useRouter: () => ({ push: vi.fn() }),
|
||||
}))
|
||||
|
||||
const createAppEntry = (name: string, category: string) => ({
|
||||
app_id: name,
|
||||
category,
|
||||
app: {
|
||||
id: name,
|
||||
name,
|
||||
icon_type: 'emoji',
|
||||
icon: '🙂',
|
||||
icon_background: '#000',
|
||||
icon_url: null,
|
||||
description: 'desc',
|
||||
mode: AppModeEnum.CHAT,
|
||||
},
|
||||
})
|
||||
|
||||
describe('Apps', () => {
|
||||
const defaultData = {
|
||||
allList: [
|
||||
createAppEntry('Alpha', 'Cat A'),
|
||||
createAppEntry('Bravo', 'Cat B'),
|
||||
],
|
||||
categories: ['Cat A', 'Cat B'],
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockUseExploreAppList.mockReturnValue({
|
||||
data: defaultData,
|
||||
isLoading: false,
|
||||
})
|
||||
})
|
||||
|
||||
it('renders template cards when data is available', () => {
|
||||
render(<Apps />)
|
||||
|
||||
expect(screen.getAllByTestId('app-card')).toHaveLength(2)
|
||||
expect(screen.getByText('Alpha')).toBeInTheDocument()
|
||||
expect(screen.getByText('Bravo')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('opens create modal when a template card is clicked', () => {
|
||||
render(<Apps />)
|
||||
|
||||
fireEvent.click(screen.getAllByTestId('app-card')[0])
|
||||
expect(screen.getByTestId('create-from-template-modal')).toBeInTheDocument()
|
||||
})
|
||||
it('shows no template message when list is empty', () => {
|
||||
mockUseExploreAppList.mockReturnValueOnce({
|
||||
data: { allList: [], categories: [] },
|
||||
isLoading: false,
|
||||
})
|
||||
|
||||
render(<Apps />)
|
||||
|
||||
expect(screen.getByText('app.newApp.noTemplateFound')).toBeInTheDocument()
|
||||
expect(screen.getByText('app.newApp.noTemplateFoundTip')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import Sidebar, { AppCategories } from './sidebar'
|
||||
|
||||
vi.mock('@remixicon/react', () => ({
|
||||
RiStickyNoteAddLine: () => <span>sticky</span>,
|
||||
RiThumbUpLine: () => <span>thumb</span>,
|
||||
}))
|
||||
describe('Sidebar', () => {
|
||||
it('renders recommended and custom categories', () => {
|
||||
render(<Sidebar current={AppCategories.RECOMMENDED} categories={['Cat A', 'Cat B']} />)
|
||||
|
||||
expect(screen.getByText('app.newAppFromTemplate.sidebar.Recommended')).toBeInTheDocument()
|
||||
expect(screen.getByText('Cat A')).toBeInTheDocument()
|
||||
expect(screen.getByText('Cat B')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('notifies callbacks when items are clicked', () => {
|
||||
const onClick = vi.fn()
|
||||
const onCreate = vi.fn()
|
||||
render(
|
||||
<Sidebar
|
||||
current="Cat A"
|
||||
categories={['Cat A']}
|
||||
onClick={onClick}
|
||||
onCreateFromBlank={onCreate}
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByText('app.newAppFromTemplate.sidebar.Recommended'))
|
||||
expect(onClick).toHaveBeenCalledWith(AppCategories.RECOMMENDED)
|
||||
|
||||
fireEvent.click(screen.getByText('Cat A'))
|
||||
expect(onClick).toHaveBeenCalledWith('Cat A')
|
||||
|
||||
fireEvent.click(screen.getByText('app.newApp.startFromBlank'))
|
||||
expect(onCreate).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,217 @@
|
|||
import type { ReactNode } from 'react'
|
||||
import type { ModalContextState } from '@/context/modal-context'
|
||||
import type { ProviderContextState } from '@/context/provider-context'
|
||||
import type { AppDetailResponse } from '@/models/app'
|
||||
import type { AppSSO } from '@/types/app'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { Plan } from '@/app/components/billing/type'
|
||||
import { baseProviderContextValue } from '@/context/provider-context'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import SettingsModal from './index'
|
||||
|
||||
vi.mock('react-i18next', async () => {
|
||||
const actual = await vi.importActual<typeof import('react-i18next')>('react-i18next')
|
||||
return {
|
||||
...actual,
|
||||
useTranslation: () => ({
|
||||
t: (key: string, options?: Record<string, unknown>) => {
|
||||
if (options?.returnObjects)
|
||||
return [`${key}-feature-1`, `${key}-feature-2`]
|
||||
if (options)
|
||||
return `${key}:${JSON.stringify(options)}`
|
||||
return key
|
||||
},
|
||||
i18n: {
|
||||
language: 'en',
|
||||
changeLanguage: vi.fn(),
|
||||
},
|
||||
}),
|
||||
Trans: ({ children }: { children?: ReactNode }) => <>{children}</>,
|
||||
}
|
||||
})
|
||||
|
||||
const mockNotify = vi.fn()
|
||||
const mockOnClose = vi.fn()
|
||||
const mockOnSave = vi.fn()
|
||||
const mockSetShowPricingModal = vi.fn()
|
||||
const mockSetShowAccountSettingModal = vi.fn()
|
||||
const mockUseProviderContext = vi.fn<() => ProviderContextState>()
|
||||
|
||||
const buildModalContext = (): ModalContextState => ({
|
||||
setShowAccountSettingModal: mockSetShowAccountSettingModal,
|
||||
setShowApiBasedExtensionModal: vi.fn(),
|
||||
setShowModerationSettingModal: vi.fn(),
|
||||
setShowExternalDataToolModal: vi.fn(),
|
||||
setShowPricingModal: mockSetShowPricingModal,
|
||||
setShowAnnotationFullModal: vi.fn(),
|
||||
setShowModelModal: vi.fn(),
|
||||
setShowExternalKnowledgeAPIModal: vi.fn(),
|
||||
setShowModelLoadBalancingModal: vi.fn(),
|
||||
setShowOpeningModal: vi.fn(),
|
||||
setShowUpdatePluginModal: vi.fn(),
|
||||
setShowEducationExpireNoticeModal: vi.fn(),
|
||||
setShowTriggerEventsLimitModal: vi.fn(),
|
||||
})
|
||||
|
||||
vi.mock('@/context/modal-context', () => ({
|
||||
useModalContext: () => buildModalContext(),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/toast', async () => {
|
||||
const actual = await vi.importActual<typeof import('@/app/components/base/toast')>('@/app/components/base/toast')
|
||||
return {
|
||||
...actual,
|
||||
useToastContext: () => ({
|
||||
notify: mockNotify,
|
||||
close: vi.fn(),
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/context/i18n', async () => {
|
||||
const actual = await vi.importActual<typeof import('@/context/i18n')>('@/context/i18n')
|
||||
return {
|
||||
...actual,
|
||||
useDocLink: () => (path?: string) => `https://docs.example.com${path ?? ''}`,
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/context/provider-context', async () => {
|
||||
const actual = await vi.importActual<typeof import('@/context/provider-context')>('@/context/provider-context')
|
||||
return {
|
||||
...actual,
|
||||
useProviderContext: () => mockUseProviderContext(),
|
||||
}
|
||||
})
|
||||
|
||||
const mockAppInfo = {
|
||||
site: {
|
||||
title: 'Test App',
|
||||
icon_type: 'emoji',
|
||||
icon: '😀',
|
||||
icon_background: '#ABCDEF',
|
||||
icon_url: 'https://example.com/icon.png',
|
||||
description: 'A description',
|
||||
chat_color_theme: '#123456',
|
||||
chat_color_theme_inverted: true,
|
||||
copyright: '© Dify',
|
||||
privacy_policy: '',
|
||||
custom_disclaimer: 'Disclaimer',
|
||||
default_language: 'en-US',
|
||||
show_workflow_steps: true,
|
||||
use_icon_as_answer_icon: true,
|
||||
},
|
||||
mode: AppModeEnum.ADVANCED_CHAT,
|
||||
enable_sso: false,
|
||||
} as unknown as AppDetailResponse & Partial<AppSSO>
|
||||
|
||||
const renderSettingsModal = () => render(
|
||||
<SettingsModal
|
||||
isChat
|
||||
isShow
|
||||
appInfo={mockAppInfo}
|
||||
onClose={mockOnClose}
|
||||
onSave={mockOnSave}
|
||||
/>,
|
||||
)
|
||||
|
||||
describe('SettingsModal', () => {
|
||||
beforeEach(() => {
|
||||
mockNotify.mockClear()
|
||||
mockOnClose.mockClear()
|
||||
mockOnSave.mockClear()
|
||||
mockSetShowPricingModal.mockClear()
|
||||
mockSetShowAccountSettingModal.mockClear()
|
||||
mockUseProviderContext.mockReturnValue({
|
||||
...baseProviderContextValue,
|
||||
enableBilling: true,
|
||||
plan: {
|
||||
...baseProviderContextValue.plan,
|
||||
type: Plan.sandbox,
|
||||
},
|
||||
webappCopyrightEnabled: true,
|
||||
})
|
||||
})
|
||||
|
||||
it('should render the modal and expose the expanded settings section', async () => {
|
||||
renderSettingsModal()
|
||||
expect(screen.getByText('appOverview.overview.appInfo.settings.title')).toBeInTheDocument()
|
||||
|
||||
const showMoreEntry = screen.getByText('appOverview.overview.appInfo.settings.more.entry')
|
||||
fireEvent.click(showMoreEntry)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByPlaceholderText('appOverview.overview.appInfo.settings.more.copyRightPlaceholder')).toBeInTheDocument()
|
||||
expect(screen.getByPlaceholderText('appOverview.overview.appInfo.settings.more.privacyPolicyPlaceholder')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should notify the user when the name is empty', async () => {
|
||||
renderSettingsModal()
|
||||
const nameInput = screen.getByPlaceholderText('app.appNamePlaceholder')
|
||||
fireEvent.change(nameInput, { target: { value: '' } })
|
||||
fireEvent.click(screen.getByText('common.operation.save'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockNotify).toHaveBeenCalledWith(expect.objectContaining({ message: 'app.newApp.nameNotEmpty' }))
|
||||
})
|
||||
expect(mockOnSave).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should validate the theme color and show an error when the hex is invalid', async () => {
|
||||
renderSettingsModal()
|
||||
const colorInput = screen.getByPlaceholderText('E.g #A020F0')
|
||||
fireEvent.change(colorInput, { target: { value: 'not-a-hex' } })
|
||||
|
||||
fireEvent.click(screen.getByText('common.operation.save'))
|
||||
await waitFor(() => {
|
||||
expect(mockNotify).toHaveBeenCalledWith(expect.objectContaining({
|
||||
message: 'appOverview.overview.appInfo.settings.invalidHexMessage',
|
||||
}))
|
||||
})
|
||||
expect(mockOnSave).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should validate the privacy policy URL when advanced settings are open', async () => {
|
||||
renderSettingsModal()
|
||||
fireEvent.click(screen.getByText('appOverview.overview.appInfo.settings.more.entry'))
|
||||
const privacyInput = screen.getByPlaceholderText('appOverview.overview.appInfo.settings.more.privacyPolicyPlaceholder')
|
||||
// eslint-disable-next-line sonarjs/no-clear-text-protocols
|
||||
fireEvent.change(privacyInput, { target: { value: 'ftp://invalid-url' } })
|
||||
|
||||
fireEvent.click(screen.getByText('common.operation.save'))
|
||||
await waitFor(() => {
|
||||
expect(mockNotify).toHaveBeenCalledWith(expect.objectContaining({
|
||||
message: 'appOverview.overview.appInfo.settings.invalidPrivacyPolicy',
|
||||
}))
|
||||
})
|
||||
expect(mockOnSave).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should save valid settings and close the modal', async () => {
|
||||
mockOnSave.mockResolvedValueOnce(undefined)
|
||||
renderSettingsModal()
|
||||
|
||||
fireEvent.click(screen.getByText('common.operation.save'))
|
||||
|
||||
await waitFor(() => expect(mockOnSave).toHaveBeenCalled())
|
||||
expect(mockOnSave).toHaveBeenCalledWith(expect.objectContaining({
|
||||
title: mockAppInfo.site.title,
|
||||
description: mockAppInfo.site.description,
|
||||
default_language: mockAppInfo.site.default_language,
|
||||
chat_color_theme: mockAppInfo.site.chat_color_theme,
|
||||
chat_color_theme_inverted: mockAppInfo.site.chat_color_theme_inverted,
|
||||
prompt_public: false,
|
||||
copyright: mockAppInfo.site.copyright,
|
||||
privacy_policy: mockAppInfo.site.privacy_policy,
|
||||
custom_disclaimer: mockAppInfo.site.custom_disclaimer,
|
||||
icon_type: 'emoji',
|
||||
icon: mockAppInfo.site.icon,
|
||||
icon_background: mockAppInfo.site.icon_background,
|
||||
show_workflow_steps: mockAppInfo.site.show_workflow_steps,
|
||||
use_icon_as_answer_icon: mockAppInfo.site.use_icon_as_answer_icon,
|
||||
enable_sso: mockAppInfo.enable_sso,
|
||||
}))
|
||||
expect(mockOnClose).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
import type { Credential } from '@/app/components/tools/types'
|
||||
import { act, fireEvent, render, screen } from '@testing-library/react'
|
||||
import { AuthHeaderPrefix, AuthType } from '@/app/components/tools/types'
|
||||
import ConfigCredential from './config-credentials'
|
||||
|
||||
describe('ConfigCredential', () => {
|
||||
const baseCredential: Credential = {
|
||||
auth_type: AuthType.none,
|
||||
}
|
||||
const mockOnChange = vi.fn()
|
||||
const mockOnHide = vi.fn()
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('renders and calls onHide when cancel is pressed', async () => {
|
||||
await act(async () => {
|
||||
render(
|
||||
<ConfigCredential
|
||||
credential={baseCredential}
|
||||
onChange={mockOnChange}
|
||||
onHide={mockOnHide}
|
||||
/>,
|
||||
)
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByText('common.operation.cancel'))
|
||||
|
||||
expect(mockOnHide).toHaveBeenCalledTimes(1)
|
||||
expect(mockOnChange).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('allows selecting apiKeyHeader and submits the new credential', async () => {
|
||||
await act(async () => {
|
||||
render(
|
||||
<ConfigCredential
|
||||
credential={baseCredential}
|
||||
onChange={mockOnChange}
|
||||
onHide={mockOnHide}
|
||||
/>,
|
||||
)
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByText('tools.createTool.authMethod.types.api_key_header'))
|
||||
const headerInput = screen.getByPlaceholderText('tools.createTool.authMethod.types.apiKeyPlaceholder')
|
||||
const valueInput = screen.getByPlaceholderText('tools.createTool.authMethod.types.apiValuePlaceholder')
|
||||
fireEvent.change(headerInput, { target: { value: 'X-Auth' } })
|
||||
fireEvent.change(valueInput, { target: { value: 'sEcReT' } })
|
||||
fireEvent.click(screen.getByText('common.operation.save'))
|
||||
|
||||
expect(mockOnChange).toHaveBeenCalledWith({
|
||||
auth_type: AuthType.apiKeyHeader,
|
||||
api_key_header: 'X-Auth',
|
||||
api_key_header_prefix: AuthHeaderPrefix.custom,
|
||||
api_key_value: 'sEcReT',
|
||||
})
|
||||
expect(mockOnHide).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { importSchemaFromURL } from '@/service/tools'
|
||||
import Toast from '../../base/toast'
|
||||
import examples from './examples'
|
||||
import GetSchema from './get-schema'
|
||||
|
||||
vi.mock('@/service/tools', () => ({
|
||||
importSchemaFromURL: vi.fn(),
|
||||
}))
|
||||
const importSchemaFromURLMock = vi.mocked(importSchemaFromURL)
|
||||
|
||||
describe('GetSchema', () => {
|
||||
const notifySpy = vi.spyOn(Toast, 'notify')
|
||||
const mockOnChange = vi.fn()
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
notifySpy.mockClear()
|
||||
importSchemaFromURLMock.mockReset()
|
||||
render(<GetSchema onChange={mockOnChange} />)
|
||||
})
|
||||
|
||||
it('shows an error when the URL is not http', () => {
|
||||
fireEvent.click(screen.getByText('tools.createTool.importFromUrl'))
|
||||
const input = screen.getByPlaceholderText('tools.createTool.importFromUrlPlaceHolder')
|
||||
// eslint-disable-next-line sonarjs/no-clear-text-protocols
|
||||
fireEvent.change(input, { target: { value: 'ftp://invalid' } })
|
||||
fireEvent.click(screen.getByText('common.operation.ok'))
|
||||
|
||||
expect(notifySpy).toHaveBeenCalledWith({
|
||||
type: 'error',
|
||||
message: 'tools.createTool.urlError',
|
||||
})
|
||||
})
|
||||
|
||||
it('imports schema from url when valid', async () => {
|
||||
fireEvent.click(screen.getByText('tools.createTool.importFromUrl'))
|
||||
const input = screen.getByPlaceholderText('tools.createTool.importFromUrlPlaceHolder')
|
||||
fireEvent.change(input, { target: { value: 'https://example.com' } })
|
||||
importSchemaFromURLMock.mockResolvedValueOnce({ schema: 'result-schema' })
|
||||
|
||||
fireEvent.click(screen.getByText('common.operation.ok'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockOnChange).toHaveBeenCalledWith('result-schema')
|
||||
})
|
||||
})
|
||||
|
||||
it('selects example schema when example option clicked', () => {
|
||||
fireEvent.click(screen.getByText('tools.createTool.examples'))
|
||||
fireEvent.click(screen.getByText(`tools.createTool.exampleOptions.${examples[0].key}`))
|
||||
|
||||
expect(mockOnChange).toHaveBeenCalledWith(examples[0].content)
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,154 @@
|
|||
import type { ModalContextState } from '@/context/modal-context'
|
||||
import type { ProviderContextState } from '@/context/provider-context'
|
||||
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { Plan } from '@/app/components/billing/type'
|
||||
import { parseParamsSchema } from '@/service/tools'
|
||||
import EditCustomCollectionModal from './index'
|
||||
|
||||
vi.mock('ahooks', async () => {
|
||||
const actual = await vi.importActual<typeof import('ahooks')>('ahooks')
|
||||
return {
|
||||
...actual,
|
||||
useDebounce: (value: unknown) => value,
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/service/tools', () => ({
|
||||
parseParamsSchema: vi.fn(),
|
||||
}))
|
||||
const parseParamsSchemaMock = vi.mocked(parseParamsSchema)
|
||||
|
||||
const mockSetShowPricingModal = vi.fn()
|
||||
const mockSetShowAccountSettingModal = vi.fn()
|
||||
vi.mock('@/context/modal-context', () => ({
|
||||
useModalContext: (): ModalContextState => ({
|
||||
setShowAccountSettingModal: mockSetShowAccountSettingModal,
|
||||
setShowApiBasedExtensionModal: vi.fn(),
|
||||
setShowModerationSettingModal: vi.fn(),
|
||||
setShowExternalDataToolModal: vi.fn(),
|
||||
setShowPricingModal: mockSetShowPricingModal,
|
||||
setShowAnnotationFullModal: vi.fn(),
|
||||
setShowModelModal: vi.fn(),
|
||||
setShowExternalKnowledgeAPIModal: vi.fn(),
|
||||
setShowModelLoadBalancingModal: vi.fn(),
|
||||
setShowOpeningModal: vi.fn(),
|
||||
setShowUpdatePluginModal: vi.fn(),
|
||||
setShowEducationExpireNoticeModal: vi.fn(),
|
||||
setShowTriggerEventsLimitModal: vi.fn(),
|
||||
}),
|
||||
}))
|
||||
|
||||
const mockUseProviderContext = vi.fn()
|
||||
vi.mock('@/context/provider-context', () => ({
|
||||
useProviderContext: () => mockUseProviderContext(),
|
||||
}))
|
||||
|
||||
vi.mock('@/context/i18n', async () => {
|
||||
const actual = await vi.importActual<typeof import('@/context/i18n')>('@/context/i18n')
|
||||
return {
|
||||
...actual,
|
||||
useDocLink: () => (path?: string) => `https://docs.example.com${path ?? ''}`,
|
||||
}
|
||||
})
|
||||
|
||||
describe('EditCustomCollectionModal', () => {
|
||||
const mockOnHide = vi.fn()
|
||||
const mockOnAdd = vi.fn()
|
||||
const mockOnEdit = vi.fn()
|
||||
const mockOnRemove = vi.fn()
|
||||
const toastNotifySpy = vi.spyOn(Toast, 'notify')
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
toastNotifySpy.mockClear()
|
||||
parseParamsSchemaMock.mockResolvedValue({
|
||||
parameters_schema: [],
|
||||
schema_type: 'openapi',
|
||||
})
|
||||
mockUseProviderContext.mockReturnValue({
|
||||
plan: {
|
||||
type: Plan.sandbox,
|
||||
},
|
||||
enableBilling: false,
|
||||
webappCopyrightEnabled: true,
|
||||
} as ProviderContextState)
|
||||
})
|
||||
|
||||
const renderModal = () => render(
|
||||
<EditCustomCollectionModal
|
||||
payload={undefined}
|
||||
onHide={mockOnHide}
|
||||
onAdd={mockOnAdd}
|
||||
onEdit={mockOnEdit}
|
||||
onRemove={mockOnRemove}
|
||||
/>,
|
||||
)
|
||||
|
||||
it('shows an error when the provider name is missing', async () => {
|
||||
renderModal()
|
||||
|
||||
const schemaInput = screen.getByPlaceholderText('tools.createTool.schemaPlaceHolder')
|
||||
fireEvent.change(schemaInput, { target: { value: '{}' } })
|
||||
await waitFor(() => {
|
||||
expect(parseParamsSchemaMock).toHaveBeenCalledWith('{}')
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByText('common.operation.save'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(toastNotifySpy).toHaveBeenCalledWith(expect.objectContaining({
|
||||
message: 'common.errorMsg.fieldRequired:{"field":"tools.createTool.name"}',
|
||||
type: 'error',
|
||||
}))
|
||||
})
|
||||
expect(mockOnAdd).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('shows an error when the schema is missing', async () => {
|
||||
renderModal()
|
||||
|
||||
const providerInput = screen.getByPlaceholderText('tools.createTool.toolNamePlaceHolder')
|
||||
fireEvent.change(providerInput, { target: { value: 'provider' } })
|
||||
|
||||
fireEvent.click(screen.getByText('common.operation.save'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(toastNotifySpy).toHaveBeenCalledWith(expect.objectContaining({
|
||||
message: 'common.errorMsg.fieldRequired:{"field":"tools.createTool.schema"}',
|
||||
type: 'error',
|
||||
}))
|
||||
})
|
||||
expect(mockOnAdd).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('saves a valid custom collection', async () => {
|
||||
renderModal()
|
||||
const providerInput = screen.getByPlaceholderText('tools.createTool.toolNamePlaceHolder')
|
||||
fireEvent.change(providerInput, { target: { value: 'provider' } })
|
||||
|
||||
const schemaInput = screen.getByPlaceholderText('tools.createTool.schemaPlaceHolder')
|
||||
fireEvent.change(schemaInput, { target: { value: '{}' } })
|
||||
|
||||
await waitFor(() => {
|
||||
expect(parseParamsSchemaMock).toHaveBeenCalledWith('{}')
|
||||
})
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByText('common.operation.save'))
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockOnAdd).toHaveBeenCalledWith(expect.objectContaining({
|
||||
provider: 'provider',
|
||||
schema: '{}',
|
||||
schema_type: 'openapi',
|
||||
credentials: {
|
||||
auth_type: 'none',
|
||||
},
|
||||
labels: [],
|
||||
}))
|
||||
expect(toastNotifySpy).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
import type { CustomCollectionBackend, CustomParamSchema } from '@/app/components/tools/types'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { AuthType } from '@/app/components/tools/types'
|
||||
import I18n from '@/context/i18n'
|
||||
import { testAPIAvailable } from '@/service/tools'
|
||||
import TestApi from './test-api'
|
||||
|
||||
vi.mock('@/service/tools', () => ({
|
||||
testAPIAvailable: vi.fn(),
|
||||
}))
|
||||
const testAPIAvailableMock = vi.mocked(testAPIAvailable)
|
||||
|
||||
describe('TestApi', () => {
|
||||
const customCollection: CustomCollectionBackend = {
|
||||
provider: 'custom',
|
||||
credentials: {
|
||||
auth_type: AuthType.none,
|
||||
},
|
||||
schema_type: 'openapi',
|
||||
schema: '{ }',
|
||||
icon: { background: '', content: '' },
|
||||
privacy_policy: '',
|
||||
custom_disclaimer: '',
|
||||
id: 'test-id',
|
||||
labels: [],
|
||||
}
|
||||
const tool: CustomParamSchema = {
|
||||
operation_id: 'testOp',
|
||||
summary: 'summary',
|
||||
method: 'GET',
|
||||
server_url: 'https://api.example.com',
|
||||
parameters: [{
|
||||
name: 'limit',
|
||||
label: {
|
||||
en_US: 'Limit',
|
||||
zh_Hans: '限制',
|
||||
},
|
||||
// eslint-disable-next-line ts/no-explicit-any
|
||||
} as any],
|
||||
}
|
||||
|
||||
const renderTestApi = () => {
|
||||
const providerValue = {
|
||||
locale: 'en-US',
|
||||
i18n: {},
|
||||
setLocaleOnClient: vi.fn(),
|
||||
}
|
||||
return render(
|
||||
<I18n.Provider value={providerValue as any}>
|
||||
<TestApi
|
||||
customCollection={customCollection}
|
||||
tool={tool}
|
||||
onHide={vi.fn()}
|
||||
/>
|
||||
</I18n.Provider>,
|
||||
)
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('renders parameters and runs the API test', async () => {
|
||||
testAPIAvailableMock.mockResolvedValueOnce({ result: 'ok' })
|
||||
renderTestApi()
|
||||
|
||||
const parameterInput = screen.getAllByRole('textbox')[0]
|
||||
fireEvent.change(parameterInput, { target: { value: '5' } })
|
||||
fireEvent.click(screen.getByRole('button', { name: 'tools.test.title' }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(testAPIAvailableMock).toHaveBeenCalledWith({
|
||||
provider_name: customCollection.provider,
|
||||
tool_name: tool.operation_id,
|
||||
credentials: {
|
||||
auth_type: AuthType.none,
|
||||
},
|
||||
schema_type: customCollection.schema_type,
|
||||
schema: customCollection.schema,
|
||||
parameters: {
|
||||
limit: '5',
|
||||
},
|
||||
})
|
||||
expect(screen.getByText('ok')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
Loading…
Reference in New Issue