mirror of https://github.com/langgenius/dify.git
588 lines
17 KiB
TypeScript
588 lines
17 KiB
TypeScript
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()
|
|
})
|
|
})
|
|
})
|