mirror of
https://github.com/eigent-ai/eigent.git
synced 2026-06-01 06:00:19 +00:00
test: add feature test guideline and example
This commit is contained in:
parent
300ccc3af5
commit
a6512e99c9
3 changed files with 402 additions and 4 deletions
158
test/feature/README.md
Normal file
158
test/feature/README.md
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
# Feature Testing Examples
|
||||
|
||||
## 📋 Overview
|
||||
|
||||
This folder contains integration test examples that focus on product features. These tests verify complete user scenarios rather than internal implementation details.
|
||||
|
||||
## 🎯 Why Feature Tests?
|
||||
|
||||
Feature tests deliver higher ROI:
|
||||
|
||||
### Feature Tests vs Unit Tests
|
||||
|
||||
| Characteristic | Unit Tests | Feature Tests |
|
||||
|-----|---------|---------|
|
||||
| **Scope** | Single function/component | Full user scenario |
|
||||
| **Number of tests** | Many (one per function) | Few (one per feature) |
|
||||
| **Maintenance cost** | High (heavy churn during refactors) | Low (stable as long as behavior stays) |
|
||||
| **Execution speed** | Fast | Relatively slower |
|
||||
| **Bug discovery** | Internal logic issues | Real user experience issues |
|
||||
| **Refactor friendliness** | Low | High |
|
||||
|
||||
### Example Comparison
|
||||
|
||||
**Unit-test style** (needs multiple tests):
|
||||
```typescript
|
||||
// ❌ Requires dedicated tests per function
|
||||
it('handleInputChange should update state', ...)
|
||||
it('validateMessage should reject empty input', ...)
|
||||
it('sendMessage should call the API', ...)
|
||||
it('clearInput should reset the field', ...)
|
||||
```
|
||||
|
||||
**Feature-test style** (one test covers the flow):
|
||||
```typescript
|
||||
// ✅ One test verifies the full flow
|
||||
it('user can type and send a message', () => {
|
||||
// User types text
|
||||
// Clicks the send button
|
||||
// Validates the message appears in the UI
|
||||
// Confirms the input box is cleared
|
||||
})
|
||||
```
|
||||
|
||||
## ✅ Testing Best Practices
|
||||
|
||||
### 1. Assert user-facing behavior, not implementation details
|
||||
|
||||
```typescript
|
||||
// ❌ Wrong: assert internal state
|
||||
expect(component.state.messages).toHaveLength(1)
|
||||
|
||||
// ✅ Correct: assert what the user sees
|
||||
expect(screen.getByText('Hello')).toBeInTheDocument()
|
||||
```
|
||||
|
||||
### 2. Query only what a user can perceive
|
||||
|
||||
```typescript
|
||||
// ❌ Wrong: use test IDs
|
||||
screen.getByTestId('message-list')
|
||||
|
||||
// ✅ Correct: use roles or visible text
|
||||
screen.getByRole('list')
|
||||
screen.getByText('Messages')
|
||||
```
|
||||
|
||||
### 3. Cover the entire workflow
|
||||
|
||||
```typescript
|
||||
// ❌ Wrong: test each handler separately
|
||||
it('handleInput works', ...)
|
||||
it('handleSubmit works', ...)
|
||||
|
||||
// ✅ Correct: cover the whole user scenario
|
||||
it('user can type and send a message', ...)
|
||||
```
|
||||
|
||||
### 4. Use descriptive test names
|
||||
|
||||
```typescript
|
||||
// ❌ Wrong: vague names
|
||||
it('test 1', ...)
|
||||
it('works', ...)
|
||||
|
||||
// ✅ Correct: describe expected behavior
|
||||
it('disables send button when the input is empty', ...)
|
||||
it('clears the input after sending a message', ...)
|
||||
```
|
||||
|
||||
### 5. Avoid excessive mocking
|
||||
|
||||
```typescript
|
||||
// ❌ Wrong: mock everything
|
||||
vi.mock('./MessageList')
|
||||
vi.mock('./InputBox')
|
||||
vi.mock('./SendButton')
|
||||
|
||||
// ✅ Correct: mock only external dependencies
|
||||
vi.mock('@/api/http') // API calls
|
||||
vi.mock('electron') // Electron APIs
|
||||
// Let other components run normally
|
||||
```
|
||||
|
||||
## 💡 Test Strategy Guidance
|
||||
|
||||
Consider this split:
|
||||
|
||||
1. **Core features** (80% effort) – cover with feature tests
|
||||
- User sign-in/sign-up
|
||||
- Message sending
|
||||
- File upload
|
||||
- Task management
|
||||
|
||||
2. **Utility helpers** (15% effort) – cover with unit tests
|
||||
- Data formatting
|
||||
- Validation helpers
|
||||
- Calculation helpers
|
||||
|
||||
3. **Edge cases** (5% effort) – add as needed
|
||||
- Extreme inputs
|
||||
- Concurrency scenarios
|
||||
- Performance tests
|
||||
|
||||
## ❓ FAQ
|
||||
|
||||
### Q: How do I debug a failing feature test?
|
||||
|
||||
A:
|
||||
1. Inspect the test output to identify the failing assertion
|
||||
2. Call `screen.debug()` to print the current DOM
|
||||
3. Check whether you need `waitFor` for pending async work
|
||||
4. Gradually simplify the test until you isolate the minimal failing scenario
|
||||
|
||||
### Q: What if the tests run too slowly?
|
||||
|
||||
A:
|
||||
1. Use `npm run test:watch` to run only changed tests
|
||||
2. Temporarily focus with `it.only`
|
||||
3. Review any unnecessary `waitFor` timeouts
|
||||
4. Consider splitting very large tests into smaller ones
|
||||
|
||||
### Q: How do I test flows that require authentication?
|
||||
|
||||
A:
|
||||
1. Mock the logged-in state in `beforeEach`
|
||||
2. Stub `authStore` to return an authenticated user
|
||||
3. Or create a `setupLoggedInUser()` helper
|
||||
|
||||
## 📝 Takeaway
|
||||
|
||||
Keep this principle in mind:
|
||||
|
||||
> **Tests should exercise your app the way a user would**
|
||||
>
|
||||
> If a test must understand internal implementation, it is likely over-specified.
|
||||
>
|
||||
> Focus on what users can see and do.
|
||||
|
||||
239
test/feature/SendFirstMessage.feature.test.tsx
Normal file
239
test/feature/SendFirstMessage.feature.test.tsx
Normal file
|
|
@ -0,0 +1,239 @@
|
|||
/**
|
||||
* 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 } from '@testing-library/react'
|
||||
import { BrowserRouter } from 'react-router-dom'
|
||||
import ChatBox from '../../../src/components/ChatBox/index'
|
||||
|
||||
// Import necessary mocks
|
||||
import '../../../test/mocks/proxy.mock'
|
||||
import '../../../test/mocks/authStore.mock'
|
||||
import '../../../test/mocks/sse.mock'
|
||||
|
||||
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),
|
||||
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')
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* ============================================================================
|
||||
* 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.
|
||||
*/
|
||||
|
|
@ -34,12 +34,13 @@ const mockImplementation = {
|
|||
if (url.includes('/api/providers')) {
|
||||
return Promise.resolve({ items: [] })
|
||||
}
|
||||
// Mock privacy settings
|
||||
// Mock privacy settings - all required fields must be true
|
||||
if (url.includes('/api/user/privacy')) {
|
||||
return Promise.resolve({
|
||||
dataCollection: true,
|
||||
analytics: true,
|
||||
marketing: true
|
||||
take_screenshot: true,
|
||||
access_local_software: true,
|
||||
access_your_address: true,
|
||||
password_storage: true
|
||||
})
|
||||
}
|
||||
// Mock configs
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue