Skip to content

Commit

Permalink
feat(chat): Implement user private and public chat
Browse files Browse the repository at this point in the history
- a user should be able to read previous chats
- a user should be able to post a public chat
- users should be able to chat privately

Delivers #187419133
  • Loading branch information
Heisjabo committed Jul 22, 2024
1 parent 837a593 commit a9267cc
Show file tree
Hide file tree
Showing 30 changed files with 1,598 additions and 355 deletions.
353 changes: 143 additions & 210 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,12 @@
"axios-mock-adapter": "^1.22.0",
"eslint-config-airbnb-typescript": "^18.0.0",
"expect-puppeteer": "^10.0.0",
"flowbite": "^2.4.1",
"flowbite-react": "^0.10.1",
"gsap": "^3.12.5",
"install": "^0.13.0",
"jest-environment-jsdom": "^29.7.0",
"jest-fetch-mock": "^3.0.3",
"jest-mock-extended": "^3.0.7",
"moment": "^2.30.1",
"node-fetch": "^3.3.2",
"npm": "^10.8.1",
"prop-types": "^15.8.1",
Expand All @@ -57,6 +56,7 @@
"react-toastify": "^10.0.5",
"redux": "^5.0.1",
"redux-thunk": "^3.1.0",
"socket.io-client": "^4.7.5",
"swiper": "^11.1.4",
"tailwindcss": "^3.4.4",
"vite-plugin-environment": "^1.1.3",
Expand Down
26 changes: 21 additions & 5 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,28 @@ import * as React from "react";
import "react-toastify/dist/ReactToastify.css";
import "./App.css";

import { IoChatbubbleEllipsesOutline } from "react-icons/io5";
import { Link, useLocation } from "react-router-dom";

import AppRoutes from "./routes/AppRoutes";

const App: React.FC = () => (
<main>
<AppRoutes />
</main>
);
const App: React.FC = () => {
const location = useLocation();

return (
<main>
<AppRoutes />
{location.pathname !== "/chat"
&& location.pathname !== "/login"
&& location.pathname !== "/register" && (
<Link to="/chat">
<div className="fixed bg-primary text-white shadow-md rounded px-3 py-3 z-50 right-6 bottom-6 cursor-pointer group">
<IoChatbubbleEllipsesOutline className="text-[30px] text-white" />
</div>
</Link>
)}
</main>
);
};

export default App;
84 changes: 84 additions & 0 deletions src/__test__/authSlice.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { configureStore } from "@reduxjs/toolkit";
import MockAdapter from "axios-mock-adapter";

import api from "../redux/api/api";
import authSlice, { fetchUser, setUser } from "../redux/reducers/authSlice";

const mockApi = new MockAdapter(api);
const mockStore = (initialState) =>
configureStore({
reducer: {
// @ts-ignore
auth: authSlice,
},
preloadedState: initialState,
});

describe("Auth Slice Thunks", () => {
let store;

beforeEach(() => {
mockApi.reset();
store = mockStore({
auth: {
loading: false,
data: [],
error: null,
userInfo: JSON.parse(localStorage.getItem("userInfo") || "{}"),
},
});
});

it("should handle fetchUser pending", async () => {
mockApi.onGet("/users/me").reply(200, {});

store.dispatch(fetchUser());
expect(store.getState().auth.loading).toBe(true);
});

it("should handle fetchUser fulfilled", async () => {
const mockData = { id: "1", name: "John Doe" };
mockApi.onGet("/users/me").reply(200, mockData);

await store.dispatch(fetchUser());

expect(store.getState().auth.loading).toBe(false);
expect(store.getState().auth.data).toEqual(mockData);
expect(localStorage.getItem("userInfo")).toEqual(JSON.stringify(mockData));
});

it("should handle fetchUser rejected", async () => {
mockApi.onGet("/users/me").reply(500);

await store.dispatch(fetchUser());

expect(store.getState().auth.loading).toBe(false);
expect(store.getState().auth.error).toBe("Rejected");
});

it("should handle setUser", () => {
const mockUser = { id: "2", name: "Jane Doe" };
store.dispatch(setUser(mockUser));

expect(store.getState().auth.userInfo).toEqual(mockUser);
expect(localStorage.getItem("userInfo")).toEqual(JSON.stringify(mockUser));
});

it("should handle fetchUser axios error with specific message", async () => {
mockApi.onGet("/users/me").reply(500, { message: "Server error" });

await store.dispatch(fetchUser());

expect(store.getState().auth.loading).toBe(false);
expect(store.getState().auth.error.message).toBe("Server error");
});

it("should handle fetchUser axios network error", async () => {
mockApi.onGet("/users/me").networkError();

await store.dispatch(fetchUser());

expect(store.getState().auth.loading).toBe(false);
expect(store.getState().auth.error).toBe("Rejected");
});
});
157 changes: 157 additions & 0 deletions src/__test__/chatSlice.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import { configureStore } from "@reduxjs/toolkit";
import MockAdapter from "axios-mock-adapter";

