Added tests for XosTable Component

Change-Id: I78fbb53176fc02e547bde316580c943eecd3d51f
diff --git a/src/app/core/table/array-to-list.filter.spec.ts b/src/app/core/table/array-to-list.filter.spec.ts
new file mode 100644
index 0000000..7e1e9da
--- /dev/null
+++ b/src/app/core/table/array-to-list.filter.spec.ts
@@ -0,0 +1,25 @@
+import * as angular from 'angular';
+import 'angular-mocks';
+import {ArrayToListFilter} from './array-to-list.filter';
+
+describe('The pagination filter', function () {
+
+  let $filter;
+
+  beforeEach(() => {
+    angular
+      .module('array', [])
+      .filter('arrayToList', ArrayToListFilter);
+    angular.mock.module('array');
+
+    inject(function (_$filter_: ng.ICompileService) {
+      $filter = _$filter_;
+    });
+  });
+
+  it('should return element from given to the end', function () {
+    let list = ['a', 'b', 'c', 'd'], result;
+    result = $filter('arrayToList')(list);
+    expect(result).toEqual('a, b, c, d');
+  });
+});
diff --git a/src/app/core/table/array-to-list.filter.ts b/src/app/core/table/array-to-list.filter.ts
new file mode 100644
index 0000000..ff5ab38
--- /dev/null
+++ b/src/app/core/table/array-to-list.filter.ts
@@ -0,0 +1,8 @@
+export function ArrayToListFilter() {
+  return (input: any) => {
+    if (!angular.isArray(input)) {
+      return input;
+    }
+    return input.join(', ');
+  };
+}
diff --git a/src/app/core/table/table.html b/src/app/core/table/table.html
index d7df1db..6621e5b 100644
--- a/src/app/core/table/table.html
+++ b/src/app/core/table/table.html
@@ -1,4 +1,4 @@
-<!--<div ng-show="vm.data.length > 0 && vm.loader == false">-->
+<div ng-show="vm.data.length > 0 && vm.loader == false">
     <div class="row" ng-if="vm.config.filter == 'fulltext'">
         <div class="col-xs-12">
             <input
@@ -104,12 +104,12 @@
             change="vm.goToPage">
     </xos-pagination>
     </div>
