test: add feature test guideline and example

This commit is contained in:
Wendong-Fan 2025-11-24 17:13:42 +08:00
parent 300ccc3af5
commit a6512e99c9
3 changed files with 402 additions and 4 deletions

158
test/feature/README.md Normal file
View 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.

View 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.
*/

View file

@ -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