chore: add tests for config string and dataset card item (#29743)
This commit is contained in:
@@ -0,0 +1,121 @@
|
|||||||
|
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||||
|
import ConfigString, { type IConfigStringProps } from './index'
|
||||||
|
|
||||||
|
const renderConfigString = (props?: Partial<IConfigStringProps>) => {
|
||||||
|
const onChange = jest.fn()
|
||||||
|
const defaultProps: IConfigStringProps = {
|
||||||
|
value: 5,
|
||||||
|
maxLength: 10,
|
||||||
|
modelId: 'model-id',
|
||||||
|
onChange,
|
||||||
|
}
|
||||||
|
|
||||||
|
render(<ConfigString {...defaultProps} {...props} />)
|
||||||
|
|
||||||
|
return { onChange }
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('ConfigString', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Rendering', () => {
|
||||||
|
it('should render numeric input with bounds', () => {
|
||||||
|
renderConfigString({ value: 3, maxLength: 8 })
|
||||||
|
|
||||||
|
const input = screen.getByRole('spinbutton')
|
||||||
|
|
||||||
|
expect(input).toHaveValue(3)
|
||||||
|
expect(input).toHaveAttribute('min', '1')
|
||||||
|
expect(input).toHaveAttribute('max', '8')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should render empty input when value is undefined', () => {
|
||||||
|
const { onChange } = renderConfigString({ value: undefined })
|
||||||
|
|
||||||
|
expect(screen.getByRole('spinbutton')).toHaveValue(null)
|
||||||
|
expect(onChange).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Effect behavior', () => {
|
||||||
|
it('should clamp initial value to maxLength when it exceeds limit', async () => {
|
||||||
|
const onChange = jest.fn()
|
||||||
|
render(
|
||||||
|
<ConfigString
|
||||||
|
value={15}
|
||||||
|
maxLength={10}
|
||||||
|
modelId="model-id"
|
||||||
|
onChange={onChange}
|
||||||
|
/>,
|
||||||
|
)
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(onChange).toHaveBeenCalledWith(10)
|
||||||
|
})
|
||||||
|
expect(onChange).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should clamp when updated prop value exceeds maxLength', async () => {
|
||||||
|
const onChange = jest.fn()
|
||||||
|
const { rerender } = render(
|
||||||
|
<ConfigString
|
||||||
|
value={4}
|
||||||
|
maxLength={6}
|
||||||
|
modelId="model-id"
|
||||||
|
onChange={onChange}
|
||||||
|
/>,
|
||||||
|
)
|
||||||
|
|
||||||
|
rerender(
|
||||||
|
<ConfigString
|
||||||
|
value={9}
|
||||||
|
maxLength={6}
|
||||||
|
modelId="model-id"
|
||||||
|
onChange={onChange}
|
||||||
|
/>,
|
||||||
|
)
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(onChange).toHaveBeenCalledWith(6)
|
||||||
|
})
|
||||||
|
expect(onChange).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('User interactions', () => {
|
||||||
|
it('should clamp entered value above maxLength', () => {
|
||||||
|
const { onChange } = renderConfigString({ maxLength: 7 })
|
||||||
|
|
||||||
|
fireEvent.change(screen.getByRole('spinbutton'), { target: { value: '12' } })
|
||||||
|
|
||||||
|
expect(onChange).toHaveBeenCalledWith(7)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should raise value below minimum to one', () => {
|
||||||
|
const { onChange } = renderConfigString()
|
||||||
|
|
||||||
|
fireEvent.change(screen.getByRole('spinbutton'), { target: { value: '0' } })
|
||||||
|
|
||||||
|
expect(onChange).toHaveBeenCalledWith(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should forward parsed value when within bounds', () => {
|
||||||
|
const { onChange } = renderConfigString({ maxLength: 9 })
|
||||||
|
|
||||||
|
fireEvent.change(screen.getByRole('spinbutton'), { target: { value: '7' } })
|
||||||
|
|
||||||
|
expect(onChange).toHaveBeenCalledWith(7)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should pass through NaN when input is cleared', () => {
|
||||||
|
const { onChange } = renderConfigString()
|
||||||
|
|
||||||
|
fireEvent.change(screen.getByRole('spinbutton'), { target: { value: '' } })
|
||||||
|
|
||||||
|
expect(onChange).toHaveBeenCalledTimes(1)
|
||||||
|
expect(onChange.mock.calls[0][0]).toBeNaN()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,242 @@
|
|||||||
|
import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'
|
||||||
|
import userEvent from '@testing-library/user-event'
|
||||||
|
import Item from './index'
|
||||||
|
import type React from 'react'
|
||||||
|
import type { DataSet } from '@/models/datasets'
|
||||||
|
import { ChunkingMode, DataSourceType, DatasetPermission } from '@/models/datasets'
|
||||||
|
import type { IndexingType } from '@/app/components/datasets/create/step-two'
|
||||||
|
import type { RetrievalConfig } from '@/types/app'
|
||||||
|
import { RETRIEVE_METHOD } from '@/types/app'
|
||||||
|
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||||
|
|
||||||
|
jest.mock('../settings-modal', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: ({ onSave, onCancel, currentDataset }: any) => (
|
||||||
|
<div>
|
||||||
|
<div>Mock settings modal</div>
|
||||||
|
<button onClick={() => onSave({ ...currentDataset, name: 'Updated dataset' })}>Save changes</button>
|
||||||
|
<button onClick={onCancel}>Close</button>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
}))
|
||||||
|
|
||||||
|
jest.mock('@/hooks/use-breakpoints', () => {
|
||||||
|
const actual = jest.requireActual('@/hooks/use-breakpoints')
|
||||||
|
return {
|
||||||
|
__esModule: true,
|
||||||
|
...actual,
|
||||||
|
default: jest.fn(() => actual.MediaType.pc),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const mockedUseBreakpoints = useBreakpoints as jest.MockedFunction<typeof useBreakpoints>
|
||||||
|
|
||||||
|
const baseRetrievalConfig: RetrievalConfig = {
|
||||||
|
search_method: RETRIEVE_METHOD.semantic,
|
||||||
|
reranking_enable: false,
|
||||||
|
reranking_model: {
|
||||||
|
reranking_provider_name: 'provider',
|
||||||
|
reranking_model_name: 'rerank-model',
|
||||||
|
},
|
||||||
|
top_k: 4,
|
||||||
|
score_threshold_enabled: false,
|
||||||
|
score_threshold: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultIndexingTechnique: IndexingType = 'high_quality' as IndexingType
|
||||||
|
|
||||||
|
const createDataset = (overrides: Partial<DataSet> = {}): DataSet => {
|
||||||
|
const {
|
||||||
|
retrieval_model,
|
||||||
|
retrieval_model_dict,
|
||||||
|
icon_info,
|
||||||
|
...restOverrides
|
||||||
|
} = overrides
|
||||||
|
|
||||||
|
const resolvedRetrievalModelDict = {
|
||||||
|
...baseRetrievalConfig,
|
||||||
|
...retrieval_model_dict,
|
||||||
|
}
|
||||||
|
const resolvedRetrievalModel = {
|
||||||
|
...baseRetrievalConfig,
|
||||||
|
...(retrieval_model ?? retrieval_model_dict),
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultIconInfo = {
|
||||||
|
icon: '📘',
|
||||||
|
icon_type: 'emoji',
|
||||||
|
icon_background: '#FFEAD5',
|
||||||
|
icon_url: '',
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolvedIconInfo = ('icon_info' in overrides)
|
||||||
|
? icon_info
|
||||||
|
: defaultIconInfo
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: 'dataset-id',
|
||||||
|
name: 'Dataset Name',
|
||||||
|
indexing_status: 'completed',
|
||||||
|
icon_info: resolvedIconInfo as DataSet['icon_info'],
|
||||||
|
description: 'A test dataset',
|
||||||
|
permission: DatasetPermission.onlyMe,
|
||||||
|
data_source_type: DataSourceType.FILE,
|
||||||
|
indexing_technique: defaultIndexingTechnique,
|
||||||
|
author_name: 'author',
|
||||||
|
created_by: 'creator',
|
||||||
|
updated_by: 'updater',
|
||||||
|
updated_at: 0,
|
||||||
|
app_count: 0,
|
||||||
|
doc_form: ChunkingMode.text,
|
||||||
|
document_count: 0,
|
||||||
|
total_document_count: 0,
|
||||||
|
total_available_documents: 0,
|
||||||
|
word_count: 0,
|
||||||
|
provider: 'dify',
|
||||||
|
embedding_model: 'text-embedding',
|
||||||
|
embedding_model_provider: 'openai',
|
||||||
|
embedding_available: true,
|
||||||
|
retrieval_model_dict: resolvedRetrievalModelDict,
|
||||||
|
retrieval_model: resolvedRetrievalModel,
|
||||||
|
tags: [],
|
||||||
|
external_knowledge_info: {
|
||||||
|
external_knowledge_id: 'external-id',
|
||||||
|
external_knowledge_api_id: 'api-id',
|
||||||
|
external_knowledge_api_name: 'api-name',
|
||||||
|
external_knowledge_api_endpoint: 'https://endpoint',
|
||||||
|
},
|
||||||
|
external_retrieval_model: {
|
||||||
|
top_k: 2,
|
||||||
|
score_threshold: 0.5,
|
||||||
|
score_threshold_enabled: true,
|
||||||
|
},
|
||||||
|
built_in_field_enabled: true,
|
||||||
|
doc_metadata: [],
|
||||||
|
keyword_number: 3,
|
||||||
|
pipeline_id: 'pipeline-id',
|
||||||
|
is_published: true,
|
||||||
|
runtime_mode: 'general',
|
||||||
|
enable_api: true,
|
||||||
|
is_multimodal: false,
|
||||||
|
...restOverrides,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderItem = (config: DataSet, props?: Partial<React.ComponentProps<typeof Item>>) => {
|
||||||
|
const onSave = jest.fn()
|
||||||
|
const onRemove = jest.fn()
|
||||||
|
|
||||||
|
render(
|
||||||
|
<Item
|
||||||
|
config={config}
|
||||||
|
onSave={onSave}
|
||||||
|
onRemove={onRemove}
|
||||||
|
{...props}
|
||||||
|
/>,
|
||||||
|
)
|
||||||
|
|
||||||
|
return { onSave, onRemove }
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('dataset-config/card-item', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
mockedUseBreakpoints.mockReturnValue(MediaType.pc)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should render dataset details with indexing and external badges', () => {
|
||||||
|
const dataset = createDataset({
|
||||||
|
provider: 'external',
|
||||||
|
retrieval_model_dict: {
|
||||||
|
...baseRetrievalConfig,
|
||||||
|
search_method: RETRIEVE_METHOD.semantic,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
renderItem(dataset)
|
||||||
|
|
||||||
|
const card = screen.getByText(dataset.name).closest('.group') as HTMLElement
|
||||||
|
const actionButtons = within(card).getAllByRole('button', { hidden: true })
|
||||||
|
|
||||||
|
expect(screen.getByText(dataset.name)).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('dataset.indexingTechnique.high_quality · dataset.indexingMethod.semantic_search')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('dataset.externalTag')).toBeInTheDocument()
|
||||||
|
expect(actionButtons).toHaveLength(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should open settings drawer from edit action and close after saving', async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
const dataset = createDataset()
|
||||||
|
const { onSave } = renderItem(dataset)
|
||||||
|
|
||||||
|
const card = screen.getByText(dataset.name).closest('.group') as HTMLElement
|
||||||
|
const [editButton] = within(card).getAllByRole('button', { hidden: true })
|
||||||
|
await user.click(editButton)
|
||||||
|
|
||||||
|
expect(screen.getByText('Mock settings modal')).toBeInTheDocument()
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByRole('dialog')).toBeVisible()
|
||||||
|
})
|
||||||
|
|
||||||
|
await user.click(screen.getByText('Save changes'))
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(onSave).toHaveBeenCalledWith(expect.objectContaining({ name: 'Updated dataset' }))
|
||||||
|
})
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Mock settings modal')).not.toBeVisible()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should call onRemove and toggle destructive state on hover', async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
const dataset = createDataset()
|
||||||
|
const { onRemove } = renderItem(dataset)
|
||||||
|
|
||||||
|
const card = screen.getByText(dataset.name).closest('.group') as HTMLElement
|
||||||
|
const buttons = within(card).getAllByRole('button', { hidden: true })
|
||||||
|
const deleteButton = buttons[buttons.length - 1]
|
||||||
|
|
||||||
|
expect(deleteButton.className).not.toContain('action-btn-destructive')
|
||||||
|
|
||||||
|
fireEvent.mouseEnter(deleteButton)
|
||||||
|
expect(deleteButton.className).toContain('action-btn-destructive')
|
||||||
|
expect(card.className).toContain('border-state-destructive-border')
|
||||||
|
|
||||||
|
fireEvent.mouseLeave(deleteButton)
|
||||||
|
expect(deleteButton.className).not.toContain('action-btn-destructive')
|
||||||
|
|
||||||
|
await user.click(deleteButton)
|
||||||
|
expect(onRemove).toHaveBeenCalledWith(dataset.id)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should use default icon information when icon details are missing', () => {
|
||||||
|
const dataset = createDataset({ icon_info: undefined })
|
||||||
|
|
||||||
|
renderItem(dataset)
|
||||||
|
|
||||||
|
const nameElement = screen.getByText(dataset.name)
|
||||||
|
const iconElement = nameElement.parentElement?.firstElementChild as HTMLElement
|
||||||
|
|
||||||
|
expect(iconElement).toHaveStyle({ background: '#FFF4ED' })
|
||||||
|
expect(iconElement.querySelector('em-emoji')).toHaveAttribute('id', '📙')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should apply mask overlay on mobile when drawer is open', async () => {
|
||||||
|
mockedUseBreakpoints.mockReturnValue(MediaType.mobile)
|
||||||
|
const user = userEvent.setup()
|
||||||
|
const dataset = createDataset()
|
||||||
|
|
||||||
|
renderItem(dataset)
|
||||||
|
|
||||||
|
const card = screen.getByText(dataset.name).closest('.group') as HTMLElement
|
||||||
|
const [editButton] = within(card).getAllByRole('button', { hidden: true })
|
||||||
|
await user.click(editButton)
|
||||||
|
expect(screen.getByText('Mock settings modal')).toBeInTheDocument()
|
||||||
|
|
||||||
|
const overlay = Array.from(document.querySelectorAll('[class]'))
|
||||||
|
.find(element => element.className.toString().includes('bg-black/30'))
|
||||||
|
|
||||||
|
expect(overlay).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user