diff --git a/karma.conf.js b/karma.conf.js index 8418312b1ab..f96558bfaff 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -15,7 +15,10 @@ module.exports = function (config) { ], client: { clearContext: false, // leave Jasmine Spec Runner output visible in browser - captureConsole: false + captureConsole: false, + jasmine: { + failSpecWithNoExpectations: true + } }, coverageIstanbulReporter: { dir: require('path').join(__dirname, './coverage/dspace-angular'), diff --git a/src/app/access-control/epeople-registry/epeople-registry.component.spec.ts b/src/app/access-control/epeople-registry/epeople-registry.component.spec.ts index e2cee5e9356..db5405c70b1 100644 --- a/src/app/access-control/epeople-registry/epeople-registry.component.spec.ts +++ b/src/app/access-control/epeople-registry/epeople-registry.component.spec.ts @@ -5,8 +5,8 @@ import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core'; import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { BrowserModule, By } from '@angular/platform-browser'; -import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; -import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core'; +import { NgbModule, NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { TranslateModule } from '@ngx-translate/core'; import { buildPaginatedList, PaginatedList } from '../../core/data/paginated-list.model'; import { RemoteData } from '../../core/data/remote-data'; import { EPersonDataService } from '../../core/eperson/eperson-data.service'; @@ -18,8 +18,6 @@ import { EPeopleRegistryComponent } from './epeople-registry.component'; import { EPersonMock, EPersonMock2 } from '../../shared/testing/eperson.mock'; import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; import { getMockFormBuilderService } from '../../shared/mocks/form-builder-service.mock'; -import { getMockTranslateService } from '../../shared/mocks/translate.service.mock'; -import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock'; import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub'; import { RouterStub } from '../../shared/testing/router.stub'; import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; @@ -31,17 +29,15 @@ import { FindListOptions } from '../../core/data/find-list-options.model'; describe('EPeopleRegistryComponent', () => { let component: EPeopleRegistryComponent; let fixture: ComponentFixture; - let translateService: TranslateService; let builderService: FormBuilderService; - let mockEPeople; + let mockEPeople: EPerson[]; let ePersonDataServiceStub: any; let authorizationService: AuthorizationDataService; - let modalService; + let modalService: NgbModal; + let paginationService: PaginationServiceStub; - let paginationService; - - beforeEach(waitForAsync(() => { + beforeEach(waitForAsync(async () => { jasmine.getEnv().allowRespy(true); mockEPeople = [EPersonMock, EPersonMock2]; ePersonDataServiceStub = { @@ -99,7 +95,7 @@ describe('EPeopleRegistryComponent', () => { deleteEPerson(ePerson: EPerson): Observable { this.allEpeople = this.allEpeople.filter((ePerson2: EPerson) => { return (ePerson2.uuid !== ePerson.uuid); - }); + }); return observableOf(true); }, editEPerson(ePerson: EPerson) { @@ -119,17 +115,11 @@ describe('EPeopleRegistryComponent', () => { isAuthorized: observableOf(true) }); builderService = getMockFormBuilderService(); - translateService = getMockTranslateService(); paginationService = new PaginationServiceStub(); - TestBed.configureTestingModule({ + await TestBed.configureTestingModule({ imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule, - TranslateModule.forRoot({ - loader: { - provide: TranslateLoader, - useClass: TranslateLoaderMock - } - }), + TranslateModule.forRoot(), ], declarations: [EPeopleRegistryComponent], providers: [ @@ -148,7 +138,7 @@ describe('EPeopleRegistryComponent', () => { beforeEach(() => { fixture = TestBed.createComponent(EPeopleRegistryComponent); component = fixture.componentInstance; - modalService = (component as any).modalService; + modalService = TestBed.inject(NgbModal); spyOn(modalService, 'open').and.returnValue(Object.assign({ componentInstance: Object.assign({ response: observableOf(true) }) })); fixture.detectChanges(); }); @@ -158,10 +148,10 @@ describe('EPeopleRegistryComponent', () => { }); it('should display list of ePeople', () => { - const ePeopleIdsFound = fixture.debugElement.queryAll(By.css('#epeople tr td:first-child')); + const ePeopleIdsFound: DebugElement[] = fixture.debugElement.queryAll(By.css('#epeople tr td:first-child')); expect(ePeopleIdsFound.length).toEqual(2); mockEPeople.map((ePerson: EPerson) => { - expect(ePeopleIdsFound.find((foundEl) => { + expect(ePeopleIdsFound.find((foundEl: DebugElement) => { return (foundEl.nativeElement.textContent.trim() === ePerson.uuid); })).toBeTruthy(); }); @@ -169,7 +159,7 @@ describe('EPeopleRegistryComponent', () => { describe('search', () => { describe('when searching with scope/query (scope metadata)', () => { - let ePeopleIdsFound; + let ePeopleIdsFound: DebugElement[]; beforeEach(fakeAsync(() => { component.search({ scope: 'metadata', query: EPersonMock2.name }); tick(); @@ -179,14 +169,14 @@ describe('EPeopleRegistryComponent', () => { it('should display search result', () => { expect(ePeopleIdsFound.length).toEqual(1); - expect(ePeopleIdsFound.find((foundEl) => { + expect(ePeopleIdsFound.find((foundEl: DebugElement) => { return (foundEl.nativeElement.textContent.trim() === EPersonMock2.uuid); })).toBeTruthy(); }); }); describe('when searching with scope/query (scope email)', () => { - let ePeopleIdsFound; + let ePeopleIdsFound: DebugElement[]; beforeEach(fakeAsync(() => { component.search({ scope: 'email', query: EPersonMock.email }); tick(); @@ -196,7 +186,7 @@ describe('EPeopleRegistryComponent', () => { it('should display search result', () => { expect(ePeopleIdsFound.length).toEqual(1); - expect(ePeopleIdsFound.find((foundEl) => { + expect(ePeopleIdsFound.find((foundEl: DebugElement) => { return (foundEl.nativeElement.textContent.trim() === EPersonMock.uuid); })).toBeTruthy(); }); @@ -228,19 +218,12 @@ describe('EPeopleRegistryComponent', () => { }); }); - describe('delete EPerson button when the isAuthorized returns false', () => { - let ePeopleDeleteButton; - beforeEach(() => { - spyOn(authorizationService, 'isAuthorized').and.returnValue(observableOf(false)); - component.initialisePage(); - fixture.detectChanges(); - }); - it('should be disabled', () => { - ePeopleDeleteButton = fixture.debugElement.queryAll(By.css('#epeople tr td div button.delete-button')); - ePeopleDeleteButton.forEach((deleteButton: DebugElement) => { - expect(deleteButton.nativeElement.disabled).toBe(true); - }); - }); + it('should hide delete EPerson button when the isAuthorized returns false', () => { + spyOn(authorizationService, 'isAuthorized').and.returnValue(observableOf(false)); + component.initialisePage(); + fixture.detectChanges(); + + expect(fixture.debugElement.query(By.css('#epeople tr td div button.delete-button'))).toBeNull(); }); }); diff --git a/src/app/community-page/sections/sub-com-col-section/sub-collection-list/community-page-sub-collection-list.component.spec.ts b/src/app/community-page/sections/sub-com-col-section/sub-collection-list/community-page-sub-collection-list.component.spec.ts index 8c4af30991b..2655753021a 100644 --- a/src/app/community-page/sections/sub-com-col-section/sub-collection-list/community-page-sub-collection-list.component.spec.ts +++ b/src/app/community-page/sections/sub-com-col-section/sub-collection-list/community-page-sub-collection-list.component.spec.ts @@ -1,6 +1,6 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { TranslateModule } from '@ngx-translate/core'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core'; import { By } from '@angular/platform-browser'; import { RouterTestingModule } from '@angular/router/testing'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; @@ -30,7 +30,7 @@ import { ConfigurationProperty } from '../../../../core/shared/configuration-pro import { createPaginatedList } from '../../../../shared/testing/utils.test'; import { SearchConfigurationServiceStub } from '../../../../shared/testing/search-configuration-service.stub'; -describe('CommunityPageSubCollectionList Component', () => { +describe('CommunityPageSubCollectionListComponent', () => { let comp: CommunityPageSubCollectionListComponent; let fixture: ComponentFixture; let collectionDataServiceStub: any; @@ -177,19 +177,19 @@ describe('CommunityPageSubCollectionList Component', () => { }); - it('should display a list of collections', () => { - waitForAsync(() => { - subCollList = collections; - fixture.detectChanges(); + it('should display a list of collections', async () => { + subCollList = collections; + fixture.detectChanges(); + await fixture.whenStable(); + fixture.detectChanges(); - const collList = fixture.debugElement.queryAll(By.css('li')); - expect(collList.length).toEqual(5); - expect(collList[0].nativeElement.textContent).toContain('Collection 1'); - expect(collList[1].nativeElement.textContent).toContain('Collection 2'); - expect(collList[2].nativeElement.textContent).toContain('Collection 3'); - expect(collList[3].nativeElement.textContent).toContain('Collection 4'); - expect(collList[4].nativeElement.textContent).toContain('Collection 5'); - }); + const collList: DebugElement[] = fixture.debugElement.queryAll(By.css('ul[data-test="objects"] li')); + expect(collList.length).toEqual(5); + expect(collList[0].nativeElement.textContent).toContain('Collection 1'); + expect(collList[1].nativeElement.textContent).toContain('Collection 2'); + expect(collList[2].nativeElement.textContent).toContain('Collection 3'); + expect(collList[3].nativeElement.textContent).toContain('Collection 4'); + expect(collList[4].nativeElement.textContent).toContain('Collection 5'); }); it('should not display the header when list of collections is empty', () => { diff --git a/src/app/community-page/sections/sub-com-col-section/sub-community-list/community-page-sub-community-list.component.spec.ts b/src/app/community-page/sections/sub-com-col-section/sub-community-list/community-page-sub-community-list.component.spec.ts index c5efc9c2c1c..a51f09d7a56 100644 --- a/src/app/community-page/sections/sub-com-col-section/sub-community-list/community-page-sub-community-list.component.spec.ts +++ b/src/app/community-page/sections/sub-com-col-section/sub-community-list/community-page-sub-community-list.component.spec.ts @@ -1,6 +1,6 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { TranslateModule } from '@ngx-translate/core'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core'; import { RouterTestingModule } from '@angular/router/testing'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { By } from '@angular/platform-browser'; @@ -30,7 +30,7 @@ import { SearchConfigurationServiceStub } from '../../../../shared/testing/searc import { ConfigurationProperty } from '../../../../core/shared/configuration-property.model'; import { createPaginatedList } from '../../../../shared/testing/utils.test'; -describe('CommunityPageSubCommunityListComponent Component', () => { +describe('CommunityPageSubCommunityListComponent', () => { let comp: CommunityPageSubCommunityListComponent; let fixture: ComponentFixture; let communityDataServiceStub: any; @@ -179,19 +179,19 @@ describe('CommunityPageSubCommunityListComponent Component', () => { }); - it('should display a list of sub-communities', () => { - waitForAsync(() => { - subCommList = subcommunities; - fixture.detectChanges(); + it('should display a list of sub-communities', async () => { + subCommList = subcommunities; + fixture.detectChanges(); + await fixture.whenStable(); + fixture.detectChanges(); - const subComList = fixture.debugElement.queryAll(By.css('li')); - expect(subComList.length).toEqual(5); - expect(subComList[0].nativeElement.textContent).toContain('SubCommunity 1'); - expect(subComList[1].nativeElement.textContent).toContain('SubCommunity 2'); - expect(subComList[2].nativeElement.textContent).toContain('SubCommunity 3'); - expect(subComList[3].nativeElement.textContent).toContain('SubCommunity 4'); - expect(subComList[4].nativeElement.textContent).toContain('SubCommunity 5'); - }); + const subComList: DebugElement[] = fixture.debugElement.queryAll(By.css('ul[data-test="objects"] li')); + expect(subComList.length).toEqual(5); + expect(subComList[0].nativeElement.textContent).toContain('SubCommunity 1'); + expect(subComList[1].nativeElement.textContent).toContain('SubCommunity 2'); + expect(subComList[2].nativeElement.textContent).toContain('SubCommunity 3'); + expect(subComList[3].nativeElement.textContent).toContain('SubCommunity 4'); + expect(subComList[4].nativeElement.textContent).toContain('SubCommunity 5'); }); it('should not display the header when list of sub-communities is empty', () => { diff --git a/src/app/core/auth/auth.interceptor.spec.ts b/src/app/core/auth/auth.interceptor.spec.ts index 04bbc4acaf0..1cf812fcbf5 100644 --- a/src/app/core/auth/auth.interceptor.spec.ts +++ b/src/app/core/auth/auth.interceptor.spec.ts @@ -20,9 +20,7 @@ describe(`AuthInterceptor`, () => { const authServiceStub = new AuthServiceStub(); const store: Store = jasmine.createSpyObj('store', { - /* eslint-disable no-empty,@typescript-eslint/no-empty-function */ dispatch: {}, - /* eslint-enable no-empty, @typescript-eslint/no-empty-function */ select: observableOf(true) }); @@ -46,6 +44,10 @@ describe(`AuthInterceptor`, () => { httpMock = TestBed.inject(HttpTestingController); }); + afterEach(() => { + httpMock.verify(); + }); + describe('when has a valid token', () => { it('should not add an Authorization header when we’re sending a HTTP request to \'authn\' endpoint that is not the logout endpoint', () => { @@ -95,14 +97,11 @@ describe(`AuthInterceptor`, () => { }); it('should redirect to login', () => { - - service.request(RestRequestMethod.POST, 'dspace-spring-rest/api/submission/workspaceitems', 'password=password&user=user').subscribe((response) => { - expect(response).toBeTruthy(); - }); - service.request(RestRequestMethod.POST, 'dspace-spring-rest/api/submission/workspaceitems', 'password=password&user=user'); httpMock.expectNone('dspace-spring-rest/api/submission/workspaceitems'); + // HttpTestingController.expectNone will throw an error when a requests is made + expect().nothing(); }); }); diff --git a/src/app/core/cache/object-cache.reducer.spec.ts b/src/app/core/cache/object-cache.reducer.spec.ts index 919edc8e577..df22a65da7a 100644 --- a/src/app/core/cache/object-cache.reducer.spec.ts +++ b/src/app/core/cache/object-cache.reducer.spec.ts @@ -126,6 +126,10 @@ describe('objectCacheReducer', () => { deepFreeze(state); objectCacheReducer(state, action); + + // no expect required, deepFreeze will ensure an exception is thrown if the state + // is mutated, and any uncaught exception will cause the test to fail + expect().nothing(); }); it('should remove the specified object from the cache in response to the REMOVE action', () => { @@ -149,6 +153,10 @@ describe('objectCacheReducer', () => { const action = new RemoveFromObjectCacheAction(selfLink1); // testState has already been frozen above objectCacheReducer(testState, action); + + // no expect required, deepFreeze will ensure an exception is thrown if the state + // is mutated, and any uncaught exception will cause the test to fail + expect().nothing(); }); it('should set the timestamp of all objects in the cache in response to a RESET_TIMESTAMPS action', () => { @@ -164,6 +172,10 @@ describe('objectCacheReducer', () => { const action = new ResetObjectCacheTimestampsAction(new Date().getTime()); // testState has already been frozen above objectCacheReducer(testState, action); + + // no expect required, deepFreeze will ensure an exception is thrown if the state + // is mutated, and any uncaught exception will cause the test to fail + expect().nothing(); }); it('should perform the ADD_PATCH action without affecting the previous state', () => { @@ -174,6 +186,10 @@ describe('objectCacheReducer', () => { }]); // testState has already been frozen above objectCacheReducer(testState, action); + + // no expect required, deepFreeze will ensure an exception is thrown if the state + // is mutated, and any uncaught exception will cause the test to fail + expect().nothing(); }); it('should when the ADD_PATCH action dispatched', () => { diff --git a/src/app/core/cache/server-sync-buffer.reducer.spec.ts b/src/app/core/cache/server-sync-buffer.reducer.spec.ts index 51ba010c1e3..087080a1946 100644 --- a/src/app/core/cache/server-sync-buffer.reducer.spec.ts +++ b/src/app/core/cache/server-sync-buffer.reducer.spec.ts @@ -52,12 +52,20 @@ describe('serverSyncBufferReducer', () => { const action = new AddToSSBAction(selfLink1, RestRequestMethod.POST); // testState has already been frozen above serverSyncBufferReducer(testState, action); + + // no expect required, deepFreeze will ensure an exception is thrown if the state + // is mutated, and any uncaught exception will cause the test to fail + expect().nothing(); }); it('should perform the EMPTY action without affecting the previous state', () => { const action = new EmptySSBAction(); // testState has already been frozen above serverSyncBufferReducer(testState, action); + + // no expect required, deepFreeze will ensure an exception is thrown if the state + // is mutated, and any uncaught exception will cause the test to fail + expect().nothing(); }); it('should empty the buffer if the EmptySSBAction is dispatched without a payload', () => { diff --git a/src/app/core/coar-notify/notify-info/notify-info.service.spec.ts b/src/app/core/coar-notify/notify-info/notify-info.service.spec.ts index a3cc360a969..6d51c33c996 100644 --- a/src/app/core/coar-notify/notify-info/notify-info.service.spec.ts +++ b/src/app/core/coar-notify/notify-info/notify-info.service.spec.ts @@ -1,9 +1,9 @@ import { TestBed } from '@angular/core/testing'; - import { NotifyInfoService } from './notify-info.service'; import { ConfigurationDataService } from '../../data/configuration-data.service'; import { of } from 'rxjs'; import { AuthorizationDataService } from '../../data/feature-authorization/authorization-data.service'; +import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; describe('NotifyInfoService', () => { let service: NotifyInfoService; @@ -32,21 +32,21 @@ describe('NotifyInfoService', () => { expect(service).toBeTruthy(); }); - it('should retrieve and map coar configuration', () => { - const mockResponse = { payload: { values: ['true'] } }; - (configurationDataService.findByPropertyName as jasmine.Spy).and.returnValue(of(mockResponse)); + it('should retrieve and map coar configuration', (done: DoneFn) => { + (configurationDataService.findByPropertyName as jasmine.Spy).and.returnValue(createSuccessfulRemoteDataObject$({ values: ['true'] })); service.isCoarConfigEnabled().subscribe((result) => { expect(result).toBe(true); + done(); }); }); - it('should retrieve and map LDN local inbox URLs', () => { - const mockResponse = { values: ['inbox1', 'inbox2'] }; - (configurationDataService.findByPropertyName as jasmine.Spy).and.returnValue(of(mockResponse)); + it('should retrieve and map LDN local inbox URLs', (done: DoneFn) => { + (configurationDataService.findByPropertyName as jasmine.Spy).and.returnValue(createSuccessfulRemoteDataObject$({ values: ['inbox1', 'inbox2'] })); service.getCoarLdnLocalInboxUrls().subscribe((result) => { expect(result).toEqual(['inbox1', 'inbox2']); + done(); }); }); diff --git a/src/app/core/data/external-source-data.service.spec.ts b/src/app/core/data/external-source-data.service.spec.ts index 723d7f9bed6..78d91437efc 100644 --- a/src/app/core/data/external-source-data.service.spec.ts +++ b/src/app/core/data/external-source-data.service.spec.ts @@ -96,12 +96,6 @@ describe('ExternalSourceService', () => { result.pipe(take(1)).subscribe(); expect(requestService.send).toHaveBeenCalledWith(jasmine.any(GetRequest), false); }); - - it('should return the entries', () => { - result.subscribe((resultRD) => { - expect(resultRD.payload.page).toBe(entries); - }); - }); }); }); }); diff --git a/src/app/core/data/object-updates/object-updates.effects.spec.ts b/src/app/core/data/object-updates/object-updates.effects.spec.ts index ffd20a73006..2a21d1923e0 100644 --- a/src/app/core/data/object-updates/object-updates.effects.spec.ts +++ b/src/app/core/data/object-updates/object-updates.effects.spec.ts @@ -1,5 +1,6 @@ import { TestBed, waitForAsync } from '@angular/core/testing'; import { Observable, Subject } from 'rxjs'; +import { take } from 'rxjs/operators'; import { provideMockActions } from '@ngrx/effects/testing'; import { cold, hot } from 'jasmine-marbles'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; @@ -11,13 +12,10 @@ import { RemoveFieldUpdateAction, RemoveObjectUpdatesAction } from './object-updates.actions'; -import { - INotification, - Notification -} from '../../../shared/notifications/models/notification.model'; +import { INotification, Notification } from '../../../shared/notifications/models/notification.model'; +import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub'; +import { Action } from '@ngrx/store'; import { NotificationType } from '../../../shared/notifications/models/notification-type'; -import { filter } from 'rxjs/operators'; -import { hasValue } from '../../../shared/empty.util'; import { NoOpAction } from '../../../shared/ngrx/no-op.action'; describe('ObjectUpdatesEffects', () => { @@ -31,13 +29,7 @@ describe('ObjectUpdatesEffects', () => { providers: [ ObjectUpdatesEffects, provideMockActions(() => actions), - { - provide: NotificationsService, - useValue: { - remove: (notification) => { /* empty */ - } - } - }, + { provide: NotificationsService, useClass: NotificationsServiceStub }, ], }); })); @@ -59,7 +51,6 @@ describe('ObjectUpdatesEffects', () => { action = new RemoveObjectUpdatesAction(testURL); }); it('should emit the action from the actionMap\'s value which key matches the action\'s URL', () => { - action = new RemoveObjectUpdatesAction(testURL); actions = hot('--a-', { a: action }); (updatesEffects as any).actionMap$[testURL].subscribe((act) => emittedAction = act); const expected = cold('--b-', { b: undefined }); @@ -81,14 +72,19 @@ describe('ObjectUpdatesEffects', () => { removeAction = new RemoveObjectUpdatesAction(testURL); }); it('should return a RemoveObjectUpdatesAction', () => { - actions = hot('a|', { a: new DiscardObjectUpdatesAction(testURL, infoNotification) }); - updatesEffects.removeAfterDiscardOrReinstateOnUndo$.pipe( - filter(((action) => hasValue(action)))) - .subscribe((t) => { - expect(t).toEqual(removeAction); - } - ) - ; + actions = hot('a', { a: new DiscardObjectUpdatesAction(testURL, infoNotification) }); + + // Because we use Subject and not BehaviourSubject we need to subscribe to it beforehand because it does not + // keep track of the current state + let emittedAction: Action | undefined; + updatesEffects.removeAfterDiscardOrReinstateOnUndo$.subscribe((action: Action | NoOpAction) => { + emittedAction = action; + }); + + // This expect ensures that the mapLastActions$ was processed + expect(updatesEffects.mapLastActions$).toBeObservable(cold('a', { a: undefined })); + + expect(emittedAction).toEqual(removeAction); }); }); @@ -98,12 +94,24 @@ describe('ObjectUpdatesEffects', () => { infoNotification.options.timeOut = 10; }); it('should return an action with type NO_ACTION', () => { - actions = hot('a', { a: new DiscardObjectUpdatesAction(testURL, infoNotification) }); - actions = hot('b', { b: new ReinstateObjectUpdatesAction(testURL) }); - updatesEffects.removeAfterDiscardOrReinstateOnUndo$.subscribe((t) => { - expect(t).toEqual(new NoOpAction()); - } - ); + actions = hot('--(ab)', { + a: new DiscardObjectUpdatesAction(testURL, infoNotification), + b: new ReinstateObjectUpdatesAction(testURL), + }); + + // Because we use Subject and not BehaviourSubject we need to subscribe to it beforehand because it does not + // keep track of the current state + let emittedAction: Action | undefined; + updatesEffects.removeAfterDiscardOrReinstateOnUndo$.pipe( + take(2) + ).subscribe((action: Action | NoOpAction) => { + emittedAction = action; + }); + + // This expect ensures that the mapLastActions$ was processed + expect(updatesEffects.mapLastActions$).toBeObservable(cold('--(ab)', { a: undefined, b: undefined })); + + expect(emittedAction).toEqual(new RemoveObjectUpdatesAction(testURL)); }); }); @@ -113,12 +121,22 @@ describe('ObjectUpdatesEffects', () => { infoNotification.options.timeOut = 10; }); it('should return a RemoveObjectUpdatesAction', () => { - actions = hot('a', { a: new DiscardObjectUpdatesAction(testURL, infoNotification) }); - actions = hot('b', { b: new RemoveFieldUpdateAction(testURL, testUUID) }); + actions = hot('--(ab)', { + a: new DiscardObjectUpdatesAction(testURL, infoNotification), + b: new RemoveFieldUpdateAction(testURL, testUUID), + }); + + // Because we use Subject and not BehaviourSubject we need to subscribe to it beforehand because it does not + // keep track of the current state + let emittedAction: Action | undefined; + updatesEffects.removeAfterDiscardOrReinstateOnUndo$.subscribe((action: Action | NoOpAction) => { + emittedAction = action; + }); + + // This expect ensures that the mapLastActions$ was processed + expect(updatesEffects.mapLastActions$).toBeObservable(cold('--(ab)', { a: undefined, b: undefined })); - updatesEffects.removeAfterDiscardOrReinstateOnUndo$.subscribe((t) => - expect(t).toEqual(new RemoveObjectUpdatesAction(testURL)) - ); + expect(emittedAction).toEqual(new RemoveObjectUpdatesAction(testURL)); }); }); }); diff --git a/src/app/core/data/object-updates/object-updates.reducer.spec.ts b/src/app/core/data/object-updates/object-updates.reducer.spec.ts index 08944a073f7..90e0b70dbd8 100644 --- a/src/app/core/data/object-updates/object-updates.reducer.spec.ts +++ b/src/app/core/data/object-updates/object-updates.reducer.spec.ts @@ -12,7 +12,7 @@ import { SetEditableFieldUpdateAction, SetValidFieldUpdateAction } from './object-updates.actions'; -import { OBJECT_UPDATES_TRASH_PATH, objectUpdatesReducer } from './object-updates.reducer'; +import { OBJECT_UPDATES_TRASH_PATH, objectUpdatesReducer, ObjectUpdatesState } from './object-updates.reducer'; import { Relationship } from '../../shared/item-relationships/relationship.model'; import { FieldChangeType } from './field-change-type.model'; @@ -56,7 +56,7 @@ const modDate = new Date(2010, 2, 11); const uuid = identifiable1.uuid; const url = 'test-object.url/edit'; describe('objectUpdatesReducer', () => { - const testState = { + const testState: ObjectUpdatesState = { [url]: { fieldStates: { [identifiable1.uuid]: { @@ -79,9 +79,6 @@ describe('objectUpdatesReducer', () => { [identifiable2.uuid]: { field: { uuid: identifiable2.uuid, - key: 'dc.titl', - language: null, - value: 'New title' }, changeType: FieldChangeType.ADD } @@ -93,7 +90,7 @@ describe('objectUpdatesReducer', () => { } }; - const discardedTestState = { + const discardedTestState: ObjectUpdatesState = { [url]: { fieldStates: { [identifiable1.uuid]: { @@ -139,9 +136,6 @@ describe('objectUpdatesReducer', () => { [identifiable2.uuid]: { field: { uuid: identifiable2.uuid, - key: 'dc.titl', - language: null, - value: 'New title' }, changeType: FieldChangeType.ADD } @@ -173,48 +167,80 @@ describe('objectUpdatesReducer', () => { const action = new InitializeFieldsAction(url, [identifiable1, identifiable2], modDate); // testState has already been frozen above objectUpdatesReducer(testState, action); + + // no expect required, deepFreeze will ensure an exception is thrown if the state + // is mutated, and any uncaught exception will cause the test to fail + expect().nothing(); }); it('should perform the SET_EDITABLE_FIELD action without affecting the previous state', () => { const action = new SetEditableFieldUpdateAction(url, uuid, false); // testState has already been frozen above objectUpdatesReducer(testState, action); + + // no expect required, deepFreeze will ensure an exception is thrown if the state + // is mutated, and any uncaught exception will cause the test to fail + expect().nothing(); }); it('should perform the ADD_FIELD action without affecting the previous state', () => { const action = new AddFieldUpdateAction(url, identifiable1update, FieldChangeType.UPDATE); // testState has already been frozen above objectUpdatesReducer(testState, action); + + // no expect required, deepFreeze will ensure an exception is thrown if the state + // is mutated, and any uncaught exception will cause the test to fail + expect().nothing(); }); it('should perform the DISCARD action without affecting the previous state', () => { const action = new DiscardObjectUpdatesAction(url, null); // testState has already been frozen above objectUpdatesReducer(testState, action); + + // no expect required, deepFreeze will ensure an exception is thrown if the state + // is mutated, and any uncaught exception will cause the test to fail + expect().nothing(); }); it('should perform the REINSTATE action without affecting the previous state', () => { const action = new ReinstateObjectUpdatesAction(url); // testState has already been frozen above objectUpdatesReducer(testState, action); + + // no expect required, deepFreeze will ensure an exception is thrown if the state + // is mutated, and any uncaught exception will cause the test to fail + expect().nothing(); }); it('should perform the REMOVE action without affecting the previous state', () => { const action = new RemoveFieldUpdateAction(url, uuid); // testState has already been frozen above objectUpdatesReducer(testState, action); + + // no expect required, deepFreeze will ensure an exception is thrown if the state + // is mutated, and any uncaught exception will cause the test to fail + expect().nothing(); }); it('should perform the REMOVE_FIELD action without affecting the previous state', () => { const action = new RemoveFieldUpdateAction(url, uuid); // testState has already been frozen above objectUpdatesReducer(testState, action); + + // no expect required, deepFreeze will ensure an exception is thrown if the state + // is mutated, and any uncaught exception will cause the test to fail + expect().nothing(); }); it('should perform the SELECT_VIRTUAL_METADATA action without affecting the previous state', () => { const action = new SelectVirtualMetadataAction(url, relationship.uuid, identifiable1.uuid, true); // testState has already been frozen above objectUpdatesReducer(testState, action); + + // no expect required, deepFreeze will ensure an exception is thrown if the state + // is mutated, and any uncaught exception will cause the test to fail + expect().nothing(); }); it('should initialize all fields when the INITIALIZE action is dispatched, based on the payload', () => { diff --git a/src/app/core/data/object-updates/object-updates.reducer.ts b/src/app/core/data/object-updates/object-updates.reducer.ts index 14bacc52db4..5c03001972a 100644 --- a/src/app/core/data/object-updates/object-updates.reducer.ts +++ b/src/app/core/data/object-updates/object-updates.reducer.ts @@ -77,7 +77,7 @@ export interface DeleteRelationship extends RelationshipIdentifiable { */ export interface ObjectUpdatesEntry { fieldStates: FieldStates; - fieldUpdates: FieldUpdates; + fieldUpdates?: FieldUpdates; virtualMetadataSources: VirtualMetadataSources; lastModified: Date; patchOperationService?: GenericConstructor; diff --git a/src/app/core/eperson/eperson-data.service.spec.ts b/src/app/core/eperson/eperson-data.service.spec.ts index cbddf1e6c3e..a2d3234254a 100644 --- a/src/app/core/eperson/eperson-data.service.spec.ts +++ b/src/app/core/eperson/eperson-data.service.spec.ts @@ -1,40 +1,40 @@ -import { CommonModule } from '@angular/common'; -import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; -import { TestBed, waitForAsync } from '@angular/core/testing'; -import { Store, StoreModule } from '@ngrx/store'; -import { compare, Operation } from 'fast-json-patch'; -import { getTestScheduler } from 'jasmine-marbles'; -import { Observable, of as observableOf } from 'rxjs'; -import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; -import { TestScheduler } from 'rxjs/testing'; +import { fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; +import { Store } from '@ngrx/store'; +import { cold } from 'jasmine-marbles'; +import { of as observableOf } from 'rxjs'; import { EPeopleRegistryCancelEPersonAction, EPeopleRegistryEditEPersonAction } from '../../access-control/epeople-registry/epeople-registry.actions'; import { GroupMock } from '../../shared/testing/group-mock'; import { RequestParam } from '../cache/models/request-param.model'; -import { ChangeAnalyzer } from '../data/change-analyzer'; import { PatchRequest, PostRequest } from '../data/request.models'; import { RequestService } from '../data/request.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { Item } from '../shared/item.model'; -import { EPersonDataService } from './eperson-data.service'; +import { editEPersonSelector, EPersonDataService } from './eperson-data.service'; import { EPerson } from './models/eperson.model'; import { EPersonMock, EPersonMock2 } from '../../shared/testing/eperson.mock'; import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub'; import { createNoContentRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; import { getMockRemoteDataBuildServiceHrefMap } from '../../shared/mocks/remote-data-build.service.mock'; -import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock'; import { getMockRequestService } from '../../shared/mocks/request.service.mock'; import { createPaginatedList, createRequestEntry$ } from '../../shared/testing/utils.test'; import { CoreState } from '../core-state.model'; import { FindListOptions } from '../data/find-list-options.model'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub'; +import { MockStore, provideMockStore } from '@ngrx/store/testing'; +import { compare, Operation } from 'fast-json-patch'; +import { Item } from '../shared/item.model'; +import { ChangeAnalyzer } from '../data/change-analyzer'; describe('EPersonDataService', () => { let service: EPersonDataService; - let store: Store; + let store: MockStore; let requestService: RequestService; - let scheduler: TestScheduler; let epeople; @@ -44,50 +44,38 @@ describe('EPersonDataService', () => { let epeople$; let rdbService; - function initTestService() { - return new EPersonDataService( - requestService, - rdbService, - null, - halService, - new DummyChangeAnalyzer() as any, - null, - store, - ); - } + const initialState = { + epeopleRegistry: { + editEPerson: null + }, + }; - function init() { + beforeEach(waitForAsync(() => { restEndpointURL = 'https://rest.api/dspace-spring-rest/api/eperson'; epersonsEndpoint = `${restEndpointURL}/epersons`; epeople = [EPersonMock, EPersonMock2]; epeople$ = createSuccessfulRemoteDataObject$(createPaginatedList([epeople])); rdbService = getMockRemoteDataBuildServiceHrefMap(undefined, { 'https://rest.api/dspace-spring-rest/api/eperson/epersons': epeople$ }); halService = new HALEndpointServiceStub(restEndpointURL); + requestService = getMockRequestService(createRequestEntry$(epeople)); TestBed.configureTestingModule({ - imports: [ - CommonModule, - StoreModule.forRoot({}), - TranslateModule.forRoot({ - loader: { - provide: TranslateLoader, - useClass: TranslateLoaderMock - } - }), + providers: [ + EPersonDataService, + { provide: RequestService, useValue: requestService }, + { provide: RemoteDataBuildService, useValue: rdbService }, + { provide: HALEndpointService, useValue: halService }, + provideMockStore({ initialState }), + { provide: ObjectCacheService, useValue: {} }, + { provide: DSOChangeAnalyzer, useClass: DummyChangeAnalyzer }, + { provide: NotificationsService, useClass: NotificationsServiceStub }, ], - declarations: [], - providers: [], - schemas: [CUSTOM_ELEMENTS_SCHEMA] }); - } - beforeEach(() => { - init(); - requestService = getMockRequestService(createRequestEntry$(epeople)); - store = new Store(undefined, undefined, undefined); - service = initTestService(); - spyOn(store, 'dispatch'); - }); + service = TestBed.inject(EPersonDataService); + store = TestBed.inject(Store) as MockStore; + spyOn(store, 'dispatch').and.callThrough(); + })); describe('searchByScope', () => { beforeEach(() => { @@ -264,34 +252,29 @@ describe('EPersonDataService', () => { }); describe('clearEPersonRequests', () => { - beforeEach(waitForAsync(() => { - scheduler = getTestScheduler(); - halService = { - getEndpoint(linkPath: string): Observable { - return observableOf(restEndpointURL + '/' + linkPath); - } - } as HALEndpointService; - initTestService(); + beforeEach(() => { + spyOn(halService, 'getEndpoint').and.callFake((linkPath: string) => { + return observableOf(`${restEndpointURL}/${linkPath}`); + }); + }); + it('should remove the eperson hrefs in the request service', fakeAsync(() => { service.clearEPersonRequests(); - })); - it('should remove the eperson hrefs in the request service', () => { + tick(); + expect(requestService.removeByHrefSubstring).toHaveBeenCalledWith(epersonsEndpoint); - }); + })); }); describe('getActiveEPerson', () => { it('should retrieve the ePerson currently getting edited, if any', () => { - service.editEPerson(EPersonMock); + // Update the state with the ePerson (the provideMockStore doesn't update itself when dispatch is called) + store.overrideSelector(editEPersonSelector, EPersonMock); - service.getActiveEPerson().subscribe((activeEPerson: EPerson) => { - expect(activeEPerson).toEqual(EPersonMock); - }); + expect(service.getActiveEPerson()).toBeObservable(cold('a', { a: EPersonMock })); }); it('should retrieve the ePerson currently getting edited, null if none being edited', () => { - service.getActiveEPerson().subscribe((activeEPerson: EPerson) => { - expect(activeEPerson).toEqual(null); - }); + expect(service.getActiveEPerson()).toBeObservable(cold('a', { a: null })); }); }); diff --git a/src/app/core/eperson/eperson-data.service.ts b/src/app/core/eperson/eperson-data.service.ts index a85d471e7db..8ded53dd9a9 100644 --- a/src/app/core/eperson/eperson-data.service.ts +++ b/src/app/core/eperson/eperson-data.service.ts @@ -37,7 +37,7 @@ import { dataService } from '../data/base/data-service.decorator'; import { getEPersonEditRoute } from '../../access-control/access-control-routing-paths'; const ePeopleRegistryStateSelector = (state: AppState) => state.epeopleRegistry; -const editEPersonSelector = createSelector(ePeopleRegistryStateSelector, (ePeopleRegistryState: EPeopleRegistryState) => ePeopleRegistryState.editEPerson); +export const editEPersonSelector = createSelector(ePeopleRegistryStateSelector, (ePeopleRegistryState: EPeopleRegistryState) => ePeopleRegistryState.editEPerson); /** * A service to retrieve {@link EPerson}s from the REST API & EPerson related CRUD actions diff --git a/src/app/core/shared/item.model.spec.ts b/src/app/core/shared/item.model.spec.ts index 732ae5b19ca..96cca5dd500 100644 --- a/src/app/core/shared/item.model.spec.ts +++ b/src/app/core/shared/item.model.spec.ts @@ -12,14 +12,13 @@ describe('Item', () => { const bitstream1Path = 'document.pdf'; const bitstream2Path = 'otherfile.doc'; - const nonExistingBundleName = 'c1e568f7-d14e-496b-bdd7-07026998cc00'; let bitstreams; let remoteDataThumbnail; let remoteDataThumbnailList; let remoteDataFiles; let remoteDataBundles; - beforeEach(() => { + it('should be possible to create an Item without any errors', () => { const thumbnail = { content: thumbnailPath }; @@ -51,5 +50,6 @@ describe('Item', () => { remoteDataBundles = createSuccessfulRemoteDataObject$(createPaginatedList(bundles)); item = Object.assign(new Item(), { bundles: remoteDataBundles }); + expect().nothing(); }); }); diff --git a/src/app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component.html b/src/app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component.html index 75f4587a3da..3648b53b51f 100644 --- a/src/app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component.html +++ b/src/app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component.html @@ -1,4 +1,4 @@ - +
diff --git a/src/app/entity-groups/journal-entities/item-pages/journal-volume/journal-volume.component.html b/src/app/entity-groups/journal-entities/item-pages/journal-volume/journal-volume.component.html index 213581a519c..b906905b474 100644 --- a/src/app/entity-groups/journal-entities/item-pages/journal-volume/journal-volume.component.html +++ b/src/app/entity-groups/journal-entities/item-pages/journal-volume/journal-volume.component.html @@ -1,4 +1,4 @@ - +
diff --git a/src/app/entity-groups/journal-entities/item-pages/journal/journal.component.html b/src/app/entity-groups/journal-entities/item-pages/journal/journal.component.html index 3b2cce061b6..e8a6904242b 100644 --- a/src/app/entity-groups/journal-entities/item-pages/journal/journal.component.html +++ b/src/app/entity-groups/journal-entities/item-pages/journal/journal.component.html @@ -1,4 +1,4 @@ - +
diff --git a/src/app/entity-groups/research-entities/item-pages/org-unit/org-unit.component.html b/src/app/entity-groups/research-entities/item-pages/org-unit/org-unit.component.html index 52df841d3b6..f28550a4e0d 100644 --- a/src/app/entity-groups/research-entities/item-pages/org-unit/org-unit.component.html +++ b/src/app/entity-groups/research-entities/item-pages/org-unit/org-unit.component.html @@ -1,4 +1,4 @@ - +
diff --git a/src/app/entity-groups/research-entities/item-pages/person/person.component.html b/src/app/entity-groups/research-entities/item-pages/person/person.component.html index 83a2e76dd8f..ec99c845be6 100644 --- a/src/app/entity-groups/research-entities/item-pages/person/person.component.html +++ b/src/app/entity-groups/research-entities/item-pages/person/person.component.html @@ -1,4 +1,4 @@ - +
diff --git a/src/app/entity-groups/research-entities/item-pages/project/project.component.html b/src/app/entity-groups/research-entities/item-pages/project/project.component.html index 0d9679ef1ef..944766b6f7c 100644 --- a/src/app/entity-groups/research-entities/item-pages/project/project.component.html +++ b/src/app/entity-groups/research-entities/item-pages/project/project.component.html @@ -1,4 +1,4 @@ - +
diff --git a/src/app/home-page/top-level-community-list/top-level-community-list.component.spec.ts b/src/app/home-page/top-level-community-list/top-level-community-list.component.spec.ts index d1a3d3631f1..a3332e4a83f 100644 --- a/src/app/home-page/top-level-community-list/top-level-community-list.component.spec.ts +++ b/src/app/home-page/top-level-community-list/top-level-community-list.component.spec.ts @@ -32,7 +32,7 @@ import { SearchConfigurationServiceStub } from '../../shared/testing/search-conf import { APP_CONFIG } from 'src/config/app-config.interface'; import { environment } from 'src/environments/environment.test'; -describe('TopLevelCommunityList Component', () => { +describe('TopLevelCommunityListComponent', () => { let comp: TopLevelCommunityListComponent; let fixture: ComponentFixture; let communityDataServiceStub: any; @@ -173,17 +173,17 @@ describe('TopLevelCommunityList Component', () => { }); - it('should display a list of top-communities', () => { - waitForAsync(() => { - const subComList = fixture.debugElement.queryAll(By.css('li')); - - expect(subComList.length).toEqual(5); - expect(subComList[0].nativeElement.textContent).toContain('TopCommunity 1'); - expect(subComList[1].nativeElement.textContent).toContain('TopCommunity 2'); - expect(subComList[2].nativeElement.textContent).toContain('TopCommunity 3'); - expect(subComList[3].nativeElement.textContent).toContain('TopCommunity 4'); - expect(subComList[4].nativeElement.textContent).toContain('TopCommunity 5'); - }); + it('should display a list of top-communities', async () => { + await fixture.whenStable(); + fixture.detectChanges(); + const subComList = fixture.debugElement.queryAll(By.css('li')); + + expect(subComList.length).toEqual(5); + expect(subComList[0].nativeElement.textContent).toContain('TopCommunity 1'); + expect(subComList[1].nativeElement.textContent).toContain('TopCommunity 2'); + expect(subComList[2].nativeElement.textContent).toContain('TopCommunity 3'); + expect(subComList[3].nativeElement.textContent).toContain('TopCommunity 4'); + expect(subComList[4].nativeElement.textContent).toContain('TopCommunity 5'); }); }); diff --git a/src/app/item-page/edit-item-page/edit-item-operators.spec.ts b/src/app/item-page/edit-item-page/edit-item-operators.spec.ts index e7bd5b98ce9..f72ace495f0 100644 --- a/src/app/item-page/edit-item-page/edit-item-operators.spec.ts +++ b/src/app/item-page/edit-item-page/edit-item-operators.spec.ts @@ -18,7 +18,7 @@ describe('findSuccessfulAccordingTo', () => { mockItem1.isWithdrawn = true; mockItem2 = new Item(); - mockItem1.isWithdrawn = false; + mockItem2.isWithdrawn = false; predicate = (rd: RemoteData) => isNotEmpty(rd.payload) ? rd.payload.isWithdrawn : false; }); @@ -34,7 +34,7 @@ describe('findSuccessfulAccordingTo', () => { const source = hot('abcde', testRD); const result = source.pipe(findSuccessfulAccordingTo(predicate)); - result.subscribe((value) => expect(value).toEqual(testRD.d)); + expect(result).toBeObservable(hot('---(d|)', { d: testRD.d })); }); }); diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.spec.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.spec.ts index 10e18121313..3177ea9e334 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.spec.ts +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.spec.ts @@ -191,20 +191,6 @@ describe('ItemBitstreamsComponent', () => { }); }); - describe('when dropBitstream is called', () => { - const event = { - fromIndex: 0, - toIndex: 50, - // eslint-disable-next-line no-empty,@typescript-eslint/no-empty-function - finish: () => { - } - }; - - beforeEach(() => { - comp.dropBitstream(bundle, event); - }); - }); - describe('when dropBitstream is called', () => { beforeEach((done) => { comp.dropBitstream(bundle, { diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts index 70f4b632178..8130c51367f 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts @@ -1,4 +1,4 @@ -import { Component, EventEmitter, Input, OnInit, Output, ViewChild, ViewContainerRef } from '@angular/core'; +import { Component, EventEmitter, Input, OnInit, Output, ViewChild, ViewContainerRef, OnDestroy } from '@angular/core'; import { Bundle } from '../../../../core/shared/bundle.model'; import { Item } from '../../../../core/shared/item.model'; import { ResponsiveColumnSizes } from '../../../../shared/responsive-table-sizes/responsive-column-sizes'; @@ -16,7 +16,7 @@ import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service'; * Creates an embedded view of the contents. This is to ensure the table structure won't break. * (which means it'll be added to the parents html without a wrapping ds-item-edit-bitstream-bundle element) */ -export class ItemEditBitstreamBundleComponent implements OnInit { +export class ItemEditBitstreamBundleComponent implements OnInit, OnDestroy { /** * The view on the bundle information and bitstreams @@ -67,4 +67,9 @@ export class ItemEditBitstreamBundleComponent implements OnInit { this.viewContainerRef.createEmbeddedView(this.bundleView); this.itemPageRoute = getItemPageRoute(this.item); } + + ngOnDestroy(): void { + this.viewContainerRef.clear(); + } + } diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-drag-handle/item-edit-bitstream-drag-handle.component.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-drag-handle/item-edit-bitstream-drag-handle.component.ts index e5cb9ba4034..d9c9a8f34af 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-drag-handle/item-edit-bitstream-drag-handle.component.ts +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-drag-handle/item-edit-bitstream-drag-handle.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit, ViewChild, ViewContainerRef } from '@angular/core'; +import { Component, OnInit, ViewChild, ViewContainerRef, OnDestroy } from '@angular/core'; @Component({ selector: 'ds-item-edit-bitstream-drag-handle', @@ -10,7 +10,7 @@ import { Component, OnInit, ViewChild, ViewContainerRef } from '@angular/core'; * Creates an embedded view of the contents * (which means it'll be added to the parents html without a wrapping ds-item-edit-bitstream-drag-handle element) */ -export class ItemEditBitstreamDragHandleComponent implements OnInit { +export class ItemEditBitstreamDragHandleComponent implements OnInit, OnDestroy { /** * The view on the drag-handle */ @@ -23,4 +23,8 @@ export class ItemEditBitstreamDragHandleComponent implements OnInit { this.viewContainerRef.createEmbeddedView(this.handleView); } + ngOnDestroy(): void { + this.viewContainerRef.clear(); + } + } diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream/item-edit-bitstream.component.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream/item-edit-bitstream.component.ts index fcb5c706ac7..43c201aa9a0 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream/item-edit-bitstream.component.ts +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream/item-edit-bitstream.component.ts @@ -1,4 +1,4 @@ -import { Component, Input, OnChanges, OnInit, SimpleChanges, ViewChild, ViewContainerRef } from '@angular/core'; +import { Component, Input, OnChanges, OnDestroy, OnInit, SimpleChanges, ViewChild, ViewContainerRef } from '@angular/core'; import { Bitstream } from '../../../../core/shared/bitstream.model'; import cloneDeep from 'lodash/cloneDeep'; import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service'; @@ -21,7 +21,7 @@ import { getBitstreamDownloadRoute } from '../../../../app-routing-paths'; * Creates an embedded view of the contents * (which means it'll be added to the parents html without a wrapping ds-item-edit-bitstream element) */ -export class ItemEditBitstreamComponent implements OnChanges, OnInit { +export class ItemEditBitstreamComponent implements OnChanges, OnDestroy, OnInit { /** * The view on the bitstream @@ -72,6 +72,10 @@ export class ItemEditBitstreamComponent implements OnChanges, OnInit { this.viewContainerRef.createEmbeddedView(this.bitstreamView); } + ngOnDestroy(): void { + this.viewContainerRef.clear(); + } + /** * Update the current bitstream and its format on changes * @param changes diff --git a/src/app/item-page/simple/field-components/specific-field/item-page-field.component.spec.ts b/src/app/item-page/simple/field-components/specific-field/item-page-field.component.spec.ts index 15b7a9df212..5fc4644c723 100644 --- a/src/app/item-page/simple/field-components/specific-field/item-page-field.component.spec.ts +++ b/src/app/item-page/simple/field-components/specific-field/item-page-field.component.spec.ts @@ -37,8 +37,8 @@ describe('ItemPageFieldComponent', () => { } }); - const buildTestEnvironment = async () => { - await TestBed.configureTestingModule({ + beforeEach(waitForAsync(() => { + void TestBed.configureTestingModule({ imports: [ RouterTestingModule.withRoutes([]), TranslateModule.forRoot({ @@ -65,19 +65,16 @@ describe('ItemPageFieldComponent', () => { comp.fields = mockFields; comp.label = mockLabel; fixture.detectChanges(); - }; + })); - it('should display display the correct metadata value', waitForAsync(async () => { - await buildTestEnvironment(); + it('should display display the correct metadata value', () => { expect(fixture.nativeElement.innerHTML).toContain(mockValue); - })); + }); describe('when markdown is disabled in the environment config', () => { - - beforeEach(waitForAsync(async () => { + beforeEach( () => { appConfig.markdown.enabled = false; - await buildTestEnvironment(); - })); + }); describe('and markdown is disabled in this component', () => { @@ -105,11 +102,9 @@ describe('ItemPageFieldComponent', () => { }); describe('when markdown is enabled in the environment config', () => { - - beforeEach(waitForAsync(async () => { + beforeEach(() => { appConfig.markdown.enabled = true; - await buildTestEnvironment(); - })); + }); describe('and markdown is disabled in this component', () => { @@ -139,12 +134,13 @@ describe('ItemPageFieldComponent', () => { describe('test rendering of configured browse links', () => { beforeEach(() => { + appConfig.markdown.enabled = false; + comp.enableMarkdown = true; fixture.detectChanges(); }); - waitForAsync(() => { - it('should have a browse link', () => { - expect(fixture.debugElement.query(By.css('a.ds-browse-link')).nativeElement.innerHTML).toContain(mockValue); - }); + + it('should have a browse link', async () => { + expect(fixture.debugElement.query(By.css('a.ds-browse-link')).nativeElement.innerHTML).toContain(mockValue); }); }); @@ -153,10 +149,9 @@ describe('ItemPageFieldComponent', () => { comp.urlRegex = '^test'; fixture.detectChanges(); }); - waitForAsync(() => { - it('should have a rendered (non-browse) link since the value matches ^test', () => { - expect(fixture.debugElement.query(By.css('a.ds-simple-metadata-link')).nativeElement.innerHTML).toContain(mockValue); - }); + + it('should have a rendered (non-browse) link since the value matches ^test', () => { + expect(fixture.debugElement.query(By.css('a.ds-simple-metadata-link')).nativeElement.innerHTML).toContain(mockValue); }); }); @@ -165,14 +160,11 @@ describe('ItemPageFieldComponent', () => { comp.urlRegex = '^nope'; fixture.detectChanges(); }); - beforeEach(waitForAsync(() => { - it('should NOT have a rendered (non-browse) link since the value matches ^test', () => { - expect(fixture.debugElement.query(By.css('a.ds-simple-metadata-link'))).toBeNull(); - }); - })); - }); - + it('should NOT have a rendered (non-browse) link since the value matches ^test', () => { + expect(fixture.debugElement.query(By.css('a.ds-simple-metadata-link'))).toBeNull(); + }); + }); }); export function mockItemWithMetadataFieldsAndValue(fields: string[], value: string): Item { diff --git a/src/app/item-page/simple/item-types/publication/publication.component.html b/src/app/item-page/simple/item-types/publication/publication.component.html index 771ea84af0c..821b4b300b3 100644 --- a/src/app/item-page/simple/item-types/publication/publication.component.html +++ b/src/app/item-page/simple/item-types/publication/publication.component.html @@ -1,4 +1,4 @@ - +
{ fixture.detectChanges(); })); - it('should hide back button',() => { + it('should hide back button', () => { spyOn(mockRouteService, 'getPreviousUrl').and.returnValue(observableOf('/item')); - comp.showBackButton.subscribe((val) => { + comp.ngOnInit(); + comp.showBackButton$.subscribe((val) => { expect(val).toBeFalse(); }); }); it('should show back button for search', () => { spyOn(mockRouteService, 'getPreviousUrl').and.returnValue(observableOf(searchUrl)); comp.ngOnInit(); - comp.showBackButton.subscribe((val) => { + comp.showBackButton$.subscribe((val) => { expect(val).toBeTrue(); }); }); it('should show back button for browse', () => { spyOn(mockRouteService, 'getPreviousUrl').and.returnValue(observableOf(browseUrl)); comp.ngOnInit(); - comp.showBackButton.subscribe((val) => { + comp.showBackButton$.subscribe((val) => { expect(val).toBeTrue(); }); }); it('should show back button for recent submissions', () => { spyOn(mockRouteService, 'getPreviousUrl').and.returnValue(observableOf(recentSubmissionsUrl)); comp.ngOnInit(); - comp.showBackButton.subscribe((val) => { + comp.showBackButton$.subscribe((val) => { expect(val).toBeTrue(); }); }); diff --git a/src/app/item-page/simple/item-types/shared/item.component.ts b/src/app/item-page/simple/item-types/shared/item.component.ts index 93e6a0b3465..edcfd57a69c 100644 --- a/src/app/item-page/simple/item-types/shared/item.component.ts +++ b/src/app/item-page/simple/item-types/shared/item.component.ts @@ -5,7 +5,7 @@ import { getItemPageRoute } from '../../../item-page-routing-paths'; import { RouteService } from '../../../../core/services/route.service'; import { Observable } from 'rxjs'; import { getDSpaceQuery, isIiifEnabled, isIiifSearchEnabled } from './item-iiif-utils'; -import { filter, map, take } from 'rxjs/operators'; +import { map, take } from 'rxjs/operators'; import { Router } from '@angular/router'; @Component({ @@ -27,7 +27,7 @@ export class ItemComponent implements OnInit { /** * Used to show or hide the back to results button in the view. */ - showBackButton: Observable; + showBackButton$: Observable; /** * Route to the item page @@ -73,10 +73,9 @@ export class ItemComponent implements OnInit { this.itemPageRoute = getItemPageRoute(this.object); // hide/show the back button - this.showBackButton = this.routeService.getPreviousUrl().pipe( - filter(url => this.previousRoute.test(url)), + this.showBackButton$ = this.routeService.getPreviousUrl().pipe( + map((url: string) => this.previousRoute.test(url)), take(1), - map(() => true) ); // check to see if iiif viewer is required. this.iiifEnabled = isIiifEnabled(this.object); diff --git a/src/app/item-page/simple/item-types/untyped-item/untyped-item.component.html b/src/app/item-page/simple/item-types/untyped-item/untyped-item.component.html index d30250f9567..0a8b4e7e595 100644 --- a/src/app/item-page/simple/item-types/untyped-item/untyped-item.component.html +++ b/src/app/item-page/simple/item-types/untyped-item/untyped-item.component.html @@ -1,4 +1,4 @@ - +
{ authorizationServiceSpy.isAuthorized.and.callFake(canDelete); })); it('should not disable the delete button', () => { - const deleteButtons = fixture.debugElement.queryAll(By.css(`.version-row-element-delete`)); - deleteButtons.forEach((btn) => { + const deleteButtons: DebugElement[] = fixture.debugElement.queryAll(By.css('.version-row-element-delete')); + expect(deleteButtons.length).not.toBe(0); + deleteButtons.forEach((btn: DebugElement) => { expect(btn.nativeElement.disabled).toBe(false); }); }); - it('should disable other buttons', () => { - const createButtons = fixture.debugElement.queryAll(By.css(`.version-row-element-create`)); - createButtons.forEach((btn) => { - expect(btn.nativeElement.disabled).toBe(true); - }); - const editButtons = fixture.debugElement.queryAll(By.css(`.version-row-element-create`)); - editButtons.forEach((btn) => { - expect(btn.nativeElement.disabled).toBe(true); - }); + + it('should hide the create buttons', () => { + const createButtons: DebugElement[] = fixture.debugElement.queryAll(By.css('.version-row-element-create')); + expect(createButtons.length).toBe(0); + }); + + it('should hide the edit buttons', () => { + const editButtons: DebugElement[] = fixture.debugElement.queryAll(By.css('.version-row-element-edit')); + expect(editButtons.length).toBe(0); }); }); diff --git a/src/app/shared/browse-by/browse-by.component.spec.ts b/src/app/shared/browse-by/browse-by.component.spec.ts index 9317a68007a..ab98cd583eb 100644 --- a/src/app/shared/browse-by/browse-by.component.spec.ts +++ b/src/app/shared/browse-by/browse-by.component.spec.ts @@ -5,7 +5,6 @@ import { By } from '@angular/platform-browser'; import { Component, NO_ERRORS_SCHEMA } from '@angular/core'; import { of as observableOf } from 'rxjs'; import { CommonModule } from '@angular/common'; -import { Item } from '../../core/shared/item.model'; import { buildPaginatedList } from '../../core/data/paginated-list.model'; import { PageInfo } from '../../core/shared/page-info.model'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; @@ -48,10 +47,10 @@ import { SharedModule } from '../shared.module'; import { BrowseByRoutingModule } from '../../browse-by/browse-by-routing.module'; import { AccessControlRoutingModule } from '../../access-control/access-control-routing.module'; -@listableObjectComponent(BrowseEntry, ViewMode.ListElement, DEFAULT_CONTEXT, 'custom') +@listableObjectComponent(BrowseEntry, ViewMode.ListElement, DEFAULT_CONTEXT, 'dspace') @Component({ // eslint-disable-next-line @angular-eslint/component-selector - selector: '', + selector: 'ds-browse-entry-list-element', template: '' }) class MockThemedBrowseEntryListElementComponent { @@ -61,28 +60,6 @@ describe('BrowseByComponent', () => { let comp: BrowseByComponent; let fixture: ComponentFixture; - const mockItems = [ - Object.assign(new Item(), { - id: 'fakeId-1', - metadata: [ - { - key: 'dc.title', - value: 'First Fake Title' - } - ] - }), - Object.assign(new Item(), { - id: 'fakeId-2', - metadata: [ - { - key: 'dc.title', - value: 'Second Fake Title' - } - ] - }) - ]; - const mockItemsRD$ = createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), mockItems)); - const groupDataService = jasmine.createSpyObj('groupsDataService', { findListByHref: createSuccessfulRemoteDataObject$(createPaginatedList([])), getGroupRegistryRouterLink: '', @@ -113,8 +90,8 @@ describe('BrowseByComponent', () => { let themeService; beforeEach(waitForAsync(() => { - themeService = getMockThemeService('dspace'); - TestBed.configureTestingModule({ + themeService = getMockThemeService('base'); + void TestBed.configureTestingModule({ imports: [ BrowseByRoutingModule, AccessControlRoutingModule, @@ -200,40 +177,40 @@ describe('BrowseByComponent', () => { }); describe('when theme is base', () => { - beforeEach(() => { + beforeEach(async () => { themeService.getThemeName.and.returnValue('base'); themeService.getThemeName$.and.returnValue(observableOf('base')); fixture.detectChanges(); + await fixture.whenStable(); + fixture.detectChanges(); }); it('should use the base component to render browse entries', () => { - waitForAsync(() => { - const componentLoaders = fixture.debugElement.queryAll(By.directive(ListableObjectComponentLoaderComponent)); - expect(componentLoaders.length).toEqual(browseEntries.length); - componentLoaders.forEach((componentLoader) => { - const browseEntry = componentLoader.query(By.css('ds-browse-entry-list-element')); - expect(browseEntry.componentInstance).toBeInstanceOf(BrowseEntryListElementComponent); - }); + const componentLoaders = fixture.debugElement.queryAll(By.directive(ListableObjectComponentLoaderComponent)); + expect(componentLoaders.length).toEqual(browseEntries.length); + componentLoaders.forEach((componentLoader) => { + const browseEntry = componentLoader.query(By.css('ds-browse-entry-list-element')); + expect(browseEntry.componentInstance).toBeInstanceOf(BrowseEntryListElementComponent); }); }); }); - describe('when theme is custom', () => { - beforeEach(() => { - themeService.getThemeName.and.returnValue('custom'); - themeService.getThemeName$.and.returnValue(observableOf('custom')); + describe('when theme is dspace', () => { + beforeEach(async () => { + themeService.getThemeName.and.returnValue('dspace'); + themeService.getThemeName$.and.returnValue(observableOf('dspace')); + fixture.detectChanges(); + await fixture.whenStable(); fixture.detectChanges(); }); it('should use the themed component to render browse entries', () => { - waitForAsync(() => { const componentLoaders = fixture.debugElement.queryAll(By.directive(ListableObjectComponentLoaderComponent)); expect(componentLoaders.length).toEqual(browseEntries.length); componentLoaders.forEach((componentLoader) => { const browseEntry = componentLoader.query(By.css('ds-browse-entry-list-element')); expect(browseEntry.componentInstance).toBeInstanceOf(MockThemedBrowseEntryListElementComponent); }); - }); }); }); }); diff --git a/src/app/shared/browse-by/shared-browse-by.module.ts b/src/app/shared/browse-by/shared-browse-by.module.ts index 4041f296c86..63128989e14 100644 --- a/src/app/shared/browse-by/shared-browse-by.module.ts +++ b/src/app/shared/browse-by/shared-browse-by.module.ts @@ -27,4 +27,5 @@ const DECLARATIONS = [ ...DECLARATIONS, ] }) -export class SharedBrowseByModule { } +export class SharedBrowseByModule { +} diff --git a/src/app/shared/dso-page/dso-edit-menu.resolver.spec.ts b/src/app/shared/dso-page/dso-edit-menu.resolver.spec.ts index 14b4313ac22..2e874508cc4 100644 --- a/src/app/shared/dso-page/dso-edit-menu.resolver.spec.ts +++ b/src/app/shared/dso-page/dso-edit-menu.resolver.spec.ts @@ -24,9 +24,8 @@ import { Community } from '../../core/shared/community.model'; import { Collection } from '../../core/shared/collection.model'; import flatten from 'lodash/flatten'; import { DsoWithdrawnReinstateModalService } from './dso-withdrawn-reinstate-service/dso-withdrawn-reinstate-modal.service'; -import { AuthService } from 'src/app/core/auth/auth.service'; -import { AuthServiceMock } from '../mocks/auth.service.mock'; import { CorrectionTypeDataService } from 'src/app/core/submission/correctiontype-data.service'; +import { createPaginatedList } from '../testing/utils.test'; describe('DSOEditMenuResolver', () => { @@ -152,7 +151,7 @@ describe('DSOEditMenuResolver', () => { }); correctionsDataService = jasmine.createSpyObj('correctionsDataService', { - findByItem: observableOf([]) + findByItem: createSuccessfulRemoteDataObject$(createPaginatedList([])), }); TestBed.configureTestingModule({ @@ -167,7 +166,6 @@ describe('DSOEditMenuResolver', () => { {provide: TranslateService, useValue: translate}, {provide: NotificationsService, useValue: notificationsService}, {provide: DsoWithdrawnReinstateModalService, useValue: dsoWithdrawnReinstateModalService}, - {provide: AuthService, useValue: new AuthServiceMock()}, {provide: CorrectionTypeDataService, useValue: correctionsDataService}, { provide: NgbModal, useValue: { @@ -367,7 +365,7 @@ describe('DSOEditMenuResolver', () => { route = dsoRoute(testItem); }); - it('should return Item-specific entries', () => { + it('should return Item-specific entries', (done: DoneFn) => { const result = resolver.getDsoMenus(testObject, route, state); combineLatest(result).pipe(map(flatten)).subscribe((menu) => { const orcidEntry = menu.find(entry => entry.id === 'orcid-dso'); @@ -388,18 +386,20 @@ describe('DSOEditMenuResolver', () => { expect(claimEntry.active).toBeFalse(); expect(claimEntry.visible).toBeFalse(); expect(claimEntry.model.type).toEqual(MenuItemType.ONCLICK); + done(); }); }); - it('should not return Community/Collection-specific entries', () => { + it('should not return Community/Collection-specific entries', (done: DoneFn) => { const result = resolver.getDsoMenus(testObject, route, state); combineLatest(result).pipe(map(flatten)).subscribe((menu) => { const subscribeEntry = menu.find(entry => entry.id === 'subscribe'); expect(subscribeEntry).toBeFalsy(); + done(); }); }); - it('should return as third part the common list ', () => { + it('should return as third part the common list ', (done: DoneFn) => { const result = resolver.getDsoMenus(testObject, route, state); combineLatest(result).pipe(map(flatten)).subscribe((menu) => { const editEntry = menu.find(entry => entry.id === 'edit-dso'); @@ -410,6 +410,7 @@ describe('DSOEditMenuResolver', () => { expect((editEntry.model as LinkMenuItemModel).link).toEqual( '/items/test-item-uuid/edit/metadata' ); + done(); }); }); }); diff --git a/src/app/shared/dso-page/dso-edit-menu.resolver.ts b/src/app/shared/dso-page/dso-edit-menu.resolver.ts index bcded3acd5a..98d82eece44 100644 --- a/src/app/shared/dso-page/dso-edit-menu.resolver.ts +++ b/src/app/shared/dso-page/dso-edit-menu.resolver.ts @@ -24,9 +24,6 @@ import { ResearcherProfileDataService } from '../../core/profile/researcher-prof import { NotificationsService } from '../notifications/notifications.service'; import { TranslateService } from '@ngx-translate/core'; import { DsoWithdrawnReinstateModalService, REQUEST_REINSTATE, REQUEST_WITHDRAWN } from './dso-withdrawn-reinstate-service/dso-withdrawn-reinstate-modal.service'; -import { AuthService } from '../../core/auth/auth.service'; -import { FindListOptions } from '../../core/data/find-list-options.model'; -import { RequestParam } from '../../core/cache/models/request-param.model'; import { CorrectionTypeDataService } from '../../core/submission/correctiontype-data.service'; import { SubscriptionModalComponent } from '../subscriptions/subscription-modal/subscription-modal.component'; import { Community } from '../../core/shared/community.model'; @@ -50,7 +47,6 @@ export class DSOEditMenuResolver implements Resolve<{ [key: string]: MenuSection protected notificationsService: NotificationsService, protected translate: TranslateService, protected dsoWithdrawnReinstateModalService: DsoWithdrawnReinstateModalService, - private auth: AuthService, private correctionTypeDataService: CorrectionTypeDataService ) { } @@ -133,9 +129,6 @@ export class DSOEditMenuResolver implements Resolve<{ [key: string]: MenuSection */ protected getItemMenu(dso): Observable { if (dso instanceof Item) { - const findListTopicOptions: FindListOptions = { - searchParams: [new RequestParam('target', dso.uuid)] - }; return combineLatest([ this.authorizationService.isAuthorized(FeatureID.CanCreateVersion, dso.self), this.dsoVersioningModalService.isNewVersionButtonDisabled(dso), diff --git a/src/app/shared/form/vocabulary-treeview-modal/vocabulary-treeview-modal.component.ts b/src/app/shared/form/vocabulary-treeview-modal/vocabulary-treeview-modal.component.ts index c6b0bf20feb..8213b91f9c4 100644 --- a/src/app/shared/form/vocabulary-treeview-modal/vocabulary-treeview-modal.component.ts +++ b/src/app/shared/form/vocabulary-treeview-modal/vocabulary-treeview-modal.component.ts @@ -1,4 +1,4 @@ -import { Component, Input } from '@angular/core'; +import { Component, EventEmitter, Input, Output } from '@angular/core'; import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; import { VocabularyOptions } from '../../../core/submission/vocabularies/models/vocabulary-options.model'; import { VocabularyEntryDetail } from '../../../core/submission/vocabularies/models/vocabulary-entry-detail.model'; @@ -33,6 +33,12 @@ export class VocabularyTreeviewModalComponent { */ @Input() multiSelect = false; + /** + * An event fired when a vocabulary entry is selected. + * Event's payload equals to {@link VocabularyEntryDetail} selected. + */ + @Output() select: EventEmitter = new EventEmitter(null); + /** * Initialize instance variables * @@ -46,6 +52,7 @@ export class VocabularyTreeviewModalComponent { * Method called on entry select */ onSelect(item: VocabularyEntryDetail) { + this.select.emit(item); this.activeModal.close(item); } } diff --git a/src/app/shared/host-window.reducer.spec.ts b/src/app/shared/host-window.reducer.spec.ts index f580c0e1dab..f8e5802e301 100644 --- a/src/app/shared/host-window.reducer.spec.ts +++ b/src/app/shared/host-window.reducer.spec.ts @@ -44,6 +44,10 @@ describe('hostWindowReducer', () => { const action = new HostWindowResizeAction(1024, 768); hostWindowReducer(state, action); + + // no expect required, deepFreeze will ensure an exception is thrown if the state + // is mutated, and any uncaught exception will cause the test to fail + expect().nothing(); }); }); diff --git a/src/app/shared/menu/menu.reducer.spec.ts b/src/app/shared/menu/menu.reducer.spec.ts index 8f540c016d5..73710ae8251 100644 --- a/src/app/shared/menu/menu.reducer.spec.ts +++ b/src/app/shared/menu/menu.reducer.spec.ts @@ -180,6 +180,7 @@ describe('menusReducer', () => { // no expect required, deepFreeze will ensure an exception is thrown if the state // is mutated, and any uncaught exception will cause the test to fail + expect().nothing(); }); it('should set collapsed to false for the correct menu in response to the EXPAND_MENU action', () => { @@ -201,6 +202,7 @@ describe('menusReducer', () => { // no expect required, deepFreeze will ensure an exception is thrown if the state // is mutated, and any uncaught exception will cause the test to fail + expect().nothing(); }); it('should set collapsed to false for the correct menu in response to the TOGGLE_MENU action when collapsed is true', () => { @@ -231,6 +233,7 @@ describe('menusReducer', () => { // no expect required, deepFreeze will ensure an exception is thrown if the state // is mutated, and any uncaught exception will cause the test to fail + expect().nothing(); }); it('should set previewCollapsed to true for the correct menu in response to the COLLAPSE_MENU_PREVIEW action', () => { @@ -252,6 +255,7 @@ describe('menusReducer', () => { // no expect required, deepFreeze will ensure an exception is thrown if the state // is mutated, and any uncaught exception will cause the test to fail + expect().nothing(); }); it('should set previewCollapsed to false for the correct menu in response to the EXPAND_MENU_PREVIEW action', () => { @@ -273,6 +277,7 @@ describe('menusReducer', () => { // no expect required, deepFreeze will ensure an exception is thrown if the state // is mutated, and any uncaught exception will cause the test to fail + expect().nothing(); }); it('should set visible to true for the correct menu in response to the SHOW_MENU action', () => { @@ -294,6 +299,7 @@ describe('menusReducer', () => { // no expect required, deepFreeze will ensure an exception is thrown if the state // is mutated, and any uncaught exception will cause the test to fail + expect().nothing(); }); it('should set previewCollapsed to false for the correct menu in response to the HIDE_MENU action', () => { @@ -315,6 +321,7 @@ describe('menusReducer', () => { // no expect required, deepFreeze will ensure an exception is thrown if the state // is mutated, and any uncaught exception will cause the test to fail + expect().nothing(); }); it('should reset the menu state to the initial state when performing the REINIT_MENUS action without affecting the previous state', () => { @@ -358,6 +365,7 @@ describe('menusReducer', () => { // no expect required, deepFreeze will ensure an exception is thrown if the state // is mutated, and any uncaught exception will cause the test to fail + expect().nothing(); }); it('should remove a section for the correct menu in response to the REMOVE_SECTION action', () => { @@ -394,6 +402,10 @@ describe('menusReducer', () => { const action = new ActivateMenuSectionAction(menuID, topSectionID); menusReducer(state, action); + + // no expect required, deepFreeze will ensure an exception is thrown if the state + // is mutated, and any uncaught exception will cause the test to fail + expect().nothing(); }); it('should set active to false for the correct menu section in response to the DEACTIVATE_SECTION action', () => { @@ -412,6 +424,10 @@ describe('menusReducer', () => { const action = new DeactivateMenuSectionAction(menuID, topSectionID); menusReducer(state, action); + + // no expect required, deepFreeze will ensure an exception is thrown if the state + // is mutated, and any uncaught exception will cause the test to fail + expect().nothing(); }); it('should set active to false for the correct menu in response to the TOGGLE_ACTIVE_SECTION action when active is true', () => { @@ -441,6 +457,7 @@ describe('menusReducer', () => { // no expect required, deepFreeze will ensure an exception is thrown if the state // is mutated, and any uncaught exception will cause the test to fail + expect().nothing(); }); it('should set visible to true for the correct menu section in response to the SHOW_SECTION action', () => { @@ -459,6 +476,10 @@ describe('menusReducer', () => { const action = new ShowMenuSectionAction(menuID, topSectionID); menusReducer(state, action); + + // no expect required, deepFreeze will ensure an exception is thrown if the state + // is mutated, and any uncaught exception will cause the test to fail + expect().nothing(); }); it('should set visible to false for the correct menu section in response to the HIDE_SECTION action', () => { @@ -477,5 +498,9 @@ describe('menusReducer', () => { const action = new HideMenuSectionAction(menuID, topSectionID); menusReducer(state, action); + + // no expect required, deepFreeze will ensure an exception is thrown if the state + // is mutated, and any uncaught exception will cause the test to fail + expect().nothing(); }); }); diff --git a/src/app/shared/notifications/notifications.reducers.spec.ts b/src/app/shared/notifications/notifications.reducers.spec.ts index fde92e8891b..f1d83423a20 100644 --- a/src/app/shared/notifications/notifications.reducers.spec.ts +++ b/src/app/shared/notifications/notifications.reducers.spec.ts @@ -1,7 +1,7 @@ import { notificationsReducer } from './notifications.reducers'; import { NewNotificationAction, RemoveAllNotificationsAction, RemoveNotificationAction } from './notifications.actions'; import { NotificationsService } from './notifications.service'; -import { fakeAsync, flush, inject, TestBed, tick } from '@angular/core/testing'; +import { TestBed } from '@angular/core/testing'; import { NotificationsBoardComponent } from './notifications-board/notifications-board.component'; import { StoreModule } from '@ngrx/store'; import { NotificationComponent } from './notification/notification.component'; @@ -106,38 +106,4 @@ describe('Notifications reducer', () => { expect(state3.length).toEqual(0); }); - it('should create 2 notifications and check they close after different timeout', fakeAsync(() => { - inject([ChangeDetectorRef], (cdr: ChangeDetectorRef) => { - const optionsWithTimeout = new NotificationOptions( - 1000, - true, - NotificationAnimationsType.Rotate); - // Timeout 1000ms - const notification = new Notification(uniqueId(), NotificationType.Success, 'title', 'content', optionsWithTimeout, null); - const state = notificationsReducer(undefined, new NewNotificationAction(notification)); - expect(state.length).toEqual(1); - - // Timeout default 5000ms - const notificationBis = new Notification(uniqueId(), NotificationType.Success, 'title', 'content'); - const stateBis = notificationsReducer(state, new NewNotificationAction(notification)); - expect(stateBis.length).toEqual(2); - - tick(1000); - cdr.detectChanges(); - - const action = new NewNotificationAction(notification); - action.type = 'NothingToDo, return only the state'; - - const lastState = notificationsReducer(stateBis, action); - expect(lastState.length).toEqual(1); - - flush(); - cdr.detectChanges(); - - const finalState = notificationsReducer(lastState, action); - expect(finalState.length).toEqual(0); - }); - - })); - }); diff --git a/src/app/shared/object-list/metadata-representation-list-element/browse-link/browse-link-metadata-list-element.component.spec.ts b/src/app/shared/object-list/metadata-representation-list-element/browse-link/browse-link-metadata-list-element.component.spec.ts index 3527b9fddd5..1eb97a03bfb 100644 --- a/src/app/shared/object-list/metadata-representation-list-element/browse-link/browse-link-metadata-list-element.component.spec.ts +++ b/src/app/shared/object-list/metadata-representation-list-element/browse-link/browse-link-metadata-list-element.component.spec.ts @@ -1,24 +1,36 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core'; import { BrowseLinkMetadataListElementComponent } from './browse-link-metadata-list-element.component'; -import { MetadatumRepresentation } from '../../../../core/shared/metadata-representation/metadatum/metadatum-representation.model'; +import { + MetadatumRepresentation +} from '../../../../core/shared/metadata-representation/metadatum/metadatum-representation.model'; +import { + MetadataRepresentationType +} from '../../../../core/shared/metadata-representation/metadata-representation.model'; +import { ValueListBrowseDefinition } from '../../../../core/shared/value-list-browse-definition.model'; const mockMetadataRepresentation = Object.assign(new MetadatumRepresentation('type'), { key: 'dc.contributor.author', - value: 'Test Author' -}); + value: 'Test Author', + browseDefinition: Object.assign(new ValueListBrowseDefinition(), { + id: 'author', + }), +} as Partial); const mockMetadataRepresentationWithUrl = Object.assign(new MetadatumRepresentation('type'), { key: 'dc.subject', - value: 'http://purl.org/test/subject' -}); + value: 'https://purl.org/test/subject', + browseDefinition: Object.assign(new ValueListBrowseDefinition(), { + id: 'subject', + }), +} as Partial); describe('BrowseLinkMetadataListElementComponent', () => { let comp: BrowseLinkMetadataListElementComponent; let fixture: ComponentFixture; beforeEach(waitForAsync(() => { - TestBed.configureTestingModule({ + void TestBed.configureTestingModule({ imports: [], declarations: [BrowseLinkMetadataListElementComponent], schemas: [NO_ERRORS_SCHEMA] @@ -27,35 +39,40 @@ describe('BrowseLinkMetadataListElementComponent', () => { }).compileComponents(); })); - beforeEach(waitForAsync(() => { + beforeEach(() => { fixture = TestBed.createComponent(BrowseLinkMetadataListElementComponent); comp = fixture.componentInstance; - comp.mdRepresentation = mockMetadataRepresentation; - fixture.detectChanges(); - })); + }); + + describe('with normal metadata', () => { + beforeEach(() => { + comp.mdRepresentation = mockMetadataRepresentation; + spyOnProperty(comp.mdRepresentation, 'representationType', 'get').and.returnValue(MetadataRepresentationType.BrowseLink); + fixture.detectChanges(); + }); - waitForAsync(() => { it('should contain the value as a browse link', () => { expect(fixture.debugElement.nativeElement.textContent).toContain(mockMetadataRepresentation.value); }); + it('should NOT match isLink', () => { - expect(comp.isLink).toBe(false); + expect(comp.isLink()).toBe(false); }); }); - beforeEach(waitForAsync(() => { - fixture = TestBed.createComponent(BrowseLinkMetadataListElementComponent); - comp = fixture.componentInstance; - comp.mdRepresentation = mockMetadataRepresentationWithUrl; - fixture.detectChanges(); - })); + describe('with metadata wit an url', () => { + beforeEach(() => { + comp.mdRepresentation = mockMetadataRepresentationWithUrl; + spyOnProperty(comp.mdRepresentation, 'representationType', 'get').and.returnValue(MetadataRepresentationType.BrowseLink); + fixture.detectChanges(); + }); - waitForAsync(() => { it('should contain the value expected', () => { expect(fixture.debugElement.nativeElement.textContent).toContain(mockMetadataRepresentationWithUrl.value); }); + it('should match isLink', () => { - expect(comp.isLink).toBe(true); + expect(comp.isLink()).toBe(true); }); }); diff --git a/src/app/shared/object-list/metadata-representation-list-element/metadata-representation-list-element.component.spec.ts b/src/app/shared/object-list/metadata-representation-list-element/metadata-representation-list-element.component.spec.ts index dc8febe84ac..cf85154a192 100644 --- a/src/app/shared/object-list/metadata-representation-list-element/metadata-representation-list-element.component.spec.ts +++ b/src/app/shared/object-list/metadata-representation-list-element/metadata-representation-list-element.component.spec.ts @@ -19,7 +19,7 @@ describe('MetadataRepresentationListElementComponent', () => { let fixture: ComponentFixture; beforeEach(waitForAsync(() => { - TestBed.configureTestingModule({ + return TestBed.configureTestingModule({ imports: [], declarations: [MetadataRepresentationListElementComponent], schemas: [NO_ERRORS_SCHEMA] @@ -39,9 +39,7 @@ describe('MetadataRepresentationListElementComponent', () => { comp.mdRepresentation = mockMetadataRepresentation; }); it('isLink correctly detects a non-URL string as false', () => { - waitForAsync(() => { - expect(comp.isLink()).toBe(false); - }); + expect(comp.isLink()).toBe(false); }); }); @@ -50,9 +48,7 @@ describe('MetadataRepresentationListElementComponent', () => { comp.mdRepresentation = mockMetadataRepresentationUrl; }); it('isLink correctly detects a URL string as true', () => { - waitForAsync(() => { - expect(comp.isLink()).toBe(true); - }); + expect(comp.isLink()).toBe(true); }); }); diff --git a/src/app/shared/object-list/metadata-representation-list-element/metadata-representation-list-element.component.ts b/src/app/shared/object-list/metadata-representation-list-element/metadata-representation-list-element.component.ts index d8f8621ca61..3539502843d 100644 --- a/src/app/shared/object-list/metadata-representation-list-element/metadata-representation-list-element.component.ts +++ b/src/app/shared/object-list/metadata-representation-list-element/metadata-representation-list-element.component.ts @@ -25,7 +25,7 @@ export class MetadataRepresentationListElementComponent { */ isLink(): boolean { // Match any string that begins with http:// or https:// - const linkPattern = new RegExp(/^https?\/\/.*/); + const linkPattern = new RegExp(/^https?:\/\/.*/); return linkPattern.test(this.mdRepresentation.getValue()); } diff --git a/src/app/shared/object-list/object-list.component.html b/src/app/shared/object-list/object-list.component.html index b8712b85c57..5fe57e13698 100644 --- a/src/app/shared/object-list/object-list.component.html +++ b/src/app/shared/object-list/object-list.component.html @@ -15,7 +15,7 @@ (paginationChange)="onPaginationChange($event)" (prev)="goPrev()" (next)="goNext()"> -
    +
    • { describe('when environment is set to show thumbnail images', () => { it('should offset content', () => { - const offset = fixture.debugElement.query(By.css('offset-md-2')); + const offset: DebugElement = fixture.debugElement.query(By.css('.offset-md-2')); + expect(offset).not.toBeNull(); }); }); diff --git a/src/app/shared/object-list/search-result-list-element/community-search-result/community-search-result-list-element.component.spec.ts b/src/app/shared/object-list/search-result-list-element/community-search-result/community-search-result-list-element.component.spec.ts index ce12f5f7da3..cb9492c945b 100644 --- a/src/app/shared/object-list/search-result-list-element/community-search-result/community-search-result-list-element.component.spec.ts +++ b/src/app/shared/object-list/search-result-list-element/community-search-result/community-search-result-list-element.component.spec.ts @@ -1,7 +1,7 @@ import { CommunitySearchResultListElementComponent } from './community-search-result-list-element.component'; import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { of as observableOf } from 'rxjs'; -import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core'; +import { ChangeDetectionStrategy, DebugElement, NO_ERRORS_SCHEMA } from '@angular/core'; import { By } from '@angular/platform-browser'; import { TruncatePipe } from '../../../utils/truncate.pipe'; import { Community } from '../../../../core/shared/community.model'; @@ -99,7 +99,8 @@ describe('CommunitySearchResultListElementComponent', () => { describe('when environment is set to show thumbnail images', () => { it('should offset content', () => { - const offset = fixture.debugElement.query(By.css('offset-md-2')); + const offset: DebugElement = fixture.debugElement.query(By.css('.offset-md-2')); + expect(offset).not.toBeNull(); }); }); }); diff --git a/src/app/shared/search/search-filters/search-filter/search-filter.reducer.spec.ts b/src/app/shared/search/search-filters/search-filter/search-filter.reducer.spec.ts index aa64589d2e0..e94f175a8a5 100644 --- a/src/app/shared/search/search-filters/search-filter/search-filter.reducer.spec.ts +++ b/src/app/shared/search/search-filters/search-filter/search-filter.reducer.spec.ts @@ -60,6 +60,7 @@ describe('filterReducer', () => { // no expect required, deepFreeze will ensure an exception is thrown if the state // is mutated, and any uncaught exception will cause the test to fail + expect().nothing(); }); it('should set filterCollapsed to false in response to the EXPAND action', () => { @@ -78,6 +79,10 @@ describe('filterReducer', () => { const action = new SearchFilterExpandAction(filterName1); filterReducer(state, action); + + // no expect required, deepFreeze will ensure an exception is thrown if the state + // is mutated, and any uncaught exception will cause the test to fail + expect().nothing(); }); it('should flip the value of filterCollapsed in response to the TOGGLE action', () => { @@ -99,6 +104,10 @@ describe('filterReducer', () => { const action = new SearchFilterToggleAction(filterName1); filterReducer(state, action); + + // no expect required, deepFreeze will ensure an exception is thrown if the state + // is mutated, and any uncaught exception will cause the test to fail + expect().nothing(); }); it('should set filterCollapsed to true in response to the INITIALIZE action with isOpenByDefault to false when no state has been set for this filter', () => { diff --git a/src/app/shared/search/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.spec.ts b/src/app/shared/search/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.spec.ts index a6212ca9c5e..a36280d05fa 100644 --- a/src/app/shared/search/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.spec.ts +++ b/src/app/shared/search/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.spec.ts @@ -1,5 +1,5 @@ import { SearchHierarchyFilterComponent } from './search-hierarchy-filter.component'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; import { DebugElement, EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core'; import { By } from '@angular/platform-browser'; import { VocabularyService } from '../../../../../core/submission/vocabularies/vocabulary.service'; @@ -59,7 +59,7 @@ describe('SearchHierarchyFilterComponent', () => { }; beforeEach(() => { - TestBed.configureTestingModule({ + return TestBed.configureTestingModule({ imports: [ CommonModule, NgbModule, @@ -128,10 +128,10 @@ describe('SearchHierarchyFilterComponent', () => { const newSelectedValue = 'new-selected-value'; beforeEach(async () => { - showVocabularyTreeLink.nativeElement.click(); fixture.componentInstance.selectedValues$ = observableOf( alreadySelectedValues.map(value => Object.assign(new FacetValue(), { value })) ); + showVocabularyTreeLink.nativeElement.click(); VocabularyTreeViewComponent.select.emit(Object.assign(new VocabularyEntryDetail(), { value: newSelectedValue, })); @@ -143,8 +143,9 @@ describe('SearchHierarchyFilterComponent', () => { describe('when selecting a value from the vocabulary tree', () => { - it('should add a new search filter to the existing search filters', () => { - waitForAsync(() => expect(router.navigate).toHaveBeenCalledWith([testSearchLink], { + it('should add a new search filter to the existing search filters', fakeAsync(() => { + tick(); + expect(router.navigate).toHaveBeenCalledWith([testSearchLink], { queryParams: { [`f.${testSearchFilter}`]: [ ...alreadySelectedValues, @@ -152,8 +153,8 @@ describe('SearchHierarchyFilterComponent', () => { ].map((value => `${value},equals`)), }, queryParamsHandling: 'merge', - })); - }); + }); + })); }); }); }); diff --git a/src/app/shared/search/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.ts b/src/app/shared/search/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.ts index 18ddbbdf97e..2e3b328d618 100644 --- a/src/app/shared/search/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.ts +++ b/src/app/shared/search/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.ts @@ -1,4 +1,4 @@ -import { Component, Inject, OnInit } from '@angular/core'; +import { Component, Inject, OnDestroy, OnInit } from '@angular/core'; import { renderFacetFor } from '../search-filter-type-decorator'; import { FilterType } from '../../../models/filter-type.model'; import { facetLoad, SearchFacetFilterComponent } from '../search-facet-filter/search-facet-filter.component'; @@ -22,7 +22,7 @@ import { FacetValue } from '../../../models/facet-value.model'; import { getFacetValueForType } from '../../../search.utils'; import { filter, map, take } from 'rxjs/operators'; import { VocabularyService } from '../../../../../core/submission/vocabularies/vocabulary.service'; -import { Observable, BehaviorSubject } from 'rxjs'; +import { Observable, BehaviorSubject, Subscription, combineLatest } from 'rxjs'; import { PageInfo } from '../../../../../core/shared/page-info.model'; import { addOperatorToFilterValue } from '../../../search.utils'; import { VocabularyTreeviewModalComponent } from '../../../../form/vocabulary-treeview-modal/vocabulary-treeview-modal.component'; @@ -41,7 +41,9 @@ import { FilterVocabularyConfig } from '../../../../../../config/filter-vocabula * Component that represents a hierarchy facet for a specific filter configuration */ @renderFacetFor(FilterType.hierarchy) -export class SearchHierarchyFilterComponent extends SearchFacetFilterComponent implements OnInit { +export class SearchHierarchyFilterComponent extends SearchFacetFilterComponent implements OnDestroy, OnInit { + + subscriptions: Subscription[] = []; constructor(protected searchService: SearchService, protected filterService: SearchFilterService, @@ -61,6 +63,11 @@ export class SearchHierarchyFilterComponent extends SearchFacetFilterComponent i vocabularyExists$: Observable; + ngOnDestroy(): void { + super.ngOnDestroy(); + this.subscriptions.forEach((subscription: Subscription) => subscription.unsubscribe()); + } + /** * Submits a new active custom value to the filter from the input field * Overwritten method from parent component, adds the "query" operator to the received data before passing it on @@ -99,22 +106,21 @@ export class SearchHierarchyFilterComponent extends SearchFacetFilterComponent i name: this.getVocabularyEntry(), closed: true }; - void modalRef.result.then((detail: VocabularyEntryDetail) => { - this.subs.push(this.selectedValues$ - .pipe(take(1)) - .subscribe((selectedValues) => { - void this.router.navigate( - [this.searchService.getSearchLink()], - { - queryParams: { - [this.filterConfig.paramName]: [...selectedValues, {value: detail.value}] - .map((facetValue: FacetValue) => getFacetValueForType(facetValue, this.filterConfig)), - }, - queryParamsHandling: 'merge', - }, - ); - })); - }); + this.subscriptions.push(combineLatest([ + (modalRef.componentInstance as VocabularyTreeviewModalComponent).select, + this.selectedValues$.pipe(take(1)), + ]).subscribe(([detail, selectedValues]: [VocabularyEntryDetail, FacetValue[]]) => { + void this.router.navigate( + [this.searchService.getSearchLink()], + { + queryParams: { + [this.filterConfig.paramName]: [...selectedValues, {value: detail.value}] + .map((facetValue: FacetValue) => getFacetValueForType(facetValue, this.filterConfig)), + }, + queryParamsHandling: 'merge', + }, + ); + })); } /** diff --git a/src/app/shared/sidebar/sidebar.reducer.spec.ts b/src/app/shared/sidebar/sidebar.reducer.spec.ts index 76962f60c11..ffb3dbaabc9 100644 --- a/src/app/shared/sidebar/sidebar.reducer.spec.ts +++ b/src/app/shared/sidebar/sidebar.reducer.spec.ts @@ -47,6 +47,7 @@ describe('sidebarReducer', () => { // no expect required, deepFreeze will ensure an exception is thrown if the state // is mutated, and any uncaught exception will cause the test to fail + expect().nothing(); }); it('should set sidebarCollapsed to false in response to the EXPAND action', () => { @@ -63,6 +64,10 @@ describe('sidebarReducer', () => { const action = new SidebarExpandAction(); sidebarReducer(state, action); + + // no expect required, deepFreeze will ensure an exception is thrown if the state + // is mutated, and any uncaught exception will cause the test to fail + expect().nothing(); }); it('should flip the value of sidebarCollapsed in response to the TOGGLE action', () => { @@ -82,6 +87,10 @@ describe('sidebarReducer', () => { const action = new SidebarToggleAction(); sidebarReducer(state, action); + + // no expect required, deepFreeze will ensure an exception is thrown if the state + // is mutated, and any uncaught exception will cause the test to fail + expect().nothing(); }); }); diff --git a/src/app/shared/subscriptions/subscriptions-data.service.spec.ts b/src/app/shared/subscriptions/subscriptions-data.service.spec.ts index 9c4c69123d3..790fde3ca0b 100644 --- a/src/app/shared/subscriptions/subscriptions-data.service.spec.ts +++ b/src/app/shared/subscriptions/subscriptions-data.service.spec.ts @@ -12,7 +12,6 @@ import { HALEndpointService } from '../../core/shared/hal-endpoint.service'; import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; import { getMockRequestService } from '../mocks/request.service.mock'; import { getMockRemoteDataBuildService } from '../mocks/remote-data-build.service.mock'; -import { SearchDataImpl } from '../../core/data/base/search-data'; import { NotificationsServiceStub } from '../testing/notifications-service.stub'; import { HALEndpointServiceStub } from '../testing/hal-endpoint-service.stub'; import { createPaginatedList } from '../testing/utils.test'; @@ -22,7 +21,6 @@ describe('SubscriptionsDataService', () => { let service: SubscriptionsDataService; - let searchData: SearchDataImpl; let comparator: DSOChangeAnalyzer; let http: HttpClient; @@ -121,11 +119,11 @@ describe('SubscriptionsDataService', () => { }); it('should get the subscriptions', () => { + spyOn((service as any).searchData, 'searchBy'); const id = 'test-id'; const ePersonId = 'test-ePersonId'; - service.getSubscriptionsByPersonDSO(ePersonId, id).subscribe(() => { - expect(searchData.searchBy).toHaveBeenCalled(); - }); + service.getSubscriptionsByPersonDSO(ePersonId, id); + expect((service as any).searchData.searchBy).toHaveBeenCalled(); }); }); diff --git a/src/app/shared/truncatable/truncatable.reducer.spec.ts b/src/app/shared/truncatable/truncatable.reducer.spec.ts index 9866f382f72..a4628c309ed 100644 --- a/src/app/shared/truncatable/truncatable.reducer.spec.ts +++ b/src/app/shared/truncatable/truncatable.reducer.spec.ts @@ -53,6 +53,7 @@ describe('truncatableReducer', () => { // no expect required, deepFreeze will ensure an exception is thrown if the state // is mutated, and any uncaught exception will cause the test to fail + expect().nothing(); }); it('should set filterCollapsed to false in response to the EXPAND action', () => { @@ -71,6 +72,10 @@ describe('truncatableReducer', () => { const action = new TruncatableExpandAction(id1); truncatableReducer(state, action); + + // no expect required, deepFreeze will ensure an exception is thrown if the state + // is mutated, and any uncaught exception will cause the test to fail + expect().nothing(); }); it('should flip the value of filterCollapsed in response to the TOGGLE action', () => { @@ -92,5 +97,9 @@ describe('truncatableReducer', () => { const action = new TruncatableToggleAction(id2); truncatableReducer(state, action); + + // no expect required, deepFreeze will ensure an exception is thrown if the state + // is mutated, and any uncaught exception will cause the test to fail + expect().nothing(); }); }); diff --git a/src/app/submission/sections/upload/file/section-upload-file.component.spec.ts b/src/app/submission/sections/upload/file/section-upload-file.component.spec.ts index c12991c55f2..bb72439aabd 100644 --- a/src/app/submission/sections/upload/file/section-upload-file.component.spec.ts +++ b/src/app/submission/sections/upload/file/section-upload-file.component.spec.ts @@ -180,7 +180,7 @@ describe('SubmissionSectionUploadFileComponent test suite', () => { expect(comp.fileData).toEqual(fileData); }); - it('should call deleteFile on delete confirmation', () => { + it('should call deleteFile on delete confirmation', async () => { spyOn(compAsAny, 'deleteFile'); comp.fileData = fileData; @@ -196,9 +196,8 @@ describe('SubmissionSectionUploadFileComponent test suite', () => { fixture.detectChanges(); - fixture.whenStable().then(() => { - expect(compAsAny.deleteFile).toHaveBeenCalled(); - }); + await fixture.whenStable(); + expect(compAsAny.deleteFile).toHaveBeenCalled(); }); it('should delete primary if file we delete is primary', () => {