mirror of
https://github.com/langgenius/dify.git
synced 2026-04-22 03:37:44 +08:00
test: add comprehensive tests for try-app components with 95%+ coverage
Add automated tests for all components in web/app/components/explore/try-app: - index.tsx and tab.tsx (root components) - app/index.tsx, app/chat.tsx, app/text-generation.tsx - app-info/index.tsx, app-info/use-get-requirements.ts - preview/index.tsx, preview/basic-app-preview.tsx, preview/flow-app-preview.tsx Test coverage: - 137 tests passing - All files exceed 95% coverage threshold - Tests cover loading states, user interactions, different app modes, mobile/PC responsive behavior, and edge cases Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
395
web/app/components/explore/try-app/app-info/index.spec.tsx
Normal file
395
web/app/components/explore/try-app/app-info/index.spec.tsx
Normal file
@ -0,0 +1,395 @@
|
||||
import type { TryAppInfo } from '@/service/try-app'
|
||||
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import AppInfo from './index'
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => {
|
||||
const translations: Record<string, string> = {
|
||||
'types.advanced': 'Advanced',
|
||||
'types.chatbot': 'Chatbot',
|
||||
'types.agent': 'Agent',
|
||||
'types.workflow': 'Workflow',
|
||||
'types.completion': 'Completion',
|
||||
'tryApp.createFromSampleApp': 'Create from Sample',
|
||||
'tryApp.category': 'Category',
|
||||
'tryApp.requirements': 'Requirements',
|
||||
}
|
||||
return translations[key] || key
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
const mockUseGetRequirements = vi.fn()
|
||||
|
||||
vi.mock('./use-get-requirements', () => ({
|
||||
default: (...args: unknown[]) => mockUseGetRequirements(...args),
|
||||
}))
|
||||
|
||||
const createMockAppDetail = (mode: string, overrides: Partial<TryAppInfo> = {}): TryAppInfo => ({
|
||||
id: 'test-app-id',
|
||||
name: 'Test App Name',
|
||||
description: 'Test App 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',
|
||||
},
|
||||
dataset_configs: {
|
||||
datasets: {
|
||||
datasets: [],
|
||||
},
|
||||
},
|
||||
agent_mode: {
|
||||
tools: [],
|
||||
},
|
||||
user_input_form: [],
|
||||
},
|
||||
...overrides,
|
||||
} as unknown as TryAppInfo)
|
||||
|
||||
describe('AppInfo', () => {
|
||||
beforeEach(() => {
|
||||
mockUseGetRequirements.mockReturnValue({
|
||||
requirements: [],
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('app name and icon', () => {
|
||||
it('renders app name', () => {
|
||||
const appDetail = createMockAppDetail('chat')
|
||||
const mockOnCreate = vi.fn()
|
||||
|
||||
render(
|
||||
<AppInfo
|
||||
appId="test-app-id"
|
||||
appDetail={appDetail}
|
||||
onCreate={mockOnCreate}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByText('Test App Name')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders app name with title attribute', () => {
|
||||
const appDetail = createMockAppDetail('chat', {
|
||||
name: 'Very Long App Name That Should Be Truncated',
|
||||
} as Partial<TryAppInfo>)
|
||||
const mockOnCreate = vi.fn()
|
||||
|
||||
render(
|
||||
<AppInfo
|
||||
appId="test-app-id"
|
||||
appDetail={appDetail}
|
||||
onCreate={mockOnCreate}
|
||||
/>,
|
||||
)
|
||||
|
||||
const nameElement = screen.getByText('Very Long App Name That Should Be Truncated')
|
||||
expect(nameElement).toHaveAttribute('title', 'Very Long App Name That Should Be Truncated')
|
||||
})
|
||||
})
|
||||
|
||||
describe('app type', () => {
|
||||
it('displays ADVANCED for advanced-chat mode', () => {
|
||||
const appDetail = createMockAppDetail('advanced-chat')
|
||||
const mockOnCreate = vi.fn()
|
||||
|
||||
render(
|
||||
<AppInfo
|
||||
appId="test-app-id"
|
||||
appDetail={appDetail}
|
||||
onCreate={mockOnCreate}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByText('ADVANCED')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('displays CHATBOT for chat mode', () => {
|
||||
const appDetail = createMockAppDetail('chat')
|
||||
const mockOnCreate = vi.fn()
|
||||
|
||||
render(
|
||||
<AppInfo
|
||||
appId="test-app-id"
|
||||
appDetail={appDetail}
|
||||
onCreate={mockOnCreate}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByText('CHATBOT')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('displays AGENT for agent-chat mode', () => {
|
||||
const appDetail = createMockAppDetail('agent-chat')
|
||||
const mockOnCreate = vi.fn()
|
||||
|
||||
render(
|
||||
<AppInfo
|
||||
appId="test-app-id"
|
||||
appDetail={appDetail}
|
||||
onCreate={mockOnCreate}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByText('AGENT')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('displays WORKFLOW for workflow mode', () => {
|
||||
const appDetail = createMockAppDetail('workflow')
|
||||
const mockOnCreate = vi.fn()
|
||||
|
||||
render(
|
||||
<AppInfo
|
||||
appId="test-app-id"
|
||||
appDetail={appDetail}
|
||||
onCreate={mockOnCreate}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByText('WORKFLOW')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('displays COMPLETION for completion mode', () => {
|
||||
const appDetail = createMockAppDetail('completion')
|
||||
const mockOnCreate = vi.fn()
|
||||
|
||||
render(
|
||||
<AppInfo
|
||||
appId="test-app-id"
|
||||
appDetail={appDetail}
|
||||
onCreate={mockOnCreate}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByText('COMPLETION')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('description', () => {
|
||||
it('renders description when provided', () => {
|
||||
const appDetail = createMockAppDetail('chat', {
|
||||
description: 'This is a test description',
|
||||
} as Partial<TryAppInfo>)
|
||||
const mockOnCreate = vi.fn()
|
||||
|
||||
render(
|
||||
<AppInfo
|
||||
appId="test-app-id"
|
||||
appDetail={appDetail}
|
||||
onCreate={mockOnCreate}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByText('This is a test description')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('does not render description when empty', () => {
|
||||
const appDetail = createMockAppDetail('chat', {
|
||||
description: '',
|
||||
} as Partial<TryAppInfo>)
|
||||
const mockOnCreate = vi.fn()
|
||||
|
||||
const { container } = render(
|
||||
<AppInfo
|
||||
appId="test-app-id"
|
||||
appDetail={appDetail}
|
||||
onCreate={mockOnCreate}
|
||||
/>,
|
||||
)
|
||||
|
||||
// Check that there's no element with the description class that has empty content
|
||||
const descriptionElements = container.querySelectorAll('.system-sm-regular.mt-\\[14px\\]')
|
||||
expect(descriptionElements.length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('create button', () => {
|
||||
it('renders create button with correct text', () => {
|
||||
const appDetail = createMockAppDetail('chat')
|
||||
const mockOnCreate = vi.fn()
|
||||
|
||||
render(
|
||||
<AppInfo
|
||||
appId="test-app-id"
|
||||
appDetail={appDetail}
|
||||
onCreate={mockOnCreate}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByText('Create from Sample')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('calls onCreate when button is clicked', () => {
|
||||
const appDetail = createMockAppDetail('chat')
|
||||
const mockOnCreate = vi.fn()
|
||||
|
||||
render(
|
||||
<AppInfo
|
||||
appId="test-app-id"
|
||||
appDetail={appDetail}
|
||||
onCreate={mockOnCreate}
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByText('Create from Sample'))
|
||||
expect(mockOnCreate).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('category', () => {
|
||||
it('renders category when provided', () => {
|
||||
const appDetail = createMockAppDetail('chat')
|
||||
const mockOnCreate = vi.fn()
|
||||
|
||||
render(
|
||||
<AppInfo
|
||||
appId="test-app-id"
|
||||
appDetail={appDetail}
|
||||
category="AI Assistant"
|
||||
onCreate={mockOnCreate}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByText('Category')).toBeInTheDocument()
|
||||
expect(screen.getByText('AI Assistant')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('does not render category section when not provided', () => {
|
||||
const appDetail = createMockAppDetail('chat')
|
||||
const mockOnCreate = vi.fn()
|
||||
|
||||
render(
|
||||
<AppInfo
|
||||
appId="test-app-id"
|
||||
appDetail={appDetail}
|
||||
onCreate={mockOnCreate}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.queryByText('Category')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('requirements', () => {
|
||||
it('renders requirements when available', () => {
|
||||
mockUseGetRequirements.mockReturnValue({
|
||||
requirements: [
|
||||
{ name: 'OpenAI GPT-4', iconUrl: 'https://example.com/icon1.png' },
|
||||
{ name: 'Google Search', iconUrl: 'https://example.com/icon2.png' },
|
||||
],
|
||||
})
|
||||
|
||||
const appDetail = createMockAppDetail('chat')
|
||||
const mockOnCreate = vi.fn()
|
||||
|
||||
render(
|
||||
<AppInfo
|
||||
appId="test-app-id"
|
||||
appDetail={appDetail}
|
||||
onCreate={mockOnCreate}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByText('Requirements')).toBeInTheDocument()
|
||||
expect(screen.getByText('OpenAI GPT-4')).toBeInTheDocument()
|
||||
expect(screen.getByText('Google Search')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('does not render requirements section when empty', () => {
|
||||
mockUseGetRequirements.mockReturnValue({
|
||||
requirements: [],
|
||||
})
|
||||
|
||||
const appDetail = createMockAppDetail('chat')
|
||||
const mockOnCreate = vi.fn()
|
||||
|
||||
render(
|
||||
<AppInfo
|
||||
appId="test-app-id"
|
||||
appDetail={appDetail}
|
||||
onCreate={mockOnCreate}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.queryByText('Requirements')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders requirement icons with correct background image', () => {
|
||||
mockUseGetRequirements.mockReturnValue({
|
||||
requirements: [
|
||||
{ name: 'Test Tool', iconUrl: 'https://example.com/test-icon.png' },
|
||||
],
|
||||
})
|
||||
|
||||
const appDetail = createMockAppDetail('chat')
|
||||
const mockOnCreate = vi.fn()
|
||||
|
||||
const { container } = render(
|
||||
<AppInfo
|
||||
appId="test-app-id"
|
||||
appDetail={appDetail}
|
||||
onCreate={mockOnCreate}
|
||||
/>,
|
||||
)
|
||||
|
||||
const iconElement = container.querySelector('[style*="background-image"]')
|
||||
expect(iconElement).toBeInTheDocument()
|
||||
expect(iconElement).toHaveStyle({ backgroundImage: 'url(https://example.com/test-icon.png)' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('className prop', () => {
|
||||
it('applies custom className', () => {
|
||||
const appDetail = createMockAppDetail('chat')
|
||||
const mockOnCreate = vi.fn()
|
||||
|
||||
const { container } = render(
|
||||
<AppInfo
|
||||
appId="test-app-id"
|
||||
appDetail={appDetail}
|
||||
className="custom-class"
|
||||
onCreate={mockOnCreate}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(container.firstChild).toHaveClass('custom-class')
|
||||
})
|
||||
})
|
||||
|
||||
describe('hook calls', () => {
|
||||
it('calls useGetRequirements with correct parameters', () => {
|
||||
const appDetail = createMockAppDetail('chat')
|
||||
const mockOnCreate = vi.fn()
|
||||
|
||||
render(
|
||||
<AppInfo
|
||||
appId="my-app-id"
|
||||
appDetail={appDetail}
|
||||
onCreate={mockOnCreate}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(mockUseGetRequirements).toHaveBeenCalledWith({
|
||||
appDetail,
|
||||
appId: 'my-app-id',
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,425 @@
|
||||
import type { TryAppInfo } from '@/service/try-app'
|
||||
import { renderHook } from '@testing-library/react'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import useGetRequirements from './use-get-requirements'
|
||||
|
||||
const mockUseGetTryAppFlowPreview = vi.fn()
|
||||
|
||||
vi.mock('@/service/use-try-app', () => ({
|
||||
useGetTryAppFlowPreview: (...args: unknown[]) => mockUseGetTryAppFlowPreview(...args),
|
||||
}))
|
||||
|
||||
vi.mock('@/config', () => ({
|
||||
MARKETPLACE_API_PREFIX: 'https://marketplace.api',
|
||||
}))
|
||||
|
||||
const createMockAppDetail = (mode: string, overrides: Partial<TryAppInfo> = {}): TryAppInfo => ({
|
||||
id: 'test-app-id',
|
||||
name: 'Test App',
|
||||
description: 'Test Description',
|
||||
mode,
|
||||
site: {
|
||||
title: 'Test Site Title',
|
||||
icon: 'icon',
|
||||
icon_type: 'emoji',
|
||||
icon_background: '#FFFFFF',
|
||||
icon_url: '',
|
||||
},
|
||||
model_config: {
|
||||
model: {
|
||||
provider: 'langgenius/openai/openai',
|
||||
name: 'gpt-4',
|
||||
mode: 'chat',
|
||||
},
|
||||
dataset_configs: {
|
||||
datasets: {
|
||||
datasets: [],
|
||||
},
|
||||
},
|
||||
agent_mode: {
|
||||
tools: [],
|
||||
},
|
||||
user_input_form: [],
|
||||
},
|
||||
...overrides,
|
||||
} as unknown as TryAppInfo)
|
||||
|
||||
describe('useGetRequirements', () => {
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('basic app modes (chat, completion, agent-chat)', () => {
|
||||
it('returns model provider for chat mode', () => {
|
||||
mockUseGetTryAppFlowPreview.mockReturnValue({ data: null })
|
||||
|
||||
const appDetail = createMockAppDetail('chat')
|
||||
const { result } = renderHook(() =>
|
||||
useGetRequirements({ appDetail, appId: 'test-app-id' }),
|
||||
)
|
||||
|
||||
expect(result.current.requirements).toHaveLength(1)
|
||||
expect(result.current.requirements[0].name).toBe('openai')
|
||||
expect(result.current.requirements[0].iconUrl).toBe('https://marketplace.api/plugins/langgenius/openai/icon')
|
||||
})
|
||||
|
||||
it('returns model provider for completion mode', () => {
|
||||
mockUseGetTryAppFlowPreview.mockReturnValue({ data: null })
|
||||
|
||||
const appDetail = createMockAppDetail('completion', {
|
||||
model_config: {
|
||||
model: {
|
||||
provider: 'anthropic/claude/claude',
|
||||
name: 'claude-3',
|
||||
mode: 'completion',
|
||||
},
|
||||
dataset_configs: { datasets: { datasets: [] } },
|
||||
agent_mode: { tools: [] },
|
||||
user_input_form: [],
|
||||
},
|
||||
} as unknown as Partial<TryAppInfo>)
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useGetRequirements({ appDetail, appId: 'test-app-id' }),
|
||||
)
|
||||
|
||||
expect(result.current.requirements).toHaveLength(1)
|
||||
expect(result.current.requirements[0].name).toBe('claude')
|
||||
})
|
||||
|
||||
it('returns model provider and tools for agent-chat mode', () => {
|
||||
mockUseGetTryAppFlowPreview.mockReturnValue({ data: null })
|
||||
|
||||
const appDetail = createMockAppDetail('agent-chat', {
|
||||
model_config: {
|
||||
model: {
|
||||
provider: 'langgenius/openai/openai',
|
||||
name: 'gpt-4',
|
||||
mode: 'chat',
|
||||
},
|
||||
dataset_configs: { datasets: { datasets: [] } },
|
||||
agent_mode: {
|
||||
tools: [
|
||||
{
|
||||
enabled: true,
|
||||
provider_id: 'langgenius/google_search/google_search',
|
||||
tool_label: 'Google Search',
|
||||
},
|
||||
{
|
||||
enabled: true,
|
||||
provider_id: 'langgenius/web_scraper/web_scraper',
|
||||
tool_label: 'Web Scraper',
|
||||
},
|
||||
{
|
||||
enabled: false,
|
||||
provider_id: 'langgenius/disabled_tool/disabled_tool',
|
||||
tool_label: 'Disabled Tool',
|
||||
},
|
||||
],
|
||||
},
|
||||
user_input_form: [],
|
||||
},
|
||||
} as unknown as Partial<TryAppInfo>)
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useGetRequirements({ appDetail, appId: 'test-app-id' }),
|
||||
)
|
||||
|
||||
expect(result.current.requirements).toHaveLength(3)
|
||||
expect(result.current.requirements.map(r => r.name)).toContain('openai')
|
||||
expect(result.current.requirements.map(r => r.name)).toContain('Google Search')
|
||||
expect(result.current.requirements.map(r => r.name)).toContain('Web Scraper')
|
||||
expect(result.current.requirements.map(r => r.name)).not.toContain('Disabled Tool')
|
||||
})
|
||||
|
||||
it('filters out disabled tools in agent mode', () => {
|
||||
mockUseGetTryAppFlowPreview.mockReturnValue({ data: null })
|
||||
|
||||
const appDetail = createMockAppDetail('agent-chat', {
|
||||
model_config: {
|
||||
model: {
|
||||
provider: 'langgenius/openai/openai',
|
||||
name: 'gpt-4',
|
||||
mode: 'chat',
|
||||
},
|
||||
dataset_configs: { datasets: { datasets: [] } },
|
||||
agent_mode: {
|
||||
tools: [
|
||||
{
|
||||
enabled: false,
|
||||
provider_id: 'langgenius/tool1/tool1',
|
||||
tool_label: 'Tool 1',
|
||||
},
|
||||
{
|
||||
enabled: false,
|
||||
provider_id: 'langgenius/tool2/tool2',
|
||||
tool_label: 'Tool 2',
|
||||
},
|
||||
],
|
||||
},
|
||||
user_input_form: [],
|
||||
},
|
||||
} as unknown as Partial<TryAppInfo>)
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useGetRequirements({ appDetail, appId: 'test-app-id' }),
|
||||
)
|
||||
|
||||
// Only model provider should be included, no disabled tools
|
||||
expect(result.current.requirements).toHaveLength(1)
|
||||
expect(result.current.requirements[0].name).toBe('openai')
|
||||
})
|
||||
})
|
||||
|
||||
describe('advanced app modes (workflow, advanced-chat)', () => {
|
||||
it('returns requirements from flow data for workflow mode', () => {
|
||||
mockUseGetTryAppFlowPreview.mockReturnValue({
|
||||
data: {
|
||||
graph: {
|
||||
nodes: [
|
||||
{
|
||||
data: {
|
||||
type: 'llm',
|
||||
model: {
|
||||
provider: 'langgenius/openai/openai',
|
||||
name: 'gpt-4',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
data: {
|
||||
type: 'tool',
|
||||
provider_id: 'langgenius/google/google',
|
||||
tool_label: 'Google Tool',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const appDetail = createMockAppDetail('workflow')
|
||||
const { result } = renderHook(() =>
|
||||
useGetRequirements({ appDetail, appId: 'test-app-id' }),
|
||||
)
|
||||
|
||||
expect(result.current.requirements).toHaveLength(2)
|
||||
expect(result.current.requirements.map(r => r.name)).toContain('gpt-4')
|
||||
expect(result.current.requirements.map(r => r.name)).toContain('Google Tool')
|
||||
})
|
||||
|
||||
it('returns requirements from flow data for advanced-chat mode', () => {
|
||||
mockUseGetTryAppFlowPreview.mockReturnValue({
|
||||
data: {
|
||||
graph: {
|
||||
nodes: [
|
||||
{
|
||||
data: {
|
||||
type: 'llm',
|
||||
model: {
|
||||
provider: 'anthropic/claude/claude',
|
||||
name: 'claude-3-opus',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const appDetail = createMockAppDetail('advanced-chat')
|
||||
const { result } = renderHook(() =>
|
||||
useGetRequirements({ appDetail, appId: 'test-app-id' }),
|
||||
)
|
||||
|
||||
expect(result.current.requirements).toHaveLength(1)
|
||||
expect(result.current.requirements[0].name).toBe('claude-3-opus')
|
||||
})
|
||||
|
||||
it('returns empty requirements when flow data has no nodes', () => {
|
||||
mockUseGetTryAppFlowPreview.mockReturnValue({
|
||||
data: {
|
||||
graph: {
|
||||
nodes: [],
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const appDetail = createMockAppDetail('workflow')
|
||||
const { result } = renderHook(() =>
|
||||
useGetRequirements({ appDetail, appId: 'test-app-id' }),
|
||||
)
|
||||
|
||||
expect(result.current.requirements).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('returns empty requirements when flow data is null', () => {
|
||||
mockUseGetTryAppFlowPreview.mockReturnValue({
|
||||
data: null,
|
||||
})
|
||||
|
||||
const appDetail = createMockAppDetail('workflow')
|
||||
const { result } = renderHook(() =>
|
||||
useGetRequirements({ appDetail, appId: 'test-app-id' }),
|
||||
)
|
||||
|
||||
expect(result.current.requirements).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('extracts multiple LLM nodes from flow data', () => {
|
||||
mockUseGetTryAppFlowPreview.mockReturnValue({
|
||||
data: {
|
||||
graph: {
|
||||
nodes: [
|
||||
{
|
||||
data: {
|
||||
type: 'llm',
|
||||
model: {
|
||||
provider: 'langgenius/openai/openai',
|
||||
name: 'gpt-4',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
data: {
|
||||
type: 'llm',
|
||||
model: {
|
||||
provider: 'anthropic/claude/claude',
|
||||
name: 'claude-3',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const appDetail = createMockAppDetail('workflow')
|
||||
const { result } = renderHook(() =>
|
||||
useGetRequirements({ appDetail, appId: 'test-app-id' }),
|
||||
)
|
||||
|
||||
expect(result.current.requirements).toHaveLength(2)
|
||||
expect(result.current.requirements.map(r => r.name)).toContain('gpt-4')
|
||||
expect(result.current.requirements.map(r => r.name)).toContain('claude-3')
|
||||
})
|
||||
|
||||
it('extracts multiple tool nodes from flow data', () => {
|
||||
mockUseGetTryAppFlowPreview.mockReturnValue({
|
||||
data: {
|
||||
graph: {
|
||||
nodes: [
|
||||
{
|
||||
data: {
|
||||
type: 'tool',
|
||||
provider_id: 'langgenius/tool1/tool1',
|
||||
tool_label: 'Tool 1',
|
||||
},
|
||||
},
|
||||
{
|
||||
data: {
|
||||
type: 'tool',
|
||||
provider_id: 'langgenius/tool2/tool2',
|
||||
tool_label: 'Tool 2',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const appDetail = createMockAppDetail('workflow')
|
||||
const { result } = renderHook(() =>
|
||||
useGetRequirements({ appDetail, appId: 'test-app-id' }),
|
||||
)
|
||||
|
||||
expect(result.current.requirements).toHaveLength(2)
|
||||
expect(result.current.requirements.map(r => r.name)).toContain('Tool 1')
|
||||
expect(result.current.requirements.map(r => r.name)).toContain('Tool 2')
|
||||
})
|
||||
})
|
||||
|
||||
describe('deduplication', () => {
|
||||
it('removes duplicate requirements by name', () => {
|
||||
mockUseGetTryAppFlowPreview.mockReturnValue({
|
||||
data: {
|
||||
graph: {
|
||||
nodes: [
|
||||
{
|
||||
data: {
|
||||
type: 'llm',
|
||||
model: {
|
||||
provider: 'langgenius/openai/openai',
|
||||
name: 'gpt-4',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
data: {
|
||||
type: 'llm',
|
||||
model: {
|
||||
provider: 'langgenius/openai/openai',
|
||||
name: 'gpt-4',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const appDetail = createMockAppDetail('workflow')
|
||||
const { result } = renderHook(() =>
|
||||
useGetRequirements({ appDetail, appId: 'test-app-id' }),
|
||||
)
|
||||
|
||||
expect(result.current.requirements).toHaveLength(1)
|
||||
expect(result.current.requirements[0].name).toBe('gpt-4')
|
||||
})
|
||||
})
|
||||
|
||||
describe('icon URL generation', () => {
|
||||
it('generates correct icon URL for model providers', () => {
|
||||
mockUseGetTryAppFlowPreview.mockReturnValue({ data: null })
|
||||
|
||||
const appDetail = createMockAppDetail('chat', {
|
||||
model_config: {
|
||||
model: {
|
||||
provider: 'org/plugin/model',
|
||||
name: 'model-name',
|
||||
mode: 'chat',
|
||||
},
|
||||
dataset_configs: { datasets: { datasets: [] } },
|
||||
agent_mode: { tools: [] },
|
||||
user_input_form: [],
|
||||
},
|
||||
} as unknown as Partial<TryAppInfo>)
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useGetRequirements({ appDetail, appId: 'test-app-id' }),
|
||||
)
|
||||
|
||||
expect(result.current.requirements[0].iconUrl).toBe('https://marketplace.api/plugins/org/plugin/icon')
|
||||
})
|
||||
})
|
||||
|
||||
describe('hook calls', () => {
|
||||
it('calls useGetTryAppFlowPreview with correct parameters for basic apps', () => {
|
||||
mockUseGetTryAppFlowPreview.mockReturnValue({ data: null })
|
||||
|
||||
const appDetail = createMockAppDetail('chat')
|
||||
renderHook(() => useGetRequirements({ appDetail, appId: 'test-app-id' }))
|
||||
|
||||
expect(mockUseGetTryAppFlowPreview).toHaveBeenCalledWith('test-app-id', true)
|
||||
})
|
||||
|
||||
it('calls useGetTryAppFlowPreview with correct parameters for advanced apps', () => {
|
||||
mockUseGetTryAppFlowPreview.mockReturnValue({ data: null })
|
||||
|
||||
const appDetail = createMockAppDetail('workflow')
|
||||
renderHook(() => useGetRequirements({ appDetail, appId: 'test-app-id' }))
|
||||
|
||||
expect(mockUseGetTryAppFlowPreview).toHaveBeenCalledWith('test-app-id', false)
|
||||
})
|
||||
})
|
||||
})
|
||||
357
web/app/components/explore/try-app/app/chat.spec.tsx
Normal file
357
web/app/components/explore/try-app/app/chat.spec.tsx
Normal file
@ -0,0 +1,357 @@
|
||||
import type { TryAppInfo } from '@/service/try-app'
|
||||
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import TryApp from './chat'
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => {
|
||||
const translations: Record<string, string> = {
|
||||
'chat.resetChat': 'Reset Chat',
|
||||
'tryApp.tryInfo': 'This is try mode info',
|
||||
}
|
||||
return translations[key] || key
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
const mockRemoveConversationIdInfo = vi.fn()
|
||||
const mockHandleNewConversation = vi.fn()
|
||||
const mockUseEmbeddedChatbot = vi.fn()
|
||||
|
||||
vi.mock('@/app/components/base/chat/embedded-chatbot/hooks', () => ({
|
||||
useEmbeddedChatbot: (...args: unknown[]) => mockUseEmbeddedChatbot(...args),
|
||||
}))
|
||||
|
||||
vi.mock('@/hooks/use-breakpoints', () => ({
|
||||
default: () => 'pc',
|
||||
MediaType: {
|
||||
mobile: 'mobile',
|
||||
pc: 'pc',
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('../../../base/chat/embedded-chatbot/theme/theme-context', () => ({
|
||||
useThemeContext: () => ({
|
||||
primaryColor: '#1890ff',
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/chat/embedded-chatbot/chat-wrapper', () => ({
|
||||
default: () => <div data-testid="chat-wrapper">ChatWrapper</div>,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/chat/embedded-chatbot/inputs-form/view-form-dropdown', () => ({
|
||||
default: () => <div data-testid="view-form-dropdown">ViewFormDropdown</div>,
|
||||
}))
|
||||
|
||||
const createMockAppDetail = (overrides: Partial<TryAppInfo> = {}): TryAppInfo => ({
|
||||
id: 'test-app-id',
|
||||
name: 'Test Chat App',
|
||||
description: 'Test Description',
|
||||
mode: 'chat',
|
||||
site: {
|
||||
title: 'Test Site Title',
|
||||
icon: '💬',
|
||||
icon_type: 'emoji',
|
||||
icon_background: '#4F46E5',
|
||||
icon_url: '',
|
||||
},
|
||||
model_config: {
|
||||
model: {
|
||||
provider: 'langgenius/openai/openai',
|
||||
name: 'gpt-4',
|
||||
mode: 'chat',
|
||||
},
|
||||
dataset_configs: {
|
||||
datasets: {
|
||||
datasets: [],
|
||||
},
|
||||
},
|
||||
agent_mode: {
|
||||
tools: [],
|
||||
},
|
||||
user_input_form: [],
|
||||
},
|
||||
...overrides,
|
||||
} as unknown as TryAppInfo)
|
||||
|
||||
describe('TryApp (chat.tsx)', () => {
|
||||
beforeEach(() => {
|
||||
mockUseEmbeddedChatbot.mockReturnValue({
|
||||
removeConversationIdInfo: mockRemoveConversationIdInfo,
|
||||
handleNewConversation: mockHandleNewConversation,
|
||||
currentConversationId: null,
|
||||
inputsForms: [],
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('basic rendering', () => {
|
||||
it('renders app name', () => {
|
||||
const appDetail = createMockAppDetail()
|
||||
|
||||
render(
|
||||
<TryApp
|
||||
appId="test-app-id"
|
||||
appDetail={appDetail}
|
||||
className="test-class"
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByText('Test Chat App')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders app name with title attribute', () => {
|
||||
const appDetail = createMockAppDetail({ name: 'Long App Name' } as Partial<TryAppInfo>)
|
||||
|
||||
render(
|
||||
<TryApp
|
||||
appId="test-app-id"
|
||||
appDetail={appDetail}
|
||||
className="test-class"
|
||||
/>,
|
||||
)
|
||||
|
||||
const nameElement = screen.getByText('Long App Name')
|
||||
expect(nameElement).toHaveAttribute('title', 'Long App Name')
|
||||
})
|
||||
|
||||
it('renders ChatWrapper', () => {
|
||||
const appDetail = createMockAppDetail()
|
||||
|
||||
render(
|
||||
<TryApp
|
||||
appId="test-app-id"
|
||||
appDetail={appDetail}
|
||||
className="test-class"
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('chat-wrapper')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders alert with try info', () => {
|
||||
const appDetail = createMockAppDetail()
|
||||
|
||||
render(
|
||||
<TryApp
|
||||
appId="test-app-id"
|
||||
appDetail={appDetail}
|
||||
className="test-class"
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByText('This is try mode info')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('applies className prop', () => {
|
||||
const appDetail = createMockAppDetail()
|
||||
|
||||
const { container } = render(
|
||||
<TryApp
|
||||
appId="test-app-id"
|
||||
appDetail={appDetail}
|
||||
className="custom-class"
|
||||
/>,
|
||||
)
|
||||
|
||||
// The component wraps with EmbeddedChatbotContext.Provider, first child is the div with className
|
||||
const innerDiv = container.querySelector('.custom-class')
|
||||
expect(innerDiv).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('reset button', () => {
|
||||
it('does not render reset button when no conversation', () => {
|
||||
mockUseEmbeddedChatbot.mockReturnValue({
|
||||
removeConversationIdInfo: mockRemoveConversationIdInfo,
|
||||
handleNewConversation: mockHandleNewConversation,
|
||||
currentConversationId: null,
|
||||
inputsForms: [],
|
||||
})
|
||||
|
||||
const appDetail = createMockAppDetail()
|
||||
|
||||
render(
|
||||
<TryApp
|
||||
appId="test-app-id"
|
||||
appDetail={appDetail}
|
||||
className="test-class"
|
||||
/>,
|
||||
)
|
||||
|
||||
// Reset button should not be present
|
||||
expect(screen.queryByRole('button')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders reset button when conversation exists', () => {
|
||||
mockUseEmbeddedChatbot.mockReturnValue({
|
||||
removeConversationIdInfo: mockRemoveConversationIdInfo,
|
||||
handleNewConversation: mockHandleNewConversation,
|
||||
currentConversationId: 'conv-123',
|
||||
inputsForms: [],
|
||||
})
|
||||
|
||||
const appDetail = createMockAppDetail()
|
||||
|
||||
render(
|
||||
<TryApp
|
||||
appId="test-app-id"
|
||||
appDetail={appDetail}
|
||||
className="test-class"
|
||||
/>,
|
||||
)
|
||||
|
||||
// Should have a button (the reset button)
|
||||
expect(screen.getByRole('button')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('calls handleNewConversation when reset button is clicked', () => {
|
||||
mockUseEmbeddedChatbot.mockReturnValue({
|
||||
removeConversationIdInfo: mockRemoveConversationIdInfo,
|
||||
handleNewConversation: mockHandleNewConversation,
|
||||
currentConversationId: 'conv-123',
|
||||
inputsForms: [],
|
||||
})
|
||||
|
||||
const appDetail = createMockAppDetail()
|
||||
|
||||
render(
|
||||
<TryApp
|
||||
appId="test-app-id"
|
||||
appDetail={appDetail}
|
||||
className="test-class"
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByRole('button'))
|
||||
|
||||
expect(mockRemoveConversationIdInfo).toHaveBeenCalledWith('test-app-id')
|
||||
expect(mockHandleNewConversation).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('view form dropdown', () => {
|
||||
it('does not render view form dropdown when no conversation', () => {
|
||||
mockUseEmbeddedChatbot.mockReturnValue({
|
||||
removeConversationIdInfo: mockRemoveConversationIdInfo,
|
||||
handleNewConversation: mockHandleNewConversation,
|
||||
currentConversationId: null,
|
||||
inputsForms: [{ id: 'form1' }],
|
||||
})
|
||||
|
||||
const appDetail = createMockAppDetail()
|
||||
|
||||
render(
|
||||
<TryApp
|
||||
appId="test-app-id"
|
||||
appDetail={appDetail}
|
||||
className="test-class"
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.queryByTestId('view-form-dropdown')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('does not render view form dropdown when no input forms', () => {
|
||||
mockUseEmbeddedChatbot.mockReturnValue({
|
||||
removeConversationIdInfo: mockRemoveConversationIdInfo,
|
||||
handleNewConversation: mockHandleNewConversation,
|
||||
currentConversationId: 'conv-123',
|
||||
inputsForms: [],
|
||||
})
|
||||
|
||||
const appDetail = createMockAppDetail()
|
||||
|
||||
render(
|
||||
<TryApp
|
||||
appId="test-app-id"
|
||||
appDetail={appDetail}
|
||||
className="test-class"
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.queryByTestId('view-form-dropdown')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders view form dropdown when conversation and input forms exist', () => {
|
||||
mockUseEmbeddedChatbot.mockReturnValue({
|
||||
removeConversationIdInfo: mockRemoveConversationIdInfo,
|
||||
handleNewConversation: mockHandleNewConversation,
|
||||
currentConversationId: 'conv-123',
|
||||
inputsForms: [{ id: 'form1' }],
|
||||
})
|
||||
|
||||
const appDetail = createMockAppDetail()
|
||||
|
||||
render(
|
||||
<TryApp
|
||||
appId="test-app-id"
|
||||
appDetail={appDetail}
|
||||
className="test-class"
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('view-form-dropdown')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('alert hiding', () => {
|
||||
it('hides alert when onHide is called', () => {
|
||||
const appDetail = createMockAppDetail()
|
||||
|
||||
render(
|
||||
<TryApp
|
||||
appId="test-app-id"
|
||||
appDetail={appDetail}
|
||||
className="test-class"
|
||||
/>,
|
||||
)
|
||||
|
||||
// Find and click the hide button on the alert
|
||||
const alertElement = screen.getByText('This is try mode info').closest('[class*="alert"]')?.parentElement
|
||||
const hideButton = alertElement?.querySelector('button, [role="button"], svg')
|
||||
|
||||
if (hideButton) {
|
||||
fireEvent.click(hideButton)
|
||||
// After hiding, the alert should not be visible
|
||||
expect(screen.queryByText('This is try mode info')).not.toBeInTheDocument()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('hook calls', () => {
|
||||
it('calls useEmbeddedChatbot with correct parameters', () => {
|
||||
const appDetail = createMockAppDetail()
|
||||
|
||||
render(
|
||||
<TryApp
|
||||
appId="my-app-id"
|
||||
appDetail={appDetail}
|
||||
className="test-class"
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(mockUseEmbeddedChatbot).toHaveBeenCalledWith('tryApp', 'my-app-id')
|
||||
})
|
||||
|
||||
it('calls removeConversationIdInfo on mount', () => {
|
||||
const appDetail = createMockAppDetail()
|
||||
|
||||
render(
|
||||
<TryApp
|
||||
appId="my-app-id"
|
||||
appDetail={appDetail}
|
||||
className="test-class"
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(mockRemoveConversationIdInfo).toHaveBeenCalledWith('my-app-id')
|
||||
})
|
||||
})
|
||||
})
|
||||
188
web/app/components/explore/try-app/app/index.spec.tsx
Normal file
188
web/app/components/explore/try-app/app/index.spec.tsx
Normal file
@ -0,0 +1,188 @@
|
||||
import type { TryAppInfo } from '@/service/try-app'
|
||||
import { cleanup, render, screen } from '@testing-library/react'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import TryApp from './index'
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/hooks/use-document-title', () => ({
|
||||
default: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('./chat', () => ({
|
||||
default: ({ appId, appDetail, className }: { appId: string, appDetail: TryAppInfo, className: string }) => (
|
||||
<div data-testid="chat-component" data-app-id={appId} data-mode={appDetail.mode} className={className}>
|
||||
Chat Component
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('./text-generation', () => ({
|
||||
default: ({
|
||||
appId,
|
||||
className,
|
||||
isWorkflow,
|
||||
appData,
|
||||
}: { appId: string, className: string, isWorkflow: boolean, appData: { mode: string } }) => (
|
||||
<div
|
||||
data-testid="text-generation-component"
|
||||
data-app-id={appId}
|
||||
data-is-workflow={isWorkflow}
|
||||
data-mode={appData?.mode}
|
||||
className={className}
|
||||
>
|
||||
TextGeneration Component
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
const createMockAppDetail = (mode: string): TryAppInfo => ({
|
||||
id: 'test-app-id',
|
||||
name: 'Test App',
|
||||
description: 'Test Description',
|
||||
mode,
|
||||
site: {
|
||||
title: 'Test Site Title',
|
||||
icon: 'icon',
|
||||
icon_type: 'emoji',
|
||||
icon_background: '#FFFFFF',
|
||||
icon_url: '',
|
||||
},
|
||||
model_config: {
|
||||
model: {
|
||||
provider: 'test/provider',
|
||||
name: 'test-model',
|
||||
mode: 'chat',
|
||||
},
|
||||
dataset_configs: {
|
||||
datasets: {
|
||||
datasets: [],
|
||||
},
|
||||
},
|
||||
agent_mode: {
|
||||
tools: [],
|
||||
},
|
||||
user_input_form: [],
|
||||
},
|
||||
} as unknown as TryAppInfo)
|
||||
|
||||
describe('TryApp (app/index.tsx)', () => {
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
})
|
||||
|
||||
describe('chat mode rendering', () => {
|
||||
it('renders Chat component for chat mode', () => {
|
||||
const appDetail = createMockAppDetail('chat')
|
||||
render(<TryApp appId="test-app-id" appDetail={appDetail} />)
|
||||
|
||||
expect(screen.getByTestId('chat-component')).toBeInTheDocument()
|
||||
expect(screen.queryByTestId('text-generation-component')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders Chat component for advanced-chat mode', () => {
|
||||
const appDetail = createMockAppDetail('advanced-chat')
|
||||
render(<TryApp appId="test-app-id" appDetail={appDetail} />)
|
||||
|
||||
expect(screen.getByTestId('chat-component')).toBeInTheDocument()
|
||||
expect(screen.queryByTestId('text-generation-component')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders Chat component for agent-chat mode', () => {
|
||||
const appDetail = createMockAppDetail('agent-chat')
|
||||
render(<TryApp appId="test-app-id" appDetail={appDetail} />)
|
||||
|
||||
expect(screen.getByTestId('chat-component')).toBeInTheDocument()
|
||||
expect(screen.queryByTestId('text-generation-component')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('passes correct props to Chat component', () => {
|
||||
const appDetail = createMockAppDetail('chat')
|
||||
render(<TryApp appId="test-app-id" appDetail={appDetail} />)
|
||||
|
||||
const chatComponent = screen.getByTestId('chat-component')
|
||||
expect(chatComponent).toHaveAttribute('data-app-id', 'test-app-id')
|
||||
expect(chatComponent).toHaveAttribute('data-mode', 'chat')
|
||||
expect(chatComponent).toHaveClass('h-full', 'grow')
|
||||
})
|
||||
})
|
||||
|
||||
describe('completion mode rendering', () => {
|
||||
it('renders TextGeneration component for completion mode', () => {
|
||||
const appDetail = createMockAppDetail('completion')
|
||||
render(<TryApp appId="test-app-id" appDetail={appDetail} />)
|
||||
|
||||
expect(screen.getByTestId('text-generation-component')).toBeInTheDocument()
|
||||
expect(screen.queryByTestId('chat-component')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders TextGeneration component for workflow mode', () => {
|
||||
const appDetail = createMockAppDetail('workflow')
|
||||
render(<TryApp appId="test-app-id" appDetail={appDetail} />)
|
||||
|
||||
expect(screen.getByTestId('text-generation-component')).toBeInTheDocument()
|
||||
expect(screen.queryByTestId('chat-component')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('passes isWorkflow=true for workflow mode', () => {
|
||||
const appDetail = createMockAppDetail('workflow')
|
||||
render(<TryApp appId="test-app-id" appDetail={appDetail} />)
|
||||
|
||||
const textGenComponent = screen.getByTestId('text-generation-component')
|
||||
expect(textGenComponent).toHaveAttribute('data-is-workflow', 'true')
|
||||
})
|
||||
|
||||
it('passes isWorkflow=false for completion mode', () => {
|
||||
const appDetail = createMockAppDetail('completion')
|
||||
render(<TryApp appId="test-app-id" appDetail={appDetail} />)
|
||||
|
||||
const textGenComponent = screen.getByTestId('text-generation-component')
|
||||
expect(textGenComponent).toHaveAttribute('data-is-workflow', 'false')
|
||||
})
|
||||
|
||||
it('passes correct props to TextGeneration component', () => {
|
||||
const appDetail = createMockAppDetail('completion')
|
||||
render(<TryApp appId="test-app-id" appDetail={appDetail} />)
|
||||
|
||||
const textGenComponent = screen.getByTestId('text-generation-component')
|
||||
expect(textGenComponent).toHaveAttribute('data-app-id', 'test-app-id')
|
||||
expect(textGenComponent).toHaveClass('h-full', 'grow')
|
||||
})
|
||||
})
|
||||
|
||||
describe('document title', () => {
|
||||
it('calls useDocumentTitle with site title', async () => {
|
||||
const useDocumentTitle = (await import('@/hooks/use-document-title')).default
|
||||
const appDetail = createMockAppDetail('chat')
|
||||
appDetail.site.title = 'My App Title'
|
||||
|
||||
render(<TryApp appId="test-app-id" appDetail={appDetail} />)
|
||||
|
||||
expect(useDocumentTitle).toHaveBeenCalledWith('My App Title')
|
||||
})
|
||||
|
||||
it('calls useDocumentTitle with empty string when site.title is undefined', async () => {
|
||||
const useDocumentTitle = (await import('@/hooks/use-document-title')).default
|
||||
const appDetail = createMockAppDetail('chat')
|
||||
appDetail.site = undefined as unknown as TryAppInfo['site']
|
||||
|
||||
render(<TryApp appId="test-app-id" appDetail={appDetail} />)
|
||||
|
||||
expect(useDocumentTitle).toHaveBeenCalledWith('')
|
||||
})
|
||||
})
|
||||
|
||||
describe('wrapper styling', () => {
|
||||
it('renders with correct wrapper classes', () => {
|
||||
const appDetail = createMockAppDetail('chat')
|
||||
const { container } = render(<TryApp appId="test-app-id" appDetail={appDetail} />)
|
||||
|
||||
const wrapper = container.firstChild as HTMLElement
|
||||
expect(wrapper).toHaveClass('flex', 'h-full', 'w-full')
|
||||
})
|
||||
})
|
||||
})
|
||||
468
web/app/components/explore/try-app/app/text-generation.spec.tsx
Normal file
468
web/app/components/explore/try-app/app/text-generation.spec.tsx
Normal file
@ -0,0 +1,468 @@
|
||||
import type { AppData } from '@/models/share'
|
||||
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import TextGeneration from './text-generation'
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => {
|
||||
const translations: Record<string, string> = {
|
||||
'tryApp.tryInfo': 'This is a try app notice',
|
||||
}
|
||||
return translations[key] || key
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
const mockUpdateAppInfo = vi.fn()
|
||||
const mockUpdateAppParams = vi.fn()
|
||||
const mockAppParams = {
|
||||
user_input_form: [],
|
||||
more_like_this: { enabled: false },
|
||||
file_upload: null,
|
||||
text_to_speech: { enabled: false },
|
||||
system_parameters: {},
|
||||
}
|
||||
let mockStoreAppParams: typeof mockAppParams | null = mockAppParams
|
||||
|
||||
vi.mock('@/context/web-app-context', () => ({
|
||||
useWebAppStore: (selector: (state: unknown) => unknown) => {
|
||||
const state = {
|
||||
updateAppInfo: mockUpdateAppInfo,
|
||||
updateAppParams: mockUpdateAppParams,
|
||||
appParams: mockStoreAppParams,
|
||||
}
|
||||
return selector(state)
|
||||
},
|
||||
}))
|
||||
|
||||
const mockUseGetTryAppParams = vi.fn()
|
||||
|
||||
vi.mock('@/service/use-try-app', () => ({
|
||||
useGetTryAppParams: (...args: unknown[]) => mockUseGetTryAppParams(...args),
|
||||
}))
|
||||
|
||||
let mockMediaType = 'pc'
|
||||
|
||||
vi.mock('@/hooks/use-breakpoints', () => ({
|
||||
default: () => mockMediaType,
|
||||
MediaType: {
|
||||
mobile: 'mobile',
|
||||
pc: 'pc',
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/share/text-generation/run-once', () => ({
|
||||
default: ({
|
||||
siteInfo,
|
||||
onSend,
|
||||
onInputsChange,
|
||||
}: { siteInfo: { title: string }, onSend: () => void, onInputsChange: (inputs: Record<string, unknown>) => void }) => (
|
||||
<div data-testid="run-once">
|
||||
<span data-testid="site-title">{siteInfo?.title}</span>
|
||||
<button data-testid="send-button" onClick={onSend}>Send</button>
|
||||
<button data-testid="inputs-change-button" onClick={() => onInputsChange({ testInput: 'testValue' })}>Change Inputs</button>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/share/text-generation/result', () => ({
|
||||
default: ({
|
||||
isWorkflow,
|
||||
appId,
|
||||
onCompleted,
|
||||
onRunStart,
|
||||
}: { isWorkflow: boolean, appId: string, onCompleted: () => void, onRunStart: () => void }) => (
|
||||
<div data-testid="result-component" data-is-workflow={isWorkflow} data-app-id={appId}>
|
||||
<button data-testid="complete-button" onClick={onCompleted}>Complete</button>
|
||||
<button data-testid="run-start-button" onClick={onRunStart}>Run Start</button>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
const createMockAppData = (overrides: Partial<AppData> = {}): AppData => ({
|
||||
app_id: 'test-app-id',
|
||||
site: {
|
||||
title: 'Test App Title',
|
||||
description: 'Test App Description',
|
||||
icon: '🚀',
|
||||
icon_type: 'emoji',
|
||||
icon_background: '#FFFFFF',
|
||||
icon_url: '',
|
||||
default_language: 'en',
|
||||
prompt_public: true,
|
||||
copyright: '',
|
||||
privacy_policy: '',
|
||||
custom_disclaimer: '',
|
||||
},
|
||||
custom_config: {
|
||||
remove_webapp_brand: false,
|
||||
},
|
||||
...overrides,
|
||||
} as AppData)
|
||||
|
||||
describe('TextGeneration', () => {
|
||||
beforeEach(() => {
|
||||
mockStoreAppParams = mockAppParams
|
||||
mockMediaType = 'pc'
|
||||
mockUseGetTryAppParams.mockReturnValue({
|
||||
data: mockAppParams,
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('loading state', () => {
|
||||
it('renders loading when appData is null', () => {
|
||||
render(
|
||||
<TextGeneration
|
||||
appId="test-app-id"
|
||||
appData={null}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByRole('status')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders loading when appParams is not available', () => {
|
||||
mockStoreAppParams = null
|
||||
mockUseGetTryAppParams.mockReturnValue({
|
||||
data: null,
|
||||
})
|
||||
|
||||
render(
|
||||
<TextGeneration
|
||||
appId="test-app-id"
|
||||
appData={createMockAppData()}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByRole('status')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('content rendering', () => {
|
||||
it('renders app title', async () => {
|
||||
const appData = createMockAppData()
|
||||
|
||||
render(
|
||||
<TextGeneration
|
||||
appId="test-app-id"
|
||||
appData={appData}
|
||||
/>,
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
// Multiple elements may have the title (header and RunOnce mock)
|
||||
const titles = screen.getAllByText('Test App Title')
|
||||
expect(titles.length).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
|
||||
it('renders app description when available', async () => {
|
||||
const appData = createMockAppData({
|
||||
site: {
|
||||
title: 'Test App',
|
||||
description: 'This is a description',
|
||||
icon: '🚀',
|
||||
icon_type: 'emoji',
|
||||
icon_background: '#FFFFFF',
|
||||
icon_url: '',
|
||||
default_language: 'en',
|
||||
prompt_public: true,
|
||||
copyright: '',
|
||||
privacy_policy: '',
|
||||
custom_disclaimer: '',
|
||||
},
|
||||
} as unknown as Partial<AppData>)
|
||||
|
||||
render(
|
||||
<TextGeneration
|
||||
appId="test-app-id"
|
||||
appData={appData}
|
||||
/>,
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('This is a description')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('renders RunOnce component', async () => {
|
||||
const appData = createMockAppData()
|
||||
|
||||
render(
|
||||
<TextGeneration
|
||||
appId="test-app-id"
|
||||
appData={appData}
|
||||
/>,
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('run-once')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('renders Result component', async () => {
|
||||
const appData = createMockAppData()
|
||||
|
||||
render(
|
||||
<TextGeneration
|
||||
appId="test-app-id"
|
||||
appData={appData}
|
||||
/>,
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('result-component')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('workflow mode', () => {
|
||||
it('passes isWorkflow=true to Result when isWorkflow prop is true', async () => {
|
||||
const appData = createMockAppData()
|
||||
|
||||
render(
|
||||
<TextGeneration
|
||||
appId="test-app-id"
|
||||
appData={appData}
|
||||
isWorkflow
|
||||
/>,
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
const resultComponent = screen.getByTestId('result-component')
|
||||
expect(resultComponent).toHaveAttribute('data-is-workflow', 'true')
|
||||
})
|
||||
})
|
||||
|
||||
it('passes isWorkflow=false to Result when isWorkflow prop is false', async () => {
|
||||
const appData = createMockAppData()
|
||||
|
||||
render(
|
||||
<TextGeneration
|
||||
appId="test-app-id"
|
||||
appData={appData}
|
||||
isWorkflow={false}
|
||||
/>,
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
const resultComponent = screen.getByTestId('result-component')
|
||||
expect(resultComponent).toHaveAttribute('data-is-workflow', 'false')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('send functionality', () => {
|
||||
it('triggers send when RunOnce sends', async () => {
|
||||
const appData = createMockAppData()
|
||||
|
||||
render(
|
||||
<TextGeneration
|
||||
appId="test-app-id"
|
||||
appData={appData}
|
||||
/>,
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('send-button')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByTestId('send-button'))
|
||||
|
||||
// The send should work without errors
|
||||
expect(screen.getByTestId('result-component')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('completion handling', () => {
|
||||
it('shows alert after completion', async () => {
|
||||
const appData = createMockAppData()
|
||||
|
||||
render(
|
||||
<TextGeneration
|
||||
appId="test-app-id"
|
||||
appData={appData}
|
||||
/>,
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('complete-button')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByTestId('complete-button'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('This is a try app notice')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('className prop', () => {
|
||||
it('applies custom className', async () => {
|
||||
const appData = createMockAppData()
|
||||
|
||||
const { container } = render(
|
||||
<TextGeneration
|
||||
appId="test-app-id"
|
||||
appData={appData}
|
||||
className="custom-class"
|
||||
/>,
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
const element = container.querySelector('.custom-class')
|
||||
expect(element).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('hook effects', () => {
|
||||
it('calls updateAppInfo when appData changes', async () => {
|
||||
const appData = createMockAppData()
|
||||
|
||||
render(
|
||||
<TextGeneration
|
||||
appId="test-app-id"
|
||||
appData={appData}
|
||||
/>,
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockUpdateAppInfo).toHaveBeenCalledWith(appData)
|
||||
})
|
||||
})
|
||||
|
||||
it('calls updateAppParams when tryAppParams changes', async () => {
|
||||
const appData = createMockAppData()
|
||||
|
||||
render(
|
||||
<TextGeneration
|
||||
appId="test-app-id"
|
||||
appData={appData}
|
||||
/>,
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockUpdateAppParams).toHaveBeenCalledWith(mockAppParams)
|
||||
})
|
||||
})
|
||||
|
||||
it('calls useGetTryAppParams with correct appId', () => {
|
||||
const appData = createMockAppData()
|
||||
|
||||
render(
|
||||
<TextGeneration
|
||||
appId="my-app-id"
|
||||
appData={appData}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(mockUseGetTryAppParams).toHaveBeenCalledWith('my-app-id')
|
||||
})
|
||||
})
|
||||
|
||||
describe('result panel visibility', () => {
|
||||
it('shows result panel after run starts', async () => {
|
||||
const appData = createMockAppData()
|
||||
|
||||
render(
|
||||
<TextGeneration
|
||||
appId="test-app-id"
|
||||
appData={appData}
|
||||
/>,
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('run-start-button')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByTestId('run-start-button'))
|
||||
|
||||
// Result panel should remain visible
|
||||
expect(screen.getByTestId('result-component')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('input handling', () => {
|
||||
it('handles input changes from RunOnce', async () => {
|
||||
const appData = createMockAppData()
|
||||
|
||||
render(
|
||||
<TextGeneration
|
||||
appId="test-app-id"
|
||||
appData={appData}
|
||||
/>,
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('inputs-change-button')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
// Trigger input change which should call setInputs callback
|
||||
fireEvent.click(screen.getByTestId('inputs-change-button'))
|
||||
|
||||
// The component should handle the input change without errors
|
||||
expect(screen.getByTestId('run-once')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('mobile behavior', () => {
|
||||
it('renders mobile toggle panel on mobile', async () => {
|
||||
mockMediaType = 'mobile'
|
||||
const appData = createMockAppData()
|
||||
|
||||
const { container } = render(
|
||||
<TextGeneration
|
||||
appId="test-app-id"
|
||||
appData={appData}
|
||||
/>,
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
// Mobile toggle panel should be rendered
|
||||
const togglePanel = container.querySelector('.cursor-grab')
|
||||
expect(togglePanel).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('toggles result panel visibility on mobile', async () => {
|
||||
mockMediaType = 'mobile'
|
||||
const appData = createMockAppData()
|
||||
|
||||
const { container } = render(
|
||||
<TextGeneration
|
||||
appId="test-app-id"
|
||||
appData={appData}
|
||||
/>,
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
const togglePanel = container.querySelector('.cursor-grab')
|
||||
expect(togglePanel).toBeInTheDocument()
|
||||
})
|
||||
|
||||
// Click to show result panel
|
||||
const toggleParent = container.querySelector('.cursor-grab')?.parentElement
|
||||
if (toggleParent) {
|
||||
fireEvent.click(toggleParent)
|
||||
}
|
||||
|
||||
// Click again to hide result panel
|
||||
await waitFor(() => {
|
||||
const newToggleParent = container.querySelector('.cursor-grab')?.parentElement
|
||||
if (newToggleParent) {
|
||||
fireEvent.click(newToggleParent)
|
||||
}
|
||||
})
|
||||
|
||||
// Component should handle both show and hide without errors
|
||||
expect(screen.getByTestId('result-component')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
411
web/app/components/explore/try-app/index.spec.tsx
Normal file
411
web/app/components/explore/try-app/index.spec.tsx
Normal file
@ -0,0 +1,411 @@
|
||||
import type { TryAppInfo } from '@/service/try-app'
|
||||
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import TryApp from './index'
|
||||
import { TypeEnum } from './tab'
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => {
|
||||
const translations: Record<string, string> = {
|
||||
'tryApp.tabHeader.try': 'Try',
|
||||
'tryApp.tabHeader.detail': 'Detail',
|
||||
}
|
||||
return translations[key] || key
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
const mockUseGetTryAppInfo = vi.fn()
|
||||
|
||||
vi.mock('@/service/use-try-app', () => ({
|
||||
useGetTryAppInfo: (...args: unknown[]) => mockUseGetTryAppInfo(...args),
|
||||
}))
|
||||
|
||||
vi.mock('./app', () => ({
|
||||
default: ({ appId, appDetail }: { appId: string, appDetail: TryAppInfo }) => (
|
||||
<div data-testid="app-component" data-app-id={appId} data-mode={appDetail?.mode}>
|
||||
App Component
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('./preview', () => ({
|
||||
default: ({ appId, appDetail }: { appId: string, appDetail: TryAppInfo }) => (
|
||||
<div data-testid="preview-component" data-app-id={appId} data-mode={appDetail?.mode}>
|
||||
Preview Component
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('./app-info', () => ({
|
||||
default: ({
|
||||
appId,
|
||||
appDetail,
|
||||
category,
|
||||
className,
|
||||
onCreate,
|
||||
}: { appId: string, appDetail: TryAppInfo, category?: string, className?: string, onCreate: () => void }) => (
|
||||
<div
|
||||
data-testid="app-info-component"
|
||||
data-app-id={appId}
|
||||
data-category={category}
|
||||
className={className}
|
||||
>
|
||||
<button data-testid="create-button" onClick={onCreate}>Create</button>
|
||||
App Info:
|
||||
{' '}
|
||||
{appDetail?.name}
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
const createMockAppDetail = (mode: string = 'chat'): TryAppInfo => ({
|
||||
id: 'test-app-id',
|
||||
name: 'Test App Name',
|
||||
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',
|
||||
},
|
||||
dataset_configs: {
|
||||
datasets: {
|
||||
datasets: [],
|
||||
},
|
||||
},
|
||||
agent_mode: {
|
||||
tools: [],
|
||||
},
|
||||
user_input_form: [],
|
||||
},
|
||||
} as unknown as TryAppInfo)
|
||||
|
||||
describe('TryApp (main index.tsx)', () => {
|
||||
beforeEach(() => {
|
||||
mockUseGetTryAppInfo.mockReturnValue({
|
||||
data: createMockAppDetail(),
|
||||
isLoading: false,
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('loading state', () => {
|
||||
it('renders loading when isLoading is true', () => {
|
||||
mockUseGetTryAppInfo.mockReturnValue({
|
||||
data: null,
|
||||
isLoading: true,
|
||||
})
|
||||
|
||||
render(
|
||||
<TryApp
|
||||
appId="test-app-id"
|
||||
onClose={vi.fn()}
|
||||
onCreate={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(document.body.querySelector('[role="status"]')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('content rendering', () => {
|
||||
it('renders Tab component', async () => {
|
||||
render(
|
||||
<TryApp
|
||||
appId="test-app-id"
|
||||
onClose={vi.fn()}
|
||||
onCreate={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Try')).toBeInTheDocument()
|
||||
expect(screen.getByText('Detail')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('renders App component by default (TRY mode)', async () => {
|
||||
render(
|
||||
<TryApp
|
||||
appId="test-app-id"
|
||||
onClose={vi.fn()}
|
||||
onCreate={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(document.body.querySelector('[data-testid="app-component"]')).toBeInTheDocument()
|
||||
expect(document.body.querySelector('[data-testid="preview-component"]')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('renders AppInfo component', async () => {
|
||||
render(
|
||||
<TryApp
|
||||
appId="test-app-id"
|
||||
onClose={vi.fn()}
|
||||
onCreate={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(document.body.querySelector('[data-testid="app-info-component"]')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('renders close button', async () => {
|
||||
render(
|
||||
<TryApp
|
||||
appId="test-app-id"
|
||||
onClose={vi.fn()}
|
||||
onCreate={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
// Find the close button (the one with RiCloseLine icon)
|
||||
const buttons = document.body.querySelectorAll('button')
|
||||
expect(buttons.length).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('tab switching', () => {
|
||||
it('switches to Preview when Detail tab is clicked', async () => {
|
||||
render(
|
||||
<TryApp
|
||||
appId="test-app-id"
|
||||
onClose={vi.fn()}
|
||||
onCreate={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Detail')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByText('Detail'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(document.body.querySelector('[data-testid="preview-component"]')).toBeInTheDocument()
|
||||
expect(document.body.querySelector('[data-testid="app-component"]')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('switches back to App when Try tab is clicked', async () => {
|
||||
render(
|
||||
<TryApp
|
||||
appId="test-app-id"
|
||||
onClose={vi.fn()}
|
||||
onCreate={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Detail')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
// First switch to Detail
|
||||
fireEvent.click(screen.getByText('Detail'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(document.body.querySelector('[data-testid="preview-component"]')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
// Then switch back to Try
|
||||
fireEvent.click(screen.getByText('Try'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(document.body.querySelector('[data-testid="app-component"]')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('close functionality', () => {
|
||||
it('calls onClose when close button is clicked', async () => {
|
||||
const mockOnClose = vi.fn()
|
||||
|
||||
render(
|
||||
<TryApp
|
||||
appId="test-app-id"
|
||||
onClose={mockOnClose}
|
||||
onCreate={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
// Find the button with close icon
|
||||
const buttons = document.body.querySelectorAll('button')
|
||||
const closeButton = Array.from(buttons).find(btn =>
|
||||
btn.querySelector('svg') || btn.className.includes('rounded-[10px]'),
|
||||
)
|
||||
expect(closeButton).toBeInTheDocument()
|
||||
|
||||
if (closeButton)
|
||||
fireEvent.click(closeButton)
|
||||
})
|
||||
|
||||
expect(mockOnClose).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('create functionality', () => {
|
||||
it('calls onCreate when create button in AppInfo is clicked', async () => {
|
||||
const mockOnCreate = vi.fn()
|
||||
|
||||
render(
|
||||
<TryApp
|
||||
appId="test-app-id"
|
||||
onClose={vi.fn()}
|
||||
onCreate={mockOnCreate}
|
||||
/>,
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
const createButton = document.body.querySelector('[data-testid="create-button"]')
|
||||
expect(createButton).toBeInTheDocument()
|
||||
|
||||
if (createButton)
|
||||
fireEvent.click(createButton)
|
||||
})
|
||||
|
||||
expect(mockOnCreate).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('category prop', () => {
|
||||
it('passes category to AppInfo when provided', async () => {
|
||||
render(
|
||||
<TryApp
|
||||
appId="test-app-id"
|
||||
category="AI Assistant"
|
||||
onClose={vi.fn()}
|
||||
onCreate={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
const appInfo = document.body.querySelector('[data-testid="app-info-component"]')
|
||||
expect(appInfo).toHaveAttribute('data-category', 'AI Assistant')
|
||||
})
|
||||
})
|
||||
|
||||
it('does not pass category to AppInfo when not provided', async () => {
|
||||
render(
|
||||
<TryApp
|
||||
appId="test-app-id"
|
||||
onClose={vi.fn()}
|
||||
onCreate={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
const appInfo = document.body.querySelector('[data-testid="app-info-component"]')
|
||||
expect(appInfo).not.toHaveAttribute('data-category', expect.any(String))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('hook calls', () => {
|
||||
it('calls useGetTryAppInfo with correct appId', () => {
|
||||
render(
|
||||
<TryApp
|
||||
appId="my-specific-app-id"
|
||||
onClose={vi.fn()}
|
||||
onCreate={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(mockUseGetTryAppInfo).toHaveBeenCalledWith('my-specific-app-id')
|
||||
})
|
||||
})
|
||||
|
||||
describe('props passing', () => {
|
||||
it('passes appId to App component', async () => {
|
||||
render(
|
||||
<TryApp
|
||||
appId="my-app-id"
|
||||
onClose={vi.fn()}
|
||||
onCreate={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
const appComponent = document.body.querySelector('[data-testid="app-component"]')
|
||||
expect(appComponent).toHaveAttribute('data-app-id', 'my-app-id')
|
||||
})
|
||||
})
|
||||
|
||||
it('passes appId to Preview component when in Detail mode', async () => {
|
||||
render(
|
||||
<TryApp
|
||||
appId="my-app-id"
|
||||
onClose={vi.fn()}
|
||||
onCreate={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Detail')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByText('Detail'))
|
||||
|
||||
await waitFor(() => {
|
||||
const previewComponent = document.body.querySelector('[data-testid="preview-component"]')
|
||||
expect(previewComponent).toHaveAttribute('data-app-id', 'my-app-id')
|
||||
})
|
||||
})
|
||||
|
||||
it('passes appId to AppInfo component', async () => {
|
||||
render(
|
||||
<TryApp
|
||||
appId="my-app-id"
|
||||
onClose={vi.fn()}
|
||||
onCreate={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
const appInfoComponent = document.body.querySelector('[data-testid="app-info-component"]')
|
||||
expect(appInfoComponent).toHaveAttribute('data-app-id', 'my-app-id')
|
||||
})
|
||||
})
|
||||
|
||||
it('passes appDetail to AppInfo component', async () => {
|
||||
render(
|
||||
<TryApp
|
||||
appId="test-app-id"
|
||||
onClose={vi.fn()}
|
||||
onCreate={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
const appInfoComponent = document.body.querySelector('[data-testid="app-info-component"]')
|
||||
expect(appInfoComponent?.textContent).toContain('Test App Name')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('TypeEnum export', () => {
|
||||
it('exports TypeEnum correctly', () => {
|
||||
expect(TypeEnum.TRY).toBe('try')
|
||||
expect(TypeEnum.DETAIL).toBe('detail')
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,527 @@
|
||||
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()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,179 @@
|
||||
import { cleanup, render, screen } from '@testing-library/react'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import FlowAppPreview from './flow-app-preview'
|
||||
|
||||
const mockUseGetTryAppFlowPreview = vi.fn()
|
||||
|
||||
vi.mock('@/service/use-try-app', () => ({
|
||||
useGetTryAppFlowPreview: (...args: unknown[]) => mockUseGetTryAppFlowPreview(...args),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/workflow-preview', () => ({
|
||||
default: ({
|
||||
className,
|
||||
miniMapToRight,
|
||||
nodes,
|
||||
edges,
|
||||
}: { className?: string, miniMapToRight?: boolean, nodes?: unknown[], edges?: unknown[] }) => (
|
||||
<div
|
||||
data-testid="workflow-preview"
|
||||
className={className}
|
||||
data-mini-map-to-right={miniMapToRight}
|
||||
data-nodes-count={nodes?.length}
|
||||
data-edges-count={edges?.length}
|
||||
>
|
||||
WorkflowPreview
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
describe('FlowAppPreview', () => {
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('loading state', () => {
|
||||
it('renders Loading component when isLoading is true', () => {
|
||||
mockUseGetTryAppFlowPreview.mockReturnValue({
|
||||
data: null,
|
||||
isLoading: true,
|
||||
})
|
||||
|
||||
render(<FlowAppPreview appId="test-app-id" />)
|
||||
|
||||
expect(screen.getByRole('status')).toBeInTheDocument()
|
||||
expect(screen.queryByTestId('workflow-preview')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('no data state', () => {
|
||||
it('returns null when data is null', () => {
|
||||
mockUseGetTryAppFlowPreview.mockReturnValue({
|
||||
data: null,
|
||||
isLoading: false,
|
||||
})
|
||||
|
||||
const { container } = render(<FlowAppPreview appId="test-app-id" />)
|
||||
|
||||
expect(container.firstChild).toBeNull()
|
||||
})
|
||||
|
||||
it('returns null when data is undefined', () => {
|
||||
mockUseGetTryAppFlowPreview.mockReturnValue({
|
||||
data: undefined,
|
||||
isLoading: false,
|
||||
})
|
||||
|
||||
const { container } = render(<FlowAppPreview appId="test-app-id" />)
|
||||
|
||||
expect(container.firstChild).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('data loaded state', () => {
|
||||
it('renders WorkflowPreview when data is loaded', () => {
|
||||
mockUseGetTryAppFlowPreview.mockReturnValue({
|
||||
data: {
|
||||
graph: {
|
||||
nodes: [{ id: 'node1' }],
|
||||
edges: [{ id: 'edge1' }],
|
||||
},
|
||||
},
|
||||
isLoading: false,
|
||||
})
|
||||
|
||||
render(<FlowAppPreview appId="test-app-id" />)
|
||||
|
||||
expect(screen.getByTestId('workflow-preview')).toBeInTheDocument()
|
||||
expect(screen.queryByRole('status')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('passes graph data to WorkflowPreview', () => {
|
||||
const mockNodes = [{ id: 'node1' }, { id: 'node2' }, { id: 'node3' }]
|
||||
const mockEdges = [{ id: 'edge1' }, { id: 'edge2' }]
|
||||
|
||||
mockUseGetTryAppFlowPreview.mockReturnValue({
|
||||
data: {
|
||||
graph: {
|
||||
nodes: mockNodes,
|
||||
edges: mockEdges,
|
||||
},
|
||||
},
|
||||
isLoading: false,
|
||||
})
|
||||
|
||||
render(<FlowAppPreview appId="test-app-id" />)
|
||||
|
||||
const workflowPreview = screen.getByTestId('workflow-preview')
|
||||
expect(workflowPreview).toHaveAttribute('data-nodes-count', '3')
|
||||
expect(workflowPreview).toHaveAttribute('data-edges-count', '2')
|
||||
})
|
||||
|
||||
it('passes miniMapToRight=true to WorkflowPreview', () => {
|
||||
mockUseGetTryAppFlowPreview.mockReturnValue({
|
||||
data: {
|
||||
graph: {
|
||||
nodes: [],
|
||||
edges: [],
|
||||
},
|
||||
},
|
||||
isLoading: false,
|
||||
})
|
||||
|
||||
render(<FlowAppPreview appId="test-app-id" />)
|
||||
|
||||
const workflowPreview = screen.getByTestId('workflow-preview')
|
||||
expect(workflowPreview).toHaveAttribute('data-mini-map-to-right', 'true')
|
||||
})
|
||||
|
||||
it('passes className to WorkflowPreview', () => {
|
||||
mockUseGetTryAppFlowPreview.mockReturnValue({
|
||||
data: {
|
||||
graph: {
|
||||
nodes: [],
|
||||
edges: [],
|
||||
},
|
||||
},
|
||||
isLoading: false,
|
||||
})
|
||||
|
||||
render(<FlowAppPreview appId="test-app-id" className="custom-class" />)
|
||||
|
||||
const workflowPreview = screen.getByTestId('workflow-preview')
|
||||
expect(workflowPreview).toHaveClass('custom-class')
|
||||
})
|
||||
})
|
||||
|
||||
describe('hook calls', () => {
|
||||
it('calls useGetTryAppFlowPreview with correct appId', () => {
|
||||
mockUseGetTryAppFlowPreview.mockReturnValue({
|
||||
data: null,
|
||||
isLoading: true,
|
||||
})
|
||||
|
||||
render(<FlowAppPreview appId="my-specific-app-id" />)
|
||||
|
||||
expect(mockUseGetTryAppFlowPreview).toHaveBeenCalledWith('my-specific-app-id')
|
||||
})
|
||||
})
|
||||
|
||||
describe('wrapper styling', () => {
|
||||
it('renders with correct wrapper classes when data is loaded', () => {
|
||||
mockUseGetTryAppFlowPreview.mockReturnValue({
|
||||
data: {
|
||||
graph: {
|
||||
nodes: [],
|
||||
edges: [],
|
||||
},
|
||||
},
|
||||
isLoading: false,
|
||||
})
|
||||
|
||||
const { container } = render(<FlowAppPreview appId="test-app-id" />)
|
||||
|
||||
const wrapper = container.firstChild as HTMLElement
|
||||
expect(wrapper).toHaveClass('h-full', 'w-full')
|
||||
})
|
||||
})
|
||||
})
|
||||
127
web/app/components/explore/try-app/preview/index.spec.tsx
Normal file
127
web/app/components/explore/try-app/preview/index.spec.tsx
Normal file
@ -0,0 +1,127 @@
|
||||
import type { TryAppInfo } from '@/service/try-app'
|
||||
import { cleanup, render, screen } from '@testing-library/react'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import Preview from './index'
|
||||
|
||||
vi.mock('./basic-app-preview', () => ({
|
||||
default: ({ appId }: { appId: string }) => (
|
||||
<div data-testid="basic-app-preview" data-app-id={appId}>
|
||||
BasicAppPreview
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('./flow-app-preview', () => ({
|
||||
default: ({ appId, className }: { appId: string, className?: string }) => (
|
||||
<div data-testid="flow-app-preview" data-app-id={appId} className={className}>
|
||||
FlowAppPreview
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
const createMockAppDetail = (mode: string): TryAppInfo => ({
|
||||
id: 'test-app-id',
|
||||
name: 'Test App',
|
||||
description: 'Test Description',
|
||||
mode,
|
||||
site: {
|
||||
title: 'Test Site Title',
|
||||
icon: 'icon',
|
||||
icon_type: 'emoji',
|
||||
icon_background: '#FFFFFF',
|
||||
icon_url: '',
|
||||
},
|
||||
model_config: {
|
||||
model: {
|
||||
provider: 'test/provider',
|
||||
name: 'test-model',
|
||||
mode: 'chat',
|
||||
},
|
||||
dataset_configs: {
|
||||
datasets: {
|
||||
datasets: [],
|
||||
},
|
||||
},
|
||||
agent_mode: {
|
||||
tools: [],
|
||||
},
|
||||
user_input_form: [],
|
||||
},
|
||||
} as unknown as TryAppInfo)
|
||||
|
||||
describe('Preview', () => {
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
})
|
||||
|
||||
describe('basic app rendering', () => {
|
||||
it('renders BasicAppPreview for agent-chat mode', () => {
|
||||
const appDetail = createMockAppDetail('agent-chat')
|
||||
render(<Preview appId="test-app-id" appDetail={appDetail} />)
|
||||
|
||||
expect(screen.getByTestId('basic-app-preview')).toBeInTheDocument()
|
||||
expect(screen.queryByTestId('flow-app-preview')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders BasicAppPreview for chat mode', () => {
|
||||
const appDetail = createMockAppDetail('chat')
|
||||
render(<Preview appId="test-app-id" appDetail={appDetail} />)
|
||||
|
||||
expect(screen.getByTestId('basic-app-preview')).toBeInTheDocument()
|
||||
expect(screen.queryByTestId('flow-app-preview')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders BasicAppPreview for completion mode', () => {
|
||||
const appDetail = createMockAppDetail('completion')
|
||||
render(<Preview appId="test-app-id" appDetail={appDetail} />)
|
||||
|
||||
expect(screen.getByTestId('basic-app-preview')).toBeInTheDocument()
|
||||
expect(screen.queryByTestId('flow-app-preview')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('passes appId to BasicAppPreview', () => {
|
||||
const appDetail = createMockAppDetail('chat')
|
||||
render(<Preview appId="my-app-id" appDetail={appDetail} />)
|
||||
|
||||
const basicPreview = screen.getByTestId('basic-app-preview')
|
||||
expect(basicPreview).toHaveAttribute('data-app-id', 'my-app-id')
|
||||
})
|
||||
})
|
||||
|
||||
describe('flow app rendering', () => {
|
||||
it('renders FlowAppPreview for workflow mode', () => {
|
||||
const appDetail = createMockAppDetail('workflow')
|
||||
render(<Preview appId="test-app-id" appDetail={appDetail} />)
|
||||
|
||||
expect(screen.getByTestId('flow-app-preview')).toBeInTheDocument()
|
||||
expect(screen.queryByTestId('basic-app-preview')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders FlowAppPreview for advanced-chat mode', () => {
|
||||
const appDetail = createMockAppDetail('advanced-chat')
|
||||
render(<Preview appId="test-app-id" appDetail={appDetail} />)
|
||||
|
||||
expect(screen.getByTestId('flow-app-preview')).toBeInTheDocument()
|
||||
expect(screen.queryByTestId('basic-app-preview')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('passes appId and className to FlowAppPreview', () => {
|
||||
const appDetail = createMockAppDetail('workflow')
|
||||
render(<Preview appId="my-flow-app-id" appDetail={appDetail} />)
|
||||
|
||||
const flowPreview = screen.getByTestId('flow-app-preview')
|
||||
expect(flowPreview).toHaveAttribute('data-app-id', 'my-flow-app-id')
|
||||
expect(flowPreview).toHaveClass('h-full')
|
||||
})
|
||||
})
|
||||
|
||||
describe('wrapper styling', () => {
|
||||
it('renders with correct wrapper classes', () => {
|
||||
const appDetail = createMockAppDetail('chat')
|
||||
const { container } = render(<Preview appId="test-app-id" appDetail={appDetail} />)
|
||||
|
||||
const wrapper = container.firstChild as HTMLElement
|
||||
expect(wrapper).toHaveClass('h-full', 'w-full')
|
||||
})
|
||||
})
|
||||
})
|
||||
58
web/app/components/explore/try-app/tab.spec.tsx
Normal file
58
web/app/components/explore/try-app/tab.spec.tsx
Normal file
@ -0,0 +1,58 @@
|
||||
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import Tab, { TypeEnum } from './tab'
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => {
|
||||
const translations: Record<string, string> = {
|
||||
'tryApp.tabHeader.try': 'Try',
|
||||
'tryApp.tabHeader.detail': 'Detail',
|
||||
}
|
||||
return translations[key] || key
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
describe('Tab', () => {
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
})
|
||||
|
||||
it('renders tab with TRY value selected', () => {
|
||||
const mockOnChange = vi.fn()
|
||||
render(<Tab value={TypeEnum.TRY} onChange={mockOnChange} />)
|
||||
|
||||
expect(screen.getByText('Try')).toBeInTheDocument()
|
||||
expect(screen.getByText('Detail')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders tab with DETAIL value selected', () => {
|
||||
const mockOnChange = vi.fn()
|
||||
render(<Tab value={TypeEnum.DETAIL} onChange={mockOnChange} />)
|
||||
|
||||
expect(screen.getByText('Try')).toBeInTheDocument()
|
||||
expect(screen.getByText('Detail')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('calls onChange when clicking a tab', () => {
|
||||
const mockOnChange = vi.fn()
|
||||
render(<Tab value={TypeEnum.TRY} onChange={mockOnChange} />)
|
||||
|
||||
fireEvent.click(screen.getByText('Detail'))
|
||||
expect(mockOnChange).toHaveBeenCalledWith(TypeEnum.DETAIL)
|
||||
})
|
||||
|
||||
it('calls onChange when clicking Try tab', () => {
|
||||
const mockOnChange = vi.fn()
|
||||
render(<Tab value={TypeEnum.DETAIL} onChange={mockOnChange} />)
|
||||
|
||||
fireEvent.click(screen.getByText('Try'))
|
||||
expect(mockOnChange).toHaveBeenCalledWith(TypeEnum.TRY)
|
||||
})
|
||||
|
||||
it('exports TypeEnum correctly', () => {
|
||||
expect(TypeEnum.TRY).toBe('try')
|
||||
expect(TypeEnum.DETAIL).toBe('detail')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user