1 import { ComponentFixture, TestBed } from '@angular/core/testing';
2 import { FormsModule } from '@angular/forms';
3 import { RouterTestingModule } from '@angular/router/testing';
5 import { NgxDatatableModule } from '@swimlane/ngx-datatable';
6 import * as _ from 'lodash';
7 import { BsDropdownModule } from 'ngx-bootstrap/dropdown';
9 import { configureTestBed } from '../../../../testing/unit-test-helper';
10 import { ComponentsModule } from '../../components/components.module';
11 import { CdTableColumnFilter } from '../../models/cd-table-column-filter';
12 import { CdTableFetchDataContext } from '../../models/cd-table-fetch-data-context';
13 import { PipesModule } from '../../pipes/pipes.module';
14 import { TableComponent } from './table.component';
16 describe('TableComponent', () => {
17 let component: TableComponent;
18 let fixture: ComponentFixture<TableComponent>;
20 const createFakeData = (n: number) => {
22 for (let i = 0; i < n; i++) {
32 const clearLocalStorage = () => {
33 component.localStorage.clear();
37 declarations: [TableComponent],
43 BsDropdownModule.forRoot(),
49 fixture = TestBed.createComponent(TableComponent);
50 component = fixture.componentInstance;
52 component.data = createFakeData(10);
54 { prop: 'a', name: 'Index', filterable: true },
55 { prop: 'b', name: 'Index times ten' },
56 { prop: 'c', name: 'Odd?', filterable: true }
60 it('should create', () => {
61 expect(component).toBeTruthy();
64 it('should force an identifier', () => {
65 component.identifier = 'x';
66 component.forceIdentifier = true;
68 expect(component.identifier).toBe('x');
69 expect(component.sorts[0].prop).toBe('a');
70 expect(component.sorts).toEqual(component.createSortingDefinition('a'));
73 it('should have rows', () => {
75 expect(component.data.length).toBe(10);
76 expect(component.rows.length).toBe(component.data.length);
79 it('should have an int in setLimit parsing a string', () => {
80 expect(component.limit).toBe(10);
81 expect(component.limit).toEqual(jasmine.any(Number));
83 const e = { target: { value: '1' } };
84 component.setLimit(e);
85 expect(component.userConfig.limit).toBe(1);
86 expect(component.userConfig.limit).toEqual(jasmine.any(Number));
87 e.target.value = '-20';
88 component.setLimit(e);
89 expect(component.userConfig.limit).toBe(1);
92 it('should prevent propagation of mouseenter event', (done) => {
93 let wasCalled = false;
94 const mouseEvent = new MouseEvent('mouseenter');
95 mouseEvent.stopPropagation = () => {
98 spyOn(component.table.element, 'addEventListener').and.callFake((eventName, fn) => {
100 expect(eventName).toBe('mouseenter');
101 expect(wasCalled).toBe(true);
104 component.ngOnInit();
107 describe('test column filtering', () => {
108 let filterIndex: CdTableColumnFilter;
109 let filterOdd: CdTableColumnFilter;
110 let filterCustom: CdTableColumnFilter;
112 const expectColumnFilterCreated = (
113 filter: CdTableColumnFilter,
116 value?: { raw: string; formatted: string }
118 expect(filter.column.prop).toBe(prop);
119 expect(_.map(filter.options, 'raw')).toEqual(options);
120 expect(filter.value).toEqual(value);
123 const expectColumnFiltered = (
124 changes: { filter: CdTableColumnFilter; value?: string }[],
128 component.search = search;
129 _.forEach(changes, (change) => {
130 component.onChangeFilter(
132 change.value ? { raw: change.value, formatted: change.value } : undefined
135 expect(component.rows).toEqual(results);
136 component.onClearSearch();
137 component.onClearFilters();
140 describe('with visible columns', () => {
142 component.initColumnFilters();
143 component.updateColumnFilterOptions();
144 filterIndex = component.columnFilters[0];
145 filterOdd = component.columnFilters[1];
148 it('should have filters initialized', () => {
149 expect(component.columnFilters.length).toBe(2);
150 expectColumnFilterCreated(
153 _.map(component.data, (row) => _.toString(row.a))
155 expectColumnFilterCreated(filterOdd, 'c', ['false', 'true']);
158 it('should add filters', () => {
160 expectColumnFiltered([{ filter: filterIndex, value: '1' }], [{ a: 1, b: 10, c: true }]);
163 expectColumnFiltered(
164 [{ filter: filterOdd, value: 'false' }, { filter: filterIndex, value: '2' }],
165 [{ a: 2, b: 20, c: false }]
169 expect(component.rows).toEqual(component.data);
172 it('should remove filters', () => {
174 expectColumnFiltered(
176 { filter: filterOdd, value: 'true' },
177 { filter: filterIndex, value: '1' },
178 { filter: filterIndex, value: undefined }
181 { a: 1, b: 10, c: true },
182 { a: 3, b: 30, c: true },
183 { a: 5, b: 50, c: true },
184 { a: 7, b: 70, c: true },
185 { a: 9, b: 90, c: true }
190 expectColumnFiltered(
192 { filter: filterOdd, value: 'true' },
193 { filter: filterIndex, value: '1' },
194 { filter: filterIndex, value: undefined },
195 { filter: filterOdd, value: undefined }
200 // a selected filter should be removed if it's selected again
201 expectColumnFiltered(
203 { filter: filterOdd, value: 'true' },
204 { filter: filterIndex, value: '1' },
205 { filter: filterIndex, value: '1' }
208 { a: 1, b: 10, c: true },
209 { a: 3, b: 30, c: true },
210 { a: 5, b: 50, c: true },
211 { a: 7, b: 70, c: true },
212 { a: 9, b: 90, c: true }
217 it('should search from filtered rows', () => {
218 expectColumnFiltered(
219 [{ filter: filterOdd, value: 'true' }],
220 [{ a: 9, b: 90, c: true }],
225 expect(component.rows).toEqual(component.data);
229 describe('with custom columns', () => {
231 // create a new additional column in data
232 for (let i = 0; i < component.data.length; i++) {
233 const row = component.data[i];
236 // create a custom column filter
237 component.extraFilterableColumns = [
239 name: 'd less than 5',
241 filterOptions: ['yes', 'no'],
242 filterInitValue: 'yes',
243 filterPredicate: (row, value) => {
244 if (value === 'yes') {
252 component.initColumnFilters();
253 component.updateColumnFilterOptions();
254 filterIndex = component.columnFilters[0];
255 filterOdd = component.columnFilters[1];
256 filterCustom = component.columnFilters[2];
259 it('should have filters initialized', () => {
260 expect(component.columnFilters.length).toBe(3);
261 expectColumnFilterCreated(filterCustom, 'd', ['yes', 'no'], {
266 expect(component.rows).toEqual(_.slice(component.data, 0, 5));
269 it('should remove filters', () => {
270 expectColumnFiltered([{ filter: filterCustom, value: 'no' }], _.slice(component.data, 5));
275 describe('test search', () => {
276 const expectSearch = (keyword: string, expectedResult: object[]) => {
277 component.search = keyword;
278 component.updateFilter();
279 expect(component.rows).toEqual(expectedResult);
280 component.onClearSearch();
283 describe('searchableObjects', () => {
292 component.data = [testObject];
293 component.columns = [{ prop: 'obj', name: 'Object' }];
296 it('should not search through objects as default case', () => {
297 expect(component.searchableObjects).toBe(false);
298 expectSearch('8', []);
301 it('should search through objects if searchableObjects is set to true', () => {
302 component.searchableObjects = true;
303 expectSearch('28', []);
304 expectSearch('8', [testObject]);
305 expectSearch('123', [testObject]);
306 expectSearch('max', [testObject]);
310 it('should find a particular number', () => {
311 expectSearch('5', [{ a: 5, b: 50, c: true }]);
312 expectSearch('9', [{ a: 9, b: 90, c: true }]);
315 it('should find boolean values', () => {
316 expectSearch('true', [
317 { a: 1, b: 10, c: true },
318 { a: 3, b: 30, c: true },
319 { a: 5, b: 50, c: true },
320 { a: 7, b: 70, c: true },
321 { a: 9, b: 90, c: true }
323 expectSearch('false', [
324 { a: 0, b: 0, c: false },
325 { a: 2, b: 20, c: false },
326 { a: 4, b: 40, c: false },
327 { a: 6, b: 60, c: false },
328 { a: 8, b: 80, c: false }
332 it('should test search keyword preparation', () => {
333 const prepare = TableComponent.prepareSearch;
334 const expected = ['a', 'b', 'c'];
335 expect(prepare('a b c')).toEqual(expected);
336 expect(prepare('a,, b,, c')).toEqual(expected);
337 expect(prepare('a,,,, b,,, c')).toEqual(expected);
338 expect(prepare('a+b c')).toEqual(['a+b', 'c']);
339 expect(prepare('a,,,+++b,,, c')).toEqual(['a+++b', 'c']);
340 expect(prepare('"a b c" "d e f", "g, h i"')).toEqual(['a+b+c', 'd+e++f', 'g+h+i']);
343 it('should search for multiple values', () => {
344 expectSearch('2 20 false', [{ a: 2, b: 20, c: false }]);
345 expectSearch('false 2', [{ a: 2, b: 20, c: false }]);
348 it('should filter by column', () => {
349 expectSearch('index:5', [{ a: 5, b: 50, c: true }]);
350 expectSearch('times:50', [{ a: 5, b: 50, c: true }]);
351 expectSearch('times:50 index:5', [{ a: 5, b: 50, c: true }]);
352 expectSearch('Odd?:true', [
353 { a: 1, b: 10, c: true },
354 { a: 3, b: 30, c: true },
355 { a: 5, b: 50, c: true },
356 { a: 7, b: 70, c: true },
357 { a: 9, b: 90, c: true }
359 component.data = createFakeData(100);
360 expectSearch('index:1 odd:true times:110', [{ a: 11, b: 110, c: true }]);
363 it('should search through arrays', () => {
364 component.columns = [{ prop: 'a', name: 'Index' }, { prop: 'b', name: 'ArrayColumn' }];
366 component.data = [{ a: 1, b: ['foo', 'bar'] }, { a: 2, b: ['baz', 'bazinga'] }];
367 expectSearch('bar', [{ a: 1, b: ['foo', 'bar'] }]);
368 expectSearch('arraycolumn:bar arraycolumn:foo', [{ a: 1, b: ['foo', 'bar'] }]);
369 expectSearch('arraycolumn:baz arraycolumn:inga', [{ a: 2, b: ['baz', 'bazinga'] }]);
371 component.data = [{ a: 1, b: [1, 2] }, { a: 2, b: [3, 4] }];
372 expectSearch('arraycolumn:1 arraycolumn:2', [{ a: 1, b: [1, 2] }]);
375 it('should search with spaces', () => {
376 const expectedResult = [{ a: 2, b: 20, c: false }];
377 expectSearch(`'Index times ten':20`, expectedResult);
378 expectSearch('index+times+ten:20', expectedResult);
381 it('should filter results although column name is incomplete', () => {
382 component.data = createFakeData(3);
383 expectSearch(`'Index times ten'`, []);
384 expectSearch(`'Ind'`, []);
385 expectSearch(`'Ind:'`, [
386 { a: 0, b: 0, c: false },
387 { a: 1, b: 10, c: true },
388 { a: 2, b: 20, c: false }
392 it('should search if column name is incomplete', () => {
393 const expectedData = [
394 { a: 0, b: 0, c: false },
395 { a: 1, b: 10, c: true },
396 { a: 2, b: 20, c: false }
398 component.data = _.clone(expectedData);
399 expectSearch('inde', []);
400 expectSearch('index:', expectedData);
401 expectSearch('index times te', []);
404 it('should restore full table after search', () => {
406 expect(component.rows.length).toBe(10);
407 component.search = '3';
408 component.updateFilter();
409 expect(component.rows.length).toBe(1);
410 component.onClearSearch();
411 expect(component.rows.length).toBe(10);
415 describe('after ngInit', () => {
416 const toggleColumn = (prop: string, checked: boolean) => {
417 component.toggleColumn({
425 const equalStorageConfig = () => {
426 expect(JSON.stringify(component.userConfig)).toBe(
427 component.localStorage.getItem(component.tableName)
432 component.ngOnInit();
435 it('should have updated the column definitions', () => {
436 expect(component.columns[0].flexGrow).toBe(1);
437 expect(component.columns[1].flexGrow).toBe(2);
438 expect(component.columns[2].flexGrow).toBe(2);
439 expect(component.columns[2].resizeable).toBe(false);
442 it('should have table columns', () => {
443 expect(component.tableColumns.length).toBe(3);
444 expect(component.tableColumns).toEqual(component.columns);
447 it('should have a unique identifier which it searches for', () => {
448 expect(component.identifier).toBe('a');
449 expect(component.userConfig.sorts[0].prop).toBe('a');
450 expect(component.userConfig.sorts).toEqual(component.createSortingDefinition('a'));
451 equalStorageConfig();
454 it('should remove column "a"', () => {
455 expect(component.userConfig.sorts[0].prop).toBe('a');
456 toggleColumn('a', false);
457 expect(component.userConfig.sorts[0].prop).toBe('b');
458 expect(component.tableColumns.length).toBe(2);
459 equalStorageConfig();
462 it('should not be able to remove all columns', () => {
463 expect(component.userConfig.sorts[0].prop).toBe('a');
464 toggleColumn('a', false);
465 toggleColumn('b', false);
466 toggleColumn('c', false);
467 expect(component.userConfig.sorts[0].prop).toBe('c');
468 expect(component.tableColumns.length).toBe(1);
469 equalStorageConfig();
472 it('should enable column "a" again', () => {
473 expect(component.userConfig.sorts[0].prop).toBe('a');
474 toggleColumn('a', false);
475 toggleColumn('a', true);
476 expect(component.userConfig.sorts[0].prop).toBe('b');
477 expect(component.tableColumns.length).toBe(3);
478 equalStorageConfig();
486 describe('reload data', () => {
488 component.ngOnInit();
490 component['updating'] = false;
493 it('should call fetchData callback function', () => {
494 component.fetchData.subscribe((context: any) => {
495 expect(context instanceof CdTableFetchDataContext).toBeTruthy();
497 component.reloadData();
500 it('should call error function', () => {
501 component.data = createFakeData(5);
502 component.fetchData.subscribe((context: any) => {
504 expect(component.loadingError).toBeTruthy();
505 expect(component.data.length).toBe(0);
506 expect(component.loadingIndicator).toBeFalsy();
507 expect(component['updating']).toBeFalsy();
509 component.reloadData();
512 it('should call error function with custom config', () => {
513 component.data = createFakeData(10);
514 component.fetchData.subscribe((context: any) => {
515 context.errorConfig.resetData = false;
516 context.errorConfig.displayError = false;
518 expect(component.loadingError).toBeFalsy();
519 expect(component.data.length).toBe(10);
520 expect(component.loadingIndicator).toBeFalsy();
521 expect(component['updating']).toBeFalsy();
523 component.reloadData();
526 it('should update selection on refresh - "onChange"', () => {
527 spyOn(component, 'onSelect').and.callThrough();
528 component.data = createFakeData(10);
529 component.selection.selected = [_.clone(component.data[1])];
530 component.updateSelectionOnRefresh = 'onChange';
531 component.updateSelected();
532 expect(component.onSelect).toHaveBeenCalledTimes(0);
533 component.data[1].d = !component.data[1].d;
534 component.updateSelected();
535 expect(component.onSelect).toHaveBeenCalled();
538 it('should update selection on refresh - "always"', () => {
539 spyOn(component, 'onSelect').and.callThrough();
540 component.data = createFakeData(10);
541 component.selection.selected = [_.clone(component.data[1])];
542 component.updateSelectionOnRefresh = 'always';
543 component.updateSelected();
544 expect(component.onSelect).toHaveBeenCalled();
545 component.data[1].d = !component.data[1].d;
546 component.updateSelected();
547 expect(component.onSelect).toHaveBeenCalled();
550 it('should update selection on refresh - "never"', () => {
551 spyOn(component, 'onSelect').and.callThrough();
552 component.data = createFakeData(10);
553 component.selection.selected = [_.clone(component.data[1])];
554 component.updateSelectionOnRefresh = 'never';
555 component.updateSelected();
556 expect(component.onSelect).toHaveBeenCalledTimes(0);
557 component.data[1].d = !component.data[1].d;
558 component.updateSelected();
559 expect(component.onSelect).toHaveBeenCalledTimes(0);
567 describe('useCustomClass', () => {
569 component.customCss = {
570 'badge badge-danger': 'active',
571 'secret secret-number': 123.456,
572 btn: (v) => _.isString(v) && v.startsWith('http'),
573 secure: (v) => _.isString(v) && v.startsWith('https')
577 it('should throw an error if custom classes are not set', () => {
578 component.customCss = undefined;
579 expect(() => component.useCustomClass('active')).toThrowError('Custom classes are not set!');
582 it('should not return any class', () => {
583 ['', 'something', 123, { complex: 1 }, [1, 2, 3]].forEach((value) =>
584 expect(component.useCustomClass(value)).toBe(undefined)
588 it('should match a string and return the corresponding class', () => {
589 expect(component.useCustomClass('active')).toBe('badge badge-danger');
592 it('should match a number and return the corresponding class', () => {
593 expect(component.useCustomClass(123.456)).toBe('secret secret-number');
596 it('should match against a function and return the corresponding class', () => {
597 expect(component.useCustomClass('http://no.ssl')).toBe('btn');
600 it('should match against multiple functions and return the corresponding classes', () => {
601 expect(component.useCustomClass('https://secure.it')).toBe('btn secure');