Frontend/test/chat (#496)

* refactor(<ChatPage/>)

* test(<ChatInput />): add unit tests

* test(<ChatMessages />): add unit tests
This commit is contained in:
Mamadou DICKO 2023-07-03 17:39:59 +02:00 committed by GitHub
parent 4f638544bb
commit 6acb13d4ae
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 207 additions and 40 deletions

View File

@ -0,0 +1,94 @@
import { fireEvent, render } from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest";
import { ChatInput } from "../index";
const addQuestionMock = vi.fn((...params: unknown[]) => ({ params }));
vi.mock("@/app/chat/[chatId]/hooks/useChat", () => ({
useChat: () => ({
addQuestion: (...params: unknown[]) => addQuestionMock(...params),
generatingAnswer: false,
}),
}));
afterEach(() => {
addQuestionMock.mockClear();
});
describe("ChatInput", () => {
it("should render correctly", () => {
// Rendering the ChatInput component
const { getByTestId, baseElement } = render(<ChatInput />);
console.log({ baseElement });
const chatInputForm = getByTestId("chat-input-form");
expect(chatInputForm).toBeDefined();
const chatInput = getByTestId("chat-input");
expect(chatInput).toBeDefined();
const submitButton = getByTestId("submit-button");
expect(submitButton).toBeDefined();
const configButton = getByTestId("config-button");
expect(configButton).toBeDefined();
const micButton = getByTestId("mic-button");
expect(micButton).toBeDefined();
});
it("should not call addQuestion on form submit when message is empty", () => {
const { getByTestId } = render(<ChatInput />);
const chatInputForm = getByTestId("chat-input-form");
fireEvent.submit(chatInputForm);
// Asserting that the addQuestion function was called with the expected arguments
expect(addQuestionMock).not.toHaveBeenCalled();
});
it("should call addQuestion once on form submit when message is not empty", () => {
const { getByTestId } = render(<ChatInput />);
const chatInput = getByTestId("chat-input");
fireEvent.change(chatInput, { target: { value: "Test question" } });
const chatInputForm = getByTestId("chat-input-form");
fireEvent.submit(chatInputForm);
// Asserting that the addQuestion function was called with the expected arguments
expect(addQuestionMock).toHaveBeenCalledTimes(1);
expect(addQuestionMock).toHaveBeenCalledWith(
"Test question",
expect.any(Function)
);
});
it('should submit a question when "Enter" key is pressed without shift', () => {
// Mocking the addQuestion function
// Rendering the ChatInput component with the mock function
const { getByTestId } = render(<ChatInput />);
const chatInput = getByTestId("chat-input");
fireEvent.change(chatInput, { target: { value: "Another test question" } });
fireEvent.keyDown(chatInput, { key: "Enter", shiftKey: false });
// Asserting that the addQuestion function was called with the expected arguments
expect(addQuestionMock).toHaveBeenCalledTimes(1);
expect(addQuestionMock).toHaveBeenCalledWith(
"Another test question",
expect.any(Function)
);
});
it('should not submit a question when "Enter" key is pressed with shift', () => {
const { getByTestId } = render(<ChatInput />);
const inputElement = getByTestId("chat-input");
fireEvent.change(inputElement, { target: { value: "Test question" } });
fireEvent.keyDown(inputElement, { key: "Enter", shiftKey: true });
expect(addQuestionMock).not.toHaveBeenCalled();
});
});

View File

@ -7,7 +7,11 @@ import Button from "@/lib/components/ui/Button";
export const ConfigButton = (): JSX.Element => {
return (
<Link href={"/config"}>
<Button className="p-2 sm:px-3" variant={"tertiary"}>
<Button
className="p-2 sm:px-3"
variant={"tertiary"}
data-testid="config-button"
>
<MdSettings className="text-lg sm:text-xl lg:text-2xl" />
</Button>
</Link>

View File

@ -21,6 +21,7 @@ export const MicButton = ({ setMessage }: MicButtonProps): JSX.Element => {
type="button"
onClick={startListening}
disabled={!speechSupported}
data-testid="mic-button"
>
{isListening ? (
<MdMicOff className="text-lg sm:text-xl lg:text-2xl" />

View File

@ -4,23 +4,26 @@ import Button from "@/lib/components/ui/Button";
import { useChat } from "@/app/chat/[chatId]/hooks/useChat";
import { useState } from "react";
import { ConfigButton } from "./ConfigButton";
import { MicButton } from "./MicButton";
import { ConfigButton } from "./components/ConfigButton";
import { MicButton } from "./components/MicButton";
export const ChatInput = (): JSX.Element => {
const [message, setMessage] = useState<string>(""); // for optimistic updates
const { addQuestion, generatingAnswer } = useChat();
const submitQuestion = () => {
addQuestion(message, () => setMessage(""));
if (message.length === 0) return;
if (!generatingAnswer) {
addQuestion(message, () => setMessage(""));
}
};
return (
<form
data-testid="chat-input-form"
onSubmit={(e) => {
e.preventDefault();
if (!generatingAnswer) {
submitQuestion();
}
submitQuestion();
}}
className="sticky bottom-0 p-5 bg-white dark:bg-black rounded-t-md border border-black/10 dark:border-white/25 border-b-0 w-full max-w-3xl flex items-center justify-center gap-2 z-20"
>
@ -30,21 +33,20 @@ export const ChatInput = (): JSX.Element => {
required
onChange={(e) => setMessage(e.target.value)}
onKeyDown={(e) => {
if (message.length === 0) return;
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault(); // Prevents the newline from being entered in the textarea
if (!generatingAnswer) {
submitQuestion();
}
submitQuestion();
}
}}
className="w-full p-2 border border-gray-300 dark:border-gray-500 outline-none rounded dark:bg-gray-800"
placeholder="Begin conversation here..."
data-testid="chat-input"
/>
<Button
className="px-3 py-2 sm:px-4 sm:py-2"
type="submit"
isLoading={generatingAnswer}
data-testid="submit-button"
>
{generatingAnswer ? "Thinking..." : "Chat"}
</Button>

View File

@ -0,0 +1,50 @@
import { render } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { ChatMessages } from "../index";
// Mocking the useChatMessages hook
vi.mock("../hooks/useChatMessages", () => ({
useChatMessages: vi.fn(() => ({
chatListRef: {
current: null,
},
})),
}));
const useChatContextMock = vi.fn(() => ({
history: [
{
assistant: "Test assistant message",
message_id: "123",
user_message: "Test user message",
},
],
}));
// Mocking the useChatContext hook
vi.mock("@/lib/context", () => ({
useChatContext: () => useChatContextMock(),
}));
describe("ChatMessages", () => {
it("should render chat messages correctly", () => {
const { getByText } = render(<ChatMessages />);
const userMessage = getByText("Test user message");
expect(userMessage).toBeDefined();
const assistantMessage = getByText("Test assistant message");
expect(assistantMessage).toBeDefined();
});
it("should render placeholder text when history is empty", () => {
// Mocking the useChatContext hook to return an empty history
useChatContextMock.mockReturnValue({ history: [] });
const { getByText } = render(<ChatMessages />);
const placeholderText = getByText("Ask a question, or describe a task.");
expect(placeholderText).toBeDefined();
});
});

View File

@ -0,0 +1,18 @@
import { render } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { ChatMessage } from "../components/ChatMessage";
describe("ChatMessage", () => {
it("should render chat messages with correct speaker and text", () => {
const speaker = "user";
const text = "Test user message";
const { getByText } = render(<ChatMessage speaker={speaker} text={text} />);
const speakerElement = getByText(speaker);
const textElement = getByText(text);
expect(speakerElement).toBeDefined();
expect(textElement).toBeDefined();
});
});

View File

@ -34,14 +34,10 @@ export const ChatMessage = forwardRef(
>
{speaker}
</span>
<>
<ReactMarkdown
// remarkRehypeOptions={{}}
className="prose dark:prose-invert ml-[6px] mt-1"
>
{text}
</ReactMarkdown>
</>
<ReactMarkdown className="prose dark:prose-invert ml-[6px] mt-1">
{text}
</ReactMarkdown>
</div>
);
}

View File

@ -0,0 +1 @@
export * from "./components/ChatMessage";

View File

@ -1,10 +1,10 @@
import React from "react";
import Card from "@/lib/components/ui/Card";
import { useChatContext } from "@/lib/context";
import { ChatMessage } from "./components/ChatMessage";
import { ChatMessage } from "./components/ChatMessage/components/ChatMessage";
import { useChatMessages } from "./hooks/useChatMessages";
import { useChatContext } from "../../[chatId]/context/ChatContext";
export const ChatMessages = (): JSX.Element => {
const { chatListRef } = useChatMessages();

View File

@ -0,0 +1,3 @@
export * from "./ChatInput";
export * from "./ChatMessages";
export * from "./ChatMessages/components/ChatMessage/components/ChatMessage";

View File

@ -8,7 +8,7 @@ import { useToast } from "@/lib/hooks";
import { useEventTracking } from "@/services/analytics/useEventTracking";
import { useChatService } from "./useChatService";
import { useChatContext } from "../context/ChatContext";
import { useChatContext } from "../../../../lib/context/ChatProvider";
import { ChatQuestion } from "../types";
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types

View File

@ -5,7 +5,7 @@ import { useCallback } from "react";
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
import { useAxios, useFetch } from "@/lib/hooks";
import { useChatContext } from "../context/ChatContext";
import { useChatContext } from "../../../../lib/context/ChatProvider";
import { ChatEntity, ChatHistory, ChatQuestion } from "../types";
interface UseChatService {

View File

@ -3,8 +3,8 @@
import PageHeading from "@/lib/components/ui/PageHeading";
import { ChatInput, ChatMessages } from "../components";
import { ChatProvider } from "./context/ChatContext";
import { ChatProvider } from "../../../lib/context/ChatProvider";
import { ChatInput, ChatMessages } from "./components";
export default function ChatPage() {
return (

View File

@ -5,9 +5,9 @@ import { cn } from "@/lib/utils";
import { MotionConfig, motion } from "framer-motion";
import { MdChevronRight } from "react-icons/md";
import { NewChatButton } from "./NewChatButton";
import { ChatsListItem } from "./components/ChatsListItem/";
import { ChatsListItem } from "./components/ChatsListItem";
import { MiniFooter } from "./components/ChatsListItem/components/MiniFooter";
import { NewChatButton } from "./components/NewChatButton";
import { useChatsList } from "./hooks/useChatsList";
export const ChatsList = (): JSX.Element => {

View File

@ -1,4 +1 @@
export * from "./ChatInput";
export * from "./ChatMessages";
export * from "./ChatMessages/components/ChatMessage";
export * from "./ChatsList";

View File

@ -5,7 +5,7 @@ import { ReactNode } from "react";
import { ChatsProvider } from "@/lib/context/ChatsProvider/chats-provider";
import { useSupabase } from "@/lib/context/SupabaseProvider";
import { ChatsList } from "./components";
import { ChatsList } from "./components/ChatsList";
interface LayoutProps {
children?: ReactNode;

View File

@ -2,7 +2,7 @@
import { createContext, useContext, useState } from "react";
import { ChatHistory } from "../types";
import { ChatHistory } from "../../app/chat/[chatId]/types";
type ChatContextProps = {
history: ChatHistory[];
@ -34,10 +34,10 @@ export const ChatProvider = ({
(item) => item.message_id === streamedChat.message_id
)
? prevHistory.map((item: ChatHistory) =>
item.message_id === streamedChat.message_id
? { ...item, assistant: item.assistant + streamedChat.assistant }
: item
)
item.message_id === streamedChat.message_id
? { ...item, assistant: item.assistant + streamedChat.assistant }
: item
)
: [...prevHistory, streamedChat];
console.log("updated history", updatedHistory);
@ -52,10 +52,10 @@ export const ChatProvider = ({
(item) => item.message_id === chat.message_id
)
? prevHistory.map((item: ChatHistory) =>
item.message_id === chat.message_id
? { ...item, assistant: chat.assistant }
: item
)
item.message_id === chat.message_id
? { ...item, assistant: chat.assistant }
: item
)
: [...prevHistory, chat];
return updatedHistory;

View File

@ -1,2 +1,3 @@
export * from "./BrainProvider";
export * from "./ChatProvider";
export * from "./FeatureFlagProvider";