diff --git a/src/app/core/auth/auth-response-parsing.service.spec.ts b/src/app/core/auth/auth-response-parsing.service.spec.ts index 0b8f09c54e..ff8304335c 100644 --- a/src/app/core/auth/auth-response-parsing.service.spec.ts +++ b/src/app/core/auth/auth-response-parsing.service.spec.ts @@ -6,16 +6,30 @@ import { AuthStatus } from './models/auth-status.model'; import { AuthResponseParsingService } from './auth-response-parsing.service'; import { AuthGetRequest, AuthPostRequest } from '../data/request.models'; import { MockStore } from '../../shared/testing/mock-store'; -import { ObjectCacheState } from '../cache/object-cache.reducer'; +import { async, TestBed } from '@angular/core/testing'; +import { Store, StoreModule } from '@ngrx/store'; describe('AuthResponseParsingService', () => { let service: AuthResponseParsingService; const EnvConfig = { cache: { msToLive: 1000 } } as GlobalConfig; - const store = new MockStore({}); - const objectCacheService = new ObjectCacheService(store as any); + let store: any; + let objectCacheService: ObjectCacheService; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + StoreModule.forRoot({}), + ], + providers: [ + { provide: Store, useClass: MockStore } + ] + }).compileComponents(); + })); beforeEach(() => { + store = TestBed.get(Store); + objectCacheService = new ObjectCacheService(store as any); service = new AuthResponseParsingService(EnvConfig, objectCacheService); }); diff --git a/src/app/core/integration/integration.service.spec.ts b/src/app/core/integration/integration.service.spec.ts index edb1acfc03..573a69b8c0 100644 --- a/src/app/core/integration/integration.service.spec.ts +++ b/src/app/core/integration/integration.service.spec.ts @@ -42,10 +42,12 @@ describe('IntegrationService', () => { const name = 'type'; const metadata = 'dc.type'; const query = ''; + const value = 'test'; const uuid = 'd9d30c0c-69b7-4369-8397-ca67c888974d'; const integrationEndpoint = 'https://rest.api/integration'; const serviceEndpoint = `${integrationEndpoint}/${LINK_NAME}`; const entriesEndpoint = `${serviceEndpoint}/${name}/entries?query=${query}&metadata=${metadata}&uuid=${uuid}`; + const entryValueEndpoint = `${serviceEndpoint}/${name}/entryValue/${value}?metadata=${metadata}`; findOptions = new IntegrationSearchOptions(uuid, name, metadata); @@ -88,4 +90,20 @@ describe('IntegrationService', () => { }); }); + describe('getEntryByValue', () => { + + it('should configure a new IntegrationRequest', () => { + findOptions = new IntegrationSearchOptions( + null, + name, + metadata, + value); + + const expected = new IntegrationRequest(requestService.generateRequestId(), entryValueEndpoint); + scheduler.schedule(() => service.getEntryByValue(findOptions).subscribe()); + scheduler.flush(); + + expect(requestService.configure).toHaveBeenCalledWith(expected); + }); + }); }); diff --git a/src/app/core/json-patch/json-patch-operations.reducer.spec.ts b/src/app/core/json-patch/json-patch-operations.reducer.spec.ts index b2d65975f2..c6b21ce037 100644 --- a/src/app/core/json-patch/json-patch-operations.reducer.spec.ts +++ b/src/app/core/json-patch/json-patch-operations.reducer.spec.ts @@ -315,8 +315,6 @@ describe('jsonPatchOperationsReducer test suite', () => { const action = new FlushPatchOperationsAction(testJsonPatchResourceType, undefined); const newState = jsonPatchOperationsReducer(initState, action); - console.log(initState); - console.log(newState); expect(newState[testJsonPatchResourceType].transactionStartTime).toBeNull(); expect(newState[testJsonPatchResourceType].commitPending).toBeFalsy(); expect(newState[testJsonPatchResourceType].children[testJsonPatchResourceId].body).toEqual([]); diff --git a/src/app/core/json-patch/json-patch-operations.service.spec.ts b/src/app/core/json-patch/json-patch-operations.service.spec.ts index 3d1237989f..a06f1ec5e4 100644 --- a/src/app/core/json-patch/json-patch-operations.service.spec.ts +++ b/src/app/core/json-patch/json-patch-operations.service.spec.ts @@ -1,8 +1,9 @@ -import { cold, getTestScheduler } from 'jasmine-marbles'; +import { async, TestBed } from '@angular/core/testing'; +import { cold, getTestScheduler } from 'jasmine-marbles'; import { TestScheduler } from 'rxjs/testing'; import { of as observableOf } from 'rxjs'; -import { Store } from '@ngrx/store'; +import { Store, StoreModule } from '@ngrx/store'; import { getMockRequestService } from '../../shared/mocks/mock-request.service'; import { ResponseCacheService } from '../cache/response-cache.service'; @@ -16,12 +17,12 @@ import { SubmitDataResponseDefinitionObject } from '../shared/submit-data-respon import { CoreState } from '../core.reducers'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { JsonPatchOperationsEntry, JsonPatchOperationsResourceEntry } from './json-patch-operations.reducer'; -import { MockStore } from '../../shared/testing/mock-store'; import { CommitPatchOperationsAction, RollbacktPatchOperationsAction, StartTransactionPatchOperationsAction } from './json-patch-operations.actions'; +import { MockStore } from '../../shared/testing/mock-store'; class TestService extends JsonPatchOperationsService { protected linkPath = ''; @@ -102,8 +103,19 @@ describe('JsonPatchOperationsService test suite', () => { } + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + StoreModule.forRoot({}), + ], + providers: [ + { provide: Store, useClass: MockStore } + ] + }).compileComponents(); + })); + beforeEach(() => { - store = new MockStore({} as CoreState); + store = TestBed.get(Store); responseCache = initMockResponseCacheService(true); requestService = getMockRequestService(); rdbService = getMockRemoteDataBuildService(); @@ -111,8 +123,8 @@ describe('JsonPatchOperationsService test suite', () => { halService = new HALEndpointServiceStub(resourceEndpointURL); service = initTestService(); - spyOn((service as any).store, 'select').and.returnValue(observableOf(mockState['json/patch'][testJsonPatchResourceType])); - spyOn((service as any).store, 'dispatch').and.callThrough(); + spyOn(store, 'select').and.returnValue(observableOf(mockState['json/patch'][testJsonPatchResourceType])); + spyOn(store, 'dispatch').and.callThrough(); spyOn(Date.prototype, 'getTime').and.callFake(() => { return timestamp; }); @@ -142,7 +154,7 @@ describe('JsonPatchOperationsService test suite', () => { scheduler.schedule(() => service.jsonPatchByResourceType(resourceEndpoint, resourceScope, testJsonPatchResourceType).subscribe()); scheduler.flush(); - expect((service as any).store.dispatch).toHaveBeenCalledWith(expectedAction); + expect(store.dispatch).toHaveBeenCalledWith(expectedAction); }); describe('when request is successful', () => { @@ -151,13 +163,13 @@ describe('JsonPatchOperationsService test suite', () => { scheduler.schedule(() => service.jsonPatchByResourceType(resourceEndpoint, resourceScope, testJsonPatchResourceType).subscribe()); scheduler.flush(); - expect((service as any).store.dispatch).toHaveBeenCalledWith(expectedAction); + expect(store.dispatch).toHaveBeenCalledWith(expectedAction); }); }); describe('when request is not successful', () => { beforeEach(() => { - store = new MockStore({} as CoreState); + store = TestBed.get(Store); responseCache = initMockResponseCacheService(false); requestService = getMockRequestService(); rdbService = getMockRemoteDataBuildService(); @@ -165,8 +177,8 @@ describe('JsonPatchOperationsService test suite', () => { halService = new HALEndpointServiceStub(resourceEndpointURL); service = initTestService(); - spyOn((service as any).store, 'select').and.returnValue(observableOf(mockState['json/patch'][testJsonPatchResourceType])); - spyOn((service as any).store, 'dispatch').and.callThrough(); + store.select.and.returnValue(observableOf(mockState['json/patch'][testJsonPatchResourceType])); + store.dispatch.and.callThrough(); }); it('should dispatch a new RollbacktPatchOperationsAction', () => { @@ -175,7 +187,7 @@ describe('JsonPatchOperationsService test suite', () => { scheduler.schedule(() => service.jsonPatchByResourceType(resourceEndpoint, resourceScope, testJsonPatchResourceType).subscribe()); scheduler.flush(); - expect((service as any).store.dispatch).toHaveBeenCalledWith(expectedAction); + expect(store.dispatch).toHaveBeenCalledWith(expectedAction); }); }); }); @@ -204,7 +216,7 @@ describe('JsonPatchOperationsService test suite', () => { scheduler.schedule(() => service.jsonPatchByResourceID(resourceEndpoint, resourceScope, testJsonPatchResourceType, testJsonPatchResourceId).subscribe()); scheduler.flush(); - expect((service as any).store.dispatch).toHaveBeenCalledWith(expectedAction); + expect(store.dispatch).toHaveBeenCalledWith(expectedAction); }); describe('when request is successful', () => { @@ -213,13 +225,13 @@ describe('JsonPatchOperationsService test suite', () => { scheduler.schedule(() => service.jsonPatchByResourceID(resourceEndpoint, resourceScope, testJsonPatchResourceType, testJsonPatchResourceId).subscribe()); scheduler.flush(); - expect((service as any).store.dispatch).toHaveBeenCalledWith(expectedAction); + expect(store.dispatch).toHaveBeenCalledWith(expectedAction); }); }); describe('when request is not successful', () => { beforeEach(() => { - store = new MockStore({} as CoreState); + store = TestBed.get(Store); responseCache = initMockResponseCacheService(false); requestService = getMockRequestService(); rdbService = getMockRemoteDataBuildService(); @@ -227,8 +239,8 @@ describe('JsonPatchOperationsService test suite', () => { halService = new HALEndpointServiceStub(resourceEndpointURL); service = initTestService(); - spyOn((service as any).store, 'select').and.returnValue(observableOf(mockState['json/patch'][testJsonPatchResourceType])); - spyOn((service as any).store, 'dispatch').and.callThrough(); + store.select.and.returnValue(observableOf(mockState['json/patch'][testJsonPatchResourceType])); + store.dispatch.and.callThrough(); }); it('should dispatch a new RollbacktPatchOperationsAction', () => { @@ -237,7 +249,7 @@ describe('JsonPatchOperationsService test suite', () => { scheduler.schedule(() => service.jsonPatchByResourceID(resourceEndpoint, resourceScope, testJsonPatchResourceType, testJsonPatchResourceId).subscribe()); scheduler.flush(); - expect((service as any).store.dispatch).toHaveBeenCalledWith(expectedAction); + expect(store.dispatch).toHaveBeenCalledWith(expectedAction); }); }); }); diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.component.spec.ts index 20547d6b5b..200cc08aae 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.component.spec.ts @@ -20,14 +20,11 @@ import { FormFieldMetadataValueObject } from '../../../models/form-field-metadat import { DsDynamicInputModel } from '../ds-dynamic-input.model'; import { createTestComponent } from '../../../../../testing/utils'; import { DynamicFormLayoutService, DynamicFormValidationService } from '@ng-dynamic-forms/core'; -import { MockStore } from '../../../../../testing/mock-store'; -import { Store } from '@ngrx/store'; -import { AppState } from '../../../../../../app.reducer'; -import { AuthService } from '../../../../../../core/auth/auth.service'; -import { AuthServiceStub } from '../../../../../testing/auth-service-stub'; import { AuthorityService } from '../../../../../../core/integration/authority.service'; import { AuthorityServiceStub } from '../../../../../testing/authority-service-stub'; import { MOCK_SUBMISSION_CONFIG } from '../../../../../testing/mock-submission-config'; +import { Store, StoreModule } from '@ngrx/store'; +import { MockStore } from '../../../../../testing/mock-store'; export let FORM_GROUP_TEST_MODEL_CONFIG; @@ -103,7 +100,7 @@ describe('DsDynamicRelationGroupComponent test suite', () => { // async beforeEach beforeEach(async(() => { init(); - const store = new MockStore(Object.create(null)); + /* TODO make sure these files use mocks instead of real services/components https://github.com/DSpace/dspace-angular/issues/281 */ TestBed.configureTestingModule({ imports: [ @@ -111,6 +108,7 @@ describe('DsDynamicRelationGroupComponent test suite', () => { FormsModule, ReactiveFormsModule, NgbModule.forRoot(), + StoreModule.forRoot({}), TranslateModule.forRoot() ], declarations: [ @@ -128,7 +126,7 @@ describe('DsDynamicRelationGroupComponent test suite', () => { FormService, { provide: AuthorityService, useValue: new AuthorityServiceStub() }, { provide: GLOBAL_CONFIG, useValue: config }, - { provide: Store, useValue: store }, + { provide: Store, useClass: MockStore } ], schemas: [CUSTOM_ELEMENTS_SCHEMA] }); diff --git a/src/app/shared/form/form.component.spec.ts b/src/app/shared/form/form.component.spec.ts index 06676d191e..69ef53e547 100644 --- a/src/app/shared/form/form.component.spec.ts +++ b/src/app/shared/form/form.component.spec.ts @@ -10,7 +10,7 @@ import { DynamicFormValidationService, DynamicInputModel } from '@ng-dynamic-forms/core'; -import { Store } from '@ngrx/store'; +import { Store, StoreModule } from '@ngrx/store'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { TranslateModule } from '@ngx-translate/core'; @@ -124,7 +124,6 @@ function init() { } }; - store = new MockStore(formState); } describe('FormComponent test suite', () => { @@ -144,6 +143,7 @@ describe('FormComponent test suite', () => { FormsModule, ReactiveFormsModule, NgbModule.forRoot(), + StoreModule.forRoot({}), TranslateModule.forRoot() ], declarations: [ @@ -157,9 +157,7 @@ describe('FormComponent test suite', () => { FormComponent, FormService, { provide: GLOBAL_CONFIG, useValue: config }, - { - provide: Store, useValue: store - } + { provide: Store, useClass: MockStore } ], schemas: [CUSTOM_ELEMENTS_SCHEMA] }); @@ -177,6 +175,7 @@ describe('FormComponent test suite', () => { testFixture = createTestComponent(html, TestComponent) as ComponentFixture; testComp = testFixture.componentInstance; + }); afterEach(() => { testFixture.destroy(); @@ -194,6 +193,7 @@ describe('FormComponent test suite', () => { beforeEach(() => { formFixture = TestBed.createComponent(FormComponent); + store = TestBed.get(Store); formComp = formFixture.componentInstance; // FormComponent test instance formComp.formId = 'testForm'; formComp.formModel = TEST_FORM_MODEL; @@ -384,6 +384,7 @@ describe('FormComponent test suite', () => { beforeEach(() => { formFixture = TestBed.createComponent(FormComponent); + store = TestBed.get(Store); formComp = formFixture.componentInstance; // FormComponent test instance formComp.formId = 'testFormArray'; formComp.formModel = TEST_FORM_MODEL_WITH_ARRAY; diff --git a/src/app/shared/mocks/mock-translate.service.ts b/src/app/shared/mocks/mock-translate.service.ts new file mode 100644 index 0000000000..1b103b92c8 --- /dev/null +++ b/src/app/shared/mocks/mock-translate.service.ts @@ -0,0 +1,7 @@ +import { TranslateService } from '@ngx-translate/core'; + +export function getMockTranslateService(): TranslateService { + return jasmine.createSpyObj('translateService', { + get: jasmine.createSpy('get') + }); +} diff --git a/src/app/shared/testing/mock-store.ts b/src/app/shared/testing/mock-store.ts index 5223852c59..a6093f6bcb 100644 --- a/src/app/shared/testing/mock-store.ts +++ b/src/app/shared/testing/mock-store.ts @@ -1,24 +1,21 @@ -import { map } from 'rxjs/operators'; -import { Action } from '@ngrx/store'; -import { BehaviorSubject, Observable } from 'rxjs'; +import { Injectable } from '@angular/core'; +import { ActionsSubject, ReducerManager, StateObservable, Store } from '@ngrx/store'; +import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject'; -export class MockStore extends BehaviorSubject { +@Injectable() +export class MockStore extends Store { + private stateSubject = new BehaviorSubject({} as T); - constructor(private _initialState: T) { - super(_initialState); + constructor( + state$: StateObservable, + actionsObserver: ActionsSubject, + reducerManager: ReducerManager + ) { + super(state$, actionsObserver, reducerManager); + this.source = this.stateSubject.asObservable(); } - dispatch = (action: Action): void => { - // console.info(action); - }; - - select = (pathOrMapFn: any): Observable => { - return this.asObservable().pipe( - map((value) => pathOrMapFn.projector(value))) - }; - - nextState(_newState: T) { - this.next(_newState); + nextState(nextState: T) { + this.stateSubject.next(nextState); } - } diff --git a/src/app/submission/objects/submission-objects.effects.spec.ts b/src/app/submission/objects/submission-objects.effects.spec.ts index c0f20c1e9f..80b85de4e4 100644 --- a/src/app/submission/objects/submission-objects.effects.spec.ts +++ b/src/app/submission/objects/submission-objects.effects.spec.ts @@ -2,7 +2,7 @@ import { TestBed } from '@angular/core/testing'; import { cold, hot } from 'jasmine-marbles'; import { provideMockActions } from '@ngrx/effects/testing'; -import { Store } from '@ngrx/store'; +import { Store, StoreModule } from '@ngrx/store'; import { Observable, of as observableOf, throwError as observableThrowError } from 'rxjs'; import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core'; @@ -65,6 +65,7 @@ describe('SubmissionObjectEffects test suite', () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [ + StoreModule.forRoot({}), TranslateModule.forRoot({ loader: { provide: TranslateLoader, @@ -75,16 +76,17 @@ describe('SubmissionObjectEffects test suite', () => { providers: [ SubmissionObjectEffects, TranslateService, - {provide: Store, useValue: new MockStore({})}, + { provide: Store, useClass: MockStore }, provideMockActions(() => actions), - {provide: NotificationsService, useValue: notificationsServiceStub}, - {provide: SectionsService, useClass: SectionsServiceStub}, - {provide: SubmissionService, useValue: submissionServiceStub}, - {provide: SubmissionJsonPatchOperationsService, useValue: submissionJsonPatchOperationsServiceStub}, + { provide: NotificationsService, useValue: notificationsServiceStub }, + { provide: SectionsService, useClass: SectionsServiceStub }, + { provide: SubmissionService, useValue: submissionServiceStub }, + { provide: SubmissionJsonPatchOperationsService, useValue: submissionJsonPatchOperationsServiceStub }, ], }); submissionObjectEffects = TestBed.get(SubmissionObjectEffects); + store = TestBed.get(Store); }); describe('loadForm$', () => { @@ -263,7 +265,6 @@ describe('SubmissionObjectEffects test suite', () => { describe('saveSubmissionSuccess$', () => { it('should return a UPLOAD_SECTION_DATA action for each updated section', () => { - store = TestBed.get(Store); store.nextState({ submission: { objects: mockSubmissionState @@ -312,7 +313,6 @@ describe('SubmissionObjectEffects test suite', () => { }); it('should display a success notification', () => { - store = TestBed.get(Store); store.nextState({ submission: { objects: mockSubmissionState @@ -358,7 +358,6 @@ describe('SubmissionObjectEffects test suite', () => { }); it('should display a warning notification when there are errors', () => { - store = TestBed.get(Store); store.nextState({ submission: { objects: mockSubmissionState @@ -406,7 +405,6 @@ describe('SubmissionObjectEffects test suite', () => { }); it('should detect and notify a new section', () => { - store = TestBed.get(Store); store.nextState({ submission: { objects: mockSubmissionState @@ -534,7 +532,6 @@ describe('SubmissionObjectEffects test suite', () => { }); it('should not allow to deposit when there are errors', () => { - store = TestBed.get(Store); store.nextState({ submission: { objects: mockSubmissionState @@ -609,7 +606,6 @@ describe('SubmissionObjectEffects test suite', () => { describe('depositSubmission$', () => { it('should return a DEPOSIT_SUBMISSION_SUCCESS action on success', () => { - store = TestBed.get(Store); store.nextState({ submission: { objects: mockSubmissionState @@ -636,7 +632,6 @@ describe('SubmissionObjectEffects test suite', () => { }); it('should return a DEPOSIT_SUBMISSION_ERROR action on error', () => { - store = TestBed.get(Store); store.nextState({ submission: { objects: mockSubmissionState @@ -721,7 +716,6 @@ describe('SubmissionObjectEffects test suite', () => { describe('discardSubmission$', () => { it('should return a DISCARD_SUBMISSION_SUCCESS action on success', () => { - store = TestBed.get(Store); store.nextState({ submission: { objects: mockSubmissionState @@ -748,7 +742,6 @@ describe('SubmissionObjectEffects test suite', () => { }); it('should return a DISCARD_SUBMISSION_ERROR action on error', () => { - store = TestBed.get(Store); store.nextState({ submission: { objects: mockSubmissionState diff --git a/src/app/submission/sections/sections.directive.ts b/src/app/submission/sections/sections.directive.ts index 3ad8237ce4..54d28b4d5f 100644 --- a/src/app/submission/sections/sections.directive.ts +++ b/src/app/submission/sections/sections.directive.ts @@ -69,7 +69,7 @@ export class SectionsDirective implements OnDestroy, OnInit { this.changeDetectorRef.detectChanges(); // If section is no longer active dispatch save action if (!this.active && isNotNull(activeSectionId)) { - this.submissionService.dispatchSaveSection(this.submissionId, this.sectionId); + this.submissionService.dispatchSave(this.submissionId); } } }) @@ -89,7 +89,7 @@ export class SectionsDirective implements OnDestroy, OnInit { } public isOpen() { - return (this.sectionState) ? true : false; + return this.sectionState; } public isMandatory() { diff --git a/src/app/submission/sections/sections.service.spec.ts b/src/app/submission/sections/sections.service.spec.ts new file mode 100644 index 0000000000..ba007c6592 --- /dev/null +++ b/src/app/submission/sections/sections.service.spec.ts @@ -0,0 +1,378 @@ +import { async, TestBed } from '@angular/core/testing'; + +import { cold, getTestScheduler } from 'jasmine-marbles'; +import { of as observableOf } from 'rxjs'; +import { Store, StoreModule } from '@ngrx/store'; +import { ScrollToService } from '@nicky-lenaers/ngx-scroll-to'; +import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core'; + +import { submissionReducers } from '../submission.reducers'; +import { MockTranslateLoader } from '../../shared/mocks/mock-translate-loader'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { SubmissionService } from '../submission.service'; +import { NotificationsServiceStub } from '../../shared/testing/notifications-service-stub'; +import { SubmissionServiceStub } from '../../shared/testing/submission-service-stub'; +import { getMockTranslateService } from '../../shared/mocks/mock-translate.service'; +import { SectionsService } from './sections.service'; +import { mockSectionsData, mockSectionsErrors, mockSubmissionState } from '../../shared/mocks/mock-submission'; +import { + DisableSectionAction, + EnableSectionAction, + InertSectionErrorsAction, + RemoveSectionErrorsAction, + SectionStatusChangeAction, + UpdateSectionDataAction +} from '../objects/submission-objects.actions'; +import { FormAddError, FormClearErrorsAction, FormRemoveErrorAction } from '../../shared/form/form.actions'; +import parseSectionErrors from '../utils/parseSectionErrors'; +import { SubmissionScopeType } from '../../core/submission/submission-scope-type'; +import { SubmissionSectionError } from '../objects/submission-objects.reducer'; + +describe('SectionsService test suite', () => { + let notificationsServiceStub: NotificationsServiceStub; + let scrollToService: ScrollToService; + let service: SectionsService; + let submissionServiceStub: SubmissionServiceStub; + let translateService: any; + + const formId = 'formTest'; + const submissionId = '826'; + const sectionId = 'traditionalpageone'; + const sectionErrors: any = parseSectionErrors(mockSectionsErrors); + const sectionData: any = mockSectionsData; + const sectionState: any = mockSubmissionState['826'].sections[sectionId]; + + const store: any = jasmine.createSpyObj('store', { + dispatch: jasmine.createSpy('dispatch'), + select: jasmine.createSpy('select') + }); + + function getMockScrollToService(): ScrollToService { + return jasmine.createSpyObj('scrollToService', { + scrollTo: jasmine.createSpy('scrollTo') + }); + } + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + StoreModule.forRoot({submissionReducers} as any), + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: MockTranslateLoader + } + }) + ], + providers: [ + { provide: NotificationsService, useClass: NotificationsServiceStub }, + { provide: ScrollToService, useValue: getMockScrollToService() }, + { provide: SubmissionService, useClass: SubmissionServiceStub }, + { provide: TranslateService, useValue: getMockTranslateService() }, + { provide: Store, useValue: store }, + SectionsService + ] + }).compileComponents(); + })); + + beforeEach(() => { + service = TestBed.get(SectionsService); + submissionServiceStub = TestBed.get(SubmissionService); + notificationsServiceStub = TestBed.get(NotificationsService); + scrollToService = TestBed.get(ScrollToService); + translateService = TestBed.get(TranslateService); + }); + + describe('checkSectionErrors', () => { + it('should dispatch a new RemoveSectionErrorsAction and FormClearErrorsAction when there are no errors', () => { + service.checkSectionErrors(submissionId, sectionId, formId, []); + + expect(store.dispatch).toHaveBeenCalledWith(new RemoveSectionErrorsAction(submissionId, sectionId)); + expect(store.dispatch).toHaveBeenCalledWith(new FormClearErrorsAction(formId)); + }); + + it('should dispatch a new FormAddError for each section\'s error', () => { + service.checkSectionErrors(submissionId, sectionId, formId, sectionErrors[sectionId]); + + expect(store.dispatch).toHaveBeenCalledWith(new FormAddError( + formId, + 'dc_contributor_author', + 0, + 'error.validation.required')); + + expect(store.dispatch).toHaveBeenCalledWith(new FormAddError( + formId, + 'dc_title', + 0, + 'error.validation.required')); + + expect(store.dispatch).toHaveBeenCalledWith(new FormAddError(formId, + 'dc_date_issued', + 0, + 'error.validation.required')); + }); + + it('should dispatch a new FormRemoveErrorAction for each section\'s error that no longer exists', () => { + const currentErrors = Array.of(...sectionErrors[sectionId]); + const prevErrors = Array.of(...sectionErrors[sectionId]); + currentErrors.pop(); + + service.checkSectionErrors(submissionId, sectionId, formId, currentErrors, prevErrors); + + expect(store.dispatch).toHaveBeenCalledWith(new FormAddError( + formId, + 'dc_contributor_author', + 0, + 'error.validation.required')); + + expect(store.dispatch).toHaveBeenCalledWith(new FormAddError( + formId, + 'dc_title', + 0, + 'error.validation.required')); + expect(store.dispatch).toHaveBeenCalledWith(new FormRemoveErrorAction( + formId, + 'dc_date_issued', + 0)); + }); + }); + + describe('dispatchRemoveSectionErrors', () => { + it('should dispatch a new RemoveSectionErrorsAction', () => { + service.dispatchRemoveSectionErrors(submissionId, sectionId); + const expected = new RemoveSectionErrorsAction(submissionId, sectionId); + + expect(store.dispatch).toHaveBeenCalledWith(expected); + }); + }); + + describe('getSectionData', () => { + it('should return an observable with section\'s data', () => { + store.select.and.returnValue(observableOf(sectionData[sectionId])); + + const expected = cold('(b|)', { + b: sectionData[sectionId] + }); + + expect(service.getSectionData(submissionId, sectionId)).toBeObservable(expected); + }); + }); + + describe('getSectionErrors', () => { + it('should return an observable with section\'s errors', () => { + store.select.and.returnValue(observableOf(sectionErrors[sectionId])); + + const expected = cold('(b|)', { + b: sectionErrors[sectionId] + }); + + expect(service.getSectionErrors(submissionId, sectionId)).toBeObservable(expected); + }); + }); + + describe('getSectionState', () => { + it('should return an observable with section\'s state', () => { + store.select.and.returnValue(observableOf(sectionState)); + + const expected = cold('(b|)', { + b: sectionState + }); + + expect(service.getSectionState(submissionId, sectionId)).toBeObservable(expected); + }); + }); + + describe('isSectionValid', () => { + it('should return an observable of boolean', () => { + store.select.and.returnValue(observableOf({isValid: false})); + + let expected = cold('(b|)', { + b: false + }); + + expect(service.isSectionValid(submissionId, sectionId)).toBeObservable(expected); + + store.select.and.returnValue(observableOf({isValid: true})); + + expected = cold('(b|)', { + b: true + }); + + expect(service.isSectionValid(submissionId, sectionId)).toBeObservable(expected); + }); + }); + + describe('isSectionActive', () => { + it('should return an observable of boolean', () => { + submissionServiceStub.getActiveSectionId.and.returnValue(observableOf(sectionId)); + + let expected = cold('(b|)', { + b: true + }); + + expect(service.isSectionActive(submissionId, sectionId)).toBeObservable(expected); + + submissionServiceStub.getActiveSectionId.and.returnValue(observableOf('test')); + + expected = cold('(b|)', { + b: false + }); + + expect(service.isSectionActive(submissionId, sectionId)).toBeObservable(expected); + }); + }); + + describe('isSectionEnabled', () => { + it('should return an observable of boolean', () => { + store.select.and.returnValue(observableOf({enabled: false})); + + let expected = cold('(b|)', { + b: false + }); + + expect(service.isSectionEnabled(submissionId, sectionId)).toBeObservable(expected); + + store.select.and.returnValue(observableOf({enabled: true})); + + expected = cold('(b|)', { + b: true + }); + + expect(service.isSectionEnabled(submissionId, sectionId)).toBeObservable(expected); + }); + }); + + describe('isSectionReadOnly', () => { + it('should return an observable of true when it\'s a readonly section and scope is not workspace', () => { + store.select.and.returnValue(observableOf({ + visibility: { + main: null, + other: 'READONLY' + } + })); + + const expected = cold('(b|)', { + b: true + }); + + expect(service.isSectionReadOnly(submissionId, sectionId, SubmissionScopeType.WorkflowItem)).toBeObservable(expected); + }); + + it('should return an observable of false when it\'s a readonly section and scope is workspace', () => { + store.select.and.returnValue(observableOf({ + visibility: { + main: null, + other: 'READONLY' + } + })); + + const expected = cold('(b|)', { + b: false + }); + + expect(service.isSectionReadOnly(submissionId, sectionId, SubmissionScopeType.WorkspaceItem)).toBeObservable(expected); + }); + + it('should return an observable of false when it\'s not a readonly section', () => { + store.select.and.returnValue(observableOf({ + visibility: null + })); + + const expected = cold('(b|)', { + b: false + }); + + expect(service.isSectionReadOnly(submissionId, sectionId, SubmissionScopeType.WorkflowItem)).toBeObservable(expected); + }); + }); + + describe('isSectionAvailable', () => { + it('should return an observable of true when section is available', () => { + store.select.and.returnValue(observableOf(mockSubmissionState[submissionId])); + + const expected = cold('(b|)', { + b: true + }); + + expect(service.isSectionAvailable(submissionId, sectionId)).toBeObservable(expected); + }); + + it('should return an observable of false when section is not available', () => { + store.select.and.returnValue(observableOf(mockSubmissionState[submissionId])); + + const expected = cold('(b|)', { + b: false + }); + + expect(service.isSectionAvailable(submissionId, 'test')).toBeObservable(expected); + }); + }); + + describe('addSection', () => { + it('should dispatch a new EnableSectionAction a move target to new section', () => { + + service.addSection(submissionId, 'newSection'); + + expect(store.dispatch).toHaveBeenCalledWith(new EnableSectionAction(submissionId, 'newSection')); + expect(scrollToService.scrollTo).toHaveBeenCalled(); + }); + }); + + describe('removeSection', () => { + it('should dispatch a new DisableSectionAction', () => { + + service.removeSection(submissionId, 'newSection'); + + expect(store.dispatch).toHaveBeenCalledWith(new DisableSectionAction(submissionId, 'newSection')); + }); + }); + + describe('setSectionError', () => { + it('should dispatch a new InertSectionErrorsAction', () => { + + const error: SubmissionSectionError = { + path: 'test', + message: 'message test' + }; + service.setSectionError(submissionId, sectionId, error); + + expect(store.dispatch).toHaveBeenCalledWith(new InertSectionErrorsAction(submissionId, sectionId, error)); + }); + }); + + describe('setSectionStatus', () => { + it('should dispatch a new SectionStatusChangeAction', () => { + + service.setSectionStatus(submissionId, sectionId, true); + + expect(store.dispatch).toHaveBeenCalledWith(new SectionStatusChangeAction(submissionId, sectionId, true)); + }); + }); + + describe('updateSectionData', () => { + + it('should dispatch a new UpdateSectionDataAction', () => { + const scheduler = getTestScheduler(); + const data: any = {test: 'test'}; + spyOn(service, 'isSectionAvailable').and.returnValue(observableOf(true)); + spyOn(service, 'isSectionEnabled').and.returnValue(observableOf(true)); + scheduler.schedule(() => service.updateSectionData(submissionId, sectionId, data, [])); + scheduler.flush(); + + expect(store.dispatch).toHaveBeenCalledWith(new UpdateSectionDataAction(submissionId, sectionId, data, [])); + }); + + it('should dispatch a new UpdateSectionDataAction and display a new notification when section is not enabled', () => { + const scheduler = getTestScheduler(); + const data: any = {test: 'test'}; + spyOn(service, 'isSectionAvailable').and.returnValue(observableOf(true)); + spyOn(service, 'isSectionEnabled').and.returnValue(observableOf(false)); + translateService.get.and.returnValue(observableOf('test')); + scheduler.schedule(() => service.updateSectionData(submissionId, sectionId, data, [])); + scheduler.flush(); + + expect(notificationsServiceStub.info).toHaveBeenCalled(); + expect(store.dispatch).toHaveBeenCalledWith(new UpdateSectionDataAction(submissionId, sectionId, data, [])); + }); + }); +}); diff --git a/src/app/submission/sections/sections.service.ts b/src/app/submission/sections/sections.service.ts index e1a92cc07e..fb90e37bfc 100644 --- a/src/app/submission/sections/sections.service.ts +++ b/src/app/submission/sections/sections.service.ts @@ -133,7 +133,9 @@ export class SectionsService { return this.store.select(submissionSectionFromIdSelector(submissionId, sectionId)).pipe( filter((sectionObj) => hasValue(sectionObj)), map((sectionObj: SubmissionSectionObject) => { - return sectionObj.visibility.other === 'READONLY' && submissionScope !== SubmissionScopeType.WorkspaceItem + return isNotEmpty(sectionObj.visibility) + && sectionObj.visibility.other === 'READONLY' + && submissionScope !== SubmissionScopeType.WorkspaceItem }), distinctUntilChanged()); } diff --git a/src/app/submission/sections/upload/file/file.component.html b/src/app/submission/sections/upload/file/file.component.html index 5db4843bf7..41768a88aa 100644 --- a/src/app/submission/sections/upload/file/file.component.html +++ b/src/app/submission/sections/upload/file/file.component.html @@ -12,7 +12,7 @@ - + diff --git a/src/app/submission/submission-rest.service.spec.ts b/src/app/submission/submission-rest.service.spec.ts new file mode 100644 index 0000000000..0759e7cfba --- /dev/null +++ b/src/app/submission/submission-rest.service.spec.ts @@ -0,0 +1,102 @@ +import { TestScheduler } from 'rxjs/testing'; +import { cold, getTestScheduler } from 'jasmine-marbles'; + +import { SubmissionRestService } from './submission-rest.service'; +import { ResponseCacheService } from '../core/cache/response-cache.service'; +import { RequestService } from '../core/data/request.service'; +import { RemoteDataBuildService } from '../core/cache/builders/remote-data-build.service'; +import { getMockRequestService } from '../shared/mocks/mock-request.service'; +import { getMockRemoteDataBuildService } from '../shared/mocks/mock-remote-data-build.service'; +import { HALEndpointServiceStub } from '../shared/testing/hal-endpoint-service-stub'; +import { + SubmissionDeleteRequest, + SubmissionPatchRequest, + SubmissionPostRequest, + SubmissionRequest +} from '../core/data/request.models'; +import { FormFieldMetadataValueObject } from '../shared/form/builder/models/form-field-metadata-value.model'; + +describe('SubmissionRestService test suite', () => { + let scheduler: TestScheduler; + let service: SubmissionRestService; + let responseCache: ResponseCacheService; + let requestService: RequestService; + let rdbService: RemoteDataBuildService; + let halService: any; + + const resourceEndpointURL = 'https://rest.api/endpoint'; + const resourceEndpoint = 'workspaceitems'; + const resourceScope = '260'; + const body = { test: new FormFieldMetadataValueObject('test')}; + const resourceHref = resourceEndpointURL + '/' + resourceEndpoint + '/' + resourceScope; + const timestampResponse = 1545994811992; + + function initMockResponseCacheService(isSuccessful: boolean): ResponseCacheService { + return jasmine.createSpyObj('responseCache', { + get: cold('c-', { + c: {response: {isSuccessful}, + timeAdded: timestampResponse} + }), + remove: jasmine.createSpy('remove') + }); + } + + function initTestService() { + return new SubmissionRestService( + rdbService, + responseCache, + requestService, + halService + ); + } + + beforeEach(() => { + responseCache = initMockResponseCacheService(true); + requestService = getMockRequestService(); + rdbService = getMockRemoteDataBuildService(); + scheduler = getTestScheduler(); + halService = new HALEndpointServiceStub(resourceEndpointURL); + service = initTestService(); + + }); + + describe('deleteById', () => { + it('should configure a new SubmissionDeleteRequest', () => { + const expected = new SubmissionDeleteRequest(requestService.generateRequestId(), resourceHref); + scheduler.schedule(() => service.deleteById(resourceScope, resourceEndpoint).subscribe()); + scheduler.flush(); + + expect(requestService.configure).toHaveBeenCalledWith(expected); + }); + }); + + describe('getDataById', () => { + it('should configure a new SubmissionRequest', () => { + const expected = new SubmissionRequest(requestService.generateRequestId(), resourceHref); + scheduler.schedule(() => service.getDataById(resourceEndpoint, resourceScope).subscribe()); + scheduler.flush(); + + expect(requestService.configure).toHaveBeenCalledWith(expected, true); + }); + }); + + describe('postToEndpoint', () => { + it('should configure a new SubmissionPostRequest', () => { + const expected = new SubmissionPostRequest(requestService.generateRequestId(), resourceHref, body); + scheduler.schedule(() => service.postToEndpoint(resourceEndpoint, body, resourceScope).subscribe()); + scheduler.flush(); + + expect(requestService.configure).toHaveBeenCalledWith(expected, true); + }); + }); + + describe('patchToEndpoint', () => { + it('should configure a new SubmissionPatchRequest', () => { + const expected = new SubmissionPatchRequest(requestService.generateRequestId(), resourceHref, body); + scheduler.schedule(() => service.patchToEndpoint(resourceEndpoint, body, resourceScope).subscribe()); + scheduler.flush(); + + expect(requestService.configure).toHaveBeenCalledWith(expected, true); + }); + }); +}); diff --git a/src/app/submission/submission-rest.service.ts b/src/app/submission/submission-rest.service.ts index 469ea80ac9..b364611ae8 100644 --- a/src/app/submission/submission-rest.service.ts +++ b/src/app/submission/submission-rest.service.ts @@ -2,7 +2,6 @@ import { Injectable } from '@angular/core'; import { merge as observableMerge, Observable, throwError as observableThrowError } from 'rxjs'; import { distinctUntilChanged, filter, flatMap, map, mergeMap, tap } from 'rxjs/operators'; -import { Store } from '@ngrx/store'; import { ResponseCacheService } from '../core/cache/response-cache.service'; import { RequestService } from '../core/data/request.service'; @@ -10,7 +9,6 @@ import { ResponseCacheEntry } from '../core/cache/response-cache.reducer'; import { SubmissionSuccessResponse } from '../core/cache/response-cache.models'; import { isNotEmpty } from '../shared/empty.util'; import { - ConfigRequest, DeleteRequest, PostRequest, RestRequest, @@ -20,7 +18,6 @@ import { SubmissionRequest } from '../core/data/request.models'; import { SubmitDataResponseDefinitionObject } from '../core/shared/submit-data-response-definition.model'; -import { CoreState } from '../core/core.reducers'; import { HttpOptions } from '../core/dspace-rest-v2/dspace-rest-v2.service'; import { HALEndpointService } from '../core/shared/hal-endpoint.service'; import { RemoteDataBuildService } from '../core/cache/builders/remote-data-build.service'; @@ -33,7 +30,6 @@ export class SubmissionRestService { protected rdbService: RemoteDataBuildService, protected responseCache: ResponseCacheService, protected requestService: RequestService, - protected store: Store, protected halService: HALEndpointService) { } @@ -104,13 +100,6 @@ export class SubmissionRestService { distinctUntilChanged()); } - public getDataByHref(href: string, options?: HttpOptions): Observable { - const request = new ConfigRequest(this.requestService.generateRequestId(), href, options); - this.requestService.configure(request, true); - - return this.fetchRequest(request); - } - public getDataById(linkName: string, id: string): Observable { return this.halService.getEndpoint(linkName).pipe( map((endpointURL: string) => this.getEndpointByIDHref(endpointURL, id)), diff --git a/src/app/submission/submission.service.spec.ts b/src/app/submission/submission.service.spec.ts index 5ba633b7a8..87e66f87a3 100644 --- a/src/app/submission/submission.service.spec.ts +++ b/src/app/submission/submission.service.spec.ts @@ -1,9 +1,9 @@ import { StoreModule } from '@ngrx/store'; -import { async, fakeAsync, TestBed, tick } from '@angular/core/testing'; +import { async, fakeAsync, flush, TestBed, tick } from '@angular/core/testing'; import { ActivatedRoute, Router } from '@angular/router'; import { HttpHeaders } from '@angular/common/http'; -import { ScrollToService } from '@nicky-lenaers/ngx-scroll-to'; +import { of as observableOf } from 'rxjs'; import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core'; import { cold, hot, } from 'jasmine-marbles'; @@ -17,14 +17,27 @@ import { MockActivatedRoute } from '../shared/mocks/mock-active-router'; import { GLOBAL_CONFIG } from '../../config'; import { HttpOptions } from '../core/dspace-rest-v2/dspace-rest-v2.service'; import { SubmissionScopeType } from '../core/submission/submission-scope-type'; -import { submissionRestResponse } from '../shared/mocks/mock-submission'; +import { mockSubmissionDefinition, submissionRestResponse } from '../shared/mocks/mock-submission'; import { NotificationsService } from '../shared/notifications/notifications.service'; import { MockTranslateLoader } from '../shared/mocks/mock-translate-loader'; import { MOCK_SUBMISSION_CONFIG } from '../shared/testing/mock-submission-config'; +import { + CancelSubmissionFormAction, + ChangeSubmissionCollectionAction, + DiscardSubmissionAction, + InitSubmissionFormAction, + ResetSubmissionFormAction, + SaveAndDepositSubmissionAction, + SaveForLaterSubmissionFormAction, + SaveSubmissionFormAction, SaveSubmissionSectionFormAction, + SetActiveSectionAction +} from './objects/submission-objects.actions'; describe('SubmissionService test suite', () => { const config = MOCK_SUBMISSION_CONFIG; - + const collectionId = '43fe1f8c-09a6-4fcf-9c78-5d4fed8f2c8f'; + const submissionId = '826'; + const sectionId = 'test'; const subState = { objects: { 826: { @@ -317,13 +330,15 @@ describe('SubmissionService test suite', () => { }; const restService = new SubmissionRestServiceStub(); const router = new MockRouter(); + const selfUrl = 'https://rest.api/dspace-spring-rest/api/submission/workspaceitems/826'; + const submissionDefinition: any = mockSubmissionDefinition; let service: SubmissionService; beforeEach(async(() => { TestBed.configureTestingModule({ imports: [ - StoreModule.forRoot({ submissionReducers } as any), + StoreModule.forRoot({submissionReducers} as any), TranslateModule.forRoot({ loader: { provide: TranslateLoader, @@ -332,13 +347,12 @@ describe('SubmissionService test suite', () => { }) ], providers: [ - { provide: GLOBAL_CONFIG, useValue: config }, - { provide: Router, useValue: router }, - { provide: SubmissionRestService, useValue: restService }, - { provide: ActivatedRoute, useValue: new MockActivatedRoute() }, + {provide: GLOBAL_CONFIG, useValue: config}, + {provide: Router, useValue: router}, + {provide: SubmissionRestService, useValue: restService}, + {provide: ActivatedRoute, useValue: new MockActivatedRoute()}, NotificationsService, RouteService, - ScrollToService, SubmissionService, TranslateService ] @@ -347,334 +361,514 @@ describe('SubmissionService test suite', () => { beforeEach(() => { service = TestBed.get(SubmissionService); + spyOn((service as any).store, 'dispatch').and.callThrough() }); - it('should create a new submission', () => { - service.createSubmission(); + describe('changeSubmissionCollection', () => { + it('should dispatch a new ChangeSubmissionCollectionAction', () => { + service.changeSubmissionCollection(submissionId, collectionId); + const expected = new ChangeSubmissionCollectionAction(submissionId, collectionId); - expect((service as any).restService.postToEndpoint).toHaveBeenCalled(); - }); - - it('should deposit submission', () => { - - const selfUrl = 'https://rest.api/dspace-spring-rest/api/submission/workspaceitems/826'; - const options: HttpOptions = Object.create({}); - let headers = new HttpHeaders(); - headers = headers.append('Content-Type', 'text/uri-list'); - options.headers = headers; - - service.depositSubmission(selfUrl); - - expect((service as any).restService.postToEndpoint).toHaveBeenCalledWith('workflowitems', selfUrl, null, options); - }); - - it('should discard submission', () => { - service.discardSubmission('826'); - - expect((service as any).restService.deleteById).toHaveBeenCalledWith('826'); - }); - - it('should return submission object state from the store', () => { - spyOn((service as any).store, 'select').and.returnValue(hot('a', { - a: subState.objects[826] - })); - - const result = service.getSubmissionObject('826'); - const expected = cold('b', { b: subState.objects[826] }); - - expect(result).toBeObservable(expected); - }); - - it('should return current active submission form section', () => { - spyOn((service as any).store, 'select').and.returnValue(hot('a', { - a: subState.objects[826] - })); - - const result = service.getActiveSectionId('826'); - const expected = cold('b', { b: 'keyinformation' }); - - expect(result).toBeObservable(expected); - - }); - - it('should return submission form sections', () => { - spyOn((service as any).store, 'select').and.returnValue(hot('a|', { - a: subState.objects[826] - })); - - const result = service.getSubmissionSections('826'); - const expected = cold('(bc|)', { - b: [], - c: - [ - { - header: 'submit.progressbar.describe.keyinformation', - id: 'keyinformation', - config: 'https://rest.api/dspace-spring-rest/api/config/submissionforms/keyinformation', - mandatory: true, - sectionType: 'submission-form', - data: {}, - errors: [] - }, - { - header: 'submit.progressbar.describe.indexing', - id: 'indexing', - config: 'https://rest.api/dspace-spring-rest/api/config/submissionforms/indexing', - mandatory: false, - sectionType: 'submission-form', - data: {}, - errors: [] - }, - { - header: 'submit.progressbar.describe.publicationchannel', - id: 'publicationchannel', - config: 'https://rest.api/dspace-spring-rest/api/config/submissionforms/publicationchannel', - mandatory: true, - sectionType: 'submission-form', - data: {}, - errors: [] - }, - { - header: 'submit.progressbar.describe.acknowledgement', - id: 'acknowledgement', - config: 'https://rest.api/dspace-spring-rest/api/config/submissionforms/acknowledgement', - mandatory: false, - sectionType: 'submission-form', - data: {}, - errors: [] - }, - { - header: 'submit.progressbar.describe.identifiers', - id: 'identifiers', - config: 'https://rest.api/dspace-spring-rest/api/config/submissionforms/identifiers', - mandatory: false, - sectionType: 'submission-form', - data: {}, - errors: [] - }, - { - header: 'submit.progressbar.describe.references', - id: 'references', - config: 'https://rest.api/dspace-spring-rest/api/config/submissionforms/references', - mandatory: false, - sectionType: 'submission-form', - data: {}, - errors: [] - }, - { - header: 'submit.progressbar.upload', - id: 'upload', - config: 'https://rest.api/dspace-spring-rest/api/config/submissionuploads/upload', - mandatory: true, - sectionType: 'upload', - data: {}, - errors: [] - }, - { - header: 'submit.progressbar.license', - id: 'license', - config: '', - mandatory: true, - sectionType: 'license', - data: {}, - errors: [] - } - ] + expect((service as any).store.dispatch).toHaveBeenCalledWith(expected); }); - - expect(result).toBeObservable(expected); }); - it('should return list of submission disabled sections', () => { - spyOn((service as any).store, 'select').and.returnValue(hot('-a|', { - a: subState.objects[826] - })); + describe('createSubmission', () => { + it('should create a new submission', () => { + service.createSubmission(); - const result = service.getDisabledSectionsList('826'); - const expected = cold('bc|', { - b: [], - c: - [ - { - header: 'submit.progressbar.describe.indexing', - id: 'indexing', - }, - { - header: 'submit.progressbar.describe.acknowledgement', - id: 'acknowledgement', - }, - { - header: 'submit.progressbar.describe.identifiers', - id: 'identifiers', - }, - { - header: 'submit.progressbar.describe.references', - id: 'references', - } - ] + expect((service as any).restService.postToEndpoint).toHaveBeenCalled(); }); - - expect(result).toBeObservable(expected); }); - it('should return true/false when section is hidden/visible', () => { - let section: any = { - config: '', - header: '', - mandatory: true, - sectionType: 'collection' as any, - visibility: { - main: 'HIDDEN', - other: 'HIDDEN' - }, - collapsed: false, - enabled: true, - data: {}, - errors: [], - isLoading: false, - isValid: false - }; - expect(service.isSectionHidden(section)).toBeTruthy(); + describe('depositSubmission', () => { + it('should deposit submission', () => { + const options: HttpOptions = Object.create({}); + let headers = new HttpHeaders(); + headers = headers.append('Content-Type', 'text/uri-list'); + options.headers = headers; - section = { - header: 'submit.progressbar.describe.keyinformation', - config: 'https://rest.api/dspace-spring-rest/api/config/submissionforms/keyinformation', - mandatory: true, - sectionType: 'submission-form', - collapsed: false, - enabled: true, - data: {}, - errors: [], - isLoading: false, - isValid: false - }; - expect(service.isSectionHidden(section)).toBeFalsy(); - }); + service.depositSubmission(selfUrl); - it('should return properly submission link name', () => { - let expected = 'workspaceitems'; - router.setRoute('/workspaceitems/826/edit'); - expect(service.getSubmissionObjectLinkName()).toBe(expected); - - expected = 'workspaceitems'; - router.setRoute('/submit'); - expect(service.getSubmissionObjectLinkName()).toBe(expected); - - expected = 'workflowitems'; - router.setRoute('/workflowitems/826/edit'); - expect(service.getSubmissionObjectLinkName()).toBe(expected); - - expected = 'edititems'; - router.setRoute('/items/9e79b1f2-ae0f-4737-9a4b-990952a8857c/edit'); - expect(service.getSubmissionObjectLinkName()).toBe(expected); - }); - - it('should return properly submission scope', () => { - let expected = SubmissionScopeType.WorkspaceItem; - - router.setRoute('/workspaceitems/826/edit'); - expect(service.getSubmissionScope()).toBe(expected); - - router.setRoute('/submit'); - expect(service.getSubmissionScope()).toBe(expected); - - expected = SubmissionScopeType.WorkflowItem; - router.setRoute('/workflowitems/826/edit'); - expect(service.getSubmissionScope()).toBe(expected); - - expected = SubmissionScopeType.EditItem; - router.setRoute('/items/9e79b1f2-ae0f-4737-9a4b-990952a8857c/edit'); - expect(service.getSubmissionScope()).toBe(expected); - }); - - it('should return properly submission status', () => { - spyOn((service as any).store, 'select').and.returnValue(hot('-a-b', { - a: subState, - b: validSubState - })); - const result = service.getSubmissionStatus('826'); - const expected = cold('cc-d', { - c: false, - d: true + expect((service as any).restService.postToEndpoint).toHaveBeenCalledWith('workflowitems', selfUrl, null, options); }); - - expect(result).toBeObservable(expected); }); - it('should return submission save processing status', () => { - spyOn((service as any).store, 'select').and.returnValue(hot('-a', { - a: subState.objects[826] - })); + describe('discardSubmission', () => { + it('should discard submission', () => { + service.discardSubmission('826'); - const result = service.getSubmissionSaveProcessingStatus('826'); - const expected = cold('bb', { - b: false + expect((service as any).restService.deleteById).toHaveBeenCalledWith('826'); }); - - expect(result).toBeObservable(expected); }); - it('should return submission deposit processing status', () => { - spyOn((service as any).store, 'select').and.returnValue(hot('-a', { - a: subState.objects[826] - })); + describe('dispatchInit', () => { + it('should dispatch a new InitSubmissionFormAction', () => { + service.dispatchInit( + collectionId, + submissionId, + selfUrl, + submissionDefinition, + {}, + [] + ); + const expected = new InitSubmissionFormAction( + collectionId, + submissionId, + selfUrl, + submissionDefinition, + {}, + []); - const result = service.getSubmissionDepositProcessingStatus('826'); - const expected = cold('bb', { - b: false + expect((service as any).store.dispatch).toHaveBeenCalledWith(expected); }); - - expect(result).toBeObservable(expected); }); - it('should redirect to MyDspace page', () => { - const spy = spyOn((service as any).routeService, 'getPreviousUrl'); + describe('dispatchDeposit', () => { + it('should dispatch a new SaveAndDepositSubmissionAction', () => { + service.dispatchDeposit(submissionId,); + const expected = new SaveAndDepositSubmissionAction(submissionId); - spy.and.returnValue('/mydspace?configuration=workflow'); - service.redirectToMyDSpace(); - - expect((service as any).router.navigateByUrl).toHaveBeenCalledWith('/mydspace?configuration=workflow'); - - spy.and.returnValue(''); - service.redirectToMyDSpace(); - - expect((service as any).router.navigate).toHaveBeenCalledWith(['/mydspace']); - }); - - it('should retrieve submission from REST endpoint', () => { - (service as any).restService.getDataById.and.returnValue(hot('a|', { - a: submissionRestResponse - })); - - const result = service.retrieveSubmission('826'); - const expected = cold('(b|)', { - b: submissionRestResponse[0] + expect((service as any).store.dispatch).toHaveBeenCalledWith(expected); }); - - expect(result).toBeObservable(expected); }); - it('should start Auto Save', fakeAsync(() => { - const duration = config.submission.autosave.timer * (1000 * 60); - spyOn((service as any).store, 'dispatch'); + describe('dispatchDiscard', () => { + it('should dispatch a new DiscardSubmissionAction', () => { + service.dispatchDiscard(submissionId,); + const expected = new DiscardSubmissionAction(submissionId); - service.startAutoSave('826'); - const sub = (service as any).timerObs.subscribe(); + expect((service as any).store.dispatch).toHaveBeenCalledWith(expected); + }); + }); - tick(duration / 2); - expect((service as any).store.dispatch).not.toHaveBeenCalled(); + describe('dispatchSave', () => { + it('should dispatch a new SaveSubmissionFormAction', () => { + service.dispatchSave(submissionId,); + const expected = new SaveSubmissionFormAction(submissionId); - tick(duration / 2); - expect((service as any).store.dispatch).toHaveBeenCalled(); + expect((service as any).store.dispatch).toHaveBeenCalledWith(expected); + }); + }); - sub.unsubscribe(); - (service as any).autoSaveSub.unsubscribe(); - })); + describe('dispatchSaveForLater', () => { + it('should dispatch a new SaveForLaterSubmissionFormAction', () => { + service.dispatchSaveForLater(submissionId,); + const expected = new SaveForLaterSubmissionFormAction(submissionId); - it('should stop Auto Save', () => { - service.startAutoSave('826'); - service.stopAutoSave(); + expect((service as any).store.dispatch).toHaveBeenCalledWith(expected); + }); + }); - expect((service as any).autoSaveSub).toBeNull(); + describe('dispatchSaveSection', () => { + it('should dispatch a new SaveSubmissionSectionFormAction', () => { + service.dispatchSaveSection(submissionId, sectionId); + const expected = new SaveSubmissionSectionFormAction(submissionId, sectionId); + + expect((service as any).store.dispatch).toHaveBeenCalledWith(expected); + }); + }); + + describe('getSubmissionObject', () => { + it('should return submission object state from the store', () => { + spyOn((service as any).store, 'select').and.returnValue(hot('a', { + a: subState.objects[826] + })); + + const result = service.getSubmissionObject('826'); + const expected = cold('b', {b: subState.objects[826]}); + + expect(result).toBeObservable(expected); + }); + }); + + describe('getActiveSectionId', () => { + it('should return current active submission form section', () => { + spyOn((service as any).store, 'select').and.returnValue(hot('a', { + a: subState.objects[826] + })); + + const result = service.getActiveSectionId('826'); + const expected = cold('b', {b: 'keyinformation'}); + + expect(result).toBeObservable(expected); + + }); + }); + + describe('getSubmissionSections', () => { + it('should return submission form sections', () => { + spyOn((service as any).store, 'select').and.returnValue(hot('a|', { + a: subState.objects[826] + })); + + const result = service.getSubmissionSections('826'); + const expected = cold('(bc|)', { + b: [], + c: + [ + { + header: 'submit.progressbar.describe.keyinformation', + id: 'keyinformation', + config: 'https://rest.api/dspace-spring-rest/api/config/submissionforms/keyinformation', + mandatory: true, + sectionType: 'submission-form', + data: {}, + errors: [] + }, + { + header: 'submit.progressbar.describe.indexing', + id: 'indexing', + config: 'https://rest.api/dspace-spring-rest/api/config/submissionforms/indexing', + mandatory: false, + sectionType: 'submission-form', + data: {}, + errors: [] + }, + { + header: 'submit.progressbar.describe.publicationchannel', + id: 'publicationchannel', + config: 'https://rest.api/dspace-spring-rest/api/config/submissionforms/publicationchannel', + mandatory: true, + sectionType: 'submission-form', + data: {}, + errors: [] + }, + { + header: 'submit.progressbar.describe.acknowledgement', + id: 'acknowledgement', + config: 'https://rest.api/dspace-spring-rest/api/config/submissionforms/acknowledgement', + mandatory: false, + sectionType: 'submission-form', + data: {}, + errors: [] + }, + { + header: 'submit.progressbar.describe.identifiers', + id: 'identifiers', + config: 'https://rest.api/dspace-spring-rest/api/config/submissionforms/identifiers', + mandatory: false, + sectionType: 'submission-form', + data: {}, + errors: [] + }, + { + header: 'submit.progressbar.describe.references', + id: 'references', + config: 'https://rest.api/dspace-spring-rest/api/config/submissionforms/references', + mandatory: false, + sectionType: 'submission-form', + data: {}, + errors: [] + }, + { + header: 'submit.progressbar.upload', + id: 'upload', + config: 'https://rest.api/dspace-spring-rest/api/config/submissionuploads/upload', + mandatory: true, + sectionType: 'upload', + data: {}, + errors: [] + }, + { + header: 'submit.progressbar.license', + id: 'license', + config: '', + mandatory: true, + sectionType: 'license', + data: {}, + errors: [] + } + ] + }); + + expect(result).toBeObservable(expected); + }); + }); + + describe('getDisabledSectionsList', () => { + it('should return list of submission disabled sections', () => { + spyOn((service as any).store, 'select').and.returnValue(hot('-a|', { + a: subState.objects[826] + })); + + const result = service.getDisabledSectionsList('826'); + const expected = cold('bc|', { + b: [], + c: + [ + { + header: 'submit.progressbar.describe.indexing', + id: 'indexing', + }, + { + header: 'submit.progressbar.describe.acknowledgement', + id: 'acknowledgement', + }, + { + header: 'submit.progressbar.describe.identifiers', + id: 'identifiers', + }, + { + header: 'submit.progressbar.describe.references', + id: 'references', + } + ] + }); + + expect(result).toBeObservable(expected); + }); + }); + + describe('getSubmissionObjectLinkName', () => { + it('should return properly submission link name', () => { + let expected = 'workspaceitems'; + router.setRoute('/workspaceitems/826/edit'); + expect(service.getSubmissionObjectLinkName()).toBe(expected); + + expected = 'workspaceitems'; + router.setRoute('/submit'); + expect(service.getSubmissionObjectLinkName()).toBe(expected); + + expected = 'workflowitems'; + router.setRoute('/workflowitems/826/edit'); + expect(service.getSubmissionObjectLinkName()).toBe(expected); + + expected = 'edititems'; + router.setRoute('/items/9e79b1f2-ae0f-4737-9a4b-990952a8857c/edit'); + expect(service.getSubmissionObjectLinkName()).toBe(expected); + }); + }); + + describe('getSubmissionScope', () => { + it('should return properly submission scope', () => { + let expected = SubmissionScopeType.WorkspaceItem; + + router.setRoute('/workspaceitems/826/edit'); + expect(service.getSubmissionScope()).toBe(expected); + + router.setRoute('/submit'); + expect(service.getSubmissionScope()).toBe(expected); + + expected = SubmissionScopeType.WorkflowItem; + router.setRoute('/workflowitems/826/edit'); + expect(service.getSubmissionScope()).toBe(expected); + + expected = SubmissionScopeType.EditItem; + router.setRoute('/items/9e79b1f2-ae0f-4737-9a4b-990952a8857c/edit'); + expect(service.getSubmissionScope()).toBe(expected); + }); + }); + + describe('getSubmissionStatus', () => { + it('should return properly submission status', () => { + spyOn((service as any).store, 'select').and.returnValue(hot('-a-b', { + a: subState, + b: validSubState + })); + const result = service.getSubmissionStatus('826'); + const expected = cold('cc-d', { + c: false, + d: true + }); + + expect(result).toBeObservable(expected); + }); + }); + + describe('getSubmissionSaveProcessingStatus', () => { + it('should return submission save processing status', () => { + spyOn((service as any).store, 'select').and.returnValue(hot('-a', { + a: subState.objects[826] + })); + + const result = service.getSubmissionSaveProcessingStatus('826'); + const expected = cold('bb', { + b: false + }); + + expect(result).toBeObservable(expected); + }); + }); + + describe('getSubmissionDepositProcessingStatus', () => { + it('should return submission deposit processing status', () => { + spyOn((service as any).store, 'select').and.returnValue(hot('-a', { + a: subState.objects[826] + })); + + const result = service.getSubmissionDepositProcessingStatus('826'); + const expected = cold('bb', { + b: false + }); + + expect(result).toBeObservable(expected); + }); + }); + + describe('isSectionHidden', () => { + it('should return true/false when section is hidden/visible', () => { + let section: any = { + config: '', + header: '', + mandatory: true, + sectionType: 'collection' as any, + visibility: { + main: 'HIDDEN', + other: 'HIDDEN' + }, + collapsed: false, + enabled: true, + data: {}, + errors: [], + isLoading: false, + isValid: false + }; + expect(service.isSectionHidden(section)).toBeTruthy(); + + section = { + header: 'submit.progressbar.describe.keyinformation', + config: 'https://rest.api/dspace-spring-rest/api/config/submissionforms/keyinformation', + mandatory: true, + sectionType: 'submission-form', + collapsed: false, + enabled: true, + data: {}, + errors: [], + isLoading: false, + isValid: false + }; + expect(service.isSectionHidden(section)).toBeFalsy(); + }); + }); + + describe('isSubmissionLoading', () => { + it('should return true/false when section is loading/not loading', () => { + const spy = spyOn(service, 'getSubmissionObject').and.returnValue(observableOf({isLoading: true})); + + let expected = cold('(b|)', { + b: true + }); + + expect(service.isSubmissionLoading(submissionId)).toBeObservable(expected); + + spy.and.returnValue(observableOf({isLoading: false})); + + expected = cold('(b|)', { + b: false + }); + + expect(service.isSubmissionLoading(submissionId)).toBeObservable(expected); + }); + }); + + describe('notifyNewSection', () => { + it('should return true/false when section is loading/not loading', fakeAsync(() => { + const spy = spyOn((service as any).translate, 'get').and.returnValue(observableOf('test')); + + spyOn((service as any).notificationsService, 'info').and.callThrough(); + + service.notifyNewSection(submissionId, sectionId); + flush(); + + expect((service as any).notificationsService.info).toHaveBeenCalledWith(null, 'test', null, true); + })); + }); + + describe('redirectToMyDSpace', () => { + it('should redirect to MyDspace page', () => { + const spy = spyOn((service as any).routeService, 'getPreviousUrl'); + + spy.and.returnValue('/mydspace?configuration=workflow'); + service.redirectToMyDSpace(); + + expect((service as any).router.navigateByUrl).toHaveBeenCalledWith('/mydspace?configuration=workflow'); + + spy.and.returnValue(''); + service.redirectToMyDSpace(); + + expect((service as any).router.navigate).toHaveBeenCalledWith(['/mydspace']); + }); + }); + + describe('resetAllSubmissionObjects', () => { + it('should dispatch a new CancelSubmissionFormAction', () => { + service.resetAllSubmissionObjects(); + const expected = new CancelSubmissionFormAction(); + + expect((service as any).store.dispatch).toHaveBeenCalledWith(expected); + }); + }); + + describe('resetSubmissionObject', () => { + it('should dispatch a new ResetSubmissionFormAction', () => { + service.resetSubmissionObject( + collectionId, + submissionId, + selfUrl, + submissionDefinition, + {} + ); + const expected = new ResetSubmissionFormAction( + collectionId, + submissionId, + selfUrl, + {}, + submissionDefinition + ); + + expect((service as any).store.dispatch).toHaveBeenCalledWith(expected); + }); + }); + + describe('retrieveSubmission', () => { + it('should retrieve submission from REST endpoint', () => { + (service as any).restService.getDataById.and.returnValue(hot('a|', { + a: submissionRestResponse + })); + + const result = service.retrieveSubmission('826'); + const expected = cold('(b|)', { + b: submissionRestResponse[0] + }); + + expect(result).toBeObservable(expected); + }); + }); + + describe('setActiveSection', () => { + it('should dispatch a new SetActiveSectionAction', () => { + service.setActiveSection(submissionId, sectionId); + const expected = new SetActiveSectionAction(submissionId, sectionId); + + expect((service as any).store.dispatch).toHaveBeenCalledWith(expected); + }); + }); + + describe('startAutoSave', () => { + it('should start Auto Save', fakeAsync(() => { + const duration = config.submission.autosave.timer * (1000 * 60); + + service.startAutoSave('826'); + const sub = (service as any).timerObs.subscribe(); + + tick(duration / 2); + expect((service as any).store.dispatch).not.toHaveBeenCalled(); + + tick(duration / 2); + expect((service as any).store.dispatch).toHaveBeenCalled(); + + sub.unsubscribe(); + (service as any).autoSaveSub.unsubscribe(); + })); + }); + + describe('stopAutoSave', () => { + it('should stop Auto Save', () => { + service.startAutoSave('826'); + service.stopAutoSave(); + + expect((service as any).autoSaveSub).toBeNull(); + }); }); }); diff --git a/src/app/submission/submission.service.ts b/src/app/submission/submission.service.ts index 0d919d4adf..d5d6a63e96 100644 --- a/src/app/submission/submission.service.ts +++ b/src/app/submission/submission.service.ts @@ -6,7 +6,6 @@ import { Observable, of as observableOf, Subscription, timer as observableTimer import { catchError, distinctUntilChanged, filter, first, map, startWith } from 'rxjs/operators'; import { Store } from '@ngrx/store'; import { TranslateService } from '@ngx-translate/core'; -import { ScrollToService } from '@nicky-lenaers/ngx-scroll-to'; import { submissionSelector, SubmissionState } from './submission.reducers'; import { hasValue, isEmpty, isNotUndefined } from '../shared/empty.util'; @@ -18,7 +17,7 @@ import { ResetSubmissionFormAction, SaveAndDepositSubmissionAction, SaveForLaterSubmissionFormAction, - SaveSubmissionFormAction, + SaveSubmissionFormAction, SaveSubmissionSectionFormAction, SetActiveSectionAction } from './objects/submission-objects.actions'; import { @@ -52,7 +51,6 @@ export class SubmissionService { protected restService: SubmissionRestService, protected router: Router, protected routeService: RouteService, - protected scrollToService: ScrollToService, protected store: Store, protected translate: TranslateService) { } @@ -106,7 +104,7 @@ export class SubmissionService { } dispatchSaveSection(submissionId, sectionId) { - this.store.dispatch(new SaveSubmissionFormAction(submissionId)); + this.store.dispatch(new SaveSubmissionSectionFormAction(submissionId, sectionId)); } getActiveSectionId(submissionId: string): Observable {