Spaces:
Build error
Build error
OpenHands
/
frontend
/__tests__
/components
/features
/conversation-panel
/conversation-panel.test.tsx
import { screen, waitFor, within } from "@testing-library/react"; | |
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; | |
import { QueryClientConfig } from "@tanstack/react-query"; | |
import userEvent from "@testing-library/user-event"; | |
import { createRoutesStub } from "react-router"; | |
import React from "react"; | |
import { renderWithProviders } from "test-utils"; | |
import { ConversationPanel } from "#/components/features/conversation-panel/conversation-panel"; | |
import OpenHands from "#/api/open-hands"; | |
import { Conversation } from "#/api/open-hands.types"; | |
describe("ConversationPanel", () => { | |
const onCloseMock = vi.fn(); | |
const RouterStub = createRoutesStub([ | |
{ | |
Component: () => <ConversationPanel onClose={onCloseMock} />, | |
path: "/", | |
}, | |
]); | |
const renderConversationPanel = (config?: QueryClientConfig) => | |
renderWithProviders(<RouterStub />, { | |
preloadedState: { | |
metrics: { | |
cost: null, | |
usage: null, | |
}, | |
}, | |
}); | |
beforeAll(() => { | |
vi.mock("react-router", async (importOriginal) => ({ | |
...(await importOriginal<typeof import("react-router")>()), | |
Link: ({ children }: React.PropsWithChildren) => children, | |
useNavigate: vi.fn(() => vi.fn()), | |
useLocation: vi.fn(() => ({ pathname: "/conversation" })), | |
useParams: vi.fn(() => ({ conversationId: "2" })), | |
})); | |
}); | |
const mockConversations: Conversation[] = [ | |
{ | |
conversation_id: "1", | |
title: "Conversation 1", | |
selected_repository: null, | |
git_provider: null, | |
selected_branch: null, | |
last_updated_at: "2021-10-01T12:00:00Z", | |
created_at: "2021-10-01T12:00:00Z", | |
status: "STOPPED" as const, | |
url: null, | |
session_api_key: null, | |
}, | |
{ | |
conversation_id: "2", | |
title: "Conversation 2", | |
selected_repository: null, | |
git_provider: null, | |
selected_branch: null, | |
last_updated_at: "2021-10-02T12:00:00Z", | |
created_at: "2021-10-02T12:00:00Z", | |
status: "STOPPED" as const, | |
url: null, | |
session_api_key: null, | |
}, | |
{ | |
conversation_id: "3", | |
title: "Conversation 3", | |
selected_repository: null, | |
git_provider: null, | |
selected_branch: null, | |
last_updated_at: "2021-10-03T12:00:00Z", | |
created_at: "2021-10-03T12:00:00Z", | |
status: "STOPPED" as const, | |
url: null, | |
session_api_key: null, | |
}, | |
]; | |
beforeEach(() => { | |
vi.clearAllMocks(); | |
vi.restoreAllMocks(); | |
// Setup default mock for getUserConversations | |
vi.spyOn(OpenHands, "getUserConversations").mockResolvedValue([ | |
...mockConversations, | |
]); | |
}); | |
it("should render the conversations", async () => { | |
renderConversationPanel(); | |
const cards = await screen.findAllByTestId("conversation-card"); | |
// NOTE that we filter out conversations that don't have a created_at property | |
// (mock data has 4 conversations, but only 3 have a created_at property) | |
expect(cards).toHaveLength(3); | |
}); | |
it("should display an empty state when there are no conversations", async () => { | |
const getUserConversationsSpy = vi.spyOn(OpenHands, "getUserConversations"); | |
getUserConversationsSpy.mockResolvedValue([]); | |
renderConversationPanel(); | |
const emptyState = await screen.findByText("CONVERSATION$NO_CONVERSATIONS"); | |
expect(emptyState).toBeInTheDocument(); | |
}); | |
it("should handle an error when fetching conversations", async () => { | |
const getUserConversationsSpy = vi.spyOn(OpenHands, "getUserConversations"); | |
getUserConversationsSpy.mockRejectedValue( | |
new Error("Failed to fetch conversations"), | |
); | |
renderConversationPanel(); | |
const error = await screen.findByText("Failed to fetch conversations"); | |
expect(error).toBeInTheDocument(); | |
}); | |
it("should cancel deleting a conversation", async () => { | |
const user = userEvent.setup(); | |
renderConversationPanel(); | |
let cards = await screen.findAllByTestId("conversation-card"); | |
expect( | |
within(cards[0]).queryByTestId("delete-button"), | |
).not.toBeInTheDocument(); | |
const ellipsisButton = within(cards[0]).getByTestId("ellipsis-button"); | |
await user.click(ellipsisButton); | |
const deleteButton = screen.getByTestId("delete-button"); | |
// Click the first delete button | |
await user.click(deleteButton); | |
// Cancel the deletion | |
const cancelButton = screen.getByRole("button", { name: /cancel/i }); | |
await user.click(cancelButton); | |
expect( | |
screen.queryByRole("button", { name: /cancel/i }), | |
).not.toBeInTheDocument(); | |
// Ensure the conversation is not deleted | |
cards = await screen.findAllByTestId("conversation-card"); | |
expect(cards).toHaveLength(3); | |
}); | |
it("should delete a conversation", async () => { | |
const user = userEvent.setup(); | |
const mockData: Conversation[] = [ | |
{ | |
conversation_id: "1", | |
title: "Conversation 1", | |
selected_repository: null, | |
git_provider: null, | |
selected_branch: null, | |
last_updated_at: "2021-10-01T12:00:00Z", | |
created_at: "2021-10-01T12:00:00Z", | |
status: "STOPPED" as const, | |
url: null, | |
session_api_key: null, | |
}, | |
{ | |
conversation_id: "2", | |
title: "Conversation 2", | |
selected_repository: null, | |
git_provider: null, | |
selected_branch: null, | |
last_updated_at: "2021-10-02T12:00:00Z", | |
created_at: "2021-10-02T12:00:00Z", | |
status: "STOPPED" as const, | |
url: null, | |
session_api_key: null, | |
}, | |
{ | |
conversation_id: "3", | |
title: "Conversation 3", | |
selected_repository: null, | |
git_provider: null, | |
selected_branch: null, | |
last_updated_at: "2021-10-03T12:00:00Z", | |
created_at: "2021-10-03T12:00:00Z", | |
status: "STOPPED" as const, | |
url: null, | |
session_api_key: null, | |
}, | |
]; | |
const getUserConversationsSpy = vi.spyOn(OpenHands, "getUserConversations"); | |
getUserConversationsSpy.mockImplementation(async () => mockData); | |
const deleteUserConversationSpy = vi.spyOn( | |
OpenHands, | |
"deleteUserConversation", | |
); | |
deleteUserConversationSpy.mockImplementation(async (id: string) => { | |
const index = mockData.findIndex((conv) => conv.conversation_id === id); | |
if (index !== -1) { | |
mockData.splice(index, 1); | |
} | |
}); | |
renderConversationPanel(); | |
const cards = await screen.findAllByTestId("conversation-card"); | |
expect(cards).toHaveLength(3); | |
const ellipsisButton = within(cards[0]).getByTestId("ellipsis-button"); | |
await user.click(ellipsisButton); | |
const deleteButton = screen.getByTestId("delete-button"); | |
// Click the first delete button | |
await user.click(deleteButton); | |
// Confirm the deletion | |
const confirmButton = screen.getByRole("button", { name: /confirm/i }); | |
await user.click(confirmButton); | |
expect( | |
screen.queryByRole("button", { name: /confirm/i }), | |
).not.toBeInTheDocument(); | |
// Wait for the cards to update | |
await waitFor(() => { | |
const updatedCards = screen.getAllByTestId("conversation-card"); | |
expect(updatedCards).toHaveLength(2); | |
}); | |
}); | |
it("should call onClose after clicking a card", async () => { | |
const user = userEvent.setup(); | |
renderConversationPanel(); | |
const cards = await screen.findAllByTestId("conversation-card"); | |
const firstCard = cards[1]; | |
await user.click(firstCard); | |
expect(onCloseMock).toHaveBeenCalledOnce(); | |
}); | |
it("should refetch data on rerenders", async () => { | |
const user = userEvent.setup(); | |
const getUserConversationsSpy = vi.spyOn(OpenHands, "getUserConversations"); | |
getUserConversationsSpy.mockResolvedValue([...mockConversations]); | |
function PanelWithToggle() { | |
const [isOpen, setIsOpen] = React.useState(true); | |
return ( | |
<> | |
<button type="button" onClick={() => setIsOpen((prev) => !prev)}> | |
Toggle | |
</button> | |
{isOpen && <ConversationPanel onClose={onCloseMock} />} | |
</> | |
); | |
} | |
const MyRouterStub = createRoutesStub([ | |
{ | |
Component: PanelWithToggle, | |
path: "/", | |
}, | |
]); | |
renderWithProviders(<MyRouterStub />, { | |
preloadedState: { | |
metrics: { | |
cost: null, | |
usage: null, | |
}, | |
}, | |
}); | |
const toggleButton = screen.getByText("Toggle"); | |
// Initial render | |
const cards = await screen.findAllByTestId("conversation-card"); | |
expect(cards).toHaveLength(3); | |
// Toggle off | |
await user.click(toggleButton); | |
expect(screen.queryByTestId("conversation-card")).not.toBeInTheDocument(); | |
// Toggle on | |
await user.click(toggleButton); | |
const newCards = await screen.findAllByTestId("conversation-card"); | |
expect(newCards).toHaveLength(3); | |
}); | |
}); | |