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&amp;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==