import { TestBed } from '@angular/core/testing'; import { provideMockActions } from '@ngrx/effects/testing'; import { EffectsMetadata, getEffectsMetadata } from '@ngrx/effects'; import { MockStore, provideMockStore } from '@ngrx/store/testing'; import { Observable } from 'rxjs'; import { cold, hot } from 'jasmine-marbles'; import { ToastrService } from 'ngx-toastr'; import { SearchEffects } from './search.effects'; import { SearchService } from '../services/search.service'; import * as searchActions from '../actions/search.actions'; import * as fromSearch from '../reducers/search.reducer'; import * as datasetSelector from '../../../metamodel/selectors/dataset.selector'; import * as searchSelector from '../selectors/search.selector'; import * as attributeActions from '../../../metamodel/actions/attribute.actions'; import * as criteriaFamilyActions from '../../../metamodel/actions/criteria-family.actions'; import * as outputFamilyActions from '../../../metamodel/actions/output-family.actions'; import * as outputCategoryActions from '../../../metamodel/actions/output-category.actions'; import * as attributeSelector from '../../../metamodel/selectors/attribute.selector'; import * as coneSearchSelector from '../selectors/cone-search.selector'; import * as coneSearchActions from '../actions/cone-search.actions'; import { Criterion, PaginationOrder } from '../models'; describe('SearchEffects', () => { let actions = new Observable(); let effects: SearchEffects; let metadata: EffectsMetadata<SearchEffects>; let searchService: SearchService; let toastr: ToastrService; let store: MockStore; const initialState = { search: { ...fromSearch.initialState } }; let mockDatasetSelectorSelectDatasetNameByRoute; let mockSearchSelectorSelectCurrentDataset; let mockSearchSelectorSelectPristine; let mockSearchSelectorSelectStepsByRoute; let mockAttributeSelectorSelectAllAttributes; let mockSearchSelectorSelectCriteriaListByRoute; let mockSearchSelectorSelectCriteriaList; let mockConeSearchSelectorSelectConeSearchByRoute; let mockConeSearchSelectorSelectConeSearch; let mockSearchSelectorSelectOutputListByRoute; let mockSearchSelectorSelectOutputList; beforeEach(() => { TestBed.configureTestingModule({ providers: [ SearchEffects, { provide: SearchService, useValue: { retrieveData: jest.fn(), retrieveDataLength: jest.fn() }}, { provide: ToastrService, useValue: { error: jest.fn() }}, provideMockActions(() => actions), provideMockStore({ initialState }), ] }).compileComponents(); effects = TestBed.inject(SearchEffects); metadata = getEffectsMetadata(effects); searchService = TestBed.inject(SearchService); toastr = TestBed.inject(ToastrService); store = TestBed.inject(MockStore); mockDatasetSelectorSelectDatasetNameByRoute = store.overrideSelector( datasetSelector.selectDatasetNameByRoute,'' ); mockSearchSelectorSelectCurrentDataset = store.overrideSelector( searchSelector.selectCurrentDataset,'' ); mockSearchSelectorSelectPristine = store.overrideSelector( searchSelector.selectPristine,true ); mockAttributeSelectorSelectAllAttributes = store.overrideSelector( attributeSelector.selectAllAttributes,[] ); mockSearchSelectorSelectStepsByRoute = store.overrideSelector( searchSelector.selectStepsByRoute,'' ); mockSearchSelectorSelectCriteriaListByRoute = store.overrideSelector( searchSelector.selectCriteriaListByRoute,'' ); mockSearchSelectorSelectCriteriaList = store.overrideSelector( searchSelector.selectCriteriaList,[] ); mockConeSearchSelectorSelectConeSearchByRoute = store.overrideSelector( coneSearchSelector.selectConeSearchByRoute,'' ); mockConeSearchSelectorSelectConeSearch = store.overrideSelector( coneSearchSelector.selectConeSearch,{ ra: 1, dec: 2, radius: 3 } ); mockSearchSelectorSelectOutputListByRoute = store.overrideSelector( searchSelector.selectOutputListByRoute,'' ); mockSearchSelectorSelectOutputList = store.overrideSelector( searchSelector.selectOutputList,[] ); }); it('should be created', () => { expect(effects).toBeTruthy(); }); describe('initSearch$ effect', () => { it('should dispatch the restartSearch action when dataset changed', () => { mockDatasetSelectorSelectDatasetNameByRoute = store.overrideSelector( datasetSelector.selectDatasetNameByRoute, 'myNewDataset' ); mockSearchSelectorSelectCurrentDataset = store.overrideSelector( searchSelector.selectCurrentDataset,'myOldDataset' ); const action = searchActions.initSearch(); const outcome = searchActions.restartSearch(); actions = hot('-a', { a: action }); const expected = cold('-b', { b: outcome }); expect(effects.initSearch$).toBeObservable(expected); }); it('should dispatch a bunch of actions when a dataset is selected or a page is reloaded', () => { mockDatasetSelectorSelectDatasetNameByRoute = store.overrideSelector( datasetSelector.selectDatasetNameByRoute, 'myDatasetName' ); mockSearchSelectorSelectPristine = store.overrideSelector( searchSelector.selectPristine,true ); mockSearchSelectorSelectCurrentDataset = store.overrideSelector( searchSelector.selectCurrentDataset,'myDatasetName' ); const action = searchActions.initSearch(); actions = hot('-a', { a: action }); const expected = cold('-(bcdef)', { b: searchActions.changeCurrentDataset({ currentDataset: 'myDatasetName' }), c: attributeActions.loadAttributeList(), d: criteriaFamilyActions.loadCriteriaFamilyList(), e: outputFamilyActions.loadOutputFamilyList(), f: outputCategoryActions.loadOutputCategoryList() }); expect(effects.initSearch$).toBeObservable(expected); }); it('should dispatch a bunch of actions when a dataset is selected or a page is reloaded with steps checked', () => { mockDatasetSelectorSelectDatasetNameByRoute = store.overrideSelector( datasetSelector.selectDatasetNameByRoute, 'myDatasetName' ); mockSearchSelectorSelectPristine = store.overrideSelector( searchSelector.selectPristine,true ); mockSearchSelectorSelectCurrentDataset = store.overrideSelector( searchSelector.selectCurrentDataset,'myDatasetName' ); mockSearchSelectorSelectStepsByRoute = store.overrideSelector( searchSelector.selectStepsByRoute, '111' ); const action = searchActions.initSearch(); actions = hot('-a', { a: action }); const expected = cold('-(bcdefghi)', { b: searchActions.changeCurrentDataset({ currentDataset: 'myDatasetName' }), c: attributeActions.loadAttributeList(), d: criteriaFamilyActions.loadCriteriaFamilyList(), e: outputFamilyActions.loadOutputFamilyList(), f: outputCategoryActions.loadOutputCategoryList(), g: searchActions.checkCriteria(), h: searchActions.checkOutput(), i: searchActions.checkResult() }); expect(effects.initSearch$).toBeObservable(expected); }); it('should dispatch a resetSearch action when user get back to search module', () => { mockDatasetSelectorSelectDatasetNameByRoute = store.overrideSelector( datasetSelector.selectDatasetNameByRoute, '' ); mockSearchSelectorSelectPristine = store.overrideSelector( searchSelector.selectPristine,false ); const action = searchActions.initSearch(); const outcome = searchActions.resetSearch(); actions = hot('-a', { a: action }); const expected = cold('-b', { b: outcome }); expect(effects.initSearch$).toBeObservable(expected); }); it('should not dispatch action when step changed on same search', () => { mockDatasetSelectorSelectDatasetNameByRoute = store.overrideSelector( datasetSelector.selectDatasetNameByRoute, '' ); const action = searchActions.initSearch(); const outcome = { type: '[No Action] Init Search' }; actions = hot('-a', { a: action }); const expected = cold('-b', { b: outcome }); expect(effects.initSearch$).toBeObservable(expected); }); }); describe('restartSearch$ effect', () => { it('should dispatch the initSearch action', () => { const action = searchActions.restartSearch(); const outcome = searchActions.initSearch(); actions = hot('-a', { a: action }); const expected = cold('-b', { b: outcome }); expect(effects.restartSearch$).toBeObservable(expected); }); }); describe('loadDefaultFormParameters$ effect', () => { it('should not dispatch action if params already loaded', () => { mockSearchSelectorSelectPristine = store.overrideSelector( searchSelector.selectPristine, false ); mockSearchSelectorSelectCurrentDataset = store.overrideSelector( searchSelector.selectCurrentDataset, 'myDataset' ); const action = searchActions.loadDefaultFormParameters(); const outcome = { type: '[No Action] Load Default Form Parameters' }; actions = hot('-a', { a: action }); const expected = cold('-b', { b: outcome }); expect(effects.loadDefaultFormParameters$).toBeObservable(expected); }); it('should not dispatch action if no dataset selected', () => { const action = searchActions.loadDefaultFormParameters(); const outcome = { type: '[No Action] Load Default Form Parameters' }; actions = hot('-a', { a: action }); const expected = cold('-b', { b: outcome }); expect(effects.loadDefaultFormParameters$).toBeObservable(expected); }); it('should dispatch a bunch of actions to update search', () => { mockSearchSelectorSelectPristine = store.overrideSelector( searchSelector.selectPristine, true ); mockSearchSelectorSelectCurrentDataset = store.overrideSelector( searchSelector.selectCurrentDataset, 'myDataset' ); const action = searchActions.loadDefaultFormParameters(); actions = hot('-a', { a: action }); const defaultCriteriaList = []; const defaultConeSearch = null; const defaultOutputList = []; const expected = cold('-(bcde)', { b: searchActions.updateCriteriaList({ criteriaList: defaultCriteriaList }), c: coneSearchActions.addConeSearch({ coneSearch: defaultConeSearch }), d: searchActions.updateOutputList({ outputList: defaultOutputList }), e: searchActions.markAsDirty() }); expect(effects.loadDefaultFormParameters$).toBeObservable(expected); }); it('should set a default criteria list', () => { mockSearchSelectorSelectPristine = store.overrideSelector( searchSelector.selectPristine, true ); mockSearchSelectorSelectCurrentDataset = store.overrideSelector( searchSelector.selectCurrentDataset, 'myDataset' ); mockAttributeSelectorSelectAllAttributes = store.overrideSelector( attributeSelector.selectAllAttributes, [ { id: 1, name: 'att1', label: 'attribute1', form_label: 'Attribute 1', output_display: 1, criteria_display: 1, search_type: 'field', operator: 'eq', type: 'string', min: 'one', display_detail: 1, id_criteria_family: 1, id_output_category: 1 }, { id: 2, name: 'att2', label: 'attribute2', form_label: 'Attribute 2', output_display: 1, criteria_display: 1, search_type: 'field', operator: 'eq', type: 'string', min: 'two', display_detail: 1, id_criteria_family: 2, id_output_category: 1 } ] ); const action = searchActions.loadDefaultFormParameters(); actions = hot('-a', { a: action }); const defaultCriteriaList = [ {'id':1,'type':'field','operator':'eq','value':'one'}, {'id':2,'type':'field','operator':'eq','value':'two'} ]; const defaultConeSearch = null; const defaultOutputList = []; const expected = cold('-(bcde)', { b: searchActions.updateCriteriaList({ criteriaList: defaultCriteriaList }), c: coneSearchActions.addConeSearch({ coneSearch: defaultConeSearch }), d: searchActions.updateOutputList({ outputList: defaultOutputList }), e: searchActions.markAsDirty() }); expect(effects.loadDefaultFormParameters$).toBeObservable(expected); }); it('should set criteria list from URL', () => { mockSearchSelectorSelectPristine = store.overrideSelector( searchSelector.selectPristine, true ); mockSearchSelectorSelectCurrentDataset = store.overrideSelector( searchSelector.selectCurrentDataset, 'myDataset' ); mockAttributeSelectorSelectAllAttributes = store.overrideSelector( attributeSelector.selectAllAttributes, [ { id: 1, name: 'att1', label: 'attribute1', form_label: 'Attribute 1', output_display: 1, criteria_display: 1, search_type: 'field', operator: 'eq', type: 'string', min: 'one', display_detail: 1, id_criteria_family: 1, id_output_category: 1 }, { id: 2, name: 'att2', label: 'attribute2', form_label: 'Attribute 2', output_display: 1, criteria_display: 1, search_type: 'field', operator: 'eq', type: 'string', min: 'two', display_detail: 1, id_criteria_family: 2, id_output_category: 1 } ] ); mockSearchSelectorSelectCriteriaListByRoute = store.overrideSelector( searchSelector.selectCriteriaListByRoute, '1::eq::un;2::eq::deux' ); const action = searchActions.loadDefaultFormParameters(); actions = hot('-a', { a: action }); const criteriaList = [ {'id':1,'type':'field','operator':'eq','value':'un'}, {'id':2,'type':'field','operator':'eq','value':'deux'} ]; const defaultConeSearch = null; const defaultOutputList = []; const expected = cold('-(bcde)', { b: searchActions.updateCriteriaList({ criteriaList: criteriaList }), c: coneSearchActions.addConeSearch({ coneSearch: defaultConeSearch }), d: searchActions.updateOutputList({ outputList: defaultOutputList }), e: searchActions.markAsDirty() }); expect(effects.loadDefaultFormParameters$).toBeObservable(expected); }); it('should set cone search from URL', () => { mockSearchSelectorSelectPristine = store.overrideSelector( searchSelector.selectPristine, true ); mockSearchSelectorSelectCurrentDataset = store.overrideSelector( searchSelector.selectCurrentDataset, 'myDataset' ); mockConeSearchSelectorSelectConeSearchByRoute = store.overrideSelector( coneSearchSelector.selectConeSearchByRoute, '1:2:3' ); const action = searchActions.loadDefaultFormParameters(); actions = hot('-a', { a: action }); const defaultCriteriaList = []; const coneSearch = { ra: 1, dec: 2, radius: 3 }; const defaultOutputList = []; const expected = cold('-(bcde)', { b: searchActions.updateCriteriaList({ criteriaList: defaultCriteriaList }), c: coneSearchActions.addConeSearch({ coneSearch: coneSearch }), d: searchActions.updateOutputList({ outputList: defaultOutputList }), e: searchActions.markAsDirty() }); expect(effects.loadDefaultFormParameters$).toBeObservable(expected); }); it('should set a default output list', () => { mockSearchSelectorSelectPristine = store.overrideSelector( searchSelector.selectPristine, true ); mockSearchSelectorSelectCurrentDataset = store.overrideSelector( searchSelector.selectCurrentDataset, 'myDataset' ); mockAttributeSelectorSelectAllAttributes = store.overrideSelector( attributeSelector.selectAllAttributes, [ { id: 1, name: 'att1', label: 'attribute1', form_label: 'Attribute 1', output_display: 1, criteria_display: 1, search_type: 'field', operator: 'eq', type: 'string', display_detail: 1, selected: true, id_criteria_family: 1, id_output_category: 1 }, { id: 2, name: 'att2', label: 'attribute2', form_label: 'Attribute 2', output_display: 1, criteria_display: 1, search_type: 'field', operator: 'eq', type: 'string', display_detail: 1, selected: true, id_criteria_family: 2, id_output_category: 1 } ] ); const action = searchActions.loadDefaultFormParameters(); actions = hot('-a', { a: action }); const defaultCriteriaList = []; const defaultConeSearch = null; const defaultOutputList = [1, 2]; const expected = cold('-(bcde)', { b: searchActions.updateCriteriaList({ criteriaList: defaultCriteriaList }), c: coneSearchActions.addConeSearch({ coneSearch: defaultConeSearch }), d: searchActions.updateOutputList({ outputList: defaultOutputList }), e: searchActions.markAsDirty() }); expect(effects.loadDefaultFormParameters$).toBeObservable(expected); }); it('should set output list from URL', () => { mockSearchSelectorSelectPristine = store.overrideSelector( searchSelector.selectPristine, true ); mockSearchSelectorSelectCurrentDataset = store.overrideSelector( searchSelector.selectCurrentDataset, 'myDataset' ); mockSearchSelectorSelectOutputListByRoute = store.overrideSelector( searchSelector.selectOutputListByRoute, '1;2;3' ); const action = searchActions.loadDefaultFormParameters(); actions = hot('-a', { a: action }); const defaultCriteriaList = []; const defaultConeSearch = null; const outputList = [1, 2, 3]; const expected = cold('-(bcde)', { b: searchActions.updateCriteriaList({ criteriaList: defaultCriteriaList }), c: coneSearchActions.addConeSearch({ coneSearch: defaultConeSearch }), d: searchActions.updateOutputList({ outputList: outputList }), e: searchActions.markAsDirty() }); expect(effects.loadDefaultFormParameters$).toBeObservable(expected); }); }); describe('retrieveDataLength$ effect', () => { it('should dispatch the retrieveDataLengthSuccess action on success', () => { const action = searchActions.retrieveDataLength(); const outcome = searchActions.retrieveDataLengthSuccess({ length: 5 }); actions = hot('-a', { a: action }); const response = cold('-b|', { b: [{ nb: 5 }] }); const expected = cold('--c', { c: outcome }); searchService.retrieveDataLength = jest.fn(() => response); expect(effects.retrieveDataLength$).toBeObservable(expected); }); it('should dispatch the retrieveDataLengthFail action on failure', () => { const action = searchActions.retrieveDataLength(); const error = new Error(); const outcome = searchActions.retrieveDataLengthFail(); actions = hot('-a', { a: action }); const response = cold('-#|', {}, error); const expected = cold('--b', { b: outcome }); searchService.retrieveDataLength = jest.fn(() => response); expect(effects.retrieveDataLength$).toBeObservable(expected); }); it('should pass correct query to the service', () => { mockDatasetSelectorSelectDatasetNameByRoute = store.overrideSelector( datasetSelector.selectDatasetNameByRoute, 'myDataset' ); mockSearchSelectorSelectCriteriaList = store.overrideSelector( searchSelector.selectCriteriaList, [{'id':1,'type':'field','operator':'eq','value':'one'} as Criterion] ); mockConeSearchSelectorSelectConeSearch = store.overrideSelector( coneSearchSelector.selectConeSearch, { ra: 1, dec: 2, radius: 3 } ); jest.spyOn(searchService, 'retrieveDataLength'); const action = searchActions.retrieveDataLength(); const outcome = searchActions.retrieveDataLengthSuccess({ length: 5 }); actions = hot('-a', { a: action }); const response = cold('-b|', { b: [{ nb: 5 }] }); const expected = cold('--c', { c: outcome }); searchService.retrieveDataLength = jest.fn(() => response); expect(effects.retrieveDataLength$).toBeObservable(expected); expect(searchService.retrieveDataLength).toHaveBeenCalledTimes(1); expect(searchService.retrieveDataLength).toHaveBeenCalledWith('myDataset?a=count&c=1::eq::one&cs=1:2:3'); }); }); describe('retrieveDataLengthFail$ effect', () => { it('should not dispatch', () => { expect(metadata.retrieveDataLengthFail$).toEqual( expect.objectContaining({ dispatch: false }) ); }); it('should display a error notification', () => { const spy = jest.spyOn(toastr, 'error'); const action = searchActions.retrieveDataLengthFail(); actions = hot('a', { a: action }); const expected = cold('a', { a: action }); expect(effects.retrieveDataLengthFail$).toBeObservable(expected); expect(spy).toHaveBeenCalledTimes(1); expect(spy).toHaveBeenCalledWith('Loading Failed', 'The search data length loading failed'); }); }); describe('retrieveData$ effect', () => { it('should dispatch the retrieveDataSuccess action on success', () => { const action = searchActions.retrieveData( { pagination: { dname: 'myDatasetName', page: 1, nbItems: 10, sortedCol: 1, order: PaginationOrder.a } }); const outcome = searchActions.retrieveDataSuccess({ data: ['data'] }); actions = hot('-a', { a: action }); const response = cold('-b|', { b: ['data'] }); const expected = cold('--c', { c: outcome }); searchService.retrieveData = jest.fn(() => response); expect(effects.retrieveData$).toBeObservable(expected); }); it('should dispatch the retrieveDataFail action on failure', () => { const action = searchActions.retrieveData({ pagination: { dname: 'myDatasetName', page: 1, nbItems: 10, sortedCol: 1, order: PaginationOrder.a } }); const error = new Error(); const outcome = searchActions.retrieveDataFail(); actions = hot('-a', { a: action }); const response = cold('-#|', {}, error); const expected = cold('--b', { b: outcome }); searchService.retrieveData = jest.fn(() => response); expect(effects.retrieveData$).toBeObservable(expected); }); it('should pass correct query to the service', () => { mockDatasetSelectorSelectDatasetNameByRoute = store.overrideSelector( datasetSelector.selectDatasetNameByRoute, 'myDataset' ); mockSearchSelectorSelectCriteriaList = store.overrideSelector( searchSelector.selectCriteriaList, [{'id':1,'type':'field','operator':'eq','value':'one'} as Criterion] ); mockConeSearchSelectorSelectConeSearch = store.overrideSelector( coneSearchSelector.selectConeSearch, { ra: 1, dec: 2, radius: 3 } ); mockSearchSelectorSelectOutputList = store.overrideSelector( searchSelector.selectOutputList, [1, 2] ); jest.spyOn(searchService, 'retrieveData'); const action = searchActions.retrieveData({ pagination: { dname: 'myDatasetName', page: 1, nbItems: 10, sortedCol: 1, order: PaginationOrder.a } }); const outcome = searchActions.retrieveDataSuccess({ data: ['data'] }); actions = hot('-a', { a: action }); const response = cold('-b|', { b: ['data'] }); const expected = cold('--c', { c: outcome }); searchService.retrieveData = jest.fn(() => response); expect(effects.retrieveData$).toBeObservable(expected); expect(searchService.retrieveData).toHaveBeenCalledTimes(1); expect(searchService.retrieveData).toHaveBeenCalledWith('myDataset?a=1;2&c=1::eq::one&cs=1:2:3&p=10:1&o=1:a'); }); }); describe('retrieveDataFail$ effect', () => { it('should not dispatch', () => { expect(metadata.retrieveDataFail$).toEqual( expect.objectContaining({ dispatch: false }) ); }); it('should display a error notification', () => { const spy = jest.spyOn(toastr, 'error'); const action = searchActions.retrieveDataFail(); actions = hot('a', { a: action }); const expected = cold('a', { a: action }); expect(effects.retrieveDataFail$).toBeObservable(expected); expect(spy).toHaveBeenCalledTimes(1); expect(spy).toHaveBeenCalledWith('Loading Failed', 'The search data loading failed'); }); }); });