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