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:
Coding On Star
2026-01-20 13:07:00 +08:00
committed by GitHub
parent a715c015e7
commit 76b64dda52
56 changed files with 18890 additions and 124 deletions

View File

@ -0,0 +1,257 @@
import type { MetadataItemWithEdit } from '../types'
import { fireEvent, render, screen } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
import { DataType } from '../types'
import AddRow from './add-row'
type InputCombinedProps = {
type: DataType
value: string | number | null
onChange: (value: string | number) => void
}
type LabelProps = {
text: string
}
// Mock InputCombined component
vi.mock('./input-combined', () => ({
default: ({ type, value, onChange }: InputCombinedProps) => (
<input
data-testid="input-combined"
data-type={type}
value={value || ''}
onChange={e => onChange(e.target.value)}
/>
),
}))
// Mock Label component
vi.mock('./label', () => ({
default: ({ text }: LabelProps) => <div data-testid="label">{text}</div>,
}))
describe('AddRow', () => {
const mockPayload: MetadataItemWithEdit = {
id: 'test-id',
name: 'test_field',
type: DataType.string,
value: 'test value',
}
describe('Rendering', () => {
it('should render without crashing', () => {
const handleChange = vi.fn()
const handleRemove = vi.fn()
const { container } = render(
<AddRow payload={mockPayload} onChange={handleChange} onRemove={handleRemove} />,
)
expect(container.firstChild).toBeInTheDocument()
})
it('should render label with payload name', () => {
const handleChange = vi.fn()
const handleRemove = vi.fn()
render(
<AddRow payload={mockPayload} onChange={handleChange} onRemove={handleRemove} />,
)
expect(screen.getByTestId('label')).toHaveTextContent('test_field')
})
it('should render input combined component', () => {
const handleChange = vi.fn()
const handleRemove = vi.fn()
render(
<AddRow payload={mockPayload} onChange={handleChange} onRemove={handleRemove} />,
)
expect(screen.getByTestId('input-combined')).toBeInTheDocument()
})
it('should render remove button icon', () => {
const handleChange = vi.fn()
const handleRemove = vi.fn()
const { container } = render(
<AddRow payload={mockPayload} onChange={handleChange} onRemove={handleRemove} />,
)
const svg = container.querySelector('svg')
expect(svg).toBeInTheDocument()
})
it('should pass correct type to input combined', () => {
const handleChange = vi.fn()
const handleRemove = vi.fn()
render(
<AddRow payload={mockPayload} onChange={handleChange} onRemove={handleRemove} />,
)
expect(screen.getByTestId('input-combined')).toHaveAttribute('data-type', DataType.string)
})
it('should pass correct value to input combined', () => {
const handleChange = vi.fn()
const handleRemove = vi.fn()
render(
<AddRow payload={mockPayload} onChange={handleChange} onRemove={handleRemove} />,
)
expect(screen.getByTestId('input-combined')).toHaveValue('test value')
})
})
describe('Props', () => {
it('should apply custom className', () => {
const handleChange = vi.fn()
const handleRemove = vi.fn()
const { container } = render(
<AddRow
payload={mockPayload}
onChange={handleChange}
onRemove={handleRemove}
className="custom-class"
/>,
)
expect(container.firstChild).toHaveClass('custom-class')
})
it('should have default flex styling', () => {
const handleChange = vi.fn()
const handleRemove = vi.fn()
const { container } = render(
<AddRow payload={mockPayload} onChange={handleChange} onRemove={handleRemove} />,
)
expect(container.firstChild).toHaveClass('flex', 'h-6', 'items-center', 'space-x-0.5')
})
it('should handle different data types', () => {
const handleChange = vi.fn()
const handleRemove = vi.fn()
const numberPayload: MetadataItemWithEdit = {
...mockPayload,
type: DataType.number,
value: 42,
}
render(
<AddRow payload={numberPayload} onChange={handleChange} onRemove={handleRemove} />,
)
expect(screen.getByTestId('input-combined')).toHaveAttribute('data-type', DataType.number)
})
})
describe('User Interactions', () => {
it('should call onChange with updated payload when input changes', () => {
const handleChange = vi.fn()
const handleRemove = vi.fn()
render(
<AddRow payload={mockPayload} onChange={handleChange} onRemove={handleRemove} />,
)
fireEvent.change(screen.getByTestId('input-combined'), { target: { value: 'new value' } })
expect(handleChange).toHaveBeenCalledWith({
...mockPayload,
value: 'new value',
})
})
it('should call onRemove when remove button is clicked', () => {
const handleChange = vi.fn()
const handleRemove = vi.fn()
const { container } = render(
<AddRow payload={mockPayload} onChange={handleChange} onRemove={handleRemove} />,
)
const removeButton = container.querySelector('.cursor-pointer')
if (removeButton)
fireEvent.click(removeButton)
expect(handleRemove).toHaveBeenCalledTimes(1)
})
it('should preserve other payload properties on change', () => {
const handleChange = vi.fn()
const handleRemove = vi.fn()
render(
<AddRow payload={mockPayload} onChange={handleChange} onRemove={handleRemove} />,
)
fireEvent.change(screen.getByTestId('input-combined'), { target: { value: 'updated' } })
expect(handleChange).toHaveBeenCalledWith(
expect.objectContaining({
id: 'test-id',
name: 'test_field',
type: DataType.string,
}),
)
})
})
describe('Remove Button Styling', () => {
it('should have hover styling on remove button', () => {
const handleChange = vi.fn()
const handleRemove = vi.fn()
const { container } = render(
<AddRow payload={mockPayload} onChange={handleChange} onRemove={handleRemove} />,
)
const removeButton = container.querySelector('.cursor-pointer')
expect(removeButton).toHaveClass('hover:bg-state-destructive-hover', 'hover:text-text-destructive')
})
})
describe('Edge Cases', () => {
it('should handle null value', () => {
const handleChange = vi.fn()
const handleRemove = vi.fn()
const nullPayload: MetadataItemWithEdit = {
...mockPayload,
value: null,
}
render(
<AddRow payload={nullPayload} onChange={handleChange} onRemove={handleRemove} />,
)
expect(screen.getByTestId('input-combined')).toBeInTheDocument()
})
it('should handle empty string value', () => {
const handleChange = vi.fn()
const handleRemove = vi.fn()
const emptyPayload: MetadataItemWithEdit = {
...mockPayload,
value: '',
}
render(
<AddRow payload={emptyPayload} onChange={handleChange} onRemove={handleRemove} />,
)
expect(screen.getByTestId('input-combined')).toHaveValue('')
})
it('should handle time type payload', () => {
const handleChange = vi.fn()
const handleRemove = vi.fn()
const timePayload: MetadataItemWithEdit = {
...mockPayload,
type: DataType.time,
value: 1609459200,
}
render(
<AddRow payload={timePayload} onChange={handleChange} onRemove={handleRemove} />,
)
expect(screen.getByTestId('input-combined')).toHaveAttribute('data-type', DataType.time)
})
it('should handle multiple onRemove calls', () => {
const handleChange = vi.fn()
const handleRemove = vi.fn()
const { container } = render(
<AddRow payload={mockPayload} onChange={handleChange} onRemove={handleRemove} />,
)
const removeButton = container.querySelector('.cursor-pointer')
if (removeButton) {
fireEvent.click(removeButton)
fireEvent.click(removeButton)
fireEvent.click(removeButton)
}
expect(handleRemove).toHaveBeenCalledTimes(3)
})
})
})

