mirror of
https://github.com/langgenius/dify.git
synced 2026-05-04 17:38:04 +08:00
test: add tests for dataset list (#31231)
Co-authored-by: CodingOnStar <hanxujiang@dify.ai> Co-authored-by: yyh <92089059+lyzno1@users.noreply.github.com>
This commit is contained in:
@ -0,0 +1,647 @@
|
||||
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([])
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,166 @@
|
||||
import { renderHook } from '@testing-library/react'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import useCheckMetadataName from './use-check-metadata-name'
|
||||
|
||||
describe('useCheckMetadataName', () => {
|
||||
describe('Hook Initialization', () => {
|
||||
it('should return an object with checkName function', () => {
|
||||
const { result } = renderHook(() => useCheckMetadataName())
|
||||
expect(result.current).toHaveProperty('checkName')
|
||||
expect(typeof result.current.checkName).toBe('function')
|
||||
})
|
||||
})
|
||||
|
||||
describe('checkName - Empty Name Validation', () => {
|
||||
it('should return error for empty string', () => {
|
||||
const { result } = renderHook(() => useCheckMetadataName())
|
||||
const { errorMsg } = result.current.checkName('')
|
||||
expect(errorMsg).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should return error for whitespace-only string', () => {
|
||||
const { result } = renderHook(() => useCheckMetadataName())
|
||||
// Whitespace is not valid since it doesn't match the pattern
|
||||
const { errorMsg } = result.current.checkName(' ')
|
||||
expect(errorMsg).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('checkName - Pattern Validation', () => {
|
||||
it('should return error for name starting with number', () => {
|
||||
const { result } = renderHook(() => useCheckMetadataName())
|
||||
const { errorMsg } = result.current.checkName('1name')
|
||||
expect(errorMsg).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should return error for name starting with uppercase', () => {
|
||||
const { result } = renderHook(() => useCheckMetadataName())
|
||||
const { errorMsg } = result.current.checkName('Name')
|
||||
expect(errorMsg).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should return error for name starting with underscore', () => {
|
||||
const { result } = renderHook(() => useCheckMetadataName())
|
||||
const { errorMsg } = result.current.checkName('_name')
|
||||
expect(errorMsg).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should return error for name with spaces', () => {
|
||||
const { result } = renderHook(() => useCheckMetadataName())
|
||||
const { errorMsg } = result.current.checkName('my name')
|
||||
expect(errorMsg).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should return error for name with special characters', () => {
|
||||
const { result } = renderHook(() => useCheckMetadataName())
|
||||
const { errorMsg } = result.current.checkName('name-with-dash')
|
||||
expect(errorMsg).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should return error for name with dots', () => {
|
||||
const { result } = renderHook(() => useCheckMetadataName())
|
||||
const { errorMsg } = result.current.checkName('name.with.dot')
|
||||
expect(errorMsg).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should accept valid name starting with lowercase letter', () => {
|
||||
const { result } = renderHook(() => useCheckMetadataName())
|
||||
const { errorMsg } = result.current.checkName('validname')
|
||||
expect(errorMsg).toBe('')
|
||||
})
|
||||
|
||||
it('should accept valid name with numbers after first character', () => {
|
||||
const { result } = renderHook(() => useCheckMetadataName())
|
||||
const { errorMsg } = result.current.checkName('name123')
|
||||
expect(errorMsg).toBe('')
|
||||
})
|
||||
|
||||
it('should accept valid name with underscores after first character', () => {
|
||||
const { result } = renderHook(() => useCheckMetadataName())
|
||||
const { errorMsg } = result.current.checkName('name_with_underscore')
|
||||
expect(errorMsg).toBe('')
|
||||
})
|
||||
|
||||
it('should accept single lowercase letter', () => {
|
||||
const { result } = renderHook(() => useCheckMetadataName())
|
||||
const { errorMsg } = result.current.checkName('a')
|
||||
expect(errorMsg).toBe('')
|
||||
})
|
||||
})
|
||||
|
||||
describe('checkName - Length Validation', () => {
|
||||
it('should return error for name longer than 255 characters', () => {
|
||||
const { result } = renderHook(() => useCheckMetadataName())
|
||||
const longName = 'a'.repeat(256)
|
||||
const { errorMsg } = result.current.checkName(longName)
|
||||
expect(errorMsg).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should accept name with exactly 255 characters', () => {
|
||||
const { result } = renderHook(() => useCheckMetadataName())
|
||||
const maxLengthName = 'a'.repeat(255)
|
||||
const { errorMsg } = result.current.checkName(maxLengthName)
|
||||
expect(errorMsg).toBe('')
|
||||
})
|
||||
|
||||
it('should accept name with less than 255 characters', () => {
|
||||
const { result } = renderHook(() => useCheckMetadataName())
|
||||
const shortName = 'a'.repeat(100)
|
||||
const { errorMsg } = result.current.checkName(shortName)
|
||||
expect(errorMsg).toBe('')
|
||||
})
|
||||
})
|
||||
|
||||
describe('checkName - Edge Cases', () => {
|
||||
it('should validate all lowercase letters', () => {
|
||||
const { result } = renderHook(() => useCheckMetadataName())
|
||||
const { errorMsg } = result.current.checkName('abcdefghijklmnopqrstuvwxyz')
|
||||
expect(errorMsg).toBe('')
|
||||
})
|
||||
|
||||
it('should validate name with mixed numbers and underscores', () => {
|
||||
const { result } = renderHook(() => useCheckMetadataName())
|
||||
const { errorMsg } = result.current.checkName('a1_2_3_test')
|
||||
expect(errorMsg).toBe('')
|
||||
})
|
||||
|
||||
it('should reject uppercase letters anywhere in name', () => {
|
||||
const { result } = renderHook(() => useCheckMetadataName())
|
||||
const { errorMsg } = result.current.checkName('nameWithUppercase')
|
||||
expect(errorMsg).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should reject unicode characters', () => {
|
||||
const { result } = renderHook(() => useCheckMetadataName())
|
||||
const { errorMsg } = result.current.checkName('名字')
|
||||
expect(errorMsg).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should reject emoji characters', () => {
|
||||
const { result } = renderHook(() => useCheckMetadataName())
|
||||
const { errorMsg } = result.current.checkName('name😀')
|
||||
expect(errorMsg).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Return Value Structure', () => {
|
||||
it('should return object with errorMsg property', () => {
|
||||
const { result } = renderHook(() => useCheckMetadataName())
|
||||
const returnValue = result.current.checkName('test')
|
||||
expect(returnValue).toHaveProperty('errorMsg')
|
||||
})
|
||||
|
||||
it('should return empty string for valid name', () => {
|
||||
const { result } = renderHook(() => useCheckMetadataName())
|
||||
const { errorMsg } = result.current.checkName('valid_name')
|
||||
expect(errorMsg).toBe('')
|
||||
})
|
||||
|
||||
it('should return non-empty string for invalid name', () => {
|
||||
const { result } = renderHook(() => useCheckMetadataName())
|
||||
const { errorMsg } = result.current.checkName('')
|
||||
expect(typeof errorMsg).toBe('string')
|
||||
expect(errorMsg.length).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,308 @@
|
||||
import { act, renderHook, waitFor } from '@testing-library/react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { DataType } from '../types'
|
||||
import useEditDatasetMetadata from './use-edit-dataset-metadata'
|
||||
|
||||
// Mock service hooks
|
||||
const mockDoAddMetaData = vi.fn().mockResolvedValue({})
|
||||
const mockDoRenameMetaData = vi.fn().mockResolvedValue({})
|
||||
const mockDoDeleteMetaData = vi.fn().mockResolvedValue({})
|
||||
const mockToggleBuiltInStatus = vi.fn().mockResolvedValue({})
|
||||
|
||||
vi.mock('@/service/knowledge/use-metadata', () => ({
|
||||
useDatasetMetaData: () => ({
|
||||
data: {
|
||||
doc_metadata: [
|
||||
{ id: '1', name: 'field_one', type: DataType.string, count: 5 },
|
||||
{ id: '2', name: 'field_two', type: DataType.number, count: 3 },
|
||||
],
|
||||
built_in_field_enabled: false,
|
||||
},
|
||||
}),
|
||||
useCreateMetaData: () => ({
|
||||
mutate: mockDoAddMetaData,
|
||||
}),
|
||||
useRenameMeta: () => ({
|
||||
mutate: mockDoRenameMetaData,
|
||||
}),
|
||||
useDeleteMetaData: () => ({
|
||||
mutateAsync: mockDoDeleteMetaData,
|
||||
}),
|
||||
useUpdateBuiltInStatus: () => ({
|
||||
mutateAsync: mockToggleBuiltInStatus,
|
||||
}),
|
||||
useBuiltInMetaDataFields: () => ({
|
||||
data: {
|
||||
fields: [
|
||||
{ name: 'created_at', type: DataType.time },
|
||||
{ name: 'modified_at', type: DataType.time },
|
||||
],
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock Toast
|
||||
vi.mock('@/app/components/base/toast', () => ({
|
||||
default: {
|
||||
notify: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock useCheckMetadataName
|
||||
vi.mock('./use-check-metadata-name', () => ({
|
||||
default: () => ({
|
||||
checkName: (name: string) => ({
|
||||
errorMsg: name && /^[a-z][a-z0-9_]*$/.test(name) ? '' : 'Invalid name',
|
||||
}),
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock localStorage
|
||||
const localStorageMock = {
|
||||
getItem: vi.fn(),
|
||||
setItem: vi.fn(),
|
||||
removeItem: vi.fn(),
|
||||
clear: vi.fn(),
|
||||
}
|
||||
Object.defineProperty(window, 'localStorage', { value: localStorageMock })
|
||||
|
||||
describe('useEditDatasetMetadata', () => {
|
||||
const defaultProps = {
|
||||
datasetId: 'ds-1',
|
||||
onUpdateDocList: vi.fn(),
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
localStorageMock.getItem.mockReturnValue(null)
|
||||
})
|
||||
|
||||
describe('Hook Initialization', () => {
|
||||
it('should initialize with isShowEditModal as false', () => {
|
||||
const { result } = renderHook(() => useEditDatasetMetadata(defaultProps))
|
||||
expect(result.current.isShowEditModal).toBe(false)
|
||||
})
|
||||
|
||||
it('should return showEditModal function', () => {
|
||||
const { result } = renderHook(() => useEditDatasetMetadata(defaultProps))
|
||||
expect(typeof result.current.showEditModal).toBe('function')
|
||||
})
|
||||
|
||||
it('should return hideEditModal function', () => {
|
||||
const { result } = renderHook(() => useEditDatasetMetadata(defaultProps))
|
||||
expect(typeof result.current.hideEditModal).toBe('function')
|
||||
})
|
||||
|
||||
it('should return datasetMetaData', () => {
|
||||
const { result } = renderHook(() => useEditDatasetMetadata(defaultProps))
|
||||
expect(result.current.datasetMetaData).toBeDefined()
|
||||
})
|
||||
|
||||
it('should return handleAddMetaData function', () => {
|
||||
const { result } = renderHook(() => useEditDatasetMetadata(defaultProps))
|
||||
expect(typeof result.current.handleAddMetaData).toBe('function')
|
||||
})
|
||||
|
||||
it('should return handleRename function', () => {
|
||||
const { result } = renderHook(() => useEditDatasetMetadata(defaultProps))
|
||||
expect(typeof result.current.handleRename).toBe('function')
|
||||
})
|
||||
|
||||
it('should return handleDeleteMetaData function', () => {
|
||||
const { result } = renderHook(() => useEditDatasetMetadata(defaultProps))
|
||||
expect(typeof result.current.handleDeleteMetaData).toBe('function')
|
||||
})
|
||||
|
||||
it('should return builtInMetaData', () => {
|
||||
const { result } = renderHook(() => useEditDatasetMetadata(defaultProps))
|
||||
expect(result.current.builtInMetaData).toBeDefined()
|
||||
})
|
||||
|
||||
it('should return builtInEnabled', () => {
|
||||
const { result } = renderHook(() => useEditDatasetMetadata(defaultProps))
|
||||
expect(typeof result.current.builtInEnabled).toBe('boolean')
|
||||
})
|
||||
|
||||
it('should return setBuiltInEnabled function', () => {
|
||||
const { result } = renderHook(() => useEditDatasetMetadata(defaultProps))
|
||||
expect(typeof result.current.setBuiltInEnabled).toBe('function')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Modal Control', () => {
|
||||
it('should show modal when showEditModal is called', () => {
|
||||
const { result } = renderHook(() => useEditDatasetMetadata(defaultProps))
|
||||
|
||||
act(() => {
|
||||
result.current.showEditModal()
|
||||
})
|
||||
|
||||
expect(result.current.isShowEditModal).toBe(true)
|
||||
})
|
||||
|
||||
it('should hide modal when hideEditModal is called', () => {
|
||||
const { result } = renderHook(() => useEditDatasetMetadata(defaultProps))
|
||||
|
||||
act(() => {
|
||||
result.current.showEditModal()
|
||||
})
|
||||
|
||||
act(() => {
|
||||
result.current.hideEditModal()
|
||||
})
|
||||
|
||||
expect(result.current.isShowEditModal).toBe(false)
|
||||
})
|
||||
|
||||
it('should handle toggle of modal state', () => {
|
||||
const { result } = renderHook(() => useEditDatasetMetadata(defaultProps))
|
||||
|
||||
// Initially closed
|
||||
expect(result.current.isShowEditModal).toBe(false)
|
||||
|
||||
// Show, hide, show
|
||||
act(() => result.current.showEditModal())
|
||||
expect(result.current.isShowEditModal).toBe(true)
|
||||
|
||||
act(() => result.current.hideEditModal())
|
||||
expect(result.current.isShowEditModal).toBe(false)
|
||||
|
||||
act(() => result.current.showEditModal())
|
||||
expect(result.current.isShowEditModal).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('handleAddMetaData', () => {
|
||||
it('should call doAddMetaData with valid name', async () => {
|
||||
const { result } = renderHook(() => useEditDatasetMetadata(defaultProps))
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleAddMetaData({
|
||||
name: 'valid_name',
|
||||
type: DataType.string,
|
||||
})
|
||||
})
|
||||
|
||||
expect(mockDoAddMetaData).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should reject invalid name', async () => {
|
||||
const { result } = renderHook(() => useEditDatasetMetadata(defaultProps))
|
||||
|
||||
await expect(
|
||||
act(async () => {
|
||||
await result.current.handleAddMetaData({
|
||||
name: '',
|
||||
type: DataType.string,
|
||||
})
|
||||
}),
|
||||
).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe('handleRename', () => {
|
||||
it('should call doRenameMetaData with valid name', async () => {
|
||||
const { result } = renderHook(() => useEditDatasetMetadata(defaultProps))
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleRename({
|
||||
id: '1',
|
||||
name: 'new_valid_name',
|
||||
type: DataType.string,
|
||||
count: 5,
|
||||
})
|
||||
})
|
||||
|
||||
expect(mockDoRenameMetaData).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should call onUpdateDocList after rename', async () => {
|
||||
const onUpdateDocList = vi.fn()
|
||||
const { result } = renderHook(() =>
|
||||
useEditDatasetMetadata({ ...defaultProps, onUpdateDocList }),
|
||||
)
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleRename({
|
||||
id: '1',
|
||||
name: 'renamed',
|
||||
type: DataType.string,
|
||||
count: 5,
|
||||
})
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onUpdateDocList).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
it('should reject invalid name for rename', async () => {
|
||||
const { result } = renderHook(() => useEditDatasetMetadata(defaultProps))
|
||||
|
||||
await expect(
|
||||
act(async () => {
|
||||
await result.current.handleRename({
|
||||
id: '1',
|
||||
name: 'Invalid Name',
|
||||
type: DataType.string,
|
||||
count: 5,
|
||||
})
|
||||
}),
|
||||
).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe('handleDeleteMetaData', () => {
|
||||
it('should call doDeleteMetaData', async () => {
|
||||
const { result } = renderHook(() => useEditDatasetMetadata(defaultProps))
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleDeleteMetaData('1')
|
||||
})
|
||||
|
||||
expect(mockDoDeleteMetaData).toHaveBeenCalledWith('1')
|
||||
})
|
||||
|
||||
it('should call onUpdateDocList after delete', async () => {
|
||||
const onUpdateDocList = vi.fn()
|
||||
const { result } = renderHook(() =>
|
||||
useEditDatasetMetadata({ ...defaultProps, onUpdateDocList }),
|
||||
)
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleDeleteMetaData('1')
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onUpdateDocList).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Built-in Status', () => {
|
||||
it('should toggle built-in status', async () => {
|
||||
const { result } = renderHook(() => useEditDatasetMetadata(defaultProps))
|
||||
|
||||
await act(async () => {
|
||||
await result.current.setBuiltInEnabled(true)
|
||||
})
|
||||
|
||||
expect(mockToggleBuiltInStatus).toHaveBeenCalledWith(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle different datasetIds', () => {
|
||||
const { result, rerender } = renderHook(
|
||||
props => useEditDatasetMetadata(props),
|
||||
{ initialProps: defaultProps },
|
||||
)
|
||||
|
||||
expect(result.current).toBeDefined()
|
||||
|
||||
rerender({ ...defaultProps, datasetId: 'ds-2' })
|
||||
|
||||
expect(result.current).toBeDefined()
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,587 @@
|
||||
import { act, renderHook, waitFor } from '@testing-library/react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { DataType } from '../types'
|
||||
import useMetadataDocument from './use-metadata-document'
|
||||
|
||||
type DocDetail = {
|
||||
id: string
|
||||
name: string
|
||||
data_source_type: string
|
||||
word_count: number
|
||||
language?: string
|
||||
hit_count?: number
|
||||
segment_count?: number
|
||||
}
|
||||
|
||||
// Mock service hooks
|
||||
const mockMutateAsync = vi.fn().mockResolvedValue({})
|
||||
const mockDoAddMetaData = vi.fn().mockResolvedValue({})
|
||||
|
||||
vi.mock('@/service/knowledge/use-metadata', () => ({
|
||||
useBatchUpdateDocMetadata: () => ({
|
||||
mutateAsync: mockMutateAsync,
|
||||
}),
|
||||
useCreateMetaData: () => ({
|
||||
mutateAsync: mockDoAddMetaData,
|
||||
}),
|
||||
useDocumentMetaData: () => ({
|
||||
data: {
|
||||
doc_metadata: [
|
||||
{ id: '1', name: 'field_one', type: DataType.string, value: 'Value 1' },
|
||||
{ id: '2', name: 'field_two', type: DataType.number, value: 42 },
|
||||
{ id: 'built-in', name: 'created_at', type: DataType.time, value: 1609459200 },
|
||||
],
|
||||
},
|
||||
}),
|
||||
useDatasetMetaData: () => ({
|
||||
data: {
|
||||
built_in_field_enabled: true,
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock useDatasetDetailContext
|
||||
vi.mock('@/context/dataset-detail', () => ({
|
||||
useDatasetDetailContext: () => ({
|
||||
dataset: {
|
||||
embedding_available: true,
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock useMetadataMap and useLanguages with comprehensive field definitions
|
||||
vi.mock('@/hooks/use-metadata', () => ({
|
||||
useMetadataMap: () => ({
|
||||
originInfo: {
|
||||
subFieldsMap: {
|
||||
data_source_type: { label: 'Source Type', inputType: 'text' },
|
||||
language: { label: 'Language', inputType: 'select' },
|
||||
empty_field: { label: 'Empty Field', inputType: 'text' },
|
||||
},
|
||||
},
|
||||
technicalParameters: {
|
||||
subFieldsMap: {
|
||||
word_count: { label: 'Word Count', inputType: 'text' },
|
||||
hit_count: {
|
||||
label: 'Hit Count',
|
||||
inputType: 'text',
|
||||
render: (val: number, segmentCount?: number) => `${val}/${segmentCount || 0}`,
|
||||
},
|
||||
custom_render: {
|
||||
label: 'Custom Render',
|
||||
inputType: 'text',
|
||||
render: (val: string) => `Rendered: ${val}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
useLanguages: () => ({
|
||||
en: 'English',
|
||||
zh: 'Chinese',
|
||||
ja: 'Japanese',
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock Toast
|
||||
vi.mock('@/app/components/base/toast', () => ({
|
||||
default: {
|
||||
notify: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock useCheckMetadataName
|
||||
vi.mock('./use-check-metadata-name', () => ({
|
||||
default: () => ({
|
||||
checkName: (name: string) => ({
|
||||
errorMsg: name && /^[a-z][a-z0-9_]*$/.test(name) ? '' : 'Invalid name',
|
||||
}),
|
||||
}),
|
||||
}))
|
||||
|
||||
describe('useMetadataDocument', () => {
|
||||
const mockDocDetail: DocDetail = {
|
||||
id: 'doc-1',
|
||||
name: 'Test Document',
|
||||
data_source_type: 'upload_file',
|
||||
word_count: 100,
|
||||
language: 'en',
|
||||
hit_count: 50,
|
||||
segment_count: 10,
|
||||
}
|
||||
|
||||
const defaultProps = {
|
||||
datasetId: 'ds-1',
|
||||
documentId: 'doc-1',
|
||||
docDetail: mockDocDetail as Parameters<typeof useMetadataDocument>[0]['docDetail'],
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('Hook Initialization', () => {
|
||||
it('should return embeddingAvailable', () => {
|
||||
const { result } = renderHook(() => useMetadataDocument(defaultProps))
|
||||
expect(result.current.embeddingAvailable).toBe(true)
|
||||
})
|
||||
|
||||
it('should return isEdit as false initially', () => {
|
||||
const { result } = renderHook(() => useMetadataDocument(defaultProps))
|
||||
expect(result.current.isEdit).toBe(false)
|
||||
})
|
||||
|
||||
it('should return setIsEdit function', () => {
|
||||
const { result } = renderHook(() => useMetadataDocument(defaultProps))
|
||||
expect(typeof result.current.setIsEdit).toBe('function')
|
||||
})
|
||||
|
||||
it('should return list without built-in items', () => {
|
||||
const { result } = renderHook(() => useMetadataDocument(defaultProps))
|
||||
const hasBuiltIn = result.current.list.some(item => item.id === 'built-in')
|
||||
expect(hasBuiltIn).toBe(false)
|
||||
})
|
||||
|
||||
it('should return builtList with only built-in items', () => {
|
||||
const { result } = renderHook(() => useMetadataDocument(defaultProps))
|
||||
const allBuiltIn = result.current.builtList.every(item => item.id === 'built-in')
|
||||
expect(allBuiltIn).toBe(true)
|
||||
})
|
||||
|
||||
it('should return tempList', () => {
|
||||
const { result } = renderHook(() => useMetadataDocument(defaultProps))
|
||||
expect(Array.isArray(result.current.tempList)).toBe(true)
|
||||
})
|
||||
|
||||
it('should return setTempList function', () => {
|
||||
const { result } = renderHook(() => useMetadataDocument(defaultProps))
|
||||
expect(typeof result.current.setTempList).toBe('function')
|
||||
})
|
||||
|
||||
it('should return hasData based on list length', () => {
|
||||
const { result } = renderHook(() => useMetadataDocument(defaultProps))
|
||||
expect(result.current.hasData).toBe(result.current.list.length > 0)
|
||||
})
|
||||
|
||||
it('should return builtInEnabled', () => {
|
||||
const { result } = renderHook(() => useMetadataDocument(defaultProps))
|
||||
expect(typeof result.current.builtInEnabled).toBe('boolean')
|
||||
})
|
||||
|
||||
it('should return originInfo', () => {
|
||||
const { result } = renderHook(() => useMetadataDocument(defaultProps))
|
||||
expect(Array.isArray(result.current.originInfo)).toBe(true)
|
||||
})
|
||||
|
||||
it('should return technicalParameters', () => {
|
||||
const { result } = renderHook(() => useMetadataDocument(defaultProps))
|
||||
expect(Array.isArray(result.current.technicalParameters)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Edit Mode', () => {
|
||||
it('should enter edit mode when startToEdit is called', () => {
|
||||
const { result } = renderHook(() => useMetadataDocument(defaultProps))
|
||||
|
||||
act(() => {
|
||||
result.current.startToEdit()
|
||||
})
|
||||
|
||||
expect(result.current.isEdit).toBe(true)
|
||||
})
|
||||
|
||||
it('should exit edit mode when handleCancel is called', () => {
|
||||
const { result } = renderHook(() => useMetadataDocument(defaultProps))
|
||||
|
||||
act(() => {
|
||||
result.current.startToEdit()
|
||||
})
|
||||
|
||||
act(() => {
|
||||
result.current.handleCancel()
|
||||
})
|
||||
|
||||
expect(result.current.isEdit).toBe(false)
|
||||
})
|
||||
|
||||
it('should reset tempList when handleCancel is called', () => {
|
||||
const { result } = renderHook(() => useMetadataDocument(defaultProps))
|
||||
|
||||
act(() => {
|
||||
result.current.startToEdit()
|
||||
})
|
||||
|
||||
const originalLength = result.current.list.length
|
||||
|
||||
act(() => {
|
||||
result.current.setTempList([])
|
||||
})
|
||||
|
||||
act(() => {
|
||||
result.current.handleCancel()
|
||||
})
|
||||
|
||||
expect(result.current.tempList.length).toBe(originalLength)
|
||||
})
|
||||
})
|
||||
|
||||
describe('handleSelectMetaData', () => {
|
||||
it('should add metadata to tempList if not exists', () => {
|
||||
const { result } = renderHook(() => useMetadataDocument(defaultProps))
|
||||
|
||||
act(() => {
|
||||
result.current.startToEdit()
|
||||
})
|
||||
|
||||
const initialLength = result.current.tempList.length
|
||||
|
||||
act(() => {
|
||||
result.current.handleSelectMetaData({
|
||||
id: 'new-id',
|
||||
name: 'new_field',
|
||||
type: DataType.string,
|
||||
value: null,
|
||||
})
|
||||
})
|
||||
|
||||
expect(result.current.tempList.length).toBe(initialLength + 1)
|
||||
})
|
||||
|
||||
it('should not add duplicate metadata', () => {
|
||||
const { result } = renderHook(() => useMetadataDocument(defaultProps))
|
||||
|
||||
act(() => {
|
||||
result.current.startToEdit()
|
||||
})
|
||||
|
||||
const initialLength = result.current.tempList.length
|
||||
|
||||
// Try to add existing item
|
||||
if (result.current.tempList.length > 0) {
|
||||
act(() => {
|
||||
result.current.handleSelectMetaData(result.current.tempList[0])
|
||||
})
|
||||
|
||||
expect(result.current.tempList.length).toBe(initialLength)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('handleAddMetaData', () => {
|
||||
it('should call doAddMetaData with valid name', async () => {
|
||||
const { result } = renderHook(() => useMetadataDocument(defaultProps))
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleAddMetaData({
|
||||
name: 'valid_field',
|
||||
type: DataType.string,
|
||||
})
|
||||
})
|
||||
|
||||
expect(mockDoAddMetaData).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should reject invalid name', async () => {
|
||||
const { result } = renderHook(() => useMetadataDocument(defaultProps))
|
||||
|
||||
await expect(
|
||||
act(async () => {
|
||||
await result.current.handleAddMetaData({
|
||||
name: '',
|
||||
type: DataType.string,
|
||||
})
|
||||
}),
|
||||
).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe('handleSave', () => {
|
||||
it('should call mutateAsync to save metadata', async () => {
|
||||
const { result } = renderHook(() => useMetadataDocument(defaultProps))
|
||||
|
||||
act(() => {
|
||||
result.current.startToEdit()
|
||||
})
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleSave()
|
||||
})
|
||||
|
||||
expect(mockMutateAsync).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should exit edit mode after save', async () => {
|
||||
const { result } = renderHook(() => useMetadataDocument(defaultProps))
|
||||
|
||||
act(() => {
|
||||
result.current.startToEdit()
|
||||
})
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleSave()
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isEdit).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('getReadOnlyMetaData - originInfo', () => {
|
||||
it('should return origin info with correct structure', () => {
|
||||
const { result } = renderHook(() => useMetadataDocument(defaultProps))
|
||||
|
||||
expect(result.current.originInfo).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
type: DataType.string,
|
||||
}),
|
||||
]),
|
||||
)
|
||||
})
|
||||
|
||||
it('should use languageMap for language field (select type)', () => {
|
||||
const { result } = renderHook(() => useMetadataDocument(defaultProps))
|
||||
|
||||
// Find language field in originInfo
|
||||
const languageField = result.current.originInfo.find(
|
||||
item => item.name === 'Language',
|
||||
)
|
||||
|
||||
// If language field exists and docDetail has language 'en', value should be 'English'
|
||||
if (languageField)
|
||||
expect(languageField.value).toBe('English')
|
||||
})
|
||||
|
||||
it('should return dash for empty field values', () => {
|
||||
const docDetailWithEmpty: DocDetail = {
|
||||
id: 'doc-1',
|
||||
name: 'Test Document',
|
||||
data_source_type: 'upload_file',
|
||||
word_count: 100,
|
||||
}
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useMetadataDocument({
|
||||
...defaultProps,
|
||||
docDetail: docDetailWithEmpty as Parameters<typeof useMetadataDocument>[0]['docDetail'],
|
||||
}),
|
||||
)
|
||||
|
||||
// Check if there's any field with '-' value (meaning empty)
|
||||
const hasEmptyField = result.current.originInfo.some(
|
||||
item => item.value === '-',
|
||||
)
|
||||
// language field should return '-' since it's not set
|
||||
expect(hasEmptyField).toBe(true)
|
||||
})
|
||||
|
||||
it('should return empty object for non-language select fields', () => {
|
||||
// This tests the else branch of getTargetMap where field !== 'language'
|
||||
const { result } = renderHook(() => useMetadataDocument(defaultProps))
|
||||
|
||||
// The data_source_type field is a text field, not select
|
||||
const sourceTypeField = result.current.originInfo.find(
|
||||
item => item.name === 'Source Type',
|
||||
)
|
||||
|
||||
// It should return the raw value since it's not a select type
|
||||
if (sourceTypeField)
|
||||
expect(sourceTypeField.value).toBe('upload_file')
|
||||
})
|
||||
})
|
||||
|
||||
describe('getReadOnlyMetaData - technicalParameters', () => {
|
||||
it('should return technical parameters with correct structure', () => {
|
||||
const { result } = renderHook(() => useMetadataDocument(defaultProps))
|
||||
|
||||
expect(result.current.technicalParameters).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
type: DataType.string,
|
||||
}),
|
||||
]),
|
||||
)
|
||||
})
|
||||
|
||||
it('should use render function when available', () => {
|
||||
const { result } = renderHook(() => useMetadataDocument(defaultProps))
|
||||
|
||||
// Find hit_count field which has a render function
|
||||
const hitCountField = result.current.technicalParameters.find(
|
||||
item => item.name === 'Hit Count',
|
||||
)
|
||||
|
||||
// The render function should format as "val/segmentCount"
|
||||
if (hitCountField)
|
||||
expect(hitCountField.value).toBe('50/10')
|
||||
})
|
||||
|
||||
it('should return raw value when no render function', () => {
|
||||
const { result } = renderHook(() => useMetadataDocument(defaultProps))
|
||||
|
||||
// Find word_count field which has no render function
|
||||
const wordCountField = result.current.technicalParameters.find(
|
||||
item => item.name === 'Word Count',
|
||||
)
|
||||
|
||||
if (wordCountField)
|
||||
expect(wordCountField.value).toBe(100)
|
||||
})
|
||||
|
||||
it('should handle fields with render function and undefined segment_count', () => {
|
||||
const docDetailNoSegment: DocDetail = {
|
||||
id: 'doc-1',
|
||||
name: 'Test Document',
|
||||
data_source_type: 'upload_file',
|
||||
word_count: 100,
|
||||
hit_count: 25,
|
||||
}
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useMetadataDocument({
|
||||
...defaultProps,
|
||||
docDetail: docDetailNoSegment as Parameters<typeof useMetadataDocument>[0]['docDetail'],
|
||||
}),
|
||||
)
|
||||
|
||||
const hitCountField = result.current.technicalParameters.find(
|
||||
item => item.name === 'Hit Count',
|
||||
)
|
||||
|
||||
// Should use 0 as default for segment_count
|
||||
if (hitCountField)
|
||||
expect(hitCountField.value).toBe('25/0')
|
||||
})
|
||||
|
||||
it('should return dash for null/undefined values', () => {
|
||||
const docDetailWithNull: DocDetail = {
|
||||
id: 'doc-1',
|
||||
name: 'Test Document',
|
||||
data_source_type: '',
|
||||
word_count: 0,
|
||||
}
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useMetadataDocument({
|
||||
...defaultProps,
|
||||
docDetail: docDetailWithNull as Parameters<typeof useMetadataDocument>[0]['docDetail'],
|
||||
}),
|
||||
)
|
||||
|
||||
// 0 should still be shown, but empty string should show '-'
|
||||
const sourceTypeField = result.current.originInfo.find(
|
||||
item => item.name === 'Source Type',
|
||||
)
|
||||
|
||||
if (sourceTypeField)
|
||||
expect(sourceTypeField.value).toBe('-')
|
||||
})
|
||||
|
||||
it('should handle 0 value correctly (not treated as empty)', () => {
|
||||
const docDetailWithZero: DocDetail = {
|
||||
id: 'doc-1',
|
||||
name: 'Test Document',
|
||||
data_source_type: 'upload_file',
|
||||
word_count: 0,
|
||||
}
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useMetadataDocument({
|
||||
...defaultProps,
|
||||
docDetail: docDetailWithZero as Parameters<typeof useMetadataDocument>[0]['docDetail'],
|
||||
}),
|
||||
)
|
||||
|
||||
// word_count of 0 should still show 0, not '-'
|
||||
const wordCountField = result.current.technicalParameters.find(
|
||||
item => item.name === 'Word Count',
|
||||
)
|
||||
|
||||
if (wordCountField)
|
||||
expect(wordCountField.value).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle empty docDetail', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useMetadataDocument({
|
||||
...defaultProps,
|
||||
docDetail: {} as Parameters<typeof useMetadataDocument>[0]['docDetail'],
|
||||
}),
|
||||
)
|
||||
|
||||
expect(result.current).toBeDefined()
|
||||
})
|
||||
|
||||
it('should handle different datasetIds', () => {
|
||||
const { result, rerender } = renderHook(
|
||||
props => useMetadataDocument(props),
|
||||
{ initialProps: defaultProps },
|
||||
)
|
||||
|
||||
expect(result.current).toBeDefined()
|
||||
|
||||
rerender({ ...defaultProps, datasetId: 'ds-2' })
|
||||
|
||||
expect(result.current).toBeDefined()
|
||||
})
|
||||
|
||||
it('should handle docDetail with all fields', () => {
|
||||
const fullDocDetail: DocDetail = {
|
||||
id: 'doc-1',
|
||||
name: 'Full Document',
|
||||
data_source_type: 'website',
|
||||
word_count: 500,
|
||||
language: 'zh',
|
||||
hit_count: 100,
|
||||
segment_count: 20,
|
||||
}
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useMetadataDocument({
|
||||
...defaultProps,
|
||||
docDetail: fullDocDetail as Parameters<typeof useMetadataDocument>[0]['docDetail'],
|
||||
}),
|
||||
)
|
||||
|
||||
// Language should be mapped
|
||||
const languageField = result.current.originInfo.find(
|
||||
item => item.name === 'Language',
|
||||
)
|
||||
if (languageField)
|
||||
expect(languageField.value).toBe('Chinese')
|
||||
|
||||
// Hit count should be rendered
|
||||
const hitCountField = result.current.technicalParameters.find(
|
||||
item => item.name === 'Hit Count',
|
||||
)
|
||||
if (hitCountField)
|
||||
expect(hitCountField.value).toBe('100/20')
|
||||
})
|
||||
|
||||
it('should handle unknown language', () => {
|
||||
const unknownLangDetail: DocDetail = {
|
||||
id: 'doc-1',
|
||||
name: 'Unknown Lang Document',
|
||||
data_source_type: 'upload_file',
|
||||
word_count: 100,
|
||||
language: 'unknown_lang',
|
||||
}
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useMetadataDocument({
|
||||
...defaultProps,
|
||||
docDetail: unknownLangDetail as Parameters<typeof useMetadataDocument>[0]['docDetail'],
|
||||
}),
|
||||
)
|
||||
|
||||
// Unknown language should return undefined from the map
|
||||
const languageField = result.current.originInfo.find(
|
||||
item => item.name === 'Language',
|
||||
)
|
||||
// When language is not in map, it returns undefined
|
||||
expect(languageField?.value).toBeUndefined()
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user