Skip to content

Commit

Permalink
EXUI-2058: Add error message on staffUI (#3778)
Browse files Browse the repository at this point in the history
* Add ngrx store for staff ui, Add error handling when staff user select returns 404

* add Unit tests

* Code review comments resolved

---------

Co-authored-by: RiteshHMCTS <[email protected]>
  • Loading branch information
Josh-HMCTS and RiteshHMCTS committed Jul 22, 2024
1 parent 069b8bc commit 97962c9
Show file tree
Hide file tree
Showing 13 changed files with 231 additions and 6 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<div class="govuk-!-margin-top-3">
<exui-info-message-container></exui-info-message-container>
<exui-error-message *ngIf="staffSelectError" [error]="staffSelectErrors"></exui-error-message>
</div>

<main class="govuk-main-wrapper govuk-main-wrapper--auto-spacing" id="main-content" role="main">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { Store } from '@ngrx/store';
import SpyObj = jasmine.SpyObj;
import { of } from 'rxjs';
import { ErrorMessage } from '../../../app/models';
Expand All @@ -15,6 +16,7 @@ describe('StaffUsersComponent', () => {
let fixture: ComponentFixture<StaffUsersComponent>;
let infoMessageCommMock: SpyObj<InfoMessageCommService>;
let staffDataFilterServiceMock: Partial<StaffDataFilterService>;
let storeMock: SpyObj<Store>;

beforeEach(waitForAsync(() => {
infoMessageCommMock = jasmine.createSpyObj('InfoMessageCommService', ['removeAllMessages']);
Expand All @@ -26,6 +28,8 @@ describe('StaffUsersComponent', () => {
errors: []
} as ErrorMessage)
};
storeMock = jasmine.createSpyObj('Store', ['pipe']);
storeMock.pipe.and.returnValue(of([]));

TestBed.configureTestingModule({
imports: [
Expand All @@ -36,7 +40,8 @@ describe('StaffUsersComponent', () => {
],
providers: [
{ provide: StaffDataFilterService, useValue: staffDataFilterServiceMock },
{ provide: InfoMessageCommService, useValue: infoMessageCommMock }
{ provide: InfoMessageCommService, useValue: infoMessageCommMock },
{ provide: Store, useValue: storeMock }
],
schemas: [NO_ERRORS_SCHEMA]
})
Expand All @@ -59,6 +64,7 @@ describe('StaffUsersComponent', () => {
component.advancedSearchClicked();
expect(component.advancedSearchEnabled).toBe(true);
});

it('should call removeAllMessages', () => {
// @ts-expect-error - infoMessageCommService is private
expect(component.infoMessageCommService.removeAllMessages).not.toHaveBeenCalled();
Expand All @@ -67,4 +73,11 @@ describe('StaffUsersComponent', () => {
expect(component.infoMessageCommService.removeAllMessages).toHaveBeenCalled();
});
});

it('should update staffSelectError and staffSelectErrors when error is emitted', () => {
storeMock.pipe.and.returnValue(of({ errorDescription: 'Test error' }));
component = new StaffUsersComponent(staffDataFilterServiceMock as any, infoMessageCommMock, storeMock);
expect(component.staffSelectError).toBe(true);
expect(component.staffSelectErrors).toEqual({ title: 'Staff error', description: 'Test error' });
});
});
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { Component } from '@angular/core';
import { Store, select } from '@ngrx/store';
import { Observable, Subject, takeUntil, tap } from 'rxjs';
import { InfoMessageCommService } from '../../../app/shared/services/info-message-comms.service';
import { StaffDataFilterService } from '../../components/staff-users/services/staff-data-filter/staff-data-filter.service';
import { selectStaffError } from '../../store/selectors/staff-select.selector';

@Component({
selector: 'exui-staff-users',
Expand All @@ -10,11 +13,31 @@ import { StaffDataFilterService } from '../../components/staff-users/services/st
})
export class StaffUsersComponent {
public advancedSearchEnabled = false;
public staffSelectError: boolean = false;
public staffSelectErrors: { title: string, description: string } = null;
public errorSubscription$: Observable<any>;
private unsubscribe$: Subject<void> = new Subject<void>();

constructor(
public staffDataFilterService: StaffDataFilterService,
private infoMessageCommService: InfoMessageCommService,
) {}
private store: Store
) {
this.errorSubscription$ = this.store.pipe(select(selectStaffError));
this.errorSubscription$.pipe(
takeUntil(this.unsubscribe$),
tap((error) => {
this.staffSelectError = !!error;
this.staffSelectErrors = error ? { title: 'Staff error', description: error?.errorDescription ? error?.errorDescription: 'An unknown error has occured' } : null;
})
)
.subscribe();
}

ngOnDestroy() {
this.unsubscribe$.next();
this.unsubscribe$.complete();
}

public advancedSearchClicked(): void {
this.advancedSearchEnabled = !this.advancedSearchEnabled;
Expand Down
5 changes: 5 additions & 0 deletions src/staff-administrator/models/staff-state.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { StaffUserResponseError } from './staff-user-response-error.model';

export interface StaffState {
staffGetError: null | StaffUserResponseError;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export interface StaffUserResponseError {
errorCode: number;
errorDescription: string;
errorMessage: string;
status: string;
timeStamp: string;
}
Original file line number Diff line number Diff line change
@@ -1,19 +1,29 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot } from '@angular/router';
import { map } from 'rxjs/operators';
import { Store } from '@ngrx/store';
import { catchError, map } from 'rxjs/operators';
import { of } from 'rxjs';
import { StaffUser } from '../models/staff-user.model';
import { StaffDataAccessService } from '../services/staff-data-access/staff-data-access.service';
import { SetError, ResetStaffSelect } from '../store/actions/staff-select.action';

@Injectable({
providedIn: 'root'
})
export class StaffUserDetailsResolverService {
constructor(private staffDataAccessService: StaffDataAccessService) {
}
constructor(
private staffDataAccessService: StaffDataAccessService,
private store: Store
) {}

public resolve(route?: ActivatedRouteSnapshot) {
this.store.dispatch(new ResetStaffSelect());
return this.staffDataAccessService.fetchSingleUserById(route.params.id).pipe(
map((user) => StaffUser.from(user))
map((user) => StaffUser.from(user)),
catchError((error) => {
this.store.dispatch(new SetError(error.error));
return of();
})
);
}
}
3 changes: 3 additions & 0 deletions src/staff-administrator/staff-administrator.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { CdkTableModule } from '@angular/cdk/table';
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
import { StoreModule } from '@ngrx/store';
import { MatLegacyAutocompleteModule as MatAutocompleteModule } from '@angular/material/legacy-autocomplete';
import { MatLegacyTooltipModule as MatTooltipModule } from '@angular/material/legacy-tooltip';
import { ExuiCommonLibModule } from '@hmcts/rpx-xui-common-lib';
Expand All @@ -28,6 +29,7 @@ import { StaffFilterOptionsUserTypesResolver } from './resolvers/staff-filter-op
import { StaffUserDetailsResolverService } from './resolvers/staff-user-details-resolver.service';
import { StaffDataAccessService } from './services/staff-data-access/staff-data-access.service';
import { staffAdministratorRouting } from './staff-administrator.routes';
import { staffSelectReducer } from './store/reducers/staff-select.reducer';

@NgModule({
declarations: [
Expand Down Expand Up @@ -55,6 +57,7 @@ import { staffAdministratorRouting } from './staff-administrator.routes';
ReactiveFormsModule,
staffAdministratorRouting,
NgxPaginationModule,
StoreModule.forFeature('staffUI', staffSelectReducer),
MatTooltipModule,
MatAutocompleteModule
],
Expand Down
32 changes: 32 additions & 0 deletions src/staff-administrator/store/actions/staff-select.action.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { StaffUserResponseError } from '../../models/staff-user-response-error.model';
import { SetError, ResetStaffSelect, SET_STAFF_SELECT_ERROR, RESET_STAFF_SELECT_ERROR } from './staff-select.action';

describe('StaffSelect Actions', () => {
describe('SetError', () => {
it('should create an action', () => {
const error: StaffUserResponseError = {
errorMessage: 'An error occurred.',
errorCode: 404,
errorDescription: 'A description of the error.',
status: 'Failed',
timeStamp: new Date().toISOString()
};
const action = new SetError(error);

expect({ ...action }).toEqual({
type: SET_STAFF_SELECT_ERROR,
payload: error
});
});
});

describe('ResetStaffSelect', () => {
it('should create an action', () => {
const action = new ResetStaffSelect();

expect({ ...action }).toEqual({
type: RESET_STAFF_SELECT_ERROR
});
});
});
});
18 changes: 18 additions & 0 deletions src/staff-administrator/store/actions/staff-select.action.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Action } from '@ngrx/store';
import { StaffUserResponseError } from '../../models/staff-user-response-error.model';

export const SET_STAFF_SELECT_ERROR = '[Staff Select] Set Select Staff Error';
export const RESET_STAFF_SELECT_ERROR = '[Staff Select] Reset Staff Select Error';

export class SetError implements Action {
public readonly type = SET_STAFF_SELECT_ERROR;
constructor(public payload: StaffUserResponseError) { }
}

export class ResetStaffSelect implements Action {
public readonly type = RESET_STAFF_SELECT_ERROR;
}

export type StaffSelectAction =
| SetError
| ResetStaffSelect;
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { TestBed } from '@angular/core/testing';
import { provideMockStore } from '@ngrx/store/testing';
import { staffSelectReducer, initialState } from './staff-select.reducer';
import * as ErrorActions from '../actions/staff-select.action';
import { StaffUserResponseError } from '../../models/staff-user-response-error.model';

describe('staffSelectReducer', () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [provideMockStore({ initialState })]
});
});

it('should return the initial state', () => {
const action = { type: 'NOOP' } as any;
const result = staffSelectReducer(initialState, action);

expect(result).toEqual(initialState);
});

it('should handle RESET_STAFF_SELECT_ERROR', () => {
const action = new ErrorActions.ResetStaffSelect();
const result = staffSelectReducer(initialState, action);

expect(result).toEqual(initialState);
});

it('should handle SET_STAFF_SELECT_ERROR', () => {
const error: StaffUserResponseError = {
errorMessage: 'Test error',
errorCode: 123,
errorDescription: 'Test error description',
status: 'Error',
timeStamp: '2022-01-01T00:00:00Z'
};
const action = new ErrorActions.SetError(error);
const result = staffSelectReducer(initialState, action);

expect(result).toEqual({
...initialState,
staffGetError: error
});
});
});
31 changes: 31 additions & 0 deletions src/staff-administrator/store/reducers/staff-select.reducer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { StaffState } from '../../models/staff-state.model';
import * as ErrorActions from '../actions/staff-select.action';

export const initialState: StaffState = {
staffGetError: null
};

export function staffSelectReducer(
currentState = initialState,
action: ErrorActions.StaffSelectAction
) {
switch (action.type) {
case ErrorActions.RESET_STAFF_SELECT_ERROR: {
return {
...initialState
};
}
case ErrorActions.SET_STAFF_SELECT_ERROR: {
return {
...currentState,
staffGetError: action.payload
};
}
default: {
return {
...currentState
};
}
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { TestBed } from '@angular/core/testing';
import { StoreModule, Store, select } from '@ngrx/store';
import { selectStaffError } from './staff-select.selector';
import { staffSelectReducer } from '../reducers/staff-select.reducer';

describe('Staff Selectors', () => {
let store: Store;

beforeEach(() => {
TestBed.configureTestingModule({
imports: [
StoreModule.forRoot({}),
StoreModule.forFeature('staffUI', staffSelectReducer)
]
});
store = TestBed.inject(Store);
spyOn(store, 'dispatch').and.callThrough();
});

describe('getHearingConditions', () => {
it('should return hearings navigation state', () => {
let result = null;
store.pipe(select(selectStaffError)).subscribe((value) => {
result = value;
});
expect(result).toBeNull();
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { createFeatureSelector, createSelector } from '@ngrx/store';
import { StaffState } from '../../models/staff-state.model';

export const selectStaffFeature = createFeatureSelector<any>('staffUI');

export const selectStaffError = createSelector(
selectStaffFeature,
(state: StaffState) => state.staffGetError
);

0 comments on commit 97962c9

Please sign in to comment.