describe('ng-table', function() { var data = [ { id: 1, name: "Moroni", age: 50, money: -10 }, { id: 2, name: "Tiancum", age: 43, money: 120 }, { id: 3, name: "Jacob", age: 27, money: 5.5 }, { id: 4, name: "Nephi", age: 29, money: -54 }, { id: 5, name: "Enos", age: 34, money: 110 }, { id: 6, name: "Tiancum", age: 43, money: 1000 }, { id: 7, name: "Jacob", age: 27, money: -201 }, { id: 8, name: "Nephi", age: 29, money: 100 }, { id: 9, name: "Enos", age: 34, money: -52.5 }, { id: 10, name: "Tiancum", age: 43, money: 52.1 }, { id: 11, name: "Jacob", age: 27, money: 110 }, { id: 12, name: "Nephi", age: 29, money: -55 }, { id: 13, name: "Enos", age: 34, money: 551 }, { id: 14, name: "Tiancum", age: 43, money: -1410 }, { id: 15, name: "Jacob", age: 27, money: 410 }, { id: 16, name: "Nephi", age: 29, money: 100 }, { id: 17, name: "Enos", age: 34, money: -100 } ]; var NgTableParams; beforeEach(module('ngTable')); var scope; beforeEach(inject(function($rootScope, _NgTableParams_) { scope = $rootScope.$new(true); NgTableParams = _NgTableParams_; })); function createNgTableParams(settings) { var initialParams; if (arguments.length === 2){ initialParams = arguments[0]; settings = arguments[1]; } settings = angular.extend({}, { filterDelay: 0 }, settings); var tableParams = new NgTableParams(initialParams, settings); spyOn(tableParams.settings(), 'getData').and.callThrough(); return tableParams; } describe('basics', function(){ var elm; beforeEach(inject(function($compile, $q) { elm = angular.element( '
' + '' + '' + '' + '' + '' + '' + '
' + '{{user.name}}' + '' + '{{user.age}}' + '' + '{{user.money}}' + '
' + '
'); scope.nameTitle = function(){ return 'Name of person'; }; scope.ageTitle = function(){ return 'Age'; }; scope.moneyTitle = function(){ return 'Money'; }; scope.showName = true; scope.showAge = true; scope.showMoney = true; scope.ageTitle = function(){ return 'Age'; }; scope.moneyTitle = function(){ return 'Money'; }; scope.getCustomClass = function($column){ if ($column.title().indexOf('Money') !== -1){ return 'moneyHeaderClass'; } else{ return 'customClass'; } }; scope.money = function(/*$column*/) { var def = $q.defer(); def.resolve([{ 'id': 10, 'title': '10' }]); return def; }; $compile(elm)(scope); scope.$digest(); })); it('should create table header', function() { var thead = elm.find('thead'); expect(thead.length).toBe(1); var rows = thead.find('tr'); expect(rows.length).toBe(2); var titles = angular.element(rows[0]).find('th'); expect(titles.length).toBe(3); expect(angular.element(titles[0]).text().trim()).toBe('Name of person'); expect(angular.element(titles[1]).text().trim()).toBe('Age'); expect(angular.element(titles[2]).text().trim()).toBe('Money'); expect(angular.element(rows[1]).hasClass('ng-table-filters')).toBeTruthy(); var filters = angular.element(rows[1]).find('th'); expect(filters.length).toBe(3); expect(angular.element(filters[0]).hasClass('filter')).toBeTruthy(); expect(angular.element(filters[1]).hasClass('filter')).toBeTruthy(); expect(angular.element(filters[2]).hasClass('filter')).toBeTruthy(); }); it('should create table header classes', function() { var thead = elm.find('thead'); var rows = thead.find('tr'); var titles = angular.element(rows[0]).find('th'); expect(angular.element(titles[0]).hasClass('header')).toBeTruthy(); expect(angular.element(titles[1]).hasClass('header')).toBeTruthy(); expect(angular.element(titles[2]).hasClass('header')).toBeTruthy(); expect(angular.element(titles[0]).hasClass('sortable')).toBeTruthy(); expect(angular.element(titles[1]).hasClass('sortable')).toBeTruthy(); expect(angular.element(titles[2]).hasClass('sortable')).toBeFalsy(); expect(angular.element(titles[0]).hasClass('customClass')).toBeTruthy(); expect(angular.element(titles[1]).hasClass('customClass')).toBeTruthy(); expect(angular.element(titles[2]).hasClass('moneyHeaderClass')).toBeTruthy(); }); it('should create table header titles', function() { var thead = elm.find('thead'); var rows = thead.find('tr'); var titles = angular.element(rows[0]).find('th'); expect(angular.element(titles[0]).attr('title').trim()).toBe('Sort by Name'); expect(angular.element(titles[1]).attr('title').trim()).toBe('Sort by Age'); expect(angular.element(titles[2]).attr('title').trim()).toBe('Sort by Money'); }); it('should show scope data', function() { var tbody = elm.find('tbody'); expect(tbody.length).toBe(1); var rows = tbody.find('tr'); expect(rows.length).toBe(0); var params = new NgTableParams({ page: 1, // show first page count: 10 // count per page }, { total: data.length, // length of data getData: function($defer, params) { $defer.resolve(data.slice((params.page() - 1) * params.count(), params.page() * params.count())); } }); scope.tableParams = params; scope.$digest(); rows = tbody.find('tr'); expect(rows.length).toBe(10); scope.tableParams.page(2); scope.$digest(); rows = tbody.find('tr'); expect(rows.length).toBe(7); params.total(20); scope.$digest(); rows = tbody.find('tr'); expect(rows.length).toBe(7); }); it('should show data-title-text', function() { var tbody = elm.find('tbody'); var params = new NgTableParams({ page: 1, // show first page count: 10 // count per page }, { total: data.length, // length of data getData: function($defer, params) { $defer.resolve(data); } }); scope.tableParams = params; scope.$digest(); var filterRow = angular.element(elm.find('thead').find('tr')[1]); var filterCells = filterRow.find('th'); expect(angular.element(filterCells[0]).attr('data-title-text').trim()).toBe('Name of person'); expect(angular.element(filterCells[1]).attr('data-title-text').trim()).toBe('Age'); expect(angular.element(filterCells[2]).attr('data-title-text').trim()).toBe('Money'); var dataRows = elm.find('tbody').find('tr'); var dataCells = angular.element(dataRows[0]).find('td'); expect(angular.element(dataCells[0]).attr('data-title-text').trim()).toBe('Name of person'); expect(angular.element(dataCells[1]).attr('data-title-text').trim()).toBe('Age'); expect(angular.element(dataCells[2]).attr('data-title-text').trim()).toBe('Money'); }); it('should show/hide columns', function() { var tbody = elm.find('tbody'); scope.tableParams = new NgTableParams({ page: 1, // show first page count: 10 // count per page }, { total: data.length, data: data }); scope.$digest(); var headerRow = angular.element(elm.find('thead').find('tr')[0]); expect(headerRow.find('th').length).toBe(3); var filterRow = angular.element(elm.find('thead').find('tr')[1]); expect(filterRow.find('th').length).toBe(3); var dataRow = angular.element(elm.find('tbody').find('tr')[0]); expect(dataRow.find('td').length).toBe(3); scope.showName = false; scope.$digest(); expect(headerRow.find('th').length).toBe(2); expect(filterRow.find('th').length).toBe(2); expect(dataRow.find('td').length).toBe(2); expect(angular.element(headerRow.find('th')[0]).text().trim()).toBe('Age'); expect(angular.element(headerRow.find('th')[1]).text().trim()).toBe('Money'); expect(angular.element(filterRow.find('th')[0]).find('input').length).toBe(0); expect(angular.element(filterRow.find('th')[1]).find('select').length).toBe(1); }); }); describe('title-alt', function() { var elm; beforeEach(inject(function($compile) { elm = angular.element( '' + '' + '' + '' + '' + '' + '
{{user.name}}{{user.age}}{{user.money}}
'); $compile(elm)(scope); scope.$digest(); var params = new NgTableParams({ page: 1, // show first page count: 10 // count per page }, { total: data.length, // length of data getData: function($defer, params) { $defer.resolve(data); } }); scope.tableParams = params; scope.$digest(); })); it('should show as data-title-text', inject(function($compile) { var filterRow = angular.element(elm.find('thead').find('tr')[1]); var filterCells = filterRow.find('th'); expect(angular.element(filterCells[0]).attr('data-title-text').trim()).toBe('Name'); expect(angular.element(filterCells[1]).attr('data-title-text').trim()).toBe('Age'); expect(angular.element(filterCells[2]).attr('data-title-text').trim()).toBe('£'); var dataRows = elm.find('tbody').find('tr'); var dataCells = angular.element(dataRows[0]).find('td'); expect(angular.element(dataCells[0]).attr('data-title-text').trim()).toBe('Name'); expect(angular.element(dataCells[1]).attr('data-title-text').trim()).toBe('Age'); expect(angular.element(dataCells[2]).attr('data-title-text').trim()).toBe('£'); })); }); describe('sorting', function() { it('should provide $column definition', inject(function($compile) { var columnDef; var elm = angular.element( '' + '' + '' + '' + '
{{user.age}}
'); scope.captureColumn = function($column){ columnDef = $column; return 'age' }; $compile(elm)(scope); scope.$digest(); expect(columnDef).toBeDefined(); })); it('should apply initial sort', inject(function ($compile) { var elm = angular.element( '' + '' + '
{{user.age}}
'); $compile(elm)(scope); var actualSort; scope.tableParams = new NgTableParams({ sorting: { age: 'desc' } }, { getData: function($defer, params){ actualSort = params.sorting(); $defer.resolve([]); }}); scope.$digest(); expect(actualSort.age).toBe('desc'); })); it('when sorting changes should trigger reload of table', inject(function ($compile) { var elm = angular.element( '' + '' + '
{{user.age}}
'); $compile(elm)(scope); var params = createNgTableParams(); scope.tableParams = params; scope.$digest(); params.settings().getData.calls.reset(); params.sorting()['age'] = 'desc'; scope.$digest(); expect(params.settings().getData.calls.count()).toBe(1); params.sorting()['age'] = 'asc'; scope.$digest(); expect(params.settings().getData.calls.count()).toBe(2); // setting the same sort order should not trigger reload params.sorting({ age: 'asc'}); scope.$digest(); expect(params.settings().getData.calls.count()).toBe(2); })); }); describe('paging', function() { var elm; beforeEach(inject(function($compile) { elm = angular.element( '' + '' + '' + '' + '
{{user.age}}
'); dataCallCount = 0; $compile(elm)(scope); scope.$digest(); })); function verifyPageWas(expectedPage){ expect(scope.tableParams.settings().getData.calls.argsFor(0)[0].page()).toBe(expectedPage); } it('should use initial NgTableParams constructor value', function(){ var params = createNgTableParams({ page: 2}, null); scope.tableParams = params; scope.$digest(); verifyPageWas(2); expect(params.settings().getData.calls.count()).toBe(1); }); it('should use initial NgTableParams constructor value combined with filter', function(){ var params = createNgTableParams({ page: 2, filter: { age: 5}}, null); scope.tableParams = params; scope.$digest(); verifyPageWas(2); expect(params.settings().getData.calls.count()).toBe(1); }); it('changing page # should trigger reload of data', function(){ var params = createNgTableParams({ page: 3}, null); scope.tableParams = params; scope.$digest(); verifyPageWas(3); params.settings().getData.calls.reset(); scope.tableParams.page(5); scope.$digest(); verifyPageWas(5); }); }); describe('filters', function(){ var $capturedColumn; beforeEach(inject(function() { // stash a reference to $column definition so that its available in asserts scope.captureColumn = function ($column) { $capturedColumn = $column; }; })); describe('filter specified as alias', function(){ var elm, tp; beforeEach(inject(function($compile) { elm = angular.element( '
' + '' + '' + '' + '' + '
{{user.name}}
' + '
'); $compile(elm)(scope); scope.$digest(); // 'text' is a shortcut alias for the template ng-table/filters/text scope.usernameFilter = {username: 'text'}; tp = scope.tableParams = createNgTableParams({ filterDelay: 10 }); scope.$digest(); })); it('should render named filter template', function() { var inputs = elm.find('thead').find('tr').eq(1).find('th').find('input'); expect(inputs.length).toBe(1); expect(inputs.eq(0).attr('type')).toBe('text'); expect(inputs.eq(0).attr('ng-model')).not.toBeUndefined(); expect(inputs.eq(0).attr('name')).toBe('username'); }); it('should databind ngTableParams.filter to filter input', function () { scope.tableParams.filter()['username'] = 'my name is...'; scope.$digest(); var input = elm.find('thead').find('tr').eq(1).find('th').find('input'); expect(input.val()).toBe('my name is...'); }); it('should make filter def available on $column', function () { expect($capturedColumn).toBeDefined(); expect($capturedColumn.filter).toBeDefined(); expect($capturedColumn.filter()['username']).toBe('text'); }); it('when filter changes should trigger reload of table', inject(function ($timeout) { tp.settings().getData.calls.reset(); tp.filter()['username'] = 'new value'; scope.$digest(); $timeout.flush(); // trigger delayed filter tp.filter()['username'] = 'another value'; scope.$digest(); $timeout.flush(); // trigger delayed filter expect(tp.settings().getData.calls.count()).toBe(2); // same value - should not trigger reload tp.filter()['username'] = 'another value'; scope.$digest(); try{ $timeout.flush(); // trigger delayed filter } catch (ex) { } expect(tp.settings().getData.calls.count()).toBe(2); })); it('when filter changes should reset page number to 1', inject(function ($timeout) { // trigger initial load of data so that subsequent changes to filter will trigger reset of page # tp.filter()['username'] = 'initial value'; scope.$digest(); $timeout.flush(); // trigger delayed filter // set page to something other than 1 tp.page(5); expect(tp.page()).toBe(5); // checking assumptions // when tp.filter()['username'] = 'new value'; scope.$digest(); $timeout.flush(); // trigger delayed filter expect(tp.page()).toBe(1); })); }); describe('filter specified with url', function(){ var elm; beforeEach(inject(function($compile) { elm = angular.element( '
' + '' + '' + '' + '' + '' + '
{{user.age}}
' + '
'); $compile(elm)(scope); scope.tableParams = createNgTableParams(); scope.$digest(); })); it('should render filter template specified by url', function() { var inputs = elm.find('thead').find('tr').eq(1).find('th').find('input'); expect(inputs.length).toBe(1); expect(inputs.eq(0).attr('type')).toBe('number'); expect(inputs.eq(0).attr('id')).toBe('age'); }); }); describe('multiple filter inputs', function(){ var elm; beforeEach(inject(function($compile) { elm = angular.element( '
' + '' + '' + '' + '' + '
{{user.name}}
' + '
'); $compile(elm)(scope); scope.$digest(); scope.tableParams = createNgTableParams(); scope.$digest(); })); it('should render filter template for each key/value pair ordered by key', function() { var inputs = elm.find('thead').find('tr').eq(1).find('th').find('input'); expect(inputs.length).toBe(2); expect(inputs.eq(0).attr('type')).toBe('text'); expect(inputs.eq(0).attr('ng-model')).not.toBeUndefined(); expect(inputs.eq(1).attr('type')).toBe('text'); expect(inputs.eq(1).attr('ng-model')).not.toBeUndefined(); }); it('should databind ngTableParams.filter to filter inputs', function () { scope.tableParams.filter()['name'] = 'my name is...'; scope.tableParams.filter()['age'] = '10'; scope.$digest(); var inputs = elm.find('thead').find('tr').eq(1).find('th').find('input'); expect(inputs.eq(0).val()).toBe('my name is...'); expect(inputs.eq(1).val()).toBe('10'); }); it('should make filter def available on $column', function () { expect($capturedColumn).toBeDefined(); expect($capturedColumn.filter).toBeDefined(); expect($capturedColumn.filter()['name']).toBe('text'); expect($capturedColumn.filter()['age']).toBe('text'); }); }); describe('dynamic filter', function(){ var elm, ageFilter; beforeEach(inject(function($compile) { ageFilter = {age: 'text'}; elm = angular.element( '
' + '' + '' + '' + '' + '' + '' + '
{{user.name}}{{user.age}}
' + '
'); $compile(elm)(scope); scope.$digest(); scope.getFilter = function(colDef){ if (colDef.id === 0) { return {username: 'text'}; } else if (colDef.id === 1) { return ageFilter; } }; scope.tableParams = createNgTableParams(); scope.$digest(); })); it('should render named filter template', function() { var usernameInput = elm.find('thead').find('tr').eq(1).find('th').eq(0).find('input'); expect(usernameInput.attr('type')).toBe('text'); expect(usernameInput.attr('name')).toBe('username'); var ageInput = elm.find('thead').find('tr').eq(1).find('th').eq(1).find('input'); expect(ageInput.attr('type')).toBe('text'); expect(ageInput.attr('name')).toBe('age'); }); it('should databind ngTableParams.filter to filter input', function () { scope.tableParams.filter()['username'] = 'my name is...'; scope.tableParams.filter()['age'] = '10'; scope.$digest(); var usernameInput = elm.find('thead').find('tr').eq(1).find('th').eq(0).find('input'); expect(usernameInput.val()).toBe('my name is...'); var ageInput = elm.find('thead').find('tr').eq(1).find('th').eq(1).find('input'); expect(ageInput.val()).toBe('10'); }); it('should render new template as filter changes', function() { ageFilter.age = 'number'; scope.$digest(); var ageInput = elm.find('thead').find('tr').eq(1).find('th').eq(1).find('input'); expect(ageInput.attr('type')).toBe('number'); expect(ageInput.attr('name')).toBe('age'); }); }); }); describe('internals', function(){ var elm, $timeout; beforeEach(inject(function($compile, _$timeout_) { $timeout = _$timeout_; elm = angular.element( '' + '' + '' + '' + '
{{user.age}}
'); $compile(elm)(scope); scope.$digest(); })); it('should reload when binding a new tableParams to scope', function(){ var tp = createNgTableParams(); scope.tableParams = tp; scope.$digest(); expect(tp.settings().getData.calls.count()).toBe(1); }); it('should reload 1 time when binding a new tableParams that has an initial settings data field', function(){ var tp = createNgTableParams({ data: [1,2,3] }); scope.tableParams = tp; scope.$digest(); expect(tp.settings().getData.calls.count()).toBe(1); }); it('should reload 1 time when binding a new tableParams with initial filter that has an initial settings data field', function(){ var tp = createNgTableParams({filter: {age: 1}}, { data: [1,2,3] }); scope.tableParams = tp; scope.$digest(); expect(tp.settings().getData.calls.count()).toBe(1); }); it('should reload when binding a new tableParams to scope multiple times', function(){ var tp1 = createNgTableParams(); scope.tableParams = tp1; scope.$digest(); expect(tp1.settings().getData.calls.count()).toBe(1); var tp2 = createNgTableParams(); scope.tableParams = tp2; scope.$digest(); expect(tp2.settings().getData.calls.count()).toBe(1); }); it('should reload 1 time when binding a new settings data value and changing the filter', function(){ // given var tp = createNgTableParams({filterDelay: 100, data: [{age: 1}, {age: 2}]}); scope.tableParams = tp; scope.$digest(); tp.settings().getData.calls.reset(); // when tp.filter({ age: 1 }); tp.settings({ data: [{ age: 1 }, { age: 11 }, { age: 22 }]}); scope.$digest(); $timeout.flush(); // trigger the delayed reload expect(tp.settings().getData.calls.count()).toBe(1); }); it('should reload 1 time when multiple filter changes are debounced', function(){ // given var tp = createNgTableParams({filterDelay: 100, data: [{age: 1}, {age: 2}]}); scope.tableParams = tp; scope.$digest(); tp.settings().getData.calls.reset(); // when tp.filter({ age: 1 }); scope.$digest(); tp.filter({ age: 2 }); scope.$digest(); $timeout.flush(); // trigger the delayed reload expect(tp.settings().getData.calls.count()).toBe(1); }); it('should reload 1 time with page reset to 1 when binding a new settings data value and changing the filter', function(){ var settings = { counts: [1], data: [{age: 1}, {age: 2}], filterDelay: 100 }; var tp = createNgTableParams({ count: 1, page: 2 }, settings); scope.tableParams = tp; scope.$digest(); expect(tp.page()).toBe(2); // checking assumptions tp.settings().getData.calls.reset(); // when tp.filter({ age: 1 }); tp.settings({ data: [{ age: 1 }, { age: 11 }, { age: 22 }]}); scope.$digest(); $timeout.flush(); // trigger the delayed reload expect(tp.settings().getData.calls.count()).toBe(1); expect(tp.page()).toBe(1); }); it('changing filter, orderBy, or page and then calling reload should not invoke getData twice', function(){ // todo: refactor the watches in ngTableController to handle this case var tp = createNgTableParams(); scope.tableParams = tp; scope.$digest(); tp.settings().getData.calls.reset(); // when tp.filter({ age: 5 }); tp.reload(); scope.$digest(); // then expect(tp.settings().getData.calls.count()).toBe(1); }); it('changing filter, orderBy, or page in a callback to reload should re-invoke getData 1 time only', function(){ var tp = createNgTableParams(); scope.tableParams = tp; scope.$digest(); tp.settings().getData.calls.reset(); // when tp.filter({ age: 5 }); tp.reload().then(function(){ tp.sorting({ age: 'desc'}); // note: better to call tp.reload() here rather than rely on a watch firing later to do it for us // that way the second reload is chained to the first and returned as a single promise }); scope.$digest(); // then // ie calls.count() === (1 x reload) + (1 x sorting) expect(tp.settings().getData.calls.count()).toBe(2); }); it('changing filter, orderBy, or page then reload in a callback to reload should re-invoke getData 1 time only', function(){ // todo: refactor the watches in ngTableController to handle this case var tp = createNgTableParams(); scope.tableParams = tp; scope.$digest(); tp.settings().getData.calls.reset(); // when tp.filter({ age: 5 }); tp.reload().then(function(){ tp.sorting({ age: 'desc'}); return tp.reload(); }); scope.$digest(); // then // ie calls.count() === (1 x reload) + (1 x sorting) expect(tp.settings().getData.calls.count()).toBe(2); }); it('should not reload when filter value is assigned the same value', function(){ // given var tp = createNgTableParams({ filter: {age: 10} }, { filterDelay: 0 }); scope.tableParams = tp; scope.$digest(); tp.settings().getData.calls.reset(); // when tp.filter({ age: 10}); scope.$digest(); expect(tp.settings().getData.calls.count()).toBe(0); }); it('should reload when filter value changes', function(){ // given var tp = createNgTableParams({ filter: {age: 10} }, { filterDelay: 0 }); scope.tableParams = tp; scope.$digest(); tp.settings().getData.calls.reset(); // when tp.filter({ age: 12}); scope.$digest(); expect(tp.settings().getData.calls.count()).toBe(1); }); it('should reload when new dataset supplied', function(){ // given var initialDataset = [ {age: 1}, {age: 2} ]; var tp = createNgTableParams(); scope.tableParams = tp; scope.$digest(); tp.settings().getData.calls.reset(); // when tp.settings({ data: [{ age: 10}, { age: 11}, { age: 12}]}); scope.$digest(); expect(tp.settings().getData.calls.count()).toBe(1); }); }); });