blob: c7856f373a00527c0d0ed70f1b53fef089b4b05b [file] [log] [blame]
Matteo Scandolo710dc152017-04-11 13:54:23 -07001import * as angular from 'angular';
2import * as $ from 'jquery';
3import 'angular-mocks';
4import {xosTable} from './table';
5import {PaginationFilter} from '../pagination/pagination.filter';
6import {ArrayToListFilter} from './array-to-list.filter';
7import {xosLinkWrapper} from '../link-wrapper/link-wrapper';
8
9
10describe('The Xos Table component', () => {
11 beforeEach(() => {
12 angular
13 .module('table', [])
14 .component('xosTable', xosTable)
15 .filter('pagination', PaginationFilter)
16 .filter('arrayToList', ArrayToListFilter)
17 .directive('xosLinkWrapper', xosLinkWrapper);
18 angular.mock.module('table');
19 });
20
21 let scope, element, isolatedScope, rootScope, compile, filter;
22 const compileElement = () => {
23
24 if (!scope) {
25 scope = rootScope.$new();
26 }
27
28 element = angular.element('<xos-table config="config" data="data"></xos-table>');
29 compile(element)(scope);
30 scope.$digest();
31 isolatedScope = element.isolateScope().vm;
32 };
33
34 beforeEach(inject(function ($compile: ng.ICompileService, $rootScope: ng.IScope, $filter: ng.IFilterService) {
35 compile = $compile;
36 rootScope = $rootScope;
37 filter = $filter;
38 }));
39
40 it('should throw an error if no config is specified', () => {
41 function errorFunctionWrapper() {
42 compileElement();
43 }
44 expect(errorFunctionWrapper).toThrow(new Error('[xosTable] Please provide a configuration via the "config" attribute'));
45 });
46
47 it('should throw an error if no config columns are specified', () => {
48 function errorFunctionWrapper() {
49 // setup the parent scope
50 scope = rootScope.$new();
51 scope.config = 'green';
52 compileElement();
53 }
54 expect(errorFunctionWrapper).toThrow(new Error('[xosTable] Please provide a columns list in the configuration'));
55 });
56
57 describe('when basically configured', function() {
58
59 beforeEach(inject(function ($compile: ng.ICompileService, $rootScope: ng.IScope) {
60
61 scope = $rootScope.$new();
62
63 scope.config = {
64 columns: [
65 {
66 label: 'Label 1',
67 prop: 'label-1'
68 },
69 {
70 label: 'Label 2',
71 prop: 'label-2'
72 }
73 ]
74 };
75
76 scope.data = [
77 {
78 'label-1': 'Sample 1.1',
79 'label-2': 'Sample 1.2'
80 },
81 {
82 'label-1': 'Sample 2.1',
83 'label-2': 'Sample 2.2'
84 }
85 ];
86
87 element = angular.element('<xos-table config="config" data="data"></xos-table>');
88 $compile(element)(scope);
89 scope.$digest();
90 isolatedScope = element.isolateScope().vm;
91 }));
92
93 it('should contain 2 columns', function() {
94 const th = element[0].getElementsByTagName('th');
95 expect(th.length).toEqual(2);
96 expect(isolatedScope.columns.length).toEqual(2);
97 });
98
99 it('should contain 3 rows', function() {
100 const tr = element[0].getElementsByTagName('tr');
101 expect(tr.length).toEqual(3);
102 });
103
104 it('should render labels', () => {
105 let label1 = $(element).find('thead tr th')[0];
106 let label2 = $(element).find('thead tr th')[1];
107 expect($(label1).text().trim()).toEqual('Label 1');
108 expect($(label2).text().trim()).toEqual('Label 2');
109 });
110
111 describe('when no data are provided', () => {
112 beforeEach(() => {
113 isolatedScope.data = [];
114 scope.$digest();
115 });
116 it('should render an alert', () => {
117 let alert = $('xos-alert', element);
118 let table = $('table', element);
119 expect(alert.length).toEqual(1);
120 expect(table.length).toEqual(1);
121 });
122 });
123
124 describe('when a field type is provided', () => {
125 describe('and is boolean', () => {
126 beforeEach(() => {
127 scope.config = {
128 columns: [
129 {
130 label: 'Label 1',
131 prop: 'label-1',
132 type: 'boolean'
133 },
134 {
135 label: 'Label 2',
136 prop: 'label-2',
137 type: 'boolean'
138 }
139 ]
140 };
141 scope.data = [
142 {
143 'label-1': true,
144 'label-2': 1
145 },
146 {
147 'label-1': false,
148 'label-2': 0
149 }
150 ];
151 compileElement();
152 });
153
154 it('should render an incon', () => {
155 let td1 = $(element).find('tbody tr:first-child td')[0];
156 let td2 = $(element).find('tbody tr:last-child td')[0];
157 expect($(td1).find('i')).toHaveClass('fa-ok');
158 expect($(td2).find('i')).toHaveClass('fa-remove');
159 });
160
161 describe('with field filters', () => {
162 beforeEach(() => {
163 scope.config.filter = 'field';
164 compileElement();
165 });
166
167 it('should render a dropdown for filtering', () => {
168 let td1 = $(element).find('table tbody tr td')[0];
169 expect(td1).toContainElement('select');
170 expect(td1).not.toContainElement('input');
171 });
172
173 it('should correctly filter results', () => {
174 isolatedScope.query = {
175 'label-1': false
176 };
177 scope.$digest();
178 expect(isolatedScope.query['label-1']).toBeFalsy();
179 const tr = $(element).find('tbody:last-child > tr');
180 const icon = $(tr[0]).find('td i');
181 expect(tr.length).toEqual(1);
182 expect(icon).toHaveClass('fa-remove');
183 });
184
185 // NOTE the custom comparator we had has not been imported yet:
186 // https://github.com/opencord/ng-xos-lib/blob/master/src/services/helpers/ui/comparator.service.js
187 xit('should correctly filter results if the field is in the form of 0|1', () => {
188 isolatedScope.query = {
189 'label-2': false
190 };
191 scope.$digest();
192 expect(isolatedScope.query['label-2']).toBeFalsy();
193 const tr = $('tbody:last-child > tr', element);
194 expect(tr.length).toEqual(1);
195 const icon = $(tr[0]).find('td i');
196 expect(icon).toHaveClass('fa-remove');
197 });
198 });
199 });
200
201 describe('and is date', () => {
202 beforeEach(() => {
203 scope.config = {
204 columns: [
205 {
206 label: 'Label 1',
207 prop: 'label-1',
208 type: 'date'
209 }
210 ]
211 };
212 scope.data = [
213 {
214 'label-1': '2015-02-17T22:06:38.059000Z'
215 }
216 ];
217 compileElement();
218 });
219
220 it('should render an formatted date', () => {
221 let td1 = $(element).find('tbody tr:first-child td')[0];
222 const expectedDate = filter('date')(scope.data[0]['label-1'], 'H:mm MMM d, yyyy');
223 expect($(td1).text().trim()).toEqual(expectedDate);
224 });
225 });
226
227 describe('and is array', () => {
228 beforeEach(() => {
229 scope.data = [
230 {categories: ['Film', 'Music']}
231 ];
232 scope.config = {
233 filter: 'field',
234 columns: [
235 {
236 label: 'Categories',
237 prop: 'categories',
238 type: 'array'
239 }
240 ]
241 };
242 compileElement();
243 });
244 it('should render a comma separated list', () => {
245 let td1 = $(element).find('tbody:last-child tr:first-child')[0];
246 expect($(td1).text().trim()).toEqual('Film, Music');
247 });
248
249 it('should not render the filter field', () => {
250 let filter = $(element).find('tbody tr td')[0];
251 expect($(filter)).not.toContainElement('input');
252 });
253 });
254
255 describe('and is object', () => {
256 beforeEach(() => {
257 scope.data = [
258 {
259 attributes: {
260 age: 20,
261 height: 50
262 }
263 }
264 ];
265 scope.config = {
266 filter: 'field',
267 columns: [
268 {
269 label: 'Categories',
270 prop: 'attributes',
271 type: 'object'
272 }
273 ]
274 };
275 compileElement();
276 });
277 it('should render a list of key-values', () => {
278 let td = $(element).find('tbody:last-child tr:first-child')[0];
279 let ageLabel = $(td).find('dl dt')[0];
280 let ageValue = $(td).find('dl dd')[0];
281 let heightLabel = $(td).find('dl dt')[1];
282 let heightValue = $(td).find('dl dd')[1];
283 expect($(ageLabel).text().trim()).toEqual('age');
284 expect($(ageValue).text().trim()).toEqual('20');
285 expect($(heightLabel).text().trim()).toEqual('height');
286 expect($(heightValue).text().trim()).toEqual('50');
287 });
288
289 it('should not render the filter field', () => {
290 let filter = $(element).find('tbody tr td')[0];
291 expect($(filter)).not.toContainElement('input');
292 });
293 });
294
295 describe('and is custom', () => {
296
297 let formatterFn = jasmine.createSpy('formatter').and.returnValue('Formatted Content');
298
299 beforeEach(() => {
300 scope.data = [
301 {categories: ['Film', 'Music']}
302 ];
303 scope.config = {
304 filter: 'field',
305 columns: [
306 {
307 label: 'Categories',
308 prop: 'categories',
309 type: 'custom',
310 formatter: formatterFn
311 }
312 ]
313 };
314 compileElement();
315 });
316
317 it('should check for a formatter property', () => {
318 function errorFunctionWrapper() {
319 // setup the parent scope
320 scope = rootScope.$new();
321 scope.config = {
322 columns: [
323 {
324 label: 'Categories',
325 prop: 'categories',
326 type: 'custom'
327 }
328 ]
329 };
330 compileElement();
331 }
332 expect(errorFunctionWrapper).toThrow(new Error('[xosTable] You have provided a custom field type, a formatter function should provided too.'));
333 });
334
335 it('should check that the formatter property is a function', () => {
336 function errorFunctionWrapper() {
337 // setup the parent scope
338 scope = rootScope.$new();
339 scope.config = {
340 columns: [
341 {
342 label: 'Categories',
343 prop: 'categories',
344 type: 'custom',
345 formatter: 'formatter'
346 }
347 ]
348 };
349 compileElement();
350 }
351 expect(errorFunctionWrapper).toThrow(new Error('[xosTable] You have provided a custom field type, a formatter function should provided too.'));
352 });
353
354 it('should format data using the formatter property', () => {
355 let td1 = $(element).find('tbody:last-child tr:first-child')[0];
356 expect($(td1).text().trim()).toEqual('Formatted Content');
357 // the custom formatted should receive the entire object, otherwise is not so custom
358 expect(formatterFn).toHaveBeenCalledWith({categories: ['Film', 'Music']});
359 });
360
361 it('should not render the filter field', () => {
362 // displayed value is different from model val, filter would not work
363 let filter = $(element).find('tbody tr td')[0];
364 expect($(filter)).not.toContainElement('input');
365 });
366 });
367
368 describe('and is icon', () => {
369
370 beforeEach(() => {
371 scope.config = {
372 columns: [
373 {
374 label: 'Label 1',
375 prop: 'label-1',
376 type: 'icon',
377 formatter: item => {
378 switch (item['label-1']) {
379 case 1:
380 return 'ok';
381 case 2:
382 return 'remove';
383 case 3:
384 return 'plus';
385 }
386 }
387 }
388 ]
389 };
390 scope.data = [
391 {
392 'label-1': 1
393 },
394 {
395 'label-1': 2
396 },
397 {
398 'label-1': 3
399 }
400 ];
401 compileElement();
402 });
403
404 it('should render a custom icon', () => {
405 let td1 = $(element).find('tbody tr:first-child td')[0];
406 let td2 = $(element).find('tbody tr:nth-child(2) td')[0];
407 let td3 = $(element).find('tbody tr:last-child td')[0];
408 expect($(td1).find('i')).toHaveClass('fa-ok');
409 expect($(td2).find('i')).toHaveClass('fa-remove');
410 expect($(td3).find('i')).toHaveClass('fa-plus');
411 });
412 });
413 });
414
415 describe('when a link property is provided', () => {
416 beforeEach(() => {
417 scope.data = [
418 {
419 id: 1
420 }
421 ];
422 scope.config = {
423 columns: [
424 {
425 label: 'Id',
426 prop: 'id',
427 link: (item) => {
428 return `state({id: ${item.id}})`;
429 }
430 }
431 ]
432 };
433 compileElement();
434 });
435
436 it('should check that the link property is a function', () => {
437 function errorFunctionWrapper() {
438 // setup the parent scope
439 scope = rootScope.$new();
440 scope.config = {
441 columns: [
442 {
443 label: 'Categories',
444 prop: 'categories',
445 link: 'custom'
446 }
447 ]
448 };
449 compileElement();
450 }
451 expect(errorFunctionWrapper).toThrow(new Error('[xosTable] The link property should be a function.'));
452 });
453
454 it('should render a link with the correct url', () => {
455 let link = $('tbody tr:first-child td a', element)[0];
456 expect($(link).attr('ui-sref')).toEqual('state({id: 1})');
457 });
458 });
459
460 describe('when actions are passed', () => {
461
462 let cb = jasmine.createSpy('callback');
463
464 beforeEach(() => {
465 isolatedScope.config.actions = [
466 {
467 label: 'delete',
468 icon: 'remove',
469 cb: cb,
470 color: 'red'
471 }
472 ];
473 scope.$digest();
474 });
475
476 it('should have 3 columns', () => {
477 const th = element[0].getElementsByTagName('th');
478 expect(th.length).toEqual(3);
479 expect(isolatedScope.columns.length).toEqual(2);
480 });
481
482 it('when clicking on action should invoke callback', () => {
483 const link = element[0].getElementsByTagName('a')[0];
484 link.click();
485 expect(cb).toHaveBeenCalledWith(scope.data[0]);
486 });
487 });
488
489 describe('when filter is fulltext', () => {
490 beforeEach(() => {
491 isolatedScope.config.filter = 'fulltext';
492 scope.$digest();
493 });
494
495 it('should render a text field', () => {
496 const textField = element[0].getElementsByTagName('input');
497 expect(textField.length).toEqual(1);
498 });
499
500 describe('and a value is enterd', () => {
501 beforeEach(() => {
502 isolatedScope.query = '2.2';
503 scope.$digest();
504 });
505
506 it('should contain 2 rows', function() {
507 const tr = element[0].getElementsByTagName('tr');
508 expect(tr.length).toEqual(2);
509 });
510 });
511 });
512
513 describe('when filter is field', () => {
514 beforeEach(() => {
515 isolatedScope.config.filter = 'field';
516 scope.$digest();
517 });
518
519 it('should render a text field for each column', () => {
520 const textField = element[0].getElementsByTagName('input');
521 expect(textField.length).toEqual(2);
522 });
523
524 describe('and a value is enterd', () => {
525 beforeEach(() => {
526 isolatedScope.query = {'label-1': '2.1'};
527 scope.$digest();
528 });
529
530 it('should contain 3 rows', function() {
531 const tr = element[0].getElementsByTagName('tr');
532 expect(tr.length).toEqual(3);
533 });
534 });
535 });
536
537 describe('when order is true', () => {
538 beforeEach(() => {
539 isolatedScope.config.order = true;
540 scope.$digest();
541 });
542
543 it('should render a arrows beside', () => {
544 const arrows = element[0].getElementsByTagName('i');
545 expect(arrows.length).toEqual(4);
546 });
547
548 describe('and a default ordering is passed', () => {
549
550 beforeEach(() => {
551 scope.config.order = {
552 field: 'label-1',
553 reverse: true
554 };
555 compileElement();
556 });
557
558 it('should orderBy the default order', () => {
559 const tr = $(element).find('tr');
560 expect($(tr[1]).text()).toContain('Sample 2.2');
561 expect($(tr[2]).text()).toContain('Sample 1.1');
562 });
563 });
564
565 describe('and an order is set', () => {
566 beforeEach(() => {
567 isolatedScope.orderBy = 'label-1';
568 isolatedScope.reverse = true;
569 scope.$digest();
570 });
571
572 it('should orderBy', function() {
573 const tr = $(element).find('tr');
574 expect($(tr[1]).text()).toContain('Sample 2.2');
575 expect($(tr[2]).text()).toContain('Sample 1.1');
576 });
577 });
578 });
579 });
580});