-
Notifications
You must be signed in to change notification settings - Fork 27
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
THEMES-762: Identity Blocks | Reset Password block (#1717)
* 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
Showing
14 changed files
with
566 additions
and
40 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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]" }, | ||
|
@@ -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
181
blocks/identity-block/features/reset-password/default.jsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
35 changes: 35 additions & 0 deletions
35
blocks/identity-block/features/reset-password/default.story-ignore.jsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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="./" /> | ||
); |
Oops, something went wrong.