mirror of
https://github.com/eigent-ai/eigent.git
synced 2026-05-16 19:50:50 +00:00
479 lines
No EOL
16 KiB
TypeScript
479 lines
No EOL
16 KiB
TypeScript
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'
|
|
import { render, screen, waitFor, act, renderHook } from '@testing-library/react'
|
|
import userEvent from '@testing-library/user-event'
|
|
import { BrowserRouter } from 'react-router-dom'
|
|
import ChatBox from '../../../src/components/ChatBox/index'
|
|
import { generateUniqueId } from '../../../src/lib'
|
|
|
|
// Import proxy mock to enable API mocking
|
|
import '../../mocks/proxy.mock'
|
|
// Also Mock authStore & sse
|
|
import '../../mocks/authStore.mock'
|
|
import '../../mocks/sse.mock'
|
|
|
|
// Import chat store to ensure it's available
|
|
import '../../../src/store/chatStore'
|
|
|
|
import { useProjectStore } from '../../../src/store/projectStore'
|
|
import useChatStoreAdapter from '../../../src/hooks/useChatStoreAdapter'
|
|
import { mockFetchEventSource } from '../../mocks/sse.mock'
|
|
|
|
// Mock electron IPC
|
|
(global as any).ipcRenderer = {
|
|
invoke: vi.fn((channel) => {
|
|
if (channel === 'get-system-language') return Promise.resolve('en')
|
|
if (channel === 'get-browser-port') return Promise.resolve(9222)
|
|
if (channel === 'get-env-path') return Promise.resolve('/path/to/env')
|
|
if (channel === 'mcp-list') return Promise.resolve({})
|
|
if (channel === 'get-file-list') return Promise.resolve([])
|
|
return Promise.resolve()
|
|
}),
|
|
}
|
|
|
|
// Mock window.electronAPI
|
|
Object.defineProperty(window, 'electronAPI', {
|
|
value: {
|
|
uploadLog: vi.fn().mockResolvedValue(undefined),
|
|
// Add other electronAPI methods as needed
|
|
},
|
|
writable: true,
|
|
})
|
|
|
|
const TestWrapper = ({ children }: { children: React.ReactNode }) => (
|
|
<BrowserRouter>{children}</BrowserRouter>
|
|
)
|
|
|
|
describe('ChatBox Integration Tests - Different ChatStore Configurations', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
|
|
const { result } = renderHook(() => useProjectStore());
|
|
//Reset projectStore
|
|
result.current.getAllProjects().forEach(project => {
|
|
result.current.removeProject(project.id)
|
|
})
|
|
|
|
//Create initial Project
|
|
const projectId = result.current.createProject(
|
|
'ChatBox Test Project',
|
|
'Testing ChatBox UI functionality'
|
|
)
|
|
expect(projectId).toBeDefined()
|
|
|
|
// Get chatStore (automatically created)
|
|
let chatStore = result.current.getActiveChatStore(projectId)!
|
|
expect(chatStore).toBeDefined()
|
|
const initiatorTaskId = chatStore.getState().activeTaskId!
|
|
expect(initiatorTaskId).toBeDefined()
|
|
})
|
|
|
|
afterEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
describe('Task States and UI Rendering', () => {
|
|
it('should render welcome screen when no messages exist', () => {
|
|
const { result, rerender } = renderHook(() => useChatStoreAdapter())
|
|
|
|
render(
|
|
<TestWrapper>
|
|
<ChatBox />
|
|
</TestWrapper>
|
|
)
|
|
|
|
expect(screen.getByText(/layout.welcome-to-eigent/i)).toBeInTheDocument()
|
|
expect(screen.getByText(/layout.how-can-i-help-you/i)).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render task splitting UI when task is in to_sub_tasks state', async () => {
|
|
const { result, rerender } = renderHook(() => useChatStoreAdapter())
|
|
const { chatStore, projectStore } = result.current
|
|
const projectId = projectStore.activeProjectId as string
|
|
const taskId = chatStore?.activeTaskId
|
|
|
|
if (!chatStore || !taskId) {
|
|
throw new Error('ChatStore or taskId is null')
|
|
}
|
|
|
|
// Simulate the state after receiving to_sub_tasks SSE event
|
|
await act(async () => {
|
|
chatStore.setHasMessages(taskId, true)
|
|
chatStore.addMessages(taskId, {
|
|
id: 'user-msg-1',
|
|
role: 'user',
|
|
content: 'Build a calculator app',
|
|
attaches: []
|
|
})
|
|
chatStore.addMessages(taskId, {
|
|
id: 'assistant-msg-1',
|
|
role: 'assistant',
|
|
content: '',
|
|
step: 'to_sub_tasks',
|
|
data: {
|
|
summary_task: 'Calculator App|Build a simple calculator app',
|
|
sub_tasks: [
|
|
{ id: 'task-1', content: 'Create UI components', status: '' },
|
|
{ id: 'task-2', content: 'Implement calculator logic', status: '' }
|
|
]
|
|
}
|
|
})
|
|
rerender()
|
|
})
|
|
|
|
render(
|
|
<TestWrapper>
|
|
<ChatBox />
|
|
</TestWrapper>
|
|
)
|
|
|
|
// Should show the task card/splitting interface
|
|
await waitFor(() => {
|
|
// Look for elements that indicate task splitting UI - there might be multiple instances
|
|
const calculatorElements = screen.getAllByText('Build a calculator app')
|
|
expect(calculatorElements.length).toBeGreaterThanOrEqual(1)
|
|
// The component should show task breakdown
|
|
expect(screen.queryByText(/layout.welcome-to-eigent/i)).not.toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
it('should render active conversation with messages', async () => {
|
|
const { result, rerender } = renderHook(() => useChatStoreAdapter())
|
|
const { chatStore } = result.current
|
|
const taskId = chatStore?.activeTaskId
|
|
|
|
if (!chatStore || !taskId) {
|
|
throw new Error('ChatStore or taskId is null')
|
|
}
|
|
|
|
await act(async () => {
|
|
chatStore.setHasMessages(taskId, true)
|
|
chatStore.addMessages(taskId, {
|
|
id: 'user-1',
|
|
role: 'user',
|
|
content: 'Hello, how are you?',
|
|
attaches: []
|
|
})
|
|
chatStore.addMessages(taskId, {
|
|
id: 'assistant-1',
|
|
role: 'assistant',
|
|
content: 'I am doing well, thank you! layout.how-can-i-help-you?',
|
|
attaches: []
|
|
})
|
|
rerender()
|
|
})
|
|
|
|
render(
|
|
<TestWrapper>
|
|
<ChatBox />
|
|
</TestWrapper>
|
|
)
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Hello, how are you?')).toBeInTheDocument()
|
|
expect(screen.getByText('I am doing well, thank you! layout.how-can-i-help-you?')).toBeInTheDocument()
|
|
expect(screen.queryByText(/layout.welcome-to-eigent/i)).not.toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
it('should show loading state when task is pending', async () => {
|
|
const { result, rerender } = renderHook(() => useChatStoreAdapter())
|
|
const { chatStore } = result.current
|
|
const taskId = chatStore?.activeTaskId
|
|
|
|
if (!chatStore || !taskId) {
|
|
throw new Error('ChatStore or taskId is null')
|
|
}
|
|
|
|
await act(async () => {
|
|
chatStore.setHasMessages(taskId, true)
|
|
chatStore.addMessages(taskId, {
|
|
id: 'user-1',
|
|
role: 'user',
|
|
content: 'Calculate 2+2',
|
|
attaches: []
|
|
})
|
|
chatStore.setIsPending(taskId, true)
|
|
rerender()
|
|
})
|
|
|
|
render(
|
|
<TestWrapper>
|
|
<ChatBox />
|
|
</TestWrapper>
|
|
)
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Calculate 2+2')).toBeInTheDocument()
|
|
// Should show some loading indicator - adjust this based on actual UI
|
|
// For now, just check that we don't show the welcome screen
|
|
expect(screen.queryByText(/layout.welcome-to-eigent/i)).not.toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
it.skip('should render file attachments when message has fileList', async () => {
|
|
// This test requires understanding the exact structure of fileList handling
|
|
// Skipping for now until we understand the component's file attachment UI
|
|
const { result, rerender } = renderHook(() => useChatStoreAdapter())
|
|
const { chatStore } = result.current
|
|
const taskId = chatStore?.activeTaskId
|
|
|
|
if (!chatStore || !taskId) {
|
|
throw new Error('ChatStore or taskId is null')
|
|
}
|
|
|
|
await act(async () => {
|
|
chatStore.setHasMessages(taskId, true)
|
|
chatStore.addMessages(taskId, {
|
|
id: 'user-1',
|
|
role: 'user',
|
|
content: 'Generate a report',
|
|
attaches: []
|
|
})
|
|
chatStore.addMessages(taskId, {
|
|
id: 'assistant-1',
|
|
role: 'assistant',
|
|
content: 'I have generated the report for you.',
|
|
step: 'end',
|
|
fileList: [
|
|
{
|
|
name: 'report.pdf',
|
|
type: 'PDF',
|
|
path: '/tmp/report.pdf'
|
|
},
|
|
{
|
|
name: 'data.csv',
|
|
type: 'CSV',
|
|
path: '/tmp/data.csv'
|
|
}
|
|
]
|
|
})
|
|
rerender()
|
|
})
|
|
|
|
render(
|
|
<TestWrapper>
|
|
<ChatBox />
|
|
</TestWrapper>
|
|
)
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Generate a report')).toBeInTheDocument()
|
|
expect(screen.getByText('I have generated the report for you.')).toBeInTheDocument()
|
|
// These might be rendered differently in the actual component
|
|
// expect(screen.getByText(/report\.pdf/i)).toBeInTheDocument()
|
|
// expect(screen.getByText(/data\.csv/i)).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
it.skip('should handle activeAsk state (waiting for human reply)', async () => {
|
|
// This test requires understanding the exact input field structure
|
|
// Skipping for now until we understand the component's input handling
|
|
const { result, rerender } = renderHook(() => useChatStoreAdapter())
|
|
const { chatStore } = result.current
|
|
const taskId = chatStore?.activeTaskId
|
|
|
|
if (!chatStore || !taskId) {
|
|
throw new Error('ChatStore or taskId is null')
|
|
}
|
|
|
|
await act(async () => {
|
|
chatStore.setHasMessages(taskId, true)
|
|
chatStore.addMessages(taskId, {
|
|
id: 'user-1',
|
|
role: 'user',
|
|
content: 'Help me decide between options',
|
|
attaches: []
|
|
})
|
|
chatStore.addMessages(taskId, {
|
|
id: 'assistant-1',
|
|
role: 'assistant',
|
|
content: 'Which option would you prefer: A or B?',
|
|
agent_name: 'decision-agent'
|
|
})
|
|
chatStore.setActiveAsk(taskId, 'decision-agent')
|
|
rerender()
|
|
})
|
|
|
|
render(
|
|
<TestWrapper>
|
|
<ChatBox />
|
|
</TestWrapper>
|
|
)
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Help me decide between options')).toBeInTheDocument()
|
|
expect(screen.getByText('Which option would you prefer: A or B?')).toBeInTheDocument()
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('Multi-ChatStore Project UI', () => {
|
|
it.skip('should display multiple chat sessions in project view', async () => {
|
|
// This test requires understanding the startTask method and multi-store handling
|
|
// Skipping for now - needs investigation into the actual startTask implementation
|
|
})
|
|
|
|
it.skip('should handle queued messages UI when task is busy', async () => {
|
|
// This test requires understanding the queueing mechanism in the UI
|
|
// Skipping for now - needs investigation into the actual queue handling
|
|
})
|
|
})
|
|
|
|
describe('Error States and Edge Cases', () => {
|
|
it('should handle corrupted chatStore state gracefully', async () => {
|
|
// Test that the component doesn't crash when chatStore is in an invalid state
|
|
// This is more of a safety test to ensure the component has proper error boundaries
|
|
|
|
// Should not crash when rendering with potentially corrupted state
|
|
expect(() => {
|
|
render(
|
|
<TestWrapper>
|
|
<ChatBox />
|
|
</TestWrapper>
|
|
)
|
|
}).not.toThrow()
|
|
|
|
// Should show some content (either welcome screen or handle the error gracefully)
|
|
expect(screen.getByText(/layout.welcome-to-eigent/i) || screen.getByText(/error/i) || screen.getByRole('main')).toBeTruthy()
|
|
})
|
|
|
|
it('should handle missing activeTaskId gracefully', async () => {
|
|
const { result, rerender } = renderHook(() => useChatStoreAdapter())
|
|
const { chatStore } = result.current
|
|
|
|
if (!chatStore) {
|
|
// If chatStore is null, that's fine for this test
|
|
render(
|
|
<TestWrapper>
|
|
<ChatBox />
|
|
</TestWrapper>
|
|
)
|
|
expect(screen.getByText(/layout.welcome-to-eigent/i)).toBeInTheDocument()
|
|
return
|
|
}
|
|
|
|
await act(async () => {
|
|
// Try to set activeTaskId to null
|
|
try {
|
|
(chatStore as any).activeTaskId = null
|
|
rerender()
|
|
} catch (error) {
|
|
// Expected - the store might prevent this
|
|
}
|
|
})
|
|
|
|
expect(() => {
|
|
render(
|
|
<TestWrapper>
|
|
<ChatBox />
|
|
</TestWrapper>
|
|
)
|
|
}).not.toThrow()
|
|
})
|
|
})
|
|
|
|
describe('User Interaction Flows', () => {
|
|
it('should show textarea input field with correct placeholder', async () => {
|
|
render(
|
|
<TestWrapper>
|
|
<ChatBox />
|
|
</TestWrapper>
|
|
)
|
|
|
|
// Based on the error messages, the actual placeholder is "chat.ask-placeholder"
|
|
// This appears to be a translation key rather than plain text
|
|
const textarea = screen.getByPlaceholderText('chat.ask-placeholder')
|
|
expect(textarea).toBeInTheDocument()
|
|
expect(textarea.tagName).toBe('TEXTAREA')
|
|
})
|
|
|
|
it('should have send button that is initially disabled', async () => {
|
|
render(
|
|
<TestWrapper>
|
|
<ChatBox />
|
|
</TestWrapper>
|
|
)
|
|
|
|
// Look for disabled send button (has arrow-right icon based on HTML structure)
|
|
const buttons = screen.getAllByRole('button')
|
|
const sendButton = buttons.find(btn =>
|
|
btn.querySelector('svg.lucide-arrow-right') &&
|
|
btn.hasAttribute('disabled')
|
|
)
|
|
|
|
expect(sendButton).toBeInTheDocument()
|
|
expect(sendButton).toBeDisabled()
|
|
})
|
|
|
|
it('should display layout.terms-of-use and layout.privacy-policy links', async () => {
|
|
render(
|
|
<TestWrapper>
|
|
<ChatBox />
|
|
</TestWrapper>
|
|
)
|
|
|
|
const termsLink = screen.getByRole('link', { name: /layout.terms-of-use/i })
|
|
const privacyLink = screen.getByRole('link', { name: /layout.privacy-policy/i })
|
|
|
|
expect(termsLink).toBeInTheDocument()
|
|
expect(termsLink).toHaveAttribute('href', 'https://www.eigent.ai/terms-of-use')
|
|
expect(termsLink).toHaveAttribute('target', '_blank')
|
|
|
|
expect(privacyLink).toBeInTheDocument()
|
|
expect(privacyLink).toHaveAttribute('href', 'https://www.eigent.ai/privacy-policy')
|
|
expect(privacyLink).toHaveAttribute('target', '_blank')
|
|
})
|
|
})
|
|
|
|
describe('Integration with useChatStoreAdapter', () => {
|
|
it('should properly integrate with useChatStoreAdapter hook', async () => {
|
|
const { result, rerender } = renderHook(() => useChatStoreAdapter())
|
|
const { chatStore, projectStore } = result.current
|
|
|
|
// Verify that the adapter returns the expected structure
|
|
expect(projectStore).toBeDefined()
|
|
expect(chatStore).toBeDefined()
|
|
|
|
// Verify that we can access basic properties
|
|
if (chatStore) {
|
|
expect(chatStore.activeTaskId).toBeDefined()
|
|
expect(typeof chatStore.activeTaskId).toBe('string')
|
|
}
|
|
|
|
if (projectStore) {
|
|
expect(projectStore.activeProjectId).toBeDefined()
|
|
expect(typeof projectStore.activeProjectId).toBe('string')
|
|
}
|
|
})
|
|
|
|
it('should handle chatStore state changes through adapter', async () => {
|
|
const { result, rerender } = renderHook(() => useChatStoreAdapter())
|
|
const { chatStore } = result.current
|
|
const taskId = chatStore?.activeTaskId
|
|
|
|
if (!chatStore || !taskId) {
|
|
throw new Error('ChatStore or taskId is null')
|
|
}
|
|
|
|
// Test adding a message through the adapter
|
|
await act(async () => {
|
|
chatStore.setHasMessages(taskId, true)
|
|
chatStore.addMessages(taskId, {
|
|
id: 'test-message-1',
|
|
role: 'user',
|
|
content: 'Test message from adapter',
|
|
attaches: []
|
|
})
|
|
rerender()
|
|
})
|
|
|
|
// Verify the message was added
|
|
const updatedChatStore = result.current.chatStore
|
|
if (updatedChatStore && updatedChatStore.tasks[taskId]) {
|
|
expect(updatedChatStore.tasks[taskId].hasMessages).toBe(true)
|
|
expect(updatedChatStore.tasks[taskId].messages).toHaveLength(1)
|
|
expect(updatedChatStore.tasks[taskId].messages[0].content).toBe('Test message from adapter')
|
|
}
|
|
})
|
|
})
|
|
}) |