chat-interface.test.tsx 9.2 KB

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