View File

@ -0,0 +1,395 @@
import type { MetadataItemWithEdit } from '../types'
import { fireEvent, render, screen } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
import { DataType, UpdateType } from '../types'
import EditMetadatabatchItem from './edit-row'
type InputCombinedProps = {
type: DataType
value: string | number | null
onChange: (value: string | number) => void
readOnly?: boolean
}
type MultipleValueInputProps = {
onClear: () => void
readOnly?: boolean
}
type LabelProps = {
text: string
isDeleted?: boolean
}
type EditedBeaconProps = {
onReset: () => void
}
// Mock InputCombined component
vi.mock('./input-combined', () => ({
default: ({ type, value, onChange, readOnly }: InputCombinedProps) => (
<input
data-testid="input-combined"
data-type={type}
value={value || ''}
onChange={e => onChange(e.target.value)}
readOnly={readOnly}
/>
),
}))
// Mock InputHasSetMultipleValue component
vi.mock('./input-has-set-multiple-value', () => ({
default: ({ onClear, readOnly }: MultipleValueInputProps) => (
<div data-testid="multiple-value-input" data-readonly={readOnly}>
<button data-testid="clear-multiple" onClick={onClear}>Clear Multiple</button>
</div>
),
}))
// Mock Label component
vi.mock('./label', () => ({
default: ({ text, isDeleted }: LabelProps) => (
<div data-testid="label" data-deleted={isDeleted}>{text}</div>
),
}))
// Mock EditedBeacon component
vi.mock('./edited-beacon', () => ({
default: ({ onReset }: EditedBeaconProps) => (
<button data-testid="edited-beacon" onClick={onReset}>Reset</button>
),
}))
describe('EditMetadatabatchItem', () => {
const mockPayload: MetadataItemWithEdit = {
id: 'test-id',
name: 'test_field',
type: DataType.string,
value: 'test value',
isMultipleValue: false,
isUpdated: false,
}
describe('Rendering', () => {
it('should render without crashing', () => {
const { container } = render(
<EditMetadatabatchItem
payload={mockPayload}
onChange={vi.fn()}
onRemove={vi.fn()}
onReset={vi.fn()}
/>,
)
expect(container.firstChild).toBeInTheDocument()
})
it('should render label with payload name', () => {
render(
<EditMetadatabatchItem
payload={mockPayload}
onChange={vi.fn()}
onRemove={vi.fn()}
onReset={vi.fn()}
/>,
)
expect(screen.getByTestId('label')).toHaveTextContent('test_field')
})
it('should render input combined for single value', () => {
render(
<EditMetadatabatchItem
payload={mockPayload}
onChange={vi.fn()}
onRemove={vi.fn()}
onReset={vi.fn()}
/>,
)
expect(screen.getByTestId('input-combined')).toBeInTheDocument()
})
it('should render multiple value input when isMultipleValue is true', () => {
const multiplePayload: MetadataItemWithEdit = {
...mockPayload,
isMultipleValue: true,
}
render(
<EditMetadatabatchItem
payload={multiplePayload}
onChange={vi.fn()}
onRemove={vi.fn()}
onReset={vi.fn()}
/>,
)
expect(screen.getByTestId('multiple-value-input')).toBeInTheDocument()
})
it('should render delete button icon', () => {
const { container } = render(
<EditMetadatabatchItem
payload={mockPayload}
onChange={vi.fn()}
onRemove={vi.fn()}
onReset={vi.fn()}
/>,
)
const svg = container.querySelector('svg')
expect(svg).toBeInTheDocument()
})
})
describe('Updated State', () => {
it('should show edited beacon when isUpdated is true', () => {
const updatedPayload: MetadataItemWithEdit = {
...mockPayload,
isUpdated: true,
}
render(
<EditMetadatabatchItem
payload={updatedPayload}
onChange={vi.fn()}
onRemove={vi.fn()}
onReset={vi.fn()}
/>,
)
expect(screen.getByTestId('edited-beacon')).toBeInTheDocument()
})
it('should not show edited beacon when isUpdated is false', () => {
render(
<EditMetadatabatchItem
payload={mockPayload}
onChange={vi.fn()}
onRemove={vi.fn()}
onReset={vi.fn()}
/>,
)
expect(screen.queryByTestId('edited-beacon')).not.toBeInTheDocument()
})
})
describe('Deleted State', () => {
it('should pass isDeleted to label when updateType is delete', () => {
const deletedPayload: MetadataItemWithEdit = {
...mockPayload,
updateType: UpdateType.delete,
}
render(
<EditMetadatabatchItem
payload={deletedPayload}
onChange={vi.fn()}
onRemove={vi.fn()}
onReset={vi.fn()}
/>,
)
expect(screen.getByTestId('label')).toHaveAttribute('data-deleted', 'true')
})
it('should set readOnly on input when deleted', () => {
const deletedPayload: MetadataItemWithEdit = {
...mockPayload,
updateType: UpdateType.delete,
}
render(
<EditMetadatabatchItem
payload={deletedPayload}
onChange={vi.fn()}
onRemove={vi.fn()}
onReset={vi.fn()}
/>,
)
expect(screen.getByTestId('input-combined')).toHaveAttribute('readonly')
})
it('should have destructive styling on delete button when deleted', () => {
const deletedPayload: MetadataItemWithEdit = {
...mockPayload,
updateType: UpdateType.delete,
}
const { container } = render(
<EditMetadatabatchItem
payload={deletedPayload}
onChange={vi.fn()}
onRemove={vi.fn()}
onReset={vi.fn()}
/>,
)
const deleteButton = container.querySelector('.bg-state-destructive-hover')
expect(deleteButton).toBeInTheDocument()
})
})
describe('User Interactions', () => {
it('should call onChange with updated payload when input changes', () => {
const handleChange = vi.fn()
render(
<EditMetadatabatchItem
payload={mockPayload}
onChange={handleChange}
onRemove={vi.fn()}
onReset={vi.fn()}
/>,
)
fireEvent.change(screen.getByTestId('input-combined'), { target: { value: 'new value' } })
expect(handleChange).toHaveBeenCalledWith(
expect.objectContaining({
...mockPayload,
value: 'new value',
}),
)
})
it('should call onRemove with id when delete button is clicked', () => {
const handleRemove = vi.fn()
const { container } = render(
<EditMetadatabatchItem
payload={mockPayload}
onChange={vi.fn()}
onRemove={handleRemove}
onReset={vi.fn()}
/>,
)
const deleteButton = container.querySelector('.cursor-pointer')
if (deleteButton)
fireEvent.click(deleteButton)
expect(handleRemove).toHaveBeenCalledWith('test-id')
})
it('should call onReset with id when reset beacon is clicked', () => {
const handleReset = vi.fn()
const updatedPayload: MetadataItemWithEdit = {
...mockPayload,
isUpdated: true,
}
render(
<EditMetadatabatchItem
payload={updatedPayload}
onChange={vi.fn()}
onRemove={vi.fn()}
onReset={handleReset}
/>,
)
fireEvent.click(screen.getByTestId('edited-beacon'))
expect(handleReset).toHaveBeenCalledWith('test-id')
})
it('should call onChange to clear multiple value', () => {
const handleChange = vi.fn()
const multiplePayload: MetadataItemWithEdit = {
...mockPayload,
isMultipleValue: true,
}
render(
<EditMetadatabatchItem
payload={multiplePayload}
onChange={handleChange}
onRemove={vi.fn()}
onReset={vi.fn()}
/>,
)
fireEvent.click(screen.getByTestId('clear-multiple'))
expect(handleChange).toHaveBeenCalledWith(
expect.objectContaining({
value: null,
isMultipleValue: false,
}),
)
})
})
describe('Multiple Value State', () => {
it('should render multiple value input when isMultipleValue is true', () => {
const multiplePayload: MetadataItemWithEdit = {
...mockPayload,
isMultipleValue: true,
}
render(
<EditMetadatabatchItem
payload={multiplePayload}
onChange={vi.fn()}
onRemove={vi.fn()}
onReset={vi.fn()}
/>,
)
expect(screen.getByTestId('multiple-value-input')).toBeInTheDocument()
expect(screen.queryByTestId('input-combined')).not.toBeInTheDocument()
})
it('should pass readOnly to multiple value input when deleted', () => {
const multipleDeletedPayload: MetadataItemWithEdit = {
...mockPayload,
isMultipleValue: true,
updateType: UpdateType.delete,
}
render(
<EditMetadatabatchItem
payload={multipleDeletedPayload}
onChange={vi.fn()}
onRemove={vi.fn()}
onReset={vi.fn()}
/>,
)
expect(screen.getByTestId('multiple-value-input')).toHaveAttribute('data-readonly', 'true')
})
})
describe('Edge Cases', () => {
it('should handle payload with number type', () => {
const numberPayload: MetadataItemWithEdit = {
...mockPayload,
type: DataType.number,
value: 42,
}
render(
<EditMetadatabatchItem
payload={numberPayload}
onChange={vi.fn()}
onRemove={vi.fn()}
onReset={vi.fn()}
/>,
)
expect(screen.getByTestId('input-combined')).toHaveAttribute('data-type', DataType.number)
})
it('should handle payload with time type', () => {
const timePayload: MetadataItemWithEdit = {
...mockPayload,
type: DataType.time,
value: 1609459200,
}
render(
<EditMetadatabatchItem
payload={timePayload}
onChange={vi.fn()}
onRemove={vi.fn()}
onReset={vi.fn()}
/>,
)
expect(screen.getByTestId('input-combined')).toHaveAttribute('data-type', DataType.time)
})
it('should handle null value', () => {
const nullPayload: MetadataItemWithEdit = {
...mockPayload,
value: null,
}
render(
<EditMetadatabatchItem
payload={nullPayload}
onChange={vi.fn()}
onRemove={vi.fn()}
onReset={vi.fn()}
/>,
)
expect(screen.getByTestId('input-combined')).toBeInTheDocument()
})
})
})

