From 513b47695adf28ce0d1e0af63660107aa4d551a1 Mon Sep 17 00:00:00 2001
From: Tifenn Guillas <tifenn.guillas@lam.fr>
Date: Thu, 28 Oct 2021 16:29:01 +0200
Subject: [PATCH] Tests on renderers => DONE

---
 .../search-type/checkbox.component.spec.ts    |   2 -
 .../datatable/datatable.component.spec.ts     | 341 ++++++++++--------
 .../datatable/datatable.component.ts          |   4 +-
 .../detail-renderer.component.spec.ts         |  22 ++
 .../renderer/detail-renderer.component.ts     |   8 +-
 .../download-renderer.component.spec.ts       |  43 +++
 .../renderer/download-renderer.component.ts   |  15 +-
 .../renderer/image-renderer.component.spec.ts |  44 +++
 .../renderer/image-renderer.component.ts      |  18 +-
 .../renderer/json-renderer.component.spec.ts  |  27 ++
 .../renderer/json-renderer.component.ts       |   9 +-
 .../renderer/link-renderer.component.spec.ts  |  46 +++
 .../renderer/link-renderer.component.ts       |   8 +-
 client/src/test-data.ts                       |  58 ++-
 14 files changed, 473 insertions(+), 172 deletions(-)
 create mode 100644 client/src/app/instance/shared-search/components/datatable/renderer/detail-renderer.component.spec.ts
 create mode 100644 client/src/app/instance/shared-search/components/datatable/renderer/download-renderer.component.spec.ts
 create mode 100644 client/src/app/instance/shared-search/components/datatable/renderer/image-renderer.component.spec.ts
 create mode 100644 client/src/app/instance/shared-search/components/datatable/renderer/json-renderer.component.spec.ts
 create mode 100644 client/src/app/instance/shared-search/components/datatable/renderer/link-renderer.component.spec.ts

diff --git a/client/src/app/instance/search/components/criteria/search-type/checkbox.component.spec.ts b/client/src/app/instance/search/components/criteria/search-type/checkbox.component.spec.ts
index ed915471..98d1cc28 100644
--- a/client/src/app/instance/search/components/criteria/search-type/checkbox.component.spec.ts
+++ b/client/src/app/instance/search/components/criteria/search-type/checkbox.component.spec.ts
@@ -5,8 +5,6 @@ import { FormArray, FormsModule, ReactiveFormsModule } from '@angular/forms';
 import { CheckboxComponent } from './checkbox.component';
 import { SelectMultipleCriterion } from '../../../../store/models/criterion';
 import { Option } from '../../../../../metamodel/models';
