test(workflow): improve dataset item tests with edit and remove functionality (#33937)

This commit is contained in:
Coding On Star 2026-03-23 20:16:59 +08:00 committed by GitHub
parent 1b1df37d23
commit 8b6fc07019
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 102 additions and 35 deletions

View File

@ -21,6 +21,8 @@ let clientWidthSpy: { mockRestore: () => void } | null = null
let clientHeightSpy: { mockRestore: () => void } | null = null let clientHeightSpy: { mockRestore: () => void } | null = null
let offsetWidthSpy: { mockRestore: () => void } | null = null let offsetWidthSpy: { mockRestore: () => void } | null = null
let offsetHeightSpy: { mockRestore: () => void } | null = null let offsetHeightSpy: { mockRestore: () => void } | null = null
let consoleErrorSpy: ReturnType<typeof vi.spyOn> | null = null
let consoleWarnSpy: ReturnType<typeof vi.spyOn> | null = null
type AudioContextCtor = new () => unknown type AudioContextCtor = new () => unknown
type WindowWithLegacyAudio = Window & { type WindowWithLegacyAudio = Window & {
@ -83,6 +85,8 @@ describe('CodeBlock', () => {
beforeEach(() => { beforeEach(() => {
vi.clearAllMocks() vi.clearAllMocks()
mockUseTheme.mockReturnValue({ theme: Theme.light }) mockUseTheme.mockReturnValue({ theme: Theme.light })
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
clientWidthSpy = vi.spyOn(HTMLElement.prototype, 'clientWidth', 'get').mockReturnValue(900) clientWidthSpy = vi.spyOn(HTMLElement.prototype, 'clientWidth', 'get').mockReturnValue(900)
clientHeightSpy = vi.spyOn(HTMLElement.prototype, 'clientHeight', 'get').mockReturnValue(400) clientHeightSpy = vi.spyOn(HTMLElement.prototype, 'clientHeight', 'get').mockReturnValue(400)
offsetWidthSpy = vi.spyOn(HTMLElement.prototype, 'offsetWidth', 'get').mockReturnValue(900) offsetWidthSpy = vi.spyOn(HTMLElement.prototype, 'offsetWidth', 'get').mockReturnValue(900)
@ -98,6 +102,10 @@ describe('CodeBlock', () => {
afterEach(() => { afterEach(() => {
vi.useRealTimers() vi.useRealTimers()
consoleErrorSpy?.mockRestore()
consoleWarnSpy?.mockRestore()
consoleErrorSpy = null
consoleWarnSpy = null
clientWidthSpy?.mockRestore() clientWidthSpy?.mockRestore()
clientHeightSpy?.mockRestore() clientHeightSpy?.mockRestore()
offsetWidthSpy?.mockRestore() offsetWidthSpy?.mockRestore()

View File

@ -85,13 +85,30 @@ const CodeBlock: any = memo(({ inline, className, children = '', ...props }: any
const processedRef = useRef<boolean>(false) // Track if content was successfully processed const processedRef = useRef<boolean>(false) // Track if content was successfully processed
const isInitialRenderRef = useRef<boolean>(true) // Track if this is initial render const isInitialRenderRef = useRef<boolean>(true) // Track if this is initial render
const chartInstanceRef = useRef<any>(null) // Direct reference to ECharts instance const chartInstanceRef = useRef<any>(null) // Direct reference to ECharts instance
const resizeTimerRef = useRef<NodeJS.Timeout | null>(null) // For debounce handling const resizeTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null) // For debounce handling
const chartReadyTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
const finishedEventCountRef = useRef<number>(0) // Track finished event trigger count const finishedEventCountRef = useRef<number>(0) // Track finished event trigger count
const match = /language-(\w+)/.exec(className || '') const match = /language-(\w+)/.exec(className || '')
const language = match?.[1] const language = match?.[1]
const languageShowName = getCorrectCapitalizationLanguageName(language || '') const languageShowName = getCorrectCapitalizationLanguageName(language || '')
const isDarkMode = theme === Theme.dark const isDarkMode = theme === Theme.dark
const clearResizeTimer = useCallback(() => {
if (!resizeTimerRef.current)
return
clearTimeout(resizeTimerRef.current)
resizeTimerRef.current = null
}, [])
const clearChartReadyTimer = useCallback(() => {
if (!chartReadyTimerRef.current)
return
clearTimeout(chartReadyTimerRef.current)
chartReadyTimerRef.current = null
}, [])
const echartsStyle = useMemo(() => ({ const echartsStyle = useMemo(() => ({
height: '350px', height: '350px',
width: '100%', width: '100%',
@ -104,26 +121,27 @@ const CodeBlock: any = memo(({ inline, className, children = '', ...props }: any
// Debounce resize operations // Debounce resize operations
const debouncedResize = useCallback(() => { const debouncedResize = useCallback(() => {
if (resizeTimerRef.current) clearResizeTimer()
clearTimeout(resizeTimerRef.current)
resizeTimerRef.current = setTimeout(() => { resizeTimerRef.current = setTimeout(() => {
if (chartInstanceRef.current) if (chartInstanceRef.current)
chartInstanceRef.current.resize() chartInstanceRef.current.resize()
resizeTimerRef.current = null resizeTimerRef.current = null
}, 200) }, 200)
}, []) }, [clearResizeTimer])
// Handle ECharts instance initialization // Handle ECharts instance initialization
const handleChartReady = useCallback((instance: any) => { const handleChartReady = useCallback((instance: any) => {
chartInstanceRef.current = instance chartInstanceRef.current = instance
// Force resize to ensure timeline displays correctly // Force resize to ensure timeline displays correctly
setTimeout(() => { clearChartReadyTimer()
chartReadyTimerRef.current = setTimeout(() => {
if (chartInstanceRef.current) if (chartInstanceRef.current)
chartInstanceRef.current.resize() chartInstanceRef.current.resize()
chartReadyTimerRef.current = null
}, 200) }, 200)
}, []) }, [clearChartReadyTimer])
// Store event handlers in useMemo to avoid recreating them // Store event handlers in useMemo to avoid recreating them
const echartsEvents = useMemo(() => ({ const echartsEvents = useMemo(() => ({
@ -157,10 +175,20 @@ const CodeBlock: any = memo(({ inline, className, children = '', ...props }: any
return () => { return () => {
window.removeEventListener('resize', handleResize) window.removeEventListener('resize', handleResize)
if (resizeTimerRef.current) clearResizeTimer()
clearTimeout(resizeTimerRef.current) clearChartReadyTimer()
chartInstanceRef.current = null
} }
}, [language, debouncedResize]) }, [language, debouncedResize, clearResizeTimer, clearChartReadyTimer])
useEffect(() => {
return () => {
clearResizeTimer()
clearChartReadyTimer()
chartInstanceRef.current = null
echartsRef.current = null
}
}, [clearResizeTimer, clearChartReadyTimer])
// Process chart data when content changes // Process chart data when content changes
useEffect(() => { useEffect(() => {
// Only process echarts content // Only process echarts content

View File

@ -4,8 +4,9 @@ import type {
MetadataShape, MetadataShape,
} from '../types' } from '../types'
import type { DataSet, MetadataInDoc } from '@/models/datasets' import type { DataSet, MetadataInDoc } from '@/models/datasets'
import { fireEvent, render, screen } from '@testing-library/react' import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'
import userEvent from '@testing-library/user-event' import userEvent from '@testing-library/user-event'
import { useEffect, useRef } from 'react'
import { import {
ChunkingMode, ChunkingMode,
DatasetPermission, DatasetPermission,
@ -173,17 +174,26 @@ vi.mock('@/app/components/app/configuration/dataset-config/select-dataset', () =
vi.mock('@/app/components/app/configuration/dataset-config/settings-modal', () => ({ vi.mock('@/app/components/app/configuration/dataset-config/settings-modal', () => ({
__esModule: true, __esModule: true,
default: ({ currentDataset, onSave, onCancel }: { currentDataset: DataSet, onSave: (dataset: DataSet) => void, onCancel: () => void }) => ( default: function MockSettingsModal({ currentDataset, onSave, onCancel }: { currentDataset: DataSet, onSave: (dataset: DataSet) => void, onCancel: () => void }) {
<div> const hasSavedRef = useRef(false)
<div>{currentDataset.name}</div>
<button type="button" onClick={() => onSave(createDataset({ ...currentDataset, name: 'Updated Dataset' }))}> useEffect(() => {
save-settings if (hasSavedRef.current)
</button> return
<button type="button" onClick={onCancel}>
cancel-settings hasSavedRef.current = true
</button> onSave(createDataset({ ...currentDataset, name: 'Updated Dataset' }))
</div> }, [currentDataset, onSave])
),
return (
<div>
<div>{currentDataset.name}</div>
<button type="button" onClick={onCancel}>
cancel-settings
</button>
</div>
)
},
})) }))
vi.mock('@/app/components/app/configuration/dataset-config/params-config/config-content', () => ({ vi.mock('@/app/components/app/configuration/dataset-config/params-config/config-content', () => ({
@ -265,6 +275,13 @@ vi.mock('../components/metadata/metadata-panel', () => ({
})) }))
describe('knowledge-retrieval path', () => { describe('knowledge-retrieval path', () => {
const getDatasetItem = () => {
const datasetItem = screen.getByText('Dataset Name').closest('.group\\/dataset-item')
if (!(datasetItem instanceof HTMLElement))
throw new Error('Dataset item container not found')
return datasetItem
}
beforeEach(() => { beforeEach(() => {
vi.clearAllMocks() vi.clearAllMocks()
mockHasEditPermissionForDataset.mockReturnValue(true) mockHasEditPermissionForDataset.mockReturnValue(true)
@ -293,33 +310,43 @@ describe('knowledge-retrieval path', () => {
]) ])
}) })
it('should support editing and removing a dataset item', async () => { it('should support editing a dataset item', async () => {
const user = userEvent.setup()
const onChange = vi.fn() const onChange = vi.fn()
const onRemove = vi.fn()
render( render(
<DatasetItem <DatasetItem
payload={createDataset({ is_multimodal: true })} payload={createDataset({ is_multimodal: true })}
onChange={onChange} onChange={onChange}
onRemove={onRemove} onRemove={vi.fn()}
/>, />,
) )
expect(screen.getByText('Dataset Name')).toBeInTheDocument() expect(screen.getByText('Dataset Name')).toBeInTheDocument()
fireEvent.mouseOver(screen.getByText('Dataset Name').closest('.group\\/dataset-item')!) const datasetItem = getDatasetItem()
fireEvent.click(within(datasetItem).getByRole('button', { name: 'common.operation.edit' }))
const buttons = screen.getAllByRole('button') await waitFor(() => {
await user.click(buttons[0]!) expect(onChange).toHaveBeenCalledWith(expect.objectContaining({ name: 'Updated Dataset' }))
await user.click(screen.getByText('save-settings')) })
await user.click(buttons[1]!) })
expect(onChange).toHaveBeenCalledWith(expect.objectContaining({ name: 'Updated Dataset' })) it('should support removing a dataset item', () => {
const onRemove = vi.fn()
render(
<DatasetItem
payload={createDataset({ is_multimodal: true })}
onChange={vi.fn()}
onRemove={onRemove}
/>,
)
const datasetItem = getDatasetItem()
fireEvent.click(within(datasetItem).getByRole('button', { name: 'common.operation.remove' }))
expect(onRemove).toHaveBeenCalled() expect(onRemove).toHaveBeenCalled()
}) })
it('should render empty and populated dataset lists', async () => { it('should render empty and populated dataset lists', () => {
const user = userEvent.setup()
const onChange = vi.fn() const onChange = vi.fn()
const { rerender } = render( const { rerender } = render(
@ -338,8 +365,8 @@ describe('knowledge-retrieval path', () => {
/>, />,
) )
fireEvent.mouseOver(screen.getByText('Dataset Name').closest('.group\\/dataset-item')!) const datasetItem = getDatasetItem()
await user.click(screen.getAllByRole('button')[1]!) fireEvent.click(within(datasetItem).getByRole('button', { name: 'common.operation.remove' }))
expect(onChange).toHaveBeenCalledWith([]) expect(onChange).toHaveBeenCalledWith([])
}) })

View File

@ -85,6 +85,8 @@ const DatasetItem: FC<Props> = ({
{ {
editable && ( editable && (
<ActionButton <ActionButton
aria-label={t('operation.edit', { ns: 'common' })}
data-testid="dataset-item-edit-button"
onClick={(e) => { onClick={(e) => {
e.stopPropagation() e.stopPropagation()
showSettingsModal() showSettingsModal()
@ -95,6 +97,8 @@ const DatasetItem: FC<Props> = ({
) )
} }
<ActionButton <ActionButton
aria-label={t('operation.remove', { ns: 'common' })}
data-testid="dataset-item-remove-button"
onClick={handleRemove} onClick={handleRemove}
state={isDeleteHovered ? ActionButtonState.Destructive : ActionButtonState.Default} state={isDeleteHovered ? ActionButtonState.Destructive : ActionButtonState.Default}
onMouseEnter={() => setIsDeleteHovered(true)} onMouseEnter={() => setIsDeleteHovered(true)}