View File

@ -0,0 +1,179 @@
import { fireEvent, render, waitFor } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
import EditedBeacon from './edited-beacon'
describe('EditedBeacon', () => {
describe('Rendering', () => {
it('should render without crashing', () => {
const handleReset = vi.fn()
const { container } = render(<EditedBeacon onReset={handleReset} />)
expect(container.firstChild).toBeInTheDocument()
})
it('should render with correct size', () => {
const handleReset = vi.fn()
const { container } = render(<EditedBeacon onReset={handleReset} />)
expect(container.firstChild).toHaveClass('size-4', 'cursor-pointer')
})
it('should render beacon dot by default (not hovering)', () => {
const handleReset = vi.fn()
const { container } = render(<EditedBeacon onReset={handleReset} />)
// When not hovering, should show the small beacon dot
const beaconDot = container.querySelector('.size-1')
expect(beaconDot).toBeInTheDocument()
})
})
describe('Hover State', () => {
it('should show reset icon on hover', async () => {
const handleReset = vi.fn()
const { container } = render(<EditedBeacon onReset={handleReset} />)
const wrapper = container.firstChild as HTMLElement
fireEvent.mouseEnter(wrapper)
await waitFor(() => {
// On hover, should show the reset icon (RiResetLeftLine)
const svg = container.querySelector('svg')
expect(svg).toBeInTheDocument()
})
})
it('should show beacon dot when not hovering', () => {
const handleReset = vi.fn()
const { container } = render(<EditedBeacon onReset={handleReset} />)
// By default (not hovering), should show beacon dot
const beaconDot = container.querySelector('.size-1.rounded-full.bg-text-accent-secondary')
expect(beaconDot).toBeInTheDocument()
})
it('should hide beacon dot on hover', async () => {
const handleReset = vi.fn()
const { container } = render(<EditedBeacon onReset={handleReset} />)
const wrapper = container.firstChild as HTMLElement
fireEvent.mouseEnter(wrapper)
await waitFor(() => {
// On hover, the small beacon dot should be hidden
const beaconDot = container.querySelector('.size-1.rounded-full.bg-text-accent-secondary')
expect(beaconDot).not.toBeInTheDocument()
})
})
it('should show beacon dot again on mouse leave', async () => {
const handleReset = vi.fn()
const { container } = render(<EditedBeacon onReset={handleReset} />)
const wrapper = container.firstChild as HTMLElement
// Hover
fireEvent.mouseEnter(wrapper)
await waitFor(() => {
const svg = container.querySelector('svg')
expect(svg).toBeInTheDocument()
})
// Leave
fireEvent.mouseLeave(wrapper)
await waitFor(() => {
const beaconDot = container.querySelector('.size-1.rounded-full.bg-text-accent-secondary')
expect(beaconDot).toBeInTheDocument()
})
})
})
describe('User Interactions', () => {
it('should call onReset when reset button is clicked', async () => {
const handleReset = vi.fn()
const { container } = render(<EditedBeacon onReset={handleReset} />)
const wrapper = container.firstChild as HTMLElement
// Hover to show reset button
fireEvent.mouseEnter(wrapper)
await waitFor(() => {
const resetButton = container.querySelector('.bg-text-accent-secondary')
expect(resetButton).toBeInTheDocument()
})
// Find and click the reset button (the clickable element with onClick)
const clickableElement = container.querySelector('.flex.size-4.items-center.justify-center.rounded-full.bg-text-accent-secondary')
if (clickableElement) {
fireEvent.click(clickableElement)
}
expect(handleReset).toHaveBeenCalledTimes(1)
})
it('should not call onReset when clicking beacon dot (not hovering)', () => {
const handleReset = vi.fn()
const { container } = render(<EditedBeacon onReset={handleReset} />)
// Click on the wrapper when not hovering
const wrapper = container.firstChild as HTMLElement
fireEvent.click(wrapper)
// onReset should not be called because we're not hovering
expect(handleReset).not.toHaveBeenCalled()
})
})
describe('Tooltip', () => {
it('should render tooltip on hover', async () => {
const handleReset = vi.fn()
const { container } = render(<EditedBeacon onReset={handleReset} />)
const wrapper = container.firstChild as HTMLElement
fireEvent.mouseEnter(wrapper)
// Tooltip should be rendered (it wraps the reset button)
await waitFor(() => {
const resetIcon = container.querySelector('svg')
expect(resetIcon).toBeInTheDocument()
})
})
})
describe('Edge Cases', () => {
it('should handle multiple hover/leave cycles', async () => {
const handleReset = vi.fn()
const { container } = render(<EditedBeacon onReset={handleReset} />)
const wrapper = container.firstChild as HTMLElement
for (let i = 0; i < 3; i++) {
fireEvent.mouseEnter(wrapper)
await waitFor(() => {
expect(container.querySelector('svg')).toBeInTheDocument()
})
fireEvent.mouseLeave(wrapper)
await waitFor(() => {
expect(container.querySelector('.size-1.rounded-full')).toBeInTheDocument()
})
}
})
it('should handle rapid hover/leave', async () => {
const handleReset = vi.fn()
const { container } = render(<EditedBeacon onReset={handleReset} />)
const wrapper = container.firstChild as HTMLElement
// Rapid hover/leave
fireEvent.mouseEnter(wrapper)
fireEvent.mouseLeave(wrapper)
fireEvent.mouseEnter(wrapper)
await waitFor(() => {
expect(container.querySelector('svg')).toBeInTheDocument()
})
})
})
})

