Complete table tests

This commit is contained in:
Rashid Khan 2014-09-04 17:13:33 -07:00
parent 18601598d8
commit 0a4b104de1
5 changed files with 409 additions and 39 deletions

View file

@ -29,23 +29,28 @@ define(function (require) {
if ($scope.mapping[column] && !$scope.mapping[column].indexed) return;
var sorting = $scope.sorting;
var defaultClass = ['fa', 'fa-sort', 'table-header-sortchange'];
if (!sorting) return [];
if (!sorting) return defaultClass;
if (column === sorting[0]) {
return ['fa', sorting[1] === 'asc' ? 'fa-sort-up' : 'fa-sort-down'];
} else {
return ['fa', 'fa-sort', 'table-header-sortchange'];
return defaultClass;
}
};
$scope.moveLeft = function (column) {
var index = _.indexOf($scope.columns, column);
if (index === 0) return;
_.move($scope.columns, index, --index);
};
$scope.moveRight = function (column) {
var index = _.indexOf($scope.columns, column);
if (index === $scope.columns.length - 1) return;
_.move($scope.columns, index, ++index);
};
@ -112,7 +117,6 @@ define(function (require) {
return {
restrict: 'A',
scope: {
fields: '=',
columns: '=',
filtering: '=',
mapping: '=',
@ -139,27 +143,14 @@ define(function (require) {
$scope.maxLength = 250;
}
// for now, rows are "tracked" by their index, but this could eventually
// be configured so that changing the order of the rows won't prevent
// them from staying open on update
function rowId(row) {
var id = $scope.rows.indexOf(row);
return ~id ? id : null;
}
// inverse of rowId()
function rowForId(id) {
return $scope.rows[id];
}
// toggle display of the rows details, a full list of the fields from each row
$scope.toggleRow = function (row, event) {
$scope.toggleRow = function () {
var row = $scope.row;
var id = row._id;
$scope.open = !$scope.open;
var $tr = $(event.delegateTarget.parentElement);
var $tr = element;
var $detailsTr = $tr.next();
///
@ -169,7 +160,7 @@ define(function (require) {
$detailsTr.toggle($scope.open);
// Change the caret icon
var $toggleIcon = $($(event.delegateTarget).children('i')[0]);
var $toggleIcon = $(element.children().first().find('i')[0]);
$toggleIcon.toggleClass('fa-caret-down');
$toggleIcon.toggleClass('fa-caret-right');
@ -207,7 +198,12 @@ define(function (require) {
$scope.filtering(field, row._source[field] || row[field], operation);
};
$scope.$watch('columns', function () {
$scope.$watch('columns', function (columns) {
element.empty();
createSummaryRow($scope.row, $scope.row._id);
});
$scope.$watch('timefield', function (timefield) {
element.empty();
createSummaryRow($scope.row, $scope.row._id);
});
@ -216,7 +212,7 @@ define(function (require) {
function createSummaryRow(row, id) {
var expandTd = $('<td>').html('<i class="fa fa-caret-right"></span>')
.attr('ng-click', 'toggleRow(row, $event)');
.attr('ng-click', 'toggleRow()');
$compile(expandTd)($scope);
element.append(expandTd);
@ -240,11 +236,7 @@ define(function (require) {
*/
function _displayField(el, row, field, truncate) {
var val = _getValForField(row, field, truncate);
if (val instanceof DOMNode) {
el.append(val);
} else {
el.text(val);
}
el.text(val);
return el;
}
@ -266,17 +258,9 @@ define(function (require) {
// undefined and null should just be an empty string
val = (val == null) ? '' : val;
// truncate
// truncate the column text, not the details
if (typeof val === 'string' && val.length > $scope.maxLength) {
if (untruncate) {
var complete = val;
val = document.createElement('kbn-truncated');
val.setAttribute('orig', complete);
val.setAttribute('length', $scope.maxLength);
val = $compile(val)($scope)[0];// return the actual element
} else {
val = val.substring(0, $scope.maxLength) + '...';
}
val = val.substring(0, $scope.maxLength) + '...';
}
return val;

View file

@ -3,7 +3,8 @@
<tbody>
<tr ng-repeat="row in rows |limitTo:limit track by row._index+row._id"
kbn-table-row="row"
columns="columns" mapping="mapping" sorting="sorting" timefield="timefield" max-length="maxLength" filtering="filtering"></tr>
columns="columns" mapping="mapping" sorting="sorting" timefield="timefield" max-length="maxLength" filtering="filtering"
class="discover-table-row"></tr>
</tbody>
</table>
<kbn-infinite-scroll more="addRows"></kbn-infinite-scroll>

View file

@ -3,7 +3,7 @@
<span ng-click="sort(timefield)">Time <i ng-class="headerClass(timefield)"></i></span>
</th>
<th ng-repeat="name in columns">
<span ng-click="sort(name)">
<span ng-click="sort(name)" class="table-header-name">
{{name}} <i ng-class="headerClass(name)"></i>
</span>
<span class="table-header-move">

View file

@ -61,6 +61,7 @@
'kibana',
'sinon/sinon',
'specs/apps/discover/hit_sort_fn',
'specs/apps/discover/directives/table',
'specs/directives/confirm-click',
'specs/directives/timepicker',
'specs/directives/truncate',

View file

@ -0,0 +1,384 @@
define(function (require) {
var angular = require('angular');
var $ = require('jquery');
var _ = require('lodash');
var sinon = require('test_utils/auto_release_sinon');
// Load the kibana app dependencies.
require('angular-route');
// Load kibana and its applications
require('index');
require('apps/discover/index');
var $parentScope, $scope, config;
// Stub out a minimal mapping of 3 fields
var mapping = {
bytes: {
indexed: true,
type: 'number'
},
request: {
indexed: false,
type: 'string'
},
timestamp: {
indexed: true,
type: 'date'
},
};
// Sets up the directive, take an element, and a list of properties to attach to the parent scope.
var init = function ($elem, props) {
module('kibana');
inject(function ($rootScope, $compile, _config_) {
config = _config_;
$parentScope = $rootScope;
_.assign($parentScope, props);
$compile($elem)($parentScope);
$elem.scope().$digest();
$scope = $elem.isolateScope();
});
};
var destroy = function () {
$scope.$destroy();
$parentScope.$destroy();
};
// For testing column removing/adding for the header and the rows
//
var columnTests = function (elemType, parentElem) {
it('should create only the toggle column by default', function (done) {
var childElems = parentElem.find(elemType);
expect(childElems.length).to.be(1);
done();
});
it('should create a time column if the timefield is defined', function (done) {
// Should include a column for toggling and the time column by default
$parentScope.timefield = 'timestamp';
parentElem.scope().$digest();
var childElems = parentElem.find(elemType);
expect(childElems.length).to.be(2);
done();
});
it('should be able to add and remove columns', function (done) {
var childElems;
// Should include a column for toggling and the time column by default
$parentScope.columns = ['bytes'];
parentElem.scope().$digest();
childElems = parentElem.find(elemType);
expect(childElems.length).to.be(2);
expect($(childElems[1]).text()).to.contain('bytes');
$parentScope.columns = ['bytes', 'request'];
parentElem.scope().$digest();
childElems = parentElem.find(elemType);
expect(childElems.length).to.be(3);
expect($(childElems[2]).text()).to.contain('request');
$parentScope.columns = ['request'];
parentElem.scope().$digest();
childElems = parentElem.find(elemType);
expect(childElems.length).to.be(2);
expect($(childElems[1]).text()).to.contain('request');
done();
});
};
describe('discover table directives', function () {
describe('kbnTableHeader', function () {
var $elem = angular.element(
'<thead kbn-table-header columns="columns" mapping="mapping" sort="sort" timefield="timefield"></thead>'
);
beforeEach(function () {
init($elem, {
mapping: mapping,
columns: [],
sorting: [],
});
});
afterEach(function () {
destroy();
});
describe('adding and removing columns', function () {
columnTests('th', $elem);
});
describe('sorting', function () {
it('should have a sort function that sets the elements of the sort array', function (done) {
expect($scope.sort).to.be.a(Function);
done();
});
it('should have a headClasser function that determines the css classes of the sort icons', function (done) {
expect($scope.headerClass).to.be.a(Function);
done();
});
it('should sort asc by default, then by desc if already sorting', function (done) {
var field = 'bytes';
// Should not be sorted at first
expect($scope.sorting).to.eql(undefined);
expect($scope.headerClass(field)).to.contain('fa-sort');
$scope.sort(field);
expect($scope.sorting).to.eql([field, 'asc']);
expect($scope.headerClass(field)).to.contain('fa-sort-up');
$scope.sort(field);
expect($scope.sorting).to.eql([field, 'desc']);
expect($scope.headerClass(field)).to.contain('fa-sort-down');
$scope.sort(field);
expect($scope.sorting).to.eql([field, 'asc']);
expect($scope.headerClass(field)).to.contain('fa-sort-up');
// Should show the default sort for any other field
expect($scope.headerClass('timestamp')).to.contain('fa-sort');
done();
});
it('should NOT sort unindexed fields', function (done) {
$scope.sort('request');
expect($scope.sorting).to.be(undefined);
done();
});
});
describe('moving columns', function () {
beforeEach(function () {
$parentScope.columns = _.keys($scope.mapping);
$elem.scope().$digest();
});
it('should move columns to the right', function () {
$scope.moveRight('bytes');
expect($scope.columns[1]).to.be('bytes');
$scope.moveRight('bytes');
expect($scope.columns[2]).to.be('bytes');
});
it('shouldnt move the last column to the right', function () {
expect($scope.columns[2]).to.be('timestamp');
$scope.moveRight('timestamp');
expect($scope.columns[2]).to.be('timestamp');
});
it('should move columns to the left', function () {
$scope.moveLeft('timestamp');
expect($scope.columns[1]).to.be('timestamp');
$scope.moveLeft('request');
expect($scope.columns[1]).to.be('request');
});
it('shouldnt move the first column to the left', function () {
expect($scope.columns[0]).to.be('bytes');
$scope.moveLeft('bytes');
expect($scope.columns[0]).to.be('bytes');
});
});
});
var longString = Array(50).join('_');
var getFakeRow = function (id) {
var columns = _.keys(mapping);
return {
_formatted: _.zipObject(_.map(columns, function (c) { return [c, c + '_formatted_' + id + longString]; })),
_source: _.zipObject(_.map(columns, function (c) { return [c, c + '_original_' + id + longString]; })),
_id: id,
_index: 'test',
sort: [id]
};
};
describe('kbnTable', function () {
var $elem = angular.element(
'<kbn-table ' +
'columns="columns" ' +
'rows="rows" ' +
'sorting="sorting"' +
'filtering="filtering"' +
'maxLength=maxLength ' +
'mapping="mapping"' +
'timefield="timefield" ' +
'></thead>'
);
beforeEach(function () {
// A tiny window
sinon.stub($.prototype, 'height', function () { return 100; });
// Convince the infinite scroll that there's still a lot of room left.
sinon.stub($.prototype, 'scrollTop', function () { return -200; });
var rows = _.times(200, function (i) {
return getFakeRow(i);
});
init($elem, {
columns: ['bytes'],
rows: rows,
sorting: [],
filtering: sinon.spy(),
maxLength: 50,
mapping: mapping,
timefield: 'timestamp'
});
});
afterEach(function () {
destroy();
});
it('should have a header and a table element', function (done) {
expect($elem.find('thead').length).to.be(1);
expect($elem.find('table').length).to.be(1);
done();
});
it('should have 50 rows to start', function (done) {
var tr = $elem.find('.discover-table-row');
expect(tr.length).to.be(50);
done();
});
it('should have an addRows function that adds 50 rows', function (done) {
expect($scope.addRows).to.be.a(Function);
$scope.addRows();
$elem.scope().$digest();
var tr = $elem.find('.discover-table-row');
expect(tr.length).to.be(100);
done();
});
});
describe('kbnTableRow', function () {
var $elem = angular.element(
'<tr kbn-table-row="row" ' +
'columns="columns" ' +
'sorting="sorting"' +
'filtering="filtering"' +
'max-length=maxLength ' +
'mapping="mapping"' +
'timefield="timefield" ' +
'></tr>'
);
beforeEach(function () {
init($elem, {
row: getFakeRow(0),
columns: [],
sorting: [],
filtering: sinon.spy(),
maxLength: 50,
mapping: mapping,
});
// Ignore the metaFields (_id, _type, etc) since we don't have a mapping for them
sinon.stub(config, 'get').withArgs('metaFields').returns([]);
});
afterEach(function () {
destroy();
});
describe('adding and removing columns', function () {
columnTests('td', $elem);
});
describe('details row', function () {
it('should be an empty tr by default', function () {
expect($elem.next().is('tr')).to.be(true);
expect($elem.next().text()).to.be('');
});
});
describe('details row', function () {
it('should be an empty tr by default', function () {
expect($elem.next().is('tr')).to.be(true);
expect($elem.next().text()).to.be('');
});
it('should expand the detail row when the toggle arrow is clicked', function () {
$elem.children(':first-child').click();
$scope.$digest();
expect($elem.next().text()).to.not.be('');
});
describe('expanded', function () {
var $details;
beforeEach(function () {
// Open the row
$scope.toggleRow();
$scope.$digest();
$details = $elem.next();
});
afterEach(function () {
// Close the row
$scope.toggleRow();
$scope.$digest();
});
it('should be a tr', function () {
expect($details.is('tr')).to.be(true);
});
it('should have a row for each field', function () {
var rows = $details.find('tr');
var row = $scope.row;
expect($details.find('tr').length).to.be(3);
});
it('should have a row for each field', function () {
var rows = $details.find('tr');
var row = $scope.row;
expect($details.find('tr').length).to.be(3);
});
describe('filtering', function () {
it('should filter when you click on the filter buttons', function () {
$details.find('.fa-search-plus').first().click();
expect($scope.filtering.calledOnce).to.be(true);
$details.find('.fa-search-minus').first().click();
expect($scope.filtering.calledTwice).to.be(true);
});
});
});
});
});
});
});