mirror of
https://github.com/lobehub/lobe-chat.git
synced 2026-06-14 03:30:19 +00:00
✨ feat(image): polish ai image (#8915)
This commit is contained in:
@@ -10,7 +10,7 @@ Emoji logo: 🤯
|
||||
|
||||
## Project Technologies Stack
|
||||
|
||||
read [package.json](mdc:package.json) to know all npm packages you can use. read [folder-structure.mdx](mdc:docs/development/basic/folder-structure.mdx) to learn project structure.
|
||||
read [package.json](mdc:package.json) to know all npm packages you can use.
|
||||
|
||||
The project uses the following technologies:
|
||||
|
||||
@@ -42,17 +42,3 @@ The project uses the following technologies:
|
||||
- Cursor AI for code editing and AI coding assistance
|
||||
|
||||
Note: All tools and libraries used are the latest versions. The application only needs to be compatible with the latest browsers;
|
||||
|
||||
## Often used npm scripts and commands
|
||||
|
||||
```bash
|
||||
# !: don't any build script to check weather code can work after modify
|
||||
# type check
|
||||
bun run type-check
|
||||
|
||||
# install dependencies
|
||||
pnpm install
|
||||
|
||||
# run tests
|
||||
npx vitest run --config vitest.config.ts '[file-path-pattern]'
|
||||
```
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
---
|
||||
description: Project directory structure overview
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# LobeChat Project Structure
|
||||
|
||||
## Directory Structure
|
||||
|
||||
note: some files are not shown for simplicity.
|
||||
|
||||
```plaintext
|
||||
lobe-chat/
|
||||
├── apps/ # Applications directory
|
||||
│ └── desktop/ # Electron desktop application
|
||||
│ ├── src/ # Desktop app source code
|
||||
│ └── resources/ # Desktop app resources
|
||||
├── docs/ # Project documentation
|
||||
│ ├── development/ # Development docs
|
||||
│ ├── self-hosting/ # Self-hosting docs
|
||||
│ └── usage/ # Usage guides
|
||||
├── locales/ # Internationalization files
|
||||
│ ├── en-US/ # English
|
||||
│ └── zh-CN/ # Simplified Chinese
|
||||
├── packages/ # Monorepo packages directory
|
||||
│ ├── const/ # Constants definition package
|
||||
│ ├── database/ # Database related package
|
||||
│ ├── model-runtime/ # AI model runtime package
|
||||
│ ├── types/ # TypeScript type definitions
|
||||
│ ├── utils/ # Utility functions package
|
||||
│ ├── file-loaders/ # File processing packages
|
||||
│ ├── prompts/ # AI prompt management
|
||||
│ └── web-crawler/ # Web crawling functionality
|
||||
├── public/ # Static assets
|
||||
│ ├── icons/ # Application icons
|
||||
│ ├── images/ # Image resources
|
||||
│ └── screenshots/ # Application screenshots
|
||||
├── scripts/ # Build and tool scripts
|
||||
├── src/ # Main application source code (see below)
|
||||
├── tests/ # Test configuration
|
||||
├── .cursor/ # Cursor AI configuration
|
||||
├── docker-compose/ # Docker configuration
|
||||
├── package.json # Project dependencies
|
||||
├── pnpm-workspace.yaml # pnpm monorepo configuration
|
||||
├── next.config.ts # Next.js configuration
|
||||
├── drizzle.config.ts # Drizzle ORM configuration
|
||||
└── tsconfig.json # TypeScript configuration
|
||||
```
|
||||
|
||||
## Core Source Directory (`src/`)
|
||||
|
||||
```plaintext
|
||||
src/
|
||||
├── app/ # Next.js App Router routes
|
||||
│ ├── (backend)/ # Backend API routes
|
||||
│ │ ├── api/ # REST API endpoints
|
||||
│ │ │ ├── auth/ # Authentication endpoints
|
||||
│ │ │ └── webhooks/ # Webhook handlers for various auth providers
|
||||
│ │ ├── middleware/ # Request middleware
|
||||
│ │ ├── oidc/ # OpenID Connect endpoints
|
||||
│ │ ├── trpc/ # tRPC API routes
|
||||
│ │ │ ├── async/ # Async tRPC endpoints
|
||||
│ │ │ ├── edge/ # Edge runtime endpoints
|
||||
│ │ │ ├── lambda/ # Lambda runtime endpoints
|
||||
│ │ │ └── tools/ # Tools-specific endpoints
|
||||
│ │ └── webapi/ # Web API endpoints
|
||||
│ │ ├── chat/ # Chat-related APIs for various providers
|
||||
│ │ ├── models/ # Model management APIs
|
||||
│ │ ├── plugin/ # Plugin system APIs
|
||||
│ │ ├── stt/ # Speech-to-text APIs
|
||||
│ │ ├── text-to-image/ # Image generation APIs
|
||||
│ │ └── tts/ # Text-to-speech APIs
|
||||
│ ├── [variants]/ # Page route variants
|
||||
│ │ ├── (main)/ # Main application routes
|
||||
│ │ │ ├── chat/ # Chat interface and workspace
|
||||
│ │ │ ├── discover/ # Discover page (assistants, models, providers)
|
||||
│ │ │ ├── files/ # File management interface
|
||||
│ │ │ ├── image/ # Image generation interface
|
||||
│ │ │ ├── profile/ # User profile and stats
|
||||
│ │ │ ├── repos/ # Knowledge base repositories
|
||||
│ │ │ └── settings/ # Application settings
|
||||
│ │ └── @modal/ # Modal routes
|
||||
│ └── manifest.ts # PWA configuration
|
||||
├── components/ # Global shared components
|
||||
│ ├── Analytics/ # Analytics tracking components
|
||||
│ ├── Error/ # Error handling components
|
||||
│ └── Loading/ # Loading state components
|
||||
├── config/ # Application configuration
|
||||
│ ├── aiModels/ # AI model configurations
|
||||
│ └── modelProviders/ # Model provider configurations
|
||||
├── features/ # Feature components (UI Layer)
|
||||
│ ├── AgentSetting/ # Agent configuration and management
|
||||
│ ├── ChatInput/ # Chat input with file upload and tools
|
||||
│ ├── Conversation/ # Message display and interaction
|
||||
│ ├── FileManager/ # File upload and knowledge base
|
||||
│ └── PluginStore/ # Plugin marketplace and management
|
||||
├── hooks/ # Custom React hooks
|
||||
├── layout/ # Global layout components
|
||||
│ ├── AuthProvider/ # Authentication provider
|
||||
│ └── GlobalProvider/ # Global state provider
|
||||
├── libs/ # External library integrations
|
||||
│ ├── analytics/ # Analytics services integration
|
||||
│ ├── next-auth/ # NextAuth.js configuration
|
||||
│ └── oidc-provider/ # OIDC provider implementation
|
||||
├── locales/ # Internationalization resources
|
||||
│ └── default/ # Default language definitions
|
||||
├── migrations/ # Client-side data migrations
|
||||
├── server/ # Server-side code
|
||||
│ ├── modules/ # Server modules
|
||||
│ ├── routers/ # tRPC routers
|
||||
│ └── services/ # Server services
|
||||
├── services/ # Client service layer
|
||||
│ ├── aiModel/ # AI model services
|
||||
│ ├── session/ # Session services
|
||||
│ └── message/ # Message services
|
||||
├── store/ # Zustand state management
|
||||
│ ├── agent/ # Agent state
|
||||
│ ├── chat/ # Chat state
|
||||
│ └── user/ # User state
|
||||
├── styles/ # Global styles
|
||||
├── tools/ # Built-in tool system
|
||||
│ ├── artifacts/ # Code artifacts and preview
|
||||
│ └── web-browsing/ # Web search and browsing
|
||||
├── types/ # TypeScript type definitions
|
||||
└── utils/ # Utility functions
|
||||
├── client/ # Client-side utilities
|
||||
└── server/ # Server-side utilities
|
||||
```
|
||||
|
||||
## Key Monorepo Packages
|
||||
|
||||
```plaintext
|
||||
packages/
|
||||
├── const/ # Global constants and configurations
|
||||
├── database/ # Database schemas and models
|
||||
│ ├── src/models/ # Data models (CRUD operations)
|
||||
│ ├── src/schemas/ # Drizzle database schemas
|
||||
│ ├── src/repositories/ # Complex query layer
|
||||
│ └── migrations/ # Database migration files
|
||||
├── model-runtime/ # AI model runtime
|
||||
│ └── src/
|
||||
│ ├── openai/ # OpenAI provider integration
|
||||
│ ├── anthropic/ # Anthropic provider integration
|
||||
│ ├── google/ # Google AI provider integration
|
||||
│ ├── ollama/ # Ollama local model integration
|
||||
│ ├── types/ # Runtime type definitions
|
||||
│ └── utils/ # Runtime utilities
|
||||
├── types/ # Shared TypeScript type definitions
|
||||
│ └── src/
|
||||
│ ├── agent/ # Agent-related types
|
||||
│ ├── message/ # Message and chat types
|
||||
│ ├── user/ # User and session types
|
||||
│ └── tool/ # Tool and plugin types
|
||||
├── utils/ # Shared utility functions
|
||||
│ └── src/
|
||||
│ ├── client/ # Client-side utilities
|
||||
│ ├── server/ # Server-side utilities
|
||||
│ ├── fetch/ # HTTP request utilities
|
||||
│ └── tokenizer/ # Token counting utilities
|
||||
├── file-loaders/ # File loaders (PDF, DOCX, etc.)
|
||||
├── prompts/ # AI prompt management
|
||||
└── web-crawler/ # Web crawling functionality
|
||||
```
|
||||
|
||||
## Architecture Layers
|
||||
|
||||
### 1. **Presentation Layer**
|
||||
|
||||
- Business-specific feature components and reusable UI components
|
||||
- Global layout providers and responsive design wrappers
|
||||
|
||||
### 2. **State Management Layer**
|
||||
|
||||
- Zustand-based client state with domain-specific slices
|
||||
- Actions and selectors for predictable state updates
|
||||
|
||||
### 3. **Client Service Layer**
|
||||
|
||||
- Environment-adaptive services (local Model vs remote tRPC)
|
||||
- Dual implementation pattern for multi-runtime compatibility
|
||||
|
||||
### 4. **API Interface Layer**
|
||||
|
||||
- Type-safe tRPC routers organized by runtime environment
|
||||
- Request routing and validation
|
||||
|
||||
### 5. **Server Service Layer**
|
||||
|
||||
- Platform-agnostic business logic with implementation abstractions
|
||||
- Reusable, testable service composition
|
||||
|
||||
### 6. **Data Access Layer**
|
||||
|
||||
- **Repository**: Complex queries, joins, and transaction management
|
||||
- **Model**: Basic CRUD operations and single-table queries
|
||||
- **Schema**: Drizzle ORM definitions and migration management
|
||||
|
||||
### 7. **Integration & Extensions**
|
||||
|
||||
- **External**: Third-party service integrations and library wrappers
|
||||
- **Built-in**: AI runtime, tool system, file processing, and web crawling
|
||||
|
||||
## Data Flow Architecture
|
||||
|
||||
### Unified Flow Pattern
|
||||
|
||||
```
|
||||
UI Layer → State Management → Client Service → [Environment Branch] → Database
|
||||
↓ ↓ ↓ ↓ ↓
|
||||
React Zustand Environment Local/Remote PGLite/
|
||||
Components Store Adaptation Routing PostgreSQL
|
||||
```
|
||||
|
||||
### Environment-Specific Routing
|
||||
|
||||
| Mode | UI | Service Route | Database |
|
||||
| --------------- | -------- | ---------------------- | ------------------- |
|
||||
| **Browser/PWA** | React | Direct Model Access | PGLite (Local) |
|
||||
| **Server** | React | tRPC → Server Services | PostgreSQL (Remote) |
|
||||
| **Desktop** | Electron | tRPC → Local Node.js | PGLite/PostgreSQL\* |
|
||||
|
||||
_\*Depends on cloud sync configuration_
|
||||
|
||||
### Key Characteristics
|
||||
|
||||
- **Type Safety**: End-to-end type safety via tRPC and Drizzle ORM
|
||||
- **Local/Remote Dual Mode**: PGLite enables user data ownership and local control
|
||||
@@ -5,6 +5,8 @@ alwaysApply: false
|
||||
|
||||
## 🗃️ 数据库 Model 测试指南
|
||||
|
||||
测试 `packages/database` 下的数据库 Model 层。
|
||||
|
||||
### 测试环境选择 💡
|
||||
|
||||
数据库 Model 层通过环境变量控制数据库类型,在两种测试环境下有不同的数据库后端:客户端环境 (PGLite) 和 服务端环境 (PostgreSQL)
|
||||
@@ -17,10 +19,10 @@ alwaysApply: false
|
||||
|
||||
```bash
|
||||
# 1. 先在客户端环境测试(快速验证)
|
||||
npx vitest run --config vitest.config.ts src/database/models/__tests__/myModel.test.ts
|
||||
cd packages/database && TEST_SERVER_DB=0 bunx vitest run --silent='passed-only' src/database/models/__tests__/myModel.test.ts
|
||||
|
||||
# 2. 再在服务端环境测试(兼容性验证)
|
||||
npx vitest run --config vitest.config.server.ts src/database/models/__tests__/myModel.test.ts
|
||||
# 2. 再在服务端环境测试(兼容性验证), 需要设置环境变量 `TEST_SERVER_DB=1`
|
||||
cd packages/database && TEST_SERVER_DB=1 bunx vitest run --silent='passed-only' src/database/models/__tests__/myModel.test.ts #
|
||||
```
|
||||
|
||||
### 创建新 Model 测试的最佳实践 📋
|
||||
|
||||
@@ -5,11 +5,11 @@ alwaysApply: false
|
||||
|
||||
# 测试指南 - LobeChat Testing Guide
|
||||
|
||||
## 🧪 测试环境概览
|
||||
## 测试环境概览
|
||||
|
||||
LobeChat 项目使用 Vitest 测试库,配置了两种不同的测试环境:
|
||||
|
||||
### 客户端测试环境 (DOM Environment)
|
||||
### 客户端数据库测试环境 (DOM Environment)
|
||||
|
||||
- **配置文件**: [vitest.config.ts](mdc:vitest.config.ts)
|
||||
- **环境**: Happy DOM (浏览器环境模拟)
|
||||
@@ -17,60 +17,62 @@ LobeChat 项目使用 Vitest 测试库,配置了两种不同的测试环境:
|
||||
- **用途**: 测试前端组件、客户端逻辑、React 组件等
|
||||
- **设置文件**: [tests/setup.ts](mdc:tests/setup.ts)
|
||||
|
||||
### 服务端测试环境 (Node Environment)
|
||||
### 服务端数据库测试环境 (Node Environment)
|
||||
|
||||
- **配置文件**: [vitest.config.server.ts](mdc:vitest.config.server.ts)
|
||||
目前只有 `packages/database` 下的测试可以通过配置 `TEST_SERVER_DB=1` 环境变量来使用服务端数据库测试
|
||||
|
||||
- **配置文件**: [packages/database/vitest.config.mts](mdc:packages/database/vitest.config.mts) 并且设置环境变量 `TEST_SERVER_DB=1`
|
||||
- **环境**: Node.js
|
||||
- **数据库**: 真实的 PostgreSQL 数据库
|
||||
- **并发限制**: 单线程运行 (`singleFork: true`)
|
||||
- **用途**: 测试数据库模型、服务端逻辑、API 端点等
|
||||
- **设置文件**: [tests/setup-db.ts](mdc:tests/setup-db.ts)
|
||||
- **设置文件**: [packages/database/tests/setup-db.ts](mdc:packages/database/tests/setup-db.ts)
|
||||
|
||||
## 🚀 测试运行命令
|
||||
## 测试运行命令
|
||||
|
||||
**🚨 性能警告**: 项目包含 3000+ 测试用例,完整运行需要约 10 分钟。务必使用文件过滤或测试名称过滤。
|
||||
** 性能警告**: 项目包含 3000+ 测试用例,完整运行需要约 10 分钟。务必使用文件过滤或测试名称过滤。
|
||||
|
||||
### ✅ 正确的命令格式
|
||||
### 正确的命令格式
|
||||
|
||||
```bash
|
||||
# 运行所有客户端/服务端测试
|
||||
npx vitest run --config vitest.config.ts # 客户端测试
|
||||
npx vitest run --config vitest.config.server.ts # 服务端测试
|
||||
bunx vitest run --silent='passed-only' # 客户端测试
|
||||
cd packages/database && TEST_SERVER_DB=1 bunx vitest run --silent='passed-only' # 服务端测试
|
||||
|
||||
# 运行特定测试文件 (支持模糊匹配)
|
||||
npx vitest run --config vitest.config.ts user.test.ts
|
||||
bunx vitest run --silent='passed-only' user.test.ts
|
||||
|
||||
# 运行特定测试用例名称 (使用 -t 参数)
|
||||
npx vitest run --config vitest.config.ts -t "test case name"
|
||||
bunx vitest run --silent='passed-only' -t "test case name"
|
||||
|
||||
# 组合使用文件和测试名称过滤
|
||||
npx vitest run --config vitest.config.ts filename.test.ts -t "specific test"
|
||||
bunx vitest run --silent='passed-only' filename.test.ts -t "specific test"
|
||||
|
||||
# 生成覆盖率报告 (使用 --coverage 参数)
|
||||
npx vitest run --config vitest.config.ts --coverage
|
||||
bunx vitest run --silent='passed-only' --coverage
|
||||
```
|
||||
|
||||
### ❌ 避免的命令格式
|
||||
### 避免的命令格式
|
||||
|
||||
```bash
|
||||
# ❌ 这些命令会运行所有 3000+ 测试用例,耗时约 10 分钟!
|
||||
# 这些命令会运行所有 3000+ 测试用例,耗时约 10 分钟!
|
||||
npm test
|
||||
npm test some-file.test.ts
|
||||
|
||||
# ❌ 不要使用裸 vitest (会进入 watch 模式)
|
||||
# 不要使用裸 vitest (会进入 watch 模式)
|
||||
vitest test-file.test.ts
|
||||
```
|
||||
|
||||
## 🔧 测试修复原则
|
||||
## 测试修复原则
|
||||
|
||||
### 核心原则 ⚠️
|
||||
### 核心原则
|
||||
|
||||
1. **充分阅读测试代码**: 在修复测试之前,必须完整理解测试的意图和实现
|
||||
2. **测试优先修复**: 如果是测试本身写错了,修改测试而不是实现代码
|
||||
3. **专注单一问题**: 只修复指定的测试,不要添加额外测试或功能
|
||||
4. **不自作主张**: 不要因为发现其他问题就直接修改,先提出再讨论
|
||||
|
||||
### 测试协作最佳实践 🤝
|
||||
### 测试协作最佳实践
|
||||
|
||||
基于实际开发经验总结的重要协作原则:
|
||||
|
||||
@@ -84,10 +86,10 @@ vitest test-file.test.ts
|
||||
- **避免陷阱**: 不要陷入"不断尝试相同或类似方法"的循环
|
||||
|
||||
```typescript
|
||||
// ❌ 错误做法:连续失败后继续盲目尝试
|
||||
// 错误做法:连续失败后继续盲目尝试
|
||||
// 第3次、第4次仍在用相似的方法修复同一个问题
|
||||
|
||||
// ✅ 正确做法:失败1-2次后总结问题
|
||||
// 正确做法:失败1-2次后总结问题
|
||||
/*
|
||||
问题总结:
|
||||
1. 尝试过的方法:修改 mock 数据结构
|
||||
@@ -106,7 +108,7 @@ vitest test-file.test.ts
|
||||
- **保持稳定性**: 测试名称应该在代码重构后仍然有意义
|
||||
|
||||
```typescript
|
||||
// ❌ 错误的测试命名
|
||||
// 错误的测试命名
|
||||
describe('User component coverage', () => {
|
||||
it('covers line 45-50 in getUserData', () => {
|
||||
// 为了覆盖第45-50行而写的测试
|
||||
@@ -117,7 +119,7 @@ describe('User component coverage', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ✅ 正确的测试命名
|
||||
// 正确的测试命名
|
||||
describe('<UserAvatar />', () => {
|
||||
it('should render fallback icon when image url is not provided', () => {
|
||||
// 测试具体的业务场景,自然会覆盖相关代码分支
|
||||
@@ -131,8 +133,8 @@ describe('<UserAvatar />', () => {
|
||||
|
||||
**覆盖率提升的正确思路**:
|
||||
|
||||
- ✅ 通过设计各种业务场景(正常流程、边缘情况、错误处理)来自然提升覆盖率
|
||||
- ❌ 不要为了达到覆盖率数字而写测试,更不要在测试中注释"为了覆盖 xxx 行"
|
||||
- 通过设计各种业务场景(正常流程、边缘情况、错误处理)来自然提升覆盖率
|
||||
- 不要为了达到覆盖率数字而写测试,更不要在测试中注释"为了覆盖 xxx 行"
|
||||
|
||||
#### 3. 测试组织结构
|
||||
|
||||
@@ -143,7 +145,7 @@ describe('<UserAvatar />', () => {
|
||||
- **避免碎片化**: 不要为了单个测试用例就创建新的顶级 `describe` 块
|
||||
|
||||
```typescript
|
||||
// ❌ 错误的组织方式:创建过多顶级块
|
||||
// 错误的组织方式:创建过多顶级块
|
||||
describe('<UserProfile />', () => {
|
||||
it('should render user name', () => {});
|
||||
});
|
||||
@@ -158,7 +160,7 @@ describe('UserProfile edge cases', () => {
|
||||
it('should handle missing avatar', () => {});
|
||||
});
|
||||
|
||||
// ✅ 正确的组织方式:合并相关测试
|
||||
// 正确的组织方式:合并相关测试
|
||||
describe('<UserProfile />', () => {
|
||||
it('should render user name', () => {});
|
||||
|
||||
@@ -214,9 +216,9 @@ describe('<UserProfile />', () => {
|
||||
**修复方法**: 更新了测试文件中的 mock 数据结构,使其与最新的 API 响应格式保持一致。具体修改了 `user.test.ts` 中的 `mockUserData` 对象结构。
|
||||
```
|
||||
|
||||
## 🎯 测试编写最佳实践
|
||||
## 测试编写最佳实践
|
||||
|
||||
### Mock 数据策略:追求"低成本的真实性" 📋
|
||||
### Mock 数据策略:追求"低成本的真实性"
|
||||
|
||||
**核心原则**: 测试数据应默认追求真实性,只有在引入"高昂的测试成本"时才进行简化。
|
||||
|
||||
@@ -228,10 +230,10 @@ describe('<UserProfile />', () => {
|
||||
- **网络请求**:HTTP 调用、数据库连接
|
||||
- **系统调用**:获取系统时间、环境变量等
|
||||
|
||||
#### ✅ 推荐做法:Mock 依赖,保留真实数据
|
||||
#### 推荐做法:Mock 依赖,保留真实数据
|
||||
|
||||
```typescript
|
||||
// ✅ 好的做法:Mock I/O 操作,但使用真实的文件内容格式
|
||||
// 好的做法:Mock I/O 操作,但使用真实的文件内容格式
|
||||
describe('parseContentType', () => {
|
||||
beforeEach(() => {
|
||||
// Mock 文件读取操作(避免真实 I/O)
|
||||
@@ -249,7 +251,7 @@ describe('parseContentType', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ❌ 过度简化:使用不真实的数据
|
||||
// 过度简化:使用不真实的数据
|
||||
describe('parseContentType', () => {
|
||||
it('should detect PDF content type correctly', () => {
|
||||
// 这种简化数据没有测试价值
|
||||
@@ -259,78 +261,113 @@ describe('parseContentType', () => {
|
||||
});
|
||||
```
|
||||
|
||||
#### 🎯 真实标识符的价值
|
||||
#### 真实标识符的价值
|
||||
|
||||
```typescript
|
||||
// ✅ 使用真实的提供商标识符
|
||||
it('should parse OpenAI model list correctly', () => {
|
||||
const result = parseModelString('openai', '+gpt-4,+gpt-3.5-turbo');
|
||||
expect(result.add).toHaveLength(2);
|
||||
expect(result.add[0].id).toBe('gpt-4');
|
||||
});
|
||||
// ✅ 使用真实标识符
|
||||
const result = parseModelString('openai', '+gpt-4,+gpt-3.5-turbo');
|
||||
|
||||
// ❌ 使用占位符标识符(价值较低)
|
||||
it('should parse model list correctly', () => {
|
||||
const result = parseModelString('test-provider', '+model1,+model2');
|
||||
expect(result.add).toHaveLength(2);
|
||||
// 这种测试对理解真实场景帮助不大
|
||||
// ❌ 使用占位符(价值较低)
|
||||
const result = parseModelString('test-provider', '+model1,+model2');
|
||||
```
|
||||
|
||||
### 现代化Mock技巧:环境设置与Mock方法
|
||||
|
||||
**环境设置 + Mock方法结合使用**
|
||||
|
||||
客户端代码测试时,推荐使用环境注释配合现代化Mock方法:
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* @vitest-environment happy-dom // 提供浏览器API
|
||||
*/
|
||||
import { beforeEach, vi } from 'vitest';
|
||||
|
||||
beforeEach(() => {
|
||||
// 现代方法1:使用vi.stubGlobal替代global.xxx = ...
|
||||
const mockImage = vi.fn().mockImplementation(() => ({
|
||||
addEventListener: vi.fn(),
|
||||
naturalHeight: 600,
|
||||
naturalWidth: 800,
|
||||
}));
|
||||
vi.stubGlobal('Image', mockImage);
|
||||
|
||||
// 现代方法2:使用vi.spyOn保留原功能,只mock特定方法
|
||||
vi.spyOn(URL, 'createObjectURL').mockReturnValue('blob:mock-url');
|
||||
vi.spyOn(URL, 'revokeObjectURL').mockImplementation(() => {});
|
||||
});
|
||||
```
|
||||
|
||||
### 错误处理测试:测试"行为"而非"文本" ⚠️
|
||||
**环境选择优先级**
|
||||
|
||||
1. **@vitest-environment happy-dom** (推荐) - 轻量、快速,项目已安装
|
||||
2. **@vitest-environment jsdom** - 功能完整,但需要额外安装jsdom包
|
||||
3. **不设置环境** - Node.js环境,需要手动mock所有浏览器API
|
||||
|
||||
**Mock方法对比**
|
||||
|
||||
```typescript
|
||||
// ❌ 旧方法:直接操作global对象(类型问题)
|
||||
global.Image = mockImage;
|
||||
global.URL = { ...global.URL, createObjectURL: mockFn };
|
||||
|
||||
// ✅ 现代方法:类型安全的vi API
|
||||
vi.stubGlobal('Image', mockImage); // 完全替换全局对象
|
||||
vi.spyOn(URL, 'createObjectURL'); // 部分mock,保留其他功能
|
||||
```
|
||||
|
||||
### 测试覆盖率原则:代码分支优于用例数量
|
||||
|
||||
**核心原则**: 优先覆盖所有代码分支,而非编写大量重复用例
|
||||
|
||||
```typescript
|
||||
// ❌ 过度测试:29个测试用例都验证相同分支
|
||||
describe('getImageDimensions', () => {
|
||||
it('should reject .txt files');
|
||||
it('should reject .pdf files');
|
||||
// ... 25个类似测试,都走相同的验证分支
|
||||
});
|
||||
|
||||
// ✅ 精简测试:4个核心用例覆盖所有分支
|
||||
describe('getImageDimensions', () => {
|
||||
it('should return dimensions for valid File object'); // 成功路径 - File
|
||||
it('should return dimensions for valid data URI'); // 成功路径 - String
|
||||
it('should return undefined for invalid inputs'); // 输入验证分支
|
||||
it('should return undefined when image fails to load'); // 错误处理分支
|
||||
});
|
||||
```
|
||||
|
||||
**分支覆盖策略**
|
||||
|
||||
1. **成功路径** - 每种输入类型1个测试即可
|
||||
2. **边界条件** - 合并类似场景到单个测试
|
||||
3. **错误处理** - 测试代表性错误即可
|
||||
4. **业务逻辑** - 覆盖所有if/else分支
|
||||
|
||||
**合理测试数量**
|
||||
- 简单工具函数:2-5个测试
|
||||
- 复杂业务逻辑:5-10个测试
|
||||
- 核心安全功能:适当增加,但避免重复路径
|
||||
|
||||
### 错误处理测试:测试"行为"而非"文本"
|
||||
|
||||
**核心原则**: 测试应该验证程序在错误发生时的行为是可预测的,而不是验证易变的错误信息文本。
|
||||
|
||||
#### ✅ 推荐的错误测试方式
|
||||
#### 推荐的错误测试方式
|
||||
|
||||
```typescript
|
||||
// ✅ 测试是否抛出错误
|
||||
it('should throw error when invalid input provided', () => {
|
||||
expect(() => processInput(null)).toThrow();
|
||||
});
|
||||
// ✅ 测试错误类型和属性
|
||||
expect(() => validateUser({})).toThrow(ValidationError);
|
||||
expect(() => processPayment({})).toThrow(expect.objectContaining({
|
||||
code: 'INVALID_PAYMENT_DATA',
|
||||
statusCode: 400,
|
||||
}));
|
||||
|
||||
// ✅ 测试错误类型(最推荐)
|
||||
it('should throw ValidationError for invalid data', () => {
|
||||
expect(() => validateUser({})).toThrow(ValidationError);
|
||||
});
|
||||
|
||||
// ✅ 测试错误属性而非消息文本
|
||||
it('should throw error with correct error code', () => {
|
||||
expect(() => processPayment({})).toThrow(
|
||||
expect.objectContaining({
|
||||
code: 'INVALID_PAYMENT_DATA',
|
||||
statusCode: 400,
|
||||
}),
|
||||
);
|
||||
});
|
||||
// ❌ 避免测试具体错误文本
|
||||
expect(() => processUser({})).toThrow('用户数据不能为空,请检查输入参数');
|
||||
```
|
||||
|
||||
#### ❌ 应避免的做法
|
||||
|
||||
```typescript
|
||||
// ❌ 过度依赖具体错误信息文本
|
||||
it('should throw specific error message', () => {
|
||||
expect(() => processUser({})).toThrow('用户数据不能为空,请检查输入参数');
|
||||
// 这种测试很脆弱,错误文案稍有修改就会失败
|
||||
});
|
||||
```
|
||||
|
||||
#### 🎯 例外情况:何时可以测试错误信息
|
||||
|
||||
```typescript
|
||||
// ✅ 测试标准 API 错误(这是契约的一部分)
|
||||
it('should return proper HTTP error for API', () => {
|
||||
expect(response.statusCode).toBe(400);
|
||||
expect(response.error).toBe('Bad Request');
|
||||
});
|
||||
|
||||
// ✅ 测试错误信息的关键部分(使用正则)
|
||||
it('should include field name in validation error', () => {
|
||||
expect(() => validateField('email', '')).toThrow(/email/i);
|
||||
});
|
||||
```
|
||||
|
||||
### 疑难解答:警惕模块污染 🚨
|
||||
### 疑难解答:警惕模块污染
|
||||
|
||||
**识别信号**: 当你的测试出现以下"灵异"现象时,优先怀疑模块污染:
|
||||
|
||||
@@ -341,55 +378,25 @@ it('should include field name in validation error', () => {
|
||||
#### 典型场景:动态 Mock 同一模块
|
||||
|
||||
```typescript
|
||||
// ❌ 容易出现模块污染的写法
|
||||
describe('ConfigService', () => {
|
||||
it('should work in development mode', async () => {
|
||||
vi.doMock('./config', () => ({ isDev: true }));
|
||||
const { getSettings } = await import('./configService'); // 第一次加载
|
||||
expect(getSettings().debugMode).toBe(true);
|
||||
});
|
||||
|
||||
it('should work in production mode', async () => {
|
||||
vi.doMock('./config', () => ({ isDev: false }));
|
||||
const { getSettings } = await import('./configService'); // 可能使用缓存的旧版本!
|
||||
expect(getSettings().debugMode).toBe(false); // ❌ 可能失败
|
||||
});
|
||||
// ❌ 问题:动态Mock同一模块
|
||||
it('dev mode', async () => {
|
||||
vi.doMock('./config', () => ({ isDev: true }));
|
||||
const { getSettings } = await import('./service'); // 可能使用缓存
|
||||
});
|
||||
|
||||
// ✅ 使用 resetModules 解决模块污染
|
||||
describe('ConfigService', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules(); // 清除模块缓存,确保每个测试都是干净的环境
|
||||
});
|
||||
|
||||
it('should work in development mode', async () => {
|
||||
vi.doMock('./config', () => ({ isDev: true }));
|
||||
const { getSettings } = await import('./configService');
|
||||
expect(getSettings().debugMode).toBe(true);
|
||||
});
|
||||
|
||||
it('should work in production mode', async () => {
|
||||
vi.doMock('./config', () => ({ isDev: false }));
|
||||
const { getSettings } = await import('./configService');
|
||||
expect(getSettings().debugMode).toBe(false); // ✅ 测试通过
|
||||
});
|
||||
// ✅ 解决:清除模块缓存
|
||||
beforeEach(() => {
|
||||
vi.resetModules(); // 确保每个测试都是干净环境
|
||||
});
|
||||
```
|
||||
|
||||
#### 🔧 排查和解决步骤
|
||||
**记住**: `vi.resetModules()` 是解决测试"灵异"失败的终极武器。
|
||||
|
||||
1. **识别问题**: 测试失败时,首先问自己:"是否有多个测试在 Mock 同一个模块?"
|
||||
2. **添加隔离**: 在 `beforeEach` 中添加 `vi.resetModules()`
|
||||
3. **验证修复**: 重新运行测试,确认问题解决
|
||||
|
||||
**记住**: `vi.resetModules()` 是解决测试"灵异"失败的终极武器,当常规调试方法都无效时,它往往能一针见血地解决问题。
|
||||
|
||||
## 📂 测试文件组织
|
||||
## 测试文件组织
|
||||
|
||||
### 文件命名约定
|
||||
|
||||
- **客户端测试**: `*.test.ts`, `*.test.tsx` (任意位置)
|
||||
- **服务端测试**: `src/database/models/**/*.test.ts`, `src/database/server/**/*.test.ts` (限定路径)
|
||||
`*.test.ts`, `*.test.tsx` (任意位置)
|
||||
|
||||
### 测试文件组织风格
|
||||
|
||||
@@ -406,7 +413,10 @@ src/components/Button/
|
||||
└── index.test.tsx # 测试文件
|
||||
```
|
||||
|
||||
## 🛠️ 测试调试技巧
|
||||
- 也有少数情况会统一放到 `__tests__` 文件夹, 例如 `packages/database/src/models/__tests__`
|
||||
- 测试使用的辅助文件放到 fixtures 文件夹
|
||||
|
||||
## 测试调试技巧
|
||||
|
||||
### 测试调试步骤
|
||||
|
||||
@@ -415,40 +425,28 @@ src/components/Button/
|
||||
3. **分析错误**: 仔细阅读错误信息、堆栈跟踪和最近的文件修改记录
|
||||
4. **添加调试**: 在测试中添加 `console.log` 了解执行流程
|
||||
|
||||
### TypeScript 类型处理 📝
|
||||
### TypeScript 类型处理
|
||||
|
||||
在测试中,为了提高编写效率和可读性,可以适当放宽 TypeScript 类型检测:
|
||||
|
||||
#### ✅ 推荐的类型放宽策略
|
||||
#### 推荐的类型放宽策略
|
||||
|
||||
```typescript
|
||||
// ✅ 使用非空断言访问测试中确定存在的属性
|
||||
// 使用非空断言访问测试中确定存在的属性
|
||||
const result = await someFunction();
|
||||
expect(result!.data).toBeDefined();
|
||||
expect(result!.status).toBe('success');
|
||||
|
||||
// ✅ 使用 any 类型简化复杂的 Mock 设置
|
||||
// 使用 any 类型简化复杂的 Mock 设置
|
||||
const mockStream = new ReadableStream() as any;
|
||||
mockStream.toReadableStream = () => mockStream;
|
||||
|
||||
// ✅ 使用中括号访问私有属性和方法(推荐)
|
||||
class MyClass {
|
||||
private _cache = new Map();
|
||||
private getFromCache(key: string) { /* ... */ }
|
||||
}
|
||||
|
||||
const instance = new MyClass();
|
||||
|
||||
// 推荐:使用中括号访问私有成员
|
||||
await instance['getFromCache']('test-key');
|
||||
expect(instance['_cache'].size).toBe(1);
|
||||
|
||||
// 避免:使用 as any 访问私有成员
|
||||
await (instance as any).getFromCache('test-key'); // ❌ 不推荐
|
||||
expect((instance as any)._cache.size).toBe(1); // ❌ 不推荐
|
||||
// 访问私有成员
|
||||
await instance['getFromCache']('key'); // 推荐中括号
|
||||
await (instance as any).getFromCache('key'); // 避免as any
|
||||
```
|
||||
|
||||
#### 🎯 适用场景
|
||||
#### 适用场景
|
||||
|
||||
- **Mock 对象**: 对于测试用的 Mock 数据,使用 `as any` 避免复杂的类型定义
|
||||
- **第三方库**: 处理复杂的第三方库类型时,适当使用 `any` 提高效率
|
||||
@@ -456,44 +454,31 @@ expect((instance as any)._cache.size).toBe(1); // ❌ 不推荐
|
||||
- **私有成员访问**: 优先使用中括号 `instance['privateMethod']()` 而不是 `(instance as any).privateMethod()`
|
||||
- **临时调试**: 快速编写测试时,先用 `any` 保证功能,后续可选择性地优化类型
|
||||
|
||||
#### ⚠️ 注意事项
|
||||
#### 注意事项
|
||||
|
||||
- **适度使用**: 不要过度依赖 `any`,核心业务逻辑的类型仍应保持严格
|
||||
- **私有成员访问优先级**: 中括号访问 > `as any` 转换,保持更好的类型安全性
|
||||
- **文档说明**: 对于使用 `any` 的复杂场景,添加注释说明原因
|
||||
- **测试覆盖**: 确保即使使用了 `any`,测试仍能有效验证功能正确性
|
||||
|
||||
### 检查最近修改记录 🔍
|
||||
|
||||
系统性地检查相关文件的修改历史是问题定位的关键步骤。
|
||||
### 检查最近修改记录
|
||||
|
||||
#### 三步检查法
|
||||
**核心原则**:测试突然失败时,优先检查最近的代码修改。
|
||||
|
||||
**Step 1: 查看当前状态**
|
||||
#### 快速检查方法
|
||||
|
||||
```bash
|
||||
git status # 查看未提交的修改
|
||||
git diff path/to/component.test.ts | cat # 查看测试文件修改
|
||||
git diff path/to/component.ts | cat # 查看实现文件修改
|
||||
git status # 查看当前修改状态
|
||||
git diff HEAD -- '*.test.*' # 检查测试文件改动
|
||||
git diff main...HEAD # 对比主分支差异
|
||||
gh pr diff # 查看PR中的所有改动
|
||||
```
|
||||
|
||||
**Step 2: 查看提交历史**
|
||||
#### 常见原因与解决
|
||||
|
||||
```bash
|
||||
git log --pretty=format:"%h %ad %s" --date=relative -3 path/to/component.ts | cat
|
||||
```
|
||||
|
||||
**Step 3: 查看具体修改内容**
|
||||
|
||||
```bash
|
||||
git show HEAD -- path/to/component.ts | cat # 查看最新提交的修改
|
||||
```
|
||||
|
||||
#### 时间相关性判断
|
||||
|
||||
- **24小时内的提交**: 🔴 **高度相关** - 很可能是直接原因
|
||||
- **1-7天内的提交**: 🟡 **中等相关** - 需要仔细分析
|
||||
- **超过1周的提交**: ⚪ **低相关性** - 除非重大重构
|
||||
- **最新提交引入bug** → 检查并修复实现代码
|
||||
- **分支代码滞后** → `git rebase main` 同步主分支
|
||||
|
||||
## 特殊场景的测试
|
||||
|
||||
@@ -502,9 +487,9 @@ git show HEAD -- path/to/component.ts | cat # 查看最新提交的修改
|
||||
- [Electron IPC 接口测试策略](mdc:./electron-ipc-test.mdc)
|
||||
- [数据库 Model 测试指南](mdc:./db-model-test.mdc)
|
||||
|
||||
## 🎯 核心要点
|
||||
## 核心要点
|
||||
|
||||
- **命令格式**: 使用 `npx vitest run --config [config-file]` 并指定文件过滤
|
||||
- **命令格式**: 使用 `bunx vitest run --silent='passed-only'` 并指定文件过滤
|
||||
- **修复原则**: 失败1-2次后寻求帮助,测试命名关注行为而非实现细节
|
||||
- **调试流程**: 复现 → 分析 → 假设 → 修复 → 验证 → 总结
|
||||
- **文件组织**: 优先在现有 `describe` 块中添加测试,避免创建冗余顶级块
|
||||
|
||||
Reference in New Issue
Block a user