Skip to content

Commit b618cdc

Browse files
authored
feat(login): Show username field on its own before password field MAASENG-5636 (#5865)
- Login page now only shows `username` at the start and a "Next" button - Password field shows after username is entered and "Next" is clicked Resolves [MAASENG-5636](https://warthogs.atlassian.net/browse/MAASENG-5636)
1 parent 6b07392 commit b618cdc

File tree

4 files changed

+65
-36
lines changed

4 files changed

+65
-36
lines changed

.github/workflows/pr-lint.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ jobs:
2727
images
2828
intro
2929
kvm
30+
login
3031
machines
3132
networkDiscovery
3233
networks

cypress/e2e/with-users/login/login.spec.ts

Lines changed: 5 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -10,41 +10,18 @@ context("Login page", () => {
1010
cy.findByRole("button", { name: "Login" }).should("be.disabled");
1111
});
1212

13-
it("shows form errors if username is focused and blurred while empty", () => {
14-
cy.get("input[name='username']").focus();
15-
cy.get("input[name='username']").blur();
16-
cy.get(".p-form-validation__message").should("exist");
17-
});
18-
19-
it("shows form errors if password is focused and blurred while empty", () => {
20-
cy.get("input[name='password']").focus();
21-
cy.get("input[name='password']").blur();
22-
cy.get(".p-form-validation__message").should("exist");
23-
});
24-
2513
it("displays an error message if submitted invalid login credentials", () => {
2614
cy.findByRole("textbox", { name: /Username/ }).type("invalid-username");
15+
cy.findByRole("button", { name: /Next/ }).click();
2716
cy.findByLabelText(/Password/).type("invalid-password{enter}");
2817
cy.findByRole("alert")
2918
.should("be.visible")
3019
.should("include.text", "Please enter a correct username and password");
3120
});
3221

33-
it("enables the form if both fields have values", () => {
34-
cy.findByRole("button", { name: "Login" }).should("be.disabled");
35-
cy.get("input[name='username']").type("username");
36-
cy.get("input[name='password']").type("password");
37-
cy.findByRole("button", { name: "Login" }).should("not.be.disabled");
38-
});
39-
40-
it("displays an error notification if wrong credentials provided", () => {
41-
cy.get("input[name='username']").type("username");
42-
cy.get("input[name='password']").type("incorrect-password{enter}");
43-
cy.get(".p-notification--negative").should("exist");
44-
});
45-
4622
it("logs in and redirects to the intro", () => {
4723
cy.get("input[name='username']").type(Cypress.env("username"));
24+
cy.findByRole("button", { name: /Next/ }).click();
4825
cy.get("input[name='password']").type(Cypress.env("password"));
4926
cy.get("button[type='submit']").click();
5027
cy.location("pathname").should("eq", generateMAASURL("/intro"));
@@ -53,6 +30,7 @@ context("Login page", () => {
5330
it("logs in and redirects to the user intro if setup intro complete", () => {
5431
// Log in - should go to setup intro.
5532
cy.get("input[name='username']").type(Cypress.env("username"));
33+
cy.findByRole("button", { name: /Next/ }).click();
5634
cy.get("input[name='password']").type(Cypress.env("password"));
5735
cy.get("button[type='submit']").click();
5836
cy.location("pathname").should("eq", generateMAASURL("/intro"));
@@ -66,6 +44,7 @@ context("Login page", () => {
6644

6745
// Log in again - should go straight to user intro.
6846
cy.get("input[name='username']").type(Cypress.env("username"));
47+
cy.findByRole("button", { name: /Next/ }).click();
6948
cy.get("input[name='password']").type(Cypress.env("password"));
7049
cy.get("button[type='submit']").click();
7150
cy.location("pathname").should("eq", generateMAASURL("/intro/user"));
@@ -76,6 +55,7 @@ context("Login page", () => {
7655
cy.setCookie("skipsetupintro", "true");
7756
cy.setCookie("skipintro", "true");
7857
cy.get("input[name='username']").type(Cypress.env("username"));
58+
cy.findByRole("button", { name: /Next/ }).click();
7959
cy.get("input[name='password']").type(Cypress.env("password"));
8060
cy.get("button[type='submit']").click();
8161
cy.location("pathname").should("eq", generateMAASURL("/machines"));

src/app/login/Login/Login.test.tsx

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,33 @@ describe("Login", () => {
5050
).toBeInTheDocument();
5151
});
5252

53+
it("hides the password field when a username has not been entered", async () => {
54+
const store = mockStore(state);
55+
renderWithProviders(<Login />, { initialEntries: ["/login"], store });
56+
57+
expect(
58+
screen.getByRole("textbox", { name: Labels.Username })
59+
).toBeInTheDocument();
60+
expect(screen.queryByLabelText(Labels.Password)).not.toBeInTheDocument();
61+
});
62+
63+
it("shows the password field and hides the username field after entering a username and clicking 'Next'", async () => {
64+
const store = mockStore(state);
65+
renderWithProviders(<Login />, { initialEntries: ["/login"], store });
66+
67+
await userEvent.type(
68+
screen.getByRole("textbox", { name: Labels.Username }),
69+
"koala"
70+
);
71+
await userEvent.click(screen.getByRole("button", { name: "Next" }));
72+
73+
expect(screen.getByLabelText(Labels.Password)).toBeInTheDocument();
74+
expect(
75+
screen.queryByRole("textbox", { name: Labels.Username })
76+
).not.toBeInTheDocument();
77+
expect(screen.getByRole("button")).toHaveTextContent("Login");
78+
});
79+
5380
it("can login via the api", async () => {
5481
const store = mockStore(state);
5582
renderWithProviders(<Login />, { initialEntries: ["/login"], store });
@@ -58,6 +85,7 @@ describe("Login", () => {
5885
screen.getByRole("textbox", { name: Labels.Username }),
5986
"koala"
6087
);
88+
await userEvent.click(screen.getByRole("button", { name: "Next" }));
6189
await userEvent.type(screen.getByLabelText(Labels.Password), "gumtree");
6290
await userEvent.click(screen.getByRole("button", { name: Labels.Submit }));
6391

src/app/login/Login/Login.tsx

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useCallback, useEffect } from "react";
1+
import { useCallback, useEffect, useState } from "react";
22

33
import {
44
Button,
@@ -22,10 +22,19 @@ import { statusActions } from "@/app/store/status";
2222
import statusSelectors from "@/app/store/status/selectors";
2323
import { formatErrors } from "@/app/utils";
2424

25-
const LoginSchema = Yup.object().shape({
26-
username: Yup.string().required("Username is required"),
27-
password: Yup.string().required("Password is required"),
28-
});
25+
const generateSchema = (hasEnteredUsername: boolean) => {
26+
if (hasEnteredUsername) {
27+
return Yup.object().shape({
28+
username: Yup.string().required("Username is required"),
29+
password: Yup.string().required("Password is required"),
30+
});
31+
} else {
32+
return Yup.object().shape({
33+
username: Yup.string().required("Username is required"),
34+
password: Yup.string(),
35+
});
36+
}
37+
};
2938

3039
export type LoginValues = {
3140
password: string;
@@ -56,6 +65,9 @@ export const Login = (): React.ReactElement => {
5665
const [searchParams] = useSearchParams();
5766
const redirect = searchParams.get("redirectTo");
5867

68+
// TODO: replace this state with a mutation to check if user is local or OIDC https://warthogs.atlassian.net/browse/MAASENG-5637
69+
const [hasEnteredUsername, setHasEnteredUsername] = useState(false);
70+
5971
const noUsers = useSelector(statusSelectors.noUsers);
6072

6173
const navigate = useNavigate();
@@ -137,24 +149,32 @@ export const Login = (): React.ReactElement => {
137149
username: "",
138150
}}
139151
onSubmit={(values) => {
140-
dispatch(statusActions.login(values));
152+
if (!hasEnteredUsername) {
153+
setHasEnteredUsername(true);
154+
} else {
155+
dispatch(statusActions.login(values));
156+
}
141157
}}
142158
saved={authenticated}
143159
saving={authenticating}
144-
submitLabel={Labels.Submit}
145-
validationSchema={LoginSchema}
160+
submitLabel={hasEnteredUsername ? Labels.Submit : "Next"}
161+
validationSchema={generateSchema(hasEnteredUsername)}
146162
>
147163
<FormikField
148-
label={Labels.Username}
164+
aria-hidden={hasEnteredUsername}
165+
hidden={hasEnteredUsername}
166+
label={hasEnteredUsername ? "" : Labels.Username}
149167
name="username"
150168
required={true}
151169
takeFocus
152170
type="text"
153171
/>
154172
<FormikField
155-
label={Labels.Password}
173+
aria-hidden={!hasEnteredUsername}
174+
hidden={!hasEnteredUsername}
175+
label={!hasEnteredUsername ? "" : Labels.Password}
156176
name="password"
157-
required={true}
177+
required={hasEnteredUsername}
158178
type="password"
159179
/>
160180
</FormikForm>

0 commit comments

Comments
 (0)