Merge pull request #1104 from atmire/Angular-10-upgrade-test-fixes

Angular 10 upgrade test fixes
This commit is contained in:
Tim Donohue
2021-04-27 14:23:52 -05:00
committed by GitHub
16 changed files with 208 additions and 181 deletions

View File

@@ -1,4 +1,6 @@
import { hasNoValue } from '../../shared/empty.util';
import { InjectionToken } from '@angular/core';
import { GenericConstructor } from '../../core/shared/generic-constructor';
export enum BrowseByType {
Title = 'title',
@@ -8,6 +10,11 @@ export enum BrowseByType {
export const DEFAULT_BROWSE_BY_TYPE = BrowseByType.Metadata;
export const BROWSE_BY_COMPONENT_FACTORY = new InjectionToken<(browseByType) => GenericConstructor<any>>('getComponentByBrowseByType', {
providedIn: 'root',
factory: () => getComponentByBrowseByType
});
const map = new Map();
/**

View File

@@ -2,12 +2,11 @@ import { BrowseBySwitcherComponent } from './browse-by-switcher.component';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import * as decorator from './browse-by-decorator';
import { BehaviorSubject } from 'rxjs';
import { environment } from '../../../environments/environment';
import createSpy = jasmine.createSpy;
import { BROWSE_BY_COMPONENT_FACTORY } from './browse-by-decorator';
xdescribe('BrowseBySwitcherComponent', () => {
describe('BrowseBySwitcherComponent', () => {
let comp: BrowseBySwitcherComponent;
let fixture: ComponentFixture<BrowseBySwitcherComponent>;
@@ -23,7 +22,8 @@ xdescribe('BrowseBySwitcherComponent', () => {
TestBed.configureTestingModule({
declarations: [BrowseBySwitcherComponent],
providers: [
{ provide: ActivatedRoute, useValue: activatedRouteStub }
{ provide: ActivatedRoute, useValue: activatedRouteStub },
{ provide: BROWSE_BY_COMPONENT_FACTORY, useValue: jasmine.createSpy('getComponentByBrowseByType').and.returnValue(null) }
],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
@@ -32,7 +32,6 @@ xdescribe('BrowseBySwitcherComponent', () => {
beforeEach(waitForAsync(() => {
fixture = TestBed.createComponent(BrowseBySwitcherComponent);
comp = fixture.componentInstance;
spyOnProperty(decorator, 'getComponentByBrowseByType').and.returnValue(createSpy('getComponentByItemType'));
}));
types.forEach((type) => {
@@ -43,7 +42,7 @@ xdescribe('BrowseBySwitcherComponent', () => {
});
it(`should call getComponentByBrowseByType with type "${type.type}"`, () => {
expect(decorator.getComponentByBrowseByType).toHaveBeenCalledWith(type.type);
expect((comp as any).getComponentByBrowseByType).toHaveBeenCalledWith(type.type);
});
});
});

View File

@@ -1,10 +1,11 @@
import { Component, OnInit } from '@angular/core';
import { Component, Inject, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Observable } from 'rxjs';
import { BrowseByTypeConfig } from '../../../config/browse-by-type-config.interface';
import { map } from 'rxjs/operators';
import { getComponentByBrowseByType } from './browse-by-decorator';
import { BROWSE_BY_COMPONENT_FACTORY } from './browse-by-decorator';
import { environment } from '../../../environments/environment';
import { GenericConstructor } from '../../core/shared/generic-constructor';
@Component({
selector: 'ds-browse-by-switcher',
@@ -20,7 +21,8 @@ export class BrowseBySwitcherComponent implements OnInit {
*/
browseByComponent: Observable<any>;
public constructor(protected route: ActivatedRoute) {
public constructor(protected route: ActivatedRoute,
@Inject(BROWSE_BY_COMPONENT_FACTORY) private getComponentByBrowseByType: (browseByType) => GenericConstructor<any>) {
}
/**
@@ -32,7 +34,7 @@ export class BrowseBySwitcherComponent implements OnInit {
const id = params.id;
return environment.browseBy.types.find((config: BrowseByTypeConfig) => config.id === id);
}),
map((config: BrowseByTypeConfig) => getComponentByBrowseByType(config.type))
map((config: BrowseByTypeConfig) => this.getComponentByBrowseByType(config.type))
);
}

View File

@@ -9,6 +9,12 @@ import {
getFirstSucceededRemoteData
} from '../../../../core/shared/operators';
import { hasValue } from '../../../../shared/empty.util';
import { InjectionToken } from '@angular/core';
export const PAGINATED_RELATIONS_TO_ITEMS_OPERATOR = new InjectionToken<(thisId: string) => (source: Observable<RemoteData<PaginatedList<Relationship>>>) => Observable<RemoteData<PaginatedList<Item>>>>('paginatedRelationsToItems', {
providedIn: 'root',
factory: () => paginatedRelationsToItems
});
/**
* Operator for comparing arrays using a mapping function

View File

@@ -8,6 +8,20 @@ import {
TypedObject,
getResourceTypeValueFor
} from '../object-cache.reducer';
import { InjectionToken } from '@angular/core';
export const DATA_SERVICE_FACTORY = new InjectionToken<(resourceType: ResourceType) => GenericConstructor<any>>('getDataServiceFor', {
providedIn: 'root',
factory: () => getDataServiceFor
});
export const LINK_DEFINITION_FACTORY = new InjectionToken<<T extends HALResource>(source: GenericConstructor<T>, linkName: keyof T['_links']) => LinkDefinition<T>>('getLinkDefinition', {
providedIn: 'root',
factory: () => getLinkDefinition
});
export const LINK_DEFINITION_MAP_FACTORY = new InjectionToken<<T extends HALResource>(source: GenericConstructor<T>) => Map<keyof T['_links'], LinkDefinition<T>>>('getLinkDefinitions', {
providedIn: 'root',
factory: () => getLinkDefinitions
});
const resolvedLinkKey = Symbol('resolvedLink');

View File

@@ -5,15 +5,9 @@ import { FindListOptions } from '../../data/request.models';
import { HALLink } from '../../shared/hal-link.model';
import { HALResource } from '../../shared/hal-resource.model';
import { ResourceType } from '../../shared/resource-type';
import * as decorators from './build-decorators';
import { LinkService } from './link.service';
const spyOnFunction = <T>(obj: T, func: keyof T) => {
const spy = jasmine.createSpy(func as string);
spyOnProperty(obj, func, 'get').and.returnValue(spy);
return spy;
};
import { DATA_SERVICE_FACTORY, LINK_DEFINITION_FACTORY, LINK_DEFINITION_MAP_FACTORY } from './build-decorators';
import { isEmpty } from 'rxjs/operators';
const TEST_MODEL = new ResourceType('testmodel');
let result: any;
@@ -51,7 +45,7 @@ let testDataService: TestDataService;
let testModel: TestModel;
xdescribe('LinkService', () => {
describe('LinkService', () => {
let service: LinkService;
beforeEach(() => {
@@ -76,6 +70,30 @@ xdescribe('LinkService', () => {
providers: [LinkService, {
provide: TestDataService,
useValue: testDataService
}, {
provide: DATA_SERVICE_FACTORY,
useValue: jasmine.createSpy('getDataServiceFor').and.returnValue(TestDataService),
}, {
provide: LINK_DEFINITION_FACTORY,
useValue: jasmine.createSpy('getLinkDefinition').and.returnValue({
resourceType: TEST_MODEL,
linkName: 'predecessor',
propertyName: 'predecessor'
}),
}, {
provide: LINK_DEFINITION_MAP_FACTORY,
useValue: jasmine.createSpy('getLinkDefinitions').and.returnValue([
{
resourceType: TEST_MODEL,
linkName: 'predecessor',
propertyName: 'predecessor',
},
{
resourceType: TEST_MODEL,
linkName: 'successor',
propertyName: 'successor',
}
]),
}]
});
service = TestBed.inject(LinkService);
@@ -84,12 +102,6 @@ xdescribe('LinkService', () => {
describe('resolveLink', () => {
describe(`when the linkdefinition concerns a single object`, () => {
beforeEach(() => {
spyOnFunction(decorators, 'getLinkDefinition').and.returnValue({
resourceType: TEST_MODEL,
linkName: 'predecessor',
propertyName: 'predecessor'
});
spyOnFunction(decorators, 'getDataServiceFor').and.returnValue(TestDataService);
service.resolveLink(testModel, followLink('predecessor', {}, true, true, true, followLink('successor')));
});
it('should call dataservice.findByHref with the correct href and nested links', () => {
@@ -98,13 +110,12 @@ xdescribe('LinkService', () => {
});
describe(`when the linkdefinition concerns a list`, () => {
beforeEach(() => {
spyOnFunction(decorators, 'getLinkDefinition').and.returnValue({
((service as any).getLinkDefinition as jasmine.Spy).and.returnValue({
resourceType: TEST_MODEL,
linkName: 'predecessor',
propertyName: 'predecessor',
isList: true
});
spyOnFunction(decorators, 'getDataServiceFor').and.returnValue(TestDataService);
service.resolveLink(testModel, followLink('predecessor', { some: 'options ' } as any, true, true, true, followLink('successor')));
});
it('should call dataservice.findAllByHref with the correct href, findListOptions, and nested links', () => {
@@ -113,21 +124,15 @@ xdescribe('LinkService', () => {
});
describe('either way', () => {
beforeEach(() => {
spyOnFunction(decorators, 'getLinkDefinition').and.returnValue({
resourceType: TEST_MODEL,
linkName: 'predecessor',
propertyName: 'predecessor'
});
spyOnFunction(decorators, 'getDataServiceFor').and.returnValue(TestDataService);
result = service.resolveLink(testModel, followLink('predecessor', {}, true, true, true, followLink('successor')));
});
it('should call getLinkDefinition with the correct model and link', () => {
expect(decorators.getLinkDefinition).toHaveBeenCalledWith(testModel.constructor as any, 'predecessor');
expect((service as any).getLinkDefinition).toHaveBeenCalledWith(testModel.constructor as any, 'predecessor');
});
it('should call getDataServiceFor with the correct resource type', () => {
expect(decorators.getDataServiceFor).toHaveBeenCalledWith(TEST_MODEL);
expect((service as any).getDataServiceFor).toHaveBeenCalledWith(TEST_MODEL);
});
it('should return the model with the resolved link', () => {
@@ -140,7 +145,7 @@ xdescribe('LinkService', () => {
describe(`when the specified link doesn't exist on the model's class`, () => {
beforeEach(() => {
spyOnFunction(decorators, 'getLinkDefinition').and.returnValue(undefined);
((service as any).getLinkDefinition as jasmine.Spy).and.returnValue(undefined);
});
it('should throw an error', () => {
expect(() => {
@@ -151,12 +156,7 @@ xdescribe('LinkService', () => {
describe(`when there is no dataservice for the resourcetype in the link`, () => {
beforeEach(() => {
spyOnFunction(decorators, 'getLinkDefinition').and.returnValue({
resourceType: TEST_MODEL,
linkName: 'predecessor',
propertyName: 'predecessor'
});
spyOnFunction(decorators, 'getDataServiceFor').and.returnValue(undefined);
((service as any).getDataServiceFor as jasmine.Spy).and.returnValue(undefined);
});
it('should throw an error', () => {
expect(() => {
@@ -188,18 +188,6 @@ xdescribe('LinkService', () => {
beforeEach(() => {
testModel.predecessor = 'predecessor value' as any;
testModel.successor = 'successor value' as any;
spyOnFunction(decorators, 'getLinkDefinitions').and.returnValue([
{
resourceType: TEST_MODEL,
linkName: 'predecessor',
propertyName: 'predecessor',
},
{
resourceType: TEST_MODEL,
linkName: 'successor',
propertyName: 'successor',
}
]);
});
it('should return a new version of the object without any resolved links', () => {
@@ -231,16 +219,10 @@ xdescribe('LinkService', () => {
}
}
});
spyOnFunction(decorators, 'getDataServiceFor').and.returnValue(TestDataService);
});
describe('resolving the available link', () => {
beforeEach(() => {
spyOnFunction(decorators, 'getLinkDefinition').and.returnValue({
resourceType: TEST_MODEL,
linkName: 'predecessor',
propertyName: 'predecessor'
});
result = service.resolveLinks(testModel, followLink('predecessor'));
});
@@ -251,7 +233,7 @@ xdescribe('LinkService', () => {
describe('resolving the missing link', () => {
beforeEach(() => {
spyOnFunction(decorators, 'getLinkDefinition').and.returnValue({
((service as any).getLinkDefinition as jasmine.Spy).and.returnValue({
resourceType: TEST_MODEL,
linkName: 'successor',
propertyName: 'successor'
@@ -259,8 +241,11 @@ xdescribe('LinkService', () => {
result = service.resolveLinks(testModel, followLink('successor'));
});
it('should return the model with no resolved link', () => {
expect(result.successor).toBeUndefined();
it('should resolve to an empty observable', (done) => {
result.successor.pipe(isEmpty()).subscribe((v) => {
expect(v).toEqual(true);
done();
});
});
});
});

View File

@@ -1,17 +1,18 @@
import { Injectable, Injector } from '@angular/core';
import { Inject, Injectable, Injector } from '@angular/core';
import { hasNoValue, hasValue, isNotEmpty } from '../../../shared/empty.util';
import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model';
import { GenericConstructor } from '../../shared/generic-constructor';
import { HALResource } from '../../shared/hal-resource.model';
import {
getDataServiceFor,
getLinkDefinition,
getLinkDefinitions,
DATA_SERVICE_FACTORY,
LINK_DEFINITION_FACTORY,
LINK_DEFINITION_MAP_FACTORY,
LinkDefinition
} from './build-decorators';
import { RemoteData } from '../../data/remote-data';
import { Observable } from 'rxjs/internal/Observable';
import { EMPTY } from 'rxjs';
import { ResourceType } from '../../shared/resource-type';
/**
* A Service to handle the resolving and removing
@@ -24,6 +25,9 @@ export class LinkService {
constructor(
protected parentInjector: Injector,
@Inject(DATA_SERVICE_FACTORY) private getDataServiceFor: (resourceType: ResourceType) => GenericConstructor<any>,
@Inject(LINK_DEFINITION_FACTORY) private getLinkDefinition: <T extends HALResource>(source: GenericConstructor<T>, linkName: keyof T['_links']) => LinkDefinition<T>,
@Inject(LINK_DEFINITION_MAP_FACTORY) private getLinkDefinitions: <T extends HALResource>(source: GenericConstructor<T>) => Map<keyof T['_links'], LinkDefinition<T>>,
) {
}
@@ -49,12 +53,12 @@ export class LinkService {
* @param linkToFollow the {@link FollowLinkConfig} to resolve
*/
public resolveLinkWithoutAttaching<T extends HALResource, U extends HALResource>(model, linkToFollow: FollowLinkConfig<T>): Observable<RemoteData<U>> {
const matchingLinkDef = getLinkDefinition(model.constructor, linkToFollow.name);
const matchingLinkDef = this.getLinkDefinition(model.constructor, linkToFollow.name);
if (hasNoValue(matchingLinkDef)) {
throw new Error(`followLink('${linkToFollow.name}') was used for a ${model.constructor.name}, but there is no property on ${model.constructor.name} models with an @link() for ${linkToFollow.name}`);
} else {
const provider = getDataServiceFor(matchingLinkDef.resourceType);
const provider = this.getDataServiceFor(matchingLinkDef.resourceType);
if (hasNoValue(provider)) {
throw new Error(`The @link() for ${linkToFollow.name} on ${model.constructor.name} models uses the resource type ${matchingLinkDef.resourceType.value.toUpperCase()}, but there is no service with an @dataService(${matchingLinkDef.resourceType.value.toUpperCase()}) annotation in order to retrieve it`);
@@ -104,7 +108,7 @@ export class LinkService {
*/
public removeResolvedLinks<T extends HALResource>(model: T): T {
const result = Object.assign(new (model.constructor as GenericConstructor<T>)(), model);
const linkDefs = getLinkDefinitions(model.constructor as GenericConstructor<T>);
const linkDefs = this.getLinkDefinitions(model.constructor as GenericConstructor<T>);
if (isNotEmpty(linkDefs)) {
linkDefs.forEach((linkDef: LinkDefinition<T>) => {
result[linkDef.propertyName] = undefined;

View File

@@ -1,5 +1,4 @@
import { of as observableOf } from 'rxjs';
import * as ItemRelationshipsUtils from '../../+item-page/simple/item-types/shared/item-relationships-utils';
import { followLink } from '../../shared/utils/follow-link-config.model';
import { ObjectCacheService } from '../cache/object-cache.service';
import { RelationshipType } from '../shared/item-relationships/relationship-type.model';
@@ -15,9 +14,9 @@ import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-servic
import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
import { getMockRemoteDataBuildServiceHrefMap } from '../../shared/mocks/remote-data-build.service.mock';
import { getMockRequestService } from '../../shared/mocks/request.service.mock';
import { createPaginatedList, spyOnOperator } from '../../shared/testing/utils.test';
import { createPaginatedList } from '../../shared/testing/utils.test';
xdescribe('RelationshipService', () => {
describe('RelationshipService', () => {
let service: RelationshipService;
let requestService: RequestService;
@@ -132,7 +131,8 @@ xdescribe('RelationshipService', () => {
null,
null,
null,
null
null,
jasmine.createSpy('paginatedRelationsToItems').and.returnValue((v) => v),
);
}
@@ -195,8 +195,6 @@ xdescribe('RelationshipService', () => {
const rd$ = createSuccessfulRemoteDataObject$(relationsList);
spyOn(service, 'getItemRelationshipsByLabel').and.returnValue(rd$);
spyOnOperator(ItemRelationshipsUtils, 'paginatedRelationsToItems').and.returnValue((v) => v);
});
it('should call getItemRelationshipsByLabel with the correct params', (done) => {
@@ -225,7 +223,7 @@ xdescribe('RelationshipService', () => {
mockLabel,
mockOptions
).subscribe((result) => {
expect(ItemRelationshipsUtils.paginatedRelationsToItems).toHaveBeenCalledWith(mockItem.uuid);
expect((service as any).paginatedRelationsToItems).toHaveBeenCalledWith(mockItem.uuid);
done();
});
});

View File

@@ -1,11 +1,10 @@
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Inject, Injectable } from '@angular/core';
import { MemoizedSelector, select, Store } from '@ngrx/store';
import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
import { distinctUntilChanged, filter, map, mergeMap, startWith, switchMap, take, tap } from 'rxjs/operators';
import {
compareArraysUsingIds,
paginatedRelationsToItems,
compareArraysUsingIds, PAGINATED_RELATIONS_TO_ITEMS_OPERATOR,
relationsToItems
} from '../../+item-page/simple/item-types/shared/item-relationships-utils';
import { AppState, keySelector } from '../../app.reducer';
@@ -87,7 +86,8 @@ export class RelationshipService extends DataService<Relationship> {
protected notificationsService: NotificationsService,
protected http: HttpClient,
protected comparator: DefaultChangeAnalyzer<Relationship>,
protected appStore: Store<AppState>) {
protected appStore: Store<AppState>,
@Inject(PAGINATED_RELATIONS_TO_ITEMS_OPERATOR) private paginatedRelationsToItems: (thisId: string) => (source: Observable<RemoteData<PaginatedList<Relationship>>>) => Observable<RemoteData<PaginatedList<Item>>>) {
super();
}
@@ -254,7 +254,7 @@ export class RelationshipService extends DataService<Relationship> {
* @param options
*/
getRelatedItemsByLabel(item: Item, label: string, options?: FindListOptions): Observable<RemoteData<PaginatedList<Item>>> {
return this.getItemRelationshipsByLabel(item, label, options, true, true, followLink('leftItem'), followLink('rightItem'), followLink('relationshipType')).pipe(paginatedRelationsToItems(item.uuid));
return this.getItemRelationshipsByLabel(item, label, options, true, true, followLink('leftItem'), followLink('rightItem'), followLink('relationshipType')).pipe(this.paginatedRelationsToItems(item.uuid));
}
/**

View File

@@ -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<<T>(dueTime: number) => (source: Observable<T>) => Observable<T>>('debounceTime', {
providedIn: 'root',
factory: () => debounceTime
});
/**
* This file contains custom RxJS operators that can be used in multiple places

View File

@@ -1,5 +1,4 @@
import { TestBed } from '@angular/core/testing';
import { BrowserKlaroService, COOKIE_MDFIELD } from './browser-klaro.service';
import { getMockTranslateService } from '../mocks/translate.service.mock';
import { of as observableOf } from 'rxjs';
@@ -13,7 +12,7 @@ import { getTestScheduler } from 'jasmine-marbles';
import { MetadataValue } from '../../core/shared/metadata.models';
import { cloneDeep } from 'lodash';
xdescribe('BrowserKlaroService', () => {
describe('BrowserKlaroService', () => {
let translateService;
let ePersonService;
let authService;
@@ -81,7 +80,7 @@ xdescribe('BrowserKlaroService', () => {
}
}
},
apps: [{
services: [{
name: appName,
purposes: [purpose]
}],

View File

@@ -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);
});
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,65 +167,59 @@ 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();
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);
});
});
});
describe('When the initialActionMap contains an ADD_RELATIONSHIP action', () => {
let action;
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();
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 <b>not</b> 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();
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();
});
});
});
});
});
describe('When an REMOVE_RELATIONSHIP action is triggered', () => {
describe('When it\'s the first time for this identifier', () => {
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);
});
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<string>(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);
});
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,29 +247,26 @@ 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();
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 <b>not</b> 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();
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();
});
@@ -290,4 +275,3 @@ describe('RelationshipEffects', () => {
});
});
});
});

View File

@@ -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<string>(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: <T>(dueTime: number) => (source: Observable<T>) => Observable<T>,
) {
}

View File

@@ -1,15 +1,15 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { ChangeDetectionStrategy, ComponentFactoryResolver, NO_ERRORS_SCHEMA } from '@angular/core';
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
import { Context } from '../../core/shared/context.model';
import {
MetadataRepresentation,
MetadataRepresentationType
} from '../../core/shared/metadata-representation/metadata-representation.model';
import { MetadataRepresentationLoaderComponent } from './metadata-representation-loader.component';
import { PlainTextMetadataListElementComponent } from '../object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component';
import { spyOnExported } from '../testing/utils.test';
import { MetadataRepresentationDirective } from './metadata-representation.directive';
import * as metadataRepresentationDecorator from './metadata-representation.decorator';
import { METADATA_REPRESENTATION_COMPONENT_FACTORY } from './metadata-representation.decorator';
import { ThemeService } from '../theme-support/theme.service';
import { PlainTextMetadataListElementComponent } from '../object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component';
const testType = 'TestType';
const testContext = Context.Search;
@@ -29,16 +29,30 @@ class TestType implements MetadataRepresentation {
}
}
xdescribe('MetadataRepresentationLoaderComponent', () => {
describe('MetadataRepresentationLoaderComponent', () => {
let comp: MetadataRepresentationLoaderComponent;
let fixture: ComponentFixture<MetadataRepresentationLoaderComponent>;
let themeService: ThemeService;
const themeName = 'test-theme';
beforeEach(waitForAsync(() => {
themeService = jasmine.createSpyObj('themeService', {
getThemeName: themeName,
});
TestBed.configureTestingModule({
imports: [],
declarations: [MetadataRepresentationLoaderComponent, PlainTextMetadataListElementComponent, MetadataRepresentationDirective],
schemas: [NO_ERRORS_SCHEMA],
providers: [ComponentFactoryResolver]
providers: [
{
provide: METADATA_REPRESENTATION_COMPONENT_FACTORY,
useValue: jasmine.createSpy('getMetadataRepresentationComponent').and.returnValue(PlainTextMetadataListElementComponent)
},
{
provide: ThemeService,
useValue: themeService,
}
]
}).overrideComponent(MetadataRepresentationLoaderComponent, {
set: {
changeDetection: ChangeDetectionStrategy.Default,
@@ -53,15 +67,12 @@ xdescribe('MetadataRepresentationLoaderComponent', () => {
comp.mdRepresentation = new TestType();
comp.context = testContext;
spyOnExported(metadataRepresentationDecorator, 'getMetadataRepresentationComponent').and.returnValue(PlainTextMetadataListElementComponent);
fixture.detectChanges();
}));
describe('When the component is rendered', () => {
it('should call the getMetadataRepresentationComponent function with the right entity type, representation type and context', () => {
expect(metadataRepresentationDecorator.getMetadataRepresentationComponent).toHaveBeenCalledWith(testType, testRepresentationType, testContext);
expect((comp as any).getMetadataRepresentationComponent).toHaveBeenCalledWith(testType, testRepresentationType, testContext, themeName);
});
});
});

View File

@@ -1,6 +1,9 @@
import { Component, ComponentFactoryResolver, Input, OnInit, ViewChild } from '@angular/core';
import { MetadataRepresentation } from '../../core/shared/metadata-representation/metadata-representation.model';
import { getMetadataRepresentationComponent } from './metadata-representation.decorator';
import { Component, ComponentFactoryResolver, Inject, Input, OnInit, ViewChild } from '@angular/core';
import {
MetadataRepresentation,
MetadataRepresentationType
} from '../../core/shared/metadata-representation/metadata-representation.model';
import { METADATA_REPRESENTATION_COMPONENT_FACTORY } from './metadata-representation.decorator';
import { Context } from '../../core/shared/context.model';
import { GenericConstructor } from '../../core/shared/generic-constructor';
import { MetadataRepresentationListElementComponent } from '../object-list/metadata-representation-list-element/metadata-representation-list-element.component';
@@ -45,7 +48,8 @@ export class MetadataRepresentationLoaderComponent implements OnInit {
constructor(
private componentFactoryResolver: ComponentFactoryResolver,
private themeService: ThemeService
private themeService: ThemeService,
@Inject(METADATA_REPRESENTATION_COMPONENT_FACTORY) private getMetadataRepresentationComponent: (entityType: string, mdRepresentationType: MetadataRepresentationType, context: Context, theme: string) => GenericConstructor<any>,
) {
}
@@ -68,6 +72,6 @@ export class MetadataRepresentationLoaderComponent implements OnInit {
* @returns {string}
*/
private getComponent(): GenericConstructor<MetadataRepresentationListElementComponent> {
return getMetadataRepresentationComponent(this.mdRepresentation.itemType, this.mdRepresentation.representationType, this.context, this.themeService.getThemeName());
return this.getMetadataRepresentationComponent(this.mdRepresentation.itemType, this.mdRepresentation.representationType, this.context, this.themeService.getThemeName());
}
}

View File

@@ -1,6 +1,13 @@
import { MetadataRepresentationType } from '../../core/shared/metadata-representation/metadata-representation.model';
import { hasNoValue, hasValue } from '../empty.util';
import { Context } from '../../core/shared/context.model';
import { InjectionToken } from '@angular/core';
import { GenericConstructor } from '../../core/shared/generic-constructor';
export const METADATA_REPRESENTATION_COMPONENT_FACTORY = new InjectionToken<(entityType: string, mdRepresentationType: MetadataRepresentationType, context: Context, theme: string) => GenericConstructor<any>>('getMetadataRepresentationComponent', {
providedIn: 'root',
factory: () => getMetadataRepresentationComponent
});
export const map = new Map();