View File

@ -0,0 +1,269 @@
import { fireEvent, render, screen } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
import { DataType } from '../types'
import InputCombined from './input-combined'
type DatePickerProps = {
value: number | null
onChange: (value: number) => void
className?: string
}
// Mock the base date-picker component
vi.mock('../base/date-picker', () => ({
default: ({ value, onChange, className }: DatePickerProps) => (
<div data-testid="date-picker" className={className} onClick={() => onChange(Date.now())}>
{value || 'Pick date'}
</div>
),
}))
describe('InputCombined', () => {
describe('Rendering', () => {
it('should render without crashing', () => {
const handleChange = vi.fn()
const { container } = render(
<InputCombined type={DataType.string} value="" onChange={handleChange} />,
)
expect(container.firstChild).toBeInTheDocument()
})
it('should render text input for string type', () => {
const handleChange = vi.fn()
render(
<InputCombined type={DataType.string} value="test" onChange={handleChange} />,
)
const input = screen.getByDisplayValue('test')
expect(input).toBeInTheDocument()
expect(input.tagName.toLowerCase()).toBe('input')
})
it('should render number input for number type', () => {
const handleChange = vi.fn()
render(
<InputCombined type={DataType.number} value={42} onChange={handleChange} />,
)
const input = screen.getByDisplayValue('42')
expect(input).toBeInTheDocument()
})
it('should render date picker for time type', () => {
const handleChange = vi.fn()
render(
<InputCombined type={DataType.time} value={Date.now()} onChange={handleChange} />,
)
expect(screen.getByTestId('date-picker')).toBeInTheDocument()
})
})
describe('String Input', () => {
it('should call onChange with input value for string type', () => {
const handleChange = vi.fn()
render(
<InputCombined type={DataType.string} value="" onChange={handleChange} />,
)
const input = screen.getByRole('textbox')
fireEvent.change(input, { target: { value: 'new value' } })
expect(handleChange).toHaveBeenCalledWith('new value')
})
it('should display current value for string type', () => {
const handleChange = vi.fn()
render(
<InputCombined type={DataType.string} value="existing value" onChange={handleChange} />,
)
expect(screen.getByDisplayValue('existing value')).toBeInTheDocument()
})
it('should apply readOnly prop to string input', () => {
const handleChange = vi.fn()
render(
<InputCombined type={DataType.string} value="test" onChange={handleChange} readOnly />,
)
const input = screen.getByRole('textbox')
expect(input).toHaveAttribute('readonly')
})
})
describe('Number Input', () => {
it('should call onChange with number value for number type', () => {
const handleChange = vi.fn()
render(
<InputCombined type={DataType.number} value={0} onChange={handleChange} />,
)
const input = screen.getByRole('spinbutton')
fireEvent.change(input, { target: { value: '123' } })
expect(handleChange).toHaveBeenCalled()
})
it('should display current value for number type', () => {
const handleChange = vi.fn()
render(
<InputCombined type={DataType.number} value={999} onChange={handleChange} />,
)
expect(screen.getByDisplayValue('999')).toBeInTheDocument()
})
it('should apply readOnly prop to number input', () => {
const handleChange = vi.fn()
render(
<InputCombined type={DataType.number} value={42} onChange={handleChange} readOnly />,
)
const input = screen.getByRole('spinbutton')
expect(input).toHaveAttribute('readonly')
})
})
describe('Time/Date Input', () => {
it('should render date picker for time type', () => {
const handleChange = vi.fn()
render(
<InputCombined type={DataType.time} value={1234567890} onChange={handleChange} />,
)
expect(screen.getByTestId('date-picker')).toBeInTheDocument()
})
it('should call onChange when date is selected', () => {
const handleChange = vi.fn()
render(
<InputCombined type={DataType.time} value={null} onChange={handleChange} />,
)
fireEvent.click(screen.getByTestId('date-picker'))
expect(handleChange).toHaveBeenCalled()
})
})
describe('Props', () => {
it('should apply custom className', () => {
const handleChange = vi.fn()
const { container } = render(
<InputCombined
type={DataType.string}
value=""
onChange={handleChange}
className="custom-class"
/>,
)
// Check that custom class is applied to wrapper
const wrapper = container.querySelector('.custom-class')
expect(wrapper).toBeInTheDocument()
})
it('should handle null value for string type', () => {
const handleChange = vi.fn()
render(
<InputCombined type={DataType.string} value={null} onChange={handleChange} />,
)
const input = screen.getByRole('textbox')
expect(input).toBeInTheDocument()
})
it('should handle undefined value for string type', () => {
const handleChange = vi.fn()
render(
<InputCombined type={DataType.string} value={undefined as unknown as string} onChange={handleChange} />,
)
const input = screen.getByRole('textbox')
expect(input).toBeInTheDocument()
})
it('should handle null value for number type', () => {
const handleChange = vi.fn()
render(
<InputCombined type={DataType.number} value={null} onChange={handleChange} />,
)
const input = screen.getByRole('spinbutton')
expect(input).toBeInTheDocument()
})
})
describe('Styling', () => {
it('should have correct base styling for string input', () => {
const handleChange = vi.fn()
render(
<InputCombined type={DataType.string} value="" onChange={handleChange} />,
)
const input = screen.getByRole('textbox')
expect(input).toHaveClass('h-6', 'grow', 'p-0.5', 'text-xs', 'rounded-md')
})
it('should have correct styling for number input', () => {
const handleChange = vi.fn()
render(
<InputCombined type={DataType.number} value={0} onChange={handleChange} />,
)
const input = screen.getByRole('spinbutton')
expect(input).toHaveClass('rounded-l-md')
})
})
describe('Edge Cases', () => {
it('should handle empty string value', () => {
const handleChange = vi.fn()
render(
<InputCombined type={DataType.string} value="" onChange={handleChange} />,
)
const input = screen.getByRole('textbox')
expect(input).toHaveValue('')
})
it('should handle zero value for number', () => {
const handleChange = vi.fn()
render(
<InputCombined type={DataType.number} value={0} onChange={handleChange} />,
)
expect(screen.getByDisplayValue('0')).toBeInTheDocument()
})
it('should handle negative number', () => {
const handleChange = vi.fn()
render(
<InputCombined type={DataType.number} value={-100} onChange={handleChange} />,
)
expect(screen.getByDisplayValue('-100')).toBeInTheDocument()
})
it('should handle special characters in string', () => {
const handleChange = vi.fn()
render(
<InputCombined type={DataType.string} value={'<script>alert("xss")</script>'} onChange={handleChange} />,
)
expect(screen.getByDisplayValue('<script>alert("xss")</script>')).toBeInTheDocument()
})
it('should handle switching between types', () => {
const handleChange = vi.fn()
const { rerender } = render(
<InputCombined type={DataType.string} value="test" onChange={handleChange} />,
)
expect(screen.getByRole('textbox')).toBeInTheDocument()
rerender(
<InputCombined type={DataType.number} value={42} onChange={handleChange} />,
)
expect(screen.getByRole('spinbutton')).toBeInTheDocument()
})
})
})

