chat-interface.test.tsx 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327
  1. import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
  2. import { act, screen, waitFor, within } from "@testing-library/react";
  3. import userEvent from "@testing-library/user-event";
  4. import { renderWithProviders } from "test-utils";
  5. import { ChatInterface } from "#/components/chat-interface";
  6. import { addUserMessage } from "#/state/chatSlice";
  7. import { SUGGESTIONS } from "#/utils/suggestions";
  8. import * as ChatSlice from "#/state/chatSlice";
  9. // eslint-disable-next-line @typescript-eslint/no-unused-vars
  10. const renderChatInterface = (messages: (Message | ErrorMessage)[]) =>
  11. renderWithProviders(<ChatInterface />);
  12. describe("Empty state", () => {
  13. const { send: sendMock } = vi.hoisted(() => ({
  14. send: vi.fn(),
  15. }));
  16. const { useWsClient: useWsClientMock } = vi.hoisted(() => ({
  17. useWsClient: vi.fn(() => ({ send: sendMock, runtimeActive: true })),
  18. }));
  19. beforeAll(() => {
  20. vi.mock("@remix-run/react", async (importActual) => ({
  21. ...(await importActual<typeof import("@remix-run/react")>()),
  22. useRouteLoaderData: vi.fn(() => ({})),
  23. }));
  24. vi.mock("#/context/socket", async (importActual) => ({
  25. ...(await importActual<typeof import("#/context/ws-client-provider")>()),
  26. useWsClient: useWsClientMock,
  27. }));
  28. });
  29. afterEach(() => {
  30. vi.clearAllMocks();
  31. });
  32. it("should render suggestions if empty", () => {
  33. const { store } = renderWithProviders(<ChatInterface />, {
  34. preloadedState: {
  35. chat: { messages: [] },
  36. },
  37. });
  38. expect(screen.getByTestId("suggestions")).toBeInTheDocument();
  39. act(() => {
  40. store.dispatch(
  41. addUserMessage({
  42. content: "Hello",
  43. imageUrls: [],
  44. timestamp: new Date().toISOString(),
  45. }),
  46. );
  47. });
  48. expect(screen.queryByTestId("suggestions")).not.toBeInTheDocument();
  49. });
  50. it("should render the default suggestions", () => {
  51. renderWithProviders(<ChatInterface />, {
  52. preloadedState: {
  53. chat: { messages: [] },
  54. },
  55. });
  56. const suggestions = screen.getByTestId("suggestions");
  57. const repoSuggestions = Object.keys(SUGGESTIONS.repo);
  58. // check that there are at most 4 suggestions displayed
  59. const displayedSuggestions = within(suggestions).getAllByRole("button");
  60. expect(displayedSuggestions.length).toBeLessThanOrEqual(4);
  61. // Check that each displayed suggestion is one of the repo suggestions
  62. displayedSuggestions.forEach((suggestion) => {
  63. expect(repoSuggestions).toContain(suggestion.textContent);
  64. });
  65. });
  66. it.fails(
  67. "should load the a user message to the input when selecting",
  68. async () => {
  69. // this is to test that the message is in the UI before the socket is called
  70. useWsClientMock.mockImplementation(() => ({
  71. send: sendMock,
  72. runtimeActive: false, // mock an inactive runtime setup
  73. }));
  74. const addUserMessageSpy = vi.spyOn(ChatSlice, "addUserMessage");
  75. const user = userEvent.setup();
  76. const { store } = renderWithProviders(<ChatInterface />, {
  77. preloadedState: {
  78. chat: { messages: [] },
  79. },
  80. });
  81. const suggestions = screen.getByTestId("suggestions");
  82. const displayedSuggestions = within(suggestions).getAllByRole("button");
  83. const input = screen.getByTestId("chat-input");
  84. await user.click(displayedSuggestions[0]);
  85. // user message loaded to input
  86. expect(addUserMessageSpy).not.toHaveBeenCalled();
  87. expect(screen.queryByTestId("suggestions")).toBeInTheDocument();
  88. expect(store.getState().chat.messages).toHaveLength(0);
  89. expect(input).toHaveValue(displayedSuggestions[0].textContent);
  90. },
  91. );
  92. it.fails(
  93. "should send the message to the socket only if the runtime is active",
  94. async () => {
  95. useWsClientMock.mockImplementation(() => ({
  96. send: sendMock,
  97. runtimeActive: false, // mock an inactive runtime setup
  98. }));
  99. const user = userEvent.setup();
  100. const { rerender } = renderWithProviders(<ChatInterface />, {
  101. preloadedState: {
  102. chat: { messages: [] },
  103. },
  104. });
  105. const suggestions = screen.getByTestId("suggestions");
  106. const displayedSuggestions = within(suggestions).getAllByRole("button");
  107. await user.click(displayedSuggestions[0]);
  108. expect(sendMock).not.toHaveBeenCalled();
  109. useWsClientMock.mockImplementation(() => ({
  110. send: sendMock,
  111. runtimeActive: true, // mock an active runtime setup
  112. }));
  113. rerender(<ChatInterface />);
  114. await waitFor(() =>
  115. expect(sendMock).toHaveBeenCalledWith(expect.any(String)),
  116. );
  117. },
  118. );
  119. });
  120. describe.skip("ChatInterface", () => {
  121. beforeAll(() => {
  122. // mock useScrollToBottom hook
  123. vi.mock("#/hooks/useScrollToBottom", () => ({
  124. useScrollToBottom: vi.fn(() => ({
  125. scrollDomToBottom: vi.fn(),
  126. onChatBodyScroll: vi.fn(),
  127. hitBottom: vi.fn(),
  128. })),
  129. }));
  130. });
  131. afterEach(() => {
  132. vi.clearAllMocks();
  133. });
  134. it("should render messages", () => {
  135. const messages: Message[] = [
  136. {
  137. sender: "user",
  138. content: "Hello",
  139. imageUrls: [],
  140. timestamp: new Date().toISOString(),
  141. },
  142. {
  143. sender: "assistant",
  144. content: "Hi",
  145. imageUrls: [],
  146. timestamp: new Date().toISOString(),
  147. },
  148. ];
  149. renderChatInterface(messages);
  150. expect(screen.getAllByTestId(/-message/)).toHaveLength(2);
  151. });
  152. it("should render a chat input", () => {
  153. const messages: Message[] = [];
  154. renderChatInterface(messages);
  155. expect(screen.getByTestId("chat-input")).toBeInTheDocument();
  156. });
  157. it.todo("should call socket send when submitting a message", async () => {
  158. const user = userEvent.setup();
  159. const messages: Message[] = [];
  160. renderChatInterface(messages);
  161. const input = screen.getByTestId("chat-input");
  162. await user.type(input, "Hello");
  163. await user.keyboard("{Enter}");
  164. // spy on send and expect to have been called
  165. });
  166. it("should render an image carousel with a message", () => {
  167. let messages: Message[] = [
  168. {
  169. sender: "assistant",
  170. content: "Here are some images",
  171. imageUrls: [],
  172. timestamp: new Date().toISOString(),
  173. },
  174. ];
  175. const { rerender } = renderChatInterface(messages);
  176. expect(screen.queryByTestId("image-carousel")).not.toBeInTheDocument();
  177. messages = [
  178. {
  179. sender: "assistant",
  180. content: "Here are some images",
  181. imageUrls: ["image1", "image2"],
  182. timestamp: new Date().toISOString(),
  183. },
  184. ];
  185. rerender(<ChatInterface />);
  186. const imageCarousel = screen.getByTestId("image-carousel");
  187. expect(imageCarousel).toBeInTheDocument();
  188. expect(within(imageCarousel).getAllByTestId("image-preview")).toHaveLength(
  189. 2,
  190. );
  191. });
  192. it.todo("should render confirmation buttons");
  193. it("should render a 'continue' action when there are more than 2 messages and awaiting user input", () => {
  194. const messages: Message[] = [
  195. {
  196. sender: "assistant",
  197. content: "Hello",
  198. imageUrls: [],
  199. timestamp: new Date().toISOString(),
  200. },
  201. {
  202. sender: "user",
  203. content: "Hi",
  204. imageUrls: [],
  205. timestamp: new Date().toISOString(),
  206. },
  207. ];
  208. const { rerender } = renderChatInterface(messages);
  209. expect(
  210. screen.queryByTestId("continue-action-button"),
  211. ).not.toBeInTheDocument();
  212. messages.push({
  213. sender: "assistant",
  214. content: "How can I help you?",
  215. imageUrls: [],
  216. timestamp: new Date().toISOString(),
  217. });
  218. rerender(<ChatInterface />);
  219. expect(screen.getByTestId("continue-action-button")).toBeInTheDocument();
  220. });
  221. it("should render inline errors", () => {
  222. const messages: (Message | ErrorMessage)[] = [
  223. {
  224. sender: "assistant",
  225. content: "Hello",
  226. imageUrls: [],
  227. timestamp: new Date().toISOString(),
  228. },
  229. {
  230. error: true,
  231. id: "",
  232. message: "Something went wrong",
  233. },
  234. ];
  235. renderChatInterface(messages);
  236. const error = screen.getByTestId("error-message");
  237. expect(within(error).getByText("Something went wrong")).toBeInTheDocument();
  238. });
  239. it("should render feedback actions if there are more than 3 messages", () => {
  240. const messages: Message[] = [
  241. {
  242. sender: "assistant",
  243. content: "Hello",
  244. imageUrls: [],
  245. timestamp: new Date().toISOString(),
  246. },
  247. {
  248. sender: "user",
  249. content: "Hi",
  250. imageUrls: [],
  251. timestamp: new Date().toISOString(),
  252. },
  253. {
  254. sender: "assistant",
  255. content: "How can I help you?",
  256. imageUrls: [],
  257. timestamp: new Date().toISOString(),
  258. },
  259. ];
  260. const { rerender } = renderChatInterface(messages);
  261. expect(screen.queryByTestId("feedback-actions")).not.toBeInTheDocument();
  262. messages.push({
  263. sender: "user",
  264. content: "I need help",
  265. imageUrls: [],
  266. timestamp: new Date().toISOString(),
  267. });
  268. rerender(<ChatInterface />);
  269. expect(screen.getByTestId("feedback-actions")).toBeInTheDocument();
  270. });
  271. describe("feedback", () => {
  272. it.todo("should open the feedback modal when a feedback action is clicked");
  273. it.todo(
  274. "should submit feedback and hide the actions when feedback is shared",
  275. );
  276. it.todo("should render the actions once more after new messages are added");
  277. });
  278. });