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 18, 2024
1 parent 837a593 commit 5d79ca2
Show file tree
Hide file tree
Showing 28 changed files with 1,618 additions and 111 deletions.
69 changes: 69 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"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 +58,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");
});
});
132 changes: 132 additions & 0 deletions src/__test__/chat.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import { render, screen } from "@testing-library/react";
import { Provider } from "react-redux";
import { configureStore } from "@reduxjs/toolkit";
import { BrowserRouter } from "react-router-dom";

import chatReducer from "../redux/reducers/chatSlice";
import authReducer from "../redux/reducers/authSlice";
import cartReducer from "../redux/reducers/cartSlice";
import ChatPage from "../pages/ChatPage";

jest.mock("../config/socket", () => ({
socket: {
on: jest.fn(),
emit: jest.fn(),
off: jest.fn(),
},
}));

jest.mock("../page-sections/chat/ChatWindow", () => ({
ChatWindow: jest.fn(() => <div>Chat Window Mock</div>),
}));

const mockStore = (initialState) =>
configureStore({
reducer: {
// @ts-ignore
chats: chatReducer,
auth: authReducer,
cart: cartReducer,
},
preloadedState: initialState,
});

const mockUser = {
id: 1,
name: "John Doe",
username: "johndoe",
profile: {
profileImage: "http://example.com/profile.jpg",
},
};

const mockOtherUser = {
id: 2,
name: "Jane Doe",
profile: {
profileImage: "http://example.com/profile2.jpg",
},
};

const mockChat = {
id: 1,
messages: [],
receiverId: 2,
userId: 1,
createdAt: "2024-06-20T12:00:00Z",
updatedAt: "2024-06-20T12:00:00Z",
receiver: mockOtherUser,
user: mockUser,
};

describe("ChatPage", () => {
let store;

beforeAll(() => {
const mockAccessToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c";
global.localStorage.setItem("accessToken", mockAccessToken);
global.localStorage.setItem("userInfo", JSON.stringify(mockUser));
});

afterAll(() => {
global.localStorage.removeItem("accessToken");
global.localStorage.removeItem("userInfo");
});

beforeEach(() => {
store = mockStore({
chats: {
chats: {
loading: false,
data: [mockChat],
error: null,
sending: false,
sendError: null,
},
users: {
loading: false,
data: [mockUser, mockOtherUser],
error: null,
},
},
auth: {
loading: false,
data: [],
error: null,
userInfo: mockUser,
},
cart: {
isLoading: false,
data: [],
error: false,
delete: { isLoading: false, error: false },
add: { isLoading: false, data: [], error: null },
remove: { isLoading: false, error: false },
update: { isLoading: false, data: [], error: false },
},
});

store.dispatch({
type: "auth/setUserInfo",
payload: mockUser,
});

store.dispatch({
type: "chats/setChats",
payload: [mockChat],
});
});

test("renders the Chat Page component", () => {
render(
<Provider store={store}>
<BrowserRouter>
<ChatPage />
</BrowserRouter>
</Provider>,
);

expect(screen.getByText("eagles")).toBeDefined();
// expect(screen.getByPlaceholderText('Search...')).toBeDefined();
});
});
Loading

0 comments on commit 5d79ca2

Please sign in to comment.