mirror of
https://github.com/StanGirard/quivr.git
synced 2024-12-24 11:52:45 +03:00
Frontend/test/chat (#496)
* refactor(<ChatPage/>) * test(<ChatInput />): add unit tests * test(<ChatMessages />): add unit tests
This commit is contained in:
parent
4f638544bb
commit
6acb13d4ae
@ -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();
|
||||
});
|
||||
});
|
@ -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>
|
@ -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" />
|
@ -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>
|
@ -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();
|
||||
});
|
||||
});
|
@ -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();
|
||||
});
|
||||
});
|
@ -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>
|
||||
);
|
||||
}
|
@ -0,0 +1 @@
|
||||
export * from "./components/ChatMessage";
|
@ -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();
|
3
frontend/app/chat/[chatId]/components/index.ts
Normal file
3
frontend/app/chat/[chatId]/components/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from "./ChatInput";
|
||||
export * from "./ChatMessages";
|
||||
export * from "./ChatMessages/components/ChatMessage/components/ChatMessage";
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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 (
|
||||
|
@ -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 => {
|
||||
|
@ -1,4 +1 @@
|
||||
export * from "./ChatInput";
|
||||
export * from "./ChatMessages";
|
||||
export * from "./ChatMessages/components/ChatMessage";
|
||||
export * from "./ChatsList";
|
||||
|
@ -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;
|
||||
|
@ -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;
|
@ -1,2 +1,3 @@
|
||||
export * from "./BrainProvider";
|
||||
export * from "./ChatProvider";
|
||||
export * from "./FeatureFlagProvider";
|
||||
|
Loading…
Reference in New Issue
Block a user