90918: Fix RxJs issues

This commit is contained in:
Yura Bondarenko
2022-04-25 15:23:11 +02:00
parent 8d6f156db1
commit 809072e86a
11 changed files with 66 additions and 25 deletions

View File

@@ -3,7 +3,7 @@ import { getMockRequestService } from '../../shared/mocks/request.service.mock';
import { RequestService } from '../data/request.service'; import { RequestService } from '../data/request.service';
import { HALEndpointService } from './hal-endpoint.service'; import { HALEndpointService } from './hal-endpoint.service';
import { EndpointMapRequest } from '../data/request.models'; import { EndpointMapRequest } from '../data/request.models';
import { combineLatest as observableCombineLatest, of as observableOf } from 'rxjs'; import { combineLatest as observableCombineLatest, Observable, of as observableOf } from 'rxjs';
import { environment } from '../../../environments/environment'; import { environment } from '../../../environments/environment';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
@@ -162,9 +162,9 @@ describe('HALEndpointService', () => {
return observableOf(endpointMaps[param]); return observableOf(endpointMaps[param]);
}); });
observableCombineLatest([ observableCombineLatest<string[]>([
(service as any).getEndpointAt(start, 'one'), (service as any).getEndpointAt(start, 'one'),
(service as any).getEndpointAt(start, 'one', 'two') (service as any).getEndpointAt(start, 'one', 'two'),
]).subscribe(([endpoint1, endpoint2]) => { ]).subscribe(([endpoint1, endpoint2]) => {
expect(endpoint1).toEqual(one); expect(endpoint1).toEqual(one);
expect(endpoint2).toEqual(two); expect(endpoint2).toEqual(two);

View File

@@ -1,5 +1,5 @@
import { combineLatest as observableCombineLatest, Observable } from 'rxjs'; import { combineLatest as observableCombineLatest, Observable, interval } from 'rxjs';
import { debounceTime, filter, find, map, switchMap, take, takeWhile } from 'rxjs/operators'; import { filter, find, map, switchMap, take, takeWhile, debounce, debounceTime } from 'rxjs/operators';
import { hasNoValue, hasValue, hasValueOperator, isNotEmpty } from '../../shared/empty.util'; import { hasNoValue, hasValue, hasValueOperator, isNotEmpty } from '../../shared/empty.util';
import { SearchResult } from '../../shared/search/models/search-result.model'; import { SearchResult } from '../../shared/search/models/search-result.model';
import { PaginatedList } from '../data/paginated-list.model'; import { PaginatedList } from '../data/paginated-list.model';
@@ -9,6 +9,17 @@ import { MetadataSchema } from '../metadata/metadata-schema.model';
import { BrowseDefinition } from './browse-definition.model'; import { BrowseDefinition } from './browse-definition.model';
import { DSpaceObject } from './dspace-object.model'; import { DSpaceObject } from './dspace-object.model';
import { InjectionToken } from '@angular/core'; import { InjectionToken } from '@angular/core';
import { MonoTypeOperatorFunction, SchedulerLike } from 'rxjs/internal/types';
/**
* Use this method instead of the RxJs debounceTime if you're waiting for debouncing in tests;
* debounceTime doesn't work with fakeAsync/tick anymore as of Angular 13.2.6 & RxJs 7.5.5
* Workaround suggested in https://github.com/angular/angular/issues/44351#issuecomment-1107454054
* todo: remove once the above issue is fixed
*/
export const debounceTimeWorkaround = <T>(dueTime: number, scheduler?: SchedulerLike): MonoTypeOperatorFunction<T> => {
return debounce(() => interval(dueTime, scheduler));
};
export const DEBOUNCE_TIME_OPERATOR = new InjectionToken<<T>(dueTime: number) => (source: Observable<T>) => Observable<T>>('debounceTime', { export const DEBOUNCE_TIME_OPERATOR = new InjectionToken<<T>(dueTime: number) => (source: Observable<T>) => Observable<T>>('debounceTime', {
providedIn: 'root', providedIn: 'root',

View File

@@ -5,8 +5,9 @@ import { FormGroup } from '@angular/forms';
import { hasValue, isEmpty } from '../../shared/empty.util'; import { hasValue, isEmpty } from '../../shared/empty.util';
import { EPersonDataService } from '../../core/eperson/eperson-data.service'; import { EPersonDataService } from '../../core/eperson/eperson-data.service';
import { NotificationsService } from '../../shared/notifications/notifications.service'; import { NotificationsService } from '../../shared/notifications/notifications.service';
import { debounceTime, map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { debounceTimeWorkaround as debounceTime } from '../../core/shared/operators';
@Component({ @Component({
selector: 'ds-profile-page-security-form', selector: 'ds-profile-page-security-form',

View File

@@ -143,7 +143,7 @@ export class DSOSelectorComponent implements OnInit, OnDestroy {
this.typesString = this.types.map((type: string) => type.toString().toLowerCase()).join(', '); this.typesString = this.types.map((type: string) => type.toString().toLowerCase()).join(', ');
// Create an observable searching for the current DSO (return empty list if there's no current DSO) // Create an observable searching for the current DSO (return empty list if there's no current DSO)
let currentDSOResult$; let currentDSOResult$: Observable<PaginatedList<SearchResult<DSpaceObject>>>;
if (isNotEmpty(this.currentDSOId)) { if (isNotEmpty(this.currentDSOId)) {
currentDSOResult$ = this.search(this.getCurrentDSOQuery(), 1).pipe(getFirstSucceededRemoteDataPayload()); currentDSOResult$ = this.search(this.getCurrentDSOQuery(), 1).pipe(getFirstSucceededRemoteDataPayload());
} else { } else {

View File

@@ -100,7 +100,9 @@ describe('ItemVersionsComponent', () => {
isAuthenticated: observableOf(true), isAuthenticated: observableOf(true),
setRedirectUrl: {} setRedirectUrl: {}
}); });
const authorizationServiceSpy = jasmine.createSpyObj('authorizationService', ['isAuthorized']); const authorizationServiceSpy = jasmine.createSpyObj('authorizationService', {
isAuthorized: observableOf(true)
});
const workspaceItemDataServiceSpy = jasmine.createSpyObj('workspaceItemDataService', { const workspaceItemDataServiceSpy = jasmine.createSpyObj('workspaceItemDataService', {
findByItem: EMPTY, findByItem: EMPTY,
}); });

View File

@@ -61,7 +61,7 @@ export function createFailedRemoteDataObject<T>(errorMessage?: string, statusCod
* @param timeCompleted the moment when the remoteData was completed * @param timeCompleted the moment when the remoteData was completed
*/ */
export function createFailedRemoteDataObject$<T>(errorMessage?: string, statusCode?: number, timeCompleted?: number): Observable<RemoteData<T>> { export function createFailedRemoteDataObject$<T>(errorMessage?: string, statusCode?: number, timeCompleted?: number): Observable<RemoteData<T>> {
return observableOf(createFailedRemoteDataObject(errorMessage, statusCode, timeCompleted)); return observableOf(createFailedRemoteDataObject<T>(errorMessage, statusCode, timeCompleted));
} }
/** /**
@@ -85,7 +85,7 @@ export function createPendingRemoteDataObject<T>(lastVerified = FIXED_TIMESTAMP)
* @param lastVerified the moment when the remoteData was last verified * @param lastVerified the moment when the remoteData was last verified
*/ */
export function createPendingRemoteDataObject$<T>(lastVerified?: number): Observable<RemoteData<T>> { export function createPendingRemoteDataObject$<T>(lastVerified?: number): Observable<RemoteData<T>> {
return observableOf(createPendingRemoteDataObject(lastVerified)); return observableOf(createPendingRemoteDataObject<T>(lastVerified));
} }
/** /**

View File

@@ -80,7 +80,7 @@ describe('SearchFiltersComponent', () => {
expect(comp.initFilters).toHaveBeenCalledTimes(1); expect(comp.initFilters).toHaveBeenCalledTimes(1);
refreshFiltersEmitter.next(); refreshFiltersEmitter.next(null);
expect(comp.initFilters).toHaveBeenCalledTimes(2); expect(comp.initFilters).toHaveBeenCalledTimes(2);
}); });

View File

@@ -38,7 +38,7 @@ export class VocabularyTreeFlatDataSource<T, F> extends DataSource<F> {
this._treeControl.expansionModel.changed, this._treeControl.expansionModel.changed,
this._flattenedData this._flattenedData
]; ];
return merge(...changes).pipe(map(() => { return merge<any>(...changes).pipe(map((): F[] => {
this._expandedData.next( this._expandedData.next(
this._treeFlattener.expandFlattenedNodes(this._flattenedData.value, this._treeControl)); this._treeFlattener.expandFlattenedNodes(this._flattenedData.value, this._treeControl));
return this._expandedData.value; return this._expandedData.value;

View File

@@ -2,7 +2,7 @@ import { TestBed, waitForAsync } from '@angular/core/testing';
import { TestScheduler } from 'rxjs/testing'; import { TestScheduler } from 'rxjs/testing';
import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core'; import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core';
import { getTestScheduler, hot } from 'jasmine-marbles'; import { cold, getTestScheduler, hot } from 'jasmine-marbles';
import { VocabularyTreeviewService } from './vocabulary-treeview.service'; import { VocabularyTreeviewService } from './vocabulary-treeview.service';
import { VocabularyService } from '../../core/submission/vocabularies/vocabulary.service'; import { VocabularyService } from '../../core/submission/vocabularies/vocabulary.service';
@@ -14,6 +14,8 @@ import { VocabularyEntryDetail } from '../../core/submission/vocabularies/models
import { buildPaginatedList } from '../../core/data/paginated-list.model'; import { buildPaginatedList } from '../../core/data/paginated-list.model';
import { createSuccessfulRemoteDataObject } from '../remote-data.utils'; import { createSuccessfulRemoteDataObject } from '../remote-data.utils';
import { VocabularyEntry } from '../../core/submission/vocabularies/models/vocabulary-entry.model'; import { VocabularyEntry } from '../../core/submission/vocabularies/models/vocabulary-entry.model';
import { expand, map, switchMap } from 'rxjs/operators';
import { from as observableFrom } from 'rxjs';
describe('VocabularyTreeviewService test suite', () => { describe('VocabularyTreeviewService test suite', () => {
@@ -320,10 +322,25 @@ describe('VocabularyTreeviewService test suite', () => {
scheduler.schedule(() => service.searchByQuery(vocabularyOptions)); scheduler.schedule(() => service.searchByQuery(vocabularyOptions));
scheduler.flush(); scheduler.flush();
searchChildNode.childrenChange.next([searchChildNode3]); // We can't check the tree by comparing root TreeviewNodes directly in this particular test;
searchItemNode.childrenChange.next([searchChildNode]); // Since RxJs 7, BehaviorSubjects can no longer be reliably compared because of the new currentObservers property
expect(serviceAsAny.dataChange.value.length).toEqual(1); // (see https://github.com/ReactiveX/rxjs/pull/6842)
expect(serviceAsAny.dataChange.value).toEqual([searchItemNode]); const levels$ = serviceAsAny.dataChange.pipe(
expand((nodes: TreeviewNode[]) => { // recursively apply:
return observableFrom(nodes).pipe( // for each node in the array...
switchMap(node => node.childrenChange) // ...map it to the array its child nodes.
); // because we only have one child per node in this case,
}), // this results in an array of nodes for each level of the tree.
map((nodes: TreeviewNode[]) => nodes.map(node => node.item)), // finally, replace nodes with their vocab entries
);
// Confirm that this corresponds to the hierarchy we set up above
expect(levels$).toBeObservable(cold('-(abcd)', {
a: [item],
b: [child],
c: [child3],
d: [] // ensure that grandchild has no children & the recursion stopped there
}));
}); });
}); });

View File

@@ -64,14 +64,24 @@ describe('SubmissionImportExternalCollectionComponent test suite', () => {
compAsAny = null; compAsAny = null;
}); });
it('The variable \'selectedEvent\' should be assigned', () => { it('should emit from selectedEvent on selectObject', () => {
const event = new EventEmitter<CollectionListEntry>(); spyOn(comp.selectedEvent, 'emit').and.callThrough();
comp.selectObject(event);
expect(comp.selectedEvent).toEqual(event); const entry = {
communities: [
{ id: 'community1' },
{ id: 'community2' }
],
collection: {
id: 'collection'
}
} as CollectionListEntry;
comp.selectObject(entry);
expect(comp.selectedEvent.emit).toHaveBeenCalledWith(entry);
}); });
it('The variable \'selectedEvent\' should be assigned', () => { it('should dismiss modal on closeCollectionModal', () => {
spyOn(compAsAny.activeModal, 'dismiss'); spyOn(compAsAny.activeModal, 'dismiss');
comp.closeCollectionModal(); comp.closeCollectionModal();

View File

@@ -35,10 +35,10 @@ export class SubmissionImportExternalCollectionComponent {
) { } ) { }
/** /**
* This method populates the 'selectedEvent' variable. * This method emits the selected Collection from the 'selectedEvent' variable.
*/ */
public selectObject(event): void { public selectObject(object: CollectionListEntry): void {
this.selectedEvent.emit(event); this.selectedEvent.emit(object);
} }
/** /**