mirror of
https://github.com/langgenius/dify.git
synced 2026-03-29 01:49:57 +08:00
test: enhance unit tests for StepTwo and segment list components
- Added new tests for the StepTwo component, covering user interactions with the QA checkbox and file picker functionality. - Improved test coverage for the segment list content, ensuring proper handling of click events on segment cards. - Introduced tests for the useChildSegmentData hook, validating cache updates and scroll behavior based on child segment changes. These enhancements improve the reliability and maintainability of dataset creation and document management features.
This commit is contained in:
@ -162,6 +162,27 @@ vi.mock('@/app/components/base/amplitude', () => ({
|
||||
trackEvent: vi.fn(),
|
||||
}))
|
||||
|
||||
// Enable IS_CE_EDITION to show QA checkbox in tests
|
||||
vi.mock('@/config', async () => {
|
||||
const actual = await vi.importActual('@/config')
|
||||
return { ...actual, IS_CE_EDITION: true }
|
||||
})
|
||||
|
||||
// Mock PreviewDocumentPicker to allow testing handlePickerChange
|
||||
vi.mock('@/app/components/datasets/common/document-picker/preview-document-picker', () => ({
|
||||
// eslint-disable-next-line ts/no-explicit-any
|
||||
default: ({ onChange, value, files }: { onChange: (item: any) => void, value: any, files: any[] }) => (
|
||||
<div data-testid="preview-picker">
|
||||
<span>{value?.name}</span>
|
||||
{files?.map((f: { id: string, name: string }) => (
|
||||
<button key={f.id} data-testid={`picker-${f.id}`} onClick={() => onChange(f)}>
|
||||
{f.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/datasets/settings/utils', () => ({
|
||||
checkShowMultiModalTip: () => false,
|
||||
}))
|
||||
@ -2488,4 +2509,75 @@ describe('StepTwo Component', () => {
|
||||
expect(screen.getByText('datasetCreation.stepTwo.preview')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Handler Functions - Uncovered Paths', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockCurrentDataset = null
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
})
|
||||
|
||||
it('should switch to QUALIFIED when selecting parentChild in ECONOMICAL mode', async () => {
|
||||
render(<StepTwo {...defaultStepTwoProps} isAPIKeySet={false} />)
|
||||
await vi.waitFor(() => {
|
||||
expect(screen.getByText(/stepTwo\.segmentation/i)).toBeInTheDocument()
|
||||
})
|
||||
// Click parentChild option to trigger handleDocFormChange(ChunkingMode.parentChild) with ECONOMICAL
|
||||
const parentChildTitles = screen.getAllByText(/stepTwo\.parentChild/i)
|
||||
fireEvent.click(parentChildTitles[0])
|
||||
})
|
||||
|
||||
it('should open QA confirm dialog and confirm switch when QA selected in ECONOMICAL mode', async () => {
|
||||
render(<StepTwo {...defaultStepTwoProps} isAPIKeySet={false} />)
|
||||
await vi.waitFor(() => {
|
||||
expect(screen.getByText(/stepTwo\.segmentation/i)).toBeInTheDocument()
|
||||
})
|
||||
// Click QA checkbox (visible because IS_CE_EDITION is mocked as true)
|
||||
const qaCheckbox = screen.getByText(/stepTwo\.useQALanguage/i)
|
||||
fireEvent.click(qaCheckbox)
|
||||
// Dialog should open → click Switch to confirm (triggers handleQAConfirm)
|
||||
const switchButton = await screen.findByText(/stepTwo\.switch/i)
|
||||
expect(switchButton).toBeInTheDocument()
|
||||
fireEvent.click(switchButton)
|
||||
})
|
||||
|
||||
it('should close QA confirm dialog when cancel is clicked', async () => {
|
||||
render(<StepTwo {...defaultStepTwoProps} isAPIKeySet={false} />)
|
||||
await vi.waitFor(() => {
|
||||
expect(screen.getByText(/stepTwo\.segmentation/i)).toBeInTheDocument()
|
||||
})
|
||||
// Open QA confirm dialog
|
||||
const qaCheckbox = screen.getByText(/stepTwo\.useQALanguage/i)
|
||||
fireEvent.click(qaCheckbox)
|
||||
// Click the dialog cancel button (onQAConfirmDialogClose)
|
||||
const dialogCancelButtons = await screen.findAllByText(/stepTwo\.cancel/i)
|
||||
fireEvent.click(dialogCancelButtons[0])
|
||||
})
|
||||
|
||||
it('should handle picker change when selecting a different file', () => {
|
||||
const files = [
|
||||
createMockFile({ id: 'file-1', name: 'first.pdf', extension: 'pdf' }),
|
||||
createMockFile({ id: 'file-2', name: 'second.pdf', extension: 'pdf' }),
|
||||
]
|
||||
render(<StepTwo {...defaultStepTwoProps} files={files} />)
|
||||
// Click on the second file in the mocked picker (triggers handlePickerChange)
|
||||
const pickerButton = screen.getByTestId('picker-file-2')
|
||||
fireEvent.click(pickerButton)
|
||||
})
|
||||
|
||||
it('should show error toast when preview is clicked with maxChunkLength exceeding limit', () => {
|
||||
// Set a high maxChunkLength via the DOM attribute
|
||||
document.body.setAttribute('data-public-indexing-max-segmentation-tokens-length', '100')
|
||||
render(<StepTwo {...defaultStepTwoProps} />)
|
||||
// The default maxChunkLength (1024) now exceeds the limit (100)
|
||||
// Click preview button to trigger updatePreview error path
|
||||
const previewButtons = screen.getAllByText(/stepTwo\.previewChunk/i)
|
||||
fireEvent.click(previewButtons[0])
|
||||
// Restore
|
||||
document.body.removeAttribute('data-public-indexing-max-segmentation-tokens-length')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import type { SegmentDetailModel } from '@/models/datasets'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { FullDocModeContent, GeneralModeContent } from './segment-list-content'
|
||||
|
||||
@ -16,8 +16,8 @@ vi.mock('../child-segment-list', () => ({
|
||||
}))
|
||||
|
||||
vi.mock('../segment-card', () => ({
|
||||
default: ({ detail }: { detail: { id: string } }) => (
|
||||
<div data-testid="segment-card">{detail?.id}</div>
|
||||
default: ({ detail, onClick }: { detail: { id: string }, onClick?: () => void }) => (
|
||||
<div data-testid="segment-card" onClick={onClick}>{detail?.id}</div>
|
||||
),
|
||||
}))
|
||||
|
||||
@ -75,6 +75,13 @@ describe('FullDocModeContent', () => {
|
||||
const { container } = render(<FullDocModeContent {...defaultProps} />)
|
||||
expect(container.firstChild).toHaveClass('overflow-y-auto')
|
||||
})
|
||||
|
||||
it('should call onClickCard with first segment when segment card is clicked', () => {
|
||||
const onClickCard = vi.fn()
|
||||
render(<FullDocModeContent {...defaultProps} onClickCard={onClickCard} />)
|
||||
fireEvent.click(screen.getByTestId('segment-card'))
|
||||
expect(onClickCard).toHaveBeenCalledWith(defaultProps.segments[0])
|
||||
})
|
||||
})
|
||||
|
||||
describe('GeneralModeContent', () => {
|
||||
|
||||
@ -564,5 +564,151 @@ describe('useChildSegmentData', () => {
|
||||
|
||||
expect(mockQueryClient.setQueryData).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle updateChildSegmentInCache when old data is undefined', async () => {
|
||||
mockParentMode.current = 'full-doc'
|
||||
const onCloseChildSegmentDetail = vi.fn()
|
||||
|
||||
// Capture the setQueryData callback to verify null-safety
|
||||
mockQueryClient.setQueryData.mockImplementation((_key: unknown, updater: (old: unknown) => unknown) => {
|
||||
if (typeof updater === 'function') {
|
||||
// Invoke with undefined to cover the !old branch
|
||||
const resultWithUndefined = updater(undefined)
|
||||
expect(resultWithUndefined).toBeUndefined()
|
||||
// Also test with real data
|
||||
const resultWithData = updater({
|
||||
data: [
|
||||
createMockChildChunk({ id: 'child-1', content: 'old content' }),
|
||||
createMockChildChunk({ id: 'child-2', content: 'other' }),
|
||||
],
|
||||
total: 2,
|
||||
total_pages: 1,
|
||||
}) as ChildSegmentsResponse
|
||||
expect(resultWithData.data[0].content).toBe('new content')
|
||||
expect(resultWithData.data[1].content).toBe('other')
|
||||
}
|
||||
})
|
||||
|
||||
mockUpdateChildSegment.mockImplementation(async (_params, { onSuccess, onSettled }: MutationCallbacks) => {
|
||||
onSuccess({
|
||||
data: createMockChildChunk({
|
||||
id: 'child-1',
|
||||
content: 'new content',
|
||||
type: 'customized',
|
||||
word_count: 50,
|
||||
updated_at: 1700000001,
|
||||
}),
|
||||
})
|
||||
onSettled()
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => useChildSegmentData({
|
||||
...defaultOptions,
|
||||
onCloseChildSegmentDetail,
|
||||
}), {
|
||||
wrapper: createWrapper(),
|
||||
})
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleUpdateChildChunk('seg-1', 'child-1', 'new content')
|
||||
})
|
||||
|
||||
expect(mockQueryClient.setQueryData).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Scroll to bottom effect', () => {
|
||||
it('should scroll to bottom when childSegments change and needScrollToBottom is true', () => {
|
||||
// Start with empty data
|
||||
mockChildSegmentListData.current = { data: [], total: 0, total_pages: 0, page: 1, limit: 20 }
|
||||
|
||||
const { result, rerender } = renderHook(() => useChildSegmentData(defaultOptions), {
|
||||
wrapper: createWrapper(),
|
||||
})
|
||||
|
||||
// Set up the ref to a mock DOM element
|
||||
const mockScrollTo = vi.fn()
|
||||
Object.defineProperty(result.current.childSegmentListRef, 'current', {
|
||||
value: { scrollTo: mockScrollTo, scrollHeight: 500 },
|
||||
writable: true,
|
||||
})
|
||||
result.current.needScrollToBottom.current = true
|
||||
|
||||
// Change mock data to trigger the useEffect
|
||||
mockChildSegmentListData.current = {
|
||||
data: [createMockChildChunk({ id: 'new-child' })],
|
||||
total: 1,
|
||||
total_pages: 1,
|
||||
page: 1,
|
||||
limit: 20,
|
||||
}
|
||||
rerender()
|
||||
|
||||
expect(mockScrollTo).toHaveBeenCalledWith({ top: 500, behavior: 'smooth' })
|
||||
expect(result.current.needScrollToBottom.current).toBe(false)
|
||||
})
|
||||
|
||||
it('should not scroll when needScrollToBottom is false', () => {
|
||||
mockChildSegmentListData.current = { data: [], total: 0, total_pages: 0, page: 1, limit: 20 }
|
||||
|
||||
const { result, rerender } = renderHook(() => useChildSegmentData(defaultOptions), {
|
||||
wrapper: createWrapper(),
|
||||
})
|
||||
|
||||
const mockScrollTo = vi.fn()
|
||||
Object.defineProperty(result.current.childSegmentListRef, 'current', {
|
||||
value: { scrollTo: mockScrollTo, scrollHeight: 500 },
|
||||
writable: true,
|
||||
})
|
||||
// needScrollToBottom remains false
|
||||
|
||||
mockChildSegmentListData.current = {
|
||||
data: [createMockChildChunk()],
|
||||
total: 1,
|
||||
total_pages: 1,
|
||||
page: 1,
|
||||
limit: 20,
|
||||
}
|
||||
rerender()
|
||||
|
||||
expect(mockScrollTo).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not scroll when childSegmentListRef is null', () => {
|
||||
mockChildSegmentListData.current = { data: [], total: 0, total_pages: 0, page: 1, limit: 20 }
|
||||
|
||||
const { result, rerender } = renderHook(() => useChildSegmentData(defaultOptions), {
|
||||
wrapper: createWrapper(),
|
||||
})
|
||||
|
||||
// ref.current stays null, needScrollToBottom is true
|
||||
result.current.needScrollToBottom.current = true
|
||||
|
||||
mockChildSegmentListData.current = {
|
||||
data: [createMockChildChunk()],
|
||||
total: 1,
|
||||
total_pages: 1,
|
||||
page: 1,
|
||||
limit: 20,
|
||||
}
|
||||
rerender()
|
||||
|
||||
// needScrollToBottom stays true since scroll didn't happen
|
||||
expect(result.current.needScrollToBottom.current).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Query params edge cases', () => {
|
||||
it('should handle currentPage of 0 by defaulting to page 1', () => {
|
||||
const { result } = renderHook(() => useChildSegmentData({
|
||||
...defaultOptions,
|
||||
currentPage: 0,
|
||||
}), {
|
||||
wrapper: createWrapper(),
|
||||
})
|
||||
|
||||
// Should still work with page defaulted to 1
|
||||
expect(result.current.childSegments).toEqual([])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -12,7 +12,9 @@ describe('useIndexStatus', () => {
|
||||
const { result } = renderHook(() => useIndexStatus())
|
||||
|
||||
const expectedKeys = ['queuing', 'indexing', 'paused', 'error', 'available', 'enabled', 'disabled', 'archived']
|
||||
expect(Object.keys(result.current)).toEqual(expectedKeys)
|
||||
const keys = Object.keys(result.current)
|
||||
expect(keys).toEqual(expect.arrayContaining(expectedKeys))
|
||||
expect(keys).toHaveLength(expectedKeys.length)
|
||||
})
|
||||
|
||||
// Verify each status entry has the correct color
|
||||
|
||||
@ -69,7 +69,7 @@ describe('ResultItemFooter', () => {
|
||||
)
|
||||
|
||||
// Act
|
||||
const openButton = screen.getByText(/open/i).closest('.cursor-pointer') as HTMLElement
|
||||
const openButton = screen.getByText(/open/i)
|
||||
fireEvent.click(openButton)
|
||||
|
||||
// Assert
|
||||
|
||||
@ -9,9 +9,8 @@ vi.mock('react-i18next', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('ahooks', () => {
|
||||
// eslint-disable-next-line ts/no-require-imports
|
||||
const { useState } = require('react')
|
||||
vi.mock('ahooks', async () => {
|
||||
const { useState } = await import('react')
|
||||
return {
|
||||
useBoolean: (initial: boolean) => {
|
||||
const [val, setVal] = useState(initial)
|
||||
|
||||
Reference in New Issue
Block a user