Dify Frontend Testing Skill
This skill enables Codex 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.*).
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 (Cucumber + Playwright under
e2e/)
- User is only asking conceptual questions without code context
Quick Reference
Key Commands
Run these commands from web/. From the repository root, prefix them with pnpm -C web.
# Run all tests
pnpm test
# Watch mode
pnpm test --watch
# Run specific file
pnpm test path/to/file.spec.tsx
# Generate coverage report
pnpm test --coverage
# Analyze component complexity
pnpm analyze-component <path>
# Review existing test
pnpm analyze-component <path> --review
File Naming
- Test files:
ComponentName.spec.tsx inside a same-level __tests__/ directory
- Placement rule: Component, hook, and utility tests must live in a sibling
__tests__/ folder at the same level as the source under test. For example, foo/index.tsx maps to foo/__tests__/index.spec.tsx, and foo/bar.ts maps to foo/__tests__/bar.spec.ts.
- Integration tests:
web/__tests__/ directory
Test Structure Template
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 />)
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()
})
})
})
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 │
└────────────────────────────────────────┘
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)
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.
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
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/*) or dify-ui primitives (@langgenius/dify-ui/*)
- ❌ DO NOT mock sibling/child components in the same directory
See Test Structure Template for correct import/mock patterns.
nuqs Query State Testing (Required for URL State Hooks)
When a component or hook uses useQueryState / useQueryStates:
- ✅ Use
NuqsTestingAdapter (prefer shared helpers in web/test/nuqs-testing.tsx)
- ✅ Assert URL synchronization via
onUrlUpdate (searchParams, options.history)
- ✅ For custom parsers (
createParser), keep parse and serialize bijective and add round-trip edge cases (%2F, %25, spaces, legacy encoded values)
- ✅ Verify default-clearing behavior (default values should be removed from URL when applicable)
- ⚠️ Only mock
nuqs directly when URL behavior is explicitly out of scope for the test
Core Principles
1. AAA Pattern (Arrange-Act-Assert)
Every test should clearly separate:
- Arrange: Setup test data and render component
- Act: Perform user actions
- Assert: Verify expected outcomes
2. Black-Box Testing
- Test observable behavior, not implementation details
- Use semantic queries (
getByRole with accessible name, getByLabelText, getByPlaceholderText, getByText, and scoped within(...))
- Treat
getByTestId as a last resort. If a control cannot be found by role/name, label, landmark, or dialog scope, fix the component accessibility first instead of adding or relying on data-testid.
- Remove production
data-testid attributes when semantic selectors can cover the behavior. Keep them only for non-visual mocked boundaries, editor/browser shims such as Monaco, canvas/chart output, or third-party widgets with no accessible DOM in the test environment.
- Do not assert decorative icons by test id. Assert the named control that contains them, or mark decorative icons
aria-hidden.
- Avoid testing internal state directly
- Prefer pattern matching over hardcoded strings in assertions:
// ❌ 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()
3. Single Behavior Per Test
Each test verifies ONE user-observable behavior:
// ✅ 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')
})
4. Semantic Naming
Use should <behavior> when <condition>:
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
Conditional (When Present)
| Feature |
Test Focus |
useState |
Initial state, transitions, cleanup |
useEffect |
Execution, dependencies, cleanup |
| Event handlers |
All onClick, onChange, onSubmit, keyboard |
| API calls |
Loading, success, error states |
| Routing |
Navigation, params, query strings |
useCallback/useMemo |
Referential equality |
| Context |
Provider values, consumer behavior |
| Forms |
Validation, submission, error display |
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.
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
Authoritative References
Primary Specification (MUST follow)
web/docs/test.md - The canonical testing specification. This skill is derived from this document.
Reference Examples in Codebase
web/utils/classnames.spec.ts - Utility function tests
web/app/components/base/radio/__tests__/index.spec.tsx - Component tests
web/__mocks__/provider-context.ts - Mock factory example
Project Configuration
web/vite.config.ts - Vite/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.
1---2name: frontend-testing3description: Generate Vitest + React Testing Library tests for Dify frontend components, hooks, and utilities. Triggers on testing, spec files, coverage, Vitest, RTL, unit tests, integration tests, or write/review test requests.4---56# Dify Frontend Testing Skill78This skill enables Codex to generate high-quality, comprehensive frontend tests for the Dify project following established conventions and best practices.910> **⚠️ Authoritative Source**: This skill is derived from `web/docs/test.md`. Use Vitest mock/timer APIs (`vi.*`).1112## When to Apply This Skill1314Apply this skill when the user:1516- Asks to **write tests** for a component, hook, or utility17- Asks to **review existing tests** for completeness18- Mentions **Vitest**, **React Testing Library**, **RTL**, or **spec files**19- Requests **test coverage** improvement20- Uses `pnpm analyze-component` output as context21- Mentions **testing**, **unit tests**, or **integration tests** for frontend code22- Wants to understand **testing patterns** in the Dify codebase2324**Do NOT apply** when:2526- User is asking about backend/API tests (Python/pytest)27- User is asking about E2E tests (Cucumber + Playwright under `e2e/`)28- User is only asking conceptual questions without code context2930## Quick Reference3132### Key Commands3334Run these commands from `web/`. From the repository root, prefix them with `pnpm -C web`.3536```bash37# Run all tests38pnpm test3940# Watch mode41pnpm test --watch4243# Run specific file44pnpm test path/to/file.spec.tsx4546# Generate coverage report47pnpm test --coverage4849# Analyze component complexity50pnpm analyze-component <path>5152# Review existing test53pnpm analyze-component <path> --review54```5556### File Naming5758- Test files: `ComponentName.spec.tsx` inside a same-level `__tests__/` directory59- Placement rule: Component, hook, and utility tests must live in a sibling `__tests__/` folder at the same level as the source under test. For example, `foo/index.tsx` maps to `foo/__tests__/index.spec.tsx`, and `foo/bar.ts` maps to `foo/__tests__/bar.spec.ts`.60- Integration tests: `web/__tests__/` directory6162## Test Structure Template6364```typescript65import { render, screen, fireEvent, waitFor } from '@testing-library/react'66import Component from './index'6768// ✅ Import real project components (DO NOT mock these)69// import Loading from '@/app/components/base/loading'70// import { ChildComponent } from './child-component'7172// ✅ Mock external dependencies only73vi.mock('@/service/api')74vi.mock('next/navigation', () => ({75 useRouter: () => ({ push: vi.fn() }),76 usePathname: () => '/test',77}))7879// ✅ Zustand stores: Use real stores (auto-mocked globally)80// Set test state with: useAppStore.setState({ ... })8182// Shared state for mocks (if needed)83let mockSharedState = false8485describe('ComponentName', () => {86 beforeEach(() => {87 vi.clearAllMocks() // ✅ Reset mocks BEFORE each test88 mockSharedState = false // ✅ Reset shared state89 })9091 // Rendering tests (REQUIRED)92 describe('Rendering', () => {93 it('should render without crashing', () => {94 // Arrange95 const props = { title: 'Test' }96 97 // Act98 render(<Component {...props} />)99 100 // Assert101 expect(screen.getByText('Test')).toBeInTheDocument()102 })103 })104105 // Props tests (REQUIRED)106 describe('Props', () => {107 it('should apply custom className', () => {108 render(<Component className="custom" />)109 expect(screen.getByRole('button')).toHaveClass('custom')110 })111 })112113 // User Interactions114 describe('User Interactions', () => {115 it('should handle click events', () => {116 const handleClick = vi.fn()117 render(<Component onClick={handleClick} />)118 119 fireEvent.click(screen.getByRole('button'))120 121 expect(handleClick).toHaveBeenCalledTimes(1)122 })123 })124125 // Edge Cases (REQUIRED)126 describe('Edge Cases', () => {127 it('should handle null data', () => {128 render(<Component data={null} />)129 expect(screen.getByText(/no data/i)).toBeInTheDocument()130 })131132 it('should handle empty array', () => {133 render(<Component items={[]} />)134 expect(screen.getByText(/empty/i)).toBeInTheDocument()135 })136 })137})138```139140## Testing Workflow (CRITICAL)141142### ⚠️ Incremental Approach Required143144**NEVER generate all test files at once.** For complex components or multi-file directories:1451461. **Analyze & Plan**: List all files, order by complexity (simple → complex)1471. **Process ONE at a time**: Write test → Run test → Fix if needed → Next1481. **Verify before proceeding**: Do NOT continue to next file until current passes149150```151For each file:152 ┌────────────────────────────────────────┐153 │ 1. Write test │154 │ 2. Run: pnpm test <file>.spec.tsx │155 │ 3. PASS? → Mark complete, next file │156 │ FAIL? → Fix first, then continue │157 └────────────────────────────────────────┘158```159160### Complexity-Based Order161162Process in this order for multi-file testing:1631641. 🟢 Utility functions (simplest)1651. 🟢 Custom hooks1661. 🟡 Simple components (presentational)1671. 🟡 Medium components (state, effects)1681. 🔴 Complex components (API, routing)1691. 🔴 Integration tests (index files - last)170171### When to Refactor First172173- **Complexity > 50**: Break into smaller pieces before testing174- **500+ lines**: Consider splitting before testing175- **Many dependencies**: Extract logic into hooks first176177> 📖 See `references/workflow.md` for complete workflow details and todo list format.178179## Testing Strategy180181### Path-Level Testing (Directory Testing)182183When assigned to test a directory/path, test **ALL content** within that path:184185- Test all components, hooks, utilities in the directory (not just `index` file)186- Use incremental approach: one file at a time, verify each before proceeding187- Goal: 100% coverage of ALL files in the directory188189### Integration Testing First190191**Prefer integration testing** when writing tests for a directory:192193- ✅ **Import real project components** directly (including base components and siblings)194- ✅ **Only mock**: API services (`@/service/*`), `next/navigation`, complex context providers195- ❌ **DO NOT mock** base components (`@/app/components/base/*`) or dify-ui primitives (`@langgenius/dify-ui/*`)196- ❌ **DO NOT mock** sibling/child components in the same directory197198> See [Test Structure Template](#test-structure-template) for correct import/mock patterns.199200### `nuqs` Query State Testing (Required for URL State Hooks)201202When a component or hook uses `useQueryState` / `useQueryStates`:203204- ✅ Use `NuqsTestingAdapter` (prefer shared helpers in `web/test/nuqs-testing.tsx`)205- ✅ Assert URL synchronization via `onUrlUpdate` (`searchParams`, `options.history`)206- ✅ For custom parsers (`createParser`), keep `parse` and `serialize` bijective and add round-trip edge cases (`%2F`, `%25`, spaces, legacy encoded values)207- ✅ Verify default-clearing behavior (default values should be removed from URL when applicable)208- ⚠️ Only mock `nuqs` directly when URL behavior is explicitly out of scope for the test209210## Core Principles211212### 1. AAA Pattern (Arrange-Act-Assert)213214Every test should clearly separate:215216- **Arrange**: Setup test data and render component217- **Act**: Perform user actions218- **Assert**: Verify expected outcomes219220### 2. Black-Box Testing221222- Test observable behavior, not implementation details223- Use semantic queries (`getByRole` with accessible `name`, `getByLabelText`, `getByPlaceholderText`, `getByText`, and scoped `within(...)`)224- Treat `getByTestId` as a last resort. If a control cannot be found by role/name, label, landmark, or dialog scope, fix the component accessibility first instead of adding or relying on `data-testid`.225- Remove production `data-testid` attributes when semantic selectors can cover the behavior. Keep them only for non-visual mocked boundaries, editor/browser shims such as Monaco, canvas/chart output, or third-party widgets with no accessible DOM in the test environment.226- Do not assert decorative icons by test id. Assert the named control that contains them, or mark decorative icons `aria-hidden`.227- Avoid testing internal state directly228- **Prefer pattern matching over hardcoded strings** in assertions:229230```typescript231// ❌ Avoid: hardcoded text assertions232expect(screen.getByText('Loading...')).toBeInTheDocument()233234// ✅ Better: role-based queries235expect(screen.getByRole('status')).toBeInTheDocument()236237// ✅ Better: pattern matching238expect(screen.getByText(/loading/i)).toBeInTheDocument()239```240241### 3. Single Behavior Per Test242243Each test verifies ONE user-observable behavior:244245```typescript246// ✅ Good: One behavior247it('should disable button when loading', () => {248 render(<Button loading />)249 expect(screen.getByRole('button')).toBeDisabled()250})251252// ❌ Bad: Multiple behaviors253it('should handle loading state', () => {254 render(<Button loading />)255 expect(screen.getByRole('button')).toBeDisabled()256 expect(screen.getByText('Loading...')).toBeInTheDocument()257 expect(screen.getByRole('button')).toHaveClass('loading')258})259```260261### 4. Semantic Naming262263Use `should <behavior> when <condition>`:264265```typescript266it('should show error message when validation fails')267it('should call onSubmit when form is valid')268it('should disable input when isReadOnly is true')269```270271## Required Test Scenarios272273### Always Required (All Components)2742751. **Rendering**: Component renders without crashing2761. **Props**: Required props, optional props, default values2771. **Edge Cases**: null, undefined, empty values, boundary conditions278279### Conditional (When Present)280281| Feature | Test Focus |282|---------|-----------|283| `useState` | Initial state, transitions, cleanup |284| `useEffect` | Execution, dependencies, cleanup |285| Event handlers | All onClick, onChange, onSubmit, keyboard |286| API calls | Loading, success, error states |287| Routing | Navigation, params, query strings |288| `useCallback`/`useMemo` | Referential equality |289| Context | Provider values, consumer behavior |290| Forms | Validation, submission, error display |291292## Coverage Goals (Per File)293294For each test file generated, aim for:295296- ✅ **100%** function coverage297- ✅ **100%** statement coverage298- ✅ **>95%** branch coverage299- ✅ **>95%** line coverage300301> **Note**: For multi-file directories, process one file at a time with full coverage each. See `references/workflow.md`.302303## Detailed Guides304305For more detailed information, refer to:306307- `references/workflow.md` - **Incremental testing workflow** (MUST READ for multi-file testing)308- `references/mocking.md` - Mock patterns, Zustand store testing, and best practices309- `references/async-testing.md` - Async operations and API calls310- `references/domain-components.md` - Workflow, Dataset, Configuration testing311- `references/common-patterns.md` - Frequently used testing patterns312- `references/checklist.md` - Test generation checklist and validation steps313314## Authoritative References315316### Primary Specification (MUST follow)317318- **`web/docs/test.md`** - The canonical testing specification. This skill is derived from this document.319320### Reference Examples in Codebase321322- `web/utils/classnames.spec.ts` - Utility function tests323- `web/app/components/base/radio/__tests__/index.spec.tsx` - Component tests324- `web/__mocks__/provider-context.ts` - Mock factory example325326### Project Configuration327328- `web/vite.config.ts` - Vite/Vitest configuration329- `web/vitest.setup.ts` - Test environment setup330- `web/scripts/analyze-component.js` - Component analysis tool331- 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.