Spaces:
Build error
Build error
OpenHands
/
frontend
/__tests__
/components
/features
/conversation-panel
/conversation-card.test.tsx
import { screen, within } from "@testing-library/react"; | |
import { | |
afterAll, | |
afterEach, | |
beforeAll, | |
describe, | |
expect, | |
it, | |
test, | |
vi, | |
} from "vitest"; | |
import userEvent from "@testing-library/user-event"; | |
import { renderWithProviders } from "test-utils"; | |
import { formatTimeDelta } from "#/utils/format-time-delta"; | |
import { ConversationCard } from "#/components/features/conversation-panel/conversation-card"; | |
import { clickOnEditButton } from "./utils"; | |
// We'll use the actual i18next implementation but override the translation function | |
import { I18nextProvider } from "react-i18next"; | |
import i18n from "i18next"; | |
// Mock the t function to return our custom translations | |
vi.mock("react-i18next", async () => { | |
const actual = await vi.importActual("react-i18next"); | |
return { | |
...actual, | |
useTranslation: () => ({ | |
t: (key: string) => { | |
const translations: Record<string, string> = { | |
"CONVERSATION$CREATED": "Created", | |
"CONVERSATION$AGO": "ago", | |
"CONVERSATION$UPDATED": "Updated" | |
}; | |
return translations[key] || key; | |
}, | |
i18n: { | |
changeLanguage: () => new Promise(() => {}), | |
}, | |
}), | |
}; | |
}); | |
describe("ConversationCard", () => { | |
const onClick = vi.fn(); | |
const onDelete = vi.fn(); | |
const onChangeTitle = vi.fn(); | |
beforeAll(() => { | |
vi.stubGlobal("window", { | |
open: vi.fn(), | |
addEventListener: vi.fn(), | |
removeEventListener: vi.fn(), | |
}); | |
}); | |
afterEach(() => { | |
vi.clearAllMocks(); | |
}); | |
afterAll(() => { | |
vi.unstubAllGlobals(); | |
}); | |
it("should render the conversation card", () => { | |
renderWithProviders( | |
<ConversationCard | |
onDelete={onDelete} | |
onChangeTitle={onChangeTitle} | |
isActive | |
title="Conversation 1" | |
selectedRepository={null} | |
lastUpdatedAt="2021-10-01T12:00:00Z" | |
/>, | |
); | |
const card = screen.getByTestId("conversation-card"); | |
within(card).getByText("Conversation 1"); | |
// Just check that the card contains the expected text content | |
expect(card).toHaveTextContent("Created"); | |
expect(card).toHaveTextContent("ago"); | |
// Use a regex to match the time part since it might have whitespace | |
const timeRegex = new RegExp(formatTimeDelta(new Date("2021-10-01T12:00:00Z"))); | |
expect(card).toHaveTextContent(timeRegex); | |
}); | |
it("should render the selectedRepository if available", () => { | |
const { rerender } = renderWithProviders( | |
<ConversationCard | |
onDelete={onDelete} | |
onChangeTitle={onChangeTitle} | |
isActive | |
title="Conversation 1" | |
selectedRepository={null} | |
lastUpdatedAt="2021-10-01T12:00:00Z" | |
/>, | |
); | |
expect( | |
screen.queryByTestId("conversation-card-selected-repository"), | |
).not.toBeInTheDocument(); | |
rerender( | |
<ConversationCard | |
onDelete={onDelete} | |
onChangeTitle={onChangeTitle} | |
isActive | |
title="Conversation 1" | |
selectedRepository="org/selectedRepository" | |
lastUpdatedAt="2021-10-01T12:00:00Z" | |
/>, | |
); | |
screen.getByTestId("conversation-card-selected-repository"); | |
}); | |
it("should toggle a context menu when clicking the ellipsis button", async () => { | |
const user = userEvent.setup(); | |
renderWithProviders( | |
<ConversationCard | |
onDelete={onDelete} | |
onChangeTitle={onChangeTitle} | |
isActive | |
title="Conversation 1" | |
selectedRepository={null} | |
lastUpdatedAt="2021-10-01T12:00:00Z" | |
/>, | |
); | |
expect(screen.queryByTestId("context-menu")).not.toBeInTheDocument(); | |
const ellipsisButton = screen.getByTestId("ellipsis-button"); | |
await user.click(ellipsisButton); | |
screen.getByTestId("context-menu"); | |
await user.click(ellipsisButton); | |
expect(screen.queryByTestId("context-menu")).not.toBeInTheDocument(); | |
}); | |
it("should call onDelete when the delete button is clicked", async () => { | |
const user = userEvent.setup(); | |
renderWithProviders( | |
<ConversationCard | |
onDelete={onDelete} | |
isActive | |
onChangeTitle={onChangeTitle} | |
title="Conversation 1" | |
selectedRepository={null} | |
lastUpdatedAt="2021-10-01T12:00:00Z" | |
/>, | |
); | |
const ellipsisButton = screen.getByTestId("ellipsis-button"); | |
await user.click(ellipsisButton); | |
const menu = screen.getByTestId("context-menu"); | |
const deleteButton = within(menu).getByTestId("delete-button"); | |
await user.click(deleteButton); | |
expect(onDelete).toHaveBeenCalled(); | |
}); | |
test("clicking the selectedRepository should not trigger the onClick handler", async () => { | |
const user = userEvent.setup(); | |
renderWithProviders( | |
<ConversationCard | |
onDelete={onDelete} | |
isActive | |
onChangeTitle={onChangeTitle} | |
title="Conversation 1" | |
selectedRepository="org/selectedRepository" | |
lastUpdatedAt="2021-10-01T12:00:00Z" | |
/>, | |
); | |
const selectedRepository = screen.getByTestId( | |
"conversation-card-selected-repository", | |
); | |
await user.click(selectedRepository); | |
expect(onClick).not.toHaveBeenCalled(); | |
}); | |
test("conversation title should call onChangeTitle when changed and blurred", async () => { | |
const user = userEvent.setup(); | |
renderWithProviders( | |
<ConversationCard | |
onDelete={onDelete} | |
isActive | |
title="Conversation 1" | |
selectedRepository={null} | |
lastUpdatedAt="2021-10-01T12:00:00Z" | |
onChangeTitle={onChangeTitle} | |
/>, | |
); | |
await clickOnEditButton(user); | |
const title = screen.getByTestId("conversation-card-title"); | |
expect(title).toBeEnabled(); | |
expect(screen.queryByTestId("context-menu")).not.toBeInTheDocument(); | |
// expect to be focused | |
expect(document.activeElement).toBe(title); | |
await user.clear(title); | |
await user.type(title, "New Conversation Name "); | |
await user.tab(); | |
expect(onChangeTitle).toHaveBeenCalledWith("New Conversation Name"); | |
expect(title).toHaveValue("New Conversation Name"); | |
}); | |
it("should reset title and not call onChangeTitle when the title is empty", async () => { | |
const user = userEvent.setup(); | |
renderWithProviders( | |
<ConversationCard | |
onDelete={onDelete} | |
isActive | |
onChangeTitle={onChangeTitle} | |
title="Conversation 1" | |
selectedRepository={null} | |
lastUpdatedAt="2021-10-01T12:00:00Z" | |
/>, | |
); | |
await clickOnEditButton(user); | |
const title = screen.getByTestId("conversation-card-title"); | |
await user.clear(title); | |
await user.tab(); | |
expect(onChangeTitle).not.toHaveBeenCalled(); | |
expect(title).toHaveValue("Conversation 1"); | |
}); | |
test("clicking the title should trigger the onClick handler", async () => { | |
const user = userEvent.setup(); | |
renderWithProviders( | |
<ConversationCard | |
onClick={onClick} | |
onDelete={onDelete} | |
isActive | |
onChangeTitle={onChangeTitle} | |
title="Conversation 1" | |
selectedRepository={null} | |
lastUpdatedAt="2021-10-01T12:00:00Z" | |
/>, | |
); | |
const title = screen.getByTestId("conversation-card-title"); | |
await user.click(title); | |
expect(onClick).toHaveBeenCalled(); | |
}); | |
test("clicking the title should not trigger the onClick handler if edit mode", async () => { | |
const user = userEvent.setup(); | |
renderWithProviders( | |
<ConversationCard | |
onDelete={onDelete} | |
isActive | |
onChangeTitle={onChangeTitle} | |
title="Conversation 1" | |
selectedRepository={null} | |
lastUpdatedAt="2021-10-01T12:00:00Z" | |
/>, | |
); | |
await clickOnEditButton(user); | |
const title = screen.getByTestId("conversation-card-title"); | |
await user.click(title); | |
expect(onClick).not.toHaveBeenCalled(); | |
}); | |
test("clicking the delete button should not trigger the onClick handler", async () => { | |
const user = userEvent.setup(); | |
renderWithProviders( | |
<ConversationCard | |
onDelete={onDelete} | |
isActive | |
onChangeTitle={onChangeTitle} | |
title="Conversation 1" | |
selectedRepository={null} | |
lastUpdatedAt="2021-10-01T12:00:00Z" | |
/>, | |
); | |
const ellipsisButton = screen.getByTestId("ellipsis-button"); | |
await user.click(ellipsisButton); | |
const menu = screen.getByTestId("context-menu"); | |
const deleteButton = within(menu).getByTestId("delete-button"); | |
await user.click(deleteButton); | |
expect(onClick).not.toHaveBeenCalled(); | |
}); | |
it("should show display cost button only when showOptions is true", async () => { | |
const user = userEvent.setup(); | |
const { rerender } = renderWithProviders( | |
<ConversationCard | |
onDelete={onDelete} | |
onChangeTitle={onChangeTitle} | |
isActive | |
title="Conversation 1" | |
selectedRepository={null} | |
lastUpdatedAt="2021-10-01T12:00:00Z" | |
/>, | |
); | |
const ellipsisButton = screen.getByTestId("ellipsis-button"); | |
await user.click(ellipsisButton); | |
// Wait for context menu to appear | |
const menu = await screen.findByTestId("context-menu"); | |
expect( | |
within(menu).queryByTestId("display-cost-button"), | |
).not.toBeInTheDocument(); | |
// Close menu | |
await user.click(ellipsisButton); | |
rerender( | |
<ConversationCard | |
onDelete={onDelete} | |
onChangeTitle={onChangeTitle} | |
showOptions | |
isActive | |
title="Conversation 1" | |
selectedRepository={null} | |
lastUpdatedAt="2021-10-01T12:00:00Z" | |
/>, | |
); | |
// Open menu again | |
await user.click(ellipsisButton); | |
// Wait for context menu to appear and check for display cost button | |
const newMenu = await screen.findByTestId("context-menu"); | |
within(newMenu).getByTestId("display-cost-button"); | |
}); | |
it("should show metrics modal when clicking the display cost button", async () => { | |
const user = userEvent.setup(); | |
renderWithProviders( | |
<ConversationCard | |
onDelete={onDelete} | |
isActive | |
onChangeTitle={onChangeTitle} | |
title="Conversation 1" | |
selectedRepository={null} | |
lastUpdatedAt="2021-10-01T12:00:00Z" | |
showOptions | |
/>, | |
); | |
const ellipsisButton = screen.getByTestId("ellipsis-button"); | |
await user.click(ellipsisButton); | |
const menu = screen.getByTestId("context-menu"); | |
const displayCostButton = within(menu).getByTestId("display-cost-button"); | |
await user.click(displayCostButton); | |
// Verify if metrics modal is displayed by checking for the modal content | |
expect(screen.getByTestId("metrics-modal")).toBeInTheDocument(); | |
}); | |
it("should not display the edit or delete options if the handler is not provided", async () => { | |
const user = userEvent.setup(); | |
const { rerender } = renderWithProviders( | |
<ConversationCard | |
onClick={onClick} | |
onChangeTitle={onChangeTitle} | |
title="Conversation 1" | |
selectedRepository={null} | |
lastUpdatedAt="2021-10-01T12:00:00Z" | |
/>, | |
); | |
const ellipsisButton = screen.getByTestId("ellipsis-button"); | |
await user.click(ellipsisButton); | |
const menu = await screen.findByTestId("context-menu"); | |
expect(within(menu).queryByTestId("edit-button")).toBeInTheDocument(); | |
expect(within(menu).queryByTestId("delete-button")).not.toBeInTheDocument(); | |
// toggle to hide the context menu | |
await user.click(ellipsisButton); | |
rerender( | |
<ConversationCard | |
onClick={onClick} | |
onDelete={onDelete} | |
title="Conversation 1" | |
selectedRepository={null} | |
lastUpdatedAt="2021-10-01T12:00:00Z" | |
/>, | |
); | |
await user.click(ellipsisButton); | |
const newMenu = await screen.findByTestId("context-menu"); | |
expect( | |
within(newMenu).queryByTestId("edit-button"), | |
).not.toBeInTheDocument(); | |
expect(within(newMenu).queryByTestId("delete-button")).toBeInTheDocument(); | |
}); | |
it("should not render the ellipsis button if there are no actions", () => { | |
const { rerender } = renderWithProviders( | |
<ConversationCard | |
onClick={onClick} | |
onDelete={onDelete} | |
onChangeTitle={onChangeTitle} | |
title="Conversation 1" | |
selectedRepository={null} | |
lastUpdatedAt="2021-10-01T12:00:00Z" | |
/>, | |
); | |
expect(screen.getByTestId("ellipsis-button")).toBeInTheDocument(); | |
rerender( | |
<ConversationCard | |
onClick={onClick} | |
onDelete={onDelete} | |
title="Conversation 1" | |
selectedRepository={null} | |
lastUpdatedAt="2021-10-01T12:00:00Z" | |
/>, | |
); | |
expect(screen.getByTestId("ellipsis-button")).toBeInTheDocument(); | |
rerender( | |
<ConversationCard | |
onClick={onClick} | |
title="Conversation 1" | |
selectedRepository={null} | |
lastUpdatedAt="2021-10-01T12:00:00Z" | |
/>, | |
); | |
expect(screen.queryByTestId("ellipsis-button")).not.toBeInTheDocument(); | |
}); | |
describe("state indicator", () => { | |
it("should render the 'STOPPED' indicator by default", () => { | |
renderWithProviders( | |
<ConversationCard | |
onDelete={onDelete} | |
isActive | |
onChangeTitle={onChangeTitle} | |
title="Conversation 1" | |
selectedRepository={null} | |
lastUpdatedAt="2021-10-01T12:00:00Z" | |
/>, | |
); | |
screen.getByTestId("STOPPED-indicator"); | |
}); | |
it("should render the other indicators when provided", () => { | |
renderWithProviders( | |
<ConversationCard | |
onDelete={onDelete} | |
isActive | |
onChangeTitle={onChangeTitle} | |
title="Conversation 1" | |
selectedRepository={null} | |
lastUpdatedAt="2021-10-01T12:00:00Z" | |
status="RUNNING" | |
/>, | |
); | |
expect(screen.queryByTestId("STOPPED-indicator")).not.toBeInTheDocument(); | |
screen.getByTestId("RUNNING-indicator"); | |
}); | |
}); | |
}); | |