mirror of
https://github.com/langgenius/dify.git
synced 2026-05-03 17:08:03 +08:00
test: add tests for dataset list (#31231)
Co-authored-by: CodingOnStar <hanxujiang@dify.ai> Co-authored-by: yyh <92089059+lyzno1@users.noreply.github.com>
This commit is contained in:
@ -0,0 +1,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)
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user