diff --git a/web/app/components/datasets/create/step-two/index.spec.tsx b/web/app/components/datasets/create/step-two/index.spec.tsx
index aba9200809..92a1c8e3f6 100644
--- a/web/app/components/datasets/create/step-two/index.spec.tsx
+++ b/web/app/components/datasets/create/step-two/index.spec.tsx
@@ -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[] }) => (
+
+ {value?.name}
+ {files?.map((f: { id: string, name: string }) => (
+
+ ))}
+
+ ),
+}))
+
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()
+ 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()
+ 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()
+ 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()
+ // 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()
+ // 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')
+ })
+ })
})
diff --git a/web/app/components/datasets/documents/detail/completed/components/segment-list-content.spec.tsx b/web/app/components/datasets/documents/detail/completed/components/segment-list-content.spec.tsx
index c588594d72..22fb2a92c3 100644
--- a/web/app/components/datasets/documents/detail/completed/components/segment-list-content.spec.tsx
+++ b/web/app/components/datasets/documents/detail/completed/components/segment-list-content.spec.tsx
@@ -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 } }) => (
- {detail?.id}
+ default: ({ detail, onClick }: { detail: { id: string }, onClick?: () => void }) => (
+ {detail?.id}
),
}))
@@ -75,6 +75,13 @@ describe('FullDocModeContent', () => {
const { container } = render()
expect(container.firstChild).toHaveClass('overflow-y-auto')
})
+
+ it('should call onClickCard with first segment when segment card is clicked', () => {
+ const onClickCard = vi.fn()
+ render()
+ fireEvent.click(screen.getByTestId('segment-card'))
+ expect(onClickCard).toHaveBeenCalledWith(defaultProps.segments[0])
+ })
})
describe('GeneralModeContent', () => {
diff --git a/web/app/components/datasets/documents/detail/completed/hooks/use-child-segment-data.spec.ts b/web/app/components/datasets/documents/detail/completed/hooks/use-child-segment-data.spec.ts
index 66a2f9e541..e2bb10cb37 100644
--- a/web/app/components/datasets/documents/detail/completed/hooks/use-child-segment-data.spec.ts
+++ b/web/app/components/datasets/documents/detail/completed/hooks/use-child-segment-data.spec.ts
@@ -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([])
+ })
})
})
diff --git a/web/app/components/datasets/documents/status-item/hooks.spec.ts b/web/app/components/datasets/documents/status-item/hooks.spec.ts
index 2cc20bbf86..0ba423a3e9 100644
--- a/web/app/components/datasets/documents/status-item/hooks.spec.ts
+++ b/web/app/components/datasets/documents/status-item/hooks.spec.ts
@@ -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
diff --git a/web/app/components/datasets/hit-testing/components/result-item-footer.spec.tsx b/web/app/components/datasets/hit-testing/components/result-item-footer.spec.tsx
index 8cb1c735cf..439be26975 100644
--- a/web/app/components/datasets/hit-testing/components/result-item-footer.spec.tsx
+++ b/web/app/components/datasets/hit-testing/components/result-item-footer.spec.tsx
@@ -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
diff --git a/web/app/components/datasets/hit-testing/components/result-item.spec.tsx b/web/app/components/datasets/hit-testing/components/result-item.spec.tsx
index ffcbeb2bd4..eaa7cb30a3 100644
--- a/web/app/components/datasets/hit-testing/components/result-item.spec.tsx
+++ b/web/app/components/datasets/hit-testing/components/result-item.spec.tsx
@@ -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)