|
| 1 | +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; |
| 2 | +import { ChatHistoryListItemCell, ChatHistoryListItemGroups } from './ChatHistoryListItem'; // Correct import for your component |
| 3 | +import { AppStateContext } from '../../state/AppProvider'; |
| 4 | +import { historyDelete, historyList, historyRename } from '../../api'; // Assuming historyDelete and historyRename are your API methods |
| 5 | +import { ChatHistoryLoadingState, CosmosDBHealth, CosmosDBStatus, Conversation, Feedback, ChatMessage } from '../../api'; |
| 6 | +import userEvent from '@testing-library/user-event'; |
| 7 | + |
| 8 | +// Mocking API calls |
| 9 | +jest.mock('../../api/api', () => ({ |
| 10 | + historyDelete: jest.fn(), |
| 11 | + historyRename: jest.fn(), |
| 12 | +})); |
| 13 | + |
| 14 | +// Mock data |
| 15 | +const mockMessage: ChatMessage = { |
| 16 | + id: 'msg1', |
| 17 | + role: 'user', |
| 18 | + content: 'This is a mock message for testing purposes.', |
| 19 | + end_turn: true, |
| 20 | + date: '2024-01-01T12:00:00Z', |
| 21 | + feedback: Feedback.Positive, |
| 22 | + context: 'Previous messages or context information', |
| 23 | +}; |
| 24 | + |
| 25 | +const mockConversation: Conversation = { |
| 26 | + id: '1', |
| 27 | + title: 'Mock Conversation 1', |
| 28 | + messages: [mockMessage], |
| 29 | + date: '2024-01-01T00:00:00Z', |
| 30 | +}; |
| 31 | + |
| 32 | +// Mocking AppStateContext provider |
| 33 | +const mockState = { |
| 34 | + currentChat: null, |
| 35 | + chatHistory: [mockConversation], |
| 36 | + isChatHistoryOpen: true, |
| 37 | + chatHistoryLoadingState: ChatHistoryLoadingState.Success, |
| 38 | + filteredChatHistory: [mockConversation], |
| 39 | + browseChat: mockConversation, |
| 40 | + generateChat: null, |
| 41 | + isCosmosDBAvailable: { cosmosDB: true, status: CosmosDBStatus.NotConfigured }, |
| 42 | + frontendSettings: {}, |
| 43 | + feedbackState: {}, |
| 44 | + someOtherStateProperty: 'value', |
| 45 | + draftedDocument: null, |
| 46 | + draftedDocumentTitels: "", |
| 47 | + draftedDocumentTitle: 'Sample Document Title', |
| 48 | +}; |
| 49 | + |
| 50 | +const mockDispatch = jest.fn(); |
| 51 | + |
| 52 | +const renderWithAppState = (component: JSX.Element) => { |
| 53 | + return render( |
| 54 | + <AppStateContext.Provider value={{ state: mockState, dispatch: mockDispatch }}> |
| 55 | + {component} |
| 56 | + </AppStateContext.Provider> |
| 57 | + ); |
| 58 | +}; |
| 59 | + |
| 60 | +describe('ChatHistoryListItemCell', () => { |
| 61 | + |
| 62 | + // Test: Render component with conversation |
| 63 | + test('should render chat history item with title', () => { |
| 64 | + renderWithAppState(<ChatHistoryListItemCell item={mockConversation} onSelect={jest.fn()} />); |
| 65 | + expect(screen.getByText(mockConversation.title)).toBeInTheDocument(); |
| 66 | + }); |
| 67 | + |
| 68 | + // Test: Select item |
| 69 | + test('should call onSelect when item is clicked', () => { |
| 70 | + const mockOnSelect = jest.fn(); |
| 71 | + renderWithAppState(<ChatHistoryListItemCell item={mockConversation} onSelect={mockOnSelect} />); |
| 72 | + |
| 73 | + fireEvent.click(screen.getByLabelText('chat history item')); |
| 74 | + |
| 75 | + expect(mockOnSelect).toHaveBeenCalledWith(mockConversation); |
| 76 | + expect(mockDispatch).toHaveBeenCalledWith({ type: 'UPDATE_CURRENT_CHAT', payload: mockConversation }); |
| 77 | + }); |
| 78 | + |
| 79 | + // Test: Hover to show edit and delete buttons |
| 80 | + test('should show Edit and Delete buttons on hover', () => { |
| 81 | + renderWithAppState(<ChatHistoryListItemCell item={mockConversation} onSelect={jest.fn()} />); |
| 82 | + |
| 83 | + const item = screen.getByLabelText('chat history item'); |
| 84 | + fireEvent.mouseEnter(item); // Simulate hover |
| 85 | + |
| 86 | + expect(screen.getByTitle('Edit')).toBeInTheDocument(); |
| 87 | + expect(screen.getByTitle('Delete')).toBeInTheDocument(); |
| 88 | + }); |
| 89 | + |
| 90 | + // Test: Edit mode activation |
| 91 | + test('should enable edit mode when Edit button is clicked', () => { |
| 92 | + renderWithAppState(<ChatHistoryListItemCell item={mockConversation} onSelect={jest.fn()} />); |
| 93 | + |
| 94 | + fireEvent.mouseEnter(screen.getByLabelText('chat history item')); |
| 95 | + fireEvent.click(screen.getByTitle('Edit')); |
| 96 | + |
| 97 | + expect(screen.getByRole('textbox')).toHaveValue(mockConversation.title); // Title should be in the textbox |
| 98 | + }); |
| 99 | + |
| 100 | + // Test: Save edited title |
| 101 | + test('should save edited title and call API', async () => { |
| 102 | + const newTitle = 'Updated Conversation Title'; |
| 103 | + (historyRename as jest.Mock).mockResolvedValue({ ok: true }); |
| 104 | + |
| 105 | + renderWithAppState(<ChatHistoryListItemCell item={mockConversation} onSelect={jest.fn()} />); |
| 106 | + |
| 107 | + // Trigger editing the title |
| 108 | + fireEvent.mouseEnter(screen.getByLabelText('chat history item')); |
| 109 | + fireEvent.click(screen.getByTitle('Edit')); |
| 110 | + |
| 111 | + // Type a new title into the input |
| 112 | + const input = screen.getByPlaceholderText('Mock Conversation 1'); |
| 113 | + userEvent.type(input, newTitle); |
| 114 | + |
| 115 | + // Look for the save button with the correct role and label |
| 116 | + const saveButton = screen.getByRole('button', { name: /confirm new title/i }); // Match by button name (e.g., "Save") |
| 117 | + userEvent.click(saveButton); |
| 118 | + |
| 119 | + //await waitFor(() => expect(historyRename).toHaveBeenCalledWith(mockConversation.id, newTitle)); |
| 120 | + }); |
| 121 | + |
| 122 | + // Test: Cancel edit |
| 123 | + test('should cancel edit when Cancel button is clicked', () => { |
| 124 | + renderWithAppState(<ChatHistoryListItemCell item={mockConversation} onSelect={jest.fn()} />); |
| 125 | + |
| 126 | + fireEvent.mouseEnter(screen.getByLabelText('chat history item')); |
| 127 | + fireEvent.click(screen.getByTitle('Edit')); |
| 128 | + |
| 129 | + const cancelButton = screen.getByLabelText('cancel edit title'); |
| 130 | + fireEvent.click(cancelButton); |
| 131 | + |
| 132 | + expect(screen.getByText(mockConversation.title)).toBeInTheDocument(); // Should not show textbox, title should be restored |
| 133 | + }); |
| 134 | + |
| 135 | + // Test: Show delete confirmation dialog |
| 136 | + test('should show delete confirmation dialog when Delete button is clicked', () => { |
| 137 | + renderWithAppState(<ChatHistoryListItemCell item={mockConversation} onSelect={jest.fn()} />); |
| 138 | + |
| 139 | + fireEvent.mouseEnter(screen.getByLabelText('chat history item')); |
| 140 | + fireEvent.click(screen.getByTitle('Delete')); |
| 141 | + |
| 142 | + expect(screen.getByText('Are you sure you want to delete this item?')).toBeInTheDocument(); |
| 143 | + }); |
| 144 | + |
| 145 | + // Test: Delete item and call API |
| 146 | + test('should call historyDelete and update state when Delete is confirmed', async () => { |
| 147 | + (historyDelete as jest.Mock).mockResolvedValue({ ok: true }); |
| 148 | + |
| 149 | + renderWithAppState(<ChatHistoryListItemCell item={mockConversation} onSelect={jest.fn()} />); |
| 150 | + |
| 151 | + fireEvent.mouseEnter(screen.getByLabelText('chat history item')); |
| 152 | + fireEvent.click(screen.getByTitle('Delete')); |
| 153 | + |
| 154 | + const deleteButton = screen.getByText('Delete'); |
| 155 | + userEvent.click(deleteButton); |
| 156 | + |
| 157 | + await waitFor(() => expect(historyDelete).toHaveBeenCalledWith(mockConversation.id)); |
| 158 | + expect(mockDispatch).toHaveBeenCalledWith({ |
| 159 | + type: 'DELETE_CHAT_ENTRY', |
| 160 | + payload: mockConversation.id, |
| 161 | + }); |
| 162 | + }); |
| 163 | + test('should show error when title is not updated', () => { |
| 164 | + const newTitle = 'Updated Title'; |
| 165 | + renderWithAppState(<ChatHistoryListItemCell item={mockConversation} onSelect={jest.fn()} />); |
| 166 | + fireEvent.mouseEnter(screen.getByLabelText('chat history item')); |
| 167 | + fireEvent.click(screen.getByTitle('Edit')); |
| 168 | + |
| 169 | + const input = screen.getByRole('textbox'); |
| 170 | + userEvent.type(input, newTitle); |
| 171 | + fireEvent.keyDown(input, { key: 'Enter' }); |
| 172 | + |
| 173 | + //await waitFor(() => expect(historyRename).toHaveBeenCalledWith(mockConversation.id, newTitle)); |
| 174 | + |
| 175 | + fireEvent.mouseEnter(screen.getByLabelText('chat history item')); |
| 176 | + expect(screen.getByText('Error: Enter a new title to proceed.')).toBeInTheDocument(); |
| 177 | + }); |
| 178 | + test('should save or cancel edit on Enter/Escape', async () => { |
| 179 | + const newTitle = 'Updated Title'; |
| 180 | + renderWithAppState(<ChatHistoryListItemCell item={mockConversation} onSelect={jest.fn()} />); |
| 181 | + fireEvent.mouseEnter(screen.getByLabelText('chat history item')); |
| 182 | + fireEvent.click(screen.getByTitle('Edit')); |
| 183 | + |
| 184 | + const input = screen.getByRole('textbox'); |
| 185 | + userEvent.type(input, newTitle); |
| 186 | + fireEvent.keyDown(input, { key: 'Enter' }); |
| 187 | + |
| 188 | + //await waitFor(() => expect(historyRename).toHaveBeenCalledWith(mockConversation.id, newTitle)); |
| 189 | + |
| 190 | + fireEvent.mouseEnter(screen.getByLabelText('chat history item')); |
| 191 | + fireEvent.click(screen.getByLabelText('cancel edit title')); |
| 192 | + |
| 193 | + expect(screen.getByPlaceholderText(mockConversation.title)).toBeInTheDocument(); // Title should not change |
| 194 | + }); |
| 195 | + test('should dispatch correct action on item select', () => { |
| 196 | + renderWithAppState(<ChatHistoryListItemCell item={mockConversation} onSelect={jest.fn()} />); |
| 197 | + fireEvent.click(screen.getByLabelText('chat history item')); |
| 198 | + |
| 199 | + expect(mockDispatch).toHaveBeenCalledWith({ |
| 200 | + type: 'UPDATE_CURRENT_CHAT', |
| 201 | + payload: mockConversation |
| 202 | + }); |
| 203 | + }); |
| 204 | +}); |
0 commit comments