import api from "../redux/api/api";
import chatsSlice, {
fetchUsers,
fetchChats,
sendMessage,
setChats,
setUsers,
} from "../redux/reducers/chatSlice";

const mockApi = new MockAdapter(api);
const mockStore = (initialState) =>
configureStore({
reducer: {
// @ts-ignore
chats: chatsSlice,
},
preloadedState: initialState,
});

describe("Chats Slice Thunks", () => {
let store;

beforeEach(() => {
mockApi.reset();
store = mockStore({
chats: {
chats: {
loading: false,
data: [],
error: null,
sending: false,
sendError: null,
},
users: {
loading: false,
data: [],
error: null,
},
},
});
});

it("should handle fetchUsers pending", async () => {
mockApi.onGet("/users").reply(200, { users: [] });

store.dispatch(fetchUsers());
expect(store.getState().chats.users.loading).toBe(true);
});

it("should handle fetchUsers fulfilled", async () => {
const mockData = [{ id: "1", name: "John Doe" }];
mockApi.onGet("/users").reply(200, { users: mockData });

await store.dispatch(fetchUsers());

expect(store.getState().chats.users.loading).toBe(false);
expect(store.getState().chats.users.data).toEqual(mockData);
});

it("should handle fetchUsers rejected", async () => {
mockApi.onGet("/users").reply(500);

await store.dispatch(fetchUsers());

expect(store.getState().chats.users.loading).toBe(false);
expect(store.getState().chats.users.error).toBeTruthy();
});

it("should handle fetchChats pending", async () => {
mockApi.onGet("/chats/private").reply(200, {});

store.dispatch(fetchChats());
expect(store.getState().chats.chats.loading).toBe(true);
});

it("should handle fetchChats fulfilled", async () => {
const mockData = [{ id: "1", messages: [] }];
mockApi.onGet("/chats/private").reply(200, mockData);

await store.dispatch(fetchChats());

expect(store.getState().chats.chats.loading).toBe(false);
expect(store.getState().chats.chats.data).toEqual(mockData);
});

it("should handle fetchChats rejected", async () => {
mockApi.onGet("/chats/private").reply(500);

await store.dispatch(fetchChats());

expect(store.getState().chats.chats.loading).toBe(false);
expect(store.getState().chats.chats.error).toBeTruthy();
});

it("should handle sendMessage pending", async () => {
mockApi.onPost("/chats/private/1").reply(200, {});

store.dispatch(sendMessage({ message: "Hello", id: 1 }));
expect(store.getState().chats.chats.sending).toBe(true);
});

it("should handle sendMessage fulfilled", async () => {
const mockMessage = { message: "Hello", recipientId: 1 };
const initialChats = [{ id: "1", messages: [{ recipientId: 1 }] }];
store = mockStore({
chats: {
chats: {
loading: false,
data: initialChats,
error: null,
sending: false,
sendError: null,
},
users: {
loading: false,
data: [],
error: null,
},
},
});

mockApi.onPost("/chats/private/1").reply(200, mockMessage);

await store.dispatch(sendMessage({ message: "Hello", id: 1 }));

expect(store.getState().chats.chats.sending).toBe(false);
expect(store.getState().chats.chats.data[0].messages).toContainEqual(
mockMessage,
);
});

it("should handle sendMessage rejected", async () => {
mockApi.onPost("/chats/private/1").reply(500);

await store.dispatch(sendMessage({ message: "Hello", id: 1 }));

expect(store.getState().chats.chats.sending).toBe(false);
expect(store.getState().chats.chats.sendError).toBeTruthy();
});

it("should handle setChats", () => {
const mockChats = [{ id: "2", messages: [] }];
store.dispatch(setChats(mockChats));

expect(store.getState().chats.chats.data).toEqual(mockChats);
});

it("should handle setUsers", () => {
const mockUsers = [{ id: "3", name: "Jane Doe" }];
store.dispatch(setUsers(mockUsers));

expect(store.getState().chats.users.data).toEqual(mockUsers);
});
});
Loading

0 comments on commit a9267cc

Please sign in to comment.