UI: Login page bugs (#11520)

## Addresses #11338 

-  Validate emails on login page
- Fix jumping error state for no email provided ("Email field must be
completed")
- Fix jumping error state for password field
- Fix jumping error state for Forgot password > email field

https://www.loom.com/share/92a238fcd2614d6e8d2655d571aa2757

# Checklist for submitter

If some of the following don't apply, delete the relevant line.

- [x] Changes file added for user-visible changes in `changes/`
- [x] Added/updated tests
- [x] Manual QA for all new/changed functionality

---------

Co-authored-by: Jacob Shandling <jacob@fleetdm.com>
This commit is contained in:
Jacob Shandling 2023-05-09 10:12:29 -07:00 committed by GitHub
parent 70f18dda4a
commit 6b70d11bc6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 73 additions and 32 deletions

View File

@ -0,0 +1 @@
* On the login and password reset pages, added email validation and fixed some minor styling bugs.

View File

@ -5,7 +5,7 @@ import { renderWithSetup } from "test/test-utils";
import ForgotPasswordForm from "./ForgotPasswordForm";
const email = "hi@thegnar.co";
const [validEmail, invalidEmail] = ["hi@thegnar.co", "invalid-email"];
describe("ForgotPasswordForm - component", () => {
const handleSubmit = jest.fn();
@ -33,7 +33,7 @@ describe("ForgotPasswordForm - component", () => {
).toBeInTheDocument();
});
it("should test validation for email field", async () => {
it("correctly validates the email field", async () => {
const { user } = renderWithSetup(
<ForgotPasswordForm handleSubmit={handleSubmit} />
);
@ -43,10 +43,10 @@ describe("ForgotPasswordForm - component", () => {
expect(emailError).toBeInTheDocument();
expect(handleSubmit).not.toHaveBeenCalled();
await user.type(screen.getByPlaceholderText("Email"), "invalid-email");
await user.type(screen.getByPlaceholderText("Email"), invalidEmail);
await user.click(screen.getByRole("button", { name: "Get instructions" }));
emailError = screen.getByText("invalid-email is not a valid email");
emailError = screen.getByText("Email must be a valid email address");
expect(emailError).toBeInTheDocument();
expect(handleSubmit).not.toHaveBeenCalled();
});
@ -56,9 +56,9 @@ describe("ForgotPasswordForm - component", () => {
<ForgotPasswordForm handleSubmit={handleSubmit} />
);
await user.type(screen.getByPlaceholderText("Email"), email);
await user.type(screen.getByPlaceholderText("Email"), validEmail);
await user.click(screen.getByRole("button", { name: "Get instructions" }));
expect(handleSubmit).toHaveBeenCalledWith({ email });
expect(handleSubmit).toHaveBeenCalledWith({ email: validEmail });
});
});

View File

@ -6,12 +6,10 @@ const validate = (formData) => {
const { email } = formData;
const errors = {};
if (!validEmail(email)) {
errors.email = `${email} is not a valid email`;
}
if (!validatePresence(email)) {
errors.email = "Email field must be completed";
} else if (!validEmail(email)) {
errors.email = "Email must be a valid email address";
}
const valid = !size(errors);

View File

@ -5,6 +5,9 @@ import { renderWithSetup } from "test/test-utils";
import LoginForm from "./LoginForm";
const [validEmail, invalidEmail] = ["hi@thegnar.co", "invalid-email"];
const password = "p@ssw0rd";
describe("LoginForm - component", () => {
const settings = { sso_enabled: false };
const submitSpy = jest.fn();
@ -35,7 +38,49 @@ describe("LoginForm - component", () => {
expect(screen.getByPlaceholderText("Password")).toBeInTheDocument();
});
it("it does not submit the form when the form fields have not been filled out", async () => {
it("rejects an empty or invalid email field without submitting", async () => {
const { user } = renderWithSetup(
<LoginForm handleSubmit={submitSpy} ssoSettings={settings} />
);
// enter a valid password
await user.type(screen.getByPlaceholderText("Password"), password);
// try to log in
await user.click(screen.getByRole("button", { name: "Login" }));
expect(
screen.getByText("Email field must be completed")
).toBeInTheDocument();
expect(submitSpy).not.toHaveBeenCalled();
// enter an invalid email
await user.type(screen.getByPlaceholderText("Email"), invalidEmail);
// try to log in again
await user.click(screen.getByRole("button", { name: "Login" }));
expect(
screen.getByText("Email must be a valid email address")
).toBeInTheDocument();
expect(submitSpy).not.toHaveBeenCalled();
});
it("rejects an empty password field without submitting", async () => {
const { user } = renderWithSetup(
<LoginForm handleSubmit={submitSpy} ssoSettings={settings} />
);
await user.type(screen.getByRole("textbox", { name: "Email" }), validEmail);
// try to log in without entering a password
await user.click(screen.getByRole("button", { name: "Login" }));
expect(
screen.getByText("Password field must be completed")
).toBeInTheDocument();
expect(submitSpy).not.toHaveBeenCalled();
});
it("does not submit the form when both fields are empty", async () => {
const { user } = renderWithSetup(
<LoginForm handleSubmit={submitSpy} ssoSettings={settings} />
);
@ -43,26 +88,20 @@ describe("LoginForm - component", () => {
await user.click(screen.getByRole("button", { name: "Login" }));
expect(submitSpy).not.toHaveBeenCalled();
expect(
screen.getByText("Email field must be completed")
).toBeInTheDocument();
});
it("submits the form data when form is submitted", async () => {
it("submits the form data when valid form data is submitted", async () => {
const { user } = renderWithSetup(
<LoginForm handleSubmit={submitSpy} ssoSettings={settings} />
);
await user.type(
screen.getByRole("textbox", { name: "Email" }),
"my@email.com"
);
await user.type(screen.getByPlaceholderText("Password"), "p@ssw0rd");
await user.type(screen.getByRole("textbox", { name: "Email" }), validEmail);
await user.type(screen.getByPlaceholderText("Password"), password);
await user.click(screen.getByRole("button", { name: "Login" }));
expect(submitSpy).toHaveBeenCalledWith({
email: "my@email.com",
password: "p@ssw0rd",
email: validEmail,
password,
});
});
});

View File

@ -1,5 +1,6 @@
import { size } from "lodash";
import validatePresence from "components/forms/validators/validate_presence";
import validateEmail from "components/forms/validators/valid_email";
const validate = (formData) => {
const errors = {};
@ -7,6 +8,8 @@ const validate = (formData) => {
if (!validatePresence(email)) {
errors.email = "Email field must be completed";
} else if (!validateEmail(email)) {
errors.email = "Email must be a valid email address";
}
if (!validatePresence(password)) {

View File

@ -28,12 +28,12 @@ class InputFieldWithIcon extends InputField {
};
renderHeading = () => {
const { error, placeholder, name, label, tooltip } = this.props;
const labelClasses = classnames(`${baseClass}__label`);
const { error, placeholder, name, tooltip } = this.props;
const label = this.props.label ?? placeholder;
if (error) {
return <div className={`${baseClass}__errors`}>{error}</div>;
}
const labelClasses = classnames(`${baseClass}__label`, {
[`${baseClass}__errors`]: !!error,
});
return (
<label
@ -41,10 +41,12 @@ class InputFieldWithIcon extends InputField {
className={labelClasses}
data-has-tooltip={!!tooltip}
>
{tooltip ? (
<TooltipWrapper tipContent={tooltip}>{label}</TooltipWrapper>
{tooltip && !error ? (
<TooltipWrapper position="top" tipContent={tooltip}>
{label}
</TooltipWrapper>
) : (
<>{label || placeholder}</>
<>{error || label}</>
)}
</label>
);

View File

@ -73,8 +73,6 @@
}
&__errors {
font-size: $x-small;
font-weight: $bold;
color: $core-vibrant-red;
}