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 1, 2024
1 parent 9652708 commit 9c02cdf
Show file tree
Hide file tree
Showing 21 changed files with 17,165 additions and 4 deletions.
16,244 changes: 16,244 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

9 changes: 8 additions & 1 deletion 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 @@ -55,8 +62,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
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();
});
});
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");
});
});
66 changes: 66 additions & 0 deletions src/__test__/updateProfileSlice.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import axios from "axios";

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

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

describe("patch /users/profile", () => {
const patch = async (url, data) => {
const accessToken = localStorage.getItem("accessToken");
const axiosInstance = axios.create({
headers: {
"Content-Type": "multipart/form-data",
Authorization: `Bearer ${accessToken}`,
},
});
const response = await axiosInstance.patch(url, data);
return response.data;
};

it("should return response data on success", async () => {
const payload = {};
const accessToken = "ample-access-token";
const response = { data: {} };

const mockAxiosInstance = {
patch: jest.fn(() => Promise.resolve(response)),
};
// @ts-ignore
jest.spyOn(axios, "create").mockReturnValue(mockAxiosInstance);

const result = await patch("/users/profile", payload);

expect(axios.create).toHaveBeenCalledTimes(1);
expect(mockAxiosInstance.patch).toHaveBeenCalledTimes(1);
expect(mockAxiosInstance.patch).toHaveBeenCalledWith(
"/users/profile",
payload,
);
expect(result).toEqual(response.data);
});

it("should return error response on failure", async () => {
const payload = {};
const accessToken = "ample-access-token";
const error = { response: { data: { message: "Error message" } } };

const mockAxiosInstance = {
patch: jest.fn(() => Promise.reject(error)),
};
// @ts-ignore
jest.spyOn(axios, "create").mockReturnValue(mockAxiosInstance);
const result = await patch("/users/profile", payload).catch(
(err) => err.response.data,
);
expect(mockAxiosInstance.patch).toHaveBeenCalledWith(
"/users/profile",
payload,
);
expect(result).toEqual(error.response.data);
});
});
19 changes: 19 additions & 0 deletions src/components/profile/getProfile.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import React from "react";

interface ProfileDataProps {
label: string;
value: string | number | null;
}

const DisplayProfileData: React.FC<ProfileDataProps> = ({ label, value }) => (
<center>
<div className="block sm:flex justify-between border-solid border-b-[1px] border-gray-200 pt-[1%]">
<p className="flex w-[100%] text-md sm:text-lg">{label}</p>
<p className="flex justify-start w-[100%] text-sm sm:text-lg font-light">
{value}
</p>
</div>
</center>
);

export default DisplayProfileData;
11 changes: 11 additions & 0 deletions src/components/profile/linkToUpdate.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Link } from "react-router-dom";

interface PropTypes {
link: string;
children: React.ReactNode;
}
const LinkToUpdatePage = ({ link, children }: PropTypes) => (
<Link to={link}>{children}</Link>
);

export default LinkToUpdatePage;
10 changes: 10 additions & 0 deletions src/components/profile/spinner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const Spinner = () => (
<div className="flex justify-center items-center mt-[80%] sm:mt-[25%]">
<div
className="inline-block h-8 w-8 sm:h-16 sm:w-16 animate-spin rounded-full border-4 border-solid border-[#DB4444] border-e-transparent text-danger motion-reduce:animate-[spin_1.5s_linear_infinite]"
role="status"
/>
</div>
);

export default Spinner;
26 changes: 26 additions & 0 deletions src/components/profile/updateProfileButtons.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import React from "react";

interface ButtonProps {
text: string;
disabled?: boolean;
dataTestId?: string;
backgroundColor?: string;
onClick?: () => void;
}

const Button: React.FC<ButtonProps> = ({
text,
disabled,
dataTestId,
backgroundColor,
}) => (
<button
type="submit"
className={`${backgroundColor} text-white p-1 sm:py-3 px-3 sm:px-12 my-4 text-normal md:text-lg rounded-sm cursor-pointer`}
disabled={disabled}
data-testid={dataTestId}
>
{text}
</button>
);
export default Button;
39 changes: 39 additions & 0 deletions src/components/profile/updateProfileInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import React from "react";

type TextInputProps = {
label: string;
defaultValue?: string | number;
register: any;
name: string;
error?: any;
type?: string;
};

const ProfileTextInput: React.FC<TextInputProps> = ({
label,
defaultValue,
register,
name,
error,
type = "text",
}) => (
<div>
<label
className="text-md sm:text-lg block mb-1 sm:mb-2 text-dark-gray"
htmlFor={name}
>
{label}
</label>
<input
defaultValue={defaultValue}
type={type}
id={name}
className="w-full bg-gray-100 text-[#161616] border-[0.5px] border-[#E5E5E5] py-1 sm:py-2 text-md font-light rounded-[8px] px-4 focus:outline-none"
{...register(name)}
/>
{error && <p className="text-red-500">{error}</p>}
</div>
);

export default ProfileTextInput;
Loading

0 comments on commit 9c02cdf

Please sign in to comment.