Skip to content

Commit f306d90

Browse files
authored
Merge pull request #19 from atlp-rwanda/ft-chat-#187419133
#187419133 Users should be able to chat
2 parents 837a593 + 92cab61 commit f306d90

30 files changed

+1598
-355
lines changed

package-lock.json

+143-210
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,12 @@
3535
"axios-mock-adapter": "^1.22.0",
3636
"eslint-config-airbnb-typescript": "^18.0.0",
3737
"expect-puppeteer": "^10.0.0",
38-
"flowbite": "^2.4.1",
39-
"flowbite-react": "^0.10.1",
4038
"gsap": "^3.12.5",
4139
"install": "^0.13.0",
4240
"jest-environment-jsdom": "^29.7.0",
4341
"jest-fetch-mock": "^3.0.3",
4442
"jest-mock-extended": "^3.0.7",
43+
"moment": "^2.30.1",
4544
"node-fetch": "^3.3.2",
4645
"npm": "^10.8.1",
4746
"prop-types": "^15.8.1",
@@ -57,6 +56,7 @@
5756
"react-toastify": "^10.0.5",
5857
"redux": "^5.0.1",
5958
"redux-thunk": "^3.1.0",
59+
"socket.io-client": "^4.7.5",
6060
"swiper": "^11.1.4",
6161
"tailwindcss": "^3.4.4",
6262
"vite-plugin-environment": "^1.1.3",

src/App.tsx

+21-5
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,28 @@ import * as React from "react";
22
import "react-toastify/dist/ReactToastify.css";
33
import "./App.css";
44

5+
import { IoChatbubbleEllipsesOutline } from "react-icons/io5";
6+
import { Link, useLocation } from "react-router-dom";
7+
58
import AppRoutes from "./routes/AppRoutes";
69

7-
const App: React.FC = () => (
8-
<main>
9-
<AppRoutes />
10-
</main>
11-
);
10+
const App: React.FC = () => {
11+
const location = useLocation();
12+
13+
return (
14+
<main>
15+
<AppRoutes />
16+
{location.pathname !== "/chat"
17+
&& location.pathname !== "/login"
18+
&& location.pathname !== "/register" && (
19+
<Link to="/chat">
20+
<div className="fixed bg-primary text-white shadow-md rounded px-3 py-3 z-50 right-6 bottom-6 cursor-pointer group">
21+
<IoChatbubbleEllipsesOutline className="text-[30px] text-white" />
22+
</div>
23+
</Link>
24+
)}
25+
</main>
26+
);
27+
};
1228

1329
export default App;

src/__test__/authSlice.test.tsx

+84
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { configureStore } from "@reduxjs/toolkit";
2+
import MockAdapter from "axios-mock-adapter";
3+
4+
import api from "../redux/api/api";
5+
import authSlice, { fetchUser, setUser } from "../redux/reducers/authSlice";
6+
7+
const mockApi = new MockAdapter(api);
8+
const mockStore = (initialState) =>
9+
configureStore({
10+
reducer: {
11+
// @ts-ignore
12+
auth: authSlice,
13+
},
14+
preloadedState: initialState,
15+
});
16+
17+
describe("Auth Slice Thunks", () => {
18+
let store;
19+
20+
beforeEach(() => {
21+
mockApi.reset();
22+
store = mockStore({
23+
auth: {
24+
loading: false,
25+
data: [],
26+
error: null,
27+
userInfo: JSON.parse(localStorage.getItem("userInfo") || "{}"),
28+
},
29+
});
30+
});
31+
32+
it("should handle fetchUser pending", async () => {
33+
mockApi.onGet("/users/me").reply(200, {});
34+
35+
store.dispatch(fetchUser());
36+
expect(store.getState().auth.loading).toBe(true);
37+
});
38+
39+
it("should handle fetchUser fulfilled", async () => {
40+
const mockData = { id: "1", name: "John Doe" };
41+
mockApi.onGet("/users/me").reply(200, mockData);
42+
43+
await store.dispatch(fetchUser());
44+
45+
expect(store.getState().auth.loading).toBe(false);
46+
expect(store.getState().auth.data).toEqual(mockData);
47+
expect(localStorage.getItem("userInfo")).toEqual(JSON.stringify(mockData));
48+
});
49+
50+
it("should handle fetchUser rejected", async () => {
51+
mockApi.onGet("/users/me").reply(500);
52+
53+
await store.dispatch(fetchUser());
54+
55+
expect(store.getState().auth.loading).toBe(false);
56+
expect(store.getState().auth.error).toBe("Rejected");
57+
});
58+
59+
it("should handle setUser", () => {
60+
const mockUser = { id: "2", name: "Jane Doe" };
61+
store.dispatch(setUser(mockUser));
62+
63+
expect(store.getState().auth.userInfo).toEqual(mockUser);
64+
expect(localStorage.getItem("userInfo")).toEqual(JSON.stringify(mockUser));
65+
});
66+
67+
it("should handle fetchUser axios error with specific message", async () => {
68+
mockApi.onGet("/users/me").reply(500, { message: "Server error" });
69+
70+
await store.dispatch(fetchUser());
71+
72+
expect(store.getState().auth.loading).toBe(false);
73+
expect(store.getState().auth.error.message).toBe("Server error");
74+
});
75+
76+
it("should handle fetchUser axios network error", async () => {
77+
mockApi.onGet("/users/me").networkError();
78+
79+
await store.dispatch(fetchUser());
80+
81+
expect(store.getState().auth.loading).toBe(false);
82+
expect(store.getState().auth.error).toBe("Rejected");
83+
});
84+
});

