feat: add automated tests for pipeline setting (#29478)
Co-authored-by: CodingOnStar <hanxujiang@dify.ai>
This commit is contained in:
968
web/app/components/datasets/documents/status-item/index.spec.tsx
Normal file
968
web/app/components/datasets/documents/status-item/index.spec.tsx
Normal file
@@ -0,0 +1,968 @@
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import StatusItem from './index'
|
||||
import type { DocumentDisplayStatus } from '@/models/datasets'
|
||||
|
||||
// Mock i18n - required for translation
|
||||
jest.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock ToastContext - required to verify notifications
|
||||
const mockNotify = jest.fn()
|
||||
jest.mock('use-context-selector', () => ({
|
||||
...jest.requireActual('use-context-selector'),
|
||||
useContext: () => ({ notify: mockNotify }),
|
||||
}))
|
||||
|
||||
// Mock document service hooks - required to avoid real API calls
|
||||
const mockEnableDocument = jest.fn()
|
||||
const mockDisableDocument = jest.fn()
|
||||
const mockDeleteDocument = jest.fn()
|
||||
|
||||
jest.mock('@/service/knowledge/use-document', () => ({
|
||||
useDocumentEnable: () => ({ mutateAsync: mockEnableDocument }),
|
||||
useDocumentDisable: () => ({ mutateAsync: mockDisableDocument }),
|
||||
useDocumentDelete: () => ({ mutateAsync: mockDeleteDocument }),
|
||||
}))
|
||||
|
||||
// Mock useDebounceFn to execute immediately for testing
|
||||
jest.mock('ahooks', () => ({
|
||||
...jest.requireActual('ahooks'),
|
||||
useDebounceFn: (fn: (...args: unknown[]) => void) => ({ run: fn }),
|
||||
}))
|
||||
|
||||
// Test utilities
|
||||
const createQueryClient = () =>
|
||||
new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false },
|
||||
mutations: { retry: false },
|
||||
},
|
||||
})
|
||||
|
||||
const renderWithProviders = (ui: React.ReactElement) => {
|
||||
const queryClient = createQueryClient()
|
||||
return render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{ui}
|
||||
</QueryClientProvider>,
|
||||
)
|
||||
}
|
||||
|
||||
// Factory functions for test data
|
||||
const createDetailProps = (overrides: Partial<{
|
||||
enabled: boolean
|
||||
archived: boolean
|
||||
id: string
|
||||
}> = {}) => ({
|
||||
enabled: false,
|
||||
archived: false,
|
||||
id: 'doc-123',
|
||||
...overrides,
|
||||
})
|
||||
|
||||
describe('StatusItem', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
mockEnableDocument.mockResolvedValue({ result: 'success' })
|
||||
mockDisableDocument.mockResolvedValue({ result: 'success' })
|
||||
mockDeleteDocument.mockResolvedValue({ result: 'success' })
|
||||
})
|
||||
|
||||
// ==================== Rendering Tests ====================
|
||||
// Test basic rendering with different status values
|
||||
describe('Rendering', () => {
|
||||
it('should render without crashing', () => {
|
||||
// Arrange & Act
|
||||
renderWithProviders(<StatusItem status="available" />)
|
||||
|
||||
// Assert - check indicator element exists (real Indicator component)
|
||||
const indicator = screen.getByTestId('status-indicator')
|
||||
expect(indicator).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it.each([
|
||||
['queuing', 'bg-components-badge-status-light-warning-bg'],
|
||||
['indexing', 'bg-components-badge-status-light-normal-bg'],
|
||||
['paused', 'bg-components-badge-status-light-warning-bg'],
|
||||
['error', 'bg-components-badge-status-light-error-bg'],
|
||||
['available', 'bg-components-badge-status-light-success-bg'],
|
||||
['enabled', 'bg-components-badge-status-light-success-bg'],
|
||||
['disabled', 'bg-components-badge-status-light-disabled-bg'],
|
||||
['archived', 'bg-components-badge-status-light-disabled-bg'],
|
||||
] as const)('should render status "%s" with correct indicator background', (status, expectedBg) => {
|
||||
// Arrange & Act
|
||||
renderWithProviders(<StatusItem status={status} />)
|
||||
|
||||
// Assert
|
||||
const indicator = screen.getByTestId('status-indicator')
|
||||
expect(indicator).toHaveClass(expectedBg)
|
||||
})
|
||||
|
||||
it('should render status text from translation', () => {
|
||||
// Arrange & Act
|
||||
renderWithProviders(<StatusItem status="available" />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('datasetDocuments.list.status.available')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle case-insensitive status', () => {
|
||||
// Arrange & Act
|
||||
renderWithProviders(
|
||||
<StatusItem status={'AVAILABLE' as DocumentDisplayStatus} />,
|
||||
)
|
||||
|
||||
// Assert
|
||||
const indicator = screen.getByTestId('status-indicator')
|
||||
expect(indicator).toHaveClass('bg-components-badge-status-light-success-bg')
|
||||
})
|
||||
})
|
||||
|
||||
// ==================== Props Testing ====================
|
||||
// Test all prop variations and combinations
|
||||
describe('Props', () => {
|
||||
// reverse prop tests
|
||||
describe('reverse prop', () => {
|
||||
it('should apply default layout when reverse is false', () => {
|
||||
// Arrange & Act
|
||||
const { container } = renderWithProviders(<StatusItem status="available" reverse={false} />)
|
||||
|
||||
// Assert
|
||||
const wrapper = container.firstChild as HTMLElement
|
||||
expect(wrapper).not.toHaveClass('flex-row-reverse')
|
||||
})
|
||||
|
||||
it('should apply reversed layout when reverse is true', () => {
|
||||
// Arrange & Act
|
||||
const { container } = renderWithProviders(<StatusItem status="available" reverse />)
|
||||
|
||||
// Assert
|
||||
const wrapper = container.firstChild as HTMLElement
|
||||
expect(wrapper).toHaveClass('flex-row-reverse')
|
||||
})
|
||||
|
||||
it('should apply ml-2 to indicator when reversed', () => {
|
||||
// Arrange & Act
|
||||
renderWithProviders(<StatusItem status="available" reverse />)
|
||||
|
||||
// Assert
|
||||
const indicator = screen.getByTestId('status-indicator')
|
||||
expect(indicator).toHaveClass('ml-2')
|
||||
})
|
||||
|
||||
it('should apply mr-2 to indicator when not reversed', () => {
|
||||
// Arrange & Act
|
||||
renderWithProviders(<StatusItem status="available" reverse={false} />)
|
||||
|
||||
// Assert
|
||||
const indicator = screen.getByTestId('status-indicator')
|
||||
expect(indicator).toHaveClass('mr-2')
|
||||
})
|
||||
})
|
||||
|
||||
// scene prop tests
|
||||
describe('scene prop', () => {
|
||||
it('should not render switch in list scene', () => {
|
||||
// Arrange & Act
|
||||
renderWithProviders(
|
||||
<StatusItem
|
||||
status="available"
|
||||
scene="list"
|
||||
detail={createDetailProps()}
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert - Switch renders as a button element
|
||||
expect(screen.queryByRole('switch')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render switch in detail scene', () => {
|
||||
// Arrange & Act
|
||||
renderWithProviders(
|
||||
<StatusItem
|
||||
status="available"
|
||||
scene="detail"
|
||||
detail={createDetailProps()}
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByRole('switch')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should default to list scene', () => {
|
||||
// Arrange & Act
|
||||
renderWithProviders(
|
||||
<StatusItem
|
||||
status="available"
|
||||
detail={createDetailProps()}
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert
|
||||
expect(screen.queryByRole('switch')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// textCls prop tests
|
||||
describe('textCls prop', () => {
|
||||
it('should apply custom text class', () => {
|
||||
// Arrange & Act
|
||||
renderWithProviders(
|
||||
<StatusItem status="available" textCls="custom-text-class" />,
|
||||
)
|
||||
|
||||
// Assert
|
||||
const statusText = screen.getByText('datasetDocuments.list.status.available')
|
||||
expect(statusText).toHaveClass('custom-text-class')
|
||||
})
|
||||
|
||||
it('should default to empty string', () => {
|
||||
// Arrange & Act
|
||||
renderWithProviders(<StatusItem status="available" />)
|
||||
|
||||
// Assert
|
||||
const statusText = screen.getByText('datasetDocuments.list.status.available')
|
||||
expect(statusText).toHaveClass('text-sm')
|
||||
})
|
||||
})
|
||||
|
||||
// errorMessage prop tests
|
||||
describe('errorMessage prop', () => {
|
||||
it('should render tooltip trigger when errorMessage is provided', () => {
|
||||
// Arrange & Act
|
||||
renderWithProviders(
|
||||
<StatusItem status="error" errorMessage="Something went wrong" />,
|
||||
)
|
||||
|
||||
// Assert - tooltip trigger element should exist
|
||||
const tooltipTrigger = screen.getByTestId('error-tooltip-trigger')
|
||||
expect(tooltipTrigger).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show error message on hover', async () => {
|
||||
// Arrange
|
||||
renderWithProviders(
|
||||
<StatusItem status="error" errorMessage="Something went wrong" />,
|
||||
)
|
||||
|
||||
// Act - hover the tooltip trigger
|
||||
const tooltipTrigger = screen.getByTestId('error-tooltip-trigger')
|
||||
fireEvent.mouseEnter(tooltipTrigger)
|
||||
|
||||
// Assert - wait for tooltip content to appear
|
||||
expect(await screen.findByText('Something went wrong')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not render tooltip trigger when errorMessage is not provided', () => {
|
||||
// Arrange & Act
|
||||
renderWithProviders(<StatusItem status="error" />)
|
||||
|
||||
// Assert - tooltip trigger should not exist
|
||||
const tooltipTrigger = screen.queryByTestId('error-tooltip-trigger')
|
||||
expect(tooltipTrigger).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not render tooltip trigger when errorMessage is empty', () => {
|
||||
// Arrange & Act
|
||||
renderWithProviders(<StatusItem status="error" errorMessage="" />)
|
||||
|
||||
// Assert - tooltip trigger should not exist
|
||||
const tooltipTrigger = screen.queryByTestId('error-tooltip-trigger')
|
||||
expect(tooltipTrigger).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// detail prop tests
|
||||
describe('detail prop', () => {
|
||||
it('should use default values when detail is undefined', () => {
|
||||
// Arrange & Act
|
||||
renderWithProviders(
|
||||
<StatusItem status="available" scene="detail" />,
|
||||
)
|
||||
|
||||
// Assert - switch should be unchecked (defaultValue = false when archived = false and enabled = false)
|
||||
const switchEl = screen.getByRole('switch')
|
||||
expect(switchEl).toHaveAttribute('aria-checked', 'false')
|
||||
})
|
||||
|
||||
it('should use enabled value from detail', () => {
|
||||
// Arrange & Act
|
||||
renderWithProviders(
|
||||
<StatusItem
|
||||
status="available"
|
||||
scene="detail"
|
||||
detail={createDetailProps({ enabled: true })}
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert
|
||||
const switchEl = screen.getByRole('switch')
|
||||
expect(switchEl).toHaveAttribute('aria-checked', 'true')
|
||||
})
|
||||
|
||||
it('should set switch to false when archived regardless of enabled', () => {
|
||||
// Arrange & Act
|
||||
renderWithProviders(
|
||||
<StatusItem
|
||||
status="available"
|
||||
scene="detail"
|
||||
detail={createDetailProps({ enabled: true, archived: true })}
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert - archived overrides enabled, defaultValue becomes false
|
||||
const switchEl = screen.getByRole('switch')
|
||||
expect(switchEl).toHaveAttribute('aria-checked', 'false')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ==================== Memoization Tests ====================
|
||||
// Test useMemo logic for embedding status (disables switch)
|
||||
describe('Memoization', () => {
|
||||
it.each([
|
||||
['queuing', true],
|
||||
['indexing', true],
|
||||
['paused', true],
|
||||
['available', false],
|
||||
['enabled', false],
|
||||
['disabled', false],
|
||||
['archived', false],
|
||||
['error', false],
|
||||
] as const)('should correctly identify embedding status for "%s" - disabled: %s', (status, isEmbedding) => {
|
||||
// Arrange & Act
|
||||
renderWithProviders(
|
||||
<StatusItem
|
||||
status={status}
|
||||
scene="detail"
|
||||
detail={createDetailProps()}
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert - check if switch is visually disabled (via CSS classes)
|
||||
// The Switch component uses CSS classes for disabled state, not the native disabled attribute
|
||||
const switchEl = screen.getByRole('switch')
|
||||
if (isEmbedding)
|
||||
expect(switchEl).toHaveClass('!cursor-not-allowed', '!opacity-50')
|
||||
else
|
||||
expect(switchEl).not.toHaveClass('!cursor-not-allowed')
|
||||
})
|
||||
|
||||
it('should disable switch when archived', () => {
|
||||
// Arrange & Act
|
||||
renderWithProviders(
|
||||
<StatusItem
|
||||
status="available"
|
||||
scene="detail"
|
||||
detail={createDetailProps({ archived: true })}
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert - visually disabled via CSS classes
|
||||
const switchEl = screen.getByRole('switch')
|
||||
expect(switchEl).toHaveClass('!cursor-not-allowed', '!opacity-50')
|
||||
})
|
||||
|
||||
it('should disable switch when both embedding and archived', () => {
|
||||
// Arrange & Act
|
||||
renderWithProviders(
|
||||
<StatusItem
|
||||
status="indexing"
|
||||
scene="detail"
|
||||
detail={createDetailProps({ archived: true })}
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert - visually disabled via CSS classes
|
||||
const switchEl = screen.getByRole('switch')
|
||||
expect(switchEl).toHaveClass('!cursor-not-allowed', '!opacity-50')
|
||||
})
|
||||
})
|
||||
|
||||
// ==================== Switch Toggle Tests ====================
|
||||
// Test Switch toggle interactions
|
||||
describe('Switch Toggle', () => {
|
||||
it('should call enable operation when switch is toggled on', async () => {
|
||||
// Arrange
|
||||
const mockOnUpdate = jest.fn()
|
||||
renderWithProviders(
|
||||
<StatusItem
|
||||
status="disabled"
|
||||
scene="detail"
|
||||
detail={createDetailProps({ enabled: false })}
|
||||
datasetId="dataset-123"
|
||||
onUpdate={mockOnUpdate}
|
||||
/>,
|
||||
)
|
||||
|
||||
// Act
|
||||
const switchEl = screen.getByRole('switch')
|
||||
fireEvent.click(switchEl)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(mockEnableDocument).toHaveBeenCalledWith({
|
||||
datasetId: 'dataset-123',
|
||||
documentId: 'doc-123',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('should call disable operation when switch is toggled off', async () => {
|
||||
// Arrange
|
||||
const mockOnUpdate = jest.fn()
|
||||
renderWithProviders(
|
||||
<StatusItem
|
||||
status="enabled"
|
||||
scene="detail"
|
||||
detail={createDetailProps({ enabled: true })}
|
||||
datasetId="dataset-123"
|
||||
onUpdate={mockOnUpdate}
|
||||
/>,
|
||||
)
|
||||
|
||||
// Act
|
||||
const switchEl = screen.getByRole('switch')
|
||||
fireEvent.click(switchEl)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(mockDisableDocument).toHaveBeenCalledWith({
|
||||
datasetId: 'dataset-123',
|
||||
documentId: 'doc-123',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('should not call any operation when archived', () => {
|
||||
// Arrange
|
||||
renderWithProviders(
|
||||
<StatusItem
|
||||
status="available"
|
||||
scene="detail"
|
||||
detail={createDetailProps({ archived: true })}
|
||||
datasetId="dataset-123"
|
||||
/>,
|
||||
)
|
||||
|
||||
// Act
|
||||
const switchEl = screen.getByRole('switch')
|
||||
fireEvent.click(switchEl)
|
||||
|
||||
// Assert
|
||||
expect(mockEnableDocument).not.toHaveBeenCalled()
|
||||
expect(mockDisableDocument).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should render switch as checked when enabled is true', () => {
|
||||
// Arrange & Act
|
||||
renderWithProviders(
|
||||
<StatusItem
|
||||
status="enabled"
|
||||
scene="detail"
|
||||
detail={createDetailProps({ enabled: true })}
|
||||
datasetId="dataset-123"
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert - verify switch shows checked state
|
||||
const switchEl = screen.getByRole('switch')
|
||||
expect(switchEl).toHaveAttribute('aria-checked', 'true')
|
||||
})
|
||||
|
||||
it('should render switch as unchecked when enabled is false', () => {
|
||||
// Arrange & Act
|
||||
renderWithProviders(
|
||||
<StatusItem
|
||||
status="disabled"
|
||||
scene="detail"
|
||||
detail={createDetailProps({ enabled: false })}
|
||||
datasetId="dataset-123"
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert - verify switch shows unchecked state
|
||||
const switchEl = screen.getByRole('switch')
|
||||
expect(switchEl).toHaveAttribute('aria-checked', 'false')
|
||||
})
|
||||
|
||||
it('should skip enable operation when props.enabled is true (guard branch)', () => {
|
||||
// Covers guard condition: if (operationName === 'enable' && enabled) return
|
||||
// Note: The guard checks props.enabled, NOT the Switch's internal UI state.
|
||||
// This prevents redundant API calls when the UI toggles back to a state
|
||||
// that already matches the server-side data (props haven't been updated yet).
|
||||
const mockOnUpdate = jest.fn()
|
||||
renderWithProviders(
|
||||
<StatusItem
|
||||
status="enabled"
|
||||
scene="detail"
|
||||
detail={createDetailProps({ enabled: true })}
|
||||
datasetId="dataset-123"
|
||||
onUpdate={mockOnUpdate}
|
||||
/>,
|
||||
)
|
||||
|
||||
const switchEl = screen.getByRole('switch')
|
||||
// First click: Switch UI toggles OFF, calls disable (props.enabled=true, so allowed)
|
||||
fireEvent.click(switchEl)
|
||||
// Second click: Switch UI toggles ON, tries to call enable
|
||||
// BUT props.enabled is still true (not updated), so guard skips the API call
|
||||
fireEvent.click(switchEl)
|
||||
|
||||
// Assert - disable was called once, enable was skipped because props.enabled=true
|
||||
expect(mockDisableDocument).toHaveBeenCalledTimes(1)
|
||||
expect(mockEnableDocument).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should skip disable operation when props.enabled is false (guard branch)', () => {
|
||||
// Covers guard condition: if (operationName === 'disable' && !enabled) return
|
||||
// Note: The guard checks props.enabled, NOT the Switch's internal UI state.
|
||||
// This prevents redundant API calls when the UI toggles back to a state
|
||||
// that already matches the server-side data (props haven't been updated yet).
|
||||
const mockOnUpdate = jest.fn()
|
||||
renderWithProviders(
|
||||
<StatusItem
|
||||
status="disabled"
|
||||
scene="detail"
|
||||
detail={createDetailProps({ enabled: false })}
|
||||
datasetId="dataset-123"
|
||||
onUpdate={mockOnUpdate}
|
||||
/>,
|
||||
)
|
||||
|
||||
const switchEl = screen.getByRole('switch')
|
||||
// First click: Switch UI toggles ON, calls enable (props.enabled=false, so allowed)
|
||||
fireEvent.click(switchEl)
|
||||
// Second click: Switch UI toggles OFF, tries to call disable
|
||||
// BUT props.enabled is still false (not updated), so guard skips the API call
|
||||
fireEvent.click(switchEl)
|
||||
|
||||
// Assert - enable was called once, disable was skipped because props.enabled=false
|
||||
expect(mockEnableDocument).toHaveBeenCalledTimes(1)
|
||||
expect(mockDisableDocument).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
// ==================== onUpdate Callback Tests ====================
|
||||
// Test onUpdate callback behavior
|
||||
describe('onUpdate Callback', () => {
|
||||
it('should call onUpdate with operation name on successful enable', async () => {
|
||||
// Arrange
|
||||
const mockOnUpdate = jest.fn()
|
||||
renderWithProviders(
|
||||
<StatusItem
|
||||
status="disabled"
|
||||
scene="detail"
|
||||
detail={createDetailProps({ enabled: false })}
|
||||
datasetId="dataset-123"
|
||||
onUpdate={mockOnUpdate}
|
||||
/>,
|
||||
)
|
||||
|
||||
// Act
|
||||
const switchEl = screen.getByRole('switch')
|
||||
fireEvent.click(switchEl)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(mockOnUpdate).toHaveBeenCalledWith('enable')
|
||||
})
|
||||
})
|
||||
|
||||
it('should call onUpdate with operation name on successful disable', async () => {
|
||||
// Arrange
|
||||
const mockOnUpdate = jest.fn()
|
||||
renderWithProviders(
|
||||
<StatusItem
|
||||
status="enabled"
|
||||
scene="detail"
|
||||
detail={createDetailProps({ enabled: true })}
|
||||
datasetId="dataset-123"
|
||||
onUpdate={mockOnUpdate}
|
||||
/>,
|
||||
)
|
||||
|
||||
// Act
|
||||
const switchEl = screen.getByRole('switch')
|
||||
fireEvent.click(switchEl)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(mockOnUpdate).toHaveBeenCalledWith('disable')
|
||||
})
|
||||
})
|
||||
|
||||
it('should not call onUpdate when operation fails', async () => {
|
||||
// Arrange
|
||||
mockEnableDocument.mockRejectedValue(new Error('API Error'))
|
||||
const mockOnUpdate = jest.fn()
|
||||
renderWithProviders(
|
||||
<StatusItem
|
||||
status="disabled"
|
||||
scene="detail"
|
||||
detail={createDetailProps({ enabled: false })}
|
||||
datasetId="dataset-123"
|
||||
onUpdate={mockOnUpdate}
|
||||
/>,
|
||||
)
|
||||
|
||||
// Act
|
||||
const switchEl = screen.getByRole('switch')
|
||||
fireEvent.click(switchEl)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(mockNotify).toHaveBeenCalledWith({
|
||||
type: 'error',
|
||||
message: 'common.actionMsg.modifiedUnsuccessfully',
|
||||
})
|
||||
})
|
||||
expect(mockOnUpdate).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not throw when onUpdate is not provided', () => {
|
||||
// Arrange
|
||||
renderWithProviders(
|
||||
<StatusItem
|
||||
status="disabled"
|
||||
scene="detail"
|
||||
detail={createDetailProps({ enabled: false })}
|
||||
datasetId="dataset-123"
|
||||
/>,
|
||||
)
|
||||
|
||||
// Act
|
||||
const switchEl = screen.getByRole('switch')
|
||||
|
||||
// Assert - should not throw
|
||||
expect(() => fireEvent.click(switchEl)).not.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
// ==================== API Calls ====================
|
||||
// Test API operations and toast notifications
|
||||
describe('API Operations', () => {
|
||||
it('should show success toast on successful operation', async () => {
|
||||
// Arrange
|
||||
renderWithProviders(
|
||||
<StatusItem
|
||||
status="disabled"
|
||||
scene="detail"
|
||||
detail={createDetailProps({ enabled: false })}
|
||||
datasetId="dataset-123"
|
||||
/>,
|
||||
)
|
||||
|
||||
// Act
|
||||
const switchEl = screen.getByRole('switch')
|
||||
fireEvent.click(switchEl)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(mockNotify).toHaveBeenCalledWith({
|
||||
type: 'success',
|
||||
message: 'common.actionMsg.modifiedSuccessfully',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('should show error toast on failed operation', async () => {
|
||||
// Arrange
|
||||
mockDisableDocument.mockRejectedValue(new Error('Network error'))
|
||||
renderWithProviders(
|
||||
<StatusItem
|
||||
status="enabled"
|
||||
scene="detail"
|
||||
detail={createDetailProps({ enabled: true })}
|
||||
datasetId="dataset-123"
|
||||
/>,
|
||||
)
|
||||
|
||||
// Act
|
||||
const switchEl = screen.getByRole('switch')
|
||||
fireEvent.click(switchEl)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(mockNotify).toHaveBeenCalledWith({
|
||||
type: 'error',
|
||||
message: 'common.actionMsg.modifiedUnsuccessfully',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('should pass correct parameters to enable API', async () => {
|
||||
// Arrange
|
||||
renderWithProviders(
|
||||
<StatusItem
|
||||
status="disabled"
|
||||
scene="detail"
|
||||
detail={createDetailProps({ enabled: false, id: 'test-doc-id' })}
|
||||
datasetId="test-dataset-id"
|
||||
/>,
|
||||
)
|
||||
|
||||
// Act
|
||||
const switchEl = screen.getByRole('switch')
|
||||
fireEvent.click(switchEl)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(mockEnableDocument).toHaveBeenCalledWith({
|
||||
datasetId: 'test-dataset-id',
|
||||
documentId: 'test-doc-id',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('should pass correct parameters to disable API', async () => {
|
||||
// Arrange
|
||||
renderWithProviders(
|
||||
<StatusItem
|
||||
status="enabled"
|
||||
scene="detail"
|
||||
detail={createDetailProps({ enabled: true, id: 'test-doc-456' })}
|
||||
datasetId="test-dataset-456"
|
||||
/>,
|
||||
)
|
||||
|
||||
// Act
|
||||
const switchEl = screen.getByRole('switch')
|
||||
fireEvent.click(switchEl)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(mockDisableDocument).toHaveBeenCalledWith({
|
||||
datasetId: 'test-dataset-456',
|
||||
documentId: 'test-doc-456',
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ==================== Edge Cases ====================
|
||||
// Test boundary conditions and unusual inputs
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle empty datasetId', () => {
|
||||
// Arrange & Act
|
||||
renderWithProviders(
|
||||
<StatusItem
|
||||
status="available"
|
||||
scene="detail"
|
||||
detail={createDetailProps()}
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert - should render without errors
|
||||
expect(screen.getByRole('switch')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle undefined detail gracefully', () => {
|
||||
// Arrange & Act
|
||||
renderWithProviders(
|
||||
<StatusItem
|
||||
status="available"
|
||||
scene="detail"
|
||||
detail={undefined}
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert
|
||||
const switchEl = screen.getByRole('switch')
|
||||
expect(switchEl).toHaveAttribute('aria-checked', 'false')
|
||||
})
|
||||
|
||||
it('should handle empty string id in detail', async () => {
|
||||
// Arrange
|
||||
renderWithProviders(
|
||||
<StatusItem
|
||||
status="disabled"
|
||||
scene="detail"
|
||||
detail={createDetailProps({ enabled: false, id: '' })}
|
||||
datasetId="dataset-123"
|
||||
/>,
|
||||
)
|
||||
|
||||
// Act
|
||||
const switchEl = screen.getByRole('switch')
|
||||
fireEvent.click(switchEl)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(mockEnableDocument).toHaveBeenCalledWith({
|
||||
datasetId: 'dataset-123',
|
||||
documentId: '',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle very long error messages', async () => {
|
||||
// Arrange
|
||||
const longErrorMessage = 'A'.repeat(500)
|
||||
renderWithProviders(
|
||||
<StatusItem status="error" errorMessage={longErrorMessage} />,
|
||||
)
|
||||
|
||||
// Act - hover to show tooltip
|
||||
const tooltipTrigger = screen.getByTestId('error-tooltip-trigger')
|
||||
fireEvent.mouseEnter(tooltipTrigger)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(longErrorMessage)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle special characters in error message', async () => {
|
||||
// Arrange
|
||||
const specialChars = '<script>alert("xss")</script> & < > " \''
|
||||
renderWithProviders(
|
||||
<StatusItem status="error" errorMessage={specialChars} />,
|
||||
)
|
||||
|
||||
// Act - hover to show tooltip
|
||||
const tooltipTrigger = screen.getByTestId('error-tooltip-trigger')
|
||||
fireEvent.mouseEnter(tooltipTrigger)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(specialChars)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle all status types in sequence', () => {
|
||||
// Arrange
|
||||
const statuses: DocumentDisplayStatus[] = [
|
||||
'queuing', 'indexing', 'paused', 'error',
|
||||
'available', 'enabled', 'disabled', 'archived',
|
||||
]
|
||||
|
||||
// Act & Assert
|
||||
statuses.forEach((status) => {
|
||||
const { unmount } = renderWithProviders(<StatusItem status={status} />)
|
||||
const indicator = screen.getByTestId('status-indicator')
|
||||
expect(indicator).toBeInTheDocument()
|
||||
unmount()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ==================== Component Memoization ====================
|
||||
// Test React.memo behavior
|
||||
describe('Component Memoization', () => {
|
||||
it('should be wrapped with React.memo', () => {
|
||||
// Assert
|
||||
expect(StatusItem).toHaveProperty('$$typeof', Symbol.for('react.memo'))
|
||||
})
|
||||
|
||||
it('should render correctly with same props', () => {
|
||||
// Arrange
|
||||
const props = {
|
||||
status: 'available' as const,
|
||||
scene: 'detail' as const,
|
||||
detail: createDetailProps(),
|
||||
}
|
||||
|
||||
// Act
|
||||
const { rerender } = renderWithProviders(<StatusItem {...props} />)
|
||||
rerender(
|
||||
<QueryClientProvider client={createQueryClient()}>
|
||||
<StatusItem {...props} />
|
||||
</QueryClientProvider>,
|
||||
)
|
||||
|
||||
// Assert
|
||||
const indicator = screen.getByTestId('status-indicator')
|
||||
expect(indicator).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should update when status prop changes', () => {
|
||||
// Arrange
|
||||
const { rerender } = renderWithProviders(<StatusItem status="available" />)
|
||||
|
||||
// Assert initial - green/success background
|
||||
let indicator = screen.getByTestId('status-indicator')
|
||||
expect(indicator).toHaveClass('bg-components-badge-status-light-success-bg')
|
||||
|
||||
// Act
|
||||
rerender(
|
||||
<QueryClientProvider client={createQueryClient()}>
|
||||
<StatusItem status="error" />
|
||||
</QueryClientProvider>,
|
||||
)
|
||||
|
||||
// Assert updated - red/error background
|
||||
indicator = screen.getByTestId('status-indicator')
|
||||
expect(indicator).toHaveClass('bg-components-badge-status-light-error-bg')
|
||||
})
|
||||
})
|
||||
|
||||
// ==================== Styling Tests ====================
|
||||
// Test CSS classes and styling
|
||||
describe('Styling', () => {
|
||||
it('should apply correct status text color for green status', () => {
|
||||
// Arrange & Act
|
||||
renderWithProviders(<StatusItem status="available" />)
|
||||
|
||||
// Assert
|
||||
const statusText = screen.getByText('datasetDocuments.list.status.available')
|
||||
expect(statusText).toHaveClass('text-util-colors-green-green-600')
|
||||
})
|
||||
|
||||
it('should apply correct status text color for red status', () => {
|
||||
// Arrange & Act
|
||||
renderWithProviders(<StatusItem status="error" />)
|
||||
|
||||
// Assert
|
||||
const statusText = screen.getByText('datasetDocuments.list.status.error')
|
||||
expect(statusText).toHaveClass('text-util-colors-red-red-600')
|
||||
})
|
||||
|
||||
it('should apply correct status text color for orange status', () => {
|
||||
// Arrange & Act
|
||||
renderWithProviders(<StatusItem status="queuing" />)
|
||||
|
||||
// Assert
|
||||
const statusText = screen.getByText('datasetDocuments.list.status.queuing')
|
||||
expect(statusText).toHaveClass('text-util-colors-warning-warning-600')
|
||||
})
|
||||
|
||||
it('should apply correct status text color for blue status', () => {
|
||||
// Arrange & Act
|
||||
renderWithProviders(<StatusItem status="indexing" />)
|
||||
|
||||
// Assert
|
||||
const statusText = screen.getByText('datasetDocuments.list.status.indexing')
|
||||
expect(statusText).toHaveClass('text-util-colors-blue-light-blue-light-600')
|
||||
})
|
||||
|
||||
it('should apply correct status text color for gray status', () => {
|
||||
// Arrange & Act
|
||||
renderWithProviders(<StatusItem status="disabled" />)
|
||||
|
||||
// Assert
|
||||
const statusText = screen.getByText('datasetDocuments.list.status.disabled')
|
||||
expect(statusText).toHaveClass('text-text-tertiary')
|
||||
})
|
||||
|
||||
it('should render switch with md size in detail scene', () => {
|
||||
// Arrange & Act
|
||||
renderWithProviders(
|
||||
<StatusItem
|
||||
status="available"
|
||||
scene="detail"
|
||||
detail={createDetailProps()}
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert - check switch has the md size class (h-4 w-7)
|
||||
const switchEl = screen.getByRole('switch')
|
||||
expect(switchEl).toHaveClass('h-4', 'w-7')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -105,6 +105,7 @@ const StatusItem = ({
|
||||
<div className='max-w-[260px] break-all'>{errorMessage}</div>
|
||||
}
|
||||
triggerClassName='ml-1 w-4 h-4'
|
||||
triggerTestId='error-tooltip-trigger'
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user