mirror of
https://github.com/langgenius/dify.git
synced 2026-05-01 14:16:40 +08:00
Signed-off-by: majiayu000 <1835304752@qq.com> Signed-off-by: dependabot[bot] <support@github.com> Signed-off-by: NeatGuyCoding <15627489+NeatGuyCoding@users.noreply.github.com> Signed-off-by: -LAN- <laipz8200@outlook.com> Signed-off-by: yihong0618 <zouzou0208@gmail.com> Co-authored-by: QuantumGhost <obelisk.reg+git@gmail.com> Co-authored-by: 盐粒 Yanli <yanli@dify.ai> Co-authored-by: wangxiaolei <fatelei@gmail.com> Co-authored-by: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Co-authored-by: Cursx <33718736+Cursx@users.noreply.github.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: lif <1835304752@qq.com> Co-authored-by: 非法操作 <hjlarry@163.com> Co-authored-by: Asuka Minato <i@asukaminato.eu.org> Co-authored-by: fenglin <790872612@qq.com> Co-authored-by: qiaofenglin <qiaofenglin@baidu.com> Co-authored-by: -LAN- <laipz8200@outlook.com> Co-authored-by: TomoOkuyama <49631611+TomoOkuyama@users.noreply.github.com> Co-authored-by: Tomo Okuyama <tomo.okuyama@intersystems.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: zyssyz123 <916125788@qq.com> Co-authored-by: hj24 <mambahj24@gmail.com> Co-authored-by: Coding On Star <447357187@qq.com> Co-authored-by: CodingOnStar <hanxujiang@dify.ai> Co-authored-by: yyh <92089059+lyzno1@users.noreply.github.com> Co-authored-by: Xiangxuan Qu <fghpdf@outlook.com> Co-authored-by: fghpdf <fghpdf@users.noreply.github.com> Co-authored-by: coopercoder <whitetiger0127@163.com> Co-authored-by: zhaiguangpeng <zhaiguangpeng@didiglobal.com> Co-authored-by: Junyan Qin (Chin) <rockchinq@gmail.com> Co-authored-by: E.G <146701565+GlobalStar117@users.noreply.github.com> Co-authored-by: GlobalStar117 <GlobalStar117@users.noreply.github.com> Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com> Co-authored-by: CodingOnStar <hanxujiang@dify.com> Co-authored-by: crazywoola <100913391+crazywoola@users.noreply.github.com> Co-authored-by: heyszt <270985384@qq.com> Co-authored-by: NeatGuyCoding <15627489+NeatGuyCoding@users.noreply.github.com> Co-authored-by: Yeuoly <45712896+Yeuoly@users.noreply.github.com> Co-authored-by: zxhlyh <jasonapring2015@outlook.com> Co-authored-by: moonpanda <chuanzegao@163.com> Co-authored-by: warlocgao <warlocgao@tencent.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: KVOJJJin <jzongcode@gmail.com> Co-authored-by: eux <euxx@users.noreply.github.com> Co-authored-by: bangjiehan <bangjiehan@gmail.com> Co-authored-by: FFXN <31929997+FFXN@users.noreply.github.com> Co-authored-by: Jyong <76649700+JohnJyong@users.noreply.github.com> Co-authored-by: Nie Ronghua <nieronghua@sf-express.com> Co-authored-by: JQSevenMiao <141806521+JQSevenMiao@users.noreply.github.com> Co-authored-by: jiasiqi <jiasiqi3@tal.com> Co-authored-by: Seokrin Taron Sung <sungsjade@gmail.com> Co-authored-by: CrabSAMA <40541269+CrabSAMA@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: yihong <zouzou0208@gmail.com> Co-authored-by: Joel <iamjoel007@gmail.com> Co-authored-by: Wu Tianwei <30284043+WTW0313@users.noreply.github.com> Co-authored-by: yessenia <yessenia.contact@gmail.com> Co-authored-by: Jax <anobaka@qq.com> Co-authored-by: niveshdandyan <155956228+niveshdandyan@users.noreply.github.com> Co-authored-by: OSS Contributor <oss-contributor@example.com> Co-authored-by: niveshdandyan <niveshdandyan@users.noreply.github.com> Co-authored-by: Sean Kenneth Doherty <Smaster7772@gmail.com>
528 lines
14 KiB
TypeScript
528 lines
14 KiB
TypeScript
import { cleanup, render, screen, waitFor } from '@testing-library/react'
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import BasicAppPreview from './basic-app-preview'
|
|
|
|
vi.mock('react-i18next', () => ({
|
|
useTranslation: () => ({
|
|
t: (key: string) => key,
|
|
}),
|
|
}))
|
|
|
|
const mockUseGetTryAppInfo = vi.fn()
|
|
const mockUseAllToolProviders = vi.fn()
|
|
const mockUseGetTryAppDataSets = vi.fn()
|
|
const mockUseTextGenerationCurrentProviderAndModelAndModelList = vi.fn()
|
|
|
|
vi.mock('@/service/use-try-app', () => ({
|
|
useGetTryAppInfo: (...args: unknown[]) => mockUseGetTryAppInfo(...args),
|
|
useGetTryAppDataSets: (...args: unknown[]) => mockUseGetTryAppDataSets(...args),
|
|
}))
|
|
|
|
vi.mock('@/service/use-tools', () => ({
|
|
useAllToolProviders: () => mockUseAllToolProviders(),
|
|
}))
|
|
|
|
vi.mock('../../../header/account-setting/model-provider-page/hooks', () => ({
|
|
useTextGenerationCurrentProviderAndModelAndModelList: (...args: unknown[]) =>
|
|
mockUseTextGenerationCurrentProviderAndModelAndModelList(...args),
|
|
}))
|
|
|
|
vi.mock('@/hooks/use-breakpoints', () => ({
|
|
default: () => 'pc',
|
|
MediaType: {
|
|
mobile: 'mobile',
|
|
pc: 'pc',
|
|
},
|
|
}))
|
|
|
|
vi.mock('@/app/components/app/configuration/config', () => ({
|
|
default: () => <div data-testid="config-component">Config</div>,
|
|
}))
|
|
|
|
vi.mock('@/app/components/app/configuration/debug', () => ({
|
|
default: () => <div data-testid="debug-component">Debug</div>,
|
|
}))
|
|
|
|
vi.mock('@/app/components/base/features', () => ({
|
|
FeaturesProvider: ({ children }: { children: React.ReactNode }) => (
|
|
<div data-testid="features-provider">{children}</div>
|
|
),
|
|
}))
|
|
|
|
const createMockAppDetail = (mode: string = 'chat'): Record<string, unknown> => ({
|
|
id: 'test-app-id',
|
|
name: 'Test App',
|
|
description: 'Test Description',
|
|
mode,
|
|
site: {
|
|
title: 'Test Site Title',
|
|
icon: '🚀',
|
|
icon_type: 'emoji',
|
|
icon_background: '#FFFFFF',
|
|
icon_url: '',
|
|
},
|
|
model_config: {
|
|
model: {
|
|
provider: 'langgenius/openai/openai',
|
|
name: 'gpt-4',
|
|
mode: 'chat',
|
|
},
|
|
pre_prompt: 'You are a helpful assistant',
|
|
user_input_form: [] as unknown[],
|
|
external_data_tools: [] as unknown[],
|
|
dataset_configs: {
|
|
datasets: {
|
|
datasets: [] as unknown[],
|
|
},
|
|
},
|
|
agent_mode: {
|
|
tools: [] as unknown[],
|
|
enabled: false,
|
|
},
|
|
more_like_this: { enabled: false },
|
|
opening_statement: 'Hello!',
|
|
suggested_questions: ['Question 1'],
|
|
sensitive_word_avoidance: null,
|
|
speech_to_text: null,
|
|
text_to_speech: null,
|
|
file_upload: null as unknown,
|
|
suggested_questions_after_answer: null,
|
|
retriever_resource: null,
|
|
annotation_reply: null,
|
|
},
|
|
deleted_tools: [] as unknown[],
|
|
})
|
|
|
|
describe('BasicAppPreview', () => {
|
|
beforeEach(() => {
|
|
mockUseGetTryAppInfo.mockReturnValue({
|
|
data: createMockAppDetail(),
|
|
isLoading: false,
|
|
})
|
|
mockUseAllToolProviders.mockReturnValue({
|
|
data: [],
|
|
isLoading: false,
|
|
})
|
|
mockUseGetTryAppDataSets.mockReturnValue({
|
|
data: { data: [] },
|
|
isLoading: false,
|
|
})
|
|
mockUseTextGenerationCurrentProviderAndModelAndModelList.mockReturnValue({
|
|
currentModel: {
|
|
features: [],
|
|
},
|
|
})
|
|
})
|
|
|
|
afterEach(() => {
|
|
cleanup()
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
describe('loading state', () => {
|
|
it('renders loading when app detail is loading', () => {
|
|
mockUseGetTryAppInfo.mockReturnValue({
|
|
data: null,
|
|
isLoading: true,
|
|
})
|
|
|
|
render(<BasicAppPreview appId="test-app-id" />)
|
|
|
|
expect(screen.getByRole('status')).toBeInTheDocument()
|
|
})
|
|
|
|
it('renders loading when tool providers are loading', () => {
|
|
mockUseAllToolProviders.mockReturnValue({
|
|
data: null,
|
|
isLoading: true,
|
|
})
|
|
|
|
render(<BasicAppPreview appId="test-app-id" />)
|
|
|
|
expect(screen.getByRole('status')).toBeInTheDocument()
|
|
})
|
|
|
|
it('renders loading when datasets are loading', () => {
|
|
mockUseGetTryAppDataSets.mockReturnValue({
|
|
data: null,
|
|
isLoading: true,
|
|
})
|
|
|
|
render(<BasicAppPreview appId="test-app-id" />)
|
|
|
|
expect(screen.getByRole('status')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('content rendering', () => {
|
|
it('renders Config component when data is loaded', async () => {
|
|
render(<BasicAppPreview appId="test-app-id" />)
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('config-component')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
it('renders Debug component when data is loaded on PC', async () => {
|
|
render(<BasicAppPreview appId="test-app-id" />)
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('debug-component')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
it('renders FeaturesProvider', async () => {
|
|
render(<BasicAppPreview appId="test-app-id" />)
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('features-provider')).toBeInTheDocument()
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('different app modes', () => {
|
|
it('handles chat mode', async () => {
|
|
mockUseGetTryAppInfo.mockReturnValue({
|
|
data: createMockAppDetail('chat'),
|
|
isLoading: false,
|
|
})
|
|
|
|
render(<BasicAppPreview appId="test-app-id" />)
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('config-component')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
it('handles completion mode', async () => {
|
|
mockUseGetTryAppInfo.mockReturnValue({
|
|
data: createMockAppDetail('completion'),
|
|
isLoading: false,
|
|
})
|
|
|
|
render(<BasicAppPreview appId="test-app-id" />)
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('config-component')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
it('handles agent-chat mode', async () => {
|
|
const agentAppDetail = createMockAppDetail('agent-chat')
|
|
const modelConfig = agentAppDetail.model_config as Record<string, unknown>
|
|
modelConfig.agent_mode = {
|
|
tools: [
|
|
{
|
|
provider_id: 'test-provider',
|
|
provider_name: 'test-provider',
|
|
provider_type: 'builtin',
|
|
tool_name: 'test-tool',
|
|
enabled: true,
|
|
},
|
|
],
|
|
enabled: true,
|
|
max_iteration: 5,
|
|
}
|
|
|
|
mockUseGetTryAppInfo.mockReturnValue({
|
|
data: agentAppDetail,
|
|
isLoading: false,
|
|
})
|
|
|
|
mockUseAllToolProviders.mockReturnValue({
|
|
data: [
|
|
{
|
|
id: 'test-provider',
|
|
is_team_authorization: true,
|
|
icon: '/icon.png',
|
|
},
|
|
],
|
|
isLoading: false,
|
|
})
|
|
|
|
render(<BasicAppPreview appId="test-app-id" />)
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('config-component')).toBeInTheDocument()
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('hook calls', () => {
|
|
it('calls useGetTryAppInfo with correct appId', () => {
|
|
render(<BasicAppPreview appId="my-app-id" />)
|
|
|
|
expect(mockUseGetTryAppInfo).toHaveBeenCalledWith('my-app-id')
|
|
})
|
|
|
|
it('calls useTextGenerationCurrentProviderAndModelAndModelList with model config', async () => {
|
|
render(<BasicAppPreview appId="test-app-id" />)
|
|
|
|
await waitFor(() => {
|
|
expect(mockUseTextGenerationCurrentProviderAndModelAndModelList).toHaveBeenCalled()
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('model features', () => {
|
|
it('handles vision feature', async () => {
|
|
mockUseTextGenerationCurrentProviderAndModelAndModelList.mockReturnValue({
|
|
currentModel: {
|
|
features: ['vision'],
|
|
},
|
|
})
|
|
|
|
render(<BasicAppPreview appId="test-app-id" />)
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('config-component')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
it('handles document feature', async () => {
|
|
mockUseTextGenerationCurrentProviderAndModelAndModelList.mockReturnValue({
|
|
currentModel: {
|
|
features: ['document'],
|
|
},
|
|
})
|
|
|
|
render(<BasicAppPreview appId="test-app-id" />)
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('config-component')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
it('handles audio feature', async () => {
|
|
mockUseTextGenerationCurrentProviderAndModelAndModelList.mockReturnValue({
|
|
currentModel: {
|
|
features: ['audio'],
|
|
},
|
|
})
|
|
|
|
render(<BasicAppPreview appId="test-app-id" />)
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('config-component')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
it('handles video feature', async () => {
|
|
mockUseTextGenerationCurrentProviderAndModelAndModelList.mockReturnValue({
|
|
currentModel: {
|
|
features: ['video'],
|
|
},
|
|
})
|
|
|
|
render(<BasicAppPreview appId="test-app-id" />)
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('config-component')).toBeInTheDocument()
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('dataset handling', () => {
|
|
it('handles app with datasets in agent mode', async () => {
|
|
const appWithDatasets = createMockAppDetail('agent-chat')
|
|
const modelConfig = appWithDatasets.model_config as Record<string, unknown>
|
|
modelConfig.agent_mode = {
|
|
tools: [
|
|
{
|
|
dataset: {
|
|
enabled: true,
|
|
id: 'dataset-1',
|
|
},
|
|
},
|
|
],
|
|
enabled: true,
|
|
}
|
|
|
|
mockUseGetTryAppInfo.mockReturnValue({
|
|
data: appWithDatasets,
|
|
isLoading: false,
|
|
})
|
|
|
|
render(<BasicAppPreview appId="test-app-id" />)
|
|
|
|
await waitFor(() => {
|
|
expect(mockUseGetTryAppDataSets).toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
it('handles app with datasets in dataset_configs', async () => {
|
|
const appWithDatasets = createMockAppDetail('chat')
|
|
const modelConfig = appWithDatasets.model_config as Record<string, unknown>
|
|
modelConfig.dataset_configs = {
|
|
datasets: {
|
|
datasets: [
|
|
{ dataset: { id: 'dataset-1' } },
|
|
{ dataset: { id: 'dataset-2' } },
|
|
],
|
|
},
|
|
}
|
|
|
|
mockUseGetTryAppInfo.mockReturnValue({
|
|
data: appWithDatasets,
|
|
isLoading: false,
|
|
})
|
|
|
|
render(<BasicAppPreview appId="test-app-id" />)
|
|
|
|
await waitFor(() => {
|
|
expect(mockUseGetTryAppDataSets).toHaveBeenCalled()
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('advanced prompt mode', () => {
|
|
it('handles advanced prompt mode', async () => {
|
|
const appWithAdvancedPrompt = createMockAppDetail('chat')
|
|
const modelConfig = appWithAdvancedPrompt.model_config as Record<string, unknown>
|
|
modelConfig.prompt_type = 'advanced'
|
|
modelConfig.chat_prompt_config = {
|
|
prompt: [{ role: 'system', text: 'You are helpful' }],
|
|
}
|
|
|
|
mockUseGetTryAppInfo.mockReturnValue({
|
|
data: appWithAdvancedPrompt,
|
|
isLoading: false,
|
|
})
|
|
|
|
render(<BasicAppPreview appId="test-app-id" />)
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('config-component')).toBeInTheDocument()
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('file upload config', () => {
|
|
it('handles file upload config', async () => {
|
|
const appWithFileUpload = createMockAppDetail('chat')
|
|
const modelConfig = appWithFileUpload.model_config as Record<string, unknown>
|
|
modelConfig.file_upload = {
|
|
enabled: true,
|
|
image: {
|
|
enabled: true,
|
|
detail: 'high',
|
|
number_limits: 5,
|
|
transfer_methods: ['local_file', 'remote_url'],
|
|
},
|
|
allowed_file_types: ['image'],
|
|
allowed_file_extensions: ['.jpg', '.png'],
|
|
allowed_file_upload_methods: ['local_file'],
|
|
number_limits: 3,
|
|
}
|
|
|
|
mockUseGetTryAppInfo.mockReturnValue({
|
|
data: appWithFileUpload,
|
|
isLoading: false,
|
|
})
|
|
|
|
render(<BasicAppPreview appId="test-app-id" />)
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('config-component')).toBeInTheDocument()
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('external data tools', () => {
|
|
it('handles app with external_data_tools', async () => {
|
|
const appWithExternalTools = createMockAppDetail('chat')
|
|
const modelConfig = appWithExternalTools.model_config as Record<string, unknown>
|
|
modelConfig.external_data_tools = [
|
|
{
|
|
variable: 'test_var',
|
|
label: 'Test Label',
|
|
enabled: true,
|
|
type: 'text',
|
|
config: {},
|
|
icon: '/icon.png',
|
|
icon_background: '#FFFFFF',
|
|
},
|
|
]
|
|
|
|
mockUseGetTryAppInfo.mockReturnValue({
|
|
data: appWithExternalTools,
|
|
isLoading: false,
|
|
})
|
|
|
|
render(<BasicAppPreview appId="test-app-id" />)
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('config-component')).toBeInTheDocument()
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('deleted tools handling', () => {
|
|
it('handles app with deleted tools', async () => {
|
|
const agentAppDetail = createMockAppDetail('agent-chat')
|
|
const modelConfig = agentAppDetail.model_config as Record<string, unknown>
|
|
modelConfig.agent_mode = {
|
|
tools: [
|
|
{
|
|
id: 'tool-1',
|
|
provider_id: 'test-provider',
|
|
provider_name: 'test-provider',
|
|
provider_type: 'builtin',
|
|
tool_name: 'test-tool',
|
|
enabled: true,
|
|
},
|
|
],
|
|
enabled: true,
|
|
max_iteration: 5,
|
|
}
|
|
agentAppDetail.deleted_tools = [
|
|
{
|
|
id: 'tool-1',
|
|
tool_name: 'test-tool',
|
|
},
|
|
]
|
|
|
|
mockUseGetTryAppInfo.mockReturnValue({
|
|
data: agentAppDetail,
|
|
isLoading: false,
|
|
})
|
|
|
|
mockUseAllToolProviders.mockReturnValue({
|
|
data: [
|
|
{
|
|
id: 'test-provider',
|
|
is_team_authorization: false,
|
|
icon: '/icon.png',
|
|
},
|
|
],
|
|
isLoading: false,
|
|
})
|
|
|
|
render(<BasicAppPreview appId="test-app-id" />)
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('config-component')).toBeInTheDocument()
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('edge cases', () => {
|
|
it('handles app without model_config', async () => {
|
|
const appWithoutModelConfig = createMockAppDetail('chat')
|
|
appWithoutModelConfig.model_config = undefined
|
|
|
|
mockUseGetTryAppInfo.mockReturnValue({
|
|
data: appWithoutModelConfig,
|
|
isLoading: false,
|
|
})
|
|
|
|
render(<BasicAppPreview appId="test-app-id" />)
|
|
|
|
// Should still render (with default model config)
|
|
await waitFor(() => {
|
|
expect(mockUseGetTryAppDataSets).toHaveBeenCalled()
|
|
})
|
|
})
|
|
})
|
|
})
|