View File

@ -0,0 +1,147 @@
import { fireEvent, render, screen } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
import InputHasSetMultipleValue from './input-has-set-multiple-value'
describe('InputHasSetMultipleValue', () => {
describe('Rendering', () => {
it('should render without crashing', () => {
const handleClear = vi.fn()
const { container } = render(<InputHasSetMultipleValue onClear={handleClear} />)
expect(container.firstChild).toBeInTheDocument()
})
it('should render with correct wrapper styling', () => {
const handleClear = vi.fn()
const { container } = render(<InputHasSetMultipleValue onClear={handleClear} />)
expect(container.firstChild).toHaveClass('h-6', 'grow', 'rounded-md', 'bg-components-input-bg-normal', 'p-0.5')
})
it('should render multiple value text', () => {
const handleClear = vi.fn()
render(<InputHasSetMultipleValue onClear={handleClear} />)
// The text should come from i18n
expect(screen.getByText(/multipleValue|Multiple/i)).toBeInTheDocument()
})
it('should render close icon when not readOnly', () => {
const handleClear = vi.fn()
const { container } = render(<InputHasSetMultipleValue onClear={handleClear} />)
// Should have close icon (RiCloseLine)
const svg = container.querySelector('svg')
expect(svg).toBeInTheDocument()
})
})
describe('Props', () => {
it('should not show close icon when readOnly is true', () => {
const handleClear = vi.fn()
const { container } = render(<InputHasSetMultipleValue onClear={handleClear} readOnly />)
// Should not have close icon
const svg = container.querySelector('svg')
expect(svg).not.toBeInTheDocument()
})
it('should show close icon when readOnly is false', () => {
const handleClear = vi.fn()
const { container } = render(<InputHasSetMultipleValue onClear={handleClear} readOnly={false} />)
const svg = container.querySelector('svg')
expect(svg).toBeInTheDocument()
})
it('should show close icon when readOnly is undefined', () => {
const handleClear = vi.fn()
const { container } = render(<InputHasSetMultipleValue onClear={handleClear} readOnly={undefined} />)
const svg = container.querySelector('svg')
expect(svg).toBeInTheDocument()
})
it('should apply pr-1.5 padding when readOnly', () => {
const handleClear = vi.fn()
const { container } = render(<InputHasSetMultipleValue onClear={handleClear} readOnly />)
const badge = container.querySelector('.inline-flex')
expect(badge).toHaveClass('pr-1.5')
})
it('should apply pr-0.5 padding when not readOnly', () => {
const handleClear = vi.fn()
const { container } = render(<InputHasSetMultipleValue onClear={handleClear} />)
const badge = container.querySelector('.inline-flex')
expect(badge).toHaveClass('pr-0.5')
})
})
describe('User Interactions', () => {
it('should call onClear when close icon is clicked', () => {
const handleClear = vi.fn()
const { container } = render(<InputHasSetMultipleValue onClear={handleClear} />)
const closeIcon = container.querySelector('svg')
expect(closeIcon).toBeInTheDocument()
if (closeIcon) {
fireEvent.click(closeIcon)
}
expect(handleClear).toHaveBeenCalledTimes(1)
})
it('should not call onClear when readOnly and clicking on component', () => {
const handleClear = vi.fn()
const { container } = render(<InputHasSetMultipleValue onClear={handleClear} readOnly />)
// Click on the wrapper
fireEvent.click(container.firstChild as HTMLElement)
expect(handleClear).not.toHaveBeenCalled()
})
it('should call onClear multiple times on multiple clicks', () => {
const handleClear = vi.fn()
const { container } = render(<InputHasSetMultipleValue onClear={handleClear} />)
const closeIcon = container.querySelector('svg')
if (closeIcon) {
fireEvent.click(closeIcon)
fireEvent.click(closeIcon)
fireEvent.click(closeIcon)
}
expect(handleClear).toHaveBeenCalledTimes(3)
})
})
describe('Styling', () => {
it('should have badge styling', () => {
const handleClear = vi.fn()
const { container } = render(<InputHasSetMultipleValue onClear={handleClear} />)
const badge = container.querySelector('.inline-flex')
expect(badge).toHaveClass('h-5', 'items-center', 'rounded-[5px]', 'border-[0.5px]')
})
it('should have hover styles on close button wrapper', () => {
const handleClear = vi.fn()
const { container } = render(<InputHasSetMultipleValue onClear={handleClear} />)
const closeWrapper = container.querySelector('.cursor-pointer')
expect(closeWrapper).toHaveClass('hover:bg-state-base-hover', 'hover:text-text-secondary')
})
})
describe('Edge Cases', () => {
it('should render correctly when switching readOnly state', () => {
const handleClear = vi.fn()
const { container, rerender } = render(<InputHasSetMultipleValue onClear={handleClear} />)
// Initially not readOnly
expect(container.querySelector('svg')).toBeInTheDocument()
// Switch to readOnly
rerender(<InputHasSetMultipleValue onClear={handleClear} readOnly />)
expect(container.querySelector('svg')).not.toBeInTheDocument()
// Switch back to not readOnly
rerender(<InputHasSetMultipleValue onClear={handleClear} readOnly={false} />)
expect(container.querySelector('svg')).toBeInTheDocument()
})
})
})

