Skip to content

fix: bug(#1653) added tooltip for chat history items. #1785

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
May 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,17 @@ jest.mock('../../api/api', () => ({
historyDelete: jest.fn()
}))

// Mocking TooltipHost from @fluentui/react
jest.mock('@fluentui/react', () => {
const actual = jest.requireActual('@fluentui/react');
return {
...actual,
TooltipHost: ({ children }: { children: React.ReactNode }) => (
<div data-testid="tooltip-mock">{children}</div>
),
};
});

const conversation: Conversation = {
id: '1',
title: 'Test Chat',
Expand Down Expand Up @@ -47,7 +58,7 @@ describe('ChatHistoryListItemCell', () => {
})

test('renders the chat history item', () => {
render(<ChatHistoryListItemCell {...componentProps} />);
render(<ChatHistoryListItemCell {...componentProps} />);
const titleElement = screen.getByText(/Test Chat/i)
expect(titleElement).toBeInTheDocument()
})
Expand All @@ -71,10 +82,10 @@ render(<ChatHistoryListItemCell {...componentProps} />);
// Check if the truncated title is in the document
const truncatedTitle = screen.getByText(/A very long title that shoul .../i)
expect(truncatedTitle).toBeInTheDocument()
})
})

test('calls onSelect when clicked', () => {
render(<ChatHistoryListItemCell {...componentProps} />);
render(<ChatHistoryListItemCell {...componentProps} />);
const item = screen.getByLabelText('chat history item')
fireEvent.click(item)
expect(mockOnSelect).toHaveBeenCalledWith(conversation)
Expand All @@ -93,7 +104,7 @@ render(<ChatHistoryListItemCell {...componentProps} />);
// Expect that no content related to the title is rendered
const titleElement = screen.queryByText(/Test Chat/i);
expect(titleElement).not.toBeInTheDocument();
})
})

