diff --git a/client/src/app/metamodel/effects/attribute-distinct.effects.spec.ts b/client/src/app/metamodel/effects/attribute-distinct.effects.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..a1978b971248669d91b8c9fb424d467849451022 --- /dev/null +++ b/client/src/app/metamodel/effects/attribute-distinct.effects.spec.ts @@ -0,0 +1,88 @@ +import { TestBed } from '@angular/core/testing'; +import { Router } from '@angular/router'; + +import { provideMockActions } from '@ngrx/effects/testing'; +import { MockStore, provideMockStore } from '@ngrx/store/testing'; +import { EffectsMetadata, getEffectsMetadata } from '@ngrx/effects'; +import { Observable } from 'rxjs'; +import { cold, hot } from 'jasmine-marbles'; +import { ToastrService } from 'ngx-toastr'; + +import { AttributeDistinctEffects } from './attribute-distinct.effects'; +import { AttributeDistinctService } from '../services/attribute-distinct.service'; +import * as attributeDistinctActions from '../actions/attribute-distinct.actions'; +import * as datasetSelector from '../selectors/dataset.selector'; +import { ATTRIBUTE } from '../../../test-data'; + +describe('[Metamodel][Effects] AttributeDistinctEffects', () => { + let actions = new Observable(); + let effects: AttributeDistinctEffects; + let metadata: EffectsMetadata<AttributeDistinctEffects>; + let service: AttributeDistinctService; + let toastr: ToastrService; + let store: MockStore; + const initialState = { metamodel: { } }; + let mockDatasetSelectorSelectDatasetNameByRoute; + let router: Router; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + AttributeDistinctEffects, + { provide: AttributeDistinctService, useValue: { }}, + { provide: Router, useValue: { navigate: jest.fn() }}, + { provide: ToastrService, useValue: { + success: jest.fn(), + error: jest.fn() + }}, + provideMockActions(() => actions), + provideMockStore({ initialState }) + ] + }).compileComponents(); + effects = TestBed.inject(AttributeDistinctEffects); + metadata = getEffectsMetadata(effects); + service = TestBed.inject(AttributeDistinctService); + toastr = TestBed.inject(ToastrService); + router = TestBed.inject(Router); + store = TestBed.inject(MockStore); + mockDatasetSelectorSelectDatasetNameByRoute = store.overrideSelector( + datasetSelector.selectDatasetNameByRoute,'' + ); + }); + + it('should be created', () => { + expect(effects).toBeTruthy(); + }); + + describe('loadAttributeDistinct$ effect', () => { + it('should dispatch the loadAttributeDistinctListSuccess action on success', () => { + mockDatasetSelectorSelectDatasetNameByRoute = store.overrideSelector( + datasetSelector.selectDatasetNameByRoute, 'myDataset' + ); + + const action = attributeDistinctActions.loadAttributeDistinctList({ attribute: ATTRIBUTE }); + const outcome = attributeDistinctActions.loadAttributeDistinctListSuccess({ values: [] }); + + actions = hot('-a', { a: action }); + const response = cold('-a|', { a: [] }); + const expected = cold('--b', { b: outcome }); + service.retrieveAttributeDistinctList = jest.fn(() => response); + + expect(effects.loadAttributeDistinct$).toBeObservable(expected); + expect(service.retrieveAttributeDistinctList).toHaveBeenCalledWith('myDataset', ATTRIBUTE); + }); + + it('should dispatch the loadAttributeDistinctListFail action on HTTP failure', () => { + const action = attributeDistinctActions.loadAttributeDistinctList({ attribute: ATTRIBUTE }); + const error = new Error(); + const outcome = attributeDistinctActions.loadAttributeDistinctListFail(); + + actions = hot('-a', { a: action }); + const response = cold('-#|', { }, error); + const expected = cold('--b', { b: outcome }); + service.retrieveAttributeDistinctList = jest.fn(() => response); + + expect(effects.loadAttributeDistinct$).toBeObservable(expected); + }); + }); +}); diff --git a/client/src/app/metamodel/effects/attribute-distinct.effects.ts b/client/src/app/metamodel/effects/attribute-distinct.effects.ts index aaa51d7cdc83ba114efe06823526c7ae0395310c..6584c439e5ca4be3558606f0785413ab354a0860 100644 --- a/client/src/app/metamodel/effects/attribute-distinct.effects.ts +++ b/client/src/app/metamodel/effects/attribute-distinct.effects.ts @@ -17,10 +17,18 @@ import { map, mergeMap, catchError } from 'rxjs/operators'; import * as attributeDistinctActions from '../actions/attribute-distinct.actions'; import { AttributeDistinctService } from '../services/attribute-distinct.service'; import * as datasetSelector from '../selectors/dataset.selector'; - + +/** + * @class + * @classdesc Attribute distinct effects. + */ @Injectable() export class AttributeDistinctEffects { - loadAttributeDistinct$ = createEffect(() => + + /** + * Calls action to retrieve distinct attribute list. + */ + loadAttributeDistinct$ = createEffect((): any => this.actions$.pipe( ofType(attributeDistinctActions.loadAttributeDistinctList), concatLatestFrom(() => this.store.select(datasetSelector.selectDatasetNameByRoute)), @@ -32,7 +40,7 @@ export class AttributeDistinctEffects { ) ) ); - + constructor( private actions$: Actions, private attributeDistinctService: AttributeDistinctService, diff --git a/client/src/app/metamodel/effects/attribute.effects.spec.ts b/client/src/app/metamodel/effects/attribute.effects.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..602d95d2b34db2e6ce7aaa4e75c2a53b065b8473 --- /dev/null +++ b/client/src/app/metamodel/effects/attribute.effects.spec.ts @@ -0,0 +1,289 @@ +import { TestBed } from '@angular/core/testing'; +import { Router } from '@angular/router'; + +import { provideMockActions } from '@ngrx/effects/testing'; +import { MockStore, provideMockStore } from '@ngrx/store/testing'; +import { EffectsMetadata, getEffectsMetadata } from '@ngrx/effects'; +import { Observable } from 'rxjs'; +import { cold, hot } from 'jasmine-marbles'; + +import { ToastrService } from 'ngx-toastr'; +import { AttributeEffects } from './attribute.effects'; +import { AttributeService } from '../services/attribute.service'; +import * as attributeActions from '../actions/attribute.actions'; +import { ATTRIBUTE, ATTRIBUTE_LIST } from '../../../test-data'; +import * as datasetSelector from '../selectors/dataset.selector'; + +describe('[Metamodel][Effects] AttributeEffects', () => { + let actions = new Observable(); + let effects: AttributeEffects; + let metadata: EffectsMetadata<AttributeEffects>; + let service: AttributeService; + let toastr: ToastrService; + let store: MockStore; + const initialState = { metamodel: { } }; + let mockDatasetSelectorSelectDatasetNameByRoute; + let router: Router; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + AttributeEffects, + { provide: AttributeService, useValue: { }}, + { provide: Router, useValue: { navigate: jest.fn() }}, + { provide: ToastrService, useValue: { + success: jest.fn(), + error: jest.fn() + }}, + provideMockActions(() => actions), + provideMockStore({ initialState }) + ] + }).compileComponents(); + effects = TestBed.inject(AttributeEffects); + metadata = getEffectsMetadata(effects); + service = TestBed.inject(AttributeService); + toastr = TestBed.inject(ToastrService); + store = TestBed.inject(MockStore); + mockDatasetSelectorSelectDatasetNameByRoute = store.overrideSelector( + datasetSelector.selectDatasetNameByRoute,'' + ); + router = TestBed.inject(Router); + }); + + it('should be created', () => { + expect(effects).toBeTruthy(); + }); + + describe('loadAttributes$ effect', () => { + it('should dispatch the loadAttributeListSuccess action on success', () => { + mockDatasetSelectorSelectDatasetNameByRoute = store.overrideSelector( + datasetSelector.selectDatasetNameByRoute, 'myDataset' + ); + + const action = attributeActions.loadAttributeList(); + const outcome = attributeActions.loadAttributeListSuccess({ attributes: ATTRIBUTE_LIST }); + + actions = hot('-a', { a: action }); + const response = cold('-a|', { a: ATTRIBUTE_LIST }); + const expected = cold('--b', { b: outcome }); + service.retrieveAttributeList = jest.fn(() => response); + + expect(effects.loadAttributes$).toBeObservable(expected); + expect(service.retrieveAttributeList).toHaveBeenCalledWith('myDataset'); + }); + + it('should dispatch the loadAttributeListFail action on HTTP failure', () => { + const action = attributeActions.loadAttributeList(); + const error = new Error(); + const outcome = attributeActions.loadAttributeListFail(); + + actions = hot('-a', { a: action }); + const response = cold('-#|', { }, error); + const expected = cold('--b', { b: outcome }); + service.retrieveAttributeList = jest.fn(() => response); + + expect(effects.loadAttributes$).toBeObservable(expected); + }); + }); + + describe('addAttribute$ effect', () => { + it('should dispatch the addAttributeSuccess action on success', () => { + mockDatasetSelectorSelectDatasetNameByRoute = store.overrideSelector( + datasetSelector.selectDatasetNameByRoute, 'myDataset' + ); + + const action = attributeActions.addAttribute({ attribute: ATTRIBUTE }); + const outcome = attributeActions.addAttributeSuccess({ attribute: ATTRIBUTE }); + + actions = hot('-a', { a: action }); + const response = cold('-a|', { a: ATTRIBUTE }); + const expected = cold('--b', { b: outcome }); + service.addAttribute = jest.fn(() => response); + + expect(effects.addAttribute$).toBeObservable(expected); + expect(service.addAttribute).toHaveBeenCalledWith('myDataset', ATTRIBUTE); + }); + + it('should dispatch the addAttributeFail action on HTTP failure', () => { + const action = attributeActions.addAttribute({ attribute: ATTRIBUTE }); + const error = new Error(); + const outcome = attributeActions.addAttributeFail(); + + actions = hot('-a', { a: action }); + const response = cold('-#|', { }, error); + const expected = cold('--b', { b: outcome }); + service.addAttribute = jest.fn(() => response); + + expect(effects.addAttribute$).toBeObservable(expected); + }); + }); + + describe('addAttributeSuccess$ effect', () => { + it('should not dispatch', () => { + expect(metadata.addAttributeSuccess$).toEqual( + expect.objectContaining({ dispatch: false }) + ); + }); + + it('should display a success notification', () => { + const toastrSpy = jest.spyOn(toastr, 'success'); + const action = attributeActions.addAttributeSuccess({ attribute: ATTRIBUTE }); + + actions = hot('a', { a: action }); + const expected = cold('a', { a: action }); + + expect(effects.addAttributeSuccess$).toBeObservable(expected); + expect(toastrSpy).toHaveBeenCalledTimes(1); + expect(toastrSpy).toHaveBeenCalledWith( + 'Attribute successfully added', + 'The new attribute was added into the database' + ); + }); + }); + + describe('addAttributeFail$ effect', () => { + it('should not dispatch', () => { + expect(metadata.addAttributeFail$).toEqual( + expect.objectContaining({ dispatch: false }) + ); + }); + + it('should display an error notification', () => { + const spy = jest.spyOn(toastr, 'error'); + const action = attributeActions.addAttributeFail(); + + actions = hot('a', { a: action }); + const expected = cold('a', { a: action }); + + expect(effects.addAttributeFail$).toBeObservable(expected); + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith( + 'Failure to add attribute', + 'The new attribute could not be added into the database' + ); + }); + }); + + describe('editAttribute$ effect', () => { + it('should dispatch the editAttributeSuccess action on success', () => { + const action = attributeActions.editAttribute({ attribute: ATTRIBUTE }); + const outcome = attributeActions.editAttributeSuccess({ attribute: ATTRIBUTE }); + + actions = hot('-a', { a: action }); + const response = cold('-a|', { a: ATTRIBUTE }); + const expected = cold('--b', { b: outcome }); + service.editAttribute = jest.fn(() => response); + + expect(effects.editAttribute$).toBeObservable(expected); + }); + + it('should dispatch the editAttributeFail action on HTTP failure', () => { + const action = attributeActions.editAttribute({ attribute: ATTRIBUTE }); + const error = new Error(); + const outcome = attributeActions.editAttributeFail(); + + actions = hot('-a', { a: action }); + const response = cold('-#|', { }, error); + const expected = cold('--b', { b: outcome }); + service.editAttribute = jest.fn(() => response); + + expect(effects.editAttribute$).toBeObservable(expected); + }); + }); + + describe('editAttributeFail$ effect', () => { + it('should not dispatch', () => { + expect(metadata.editAttributeFail$).toEqual( + expect.objectContaining({ dispatch: false }) + ); + }); + + it('should display an error notification', () => { + const spy = jest.spyOn(toastr, 'error'); + const action = attributeActions.editAttributeFail(); + + actions = hot('a', { a: action }); + const expected = cold('a', { a: action }); + + expect(effects.editAttributeFail$).toBeObservable(expected); + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith( + 'Failure to edit attribute', + 'The existing attribute could not be edited into the database' + ); + }); + }); + + describe('deleteAttribute$ effect', () => { + it('should dispatch the deleteAttributeSuccess action on success', () => { + const action = attributeActions.deleteAttribute({ attribute: ATTRIBUTE }); + const outcome = attributeActions.deleteAttributeSuccess({ attribute: ATTRIBUTE }); + + actions = hot('-a', { a: action }); + const response = cold('-a|', { a: ATTRIBUTE }); + const expected = cold('--b', { b: outcome }); + service.deleteAttribute = jest.fn(() => response); + + expect(effects.deleteAttribute$).toBeObservable(expected); + }); + + it('should dispatch the deleteAttributeFail action on HTTP failure', () => { + const action = attributeActions.deleteAttribute({ attribute: ATTRIBUTE }); + const error = new Error(); + const outcome = attributeActions.deleteAttributeFail(); + + actions = hot('-a', { a: action }); + const response = cold('-#|', { }, error); + const expected = cold('--b', { b: outcome }); + service.deleteAttribute = jest.fn(() => response); + + expect(effects.deleteAttribute$).toBeObservable(expected); + }); + }); + + describe('deleteAttributeSuccess$ effect', () => { + it('should not dispatch', () => { + expect(metadata.deleteAttributeSuccess$).toEqual( + expect.objectContaining({ dispatch: false }) + ); + }); + + it('should display a success notification', () => { + const toastrSpy = jest.spyOn(toastr, 'success'); + const action = attributeActions.deleteAttributeSuccess({ attribute: ATTRIBUTE }); + + actions = hot('a', { a: action }); + const expected = cold('a', { a: action }); + + expect(effects.deleteAttributeSuccess$).toBeObservable(expected); + expect(toastrSpy).toHaveBeenCalledTimes(1); + expect(toastrSpy).toHaveBeenCalledWith( + 'Attribute successfully deleted', + 'The existing attribute has been deleted' + ); + }); + }); + + describe('deleteAttributeFail$ effect', () => { + it('should not dispatch', () => { + expect(metadata.deleteAttributeFail$).toEqual( + expect.objectContaining({ dispatch: false }) + ); + }); + + it('should display an error notification', () => { + const spy = jest.spyOn(toastr, 'error'); + const action = attributeActions.deleteAttributeFail(); + + actions = hot('a', { a: action }); + const expected = cold('a', { a: action }); + + expect(effects.deleteAttributeFail$).toBeObservable(expected); + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith( + 'Failure to delete attribute', + 'The existing attribute could not be deleted from the database' + ); + }); + }); +}); diff --git a/client/src/app/metamodel/effects/attribute.effects.ts b/client/src/app/metamodel/effects/attribute.effects.ts index 59e6c66114e1ce62e7b1bac1aa3b0f24452b7d08..c1198e7edf545cad34ef6da44b992b6a21a4e961 100644 --- a/client/src/app/metamodel/effects/attribute.effects.ts +++ b/client/src/app/metamodel/effects/attribute.effects.ts @@ -19,9 +19,18 @@ import * as attributeActions from '../actions/attribute.actions'; import { AttributeService } from '../services/attribute.service'; import * as datasetSelector from '../selectors/dataset.selector'; + +/** + * @class + * @classdesc Attribute effects. + */ @Injectable() export class AttributeEffects { - loadAttributes$ = createEffect(() => + + /** + * Calls action to retrieve attribute list. + */ + loadAttributes$ = createEffect((): any => this.actions$.pipe( ofType(attributeActions.loadAttributeList), concatLatestFrom(() => this.store.select(datasetSelector.selectDatasetNameByRoute)), @@ -34,7 +43,10 @@ export class AttributeEffects { ) ); - addAttribute$ = createEffect(() => + /** + * Calls action to add an attribute. + */ + addAttribute$ = createEffect((): any => this.actions$.pipe( ofType(attributeActions.addAttribute), concatLatestFrom(() => this.store.select(datasetSelector.selectDatasetNameByRoute)), @@ -47,6 +59,9 @@ export class AttributeEffects { ) ); + /** + * Displays add attribute success notification. + */ addAttributeSuccess$ = createEffect(() => this.actions$.pipe( ofType(attributeActions.addAttributeSuccess), @@ -56,6 +71,9 @@ export class AttributeEffects { ), { dispatch: false } ); + /** + * Displays add attribute error notification. + */ addAttributeFail$ = createEffect(() => this.actions$.pipe( ofType(attributeActions.addAttributeFail), @@ -63,7 +81,10 @@ export class AttributeEffects { ), { dispatch: false } ); - editAttribute$ = createEffect(() => + /** + * Calls action to modify an attribute. + */ + editAttribute$ = createEffect((): any => this.actions$.pipe( ofType(attributeActions.editAttribute), concatLatestFrom(() => this.store.select(datasetSelector.selectDatasetNameByRoute)), @@ -76,6 +97,9 @@ export class AttributeEffects { ) ); + /** + * Displays edit attribute error notification. + */ editAttributeFail$ = createEffect(() => this.actions$.pipe( ofType(attributeActions.editAttributeFail), @@ -83,7 +107,10 @@ export class AttributeEffects { ), { dispatch: false } ); - deleteAttribute$ = createEffect(() => + /** + * Calls action to remove an attribute. + */ + deleteAttribute$ = createEffect((): any => this.actions$.pipe( ofType(attributeActions.deleteAttribute), concatLatestFrom(() => this.store.select(datasetSelector.selectDatasetNameByRoute)), @@ -96,6 +123,9 @@ export class AttributeEffects { ) ); + /** + * Displays delete attribute success notification. + */ deleteAttributeSuccess$ = createEffect(() => this.actions$.pipe( ofType(attributeActions.deleteAttributeSuccess), @@ -103,6 +133,9 @@ export class AttributeEffects { ), { dispatch: false } ); + /** + * Displays delete attribute error notification. + */ deleteAttributeFail$ = createEffect(() => this.actions$.pipe( ofType(attributeActions.deleteAttributeFail), diff --git a/client/src/app/metamodel/effects/column.effects.spec.ts b/client/src/app/metamodel/effects/column.effects.spec.ts index ef28d818d0c4adcd08c1e4236eadeeef5ce242d4..64ef0d2deab1e4510daffd389974ebf68d60a954 100644 --- a/client/src/app/metamodel/effects/column.effects.spec.ts +++ b/client/src/app/metamodel/effects/column.effects.spec.ts @@ -54,7 +54,7 @@ describe('[Metamodel][Effects] ColumnEffects', () => { expect(effects).toBeTruthy(); }); - describe('loadOutputFamilies$ effect', () => { + describe('loadColumns$ effect', () => { it('should dispatch the loadColumnListSuccess action on success', () => { mockDatasetSelectorSelectDatasetNameByRoute = store.overrideSelector( datasetSelector.selectDatasetNameByRoute, 'myDataset'