import { act, fireEvent, render, screen } from '@testing-library/react' import { renderHook } from '@testing-library/react' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import React from 'react' import DataSourceOptions from './index' import OptionCard from './option-card' import DatasourceIcon from './datasource-icon' import { useDatasourceIcon } from './hooks' import type { DataSourceNodeType } from '@/app/components/workflow/nodes/data-source/types' import { BlockEnum, type Node } from '@/app/components/workflow/types' import type { Datasource } from '@/app/components/rag-pipeline/components/panel/test-run/types' // ========================================== // Mock External Dependencies // ========================================== // Mock useDatasourceOptions hook from parent hooks const mockUseDatasourceOptions = jest.fn() jest.mock('../hooks', () => ({ useDatasourceOptions: (nodes: Node[]) => mockUseDatasourceOptions(nodes), })) // Mock useDataSourceList API hook const mockUseDataSourceList = jest.fn() jest.mock('@/service/use-pipeline', () => ({ useDataSourceList: (enabled: boolean) => mockUseDataSourceList(enabled), })) // Mock transformDataSourceToTool utility const mockTransformDataSourceToTool = jest.fn() jest.mock('@/app/components/workflow/block-selector/utils', () => ({ transformDataSourceToTool: (item: unknown) => mockTransformDataSourceToTool(item), })) // Mock basePath jest.mock('@/utils/var', () => ({ basePath: '/mock-base-path', })) // ========================================== // Test Data Builders // ========================================== const createMockDataSourceNodeData = (overrides?: Partial): DataSourceNodeType => ({ title: 'Test Data Source', desc: 'Test description', type: BlockEnum.DataSource, plugin_id: 'test-plugin-id', provider_type: 'local_file', provider_name: 'Test Provider', datasource_name: 'test-datasource', datasource_label: 'Test Datasource Label', datasource_parameters: {}, datasource_configurations: {}, ...overrides, }) const createMockPipelineNode = (overrides?: Partial>): Node => { const nodeData = createMockDataSourceNodeData(overrides?.data) return { id: `node-${Math.random().toString(36).slice(2, 9)}`, type: 'custom', position: { x: 0, y: 0 }, data: nodeData, ...overrides, } } const createMockPipelineNodes = (count = 3): Node[] => { return Array.from({ length: count }, (_, i) => createMockPipelineNode({ id: `node-${i + 1}`, data: createMockDataSourceNodeData({ title: `Data Source ${i + 1}`, plugin_id: `plugin-${i + 1}`, datasource_name: `datasource-${i + 1}`, }), }), ) } const createMockDatasourceOption = ( node: Node, ) => ({ label: node.data.title, value: node.id, data: node.data, }) const createMockDataSourceListItem = (overrides?: Record) => ({ declaration: { identity: { icon: '/icons/test-icon.png', name: 'test-datasource', label: { en_US: 'Test Datasource' }, }, provider: 'test-provider', }, plugin_id: 'test-plugin-id', ...overrides, }) // ========================================== // Test Utilities // ========================================== const createQueryClient = () => new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false }, }, }) const renderWithProviders = ( ui: React.ReactElement, queryClient?: QueryClient, ) => { const client = queryClient || createQueryClient() return render( {ui} , ) } const createHookWrapper = () => { const queryClient = createQueryClient() return ({ children }: { children: React.ReactNode }) => ( {children} ) } // ========================================== // DatasourceIcon Tests // ========================================== describe('DatasourceIcon', () => { beforeEach(() => { jest.clearAllMocks() }) describe('Rendering', () => { it('should render without crashing', () => { // Arrange & Act const { container } = render() // Assert expect(container.firstChild).toBeInTheDocument() }) it('should render icon with background image', () => { // Arrange const iconUrl = 'https://example.com/icon.png' // Act const { container } = render() // Assert const iconDiv = container.querySelector('[style*="background-image"]') expect(iconDiv).toHaveStyle({ backgroundImage: `url(${iconUrl})` }) }) it('should render with default size (sm)', () => { // Arrange & Act const { container } = render() // Assert - Default size is 'sm' which maps to 'w-5 h-5' expect(container.firstChild).toHaveClass('w-5') expect(container.firstChild).toHaveClass('h-5') }) }) describe('Props', () => { describe('size', () => { it('should render with xs size', () => { // Arrange & Act const { container } = render( , ) // Assert expect(container.firstChild).toHaveClass('w-4') expect(container.firstChild).toHaveClass('h-4') expect(container.firstChild).toHaveClass('rounded-[5px]') }) it('should render with sm size', () => { // Arrange & Act const { container } = render( , ) // Assert expect(container.firstChild).toHaveClass('w-5') expect(container.firstChild).toHaveClass('h-5') expect(container.firstChild).toHaveClass('rounded-md') }) it('should render with md size', () => { // Arrange & Act const { container } = render( , ) // Assert expect(container.firstChild).toHaveClass('w-6') expect(container.firstChild).toHaveClass('h-6') expect(container.firstChild).toHaveClass('rounded-lg') }) }) describe('className', () => { it('should apply custom className', () => { // Arrange & Act const { container } = render( , ) // Assert expect(container.firstChild).toHaveClass('custom-class') }) it('should merge custom className with default classes', () => { // Arrange & Act const { container } = render( , ) // Assert expect(container.firstChild).toHaveClass('custom-class') expect(container.firstChild).toHaveClass('w-5') expect(container.firstChild).toHaveClass('h-5') }) }) describe('iconUrl', () => { it('should handle empty iconUrl', () => { // Arrange & Act const { container } = render() // Assert const iconDiv = container.querySelector('[style*="background-image"]') expect(iconDiv).toHaveStyle({ backgroundImage: 'url()' }) }) it('should handle special characters in iconUrl', () => { // Arrange const iconUrl = 'https://example.com/icon.png?param=value&other=123' // Act const { container } = render() // Assert const iconDiv = container.querySelector('[style*="background-image"]') expect(iconDiv).toHaveStyle({ backgroundImage: `url(${iconUrl})` }) }) it('should handle data URL as iconUrl', () => { // Arrange const dataUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==' // Act const { container } = render() // Assert const iconDiv = container.querySelector('[style*="background-image"]') expect(iconDiv).toBeInTheDocument() }) }) }) describe('Styling', () => { it('should have flex container classes', () => { // Arrange & Act const { container } = render() // Assert expect(container.firstChild).toHaveClass('flex') expect(container.firstChild).toHaveClass('items-center') expect(container.firstChild).toHaveClass('justify-center') }) it('should have shadow-xs class from size map', () => { // Arrange & Act const { container } = render() // Assert - Default size 'sm' has shadow-xs expect(container.firstChild).toHaveClass('shadow-xs') }) it('should have inner div with bg-cover class', () => { // Arrange & Act const { container } = render() // Assert const innerDiv = container.querySelector('.bg-cover') expect(innerDiv).toBeInTheDocument() expect(innerDiv).toHaveClass('bg-center') expect(innerDiv).toHaveClass('rounded-md') }) }) }) // ========================================== // useDatasourceIcon Hook Tests // ========================================== describe('useDatasourceIcon', () => { beforeEach(() => { jest.clearAllMocks() mockUseDataSourceList.mockReturnValue({ data: [], isSuccess: false, }) mockTransformDataSourceToTool.mockImplementation(item => ({ plugin_id: item.plugin_id, icon: item.declaration?.identity?.icon, })) }) describe('Loading State', () => { it('should return undefined when data is not loaded', () => { // Arrange mockUseDataSourceList.mockReturnValue({ data: undefined, isSuccess: false, }) const nodeData = createMockDataSourceNodeData() // Act const { result } = renderHook(() => useDatasourceIcon(nodeData), { wrapper: createHookWrapper(), }) // Assert expect(result.current).toBeUndefined() }) it('should call useDataSourceList with true', () => { // Arrange const nodeData = createMockDataSourceNodeData() // Act renderHook(() => useDatasourceIcon(nodeData), { wrapper: createHookWrapper(), }) // Assert expect(mockUseDataSourceList).toHaveBeenCalledWith(true) }) }) describe('Success State', () => { it('should return icon when data is loaded and plugin matches', () => { // Arrange const mockDataSourceList = [ createMockDataSourceListItem({ plugin_id: 'test-plugin-id', declaration: { identity: { icon: '/icons/test-icon.png', name: 'test', label: { en_US: 'Test' }, }, }, }), ] mockUseDataSourceList.mockReturnValue({ data: mockDataSourceList, isSuccess: true, }) mockTransformDataSourceToTool.mockImplementation(item => ({ plugin_id: item.plugin_id, icon: item.declaration?.identity?.icon, })) const nodeData = createMockDataSourceNodeData({ plugin_id: 'test-plugin-id' }) // Act const { result } = renderHook(() => useDatasourceIcon(nodeData), { wrapper: createHookWrapper(), }) // Assert - Icon should have basePath prepended expect(result.current).toBe('/mock-base-path/icons/test-icon.png') }) it('should return undefined when plugin does not match', () => { // Arrange const mockDataSourceList = [ createMockDataSourceListItem({ plugin_id: 'other-plugin-id', }), ] mockUseDataSourceList.mockReturnValue({ data: mockDataSourceList, isSuccess: true, }) const nodeData = createMockDataSourceNodeData({ plugin_id: 'test-plugin-id' }) // Act const { result } = renderHook(() => useDatasourceIcon(nodeData), { wrapper: createHookWrapper(), }) // Assert expect(result.current).toBeUndefined() }) it('should prepend basePath to icon when icon does not include basePath', () => { // Arrange const mockDataSourceList = [ createMockDataSourceListItem({ plugin_id: 'test-plugin-id', declaration: { identity: { icon: '/icons/test-icon.png', name: 'test', label: { en_US: 'Test' }, }, }, }), ] mockUseDataSourceList.mockReturnValue({ data: mockDataSourceList, isSuccess: true, }) mockTransformDataSourceToTool.mockImplementation(item => ({ plugin_id: item.plugin_id, icon: item.declaration?.identity?.icon, })) const nodeData = createMockDataSourceNodeData({ plugin_id: 'test-plugin-id' }) // Act const { result } = renderHook(() => useDatasourceIcon(nodeData), { wrapper: createHookWrapper(), }) // Assert - Icon should have basePath prepended expect(result.current).toBe('/mock-base-path/icons/test-icon.png') }) it('should not prepend basePath when icon already includes basePath', () => { // Arrange const mockDataSourceList = [ createMockDataSourceListItem({ plugin_id: 'test-plugin-id', declaration: { identity: { icon: '/mock-base-path/icons/test-icon.png', name: 'test', label: { en_US: 'Test' }, }, }, }), ] mockUseDataSourceList.mockReturnValue({ data: mockDataSourceList, isSuccess: true, }) mockTransformDataSourceToTool.mockImplementation(item => ({ plugin_id: item.plugin_id, icon: item.declaration?.identity?.icon, })) const nodeData = createMockDataSourceNodeData({ plugin_id: 'test-plugin-id' }) // Act const { result } = renderHook(() => useDatasourceIcon(nodeData), { wrapper: createHookWrapper(), }) // Assert - Icon should not be modified expect(result.current).toBe('/mock-base-path/icons/test-icon.png') }) }) describe('Edge Cases', () => { it('should handle empty dataSourceList', () => { // Arrange mockUseDataSourceList.mockReturnValue({ data: [], isSuccess: true, }) const nodeData = createMockDataSourceNodeData() // Act const { result } = renderHook(() => useDatasourceIcon(nodeData), { wrapper: createHookWrapper(), }) // Assert expect(result.current).toBeUndefined() }) it('should handle null dataSourceList', () => { // Arrange mockUseDataSourceList.mockReturnValue({ data: null, isSuccess: true, }) const nodeData = createMockDataSourceNodeData() // Act const { result } = renderHook(() => useDatasourceIcon(nodeData), { wrapper: createHookWrapper(), }) // Assert expect(result.current).toBeUndefined() }) it('should handle icon as non-string type', () => { // Arrange const mockDataSourceList = [ createMockDataSourceListItem({ plugin_id: 'test-plugin-id', declaration: { identity: { icon: { url: '/icons/test-icon.png' }, // Object instead of string name: 'test', label: { en_US: 'Test' }, }, }, }), ] mockUseDataSourceList.mockReturnValue({ data: mockDataSourceList, isSuccess: true, }) mockTransformDataSourceToTool.mockImplementation(item => ({ plugin_id: item.plugin_id, icon: item.declaration?.identity?.icon, })) const nodeData = createMockDataSourceNodeData({ plugin_id: 'test-plugin-id' }) // Act const { result } = renderHook(() => useDatasourceIcon(nodeData), { wrapper: createHookWrapper(), }) // Assert - Should return the icon object as-is since it's not a string expect(result.current).toEqual({ url: '/icons/test-icon.png' }) }) it('should memoize result based on plugin_id', () => { // Arrange const mockDataSourceList = [ createMockDataSourceListItem({ plugin_id: 'test-plugin-id', }), ] mockUseDataSourceList.mockReturnValue({ data: mockDataSourceList, isSuccess: true, }) const nodeData = createMockDataSourceNodeData({ plugin_id: 'test-plugin-id' }) // Act const { result, rerender } = renderHook(() => useDatasourceIcon(nodeData), { wrapper: createHookWrapper(), }) const firstResult = result.current // Rerender with same props rerender() // Assert - Should return the same memoized result expect(result.current).toBe(firstResult) }) }) }) // ========================================== // OptionCard Tests // ========================================== describe('OptionCard', () => { const defaultProps = { label: 'Test Option', selected: false, nodeData: createMockDataSourceNodeData(), } beforeEach(() => { jest.clearAllMocks() // Setup default mock for useDatasourceIcon mockUseDataSourceList.mockReturnValue({ data: [], isSuccess: true, }) }) describe('Rendering', () => { it('should render without crashing', () => { // Arrange & Act renderWithProviders() // Assert expect(screen.getByText('Test Option')).toBeInTheDocument() }) it('should render label text', () => { // Arrange & Act renderWithProviders() // Assert expect(screen.getByText('Custom Label')).toBeInTheDocument() }) it('should render DatasourceIcon component', () => { // Arrange & Act const { container } = renderWithProviders() // Assert - DatasourceIcon container should exist const iconContainer = container.querySelector('.size-8') expect(iconContainer).toBeInTheDocument() }) it('should set title attribute for label truncation', () => { // Arrange const longLabel = 'This is a very long label that might be truncated' // Act renderWithProviders() // Assert const labelElement = screen.getByText(longLabel) expect(labelElement).toHaveAttribute('title', longLabel) }) }) describe('Props', () => { describe('selected', () => { it('should apply selected styles when selected is true', () => { // Arrange & Act const { container } = renderWithProviders( , ) // Assert const card = container.firstChild expect(card).toHaveClass('border-components-option-card-option-selected-border') expect(card).toHaveClass('bg-components-option-card-option-selected-bg') }) it('should apply unselected styles when selected is false', () => { // Arrange & Act const { container } = renderWithProviders( , ) // Assert const card = container.firstChild expect(card).toHaveClass('border-components-option-card-option-border') expect(card).toHaveClass('bg-components-option-card-option-bg') }) it('should apply text-text-primary to label when selected', () => { // Arrange & Act renderWithProviders() // Assert const label = screen.getByText('Test Option') expect(label).toHaveClass('text-text-primary') }) it('should apply text-text-secondary to label when not selected', () => { // Arrange & Act renderWithProviders() // Assert const label = screen.getByText('Test Option') expect(label).toHaveClass('text-text-secondary') }) }) describe('onClick', () => { it('should call onClick when card is clicked', () => { // Arrange const mockOnClick = jest.fn() renderWithProviders( , ) // Act - Click on the label text's parent card const labelElement = screen.getByText('Test Option') const card = labelElement.closest('[class*="cursor-pointer"]') expect(card).toBeInTheDocument() fireEvent.click(card!) // Assert expect(mockOnClick).toHaveBeenCalledTimes(1) }) it('should not crash when onClick is not provided', () => { // Arrange & Act renderWithProviders( , ) // Act - Click on the label text's parent card should not throw const labelElement = screen.getByText('Test Option') const card = labelElement.closest('[class*="cursor-pointer"]') expect(card).toBeInTheDocument() fireEvent.click(card!) // Assert - Component should still be rendered expect(screen.getByText('Test Option')).toBeInTheDocument() }) }) describe('nodeData', () => { it('should pass nodeData to useDatasourceIcon hook', () => { // Arrange const customNodeData = createMockDataSourceNodeData({ plugin_id: 'custom-plugin' }) // Act renderWithProviders() // Assert - Hook should be called (via useDataSourceList mock) expect(mockUseDataSourceList).toHaveBeenCalled() }) }) }) describe('Styling', () => { it('should have cursor-pointer class', () => { // Arrange & Act const { container } = renderWithProviders() // Assert expect(container.firstChild).toHaveClass('cursor-pointer') }) it('should have flex layout classes', () => { // Arrange & Act const { container } = renderWithProviders() // Assert expect(container.firstChild).toHaveClass('flex') expect(container.firstChild).toHaveClass('items-center') expect(container.firstChild).toHaveClass('gap-2') }) it('should have rounded-xl border', () => { // Arrange & Act const { container } = renderWithProviders() // Assert expect(container.firstChild).toHaveClass('rounded-xl') expect(container.firstChild).toHaveClass('border') }) it('should have padding p-3', () => { // Arrange & Act const { container } = renderWithProviders() // Assert expect(container.firstChild).toHaveClass('p-3') }) it('should have line-clamp-2 for label truncation', () => { // Arrange & Act renderWithProviders() // Assert const label = screen.getByText('Test Option') expect(label).toHaveClass('line-clamp-2') }) }) describe('Memoization', () => { it('should be wrapped with React.memo', () => { // Assert - OptionCard should be a memoized component expect(OptionCard).toBeDefined() // React.memo wraps the component, so we check it renders correctly const { container } = renderWithProviders() expect(container.firstChild).toBeInTheDocument() }) }) }) // ========================================== // DataSourceOptions Tests // ========================================== describe('DataSourceOptions', () => { const defaultNodes = createMockPipelineNodes(3) const defaultOptions = defaultNodes.map(createMockDatasourceOption) const defaultProps = { pipelineNodes: defaultNodes, datasourceNodeId: '', onSelect: jest.fn(), } beforeEach(() => { jest.clearAllMocks() mockUseDatasourceOptions.mockReturnValue(defaultOptions) mockUseDataSourceList.mockReturnValue({ data: [], isSuccess: true, }) }) // ========================================== // Rendering Tests // ========================================== describe('Rendering', () => { it('should render without crashing', () => { // Arrange & Act renderWithProviders() // Assert expect(screen.getByText('Data Source 1')).toBeInTheDocument() expect(screen.getByText('Data Source 2')).toBeInTheDocument() expect(screen.getByText('Data Source 3')).toBeInTheDocument() }) it('should render correct number of option cards', () => { // Arrange & Act renderWithProviders() // Assert expect(screen.getByText('Data Source 1')).toBeInTheDocument() expect(screen.getByText('Data Source 2')).toBeInTheDocument() expect(screen.getByText('Data Source 3')).toBeInTheDocument() }) it('should render with grid layout', () => { // Arrange & Act const { container } = renderWithProviders() // Assert const gridContainer = container.firstChild expect(gridContainer).toHaveClass('grid') expect(gridContainer).toHaveClass('w-full') expect(gridContainer).toHaveClass('grid-cols-4') expect(gridContainer).toHaveClass('gap-1') }) it('should render no option cards when options is empty', () => { // Arrange mockUseDatasourceOptions.mockReturnValue([]) // Act const { container } = renderWithProviders() // Assert expect(screen.queryByText('Data Source')).not.toBeInTheDocument() // Grid container should still exist expect(container.firstChild).toHaveClass('grid') }) it('should render single option card when only one option exists', () => { // Arrange const singleOption = [createMockDatasourceOption(defaultNodes[0])] mockUseDatasourceOptions.mockReturnValue(singleOption) // Act renderWithProviders() // Assert expect(screen.getByText('Data Source 1')).toBeInTheDocument() expect(screen.queryByText('Data Source 2')).not.toBeInTheDocument() }) }) // ========================================== // Props Tests // ========================================== describe('Props', () => { describe('pipelineNodes', () => { it('should pass pipelineNodes to useDatasourceOptions hook', () => { // Arrange const customNodes = createMockPipelineNodes(2) mockUseDatasourceOptions.mockReturnValue(customNodes.map(createMockDatasourceOption)) // Act renderWithProviders( , ) // Assert expect(mockUseDatasourceOptions).toHaveBeenCalledWith(customNodes) }) it('should handle empty pipelineNodes array', () => { // Arrange mockUseDatasourceOptions.mockReturnValue([]) // Act renderWithProviders( , ) // Assert expect(mockUseDatasourceOptions).toHaveBeenCalledWith([]) }) }) describe('datasourceNodeId', () => { it('should mark corresponding option as selected', () => { // Arrange & Act const { container } = renderWithProviders( , ) // Assert - Check for selected styling on second card const cards = container.querySelectorAll('.rounded-xl.border') expect(cards[1]).toHaveClass('border-components-option-card-option-selected-border') }) it('should show no selection when datasourceNodeId is empty', () => { // Arrange & Act const { container } = renderWithProviders( , ) // Assert - No card should have selected styling const selectedCards = container.querySelectorAll('.border-components-option-card-option-selected-border') expect(selectedCards).toHaveLength(0) }) it('should show no selection when datasourceNodeId does not match any option', () => { // Arrange & Act const { container } = renderWithProviders( , ) // Assert const selectedCards = container.querySelectorAll('.border-components-option-card-option-selected-border') expect(selectedCards).toHaveLength(0) }) it('should update selection when datasourceNodeId changes', () => { // Arrange const { container, rerender } = renderWithProviders( , ) // Assert initial selection let cards = container.querySelectorAll('.rounded-xl.border') expect(cards[0]).toHaveClass('border-components-option-card-option-selected-border') // Act - Change selection rerender( , ) // Assert new selection cards = container.querySelectorAll('.rounded-xl.border') expect(cards[0]).not.toHaveClass('border-components-option-card-option-selected-border') expect(cards[1]).toHaveClass('border-components-option-card-option-selected-border') }) }) describe('onSelect', () => { it('should receive onSelect callback', () => { // Arrange const mockOnSelect = jest.fn() // Act renderWithProviders( , ) // Assert - Component renders without error expect(screen.getByText('Data Source 1')).toBeInTheDocument() }) }) }) // ========================================== // Side Effects and Cleanup Tests // ========================================== describe('Side Effects and Cleanup', () => { describe('useEffect - Auto-select first option', () => { it('should auto-select first option when options exist and no datasourceNodeId', () => { // Arrange const mockOnSelect = jest.fn() // Act renderWithProviders( , ) // Assert - Should auto-select first option on mount expect(mockOnSelect).toHaveBeenCalledTimes(1) expect(mockOnSelect).toHaveBeenCalledWith({ nodeId: 'node-1', nodeData: defaultOptions[0].data, } satisfies Datasource) }) it('should NOT auto-select when datasourceNodeId is provided', () => { // Arrange const mockOnSelect = jest.fn() // Act renderWithProviders( , ) // Assert - Should not auto-select because datasourceNodeId is provided expect(mockOnSelect).not.toHaveBeenCalled() }) it('should NOT auto-select when options array is empty', () => { // Arrange mockUseDatasourceOptions.mockReturnValue([]) const mockOnSelect = jest.fn() // Act renderWithProviders( , ) // Assert expect(mockOnSelect).not.toHaveBeenCalled() }) it('should only run useEffect once on initial mount', () => { // Arrange const mockOnSelect = jest.fn() const { rerender } = renderWithProviders( , ) // Assert - Called once on mount expect(mockOnSelect).toHaveBeenCalledTimes(1) // Act - Rerender with same props rerender( , ) // Assert - Still called only once (useEffect has empty dependency array) expect(mockOnSelect).toHaveBeenCalledTimes(1) }) }) }) // ========================================== // Callback Stability and Memoization Tests // ========================================== describe('Callback Stability and Memoization', () => { it('should maintain callback reference stability across renders with same props', () => { // Arrange const mockOnSelect = jest.fn() const { rerender } = renderWithProviders( , ) // Get initial click handlers expect(screen.getByText('Data Source 1')).toBeInTheDocument() // Trigger clicks to test handlers work fireEvent.click(screen.getByText('Data Source 1')) expect(mockOnSelect).toHaveBeenCalledTimes(2) // 1 auto-select + 1 click // Act - Rerender with same onSelect reference rerender( , ) // Assert - Component still works after rerender fireEvent.click(screen.getByText('Data Source 2')) expect(mockOnSelect).toHaveBeenCalledTimes(3) }) it('should update callback when onSelect changes', () => { // Arrange const mockOnSelect1 = jest.fn() const mockOnSelect2 = jest.fn() const { rerender } = renderWithProviders( , ) // Act - Click with first callback fireEvent.click(screen.getByText('Data Source 2')) expect(mockOnSelect1).toHaveBeenCalledTimes(1) // Act - Change callback rerender( , ) // Act - Click with new callback fireEvent.click(screen.getByText('Data Source 3')) // Assert - New callback should be called expect(mockOnSelect2).toHaveBeenCalledTimes(1) expect(mockOnSelect2).toHaveBeenCalledWith({ nodeId: 'node-3', nodeData: defaultOptions[2].data, }) }) it('should update callback when options change', () => { // Arrange const mockOnSelect = jest.fn() const { rerender } = renderWithProviders( , ) // Act - Click first option fireEvent.click(screen.getByText('Data Source 1')) expect(mockOnSelect).toHaveBeenCalledWith({ nodeId: 'node-1', nodeData: defaultOptions[0].data, }) // Act - Change options const newNodes = createMockPipelineNodes(2) const newOptions = newNodes.map(node => createMockDatasourceOption(node)) mockUseDatasourceOptions.mockReturnValue(newOptions) rerender( , ) // Act - Click updated first option fireEvent.click(screen.getByText('Data Source 1')) // Assert - Callback receives new option data expect(mockOnSelect).toHaveBeenLastCalledWith({ nodeId: newOptions[0].value, nodeData: newOptions[0].data, }) }) }) // ========================================== // User Interactions and Event Handlers Tests // ========================================== describe('User Interactions and Event Handlers', () => { describe('Option Selection', () => { it('should call onSelect with correct datasource when clicking an option', () => { // Arrange const mockOnSelect = jest.fn() renderWithProviders( , ) // Act - Click second option fireEvent.click(screen.getByText('Data Source 2')) // Assert expect(mockOnSelect).toHaveBeenCalledTimes(1) expect(mockOnSelect).toHaveBeenCalledWith({ nodeId: 'node-2', nodeData: defaultOptions[1].data, } satisfies Datasource) }) it('should allow selecting already selected option', () => { // Arrange const mockOnSelect = jest.fn() renderWithProviders( , ) // Act - Click already selected option fireEvent.click(screen.getByText('Data Source 1')) // Assert expect(mockOnSelect).toHaveBeenCalledTimes(1) expect(mockOnSelect).toHaveBeenCalledWith({ nodeId: 'node-1', nodeData: defaultOptions[0].data, }) }) it('should allow multiple sequential selections', () => { // Arrange const mockOnSelect = jest.fn() renderWithProviders( , ) // Act - Click options sequentially fireEvent.click(screen.getByText('Data Source 1')) fireEvent.click(screen.getByText('Data Source 2')) fireEvent.click(screen.getByText('Data Source 3')) // Assert expect(mockOnSelect).toHaveBeenCalledTimes(3) expect(mockOnSelect).toHaveBeenNthCalledWith(1, { nodeId: 'node-1', nodeData: defaultOptions[0].data, }) expect(mockOnSelect).toHaveBeenNthCalledWith(2, { nodeId: 'node-2', nodeData: defaultOptions[1].data, }) expect(mockOnSelect).toHaveBeenNthCalledWith(3, { nodeId: 'node-3', nodeData: defaultOptions[2].data, }) }) }) describe('handelSelect Internal Logic', () => { it('should handle rapid successive clicks', async () => { // Arrange const mockOnSelect = jest.fn() renderWithProviders( , ) // Act - Rapid clicks await act(async () => { fireEvent.click(screen.getByText('Data Source 1')) fireEvent.click(screen.getByText('Data Source 2')) fireEvent.click(screen.getByText('Data Source 3')) fireEvent.click(screen.getByText('Data Source 1')) fireEvent.click(screen.getByText('Data Source 2')) }) // Assert - All clicks should be registered expect(mockOnSelect).toHaveBeenCalledTimes(5) }) }) }) // ========================================== // Edge Cases and Error Handling Tests // ========================================== describe('Edge Cases and Error Handling', () => { describe('Empty States', () => { it('should handle empty options array gracefully', () => { // Arrange mockUseDatasourceOptions.mockReturnValue([]) // Act const { container } = renderWithProviders( , ) // Assert expect(container.firstChild).toBeInTheDocument() }) it('should not crash when datasourceNodeId is undefined', () => { // Arrange & Act renderWithProviders( , ) // Assert expect(screen.getByText('Data Source 1')).toBeInTheDocument() }) }) describe('Null/Undefined Values', () => { it('should handle option with missing data properties', () => { // Arrange const optionWithMinimalData = [{ label: 'Minimal Option', value: 'minimal-1', data: { title: 'Minimal', desc: '', type: BlockEnum.DataSource, plugin_id: '', provider_type: '', provider_name: '', datasource_name: '', datasource_label: '', datasource_parameters: {}, datasource_configurations: {}, } as DataSourceNodeType, }] mockUseDatasourceOptions.mockReturnValue(optionWithMinimalData) // Act renderWithProviders() // Assert expect(screen.getByText('Minimal Option')).toBeInTheDocument() }) }) describe('Large Data Sets', () => { it('should handle large number of options', () => { // Arrange const manyNodes = createMockPipelineNodes(50) const manyOptions = manyNodes.map(createMockDatasourceOption) mockUseDatasourceOptions.mockReturnValue(manyOptions) // Act renderWithProviders( , ) // Assert expect(screen.getByText('Data Source 1')).toBeInTheDocument() expect(screen.getByText('Data Source 50')).toBeInTheDocument() }) }) describe('Special Characters in Data', () => { it('should handle special characters in option labels', () => { // Arrange const specialNode = createMockPipelineNode({ id: 'special-node', data: createMockDataSourceNodeData({ title: 'Data Source ', }), }) const specialOptions = [createMockDatasourceOption(specialNode)] mockUseDatasourceOptions.mockReturnValue(specialOptions) // Act renderWithProviders( , ) // Assert - Special characters should be escaped/rendered safely expect(screen.getByText('Data Source ')).toBeInTheDocument() }) it('should handle unicode characters in option labels', () => { // Arrange const unicodeNode = createMockPipelineNode({ id: 'unicode-node', data: createMockDataSourceNodeData({ title: '数据源 📁 Source émoji', }), }) const unicodeOptions = [createMockDatasourceOption(unicodeNode)] mockUseDatasourceOptions.mockReturnValue(unicodeOptions) // Act renderWithProviders( , ) // Assert expect(screen.getByText('数据源 📁 Source émoji')).toBeInTheDocument() }) it('should handle empty string as option value', () => { // Arrange const emptyValueOption = [{ label: 'Empty Value Option', value: '', data: createMockDataSourceNodeData(), }] mockUseDatasourceOptions.mockReturnValue(emptyValueOption) // Act renderWithProviders() // Assert expect(screen.getByText('Empty Value Option')).toBeInTheDocument() }) }) describe('Boundary Conditions', () => { it('should handle single option selection correctly', () => { // Arrange const singleOption = [createMockDatasourceOption(defaultNodes[0])] mockUseDatasourceOptions.mockReturnValue(singleOption) const mockOnSelect = jest.fn() // Act renderWithProviders( , ) // Assert - Click should still work fireEvent.click(screen.getByText('Data Source 1')) expect(mockOnSelect).toHaveBeenCalledTimes(1) }) it('should handle options with same labels but different values', () => { // Arrange const duplicateLabelOptions = [ { label: 'Duplicate Label', value: 'node-a', data: createMockDataSourceNodeData({ plugin_id: 'plugin-a' }), }, { label: 'Duplicate Label', value: 'node-b', data: createMockDataSourceNodeData({ plugin_id: 'plugin-b' }), }, ] mockUseDatasourceOptions.mockReturnValue(duplicateLabelOptions) const mockOnSelect = jest.fn() // Act renderWithProviders( , ) // Assert - Both should render const labels = screen.getAllByText('Duplicate Label') expect(labels).toHaveLength(2) // Click second one fireEvent.click(labels[1]) expect(mockOnSelect).toHaveBeenCalledWith({ nodeId: 'node-b', nodeData: expect.objectContaining({ plugin_id: 'plugin-b' }), }) }) }) describe('Component Unmounting', () => { it('should handle unmounting without errors', () => { // Arrange const mockOnSelect = jest.fn() const { unmount } = renderWithProviders( , ) // Act unmount() // Assert - No errors thrown, component cleanly unmounted expect(screen.queryByText('Data Source 1')).not.toBeInTheDocument() }) it('should handle unmounting during rapid interactions', async () => { // Arrange const mockOnSelect = jest.fn() const { unmount } = renderWithProviders( , ) // Act - Start interactions then unmount fireEvent.click(screen.getByText('Data Source 1')) // Unmount during/after interaction unmount() // Assert - Should not throw expect(screen.queryByText('Data Source 1')).not.toBeInTheDocument() }) }) }) // ========================================== // Integration Tests // ========================================== describe('Integration', () => { it('should render OptionCard with correct props', () => { // Arrange & Act const { container } = renderWithProviders() // Assert - Verify real OptionCard components are rendered const cards = container.querySelectorAll('.rounded-xl.border') expect(cards).toHaveLength(3) }) it('should correctly pass selected state to OptionCard', () => { // Arrange & Act const { container } = renderWithProviders( , ) // Assert const cards = container.querySelectorAll('.rounded-xl.border') expect(cards[0]).not.toHaveClass('border-components-option-card-option-selected-border') expect(cards[1]).toHaveClass('border-components-option-card-option-selected-border') expect(cards[2]).not.toHaveClass('border-components-option-card-option-selected-border') }) it('should use option.value as key for React rendering', () => { // This test verifies that React doesn't throw duplicate key warnings // Arrange const uniqueValueOptions = createMockPipelineNodes(5).map(createMockDatasourceOption) mockUseDatasourceOptions.mockReturnValue(uniqueValueOptions) // Act - Should render without console warnings about duplicate keys const consoleSpy = jest.spyOn(console, 'error').mockImplementation(jest.fn()) renderWithProviders() // Assert expect(consoleSpy).not.toHaveBeenCalledWith( expect.stringContaining('key'), ) consoleSpy.mockRestore() }) }) // ========================================== // All Prop Variations Tests // ========================================== describe('All Prop Variations', () => { it.each([ { datasourceNodeId: '', description: 'empty string' }, { datasourceNodeId: 'node-1', description: 'first node' }, { datasourceNodeId: 'node-2', description: 'middle node' }, { datasourceNodeId: 'node-3', description: 'last node' }, { datasourceNodeId: 'non-existent', description: 'non-existent node' }, ])('should handle datasourceNodeId as $description', ({ datasourceNodeId }) => { // Arrange & Act renderWithProviders( , ) // Assert expect(screen.getByText('Data Source 1')).toBeInTheDocument() }) it.each([ { count: 0, description: 'zero options' }, { count: 1, description: 'single option' }, { count: 3, description: 'few options' }, { count: 10, description: 'many options' }, ])('should render correctly with $description', ({ count }) => { // Arrange const nodes = createMockPipelineNodes(count) const options = nodes.map(createMockDatasourceOption) mockUseDatasourceOptions.mockReturnValue(options) // Act renderWithProviders( , ) // Assert if (count > 0) expect(screen.getByText('Data Source 1')).toBeInTheDocument() else expect(screen.queryByText('Data Source 1')).not.toBeInTheDocument() }) }) })