import '@testing-library/jest-dom'; import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import { ChatbotDisplayMode } from '../Chatbot/Chatbot'; import ChatbotConversationHistoryNav, { Conversation } from './ChatbotConversationHistoryNav'; import { EmptyStateStatus, Spinner } from '@patternfly/react-core'; import { BellIcon, OutlinedCommentsIcon, SearchIcon } from '@patternfly/react-icons'; import { ComponentType } from 'react'; const ERROR = { bodyText: ( <> To try again, check your connection and reload this page. If the issue persists,{' '} contact the support team. ), buttonText: 'Reload', buttonIcon: , hasButton: true, titleText: 'Could not load chat history', status: EmptyStateStatus.danger, onClick: () => alert('Clicked Reload') }; const NO_RESULTS = { bodyText: 'Adjust your search query and try again. Check your spelling or try a more general term.', titleText: 'No results found', icon: SearchIcon as ComponentType }; const EMPTY_STATE = { bodyText: 'Access timely assistance by starting a conversation with an AI model.', titleText: 'Start a new chat', icon: OutlinedCommentsIcon as ComponentType }; const ERROR_WITHOUT_BUTTON = { bodyText: ( <> To try again, check your connection and reload this page. If the issue persists,{' '} contact the support team. ), buttonText: 'Reload', buttonIcon: , hasButton: false, titleText: 'Could not load chat history', status: EmptyStateStatus.danger, onClick: () => alert('Clicked Reload') }; describe('ChatbotConversationHistoryNav', () => { const onDrawerToggle = jest.fn(); const initialConversations: Conversation[] = [ { id: '1', text: 'ChatBot documentation' } ]; it('should open the conversation history navigation drawer', () => { render( ); expect(screen.queryByText('ChatBot documentation')).toBeInTheDocument(); }); it('should display the conversations for grouped conversations', () => { const groupedConversations: { [key: string]: Conversation[] } = { Today: [...initialConversations, { id: '2', text: 'Chatbot extension' }] }; render( ); expect(screen.queryByText('Chatbot extension')).toBeInTheDocument(); }); it('should apply the reversed class when reverseButtonOrder is true', () => { render( ); expect(screen.getByTestId('chatbot-nav-drawer-actions')).toHaveClass('pf-v6-c-drawer__actions--reversed'); }); it('should disable new chat button', () => { render( ); expect(screen.getByRole('button', { name: 'New chat' })).toBeDisabled(); }); it('should not apply the reversed class when reverseButtonOrder is false', () => { render( ); expect(screen.getByTestId('chatbot-nav-drawer-actions')).not.toHaveClass('pf-v6-c-drawer__actions--reversed'); }); it('should invoke handleTextInputChange callback when user searches for conversations', () => { const handleSearch = jest.fn(); const groupedConversations: { [key: string]: Conversation[] } = { Today: [...initialConversations, { id: '2', text: 'Chatbot extension' }] }; render( ); const searchInput = screen.getByPlaceholderText(/Search/i); fireEvent.change(searchInput, { target: { value: 'Chatbot' } }); expect(handleSearch).toHaveBeenCalledWith('Chatbot'); }); it('should close the drawer when escape key is pressed', async () => { render( ); fireEvent.keyDown(screen.getByPlaceholderText(/Search/i), { key: 'Escape', code: 'Escape', keyCode: 27, charCode: 27 }); waitFor(() => { expect(screen.queryByText('ChatBot documentation')).not.toBeInTheDocument(); }); }); it('should be resizable', () => { render( ); expect(screen.getByRole('dialog', { name: /Resize/i })).toBeTruthy(); expect(screen.getByRole('separator', { name: /Resize/i })).toBeTruthy(); expect(screen.getByRole('dialog', { name: /Resize/i })).toHaveAttribute( 'style', '--pf-v6-c-drawer__panel--md--FlexBasis: 384px; --pf-v6-c-drawer__panel--md--FlexBasis--min: 200px;' ); }); it('should accept drawerContentProps', () => { const { container } = render( ); const element = container.querySelector('.test'); expect(element).toBeInTheDocument(); }); it('should accept drawerContentBodyProps', () => { const { container } = render( ); const element = container.querySelector('.test'); expect(element).toBeInTheDocument(); }); it('should accept drawerHeadProps', () => { const { container } = render( ); const element = container.querySelector('.test'); expect(element).toBeInTheDocument(); }); it('should accept drawerActionsProps', () => { const { container } = render( ); const element = container.querySelector('.test'); expect(element).toBeInTheDocument(); }); it('should accept drawerCloseButtonProps', () => { const { container } = render( ); const element = container.querySelector('.test'); expect(element).toBeInTheDocument(); }); it('should accept drawerPanelBodyProps', () => { const { container } = render( ); const element = container.querySelector('.test'); expect(element).toBeInTheDocument(); }); it('should show loading state if triggered', () => { render( ); expect(screen.getByRole('dialog', { name: /Loading chatbot conversation history/i })).toBeTruthy(); expect(screen.getByRole('button', { name: /Close drawer panel/i })).toBeTruthy(); }); it('should pass alternative aria label to loading state', () => { render( ); expect(screen.getByRole('dialog', { name: /I am a test/i })).toBeTruthy(); }); it('should accept errorState', () => { render( ); expect( screen.getByRole('dialog', { name: /Could not load chat history To try again, check your connection and reload this page. If the issue persists, contact the support team . Loading... Reload/i }) ).toBeTruthy(); expect(screen.getByRole('button', { name: /Close drawer panel/i })).toBeTruthy(); expect(screen.getByRole('button', { name: /Loading... Reload/i })).toBeTruthy(); expect(screen.getByRole('textbox', { name: /Search previous conversations/i })).toBeTruthy(); expect(screen.getByRole('heading', { name: /Could not load chat history/i })).toBeTruthy(); }); it('should accept errorState without button', () => { render( ); expect( screen.getByRole('dialog', { name: /Could not load chat history To try again, check your connection and reload this page. If the issue persists, contact the support team ./i }) ).toBeTruthy(); expect(screen.getByRole('button', { name: /Close drawer panel/i })).toBeTruthy(); expect(screen.queryByRole('button', { name: /Loading... Reload/i })).toBeFalsy(); expect(screen.getByRole('textbox', { name: /Search previous conversations/i })).toBeTruthy(); expect(screen.getByRole('heading', { name: /Could not load chat history/i })).toBeTruthy(); }); it('should show loading state over error state if both are supplied', () => { render( ); expect(screen.getByRole('dialog', { name: /Loading/i })).toBeTruthy(); }); it('should accept emptyState', () => { render( ); expect( screen.getByRole('dialog', { name: /Start a new chat Access timely assistance by starting a conversation with an AI model./i }) ).toBeTruthy(); }); it('should accept no results state', () => { render( ); expect( screen.getByRole('dialog', { name: /No results found Adjust your search query and try again. Check your spelling or try a more general term./i }) ).toBeTruthy(); }); it('should handle isCompact', () => { render( ); expect(screen.getByTestId('drawer')).toHaveClass('pf-m-compact'); }); it('should display the default title', () => { render( ); expect(screen.getByText('Chat history')).toBeInTheDocument(); }); it('should display the custom title', () => { render( ); expect(screen.getByText('PatternFly history')).toBeInTheDocument(); }); it('should display the clock icon', () => { const { container } = render( ); const iconElement = container.querySelector('.pf-chatbot__title-icon'); expect(iconElement).toBeInTheDocument(); }); it('Passes menuProps to Menu', () => { render( ); expect(screen.getByRole('menu').parentElement?.parentElement).toHaveClass('test'); }); it('Passes menuContentProps to MenuContent', () => { render( ); expect(screen.getByRole('menu').parentElement).toHaveClass('test'); }); it('Passes menuListProps to MenuList when conversations is an array', () => { render( ); expect(screen.getByRole('menu')).toHaveClass('test'); }); it('Passes menuListProps to MenuList when conversations is an object', () => { render( ); expect(screen.getByRole('menu')).toHaveClass('test'); }); it('Passes menuGroupProps to MenuGroup when conversations is an object', () => { render( ); expect(screen.getByRole('menu').parentElement).toHaveClass('test'); }); it('Passes additionalProps to MenuItem', () => { render( ); expect(screen.getByRole('menuitem')).toHaveClass('test'); }); it('should be able to spread search input props when searchInputProps is passed', () => { render( ); expect(screen.getByRole('dialog', { name: /Chat history I am a sample search/i })).toBeInTheDocument(); }); it('Does not render search actions by default', () => { const handleSearch = jest.fn(); const groupedConversations: { [key: string]: Conversation[] } = { Today: [...initialConversations, { id: '2', text: 'Chatbot extension' }] }; render( ); const searchInput = screen.getByPlaceholderText(/Search/i); expect(searchInput.parentElement?.previousElementSibling).toBeNull(); expect(searchInput.parentElement?.nextElementSibling).toBeNull(); }); it('Renders with action at start when searchActionStart is passed', () => { const handleSearch = jest.fn(); const groupedConversations: { [key: string]: Conversation[] } = { Today: [...initialConversations, { id: '2', text: 'Chatbot extension' }] }; render( Search action start test} /> ); expect(screen.getByText('Search action start test')).toBeVisible(); }); it('Renders with action at end when searchActionEnd is passed', () => { const handleSearch = jest.fn(); const groupedConversations: { [key: string]: Conversation[] } = { Today: [...initialConversations, { id: '2', text: 'Chatbot extension' }] }; render( Search action end test} /> ); expect(screen.getByText('Search action end test')).toBeVisible(); }); it('Overrides default search input and actions when searchToolbar is passed', () => { const handleSearch = jest.fn(); const groupedConversations: { [key: string]: Conversation[] } = { Today: [...initialConversations, { id: '2', text: 'Chatbot extension' }] }; render( Search action start test} searchActionEnd={
Search action end test
} searchToolbar={
Custom toolbar
} /> ); expect(screen.queryByPlaceholderText(/Search/i)).not.toBeInTheDocument(); expect(screen.queryByText('Search action start test')).not.toBeInTheDocument(); expect(screen.queryByText('Search action end test')).not.toBeInTheDocument(); expect(screen.getByText('Custom toolbar')).toBeInTheDocument(); }); it('overrides nav title heading level when navTitleProps.headingLevel is passed', () => { render( ); expect(screen.queryByRole('heading', { name: /Chat history/i, level: 2 })).not.toBeInTheDocument(); expect(screen.getByRole('heading', { name: /Chat history/i, level: 1 })).toBeInTheDocument(); }); it('overrides nav title icon when navTitleIcon is passed in', () => { render( } /> ); expect(screen.getByTestId('bell')).toBeInTheDocument(); }); });