Skip to content

Commit c1535a9

Browse files
authored
Frontend/test/chat 4 (#523)
* refactor: move getHistory to api/chat * test(chat): unit-tests getHistory * refactor: rename useChatService useQuestion * feat: add updateChat to api/chat * test(chat): unit-tests updateChat * refactor(ChatsListItem): add useChatsListItem * feat: remove http request from provider and remove useChats
1 parent b6430f9 commit c1535a9

File tree

15 files changed

+244
-175
lines changed

15 files changed

+244
-175
lines changed

frontend/app/chat/[chatId]/__tests__/page.test.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import {
1818
SupabaseProviderMock,
1919
} from "@/lib/context/SupabaseProvider/mocks/SupabaseProviderMock";
2020

21-
import ChatPage from "../page";
21+
import SelectedChatPage from "../page";
2222

2323
vi.mock("@/lib/context/ChatProvider/ChatProvider", () => ({
2424
ChatContext: ChatContextMock,
@@ -43,7 +43,7 @@ describe("Chat page", () => {
4343
<SupabaseProviderMock>
4444
<BrainConfigProviderMock>
4545
<BrainProviderMock>
46-
<ChatPage />,
46+
<SelectedChatPage />,
4747
</BrainProviderMock>
4848
</BrainConfigProviderMock>
4949
</SupabaseProviderMock>

frontend/app/chat/[chatId]/hooks/useChat.ts

Lines changed: 21 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { useChatContext } from "@/lib/context/ChatProvider/hooks/useChatContext"
99
import { useToast } from "@/lib/hooks";
1010
import { useEventTracking } from "@/services/analytics/useEventTracking";
1111

12-
import { useChatService } from "./useChatService";
12+
import { useQuestion } from "./useQuestion";
1313
import { ChatQuestion } from "../types";
1414

1515
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
@@ -25,33 +25,25 @@ export const useChat = () => {
2525
} = useBrainConfig();
2626
const { history, setHistory } = useChatContext();
2727
const { publish } = useToast();
28-
const { createChat } = useChatApi();
28+
const { createChat, getHistory } = useChatApi();
2929

30-
const {
31-
getChatHistory,
32-
addStreamQuestion,
33-
addQuestion: addQuestionToModel,
34-
} = useChatService();
30+
const { addStreamQuestion, addQuestion: addQuestionToModel } = useQuestion();
3531

3632
useEffect(() => {
3733
const fetchHistory = async () => {
3834
const currentChatId = chatId;
39-
const chatHistory = await getChatHistory(currentChatId);
35+
if (currentChatId === undefined) {
36+
return;
37+
}
38+
39+
const chatHistory = await getHistory(currentChatId);
4040

4141
if (chatId === currentChatId && chatHistory.length > 0) {
4242
setHistory(chatHistory);
4343
}
4444
};
4545
void fetchHistory();
46-
}, [chatId, getChatHistory, setHistory]);
47-
48-
const generateNewChatIdFromName = async (
49-
chatName: string
50-
): Promise<string> => {
51-
const chat = await createChat(chatName);
52-
53-
return chat.chat_id;
54-
};
46+
}, [chatId, setHistory]);
5547

5648
const addQuestion = async (question: string, callback?: () => void) => {
5749
const chatQuestion: ChatQuestion = {
@@ -62,16 +54,19 @@ export const useChat = () => {
6254
};
6355

6456
try {
65-
void track("QUESTION_ASKED");
6657
setGeneratingAnswer(true);
67-
const currentChatId =
68-
chatId ??
69-
// if chatId is undefined, we need to create a new chat on fly
70-
(await generateNewChatIdFromName(
71-
question.split(" ").slice(0, 3).join(" ")
72-
));
73-
74-
setChatId(currentChatId);
58+
59+
let currentChatId = chatId;
60+
61+
//if chatId is not set, create a new chat. Chat name is from the first question
62+
if (currentChatId === undefined) {
63+
const chatName = question.split(" ").slice(0, 3).join(" ");
64+
const chat = await createChat(chatName);
65+
currentChatId = chat.chat_id;
66+
setChatId(currentChatId);
67+
}
68+
69+
void track("QUESTION_ASKED");
7570

7671
if (chatQuestion.model === "gpt-3.5-turbo") {
7772
await addStreamQuestion(currentChatId, chatQuestion);

frontend/app/chat/[chatId]/hooks/useChatService.ts renamed to frontend/app/chat/[chatId]/hooks/useQuestion.ts

Lines changed: 2 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,26 @@
11
/* eslint-disable max-lines */
22

3-
import { useCallback } from "react";
4-
53
import { useChatApi } from "@/lib/api/chat/useChatApi";
64
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
75
import { useChatContext } from "@/lib/context/ChatProvider/hooks/useChatContext";
8-
import { useAxios, useFetch } from "@/lib/hooks";
6+
import { useFetch } from "@/lib/hooks";
97

108
import { ChatHistory, ChatQuestion } from "../types";
119

1210
interface UseChatService {
13-
getChatHistory: (chatId: string | undefined) => Promise<ChatHistory[]>;
1411
addQuestion: (chatId: string, chatQuestion: ChatQuestion) => Promise<void>;
1512
addStreamQuestion: (
1613
chatId: string,
1714
chatQuestion: ChatQuestion
1815
) => Promise<void>;
1916
}
2017

21-
export const useChatService = (): UseChatService => {
22-
const { axiosInstance } = useAxios();
18+
export const useQuestion = (): UseChatService => {
2319
const { fetchInstance } = useFetch();
2420
const { updateHistory, updateStreamingHistory } = useChatContext();
2521
const { currentBrain } = useBrainContext();
2622
const { addQuestion } = useChatApi();
2723

28-
const getChatHistory = useCallback(
29-
async (chatId: string | undefined): Promise<ChatHistory[]> => {
30-
if (chatId === undefined) {
31-
return [];
32-
}
33-
const response = (
34-
await axiosInstance.get<ChatHistory[]>(`/chat/${chatId}/history`)
35-
).data;
36-
37-
return response;
38-
},
39-
[axiosInstance]
40-
);
41-
4224
const addQuestionHandler = async (
4325
chatId: string,
4426
chatQuestion: ChatQuestion
@@ -121,7 +103,6 @@ export const useChatService = (): UseChatService => {
121103
};
122104

123105
return {
124-
getChatHistory,
125106
addQuestion: addQuestionHandler,
126107
addStreamQuestion,
127108
};

frontend/app/chat/[chatId]/page.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { ChatProvider } from "@/lib/context/ChatProvider";
55

66
import { ChatInput, ChatMessages } from "./components";
77

8-
const ChatPage = (): JSX.Element => {
8+
const SelectedChatPage = (): JSX.Element => {
99
return (
1010
<main className="flex flex-col w-full pt-10" data-testid="chat-page">
1111
<section className="flex flex-col flex-1 items-center w-full h-full min-h-[70vh]">
@@ -26,4 +26,4 @@ const ChatPage = (): JSX.Element => {
2626
);
2727
};
2828

29-
export default ChatPage;
29+
export default SelectedChatPage;

frontend/app/chat/components/ChatsList/__tests__/ChatList.test.tsx renamed to frontend/app/chat/components/ChatsList/__tests__/ChatsList.test.tsx

Lines changed: 38 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
1-
import { fireEvent, render, screen } from "@testing-library/react";
2-
import { describe, expect, it, vi } from "vitest";
1+
import { act, fireEvent, render, screen } from "@testing-library/react";
2+
import { afterEach, describe, expect, it, vi } from "vitest";
33

4+
import * as useChatsListModule from "../hooks/useChatsList";
45
import { ChatsList } from "../index";
56

6-
const setOpenMock = vi.fn(() => ({}));
7+
const getChatsMock = vi.fn(() => []);
78

8-
vi.mock("../hooks/useChatsList", () => ({
9-
useChatsList: () => ({
10-
open: false,
11-
setOpen: () => setOpenMock(),
12-
}),
13-
}));
9+
const setOpenMock = vi.fn();
10+
11+
vi.mock("next/navigation", async () => {
12+
const actual = await vi.importActual<typeof import("next/navigation")>(
13+
"next/navigation"
14+
);
15+
16+
return { ...actual, useRouter: () => ({ replace: vi.fn() }) };
17+
});
1418

1519
vi.mock("@/lib/context/ChatsProvider/hooks/useChatsContext", () => ({
1620
useChatsContext: () => ({
@@ -19,6 +23,7 @@ vi.mock("@/lib/context/ChatsProvider/hooks/useChatsContext", () => ({
1923
{ chat_id: 2, name: "Chat 2" },
2024
],
2125
deleteChat: vi.fn(),
26+
setAllChats: vi.fn(),
2227
}),
2328
}));
2429

@@ -36,6 +41,10 @@ vi.mock("@/lib/hooks", async () => {
3641
});
3742

3843
describe("ChatsList", () => {
44+
afterEach(() => {
45+
vi.restoreAllMocks();
46+
});
47+
3948
it("should render correctly", () => {
4049
const { getByTestId } = render(<ChatsList />);
4150
const chatsList = getByTestId("chats-list");
@@ -54,11 +63,29 @@ describe("ChatsList", () => {
5463
expect(chatItems).toHaveLength(2);
5564
});
5665

57-
it("toggles the open state when the button is clicked", () => {
58-
render(<ChatsList />);
66+
it("toggles the open state when the button is clicked", async () => {
67+
vi.spyOn(useChatsListModule, "useChatsList").mockReturnValue({
68+
open: false,
69+
setOpen: setOpenMock,
70+
});
71+
72+
await act(() => render(<ChatsList />));
73+
5974
const toggleButton = screen.getByTestId("chats-list-toggle");
75+
6076
fireEvent.click(toggleButton);
6177

6278
expect(setOpenMock).toHaveBeenCalledTimes(1);
6379
});
80+
81+
it("should call getChats when the component mounts", async () => {
82+
vi.mock("@/lib/api/chat/useChatApi", () => ({
83+
useChatApi: () => ({
84+
getChats: () => getChatsMock(),
85+
}),
86+
}));
87+
await act(() => render(<ChatsList />));
88+
89+
expect(getChatsMock).toHaveBeenCalledTimes(1);
90+
});
6491
});

frontend/app/chat/components/ChatsList/components/ChatsListItem/ChatsListItem.tsx

Lines changed: 11 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,26 @@
1-
import { UUID } from "crypto";
21
import Link from "next/link";
3-
import { usePathname } from "next/navigation";
4-
import { useState } from "react";
52
import { FiEdit, FiSave, FiTrash2 } from "react-icons/fi";
63
import { MdChatBubbleOutline } from "react-icons/md";
74

85
import { ChatEntity } from "@/app/chat/[chatId]/types";
9-
import { useAxios, useToast } from "@/lib/hooks";
106
import { cn } from "@/lib/utils";
117

128
import { ChatName } from "./components/ChatName";
9+
import { useChatsListItem } from "./hooks/useChatsListItem";
1310

1411
interface ChatsListItemProps {
1512
chat: ChatEntity;
16-
deleteChat: (id: UUID) => void;
1713
}
1814

19-
export const ChatsListItem = ({
20-
chat,
21-
deleteChat,
22-
}: ChatsListItemProps): JSX.Element => {
23-
const pathname = usePathname()?.split("/").at(-1);
24-
const selected = chat.chat_id === pathname;
25-
const [chatName, setChatName] = useState(chat.chat_name);
26-
const { axiosInstance } = useAxios();
27-
const { publish } = useToast();
28-
const [editingName, setEditingName] = useState(false);
29-
30-
const updateChatName = async () => {
31-
if (chatName !== chat.chat_name) {
32-
await axiosInstance.put<ChatEntity>(`/chat/${chat.chat_id}/metadata`, {
33-
chat_name: chatName,
34-
});
35-
publish({ text: "Chat name updated", variant: "success" });
36-
}
37-
};
38-
39-
const handleEditNameClick = () => {
40-
if (editingName) {
41-
setEditingName(false);
42-
void updateChatName();
43-
} else {
44-
setEditingName(true);
45-
}
46-
};
15+
export const ChatsListItem = ({ chat }: ChatsListItemProps): JSX.Element => {
16+
const {
17+
setChatName,
18+
deleteChat,
19+
handleEditNameClick,
20+
selected,
21+
chatName,
22+
editingName,
23+
} = useChatsListItem(chat);
4724

4825
return (
4926
<div
@@ -76,11 +53,7 @@ export const ChatsListItem = ({
7653
<button className="p-0" type="button" onClick={handleEditNameClick}>
7754
{editingName ? <FiSave /> : <FiEdit />}
7855
</button>
79-
<button
80-
className="p-5"
81-
type="button"
82-
onClick={() => deleteChat(chat.chat_id)}
83-
>
56+
<button className="p-5" type="button" onClick={() => void deleteChat()}>
8457
<FiTrash2 />
8558
</button>
8659
</div>
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { usePathname, useRouter } from "next/navigation";
2+
import { useState } from "react";
3+
4+
import { ChatEntity } from "@/app/chat/[chatId]/types";
5+
import { useChatApi } from "@/lib/api/chat/useChatApi";
6+
import { useChatsContext } from "@/lib/context/ChatsProvider/hooks/useChatsContext";
7+
import { useToast } from "@/lib/hooks";
8+
9+
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
10+
export const useChatsListItem = (chat: ChatEntity) => {
11+
const pathname = usePathname()?.split("/").at(-1);
12+
const selected = chat.chat_id === pathname;
13+
const [chatName, setChatName] = useState(chat.chat_name);
14+
const { publish } = useToast();
15+
const [editingName, setEditingName] = useState(false);
16+
const { updateChat, deleteChat } = useChatApi();
17+
const { setAllChats } = useChatsContext();
18+
const router = useRouter();
19+
20+
const deleteChatHandler = async () => {
21+
const chatId = chat.chat_id;
22+
try {
23+
await deleteChat(chatId);
24+
setAllChats((chats) =>
25+
chats.filter((currentChat) => currentChat.chat_id !== chatId)
26+
);
27+
// TODO: Change route only when the current chat is being deleted
28+
void router.push("/chat");
29+
publish({
30+
text: `Chat sucessfully deleted. Id: ${chatId}`,
31+
variant: "success",
32+
});
33+
} catch (error) {
34+
console.error("Error deleting chat:", error);
35+
publish({
36+
text: `Error deleting chat: ${JSON.stringify(error)}`,
37+
variant: "danger",
38+
});
39+
}
40+
};
41+
42+
const updateChatName = async () => {
43+
if (chatName !== chat.chat_name) {
44+
await updateChat(chat.chat_id, { chat_name: chatName });
45+
publish({ text: "Chat name updated", variant: "success" });
46+
}
47+
};
48+
49+
const handleEditNameClick = () => {
50+
if (editingName) {
51+
setEditingName(false);
52+
void updateChatName();
53+
} else {
54+
setEditingName(true);
55+
}
56+
};
57+
58+
return {
59+
setChatName,
60+
editingName,
61+
chatName,
62+
selected,
63+
handleEditNameClick,
64+
deleteChat: deleteChatHandler,
65+
};
66+
};

0 commit comments

Comments
 (0)