mirror of
https://github.com/langgenius/dify.git
synced 2026-05-06 02:18:08 +08:00
test: add comprehensive unit and integration tests for dataset module (#32187)
Co-authored-by: CodingOnStar <hanxujiang@dify.com> Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@ -0,0 +1,186 @@
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import Card from '../card'
|
||||
|
||||
// Shared mock state for context selectors
|
||||
let mockDatasetId: string | undefined = 'dataset-123'
|
||||
let mockMutateDatasetRes: ReturnType<typeof vi.fn> = vi.fn()
|
||||
let mockIsCurrentWorkspaceManager = true
|
||||
|
||||
vi.mock('@/context/dataset-detail', () => ({
|
||||
useDatasetDetailContextWithSelector: (selector: (state: Record<string, unknown>) => unknown) =>
|
||||
selector({
|
||||
dataset: { id: mockDatasetId },
|
||||
mutateDatasetRes: mockMutateDatasetRes,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/context/app-context', () => ({
|
||||
useSelector: (selector: (state: Record<string, unknown>) => unknown) =>
|
||||
selector({ isCurrentWorkspaceManager: mockIsCurrentWorkspaceManager }),
|
||||
}))
|
||||
|
||||
const mockEnableApi = vi.fn()
|
||||
const mockDisableApi = vi.fn()
|
||||
|
||||
vi.mock('@/service/knowledge/use-dataset', () => ({
|
||||
useEnableDatasetServiceApi: () => ({
|
||||
mutateAsync: mockEnableApi,
|
||||
}),
|
||||
useDisableDatasetServiceApi: () => ({
|
||||
mutateAsync: mockDisableApi,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/hooks/use-api-access-url', () => ({
|
||||
useDatasetApiAccessUrl: () => 'https://docs.dify.ai/api-reference/datasets',
|
||||
}))
|
||||
|
||||
describe('Card (API Access)', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockDatasetId = 'dataset-123'
|
||||
mockMutateDatasetRes = vi.fn()
|
||||
mockIsCurrentWorkspaceManager = true
|
||||
})
|
||||
|
||||
// Rendering: verifies enabled/disabled states render correctly
|
||||
describe('Rendering', () => {
|
||||
it('should render without crashing when api is enabled', () => {
|
||||
render(<Card apiEnabled={true} />)
|
||||
expect(screen.getByText(/serviceApi\.enabled/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render without crashing when api is disabled', () => {
|
||||
render(<Card apiEnabled={false} />)
|
||||
expect(screen.getByText(/serviceApi\.disabled/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render API access tip text', () => {
|
||||
render(<Card apiEnabled={true} />)
|
||||
expect(screen.getByText(/appMenus\.apiAccessTip/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render API reference link', () => {
|
||||
render(<Card apiEnabled={true} />)
|
||||
const link = screen.getByRole('link')
|
||||
expect(link).toHaveAttribute('href', 'https://docs.dify.ai/api-reference/datasets')
|
||||
})
|
||||
|
||||
it('should render API doc text in link', () => {
|
||||
render(<Card apiEnabled={true} />)
|
||||
expect(screen.getByText(/apiInfo\.doc/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should open API reference link in new tab', () => {
|
||||
render(<Card apiEnabled={true} />)
|
||||
const link = screen.getByRole('link')
|
||||
expect(link).toHaveAttribute('target', '_blank')
|
||||
expect(link).toHaveAttribute('rel', 'noopener noreferrer')
|
||||
})
|
||||
})
|
||||
|
||||
// Props: tests enabled/disabled visual states
|
||||
describe('Props', () => {
|
||||
it('should show green indicator text when enabled', () => {
|
||||
render(<Card apiEnabled={true} />)
|
||||
const enabledText = screen.getByText(/serviceApi\.enabled/)
|
||||
expect(enabledText).toHaveClass('text-text-success')
|
||||
})
|
||||
|
||||
it('should show warning text when disabled', () => {
|
||||
render(<Card apiEnabled={false} />)
|
||||
const disabledText = screen.getByText(/serviceApi\.disabled/)
|
||||
expect(disabledText).toHaveClass('text-text-warning')
|
||||
})
|
||||
})
|
||||
|
||||
// User Interactions: tests toggle behavior
|
||||
describe('User Interactions', () => {
|
||||
it('should call enableDatasetServiceApi when toggling on', async () => {
|
||||
mockEnableApi.mockResolvedValue({ result: 'success' })
|
||||
render(<Card apiEnabled={false} />)
|
||||
|
||||
const switchButton = screen.getByRole('switch')
|
||||
fireEvent.click(switchButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockEnableApi).toHaveBeenCalledWith('dataset-123')
|
||||
})
|
||||
})
|
||||
|
||||
it('should call disableDatasetServiceApi when toggling off', async () => {
|
||||
mockDisableApi.mockResolvedValue({ result: 'success' })
|
||||
render(<Card apiEnabled={true} />)
|
||||
|
||||
const switchButton = screen.getByRole('switch')
|
||||
fireEvent.click(switchButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockDisableApi).toHaveBeenCalledWith('dataset-123')
|
||||
})
|
||||
})
|
||||
|
||||
it('should call mutateDatasetRes on successful toggle', async () => {
|
||||
mockEnableApi.mockResolvedValue({ result: 'success' })
|
||||
render(<Card apiEnabled={false} />)
|
||||
|
||||
const switchButton = screen.getByRole('switch')
|
||||
fireEvent.click(switchButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockMutateDatasetRes).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
it('should not call mutateDatasetRes when result is not success', async () => {
|
||||
mockEnableApi.mockResolvedValue({ result: 'fail' })
|
||||
render(<Card apiEnabled={false} />)
|
||||
|
||||
const switchButton = screen.getByRole('switch')
|
||||
fireEvent.click(switchButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockEnableApi).toHaveBeenCalled()
|
||||
})
|
||||
expect(mockMutateDatasetRes).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
// Switch disabled state
|
||||
describe('Switch State', () => {
|
||||
it('should disable switch when user is not workspace manager', () => {
|
||||
mockIsCurrentWorkspaceManager = false
|
||||
render(<Card apiEnabled={true} />)
|
||||
|
||||
const switchButton = screen.getByRole('switch')
|
||||
expect(switchButton).toHaveAttribute('aria-checked', 'true')
|
||||
// Headless UI Switch uses CSS classes for disabled state, not the disabled attribute
|
||||
expect(switchButton).toHaveClass('!cursor-not-allowed', '!opacity-50')
|
||||
})
|
||||
|
||||
it('should enable switch when user is workspace manager', () => {
|
||||
mockIsCurrentWorkspaceManager = true
|
||||
render(<Card apiEnabled={true} />)
|
||||
|
||||
const switchButton = screen.getByRole('switch')
|
||||
expect(switchButton).not.toBeDisabled()
|
||||
})
|
||||
})
|
||||
|
||||
// Edge Cases: tests boundary scenarios
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle undefined dataset id', async () => {
|
||||
mockDatasetId = undefined
|
||||
mockEnableApi.mockResolvedValue({ result: 'success' })
|
||||
render(<Card apiEnabled={false} />)
|
||||
|
||||
const switchButton = screen.getByRole('switch')
|
||||
fireEvent.click(switchButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockEnableApi).toHaveBeenCalledWith('')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,129 @@
|
||||
import { act, cleanup, fireEvent, render, screen } from '@testing-library/react'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import ApiAccess from '../index'
|
||||
|
||||
// Mock context and hooks for Card component
|
||||
vi.mock('@/context/dataset-detail', () => ({
|
||||
useDatasetDetailContextWithSelector: vi.fn(() => 'test-dataset-id'),
|
||||
}))
|
||||
|
||||
vi.mock('@/context/app-context', () => ({
|
||||
useSelector: vi.fn(() => true),
|
||||
}))
|
||||
|
||||
vi.mock('@/hooks/use-api-access-url', () => ({
|
||||
useDatasetApiAccessUrl: vi.fn(() => 'https://api.example.com/docs'),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/knowledge/use-dataset', () => ({
|
||||
useEnableDatasetServiceApi: vi.fn(() => ({ mutateAsync: vi.fn() })),
|
||||
useDisableDatasetServiceApi: vi.fn(() => ({ mutateAsync: vi.fn() })),
|
||||
}))
|
||||
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
})
|
||||
|
||||
describe('ApiAccess', () => {
|
||||
it('should render without crashing', () => {
|
||||
render(<ApiAccess expand={true} apiEnabled={true} />)
|
||||
expect(screen.getByText('common.appMenus.apiAccess')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render API access text when expanded', () => {
|
||||
render(<ApiAccess expand={true} apiEnabled={true} />)
|
||||
expect(screen.getByText('common.appMenus.apiAccess')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not render API access text when collapsed', () => {
|
||||
render(<ApiAccess expand={false} apiEnabled={true} />)
|
||||
expect(screen.queryByText('common.appMenus.apiAccess')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render with apiEnabled=true', () => {
|
||||
render(<ApiAccess expand={true} apiEnabled={true} />)
|
||||
expect(screen.getByText('common.appMenus.apiAccess')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render with apiEnabled=false', () => {
|
||||
render(<ApiAccess expand={true} apiEnabled={false} />)
|
||||
expect(screen.getByText('common.appMenus.apiAccess')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should be wrapped with React.memo', () => {
|
||||
expect((ApiAccess as unknown as { $$typeof: symbol }).$$typeof).toBe(Symbol.for('react.memo'))
|
||||
})
|
||||
|
||||
describe('toggle functionality', () => {
|
||||
it('should toggle open state when trigger is clicked', async () => {
|
||||
const { container } = render(<ApiAccess expand={true} apiEnabled={true} />)
|
||||
const trigger = container.querySelector('.cursor-pointer')
|
||||
expect(trigger).toBeInTheDocument()
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(trigger!)
|
||||
})
|
||||
|
||||
// The component should update its state - check for state change via class
|
||||
expect(trigger).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should toggle open state multiple times', async () => {
|
||||
const { container } = render(<ApiAccess expand={true} apiEnabled={true} />)
|
||||
const trigger = container.querySelector('.cursor-pointer')
|
||||
|
||||
// First click - open
|
||||
await act(async () => {
|
||||
fireEvent.click(trigger!)
|
||||
})
|
||||
|
||||
// Second click - close
|
||||
await act(async () => {
|
||||
fireEvent.click(trigger!)
|
||||
})
|
||||
|
||||
expect(trigger).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should work when collapsed', async () => {
|
||||
const { container } = render(<ApiAccess expand={false} apiEnabled={true} />)
|
||||
const trigger = container.querySelector('.cursor-pointer')
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(trigger!)
|
||||
})
|
||||
|
||||
expect(trigger).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('indicator color', () => {
|
||||
it('should render with green indicator when apiEnabled is true', () => {
|
||||
const { container } = render(<ApiAccess expand={true} apiEnabled={true} />)
|
||||
// Indicator component should be present
|
||||
const indicator = container.querySelector('.shrink-0')
|
||||
expect(indicator).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render with yellow indicator when apiEnabled is false', () => {
|
||||
const { container } = render(<ApiAccess expand={true} apiEnabled={false} />)
|
||||
const indicator = container.querySelector('.shrink-0')
|
||||
expect(indicator).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('layout', () => {
|
||||
it('should have justify-center when collapsed', () => {
|
||||
const { container } = render(<ApiAccess expand={false} apiEnabled={true} />)
|
||||
const trigger = container.querySelector('.justify-center')
|
||||
expect(trigger).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not have justify-center when expanded', () => {
|
||||
const { container } = render(<ApiAccess expand={true} apiEnabled={true} />)
|
||||
const innerDiv = container.querySelector('.cursor-pointer')
|
||||
// When expanded, should have gap-2 and text, not justify-center
|
||||
expect(innerDiv).not.toHaveClass('justify-center')
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user