dify/web/app/components/base/chat/chat-with-history/sidebar/index.spec.tsx

242 lines
7.8 KiB
TypeScript

import type { ChatWithHistoryContextValue } from '../context'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import * as React from 'react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { useChatWithHistoryContext } from '../context'
import Sidebar from './index'
// Mock List to allow us to trigger operations
vi.mock('./list', () => ({
default: ({ list, onOperate, title }: { list: Array<{ id: string, name: string }>, onOperate: (type: string, item: { id: string, name: string }) => void, title?: string }) => (
<div>
{title && <div>{title}</div>}
{list.map(item => (
<div key={item.id}>
<div>{item.name}</div>
<button onClick={() => onOperate('pin', item)}>Pin</button>
<button onClick={() => onOperate('unpin', item)}>Unpin</button>
<button onClick={() => onOperate('delete', item)}>Delete</button>
<button onClick={() => onOperate('rename', item)}>Rename</button>
</div>
))}
</div>
),
}))
// Mock context hook
vi.mock('../context', () => ({
useChatWithHistoryContext: vi.fn(),
}))
// Mock global public store
vi.mock('@/context/global-public-context', () => ({
useGlobalPublicStore: vi.fn(selector => selector({
systemFeatures: {
branding: {
enabled: true,
},
},
})),
}))
// Mock next/navigation
vi.mock('next/navigation', () => ({
useRouter: () => ({ push: vi.fn() }),
usePathname: () => '/test',
}))
// Mock Modal to avoid Headless UI issues in tests
vi.mock('@/app/components/base/modal', () => ({
default: ({ children, isShow, title }: { children: React.ReactNode, isShow: boolean, title: React.ReactNode }) => {
if (!isShow)
return null
return (
<div data-testid="modal">
{!!title && <div>{title}</div>}
{children}
</div>
)
},
}))
describe('Sidebar Index', () => {
const mockContextValue = {
isInstalledApp: false,
appData: {
site: {
title: 'Test App',
icon_type: 'image',
},
custom_config: {},
},
handleNewConversation: vi.fn(),
pinnedConversationList: [],
conversationList: [
{ id: '1', name: 'Conv 1', inputs: {}, introduction: '' },
],
currentConversationId: '0',
handleChangeConversation: vi.fn(),
handlePinConversation: vi.fn(),
handleUnpinConversation: vi.fn(),
conversationRenaming: false,
handleRenameConversation: vi.fn(),
handleDeleteConversation: vi.fn(),
sidebarCollapseState: false,
handleSidebarCollapse: vi.fn(),
isMobile: false,
isResponding: false,
} as unknown as ChatWithHistoryContextValue
beforeEach(() => {
vi.clearAllMocks()
vi.mocked(useChatWithHistoryContext).mockReturnValue(mockContextValue)
})
it('should render app title', () => {
render(<Sidebar />)
expect(screen.getByText('Test App')).toBeInTheDocument()
})
it('should call handleNewConversation when button clicked', async () => {
const user = userEvent.setup()
render(<Sidebar />)
await user.click(screen.getByText('share.chat.newChat'))
expect(mockContextValue.handleNewConversation).toHaveBeenCalled()
})
it('should call handleSidebarCollapse when collapse button clicked', async () => {
const user = userEvent.setup()
render(<Sidebar />)
// Find the collapse button - it's the first ActionButton
const collapseButton = screen.getAllByRole('button')[0]
await user.click(collapseButton)
expect(mockContextValue.handleSidebarCollapse).toHaveBeenCalledWith(true)
})
it('should render conversation lists', () => {
vi.mocked(useChatWithHistoryContext).mockReturnValue({
...mockContextValue,
pinnedConversationList: [{ id: 'p1', name: 'Pinned 1', inputs: {}, introduction: '' }],
} as unknown as ChatWithHistoryContextValue)
render(<Sidebar />)
expect(screen.getByText('share.chat.pinnedTitle')).toBeInTheDocument()
expect(screen.getByText('Pinned 1')).toBeInTheDocument()
expect(screen.getByText('share.chat.unpinnedTitle')).toBeInTheDocument()
expect(screen.getByText('Conv 1')).toBeInTheDocument()
})
it('should render expand button when sidebar is collapsed', () => {
vi.mocked(useChatWithHistoryContext).mockReturnValue({
...mockContextValue,
sidebarCollapseState: true,
} as unknown as ChatWithHistoryContextValue)
render(<Sidebar />)
const buttons = screen.getAllByRole('button')
expect(buttons.length).toBeGreaterThan(0)
})
it('should call handleSidebarCollapse with false when expand button clicked', async () => {
const user = userEvent.setup()
vi.mocked(useChatWithHistoryContext).mockReturnValue({
...mockContextValue,
sidebarCollapseState: true,
} as unknown as ChatWithHistoryContextValue)
render(<Sidebar />)
const expandButton = screen.getAllByRole('button')[0]
await user.click(expandButton)
expect(mockContextValue.handleSidebarCollapse).toHaveBeenCalledWith(false)
})
it('should call handlePinConversation when pin operation is triggered', async () => {
const user = userEvent.setup()
render(<Sidebar />)
const pinButton = screen.getByText('Pin')
await user.click(pinButton)
expect(mockContextValue.handlePinConversation).toHaveBeenCalledWith('1')
})
it('should call handleUnpinConversation when unpin operation is triggered', async () => {
const user = userEvent.setup()
render(<Sidebar />)
const unpinButton = screen.getByText('Unpin')
await user.click(unpinButton)
expect(mockContextValue.handleUnpinConversation).toHaveBeenCalledWith('1')
})
it('should show delete confirmation modal when delete operation is triggered', async () => {
const user = userEvent.setup()
render(<Sidebar />)
const deleteButton = screen.getByText('Delete')
await user.click(deleteButton)
expect(screen.getByText('share.chat.deleteConversation.title')).toBeInTheDocument()
const confirmButton = screen.getByText('common.operation.confirm')
await user.click(confirmButton)
expect(mockContextValue.handleDeleteConversation).toHaveBeenCalledWith('1', expect.any(Object))
})
it('should close delete confirmation modal when cancel is clicked', async () => {
const user = userEvent.setup()
render(<Sidebar />)
const deleteButton = screen.getByText('Delete')
await user.click(deleteButton)
expect(screen.getByText('share.chat.deleteConversation.title')).toBeInTheDocument()
const cancelButton = screen.getByText('common.operation.cancel')
await user.click(cancelButton)
expect(screen.queryByText('share.chat.deleteConversation.title')).not.toBeInTheDocument()
})
it('should show rename modal when rename operation is triggered', async () => {
const user = userEvent.setup()
render(<Sidebar />)
const renameButton = screen.getByText('Rename')
await user.click(renameButton)
expect(screen.getByText('common.chat.renameConversation')).toBeInTheDocument()
const input = screen.getByDisplayValue('Conv 1') as HTMLInputElement
await user.click(input)
await user.clear(input)
await user.type(input, 'Renamed Conv')
const saveButton = screen.getByText('common.operation.save')
await user.click(saveButton)
expect(mockContextValue.handleRenameConversation).toHaveBeenCalled()
})
it('should close rename modal when cancel is clicked', async () => {
const user = userEvent.setup()
render(<Sidebar />)
const renameButton = screen.getByText('Rename')
await user.click(renameButton)
expect(screen.getByText('common.chat.renameConversation')).toBeInTheDocument()
const cancelButton = screen.getByText('common.operation.cancel')
await user.click(cancelButton)
expect(screen.queryByText('common.chat.renameConversation')).not.toBeInTheDocument()
})
})