diff --git a/client/.editorconfig b/client/.editorconfig index 59d9a3a3e73ffc640517ef488f6f89d6270195d1..07926923085123e3d7ff394a8777e16563073379 100644 --- a/client/.editorconfig +++ b/client/.editorconfig @@ -4,7 +4,7 @@ root = true [*] charset = utf-8 indent_style = space -indent_size = 2 +indent_size = 4 insert_final_newline = true trim_trailing_whitespace = true diff --git a/client/package.json b/client/package.json index 1069d167dc4188e3a4e06442bc7e19522d57a83f..25ca90b815007477c3345dd23a5ed251ff25f31d 100644 --- a/client/package.json +++ b/client/package.json @@ -44,6 +44,7 @@ "@types/jest": "^26.0.24", "@types/node": "^12.11.1", "jasmine-core": "~3.7.0", + "jasmine-marbles": "^0.8.3", "jest": "^27.0.6", "jest-preset-angular": "^9.0.5", "typescript": "~4.2.3" diff --git a/client/src/app/core/containers/app.component.html b/client/src/app/core/containers/app.component.html index 7aed41a286f0dfa4d37cef43b006c02d5d90e005..50bf7c4d2a8737dbae1e9a568c4e701b33752542 100644 --- a/client/src/app/core/containers/app.component.html +++ b/client/src/app/core/containers/app.component.html @@ -18,7 +18,7 @@ </a> </div> <div class="col text-center"> - <a href="http://lam.oamp.fr" title="Laboratoire d'Astrophysique de Marseille"> + <a href="http://lam.fr" title="Laboratoire d'Astrophysique de Marseille"> <img class="img-fluid" src="assets/logo_lam_s.png" alt="LAM" /> </a> </div> diff --git a/client/src/app/instance/store/effects/cone-search.effects.spec.ts b/client/src/app/instance/store/effects/cone-search.effects.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..172bacf70f13ad455540c59936d79069b7ca5d77 --- /dev/null +++ b/client/src/app/instance/store/effects/cone-search.effects.spec.ts @@ -0,0 +1,129 @@ +import { TestBed } from '@angular/core/testing'; + +import { provideMockActions } from '@ngrx/effects/testing'; +import { EffectsMetadata, getEffectsMetadata } from '@ngrx/effects'; +import { Observable } from 'rxjs'; +import { cold, hot } from 'jasmine-marbles'; +import { ToastrService } from 'ngx-toastr'; + +import { ConeSearchEffects } from './cone-search.effects'; +import { ConeSearchService } from '../services/cone-search.service'; +import * as coneSearchActions from '../actions/cone-search.actions'; +import { Resolver } from '../models'; + +describe('ConeSearchEffects', () => { + let actions = new Observable(); + let effects: ConeSearchEffects; + let metadata: EffectsMetadata<ConeSearchEffects>; + let coneSearchService: ConeSearchService; + let toastr: ToastrService; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + ConeSearchEffects, + { provide: ConeSearchService, useValue: { retrieveCoordinates: jest.fn() }}, + { provide: ToastrService, useValue: { error: jest.fn() } }, + provideMockActions(() => actions) + ] + }).compileComponents(); + effects = TestBed.inject(ConeSearchEffects); + metadata = getEffectsMetadata(effects); + coneSearchService = TestBed.inject(ConeSearchService); + toastr = TestBed.inject(ToastrService); + }); + + it('should be created', () => { + expect(effects).toBeTruthy(); + }); + + describe('retrieveCoordinates$ effect', () => { + it('should dispatch the retrieveCoordinatesSuccess action on success', () => { + const name: string = 'myObjectName'; + const apiResponse = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + + "<Sesame xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n" + + " xsi:noNamespaceSchemaLocation=\"http://vizier.u-strasbg.fr/xml/sesame_4x.xsd\">\n" + + "<Target option=\"NSV\">\n" + + " <name>myObjectName</name>\n" + + " <!-- Q1525761 #1 -->\n" + + " <Resolver name=\"N=NED\"><!--delay: 989ms [2] -->\n" + + " <otype>!*</otype>\n" + + " <jpos>06:45:09.24 -16:42:47.3</jpos>\n" + + " <jradeg>1</jradeg>\n" + + " <jdedeg>2</jdedeg>\n" + + " <refPos>2007A&A...474..653V</refPos>\n" + + " <errRAmas>50</errRAmas><errDEmas>500</errDEmas>\n" + + " <oname>Sirius</oname>\n" + + " </Resolver>\n" + + "</Target>\n" + + "</Sesame>\n" + + "<!--- ====Done (2021-Aug-04,15:00:56z)==== -->\n"; + const resolver: Resolver = { name: 'myObjectName', ra: 1, dec: 2 }; + const action = coneSearchActions.retrieveCoordinates({ name }); + const outcome = coneSearchActions.retrieveCoordinatesSuccess({ resolver }); + + actions = hot('-a', { a: action }); + const response = cold('-a|', { a: apiResponse }); + const expected = cold('--b', { b: outcome }); + coneSearchService.retrieveCoordinates = jest.fn(() => response); + + expect(effects.retrieveCoordinates$).toBeObservable(expected); + }); + + it('should dispatch the retrieveCoordinatesFail action on HTTP failure', () => { + const name: string = 'myObjectName'; + const action = coneSearchActions.retrieveCoordinates({ name }); + const error = new Error(); + const outcome = coneSearchActions.retrieveCoordinatesFail(); + + actions = hot('-a', { a: action }); + const response = cold('-#|', {}, error); + const expected = cold('--b', { b: outcome }); + coneSearchService.retrieveCoordinates = jest.fn(() => response); + + expect(effects.retrieveCoordinates$).toBeObservable(expected); + }); + + it('should dispatch the retrieveCoordinatesFail action on process failure', () => { + const name: string = 'myObjectName'; + const apiResponse = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + + "<Sesame xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n" + + " xsi:noNamespaceSchemaLocation=\"http://vizier.u-strasbg.fr/xml/sesame_4x.xsd\">\n" + + "<Target option=\"NSV\">\n" + + " <name>myObjectName</name>\n" + + " <!-- Q1525761 #1 -->\n" + + "</Target>\n" + + "</Sesame>\n" + + "<!--- ====Done (2021-Aug-04,15:00:56z)==== -->\n"; + const action = coneSearchActions.retrieveCoordinates({ name }); + const outcome = coneSearchActions.retrieveCoordinatesFail(); + + actions = hot('-a', { a: action }); + const response = cold('-a|', { a: apiResponse }); + const expected = cold('--b', { b: outcome }); + coneSearchService.retrieveCoordinates = jest.fn(() => response); + + expect(effects.retrieveCoordinates$).toBeObservable(expected); + }); + }); + + describe('retrieveCoordinatesFail$ effect', () => { + it('should not dispatch', () => { + expect(metadata.retrieveCoordinatesFail$).toEqual( + expect.objectContaining({ dispatch: false }) + ); + }); + + it('should display an error notification', () => { + const spy = jest.spyOn(toastr, 'error'); + const action = coneSearchActions.retrieveCoordinatesFail(); + + actions = hot('a', { a: action }); + const expected = cold('a', { a: action }); + + expect(effects.retrieveCoordinatesFail$).toBeObservable(expected); + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith('Failure to retrieve coordinates', 'The coordinates could not be retrieved'); + }); + }); +}); diff --git a/client/src/app/instance/store/effects/cone-search.effects.ts b/client/src/app/instance/store/effects/cone-search.effects.ts index 84d2ab8fe05cc36bdba788ba155cd47ae0c7270d..cf07473456a1f6fc83a62a79553ca5f033efff4a 100644 --- a/client/src/app/instance/store/effects/cone-search.effects.ts +++ b/client/src/app/instance/store/effects/cone-search.effects.ts @@ -17,9 +17,17 @@ import { ToastrService } from 'ngx-toastr'; import * as coneSearchActions from '../actions/cone-search.actions'; import { ConeSearchService } from '../services/cone-search.service'; +/** + * @class + * @classdesc Cone search effects. + */ @Injectable() export class ConeSearchEffects { - retrieveCoordinates$ = createEffect(() => + + /** + * Calls actions to retrieve object coordinates. + */ + retrieveCoordinates$ = createEffect((): any => this.actions$.pipe( ofType(coneSearchActions.retrieveCoordinates), mergeMap((action) => this.coneSearchService.retrieveCoordinates(action.name) @@ -28,7 +36,6 @@ export class ConeSearchEffects { const parser = new DOMParser(); const xml = parser.parseFromString(response,'text/xml'); if (xml.getElementsByTagName('Resolver').length === 0) { - const name = xml.getElementsByTagName('name')[0].childNodes[0].nodeValue; return coneSearchActions.retrieveCoordinatesFail(); } const name = xml.getElementsByTagName('name')[0].childNodes[0].nodeValue; @@ -43,11 +50,14 @@ export class ConeSearchEffects { ) ); + /** + * Displays retrieve object coordinates error notification. + */ retrieveCoordinatesFail$ = createEffect(() => this.actions$.pipe( ofType(coneSearchActions.retrieveCoordinatesFail), tap(() => this.toastr.error('Failure to retrieve coordinates', 'The coordinates could not be retrieved')) - ), { dispatch: false} + ), { dispatch: false } ); constructor( diff --git a/client/src/app/instance/store/effects/detail.effects.spec.ts b/client/src/app/instance/store/effects/detail.effects.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..af0058697e73fc95cebfd2a9e3c859e5a40ccd25 --- /dev/null +++ b/client/src/app/instance/store/effects/detail.effects.spec.ts @@ -0,0 +1,212 @@ +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 { DetailEffects } from './detail.effects'; +import { DetailService } from '../services/detail.service'; +import * as detailActions from '../actions/detail.actions'; +import * as detailSelector from '../selectors/detail.selector'; +import * as attributeSelector from 'src/app/metamodel/selectors/attribute.selector'; +import * as datasetSelector from 'src/app/metamodel/selectors/dataset.selector'; +import * as fromDetail from '../reducers/detail.reducer'; +import * as fromMetamodel from '../../../metamodel/metamodel.reducer'; + +describe('DetailEffects', () => { + let actions = new Observable(); + let effects: DetailEffects; + let metadata: EffectsMetadata<DetailEffects>; + let detailService: DetailService; + let store: MockStore; + let toastr: ToastrService; + let mockDatasetSelectorSelectDatasetNameByRoute; + let mockAttributeSelectorSelectAllAttributes; + let mockDetailSelectorSelectIdByRoute; + const initialState = { + metamodel: { ...fromMetamodel.getMetamodelState }, + instance: { + detail: { ...fromDetail.initialState } + } + }; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + DetailEffects, + { provide: DetailService, useValue: { + retrieveObject: jest.fn(), + retrieveSpectra: jest.fn() + }}, + { provide: ToastrService, useValue: { error: jest.fn() }}, + provideMockActions(() => actions), + provideMockStore({ initialState }), + ] + }).compileComponents(); + effects = TestBed.inject(DetailEffects); + metadata = getEffectsMetadata(effects); + detailService = TestBed.inject(DetailService); + store = TestBed.inject(MockStore); + toastr = TestBed.inject(ToastrService); + mockDatasetSelectorSelectDatasetNameByRoute = store.overrideSelector( + datasetSelector.selectDatasetNameByRoute,'' + ); + mockAttributeSelectorSelectAllAttributes = store.overrideSelector( + attributeSelector.selectAllAttributes,[] + ); + mockDetailSelectorSelectIdByRoute = store.overrideSelector( + detailSelector.selectIdByRoute,1 + ); + }); + + it('should be created', () => { + expect(effects).toBeTruthy(); + }); + + describe('retrieveObject$ effect', () => { + it('should dispatch the retrieveObjectSuccess action on success', () => { + mockDatasetSelectorSelectDatasetNameByRoute = store.overrideSelector( + datasetSelector.selectDatasetNameByRoute, 'myDatasetName' + ); + mockAttributeSelectorSelectAllAttributes = store.overrideSelector( + attributeSelector.selectAllAttributes, [{ + id: 1, + name: 'myFirstAttribute', + label: 'My First Attribute', + form_label: 'My First Attribute', + output_display: 1, + criteria_display: 1, + type: 'type', + display_detail: 1, + order_by: true, + detail: true + }] + ); + mockDetailSelectorSelectIdByRoute = store.overrideSelector( + detailSelector.selectIdByRoute, 1 + ); + + const action = detailActions.retrieveObject(); + const outcome = detailActions.retrieveObjectSuccess({ object: 'myObject' }); + + actions = hot('-a', { a: action }); + const response = cold('-b|', { b: ['myObject'] }); + const expected = cold('--c', { c: outcome }); + detailService.retrieveObject = jest.fn(() => response); + + expect(effects.retrieveObject$).toBeObservable(expected); + }); + + it('should dispatch the retrieveObjectFail action on failure', () => { + mockDatasetSelectorSelectDatasetNameByRoute = store.overrideSelector( + datasetSelector.selectDatasetNameByRoute, 'myDatasetName' + ); + mockAttributeSelectorSelectAllAttributes = store.overrideSelector( + attributeSelector.selectAllAttributes, [{ + id: 1, + name: 'myFirstAttribute', + label: 'My First Attribute', + form_label: 'My First Attribute', + output_display: 1, + criteria_display: 1, + type: 'type', + display_detail: 1, + order_by: true, + detail: true + }] + ); + mockDetailSelectorSelectIdByRoute = store.overrideSelector( + detailSelector.selectIdByRoute, 1 + ); + + const action = detailActions.retrieveObject(); + const error = new Error(); + const outcome = detailActions.retrieveObjectFail(); + + actions = hot('-a', { a: action }); + const response = cold('-#|', {}, error); + const expected = cold('--b', { b: outcome }); + detailService.retrieveObject = jest.fn(() => response); + + expect(effects.retrieveObject$).toBeObservable(expected); + }); + }); + + describe('retrieveObjectFail$ effect', () => { + it('should not dispatch', () => { + expect(metadata.retrieveObjectFail$).toEqual( + expect.objectContaining({ dispatch: false }) + ); + }); + + it('should display an error notification', () => { + const spy = jest.spyOn(toastr, 'error'); + const action = detailActions.retrieveObjectFail(); + + actions = hot('a', { a: action }); + const expected = cold('a', { a: action }); + + expect(effects.retrieveObjectFail$).toBeObservable(expected); + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith('Loading Failed!', 'Unable to load the object'); + }); + }); + + describe('retrieveSpectra$ effect', () => { + it('should dispatch the retrieveSpectraSuccess action on success', () => { + mockDatasetSelectorSelectDatasetNameByRoute = store.overrideSelector( + datasetSelector.selectDatasetNameByRoute, 'myDatasetName' + ); + + const action = detailActions.retrieveSpectra({ filename: 'mySpectraFilename' }); + const outcome = detailActions.retrieveSpectraSuccess({ spectraCSV: 'mySpectraFile' }); + + actions = hot('-a', { a: action }); + const response = cold('-b|', { b: 'mySpectraFile' }); + const expected = cold('--c', { c: outcome }); + detailService.retrieveSpectra = jest.fn(() => response); + + expect(effects.retrieveSpectra$).toBeObservable(expected); + }); + + it('should dispatch the retrieveSpectraFail action on failure', () => { + mockDatasetSelectorSelectDatasetNameByRoute = store.overrideSelector( + datasetSelector.selectDatasetNameByRoute, 'myDatasetName' + ); + + const action = detailActions.retrieveSpectra({ filename: 'mySpectraFilename' }); + const error = new Error(); + const outcome = detailActions.retrieveSpectraFail(); + + actions = hot('-a', { a: action }); + const response = cold('-#|', {}, error); + const expected = cold('--b', { b: outcome }); + detailService.retrieveSpectra = jest.fn(() => response); + + expect(effects.retrieveSpectra$).toBeObservable(expected); + }); + }); + + describe('retrieveSpectraFail$ effect', () => { + it('should not dispatch', () => { + expect(metadata.retrieveSpectraFail$).toEqual( + expect.objectContaining({ dispatch: false }) + ); + }); + + it('should display an error notification', () => { + const spy = jest.spyOn(toastr, 'error'); + const action = detailActions.retrieveSpectraFail(); + + actions = hot('a', { a: action }); + const expected = cold('a', { a: action }); + + expect(effects.retrieveSpectraFail$).toBeObservable(expected); + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith('Loading Failed!', 'Unable to load spectra'); + }); + }); +}); diff --git a/client/src/app/instance/store/effects/detail.effects.ts b/client/src/app/instance/store/effects/detail.effects.ts index 256412aea966b727115af6394a73795b181e22e7..2a18c7ff61ff9392494d74f4b1ab4313944ed118 100644 --- a/client/src/app/instance/store/effects/detail.effects.ts +++ b/client/src/app/instance/store/effects/detail.effects.ts @@ -20,10 +20,18 @@ import * as detailActions from '../actions/detail.actions'; import * as detailSelector from '../selectors/detail.selector'; import * as attributeSelector from 'src/app/metamodel/selectors/attribute.selector'; import * as datasetSelector from 'src/app/metamodel/selectors/dataset.selector'; - + +/** + * @class + * @classdesc Detail effects. + */ @Injectable() export class DetailEffects { - retrieveObject$ = createEffect(() => + + /** + * Calls actions to retrieve object. + */ + retrieveObject$ = createEffect((): any => this.actions$.pipe( ofType(detailActions.retrieveObject), concatLatestFrom(() => [ @@ -43,6 +51,9 @@ export class DetailEffects { ) ); + /** + * Displays retrieve object error notification. + */ retrieveObjectFail$ = createEffect(() => this.actions$.pipe( ofType(detailActions.retrieveObjectFail), @@ -50,7 +61,10 @@ export class DetailEffects { ), { dispatch: false} ); - retrieveSpectra$ = createEffect(() => + /** + * Calls actions to retrieve spectra. + */ + retrieveSpectra$ = createEffect((): any => this.actions$.pipe( ofType(detailActions.retrieveSpectra), concatLatestFrom(() => this.store.select(datasetSelector.selectDatasetNameByRoute)), @@ -64,6 +78,9 @@ export class DetailEffects { ) ); + /** + * Displays retrieve spectra error notification. + */ retrieveSpectraFail$ = createEffect(() => this.actions$.pipe( ofType(detailActions.retrieveSpectraFail), diff --git a/client/src/app/instance/store/effects/samp.effects.spec.ts b/client/src/app/instance/store/effects/samp.effects.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..918908041acfa23c5658359f39b0a4fc6c57b163 --- /dev/null +++ b/client/src/app/instance/store/effects/samp.effects.spec.ts @@ -0,0 +1,150 @@ +import { TestBed } from '@angular/core/testing'; + +import { provideMockActions } from '@ngrx/effects/testing'; +import { EffectsMetadata, getEffectsMetadata } from '@ngrx/effects'; +import { Observable } from 'rxjs'; +import { cold, hot } from 'jasmine-marbles'; +import { ToastrService } from 'ngx-toastr'; + +import { SampEffects } from './samp.effects'; +import { SampService } from '../services/samp.service'; +import * as sampActions from '../actions/samp.actions'; + +describe('SampEffects', () => { + let actions = new Observable(); + let effects: SampEffects; + let metadata: EffectsMetadata<SampEffects>; + let sampService: SampService; + let toastr: ToastrService; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + SampEffects, + { provide: SampService, useValue: { + register: jest.fn(), + unregister: jest.fn(), + broadcast: jest.fn() + }}, + { provide: ToastrService, useValue: { + success: jest.fn(), + error: jest.fn() + }}, + provideMockActions(() => actions) + ] + }).compileComponents(); + effects = TestBed.inject(SampEffects); + metadata = getEffectsMetadata(effects); + sampService = TestBed.inject(SampService); + toastr = TestBed.inject(ToastrService); + }); + + it('should be created', () => { + expect(effects).toBeTruthy(); + }); + + describe('register$ effect', () => { + it('should dispatch the registerSuccess action on success', () => { + const action = sampActions.register(); + const outcome = sampActions.registerSuccess(); + + actions = hot('-a', { a: action }); + const response = cold('-a|', { a: action }); + const expected = cold('--b', { b: outcome }); + sampService.register = jest.fn(() => response); + + expect(effects.register$).toBeObservable(expected); + }); + + it('should dispatch the registerFail action on failure', () => { + const action = sampActions.register(); + const error = new Error(); + const outcome = sampActions.registerFail(); + + actions = hot('-a', { a: action }); + const response = cold('-#|', {}, error); + const expected = cold('--b', { b: outcome }); + sampService.register = jest.fn(() => response); + + expect(effects.register$).toBeObservable(expected); + }); + }); + + describe('registerSuccess$ effect', () => { + it('should not dispatch', () => { + expect(metadata.registerSuccess$).toEqual( + expect.objectContaining({ dispatch: false }) + ); + }); + + it('should display a success notification', () => { + const spy = jest.spyOn(toastr, 'success'); + const action = sampActions.registerSuccess(); + + actions = hot('a', { a: action }); + const expected = cold('a', { a: action }); + + expect(effects.registerSuccess$).toBeObservable(expected); + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith('You are now connected to a SAMP-hub', 'SAMP-hub register success'); + }); + }); + + describe('registerFail$ effect', () => { + it('should not dispatch', () => { + expect(metadata.registerFail$).toEqual( + expect.objectContaining({ dispatch: false }) + ); + }); + + it('should display a error notification', () => { + const spy = jest.spyOn(toastr, 'error'); + const action = sampActions.registerFail(); + + actions = hot('a', { a: action }); + const expected = cold('a', { a: action }); + + expect(effects.registerFail$).toBeObservable(expected); + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith('Connection to a SAMP-hub has failed', 'SAMP-hub register fail'); + }); + }); + + describe('unregister$ effect', () => { + it('should not dispatch', () => { + expect(metadata.registerFail$).toEqual( + expect.objectContaining({ dispatch: false }) + ); + }); + + it('should call unregister from sampService', () => { + const spy = jest.spyOn(sampService, 'unregister'); + const action = sampActions.unregister(); + + actions = hot('a', { a: action }); + const expected = cold('a', { a: action }); + + expect(effects.unregister$).toBeObservable(expected); + expect(spy).toHaveBeenCalledTimes(1); + }); + }); + + describe('broadcastVotable$ effect', () => { + it('should not dispatch', () => { + expect(metadata.registerFail$).toEqual( + expect.objectContaining({ dispatch: false }) + ); + }); + + it('should call broadcast from sampService', () => { + const spy = jest.spyOn(sampService, 'broadcast'); + const action = sampActions.broadcastVotable({ url: 'url' }); + + actions = hot('a', { a: action }); + const expected = cold('a', { a: action }); + + expect(effects.broadcastVotable$).toBeObservable(expected); + expect(spy).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/client/src/app/instance/store/effects/samp.effects.ts b/client/src/app/instance/store/effects/samp.effects.ts index f98e2bcb2b70a690e06375e3057d3d9d46adfb26..6d685853788c586e1bbc8845d8e794c48c0087fa 100644 --- a/client/src/app/instance/store/effects/samp.effects.ts +++ b/client/src/app/instance/store/effects/samp.effects.ts @@ -8,6 +8,7 @@ */ import { Injectable } from '@angular/core'; + import { Actions, createEffect, ofType } from '@ngrx/effects'; import { of } from 'rxjs'; import { map, tap, mergeMap, catchError } from 'rxjs/operators'; @@ -15,10 +16,18 @@ import { ToastrService } from 'ngx-toastr'; import { SampService } from '../services/samp.service'; import * as sampActions from '../actions/samp.actions'; - + +/** + * @class + * @classdesc Samp effects. + */ @Injectable() export class SampEffects { - register$ = createEffect(() => + + /** + * Calls actions to register. + */ + register$ = createEffect((): any => this.actions$.pipe( ofType(sampActions.register), mergeMap(() => this.sampService.register() @@ -30,6 +39,9 @@ export class SampEffects { ) ); + /** + * Displays register success notification. + */ registerSuccess$ = createEffect(() => this.actions$.pipe( ofType(sampActions.registerSuccess), @@ -38,6 +50,9 @@ export class SampEffects { { dispatch: false } ); + /** + * Displays register error notification. + */ registerFail$ = createEffect(() => this.actions$.pipe( ofType(sampActions.registerFail), @@ -46,6 +61,9 @@ export class SampEffects { { dispatch: false } ); + /** + * Calls actions to disconnect. + */ unregister$ = createEffect(() => this.actions$.pipe( ofType(sampActions.unregister), @@ -56,6 +74,9 @@ export class SampEffects { { dispatch: false } ); + /** + * Calls actions to broadcast. + */ broadcastVotable$ = createEffect(() => this.actions$.pipe( ofType(sampActions.broadcastVotable), diff --git a/client/src/app/instance/store/effects/search.effects.spec.ts b/client/src/app/instance/store/effects/search.effects.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..56bc30071a45ef3e0927c3e752f099da04123ed3 --- /dev/null +++ b/client/src/app/instance/store/effects/search.effects.spec.ts @@ -0,0 +1,662 @@ +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'); + }); + }); +}); diff --git a/client/src/app/instance/store/effects/search.effects.ts b/client/src/app/instance/store/effects/search.effects.ts index b92513c667482841cf169ebeef11fa595520adc9..42fe09265780b2421e4e2741f9e94064ec580950 100644 --- a/client/src/app/instance/store/effects/search.effects.ts +++ b/client/src/app/instance/store/effects/search.effects.ts @@ -28,9 +28,17 @@ import * as searchSelector from '../selectors/search.selector'; import * as coneSearchActions from '../actions/cone-search.actions'; import * as coneSearchSelector from '../selectors/cone-search.selector'; +/** + * @class + * @classdesc Search effects. + */ @Injectable() export class SearchEffects { - initSearch$ = createEffect(() => + + /** + * Calls actions to initialize search. + */ + initSearch$ = createEffect((): any => this.actions$.pipe( ofType(searchActions.initSearch), concatLatestFrom(() => [ @@ -79,14 +87,20 @@ export class SearchEffects { ) ); - restartSearch$ = createEffect(() => + /** + * Calls actions to restart search. + */ + restartSearch$ = createEffect((): any => this.actions$.pipe( ofType(searchActions.restartSearch), map(() => searchActions.initSearch()) ) ); - loadDefaultFormParameters$ = createEffect(() => + /** + * Calls actions to load default form parameters. + */ + loadDefaultFormParameters$ = createEffect((): any => this.actions$.pipe( ofType(searchActions.loadDefaultFormParameters), concatLatestFrom(() => [ @@ -98,8 +112,8 @@ export class SearchEffects { this.store.select(searchSelector.selectOutputListByRoute) ]), mergeMap(([action, pristine, currentDataset, attributeList, criteriaList, coneSearch, outputList]) => { + // Default form parameters already loaded or no dataset selected if (!pristine || !currentDataset) { - // Default form parameters already loaded or no dataset selected return of({ type: '[No Action] Load Default Form Parameters' }); } @@ -153,7 +167,10 @@ export class SearchEffects { ) ); - retrieveDataLength$ = createEffect(() => + /** + * Calls actions to retrieve data length. + */ + retrieveDataLength$ = createEffect((): any => this.actions$.pipe( ofType(searchActions.retrieveDataLength), concatLatestFrom(() => [ @@ -179,6 +196,9 @@ export class SearchEffects { ) ); + /** + * Displays retrieve data length error notification. + */ retrieveDataLengthFail$ = createEffect(() => this.actions$.pipe( ofType(searchActions.retrieveDataLengthFail), @@ -186,7 +206,10 @@ export class SearchEffects { ), { dispatch: false} ); - retrieveData$ = createEffect(() => + /** + * Calls actions to retrieve data. + */ + retrieveData$ = createEffect((): any => this.actions$.pipe( ofType(searchActions.retrieveData), concatLatestFrom(() => [ @@ -216,6 +239,9 @@ export class SearchEffects { ) ); + /** + * Displays retrieve data error notification. + */ retrieveDataFail$ = createEffect(() => this.actions$.pipe( ofType(searchActions.retrieveDataFail), diff --git a/client/src/app/instance/store/models/criterion.model.spec.ts b/client/src/app/instance/store/models/criterion.model.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..707c8dc6ea26f29dab74835a1913a4f33030abae --- /dev/null +++ b/client/src/app/instance/store/models/criterion.model.spec.ts @@ -0,0 +1,235 @@ +import { criterionToString, stringToCriterion, getPrettyCriterion, getPrettyOperator } from './'; +import { + FieldCriterion, + JsonCriterion, + SelectMultipleCriterion, + BetweenCriterion, + Criterion, + ListCriterion +} from '.'; +import { Attribute } from '../../../metamodel/models'; + +describe('CriterionModel', () => { + it('#criterionToString should convert criterion object to string', () => { + let betweenCriterion: BetweenCriterion = { id: 1, type: 'between', min: 'un', max: null }; + let expected = '1::gte::un'; + expect(criterionToString(betweenCriterion)).toEqual(expected); + betweenCriterion = { id: 1, type: 'between', min: null, max: 'deux' }; + expected = '1::lte::deux'; + expect(criterionToString(betweenCriterion)).toEqual(expected); + betweenCriterion = { id: 1, type: 'between', min: 'un', max: 'deux' }; + expected = '1::bw::un|deux'; + expect(criterionToString(betweenCriterion)).toEqual(expected); + const fieldCriterion = { id: 1, type: 'field', operator: 'eq', value: 'value' } as FieldCriterion; + expected = '1::eq::value'; + expect(criterionToString(fieldCriterion)).toEqual(expected); + const listCriterion = { id: 1, type: 'list', values: ['un', 'deux'] } as ListCriterion; + expected = '1::in::un|deux'; + expect(criterionToString(listCriterion)).toEqual(expected); + const jsonCriterion = { id: 1, type: 'json', path: 'path', operator: 'eq', value: 'value' } as JsonCriterion; + expected = '1::js::path|eq|value'; + expect(criterionToString(jsonCriterion)).toEqual(expected); + const selectMultipleCriterion = { id: 1, type: 'multiple', options: [{ label: 'un', value: '1', display: 1 }, { label: 'deux', value: '2', display: 2 }] } as SelectMultipleCriterion; + expected = '1::in::1|2'; + expect(criterionToString(selectMultipleCriterion)).toEqual(expected); + }); + + it('#stringToCriterion should convert string criterion into object', () => { + let attribute: Attribute = { + id: 1, + name: 'myAttribute', + label: 'my attribute', + form_label: 'My Attribute', + output_display: 1, + criteria_display: 1, + search_type: 'field', + operator: 'eq', + type: 'field', + display_detail: 1 + }; + let expected: Criterion = { id: 1, type: 'field', operator: 'neq', value: 'otherValue' } as FieldCriterion; + expect(stringToCriterion(attribute, ['', 'neq', 'otherValue'])).toEqual(expected); + attribute = { + id: 1, + name: 'myAttribute', + label: 'my attribute', + form_label: 'My Attribute', + output_display: 1, + criteria_display: 1, + search_type: 'field', + operator: 'eq', + type: 'field', + min: 'value', + display_detail: 1 + }; + expected = { id: 1, type: 'field', operator: 'eq', value: 'value' } as FieldCriterion; + expect(stringToCriterion(attribute)).toEqual(expected); + attribute = { + id: 1, + name: 'myAttribute', + label: 'my attribute', + form_label: 'My Attribute', + output_display: 1, + criteria_display: 1, + search_type: 'list', + operator: 'eq', + type: 'field', + display_detail: 1 + }; + expected = { id: 1, type: 'list', values: ['one', 'two'] } as ListCriterion; + expect(stringToCriterion(attribute, ['', '', 'one|two'])).toEqual(expected); + attribute = { + id: 1, + name: 'myAttribute', + label: 'my attribute', + form_label: 'My Attribute', + output_display: 1, + criteria_display: 1, + search_type: 'list', + operator: 'eq', + type: 'field', + min: 'valueA|valueB', + display_detail: 1 + }; + expected = { id: 1, type: 'list', values: ['valueA', 'valueB'] } as ListCriterion; + expect(stringToCriterion(attribute)).toEqual(expected); + attribute = { + id: 1, + name: 'myAttribute', + label: 'my attribute', + form_label: 'My Attribute', + output_display: 1, + criteria_display: 1, + search_type: 'between', + operator: 'eq', + type: 'field', + min: 'one', + max: 'two', + display_detail: 1 + }; + expected = { id: 1, type: 'between', min: 'valueA', max: 'valueB' } as BetweenCriterion; + expect(stringToCriterion(attribute, ['', 'bw', 'valueA|valueB'])).toEqual(expected); + expected = { id: 1, type: 'between', min: 'valueA', max: null } as BetweenCriterion; + expect(stringToCriterion(attribute, ['', 'gte', 'valueA'])).toEqual(expected); + expected = { id: 1, type: 'between', min: null, max: 'valueB' } as BetweenCriterion; + expect(stringToCriterion(attribute, ['', 'lte', 'valueB'])).toEqual(expected); + expected = { id: 1, type: 'between', min: 'one', max: 'two' } as BetweenCriterion; + expect(stringToCriterion(attribute)).toEqual(expected); + attribute = { + id: 1, + name: 'myAttribute', + label: 'my attribute', + form_label: 'My Attribute', + output_display: 1, + criteria_display: 1, + search_type: 'select-multiple', + operator: 'eq', + type: 'field', + min: '1|2', + display_detail: 1, + options: [{ label: 'one', value: '1', display: 1 }, { label: 'two', value: '2', display: 2 }, { label: 'three', value: '3', display: 3 }] + }; + expected = { id: 1, type: 'multiple', options: [{ label: 'one', value: '1', display: 1 }, { label: 'three', value: '3', display: 3 }] } as SelectMultipleCriterion; + expect(stringToCriterion(attribute, ['', '', '1|3'])).toEqual(expected); + attribute = { + id: 1, + name: 'myAttribute', + label: 'my attribute', + form_label: 'My Attribute', + output_display: 1, + criteria_display: 1, + search_type: 'select-multiple', + operator: 'eq', + type: 'field', + min: '1|2', + display_detail: 1, + options: [{ label: 'one', value: '1', display: 1 }, { label: 'two', value: '2', display: 2 }, { label: 'three', value: '3', display: 3 }] + }; + expected = { id: 1, type: 'multiple', options: [{ label: 'one', value: '1', display: 1 }, { label: 'two', value: '2', display: 2 }] } as SelectMultipleCriterion; + expect(stringToCriterion(attribute)).toEqual(expected); + attribute = { + id: 1, + name: 'myAttribute', + label: 'my attribute', + form_label: 'My Attribute', + output_display: 1, + criteria_display: 1, + search_type: 'json', + operator: 'eq', + type: 'field', + display_detail: 1 + }; + expected = { id: 1, type: 'json', path: 'path', operator: 'op', value: 'value' } as JsonCriterion; + expect(stringToCriterion(attribute, ['', '', 'path|op|value'])).toEqual(expected); + attribute = { + id: 1, + name: 'myAttribute', + label: 'my attribute', + form_label: 'My Attribute', + output_display: 1, + criteria_display: 1, + search_type: 'json', + operator: 'eq', + type: 'field', + min: 'path|op|value', + display_detail: 1 + }; + expect(stringToCriterion(attribute)).toEqual(expected); + attribute = { + id: 1, + name: 'myAttribute', + label: 'my attribute', + form_label: 'My Attribute', + output_display: 1, + criteria_display: 1, + search_type: '', + operator: 'eq', + type: 'field', + display_detail: 1 + }; + expect(stringToCriterion(attribute)).toBeNull(); + }); + + it('#getPrettyCriterion should print criterion correctly', () => { + const fieldCriterion = { id: 1, type: 'field', operator: 'eq', value: 'value' } as FieldCriterion; + let expectedPrintedCriterion = '= value'; + expect(getPrettyCriterion(fieldCriterion)).toEqual(expectedPrintedCriterion); + const jsonCriterion = { id: 1, type: 'json', path: 'path', operator: 'eq', value: 'value' } as JsonCriterion; + expectedPrintedCriterion = 'path eq value'; + expect(getPrettyCriterion(jsonCriterion)).toEqual(expectedPrintedCriterion); + const selectMultipleCriterion = { id: 1, type: 'multiple', options: [{label: 'un', value: '1', display: 1}, {label: 'deux', value: '2', display: 2}] } as SelectMultipleCriterion; + expectedPrintedCriterion = '[un,deux]'; + expect(getPrettyCriterion(selectMultipleCriterion)).toEqual(expectedPrintedCriterion); + let betweenCriterion = { id: 1, type: 'between', min: 'un', max: null } as BetweenCriterion; + expectedPrintedCriterion = '>= un'; + expect(getPrettyCriterion(betweenCriterion)).toEqual(expectedPrintedCriterion); + betweenCriterion = { id: 1, type: 'between', min: null, max: 'deux' } as BetweenCriterion; + expectedPrintedCriterion = '<= deux'; + expect(getPrettyCriterion(betweenCriterion)).toEqual(expectedPrintedCriterion); + betweenCriterion = { id: 1, type: 'between', min: 'un', max: 'deux' } as BetweenCriterion; + expectedPrintedCriterion = '∈ [un;deux]'; + expect(getPrettyCriterion(betweenCriterion)).toEqual(expectedPrintedCriterion); + const listCriterion = { id: 1, type: 'list', values: ['un', 'deux'] } as ListCriterion; + expectedPrintedCriterion = '= [un,deux]'; + expect(getPrettyCriterion(listCriterion)).toEqual(expectedPrintedCriterion); + const notCriterion = {id: 1, type: null} as Criterion; + expectedPrintedCriterion = 'Criterion type not valid!'; + expect(getPrettyCriterion(notCriterion)).toEqual(expectedPrintedCriterion); + }); + + it('#getPrettyOperator() should prettify operator', () => { + expect(getPrettyOperator('eq')).toEqual('='); + expect(getPrettyOperator('neq')).toEqual('≠'); + expect(getPrettyOperator('gt')).toEqual('>'); + expect(getPrettyOperator('gte')).toEqual('>='); + expect(getPrettyOperator('lt')).toEqual('<'); + expect(getPrettyOperator('lte')).toEqual('<='); + expect(getPrettyOperator('lk')).toEqual('like'); + expect(getPrettyOperator('nlk')).toEqual('not like'); + expect(getPrettyOperator('in')).toEqual('in'); + expect(getPrettyOperator('nin')).toEqual('not in'); + expect(getPrettyOperator('toto')).toEqual('toto'); + expect(getPrettyOperator('')).toEqual(''); + expect(getPrettyOperator('')).toEqual(''); + }); +}); diff --git a/client/src/app/instance/store/models/criterion.model.ts b/client/src/app/instance/store/models/criterion.model.ts index 81d6161cb3445f20278ebc5cb909c4a10f2b2fff..a6118e881162183a5c5bcbf1a4b63b5ad72ec2e9 100644 --- a/client/src/app/instance/store/models/criterion.model.ts +++ b/client/src/app/instance/store/models/criterion.model.ts @@ -66,6 +66,17 @@ export const criterionToString = (criterion: Criterion): string => { return str; } +/** + * Returns criterion object from serialized criterion notation. + * + * @param {Attribute} attribute - The criterion to transform. + * @param {string[]} params - The criterion parameters. + * + * @return Criterion + * + * @example + * stringToCriterion(myAttribute, ['firstParameter', 'secondParameter']) + */ export const stringToCriterion = (attribute: Attribute, params: string[] = null): Criterion => { switch (attribute.search_type) { case 'field': diff --git a/client/src/app/instance/store/reducers/cone-search.reducer.spec.ts b/client/src/app/instance/store/reducers/cone-search.reducer.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..81b7f4c6dbfd4dbb5eb4d7fe497450d98430f4c8 --- /dev/null +++ b/client/src/app/instance/store/reducers/cone-search.reducer.spec.ts @@ -0,0 +1,110 @@ +import * as fromConeSearch from './cone-search.reducer'; +import * as coneSearchActions from '../actions/cone-search.actions'; +import { ConeSearch, Resolver } from '../models'; +import { Action } from '@ngrx/store'; + +describe('ConeSearch reducer', () => { + it('unknown action should return the default state', () => { + const { initialState } = fromConeSearch; + const action = { type: 'Unknown' }; + const state = fromConeSearch.coneSearchReducer(initialState, action); + + expect(state).toBe(initialState); + }); + + it('addConeSearch action should add conesearch', () => { + const { initialState } = fromConeSearch; + const coneSearch: ConeSearch = { ra: 1, dec: 2, radius: 3 }; + const action = coneSearchActions.addConeSearch({ coneSearch }); + const state = fromConeSearch.coneSearchReducer(initialState, action); + + expect(state.coneSearch).toEqual(coneSearch); + expect(state.resolver).toBeNull(); + expect(state.resolverIsLoading).toBeFalsy(); + expect(state.resolverIsLoaded).toBeFalsy(); + expect(state).not.toBe(initialState); + }); + + it('deleteConeSearch action should delete conesearch', () => { + const initialState = { + ...fromConeSearch.initialState, + coneSearch: { ra: 1, dec: 2, radius: 3 } + }; + const action = coneSearchActions.deleteConeSearch(); + const state = fromConeSearch.coneSearchReducer(initialState, action); + + expect(state.coneSearch).toBeNull(); + expect(state.resolver).toBeNull(); + expect(state.resolverIsLoading).toBeFalsy(); + expect(state.resolverIsLoaded).toBeFalsy(); + expect(state).not.toBe(initialState); + }); + + it('retrieveCoordinates action should set resolverIsLoading to true and resolverIsLoaded to false', () => { + const { initialState } = fromConeSearch; + const action = coneSearchActions.retrieveCoordinates({ name: 'myObject' }); + const state = fromConeSearch.coneSearchReducer(initialState, action); + + expect(state.coneSearch).toBeNull(); + expect(state.resolver).toBeNull(); + expect(state.resolverIsLoading).toBeTruthy(); + expect(state.resolverIsLoaded).toBeFalsy(); + expect(state).not.toBe(initialState); + }); + + it('retrieveCoordinatesSuccess action should set resolverIsLoading to false and resolverIsLoaded to true', () => { + const { initialState } = fromConeSearch; + const resolver: Resolver = { name: 'myObject', ra: 1, dec: 2 }; + const action = coneSearchActions.retrieveCoordinatesSuccess({ resolver }); + const state = fromConeSearch.coneSearchReducer(initialState, action); + + expect(state.coneSearch).toBeNull(); + expect(state.resolver).toBe(resolver); + expect(state.resolverIsLoading).toBeFalsy(); + expect(state.resolverIsLoaded).toBeTruthy(); + expect(state).not.toBe(initialState); + }); + + it('retrieveCoordinatesFail action should set resolverIsLoading to false', () => { + const initialState = { + ...fromConeSearch.initialState, + resolverIsLoading: true + }; + const action = coneSearchActions.retrieveCoordinatesFail(); + const state = fromConeSearch.coneSearchReducer(initialState, action); + + expect(state.coneSearch).toBeNull(); + expect(state.resolver).toBeNull(); + expect(state.resolverIsLoading).toBeFalsy(); + expect(state.resolverIsLoaded).toBeFalsy(); + expect(state).not.toBe(initialState); + }); + + it('should get coneSearch', () => { + const action = {} as Action; + const state = fromConeSearch.coneSearchReducer(undefined, action); + + expect(fromConeSearch.selectConeSearch(state)).toBeNull(); + }); + + it('should get resolver', () => { + const action = {} as Action; + const state = fromConeSearch.coneSearchReducer(undefined, action); + + expect(fromConeSearch.selectResolver(state)).toBeNull(); + }); + + it('should get resolverIsLoading', () => { + const action = {} as Action; + const state = fromConeSearch.coneSearchReducer(undefined, action); + + expect(fromConeSearch.selectResolverIsLoading(state)).toBeFalsy(); + }); + + it('should get resolverIsLoaded', () => { + const action = {} as Action; + const state = fromConeSearch.coneSearchReducer(undefined, action); + + expect(fromConeSearch.selectResolverIsLoaded(state)).toBeFalsy(); + }); +}); diff --git a/client/src/app/instance/store/reducers/cone-search.reducer.ts b/client/src/app/instance/store/reducers/cone-search.reducer.ts index 6a4ee35ce53cc7f5271515cf2811850994e04353..63715d1aa66fae148a0659776ca14df072b727c8 100644 --- a/client/src/app/instance/store/reducers/cone-search.reducer.ts +++ b/client/src/app/instance/store/reducers/cone-search.reducer.ts @@ -12,6 +12,11 @@ import { createReducer, on } from '@ngrx/store'; import * as coneSearchActions from '../actions/cone-search.actions'; import { ConeSearch, Resolver } from '../models'; +/** + * Interface for cone search state. + * + * @interface State + */ export interface State { coneSearch: ConeSearch; resolver: Resolver; diff --git a/client/src/app/instance/store/reducers/detail.reducer.spec.ts b/client/src/app/instance/store/reducers/detail.reducer.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..d10671dc02884218734a3ed6ce49ad3351c734be --- /dev/null +++ b/client/src/app/instance/store/reducers/detail.reducer.spec.ts @@ -0,0 +1,157 @@ +import * as fromDetail from './detail.reducer'; +import * as detailActions from '../actions/detail.actions'; +import { Action } from '@ngrx/store'; + +describe('Detail reducer', () => { + it('unknown action should return the default state', () => { + const { initialState } = fromDetail; + const action = { type: 'Unknown' }; + const state = fromDetail.detailReducer(initialState, action); + + expect(state).toBe(initialState); + }); + + it('retrieveObject action should set objectIsLoading to true and objectIsLoaded to false', () => { + const initialState = { + ...fromDetail.initialState, + objectIsLoaded: false + }; + const action = detailActions.retrieveObject(); + const state = fromDetail.detailReducer(initialState, action); + + expect(state.object).toBeNull(); + expect(state.objectIsLoading).toBeTruthy(); + expect(state.objectIsLoaded).toBeFalsy(); + expect(state.spectraCSV).toBeNull(); + expect(state.spectraIsLoading).toBeFalsy(); + expect(state.spectraIsLoaded).toBeFalsy(); + expect(state).not.toBe(initialState); + }); + + it('retrieveObjectSuccess action should add object, set objectIsLoading to false and objectIsLoaded to true', () => { + const initialState = { + ...fromDetail.initialState, + objectIsLoading: true + }; + const action = detailActions.retrieveObjectSuccess({ object: 'myObject' }); + const state = fromDetail.detailReducer(initialState, action); + + expect(state.object).toBe('myObject'); + expect(state.objectIsLoading).toBeFalsy(); + expect(state.objectIsLoaded).toBeTruthy(); + expect(state.spectraCSV).toBeNull(); + expect(state.spectraIsLoading).toBeFalsy(); + expect(state.spectraIsLoaded).toBeFalsy(); + expect(state).not.toBe(initialState); + }); + + it('retrieveObjectFail action should set objectIsLoading to false', () => { + const initialState = { + ...fromDetail.initialState, + objectIsLoading: true + }; + const action = detailActions.retrieveObjectFail(); + const state = fromDetail.detailReducer(initialState, action); + + expect(state.object).toBeNull(); + expect(state.objectIsLoading).toBeFalsy(); + expect(state.objectIsLoaded).toBeFalsy(); + expect(state.spectraCSV).toBeNull(); + expect(state.spectraIsLoading).toBeFalsy(); + expect(state.spectraIsLoaded).toBeFalsy(); + expect(state).not.toBe(initialState); + }); + + it('retrieveSpectra action should set spectraIsLoading to true and spectraIsLoaded to false', () => { + const initialState = { + ...fromDetail.initialState, + spectraIsLoaded: true + }; + const action = detailActions.retrieveSpectra({ filename: 'mySpectra' }); + const state = fromDetail.detailReducer(initialState, action); + + expect(state.object).toBeNull(); + expect(state.objectIsLoading).toBeFalsy(); + expect(state.objectIsLoaded).toBeFalsy(); + expect(state.spectraCSV).toBeNull(); + expect(state.spectraIsLoading).toBeTruthy(); + expect(state.spectraIsLoaded).toBeFalsy(); + expect(state).not.toBe(initialState); + }); + + it('retrieveSpectraSuccess action should add spectraCSV, set spectraIsLoading to false and spectraIsLoaded to true', () => { + const initialState = { + ...fromDetail.initialState, + spectraIsLoading: true + }; + const action = detailActions.retrieveSpectraSuccess({ spectraCSV: 'mySpectra' }); + const state = fromDetail.detailReducer(initialState, action); + + expect(state.object).toBeNull(); + expect(state.objectIsLoading).toBeFalsy(); + expect(state.objectIsLoaded).toBeFalsy(); + expect(state.spectraCSV).toBe('mySpectra'); + expect(state.spectraIsLoading).toBeFalsy(); + expect(state.spectraIsLoaded).toBeTruthy(); + expect(state).not.toBe(initialState); + }); + + it('retrieveSpectraFail action should set spectraIsLoading to false', () => { + const initialState = { + ...fromDetail.initialState, + spectraIsLoading: true + }; + const action = detailActions.retrieveSpectraFail(); + const state = fromDetail.detailReducer(initialState, action); + + expect(state.object).toBeNull(); + expect(state.objectIsLoading).toBeFalsy(); + expect(state.objectIsLoaded).toBeFalsy(); + expect(state.spectraCSV).toBeNull(); + expect(state.spectraIsLoading).toBeFalsy(); + expect(state.spectraIsLoaded).toBeFalsy(); + expect(state).not.toBe(initialState); + }); + + it('should get object', () => { + const action = {} as Action; + const state = fromDetail.detailReducer(undefined, action); + + expect(fromDetail.selectObject(state)).toBeNull(); + }); + + it('should get objectIsLoading', () => { + const action = {} as Action; + const state = fromDetail.detailReducer(undefined, action); + + expect(fromDetail.selectObjectIsLoading(state)).toBeFalsy(); + }); + + it('should get objectIsLoaded', () => { + const action = {} as Action; + const state = fromDetail.detailReducer(undefined, action); + + expect(fromDetail.selectObjectIsLoaded(state)).toBeFalsy(); + }); + + it('should get spectraCSV', () => { + const action = {} as Action; + const state = fromDetail.detailReducer(undefined, action); + + expect(fromDetail.selectSpectraCSV(state)).toBeNull(); + }); + + it('should get spectraIsLoading', () => { + const action = {} as Action; + const state = fromDetail.detailReducer(undefined, action); + + expect(fromDetail.selectSpectraIsLoading(state)).toBeFalsy(); + }); + + it('should get spectraIsLoaded', () => { + const action = {} as Action; + const state = fromDetail.detailReducer(undefined, action); + + expect(fromDetail.selectSpectraIsLoaded(state)).toBeFalsy(); + }); +}); diff --git a/client/src/app/instance/store/reducers/detail.reducer.ts b/client/src/app/instance/store/reducers/detail.reducer.ts index 466fcfc1838c6e79d790cf87fafb944b96081189..f73d766010b38ac4ce5d16ec43b8dc5bcea9d4cc 100644 --- a/client/src/app/instance/store/reducers/detail.reducer.ts +++ b/client/src/app/instance/store/reducers/detail.reducer.ts @@ -11,6 +11,11 @@ import { createReducer, on } from '@ngrx/store'; import * as detailActions from '../actions/detail.actions'; +/** + * Interface for detail state. + * + * @interface State + */ export interface State { object: any; objectIsLoading: boolean; diff --git a/client/src/app/instance/store/reducers/samp.reducer.spec.ts b/client/src/app/instance/store/reducers/samp.reducer.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..cb682d7ba353eed0be9dc414e39254bcbd2225df --- /dev/null +++ b/client/src/app/instance/store/reducers/samp.reducer.spec.ts @@ -0,0 +1,41 @@ +import * as fromSamp from './samp.reducer'; +import * as sampActions from '../actions/samp.actions'; +import { Action } from '@ngrx/store'; + +describe('Samp reducer', () => { + it('unknown action should return the default state', () => { + const { initialState } = fromSamp; + const action = { type: 'Unknown' }; + const state = fromSamp.sampReducer(initialState, action); + + expect(state).toBe(initialState); + }); + + it('registerSuccess action should set registered to true', () => { + const { initialState } = fromSamp; + const action = sampActions.registerSuccess(); + const state = fromSamp.sampReducer(initialState, action); + + expect(state.registered).toBeTruthy(); + expect(state).not.toBe(initialState); + }); + + it('unregister action should set registered to false', () => { + const initialState = { + ...fromSamp.initialState, + registered: true + }; + const action = sampActions.unregister(); + const state = fromSamp.sampReducer(initialState, action); + + expect(state.registered).toBeFalsy(); + expect(state).not.toBe(initialState); + }); + + it('should get registered', () => { + const action = {} as Action; + const state = fromSamp.sampReducer(undefined, action); + + expect(fromSamp.selectRegistered(state)).toBeFalsy(); + }); +}); diff --git a/client/src/app/instance/store/reducers/samp.reducer.ts b/client/src/app/instance/store/reducers/samp.reducer.ts index 75d477579074d15f83c7ee0e9df43976449a1741..f16136d25d96e7789d838d8da2462a14424c37ec 100644 --- a/client/src/app/instance/store/reducers/samp.reducer.ts +++ b/client/src/app/instance/store/reducers/samp.reducer.ts @@ -11,6 +11,11 @@ import { createReducer, on } from '@ngrx/store'; import * as sampActions from '../actions/samp.actions'; +/** + * Interface for samp state. + * + * @interface State + */ export interface State { registered: boolean; } diff --git a/client/src/app/instance/store/reducers/search.reducer.spec.ts b/client/src/app/instance/store/reducers/search.reducer.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..f621f060cbdcfd3fe8967de18289de16c1f0d8d1 --- /dev/null +++ b/client/src/app/instance/store/reducers/search.reducer.spec.ts @@ -0,0 +1,691 @@ +import * as fromSearch from './search.reducer'; +import * as searchActions from '../actions/search.actions'; +import { Criterion, Pagination, PaginationOrder } from '../models'; +import { Action } from '@ngrx/store'; + +describe('Search reducer', () => { + it('unknown action should return the default state', () => { + const { initialState } = fromSearch; + const action = { type: 'Unknown' }; + const state = fromSearch.searchReducer(initialState, action); + + expect(state).toBe(initialState); + }); + + it('restartSearch action should set currentStep to \'dataset\'', () => { + const { initialState } = fromSearch; + const action = searchActions.restartSearch(); + const state = fromSearch.searchReducer(initialState, action); + + expect(state.pristine).toBeTruthy(); + expect(state.currentDataset).toBeNull(); + expect(state.currentStep).toBe('dataset'); + expect(state.criteriaStepChecked).toBeFalsy(); + expect(state.outputStepChecked).toBeFalsy(); + expect(state.resultStepChecked).toBeFalsy(); + expect(state.coneSearchAdded).toBeFalsy(); + expect(state.criteriaList.length).toEqual(0); + expect(state.outputList.length).toEqual(0); + expect(state.dataLengthIsLoading).toBeFalsy(); + expect(state.dataLengthIsLoaded).toBeFalsy(); + expect(state.dataLength).toBeNull(); + expect(state.data.length).toEqual(0); + expect(state.dataIsLoading).toBeFalsy(); + expect(state.dataIsLoaded).toBeFalsy(); + expect(state.selectedData.length).toEqual(0); + expect(state).not.toBe(initialState); + }); + + it('changeStep action should change the currentStep', () => { + const { initialState } = fromSearch; + const action = searchActions.changeStep({ step: 'myStep' }); + const state = fromSearch.searchReducer(initialState, action); + + expect(state.pristine).toBeTruthy(); + expect(state.currentDataset).toBeNull(); + expect(state.currentStep).toBe('myStep'); + expect(state.criteriaStepChecked).toBeFalsy(); + expect(state.outputStepChecked).toBeFalsy(); + expect(state.resultStepChecked).toBeFalsy(); + expect(state.coneSearchAdded).toBeFalsy(); + expect(state.criteriaList.length).toEqual(0); + expect(state.outputList.length).toEqual(0); + expect(state.dataLengthIsLoading).toBeFalsy(); + expect(state.dataLengthIsLoaded).toBeFalsy(); + expect(state.dataLength).toBeNull(); + expect(state.data.length).toEqual(0); + expect(state.dataIsLoading).toBeFalsy(); + expect(state.dataIsLoaded).toBeFalsy(); + expect(state.selectedData.length).toEqual(0); + expect(state).not.toBe(initialState); + }); + + it('markAsDirty action should set pristine to false', () => { + const { initialState } = fromSearch; + const action = searchActions.markAsDirty(); + const state = fromSearch.searchReducer(initialState, action); + + expect(state.pristine).toBeFalsy(); + expect(state.currentDataset).toBeNull(); + expect(state.currentStep).toBeNull(); + expect(state.criteriaStepChecked).toBeFalsy(); + expect(state.outputStepChecked).toBeFalsy(); + expect(state.resultStepChecked).toBeFalsy(); + expect(state.coneSearchAdded).toBeFalsy(); + expect(state.criteriaList.length).toEqual(0); + expect(state.outputList.length).toEqual(0); + expect(state.dataLengthIsLoading).toBeFalsy(); + expect(state.dataLengthIsLoaded).toBeFalsy(); + expect(state.dataLength).toBeNull(); + expect(state.data.length).toEqual(0); + expect(state.dataIsLoading).toBeFalsy(); + expect(state.dataIsLoaded).toBeFalsy(); + expect(state.selectedData.length).toEqual(0); + expect(state).not.toBe(initialState); + }); + + it('changeCurrentDataset action should set pristine to false', () => { + const { initialState } = fromSearch; + const action = searchActions.changeCurrentDataset({ currentDataset: 'myDataset' }); + const state = fromSearch.searchReducer(initialState, action); + + expect(state.pristine).toBeTruthy(); + expect(state.currentDataset).toBe('myDataset'); + expect(state.currentStep).toBeNull(); + expect(state.criteriaStepChecked).toBeFalsy(); + expect(state.outputStepChecked).toBeFalsy(); + expect(state.resultStepChecked).toBeFalsy(); + expect(state.coneSearchAdded).toBeFalsy(); + expect(state.criteriaList.length).toEqual(0); + expect(state.outputList.length).toEqual(0); + expect(state.dataLengthIsLoading).toBeFalsy(); + expect(state.dataLengthIsLoaded).toBeFalsy(); + expect(state.dataLength).toBeNull(); + expect(state.data.length).toEqual(0); + expect(state.dataIsLoading).toBeFalsy(); + expect(state.dataIsLoaded).toBeFalsy(); + expect(state.selectedData.length).toEqual(0); + expect(state).not.toBe(initialState); + }); + + it('checkCriteria action should set criteriaStepChecked to true', () => { + const { initialState } = fromSearch; + const action = searchActions.checkCriteria(); + const state = fromSearch.searchReducer(initialState, action); + + expect(state.pristine).toBeTruthy(); + expect(state.currentDataset).toBeNull(); + expect(state.currentStep).toBeNull(); + expect(state.criteriaStepChecked).toBeTruthy(); + expect(state.outputStepChecked).toBeFalsy(); + expect(state.resultStepChecked).toBeFalsy(); + expect(state.coneSearchAdded).toBeFalsy(); + expect(state.criteriaList.length).toEqual(0); + expect(state.outputList.length).toEqual(0); + expect(state.dataLengthIsLoading).toBeFalsy(); + expect(state.dataLengthIsLoaded).toBeFalsy(); + expect(state.dataLength).toBeNull(); + expect(state.data.length).toEqual(0); + expect(state.dataIsLoading).toBeFalsy(); + expect(state.dataIsLoaded).toBeFalsy(); + expect(state.selectedData.length).toEqual(0); + expect(state).not.toBe(initialState); + }); + + it('checkOutput action should set outputStepChecked to true', () => { + const { initialState } = fromSearch; + const action = searchActions.checkOutput(); + const state = fromSearch.searchReducer(initialState, action); + + expect(state.pristine).toBeTruthy(); + expect(state.currentDataset).toBeNull(); + expect(state.currentStep).toBeNull(); + expect(state.criteriaStepChecked).toBeFalsy(); + expect(state.outputStepChecked).toBeTruthy(); + expect(state.resultStepChecked).toBeFalsy(); + expect(state.coneSearchAdded).toBeFalsy(); + expect(state.criteriaList.length).toEqual(0); + expect(state.outputList.length).toEqual(0); + expect(state.dataLengthIsLoading).toBeFalsy(); + expect(state.dataLengthIsLoaded).toBeFalsy(); + expect(state.dataLength).toBeNull(); + expect(state.data.length).toEqual(0); + expect(state.dataIsLoading).toBeFalsy(); + expect(state.dataIsLoaded).toBeFalsy(); + expect(state.selectedData.length).toEqual(0); + expect(state).not.toBe(initialState); + }); + + it('checkResult action should set resultStepChecked to true', () => { + const { initialState } = fromSearch; + const action = searchActions.checkResult(); + const state = fromSearch.searchReducer(initialState, action); + + expect(state.pristine).toBeTruthy(); + expect(state.currentDataset).toBeNull(); + expect(state.currentStep).toBeNull(); + expect(state.criteriaStepChecked).toBeFalsy(); + expect(state.outputStepChecked).toBeFalsy(); + expect(state.resultStepChecked).toBeTruthy(); + expect(state.coneSearchAdded).toBeFalsy(); + expect(state.criteriaList.length).toEqual(0); + expect(state.outputList.length).toEqual(0); + expect(state.dataLengthIsLoading).toBeFalsy(); + expect(state.dataLengthIsLoaded).toBeFalsy(); + expect(state.dataLength).toBeNull(); + expect(state.data.length).toEqual(0); + expect(state.dataIsLoading).toBeFalsy(); + expect(state.dataIsLoaded).toBeFalsy(); + expect(state.selectedData.length).toEqual(0); + expect(state).not.toBe(initialState); + }); + + it('checkResult action should set resultStepChecked to true', () => { + const { initialState } = fromSearch; + const action = searchActions.checkResult(); + const state = fromSearch.searchReducer(initialState, action); + + expect(state.pristine).toBeTruthy(); + expect(state.currentDataset).toBeNull(); + expect(state.currentStep).toBeNull(); + expect(state.criteriaStepChecked).toBeFalsy(); + expect(state.outputStepChecked).toBeFalsy(); + expect(state.resultStepChecked).toBeTruthy(); + expect(state.coneSearchAdded).toBeFalsy(); + expect(state.criteriaList.length).toEqual(0); + expect(state.outputList.length).toEqual(0); + expect(state.dataLengthIsLoading).toBeFalsy(); + expect(state.dataLengthIsLoaded).toBeFalsy(); + expect(state.dataLength).toBeNull(); + expect(state.data.length).toEqual(0); + expect(state.dataIsLoading).toBeFalsy(); + expect(state.dataIsLoaded).toBeFalsy(); + expect(state.selectedData.length).toEqual(0); + expect(state).not.toBe(initialState); + }); + + it('updateCriteriaList action should set criteriaList', () => { + const { initialState } = fromSearch; + const criteriaList: Criterion[] = [{ id: 1, type: 'field' }]; + const action = searchActions.updateCriteriaList({ criteriaList }); + const state = fromSearch.searchReducer(initialState, action); + + expect(state.pristine).toBeTruthy(); + expect(state.currentDataset).toBeNull(); + expect(state.currentStep).toBeNull(); + expect(state.criteriaStepChecked).toBeFalsy(); + expect(state.outputStepChecked).toBeFalsy(); + expect(state.resultStepChecked).toBeFalsy(); + expect(state.coneSearchAdded).toBeFalsy(); + expect(state.criteriaList.length).toEqual(1); + expect(state.criteriaList).toEqual(criteriaList); + expect(state.outputList.length).toEqual(0); + expect(state.dataLengthIsLoading).toBeFalsy(); + expect(state.dataLengthIsLoaded).toBeFalsy(); + expect(state.dataLength).toBeNull(); + expect(state.data.length).toEqual(0); + expect(state.dataIsLoading).toBeFalsy(); + expect(state.dataIsLoaded).toBeFalsy(); + expect(state.selectedData.length).toEqual(0); + expect(state).not.toBe(initialState); + }); + + it('addCriterion action should add criterion to the criteriaList', () => { + const { initialState } = fromSearch; + const criterion: Criterion = { id: 1, type: 'field' }; + const action = searchActions.addCriterion({ criterion }); + const state = fromSearch.searchReducer(initialState, action); + + expect(state.pristine).toBeTruthy(); + expect(state.currentDataset).toBeNull(); + expect(state.currentStep).toBeNull(); + expect(state.criteriaStepChecked).toBeFalsy(); + expect(state.outputStepChecked).toBeFalsy(); + expect(state.resultStepChecked).toBeFalsy(); + expect(state.coneSearchAdded).toBeFalsy(); + expect(state.criteriaList.length).toEqual(1); + expect(state.criteriaList).toEqual([criterion]); + expect(state.outputList.length).toEqual(0); + expect(state.dataLengthIsLoading).toBeFalsy(); + expect(state.dataLengthIsLoaded).toBeFalsy(); + expect(state.dataLength).toBeNull(); + expect(state.data.length).toEqual(0); + expect(state.dataIsLoading).toBeFalsy(); + expect(state.dataIsLoaded).toBeFalsy(); + expect(state.selectedData.length).toEqual(0); + expect(state).not.toBe(initialState); + }); + + it('deleteCriterion action should remove criterion to the criteriaList', () => { + const initialState = { + ...fromSearch.initialState, + criteriaList: [{ id: 1, type: 'field' }] + }; + const action = searchActions.deleteCriterion({ idCriterion: 1 }); + const state = fromSearch.searchReducer(initialState, action); + + expect(state.pristine).toBeTruthy(); + expect(state.currentDataset).toBeNull(); + expect(state.currentStep).toBeNull(); + expect(state.criteriaStepChecked).toBeFalsy(); + expect(state.outputStepChecked).toBeFalsy(); + expect(state.resultStepChecked).toBeFalsy(); + expect(state.coneSearchAdded).toBeFalsy(); + expect(state.criteriaList.length).toEqual(0); + expect(state.outputList.length).toEqual(0); + expect(state.dataLengthIsLoading).toBeFalsy(); + expect(state.dataLengthIsLoaded).toBeFalsy(); + expect(state.dataLength).toBeNull(); + expect(state.data.length).toEqual(0); + expect(state.dataIsLoading).toBeFalsy(); + expect(state.dataIsLoaded).toBeFalsy(); + expect(state.selectedData.length).toEqual(0); + expect(state).not.toBe(initialState); + }); + + it('updateOutputList action should set outputList', () => { + const { initialState } = fromSearch; + const outputList: number[] = [1, 2, 3]; + const action = searchActions.updateOutputList({ outputList }); + const state = fromSearch.searchReducer(initialState, action); + + expect(state.pristine).toBeTruthy(); + expect(state.currentDataset).toBeNull(); + expect(state.currentStep).toBeNull(); + expect(state.criteriaStepChecked).toBeFalsy(); + expect(state.outputStepChecked).toBeFalsy(); + expect(state.resultStepChecked).toBeFalsy(); + expect(state.coneSearchAdded).toBeFalsy(); + expect(state.criteriaList.length).toEqual(0); + expect(state.outputList.length).toEqual(3); + expect(state.outputList).toEqual(outputList); + expect(state.dataLengthIsLoading).toBeFalsy(); + expect(state.dataLengthIsLoaded).toBeFalsy(); + expect(state.dataLength).toBeNull(); + expect(state.data.length).toEqual(0); + expect(state.dataIsLoading).toBeFalsy(); + expect(state.dataIsLoaded).toBeFalsy(); + expect(state.selectedData.length).toEqual(0); + expect(state).not.toBe(initialState); + }); + + it('addSelectedData action should add data id to the selectedData list', () => { + const { initialState } = fromSearch; + const action = searchActions.addSelectedData({ id: 1 }); + const state = fromSearch.searchReducer(initialState, action); + + expect(state.pristine).toBeTruthy(); + expect(state.currentDataset).toBeNull(); + expect(state.currentStep).toBeNull(); + expect(state.criteriaStepChecked).toBeFalsy(); + expect(state.outputStepChecked).toBeFalsy(); + expect(state.resultStepChecked).toBeFalsy(); + expect(state.coneSearchAdded).toBeFalsy(); + expect(state.criteriaList.length).toEqual(0); + expect(state.outputList.length).toEqual(0); + expect(state.dataLengthIsLoading).toBeFalsy(); + expect(state.dataLengthIsLoaded).toBeFalsy(); + expect(state.dataLength).toBeNull(); + expect(state.data.length).toEqual(0); + expect(state.dataIsLoading).toBeFalsy(); + expect(state.dataIsLoaded).toBeFalsy(); + expect(state.selectedData.length).toEqual(1); + expect(state.selectedData).toEqual([1]); + expect(state).not.toBe(initialState); + }); + + it('deleteSelectedData action should remove data id to the selectedData list', () => { + const initialState = { + ...fromSearch.initialState, + selectedData: [1] + }; + const action = searchActions.deleteSelectedData({ id: 1 }); + const state = fromSearch.searchReducer(initialState, action); + + expect(state.pristine).toBeTruthy(); + expect(state.currentDataset).toBeNull(); + expect(state.currentStep).toBeNull(); + expect(state.criteriaStepChecked).toBeFalsy(); + expect(state.outputStepChecked).toBeFalsy(); + expect(state.resultStepChecked).toBeFalsy(); + expect(state.coneSearchAdded).toBeFalsy(); + expect(state.criteriaList.length).toEqual(0); + expect(state.outputList.length).toEqual(0); + expect(state.dataLengthIsLoading).toBeFalsy(); + expect(state.dataLengthIsLoaded).toBeFalsy(); + expect(state.dataLength).toBeNull(); + expect(state.data.length).toEqual(0); + expect(state.dataIsLoading).toBeFalsy(); + expect(state.dataIsLoaded).toBeFalsy(); + expect(state.selectedData.length).toEqual(0); + expect(state).not.toBe(initialState); + }); + + it('retrieveDataLength action should set dataLengthIsLoading to true and dataLengthIsLoaded to false', () => { + const initialState = { + ...fromSearch.initialState, + dataLengthIsLoaded: true + }; + const action = searchActions.retrieveDataLength(); + const state = fromSearch.searchReducer(initialState, action); + + expect(state.pristine).toBeTruthy(); + expect(state.currentDataset).toBeNull(); + expect(state.currentStep).toBeNull(); + expect(state.criteriaStepChecked).toBeFalsy(); + expect(state.outputStepChecked).toBeFalsy(); + expect(state.resultStepChecked).toBeFalsy(); + expect(state.coneSearchAdded).toBeFalsy(); + expect(state.criteriaList.length).toEqual(0); + expect(state.outputList.length).toEqual(0); + expect(state.dataLengthIsLoading).toBeTruthy(); + expect(state.dataLengthIsLoaded).toBeFalsy(); + expect(state.dataLength).toBeNull(); + expect(state.data.length).toEqual(0); + expect(state.dataIsLoading).toBeFalsy(); + expect(state.dataIsLoaded).toBeFalsy(); + expect(state.selectedData.length).toEqual(0); + expect(state).not.toBe(initialState); + }); + + it('retrieveDataLengthSuccess action should set dataLength, set dataLengthIsLoading to true and dataLengthIsLoaded to false', () => { + const initialState = { + ...fromSearch.initialState, + dataLengthIsLoading: true + }; + const action = searchActions.retrieveDataLengthSuccess({ length: 1 }); + const state = fromSearch.searchReducer(initialState, action); + + expect(state.pristine).toBeTruthy(); + expect(state.currentDataset).toBeNull(); + expect(state.currentStep).toBeNull(); + expect(state.criteriaStepChecked).toBeFalsy(); + expect(state.outputStepChecked).toBeFalsy(); + expect(state.resultStepChecked).toBeFalsy(); + expect(state.coneSearchAdded).toBeFalsy(); + expect(state.criteriaList.length).toEqual(0); + expect(state.outputList.length).toEqual(0); + expect(state.dataLengthIsLoading).toBeFalsy(); + expect(state.dataLengthIsLoaded).toBeTruthy(); + expect(state.dataLength).toEqual(1); + expect(state.data.length).toEqual(0); + expect(state.dataIsLoading).toBeFalsy(); + expect(state.dataIsLoaded).toBeFalsy(); + expect(state.selectedData.length).toEqual(0); + expect(state).not.toBe(initialState); + }); + + it('retrieveDataLengthFail action should set dataLengthIsLoading to false', () => { + const initialState = { + ...fromSearch.initialState, + dataLengthIsLoading: true + }; + const action = searchActions.retrieveDataLengthFail(); + const state = fromSearch.searchReducer(initialState, action); + + expect(state.pristine).toBeTruthy(); + expect(state.currentDataset).toBeNull(); + expect(state.currentStep).toBeNull(); + expect(state.criteriaStepChecked).toBeFalsy(); + expect(state.outputStepChecked).toBeFalsy(); + expect(state.resultStepChecked).toBeFalsy(); + expect(state.coneSearchAdded).toBeFalsy(); + expect(state.criteriaList.length).toEqual(0); + expect(state.outputList.length).toEqual(0); + expect(state.dataLengthIsLoading).toBeFalsy(); + expect(state.dataLengthIsLoaded).toBeFalsy(); + expect(state.dataLength).toBeNull(); + expect(state.data.length).toEqual(0); + expect(state.dataIsLoading).toBeFalsy(); + expect(state.dataIsLoaded).toBeFalsy(); + expect(state.selectedData.length).toEqual(0); + expect(state).not.toBe(initialState); + }); + + it('retrieveData action should set dataIsLoading to true and dataIsLoaded to false', () => { + const initialState = { + ...fromSearch.initialState, + dataIsLoaded: true + }; + const pagination: Pagination = { dname: 'myDataset', page: 1, nbItems: 10, sortedCol: 1, order: PaginationOrder.a }; + const action = searchActions.retrieveData({ pagination }); + const state = fromSearch.searchReducer(initialState, action); + + expect(state.pristine).toBeTruthy(); + expect(state.currentDataset).toBeNull(); + expect(state.currentStep).toBeNull(); + expect(state.criteriaStepChecked).toBeFalsy(); + expect(state.outputStepChecked).toBeFalsy(); + expect(state.resultStepChecked).toBeFalsy(); + expect(state.coneSearchAdded).toBeFalsy(); + expect(state.criteriaList.length).toEqual(0); + expect(state.outputList.length).toEqual(0); + expect(state.dataLengthIsLoading).toBeFalsy(); + expect(state.dataLengthIsLoaded).toBeFalsy(); + expect(state.dataLength).toBeNull(); + expect(state.data.length).toEqual(0); + expect(state.dataIsLoading).toBeTruthy(); + expect(state.dataIsLoaded).toBeFalsy(); + expect(state.selectedData.length).toEqual(0); + expect(state).not.toBe(initialState); + }); + + it('retrieveDataSuccess action should set data, set dataIsLoading to false and dataIsLoaded to true', () => { + const initialState = { + ...fromSearch.initialState, + dataIsLoading: true + }; + const action = searchActions.retrieveDataSuccess({ data: ['myData'] }); + const state = fromSearch.searchReducer(initialState, action); + + expect(state.pristine).toBeTruthy(); + expect(state.currentDataset).toBeNull(); + expect(state.currentStep).toBeNull(); + expect(state.criteriaStepChecked).toBeFalsy(); + expect(state.outputStepChecked).toBeFalsy(); + expect(state.resultStepChecked).toBeFalsy(); + expect(state.coneSearchAdded).toBeFalsy(); + expect(state.criteriaList.length).toEqual(0); + expect(state.outputList.length).toEqual(0); + expect(state.dataLengthIsLoading).toBeFalsy(); + expect(state.dataLengthIsLoaded).toBeFalsy(); + expect(state.dataLength).toBeNull(); + expect(state.data.length).toEqual(1); + expect(state.data).toEqual(['myData']); + expect(state.dataIsLoading).toBeFalsy(); + expect(state.dataIsLoaded).toBeTruthy(); + expect(state.selectedData.length).toEqual(0); + expect(state).not.toBe(initialState); + }); + + it('retrieveDataFail action should set dataIsLoading to false', () => { + const initialState = { + ...fromSearch.initialState, + dataIsLoading: true + }; + const action = searchActions.retrieveDataFail(); + const state = fromSearch.searchReducer(initialState, action); + + expect(state.pristine).toBeTruthy(); + expect(state.currentDataset).toBeNull(); + expect(state.currentStep).toBeNull(); + expect(state.criteriaStepChecked).toBeFalsy(); + expect(state.outputStepChecked).toBeFalsy(); + expect(state.resultStepChecked).toBeFalsy(); + expect(state.coneSearchAdded).toBeFalsy(); + expect(state.criteriaList.length).toEqual(0); + expect(state.outputList.length).toEqual(0); + expect(state.dataLengthIsLoading).toBeFalsy(); + expect(state.dataLengthIsLoaded).toBeFalsy(); + expect(state.dataLength).toBeNull(); + expect(state.data.length).toEqual(0); + expect(state.dataIsLoading).toBeFalsy(); + expect(state.dataIsLoaded).toBeFalsy(); + expect(state.selectedData.length).toEqual(0); + expect(state).not.toBe(initialState); + }); + + it('destroyResults action should reset data and dataLength', () => { + const initialState = { + ...fromSearch.initialState, + data: ['myData'], + dataLength: 1 + }; + const action = searchActions.destroyResults(); + const state = fromSearch.searchReducer(initialState, action); + + expect(state.pristine).toBeTruthy(); + expect(state.currentDataset).toBeNull(); + expect(state.currentStep).toBeNull(); + expect(state.criteriaStepChecked).toBeFalsy(); + expect(state.outputStepChecked).toBeFalsy(); + expect(state.resultStepChecked).toBeFalsy(); + expect(state.coneSearchAdded).toBeFalsy(); + expect(state.criteriaList.length).toEqual(0); + expect(state.outputList.length).toEqual(0); + expect(state.dataLengthIsLoading).toBeFalsy(); + expect(state.dataLengthIsLoaded).toBeFalsy(); + expect(state.dataLength).toBeNull(); + expect(state.data.length).toEqual(0); + expect(state.dataIsLoading).toBeFalsy(); + expect(state.dataIsLoaded).toBeFalsy(); + expect(state.selectedData.length).toEqual(0); + expect(state).not.toBe(initialState); + }); + + it('resetSearch action should set currentStep to \'dataset\'', () => { + const { initialState } = fromSearch; + const action = searchActions.resetSearch(); + const state = fromSearch.searchReducer(initialState, action); + + expect(state.pristine).toBeTruthy(); + expect(state.currentDataset).toBeNull(); + expect(state.currentStep).toEqual('dataset'); + expect(state.criteriaStepChecked).toBeFalsy(); + expect(state.outputStepChecked).toBeFalsy(); + expect(state.resultStepChecked).toBeFalsy(); + expect(state.coneSearchAdded).toBeFalsy(); + expect(state.criteriaList.length).toEqual(0); + expect(state.outputList.length).toEqual(0); + expect(state.dataLengthIsLoading).toBeFalsy(); + expect(state.dataLengthIsLoaded).toBeFalsy(); + expect(state.dataLength).toBeNull(); + expect(state.data.length).toEqual(0); + expect(state.dataIsLoading).toBeFalsy(); + expect(state.dataIsLoaded).toBeFalsy(); + expect(state.selectedData.length).toEqual(0); + expect(state).not.toBe(initialState); + }); + + it('should get pristine', () => { + const action = {} as Action; + const state = fromSearch.searchReducer(undefined, action); + + expect(fromSearch.selectPristine(state)).toBeTruthy(); + }); + + it('should get currentDataset', () => { + const action = {} as Action; + const state = fromSearch.searchReducer(undefined, action); + + expect(fromSearch.selectCurrentDataset(state)).toBeNull(); + }); + + it('should get currentStep', () => { + const action = {} as Action; + const state = fromSearch.searchReducer(undefined, action); + + expect(fromSearch.selectCurrentStep(state)).toBeNull(); + }); + + it('should get criteriaStepChecked', () => { + const action = {} as Action; + const state = fromSearch.searchReducer(undefined, action); + + expect(fromSearch.selectCriteriaStepChecked(state)).toBeFalsy(); + }); + + it('should get outputStepChecked', () => { + const action = {} as Action; + const state = fromSearch.searchReducer(undefined, action); + + expect(fromSearch.selectOutputStepChecked(state)).toBeFalsy(); + }); + + it('should get resultStepChecked', () => { + const action = {} as Action; + const state = fromSearch.searchReducer(undefined, action); + + expect(fromSearch.selectResultStepChecked(state)).toBeFalsy(); + }); + + it('should get coneSearchAdded', () => { + const action = {} as Action; + const state = fromSearch.searchReducer(undefined, action); + + expect(fromSearch.selectIsConeSearchAdded(state)).toBeFalsy(); + }); + + it('should get criteriaList', () => { + const action = {} as Action; + const state = fromSearch.searchReducer(undefined, action); + + expect(fromSearch.selectCriteriaList(state).length).toEqual(0); + }); + + it('should get outputList', () => { + const action = {} as Action; + const state = fromSearch.searchReducer(undefined, action); + + expect(fromSearch.selectOutputList(state).length).toEqual(0); + }); + + it('should get dataLengthIsLoading', () => { + const action = {} as Action; + const state = fromSearch.searchReducer(undefined, action); + + expect(fromSearch.selectDataLengthIsLoading(state)).toBeFalsy(); + }); + + it('should get dataLengthIsLoaded', () => { + const action = {} as Action; + const state = fromSearch.searchReducer(undefined, action); + + expect(fromSearch.selectDataLengthIsLoaded(state)).toBeFalsy(); + }); + + it('should get dataLength', () => { + const action = {} as Action; + const state = fromSearch.searchReducer(undefined, action); + + expect(fromSearch.selectDataLength(state)).toBeNull(); + }); + + it('should get data', () => { + const action = {} as Action; + const state = fromSearch.searchReducer(undefined, action); + + expect(fromSearch.selectData(state).length).toEqual(0); + }); + + it('should get dataIsLoading', () => { + const action = {} as Action; + const state = fromSearch.searchReducer(undefined, action); + + expect(fromSearch.selectDataIsLoading(state)).toBeFalsy(); + }); + + it('should get dataIsLoaded', () => { + const action = {} as Action; + const state = fromSearch.searchReducer(undefined, action); + + expect(fromSearch.selectDataIsLoaded(state)).toBeFalsy(); + }); + + it('should get selectedData', () => { + const action = {} as Action; + const state = fromSearch.searchReducer(undefined, action); + + expect(fromSearch.selectSelectedData(state).length).toEqual(0); + }); +}); diff --git a/client/src/app/instance/store/reducers/search.reducer.ts b/client/src/app/instance/store/reducers/search.reducer.ts index ee7d006a215d73bd0c44ac1a75b98bc4233f2f06..e81da50750d1d573e65d2d3e307ef8408b147a5d 100644 --- a/client/src/app/instance/store/reducers/search.reducer.ts +++ b/client/src/app/instance/store/reducers/search.reducer.ts @@ -12,6 +12,11 @@ import { createReducer, on } from '@ngrx/store'; import { Criterion } from '../models'; import * as searchActions from '../actions/search.actions'; +/** + * Interface for search state. + * + * @interface State + */ export interface State { pristine: boolean; currentDataset: string, @@ -92,7 +97,7 @@ export const searchReducer = createReducer( ...state, criteriaList: [...state.criteriaList.filter(c => c.id !== idCriterion)] })), - on(searchActions.updateOutputList, (state, { outputList}) => ({ + on(searchActions.updateOutputList, (state, { outputList }) => ({ ...state, outputList })), @@ -136,7 +141,7 @@ export const searchReducer = createReducer( })), on(searchActions.destroyResults, state => ({ ...state, - searchData: [], + data: [], dataLength: null })), on(searchActions.resetSearch, () => ({ diff --git a/client/src/app/instance/store/selectors/cone-search.selector.spec.ts b/client/src/app/instance/store/selectors/cone-search.selector.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..82b1b31811c36fec225d9dfe785e76aa9df1cd74 --- /dev/null +++ b/client/src/app/instance/store/selectors/cone-search.selector.spec.ts @@ -0,0 +1,29 @@ +import * as coneSearchSelector from './cone-search.selector'; +import * as fromConeSearch from '../reducers/cone-search.reducer'; + +describe('Cone search selector', () => { + it('should get coneSearch', () => { + const state = { instance: { coneSearch: { ...fromConeSearch.initialState }}}; + expect(coneSearchSelector.selectConeSearch(state)).toBeNull(); + }); + + it('should get resolver', () => { + const state = { instance: { coneSearch: { ...fromConeSearch.initialState }}}; + expect(coneSearchSelector.selectResolver(state)).toBeNull(); + }); + + it('should get resolverIsLoading', () => { + const state = { instance: { coneSearch: { ...fromConeSearch.initialState }}}; + expect(coneSearchSelector.selectResolverIsLoading(state)).toBeFalsy(); + }); + + it('should get resolverIsLoaded', () => { + const state = { instance: { coneSearch: { ...fromConeSearch.initialState }}}; + expect(coneSearchSelector.selectResolverIsLoaded(state)).toBeFalsy(); + }); + + it('should get cone search by route', () => { + const state = { router: { state: { queryParams: { cs: 'myConeSearch' }}}}; + expect(coneSearchSelector.selectConeSearchByRoute(state)).toEqual('myConeSearch'); + }); +}); diff --git a/client/src/app/instance/store/selectors/detail.selector.spec.ts b/client/src/app/instance/store/selectors/detail.selector.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..b6f0a0ae2f140c3ae244a6e09c44950e60e59cb2 --- /dev/null +++ b/client/src/app/instance/store/selectors/detail.selector.spec.ts @@ -0,0 +1,39 @@ +import * as detailSelector from './detail.selector'; +import * as fromDetail from '../reducers/detail.reducer'; + +describe('Detail selector', () => { + it('should get object', () => { + const state = { instance: { detail: { ...fromDetail.initialState }}}; + expect(detailSelector.selectObject(state)).toBeNull(); + }); + + it('should get objectIsLoading', () => { + const state = { instance: { detail: { ...fromDetail.initialState }}}; + expect(detailSelector.selectObjectIsLoading(state)).toBeFalsy(); + }); + + it('should get objectIsLoaded', () => { + const state = { instance: { detail: { ...fromDetail.initialState }}}; + expect(detailSelector.selectObjectIsLoaded(state)).toBeFalsy(); + }); + + it('should get spectraCSV', () => { + const state = { instance: { detail: { ...fromDetail.initialState }}}; + expect(detailSelector.selectSpectraCSV(state)).toBeNull(); + }); + + it('should get spectraIsLoading', () => { + const state = { instance: { detail: { ...fromDetail.initialState }}}; + expect(detailSelector.selectSpectraIsLoading(state)).toBeFalsy(); + }); + + it('should get spectraIsLoaded', () => { + const state = { instance: { detail: { ...fromDetail.initialState }}}; + expect(detailSelector.selectSpectraIsLoaded(state)).toBeFalsy(); + }); + + it('should get id object by route', () => { + const state = { router: { state: { params: { id: 'myObjectId' }}}}; + expect(detailSelector.selectIdByRoute(state)).toEqual('myObjectId'); + }); +}); diff --git a/client/src/app/instance/store/selectors/samp.selector.spec.ts b/client/src/app/instance/store/selectors/samp.selector.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..bf57932e542ff0242eafdc8cc26513f440c79430 --- /dev/null +++ b/client/src/app/instance/store/selectors/samp.selector.spec.ts @@ -0,0 +1,9 @@ +import * as sampSelector from './samp.selector'; +import * as fromSamp from '../reducers/samp.reducer'; + +describe('Samp selector', () => { + it('should get registered', () => { + const state = { instance: { samp: { ...fromSamp.initialState }}}; + expect(sampSelector.selectRegistered(state)).toBeFalsy(); + }); +}); diff --git a/client/src/app/instance/store/selectors/search.selector.spec.ts b/client/src/app/instance/store/selectors/search.selector.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..db1a2d182390b0f8cdccb3334077d218e0a78299 --- /dev/null +++ b/client/src/app/instance/store/selectors/search.selector.spec.ts @@ -0,0 +1,182 @@ +import * as searchSelector from './search.selector'; +import * as fromSearch from '../reducers/search.reducer'; +import * as fromConeSearch from '../reducers/cone-search.reducer'; +import { ConeSearch, Criterion, FieldCriterion } from '../models'; + +describe('Search selector', () => { + it('should get selectPristine', () => { + const state = { instance: { search: { ...fromSearch.initialState }}}; + expect(searchSelector.selectPristine(state)).toBeTruthy(); + }); + + it('should get currentDataset', () => { + const state = { instance: { search: { ...fromSearch.initialState }}}; + expect(searchSelector.selectCurrentDataset(state)).toBeNull(); + }); + + it('should get currentStep', () => { + const state = { instance: { search: { ...fromSearch.initialState }}}; + expect(searchSelector.selectCurrentStep(state)).toBeNull(); + }); + + it('should get criteriaStepChecked', () => { + const state = { instance: { search: { ...fromSearch.initialState }}}; + expect(searchSelector.selectCriteriaStepChecked(state)).toBeFalsy(); + }); + + it('should get outputStepChecked', () => { + const state = { instance: { search: { ...fromSearch.initialState }}}; + expect(searchSelector.selectOutputStepChecked(state)).toBeFalsy(); + }); + + it('should get resultStepChecked', () => { + const state = { instance: { search: { ...fromSearch.initialState }}}; + expect(searchSelector.selectResultStepChecked(state)).toBeFalsy(); + }); + + it('should get coneSearchAdded', () => { + const state = { instance: { search: { ...fromSearch.initialState }}}; + expect(searchSelector.selectIsConeSearchAdded(state)).toBeFalsy(); + }); + + it('should get criteriaList', () => { + const state = { instance: { search: { ...fromSearch.initialState }}}; + expect(searchSelector.selectCriteriaList(state).length).toEqual(0); + }); + + it('should get outputList', () => { + const state = { instance: { search: { ...fromSearch.initialState }}}; + expect(searchSelector.selectOutputList(state).length).toEqual(0); + }); + + it('should get dataLengthIsLoading', () => { + const state = { instance: { search: { ...fromSearch.initialState }}}; + expect(searchSelector.selectDataLengthIsLoading(state)).toBeFalsy(); + }); + + it('should get dataLengthIsLoaded', () => { + const state = { instance: { search: { ...fromSearch.initialState }}}; + expect(searchSelector.selectDataLengthIsLoaded(state)).toBeFalsy(); + }); + + it('should get dataLength', () => { + const state = { instance: { search: { ...fromSearch.initialState }}}; + expect(searchSelector.selectDataLength(state)).toBeNull(); + }); + + it('should get data', () => { + const state = { instance: { search: { ...fromSearch.initialState }}}; + expect(searchSelector.selectData(state).length).toEqual(0); + }); + + it('should get dataIsLoading', () => { + const state = { instance: { search: { ...fromSearch.initialState }}}; + expect(searchSelector.selectDataIsLoading(state)).toBeFalsy(); + }); + + it('should get dataIsLoaded', () => { + const state = { instance: { search: { ...fromSearch.initialState }}}; + expect(searchSelector.selectDataIsLoaded(state)).toBeFalsy(); + }); + + it('should get selectedData', () => { + const state = { instance: { search: { ...fromSearch.initialState }}}; + expect(searchSelector.selectSelectedData(state).length).toEqual(0); + }); + + it('should get queryParams without criteria', () => { + const outputList: number[] = [1, 2]; + const state = { + instance: { + search: { + ...fromSearch.initialState, + outputList + }, + coneSearch: { ...fromConeSearch.initialState } + } + }; + const expected = { s: '000', a: '1;2' }; + + expect(searchSelector.selectQueryParams(state)).toEqual(expected); + }); + + it('should get queryParams with criteria', () => { + const outputList: number[] = [1, 2]; + const criteriaList: Criterion[] = [ + { id: 1, type: 'field', operator: 'eq', value: 'one' } as FieldCriterion, + { id: 2, type: 'field', operator: 'eq', value: 'two' } as FieldCriterion + ]; + const state = { + instance: { + search: { + ...fromSearch.initialState, + outputList, + criteriaList + }, + coneSearch: { ...fromConeSearch.initialState } + } + }; + const expected = { s: '000', a: '1;2', c: '1::eq::one;2::eq::two' }; + + expect(searchSelector.selectQueryParams(state)).toEqual(expected); + }); + + it('should get queryParams with cone search', () => { + const outputList: number[] = [1, 2]; + const coneSearchAdded: boolean = true; + const coneSearch: ConeSearch = { ra: 3, dec: 4, radius: 5 }; + const state = { + instance: { + search: { + ...fromSearch.initialState, + outputList, + coneSearchAdded + }, + coneSearch: { + ...fromConeSearch.initialState, + coneSearch + } + } + }; + const expected = { s: '000', a: '1;2', cs: '3:4:5' }; + + expect(searchSelector.selectQueryParams(state)).toEqual(expected); + }); + + it('should get queryParams with checked steps', () => { + const criteriaStepChecked: boolean = true; + const outputStepChecked: boolean = true; + const resultStepChecked: boolean = true; + const outputList: number[] = [1, 2]; + const state = { + instance: { + search: { + ...fromSearch.initialState, + criteriaStepChecked, + outputStepChecked, + resultStepChecked, + outputList + }, + coneSearch: { ...fromConeSearch.initialState } + } + }; + const expected = { s: '111', a: '1;2' }; + + expect(searchSelector.selectQueryParams(state)).toEqual(expected); + }); + + it('should get steps by route', () => { + const state = { router: { state: { queryParams: { s: 'myParams' }}}}; + expect(searchSelector.selectStepsByRoute(state)).toEqual('myParams'); + }); + + it('should get criteria by route', () => { + const state = { router: { state: { queryParams: { c: 'myParams' }}}}; + expect(searchSelector.selectCriteriaListByRoute(state)).toEqual('myParams'); + }); + + it('should get output by route', () => { + const state = { router: { state: { queryParams: { a: 'myParams' }}}}; + expect(searchSelector.selectOutputListByRoute(state)).toEqual('myParams'); + }); +}); diff --git a/client/src/app/instance/store/services/cone-search.service.spec.ts b/client/src/app/instance/store/services/cone-search.service.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..0924628fd0e18128860769794342a52ef7668197 --- /dev/null +++ b/client/src/app/instance/store/services/cone-search.service.spec.ts @@ -0,0 +1,34 @@ +import { TestBed, inject } from '@angular/core/testing'; +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; + +import { ConeSearchService } from './cone-search.service'; + +describe('ConeSearchService', () => { + let service: ConeSearchService; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + providers: [ConeSearchService] + }); + service = TestBed.inject(ConeSearchService); + }); + + it('#retrieveCoordinates() should return an Observable<any[]>', + inject([HttpTestingController, ConeSearchService],(httpMock: HttpTestingController, coneSearchService: ConeSearchService) => { + const mockResponse = 'myResponse'; + + coneSearchService.retrieveCoordinates('myObject').subscribe((event: any) => { + expect(event).toEqual(mockResponse); + }); + + const mockRequest = httpMock.expectOne('https://cdsweb.u-strasbg.fr/cgi-bin/nph-sesame/-ox/NSV?myObject'); + + expect(mockRequest.cancelled).toBeFalsy(); + expect(mockRequest.request.responseType).toEqual('text'); + mockRequest.flush(mockResponse); + + httpMock.verify(); + }) + ); +}); diff --git a/client/src/app/instance/store/services/detail.service.spec.ts b/client/src/app/instance/store/services/detail.service.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..e4acc7362bb8e7a5fa9f9fb532837edcb7fcb090 --- /dev/null +++ b/client/src/app/instance/store/services/detail.service.spec.ts @@ -0,0 +1,56 @@ +import { TestBed, inject } from '@angular/core/testing'; +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; + +import { DetailService } from './detail.service'; +import { AppConfigService } from 'src/app/app-config.service'; + +describe('DetailService', () => { + let service: DetailService; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + providers: [ + { provide: AppConfigService, useValue: { apiUrl: 'http://testing.com', servicesUrl: 'http://testingService.com' }}, + DetailService + ] + }); + service = TestBed.inject(DetailService); + }); + + it('#retrieveObject() should return an Observable<any[]>', + inject([HttpTestingController, DetailService],(httpMock: HttpTestingController, detailService: DetailService) => { + const mockResponse = ['myData']; + + detailService.retrieveObject('myDataset', 1, 'myObject', [2,3]).subscribe((event: any[]) => { + expect(event).toEqual(mockResponse); + }); + + const mockRequest = httpMock.expectOne('http://testing.com/search/myDataset?c=1::eq::myObject&a=2;3'); + + expect(mockRequest.cancelled).toBeFalsy(); + expect(mockRequest.request.responseType).toEqual('json'); + mockRequest.flush(mockResponse); + + httpMock.verify(); + }) + ); + + it('#retrieveSpectra() should return an Observable<string>', + inject([HttpTestingController, DetailService],(httpMock: HttpTestingController, detailService: DetailService) => { + const mockResponse = 'mySpectraData'; + + detailService.retrieveSpectra('myDataset', 'mySpectraFile').subscribe((event: string) => { + expect(event).toEqual(mockResponse); + }); + + const mockRequest = httpMock.expectOne('http://testingService.com/spectra-to-csv/myDataset?filename=mySpectraFile'); + + expect(mockRequest.cancelled).toBeFalsy(); + expect(mockRequest.request.responseType).toEqual('text'); + mockRequest.flush(mockResponse); + + httpMock.verify(); + }) + ); +}); diff --git a/client/src/app/instance/store/services/detail.service.ts b/client/src/app/instance/store/services/detail.service.ts index 3b946c5b17a2e9c649be86c4fa267ebe32f4907d..78b90758a95277c0fff8f1a394253fdaebcb55d1 100644 --- a/client/src/app/instance/store/services/detail.service.ts +++ b/client/src/app/instance/store/services/detail.service.ts @@ -14,11 +14,11 @@ import { Observable } from 'rxjs'; import { AppConfigService } from 'src/app/app-config.service'; -@Injectable() /** * @class * @classdesc Detail service. */ +@Injectable() export class DetailService { constructor(private http: HttpClient, private config: AppConfigService) { } @@ -38,8 +38,9 @@ export class DetailService { } /** - * Retrieves object details for the given parameters. + * Retrieves spectra data for the given spectra file. * + * @param {string} dname - The dataset name. * @param {string} spectraFile - The spectra file name. * * @return Observable<string> diff --git a/client/src/app/instance/store/services/samp.service.ts b/client/src/app/instance/store/services/samp.service.ts index 1429c9bf7394825137519be7bf9830021822d274..b74eeae43bb557ee0b979bac52a443ac426d9a24 100644 --- a/client/src/app/instance/store/services/samp.service.ts +++ b/client/src/app/instance/store/services/samp.service.ts @@ -15,11 +15,11 @@ import { AppConfigService } from 'src/app/app-config.service'; declare var samp: any; -@Injectable() /** * @class * @classdesc Samp service. */ +@Injectable() export class SampService { private connector = null; @@ -33,10 +33,15 @@ export class SampService { "home.page": "https://anis.lam.fr", "samp.icon.url": baseUrl + "/assets/cesam_anis40.png" }; - this.connector = new samp.Connector("anis-client", meta) + this.connector = new samp.Connector("anis-client", meta); } - register() { + /** + * Register to Samp. + * + * @return Observable<any> + */ + register(): Observable<any> { return new Observable(observer => { samp.register(this.connector.name, (conn) => { this.connector.setConnection(conn); @@ -49,10 +54,21 @@ export class SampService { }); } + /** + * Disconnect to Samp. + * + * @return Observable<any> + */ unregister(): void { this.connector.unregister(); } + /** + * Disconnect to Samp. + * + * @param {string} mtype - Message type. + * @param {string} url - URL. + */ broadcast(mtype: string, url: string): void { const message = new samp.Message(mtype, {"url": encodeURI(url)}); this.connector.connection.notifyAll([message]); diff --git a/client/src/app/instance/store/services/search.service.spec.ts b/client/src/app/instance/store/services/search.service.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..0e37a54148793e401d633de6f8739eaac6b8435a --- /dev/null +++ b/client/src/app/instance/store/services/search.service.spec.ts @@ -0,0 +1,58 @@ +import { TestBed, inject } from '@angular/core/testing'; +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; + +import { SearchService } from './search.service'; +import { AppConfigService } from 'src/app/app-config.service'; + +describe('SearchService', () => { + let service: SearchService; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + providers: [ + { provide: AppConfigService, useValue: { apiUrl: 'http://testing.com' } }, + SearchService + ] + }); + service = TestBed.inject(SearchService); + }); + + it('#retrieveData() should return an Observable<any[]>', + inject([HttpTestingController, SearchService],(httpMock: HttpTestingController, searchService: SearchService) => { + const mockResponse = ['myData']; + + searchService.retrieveData('myQuery').subscribe((event: any[]) => { + expect(event).toEqual(mockResponse); + }); + + const mockRequest = httpMock.expectOne('http://testing.com/search/myQuery'); + + expect(mockRequest.cancelled).toBeFalsy(); + expect(mockRequest.request.responseType).toEqual('json'); + mockRequest.flush(mockResponse); + + httpMock.verify(); + } + ) + ); + + it('#retrieveDataLength() should return an Observable<{ nb: number }[]>', + inject([HttpTestingController, SearchService],(httpMock: HttpTestingController, searchService: SearchService) => { + const mockResponse = [{ nb: 1 }]; + + searchService.retrieveDataLength('myQuery').subscribe((event: { nb: number }[]) => { + expect(event).toEqual(mockResponse); + }); + + const mockRequest = httpMock.expectOne('http://testing.com/search/myQuery'); + + expect(mockRequest.cancelled).toBeFalsy(); + expect(mockRequest.request.responseType).toEqual('json'); + mockRequest.flush(mockResponse); + + httpMock.verify(); + } + ) + ); +}); \ No newline at end of file diff --git a/client/src/app/instance/store/services/search.service.ts b/client/src/app/instance/store/services/search.service.ts index 171814321dc1e71d546be92e53c614ccd4d36410..2bdfaa2f53dc15dd30173c720db1a6d93c2add19 100644 --- a/client/src/app/instance/store/services/search.service.ts +++ b/client/src/app/instance/store/services/search.service.ts @@ -14,11 +14,11 @@ import { Observable } from 'rxjs'; import { AppConfigService } from 'src/app/app-config.service'; -@Injectable() /** * @class * @classdesc Search service. */ +@Injectable() export class SearchService { constructor(private http: HttpClient, private config: AppConfigService) { } diff --git a/client/yarn.lock b/client/yarn.lock index dfc67f9ae6f0b25094479ddb02882daaaa9a9099..6d4036f014ccabc8c05032111491ad3961de3c1a 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -5446,6 +5446,13 @@ jasmine-core@~3.7.0: resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-3.7.1.tgz#0401327f6249eac993d47bbfa18d4e8efacfb561" integrity sha512-DH3oYDS/AUvvr22+xUBW62m1Xoy7tUlY1tsxKEJvl5JeJ7q8zd1K5bUwiOxdH+erj6l2vAMM3hV25Xs9/WrmuQ== +jasmine-marbles@^0.8.3: + version "0.8.3" + resolved "https://registry.yarnpkg.com/jasmine-marbles/-/jasmine-marbles-0.8.3.tgz#a27253d1d52dfe49d8f145aba63f0bf18147b4ff" + integrity sha512-aaf7ObOC9X1jZ8VyIG49+vOTycRqIWT5Jt3vHHbECE9tN7U05mnpxi20thth02HzasOv/Fmqf+uGhcLE/f8NYg== + dependencies: + lodash "^4.17.20" + jest-changed-files@^27.0.6: version "27.0.6" resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-27.0.6.tgz#bed6183fcdea8a285482e3b50a9a7712d49a7a8b" @@ -6196,7 +6203,7 @@ lodash.uniq@^4.5.0: resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= -lodash@4.x, lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.21, lodash@^4.7.0: +lodash@4.x, lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.7.0: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==