diff --git a/frontend/src/app/features/login/login.component.html b/frontend/src/app/features/login/login.component.html
index 4a0117e46d..16db339ac8 100644
--- a/frontend/src/app/features/login/login.component.html
+++ b/frontend/src/app/features/login/login.component.html
@@ -17,14 +17,25 @@
Sign in
[class.govuk-form-group--error]="(form.get('username').errors || serverError) && submitted"
>
+
+ You cannot use an email address to sign in
+
+
- Error:
- {{ getFormErrorMessage('username', 'required') }}
+
+ Error:
+ {{ getFormErrorMessage('username', 'required') }}
+
+
+
+ Error:
+ {{ getFormErrorMessage('username', 'atSignInUsername') }}
+
Sign in
>
Error: {{ getFormErrorMessage('password', 'required') }}
+
+ {{ showPassword ? 'Hide' : 'Show' }} password
-
+
diff --git a/frontend/src/app/features/login/login.component.scss b/frontend/src/app/features/login/login.component.scss
new file mode 100644
index 0000000000..faf12e9c51
--- /dev/null
+++ b/frontend/src/app/features/login/login.component.scss
@@ -0,0 +1,5 @@
+@import 'govuk-frontend/govuk/base';
+
+.asc-colour-black {
+ color: $govuk-text-colour;
+}
diff --git a/frontend/src/app/features/login/login.component.spec.ts b/frontend/src/app/features/login/login.component.spec.ts
index d41d0a7544..fb46fc7e26 100644
--- a/frontend/src/app/features/login/login.component.spec.ts
+++ b/frontend/src/app/features/login/login.component.spec.ts
@@ -9,17 +9,19 @@ import { MockAuthService } from '@core/test-utils/MockAuthService';
import { MockUserService } from '@core/test-utils/MockUserService';
import { FeatureFlagsService } from '@shared/services/feature-flags.service';
import { SharedModule } from '@shared/shared.module';
-import { render } from '@testing-library/angular';
+import { fireEvent, render, within } from '@testing-library/angular';
import { throwError } from 'rxjs';
import { LoginComponent } from './login.component';
+import { ReactiveFormsModule, UntypedFormBuilder } from '@angular/forms';
describe('LoginComponent', () => {
async function setup(isAdmin = false, employerTypeSet = true, isAuthenticated = true) {
- const { fixture, getAllByText, getByText, queryByText, getByTestId } = await render(LoginComponent, {
- imports: [SharedModule, RouterModule, RouterTestingModule, HttpClientTestingModule],
+ const setupTools = await render(LoginComponent, {
+ imports: [SharedModule, RouterModule, RouterTestingModule, HttpClientTestingModule, ReactiveFormsModule],
providers: [
FeatureFlagsService,
+ UntypedFormBuilder,
{
provide: AuthService,
useFactory: MockAuthService.factory(true, isAdmin, employerTypeSet),
@@ -38,7 +40,7 @@ describe('LoginComponent', () => {
spy.and.returnValue(Promise.resolve(true));
const authService = injector.inject(AuthService) as AuthService;
- let authSpy;
+ let authSpy: any;
if (isAuthenticated) {
authSpy = spyOn(authService, 'authenticate');
authSpy.and.callThrough();
@@ -53,14 +55,13 @@ describe('LoginComponent', () => {
authSpy = spyOn(authService, 'authenticate').and.returnValue(throwError(mockErrorResponse));
}
+ const fixture = setupTools.fixture;
const component = fixture.componentInstance;
+
return {
component,
fixture,
- getAllByText,
- getByText,
- queryByText,
- getByTestId,
+ ...setupTools,
spy,
authSpy,
};
@@ -71,6 +72,82 @@ describe('LoginComponent', () => {
expect(component).toBeTruthy();
});
+ describe('username', () => {
+ it('should show the username hint', async () => {
+ const { getByTestId } = await setup();
+
+ const usernameHint = getByTestId('username-hint');
+ const hintText = 'You cannot use an email address to sign in';
+
+ expect(within(usernameHint).getByText(hintText)).toBeTruthy();
+ });
+ });
+
+ describe('password', () => {
+ it('should set the password as password field (to hide input) on page load', async () => {
+ const { getByTestId } = await setup();
+
+ const passwordInput = getByTestId('password');
+
+ expect(passwordInput.getAttribute('type')).toEqual('password');
+ });
+
+ it("should show the password as text field after user clicks 'Show password'", async () => {
+ const { fixture, getByTestId, getByText } = await setup();
+
+ const showToggleText = 'Show password';
+
+ fireEvent.click(getByText(showToggleText));
+ fixture.detectChanges();
+
+ const passwordInput = getByTestId('password');
+
+ expect(passwordInput.getAttribute('type')).toEqual('text');
+ });
+
+ it("should initially show 'Show password' text for the password toggle", async () => {
+ const { getByTestId } = await setup();
+
+ const passwordToggle = getByTestId('password-toggle');
+ const toggleText = 'Show password';
+
+ expect(within(passwordToggle).getByText(toggleText)).toBeTruthy();
+ });
+
+ it("should show 'Hide password' text for the password toggle when 'Show password' is clicked", async () => {
+ const { fixture, getByTestId, getByText } = await setup();
+
+ const passwordToggle = getByTestId('password-toggle');
+ const showToggleText = 'Show password';
+ const hideToggleText = 'Hide password';
+
+ fireEvent.click(getByText(showToggleText));
+ fixture.detectChanges();
+
+ expect(within(passwordToggle).getByText(hideToggleText)).toBeTruthy();
+ });
+ });
+
+ it('should show the link to forgot username or password', async () => {
+ const { getByTestId } = await setup();
+
+ const forgotUsernamePasswordText = 'Forgot your username or password?';
+ const forgotUsernamePasswordLink = getByTestId('forgot-username-password');
+
+ expect(within(forgotUsernamePasswordLink).getByText(forgotUsernamePasswordText)).toBeTruthy();
+ expect(forgotUsernamePasswordLink.getAttribute('href')).toEqual('/forgot-your-username-or-password');
+ });
+
+ it('should show the link to create an account', async () => {
+ const { getByTestId } = await setup();
+
+ const createAccountText = 'Create an account';
+ const createAccountLink = getByTestId('create-account');
+
+ expect(within(createAccountLink).getByText(createAccountText)).toBeTruthy();
+ expect(createAccountLink.getAttribute('href')).toEqual('/registration/create-account');
+ });
+
it('should send you to dashboard on login as user', async () => {
const { component, fixture, spy, authSpy } = await setup();
@@ -165,5 +242,26 @@ describe('LoginComponent', () => {
fixture.detectChanges();
expect(getAllByText('Your username or your password is incorrect')).toBeTruthy();
});
+
+ it('should not let you sign in with a username with special characters', async () => {
+ const { component, fixture, getAllByText, getByTestId } = await setup();
+
+ const signInButton = within(getByTestId('signinButton')).getByText('Sign in');
+ const form = component.form;
+
+ component.form.markAsDirty();
+ form.controls['username'].setValue('username@123.com');
+ form.controls['username'].markAsDirty();
+ component.form.get('password').setValue('1');
+ component.form.get('password').markAsDirty();
+
+ fireEvent.click(signInButton);
+ fixture.detectChanges();
+
+ expect(form.invalid).toBeTruthy();
+ expect(
+ getAllByText("You've entered an @ symbol (remember, your username cannot be an email address)").length,
+ ).toBe(2);
+ });
});
});
diff --git a/frontend/src/app/features/login/login.component.ts b/frontend/src/app/features/login/login.component.ts
index e0e48e232c..902e06924b 100644
--- a/frontend/src/app/features/login/login.component.ts
+++ b/frontend/src/app/features/login/login.component.ts
@@ -1,6 +1,13 @@
import { HttpErrorResponse } from '@angular/common/http';
import { AfterViewInit, Component, ElementRef, OnDestroy, OnInit, ViewChild } from '@angular/core';
-import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
+import {
+ AbstractControl,
+ UntypedFormBuilder,
+ UntypedFormGroup,
+ ValidationErrors,
+ ValidatorFn,
+ Validators,
+} from '@angular/forms';
import { Router } from '@angular/router';
import { ErrorDefinition, ErrorDetails } from '@core/model/errorSummary.model';
import { AuthService } from '@core/services/auth.service';
@@ -14,6 +21,7 @@ import { Subscription } from 'rxjs';
@Component({
selector: 'app-login',
templateUrl: './login.component.html',
+ styleUrls: ['./login.component.scss'],
})
export class LoginComponent implements OnInit, OnDestroy, AfterViewInit {
@ViewChild('formEl') formEl: ElementRef;
@@ -23,6 +31,7 @@ export class LoginComponent implements OnInit, OnDestroy, AfterViewInit {
public formErrorsMap: Array;
public serverErrorsMap: Array;
public serverError: string;
+ public showPassword: boolean = false;
constructor(
private idleService: IdleService,
@@ -36,8 +45,20 @@ export class LoginComponent implements OnInit, OnDestroy, AfterViewInit {
ngOnInit() {
this.form = this.formBuilder.group({
- username: [null, { validators: [Validators.required], updateOn: 'submit' }],
- password: [null, { validators: [Validators.required], updateOn: 'submit' }],
+ username: [
+ null,
+ {
+ validators: [Validators.required, this.checkUsernameForAtSign()],
+ updateOn: 'submit',
+ },
+ ],
+ password: [
+ null,
+ {
+ validators: [Validators.required],
+ updateOn: 'submit',
+ },
+ ],
});
this.setupFormErrorsMap();
@@ -52,6 +73,20 @@ export class LoginComponent implements OnInit, OnDestroy, AfterViewInit {
this.subscriptions.unsubscribe();
}
+ public checkUsernameForAtSign(): ValidatorFn {
+ return (control: AbstractControl): ValidationErrors | null => {
+ const value = control.value;
+
+ if (!value) {
+ return null;
+ }
+
+ const userNameHasAtSign = /@/.test(value);
+
+ return userNameHasAtSign ? { atSignInUsername: true } : null;
+ };
+ }
+
public setupFormErrorsMap(): void {
this.formErrorsMap = [
{
@@ -61,6 +96,10 @@ export class LoginComponent implements OnInit, OnDestroy, AfterViewInit {
name: 'required',
message: 'Enter your username',
},
+ {
+ name: 'atSignInUsername',
+ message: "You've entered an @ symbol (remember, your username cannot be an email address)",
+ },
],
},
{
@@ -154,4 +193,9 @@ export class LoginComponent implements OnInit, OnDestroy, AfterViewInit {
),
);
}
+
+ public setShowPassword(event: Event): void {
+ event.preventDefault();
+ this.showPassword = !this.showPassword;
+ }
}