dify/web/app/components/explore/try-app/app/chat.spec.tsx
Coding On Star 8f414af34e
test: add comprehensive tests (#31649)
Co-authored-by: CodingOnStar <hanxujiang@dify.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-29 11:16:26 +08:00

358 lines
9.4 KiB
TypeScript

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')
})
})
})