Skip to content

Commit

Permalink
Merge pull request #6446 from NMDSdevopsServiceAdm/feat/1567-forgotte…
Browse files Browse the repository at this point in the history
…n-username-changes-to-log-in-page

Feat/1567 forgotten username changes to log in page
  • Loading branch information
ssrome authored Dec 9, 2024
2 parents d38ad7f + 545e7c9 commit cfc3af2
Show file tree
Hide file tree
Showing 4 changed files with 192 additions and 17 deletions.
40 changes: 34 additions & 6 deletions frontend/src/app/features/login/login.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,25 @@ <h1 class="govuk-fieldset__heading" data-cy="signin-heading">Sign in</h1>
[class.govuk-form-group--error]="(form.get('username').errors || serverError) && submitted"
>
<label class="govuk-label" for="username"> Username </label>
<div id="username-hint" class="govuk-hint asc-colour-black" data-testid="username-hint">
You cannot use an email address to sign in
</div>

<span
id="username-error"
class="govuk-error-message"
*ngIf="form.get('username').errors && submitted"
data-cy="username-error"
>
<span class="govuk-visually-hidden">Error:</span>
{{ getFormErrorMessage('username', 'required') }}
<ng-container *ngIf="form.get('username').hasError('required')">
<span class="govuk-visually-hidden">Error:</span>
{{ getFormErrorMessage('username', 'required') }}
</ng-container>

<ng-container *ngIf="form.get('username').hasError('atSignInUsername')">
<span class="govuk-visually-hidden">Error:</span>
{{ getFormErrorMessage('username', 'atSignInUsername') }}
</ng-container>
</span>
<input
data-cy="username"
Expand All @@ -50,22 +61,39 @@ <h1 class="govuk-fieldset__heading" data-cy="signin-heading">Sign in</h1>
>
<span class="govuk-visually-hidden">Error:</span> {{ getFormErrorMessage('password', 'required') }}
</span>

<input
data-cy="password"
class="govuk-input govuk-input--width-20"
[class.govuk-input--error]="(form.get('password').errors || serverError) && submitted"
id="password"
name="password"
type="password"
[type]="showPassword ? 'text' : 'password'"
[formControlName]="'password'"
data-testid="password"
/>
<a
class="govuk-body-m govuk-link govuk-link--no-visited-state govuk-!-margin-left-3"
href="#"
data-testid="password-toggle"
(click)="setShowPassword($event)"
>{{ showPassword ? 'Hide' : 'Show' }} password</a
>
</div>
</fieldset>

<button type="submit" class="govuk-button" data-testid="signinButton">Sign in</button>
<button type="submit" class="govuk-button govuk-!-margin-top-3" data-testid="signinButton">Sign in</button>
</form>

<ul class="govuk-list">
<li><a [routerLink]="['/forgot-your-password']">Forgot your password?</a></li>
<li><a [routerLink]="['/registration', 'create-account']" data-cy="create-account">Create an account</a></li>
<li>
<a [routerLink]="['/forgot-your-username-or-password']" data-testid="forgot-username-password"
>Forgot your username or password?</a
>
</li>
<li>
<a [routerLink]="['/registration', 'create-account']" data-cy="create-account" data-testid="create-account"
>Create an account</a
>
</li>
</ul>
5 changes: 5 additions & 0 deletions frontend/src/app/features/login/login.component.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
@import 'govuk-frontend/govuk/base';

.asc-colour-black {
color: $govuk-text-colour;
}
114 changes: 106 additions & 8 deletions frontend/src/app/features/login/login.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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();
Expand All @@ -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,
};
Expand All @@ -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();

Expand Down Expand Up @@ -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('[email protected]');
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);
});
});
});
50 changes: 47 additions & 3 deletions frontend/src/app/features/login/login.component.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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;
Expand All @@ -23,6 +31,7 @@ export class LoginComponent implements OnInit, OnDestroy, AfterViewInit {
public formErrorsMap: Array<ErrorDetails>;
public serverErrorsMap: Array<ErrorDefinition>;
public serverError: string;
public showPassword: boolean = false;

constructor(
private idleService: IdleService,
Expand All @@ -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();
Expand All @@ -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 = [
{
Expand All @@ -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)",
},
],
},
{
Expand Down Expand Up @@ -154,4 +193,9 @@ export class LoginComponent implements OnInit, OnDestroy, AfterViewInit {
),
);
}

public setShowPassword(event: Event): void {
event.preventDefault();
this.showPassword = !this.showPassword;
}
}

0 comments on commit cfc3af2

Please sign in to comment.