web/docs/test.md
This document is the complete testing specification for the Dify frontend project. Goal: Readable, change-friendly, reusable, and debuggable tests. When I ask you to write/refactor/fix tests, follow these rules by default.
ComponentName.spec.tsx inside a same-level __tests__/ directory__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.# Run all tests
pnpm test
# Watch mode
pnpm test:watch
# Generate coverage report
pnpm test:coverage
# Run specific file
pnpm test path/to/file.spec.tsx
vite.config.ts sets the happy-dom environment, loads the Testing Library presets, and respects our path aliases (@/...). Check this file before adding new transformers or module name mappers.vitest.setup.ts already imports @testing-library/jest-dom, runs cleanup() after every test, and defines shared mocks (for example react-i18next). Add any environment-level mocks (for example ResizeObserver, matchMedia, IntersectionObserver, TextEncoder, crypto) here so they are shared consistently.web/__mocks__/ and use vi.mock('module-name') to point to them rather than redefining mocks in every spec.vi.mock(...) in tests, or place global mocks in vitest.setup.ts.web/scripts/analyze-component.js analyzes component complexity and generates test prompts for AI assistants. Commands:
pnpm analyze-component <path> - Analyze and generate test promptpnpm analyze-component <path> --json - Output analysis as JSONpnpm analyze-component <path> --review - Generate test review promptpnpm analyze-component --help - Show helpweb/__tests__/ exercise cross-component flows. Prefer adding new end-to-end style specs there rather than mixing them into component directories.getByRole) and pattern matching (/text/i) over hardcoded string assertions.should <behavior> when <condition> and group related cases with describe(<subject or scenario>).describe sections and add a brief comment before each block to explain the scenario it covers so readers can quickly understand the scope.Use pnpm analyze-component <path> to analyze component complexity and adopt different testing strategies based on the results.
test.each() for multiple scenarios"should [behavior] when [condition]"any typesvi.clearAllMocks() should be in beforeEach(), not afterEach(). This ensures mock call history is reset before each test, preventing test pollution when using assertions like toHaveBeenCalledWith() or toHaveBeenCalledTimes().β οΈ Mock components must accurately reflect actual component behavior, especially conditional rendering based on props or state.
Rules:
null or doesn't render under certain conditions, the mock must do the same. Always check the actual component implementation before creating mocks.PortalToFollowElem with PortalToFollowElemContent), use module-level variables to track state and reset them in beforeEach.beforeEach to ensure test isolation, even if you set default values elsewhere.vi.useFakeTimers() if:
setTimeout/setInterval (not mocked)@/app/components/base/ (e.g., Loading, Input, Badge, Tag) or from @langgenius/dify-ui/* (e.g., Button, Tooltip, Dialog, Select, Popover). They have their own dedicated tests. Use real components to test actual integration behavior.Why this matters: Mocks that don't match actual behavior can lead to:
When assigned to test a directory/path (not just a single file), follow these guidelines:
index fileChoose based on directory complexity:
Single spec file (Integration approach) - Preferred for related components
Multiple spec files (Unit approach) - For complex directories
When using a single spec file:
@/service/*), next/navigation, complex context providers@/app/components/base/*) or dify-ui primitives (@langgenius/dify-ui/*)See Example Structure for correct import/mock patterns.
When a component has dedicated dependencies (custom hooks, managers, utilities) that are only used by that component, use the following strategy to balance integration testing and unit testing.
When testing components with dedicated dependencies:
Apply the following test scenarios based on component features:
Key Points:
Exercise the prop combinations that change observable behavior. Show how required props gate functionality, how optional props fall back to their defaults, and how invalid combinations surface through user-facing safeguards. Let TypeScript catch structural issues; keep runtime assertions focused on what the component renders or triggers.
Treat component state as part of the public behavior: confirm the initial render in context, execute the interactions or prop updates that move the state machine, and assert the resulting UI or side effects. Use waitFor()/async queries whenever transitions resolve asynchronously, and only check cleanup paths when they change what a user sees or experiences (duplicate events, lingering timers, etc.).
web/context or app/components/.../context whenever practical.createMockWorkflowContext).renderHook with a custom wrapper that supplies required providers.ProviderContext), put it in mocks/context(for example, __mocks__/context/provider-context). To dynamically control the mock behavior (for example, toggling plan type), use module-level variables to track state and change them(for example, context/provider-context-mock.spec.tsx).Rules:
@/models/, @/types/, etc.) instead of defining inline types.Partial<T> to enable flexible customization for specific test cases.__mocks__/provider-context.ts for reusable context mock factories used across multiple test files.Cover memoized callbacks or values only when they influence observable behaviorβmemoized children, subscription updates, expensive computations. Trigger realistic re-renders and assert the outcomes (avoided rerenders, reused results) instead of inspecting hook internals.
Simulate the interactions that matter to usersβprimary clicks, change events, submits, and relevant keyboard shortcutsβand confirm the resulting behavior. When handlers prevent defaults or rely on bubbling, cover the scenarios where that choice affects the UI or downstream flows.
Must Test:
vi.mockwaitFor() for async operations@tanstack/react-query, instantiate a fresh QueryClient per spec and wrap with QueryClientProviderGuidelines:
global.fetch/axios/ky and returning deterministic responses over reaching out to the network.msw is already installed) when you need declarative request handlers across multiple specs.await waitFor(...) blocks or the async findBy* queries to avoid race conditions.Mock the specific Next.js navigation hooks your component consumes (useRouter, usePathname, useSearchParams) and drive realistic routing flowsβquery parameters, redirects, guarded routes, URL updatesβwhile asserting the rendered outcome or navigation side effects.
nuqs Query State TestingWhen testing code that uses useQueryState or useQueryStates, treat nuqs as the source of truth for URL synchronization.
NuqsAdapter in app layout (already wired in app/layout.tsx).NuqsTestingAdapter (prefer helper utilities from @/test/nuqs-testing).onUrlUpdate events (searchParams, options.history) instead of only asserting router mocks.createParser, keep parse and serialize bijective (round-trip safe). Add edge-case coverage for values like %2F, %25, spaces, and legacy encoded URLs.clearOnDefault semantics remove params when value equals default).nuqs directly when URL behavior is intentionally out of scope for the test. For ESM-safe partial mocks, use async vi.mock with importOriginal.Example:
import { renderHookWithNuqs } from '@/test/nuqs-testing'
it('should update query with push history', async () => {
const { result, onUrlUpdate } = renderHookWithNuqs(() => useMyQueryState(), {
searchParams: '?page=1',
})
act(() => {
result.current.setQuery({ page: 2 })
})
await waitFor(() => expect(onUrlUpdate).toHaveBeenCalled())
const update = onUrlUpdate.mock.calls.at(-1)![0]
expect(update.options.history).toBe('push')
expect(update.searchParams.get('page')).toBe('2')
})
Must Test:
For complex inputs/entities, use Builders with solid defaults and chainable overrides.
Reserve snapshots for static, deterministic fragments (icons, badges, layout chrome). Keep them tight, prefer explicit assertions for behavior, and review any snapshot updates deliberately instead of accepting them wholesale.
Note: Dify is a desktop application. No need for responsive/mobile testing.
import { fireEvent, render, screen, 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',
}))
// Shared state for mocks (if needed)
let mockSharedState = false
describe('ComponentName', () => {
beforeEach(() => {
vi.clearAllMocks() // β
Reset mocks before each test
mockSharedState = false // β
Reset shared state if used in mocks
})
describe('Rendering', () => {
it('should render without crashing', () => {
// Arrange
const props = { title: 'Test' }
// Act
render(<Component {...props} />)
// Assert
expect(screen.getByText('Test')).toBeInTheDocument()
})
})
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()
})
})
})
i18n: Uses global mock in web/vitest.setup.ts (auto-loaded by Vitest setup)
The global mock provides:
useTranslation - returns translation keys with namespace prefixTrans component - renders i18nKey and componentsuseMixedTranslation (from @/app/components/plugins/marketplace/hooks)useGetLanguage (from @/context/i18n) - returns 'en-US'Default behavior: Most tests should use the global mock (no local override needed).
For custom translations: Use the helper function from @/test/i18n-mock:
import { createReactI18nextMock } from '@/test/i18n-mock'
vi.mock('react-i18next', () => createReactI18nextMock({
'my.custom.key': 'Custom translation',
'button.save': 'Save',
}))
Avoid: Manually defining useTranslation mocks that just return the key - the global mock already does this.
Forms: Test validation logic thoroughly
Example - Correct mock with conditional rendering:
// β
CORRECT: Matches actual component behavior
let mockPortalOpenState = false
vi.mock('@/app/components/base/portal-to-follow-elem', () => ({
PortalToFollowElem: ({ children, open, ...props }) => {
mockPortalOpenState = open || false // Update shared state
return <div data-open={open}>{children}</div>
},
PortalToFollowElemContent: ({ children }) => {
// β
Matches actual: returns null when open is false
if (!mockPortalOpenState)
return null
return <div>{children}</div>
},
}))
describe('Component', () => {
beforeEach(() => {
vi.clearAllMocks() // β
Reset mock call history
mockPortalOpenState = false // β
Reset shared state
})
})
workflow/)Must Test:
dataset/)Must Test:
app/configuration, config/)Must Test:
describe blocksWhen generating tests for a single file, aim for 100% coverage in that generation:
Generate comprehensive tests covering all code paths and scenarios.
import { screen } from '@testing-library/react'
// Print entire DOM
screen.debug()
// Print specific element
screen.debug(screen.getByRole('button'))
Priority order (recommended top to bottom):
getByRole - Most recommended, follows accessibility standardsgetByLabelText - Form fieldsgetByPlaceholderText - Only when no labelgetByText - Non-interactive elementsgetByDisplayValue - Current form valuegetByAltText - ImagesgetByTitle - Last choicegetByTestId - Only as last resort// Wait for element to appear
await waitFor(() => {
expect(screen.getByText('Loaded')).toBeInTheDocument()
})
// Wait for element to disappear
await waitFor(() => {
expect(screen.queryByText('Loading')).not.toBeInTheDocument()
})
// Find async element
const element = await screen.findByText('Async Content')
Test examples in the project:
Remember: Writing tests is not just about coverage, but ensuring code quality and maintainability. Good tests should be clear, concise, and meaningful.