mirror of
https://github.com/eigent-ai/eigent.git
synced 2026-06-01 06:00:19 +00:00
341 lines
11 KiB
TypeScript
341 lines
11 KiB
TypeScript
/**
|
|
* Feature Testing Sample Documentation
|
|
*
|
|
* ============================================================================
|
|
* What is Integration/Feature Testing?
|
|
* ============================================================================
|
|
*
|
|
* Feature tests focus on what users can see and do, rather than how the code is implemented.
|
|
*
|
|
* ## Feature Tests vs Unit Tests
|
|
*
|
|
* ### Unit Tests
|
|
* - Validate the internal logic of a single function or component
|
|
* - Example: ensure `calculateTotal(price, quantity)` returns the correct product
|
|
* - Pros: fast, isolated, precise failure signals
|
|
* - Cons: cannot guarantee the entire feature works correctly
|
|
*
|
|
* ### Feature Tests
|
|
* - Validate end-to-end user scenarios
|
|
* - Example: “user enters price and quantity, clicks Calculate, and sees the total”
|
|
* - Pros: mirrors real usage, one test covers multiple code paths
|
|
* - Cons: comparatively slower, failures take longer to debug
|
|
*
|
|
* ## Why lean on feature tests?
|
|
*
|
|
* Feature tests deliver higher ROI:
|
|
*
|
|
* 1. **Fewer tests overall**: one feature test can replace several unit tests
|
|
* 2. **Refactor friendly**: internal changes rarely require test updates
|
|
* 3. **Higher confidence**: confirms real user journeys keep working
|
|
* 4. **Lower maintenance**: fewer tests means less upkeep
|
|
*
|
|
* ============================================================================
|
|
* Below is a feature-test example
|
|
* ============================================================================
|
|
*/
|
|
|
|
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'
|
|
import { render, screen, renderHook, waitFor, act } from '@testing-library/react'
|
|
import { BrowserRouter } from 'react-router-dom'
|
|
import ChatBox from '../../src/components/ChatBox'
|
|
// Import necessary mocks
|
|
import '../mocks/proxy.mock'
|
|
import '../mocks/authStore.mock'
|
|
import '../mocks/sse.mock'
|
|
|
|
import { useProjectStore } from '../../src/store/projectStore'
|
|
import useChatStoreAdapter from '../../src/hooks/useChatStoreAdapter'
|
|
// 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),
|
|
selectFile: vi.fn().mockResolvedValue({ success: false }),
|
|
},
|
|
writable: true,
|
|
})
|
|
|
|
const TestWrapper = ({ children }: { children: React.ReactNode }) => (
|
|
<BrowserRouter>{children}</BrowserRouter>
|
|
)
|
|
|
|
describe('Feature test example: chat experience', () => {
|
|
/**
|
|
* beforeEach runs before every spec
|
|
* Purpose: reset application state so each test starts clean
|
|
*/
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
|
|
// Reset the project store
|
|
const projectStore = useProjectStore.getState()
|
|
projectStore.getAllProjects().forEach(project => {
|
|
projectStore.removeProject(project.id)
|
|
})
|
|
|
|
// Seed an initial project (mirrors the state when the app boots)
|
|
const projectId = projectStore.createProject(
|
|
'Feature Test Project',
|
|
'Testing user message flow'
|
|
)
|
|
expect(projectId).toBeDefined()
|
|
})
|
|
|
|
afterEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
/**
|
|
* Test 1: verify the initial UI
|
|
*
|
|
* This spec ensures:
|
|
* - Users see the welcome copy on launch
|
|
* - The input field renders correctly
|
|
*
|
|
* Acts as a smoke test for the base layout.
|
|
*/
|
|
it('displays the welcome screen and input', async () => {
|
|
// 1. Render the component (akin to opening the app)
|
|
render(
|
|
<TestWrapper>
|
|
<ChatBox />
|
|
</TestWrapper>
|
|
)
|
|
|
|
// 2. Assert the welcome text is visible
|
|
expect(screen.getByText(/layout.welcome-to-eigent/i)).toBeInTheDocument()
|
|
expect(screen.getByText(/layout.how-can-i-help-you/i)).toBeInTheDocument()
|
|
|
|
// 3. Confirm the textarea exists so the user can type
|
|
const textarea = screen.getByPlaceholderText('chat.ask-placeholder')
|
|
expect(textarea).toBeInTheDocument()
|
|
})
|
|
|
|
/**
|
|
* Test 2: validate the send button state
|
|
*
|
|
* This spec asserts:
|
|
* - When the input is empty, the send button stays disabled to block empty submissions
|
|
*
|
|
* Captures a critical UX behavior.
|
|
*/
|
|
it('disables the send button when the input is empty', async () => {
|
|
// 1. Render the component
|
|
render(
|
|
<TestWrapper>
|
|
<ChatBox />
|
|
</TestWrapper>
|
|
)
|
|
|
|
// 2. Find the send button (via its icon)
|
|
const buttons = screen.getAllByRole('button')
|
|
const sendButton = buttons.find(btn =>
|
|
btn.querySelector('svg.lucide-arrow-right')
|
|
)
|
|
|
|
expect(sendButton).toBeInTheDocument()
|
|
|
|
// 3. Assert the button is disabled
|
|
// Note: we do not care why it is disabled (privacy gate, empty input, etc.);
|
|
// we only care that the observable behavior matches expectations.
|
|
expect(sendButton).toBeDisabled()
|
|
})
|
|
|
|
/**
|
|
* Test 3: verify legal links
|
|
*
|
|
* This spec ensures:
|
|
* - Privacy Policy and Terms of Use links render
|
|
* - Each link points to the correct URL
|
|
* - Each link opens in a new tab
|
|
*/
|
|
it('shows Terms of Use and Privacy Policy links', async () => {
|
|
render(
|
|
<TestWrapper>
|
|
<ChatBox />
|
|
</TestWrapper>
|
|
)
|
|
|
|
// Locate the anchor elements
|
|
const termsLink = screen.getByRole('link', { name: /layout.terms-of-use/i })
|
|
const privacyLink = screen.getByRole('link', { name: /layout.privacy-policy/i })
|
|
|
|
// Verify link attributes
|
|
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')
|
|
})
|
|
|
|
/**
|
|
* Test 4: TaskPlanning journey
|
|
*
|
|
* This spec validates the complete user workflow from sending a message to task breakdown:
|
|
* - User sends a message
|
|
* - System displays the user message
|
|
* - System splits the task into subtasks (task planning phase)
|
|
* - Task summary and subtasks are displayed to the user
|
|
*
|
|
* This test covers the core message-send and task-splitting workflow.
|
|
*/
|
|
it('processes TaskPlanning journey with task splitting and subtask display', async () => {
|
|
// 1. Get the chat store using the adapter hook
|
|
const { result, rerender: rerenderHook } = renderHook(() => useChatStoreAdapter())
|
|
const { chatStore } = result.current
|
|
|
|
if (!chatStore) {
|
|
throw new Error('ChatStore is null')
|
|
}
|
|
|
|
const taskId = chatStore.activeTaskId
|
|
expect(taskId).toBeDefined()
|
|
|
|
// 2. Render the component
|
|
render(
|
|
<TestWrapper>
|
|
<ChatBox />
|
|
</TestWrapper>
|
|
)
|
|
|
|
// 3. Simulate user sending a message
|
|
const userMessage = 'Create a simple todo list application'
|
|
|
|
await act(async () => {
|
|
chatStore.setHasMessages(taskId!, true)
|
|
chatStore.addMessages(taskId!, {
|
|
id: 'user-msg-1',
|
|
role: 'user',
|
|
content: userMessage,
|
|
attaches: []
|
|
})
|
|
rerenderHook()
|
|
})
|
|
|
|
// 4. Verify user message appears in the UI
|
|
await waitFor(() => {
|
|
expect(screen.getByText(userMessage)).toBeInTheDocument()
|
|
})
|
|
|
|
// 5. Simulate task splitting phase (to_sub_tasks SSE event)
|
|
await act(async () => {
|
|
chatStore.setSummaryTask(taskId!, 'Todo List Application|Create a simple todo list application')
|
|
chatStore.addMessages(taskId!, {
|
|
id: 'to-sub-tasks-msg',
|
|
role: 'assistant',
|
|
content: '',
|
|
step: 'to_sub_tasks',
|
|
data: {
|
|
summary_task: 'Todo List Application|Create a simple todo list application',
|
|
sub_tasks: [
|
|
{
|
|
id: 'subtask-1',
|
|
content: 'Create HTML structure for todo list',
|
|
status: '',
|
|
subtasks: []
|
|
},
|
|
{
|
|
id: 'subtask-2',
|
|
content: 'Implement JavaScript functionality',
|
|
status: '',
|
|
subtasks: []
|
|
},
|
|
{
|
|
id: 'subtask-3',
|
|
content: 'Add CSS styling',
|
|
status: '',
|
|
subtasks: []
|
|
}
|
|
]
|
|
}
|
|
})
|
|
chatStore.setTaskInfo(taskId!, [
|
|
{ id: 'subtask-1', content: 'Create HTML structure for todo list', status: '' },
|
|
{ id: 'subtask-2', content: 'Implement JavaScript functionality', status: '' },
|
|
{ id: 'subtask-3', content: 'Add CSS styling', status: '' }
|
|
])
|
|
rerenderHook()
|
|
})
|
|
|
|
// 6. Verify task summary and subtasks appear in the UI
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Todo List Application')).toBeInTheDocument()
|
|
expect(screen.getByText('Create HTML structure for todo list')).toBeInTheDocument()
|
|
expect(screen.getByText('Implement JavaScript functionality')).toBeInTheDocument()
|
|
expect(screen.getByText('Add CSS styling')).toBeInTheDocument()
|
|
})
|
|
|
|
// 7. Verify chatStore state is correct by getting fresh state
|
|
const updatedChatStore = result.current.chatStore
|
|
expect(updatedChatStore!.tasks[taskId!].summaryTask).toContain('Todo List Application')
|
|
expect(updatedChatStore!.tasks[taskId!].taskInfo).toHaveLength(3)
|
|
})
|
|
})
|
|
|
|
/**
|
|
* ============================================================================
|
|
* Testing best-practice recap
|
|
* ============================================================================
|
|
*
|
|
* 1. **Exercise user behavior, not implementation details**
|
|
* ❌ Wrong: expect(component.state.messages).toHaveLength(1)
|
|
* ✅ Correct: expect(screen.getByText('Hello')).toBeInTheDocument()
|
|
*
|
|
* 2. **Query elements the way users perceive them**
|
|
* ❌ Wrong: screen.getByTestId('message-list')
|
|
* ✅ Correct: screen.getByText('Messages') or screen.getByRole('list')
|
|
*
|
|
* 3. **Assert full user flows**
|
|
* ❌ Wrong: test handleInput, handleSubmit, addMessage separately
|
|
* ✅ Correct: test the flow “user types a message and sends it”
|
|
*
|
|
* 4. **Pick descriptive test names**
|
|
* ❌ Wrong: it('test 1', ...)
|
|
* ✅ Correct: it('disables the send button when the input is empty', ...)
|
|
*
|
|
* 5. **Avoid over-mocking**
|
|
* - Mock only external dependencies (APIs, Electron APIs)
|
|
* - Keep application functions/components real
|
|
* - Let as much code run as possible
|
|
*
|
|
* ============================================================================
|
|
* How to extend these tests
|
|
* ============================================================================
|
|
*
|
|
* Suggested follow-up feature tests:
|
|
*
|
|
* 1. Full message-send journey
|
|
* - User types copy
|
|
* - Clicks the send button or presses Ctrl+Enter
|
|
* - Message appears in the chat history
|
|
* - Input resets to empty
|
|
*
|
|
* 2. File upload flow
|
|
* - User clicks the attachment button
|
|
* - Chooses a file
|
|
* - File appears in the attachment list
|
|
*
|
|
* 3. Error-handling path
|
|
* - Simulate an API error
|
|
* - Confirm the user-facing error surface renders
|
|
*
|
|
* 4. Task state transitions
|
|
* - Task moves from pending to running
|
|
* - UI reflects the correct state changes
|
|
*
|
|
* Remember: each spec should cover a complete user scenario.
|
|
*/
|