src/__test__/chatSlice.test.tsx

+157
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import { configureStore } from "@reduxjs/toolkit";
2+
import MockAdapter from "axios-mock-adapter";
3+
4+
import api from "../redux/api/api";
5+
import chatsSlice, {
6+
fetchUsers,
7+
fetchChats,
8+
sendMessage,
9+
setChats,
10+
setUsers,
11+
} from "../redux/reducers/chatSlice";
12+
13+
const mockApi = new MockAdapter(api);
14+
const mockStore = (initialState) =>
15+
configureStore({
16+
reducer: {
17+
// @ts-ignore
18+
chats: chatsSlice,
19+
},
20+
preloadedState: initialState,
21+
});
22+
23+
describe("Chats Slice Thunks", () => {
24+
let store;
25+
26+
beforeEach(() => {
27+
mockApi.reset();
28+
store = mockStore({
29+
chats: {
30+
chats: {
31+
loading: false,
32+
data: [],
33+
error: null,
34+
sending: false,
35+
sendError: null,
36+
},
37+
users: {
38+
loading: false,
39+
data: [],
40+
error: null,
41+
},
42+
},
43+
});
44+
});
45+
46+
it("should handle fetchUsers pending", async () => {
47+
mockApi.onGet("/users").reply(200, { users: [] });
48+
49+
store.dispatch(fetchUsers());
50+
expect(store.getState().chats.users.loading).toBe(true);
51+
});
52+
53+
it("should handle fetchUsers fulfilled", async () => {
54+
const mockData = [{ id: "1", name: "John Doe" }];
55+
mockApi.onGet("/users").reply(200, { users: mockData });
56+
57+
await store.dispatch(fetchUsers());
58+
59+
expect(store.getState().chats.users.loading).toBe(false);
60+
expect(store.getState().chats.users.data).toEqual(mockData);
61+
});
62+
63+
it("should handle fetchUsers rejected", async () => {
64+
mockApi.onGet("/users").reply(500);
65+
66+
await store.dispatch(fetchUsers());
67+
68+
expect(store.getState().chats.users.loading).toBe(false);
69+
expect(store.getState().chats.users.error).toBeTruthy();
70+
});
71+
72+
it("should handle fetchChats pending", async () => {
73+
mockApi.onGet("/chats/private").reply(200, {});
74+
75+
store.dispatch(fetchChats());
76+
expect(store.getState().chats.chats.loading).toBe(true);
77+
});
78+
79+
it("should handle fetchChats fulfilled", async () => {
80+
const mockData = [{ id: "1", messages: [] }];
81+
mockApi.onGet("/chats/private").reply(200, mockData);
82+
83+
await store.dispatch(fetchChats());
84+
85+
expect(store.getState().chats.chats.loading).toBe(false);
86+
expect(store.getState().chats.chats.data).toEqual(mockData);
87+
});
88+
89+
it("should handle fetchChats rejected", async () => {
90+
mockApi.onGet("/chats/private").reply(500);
91+
92+
await store.dispatch(fetchChats());
93+
94+
expect(store.getState().chats.chats.loading).toBe(false);
95+
expect(store.getState().chats.chats.error).toBeTruthy();
96+
});
97+
98+
it("should handle sendMessage pending", async () => {
99+
mockApi.onPost("/chats/private/1").reply(200, {});
100+
101+
store.dispatch(sendMessage({ message: "Hello", id: 1 }));
102+
expect(store.getState().chats.chats.sending).toBe(true);
103+
});
104+
105+
it("should handle sendMessage fulfilled", async () => {
106+
const mockMessage = { message: "Hello", recipientId: 1 };
107+
const initialChats = [{ id: "1", messages: [{ recipientId: 1 }] }];
108+
store = mockStore({
109+
chats: {
110+
chats: {
111+
loading: false,
112+
data: initialChats,
113+
error: null,
114+
sending: false,
115+
sendError: null,
116+
},
117+
users: {
118+
loading: false,
119+
data: [],
120+
error: null,
121+
},
122+
},
123+
});
124+
125+
mockApi.onPost("/chats/private/1").reply(200, mockMessage);
126+
127+
await store.dispatch(sendMessage({ message: "Hello", id: 1 }));
128+
129+
expect(store.getState().chats.chats.sending).toBe(false);
130+
expect(store.getState().chats.chats.data[0].messages).toContainEqual(
131+
mockMessage,
132+
);
133+
});
134+
135+
it("should handle sendMessage rejected", async () => {
136+
mockApi.onPost("/chats/private/1").reply(500);
137+
138+
await store.dispatch(sendMessage({ message: "Hello", id: 1 }));
139+
140+
expect(store.getState().chats.chats.sending).toBe(false);
141+
expect(store.getState().chats.chats.sendError).toBeTruthy();
142+
});
143+
144+
it("should handle setChats", () => {
145+
const mockChats = [{ id: "2", messages: [] }];
146+
store.dispatch(setChats(mockChats));
147+
148+
expect(store.getState().chats.chats.data).toEqual(mockChats);
149+
});
150+
151+
it("should handle setUsers", () => {
152+
const mockUsers = [{ id: "3", name: "Jane Doe" }];
153+
store.dispatch(setUsers(mockUsers));
154+
155+
expect(store.getState().chats.users.data).toEqual(mockUsers);
156+
});
157+
});

0 commit comments

Comments
 (0)