ChatMessage.test.tsx 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  1. import { fireEvent, render, screen, within } from "@testing-library/react";
  2. import { describe, it, expect, vi } from "vitest";
  3. import userEvent from "@testing-library/user-event";
  4. import toast from "#/utils/toast";
  5. import ChatMessage from "#/components/chat/ChatMessage";
  6. describe("Message", () => {
  7. it("should render a user message", () => {
  8. render(
  9. <ChatMessage
  10. message={{
  11. sender: "user",
  12. content: "Hello",
  13. imageUrls: [],
  14. timestamp: new Date().toISOString(),
  15. }}
  16. isLastMessage={false}
  17. />,
  18. );
  19. expect(screen.getByTestId("article")).toBeInTheDocument();
  20. expect(screen.getByTestId("article")).toHaveClass("self-end"); // user message should be on the right side
  21. });
  22. it("should render an assistant message", () => {
  23. render(
  24. <ChatMessage
  25. message={{
  26. sender: "assistant",
  27. content: "Hi",
  28. imageUrls: [],
  29. timestamp: new Date().toISOString(),
  30. }}
  31. isLastMessage={false}
  32. />,
  33. );
  34. expect(screen.getByTestId("article")).toBeInTheDocument();
  35. expect(screen.getByTestId("article")).not.toHaveClass("self-end"); // assistant message should be on the left side
  36. });
  37. it("should render markdown content", () => {
  38. render(
  39. <ChatMessage
  40. message={{
  41. sender: "user",
  42. content: "```js\nconsole.log('Hello')\n```",
  43. imageUrls: [],
  44. timestamp: new Date().toISOString(),
  45. }}
  46. isLastMessage={false}
  47. />,
  48. );
  49. // SyntaxHighlighter breaks the code blocks into "tokens"
  50. expect(screen.getByText("console")).toBeInTheDocument();
  51. expect(screen.getByText("log")).toBeInTheDocument();
  52. expect(screen.getByText("'Hello'")).toBeInTheDocument();
  53. });
  54. describe("copy to clipboard", () => {
  55. const toastInfoSpy = vi.spyOn(toast, "info");
  56. const toastErrorSpy = vi.spyOn(toast, "error");
  57. it("should copy any message to clipboard", async () => {
  58. const user = userEvent.setup();
  59. render(
  60. <ChatMessage
  61. message={{
  62. sender: "user",
  63. content: "Hello",
  64. imageUrls: [],
  65. timestamp: new Date().toISOString(),
  66. }}
  67. isLastMessage={false}
  68. />,
  69. );
  70. const message = screen.getByTestId("article");
  71. let copyButton = within(message).queryByTestId("copy-button");
  72. expect(copyButton).not.toBeInTheDocument();
  73. // I am using `fireEvent` here because `userEvent.hover()` seems to interfere with the
  74. // `userEvent.click()` call later on
  75. fireEvent.mouseEnter(message);
  76. copyButton = within(message).getByTestId("copy-button");
  77. await user.click(copyButton);
  78. expect(navigator.clipboard.readText()).resolves.toBe("Hello");
  79. expect(toastInfoSpy).toHaveBeenCalled();
  80. });
  81. it("should show an error message when the message cannot be copied", async () => {
  82. const user = userEvent.setup();
  83. render(
  84. <ChatMessage
  85. message={{
  86. sender: "user",
  87. content: "Hello",
  88. imageUrls: [],
  89. timestamp: new Date().toISOString(),
  90. }}
  91. isLastMessage={false}
  92. />,
  93. );
  94. const message = screen.getByTestId("article");
  95. fireEvent.mouseEnter(message);
  96. const copyButton = within(message).getByTestId("copy-button");
  97. const clipboardSpy = vi
  98. .spyOn(navigator.clipboard, "writeText")
  99. .mockRejectedValue(new Error("Failed to copy"));
  100. await user.click(copyButton);
  101. expect(clipboardSpy).toHaveBeenCalled();
  102. expect(toastErrorSpy).toHaveBeenCalled();
  103. });
  104. });
  105. describe("confirmation buttons", () => {
  106. const expectButtonsNotToBeRendered = () => {
  107. expect(
  108. screen.queryByTestId("action-confirm-button"),
  109. ).not.toBeInTheDocument();
  110. expect(
  111. screen.queryByTestId("action-reject-button"),
  112. ).not.toBeInTheDocument();
  113. };
  114. it.skip("should display confirmation buttons for the last assistant message", () => {
  115. // it should not render buttons if the message is not the last one
  116. const { rerender } = render(
  117. <ChatMessage
  118. message={{
  119. sender: "assistant",
  120. content: "Are you sure?",
  121. imageUrls: [],
  122. timestamp: new Date().toISOString(),
  123. }}
  124. isLastMessage={false}
  125. awaitingUserConfirmation
  126. />,
  127. );
  128. expectButtonsNotToBeRendered();
  129. // it should not render buttons if the message is not from the assistant
  130. rerender(
  131. <ChatMessage
  132. message={{
  133. sender: "user",
  134. content: "Yes",
  135. imageUrls: [],
  136. timestamp: new Date().toISOString(),
  137. }}
  138. isLastMessage
  139. awaitingUserConfirmation
  140. />,
  141. );
  142. expectButtonsNotToBeRendered();
  143. // it should not render buttons if the message is not awaiting user confirmation
  144. rerender(
  145. <ChatMessage
  146. message={{
  147. sender: "assistant",
  148. content: "Are you sure?",
  149. imageUrls: [],
  150. timestamp: new Date().toISOString(),
  151. }}
  152. isLastMessage
  153. awaitingUserConfirmation={false}
  154. />,
  155. );
  156. expectButtonsNotToBeRendered();
  157. // it should render buttons if all conditions are met
  158. rerender(
  159. <ChatMessage
  160. message={{
  161. sender: "assistant",
  162. content: "Are you sure?",
  163. imageUrls: [],
  164. timestamp: new Date().toISOString(),
  165. }}
  166. isLastMessage
  167. awaitingUserConfirmation
  168. />,
  169. );
  170. const confirmButton = screen.getByTestId("action-confirm-button");
  171. const rejectButton = screen.getByTestId("action-reject-button");
  172. expect(confirmButton).toBeInTheDocument();
  173. expect(rejectButton).toBeInTheDocument();
  174. });
  175. });
  176. });