From 080e0bee73b24b292baa80337ff66d47e8d6af63 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Tue, 2 Apr 2019 18:29:44 +0200 Subject: [PATCH] Added tests and comments --- .../tasks/claimed-task-data.service.spec.ts | 108 +++++++++++++++ .../core/tasks/claimed-task-data.service.ts | 57 +++++++- .../tasks/models/claimed-task-object.model.ts | 3 + .../normalized-claimed-task-object.model.ts | 2 +- .../normalized-pool-task-object.model.ts | 2 +- .../models/normalized-task-object.model.ts | 2 +- .../tasks/models/pool-task-object.model.ts | 3 + .../core/tasks/models/task-object.model.ts | 3 + .../core/tasks/pool-task-data.service.spec.ts | 70 ++++++++++ src/app/core/tasks/pool-task-data.service.ts | 35 ++++- .../tasks/task-response-parsing.service.ts | 16 +++ src/app/core/tasks/tasks.service.spec.ts | 130 ++++++++++++++++++ src/app/core/tasks/tasks.service.ts | 56 +++++++- src/app/shared/mocks/mock-request.service.ts | 1 + 14 files changed, 475 insertions(+), 13 deletions(-) create mode 100644 src/app/core/tasks/claimed-task-data.service.spec.ts create mode 100644 src/app/core/tasks/pool-task-data.service.spec.ts create mode 100644 src/app/core/tasks/tasks.service.spec.ts diff --git a/src/app/core/tasks/claimed-task-data.service.spec.ts b/src/app/core/tasks/claimed-task-data.service.spec.ts new file mode 100644 index 0000000000..dbd180b1b3 --- /dev/null +++ b/src/app/core/tasks/claimed-task-data.service.spec.ts @@ -0,0 +1,108 @@ +import { HttpClient, HttpHeaders } from '@angular/common/http'; + +import { Store } from '@ngrx/store'; + +import { getMockRequestService } from '../../shared/mocks/mock-request.service'; +import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service-stub'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; +import { CoreState } from '../core.reducers'; +import { ClaimedTaskDataService } from './claimed-task-data.service'; +import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; + +describe('ClaimedTaskDataService', () => { + let service: ClaimedTaskDataService; + let options: HttpOptions; + const taskEndpoint = 'https://rest.api/task'; + const linkPath = 'claimedtasks'; + const requestService: any = getMockRequestService(); + const halService: any = new HALEndpointServiceStub(taskEndpoint); + const rdbService = {} as RemoteDataBuildService; + const notificationsService = {} as NotificationsService; + const http = {} as HttpClient; + const comparator = {} as any; + const dataBuildService = { + normalize: (object) => object + } as NormalizedObjectBuildService; + const objectCache = { + addPatch: () => { + /* empty */ + }, + getObjectBySelfLink: () => { + /* empty */ + } + } as any; + const store = {} as Store; + + function initTestService(): ClaimedTaskDataService { + return new ClaimedTaskDataService( + requestService, + rdbService, + dataBuildService, + store, + objectCache, + halService, + notificationsService, + http, + comparator + ); + } + + beforeEach(() => { + service = initTestService(); + options = Object.create({}); + let headers = new HttpHeaders(); + headers = headers.append('Content-Type', 'application/x-www-form-urlencoded'); + options.headers = headers; + }); + + describe('approveTask', () => { + + it('should call postToEndpoint method', () => { + const scopeId = '1234'; + const body = { + submit_approve: 'true' + }; + + spyOn(service, 'postToEndpoint'); + requestService.prepareBody.and.returnValue(body); + + service.approveTask(scopeId); + + expect(service.postToEndpoint).toHaveBeenCalledWith(linkPath, body, scopeId, options); + }); + }); + + describe('rejectTask', () => { + + it('should call postToEndpoint method', () => { + const scopeId = '1234'; + const reason = 'test reject'; + const body = { + submit_reject: 'true', + reason + }; + + spyOn(service, 'postToEndpoint'); + requestService.prepareBody.and.returnValue(body); + + service.rejectTask(reason, scopeId); + + expect(service.postToEndpoint).toHaveBeenCalledWith(linkPath, body, scopeId, options); + }); + }); + + describe('returnToPoolTask', () => { + + it('should call deleteById method', () => { + const scopeId = '1234'; + + spyOn(service, 'deleteById'); + + service.returnToPoolTask(scopeId); + + expect(service.deleteById).toHaveBeenCalledWith(linkPath, scopeId, options); + }); + }); +}); diff --git a/src/app/core/tasks/claimed-task-data.service.ts b/src/app/core/tasks/claimed-task-data.service.ts index f1175d1b1c..52d4f0744f 100644 --- a/src/app/core/tasks/claimed-task-data.service.ts +++ b/src/app/core/tasks/claimed-task-data.service.ts @@ -14,12 +14,37 @@ import { NormalizedObjectBuildService } from '../cache/builders/normalized-objec import { ObjectCacheService } from '../cache/object-cache.service'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service'; +import { ProcessTaskResponse } from './models/process-task-response'; +/** + * The service handling all REST requests for ClaimedTask + */ @Injectable() export class ClaimedTaskDataService extends TasksService { + + /** + * The endpoint link name + */ protected linkPath = 'claimedtasks'; + + /** + * When true, a new request is always dispatched + */ protected forceBypassCache = true; + /** + * Initialize instance variables + * + * @param {RequestService} requestService + * @param {RemoteDataBuildService} rdbService + * @param {NormalizedObjectBuildService} dataBuildService + * @param {Store} store + * @param {ObjectCacheService} objectCache + * @param {HALEndpointService} halService + * @param {NotificationsService} notificationsService + * @param {HttpClient} http + * @param {DSOChangeAnalyzer { super(); } - public approveTask(scopeId: string): Observable { + /** + * Make a request to approve the given task + * + * @param scopeId + * The task id + * @return {Observable} + * Emit the server response + */ + public approveTask(scopeId: string): Observable { const body = { submit_approve: 'true' }; return this.postToEndpoint(this.linkPath, this.requestService.prepareBody(body), scopeId, this.makeHttpOptions()); } - public rejectTask(reason: string, scopeId: string): Observable { + /** + * Make a request to reject the given task + * + * @param reason + * The reason of reject + * @param scopeId + * The task id + * @return {Observable} + * Emit the server response + */ + public rejectTask(reason: string, scopeId: string): Observable { const body = { submit_reject: 'true', reason @@ -48,7 +91,15 @@ export class ClaimedTaskDataService extends TasksService { return this.postToEndpoint(this.linkPath, this.requestService.prepareBody(body), scopeId, this.makeHttpOptions()); } - public returnToPoolTask(scopeId: string): Observable { + /** + * Make a request to return the given task to the pool + * + * @param scopeId + * The task id + * @return {Observable} + * Emit the server response + */ + public returnToPoolTask(scopeId: string): Observable { return this.deleteById(this.linkPath, scopeId, this.makeHttpOptions()); } diff --git a/src/app/core/tasks/models/claimed-task-object.model.ts b/src/app/core/tasks/models/claimed-task-object.model.ts index d0474a1aa8..212e75ed95 100644 --- a/src/app/core/tasks/models/claimed-task-object.model.ts +++ b/src/app/core/tasks/models/claimed-task-object.model.ts @@ -1,5 +1,8 @@ import { TaskObject } from './task-object.model'; +/** + * A model class for a ClaimedTask. + */ export class ClaimedTask extends TaskObject { } diff --git a/src/app/core/tasks/models/normalized-claimed-task-object.model.ts b/src/app/core/tasks/models/normalized-claimed-task-object.model.ts index e6a9096cb4..c2c3f12bc4 100644 --- a/src/app/core/tasks/models/normalized-claimed-task-object.model.ts +++ b/src/app/core/tasks/models/normalized-claimed-task-object.model.ts @@ -5,7 +5,7 @@ import { ClaimedTask } from './claimed-task-object.model'; import { ResourceType } from '../../shared/resource-type'; /** - * A model class for a NormalizedClaimedTaskObject. + * A normalized model class for a ClaimedTask. */ @mapsTo(ClaimedTask) @inheritSerialization(NormalizedTaskObject) diff --git a/src/app/core/tasks/models/normalized-pool-task-object.model.ts b/src/app/core/tasks/models/normalized-pool-task-object.model.ts index beb2d15e8c..22cda6ff9c 100644 --- a/src/app/core/tasks/models/normalized-pool-task-object.model.ts +++ b/src/app/core/tasks/models/normalized-pool-task-object.model.ts @@ -5,7 +5,7 @@ import { mapsTo, relationship } from '../../cache/builders/build-decorators'; import { ResourceType } from '../../shared/resource-type'; /** - * A model class for a NormalizedPoolTaskObject. + * A normalized model class for a PoolTask. */ @mapsTo(PoolTask) @inheritSerialization(NormalizedTaskObject) diff --git a/src/app/core/tasks/models/normalized-task-object.model.ts b/src/app/core/tasks/models/normalized-task-object.model.ts index 7e0fb3f6bb..52c274e3a8 100644 --- a/src/app/core/tasks/models/normalized-task-object.model.ts +++ b/src/app/core/tasks/models/normalized-task-object.model.ts @@ -6,7 +6,7 @@ import { TaskObject } from './task-object.model'; import { DSpaceObject } from '../../shared/dspace-object.model'; /** - * An abstract model class for a DSpaceObject. + * An abstract normalized model class for a TaskObject. */ @mapsTo(TaskObject) @inheritSerialization(NormalizedDSpaceObject) diff --git a/src/app/core/tasks/models/pool-task-object.model.ts b/src/app/core/tasks/models/pool-task-object.model.ts index fcaf4309a1..8d98d3e1a5 100644 --- a/src/app/core/tasks/models/pool-task-object.model.ts +++ b/src/app/core/tasks/models/pool-task-object.model.ts @@ -1,5 +1,8 @@ import { TaskObject } from './task-object.model'; +/** + * A model class for a PoolTask. + */ export class PoolTask extends TaskObject { } diff --git a/src/app/core/tasks/models/task-object.model.ts b/src/app/core/tasks/models/task-object.model.ts index 1475bdb14a..97a1c9f59e 100644 --- a/src/app/core/tasks/models/task-object.model.ts +++ b/src/app/core/tasks/models/task-object.model.ts @@ -6,6 +6,9 @@ import { ListableObject } from '../../../shared/object-collection/shared/listabl import { RemoteData } from '../../data/remote-data'; import { Workflowitem } from '../../submission/models/workflowitem.model'; +/** + * An abstract model class for a TaskObject. + */ export class TaskObject extends DSpaceObject implements CacheableObject, ListableObject { /** diff --git a/src/app/core/tasks/pool-task-data.service.spec.ts b/src/app/core/tasks/pool-task-data.service.spec.ts new file mode 100644 index 0000000000..7f40c6e89c --- /dev/null +++ b/src/app/core/tasks/pool-task-data.service.spec.ts @@ -0,0 +1,70 @@ +import { HttpClient, HttpHeaders } from '@angular/common/http'; + +import { Store } from '@ngrx/store'; + +import { getMockRequestService } from '../../shared/mocks/mock-request.service'; +import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service-stub'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; +import { CoreState } from '../core.reducers'; +import { PoolTaskDataService } from './pool-task-data.service'; +import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; + +describe('PoolTaskDataService', () => { + let service: PoolTaskDataService; + let options: HttpOptions; + const taskEndpoint = 'https://rest.api/task'; + const linkPath = 'pooltasks'; + const requestService = getMockRequestService(); + const halService: any = new HALEndpointServiceStub(taskEndpoint); + const rdbService = {} as RemoteDataBuildService; + const notificationsService = {} as NotificationsService; + const http = {} as HttpClient; + const comparator = {} as any; + const dataBuildService = { + normalize: (object) => object + } as NormalizedObjectBuildService; + const objectCache = { + addPatch: () => { + /* empty */ + }, + getObjectBySelfLink: () => { + /* empty */ + } + } as any; + const store = {} as Store; + + function initTestService(): PoolTaskDataService { + return new PoolTaskDataService( + requestService, + rdbService, + dataBuildService, + store, + objectCache, + halService, + notificationsService, + http, + comparator + ); + } + + beforeEach(() => { + service = initTestService(); + options = Object.create({}); + let headers = new HttpHeaders(); + headers = headers.append('Content-Type', 'application/x-www-form-urlencoded'); + options.headers = headers; + }); + + describe('claimTask', () => { + + it('should call postToEndpoint method', () => { + spyOn(service, 'postToEndpoint'); + const scopeId = '1234'; + service.claimTask(scopeId); + + expect(service.postToEndpoint).toHaveBeenCalledWith(linkPath, {}, scopeId, options); + }); + }); +}); diff --git a/src/app/core/tasks/pool-task-data.service.ts b/src/app/core/tasks/pool-task-data.service.ts index fb1d53420b..1a93450d4d 100644 --- a/src/app/core/tasks/pool-task-data.service.ts +++ b/src/app/core/tasks/pool-task-data.service.ts @@ -14,12 +14,37 @@ import { NormalizedObjectBuildService } from '../cache/builders/normalized-objec import { ObjectCacheService } from '../cache/object-cache.service'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service'; +import { ProcessTaskResponse } from './models/process-task-response'; +/** + * The service handling all REST requests for PoolTask + */ @Injectable() export class PoolTaskDataService extends TasksService { + + /** + * The endpoint link name + */ protected linkPath = 'pooltasks'; + + /** + * When true, a new request is always dispatched + */ protected forceBypassCache = true; + /** + * Initialize instance variables + * + * @param {RequestService} requestService + * @param {RemoteDataBuildService} rdbService + * @param {NormalizedObjectBuildService} dataBuildService + * @param {Store} store + * @param {ObjectCacheService} objectCache + * @param {HALEndpointService} halService + * @param {NotificationsService} notificationsService + * @param {HttpClient} http + * @param {DSOChangeAnalyzer { super(); } - public claimTask(scopeId: string): Observable { + /** + * Make a request to claim the given task + * + * @param scopeId + * The task id + * @return {Observable} + * Emit the server response + */ + public claimTask(scopeId: string): Observable { return this.postToEndpoint(this.linkPath, {}, scopeId, this.makeHttpOptions()); } } diff --git a/src/app/core/tasks/task-response-parsing.service.ts b/src/app/core/tasks/task-response-parsing.service.ts index d12b1b22f8..7445f9d267 100644 --- a/src/app/core/tasks/task-response-parsing.service.ts +++ b/src/app/core/tasks/task-response-parsing.service.ts @@ -11,17 +11,33 @@ import { ObjectCacheService } from '../cache/object-cache.service'; import { NormalizedObjectFactory } from '../cache/models/normalized-object-factory'; import { ErrorResponse, RestResponse, TaskResponse } from '../cache/response.models'; +/** + * Provides methods to parse response for a task request. + */ @Injectable() export class TaskResponseParsingService extends BaseResponseParsingService implements ResponseParsingService { protected objectFactory = NormalizedObjectFactory; protected toCache = false; + /** + * Initialize instance variables + * + * @param {GlobalConfig} EnvConfig + * @param {ObjectCacheService} objectCache + */ constructor(@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig, protected objectCache: ObjectCacheService,) { super(); } + /** + * Parses data from the tasks endpoints + * + * @param {RestRequest} request + * @param {DSpaceRESTV2Response} data + * @returns {RestResponse} + */ parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { if (this.isSuccessStatus(data.statusCode)) { return new TaskResponse( data.statusCode, data.statusText); diff --git a/src/app/core/tasks/tasks.service.spec.ts b/src/app/core/tasks/tasks.service.spec.ts new file mode 100644 index 0000000000..207a09225b --- /dev/null +++ b/src/app/core/tasks/tasks.service.spec.ts @@ -0,0 +1,130 @@ +import { getTestScheduler } from 'jasmine-marbles'; +import { TestScheduler } from 'rxjs/testing'; + +import { getMockRequestService } from '../../shared/mocks/mock-request.service'; +import { TasksService } from './tasks.service'; +import { RequestService } from '../data/request.service'; +import { TaskDeleteRequest, TaskPostRequest } from '../data/request.models'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service-stub'; +import { TaskObject } from './models/task-object.model'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; +import { Store } from '@ngrx/store'; +import { CoreState } from '../core.reducers'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { HttpClient, HttpHeaders } from '@angular/common/http'; +import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service'; +import { ChangeAnalyzer } from '../data/change-analyzer'; +import { compare, Operation } from 'fast-json-patch'; +import { NormalizedTaskObject } from './models/normalized-task-object.model'; +import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; + +const LINK_NAME = 'test'; + +/* tslint:disable:max-classes-per-file */ +class TestTask extends TaskObject { +} + +class TestService extends TasksService { + protected linkPath = LINK_NAME; + protected forceBypassCache = true; + + constructor( + protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected dataBuildService: NormalizedObjectBuildService, + protected store: Store, + protected objectCache: ObjectCacheService, + protected halService: HALEndpointService, + protected notificationsService: NotificationsService, + protected http: HttpClient, + protected comparator: DSOChangeAnalyzer) { + super(); + } +} + +class NormalizedTestTaskObject extends NormalizedTaskObject { +} + +class DummyChangeAnalyzer implements ChangeAnalyzer { + diff(object1: NormalizedTestTaskObject, object2: NormalizedTestTaskObject): Operation[] { + return compare((object1 as any).metadata, (object2 as any).metadata); + } + +} +/* tslint:enable:max-classes-per-file */ + +describe('TasksService', () => { + let scheduler: TestScheduler; + let service: TestService; + const taskEndpoint = 'https://rest.api/task'; + const linkPath = 'testTask'; + const requestService = getMockRequestService(); + const halService: any = new HALEndpointServiceStub(taskEndpoint); + const rdbService = {} as RemoteDataBuildService; + const notificationsService = {} as NotificationsService; + const http = {} as HttpClient; + const comparator = new DummyChangeAnalyzer() as any; + const dataBuildService = { + normalize: (object) => object + } as NormalizedObjectBuildService; + const objectCache = { + addPatch: () => { + /* empty */ + }, + getObjectBySelfLink: () => { + /* empty */ + } + } as any; + const store = {} as Store; + + function initTestService(): TestService { + return new TestService( + requestService, + rdbService, + dataBuildService, + store, + objectCache, + halService, + notificationsService, + http, + comparator + ); + } + + beforeEach(() => { + scheduler = getTestScheduler(); + service = initTestService(); + const options: HttpOptions = Object.create({}); + let headers = new HttpHeaders(); + headers = headers.append('Content-Type', 'application/x-www-form-urlencoded'); + options.headers = headers; + + }); + + describe('postToEndpoint', () => { + + it('should configure a new TaskPostRequest', () => { + const expected = new TaskPostRequest(requestService.generateRequestId(), `${taskEndpoint}/${linkPath}`, {}); + scheduler.schedule(() => service.postToEndpoint('testTask', {}).subscribe()); + scheduler.flush(); + + expect(requestService.configure).toHaveBeenCalledWith(expected); + }); + }); + + describe('deleteById', () => { + + it('should configure a new TaskPostRequest', () => { + const scopeId = '1234'; + const expected = new TaskDeleteRequest(requestService.generateRequestId(), `${taskEndpoint}/${linkPath}/${scopeId}`, null); + scheduler.schedule(() => service.deleteById('testTask', scopeId).subscribe()); + scheduler.flush(); + + expect(requestService.configure).toHaveBeenCalledWith(expected); + }); + }); + +}); diff --git a/src/app/core/tasks/tasks.service.ts b/src/app/core/tasks/tasks.service.ts index 19ef81bd19..f39b144c6a 100644 --- a/src/app/core/tasks/tasks.service.ts +++ b/src/app/core/tasks/tasks.service.ts @@ -13,12 +13,23 @@ import { getResponseFromEntry } from '../shared/operators'; import { ErrorResponse, MessageResponse, RestResponse } from '../cache/response.models'; import { CacheableObject } from '../cache/object-cache.reducer'; +/** + * An abstract class that provides methods to handle task requests. + */ export abstract class TasksService extends DataService { public getBrowseEndpoint(options: FindAllOptions): Observable { return this.halService.getEndpoint(this.linkPath); } + /** + * Fetch a RestRequest + * + * @param requestId + * The base endpoint for the type of object + * @return Observable + * server response + */ protected fetchRequest(requestId: string): Observable { const responses = this.requestService.getByUUID(requestId).pipe( getResponseFromEntry() @@ -39,14 +50,32 @@ export abstract class TasksService extends DataServic return observableMerge(errorResponses, successResponses); } + /** + * Create the HREF for a specific submission object based on its identifier + * + * @param endpoint + * The base endpoint for the type of object + * @param resourceID + * The identifier for the object + */ protected getEndpointByIDHref(endpoint, resourceID): string { return isNotEmpty(resourceID) ? `${endpoint}/${resourceID}` : `${endpoint}`; } - protected getEndpointByMethod(endpoint: string, method: string): string { - return isNotEmpty(method) ? `${endpoint}/${method}` : `${endpoint}`; - } - + /** + * Make a new post request + * + * @param linkPath + * The endpoint link name + * @param body + * The request body + * @param scopeId + * The task id to be removed + * @param options + * The HttpOptions object + * @return Observable + * server response + */ public postToEndpoint(linkPath: string, body: any, scopeId?: string, options?: HttpOptions): Observable { const requestId = this.requestService.generateRequestId(); return this.halService.getEndpoint(linkPath).pipe( @@ -59,9 +88,21 @@ export abstract class TasksService extends DataServic distinctUntilChanged()); } - public deleteById(linkName: string, scopeId: string, options?: HttpOptions): Observable { + /** + * Delete an existing task on the server + * + * @param linkPath + * The endpoint link name + * @param scopeId + * The task id to be removed + * @param options + * The HttpOptions object + * @return Observable + * server response + */ + public deleteById(linkPath: string, scopeId: string, options?: HttpOptions): Observable { const requestId = this.requestService.generateRequestId(); - return this.halService.getEndpoint(linkName || this.linkPath).pipe( + return this.halService.getEndpoint(linkPath || this.linkPath).pipe( filter((href: string) => isNotEmpty(href)), distinctUntilChanged(), map((endpointURL: string) => this.getEndpointByIDHref(endpointURL, scopeId)), @@ -71,6 +112,9 @@ export abstract class TasksService extends DataServic distinctUntilChanged()); } + /** + * Create a new HttpOptions + */ protected makeHttpOptions() { const options: HttpOptions = Object.create({}); let headers = new HttpHeaders(); diff --git a/src/app/shared/mocks/mock-request.service.ts b/src/app/shared/mocks/mock-request.service.ts index ce09f6d85e..ae19c00a30 100644 --- a/src/app/shared/mocks/mock-request.service.ts +++ b/src/app/shared/mocks/mock-request.service.ts @@ -8,6 +8,7 @@ export function getMockRequestService(requestEntry$: Observable = generateRequestId: 'clients/b186e8ce-e99c-4183-bc9a-42b4821bdb78', getByHref: requestEntry$, getByUUID: requestEntry$, + prepareBody: jasmine.createSpy('prepareBody'), /* tslint:disable:no-empty */ removeByHrefSubstring: () => {} /* tslint:enable:no-empty */