-<!--</div>-->
-<!--<div ng-show="(vm.data.length == 0 || !vm.data) && vm.loader == false">-->
-    <!--<xos-alert config="{type: 'info'}">-->
-        <!--No data to show.-->
-    <!--</xos-alert>-->
-<!--</div>-->
-<!--<div ng-show="vm.loader == true">-->
-    <!--<div class="loader"></div>-->
-<!--</div>-->
\ No newline at end of file
+</div>
+<div ng-show="(vm.data.length == 0 || !vm.data) && vm.loader == false">
+    <xos-alert config="{type: 'info'}" show="true">
+        No data to show.
+    </xos-alert>
+</div>
+<div ng-show="vm.loader == true">
+    <div class="loader"></div>
+</div>
\ No newline at end of file
diff --git a/src/app/core/table/table.spec.ts b/src/app/core/table/table.spec.ts
new file mode 100644
index 0000000..c7856f3
--- /dev/null
+++ b/src/app/core/table/table.spec.ts
@@ -0,0 +1,580 @@
+import * as angular from 'angular';
+import * as $ from 'jquery';
+import 'angular-mocks';
+import {xosTable} from './table';
+import {PaginationFilter} from '../pagination/pagination.filter';
+import {ArrayToListFilter} from './array-to-list.filter';
+import {xosLinkWrapper} from '../link-wrapper/link-wrapper';
+
+
+describe('The Xos Table component', () => {
+  beforeEach(() => {
+    angular
+      .module('table', [])
+      .component('xosTable', xosTable)
+      .filter('pagination', PaginationFilter)
+      .filter('arrayToList', ArrayToListFilter)
+      .directive('xosLinkWrapper', xosLinkWrapper);
+    angular.mock.module('table');
+  });
+
+  let scope, element, isolatedScope, rootScope, compile, filter;
+  const compileElement = () => {
+
+    if (!scope) {
+      scope = rootScope.$new();
+    }
+
+    element = angular.element('<xos-table config="config" data="data"></xos-table>');
+    compile(element)(scope);
+    scope.$digest();
+    isolatedScope = element.isolateScope().vm;
+  };
+
+  beforeEach(inject(function ($compile: ng.ICompileService, $rootScope: ng.IScope, $filter: ng.IFilterService) {
+    compile = $compile;
+    rootScope = $rootScope;
+    filter = $filter;
+  }));
+
+  it('should throw an error if no config is specified', () => {
+    function errorFunctionWrapper() {
+      compileElement();
+    }
+    expect(errorFunctionWrapper).toThrow(new Error('[xosTable] Please provide a configuration via the "config" attribute'));
+  });
+
+  it('should throw an error if no config columns are specified', () => {
+    function errorFunctionWrapper() {
+      // setup the parent scope
+      scope = rootScope.$new();
+      scope.config = 'green';
+      compileElement();
+    }
+    expect(errorFunctionWrapper).toThrow(new Error('[xosTable] Please provide a columns list in the configuration'));
+  });
+
+  describe('when basically configured', function() {
+
+    beforeEach(inject(function ($compile: ng.ICompileService, $rootScope: ng.IScope) {
+
+      scope = $rootScope.$new();
+
+      scope.config = {
+        columns: [
+          {
+            label: 'Label 1',
+            prop: 'label-1'
+          },
+          {
+            label: 'Label 2',
+            prop: 'label-2'
+          }
+        ]
+      };
+
+      scope.data = [
+        {
+          'label-1': 'Sample 1.1',
+          'label-2': 'Sample 1.2'
+        },
+        {
+          'label-1': 'Sample 2.1',
+          'label-2': 'Sample 2.2'
+        }
+      ];
+
+      element = angular.element('<xos-table config="config" data="data"></xos-table>');
+      $compile(element)(scope);
+      scope.$digest();
+      isolatedScope = element.isolateScope().vm;
+    }));
+
+    it('should contain 2 columns', function() {
+      const th = element[0].getElementsByTagName('th');
+      expect(th.length).toEqual(2);
+      expect(isolatedScope.columns.length).toEqual(2);
+    });
+
+    it('should contain 3 rows', function() {
+      const tr = element[0].getElementsByTagName('tr');
+      expect(tr.length).toEqual(3);
+    });
+
+    it('should render labels', () => {
+      let label1 = $(element).find('thead tr th')[0];
+      let label2 = $(element).find('thead tr th')[1];
+      expect($(label1).text().trim()).toEqual('Label 1');
+      expect($(label2).text().trim()).toEqual('Label 2');
+    });
+
+    describe('when no data are provided', () => {
+      beforeEach(() => {
+        isolatedScope.data = [];
+        scope.$digest();
+      });
+      it('should render an alert', () => {
+        let alert = $('xos-alert', element);
+        let table = $('table', element);
+        expect(alert.length).toEqual(1);
+        expect(table.length).toEqual(1);
+      });
+    });
+
+    describe('when a field type is provided', () => {
+      describe('and is boolean', () => {
+        beforeEach(() => {
+          scope.config = {
+            columns: [
+              {
+                label: 'Label 1',
+                prop: 'label-1',
+                type: 'boolean'
+              },
+              {
+                label: 'Label 2',
+                prop: 'label-2',
+                type: 'boolean'
+              }
+            ]
+          };
+          scope.data = [
+            {
+              'label-1': true,
+              'label-2': 1
+            },
+            {
+              'label-1': false,
+              'label-2': 0
+            }
+          ];
+          compileElement();
+        });
+
+        it('should render an incon', () => {
+          let td1 = $(element).find('tbody tr:first-child td')[0];
+          let td2 = $(element).find('tbody tr:last-child td')[0];
+          expect($(td1).find('i')).toHaveClass('fa-ok');
+          expect($(td2).find('i')).toHaveClass('fa-remove');
+        });
+
+        describe('with field filters', () => {
+          beforeEach(() => {
+            scope.config.filter = 'field';
+            compileElement();
+          });
+
+          it('should render a dropdown for filtering', () => {
+            let td1 = $(element).find('table tbody tr td')[0];
+            expect(td1).toContainElement('select');
+            expect(td1).not.toContainElement('input');
+          });
+
+          it('should correctly filter results', () => {
+            isolatedScope.query = {
+              'label-1': false
+            };
+            scope.$digest();
+            expect(isolatedScope.query['label-1']).toBeFalsy();
+            const tr = $(element).find('tbody:last-child > tr');
+            const icon = $(tr[0]).find('td i');
+            expect(tr.length).toEqual(1);
+            expect(icon).toHaveClass('fa-remove');
+          });
+
+          // NOTE the custom comparator we had has not been imported yet:
+          // https://github.com/opencord/ng-xos-lib/blob/master/src/services/helpers/ui/comparator.service.js
+          xit('should correctly filter results if the field is in the form of 0|1', () => {
+            isolatedScope.query = {
+              'label-2': false
+            };
+            scope.$digest();
+            expect(isolatedScope.query['label-2']).toBeFalsy();
+            const tr = $('tbody:last-child > tr', element);
+            expect(tr.length).toEqual(1);
+            const icon = $(tr[0]).find('td i');
+            expect(icon).toHaveClass('fa-remove');
+          });
+        });
+      });
+
+      describe('and is date', () => {
+        beforeEach(() => {
+          scope.config = {
+            columns: [
+              {
+                label: 'Label 1',
+                prop: 'label-1',
+                type: 'date'
+              }
+            ]
+          };
+          scope.data = [
+            {
+              'label-1': '2015-02-17T22:06:38.059000Z'
+            }
+          ];
+          compileElement();
+        });
+
+        it('should render an formatted date', () => {
+          let td1 = $(element).find('tbody tr:first-child td')[0];
+          const expectedDate = filter('date')(scope.data[0]['label-1'], 'H:mm MMM d, yyyy');
+          expect($(td1).text().trim()).toEqual(expectedDate);
+        });
+      });
+
+      describe('and is array', () => {
+        beforeEach(() => {
+          scope.data = [
+            {categories: ['Film', 'Music']}
+          ];
+          scope.config = {
+            filter: 'field',
+            columns: [
+              {
+                label: 'Categories',
+                prop: 'categories',
+                type: 'array'
+              }
+            ]
+          };
+          compileElement();
+        });
+        it('should render a comma separated list', () => {
+          let td1 = $(element).find('tbody:last-child tr:first-child')[0];
+          expect($(td1).text().trim()).toEqual('Film, Music');
+        });
+
+        it('should not render the filter field', () => {
+          let filter = $(element).find('tbody tr td')[0];
+          expect($(filter)).not.toContainElement('input');
+        });
+      });
+
+      describe('and is object', () => {
+        beforeEach(() => {
+          scope.data = [
+            {
+              attributes: {
+                age: 20,
+                height: 50
+              }
+            }
+          ];
+          scope.config = {
+            filter: 'field',
+            columns: [
+              {
+                label: 'Categories',
+                prop: 'attributes',
+                type: 'object'
+              }
+            ]
+          };
+          compileElement();
+        });
+        it('should render a list of key-values', () => {
+          let td = $(element).find('tbody:last-child tr:first-child')[0];
+          let ageLabel = $(td).find('dl dt')[0];
+          let ageValue = $(td).find('dl dd')[0];
+          let heightLabel = $(td).find('dl dt')[1];
+          let heightValue = $(td).find('dl dd')[1];
+          expect($(ageLabel).text().trim()).toEqual('age');
+          expect($(ageValue).text().trim()).toEqual('20');
+          expect($(heightLabel).text().trim()).toEqual('height');
+          expect($(heightValue).text().trim()).toEqual('50');
+        });
+
+        it('should not render the filter field', () => {
+          let filter = $(element).find('tbody tr td')[0];
+          expect($(filter)).not.toContainElement('input');
+        });
+      });
+
+      describe('and is custom', () => {
+
+        let formatterFn = jasmine.createSpy('formatter').and.returnValue('Formatted Content');
+
+        beforeEach(() => {
+          scope.data = [
+            {categories: ['Film', 'Music']}
+          ];
+          scope.config = {
+            filter: 'field',
+            columns: [
+              {
+                label: 'Categories',
+                prop: 'categories',
+                type: 'custom',
+                formatter: formatterFn
+              }
+            ]
+          };
+          compileElement();
+        });
+
+        it('should check for a formatter property', () => {
+          function errorFunctionWrapper() {
+            // setup the parent scope
+            scope = rootScope.$new();
+            scope.config = {
+              columns: [
+                {
+                  label: 'Categories',
+                  prop: 'categories',
+                  type: 'custom'
+                }
+              ]
+            };
+            compileElement();
+          }
+          expect(errorFunctionWrapper).toThrow(new Error('[xosTable] You have provided a custom field type, a formatter function should provided too.'));
+        });
+
+        it('should check that the formatter property is a function', () => {
+          function errorFunctionWrapper() {
+            // setup the parent scope
+            scope = rootScope.$new();
+            scope.config = {
+              columns: [
+                {
+                  label: 'Categories',
+                  prop: 'categories',
+                  type: 'custom',
+                  formatter: 'formatter'
+                }
+              ]
+            };
+            compileElement();
+          }
+          expect(errorFunctionWrapper).toThrow(new Error('[xosTable] You have provided a custom field type, a formatter function should provided too.'));
+        });
+
+        it('should format data using the formatter property', () => {
+          let td1 = $(element).find('tbody:last-child tr:first-child')[0];
+          expect($(td1).text().trim()).toEqual('Formatted Content');
+          // the custom formatted should receive the entire object, otherwise is not so custom
+          expect(formatterFn).toHaveBeenCalledWith({categories: ['Film', 'Music']});
+        });
+
+        it('should not render the filter field', () => {
+          // displayed value is different from model val, filter would not work
+          let filter = $(element).find('tbody tr td')[0];
+          expect($(filter)).not.toContainElement('input');
+        });
+      });
+
+      describe('and is icon', () => {
+
+        beforeEach(() => {
+          scope.config = {
+            columns: [
+              {
+                label: 'Label 1',
+                prop: 'label-1',
+                type: 'icon',
+                formatter: item => {
+                  switch (item['label-1']) {
+                    case 1:
+                      return 'ok';
+                    case 2:
+                      return 'remove';
+                    case 3:
+                      return 'plus';
+                  }
+                }
+              }
+            ]
+          };
+          scope.data = [
+            {
+              'label-1': 1
+            },
+            {
+              'label-1': 2
+            },
+            {
+              'label-1': 3
+            }
+          ];
+          compileElement();
+        });
+
+        it('should render a custom icon', () => {
+          let td1 = $(element).find('tbody tr:first-child td')[0];
+          let td2 = $(element).find('tbody tr:nth-child(2) td')[0];
+          let td3 = $(element).find('tbody tr:last-child td')[0];
+          expect($(td1).find('i')).toHaveClass('fa-ok');
+          expect($(td2).find('i')).toHaveClass('fa-remove');
+          expect($(td3).find('i')).toHaveClass('fa-plus');
+        });
+      });
+    });
+
+    describe('when a link property is provided', () => {
+      beforeEach(() => {
+        scope.data = [
+          {
+            id: 1
+          }
+        ];
+        scope.config = {
+          columns: [
+            {
+              label: 'Id',
+              prop: 'id',
+              link: (item) => {
+                return `state({id: ${item.id}})`;
+              }
+            }
+          ]
+        };
+        compileElement();
+      });
+
+      it('should check that the link property is a function', () => {
+        function errorFunctionWrapper() {
+          // setup the parent scope
+          scope = rootScope.$new();
+          scope.config = {
+            columns: [
+              {
+                label: 'Categories',
+                prop: 'categories',
+                link: 'custom'
+              }
+            ]
+          };
+          compileElement();
+        }
+        expect(errorFunctionWrapper).toThrow(new Error('[xosTable] The link property should be a function.'));
+      });
+
+      it('should render a link with the correct url', () => {
+        let link = $('tbody tr:first-child td a', element)[0];
+        expect($(link).attr('ui-sref')).toEqual('state({id: 1})');
+      });
+    });
+
+    describe('when actions are passed', () => {
+
+      let cb = jasmine.createSpy('callback');
+
+      beforeEach(() => {
+        isolatedScope.config.actions = [
+          {
+            label: 'delete',
+            icon: 'remove',
+            cb: cb,
+            color: 'red'
+          }
+        ];
+        scope.$digest();
+      });
+
+      it('should have 3 columns', () => {
+        const th = element[0].getElementsByTagName('th');
+        expect(th.length).toEqual(3);
+        expect(isolatedScope.columns.length).toEqual(2);
+      });
+
+      it('when clicking on action should invoke callback', () => {
+        const link = element[0].getElementsByTagName('a')[0];
+        link.click();
+        expect(cb).toHaveBeenCalledWith(scope.data[0]);
+      });
+    });
+
+    describe('when filter is fulltext', () => {
+      beforeEach(() => {
+        isolatedScope.config.filter = 'fulltext';
+        scope.$digest();
+      });
+
+      it('should render a text field', () => {
+        const textField = element[0].getElementsByTagName('input');
+        expect(textField.length).toEqual(1);
+      });
+
+      describe('and a value is enterd', () => {
+        beforeEach(() => {
+          isolatedScope.query = '2.2';
+          scope.$digest();
+        });
+
+        it('should contain 2 rows', function() {
+          const tr = element[0].getElementsByTagName('tr');
+          expect(tr.length).toEqual(2);
+        });
+      });
+    });
+
+    describe('when filter is field', () => {
+      beforeEach(() => {
+        isolatedScope.config.filter = 'field';
+        scope.$digest();
+      });
+
+      it('should render a text field for each column', () => {
+        const textField = element[0].getElementsByTagName('input');
+        expect(textField.length).toEqual(2);
+      });
+
+      describe('and a value is enterd', () => {
+        beforeEach(() => {
+          isolatedScope.query = {'label-1': '2.1'};
+          scope.$digest();
+        });
+
+        it('should contain 3 rows', function() {
+          const tr = element[0].getElementsByTagName('tr');
+          expect(tr.length).toEqual(3);
+        });
+      });
+    });
+
+    describe('when order is true', () => {
+      beforeEach(() => {
+        isolatedScope.config.order = true;
+        scope.$digest();
+      });
+
+      it('should render a arrows beside', () => {
+        const arrows = element[0].getElementsByTagName('i');
+        expect(arrows.length).toEqual(4);
+      });
+
+      describe('and a default ordering is passed', () => {
+
+        beforeEach(() => {
+          scope.config.order = {
+            field: 'label-1',
+            reverse: true
+          };
+          compileElement();
+        });
+
+        it('should orderBy the default order', () => {
+          const tr = $(element).find('tr');
+          expect($(tr[1]).text()).toContain('Sample 2.2');
+          expect($(tr[2]).text()).toContain('Sample 1.1');
+        });
+      });
+
+      describe('and an order is set', () => {
+        beforeEach(() => {
+          isolatedScope.orderBy = 'label-1';
+          isolatedScope.reverse = true;
+          scope.$digest();
+        });
+
+        it('should orderBy', function() {
+          const tr = $(element).find('tr');
+          expect($(tr[1]).text()).toContain('Sample 2.2');
+          expect($(tr[2]).text()).toContain('Sample 1.1');
+        });
+      });
+    });
+  });
+});
diff --git a/src/app/core/table/table.ts b/src/app/core/table/table.ts
index 2c0b272..5fb884c 100644
--- a/src/app/core/table/table.ts
+++ b/src/app/core/table/table.ts
@@ -38,18 +38,31 @@
 }
 
 class TableCtrl {
-  $inject = ['$onInit'];
+  $inject = ['$onInit', '$scope'];
 
   public columns: any[];
   public orderBy: string;
   public reverse: boolean;
   public classes: string;
+  public data: any;
   private config: IXosTableCfg;
   private currentPage: number;
+  private loader: boolean = true;
 
+  constructor(
+    private $scope: ng.IScope
+  ) {
+
+  }
 
   $onInit() {
 
+    this.$scope.$watch(() => this.data, data => {
+      if (angular.isDefined(data)) {
+        this.loader = false;
+      }
+    });
+
     this.classes = 'table table-striped'; // table-bordered
 
     if (!this.config) {