115046: Fixed failing tests & added new test to cover added code

(cherry picked from commit 479adf6519)
This commit is contained in:
Alexandre Vryghem
2024-05-14 17:46:43 +02:00
parent 71d033bf50
commit 1338712048
12 changed files with 668 additions and 190 deletions

View File

@@ -237,7 +237,7 @@ export class RelationshipDataService extends IdentifiableDataService<Relationshi
* Method to remove an item that's part of a relationship from the cache * Method to remove an item that's part of a relationship from the cache
* @param item The item to remove from the cache * @param item The item to remove from the cache
*/ */
public refreshRelationshipItemsInCache(item) { public refreshRelationshipItemsInCache(item: Item): void {
this.objectCache.remove(item._links.self.href); this.objectCache.remove(item._links.self.href);
this.requestService.removeByHrefSubstring(item.uuid); this.requestService.removeByHrefSubstring(item.uuid);
observableCombineLatest([ observableCombineLatest([

View File

@@ -1,16 +1,303 @@
import { TestBed } from '@angular/core/testing'; import { TestBed } from '@angular/core/testing';
import { TranslateModule } from '@ngx-translate/core';
import { of as observableOf } from 'rxjs';
import { v4 as uuidv4 } from 'uuid';
import { EntityTypeDataService } from '../../../core/data/entity-type-data.service';
import { ItemDataService } from '../../../core/data/item-data.service';
import { FieldChangeType } from '../../../core/data/object-updates/field-change-type.model';
import { FieldUpdate } from '../../../core/data/object-updates/field-update.model';
import { FieldUpdates } from '../../../core/data/object-updates/field-updates.model';
import {
DeleteRelationship,
RelationshipIdentifiable,
} from '../../../core/data/object-updates/object-updates.reducer';
import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service';
import { RelationshipDataService } from '../../../core/data/relationship-data.service';
import { Item } from '../../../core/shared/item.model';
import { ItemType } from '../../../core/shared/item-relationships/item-type.model';
import { Relationship } from '../../../core/shared/item-relationships/relationship.model';
import { RelationshipType } from '../../../core/shared/item-relationships/relationship-type.model';
import { NotificationsService } from '../../../shared/notifications/notifications.service';
import {
createFailedRemoteDataObject,
createSuccessfulRemoteDataObject,
createSuccessfulRemoteDataObject$,
} from '../../../shared/remote-data.utils';
import { EntityTypeDataServiceStub } from '../../../shared/testing/entity-type-data.service.stub';
import { ItemDataServiceStub } from '../../../shared/testing/item-data.service.stub';
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub';
import { ObjectUpdatesServiceStub } from '../../../shared/testing/object-updates.service.stub';
import { RelationshipDataServiceStub } from '../../../shared/testing/relationship-data.service.stub';
import { EditItemRelationshipsService } from './edit-item-relationships.service'; import { EditItemRelationshipsService } from './edit-item-relationships.service';
describe('EditItemRelationshipsService', () => { describe('EditItemRelationshipsService', () => {
let service: EditItemRelationshipsService; let service: EditItemRelationshipsService;
let itemService: ItemDataServiceStub;
let objectUpdatesService: ObjectUpdatesServiceStub;
let notificationsService: NotificationsServiceStub;
let relationshipService: RelationshipDataServiceStub;
let entityTypeDataService: EntityTypeDataServiceStub;
let currentItem: Item;
let relationshipItem1: Item;
let relationshipIdentifiable1: RelationshipIdentifiable;
let relationship1: Relationship;
let relationshipItem2: Item;
let relationshipIdentifiable2: RelationshipIdentifiable;
let relationship2: Relationship;
let orgUnitType: ItemType;
let orgUnitToOrgUnitType: RelationshipType;
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({}); itemService = new ItemDataServiceStub();
objectUpdatesService = new ObjectUpdatesServiceStub();
notificationsService = new NotificationsServiceStub();
relationshipService = new RelationshipDataServiceStub();
entityTypeDataService = new EntityTypeDataServiceStub();
TestBed.configureTestingModule({
imports: [
TranslateModule.forRoot(),
],
providers: [
{ provide: ItemDataService, useValue: itemService },
{ provide: ObjectUpdatesService, useValue: objectUpdatesService },
{ provide: NotificationsService, useValue: notificationsService },
{ provide: RelationshipDataService, useValue: relationshipService },
{ provide: EntityTypeDataService, useValue: entityTypeDataService },
],
});
service = TestBed.inject(EditItemRelationshipsService); service = TestBed.inject(EditItemRelationshipsService);
}); });
it('should be created', () => { beforeEach(() => {
expect(service).toBeTruthy(); currentItem = Object.assign(new Item(), {
uuid: uuidv4(),
metadata: {
'dspace.entity.type': 'OrgUnit',
},
_links: {
self: {
href: 'selfLink1',
},
},
});
relationshipItem1 = Object.assign(new Item(), {
uuid: uuidv4(),
metadata: {
'dspace.entity.type': 'OrgUnit',
},
_links: {
self: {
href: 'selfLink2',
},
},
});
relationshipIdentifiable1 = {
originalItem: currentItem,
relatedItem: relationshipItem1,
type: orgUnitToOrgUnitType,
uuid: `1-${relationshipItem1.uuid}`,
} as RelationshipIdentifiable;
relationship1 = Object.assign(new Relationship(), {
_links: {
leftItem: currentItem._links.self,
rightItem: relationshipItem1._links.self,
},
});
relationshipItem2 = Object.assign(new Item(), {
uuid: uuidv4(),
metadata: {
'dspace.entity.type': 'OrgUnit',
},
_links: {
self: {
href: 'selfLink3',
},
},
});
relationshipIdentifiable2 = {
originalItem: currentItem,
relatedItem: relationshipItem2,
type: orgUnitToOrgUnitType,
uuid: `1-${relationshipItem2.uuid}`,
} as RelationshipIdentifiable;
relationship2 = Object.assign(new Relationship(), {
_links: {
leftItem: currentItem._links.self,
rightItem: relationshipItem2._links.self,
},
});
orgUnitType = Object.assign(new ItemType(), {
id: '2',
label: 'OrgUnit',
});
orgUnitToOrgUnitType = Object.assign(new RelationshipType(), {
id: '1',
leftMaxCardinality: null,
leftMinCardinality: 0,
leftType: createSuccessfulRemoteDataObject$(orgUnitType),
leftwardType: 'isOrgUnitOfOrgUnit',
rightMaxCardinality: null,
rightMinCardinality: 0,
rightType: createSuccessfulRemoteDataObject$(orgUnitType),
rightwardType: 'isOrgUnitOfOrgUnit',
uuid: 'relationshiptype-1',
});
});
describe('submit', () => {
let fieldUpdateAddRelationship1: FieldUpdate;
let fieldUpdateRemoveRelationship2: FieldUpdate;
beforeEach(() => {
fieldUpdateAddRelationship1 = {
changeType: FieldChangeType.ADD,
field: relationshipIdentifiable1,
};
fieldUpdateRemoveRelationship2 = {
changeType: FieldChangeType.REMOVE,
field: relationshipIdentifiable2,
};
spyOn(service, 'addRelationship').withArgs(relationshipIdentifiable1).and.returnValue(createSuccessfulRemoteDataObject$(relationship1));
spyOn(service, 'deleteRelationship').withArgs(relationshipIdentifiable2 as DeleteRelationship).and.returnValue(createSuccessfulRemoteDataObject$({}));
spyOn(itemService, 'invalidateByHref').and.callThrough();
});
it('should support performing multiple relationships manipulations in one submit() call', () => {
spyOn(objectUpdatesService, 'getFieldUpdates').and.returnValue(observableOf({
[`1-${relationshipItem1.uuid}`]: fieldUpdateAddRelationship1,
[`1-${relationshipItem2.uuid}`]: fieldUpdateRemoveRelationship2,
} as FieldUpdates));
service.submit(currentItem, `/entities/orgunit/${currentItem.uuid}/edit/relationships`);
expect(service.addRelationship).toHaveBeenCalledWith(relationshipIdentifiable1);
expect(service.deleteRelationship).toHaveBeenCalledWith(relationshipIdentifiable2 as DeleteRelationship);
expect(itemService.invalidateByHref).toHaveBeenCalledWith(currentItem.self);
expect(itemService.invalidateByHref).toHaveBeenCalledWith(relationshipItem1.self);
// TODO currently this isn't done yet
// expect(itemService.invalidateByHref).toHaveBeenCalledWith(relationshipItem2.self);
expect(notificationsService.success).toHaveBeenCalledTimes(1);
});
});
describe('deleteRelationship', () => {
beforeEach(() => {
spyOn(relationshipService, 'deleteRelationship').and.callThrough();
});
it('should pass "all" as copyVirtualMetadata when the user want to keep the data on both sides', () => {
service.deleteRelationship({
uuid: relationshipItem1.uuid,
keepLeftVirtualMetadata: true,
keepRightVirtualMetadata: true,
} as DeleteRelationship);
expect(relationshipService.deleteRelationship).toHaveBeenCalledWith(relationshipItem1.uuid, 'all', false);
});
it('should pass "left" as copyVirtualMetadata when the user only want to keep the data on the left side', () => {
service.deleteRelationship({
uuid: relationshipItem1.uuid,
keepLeftVirtualMetadata: true,
keepRightVirtualMetadata: false,
} as DeleteRelationship);
expect(relationshipService.deleteRelationship).toHaveBeenCalledWith(relationshipItem1.uuid, 'left', false);
});
it('should pass "right" as copyVirtualMetadata when the user only want to keep the data on the right side', () => {
service.deleteRelationship({
uuid: relationshipItem1.uuid,
keepLeftVirtualMetadata: false,
keepRightVirtualMetadata: true,
} as DeleteRelationship);
expect(relationshipService.deleteRelationship).toHaveBeenCalledWith(relationshipItem1.uuid, 'right', false);
});
it('should pass "none" as copyVirtualMetadata when the user doesn\'t want to keep the virtual metadata', () => {
service.deleteRelationship({
uuid: relationshipItem1.uuid,
keepLeftVirtualMetadata: false,
keepRightVirtualMetadata: false,
} as DeleteRelationship);
expect(relationshipService.deleteRelationship).toHaveBeenCalledWith(relationshipItem1.uuid, 'none', false);
});
});
describe('addRelationship', () => {
beforeEach(() => {
spyOn(relationshipService, 'addRelationship').and.callThrough();
});
it('should call the addRelationship from relationshipService correctly when original item is on the right', () => {
service.addRelationship({
originalItem: currentItem,
originalIsLeft: false,
relatedItem: relationshipItem1,
type: orgUnitToOrgUnitType,
uuid: `1-${relationshipItem1.uuid}`,
} as RelationshipIdentifiable);
expect(relationshipService.addRelationship).toHaveBeenCalledWith(orgUnitToOrgUnitType.id, relationshipItem1, currentItem, undefined, null, false);
});
it('should call the addRelationship from relationshipService correctly when original item is on the left', () => {
service.addRelationship({
originalItem: currentItem,
originalIsLeft: true,
relatedItem: relationshipItem1,
type: orgUnitToOrgUnitType,
uuid: `1-${relationshipItem1.uuid}`,
} as RelationshipIdentifiable);
expect(relationshipService.addRelationship).toHaveBeenCalledWith(orgUnitToOrgUnitType.id, currentItem, relationshipItem1, null, undefined, false);
});
});
describe('displayNotifications', () => {
it('should show one success notification when multiple requests succeeded', () => {
service.displayNotifications([
createSuccessfulRemoteDataObject({}),
createSuccessfulRemoteDataObject({}),
]);
expect(notificationsService.success).toHaveBeenCalledTimes(1);
});
it('should show one success notification even when some requests failed', () => {
service.displayNotifications([
createSuccessfulRemoteDataObject({}),
createFailedRemoteDataObject('Request Failed'),
createSuccessfulRemoteDataObject({}),
]);
expect(notificationsService.success).toHaveBeenCalledTimes(1);
expect(notificationsService.error).toHaveBeenCalledTimes(1);
});
it('should show a separate error notification for each failed request', () => {
service.displayNotifications([
createSuccessfulRemoteDataObject({}),
createFailedRemoteDataObject('Request Failed 1'),
createSuccessfulRemoteDataObject({}),
createFailedRemoteDataObject('Request Failed 2'),
]);
expect(notificationsService.success).toHaveBeenCalledTimes(1);
expect(notificationsService.error).toHaveBeenCalledTimes(2);
});
}); });
}); });

View File

@@ -5,6 +5,7 @@ import {
BehaviorSubject, BehaviorSubject,
EMPTY, EMPTY,
Observable, Observable,
Subscription,
} from 'rxjs'; } from 'rxjs';
import { import {
concatMap, concatMap,
@@ -120,7 +121,7 @@ export class EditItemRelationshipsService {
/** /**
* Sends all initial values of this item to the object updates service * Sends all initial values of this item to the object updates service
*/ */
public initializeOriginalFields(item: Item, url: string) { public initializeOriginalFields(item: Item, url: string): Subscription {
return this.relationshipService.getRelatedItems(item).pipe( return this.relationshipService.getRelatedItems(item).pipe(
take(1), take(1),
).subscribe((items: Item[]) => { ).subscribe((items: Item[]) => {
@@ -168,7 +169,7 @@ export class EditItemRelationshipsService {
* - Success notification in case there's at least one successful response * - Success notification in case there's at least one successful response
* @param responses * @param responses
*/ */
displayNotifications(responses: RemoteData<NoContent>[]) { displayNotifications(responses: RemoteData<NoContent>[]): void {
const failedResponses = responses.filter((response: RemoteData<NoContent>) => response.hasFailed); const failedResponses = responses.filter((response: RemoteData<NoContent>) => response.hasFailed);
const successfulResponses = responses.filter((response: RemoteData<NoContent>) => response.hasSucceeded); const successfulResponses = responses.filter((response: RemoteData<NoContent>) => response.hasSucceeded);
@@ -186,7 +187,7 @@ export class EditItemRelationshipsService {
* Get translated notification title * Get translated notification title
* @param key * @param key
*/ */
getNotificationTitle(key: string) { getNotificationTitle(key: string): string {
return this.translateService.instant(this.notificationsPrefix + key + '.title'); return this.translateService.instant(this.notificationsPrefix + key + '.title');
} }
@@ -194,7 +195,7 @@ export class EditItemRelationshipsService {
* Get translated notification content * Get translated notification content
* @param key * @param key
*/ */
getNotificationContent(key: string) { getNotificationContent(key: string): string {
return this.translateService.instant(this.notificationsPrefix + key + '.content'); return this.translateService.instant(this.notificationsPrefix + key + '.content');
} }

View File

@@ -10,19 +10,17 @@ import {
import { By } from '@angular/platform-browser'; import { By } from '@angular/platform-browser';
import { import {
ActivatedRoute, ActivatedRoute,
Router, RouterModule,
} from '@angular/router'; } from '@angular/router';
import { provideMockStore } from '@ngrx/store/testing'; import { provideMockStore } from '@ngrx/store/testing';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { cold } from 'jasmine-marbles';
import { of as observableOf } from 'rxjs'; import { of as observableOf } from 'rxjs';
import { AuthRequestService } from 'src/app/core/auth/auth-request.service';
import { CookieService } from 'src/app/core/services/cookie.service';
import { HardRedirectService } from 'src/app/core/services/hard-redirect.service';
import { ActivatedRouteStub } from 'src/app/shared/testing/active-router.stub';
import { AuthRequestServiceStub } from 'src/app/shared/testing/auth-request-service.stub';
import { APP_CONFIG } from '../../../../../config/app-config.interface'; import { APP_CONFIG } from '../../../../../config/app-config.interface';
import { environment } from '../../../../../environments/environment.test';
import { REQUEST } from '../../../../../express.tokens'; import { REQUEST } from '../../../../../express.tokens';
import { AuthRequestService } from '../../../../core/auth/auth-request.service';
import { LinkService } from '../../../../core/cache/builders/link.service'; import { LinkService } from '../../../../core/cache/builders/link.service';
import { ConfigurationDataService } from '../../../../core/data/configuration-data.service'; import { ConfigurationDataService } from '../../../../core/data/configuration-data.service';
import { FieldChangeType } from '../../../../core/data/object-updates/field-change-type.model'; import { FieldChangeType } from '../../../../core/data/object-updates/field-change-type.model';
@@ -31,6 +29,8 @@ import { RelationshipDataService } from '../../../../core/data/relationship-data
import { RelationshipTypeDataService } from '../../../../core/data/relationship-type-data.service'; import { RelationshipTypeDataService } from '../../../../core/data/relationship-type-data.service';
import { GroupDataService } from '../../../../core/eperson/group-data.service'; import { GroupDataService } from '../../../../core/eperson/group-data.service';
import { PaginationService } from '../../../../core/pagination/pagination.service'; import { PaginationService } from '../../../../core/pagination/pagination.service';
import { CookieService } from '../../../../core/services/cookie.service';
import { HardRedirectService } from '../../../../core/services/hard-redirect.service';
import { LinkHeadService } from '../../../../core/services/link-head.service'; import { LinkHeadService } from '../../../../core/services/link-head.service';
import { ConfigurationProperty } from '../../../../core/shared/configuration-property.model'; import { ConfigurationProperty } from '../../../../core/shared/configuration-property.model';
import { Item } from '../../../../core/shared/item.model'; import { Item } from '../../../../core/shared/item.model';
@@ -40,51 +40,55 @@ import { RelationshipType } from '../../../../core/shared/item-relationships/rel
import { SearchConfigurationService } from '../../../../core/shared/search/search-configuration.service'; import { SearchConfigurationService } from '../../../../core/shared/search/search-configuration.service';
import { XSRFService } from '../../../../core/xsrf/xsrf.service'; import { XSRFService } from '../../../../core/xsrf/xsrf.service';
import { HostWindowService } from '../../../../shared/host-window.service'; import { HostWindowService } from '../../../../shared/host-window.service';
import { RouterMock } from '../../../../shared/mocks/router.mock';
import { SelectableListService } from '../../../../shared/object-list/selectable-list/selectable-list.service'; import { SelectableListService } from '../../../../shared/object-list/selectable-list/selectable-list.service';
import { PaginationComponent } from '../../../../shared/pagination/pagination.component'; import { PaginationComponent } from '../../../../shared/pagination/pagination.component';
import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model'; import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model';
import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils'; import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils';
import { ActivatedRouteStub } from '../../../../shared/testing/active-router.stub';
import { AuthRequestServiceStub } from '../../../../shared/testing/auth-request-service.stub';
import { EditItemRelationshipsServiceStub } from '../../../../shared/testing/edit-item-relationships.service.stub';
import { HostWindowServiceStub } from '../../../../shared/testing/host-window-service.stub'; import { HostWindowServiceStub } from '../../../../shared/testing/host-window-service.stub';
import { PaginationServiceStub } from '../../../../shared/testing/pagination-service.stub'; import { PaginationServiceStub } from '../../../../shared/testing/pagination-service.stub';
import { SearchConfigurationServiceStub } from '../../../../shared/testing/search-configuration-service.stub'; import { SearchConfigurationServiceStub } from '../../../../shared/testing/search-configuration-service.stub';
import { createPaginatedList } from '../../../../shared/testing/utils.test'; import { createPaginatedList } from '../../../../shared/testing/utils.test';
import { EditItemRelationshipsService } from '../edit-item-relationships.service';
import { EditRelationshipListComponent } from './edit-relationship-list.component'; import { EditRelationshipListComponent } from './edit-relationship-list.component';
let comp: EditRelationshipListComponent;
let fixture: ComponentFixture<EditRelationshipListComponent>;
let de: DebugElement;
let linkService;
let objectUpdatesService;
let relationshipService;
let selectableListService;
let paginationService;
let hostWindowService;
let hardRedirectService;
const relationshipTypeService = {};
const url = 'http://test-url.com/test-url';
let item;
let entityType;
let relatedEntityType;
let author1;
let author2;
let fieldUpdate1;
let fieldUpdate2;
let relationships;
let relationshipType;
let paginationOptions;
describe('EditRelationshipListComponent', () => { describe('EditRelationshipListComponent', () => {
let comp: EditRelationshipListComponent;
let fixture: ComponentFixture<EditRelationshipListComponent>;
let de: DebugElement;
let linkService;
let objectUpdatesService;
let relationshipService;
let selectableListService;
let paginationService: PaginationServiceStub;
let hostWindowService: HostWindowServiceStub;
let hardRedirectService;
const relationshipTypeService = {};
let editItemRelationshipsService: EditItemRelationshipsServiceStub;
const url = 'http://test-url.com/test-url';
let itemLeft: Item;
let entityTypeLeft: ItemType;
let entityTypeRight: ItemType;
let itemRight1: Item;
let itemRight2: Item;
let fieldUpdate1;
let fieldUpdate2;
let relationships: Relationship[];
let relationshipType: RelationshipType;
let paginationOptions: PaginationComponentOptions;
const resetComponent = () => { const resetComponent = () => {
fixture = TestBed.createComponent(EditRelationshipListComponent); fixture = TestBed.createComponent(EditRelationshipListComponent);
comp = fixture.componentInstance; comp = fixture.componentInstance;
de = fixture.debugElement; de = fixture.debugElement;
comp.item = item; comp.item = itemLeft;
comp.itemType = entityType; comp.itemType = entityTypeLeft;
comp.url = url; comp.url = url;
comp.relationshipType = relationshipType; comp.relationshipType = relationshipType;
comp.hasChanges = observableOf(false); comp.hasChanges = observableOf(false);
@@ -101,29 +105,26 @@ describe('EditRelationshipListComponent', () => {
}, },
}; };
hardRedirectService = jasmine.createSpyObj('hardRedirectService', ['redirect']); function init(leftType: string, rightType: string): void {
entityTypeLeft = Object.assign(new ItemType(), {
beforeEach(waitForAsync(() => { id: leftType,
uuid: leftType,
entityType = Object.assign(new ItemType(), { label: leftType,
id: 'Publication',
uuid: 'Publication',
label: 'Publication',
}); });
relatedEntityType = Object.assign(new ItemType(), { entityTypeRight = Object.assign(new ItemType(), {
id: 'Author', id: rightType,
uuid: 'Author', uuid: rightType,
label: 'Author', label: rightType,
}); });
relationshipType = Object.assign(new RelationshipType(), { relationshipType = Object.assign(new RelationshipType(), {
id: '1', id: '1',
uuid: '1', uuid: '1',
leftType: createSuccessfulRemoteDataObject$(entityType), leftType: createSuccessfulRemoteDataObject$(entityTypeLeft),
rightType: createSuccessfulRemoteDataObject$(relatedEntityType), rightType: createSuccessfulRemoteDataObject$(entityTypeRight),
leftwardType: 'isAuthorOfPublication', leftwardType: `is${rightType}Of${leftType}`,
rightwardType: 'isPublicationOfAuthor', rightwardType: `is${leftType}Of${rightType}`,
}); });
paginationOptions = Object.assign(new PaginationComponentOptions(), { paginationOptions = Object.assign(new PaginationComponentOptions(), {
@@ -132,13 +133,13 @@ describe('EditRelationshipListComponent', () => {
currentPage: 1, currentPage: 1,
}); });
author1 = Object.assign(new Item(), { itemRight1 = Object.assign(new Item(), {
id: 'author1', id: `${rightType}-1`,
uuid: 'author1', uuid: `${rightType}-1`,
}); });
author2 = Object.assign(new Item(), { itemRight2 = Object.assign(new Item(), {
id: 'author2', id: `${rightType}-2`,
uuid: 'author2', uuid: `${rightType}-2`,
}); });
relationships = [ relationships = [
@@ -147,25 +148,25 @@ describe('EditRelationshipListComponent', () => {
id: '2', id: '2',
uuid: '2', uuid: '2',
relationshipType: createSuccessfulRemoteDataObject$(relationshipType), relationshipType: createSuccessfulRemoteDataObject$(relationshipType),
leftItem: createSuccessfulRemoteDataObject$(item), leftItem: createSuccessfulRemoteDataObject$(itemLeft),
rightItem: createSuccessfulRemoteDataObject$(author1), rightItem: createSuccessfulRemoteDataObject$(itemRight1),
}), }),
Object.assign(new Relationship(), { Object.assign(new Relationship(), {
self: url + '/3', self: url + '/3',
id: '3', id: '3',
uuid: '3', uuid: '3',
relationshipType: createSuccessfulRemoteDataObject$(relationshipType), relationshipType: createSuccessfulRemoteDataObject$(relationshipType),
leftItem: createSuccessfulRemoteDataObject$(item), leftItem: createSuccessfulRemoteDataObject$(itemLeft),
rightItem: createSuccessfulRemoteDataObject$(author2), rightItem: createSuccessfulRemoteDataObject$(itemRight2),
}), }),
]; ];
item = Object.assign(new Item(), { itemLeft = Object.assign(new Item(), {
_links: { _links: {
self: { href: 'fake-item-url/publication' }, self: { href: 'fake-item-url/publication' },
}, },
id: 'publication', id: `1-${leftType}`,
uuid: 'publication', uuid: `1-${leftType}`,
relationships: createSuccessfulRemoteDataObject$(createPaginatedList(relationships)), relationships: createSuccessfulRemoteDataObject$(createPaginatedList(relationships)),
}); });
@@ -197,7 +198,7 @@ describe('EditRelationshipListComponent', () => {
relationshipService = jasmine.createSpyObj('relationshipService', relationshipService = jasmine.createSpyObj('relationshipService',
{ {
getRelatedItemsByLabel: createSuccessfulRemoteDataObject$(createPaginatedList([author1, author2])), getRelatedItemsByLabel: createSuccessfulRemoteDataObject$(createPaginatedList([itemRight1, itemRight2])),
getItemRelationshipsByLabel: createSuccessfulRemoteDataObject$(createPaginatedList(relationships)), getItemRelationshipsByLabel: createSuccessfulRemoteDataObject$(createPaginatedList(relationships)),
isLeftItem: observableOf(true), isLeftItem: observableOf(true),
}, },
@@ -233,14 +234,14 @@ describe('EditRelationshipListComponent', () => {
})), })),
}); });
const environmentUseThumbs = { editItemRelationshipsService = new EditItemRelationshipsServiceStub();
browseBy: {
showThumbnails: true,
},
};
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [TranslateModule.forRoot(), EditRelationshipListComponent], imports: [
EditRelationshipListComponent,
RouterModule.forRoot([]),
TranslateModule.forRoot(),
],
providers: [ providers: [
provideMockStore({ initialState }), provideMockStore({ initialState }),
{ provide: ObjectUpdatesService, useValue: objectUpdatesService }, { provide: ObjectUpdatesService, useValue: objectUpdatesService },
@@ -251,15 +252,15 @@ describe('EditRelationshipListComponent', () => {
{ provide: HostWindowService, useValue: hostWindowService }, { provide: HostWindowService, useValue: hostWindowService },
{ provide: RelationshipTypeDataService, useValue: relationshipTypeService }, { provide: RelationshipTypeDataService, useValue: relationshipTypeService },
{ provide: GroupDataService, useValue: groupDataService }, { provide: GroupDataService, useValue: groupDataService },
{ provide: Router, useValue: new RouterMock() },
{ provide: LinkHeadService, useValue: linkHeadService }, { provide: LinkHeadService, useValue: linkHeadService },
{ provide: ConfigurationDataService, useValue: configurationDataService }, { provide: ConfigurationDataService, useValue: configurationDataService },
{ provide: SearchConfigurationService, useValue: new SearchConfigurationServiceStub() }, { provide: SearchConfigurationService, useValue: new SearchConfigurationServiceStub() },
{ provide: EditItemRelationshipsService, useValue: editItemRelationshipsService },
{ provide: ActivatedRoute, useValue: new ActivatedRouteStub() }, { provide: ActivatedRoute, useValue: new ActivatedRouteStub() },
{ provide: AuthRequestService, useValue: new AuthRequestServiceStub() }, { provide: AuthRequestService, useValue: new AuthRequestServiceStub() },
{ provide: HardRedirectService, useValue: hardRedirectService }, { provide: HardRedirectService, useValue: hardRedirectService },
{ provide: XSRFService, useValue: {} }, { provide: XSRFService, useValue: {} },
{ provide: APP_CONFIG, useValue: environmentUseThumbs }, { provide: APP_CONFIG, useValue: environment },
{ provide: REQUEST, useValue: {} }, { provide: REQUEST, useValue: {} },
CookieService, CookieService,
], schemas: [ ], schemas: [
@@ -268,114 +269,127 @@ describe('EditRelationshipListComponent', () => {
}).compileComponents(); }).compileComponents();
resetComponent(); resetComponent();
})); }
describe('changeType is REMOVE', () => { describe('Publication - Author relationship', () => {
beforeEach(() => { beforeEach(waitForAsync(() => init('Publication', 'Author')));
fieldUpdate1.changeType = FieldChangeType.REMOVE;
fixture.detectChanges();
});
it('the div should have class alert-danger', () => {
const element = de.queryAll(By.css('.relationship-row'))[1].nativeElement;
expect(element.classList).toContain('alert-danger');
});
});
describe('pagination component', () => { describe('changeType is REMOVE', () => {
let paginationComp: PaginationComponent;
beforeEach(() => {
paginationComp = de.query(By.css('ds-pagination')).componentInstance;
});
it('should receive the correct pagination config', () => {
expect(paginationComp.paginationOptions).toEqual(paginationOptions);
});
it('should receive correct collection size', () => {
expect(paginationComp.collectionSize).toEqual(relationships.length);
});
});
describe('relationshipService.getItemRelationshipsByLabel', () => {
it('should receive the correct pagination info', () => {
expect(relationshipService.getItemRelationshipsByLabel).toHaveBeenCalledTimes(1);
const callArgs = relationshipService.getItemRelationshipsByLabel.calls.mostRecent().args;
const findListOptions = callArgs[2];
const linksToFollow = callArgs[5];
expect(findListOptions.elementsPerPage).toEqual(paginationOptions.pageSize);
expect(findListOptions.currentPage).toEqual(paginationOptions.currentPage);
expect(linksToFollow.linksToFollow[0].name).toEqual('thumbnail');
});
describe('when the publication is on the left side of the relationship', () => {
beforeEach(() => { beforeEach(() => {
relationshipType = Object.assign(new RelationshipType(), { fieldUpdate1.changeType = FieldChangeType.REMOVE;
id: '1',
uuid: '1',
leftType: createSuccessfulRemoteDataObject$(entityType), // publication
rightType: createSuccessfulRemoteDataObject$(relatedEntityType), // author
leftwardType: 'isAuthorOfPublication',
rightwardType: 'isPublicationOfAuthor',
});
relationshipService.getItemRelationshipsByLabel.calls.reset();
resetComponent();
});
it('should fetch isAuthorOfPublication', () => {
expect(relationshipService.getItemRelationshipsByLabel).toHaveBeenCalledTimes(1);
const callArgs = relationshipService.getItemRelationshipsByLabel.calls.mostRecent().args;
const label = callArgs[1];
expect(label).toEqual('isAuthorOfPublication');
});
});
describe('when the publication is on the right side of the relationship', () => {
beforeEach(() => {
relationshipType = Object.assign(new RelationshipType(), {
id: '1',
uuid: '1',
leftType: createSuccessfulRemoteDataObject$(relatedEntityType), // author
rightType: createSuccessfulRemoteDataObject$(entityType), // publication
leftwardType: 'isPublicationOfAuthor',
rightwardType: 'isAuthorOfPublication',
});
relationshipService.getItemRelationshipsByLabel.calls.reset();
resetComponent();
});
it('should fetch isAuthorOfPublication', () => {
expect(relationshipService.getItemRelationshipsByLabel).toHaveBeenCalledTimes(1);
const callArgs = relationshipService.getItemRelationshipsByLabel.calls.mostRecent().args;
const label = callArgs[1];
expect(label).toEqual('isAuthorOfPublication');
});
});
describe('changes managment for add buttons', () => {
it('should show enabled add buttons', () => {
const element = de.query(By.css('.btn-success'));
expect(element.nativeElement?.disabled).toBeFalse();
});
it('after hash changes changed', () => {
comp.hasChanges = observableOf(true);
fixture.detectChanges(); fixture.detectChanges();
const element = de.query(By.css('.btn-success')); });
expect(element.nativeElement?.disabled).toBeTrue(); it('the div should have class alert-danger', () => {
const element = de.queryAll(By.css('.relationship-row'))[1].nativeElement;
expect(element.classList).toContain('alert-danger');
}); });
}); });
describe('pagination component', () => {
let paginationComp: PaginationComponent;
beforeEach(() => {
paginationComp = de.query(By.css('ds-pagination')).componentInstance;
});
it('should receive the correct pagination config', () => {
expect(paginationComp.paginationOptions).toEqual(paginationOptions);
});
it('should receive correct collection size', () => {
expect(paginationComp.collectionSize).toEqual(relationships.length);
});
});
describe('relationshipService.getItemRelationshipsByLabel', () => {
it('should receive the correct pagination info', () => {
expect(relationshipService.getItemRelationshipsByLabel).toHaveBeenCalledTimes(1);
const callArgs = relationshipService.getItemRelationshipsByLabel.calls.mostRecent().args;
const findListOptions = callArgs[2];
const linksToFollow = callArgs[5];
expect(findListOptions.elementsPerPage).toEqual(paginationOptions.pageSize);
expect(findListOptions.currentPage).toEqual(paginationOptions.currentPage);
expect(linksToFollow.linksToFollow[0].name).toEqual('thumbnail');
});
describe('when the publication is on the left side of the relationship', () => {
beforeEach(() => {
relationshipType = Object.assign(new RelationshipType(), {
id: '1',
uuid: '1',
leftType: createSuccessfulRemoteDataObject$(entityTypeLeft), // publication
rightType: createSuccessfulRemoteDataObject$(entityTypeRight), // author
leftwardType: 'isAuthorOfPublication',
rightwardType: 'isPublicationOfAuthor',
});
relationshipService.getItemRelationshipsByLabel.calls.reset();
resetComponent();
});
it('should fetch isAuthorOfPublication', () => {
expect(relationshipService.getItemRelationshipsByLabel).toHaveBeenCalledTimes(1);
const callArgs = relationshipService.getItemRelationshipsByLabel.calls.mostRecent().args;
const label = callArgs[1];
expect(label).toEqual('isAuthorOfPublication');
});
});
describe('when the publication is on the right side of the relationship', () => {
beforeEach(() => {
relationshipType = Object.assign(new RelationshipType(), {
id: '1',
uuid: '1',
leftType: createSuccessfulRemoteDataObject$(entityTypeRight), // author
rightType: createSuccessfulRemoteDataObject$(entityTypeLeft), // publication
leftwardType: 'isPublicationOfAuthor',
rightwardType: 'isAuthorOfPublication',
});
relationshipService.getItemRelationshipsByLabel.calls.reset();
resetComponent();
});
it('should fetch isAuthorOfPublication', () => {
expect(relationshipService.getItemRelationshipsByLabel).toHaveBeenCalledTimes(1);
const callArgs = relationshipService.getItemRelationshipsByLabel.calls.mostRecent().args;
const label = callArgs[1];
expect(label).toEqual('isAuthorOfPublication');
});
});
describe('changes managment for add buttons', () => {
it('should show enabled add buttons', () => {
const element = de.query(By.css('.btn-success'));
expect(element.nativeElement?.disabled).toBeFalse();
});
it('after hash changes changed', () => {
comp.hasChanges = observableOf(true);
fixture.detectChanges();
const element = de.query(By.css('.btn-success'));
expect(element.nativeElement?.disabled).toBeTrue();
});
});
});
}); });
describe('OrgUnit - OrgUnit relationship', () => {
beforeEach(waitForAsync(() => init('OrgUnit', 'OrgUnit')));
it('should emit the relatedEntityType$ even for same entity relationships', () => {
expect(comp.relatedEntityType$).toBeObservable(cold('(a|)', {
a: entityTypeRight,
}));
});
});
}); });

View File

@@ -50,7 +50,6 @@ import { RelationshipIdentifiable } from '../../../../core/data/object-updates/o
import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service'; import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service';
import { PaginatedList } from '../../../../core/data/paginated-list.model'; import { PaginatedList } from '../../../../core/data/paginated-list.model';
import { RelationshipDataService } from '../../../../core/data/relationship-data.service'; import { RelationshipDataService } from '../../../../core/data/relationship-data.service';
import { RelationshipTypeDataService } from '../../../../core/data/relationship-type-data.service';
import { RemoteData } from '../../../../core/data/remote-data'; import { RemoteData } from '../../../../core/data/remote-data';
import { PaginationService } from '../../../../core/pagination/pagination.service'; import { PaginationService } from '../../../../core/pagination/pagination.service';
import { Collection } from '../../../../core/shared/collection.model'; import { Collection } from '../../../../core/shared/collection.model';
@@ -146,7 +145,7 @@ export class EditRelationshipListComponent implements OnInit, OnDestroy {
*/ */
private currentItemIsLeftItem$: BehaviorSubject<boolean> = new BehaviorSubject(undefined); private currentItemIsLeftItem$: BehaviorSubject<boolean> = new BehaviorSubject(undefined);
private relatedEntityType$: Observable<ItemType>; relatedEntityType$: Observable<ItemType>;
/** /**
* The translation key for the entity type * The translation key for the entity type
@@ -208,7 +207,6 @@ export class EditRelationshipListComponent implements OnInit, OnDestroy {
protected objectUpdatesService: ObjectUpdatesService, protected objectUpdatesService: ObjectUpdatesService,
protected linkService: LinkService, protected linkService: LinkService,
protected relationshipService: RelationshipDataService, protected relationshipService: RelationshipDataService,
protected relationshipTypeService: RelationshipTypeDataService,
protected modalService: NgbModal, protected modalService: NgbModal,
protected paginationService: PaginationService, protected paginationService: PaginationService,
protected selectableListService: SelectableListService, protected selectableListService: SelectableListService,

View File

@@ -44,6 +44,7 @@ import {
createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject,
createSuccessfulRemoteDataObject$, createSuccessfulRemoteDataObject$,
} from '../../../shared/remote-data.utils'; } from '../../../shared/remote-data.utils';
import { ItemDataServiceStub } from '../../../shared/testing/item-data.service.stub';
import { relationshipTypes } from '../../../shared/testing/relationship-types.mock'; import { relationshipTypes } from '../../../shared/testing/relationship-types.mock';
import { RouterStub } from '../../../shared/testing/router.stub'; import { RouterStub } from '../../../shared/testing/router.stub';
import { createPaginatedList } from '../../../shared/testing/utils.test'; import { createPaginatedList } from '../../../shared/testing/utils.test';
@@ -72,7 +73,7 @@ const notificationsService = jasmine.createSpyObj('notificationsService',
const router = new RouterStub(); const router = new RouterStub();
let relationshipTypeService; let relationshipTypeService;
let routeStub; let routeStub;
let itemService; let itemService: ItemDataServiceStub;
const url = 'http://test-url.com/test-url'; const url = 'http://test-url.com/test-url';
router.url = url; router.url = url;
@@ -157,10 +158,7 @@ describe('ItemRelationshipsComponent', () => {
changeType: FieldChangeType.REMOVE, changeType: FieldChangeType.REMOVE,
}; };
itemService = jasmine.createSpyObj('itemService', { itemService = new ItemDataServiceStub();
findByHref: createSuccessfulRemoteDataObject$(item),
findById: createSuccessfulRemoteDataObject$(item),
});
routeStub = { routeStub = {
data: observableOf({}), data: observableOf({}),
parent: { parent: {
@@ -251,6 +249,8 @@ describe('ItemRelationshipsComponent', () => {
})); }));
beforeEach(() => { beforeEach(() => {
spyOn(itemService, 'findByHref').and.returnValue(item);
spyOn(itemService, 'findById').and.returnValue(item);
fixture = TestBed.createComponent(ItemRelationshipsComponent); fixture = TestBed.createComponent(ItemRelationshipsComponent);
comp = fixture.componentInstance; comp = fixture.componentInstance;
de = fixture.debugElement; de = fixture.debugElement;

View File

@@ -4,12 +4,19 @@ import {
} from 'rxjs'; } from 'rxjs';
import { CacheableObject } from '../../core/cache/cacheable-object.model'; import { CacheableObject } from '../../core/cache/cacheable-object.model';
import { RemoteData } from '../../core/data/remote-data';
import { createSuccessfulRemoteDataObject$ } from '../remote-data.utils';
import { FollowLinkConfig } from '../utils/follow-link-config.model';
/** /**
* Stub class for {@link BaseDataService} * Stub class for {@link BaseDataService}
*/ */
export abstract class BaseDataServiceStub<T extends CacheableObject> { export abstract class BaseDataServiceStub<T extends CacheableObject> {
findByHref(_href$: string | Observable<string>, _useCachedVersionIfAvailable = true, _reRequestOnStale = true, ..._linksToFollow: FollowLinkConfig<T>[]): Observable<RemoteData<T>> {
return createSuccessfulRemoteDataObject$(undefined);
}
invalidateByHref(_href: string): Observable<boolean> { invalidateByHref(_href: string): Observable<boolean> {
return observableOf(true); return observableOf(true);
} }

View File

@@ -0,0 +1,48 @@
/* eslint-disable no-empty, @typescript-eslint/no-empty-function */
import {
Observable,
Subscription,
} from 'rxjs';
import {
DeleteRelationship,
RelationshipIdentifiable,
} from '../../core/data/object-updates/object-updates.reducer';
import { RemoteData } from '../../core/data/remote-data';
import { Item } from '../../core/shared/item.model';
import { Relationship } from '../../core/shared/item-relationships/relationship.model';
import { NoContent } from '../../core/shared/NoContent.model';
import { createSuccessfulRemoteDataObject$ } from '../remote-data.utils';
/**
* Stub class of {@link EditItemRelationshipsService}
*/
export class EditItemRelationshipsServiceStub {
submit(_item: Item, _url: string): void {
}
initializeOriginalFields(_item: Item, _url: string): Subscription {
return new Subscription();
}
deleteRelationship(_deleteRelationship: DeleteRelationship): Observable<RemoteData<NoContent>> {
return createSuccessfulRemoteDataObject$({});
}
addRelationship(_addRelationship: RelationshipIdentifiable): Observable<RemoteData<Relationship>> {
return createSuccessfulRemoteDataObject$(undefined);
}
displayNotifications(_responses: RemoteData<NoContent>[]): void {
}
getNotificationTitle(_key: string): string {
return '';
}
getNotificationContent(_key: string): string {
return '';
}
}

View File

@@ -0,0 +1,5 @@
/**
* Stub class of {@link EntityTypeDataService}
*/
export class EntityTypeDataServiceStub {
}

View File

@@ -0,0 +1,8 @@
import { Item } from '../../core/shared/item.model';
import { IdentifiableDataServiceStub } from './identifiable-data-service.stub';
/**
* Stub class of {@link ItemDataService}
*/
export class ItemDataServiceStub extends IdentifiableDataServiceStub<Item> {
}

View File

@@ -0,0 +1,24 @@
/* eslint-disable no-empty, @typescript-eslint/no-empty-function */
import {
Observable,
of as observableOf,
} from 'rxjs';
import { FieldUpdates } from '../../core/data/object-updates/field-updates.model';
import { Identifiable } from '../../core/data/object-updates/identifiable.model';
import { PatchOperationService } from '../../core/data/object-updates/patch-operation-service/patch-operation.service';
import { GenericConstructor } from '../../core/shared/generic-constructor';
/**
* Stub class of {@link ObjectUpdatesService}
*/
export class ObjectUpdatesServiceStub {
initialize(_url: string, _fields: Identifiable[], _lastModified: Date, _patchOperationService?: GenericConstructor<PatchOperationService>): void {
}
getFieldUpdates(_url: string, _initialFields: Identifiable[], _ignoreStates?: boolean): Observable<FieldUpdates> {
return observableOf({});
}
}

View File

@@ -0,0 +1,86 @@
/* eslint-disable no-empty, @typescript-eslint/no-empty-function */
import {
Observable,
of as observableOf,
} from 'rxjs';
import { FindListOptions } from '../../core/data/find-list-options.model';
import { PaginatedList } from '../../core/data/paginated-list.model';
import { RemoteData } from '../../core/data/remote-data';
import { DSpaceObject } from '../../core/shared/dspace-object.model';
import { Item } from '../../core/shared/item.model';
import { Relationship } from '../../core/shared/item-relationships/relationship.model';
import { MetadataValue } from '../../core/shared/metadata.models';
import { MetadataRepresentation } from '../../core/shared/metadata-representation/metadata-representation.model';
import { NoContent } from '../../core/shared/NoContent.model';
import { createSuccessfulRemoteDataObject$ } from '../remote-data.utils';
import { FollowLinkConfig } from '../utils/follow-link-config.model';
/**
* Stub class of {@link RelationshipDataService}
*/
export class RelationshipDataServiceStub {
deleteRelationship(_id: string, _copyVirtualMetadata: string, _shouldRefresh = true): Observable<RemoteData<NoContent>> {
return createSuccessfulRemoteDataObject$({});
}
addRelationship(_typeId: string, _item1: Item, _item2: Item, _leftwardValue?: string, _rightwardValue?: string, _shouldRefresh = true): Observable<RemoteData<Relationship>> {
return createSuccessfulRemoteDataObject$(new Relationship());
}
refreshRelationshipItemsInCache(_item: Item): void {
}
getItemRelationshipsArray(_item: Item, ..._linksToFollow: FollowLinkConfig<Relationship>[]): Observable<Relationship[]> {
return observableOf([]);
}
getRelatedItems(_item: Item): Observable<Item[]> {
return observableOf([]);
}
getRelatedItemsByLabel(_item: Item, _label: string, _options?: FindListOptions): Observable<RemoteData<PaginatedList<Item>>> {
return createSuccessfulRemoteDataObject$(new PaginatedList<Item>());
}
getItemRelationshipsByLabel(_item: Item, _label: string, _options?: FindListOptions, _useCachedVersionIfAvailable = true, _reRequestOnStale = true, ..._linksToFollow: FollowLinkConfig<Relationship>[]): Observable<RemoteData<PaginatedList<Relationship>>> {
return createSuccessfulRemoteDataObject$(new PaginatedList<Relationship>());
}
getRelationshipByItemsAndLabel(_item1: Item, _item2: Item, _label: string, _options?: FindListOptions): Observable<Relationship> {
return observableOf(new Relationship());
}
setNameVariant(_listID: string, _itemID: string, _nameVariant: string): void {
}
getNameVariant(_listID: string, _itemID: string): Observable<string> {
return observableOf('');
}
updateNameVariant(_item1: Item, _item2: Item, _relationshipLabel: string, _nameVariant: string): Observable<RemoteData<Relationship>> {
return createSuccessfulRemoteDataObject$(new Relationship());
}
isLeftItem(_relationship: Relationship, _item: Item): Observable<boolean> {
return observableOf(false);
}
update(_object: Relationship): Observable<RemoteData<Relationship>> {
return createSuccessfulRemoteDataObject$(new Relationship());
}
searchByItemsAndType(_typeId: string, _itemUuid: string, _relationshipLabel: string, _arrayOfItemIds: string[]): Observable<RemoteData<PaginatedList<Relationship>>> {
return createSuccessfulRemoteDataObject$(new PaginatedList<Relationship>());
}
searchBy(_searchMethod: string, _options?: FindListOptions, _useCachedVersionIfAvailable?: boolean, _reRequestOnStale?: boolean, ..._linksToFollow: FollowLinkConfig<Relationship>[]): Observable<RemoteData<PaginatedList<Relationship>>> {
return createSuccessfulRemoteDataObject$(new PaginatedList<Relationship>());
}
resolveMetadataRepresentation(_metadatum: MetadataValue, _parentItem: DSpaceObject, _itemType: string): Observable<MetadataRepresentation> {
return observableOf({} as MetadataRepresentation);
}
}