View File

@ -0,0 +1,113 @@
import { render, screen } from '@testing-library/react'
import { describe, expect, it } from 'vitest'
import Label from './label'
describe('Label', () => {
describe('Rendering', () => {
it('should render without crashing', () => {
render(<Label text="Test Label" />)
expect(screen.getByText('Test Label')).toBeInTheDocument()
})
it('should render text with correct styling', () => {
render(<Label text="My Label" />)
const labelElement = screen.getByText('My Label')
expect(labelElement).toHaveClass('system-xs-medium', 'w-[136px]', 'shrink-0', 'truncate', 'text-text-tertiary')
})
it('should not have deleted styling by default', () => {
render(<Label text="Label" />)
const labelElement = screen.getByText('Label')
expect(labelElement).not.toHaveClass('text-text-quaternary', 'line-through')
})
})
describe('Props', () => {
it('should apply custom className', () => {
render(<Label text="Label" className="custom-class" />)
const labelElement = screen.getByText('Label')
expect(labelElement).toHaveClass('custom-class')
})
it('should merge custom className with default classes', () => {
render(<Label text="Label" className="my-custom-class" />)
const labelElement = screen.getByText('Label')
expect(labelElement).toHaveClass('system-xs-medium', 'my-custom-class')
})
it('should apply deleted styling when isDeleted is true', () => {
render(<Label text="Label" isDeleted />)
const labelElement = screen.getByText('Label')
expect(labelElement).toHaveClass('text-text-quaternary', 'line-through')
})
it('should not apply deleted styling when isDeleted is false', () => {
render(<Label text="Label" isDeleted={false} />)
const labelElement = screen.getByText('Label')
expect(labelElement).not.toHaveClass('text-text-quaternary', 'line-through')
})
it('should render different text values', () => {
const { rerender } = render(<Label text="First" />)
expect(screen.getByText('First')).toBeInTheDocument()
rerender(<Label text="Second" />)
expect(screen.getByText('Second')).toBeInTheDocument()
})
})
describe('Deleted State', () => {
it('should have strikethrough when deleted', () => {
render(<Label text="Deleted Label" isDeleted />)
const labelElement = screen.getByText('Deleted Label')
expect(labelElement).toHaveClass('line-through')
})
it('should have quaternary text color when deleted', () => {
render(<Label text="Deleted Label" isDeleted />)
const labelElement = screen.getByText('Deleted Label')
expect(labelElement).toHaveClass('text-text-quaternary')
})
it('should combine deleted styling with custom className', () => {
render(<Label text="Label" isDeleted className="custom" />)
const labelElement = screen.getByText('Label')
expect(labelElement).toHaveClass('line-through', 'custom')
})
})
describe('Edge Cases', () => {
it('should render with empty text', () => {
const { container } = render(<Label text="" />)
expect(container.firstChild).toBeInTheDocument()
})
it('should render with long text (truncation)', () => {
const longText = 'This is a very long label text that should be truncated'
render(<Label text={longText} />)
const labelElement = screen.getByText(longText)
expect(labelElement).toHaveClass('truncate')
})
it('should render with undefined className', () => {
render(<Label text="Label" className={undefined} />)
expect(screen.getByText('Label')).toBeInTheDocument()
})
it('should render with undefined isDeleted', () => {
render(<Label text="Label" isDeleted={undefined} />)
const labelElement = screen.getByText('Label')
expect(labelElement).not.toHaveClass('line-through')
})
it('should handle special characters in text', () => {
render(<Label text={'Label & "chars"'} />)
expect(screen.getByText('Label & "chars"')).toBeInTheDocument()
})
it('should handle numbers in text', () => {
render(<Label text="Label 123" />)
expect(screen.getByText('Label 123')).toBeInTheDocument()
})
})
})

View File

