frontend-testing

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Dify 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
web/docs/test.md
. Use Vitest mock/timer APIs (
vi.*
).
该技能可让Claude遵循既定规范和最佳实践,为Dify项目生成高质量、全面的前端测试用例。
⚠️ 权威来源:本技能源自
web/docs/test.md
。请使用Vitest的Mock/定时器API(
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
    pnpm analyze-component
    output as context
  • 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或工具函数编写测试用例
  • 请求评审现有测试用例的完整性
  • 提及VitestReact Testing LibraryRTL测试规范文件(spec files)
  • 请求提升测试覆盖率
  • pnpm analyze-component
    的输出作为上下文
  • 提及前端代码的测试单元测试集成测试
  • 希望了解Dify代码库中的测试模式
请勿应用本技能的场景:
  • 用户询问后端/API测试(Python/pytest)
  • 用户询问端到端测试(Playwright/Cypress)
  • 用户仅询问无代码上下文的概念性问题

Quick Reference

快速参考

Tech Stack

技术栈

ToolVersionPurpose
Vitest4.0.16Test runner
React Testing Library16.0Component testing
jsdom-Test environment
nock14.0HTTP mocking
TypeScript5.xType safety
工具版本用途
Vitest4.0.16测试运行器
React Testing Library16.0组件测试
jsdom-测试环境
nock14.0HTTP模拟
TypeScript5.x类型安全

Key Commands

关键命令

bash
undefined
bash
undefined

Run 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
undefined
pnpm analyze-component <path> --review
undefined

File Naming

文件命名规范

  • Test files:
    ComponentName.spec.tsx
    (same directory as component)
  • Integration tests:
    web/__tests__/
    directory
  • 测试文件:
    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:
  1. Analyze & Plan: List all files, order by complexity (simple → complex)
  2. Process ONE at a time: Write test → Run test → Fix if needed → Next
  3. 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. 逐个处理:编写测试用例 → 运行测试 → 如有问题则修复 → 处理下一个文件
  3. 验证后再推进:当前文件测试通过前,不要继续处理下一个文件
对于每个文件:
  ┌────────────────────────────────────────┐
  │ 1. 编写测试用例                          │
  │ 2. 运行:pnpm test <file>.spec.tsx      │
  │ 3. 通过?→ 标记完成,处理下一个文件    │
  │    失败?→ 先修复问题,再继续推进    │
  └────────────────────────────────────────┘

Complexity-Based Order

基于复杂度的处理顺序

Process in this order for multi-file testing:
  1. 🟢 Utility functions (simplest)
  2. 🟢 Custom hooks
  3. 🟡 Simple components (presentational)
  4. 🟡 Medium components (state, effects)
  5. 🔴 Complex components (API, routing)
  6. 🔴 Integration tests (index files - last)
多文件测试时,请按以下顺序处理:
  1. 🟢 工具函数(最简单)
  2. 🟢 自定义Hook
  3. 🟡 简单组件(展示型)
  4. 🟡 中等复杂度组件(包含状态、副作用)
  5. 🔴 复杂组件(涉及API、路由)
  6. 🔴 集成测试(入口文件 - 最后处理)

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
📖 See
references/workflow.md
for complete workflow details and todo list format.
  • 复杂度 > 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
    index
    file)
  • 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/*
    ),
    next/navigation
    , complex context providers
  • 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)

所有组件必填测试

  1. Rendering: Component renders without crashing
  2. Props: Required props, optional props, default values
  3. Edge Cases: null, undefined, empty values, boundary conditions
  1. 渲染测试:组件可正常渲染且无崩溃
  2. Props测试:必填Props、可选Props、默认值
  3. 边界情况测试:null、undefined、空值、边界条件

Conditional (When Present)

按需测试(对应特性存在时)

FeatureTest Focus
useState
Initial state, transitions, cleanup
useEffect
Execution, dependencies, cleanup
Event handlersAll onClick, onChange, onSubmit, keyboard
API callsLoading, success, error states
RoutingNavigation, params, query strings
useCallback
/
useMemo
Referential equality
ContextProvider values, consumer behavior
FormsValidation, submission, error display
特性测试重点
useState
初始状态、状态转换、清理逻辑
useEffect
执行时机、依赖项、清理逻辑
事件处理器所有onClick、onChange、onSubmit、键盘事件
API调用加载中、成功、错误状态
路由导航、参数、查询字符串
useCallback
/
useMemo
引用相等性
上下文提供者值、消费者行为
表单验证、提交、错误提示

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:
  • references/workflow.md
    - Incremental testing workflow (MUST READ for multi-file testing)
  • references/mocking.md
    - Mock patterns, Zustand store testing, and best practices
  • references/async-testing.md
    - Async operations and API calls
  • references/domain-components.md
    - Workflow, Dataset, Configuration testing
  • references/common-patterns.md
    - Frequently used testing patterns
  • references/checklist.md
    - Test generation checklist and validation steps
如需更多详细信息,请参考:
  • references/workflow.md
    - 增量式测试工作流(多文件测试时必读)
  • references/mocking.md
    - Mock模式、Zustand状态存储测试及最佳实践
  • references/async-testing.md
    - 异步操作和API调用测试
  • references/domain-components.md
    - 工作流、数据集、配置测试
  • references/common-patterns.md
    - 常用测试模式
  • references/checklist.md
    - 测试生成检查清单和验证步骤

Authoritative References

权威参考

Primary Specification (MUST follow)

主要规范(必须遵循)

  • web/docs/test.md
    - The canonical testing specification. This skill is derived from this document.
  • web/docs/test.md
    - 官方测试规范。本技能即源自该文档。

Reference Examples in Codebase

代码库中的参考示例

  • web/utils/classnames.spec.ts
    - Utility function tests
  • web/app/components/base/button/index.spec.tsx
    - Component tests
  • web/__mocks__/provider-context.ts
    - Mock factory example
  • web/utils/classnames.spec.ts
    - 工具函数测试示例
  • web/app/components/base/button/index.spec.tsx
    - 组件测试示例
  • web/__mocks__/provider-context.ts
    - Mock工厂示例

Project Configuration

项目配置

  • web/vitest.config.ts
    - Vitest configuration
  • web/vitest.setup.ts
    - Test environment setup
  • web/scripts/analyze-component.js
    - Component analysis tool
  • Modules are not mocked automatically. Global mocks live in
    web/vitest.setup.ts
    (for example
    react-i18next
    ,
    next/image
    ); mock other modules like
    ky
    or
    mime
    locally in test files.
  • web/vitest.config.ts
    - Vitest配置文件
  • web/vitest.setup.ts
    - 测试环境设置文件
  • web/scripts/analyze-component.js
    - 组件分析工具
  • 模块不会被自动Mock。全局Mock位于
    web/vitest.setup.ts
    中(例如
    react-i18next
    next/image
    );在测试文件中本地Mock其他模块,如
    ky
    mime