Spaces:
Build error
Build error
| import { afterEach, beforeAll, describe, expect, it, vi } from "vitest"; | |
| import { screen, waitFor, within } from "@testing-library/react"; | |
| import userEvent from "@testing-library/user-event"; | |
| import { renderWithProviders } from "test-utils"; | |
| import type { Message } from "#/message"; | |
| import { SUGGESTIONS } from "#/utils/suggestions"; | |
| import { WsClientProviderStatus } from "#/context/ws-client-provider"; | |
| import { ChatInterface } from "#/components/features/chat/chat-interface"; | |
| // eslint-disable-next-line @typescript-eslint/no-unused-vars | |
| const renderChatInterface = (messages: Message[]) => | |
| renderWithProviders(<ChatInterface />); | |
| describe("Empty state", () => { | |
| const { send: sendMock } = vi.hoisted(() => ({ | |
| send: vi.fn(), | |
| })); | |
| const { useWsClient: useWsClientMock } = vi.hoisted(() => ({ | |
| useWsClient: vi.fn(() => ({ | |
| send: sendMock, | |
| status: WsClientProviderStatus.CONNECTED, | |
| isLoadingMessages: false, | |
| })), | |
| })); | |
| beforeAll(() => { | |
| vi.mock("react-router", async (importActual) => ({ | |
| ...(await importActual<typeof import("react-router")>()), | |
| useRouteLoaderData: vi.fn(() => ({})), | |
| })); | |
| vi.mock("#/context/socket", async (importActual) => ({ | |
| ...(await importActual<typeof import("#/context/ws-client-provider")>()), | |
| useWsClient: useWsClientMock, | |
| })); | |
| }); | |
| afterEach(() => { | |
| vi.clearAllMocks(); | |
| }); | |
| it.todo("should render suggestions if empty"); | |
| it("should render the default suggestions", () => { | |
| renderWithProviders(<ChatInterface />); | |
| const suggestions = screen.getByTestId("suggestions"); | |
| const repoSuggestions = Object.keys(SUGGESTIONS.repo); | |
| // check that there are at most 4 suggestions displayed | |
| const displayedSuggestions = within(suggestions).getAllByRole("button"); | |
| expect(displayedSuggestions.length).toBeLessThanOrEqual(4); | |
| // Check that each displayed suggestion is one of the repo suggestions | |
| displayedSuggestions.forEach((suggestion) => { | |
| expect(repoSuggestions).toContain(suggestion.textContent); | |
| }); | |
| }); | |
| it.fails( | |
| "should load the a user message to the input when selecting", | |
| async () => { | |
| // this is to test that the message is in the UI before the socket is called | |
| useWsClientMock.mockImplementation(() => ({ | |
| send: sendMock, | |
| status: WsClientProviderStatus.CONNECTED, | |
| isLoadingMessages: false, | |
| })); | |
| const user = userEvent.setup(); | |
| renderWithProviders(<ChatInterface />); | |
| const suggestions = screen.getByTestId("suggestions"); | |
| const displayedSuggestions = within(suggestions).getAllByRole("button"); | |
| const input = screen.getByTestId("chat-input"); | |
| await user.click(displayedSuggestions[0]); | |
| // user message loaded to input | |
| expect(screen.queryByTestId("suggestions")).toBeInTheDocument(); | |
| expect(input).toHaveValue(displayedSuggestions[0].textContent); | |
| }, | |
| ); | |
| it.fails( | |
| "should send the message to the socket only if the runtime is active", | |
| async () => { | |
| useWsClientMock.mockImplementation(() => ({ | |
| send: sendMock, | |
| status: WsClientProviderStatus.CONNECTED, | |
| isLoadingMessages: false, | |
| })); | |
| const user = userEvent.setup(); | |
| const { rerender } = renderWithProviders(<ChatInterface />); | |
| const suggestions = screen.getByTestId("suggestions"); | |
| const displayedSuggestions = within(suggestions).getAllByRole("button"); | |
| await user.click(displayedSuggestions[0]); | |
| expect(sendMock).not.toHaveBeenCalled(); | |
| useWsClientMock.mockImplementation(() => ({ | |
| send: sendMock, | |
| status: WsClientProviderStatus.CONNECTED, | |
| isLoadingMessages: false, | |
| })); | |
| rerender(<ChatInterface />); | |
| await waitFor(() => | |
| expect(sendMock).toHaveBeenCalledWith(expect.any(String)), | |
| ); | |
| }, | |
| ); | |
| }); | |
| describe.skip("ChatInterface", () => { | |
| beforeAll(() => { | |
| // mock useScrollToBottom hook | |
| vi.mock("#/hooks/useScrollToBottom", () => ({ | |
| useScrollToBottom: vi.fn(() => ({ | |
| scrollDomToBottom: vi.fn(), | |
| onChatBodyScroll: vi.fn(), | |
| hitBottom: vi.fn(), | |
| })), | |
| })); | |
| }); | |
| afterEach(() => { | |
| vi.clearAllMocks(); | |
| }); | |
| it("should render messages", () => { | |
| const messages: Message[] = [ | |
| { | |
| sender: "user", | |
| content: "Hello", | |
| imageUrls: [], | |
| timestamp: new Date().toISOString(), | |
| pending: true, | |
| }, | |
| { | |
| sender: "assistant", | |
| content: "Hi", | |
| imageUrls: [], | |
| timestamp: new Date().toISOString(), | |
| pending: true, | |
| }, | |
| ]; | |
| renderChatInterface(messages); | |
| expect(screen.getAllByTestId(/-message/)).toHaveLength(2); | |
| }); | |
| it("should render a chat input", () => { | |
| const messages: Message[] = []; | |
| renderChatInterface(messages); | |
| expect(screen.getByTestId("chat-input")).toBeInTheDocument(); | |
| }); | |
| it("should call socket send when submitting a message", async () => { | |
| const user = userEvent.setup(); | |
| const messages: Message[] = []; | |
| renderChatInterface(messages); | |
| const input = screen.getByTestId("chat-input"); | |
| await user.type(input, "Hello"); | |
| await user.keyboard("{Enter}"); | |
| // spy on send and expect to have been called | |
| }); | |
| it("should render an image carousel with a message", () => { | |
| let messages: Message[] = [ | |
| { | |
| sender: "assistant", | |
| content: "Here are some images", | |
| imageUrls: [], | |
| timestamp: new Date().toISOString(), | |
| pending: true, | |
| }, | |
| ]; | |
| const { rerender } = renderChatInterface(messages); | |
| expect(screen.queryByTestId("image-carousel")).not.toBeInTheDocument(); | |
| messages = [ | |
| { | |
| sender: "assistant", | |
| content: "Here are some images", | |
| imageUrls: ["image1", "image2"], | |
| timestamp: new Date().toISOString(), | |
| pending: true, | |
| }, | |
| ]; | |
| rerender(<ChatInterface />); | |
| const imageCarousel = screen.getByTestId("image-carousel"); | |
| expect(imageCarousel).toBeInTheDocument(); | |
| expect(within(imageCarousel).getAllByTestId("image-preview")).toHaveLength( | |
| 2, | |
| ); | |
| }); | |
| it("should render a 'continue' action when there are more than 2 messages and awaiting user input", () => { | |
| const messages: Message[] = [ | |
| { | |
| sender: "assistant", | |
| content: "Hello", | |
| imageUrls: [], | |
| timestamp: new Date().toISOString(), | |
| pending: true, | |
| }, | |
| { | |
| sender: "user", | |
| content: "Hi", | |
| imageUrls: [], | |
| timestamp: new Date().toISOString(), | |
| pending: true, | |
| }, | |
| ]; | |
| const { rerender } = renderChatInterface(messages); | |
| expect( | |
| screen.queryByTestId("continue-action-button"), | |
| ).not.toBeInTheDocument(); | |
| messages.push({ | |
| sender: "assistant", | |
| content: "How can I help you?", | |
| imageUrls: [], | |
| timestamp: new Date().toISOString(), | |
| pending: true, | |
| }); | |
| rerender(<ChatInterface />); | |
| expect(screen.getByTestId("continue-action-button")).toBeInTheDocument(); | |
| }); | |
| it("should render inline errors", () => { | |
| const messages: Message[] = [ | |
| { | |
| sender: "assistant", | |
| content: "Hello", | |
| imageUrls: [], | |
| timestamp: new Date().toISOString(), | |
| pending: true, | |
| }, | |
| { | |
| type: "error", | |
| content: "Something went wrong", | |
| sender: "assistant", | |
| timestamp: new Date().toISOString(), | |
| }, | |
| ]; | |
| renderChatInterface(messages); | |
| const error = screen.getByTestId("error-message"); | |
| expect(within(error).getByText("Something went wrong")).toBeInTheDocument(); | |
| }); | |
| it("should render both GitHub buttons initially when ghToken is available", () => { | |
| vi.mock("react-router", async (importActual) => ({ | |
| ...(await importActual<typeof import("react-router")>()), | |
| useRouteLoaderData: vi.fn(() => ({ ghToken: "test-token" })), | |
| })); | |
| const messages: Message[] = [ | |
| { | |
| sender: "assistant", | |
| content: "Hello", | |
| imageUrls: [], | |
| timestamp: new Date().toISOString(), | |
| pending: true, | |
| }, | |
| ]; | |
| renderChatInterface(messages); | |
| const pushButton = screen.getByRole("button", { name: "Push to Branch" }); | |
| const prButton = screen.getByRole("button", { name: "Push & Create PR" }); | |
| expect(pushButton).toBeInTheDocument(); | |
| expect(prButton).toBeInTheDocument(); | |
| expect(pushButton).toHaveTextContent("Push to Branch"); | |
| expect(prButton).toHaveTextContent("Push & Create PR"); | |
| }); | |
| it("should render only 'Push changes to PR' button after PR is created", async () => { | |
| vi.mock("react-router", async (importActual) => ({ | |
| ...(await importActual<typeof import("react-router")>()), | |
| useRouteLoaderData: vi.fn(() => ({ ghToken: "test-token" })), | |
| })); | |
| const messages: Message[] = [ | |
| { | |
| sender: "assistant", | |
| content: "Hello", | |
| imageUrls: [], | |
| timestamp: new Date().toISOString(), | |
| pending: true, | |
| }, | |
| ]; | |
| const { rerender } = renderChatInterface(messages); | |
| const user = userEvent.setup(); | |
| // Click the "Push & Create PR" button | |
| const prButton = screen.getByRole("button", { name: "Push & Create PR" }); | |
| await user.click(prButton); | |
| // Re-render to trigger state update | |
| rerender(<ChatInterface />); | |
| // Verify only one button is shown | |
| const pushToPrButton = screen.getByRole("button", { | |
| name: "Push changes to PR", | |
| }); | |
| expect(pushToPrButton).toBeInTheDocument(); | |
| expect( | |
| screen.queryByRole("button", { name: "Push to Branch" }), | |
| ).not.toBeInTheDocument(); | |
| expect( | |
| screen.queryByRole("button", { name: "Push & Create PR" }), | |
| ).not.toBeInTheDocument(); | |
| }); | |
| it("should render feedback actions if there are more than 3 messages", () => { | |
| const messages: Message[] = [ | |
| { | |
| sender: "assistant", | |
| content: "Hello", | |
| imageUrls: [], | |
| timestamp: new Date().toISOString(), | |
| pending: true, | |
| }, | |
| { | |
| sender: "user", | |
| content: "Hi", | |
| imageUrls: [], | |
| timestamp: new Date().toISOString(), | |
| pending: true, | |
| }, | |
| { | |
| sender: "assistant", | |
| content: "How can I help you?", | |
| imageUrls: [], | |
| timestamp: new Date().toISOString(), | |
| pending: true, | |
| }, | |
| ]; | |
| const { rerender } = renderChatInterface(messages); | |
| expect(screen.queryByTestId("feedback-actions")).not.toBeInTheDocument(); | |
| messages.push({ | |
| sender: "user", | |
| content: "I need help", | |
| imageUrls: [], | |
| timestamp: new Date().toISOString(), | |
| pending: true, | |
| }); | |
| rerender(<ChatInterface />); | |
| expect(screen.getByTestId("feedback-actions")).toBeInTheDocument(); | |
| }); | |
| }); | |