mirror of https://github.com/langgenius/dify.git
302 lines
9.6 KiB
TypeScript
302 lines
9.6 KiB
TypeScript
/**
|
|
* Integration Test: Segment CRUD Flow
|
|
*
|
|
* Tests segment selection, search/filter, and modal state management across hooks.
|
|
* Validates cross-hook data contracts in the completed segment module.
|
|
*/
|
|
|
|
import type { SegmentDetailModel } from '@/models/datasets'
|
|
import { act, renderHook } from '@testing-library/react'
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import { useModalState } from '@/app/components/datasets/documents/detail/completed/hooks/use-modal-state'
|
|
import { useSearchFilter } from '@/app/components/datasets/documents/detail/completed/hooks/use-search-filter'
|
|
import { useSegmentSelection } from '@/app/components/datasets/documents/detail/completed/hooks/use-segment-selection'
|
|
|
|
const createSegment = (id: string, content = 'Test segment content'): SegmentDetailModel => ({
|
|
id,
|
|
position: 1,
|
|
document_id: 'doc-1',
|
|
content,
|
|
sign_content: content,
|
|
answer: '',
|
|
word_count: 50,
|
|
tokens: 25,
|
|
keywords: ['test'],
|
|
index_node_id: 'idx-1',
|
|
index_node_hash: 'hash-1',
|
|
hit_count: 0,
|
|
enabled: true,
|
|
disabled_at: 0,
|
|
disabled_by: '',
|
|
status: 'completed',
|
|
created_by: 'user-1',
|
|
created_at: Date.now(),
|
|
indexing_at: Date.now(),
|
|
completed_at: Date.now(),
|
|
error: null,
|
|
stopped_at: 0,
|
|
updated_at: Date.now(),
|
|
attachments: [],
|
|
} as SegmentDetailModel)
|
|
|
|
describe('Segment CRUD Flow', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
describe('Search and Filter → Segment List Query', () => {
|
|
it('should manage search input with debounce', () => {
|
|
vi.useFakeTimers()
|
|
const onPageChange = vi.fn()
|
|
const { result } = renderHook(() => useSearchFilter({ onPageChange }))
|
|
|
|
act(() => {
|
|
result.current.handleInputChange('keyword')
|
|
})
|
|
|
|
expect(result.current.inputValue).toBe('keyword')
|
|
expect(result.current.searchValue).toBe('')
|
|
|
|
act(() => {
|
|
vi.advanceTimersByTime(500)
|
|
})
|
|
expect(result.current.searchValue).toBe('keyword')
|
|
expect(onPageChange).toHaveBeenCalledWith(1)
|
|
|
|
vi.useRealTimers()
|
|
})
|
|
|
|
it('should manage status filter state', () => {
|
|
const onPageChange = vi.fn()
|
|
const { result } = renderHook(() => useSearchFilter({ onPageChange }))
|
|
|
|
// status value 1 maps to !!1 = true (enabled)
|
|
act(() => {
|
|
result.current.onChangeStatus({ value: 1, name: 'enabled' })
|
|
})
|
|
// onChangeStatus converts: value === 'all' ? 'all' : !!value
|
|
expect(result.current.selectedStatus).toBe(true)
|
|
|
|
act(() => {
|
|
result.current.onClearFilter()
|
|
})
|
|
expect(result.current.selectedStatus).toBe('all')
|
|
expect(result.current.inputValue).toBe('')
|
|
})
|
|
|
|
it('should provide status list for filter dropdown', () => {
|
|
const { result } = renderHook(() => useSearchFilter({ onPageChange: vi.fn() }))
|
|
expect(result.current.statusList).toBeInstanceOf(Array)
|
|
expect(result.current.statusList.length).toBe(3) // all, disabled, enabled
|
|
})
|
|
|
|
it('should compute selectDefaultValue based on selectedStatus', () => {
|
|
const { result } = renderHook(() => useSearchFilter({ onPageChange: vi.fn() }))
|
|
|
|
// Initial state: 'all'
|
|
expect(result.current.selectDefaultValue).toBe('all')
|
|
|
|
// Set to enabled (true)
|
|
act(() => {
|
|
result.current.onChangeStatus({ value: 1, name: 'enabled' })
|
|
})
|
|
expect(result.current.selectDefaultValue).toBe(1)
|
|
|
|
// Set to disabled (false)
|
|
act(() => {
|
|
result.current.onChangeStatus({ value: 0, name: 'disabled' })
|
|
})
|
|
expect(result.current.selectDefaultValue).toBe(0)
|
|
})
|
|
})
|
|
|
|
describe('Segment Selection → Batch Operations', () => {
|
|
const segments = [
|
|
createSegment('seg-1'),
|
|
createSegment('seg-2'),
|
|
createSegment('seg-3'),
|
|
]
|
|
|
|
it('should manage individual segment selection', () => {
|
|
const { result } = renderHook(() => useSegmentSelection(segments))
|
|
|
|
act(() => {
|
|
result.current.onSelected('seg-1')
|
|
})
|
|
expect(result.current.selectedSegmentIds).toContain('seg-1')
|
|
|
|
act(() => {
|
|
result.current.onSelected('seg-2')
|
|
})
|
|
expect(result.current.selectedSegmentIds).toContain('seg-1')
|
|
expect(result.current.selectedSegmentIds).toContain('seg-2')
|
|
expect(result.current.selectedSegmentIds).toHaveLength(2)
|
|
})
|
|
|
|
it('should toggle selection on repeated click', () => {
|
|
const { result } = renderHook(() => useSegmentSelection(segments))
|
|
|
|
act(() => {
|
|
result.current.onSelected('seg-1')
|
|
})
|
|
expect(result.current.selectedSegmentIds).toContain('seg-1')
|
|
|
|
act(() => {
|
|
result.current.onSelected('seg-1')
|
|
})
|
|
expect(result.current.selectedSegmentIds).not.toContain('seg-1')
|
|
})
|
|
|
|
it('should support select all toggle', () => {
|
|
const { result } = renderHook(() => useSegmentSelection(segments))
|
|
|
|
act(() => {
|
|
result.current.onSelectedAll()
|
|
})
|
|
expect(result.current.selectedSegmentIds).toHaveLength(3)
|
|
expect(result.current.isAllSelected).toBe(true)
|
|
|
|
act(() => {
|
|
result.current.onSelectedAll()
|
|
})
|
|
expect(result.current.selectedSegmentIds).toHaveLength(0)
|
|
expect(result.current.isAllSelected).toBe(false)
|
|
})
|
|
|
|
it('should detect partial selection via isSomeSelected', () => {
|
|
const { result } = renderHook(() => useSegmentSelection(segments))
|
|
|
|
act(() => {
|
|
result.current.onSelected('seg-1')
|
|
})
|
|
|
|
// After selecting one of three, isSomeSelected should be true
|
|
expect(result.current.selectedSegmentIds).toEqual(['seg-1'])
|
|
expect(result.current.isSomeSelected).toBe(true)
|
|
expect(result.current.isAllSelected).toBe(false)
|
|
})
|
|
|
|
it('should clear selection via onCancelBatchOperation', () => {
|
|
const { result } = renderHook(() => useSegmentSelection(segments))
|
|
|
|
act(() => {
|
|
result.current.onSelected('seg-1')
|
|
result.current.onSelected('seg-2')
|
|
})
|
|
expect(result.current.selectedSegmentIds).toHaveLength(2)
|
|
|
|
act(() => {
|
|
result.current.onCancelBatchOperation()
|
|
})
|
|
expect(result.current.selectedSegmentIds).toHaveLength(0)
|
|
})
|
|
})
|
|
|
|
describe('Modal State Management', () => {
|
|
const onNewSegmentModalChange = vi.fn()
|
|
|
|
it('should open segment detail modal on card click', () => {
|
|
const { result } = renderHook(() => useModalState({ onNewSegmentModalChange }))
|
|
|
|
const segment = createSegment('seg-detail-1', 'Detail content')
|
|
act(() => {
|
|
result.current.onClickCard(segment)
|
|
})
|
|
expect(result.current.currSegment.showModal).toBe(true)
|
|
expect(result.current.currSegment.segInfo).toBeDefined()
|
|
expect(result.current.currSegment.segInfo!.id).toBe('seg-detail-1')
|
|
})
|
|
|
|
it('should close segment detail modal', () => {
|
|
const { result } = renderHook(() => useModalState({ onNewSegmentModalChange }))
|
|
|
|
const segment = createSegment('seg-1')
|
|
act(() => {
|
|
result.current.onClickCard(segment)
|
|
})
|
|
expect(result.current.currSegment.showModal).toBe(true)
|
|
|
|
act(() => {
|
|
result.current.onCloseSegmentDetail()
|
|
})
|
|
expect(result.current.currSegment.showModal).toBe(false)
|
|
})
|
|
|
|
it('should manage full screen toggle', () => {
|
|
const { result } = renderHook(() => useModalState({ onNewSegmentModalChange }))
|
|
|
|
expect(result.current.fullScreen).toBe(false)
|
|
act(() => {
|
|
result.current.toggleFullScreen()
|
|
})
|
|
expect(result.current.fullScreen).toBe(true)
|
|
act(() => {
|
|
result.current.toggleFullScreen()
|
|
})
|
|
expect(result.current.fullScreen).toBe(false)
|
|
})
|
|
|
|
it('should manage collapsed state', () => {
|
|
const { result } = renderHook(() => useModalState({ onNewSegmentModalChange }))
|
|
|
|
expect(result.current.isCollapsed).toBe(true)
|
|
act(() => {
|
|
result.current.toggleCollapsed()
|
|
})
|
|
expect(result.current.isCollapsed).toBe(false)
|
|
})
|
|
|
|
it('should manage new child segment modal', () => {
|
|
const { result } = renderHook(() => useModalState({ onNewSegmentModalChange }))
|
|
|
|
expect(result.current.showNewChildSegmentModal).toBe(false)
|
|
act(() => {
|
|
result.current.handleAddNewChildChunk('chunk-parent-1')
|
|
})
|
|
expect(result.current.showNewChildSegmentModal).toBe(true)
|
|
expect(result.current.currChunkId).toBe('chunk-parent-1')
|
|
|
|
act(() => {
|
|
result.current.onCloseNewChildChunkModal()
|
|
})
|
|
expect(result.current.showNewChildSegmentModal).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('Cross-Hook Data Flow: Search → Selection → Modal', () => {
|
|
it('should maintain independent state across all three hooks', () => {
|
|
const segments = [createSegment('seg-1'), createSegment('seg-2')]
|
|
|
|
const { result: filterResult } = renderHook(() =>
|
|
useSearchFilter({ onPageChange: vi.fn() }),
|
|
)
|
|
const { result: selectionResult } = renderHook(() =>
|
|
useSegmentSelection(segments),
|
|
)
|
|
const { result: modalResult } = renderHook(() =>
|
|
useModalState({ onNewSegmentModalChange: vi.fn() }),
|
|
)
|
|
|
|
// Set search filter to enabled
|
|
act(() => {
|
|
filterResult.current.onChangeStatus({ value: 1, name: 'enabled' })
|
|
})
|
|
|
|
// Select a segment
|
|
act(() => {
|
|
selectionResult.current.onSelected('seg-1')
|
|
})
|
|
|
|
// Open detail modal
|
|
act(() => {
|
|
modalResult.current.onClickCard(segments[0])
|
|
})
|
|
|
|
// All states should be independent
|
|
expect(filterResult.current.selectedStatus).toBe(true) // !!1
|
|
expect(selectionResult.current.selectedSegmentIds).toContain('seg-1')
|
|
expect(modalResult.current.currSegment.showModal).toBe(true)
|
|
})
|
|
})
|
|
})
|