-import { AbstractSearchComponent } from '../../../containers/abstract-search.component';
-import * as searchActions from '../../../../store/actions/search.actions';
 
 describe('[Instance][Search][Component][Criteria][SearchType] CheckboxComponent', () => {
     @Component({
diff --git a/client/src/app/instance/shared-search/components/datatable/datatable.component.spec.ts b/client/src/app/instance/shared-search/components/datatable/datatable.component.spec.ts
index 1a99ea6c..c8425783 100644
--- a/client/src/app/instance/shared-search/components/datatable/datatable.component.spec.ts
+++ b/client/src/app/instance/shared-search/components/datatable/datatable.component.spec.ts
@@ -1,11 +1,9 @@
-import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
 import { Component, Input } from '@angular/core';
-import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
-
-import { AccordionModule } from 'ngx-bootstrap/accordion';
+import { FormsModule } from '@angular/forms';
 
 import { DatatableComponent } from './datatable.component';
-import { SearchQueryParams } from '../../../store/models';
+import { Pagination, PaginationOrder, SearchQueryParams } from '../../../store/models';
 import {
     DetailRendererConfig,
     DownloadRendererConfig,
@@ -13,8 +11,13 @@ import {
     LinkRendererConfig,
     RendererConfig
 } from '../../../../metamodel/models/renderers';
+import { ATTRIBUTE_LIST, DATASET } from '../../../../../test-data';
+import { Attribute } from '../../../../metamodel/models';
 
 describe('[Instance][SharedSearch][Components][Datatable] DatatableComponent', () => {
+    @Component({ selector: 'app-spinner', template: '' })
+    class SpinnerStubComponent { }
+
     @Component({ selector: 'app-detail-renderer', template: '' })
     class DetailRendererStubComponent {
         @Input() value: string | number;
@@ -59,15 +62,17 @@ describe('[Instance][SharedSearch][Components][Datatable] DatatableComponent', (
         @Input() boundaryLinks: boolean;
         @Input() rotate: boolean;
         @Input() maxSize: number;
+        @Input() itemsPerPage: number;
     }
 
     let component: DatatableComponent;
     let fixture: ComponentFixture<DatatableComponent>;
 
-    beforeEach(() => {
+    beforeEach(waitForAsync(() => {
         TestBed.configureTestingModule({
             declarations: [
                 DatatableComponent,
+                SpinnerStubComponent,
                 DetailRendererStubComponent,
                 LinkRendererStubComponent,
                 DownloadRendererStubComponent,
@@ -75,148 +80,200 @@ describe('[Instance][SharedSearch][Components][Datatable] DatatableComponent', (
                 JsonRendererStubComponent,
                 PaginationStubComponent
             ],
-            // imports: [AccordionModule.forRoot(), BrowserAnimationsModule]
+            imports: [FormsModule]
         });
         fixture = TestBed.createComponent(DatatableComponent);
         component = fixture.componentInstance;
-    });
+    }));
 
     it('should create the component', () => {
         expect(component).toBeTruthy();
     });
 
-    // it('#requiredParams() should return if required params are loaded', () => {
-    //     component.attributeList = ATTRIBUTE_LIST;
-    //     component.outputList = [1];
-    //     component.dataLength = 1;
-    //     expect(component.requiredParams()).toBeTruthy();
-    //     component.dataLength = undefined;
-    //     expect(component.requiredParams()).toBeFalsy();
-    //     component.dataLength = 1;
-    //     component.outputList = [];
-    //     expect(component.requiredParams()).toBeFalsy();
-    //     component.outputList = [1];
-    //     component.attributeList = []
-    //     expect(component.requiredParams()).toBeFalsy();
-    // });
-    //
-    // it('#noSelectedData() should return true if no selectedData', () => {
-    //     component.selectedData = [];
-    //     expect(component.noSelectedData()).toBeTruthy();
-    // });
-    //
-    // it('#noSelectedData() should return false if there are selectedData', () => {
-    //     component.selectedData = [123456];
-    //     expect(component.noSelectedData()).toBeFalsy();
-    // });
-    //
-    // it('#getOutputList() should return filtered output list', () => {
-    //     component.outputList = [2]
-    //     component.attributeList = ATTRIBUTE_LIST;
-    //     expect(component.getOutputList().length).toBe(1);
-    // });
-    //
-    // it('#toggleSelection(datum) should return add datum to selectedData', () => {
-    //     const datum = { label_one: 123456 };
-    //     component.attributeList = ATTRIBUTE_LIST;
-    //     component.selectedData = [];
-    //     component.addSelectedData.subscribe((event: any) => expect(event).toBe(123456));
-    //     component.toggleSelection(datum);
-    // });
-    //
-    // it('#toggleSelection(datum) should return remove datum to selectedData', () => {
-    //     const datum = { label_one: 123456 };
-    //     component.selectedData = [123456];
-    //     component.attributeList = ATTRIBUTE_LIST;
-    //     component.deleteSelectedData.subscribe((event: any) => expect(event).toBe(123456));
-    //     component.toggleSelection(datum);
-    // });
-    //
-    // it('#isSelected(datum) should return true datum is selected', () => {
-    //     const datum = { label_one: 123456 };
-    //     component.attributeList = ATTRIBUTE_LIST;
-    //     component.selectedData = [123456];
-    //     expect(component.isSelected(datum)).toBeTruthy();
-    // });
-    //
-    // it('#isSelected(datum) should return false datum is not selected', () => {
-    //     const datum = { label_one: 123456 };
-    //     component.attributeList = ATTRIBUTE_LIST;
-    //     component.selectedData = [];
-    //     expect(component.isSelected(datum)).toBeFalsy();
-    // });
-    //
-    // it('#changePage() should change page value and raise getData event', () => {
-    //     component.dataset = DATASET;
-    //     component.sortedCol = 1;
-    //     component.sortedOrder = PaginationOrder.a;
-    //     const expectedPagination: Pagination = {
-    //         dname: 'cat_1',
-    //         page: 2,
-    //         nbItems: 10,
-    //         sortedCol: 1,
-    //         order: PaginationOrder.a
-    //     }
-    //     component.getData.subscribe((event: Pagination) => expect(event).toEqual(expectedPagination));
-    //     component.changePage(2);
-    // });
-    //
-    // it('#changeNbItems() should change nbItems value and raise getData event', () => {
-    //     component.dataset = DATASET;
-    //     component.sortedCol = 1;
-    //     component.sortedOrder = PaginationOrder.a;
-    //     const expectedPagination: Pagination = {
-    //         dname: 'cat_1',
-    //         page: 1,
-    //         nbItems: 20,
-    //         sortedCol: 1,
-    //         order: PaginationOrder.a
-    //     }
-    //     component.getData.subscribe((event: Pagination) => expect(event).toEqual(expectedPagination));
-    //     component.changeNbItems(20);
-    // });
-    //
-    // it('#sort() should raise getData event with correct parameters', () => {
-    //     component.dataset = DATASET;
-    //     component.sortedOrder = PaginationOrder.a;
-    //     let expectedPagination: Pagination = {
-    //         dname: 'cat_1',
-    //         page: 1,
-    //         nbItems: 10,
-    //         sortedCol: 1,
-    //         order: PaginationOrder.a
-    //     }
-    //     let subscribtion = component.getData.subscribe((event: Pagination) => expect(event).toEqual(expectedPagination));
-    //     component.sort(1);
-    //     subscribtion.unsubscribe();
-    //     component.sortedCol = 1;
-    //     component.sortedOrder = PaginationOrder.a;
-    //     expectedPagination = {
-    //         dname: 'cat_1',
-    //         page: 1,
-    //         nbItems: 10,
-    //         sortedCol: 1,
-    //         order: PaginationOrder.d
-    //     }
-    //     subscribtion = component.getData.subscribe((event: Pagination) => expect(event).toEqual(expectedPagination));
-    //     component.sort(1);
-    //     subscribtion.unsubscribe();
-    //     component.sortedCol = 1;
-    //     component.sortedOrder = PaginationOrder.d;
-    //     expectedPagination = {
-    //         dname: 'cat_1',
-    //         page: 1,
-    //         nbItems: 10,
-    //         sortedCol: 1,
-    //         order: PaginationOrder.a
-    //     }
-    //     subscribtion = component.getData.subscribe((event: Pagination) => expect(event).toEqual(expectedPagination));
-    //     component.sort(1);
-    // });
-    //
-    // it('#ngOnInit() should init sortedCol value', () => {
-    //     component.attributeList = ATTRIBUTE_LIST;
-    //     component.ngOnInit();
-    //     expect(component.sortedCol).toEqual(1);
-    // });
+    it('#ngOnInit() should init sortedCol value and raise retrieveData event ', (done) => {
+        component.dataset = DATASET;
+        component.attributeList = ATTRIBUTE_LIST;
+        const spy = jest.spyOn(component.retrieveData, 'emit');
+        const expectedPagination: Pagination = {
+            dname: 'myDataset',
+            page: 1,
+            nbItems: 10,
+            sortedCol: 1,
+            order: PaginationOrder.a
+        }
+        component.ngOnInit();
+        Promise.resolve(null).then(function() {
+            expect(component.sortedCol).toEqual(1);
+            expect(spy).toHaveBeenCalledTimes(1);
+            expect(spy).toHaveBeenCalledWith(expectedPagination);
+            done();
+        });
+    });
+
+    it('#getRendererConfig() should return attribute renderer configuration', () => {
+        let attribute: Attribute = {
+            id: 1,
+            name: 'myAttribute',
+            label: 'my label attribute',
+            form_label: 'my form label attribute',
+            output_display: 1,
+            criteria_display: 1,
+            type: 'integer',
+            display_detail: 1
+        };
+        expect(component.getRendererConfig(attribute)).toBeNull();
+        const detailRendererConfig: DetailRendererConfig = {
+            id: 'renderer-config',
+            display: 'display',
+            icon_button: 'icon',
+            blank: true
+        };
+        attribute.renderer = 'detail';
+        attribute.renderer_config = detailRendererConfig;
+        expect(component.getRendererConfig(attribute)).toEqual(detailRendererConfig);
+        const linkRendererConfig: LinkRendererConfig = {
+            id: 'renderer-config',
+            href: 'href',
+            display: 'display',
+            text: 'text',
+            icon: 'icon',
+            blank: true
+        };
+        attribute.renderer = 'link';
+        attribute.renderer_config = linkRendererConfig;
+        expect(component.getRendererConfig(attribute)).toEqual(linkRendererConfig);
+        const downloadRendererConfig: DownloadRendererConfig = {
+            id: 'renderer-config',
+            display: 'display',
+            text: 'text',
+            icon: 'icon'
+        };
+        attribute.renderer = 'download';
+        attribute.renderer_config = downloadRendererConfig;
+        expect(component.getRendererConfig(attribute)).toEqual(downloadRendererConfig);
+        const imageRendererConfig: ImageRendererConfig = {
+            id: 'renderer-config',
+            display: 'display',
+            type: 'type',
+            width: 'width',
+            height: 'height'
+        };
+        attribute.renderer = 'image';
+        attribute.renderer_config = imageRendererConfig;
+        expect(component.getRendererConfig(attribute)).toEqual(imageRendererConfig);
+        const jsonRendererConfig: RendererConfig = { id: 'renderer-config' };
+        attribute.renderer = 'json';
+        attribute.renderer_config = jsonRendererConfig;
+        expect(component.getRendererConfig(attribute)).toEqual(jsonRendererConfig);
+    });
+
+    it('#getOutputList() should return filtered output list', () => {
+        component.outputList = [2]
+        component.attributeList = ATTRIBUTE_LIST;
+        expect(component.getOutputList().length).toBe(1);
+    });
+
+    it('#toggleSelection(datum) should return added datum to selectedData', () => {
+        const datum = { label_one: 123456 };
+        component.attributeList = ATTRIBUTE_LIST;
+        component.selectedData = [];
+        component.addSelectedData.subscribe((event: any) => expect(event).toBe(123456));
+        component.toggleSelection(datum);
+    });
+
+    it('#toggleSelection(datum) should return remove datum to selectedData', () => {
+        const datum = { label_one: 123456 };
+        component.selectedData = [123456];
+        component.attributeList = ATTRIBUTE_LIST;
+        component.deleteSelectedData.subscribe((event: any) => expect(event).toBe(123456));
+        component.toggleSelection(datum);
+    });
+
+    it('#isSelected(datum) should return true datum is selected', () => {
+        const datum = { label_one: 123456 };
+        component.attributeList = ATTRIBUTE_LIST;
+        component.selectedData = [123456];
+        expect(component.isSelected(datum)).toBeTruthy();
+    });
+
+    it('#isSelected(datum) should return false datum is not selected', () => {
+        const datum = { label_one: 123456 };
+        component.attributeList = ATTRIBUTE_LIST;
+        component.selectedData = [];
+        expect(component.isSelected(datum)).toBeFalsy();
+    });
+
+    it('#changePage() should change page value and raise retrieveData event', () => {
+        component.dataset = DATASET;
+        component.sortedCol = 1;
+        component.sortedOrder = PaginationOrder.a;
+        const expectedPagination: Pagination = {
+            dname: 'myDataset',
+            page: 2,
+            nbItems: 10,
+            sortedCol: 1,
+            order: PaginationOrder.a
+        };
+        const spy = jest.spyOn(component.retrieveData, 'emit');
+        component.changePage(2);
+        expect(spy).toHaveBeenCalledTimes(1);
+        expect(spy).toHaveBeenCalledWith(expectedPagination);
+    });
+
+    it('#changeNbItems() should change nbItems value and raise retrieveData event', () => {
+        component.dataset = DATASET;
+        component.sortedCol = 1;
+        component.sortedOrder = PaginationOrder.a;
+        const expectedPagination: Pagination = {
+            dname: 'myDataset',
+            page: 1,
+            nbItems: 20,
+            sortedCol: 1,
+            order: PaginationOrder.a
+        };
+        const spy = jest.spyOn(component.retrieveData, 'emit');
+        component.changeNbItems(20);
+        expect(spy).toHaveBeenCalledTimes(1);
+        expect(spy).toHaveBeenCalledWith(expectedPagination);
+    });
+
+    it('#sort() should raise retrieveData event with correct parameters', () => {
+        component.dataset = DATASET;
+        component.sortedOrder = PaginationOrder.a;
+        let expectedPagination: Pagination = {
+            dname: 'myDataset',
+            page: 1,
+            nbItems: 10,
+            sortedCol: 1,
+            order: PaginationOrder.a
+        };
+        const spy = jest.spyOn(component.retrieveData, 'emit');
+        component.sort(1);
+        expect(spy).toHaveBeenCalledTimes(1);
+        expect(spy).toHaveBeenCalledWith(expectedPagination);
+        component.sortedCol = 1;
+        component.sortedOrder = PaginationOrder.a;
+        expectedPagination = {
+            dname: 'myDataset',
+            page: 1,
+            nbItems: 10,
+            sortedCol: 1,
+            order: PaginationOrder.d
+        };
+        component.sort(1);
+        expect(spy).toHaveBeenCalledTimes(2);
+        expect(spy).toHaveBeenLastCalledWith(expectedPagination);
+        component.sortedCol = 1;
+        component.sortedOrder = PaginationOrder.d;
+        expectedPagination = {
+            dname: 'myDataset',
+            page: 1,
+            nbItems: 10,
+            sortedCol: 1,
+            order: PaginationOrder.a
+        };
+        component.sort(1);
+        expect(spy).toHaveBeenCalledTimes(3);
+        expect(spy).toHaveBeenLastCalledWith(expectedPagination);
+    });
 });
diff --git a/client/src/app/instance/shared-search/components/datatable/datatable.component.ts b/client/src/app/instance/shared-search/components/datatable/datatable.component.ts
index 51615592..b9887163 100644
--- a/client/src/app/instance/shared-search/components/datatable/datatable.component.ts
+++ b/client/src/app/instance/shared-search/components/datatable/datatable.component.ts
@@ -50,7 +50,7 @@ export class DatatableComponent implements OnInit {
     public nbItems = 10;
     public sortedCol: number = null;
     public sortedOrder: PaginationOrder = PaginationOrder.a;
-    
+
     ngOnInit(): void {
         this.sortedCol = this.attributeList.find(a => a.order_by).id;
         Promise.resolve(null).then(() => this.retrieveData.emit({
@@ -72,7 +72,7 @@ export class DatatableComponent implements OnInit {
     getRendererConfig(attribute: Attribute): DetailRendererConfig | LinkRendererConfig | DownloadRendererConfig | ImageRendererConfig | RendererConfig | null {
         switch(attribute.renderer) {
             case 'detail':
-                return  attribute.renderer_config as DetailRendererConfig;
+                return attribute.renderer_config as DetailRendererConfig;
             case 'link':
                 return attribute.renderer_config as LinkRendererConfig;
             case 'download':
diff --git a/client/src/app/instance/shared-search/components/datatable/renderer/detail-renderer.component.spec.ts b/client/src/app/instance/shared-search/components/datatable/renderer/detail-renderer.component.spec.ts
new file mode 100644
index 00000000..4c6ff87e
--- /dev/null
+++ b/client/src/app/instance/shared-search/components/datatable/renderer/detail-renderer.component.spec.ts
@@ -0,0 +1,22 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { DetailRendererComponent } from './detail-renderer.component';
+import { RouterTestingModule } from '@angular/router/testing';
+
+describe('[Instance][SharedSearch][Components][Datatable][Renderer] DetailRendererComponent', () => {
+    let component: DetailRendererComponent;
+    let fixture: ComponentFixture<DetailRendererComponent>;
+
+    beforeEach(() => {
+        TestBed.configureTestingModule({
+            imports: [RouterTestingModule],
+            declarations: [DetailRendererComponent]
+        });
+        fixture = TestBed.createComponent(DetailRendererComponent);
+        component = fixture.componentInstance;
+    });
+
+    it('should create the component', () => {
+        expect(component).toBeTruthy();
+    });
+});
diff --git a/client/src/app/instance/shared-search/components/datatable/renderer/detail-renderer.component.ts b/client/src/app/instance/shared-search/components/datatable/renderer/detail-renderer.component.ts
index 8d148a3f..50d753c7 100644
--- a/client/src/app/instance/shared-search/components/datatable/renderer/detail-renderer.component.ts
+++ b/client/src/app/instance/shared-search/components/datatable/renderer/detail-renderer.component.ts
@@ -12,15 +12,15 @@ import { Component, Input, ChangeDetectionStrategy } from '@angular/core';
 import { SearchQueryParams } from 'src/app/instance/store/models';
 import { DetailRendererConfig } from 'src/app/metamodel/models/renderers/detail-renderer-config.model';
 
+/**
+ * @class
+ * @classdesc Detail renderer component.
+ */
 @Component({
     selector: 'app-detail-renderer',
     templateUrl: 'detail-renderer.component.html',
     changeDetection: ChangeDetectionStrategy.OnPush
 })
-/**
- * @class
- * @classdesc Detail renderer component.
- */
 export class DetailRendererComponent {
     @Input() value: string | number;
     @Input() datasetName: string;
diff --git a/client/src/app/instance/shared-search/components/datatable/renderer/download-renderer.component.spec.ts b/client/src/app/instance/shared-search/components/datatable/renderer/download-renderer.component.spec.ts
new file mode 100644
index 00000000..f2603916
--- /dev/null
+++ b/client/src/app/instance/shared-search/components/datatable/renderer/download-renderer.component.spec.ts
@@ -0,0 +1,43 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+
+import { DownloadRendererComponent } from './download-renderer.component';
+import { AppConfigService } from '../../../../../app-config.service';
+
+describe('[Instance][SharedSearch][Components][Datatable][Renderer] DownloadRendererComponent', () => {
+    let component: DownloadRendererComponent;
+    let fixture: ComponentFixture<DownloadRendererComponent>;
+    let appConfigServiceStub = new AppConfigService();
+
+    beforeEach(() => {
+        TestBed.configureTestingModule({
+            declarations: [DownloadRendererComponent],
+            imports: [HttpClientTestingModule],
+            providers: [{ provide: AppConfigService, useValue: appConfigServiceStub }]
+        });
+        fixture = TestBed.createComponent(DownloadRendererComponent);
+        component = fixture.componentInstance;
+    });
+
+    it('should create the component', () => {
+        expect(component).toBeTruthy();
+    });
+
+    it('#getHref() should return file url', () => {
+        appConfigServiceStub.apiUrl = 'https://test.com';
+        component.datasetName = 'myDataset';
+        component.value = 'myId';
+        expect(component.getHref()).toBe('https://test.com/download-file/myDataset/myId');
+    });
+
+    it('#getText() should return link text', () => {
+        component.config = {
+            id: 'renderer-config',
+            display: 'display',
+            text: 'This is $value',
+            icon: 'icon'
+        };
+        component.value = 'myId';
+        expect(component.getText()).toEqual('This is myId');
+    });
+});
diff --git a/client/src/app/instance/shared-search/components/datatable/renderer/download-renderer.component.ts b/client/src/app/instance/shared-search/components/datatable/renderer/download-renderer.component.ts
index 56475b8e..fd006db6 100644
--- a/client/src/app/instance/shared-search/components/datatable/renderer/download-renderer.component.ts
+++ b/client/src/app/instance/shared-search/components/datatable/renderer/download-renderer.component.ts
@@ -14,15 +14,15 @@ import { DownloadRendererConfig } from 'src/app/metamodel/models/renderers/downl
 import { getHost } from 'src/app/shared/utils';
 import { AppConfigService } from 'src/app/app-config.service';
 
+/**
+ * @class
+ * @classdesc Download renderer component.
+ */
 @Component({
     selector: 'app-download-renderer',
     templateUrl: 'download-renderer.component.html',
     changeDetection: ChangeDetectionStrategy.OnPush
 })
-/**
- * @class
- * @classdesc Download renderer component.
- */
 export class DownloadRendererComponent {
     @Input() value: string;
     @Input() datasetName: string;
@@ -49,11 +49,14 @@ export class DownloadRendererComponent {
         return this.config.text.replace('$value', this.value.toString());
     }
 
-    click(event) {
+    /**
+     * Downloads file on click.
+     */
+    click(event): void {
         event.preventDefault();
 
         const href = this.getHref();
-        this.http.get(href, {responseType: "blob"}).subscribe(
+        this.http.get(href, { responseType: "blob" }).subscribe(
             data => {
                 const filename = href.substring(href.lastIndexOf('/') + 1);
 
diff --git a/client/src/app/instance/shared-search/components/datatable/renderer/image-renderer.component.spec.ts b/client/src/app/instance/shared-search/components/datatable/renderer/image-renderer.component.spec.ts
new file mode 100644
index 00000000..eb865f56
--- /dev/null
+++ b/client/src/app/instance/shared-search/components/datatable/renderer/image-renderer.component.spec.ts
@@ -0,0 +1,44 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { ModalModule } from 'ngx-bootstrap/modal';
+
+import { ImageRendererComponent } from './image-renderer.component';
+import { AppConfigService } from '../../../../../app-config.service';
+
+describe('[Instance][SharedSearch][Components][Datatable][Renderer] ImageRendererComponent', () => {
+    let component: ImageRendererComponent;
+    let fixture: ComponentFixture<ImageRendererComponent>;
+    let appConfigServiceStub = new AppConfigService();
+
+    beforeEach(() => {
+        TestBed.configureTestingModule({
+            declarations: [ImageRendererComponent],
+            imports: [ModalModule.forRoot()],
+            providers: [{ provide: AppConfigService, useValue: appConfigServiceStub }]
+        });
+        fixture = TestBed.createComponent(ImageRendererComponent);
+        component = fixture.componentInstance;
+    });
+
+    it('should create the component', () => {
+        expect(component).toBeTruthy();
+    });
+
+    it('#getValue() should return image url', () => {
+        component.datasetName = 'myDataset';
+        component.value = 'myObjId';
+        component.config = {
+            id: 'renderer-config',
+            display: 'display',
+            type: 'type',
+            width: 'width',
+            height: 'height'
+        };
+        appConfigServiceStub.apiUrl = 'https://test.com';
+        expect(component.getValue()).toEqual('myObjId');
+        component.config.type = 'fits';
+        appConfigServiceStub.servicesUrl = 'https://services.com';
+        const expectedValue = 'https://services.com/fits-to-png/myDataset?filename=myObjId&stretch=linear&pmin=0.25&pmax=99.75&axes=true';
+        expect(component.getValue()).toEqual(expectedValue);
+    });
+});
diff --git a/client/src/app/instance/shared-search/components/datatable/renderer/image-renderer.component.ts b/client/src/app/instance/shared-search/components/datatable/renderer/image-renderer.component.ts
index 677dfd73..2660cc92 100644
--- a/client/src/app/instance/shared-search/components/datatable/renderer/image-renderer.component.ts
+++ b/client/src/app/instance/shared-search/components/datatable/renderer/image-renderer.component.ts
@@ -15,15 +15,15 @@ import { BsModalRef } from 'ngx-bootstrap/modal/bs-modal-ref.service';
 import { ImageRendererConfig } from 'src/app/metamodel/models/renderers';
 import { AppConfigService } from 'src/app/app-config.service';
 
+/**
+ * @class
+ * @classdesc Image renderer component.
+ */
 @Component({
     selector: 'app-image-renderer',
     templateUrl: 'image-renderer.component.html',
     changeDetection: ChangeDetectionStrategy.OnPush
 })
-/**
- * @class
- * @classdesc Image renderer component.
- */
 export class ImageRendererComponent {
     @Input() value: string | number;
     @Input() datasetName: string;
@@ -33,7 +33,12 @@ export class ImageRendererComponent {
 
     constructor(private modalService: BsModalService, private appConfig: AppConfigService) { }
 
-    openModal(template: TemplateRef<any>) {
+    /**
+     * Opens modal.
+     *
+     * @param  {TemplateRef<any>} template - The modal template to open.
+     */
+    openModal(template: TemplateRef<any>): void {
         this.modalRef = this.modalService.show(template);
     }
 
@@ -46,8 +51,7 @@ export class ImageRendererComponent {
         if (this.config.type === 'fits') {
             return `${this.appConfig.servicesUrl}/fits-to-png/${this.datasetName}?filename=${this.value}`
                 + `&stretch=linear&pmin=0.25&pmax=99.75&axes=true`;
-        } else {
-            return this.value as string;
         }
+        return this.value as string;
     }
 }
diff --git a/client/src/app/instance/shared-search/components/datatable/renderer/json-renderer.component.spec.ts b/client/src/app/instance/shared-search/components/datatable/renderer/json-renderer.component.spec.ts
new file mode 100644
index 00000000..9139c33d
--- /dev/null
+++ b/client/src/app/instance/shared-search/components/datatable/renderer/json-renderer.component.spec.ts
@@ -0,0 +1,27 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { ModalModule } from 'ngx-bootstrap/modal';
+import { NgxJsonViewerModule } from 'ngx-json-viewer';
+
+import { JsonRendererComponent } from './json-renderer.component';
+
+describe('[Instance][SharedSearch][Components][Datatable][Renderer] JsonRendererComponent', () => {
+    let component: JsonRendererComponent;
+    let fixture: ComponentFixture<JsonRendererComponent>;
+
+    beforeEach(() => {
+        TestBed.configureTestingModule({
+            declarations: [JsonRendererComponent],
+            imports: [
+                NgxJsonViewerModule,
+                ModalModule.forRoot()
+            ],
+        });
+        fixture = TestBed.createComponent(JsonRendererComponent);
+        component = fixture.componentInstance;
+    });
+
+    it('should create the component', () => {
+        expect(component).toBeTruthy();
+    });
+});
diff --git a/client/src/app/instance/shared-search/components/datatable/renderer/json-renderer.component.ts b/client/src/app/instance/shared-search/components/datatable/renderer/json-renderer.component.ts
index 530de465..993f5aeb 100644
--- a/client/src/app/instance/shared-search/components/datatable/renderer/json-renderer.component.ts
+++ b/client/src/app/instance/shared-search/components/datatable/renderer/json-renderer.component.ts
@@ -8,19 +8,20 @@
  */
 
 import { Component, ChangeDetectionStrategy, Input, TemplateRef } from '@angular/core';
+
 import { BsModalService, BsModalRef } from 'ngx-bootstrap/modal';
 
 import { RendererConfig } from 'src/app/metamodel/models/renderers/renderer-config.model';
 
+/**
+ * @class
+ * @classdesc JSON renderer component.
+ */
 @Component({
     selector: 'app-json-renderer',
     templateUrl: 'json-renderer.component.html',
     changeDetection: ChangeDetectionStrategy.OnPush
 })
-/**
- * @class
- * @classdesc JSON renderer component.
- */
 export class JsonRendererComponent {
     @Input() value: string | number;
     @Input() attributeLabel: string;
diff --git a/client/src/app/instance/shared-search/components/datatable/renderer/link-renderer.component.spec.ts b/client/src/app/instance/shared-search/components/datatable/renderer/link-renderer.component.spec.ts
new file mode 100644
index 00000000..1d868f49
--- /dev/null
+++ b/client/src/app/instance/shared-search/components/datatable/renderer/link-renderer.component.spec.ts
@@ -0,0 +1,46 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { LinkRendererComponent } from './link-renderer.component';
+
+describe('[Instance][SharedSearch][Components][Datatable][Renderer] LinkRendererComponent', () => {
+    let component: LinkRendererComponent;
+    let fixture: ComponentFixture<LinkRendererComponent>;
+
+    beforeEach(() => {
+        TestBed.configureTestingModule({
+            declarations: [LinkRendererComponent]
+        });
+        fixture = TestBed.createComponent(LinkRendererComponent);
+        component = fixture.componentInstance;
+    });
+
+    it('should create the component', () => {
+        expect(component).toBeTruthy();
+    });
+
+    it('#getValue() should return link url', () => {
+        component.config = {
+            id: 'renderer-config',
+            href: 'http://test.com/$value',
+            display: 'display',
+            text: 'text',
+            icon: 'icon',
+            blank: true
+        };
+        component.value = 'myId';
+        expect(component.getValue()).toEqual('http://test.com/myId');
+    });
+
+    it('#getText() should return link text', () => {
+        component.config = {
+            id: 'renderer-config',
+            href: 'href',
+            display: 'display',
+            text: 'This is $value',
+            icon: 'icon',
+            blank: true
+        };
+        component.value = 'myId';
+        expect(component.getText()).toEqual('This is myId');
+    });
+});
diff --git a/client/src/app/instance/shared-search/components/datatable/renderer/link-renderer.component.ts b/client/src/app/instance/shared-search/components/datatable/renderer/link-renderer.component.ts
index 0737e453..ea339302 100644
--- a/client/src/app/instance/shared-search/components/datatable/renderer/link-renderer.component.ts
+++ b/client/src/app/instance/shared-search/components/datatable/renderer/link-renderer.component.ts
@@ -11,15 +11,15 @@ import { Component, Input, ChangeDetectionStrategy } from '@angular/core';
 
 import { LinkRendererConfig } from 'src/app/metamodel/models/renderers/link-renderer-config.model';
 
+/**
+ * @class
+ * @classdesc Link renderer component.
+ */
 @Component({
     selector: 'app-link-renderer',
     templateUrl: 'link-renderer.component.html',
     changeDetection: ChangeDetectionStrategy.OnPush
 })
-/**
- * @class
- * @classdesc Link renderer component.
- */
 export class LinkRendererComponent {
     @Input() value: string | number;
     @Input() datasetName: string;
diff --git a/client/src/test-data.ts b/client/src/test-data.ts
index dd800418..4928da85 100644
--- a/client/src/test-data.ts
+++ b/client/src/test-data.ts
@@ -1,4 +1,59 @@
-import { Attribute, OutputCategory } from './app/metamodel/models';
+import { Attribute, Dataset, OutputCategory } from './app/metamodel/models';
+
+export const DATASET: Dataset = {
+    name: 'myDataset',
+    table_ref: 'table',
+    label: 'my dataset',
+    description: 'This is my dataset',
+    display: 1,
+    data_path: 'path',
+    survey_name: 'mySurvey',
+    id_dataset_family: 1,
+    public: true,
+    full_data_path: '/data/path',
+    config: {
+        images: ['image'],
+        survey: {
+            survey_enabled: true,
+            survey_label: 'More about this survey'
+        },
+        cone_search: {
+            cone_search_enabled: true,
+            cone_search_opened: true,
+            cone_search_column_ra: 1,
+            cone_search_column_dec: 2,
+            cone_search_plot_enabled: true,
+            cone_search_sdss_enabled: true,
+            cone_search_sdss_display: 1,
+            cone_search_background: [{ id: 1, enabled: true, display: 1 }]
+        },
+        download: {
+            download_enabled: true,
+            download_opened: true,
+            download_csv: true,
+            download_ascii: true,
+            download_vo: true,
+            download_archive: true
+        },
+        summary: {
+            summary_enabled: true,
+            summary_opened: true
+        },
+        server_link: {
+            server_link_enabled: true,
+            server_link_opened: true
+        },
+        samp: {
+            samp_enabled: true,
+            samp_opened: true
+        },
+        datatable: {
+            datatable_enabled: true,
+            datatable_opened: true,
+            datatable_selectable_rows: true
+        }
+    }
+};
 
 export const ATTRIBUTE_LIST: Attribute[] = [
     {
@@ -51,6 +106,7 @@ export const ATTRIBUTE_LIST: Attribute[] = [
         search_type: 'field',
         operator: '=',
         type: 'integer',
+        order_by: true,
         detail: true,
         display_detail: 2,
         options: [
-- 
GitLab