Skip to content

Commit

Permalink
THEMES-762: Identity Blocks | Reset Password block (#1717)
Browse files Browse the repository at this point in the history
* THEMES-762: began adding reset password block.

* THEMES-762: converted reset password block and started fixing tests.

* THEMES-762: cleaned up tests and fixed one that was breaking.

* THEMES-762: cleaned up tests, added missing ones.

* THEMES-762: added styles to the reset password form.

* THEMES-762: updating styles to better match designs.

* THEMES-762: small change to get coverage command to see test file.
  • Loading branch information
vgalatro authored Aug 15, 2023
1 parent b933214 commit 9ca6bb6
Show file tree
Hide file tree
Showing 14 changed files with 566 additions and 40 deletions.
5 changes: 5 additions & 0 deletions blocks/identity-block/_index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@
@include scss.block-properties("login-links");
}

.b-reset-password {
@include scss.block-components("reset-password");
@include scss.block-properties("reset-password");
}

.b-sign-up {
&__tos-container {
a {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import FormPasswordConfirm from ".";
describe("Form Password Confirm", () => {
it("renders with required items", () => {
render(<FormPasswordConfirm confirmLabel="Confirm" label="Password" name="field1" />);

expect(screen.getByLabelText("Password")).not.toBeNull();
expect(screen.getByLabelText("Confirm")).not.toBeNull();
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ const HeadlinedSubmitForm = ({
onSubmit = () => {},
}) => {
const formRef = useRef();

const handleSubmit = (event) => {
event.preventDefault();

Expand Down
1 change: 1 addition & 0 deletions blocks/identity-block/components/identity/index.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ describe("Identity useIdentity Hook", () => {
expect(Identity).toBe(IdentityObject);
return <div />;
};

render(<Test />);
expect(IdentityObject.options).toHaveBeenLastCalledWith({
apiOrigin: "http://origin/",
Expand Down
11 changes: 1 addition & 10 deletions blocks/identity-block/components/login/index.jsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,6 @@
import { useEffect, useState } from "react";
import useIdentity from "../identity";

const validateURL = (url) => {
if (!url) return null;
const validationRegEx = /^\/[^/].*$/;
const valid = validationRegEx.test(url);
if (valid) {
return url;
}
return "/";
};
import validateURL from "../../utils/validate-redirect-url";

const useLogin = ({ isAdmin, redirectURL, redirectToPreviousPage, loggedInPageLocation }) => {
const { Identity } = useIdentity();
Expand Down
63 changes: 43 additions & 20 deletions blocks/identity-block/features/login/default.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,58 +4,61 @@ import Login from "./default";
import useIdentity from "../../components/identity";

jest.mock("../../components/identity");
jest.mock("fusion:properties", () => jest.fn(() => ({})));

const defaultCustomFields = {
redirectURL: "",
redirectToPreviousPage: true,
};

const loginMock = jest.fn(() => Promise.resolve({}));
const loginMock = jest.fn(() => Promise.resolve());
const loginMockFail = jest.fn(() => Promise.reject());

jest.mock("fusion:properties", () => jest.fn(() => ({})));
const IdentityDefaults = {
isLoggedIn: jest.fn(() => false),
getConfig: jest.fn(() => ({})),
login: loginMock,
};

describe("Identity Login Feature", () => {
beforeEach(async () => {
beforeEach(() => {
useIdentity.mockImplementation(() => ({
isInitialized: true,
Identity: {
isLoggedIn: jest.fn(async () => false),
getConfig: jest.fn(async () => ({})),
login: loginMock,
...IdentityDefaults,
},
}));
});

afterAll(() => {
jest.restoreAllMocks();
afterEach(() => {
jest.clearAllMocks();
});

it("renders nothing if identity not initialized", async () => {
it("renders nothing if identity not initialized", () => {
useIdentity.mockImplementation(() => ({
isInitialized: false,
Identity: {
isLoggedIn: jest.fn(async () => false),
getConfig: jest.fn(async () => ({})),
...IdentityDefaults,
},
}));
await render(<Login customFields={defaultCustomFields} />);
render(<Login customFields={defaultCustomFields} />);
expect(screen.queryAllByRole("button")).toEqual([]);
});

it("renders", async () => {
await render(<Login customFields={defaultCustomFields} />);
it("renders", () => {
render(<Login customFields={defaultCustomFields} />);
expect(screen.queryByRole("form")).not.toBeNull();
});

it("shows login form", async () => {
await render(<Login customFields={defaultCustomFields} />);
it("shows login form", () => {
render(<Login customFields={defaultCustomFields} />);
expect(screen.queryByRole("form")).not.toBeNull();
expect(screen.getByLabelText("identity-block.password")).not.toBeNull();
expect(screen.getByLabelText("identity-block.email")).not.toBeNull();
});

it("submits the login form", async () => {
await render(<Login customFields={defaultCustomFields} />);
it("submits the login form", () => {
render(<Login customFields={defaultCustomFields} />);

fireEvent.change(screen.getByLabelText("identity-block.email"), {
target: { value: "[email protected]" },
Expand All @@ -65,8 +68,28 @@ describe("Identity Login Feature", () => {
});
fireEvent.click(screen.getByRole("button"));

await loginMock;

expect(loginMock).toHaveBeenCalled();
});

it("rejects the login", async () => {
useIdentity.mockImplementation(() => ({
isInitialized: true,
Identity: {
...IdentityDefaults,
login: loginMockFail,
},
}));

render(<Login customFields={defaultCustomFields} />);

fireEvent.change(screen.getByLabelText("identity-block.email"), {
target: { value: "[email protected]" },
});
fireEvent.change(screen.getByLabelText("identity-block.password"), {
target: { value: "thisNotIsMyPassword" },
});
await fireEvent.click(screen.getByRole("button"));
await expect(loginMockFail).toHaveBeenCalled();
expect(screen.getByText("identity-block.login-form-error")).not.toBeNull();
});
});
181 changes: 181 additions & 0 deletions blocks/identity-block/features/reset-password/default.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
import React, { useEffect, useState } from "react";
import PropTypes from "@arc-fusion/prop-types";
import { useFusionContext } from "fusion:context";
import getTranslatedPhrases from "fusion:intl";
import { Paragraph } from "@wpmedia/arc-themes-components";
import FormPasswordConfirm from "../../components/form-password-confirm";
import HeadlinedSubmitForm from "../../components/headlined-submit-form";
import useIdentity from "../../components/identity";
import passwordValidationMessage from "../../utils/password-validation-message";
import validatePasswordPattern from "../../utils/validate-password-pattern";
import validateURL from "../../utils/validate-redirect-url";

const BLOCK_CLASS_NAME = "b-reset-password";

const defaultSuccessURL = "/account/login/";

export const ResetPasswordPresentation = ({ isAdmin = false, phrases, successActionURL }) => {
const { Identity, isInitialized } = useIdentity();

const [error, setError] = useState();
const [passwordRequirements, setPasswordRequirements] = useState({
status: "initial",
});
const [submitted, setSubmitted] = useState(false);

// eslint doesn't handle globalThis yet, but this is appropriate
/* global globalThis */
const nonce = new URLSearchParams(globalThis.location.search).get("nonce");

useEffect(() => {
const getConfig = async () => {
await Identity.getConfig()
.then((response) => {
const { pwLowercase, pwMinLength, pwPwNumbers, pwSpecialCharacters, pwUppercase } =
response;

setPasswordRequirements({
pwLowercase,
pwMinLength,
pwPwNumbers,
pwSpecialCharacters,
pwUppercase,
status: "success",
});
})
.catch(() => setPasswordRequirements({ status: "error" }));
};

if (Identity) {
getConfig();
}
}, [Identity]);

const {
pwLowercase = 0,
pwMinLength = 0,
pwPwNumbers = 0,
pwSpecialCharacters = 0,
pwUppercase = 0,
status,
} = passwordRequirements;

const passwordErrorMessage = passwordValidationMessage({
defaultMessage: phrases.t("identity-block.password-requirements"),
options: {
lowercase: {
value: pwLowercase,
message: phrases.t("identity-block.password-requirements-lowercase", {
requirementCount: pwLowercase,
}),
},
minLength: {
value: pwMinLength,
message: phrases.t("identity-block.password-requirements-characters", {
requirementCount: pwMinLength,
}),
},
uppercase: {
value: pwUppercase,
message: phrases.t("identity-block.password-requirements-uppercase", {
requirementCount: pwUppercase,
}),
},
numbers: {
value: pwPwNumbers,
message: phrases.t("identity-block.password-requirements-numbers", {
requirementCount: pwPwNumbers,
}),
},
specialCharacters: {
value: pwSpecialCharacters,
message: phrases.t("identity-block.password-requirements-uppercase", {
requirementCount: pwUppercase,
}),
},
},
});

if (isAdmin || (isInitialized && nonce)) {
if (submitted) {
return (
<HeadlinedSubmitForm
headline={phrases.t("identity-block.reset-password-headline-submitted")}
buttonLabel={phrases.t("identity-block.reset-password-submit-submitted")}
onSubmit={() => {
const redirect = validateURL(successActionURL);
window.location.assign(redirect);
}}
>
<Paragraph>{phrases.t("identity-block.reset-password-instruction-submitted")}</Paragraph>
</HeadlinedSubmitForm>
);
}

return (
<HeadlinedSubmitForm
buttonLabel={phrases.t("identity-block.reset-password-submit")}
className={BLOCK_CLASS_NAME}
formErrorText={error}
headline={phrases.t("identity-block.reset-password-headline")}
onSubmit={({ newPassword }) => {
if (!isAdmin) {
Identity.resetPassword(nonce, newPassword)
.then(() => {
setSubmitted(true);
})
.catch(() => setError(phrases.t("identity-block.reset-password-error")));
}
}}
>
<Paragraph>{phrases.t("identity-block.reset-password-instruction")}</Paragraph>
<FormPasswordConfirm
autoComplete="new-password"
name="newPassword"
label={phrases.t("identity-block.password")}
validationErrorMessage={status === "success" ? passwordErrorMessage : ""}
validationPattern={validatePasswordPattern(
pwLowercase,
pwMinLength,
pwPwNumbers,
pwSpecialCharacters,
pwUppercase
)}
confirmLabel={phrases.t("identity-block.confirm-password")}
confirmValidationErrorMessage={phrases.t("identity-block.confirm-password-error")}
/>
</HeadlinedSubmitForm>
);
}
return null;
};

const ResetPassword = ({ customFields }) => {
const { successActionURL = defaultSuccessURL } = customFields;
const { isAdmin, siteProperties } = useFusionContext();
const { locale } = siteProperties;
const phrases = getTranslatedPhrases(locale);

return (
<ResetPasswordPresentation
isAdmin={isAdmin}
phrases={phrases}
successActionURL={successActionURL}
/>
);
};

ResetPassword.label = "Identity Reset Password - Arc Block";

ResetPassword.icon = "redo";

ResetPassword.propTypes = {
customFields: PropTypes.shape({
successActionURL: PropTypes.string.tag({
name: "Successful Action URL",
defaultValue: "/account/login/",
}),
}),
};

export default ResetPassword;
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import React from "react";

import { ResetPasswordPresentation } from "./default";

export default {
title: "Blocks/Identity/Blocks/Reset Password",
parameters: {
chromatic: { viewports: [320, 1200] },
},
};

const phrases = {
t: (phrase) =>
({
"identity-block.confirm-password": "Confirm Password",
"identity-block.confirm-password-error": "Passwords must match",
"identity-block.password": "Password",
"identity-block.password-requirements": "Passwords must meet our strict requirements.",
"identity-block.password-requirements-characters": "Characters",
"identity-block.password-requirements-lowercase": "Lowercase",
"identity-block.password-requirements-numbers": "Numbers",
"identity-block.password-requirements-uppercase": "Uppercase",
"identity-block.reset-password-error": "There was an error with your password",
"identity-block.reset-password-headline": "Create your new password",
"identity-block.reset-password-headline-submitted": "New password saved",
"identity-block.reset-password-instruction": "Enter a new password for your account.",
"identity-block.reset-password-instruction-submitted": "Your new password has been saved.",
"identity-block.reset-password-submit": "Continue",
"identity-block.reset-password-submit-submitted": "Continue to Login",
}[phrase]),
};

export const basic = () => (
<ResetPasswordPresentation isAdmin phrases={phrases} successActionUrl="./" />
);
Loading

0 comments on commit 9ca6bb6

Please sign in to comment.