diff --git a/src/app/core/data/relationship-data.service.ts b/src/app/core/data/relationship-data.service.ts index b71ec0b6778..8514ab3e2ad 100644 --- a/src/app/core/data/relationship-data.service.ts +++ b/src/app/core/data/relationship-data.service.ts @@ -237,7 +237,7 @@ export class RelationshipDataService extends IdentifiableDataService { 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(() => { - 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); }); - it('should be created', () => { - expect(service).toBeTruthy(); + beforeEach(() => { + 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); + }); }); }); diff --git a/src/app/item-page/edit-item-page/item-relationships/edit-item-relationships.service.ts b/src/app/item-page/edit-item-page/item-relationships/edit-item-relationships.service.ts index 8a5204020c5..14fa226bf41 100644 --- a/src/app/item-page/edit-item-page/item-relationships/edit-item-relationships.service.ts +++ b/src/app/item-page/edit-item-page/item-relationships/edit-item-relationships.service.ts @@ -5,6 +5,7 @@ import { BehaviorSubject, EMPTY, Observable, + Subscription, } from 'rxjs'; import { concatMap, @@ -120,7 +121,7 @@ export class EditItemRelationshipsService { /** * 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( take(1), ).subscribe((items: Item[]) => { @@ -168,7 +169,7 @@ export class EditItemRelationshipsService { * - Success notification in case there's at least one successful response * @param responses */ - displayNotifications(responses: RemoteData[]) { + displayNotifications(responses: RemoteData[]): void { const failedResponses = responses.filter((response: RemoteData) => response.hasFailed); const successfulResponses = responses.filter((response: RemoteData) => response.hasSucceeded); @@ -186,7 +187,7 @@ export class EditItemRelationshipsService { * Get translated notification title * @param key */ - getNotificationTitle(key: string) { + getNotificationTitle(key: string): string { return this.translateService.instant(this.notificationsPrefix + key + '.title'); } @@ -194,7 +195,7 @@ export class EditItemRelationshipsService { * Get translated notification content * @param key */ - getNotificationContent(key: string) { + getNotificationContent(key: string): string { return this.translateService.instant(this.notificationsPrefix + key + '.content'); } diff --git a/src/app/item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.spec.ts b/src/app/item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.spec.ts index e28a3633c4c..42296a1d71a 100644 --- a/src/app/item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.spec.ts +++ b/src/app/item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.spec.ts @@ -10,19 +10,17 @@ import { import { By } from '@angular/platform-browser'; import { ActivatedRoute, - Router, + RouterModule, } from '@angular/router'; import { provideMockStore } from '@ngrx/store/testing'; import { TranslateModule } from '@ngx-translate/core'; +import { cold } from 'jasmine-marbles'; 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 { environment } from '../../../../../environments/environment.test'; import { REQUEST } from '../../../../../express.tokens'; +import { AuthRequestService } from '../../../../core/auth/auth-request.service'; import { LinkService } from '../../../../core/cache/builders/link.service'; import { ConfigurationDataService } from '../../../../core/data/configuration-data.service'; 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 { GroupDataService } from '../../../../core/eperson/group-data.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 { ConfigurationProperty } from '../../../../core/shared/configuration-property.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 { XSRFService } from '../../../../core/xsrf/xsrf.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 { PaginationComponent } from '../../../../shared/pagination/pagination.component'; import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model'; 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 { PaginationServiceStub } from '../../../../shared/testing/pagination-service.stub'; import { SearchConfigurationServiceStub } from '../../../../shared/testing/search-configuration-service.stub'; import { createPaginatedList } from '../../../../shared/testing/utils.test'; +import { EditItemRelationshipsService } from '../edit-item-relationships.service'; import { EditRelationshipListComponent } from './edit-relationship-list.component'; -let comp: EditRelationshipListComponent; -let fixture: ComponentFixture; -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', () => { + let comp: EditRelationshipListComponent; + let fixture: ComponentFixture; + 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 = () => { fixture = TestBed.createComponent(EditRelationshipListComponent); comp = fixture.componentInstance; de = fixture.debugElement; - comp.item = item; - comp.itemType = entityType; + comp.item = itemLeft; + comp.itemType = entityTypeLeft; comp.url = url; comp.relationshipType = relationshipType; comp.hasChanges = observableOf(false); @@ -101,29 +105,26 @@ describe('EditRelationshipListComponent', () => { }, }; - hardRedirectService = jasmine.createSpyObj('hardRedirectService', ['redirect']); - - beforeEach(waitForAsync(() => { - - entityType = Object.assign(new ItemType(), { - id: 'Publication', - uuid: 'Publication', - label: 'Publication', + function init(leftType: string, rightType: string): void { + entityTypeLeft = Object.assign(new ItemType(), { + id: leftType, + uuid: leftType, + label: leftType, }); - relatedEntityType = Object.assign(new ItemType(), { - id: 'Author', - uuid: 'Author', - label: 'Author', + entityTypeRight = Object.assign(new ItemType(), { + id: rightType, + uuid: rightType, + label: rightType, }); relationshipType = Object.assign(new RelationshipType(), { id: '1', uuid: '1', - leftType: createSuccessfulRemoteDataObject$(entityType), - rightType: createSuccessfulRemoteDataObject$(relatedEntityType), - leftwardType: 'isAuthorOfPublication', - rightwardType: 'isPublicationOfAuthor', + leftType: createSuccessfulRemoteDataObject$(entityTypeLeft), + rightType: createSuccessfulRemoteDataObject$(entityTypeRight), + leftwardType: `is${rightType}Of${leftType}`, + rightwardType: `is${leftType}Of${rightType}`, }); paginationOptions = Object.assign(new PaginationComponentOptions(), { @@ -132,13 +133,13 @@ describe('EditRelationshipListComponent', () => { currentPage: 1, }); - author1 = Object.assign(new Item(), { - id: 'author1', - uuid: 'author1', + itemRight1 = Object.assign(new Item(), { + id: `${rightType}-1`, + uuid: `${rightType}-1`, }); - author2 = Object.assign(new Item(), { - id: 'author2', - uuid: 'author2', + itemRight2 = Object.assign(new Item(), { + id: `${rightType}-2`, + uuid: `${rightType}-2`, }); relationships = [ @@ -147,25 +148,25 @@ describe('EditRelationshipListComponent', () => { id: '2', uuid: '2', relationshipType: createSuccessfulRemoteDataObject$(relationshipType), - leftItem: createSuccessfulRemoteDataObject$(item), - rightItem: createSuccessfulRemoteDataObject$(author1), + leftItem: createSuccessfulRemoteDataObject$(itemLeft), + rightItem: createSuccessfulRemoteDataObject$(itemRight1), }), Object.assign(new Relationship(), { self: url + '/3', id: '3', uuid: '3', relationshipType: createSuccessfulRemoteDataObject$(relationshipType), - leftItem: createSuccessfulRemoteDataObject$(item), - rightItem: createSuccessfulRemoteDataObject$(author2), + leftItem: createSuccessfulRemoteDataObject$(itemLeft), + rightItem: createSuccessfulRemoteDataObject$(itemRight2), }), ]; - item = Object.assign(new Item(), { + itemLeft = Object.assign(new Item(), { _links: { self: { href: 'fake-item-url/publication' }, }, - id: 'publication', - uuid: 'publication', + id: `1-${leftType}`, + uuid: `1-${leftType}`, relationships: createSuccessfulRemoteDataObject$(createPaginatedList(relationships)), }); @@ -197,7 +198,7 @@ describe('EditRelationshipListComponent', () => { relationshipService = jasmine.createSpyObj('relationshipService', { - getRelatedItemsByLabel: createSuccessfulRemoteDataObject$(createPaginatedList([author1, author2])), + getRelatedItemsByLabel: createSuccessfulRemoteDataObject$(createPaginatedList([itemRight1, itemRight2])), getItemRelationshipsByLabel: createSuccessfulRemoteDataObject$(createPaginatedList(relationships)), isLeftItem: observableOf(true), }, @@ -233,14 +234,14 @@ describe('EditRelationshipListComponent', () => { })), }); - const environmentUseThumbs = { - browseBy: { - showThumbnails: true, - }, - }; + editItemRelationshipsService = new EditItemRelationshipsServiceStub(); TestBed.configureTestingModule({ - imports: [TranslateModule.forRoot(), EditRelationshipListComponent], + imports: [ + EditRelationshipListComponent, + RouterModule.forRoot([]), + TranslateModule.forRoot(), + ], providers: [ provideMockStore({ initialState }), { provide: ObjectUpdatesService, useValue: objectUpdatesService }, @@ -251,15 +252,15 @@ describe('EditRelationshipListComponent', () => { { provide: HostWindowService, useValue: hostWindowService }, { provide: RelationshipTypeDataService, useValue: relationshipTypeService }, { provide: GroupDataService, useValue: groupDataService }, - { provide: Router, useValue: new RouterMock() }, { provide: LinkHeadService, useValue: linkHeadService }, { provide: ConfigurationDataService, useValue: configurationDataService }, { provide: SearchConfigurationService, useValue: new SearchConfigurationServiceStub() }, + { provide: EditItemRelationshipsService, useValue: editItemRelationshipsService }, { provide: ActivatedRoute, useValue: new ActivatedRouteStub() }, { provide: AuthRequestService, useValue: new AuthRequestServiceStub() }, { provide: HardRedirectService, useValue: hardRedirectService }, { provide: XSRFService, useValue: {} }, - { provide: APP_CONFIG, useValue: environmentUseThumbs }, + { provide: APP_CONFIG, useValue: environment }, { provide: REQUEST, useValue: {} }, CookieService, ], schemas: [ @@ -268,114 +269,127 @@ describe('EditRelationshipListComponent', () => { }).compileComponents(); resetComponent(); - })); - - describe('changeType is REMOVE', () => { - beforeEach(() => { - 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', () => { - let paginationComp: PaginationComponent; + } - beforeEach(() => { - paginationComp = de.query(By.css('ds-pagination')).componentInstance; - }); + describe('Publication - Author relationship', () => { + beforeEach(waitForAsync(() => init('Publication', 'Author'))); - it('should receive the correct pagination config', () => { - expect(paginationComp.paginationOptions).toEqual(paginationOptions); + describe('changeType is REMOVE', () => { + beforeEach(() => { + 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'); + }); }); - it('should receive correct collection size', () => { - expect(paginationComp.collectionSize).toEqual(relationships.length); - }); + describe('pagination component', () => { + let paginationComp: PaginationComponent; - }); + beforeEach(() => { + paginationComp = de.query(By.css('ds-pagination')).componentInstance; + }); - describe('relationshipService.getItemRelationshipsByLabel', () => { - it('should receive the correct pagination info', () => { - expect(relationshipService.getItemRelationshipsByLabel).toHaveBeenCalledTimes(1); + it('should receive the correct pagination config', () => { + expect(paginationComp.paginationOptions).toEqual(paginationOptions); + }); - 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'); + it('should receive correct collection size', () => { + expect(paginationComp.collectionSize).toEqual(relationships.length); + }); }); - describe('when the publication is on the left side of the relationship', () => { - beforeEach(() => { - relationshipType = Object.assign(new RelationshipType(), { - 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', () => { + describe('relationshipService.getItemRelationshipsByLabel', () => { + it('should receive the correct pagination info', () => { expect(relationshipService.getItemRelationshipsByLabel).toHaveBeenCalledTimes(1); const callArgs = relationshipService.getItemRelationshipsByLabel.calls.mostRecent().args; - const label = callArgs[1]; + 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'); - 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', + 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(); }); - relationshipService.getItemRelationshipsByLabel.calls.reset(); - resetComponent(); - }); - it('should fetch isAuthorOfPublication', () => { - expect(relationshipService.getItemRelationshipsByLabel).toHaveBeenCalledTimes(1); + it('should fetch isAuthorOfPublication', () => { + expect(relationshipService.getItemRelationshipsByLabel).toHaveBeenCalledTimes(1); - const callArgs = relationshipService.getItemRelationshipsByLabel.calls.mostRecent().args; - const label = callArgs[1]; + const callArgs = relationshipService.getItemRelationshipsByLabel.calls.mostRecent().args; + const label = callArgs[1]; - expect(label).toEqual('isAuthorOfPublication'); + 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); - describe('changes managment for add buttons', () => { + const callArgs = relationshipService.getItemRelationshipsByLabel.calls.mostRecent().args; + const label = callArgs[1]; - it('should show enabled add buttons', () => { - const element = de.query(By.css('.btn-success')); - expect(element.nativeElement?.disabled).toBeFalse(); + expect(label).toEqual('isAuthorOfPublication'); + }); }); - 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('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, + })); + }); + }); }); diff --git a/src/app/item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.ts b/src/app/item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.ts index 3ae342e14ef..6c3b8d556a4 100644 --- a/src/app/item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.ts +++ b/src/app/item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.ts @@ -50,7 +50,6 @@ import { RelationshipIdentifiable } from '../../../../core/data/object-updates/o import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service'; import { PaginatedList } from '../../../../core/data/paginated-list.model'; 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 { PaginationService } from '../../../../core/pagination/pagination.service'; import { Collection } from '../../../../core/shared/collection.model'; @@ -146,7 +145,7 @@ export class EditRelationshipListComponent implements OnInit, OnDestroy { */ private currentItemIsLeftItem$: BehaviorSubject = new BehaviorSubject(undefined); - private relatedEntityType$: Observable; + relatedEntityType$: Observable; /** * The translation key for the entity type @@ -208,7 +207,6 @@ export class EditRelationshipListComponent implements OnInit, OnDestroy { protected objectUpdatesService: ObjectUpdatesService, protected linkService: LinkService, protected relationshipService: RelationshipDataService, - protected relationshipTypeService: RelationshipTypeDataService, protected modalService: NgbModal, protected paginationService: PaginationService, protected selectableListService: SelectableListService, diff --git a/src/app/item-page/edit-item-page/item-relationships/item-relationships.component.spec.ts b/src/app/item-page/edit-item-page/item-relationships/item-relationships.component.spec.ts index 4cd30617441..05dade88ba7 100644 --- a/src/app/item-page/edit-item-page/item-relationships/item-relationships.component.spec.ts +++ b/src/app/item-page/edit-item-page/item-relationships/item-relationships.component.spec.ts @@ -44,6 +44,7 @@ import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$, } from '../../../shared/remote-data.utils'; +import { ItemDataServiceStub } from '../../../shared/testing/item-data.service.stub'; import { relationshipTypes } from '../../../shared/testing/relationship-types.mock'; import { RouterStub } from '../../../shared/testing/router.stub'; import { createPaginatedList } from '../../../shared/testing/utils.test'; @@ -72,7 +73,7 @@ const notificationsService = jasmine.createSpyObj('notificationsService', const router = new RouterStub(); let relationshipTypeService; let routeStub; -let itemService; +let itemService: ItemDataServiceStub; const url = 'http://test-url.com/test-url'; router.url = url; @@ -157,10 +158,7 @@ describe('ItemRelationshipsComponent', () => { changeType: FieldChangeType.REMOVE, }; - itemService = jasmine.createSpyObj('itemService', { - findByHref: createSuccessfulRemoteDataObject$(item), - findById: createSuccessfulRemoteDataObject$(item), - }); + itemService = new ItemDataServiceStub(); routeStub = { data: observableOf({}), parent: { @@ -251,6 +249,8 @@ describe('ItemRelationshipsComponent', () => { })); beforeEach(() => { + spyOn(itemService, 'findByHref').and.returnValue(item); + spyOn(itemService, 'findById').and.returnValue(item); fixture = TestBed.createComponent(ItemRelationshipsComponent); comp = fixture.componentInstance; de = fixture.debugElement; diff --git a/src/app/shared/testing/base-data-service.stub.ts b/src/app/shared/testing/base-data-service.stub.ts index 3df661ba5b7..65b761ba712 100644 --- a/src/app/shared/testing/base-data-service.stub.ts +++ b/src/app/shared/testing/base-data-service.stub.ts @@ -4,12 +4,19 @@ import { } from 'rxjs'; 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} */ export abstract class BaseDataServiceStub { + findByHref(_href$: string | Observable, _useCachedVersionIfAvailable = true, _reRequestOnStale = true, ..._linksToFollow: FollowLinkConfig[]): Observable> { + return createSuccessfulRemoteDataObject$(undefined); + } + invalidateByHref(_href: string): Observable { return observableOf(true); } diff --git a/src/app/shared/testing/edit-item-relationships.service.stub.ts b/src/app/shared/testing/edit-item-relationships.service.stub.ts new file mode 100644 index 00000000000..1d295697936 --- /dev/null +++ b/src/app/shared/testing/edit-item-relationships.service.stub.ts @@ -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> { + return createSuccessfulRemoteDataObject$({}); + } + + addRelationship(_addRelationship: RelationshipIdentifiable): Observable> { + return createSuccessfulRemoteDataObject$(undefined); + } + + displayNotifications(_responses: RemoteData[]): void { + } + + getNotificationTitle(_key: string): string { + return ''; + } + + getNotificationContent(_key: string): string { + return ''; + } + +} diff --git a/src/app/shared/testing/entity-type-data.service.stub.ts b/src/app/shared/testing/entity-type-data.service.stub.ts new file mode 100644 index 00000000000..367eeb5f743 --- /dev/null +++ b/src/app/shared/testing/entity-type-data.service.stub.ts @@ -0,0 +1,5 @@ +/** + * Stub class of {@link EntityTypeDataService} + */ +export class EntityTypeDataServiceStub { +} diff --git a/src/app/shared/testing/item-data.service.stub.ts b/src/app/shared/testing/item-data.service.stub.ts new file mode 100644 index 00000000000..eed5d4bb119 --- /dev/null +++ b/src/app/shared/testing/item-data.service.stub.ts @@ -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 { +} diff --git a/src/app/shared/testing/object-updates.service.stub.ts b/src/app/shared/testing/object-updates.service.stub.ts new file mode 100644 index 00000000000..c66cc3aa0dd --- /dev/null +++ b/src/app/shared/testing/object-updates.service.stub.ts @@ -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): void { + } + + getFieldUpdates(_url: string, _initialFields: Identifiable[], _ignoreStates?: boolean): Observable { + return observableOf({}); + } + +} diff --git a/src/app/shared/testing/relationship-data.service.stub.ts b/src/app/shared/testing/relationship-data.service.stub.ts new file mode 100644 index 00000000000..f0463b3e6ca --- /dev/null +++ b/src/app/shared/testing/relationship-data.service.stub.ts @@ -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> { + return createSuccessfulRemoteDataObject$({}); + } + + addRelationship(_typeId: string, _item1: Item, _item2: Item, _leftwardValue?: string, _rightwardValue?: string, _shouldRefresh = true): Observable> { + return createSuccessfulRemoteDataObject$(new Relationship()); + } + + refreshRelationshipItemsInCache(_item: Item): void { + } + + getItemRelationshipsArray(_item: Item, ..._linksToFollow: FollowLinkConfig[]): Observable { + return observableOf([]); + } + + getRelatedItems(_item: Item): Observable { + return observableOf([]); + } + + getRelatedItemsByLabel(_item: Item, _label: string, _options?: FindListOptions): Observable>> { + return createSuccessfulRemoteDataObject$(new PaginatedList()); + } + + getItemRelationshipsByLabel(_item: Item, _label: string, _options?: FindListOptions, _useCachedVersionIfAvailable = true, _reRequestOnStale = true, ..._linksToFollow: FollowLinkConfig[]): Observable>> { + return createSuccessfulRemoteDataObject$(new PaginatedList()); + } + + getRelationshipByItemsAndLabel(_item1: Item, _item2: Item, _label: string, _options?: FindListOptions): Observable { + return observableOf(new Relationship()); + } + + setNameVariant(_listID: string, _itemID: string, _nameVariant: string): void { + } + + getNameVariant(_listID: string, _itemID: string): Observable { + return observableOf(''); + } + + updateNameVariant(_item1: Item, _item2: Item, _relationshipLabel: string, _nameVariant: string): Observable> { + return createSuccessfulRemoteDataObject$(new Relationship()); + } + + isLeftItem(_relationship: Relationship, _item: Item): Observable { + return observableOf(false); + } + + update(_object: Relationship): Observable> { + return createSuccessfulRemoteDataObject$(new Relationship()); + } + + searchByItemsAndType(_typeId: string, _itemUuid: string, _relationshipLabel: string, _arrayOfItemIds: string[]): Observable>> { + return createSuccessfulRemoteDataObject$(new PaginatedList()); + } + + searchBy(_searchMethod: string, _options?: FindListOptions, _useCachedVersionIfAvailable?: boolean, _reRequestOnStale?: boolean, ..._linksToFollow: FollowLinkConfig[]): Observable>> { + return createSuccessfulRemoteDataObject$(new PaginatedList()); + } + + resolveMetadataRepresentation(_metadatum: MetadataValue, _parentItem: DSpaceObject, _itemType: string): Observable { + return observableOf({} as MetadataRepresentation); + } + +}