Skip to content

Commit

Permalink
feat(users profile): users should view and update his/her profile
Browse files Browse the repository at this point in the history
-Ensures that user can view his or her profile
-Ensure that user can update his or her profile

[Deliver #187419128]
  • Loading branch information
niyobertin committed Jul 2, 2024
1 parent 9652708 commit a4ec87a
Show file tree
Hide file tree
Showing 24 changed files with 17,246 additions and 16 deletions.
16,246 changes: 16,246 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

13 changes: 10 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@
"dependencies": {
"@emotion/react": "^11.11.4",
"@emotion/styled": "^11.11.5",
"@fortawesome/fontawesome-svg-core": "^6.5.2",
"@fortawesome/free-regular-svg-icons": "^6.5.2",
"@fortawesome/free-solid-svg-icons": "^6.5.2",
"@fortawesome/react-fontawesome": "^0.2.2",
"@gsap/react": "^2.1.1",
"@hookform/resolvers": "^3.6.0",
"@mui/material": "^5.15.20",
Expand All @@ -32,9 +36,12 @@
"eslint-config-airbnb-typescript": "^18.0.0",
"expect-puppeteer": "^10.0.0",
"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",
"node-fetch": "^3.3.2",
"npm": "^10.8.1",
"prop-types": "^15.8.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
Expand All @@ -46,7 +53,6 @@
"react-router-dom": "^6.23.1",
"react-toastify": "^10.0.5",
"redux": "^5.0.1",
"redux-mock-store": "^1.5.4",
"redux-thunk": "^3.1.0",
"swiper": "^11.1.4",
"vite-plugin-environment": "^1.1.3",
Expand All @@ -55,8 +61,8 @@
"devDependencies": {
"@commitlint/cli": "^19.3.0",
"@commitlint/config-conventional": "^19.2.2",
"@testing-library/jest-dom": "^6.4.6",
"@testing-library/dom": "^10.2.0",
"@testing-library/jest-dom": "^6.4.6",
"@types/jest": "^29.5.12",
"@types/node": "^20.14.8",
"@types/react": "^18.2.66",
Expand Down Expand Up @@ -88,8 +94,9 @@
"msw": "^2.3.1",
"postcss": "^8.4.38",
"prettier": "^3.3.1",
"redux-mock-store": "^1.5.4",
"tailwindcss": "^3.4.4",
"ts-jest": "^29.1.4",
"ts-jest": "^29.1.5",
"ts-node": "^10.9.2",
"typescript": "^5.2.2",
"vite": "^5.2.0"
Expand Down
Binary file added public/Screenshot 2024-06-04 at 14.38.58.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
49 changes: 49 additions & 0 deletions src/__test__/getProfile.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import "@testing-library/jest-dom";
import React from "react";
import { render, fireEvent, waitFor } from "@testing-library/react";
import { Provider } from "react-redux";
import { BrowserRouter } from "react-router-dom";

import UsersProfile from "../pages/userProfile";
import store from "../redux/store";

describe("UsersProfile component", () => {
beforeAll(() => {
const mockAccessToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c";
global.localStorage.setItem("accessToken", mockAccessToken);
});

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

it("displays loading state", () => {
const { getByText } = render(
<Provider store={store}>
<BrowserRouter>
<UsersProfile />
</BrowserRouter>
</Provider>,
);
expect(getByText("Loading...")).toBeInTheDocument();
});

it("calls getProfile action on mount", () => {
const getProfileMock = jest.fn();
const { rerender } = render(
<Provider store={store}>
<BrowserRouter>
<UsersProfile />
</BrowserRouter>
</Provider>,
);
rerender(
<Provider store={store}>
<BrowserRouter>
<UsersProfile />
</BrowserRouter>
</Provider>,
);
expect(getProfileMock).toBeTruthy();
});
});
58 changes: 58 additions & 0 deletions src/__test__/getProfilesclice.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { getProfile, profileSlice } from "../redux/reducers/profileSlice";

import "@testing-library/jest-dom";
import { render, screen } from "@testing-library/react";

import DisplayProfileData from "../components/profile/getProfile";

const { reducer } = profileSlice;

describe("get profile sclice", () => {
it("handles pending state on profile .pending", () => {
// @ts-ignore
const initialState = reducer(undefined, { type: getProfile.pending });
expect(initialState.loading).toBeTruthy();
});

it("handles fulfilled state and data on profile .fulfilled", () => {
const mockData = { message: "Success" };
const initialState = reducer(undefined, {
// @ts-ignore
type: getProfile.fulfilled,
payload: mockData,
});
expect(initialState.profile).toBe(mockData);
});

it("handles rejected state and error on profile .rejected", () => {
const error = { message: "Error" };
const initialState = reducer(undefined, {
// @ts-ignore
type: getProfile.rejected,
payload: error,
});
expect(initialState.error).toBe("Error");
});
});

describe("DisplayProfileData component", () => {
it("renders the label and value correctly", () => {
const label = "Name";
const value = "John Doe";

render(<DisplayProfileData label={label} value={value} />);

expect(screen.getByText(label)).toBeInTheDocument();
expect(screen.getByText(value)).toBeInTheDocument();
});

it("handles number values correctly", () => {
const label = "Age";
const value = 30;

render(<DisplayProfileData label={label} value={value} />);

expect(screen.getByText(label)).toBeInTheDocument();
expect(screen.getByText(value.toString())).toBeInTheDocument();
});
});
File renamed without changes.
1 change: 0 additions & 1 deletion src/__test__/otpVerfication.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,6 @@ describe("OtpVerification", () => {

it("should render form elements", () => {
const { getByText } = renderComponent(store);

expect(
getByText(
"Protecting your account is our priority. Please confirm your identity by providing the code sent to your email address",
Expand Down
94 changes: 94 additions & 0 deletions src/__test__/updateProfile.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import "@testing-library/jest-dom";
import {
render, fireEvent, waitFor, screen,
} from "@testing-library/react";
import { Provider } from "react-redux";
import { BrowserRouter } from "react-router-dom";

import UpdateUserProfile, { convertUrlToFile } from "../pages/updateProfile";
import store from "../redux/store";

global.fetch = jest.fn(() => Promise.resolve({
blob: () => Promise.resolve(new Blob(["dummy content"], { type: "image/png" })),
})) as jest.Mock;

describe("UpdateUserProfile component", () => {
it("renders correctly", () => {
const { getByText } = render(
<Provider store={store}>
<BrowserRouter>
<UpdateUserProfile />
</BrowserRouter>
</Provider>,
);
expect(getByText("Update your profile")).toBeInTheDocument();
});
it("test the input in hte document ", () => {
const { getByLabelText } = render(
<Provider store={store}>
<BrowserRouter>
<UpdateUserProfile />
</BrowserRouter>
</Provider>,
);

const fullNameInput = getByLabelText("Full name");
const genderInput = getByLabelText("Gender");
const birthdateInput = getByLabelText("Birth date");
const preferredLanguageInput = getByLabelText("Preferred language");
const preferredCurrencyInput = getByLabelText("Preferred currency");
const streetInput = getByLabelText("Street");
const cityInput = getByLabelText("City");
const stateInput = getByLabelText("State");
const postalCodeInput = getByLabelText("Postal Code");
const countryInput = getByLabelText("Country");

expect(fullNameInput).toBeInTheDocument();
expect(genderInput).toBeInTheDocument();
expect(birthdateInput).toBeInTheDocument();
expect(preferredLanguageInput).toBeInTheDocument();
expect(preferredCurrencyInput).toBeInTheDocument();
expect(cityInput).toBeInTheDocument();
expect(stateInput).toBeInTheDocument();
expect(postalCodeInput).toBeInTheDocument();
expect(countryInput).toBeInTheDocument();
});

it("handles image input correctly", () => {
const { getByLabelText } = render(
<Provider store={store}>
<BrowserRouter>
<UpdateUserProfile />
</BrowserRouter>
</Provider>,
);

const file = new File(["dummy content"], "example.png", {
type: "image/png",
});
const input = getByLabelText(/upload image/i);

fireEvent.change(input, { target: { files: [file] } });

waitFor(() => expect(screen.getByAltText("Profile Preview")).toHaveAttribute(
"src",
expect.stringContaining("data:image/png;base64"),
));
});
it("should convert a URL to a File object", async () => {
const url = "http://example.com/profileImage.png";
const file = await convertUrlToFile(url);

expect(file).toBeInstanceOf(File);
expect(file.name).toBe("profileImage.png");
expect(file.type).toBe("image/png");
});

it("should handle URLs without a filename", async () => {
const url = "http://example.com/";
const file = await convertUrlToFile(url);

expect(file).toBeInstanceOf(File);
expect(file.name).toBe("profileImage");
});
});
33 changes: 33 additions & 0 deletions src/__test__/updateProfileSlice.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import {
updateProfile,
updateProfileSlice,
} from "../redux/reducers/updateProfileSlice";

const { reducer } = updateProfileSlice;
describe("profile update slice", () => {
it("handles pending state on profile update.pending", () => {
// @ts-ignore
const initialState = reducer(undefined, { type: updateProfile.pending });
expect(initialState.loading).toBeTruthy();
});

it("handles fulfilled state and data on profile update.fulfilled", () => {
const mockData = { message: "Success" };
const initialState = reducer(undefined, {
// @ts-ignore
type: updateProfile.fulfilled,
payload: mockData,
});
expect(initialState.profile).toBe(undefined);
});

it("handles rejected state and error on profile update.rejected", () => {
const error = { message: "Error" };
const initialState = reducer(undefined, {
// @ts-ignore
type: updateProfile.rejected,
payload: error,
});
expect(initialState.error).toBe(error);
});
});
62 changes: 52 additions & 10 deletions src/components/common/header/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,48 @@ import {
} from "@mui/material";
import { CiUser } from "react-icons/ci";
import { FaSearch, FaShoppingCart } from "react-icons/fa";
import React from "react";
import React, { useEffect, useState } from "react";
import { Link } from "react-router-dom";
import { useSelector } from "react-redux";

import { getProfile } from "../../../redux/reducers/profileSlice";
import Logo from "../auth/Logo";
import { RootState } from "../../../redux/store";
import { useAppDispatch } from "../../../redux/hooks";

interface ISerachProps {
searchQuery: string;
setSearchQuery: React.Dispatch<React.SetStateAction<string>>;
}

const Header: React.FC<ISerachProps> = ({ searchQuery, setSearchQuery }) => {
const [isLoggedIn, setIsLoggedIn] = useState(false);
const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearchQuery(e.target.value);
};
const { profile } = useSelector((state: RootState) => state.usersProfile);
let userInfo;
const accessToken = localStorage.getItem("accessToken");
if (accessToken) {
userInfo = JSON.parse(atob(accessToken.split(".")[1]));
}
const dispatch = useAppDispatch();
useEffect(() => {
if (accessToken) {
try {
const userInfo = JSON.parse(atob(accessToken.split(".")[1]));
if (userInfo) {
setIsLoggedIn(true);
}
} catch (error) {
console.error("Invalid token format", error);
}
}
}, []);
useEffect(() => {
// @ts-ignore
dispatch(getProfile());
}, [dispatch]);

return (
<Stack
Expand Down Expand Up @@ -58,15 +86,29 @@ const Header: React.FC<ISerachProps> = ({ searchQuery, setSearchQuery }) => {
</Stack>
</Stack>
<Stack>
<Link to="/login" className="flex items-center">
<CiUser className="text-[24px] text-black" />
<Stack className="flex flex-col">
<span className="ml-2 font-semibold text-[12px]">User</span>
<span className="ml-2 font-semibold text-[12px]">
Account
</span>
</Stack>
</Link>
{isLoggedIn ? (
<Link to="/profile" className="flex items-center">
<CiUser className="text-[24px] text-black" />
<Stack className="flex flex-col">
<span className="ml-2 font-semibold text-[12px]">
{profile?.fullName}
</span>
<span className="ml-2 font-semibold text-[12px]">
{userInfo.email}
</span>
</Stack>
</Link>
) : (
<Link to="/login" className="flex items-center">
<CiUser className="text-[24px] text-black" />
<Stack className="flex flex-col">
<span className="ml-2 font-semibold text-[12px]">User</span>
<span className="ml-2 font-semibold text-[12px]">
Account
</span>
</Stack>
</Link>
)}
</Stack>
</Stack>
</Stack>
Expand Down
Loading

0 comments on commit a4ec87a

Please sign in to comment.