@ -0,0 +1,548 @@
import type { MetadataItemInBatchEdit, MetadataItemWithEdit } from '../types'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
import { DataType, UpdateType } from '../types'
import EditMetadataBatchModal from './modal'
// Mock service/API calls
const mockDoAddMetaData = vi.fn().mockResolvedValue({})
vi.mock('@/service/knowledge/use-metadata', () => ({
useCreateMetaData: () => ({
mutate: mockDoAddMetaData,
}),
useDatasetMetaData: () => ({
data: {
doc_metadata: [
{ id: 'existing-1', name: 'existing_field', type: DataType.string },
{ id: 'existing-2', name: 'another_field', type: DataType.number },
],
},
}),
}))
// Mock check name hook to control validation
let mockCheckNameResult = { errorMsg: '' }
vi.mock('../hooks/use-check-metadata-name', () => ({
default: () => ({
checkName: () => mockCheckNameResult,
}),
}))
// Mock Toast to verify notifications
const mockToastNotify = vi.fn()
vi.mock('@/app/components/base/toast', () => ({
default: {
notify: (args: unknown) => mockToastNotify(args),
},
}))
// Type definitions for mock props
type EditRowProps = {
payload: MetadataItemWithEdit
onChange: (item: MetadataItemWithEdit) => void
onRemove: (id: string) => void
onReset: (id: string) => void
}
type AddRowProps = {
payload: MetadataItemWithEdit
onChange: (item: MetadataItemWithEdit) => void
onRemove: () => void
}
type SelectModalProps = {
trigger: React.ReactNode
onSelect: (item: MetadataItemInBatchEdit) => void
onSave: (data: { name: string, type: DataType }) => Promise<void>
onManage: () => void
}
// Mock child components with callback exposure
vi.mock('./edit-row', () => ({
default: ({ payload, onChange, onRemove, onReset }: EditRowProps) => (
<div data-testid="edit-row" data-id={payload.id}>
<span data-testid="edit-row-name">{payload.name}</span>
<button data-testid={`change-${payload.id}`} onClick={() => onChange({ ...payload, value: 'changed', isUpdated: true, updateType: UpdateType.changeValue })}>Change</button>
<button data-testid={`remove-${payload.id}`} onClick={() => onRemove(payload.id)}>Remove</button>
<button data-testid={`reset-${payload.id}`} onClick={() => onReset(payload.id)}>Reset</button>
</div>
),
}))
vi.mock('./add-row', () => ({
default: ({ payload, onChange, onRemove }: AddRowProps) => (
<div data-testid="add-row" data-id={payload.id}>
<span data-testid="add-row-name">{payload.name}</span>
<button data-testid={`add-change-${payload.id}`} onClick={() => onChange({ ...payload, value: 'new_value' })}>Change</button>
<button data-testid="add-remove" onClick={onRemove}>Remove</button>
</div>
),
}))
vi.mock('../metadata-dataset/select-metadata-modal', () => ({
default: ({ trigger, onSelect, onSave, onManage }: SelectModalProps) => (
<div data-testid="select-modal">
{trigger}
<button data-testid="select-metadata" onClick={() => onSelect({ id: 'new-1', name: 'new_field', type: DataType.string, value: null, isMultipleValue: false })}>Select</button>
<button data-testid="save-metadata" onClick={() => onSave({ name: 'created_field', type: DataType.string }).catch(() => {})}>Save</button>
<button data-testid="manage-metadata" onClick={onManage}>Manage</button>
</div>
),
}))
describe('EditMetadataBatchModal', () => {
const mockList: MetadataItemInBatchEdit[] = [
{ id: '1', name: 'field_one', type: DataType.string, value: 'Value 1', isMultipleValue: false },
{ id: '2', name: 'field_two', type: DataType.number, value: 42, isMultipleValue: false },
]
const defaultProps = {
datasetId: 'ds-1',
documentNum: 5,
list: mockList,
onSave: vi.fn(),
onHide: vi.fn(),
onShowManage: vi.fn(),
}
beforeEach(() => {
vi.clearAllMocks()
mockCheckNameResult = { errorMsg: '' }
})
describe('Rendering', () => {
it('should render without crashing', async () => {
render(<EditMetadataBatchModal {...defaultProps} />)
await waitFor(() => {
expect(screen.getByRole('dialog')).toBeInTheDocument()
})
})
it('should render document count', async () => {
render(<EditMetadataBatchModal {...defaultProps} />)
await waitFor(() => {
expect(screen.getByText(/5/)).toBeInTheDocument()
})
})
it('should render all edit rows for existing items', async () => {
render(<EditMetadataBatchModal {...defaultProps} />)
await waitFor(() => {
const editRows = screen.getAllByTestId('edit-row')
expect(editRows).toHaveLength(2)
})
})
it('should render field names for existing items', async () => {
render(<EditMetadataBatchModal {...defaultProps} />)
await waitFor(() => {
expect(screen.getByText('field_one')).toBeInTheDocument()
expect(screen.getByText('field_two')).toBeInTheDocument()
})
})
it('should render checkbox for apply to all', async () => {
render(<EditMetadataBatchModal {...defaultProps} />)
await waitFor(() => {
const checkboxes = document.querySelectorAll('[data-testid*="checkbox"]')
expect(checkboxes.length).toBeGreaterThan(0)
})
})
it('should render select metadata modal', async () => {
render(<EditMetadataBatchModal {...defaultProps} />)
await waitFor(() => {
expect(screen.getByTestId('select-modal')).toBeInTheDocument()
})
})
})
describe('User Interactions', () => {
it('should call onHide when cancel button is clicked', async () => {
const onHide = vi.fn()
render(<EditMetadataBatchModal {...defaultProps} onHide={onHide} />)
await waitFor(() => {
expect(screen.getByRole('dialog')).toBeInTheDocument()
})
const cancelButton = screen.getByText(/cancel/i)
fireEvent.click(cancelButton)
expect(onHide).toHaveBeenCalled()
})
it('should call onSave when save button is clicked', async () => {
const onSave = vi.fn()
render(<EditMetadataBatchModal {...defaultProps} onSave={onSave} />)
await waitFor(() => {
expect(screen.getByRole('dialog')).toBeInTheDocument()
})
// Find the primary save button (not the one in SelectMetadataModal)
const saveButtons = screen.getAllByText(/save/i)
const modalSaveButton = saveButtons.find(btn => btn.closest('button')?.classList.contains('btn-primary'))
if (modalSaveButton)
fireEvent.click(modalSaveButton)
expect(onSave).toHaveBeenCalled()
})
it('should toggle apply to all checkbox', async () => {
render(<EditMetadataBatchModal {...defaultProps} />)
await waitFor(() => {
expect(screen.getByRole('dialog')).toBeInTheDocument()
})
const checkboxContainer = document.querySelector('[data-testid*="checkbox"]')
expect(checkboxContainer).toBeInTheDocument()
if (checkboxContainer) {
fireEvent.click(checkboxContainer)
await waitFor(() => {
const checkIcon = checkboxContainer.querySelector('svg')
expect(checkIcon).toBeInTheDocument()
})
}
})
it('should call onHide when modal close button is clicked', async () => {
const onHide = vi.fn()
render(<EditMetadataBatchModal {...defaultProps} onHide={onHide} />)
await waitFor(() => {
expect(screen.getByRole('dialog')).toBeInTheDocument()
})
})
})
describe('Edit Row Operations', () => {
it('should update item value when change is triggered', async () => {
render(<EditMetadataBatchModal {...defaultProps} />)
await waitFor(() => {
expect(screen.getByRole('dialog')).toBeInTheDocument()
})
fireEvent.click(screen.getByTestId('change-1'))
// The component should update internally
expect(screen.getAllByTestId('edit-row').length).toBe(2)
})
it('should mark item as deleted when remove is clicked', async () => {
render(<EditMetadataBatchModal {...defaultProps} />)
await waitFor(() => {
expect(screen.getByRole('dialog')).toBeInTheDocument()
})
fireEvent.click(screen.getByTestId('remove-1'))
// The component should update internally - item marked as deleted
expect(screen.getAllByTestId('edit-row').length).toBe(2)
})
it('should reset item when reset is clicked', async () => {
render(<EditMetadataBatchModal {...defaultProps} />)
await waitFor(() => {
expect(screen.getByRole('dialog')).toBeInTheDocument()
})
// First change the item
fireEvent.click(screen.getByTestId('change-1'))
// Then reset it
fireEvent.click(screen.getByTestId('reset-1'))
expect(screen.getAllByTestId('edit-row').length).toBe(2)
})
})
describe('Add Metadata Operations', () => {
it('should add new item when metadata is selected', async () => {
render(<EditMetadataBatchModal {...defaultProps} />)
await waitFor(() => {
expect(screen.getByRole('dialog')).toBeInTheDocument()
})
fireEvent.click(screen.getByTestId('select-metadata'))
// Should now have add-row for the new item
await waitFor(() => {
expect(screen.getByTestId('add-row')).toBeInTheDocument()
})
})
it('should remove added item when remove is clicked', async () => {
render(<EditMetadataBatchModal {...defaultProps} />)
await waitFor(() => {
expect(screen.getByRole('dialog')).toBeInTheDocument()
})
// First add an item
fireEvent.click(screen.getByTestId('select-metadata'))
await waitFor(() => {
expect(screen.getByTestId('add-row')).toBeInTheDocument()
})
// Then remove it
fireEvent.click(screen.getByTestId('add-remove'))
await waitFor(() => {
expect(screen.queryByTestId('add-row')).not.toBeInTheDocument()
})
})
it('should update added item when change is triggered', async () => {
render(<EditMetadataBatchModal {...defaultProps} />)
await waitFor(() => {
expect(screen.getByRole('dialog')).toBeInTheDocument()
})
// First add an item
fireEvent.click(screen.getByTestId('select-metadata'))
await waitFor(() => {
expect(screen.getByTestId('add-row')).toBeInTheDocument()
})
// Then change it
fireEvent.click(screen.getByTestId('add-change-new-1'))
expect(screen.getByTestId('add-row')).toBeInTheDocument()
})
it('should call doAddMetaData when saving new metadata with valid name', async () => {
mockCheckNameResult = { errorMsg: '' }
render(<EditMetadataBatchModal {...defaultProps} />)
await waitFor(() => {
expect(screen.getByRole('dialog')).toBeInTheDocument()
})
fireEvent.click(screen.getByTestId('save-metadata'))
await waitFor(() => {
expect(mockDoAddMetaData).toHaveBeenCalled()
})
})
it('should show success toast when saving with valid name', async () => {
mockCheckNameResult = { errorMsg: '' }
render(<EditMetadataBatchModal {...defaultProps} />)
await waitFor(() => {
expect(screen.getByRole('dialog')).toBeInTheDocument()
})
fireEvent.click(screen.getByTestId('save-metadata'))
await waitFor(() => {
expect(mockDoAddMetaData).toHaveBeenCalled()
})
await waitFor(() => {
expect(mockToastNotify).toHaveBeenCalledWith(
expect.objectContaining({
type: 'success',
}),
)
})
})
it('should show error toast when saving with invalid name', async () => {
mockCheckNameResult = { errorMsg: 'Name already exists' }
render(<EditMetadataBatchModal {...defaultProps} />)
await waitFor(() => {
expect(screen.getByRole('dialog')).toBeInTheDocument()
})
fireEvent.click(screen.getByTestId('save-metadata'))
await waitFor(() => {
expect(mockToastNotify).toHaveBeenCalledWith(
expect.objectContaining({
type: 'error',
message: 'Name already exists',
}),
)
})
})
it('should call onShowManage when manage is clicked', async () => {
const onShowManage = vi.fn()
render(<EditMetadataBatchModal {...defaultProps} onShowManage={onShowManage} />)
await waitFor(() => {
expect(screen.getByRole('dialog')).toBeInTheDocument()
})
fireEvent.click(screen.getByTestId('manage-metadata'))
expect(onShowManage).toHaveBeenCalled()
})
})
describe('Props', () => {
it('should pass correct datasetId', async () => {
render(<EditMetadataBatchModal {...defaultProps} datasetId="custom-ds" />)
await waitFor(() => {
expect(screen.getByRole('dialog')).toBeInTheDocument()
})
})
it('should display correct document number', async () => {
render(<EditMetadataBatchModal {...defaultProps} documentNum={10} />)
await waitFor(() => {
expect(screen.getByText(/10/)).toBeInTheDocument()
})
})
it('should handle empty list', async () => {
render(<EditMetadataBatchModal {...defaultProps} list={[]} />)
await waitFor(() => {
expect(screen.queryByTestId('edit-row')).not.toBeInTheDocument()
})
})
})
describe('Edge Cases', () => {
it('should handle list with multiple value items', async () => {
const multipleValueList: MetadataItemInBatchEdit[] = [
{ id: '1', name: 'field', type: DataType.string, value: null, isMultipleValue: true },
]
render(<EditMetadataBatchModal {...defaultProps} list={multipleValueList} />)
await waitFor(() => {
expect(screen.getByTestId('edit-row')).toBeInTheDocument()
})
})
it('should handle rapid save clicks', async () => {
const onSave = vi.fn()
render(<EditMetadataBatchModal {...defaultProps} onSave={onSave} />)
await waitFor(() => {
expect(screen.getByRole('dialog')).toBeInTheDocument()
})
// Find the primary save button
const saveButtons = screen.getAllByText(/save/i)
const saveBtn = saveButtons.find(btn => btn.closest('button')?.classList.contains('btn-primary'))
if (saveBtn) {
fireEvent.click(saveBtn)
fireEvent.click(saveBtn)
fireEvent.click(saveBtn)
}
expect(onSave).toHaveBeenCalledTimes(3)
})
it('should pass correct arguments to onSave', async () => {
const onSave = vi.fn()
render(<EditMetadataBatchModal {...defaultProps} onSave={onSave} />)
await waitFor(() => {
expect(screen.getByRole('dialog')).toBeInTheDocument()
})
const saveButtons = screen.getAllByText(/save/i)
const saveBtn = saveButtons.find(btn => btn.closest('button')?.classList.contains('btn-primary'))
if (saveBtn)
fireEvent.click(saveBtn)
expect(onSave).toHaveBeenCalledWith(
expect.any(Array),
expect.any(Array),
expect.any(Boolean),
)
})
it('should pass isApplyToAllSelectDocument as true when checked', async () => {
const onSave = vi.fn()
render(<EditMetadataBatchModal {...defaultProps} onSave={onSave} />)
await waitFor(() => {
expect(screen.getByRole('dialog')).toBeInTheDocument()
})
const checkboxContainer = document.querySelector('[data-testid*="checkbox"]')
if (checkboxContainer)
fireEvent.click(checkboxContainer)
const saveButtons = screen.getAllByText(/save/i)
const saveBtn = saveButtons.find(btn => btn.closest('button')?.classList.contains('btn-primary'))
if (saveBtn)
fireEvent.click(saveBtn)
await waitFor(() => {
expect(onSave).toHaveBeenCalledWith(
expect.any(Array),
expect.any(Array),
true,
)
})
})
it('should filter out deleted items when saving', async () => {
const onSave = vi.fn()
render(<EditMetadataBatchModal {...defaultProps} onSave={onSave} />)
await waitFor(() => {
expect(screen.getByRole('dialog')).toBeInTheDocument()
})
// Remove an item
fireEvent.click(screen.getByTestId('remove-1'))
// Save
const saveButtons = screen.getAllByText(/save/i)
const saveBtn = saveButtons.find(btn => btn.closest('button')?.classList.contains('btn-primary'))
if (saveBtn)
fireEvent.click(saveBtn)
expect(onSave).toHaveBeenCalled()
// The first argument should not contain the deleted item (id '1')
const savedList = onSave.mock.calls[0][0] as MetadataItemInBatchEdit[]
const hasDeletedItem = savedList.some(item => item.id === '1')
expect(hasDeletedItem).toBe(false)
})
it('should handle multiple add and remove operations', async () => {
render(<EditMetadataBatchModal {...defaultProps} />)
await waitFor(() => {
expect(screen.getByRole('dialog')).toBeInTheDocument()
})
// Add first item
fireEvent.click(screen.getByTestId('select-metadata'))
await waitFor(() => {
expect(screen.getByTestId('add-row')).toBeInTheDocument()
})
// Remove it
fireEvent.click(screen.getByTestId('add-remove'))
await waitFor(() => {
expect(screen.queryByTestId('add-row')).not.toBeInTheDocument()
})
// Add again
fireEvent.click(screen.getByTestId('select-metadata'))
await waitFor(() => {
expect(screen.getByTestId('add-row')).toBeInTheDocument()
})
})
})
})