eigent/test/integration/components/ChatBox.integration.test.tsx
Dream 2e44ae0f0a
refactor: move privacy consent from ChatBox to Login/SignUp (#1239)
Co-authored-by: a7m-1st <Ahmed.jimi.awelkeir500@gmail.com>
Co-authored-by: Wendong-Fan <133094783+Wendong-Fan@users.noreply.github.com>
2026-03-07 18:55:48 +08:00

533 lines
17 KiB
TypeScript

// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
import {
act,
render,
renderHook,
screen,
waitFor,
} from '@testing-library/react';
import React from 'react';
import { BrowserRouter } from 'react-router-dom';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import ChatBox from '../../../src/components/ChatBox/index';
// 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 useChatStoreAdapter from '../../../src/hooks/useChatStoreAdapter';
import { useProjectStore } from '../../../src/store/projectStore';
// 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', () => {
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) {
console.error(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 not display privacy consent links (moved to login)', async () => {
render(
<TestWrapper>
<ChatBox />
</TestWrapper>
);
// Privacy consent links are now in Login/SignUp, not in ChatBox
const termsLink = screen.queryByRole('link', {
name: /layout.terms-of-use/i,
});
const privacyLink = screen.queryByRole('link', {
name: /layout.privacy-policy/i,
});
expect(termsLink).not.toBeInTheDocument();
expect(privacyLink).not.toBeInTheDocument();
});
});
describe('Integration with useChatStoreAdapter', () => {
it('should properly integrate with useChatStoreAdapter hook', async () => {
const { result, rerender: _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'
);
}
});
});
});