test('displays delete and edit buttons on hover', async () => {
const mockAppStateUpdated = {
Expand Down Expand Up @@ -132,7 +143,7 @@ render(<ChatHistoryListItemCell {...componentProps} />);
})

test('shows confirmation dialog and deletes item', async () => {
;(historyDelete as jest.Mock).mockResolvedValueOnce({
; (historyDelete as jest.Mock).mockResolvedValueOnce({
ok: true,
json: async () => ({})
})
Expand All @@ -156,7 +167,7 @@ render(<ChatHistoryListItemCell {...componentProps} />);
})

test('when delete API fails or return false', async () => {
;(historyDelete as jest.Mock).mockResolvedValueOnce({
; (historyDelete as jest.Mock).mockResolvedValueOnce({
ok: false,
json: async () => ({})
})
Expand Down Expand Up @@ -204,10 +215,10 @@ render(<ChatHistoryListItemCell {...componentProps} />);
const appStateWithRequestInitiated = {
...componentProps,
isGenerating: true,
selectedConvId:'1'
selectedConvId: '1'
}

render(<ChatHistoryListItemCell {...appStateWithRequestInitiated} onSelect={mockOnSelect} />);
render(<ChatHistoryListItemCell {...appStateWithRequestInitiated} onSelect={mockOnSelect} />);
const item = screen.getByLabelText('chat history item')
fireEvent.mouseEnter(item)
const deleteButton = screen.getByTitle(/Delete/i)
Expand All @@ -218,10 +229,10 @@ render(<ChatHistoryListItemCell {...componentProps} />);
})

test('does not disable buttons when request is not initiated', () => {
render(<ChatHistoryListItemCell {...componentProps} />);
const item = screen.getByLabelText('chat history item')
fireEvent.mouseEnter(item)
const deleteButton = screen.getByTitle(/Delete/i)
render(<ChatHistoryListItemCell {...componentProps} />);
const item = screen.getByLabelText('chat history item')
fireEvent.mouseEnter(item)
const deleteButton = screen.getByTitle(/Delete/i)
const editButton = screen.getByTitle(/Edit/i)

expect(deleteButton).not.toBeDisabled()
Expand All @@ -245,12 +256,12 @@ render(<ChatHistoryListItemCell {...componentProps} />);
})

test('handles input onChange and onKeyDown ENTER events correctly', async () => {
;(historyRename as jest.Mock).mockResolvedValueOnce({
; (historyRename as jest.Mock).mockResolvedValueOnce({
ok: true,
json: async () => ({})
})

render(<ChatHistoryListItemCell {...componentProps} />);
render(<ChatHistoryListItemCell {...componentProps} />);
// Simulate hover to reveal Edit button
const item = screen.getByLabelText('chat history item')
fireEvent.mouseEnter(item)
Expand Down Expand Up @@ -316,7 +327,7 @@ render(<ChatHistoryListItemCell {...componentProps} />);
test('Should hide the rename from when cancel it.', async () => {
userEvent.setup()

render(<ChatHistoryListItemCell {...componentProps}/>)
render(<ChatHistoryListItemCell {...componentProps} />)
const item = screen.getByLabelText('chat history item')
fireEvent.mouseEnter(item)
// Wait for the Edit button to appear and click it
Expand All @@ -336,10 +347,10 @@ render(<ChatHistoryListItemCell {...componentProps} />);

test.skip('handles rename save API failed', async () => {
userEvent.setup()
;(historyRename as jest.Mock).mockRejectedValue({
ok: false,
json: async () => ({})
})
; (historyRename as jest.Mock).mockRejectedValue({
ok: false,
json: async () => ({})
})

render(<ChatHistoryListItemCell {...componentProps} />);
// Simulate hover to reveal Edit button
Expand Down Expand Up @@ -380,12 +391,12 @@ render(<ChatHistoryListItemCell {...componentProps} />);
date: new Date().toISOString()
}

;(historyRename as jest.Mock).mockResolvedValueOnce({
ok: false,
json: async () => ({ message: 'Title already exists' })
})
; (historyRename as jest.Mock).mockResolvedValueOnce({
ok: false,
json: async () => ({ message: 'Title already exists' })
})

render(<ChatHistoryListItemCell {...componentProps} />);
render(<ChatHistoryListItemCell {...componentProps} />);
const item = screen.getByLabelText('chat history item')
fireEvent.mouseEnter(item)

Expand All @@ -406,7 +417,7 @@ render(<ChatHistoryListItemCell {...componentProps} />);


test('triggers edit functionality when Enter key is pressed', async () => {
;(historyRename as jest.Mock).mockResolvedValueOnce({
; (historyRename as jest.Mock).mockResolvedValueOnce({
ok: true,
json: async () => ({ message: 'Title changed' })
})
Expand Down Expand Up @@ -438,12 +449,12 @@ render(<ChatHistoryListItemCell {...componentProps} />);
})

test('successfully saves edited title', async () => {
;(historyRename as jest.Mock).mockResolvedValueOnce({
; (historyRename as jest.Mock).mockResolvedValueOnce({
ok: true,
json: async () => ({})
})

render(<ChatHistoryListItemCell {...componentProps} />);
render(<ChatHistoryListItemCell {...componentProps} />);
const item = screen.getByLabelText('chat history item')
fireEvent.mouseEnter(item)

Expand Down Expand Up @@ -500,7 +511,7 @@ render(<ChatHistoryListItemCell {...componentProps} />);
///////

test('opens delete confirmation dialog when Enter key is pressed on the Delete button', async () => {
render(<ChatHistoryListItemCell {...componentProps} />);
render(<ChatHistoryListItemCell {...componentProps} />);
const item = screen.getByLabelText('chat history item')
fireEvent.mouseEnter(item)

Expand All @@ -511,7 +522,7 @@ render(<ChatHistoryListItemCell {...componentProps} />);
})

test('opens delete confirmation dialog when Space key is pressed on the Delete button', async () => {
render(<ChatHistoryListItemCell {...componentProps} />);
render(<ChatHistoryListItemCell {...componentProps} />);
const item = screen.getByLabelText('chat history item')
fireEvent.mouseEnter(item)

Expand All @@ -522,7 +533,7 @@ render(<ChatHistoryListItemCell {...componentProps} />);
})

test('opens edit input when Space key is pressed on the Edit button', async () => {
render(<ChatHistoryListItemCell {...componentProps} />);
render(<ChatHistoryListItemCell {...componentProps} />);
const item = screen.getByLabelText('chat history item')
fireEvent.mouseEnter(item)

Expand All @@ -534,7 +545,7 @@ render(<ChatHistoryListItemCell {...componentProps} />);
})

test('opens edit input when Enter key is pressed on the Edit button', async () => {
render(<ChatHistoryListItemCell {...componentProps} />);
render(<ChatHistoryListItemCell {...componentProps} />);
const item = screen.getByLabelText('chat history item')
fireEvent.mouseEnter(item)

Expand All @@ -547,11 +558,11 @@ render(<ChatHistoryListItemCell {...componentProps} />);

test('handles rename save when the updated text is equal to initial text', async () => {
userEvent.setup()
;(historyRename as jest.Mock).mockRejectedValue({
ok: false,
json: async () => ({ message: 'Title not changed' })
})
render(<ChatHistoryListItemCell {...componentProps}/>)
; (historyRename as jest.Mock).mockRejectedValue({
ok: false,
json: async () => ({ message: 'Title not changed' })
})
render(<ChatHistoryListItemCell {...componentProps} />)

// Simulate hover to reveal Edit button
const item = screen.getByLabelText('chat history item')
Expand All @@ -573,26 +584,26 @@ render(<ChatHistoryListItemCell {...componentProps} />);
//fireEvent.change(inputItem, { target: { value: 'Test Chat' } });
})
expect(historyRename).not.toHaveBeenCalled()
})
test('Should hide the rename from on Enter or space .', async () => {
userEvent.setup()

render(<ChatHistoryListItemCell {...componentProps}/>)
const item = screen.getByLabelText('chat history item')
fireEvent.mouseEnter(item)
// Wait for the Edit button to appear and click it
await waitFor(() => {
const editButton = screen.getByTitle(/Edit/i)
fireEvent.click(editButton)
})
test('Should hide the rename from on Enter or space .', async () => {
userEvent.setup()

const editButton =screen.getByRole('button', { name: 'cancel edit title' })
fireEvent.keyDown(editButton, { key: 'Enter', code: 'Enter', charCode: 13 })
render(<ChatHistoryListItemCell {...componentProps} />)
const item = screen.getByLabelText('chat history item')
fireEvent.mouseEnter(item)
// Wait for the Edit button to appear and click it
await waitFor(() => {
const editButton = screen.getByTitle(/Edit/i)
fireEvent.click(editButton)
})

// Wait for the error to be hidden after 5 seconds
await waitFor(() => {
const input = screen.queryByLabelText('confirm new title')
expect(input).not.toBeInTheDocument()
const editButton = screen.getByRole('button', { name: 'cancel edit title' })
fireEvent.keyDown(editButton, { key: 'Enter', code: 'Enter', charCode: 13 })

// Wait for the error to be hidden after 5 seconds
await waitFor(() => {
const input = screen.queryByLabelText('confirm new title')
expect(input).not.toBeInTheDocument()
})
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ import {
DialogType,
IconButton,
ITextField,
ITooltipHostStyles,
PrimaryButton,
Stack,
Text,
TextField,
TooltipHost,
} from "@fluentui/react";
import { useBoolean } from "@fluentui/react-hooks";

Expand All @@ -30,6 +32,11 @@ interface ChatHistoryListItemCellProps {
toggleToggleSpinner: (toggler: boolean) => void;
}


const calloutProps = { gapSpace: 0 };
const hostStyles: Partial<ITooltipHostStyles> = { root: { display: 'inline-block' } };


export const ChatHistoryListItemCell: React.FC<
ChatHistoryListItemCellProps
> = ({
Expand All @@ -51,6 +58,7 @@ export const ChatHistoryListItemCell: React.FC<
const [textFieldFocused, setTextFieldFocused] = useState(false);
const textFieldRef = useRef<ITextField | null>(null);
const isSelected = item?.id === selectedConvId;
const tooltipId = 'tooltip'+ item?.id;
const dialogContentProps = {
type: DialogType.close,
title: "Are you sure you want to delete this item?",
Expand Down Expand Up @@ -257,7 +265,14 @@ export const ChatHistoryListItemCell: React.FC<
) : (
<>
<Stack horizontal verticalAlign={"center"} style={{ width: "100%" }}>
<div className={styles.chatTitle}>{truncatedTitle}</div>
<div className={styles.chatTitle}>
<TooltipHost
content={item?.title}
id={tooltipId}
closeDelay={100}
calloutProps={calloutProps}
styles={hostStyles}
>{truncatedTitle} </TooltipHost></div>
{(isSelected || isHovered) && (
<Stack horizontal horizontalAlign="end">
<IconButton
Expand Down
Loading