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