Added tests for XosTable Component
Change-Id: I78fbb53176fc02e547bde316580c943eecd3d51f
diff --git a/src/app/core/index.ts b/src/app/core/index.ts
index 17e15ac..0cc1e12 100644
--- a/src/app/core/index.ts
+++ b/src/app/core/index.ts
@@ -23,6 +23,7 @@
import {xosPagination} from './pagination/pagination';
import {PaginationFilter} from './pagination/pagination.filter';
import {XosDebouncer} from './services/helpers/debounce.helper';
+import {ArrayToListFilter} from './table/array-to-list.filter';
export const xosCore = 'xosCore';
@@ -55,4 +56,5 @@
.component('xosValidation', xosValidation)
.component('xosSidePanel', xosSidePanel)
.component('xosKeyBindingPanel', xosKeyBindingPanel)
- .filter('pagination', PaginationFilter);
+ .filter('pagination', PaginationFilter)
+ .filter('arrayToList', ArrayToListFilter);
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) {
diff --git a/src/app/datasources/rest/auth.rest.spec.ts b/src/app/datasources/rest/auth.rest.spec.ts
index 483bcfc..35732a3 100644
--- a/src/app/datasources/rest/auth.rest.spec.ts
+++ b/src/app/datasources/rest/auth.rest.spec.ts
@@ -61,7 +61,7 @@
done(e);
});
$scope.$apply();
- httpBackend.flush();
+ // httpBackend.flush();
});
});
@@ -77,16 +77,14 @@
it('should remove user auth from cookies', (done) => {
service.logout()
.then((res) => {
- expect($cookies.get('xoscsrftoken')).toEqual(undefined);
- expect($cookies.get('xossessionid')).toEqual(undefined);
- expect($cookies.get('xosuser')).toEqual(undefined);
+ expect($cookies.get('sessionid')).toEqual(undefined);
done();
})
.catch(e => {
done(e);
});
$scope.$apply();
- httpBackend.flush();
+ // httpBackend.flush();
});
});
});
diff --git a/src/app/datasources/rest/modeldefs.rest.spec.ts b/src/app/datasources/rest/modeldefs.rest.spec.ts
index 677463b..5b54fb8 100644
--- a/src/app/datasources/rest/modeldefs.rest.spec.ts
+++ b/src/app/datasources/rest/modeldefs.rest.spec.ts
@@ -51,6 +51,6 @@
done(e);
});
$scope.$apply();
- httpBackend.flush();
+ // httpBackend.flush();
});
});