diff --git a/src/app/core/shared/operators.ts b/src/app/core/shared/operators.ts index dd610b6ca7..5cef7e5e09 100644 --- a/src/app/core/shared/operators.ts +++ b/src/app/core/shared/operators.ts @@ -1,6 +1,6 @@ import { Router, UrlTree } from '@angular/router'; import { combineLatest as observableCombineLatest, Observable } from 'rxjs'; -import { filter, find, map, mergeMap, switchMap, take, takeWhile, tap } from 'rxjs/operators'; +import { debounceTime, filter, find, map, mergeMap, switchMap, take, takeWhile, tap } from 'rxjs/operators'; import { hasNoValue, hasValue, hasValueOperator, isNotEmpty } from '../../shared/empty.util'; import { SearchResult } from '../../shared/search/search-result.model'; import { PaginatedList } from '../data/paginated-list.model'; @@ -15,6 +15,12 @@ import { DSpaceObject } from './dspace-object.model'; import { getForbiddenRoute, getPageNotFoundRoute } from '../../app-routing-paths'; import { getEndUserAgreementPath } from '../../info/info-routing-paths'; import { AuthService } from '../auth/auth.service'; +import { InjectionToken } from '@angular/core'; + +export const DEBOUNCE_TIME_OPERATOR = new InjectionToken<(dueTime: number) => (source: Observable) => Observable>('debounceTime', { + providedIn: 'root', + factory: () => debounceTime +}); /** * This file contains custom RxJS operators that can be used in multiple places diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/relationship.effects.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/relationship.effects.spec.ts index 5e2ffd30fb..e988eba9eb 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/relationship.effects.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/relationship.effects.spec.ts @@ -1,10 +1,7 @@ import { TestBed, waitForAsync } from '@angular/core/testing'; - import { BehaviorSubject, Observable, of as observableOf } from 'rxjs'; -import { TestScheduler } from 'rxjs/testing'; import { provideMockActions } from '@ngrx/effects/testing'; import { Store } from '@ngrx/store'; - import { RelationshipEffects } from './relationship.effects'; import { AddRelationshipAction, RelationshipActionTypes, RemoveRelationshipAction } from './relationship.actions'; import { Item } from '../../../../../core/shared/item.model'; @@ -23,6 +20,9 @@ import { RequestService } from '../../../../../core/data/request.service'; import { NotificationsService } from '../../../../notifications/notifications.service'; import { TranslateService } from '@ngx-translate/core'; import { SelectableListService } from '../../../../object-list/selectable-list/selectable-list.service'; +import { cold, hot } from 'jasmine-marbles'; +import { DEBOUNCE_TIME_OPERATOR } from '../../../../../core/shared/operators'; +import { last } from 'rxjs/operators'; describe('RelationshipEffects', () => { let relationEffects: RelationshipEffects; @@ -51,7 +51,6 @@ describe('RelationshipEffects', () => { let notificationsService; let translateService; let selectableListService; - let testScheduler: TestScheduler; function init() { testUUID1 = '20e24c2f-a00a-467c-bdee-c929e79bf08d'; @@ -131,6 +130,7 @@ describe('RelationshipEffects', () => { { provide: NotificationsService, useValue: notificationsService }, { provide: TranslateService, useValue: translateService }, { provide: SelectableListService, useValue: selectableListService }, + { provide: DEBOUNCE_TIME_OPERATOR, useValue: jasmine.createSpy('debounceTime').and.returnValue((v) => v.pipe(last())) }, ], }); })); @@ -140,9 +140,6 @@ describe('RelationshipEffects', () => { identifier = (relationEffects as any).createIdentifier(leftItem, rightItem, relationshipType.leftwardType); spyOn((relationEffects as any), 'addRelationship').and.stub(); spyOn((relationEffects as any), 'removeRelationship').and.stub(); - testScheduler = new TestScheduler((actual, expected) => { - expect(actual).toEqual(expected); - }); }); describe('mapLastActions$', () => { @@ -151,15 +148,13 @@ describe('RelationshipEffects', () => { let action; it('should set the current value debounceMap and the value of the initialActionMap to ADD_RELATIONSHIP', () => { - testScheduler.run(({ hot, expectObservable, flush }) => { - action = new AddRelationshipAction(leftItem, rightItem, relationshipType.leftwardType, '1234'); - actions = hot('--a-|', { a: action }); - expectObservable(relationEffects.mapLastActions$).toBe('--b-|', { b: undefined }); - flush(); - // TODO check following expectations with the implementation - // expect((relationEffects as any).initialActionMap[identifier]).toBe(action.type); - // expect((relationEffects as any).debounceMap[identifier].value).toBe(action.type); - }); + action = new AddRelationshipAction(leftItem, rightItem, relationshipType.leftwardType, '1234'); + actions = hot('--a-|', { a: action }); + const expected = cold('--b-|', { b: undefined }); + expect(relationEffects.mapLastActions$).toBeObservable(expected); + + expect((relationEffects as any).initialActionMap[identifier]).toBe(action.type); + expect((relationEffects as any).debounceMap[identifier].value).toBe(action.type); }); }); @@ -172,14 +167,14 @@ describe('RelationshipEffects', () => { }); it('should set the current value debounceMap to ADD_RELATIONSHIP but not change the value of the initialActionMap', () => { - testScheduler.run(({ hot, expectObservable, flush }) => { - action = new AddRelationshipAction(leftItem, rightItem, relationshipType.leftwardType, '1234'); - actions = hot('--a-|', { a: action }); - expectObservable(relationEffects.mapLastActions$).toBe('--b-|', { b: undefined }); - flush(); - expect((relationEffects as any).initialActionMap[identifier]).toBe(testActionType); - expect((relationEffects as any).debounceMap[identifier].value).toBe(action.type); - }); + action = new AddRelationshipAction(leftItem, rightItem, relationshipType.leftwardType, '1234'); + actions = hot('--a-|', { a: action }); + + const expected = cold('--b-|', { b: undefined }); + expect(relationEffects.mapLastActions$).toBeObservable(expected); + + expect((relationEffects as any).initialActionMap[identifier]).toBe(testActionType); + expect((relationEffects as any).debounceMap[identifier].value).toBe(action.type); }); }); @@ -188,30 +183,26 @@ describe('RelationshipEffects', () => { describe('When the last value in the debounceMap is also an ADD_RELATIONSHIP action', () => { beforeEach(() => { (relationEffects as any).initialActionMap[identifier] = RelationshipActionTypes.ADD_RELATIONSHIP; + ((relationEffects as any).debounceTime as jasmine.Spy).and.returnValue((v) => v); }); it('should call addRelationship on the effect', () => { - testScheduler.run(({ hot, expectObservable, flush }) => { - action = new AddRelationshipAction(leftItem, rightItem, relationshipType.leftwardType, '1234'); - actions = hot('--a-|', { a: action }); - expectObservable(relationEffects.mapLastActions$).toBe('--b-|', { b: undefined }); - flush(); - expect((relationEffects as any).addRelationship).toHaveBeenCalledWith(leftItem, rightItem, relationshipType.leftwardType, '1234', undefined); - }); + action = new AddRelationshipAction(leftItem, rightItem, relationshipType.leftwardType, '1234'); + actions = hot('--a-|', { a: action }); + const expected = cold('--b-|', { b: undefined }); + expect(relationEffects.mapLastActions$).toBeObservable(expected); + expect((relationEffects as any).addRelationship).toHaveBeenCalledWith(leftItem, rightItem, relationshipType.leftwardType, '1234', undefined); }); }); describe('When the last value in the debounceMap is instead a REMOVE_RELATIONSHIP action', () => { - it('should not call removeRelationship or addRelationship on the effect', () => { - testScheduler.run(({ hot, expectObservable, flush }) => { - const actiona = new AddRelationshipAction(leftItem, rightItem, relationshipType.leftwardType, '1234'); - const actionb = new RemoveRelationshipAction(leftItem, rightItem, relationshipType.leftwardType, '1234'); - actions = hot('--ab-|', { a: actiona, b: actionb }); - expectObservable(relationEffects.mapLastActions$).toBe('--bb-|', { b: undefined }); - flush(); - expect((relationEffects as any).addRelationship).not.toHaveBeenCalled(); - expect((relationEffects as any).removeRelationship).not.toHaveBeenCalled(); - }); + const actiona = new AddRelationshipAction(leftItem, rightItem, relationshipType.leftwardType, '1234'); + const actionb = new RemoveRelationshipAction(leftItem, rightItem, relationshipType.leftwardType, '1234'); + actions = hot('--ab-|', { a: actiona, b: actionb }); + const expected = cold('--bb-|', { b: undefined }); + expect(relationEffects.mapLastActions$).toBeObservable(expected); + expect((relationEffects as any).addRelationship).not.toHaveBeenCalled(); + expect((relationEffects as any).removeRelationship).not.toHaveBeenCalled(); }); }); }); @@ -222,15 +213,13 @@ describe('RelationshipEffects', () => { let action; it('should set the current value debounceMap and the value of the initialActionMap to REMOVE_RELATIONSHIP', () => { - testScheduler.run(({ hot, expectObservable, flush }) => { - action = new RemoveRelationshipAction(leftItem, rightItem, relationshipType.leftwardType, '1234'); - actions = hot('--a-|', { a: action }); - expectObservable(relationEffects.mapLastActions$).toBe('--b-|', { b: undefined }); - flush(); - // TODO check following expectations with the implementation - // expect((relationEffects as any).initialActionMap[identifier]).toBe(action.type); - // expect((relationEffects as any).debounceMap[identifier].value).toBe(action.type); - }); + action = new RemoveRelationshipAction(leftItem, rightItem, relationshipType.leftwardType, '1234'); + actions = hot('--a-|', { a: action }); + const expected = cold('--b-|', { b: undefined }); + expect(relationEffects.mapLastActions$).toBeObservable(expected); + + expect((relationEffects as any).initialActionMap[identifier]).toBe(action.type); + expect((relationEffects as any).debounceMap[identifier].value).toBe(action.type); }); }); @@ -238,20 +227,19 @@ describe('RelationshipEffects', () => { let action; const testActionType = 'TEST_TYPE'; beforeEach(() => { + ((relationEffects as any).debounceTime as jasmine.Spy).and.returnValue((v) => v); (relationEffects as any).initialActionMap[identifier] = testActionType; (relationEffects as any).debounceMap[identifier] = new BehaviorSubject(testActionType); }); it('should set the current value debounceMap to REMOVE_RELATIONSHIP but not change the value of the initialActionMap', () => { - testScheduler.run(({ hot, expectObservable, flush }) => { - action = new RemoveRelationshipAction(leftItem, rightItem, relationshipType.leftwardType, '1234'); - actions = hot('--a-|', { a: action }); - expectObservable(relationEffects.mapLastActions$).toBe('--b-|', { b: undefined }); - flush(); - // TODO check following expectations with the implementation - // expect((relationEffects as any).initialActionMap[identifier]).toBe(testActionType); - // expect((relationEffects as any).debounceMap[identifier].value).toBe(action.type); - }); + action = new RemoveRelationshipAction(leftItem, rightItem, relationshipType.leftwardType, '1234'); + actions = hot('--a-|', { a: action }); + const expected = cold('--b-|', { b: undefined }); + expect(relationEffects.mapLastActions$).toBeObservable(expected); + + expect((relationEffects as any).initialActionMap[identifier]).toBe(testActionType); + expect((relationEffects as any).debounceMap[identifier].value).toBe(action.type); }); }); @@ -259,32 +247,28 @@ describe('RelationshipEffects', () => { let action; describe('When the last value in the debounceMap is also an REMOVE_RELATIONSHIP action', () => { beforeEach(() => { + ((relationEffects as any).debounceTime as jasmine.Spy).and.returnValue((v) => v); (relationEffects as any).initialActionMap[identifier] = RelationshipActionTypes.REMOVE_RELATIONSHIP; }); it('should call removeRelationship on the effect', () => { - testScheduler.run(({ hot, expectObservable, flush }) => { - action = new RemoveRelationshipAction(leftItem, rightItem, relationshipType.leftwardType, '1234'); - actions = hot('a', { a: action }); - expectObservable(relationEffects.mapLastActions$).toBe('b', { b: undefined }); - flush(); - expect((relationEffects as any).removeRelationship).toHaveBeenCalledWith(leftItem, rightItem, relationshipType.leftwardType, '1234',); - }); + action = new RemoveRelationshipAction(leftItem, rightItem, relationshipType.leftwardType, '1234'); + actions = hot('--a-|', { a: action }); + const expected = cold('--b-|', { b: undefined }); + expect(relationEffects.mapLastActions$).toBeObservable(expected); + expect((relationEffects as any).removeRelationship).toHaveBeenCalledWith(leftItem, rightItem, relationshipType.leftwardType, '1234',); }); }); describe('When the last value in the debounceMap is instead a ADD_RELATIONSHIP action', () => { - it('should not call addRelationship or removeRelationship on the effect', () => { - testScheduler.run(({ hot, expectObservable, flush }) => { - const actionb = new RemoveRelationshipAction(leftItem, rightItem, relationshipType.leftwardType, '1234'); - const actiona = new AddRelationshipAction(leftItem, rightItem, relationshipType.leftwardType, '1234'); - actions = hot('--ab-|', { a: actiona, b: actionb }); - expectObservable(relationEffects.mapLastActions$).toBe('--bb-|', { b: undefined }); - flush(); - expect((relationEffects as any).addRelationship).not.toHaveBeenCalled(); - expect((relationEffects as any).removeRelationship).not.toHaveBeenCalled(); - }); + const actionb = new RemoveRelationshipAction(leftItem, rightItem, relationshipType.leftwardType, '1234'); + const actiona = new AddRelationshipAction(leftItem, rightItem, relationshipType.leftwardType, '1234'); + actions = hot('--ab-|', { a: actiona, b: actionb }); + const expected = cold('--bb-|', { b: undefined }); + expect(relationEffects.mapLastActions$).toBeObservable(expected); + expect((relationEffects as any).addRelationship).not.toHaveBeenCalled(); + expect((relationEffects as any).removeRelationship).not.toHaveBeenCalled(); }); }); }); diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/relationship.effects.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/relationship.effects.ts index 5a20ed8b51..a810cb0ad9 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/relationship.effects.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/relationship.effects.ts @@ -1,11 +1,11 @@ -import { Injectable } from '@angular/core'; +import { Inject, Injectable } from '@angular/core'; import { Actions, Effect, ofType } from '@ngrx/effects'; -import { debounceTime, filter, map, mergeMap, switchMap, take } from 'rxjs/operators'; +import { filter, map, mergeMap, switchMap, take } from 'rxjs/operators'; import { BehaviorSubject, Observable } from 'rxjs'; import { RelationshipService } from '../../../../../core/data/relationship.service'; import { getRemoteDataPayload, - getFirstSucceededRemoteData + getFirstSucceededRemoteData, DEBOUNCE_TIME_OPERATOR } from '../../../../../core/shared/operators'; import { AddRelationshipAction, @@ -71,7 +71,7 @@ export class RelationshipEffects { this.initialActionMap[identifier] = action.type; this.debounceMap[identifier] = new BehaviorSubject(action.type); this.debounceMap[identifier].pipe( - debounceTime(DEBOUNCE_TIME), + this.debounceTime(DEBOUNCE_TIME), take(1) ).subscribe( (type) => { @@ -159,6 +159,7 @@ export class RelationshipEffects { private notificationsService: NotificationsService, private translateService: TranslateService, private selectableListService: SelectableListService, + @Inject(DEBOUNCE_TIME_OPERATOR) private debounceTime: (dueTime: number) => (source: Observable) => Observable, ) { }