frontend-testing
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseDify Frontend Testing Skill
Dify前端测试技能
This skill enables Claude to generate high-quality, comprehensive frontend tests for the Dify project following established conventions and best practices.
⚠️ Authoritative Source: This skill is derived from. Use Vitest mock/timer APIs (web/docs/test.md).vi.*
该技能可让Claude遵循既定规范和最佳实践,为Dify项目生成高质量、全面的前端测试用例。
⚠️ 权威来源:本技能源自。请使用Vitest的Mock/定时器API(web/docs/test.md)。vi.*
When to Apply This Skill
适用场景
Apply this skill when the user:
- Asks to write tests for a component, hook, or utility
- Asks to review existing tests for completeness
- Mentions Vitest, React Testing Library, RTL, or spec files
- Requests test coverage improvement
- Uses output as context
pnpm analyze-component - Mentions testing, unit tests, or integration tests for frontend code
- Wants to understand testing patterns in the Dify codebase
Do NOT apply when:
- User is asking about backend/API tests (Python/pytest)
- User is asking about E2E tests (Playwright/Cypress)
- User is only asking conceptual questions without code context
当用户有以下需求时,可应用本技能:
- 请求为组件、Hook或工具函数编写测试用例
- 请求评审现有测试用例的完整性
- 提及Vitest、React Testing Library、RTL或测试规范文件(spec files)
- 请求提升测试覆盖率
- 以的输出作为上下文
pnpm analyze-component - 提及前端代码的测试、单元测试或集成测试
- 希望了解Dify代码库中的测试模式
请勿应用本技能的场景:
- 用户询问后端/API测试(Python/pytest)
- 用户询问端到端测试(Playwright/Cypress)
- 用户仅询问无代码上下文的概念性问题
Quick Reference
快速参考
Tech Stack
技术栈
| Tool | Version | Purpose |
|---|---|---|
| Vitest | 4.0.16 | Test runner |
| React Testing Library | 16.0 | Component testing |
| jsdom | - | Test environment |
| nock | 14.0 | HTTP mocking |
| TypeScript | 5.x | Type safety |
| 工具 | 版本 | 用途 |
|---|---|---|
| Vitest | 4.0.16 | 测试运行器 |
| React Testing Library | 16.0 | 组件测试 |
| jsdom | - | 测试环境 |
| nock | 14.0 | HTTP模拟 |
| TypeScript | 5.x | 类型安全 |
Key Commands
关键命令
bash
undefinedbash
undefinedRun all tests
运行所有测试
pnpm test
pnpm test
Watch mode
监听模式
pnpm test:watch
pnpm test:watch
Run specific file
运行指定文件的测试
pnpm test path/to/file.spec.tsx
pnpm test path/to/file.spec.tsx
Generate coverage report
生成覆盖率报告
pnpm test:coverage
pnpm test:coverage
Analyze component complexity
分析组件复杂度
pnpm analyze-component <path>
pnpm analyze-component <path>
Review existing test
评审现有测试用例
pnpm analyze-component <path> --review
undefinedpnpm analyze-component <path> --review
undefinedFile Naming
文件命名规范
- Test files: (same directory as component)
ComponentName.spec.tsx - Integration tests: directory
web/__tests__/
- 测试文件:(与组件位于同一目录)
ComponentName.spec.tsx - 集成测试:放置在目录下
web/__tests__/
Test Structure Template
测试结构模板
typescript
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
import Component from './index'
// ✅ Import real project components (DO NOT mock these)
// import Loading from '@/app/components/base/loading'
// import { ChildComponent } from './child-component'
// ✅ Mock external dependencies only
vi.mock('@/service/api')
vi.mock('next/navigation', () => ({
useRouter: () => ({ push: vi.fn() }),
usePathname: () => '/test',
}))
// ✅ Zustand stores: Use real stores (auto-mocked globally)
// Set test state with: useAppStore.setState({ ... })
// Shared state for mocks (if needed)
let mockSharedState = false
describe('ComponentName', () => {
beforeEach(() => {
vi.clearAllMocks() // ✅ Reset mocks BEFORE each test
mockSharedState = false // ✅ Reset shared state
})
// Rendering tests (REQUIRED)
describe('Rendering', () => {
it('should render without crashing', () => {
// Arrange
const props = { title: 'Test' }
// Act
render(<Component {...props} />)
// Assert
expect(screen.getByText('Test')).toBeInTheDocument()
})
})
// Props tests (REQUIRED)
describe('Props', () => {
it('should apply custom className', () => {
render(<Component className="custom" />)
expect(screen.getByRole('button')).toHaveClass('custom')
})
})
// User Interactions
describe('User Interactions', () => {
it('should handle click events', () => {
const handleClick = vi.fn()
render(<Component onClick={handleClick} />)
fireEvent.click(screen.getByRole('button'))
expect(handleClick).toHaveBeenCalledTimes(1)
})
})
// Edge Cases (REQUIRED)
describe('Edge Cases', () => {
it('should handle null data', () => {
render(<Component data={null} />)
expect(screen.getByText(/no data/i)).toBeInTheDocument()
})
it('should handle empty array', () => {
render(<Component items={[]} />)
expect(screen.getByText(/empty/i)).toBeInTheDocument()
})
})
})typescript
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
import Component from './index'
// ✅ 直接导入项目真实组件(请勿Mock这些组件)
// import Loading from '@/app/components/base/loading'
// import { ChildComponent } from './child-component'
// ✅ 仅Mock外部依赖
vi.mock('@/service/api')
vi.mock('next/navigation', () => ({
useRouter: () => ({ push: vi.fn() }),
usePathname: () => '/test',
}))
// ✅ Zustand状态管理:使用真实的状态存储(已全局自动Mock)
// 使用以下方式设置测试状态:useAppStore.setState({ ... })
// 用于Mock的共享状态(如有需要)
let mockSharedState = false
describe('ComponentName', () => {
beforeEach(() => {
vi.clearAllMocks() // ✅ 在每个测试前重置Mock
mockSharedState = false // ✅ 重置共享状态
})
// 渲染测试(必填)
describe('Rendering', () => {
it('should render without crashing', () => {
// 准备(Arrange)
const props = { title: 'Test' }
// 执行(Act)
render(<Component {...props} />)
// 断言(Assert)
expect(screen.getByText('Test')).toBeInTheDocument()
})
})
// Props测试(必填)
describe('Props', () => {
it('should apply custom className', () => {
render(<Component className="custom" />)
expect(screen.getByRole('button')).toHaveClass('custom')
})
})
// 用户交互测试
describe('User Interactions', () => {
it('should handle click events', () => {
const handleClick = vi.fn()
render(<Component onClick={handleClick} />)
fireEvent.click(screen.getByRole('button'))
expect(handleClick).toHaveBeenCalledTimes(1)
})
})
// 边界情况测试(必填)
describe('Edge Cases', () => {
it('should handle null data', () => {
render(<Component data={null} />)
expect(screen.getByText(/no data/i)).toBeInTheDocument()
})
it('should handle empty array', () => {
render(<Component items={[]} />)
expect(screen.getByText(/empty/i)).toBeInTheDocument()
})
})
})Testing Workflow (CRITICAL)
测试工作流(至关重要)
⚠️ Incremental Approach Required
⚠️ 必须采用增量式方法
NEVER generate all test files at once. For complex components or multi-file directories:
- Analyze & Plan: List all files, order by complexity (simple → complex)
- Process ONE at a time: Write test → Run test → Fix if needed → Next
- Verify before proceeding: Do NOT continue to next file until current passes
For each file:
┌────────────────────────────────────────┐
│ 1. Write test │
│ 2. Run: pnpm test <file>.spec.tsx │
│ 3. PASS? → Mark complete, next file │
│ FAIL? → Fix first, then continue │
└────────────────────────────────────────┘切勿一次性生成所有测试文件。对于复杂组件或多文件目录:
- 分析与规划:列出所有文件,按复杂度排序(简单 → 复杂)
- 逐个处理:编写测试用例 → 运行测试 → 如有问题则修复 → 处理下一个文件
- 验证后再推进:当前文件测试通过前,不要继续处理下一个文件
对于每个文件:
┌────────────────────────────────────────┐
│ 1. 编写测试用例 │
│ 2. 运行:pnpm test <file>.spec.tsx │
│ 3. 通过?→ 标记完成,处理下一个文件 │
│ 失败?→ 先修复问题,再继续推进 │
└────────────────────────────────────────┘Complexity-Based Order
基于复杂度的处理顺序
Process in this order for multi-file testing:
- 🟢 Utility functions (simplest)
- 🟢 Custom hooks
- 🟡 Simple components (presentational)
- 🟡 Medium components (state, effects)
- 🔴 Complex components (API, routing)
- 🔴 Integration tests (index files - last)
多文件测试时,请按以下顺序处理:
- 🟢 工具函数(最简单)
- 🟢 自定义Hook
- 🟡 简单组件(展示型)
- 🟡 中等复杂度组件(包含状态、副作用)
- 🔴 复杂组件(涉及API、路由)
- 🔴 集成测试(入口文件 - 最后处理)
When to Refactor First
何时需要先重构
- Complexity > 50: Break into smaller pieces before testing
- 500+ lines: Consider splitting before testing
- Many dependencies: Extract logic into hooks first
📖 Seefor complete workflow details and todo list format.references/workflow.md
- 复杂度 > 50:在测试前拆分为更小的模块
- 代码行数 > 500:考虑拆分后再测试
- 依赖项过多:先将逻辑提取到Hook中
📖 完整工作流细节和待办事项格式请参考。references/workflow.md
Testing Strategy
测试策略
Path-Level Testing (Directory Testing)
路径级测试(目录测试)
When assigned to test a directory/path, test ALL content within that path:
- Test all components, hooks, utilities in the directory (not just file)
index - Use incremental approach: one file at a time, verify each before proceeding
- Goal: 100% coverage of ALL files in the directory
当需要测试某个目录/路径时,需测试该路径下的所有内容:
- 测试目录下的所有组件、Hook和工具函数(不只是文件)
index - 采用增量式方法:逐个处理文件,每个文件验证通过后再推进
- 目标:实现目录下所有文件的100%测试覆盖率
Integration Testing First
优先进行集成测试
Prefer integration testing when writing tests for a directory:
- ✅ Import real project components directly (including base components and siblings)
- ✅ Only mock: API services (),
@/service/*, complex context providersnext/navigation - ❌ DO NOT mock base components ()
@/app/components/base/* - ❌ DO NOT mock sibling/child components in the same directory
See Test Structure Template for correct import/mock patterns.
为目录编写测试时,优先选择集成测试:
- ✅ 直接导入项目真实组件(包括基础组件和同级组件)
- ✅ 仅Mock:API服务()、
@/service/*、复杂的上下文提供者next/navigation - ❌ 请勿Mock基础组件()
@/app/components/base/* - ❌ 请勿Mock同一目录下的同级/子组件
正确的导入/Mock模式请参考测试结构模板。
Core Principles
核心原则
1. AAA Pattern (Arrange-Act-Assert)
1. AAA模式(准备-执行-断言)
Every test should clearly separate:
- Arrange: Setup test data and render component
- Act: Perform user actions
- Assert: Verify expected outcomes
每个测试用例应清晰分离三个阶段:
- 准备(Arrange):设置测试数据并渲染组件
- 执行(Act):执行用户操作
- 断言(Assert):验证预期结果
2. Black-Box Testing
2. 黑盒测试
- Test observable behavior, not implementation details
- Use semantic queries (getByRole, getByLabelText)
- Avoid testing internal state directly
- Prefer pattern matching over hardcoded strings in assertions:
typescript
// ❌ Avoid: hardcoded text assertions
expect(screen.getByText('Loading...')).toBeInTheDocument()
// ✅ Better: role-based queries
expect(screen.getByRole('status')).toBeInTheDocument()
// ✅ Better: pattern matching
expect(screen.getByText(/loading/i)).toBeInTheDocument()- 测试可观察的行为,而非实现细节
- 使用语义化查询(getByRole、getByLabelText)
- 避免直接测试内部状态
- 断言中优先使用模式匹配而非硬编码字符串:
typescript
// ❌ 不推荐:硬编码文本断言
expect(screen.getByText('Loading...')).toBeInTheDocument()
// ✅ 推荐:基于角色的查询
expect(screen.getByRole('status')).toBeInTheDocument()
// ✅ 推荐:模式匹配
expect(screen.getByText(/loading/i)).toBeInTheDocument()3. Single Behavior Per Test
3. 每个测试验证单一行为
Each test verifies ONE user-observable behavior:
typescript
// ✅ Good: One behavior
it('should disable button when loading', () => {
render(<Button loading />)
expect(screen.getByRole('button')).toBeDisabled()
})
// ❌ Bad: Multiple behaviors
it('should handle loading state', () => {
render(<Button loading />)
expect(screen.getByRole('button')).toBeDisabled()
expect(screen.getByText('Loading...')).toBeInTheDocument()
expect(screen.getByRole('button')).toHaveClass('loading')
})每个测试用例仅验证一个用户可观察的行为:
typescript
// ✅ 良好:单一行为
it('should disable button when loading', () => {
render(<Button loading />)
expect(screen.getByRole('button')).toBeDisabled()
})
// ❌ 不佳:多个行为
it('should handle loading state', () => {
render(<Button loading />)
expect(screen.getByRole('button')).toBeDisabled()
expect(screen.getByText('Loading...')).toBeInTheDocument()
expect(screen.getByRole('button')).toHaveClass('loading')
})4. Semantic Naming
4. 语义化命名
Use :
should <behavior> when <condition>typescript
it('should show error message when validation fails')
it('should call onSubmit when form is valid')
it('should disable input when isReadOnly is true')使用的命名格式:
should <行为> when <条件>typescript
it('should show error message when validation fails')
it('should call onSubmit when form is valid')
it('should disable input when isReadOnly is true')Required Test Scenarios
必填测试场景
Always Required (All Components)
所有组件必填测试
- Rendering: Component renders without crashing
- Props: Required props, optional props, default values
- Edge Cases: null, undefined, empty values, boundary conditions
- 渲染测试:组件可正常渲染且无崩溃
- Props测试:必填Props、可选Props、默认值
- 边界情况测试:null、undefined、空值、边界条件
Conditional (When Present)
按需测试(对应特性存在时)
| Feature | Test Focus |
|---|---|
| Initial state, transitions, cleanup |
| Execution, dependencies, cleanup |
| Event handlers | All onClick, onChange, onSubmit, keyboard |
| API calls | Loading, success, error states |
| Routing | Navigation, params, query strings |
| Referential equality |
| Context | Provider values, consumer behavior |
| Forms | Validation, submission, error display |
| 特性 | 测试重点 |
|---|---|
| 初始状态、状态转换、清理逻辑 |
| 执行时机、依赖项、清理逻辑 |
| 事件处理器 | 所有onClick、onChange、onSubmit、键盘事件 |
| API调用 | 加载中、成功、错误状态 |
| 路由 | 导航、参数、查询字符串 |
| 引用相等性 |
| 上下文 | 提供者值、消费者行为 |
| 表单 | 验证、提交、错误提示 |
Coverage Goals (Per File)
覆盖率目标(每个文件)
For each test file generated, aim for:
- ✅ 100% function coverage
- ✅ 100% statement coverage
- ✅ >95% branch coverage
- ✅ >95% line coverage
Note: For multi-file directories, process one file at a time with full coverage each. See.references/workflow.md
对于生成的每个测试文件,目标是:
- ✅ 100% 函数覆盖率
- ✅ 100% 语句覆盖率
- ✅ >95% 分支覆盖率
- ✅ >95% 行覆盖率
注意:对于多文件目录,逐个处理文件,每个文件都要达到完整覆盖率。详情请参考。references/workflow.md
Detailed Guides
详细指南
For more detailed information, refer to:
- - Incremental testing workflow (MUST READ for multi-file testing)
references/workflow.md - - Mock patterns, Zustand store testing, and best practices
references/mocking.md - - Async operations and API calls
references/async-testing.md - - Workflow, Dataset, Configuration testing
references/domain-components.md - - Frequently used testing patterns
references/common-patterns.md - - Test generation checklist and validation steps
references/checklist.md
如需更多详细信息,请参考:
- - 增量式测试工作流(多文件测试时必读)
references/workflow.md - - Mock模式、Zustand状态存储测试及最佳实践
references/mocking.md - - 异步操作和API调用测试
references/async-testing.md - - 工作流、数据集、配置测试
references/domain-components.md - - 常用测试模式
references/common-patterns.md - - 测试生成检查清单和验证步骤
references/checklist.md
Authoritative References
权威参考
Primary Specification (MUST follow)
主要规范(必须遵循)
- - The canonical testing specification. This skill is derived from this document.
web/docs/test.md
- - 官方测试规范。本技能即源自该文档。
web/docs/test.md
Reference Examples in Codebase
代码库中的参考示例
- - Utility function tests
web/utils/classnames.spec.ts - - Component tests
web/app/components/base/button/index.spec.tsx - - Mock factory example
web/__mocks__/provider-context.ts
- - 工具函数测试示例
web/utils/classnames.spec.ts - - 组件测试示例
web/app/components/base/button/index.spec.tsx - - Mock工厂示例
web/__mocks__/provider-context.ts
Project Configuration
项目配置
- - Vitest configuration
web/vitest.config.ts - - Test environment setup
web/vitest.setup.ts - - Component analysis tool
web/scripts/analyze-component.js - Modules are not mocked automatically. Global mocks live in (for example
web/vitest.setup.ts,react-i18next); mock other modules likenext/imageorkylocally in test files.mime
- - Vitest配置文件
web/vitest.config.ts - - 测试环境设置文件
web/vitest.setup.ts - - 组件分析工具
web/scripts/analyze-component.js - 模块不会被自动Mock。全局Mock位于中(例如
web/vitest.setup.ts、react-i18next);在测试文件中本地Mock其他模块,如next/image或ky。mime