mirror of https://github.com/langgenius/dify.git
648 lines
18 KiB
TypeScript
648 lines
18 KiB
TypeScript
import { act, renderHook, waitFor } from '@testing-library/react'
|
|
import { describe, expect, it, vi } from 'vitest'
|
|
import { DataType, UpdateType } from '../types'
|
|
import useBatchEditDocumentMetadata from './use-batch-edit-document-metadata'
|
|
|
|
type DocMetadataItem = {
|
|
id: string
|
|
name: string
|
|
type: DataType
|
|
value: string | number | null
|
|
}
|
|
|
|
type DocListItem = {
|
|
id: string
|
|
name?: string
|
|
doc_metadata?: DocMetadataItem[] | null
|
|
}
|
|
|
|
type MetadataItemWithEdit = {
|
|
id: string
|
|
name: string
|
|
type: DataType
|
|
value: string | number | null
|
|
isMultipleValue?: boolean
|
|
updateType?: UpdateType
|
|
}
|
|
|
|
// Mock useBatchUpdateDocMetadata
|
|
const mockMutateAsync = vi.fn().mockResolvedValue({})
|
|
vi.mock('@/service/knowledge/use-metadata', () => ({
|
|
useBatchUpdateDocMetadata: () => ({
|
|
mutateAsync: mockMutateAsync,
|
|
}),
|
|
}))
|
|
|
|
// Mock Toast
|
|
vi.mock('@/app/components/base/toast', () => ({
|
|
default: {
|
|
notify: vi.fn(),
|
|
},
|
|
}))
|
|
|
|
describe('useBatchEditDocumentMetadata', () => {
|
|
const mockDocList: DocListItem[] = [
|
|
{
|
|
id: 'doc-1',
|
|
name: 'Document 1',
|
|
doc_metadata: [
|
|
{ id: '1', name: 'field_one', type: DataType.string, value: 'Value 1' },
|
|
{ id: '2', name: 'field_two', type: DataType.number, value: 42 },
|
|
],
|
|
},
|
|
{
|
|
id: 'doc-2',
|
|
name: 'Document 2',
|
|
doc_metadata: [
|
|
{ id: '1', name: 'field_one', type: DataType.string, value: 'Value 2' },
|
|
],
|
|
},
|
|
]
|
|
|
|
const defaultProps = {
|
|
datasetId: 'ds-1',
|
|
docList: mockDocList as Parameters<typeof useBatchEditDocumentMetadata>[0]['docList'],
|
|
onUpdate: vi.fn(),
|
|
}
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
describe('Hook Initialization', () => {
|
|
it('should initialize with isShowEditModal as false', () => {
|
|
const { result } = renderHook(() => useBatchEditDocumentMetadata(defaultProps))
|
|
expect(result.current.isShowEditModal).toBe(false)
|
|
})
|
|
|
|
it('should return showEditModal function', () => {
|
|
const { result } = renderHook(() => useBatchEditDocumentMetadata(defaultProps))
|
|
expect(typeof result.current.showEditModal).toBe('function')
|
|
})
|
|
|
|
it('should return hideEditModal function', () => {
|
|
const { result } = renderHook(() => useBatchEditDocumentMetadata(defaultProps))
|
|
expect(typeof result.current.hideEditModal).toBe('function')
|
|
})
|
|
|
|
it('should return originalList', () => {
|
|
const { result } = renderHook(() => useBatchEditDocumentMetadata(defaultProps))
|
|
expect(Array.isArray(result.current.originalList)).toBe(true)
|
|
})
|
|
|
|
it('should return handleSave function', () => {
|
|
const { result } = renderHook(() => useBatchEditDocumentMetadata(defaultProps))
|
|
expect(typeof result.current.handleSave).toBe('function')
|
|
})
|
|
})
|
|
|
|
describe('Modal Control', () => {
|
|
it('should show modal when showEditModal is called', () => {
|
|
const { result } = renderHook(() => useBatchEditDocumentMetadata(defaultProps))
|
|
|
|
act(() => {
|
|
result.current.showEditModal()
|
|
})
|
|
|
|
expect(result.current.isShowEditModal).toBe(true)
|
|
})
|
|
|
|
it('should hide modal when hideEditModal is called', () => {
|
|
const { result } = renderHook(() => useBatchEditDocumentMetadata(defaultProps))
|
|
|
|
act(() => {
|
|
result.current.showEditModal()
|
|
})
|
|
|
|
act(() => {
|
|
result.current.hideEditModal()
|
|
})
|
|
|
|
expect(result.current.isShowEditModal).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('Original List Processing', () => {
|
|
it('should compute originalList from docList metadata', () => {
|
|
const { result } = renderHook(() => useBatchEditDocumentMetadata(defaultProps))
|
|
|
|
expect(result.current.originalList.length).toBeGreaterThan(0)
|
|
})
|
|
|
|
it('should filter out built-in metadata', () => {
|
|
const docListWithBuiltIn: DocListItem[] = [
|
|
{
|
|
id: 'doc-1',
|
|
doc_metadata: [
|
|
{ id: 'built-in', name: 'created_at', type: DataType.time, value: 123 },
|
|
{ id: '1', name: 'custom', type: DataType.string, value: 'test' },
|
|
],
|
|
},
|
|
]
|
|
|
|
const { result } = renderHook(() =>
|
|
useBatchEditDocumentMetadata({
|
|
...defaultProps,
|
|
docList: docListWithBuiltIn as Parameters<typeof useBatchEditDocumentMetadata>[0]['docList'],
|
|
}),
|
|
)
|
|
|
|
const hasBuiltIn = result.current.originalList.some(item => item.id === 'built-in')
|
|
expect(hasBuiltIn).toBe(false)
|
|
})
|
|
|
|
it('should mark items with multiple values', () => {
|
|
const docListWithDifferentValues: DocListItem[] = [
|
|
{
|
|
id: 'doc-1',
|
|
doc_metadata: [
|
|
{ id: '1', name: 'field', type: DataType.string, value: 'Value A' },
|
|
],
|
|
},
|
|
{
|
|
id: 'doc-2',
|
|
doc_metadata: [
|
|
{ id: '1', name: 'field', type: DataType.string, value: 'Value B' },
|
|
],
|
|
},
|
|
]
|
|
|
|
const { result } = renderHook(() =>
|
|
useBatchEditDocumentMetadata({
|
|
...defaultProps,
|
|
docList: docListWithDifferentValues as Parameters<typeof useBatchEditDocumentMetadata>[0]['docList'],
|
|
}),
|
|
)
|
|
|
|
const fieldItem = result.current.originalList.find(item => item.id === '1')
|
|
expect(fieldItem?.isMultipleValue).toBe(true)
|
|
})
|
|
|
|
it('should not mark items with same values as multiple', () => {
|
|
const docListWithSameValues: DocListItem[] = [
|
|
{
|
|
id: 'doc-1',
|
|
doc_metadata: [
|
|
{ id: '1', name: 'field', type: DataType.string, value: 'Same Value' },
|
|
],
|
|
},
|
|
{
|
|
id: 'doc-2',
|
|
doc_metadata: [
|
|
{ id: '1', name: 'field', type: DataType.string, value: 'Same Value' },
|
|
],
|
|
},
|
|
]
|
|
|
|
const { result } = renderHook(() =>
|
|
useBatchEditDocumentMetadata({
|
|
...defaultProps,
|
|
docList: docListWithSameValues as Parameters<typeof useBatchEditDocumentMetadata>[0]['docList'],
|
|
}),
|
|
)
|
|
|
|
const fieldItem = result.current.originalList.find(item => item.id === '1')
|
|
expect(fieldItem?.isMultipleValue).toBe(false)
|
|
})
|
|
|
|
it('should skip already marked multiple value items', () => {
|
|
// Three docs with same field but different values
|
|
const docListThreeDocs: DocListItem[] = [
|
|
{
|
|
id: 'doc-1',
|
|
doc_metadata: [
|
|
{ id: '1', name: 'field', type: DataType.string, value: 'Value A' },
|
|
],
|
|
},
|
|
{
|
|
id: 'doc-2',
|
|
doc_metadata: [
|
|
{ id: '1', name: 'field', type: DataType.string, value: 'Value B' },
|
|
],
|
|
},
|
|
{
|
|
id: 'doc-3',
|
|
doc_metadata: [
|
|
{ id: '1', name: 'field', type: DataType.string, value: 'Value C' },
|
|
],
|
|
},
|
|
]
|
|
|
|
const { result } = renderHook(() =>
|
|
useBatchEditDocumentMetadata({
|
|
...defaultProps,
|
|
docList: docListThreeDocs as Parameters<typeof useBatchEditDocumentMetadata>[0]['docList'],
|
|
}),
|
|
)
|
|
|
|
// Should only have one item for field '1', marked as multiple
|
|
const fieldItems = result.current.originalList.filter(item => item.id === '1')
|
|
expect(fieldItems.length).toBe(1)
|
|
expect(fieldItems[0].isMultipleValue).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe('handleSave', () => {
|
|
it('should call mutateAsync with correct data', async () => {
|
|
const onUpdate = vi.fn()
|
|
const { result } = renderHook(() =>
|
|
useBatchEditDocumentMetadata({ ...defaultProps, onUpdate }),
|
|
)
|
|
|
|
await act(async () => {
|
|
await result.current.handleSave([], [], false)
|
|
})
|
|
|
|
expect(mockMutateAsync).toHaveBeenCalled()
|
|
})
|
|
|
|
it('should call onUpdate after successful save', async () => {
|
|
const onUpdate = vi.fn()
|
|
const { result } = renderHook(() =>
|
|
useBatchEditDocumentMetadata({ ...defaultProps, onUpdate }),
|
|
)
|
|
|
|
await act(async () => {
|
|
await result.current.handleSave([], [], false)
|
|
})
|
|
|
|
await waitFor(() => {
|
|
expect(onUpdate).toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
it('should hide modal after successful save', async () => {
|
|
const { result } = renderHook(() => useBatchEditDocumentMetadata(defaultProps))
|
|
|
|
act(() => {
|
|
result.current.showEditModal()
|
|
})
|
|
|
|
expect(result.current.isShowEditModal).toBe(true)
|
|
|
|
await act(async () => {
|
|
await result.current.handleSave([], [], false)
|
|
})
|
|
|
|
await waitFor(() => {
|
|
expect(result.current.isShowEditModal).toBe(false)
|
|
})
|
|
})
|
|
|
|
it('should handle edited items with changeValue updateType', async () => {
|
|
const docListSingleDoc: DocListItem[] = [
|
|
{
|
|
id: 'doc-1',
|
|
doc_metadata: [
|
|
{ id: '1', name: 'field_one', type: DataType.string, value: 'Old Value' },
|
|
],
|
|
},
|
|
]
|
|
|
|
const { result } = renderHook(() =>
|
|
useBatchEditDocumentMetadata({
|
|
...defaultProps,
|
|
docList: docListSingleDoc as Parameters<typeof useBatchEditDocumentMetadata>[0]['docList'],
|
|
}),
|
|
)
|
|
|
|
const editedList: MetadataItemWithEdit[] = [
|
|
{
|
|
id: '1',
|
|
name: 'field_one',
|
|
type: DataType.string,
|
|
value: 'New Value',
|
|
updateType: UpdateType.changeValue,
|
|
},
|
|
]
|
|
|
|
await act(async () => {
|
|
await result.current.handleSave(editedList, [], false)
|
|
})
|
|
|
|
expect(mockMutateAsync).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
metadata_list: expect.arrayContaining([
|
|
expect.objectContaining({
|
|
document_id: 'doc-1',
|
|
metadata_list: expect.arrayContaining([
|
|
expect.objectContaining({
|
|
id: '1',
|
|
value: 'New Value',
|
|
}),
|
|
]),
|
|
}),
|
|
]),
|
|
}),
|
|
)
|
|
})
|
|
|
|
it('should handle removed items', async () => {
|
|
const docListSingleDoc: DocListItem[] = [
|
|
{
|
|
id: 'doc-1',
|
|
doc_metadata: [
|
|
{ id: '1', name: 'field_one', type: DataType.string, value: 'Value 1' },
|
|
{ id: '2', name: 'field_two', type: DataType.number, value: 42 },
|
|
],
|
|
},
|
|
]
|
|
|
|
const { result } = renderHook(() =>
|
|
useBatchEditDocumentMetadata({
|
|
...defaultProps,
|
|
docList: docListSingleDoc as Parameters<typeof useBatchEditDocumentMetadata>[0]['docList'],
|
|
}),
|
|
)
|
|
|
|
// Only pass field_one in editedList, field_two should be removed
|
|
const editedList: MetadataItemWithEdit[] = [
|
|
{
|
|
id: '1',
|
|
name: 'field_one',
|
|
type: DataType.string,
|
|
value: 'Value 1',
|
|
},
|
|
]
|
|
|
|
await act(async () => {
|
|
await result.current.handleSave(editedList, [], false)
|
|
})
|
|
|
|
expect(mockMutateAsync).toHaveBeenCalled()
|
|
})
|
|
|
|
it('should handle added items', async () => {
|
|
const docListSingleDoc: DocListItem[] = [
|
|
{
|
|
id: 'doc-1',
|
|
doc_metadata: [
|
|
{ id: '1', name: 'field_one', type: DataType.string, value: 'Value 1' },
|
|
],
|
|
},
|
|
]
|
|
|
|
const { result } = renderHook(() =>
|
|
useBatchEditDocumentMetadata({
|
|
...defaultProps,
|
|
docList: docListSingleDoc as Parameters<typeof useBatchEditDocumentMetadata>[0]['docList'],
|
|
}),
|
|
)
|
|
|
|
const addedList = [
|
|
{
|
|
id: 'new-1',
|
|
name: 'new_field',
|
|
type: DataType.string,
|
|
value: 'New Value',
|
|
isMultipleValue: false,
|
|
},
|
|
]
|
|
|
|
await act(async () => {
|
|
await result.current.handleSave([], addedList, false)
|
|
})
|
|
|
|
expect(mockMutateAsync).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
metadata_list: expect.arrayContaining([
|
|
expect.objectContaining({
|
|
metadata_list: expect.arrayContaining([
|
|
expect.objectContaining({
|
|
name: 'new_field',
|
|
}),
|
|
]),
|
|
}),
|
|
]),
|
|
}),
|
|
)
|
|
})
|
|
|
|
it('should add missing metadata when isApplyToAllSelectDocument is true', async () => {
|
|
// Doc 1 has field, Doc 2 doesn't have it
|
|
const docListMissingField: DocListItem[] = [
|
|
{
|
|
id: 'doc-1',
|
|
doc_metadata: [
|
|
{ id: '1', name: 'field_one', type: DataType.string, value: 'Value 1' },
|
|
],
|
|
},
|
|
{
|
|
id: 'doc-2',
|
|
doc_metadata: [],
|
|
},
|
|
]
|
|
|
|
const { result } = renderHook(() =>
|
|
useBatchEditDocumentMetadata({
|
|
...defaultProps,
|
|
docList: docListMissingField as Parameters<typeof useBatchEditDocumentMetadata>[0]['docList'],
|
|
}),
|
|
)
|
|
|
|
const editedList: MetadataItemWithEdit[] = [
|
|
{
|
|
id: '1',
|
|
name: 'field_one',
|
|
type: DataType.string,
|
|
value: 'Updated Value',
|
|
isMultipleValue: false,
|
|
updateType: UpdateType.changeValue,
|
|
},
|
|
]
|
|
|
|
await act(async () => {
|
|
await result.current.handleSave(editedList, [], true)
|
|
})
|
|
|
|
// Both documents should have the field after applying to all
|
|
expect(mockMutateAsync).toHaveBeenCalled()
|
|
const callArgs = mockMutateAsync.mock.calls[0][0]
|
|
expect(callArgs.metadata_list.length).toBe(2)
|
|
})
|
|
|
|
it('should not add missing metadata for multiple value items when isApplyToAllSelectDocument is true', async () => {
|
|
// Two docs with different values for same field
|
|
const docListDifferentValues: DocListItem[] = [
|
|
{
|
|
id: 'doc-1',
|
|
doc_metadata: [
|
|
{ id: '1', name: 'field_one', type: DataType.string, value: 'Value A' },
|
|
],
|
|
},
|
|
{
|
|
id: 'doc-2',
|
|
doc_metadata: [
|
|
{ id: '1', name: 'field_one', type: DataType.string, value: 'Value B' },
|
|
],
|
|
},
|
|
{
|
|
id: 'doc-3',
|
|
doc_metadata: [],
|
|
},
|
|
]
|
|
|
|
const { result } = renderHook(() =>
|
|
useBatchEditDocumentMetadata({
|
|
...defaultProps,
|
|
docList: docListDifferentValues as Parameters<typeof useBatchEditDocumentMetadata>[0]['docList'],
|
|
}),
|
|
)
|
|
|
|
// Mark it as multiple value item - should not be added to doc-3
|
|
const editedList: MetadataItemWithEdit[] = [
|
|
{
|
|
id: '1',
|
|
name: 'field_one',
|
|
type: DataType.string,
|
|
value: null,
|
|
isMultipleValue: true,
|
|
updateType: UpdateType.changeValue,
|
|
},
|
|
]
|
|
|
|
await act(async () => {
|
|
await result.current.handleSave(editedList, [], true)
|
|
})
|
|
|
|
expect(mockMutateAsync).toHaveBeenCalled()
|
|
})
|
|
|
|
it('should update existing items in the list', async () => {
|
|
const docListSingleDoc: DocListItem[] = [
|
|
{
|
|
id: 'doc-1',
|
|
doc_metadata: [
|
|
{ id: '1', name: 'field_one', type: DataType.string, value: 'Old Value' },
|
|
{ id: '2', name: 'field_two', type: DataType.number, value: 100 },
|
|
],
|
|
},
|
|
]
|
|
|
|
const { result } = renderHook(() =>
|
|
useBatchEditDocumentMetadata({
|
|
...defaultProps,
|
|
docList: docListSingleDoc as Parameters<typeof useBatchEditDocumentMetadata>[0]['docList'],
|
|
}),
|
|
)
|
|
|
|
// Edit both items
|
|
const editedList: MetadataItemWithEdit[] = [
|
|
{
|
|
id: '1',
|
|
name: 'field_one',
|
|
type: DataType.string,
|
|
value: 'New Value 1',
|
|
updateType: UpdateType.changeValue,
|
|
},
|
|
{
|
|
id: '2',
|
|
name: 'field_two',
|
|
type: DataType.number,
|
|
value: 200,
|
|
updateType: UpdateType.changeValue,
|
|
},
|
|
]
|
|
|
|
await act(async () => {
|
|
await result.current.handleSave(editedList, [], false)
|
|
})
|
|
|
|
expect(mockMutateAsync).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
metadata_list: expect.arrayContaining([
|
|
expect.objectContaining({
|
|
metadata_list: expect.arrayContaining([
|
|
expect.objectContaining({ id: '1', value: 'New Value 1' }),
|
|
expect.objectContaining({ id: '2', value: 200 }),
|
|
]),
|
|
}),
|
|
]),
|
|
}),
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('Selected Document IDs', () => {
|
|
it('should use selectedDocumentIds when provided', async () => {
|
|
const selectedIds = ['doc-1']
|
|
const { result } = renderHook(() =>
|
|
useBatchEditDocumentMetadata({
|
|
...defaultProps,
|
|
selectedDocumentIds: selectedIds,
|
|
}),
|
|
)
|
|
|
|
await act(async () => {
|
|
await result.current.handleSave([], [], false)
|
|
})
|
|
|
|
expect(mockMutateAsync).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
dataset_id: 'ds-1',
|
|
metadata_list: expect.arrayContaining([
|
|
expect.objectContaining({
|
|
document_id: 'doc-1',
|
|
}),
|
|
]),
|
|
}),
|
|
)
|
|
})
|
|
|
|
it('should handle selectedDocumentIds not in docList', async () => {
|
|
// Select a document that's not in docList
|
|
const selectedIds = ['doc-1', 'doc-not-in-list']
|
|
const { result } = renderHook(() =>
|
|
useBatchEditDocumentMetadata({
|
|
...defaultProps,
|
|
selectedDocumentIds: selectedIds,
|
|
}),
|
|
)
|
|
|
|
await act(async () => {
|
|
await result.current.handleSave([], [], false)
|
|
})
|
|
|
|
expect(mockMutateAsync).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
metadata_list: expect.arrayContaining([
|
|
expect.objectContaining({
|
|
document_id: 'doc-not-in-list',
|
|
partial_update: true,
|
|
}),
|
|
]),
|
|
}),
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('Edge Cases', () => {
|
|
it('should handle empty docList', () => {
|
|
const { result } = renderHook(() =>
|
|
useBatchEditDocumentMetadata({
|
|
...defaultProps,
|
|
docList: [] as Parameters<typeof useBatchEditDocumentMetadata>[0]['docList'],
|
|
}),
|
|
)
|
|
|
|
expect(result.current.originalList).toEqual([])
|
|
})
|
|
|
|
it('should handle documents without metadata', () => {
|
|
const docListNoMetadata: DocListItem[] = [
|
|
{ id: 'doc-1', name: 'Doc 1' },
|
|
{ id: 'doc-2', name: 'Doc 2', doc_metadata: null },
|
|
]
|
|
|
|
const { result } = renderHook(() =>
|
|
useBatchEditDocumentMetadata({
|
|
...defaultProps,
|
|
docList: docListNoMetadata as Parameters<typeof useBatchEditDocumentMetadata>[0]['docList'],
|
|
}),
|
|
)
|
|
|
|
expect(result.current.originalList).toEqual([])
|
|
})
|
|
})
|
|
})
|