]> git.proxmox.com Git - ceph.git/blame - ceph/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.spec.ts
d/control: depend on python3-yaml for ceph-mgr
[ceph.git] / ceph / src / pybind / mgr / dashboard / frontend / src / app / shared / datatable / table / table.component.spec.ts
CommitLineData
11fdf7f2
TL
1import { ComponentFixture, TestBed } from '@angular/core/testing';
2import { FormsModule } from '@angular/forms';
3import { RouterTestingModule } from '@angular/router/testing';
4
5import { NgxDatatableModule } from '@swimlane/ngx-datatable';
6import * as _ from 'lodash';
9f95a23c 7import { BsDropdownModule } from 'ngx-bootstrap/dropdown';
11fdf7f2
TL
8
9import { configureTestBed } from '../../../../testing/unit-test-helper';
10import { ComponentsModule } from '../../components/components.module';
9f95a23c 11import { CdTableColumnFilter } from '../../models/cd-table-column-filter';
11fdf7f2 12import { CdTableFetchDataContext } from '../../models/cd-table-fetch-data-context';
9f95a23c 13import { PipesModule } from '../../pipes/pipes.module';
11fdf7f2
TL
14import { TableComponent } from './table.component';
15
16describe('TableComponent', () => {
17 let component: TableComponent;
18 let fixture: ComponentFixture<TableComponent>;
19
9f95a23c 20 const createFakeData = (n: number) => {
11fdf7f2
TL
21 const data = [];
22 for (let i = 0; i < n; i++) {
23 data.push({
24 a: i,
9f95a23c
TL
25 b: i * 10,
26 c: !!(i % 2)
11fdf7f2
TL
27 });
28 }
29 return data;
30 };
31
32 const clearLocalStorage = () => {
33 component.localStorage.clear();
34 };
35
36 configureTestBed({
37 declarations: [TableComponent],
9f95a23c
TL
38 imports: [
39 NgxDatatableModule,
40 FormsModule,
41 ComponentsModule,
42 RouterTestingModule,
43 BsDropdownModule.forRoot(),
44 PipesModule
45 ]
11fdf7f2
TL
46 });
47
48 beforeEach(() => {
49 fixture = TestBed.createComponent(TableComponent);
50 component = fixture.componentInstance;
11fdf7f2 51
9f95a23c 52 component.data = createFakeData(10);
11fdf7f2 53 component.columns = [
9f95a23c
TL
54 { prop: 'a', name: 'Index', filterable: true },
55 { prop: 'b', name: 'Index times ten' },
56 { prop: 'c', name: 'Odd?', filterable: true }
11fdf7f2
TL
57 ];
58 });
59
60 it('should create', () => {
61 expect(component).toBeTruthy();
62 });
63
9f95a23c
TL
64 it('should force an identifier', () => {
65 component.identifier = 'x';
66 component.forceIdentifier = true;
67 component.ngOnInit();
68 expect(component.identifier).toBe('x');
69 expect(component.sorts[0].prop).toBe('a');
70 expect(component.sorts).toEqual(component.createSortingDefinition('a'));
71 });
11fdf7f2 72
9f95a23c
TL
73 it('should have rows', () => {
74 component.useData();
75 expect(component.data.length).toBe(10);
76 expect(component.rows.length).toBe(component.data.length);
77 });
11fdf7f2 78
9f95a23c
TL
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));
82
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);
90 });
11fdf7f2 91
9f95a23c
TL
92 it('should prevent propagation of mouseenter event', (done) => {
93 let wasCalled = false;
94 const mouseEvent = new MouseEvent('mouseenter');
95 mouseEvent.stopPropagation = () => {
96 wasCalled = true;
97 };
98 spyOn(component.table.element, 'addEventListener').and.callFake((eventName, fn) => {
99 fn(mouseEvent);
100 expect(eventName).toBe('mouseenter');
101 expect(wasCalled).toBe(true);
102 done();
11fdf7f2 103 });
9f95a23c
TL
104 component.ngOnInit();
105 });
11fdf7f2 106
9f95a23c
TL
107 describe('test column filtering', () => {
108 let filterIndex: CdTableColumnFilter;
109 let filterOdd: CdTableColumnFilter;
110 let filterCustom: CdTableColumnFilter;
111
112 const expectColumnFilterCreated = (
113 filter: CdTableColumnFilter,
114 prop: string,
115 options: string[],
116 value?: { raw: string; formatted: string }
117 ) => {
118 expect(filter.column.prop).toBe(prop);
119 expect(_.map(filter.options, 'raw')).toEqual(options);
120 expect(filter.value).toEqual(value);
121 };
122
123 const expectColumnFiltered = (
124 changes: { filter: CdTableColumnFilter; value?: string }[],
125 results: any[],
126 search: string = ''
127 ) => {
128 component.search = search;
129 _.forEach(changes, (change) => {
130 component.onChangeFilter(
131 change.filter,
132 change.value ? { raw: change.value, formatted: change.value } : undefined
133 );
494da23a 134 });
9f95a23c
TL
135 expect(component.rows).toEqual(results);
136 component.onClearSearch();
137 component.onClearFilters();
138 };
81eedcae 139
9f95a23c
TL
140 describe('with visible columns', () => {
141 beforeEach(() => {
142 component.initColumnFilters();
143 component.updateColumnFilterOptions();
144 filterIndex = component.columnFilters[0];
145 filterOdd = component.columnFilters[1];
146 });
11fdf7f2 147
9f95a23c
TL
148 it('should have filters initialized', () => {
149 expect(component.columnFilters.length).toBe(2);
150 expectColumnFilterCreated(
151 filterIndex,
152 'a',
153 _.map(component.data, (row) => _.toString(row.a))
154 );
155 expectColumnFilterCreated(filterOdd, 'c', ['false', 'true']);
11fdf7f2
TL
156 });
157
9f95a23c
TL
158 it('should add filters', () => {
159 // single
160 expectColumnFiltered([{ filter: filterIndex, value: '1' }], [{ a: 1, b: 10, c: true }]);
161
162 // multiple
163 expectColumnFiltered(
801d1391
TL
164 [
165 { filter: filterOdd, value: 'false' },
166 { filter: filterIndex, value: '2' }
167 ],
9f95a23c
TL
168 [{ a: 2, b: 20, c: false }]
169 );
170
171 // Clear should work
172 expect(component.rows).toEqual(component.data);
11fdf7f2
TL
173 });
174
9f95a23c
TL
175 it('should remove filters', () => {
176 // single
177 expectColumnFiltered(
178 [
179 { filter: filterOdd, value: 'true' },
180 { filter: filterIndex, value: '1' },
181 { filter: filterIndex, value: undefined }
182 ],
183 [
184 { a: 1, b: 10, c: true },
185 { a: 3, b: 30, c: true },
186 { a: 5, b: 50, c: true },
187 { a: 7, b: 70, c: true },
188 { a: 9, b: 90, c: true }
189 ]
190 );
191
192 // multiple
193 expectColumnFiltered(
194 [
195 { filter: filterOdd, value: 'true' },
196 { filter: filterIndex, value: '1' },
197 { filter: filterIndex, value: undefined },
198 { filter: filterOdd, value: undefined }
199 ],
200 component.data
201 );
202
203 // a selected filter should be removed if it's selected again
204 expectColumnFiltered(
205 [
206 { filter: filterOdd, value: 'true' },
207 { filter: filterIndex, value: '1' },
208 { filter: filterIndex, value: '1' }
209 ],
210 [
211 { a: 1, b: 10, c: true },
212 { a: 3, b: 30, c: true },
213 { a: 5, b: 50, c: true },
214 { a: 7, b: 70, c: true },
215 { a: 9, b: 90, c: true }
216 ]
217 );
11fdf7f2
TL
218 });
219
9f95a23c
TL
220 it('should search from filtered rows', () => {
221 expectColumnFiltered(
222 [{ filter: filterOdd, value: 'true' }],
223 [{ a: 9, b: 90, c: true }],
224 '9'
225 );
226
227 // Clear should work
228 expect(component.rows).toEqual(component.data);
11fdf7f2 229 });
9f95a23c 230 });
11fdf7f2 231
9f95a23c
TL
232 describe('with custom columns', () => {
233 beforeEach(() => {
234 // create a new additional column in data
235 for (let i = 0; i < component.data.length; i++) {
236 const row = component.data[i];
237 row['d'] = row.a;
238 }
239 // create a custom column filter
240 component.extraFilterableColumns = [
241 {
242 name: 'd less than 5',
243 prop: 'd',
244 filterOptions: ['yes', 'no'],
245 filterInitValue: 'yes',
246 filterPredicate: (row, value) => {
247 if (value === 'yes') {
248 return row.d < 5;
249 } else {
250 return row.d >= 5;
251 }
252 }
253 }
254 ];
255 component.initColumnFilters();
256 component.updateColumnFilterOptions();
257 filterIndex = component.columnFilters[0];
258 filterOdd = component.columnFilters[1];
259 filterCustom = component.columnFilters[2];
11fdf7f2
TL
260 });
261
9f95a23c
TL
262 it('should have filters initialized', () => {
263 expect(component.columnFilters.length).toBe(3);
264 expectColumnFilterCreated(filterCustom, 'd', ['yes', 'no'], {
265 raw: 'yes',
266 formatted: 'yes'
267 });
268 component.useData();
269 expect(component.rows).toEqual(_.slice(component.data, 0, 5));
11fdf7f2
TL
270 });
271
9f95a23c
TL
272 it('should remove filters', () => {
273 expectColumnFiltered([{ filter: filterCustom, value: 'no' }], _.slice(component.data, 5));
11fdf7f2 274 });
9f95a23c
TL
275 });
276 });
277
278 describe('test search', () => {
279 const expectSearch = (keyword: string, expectedResult: object[]) => {
280 component.search = keyword;
281 component.updateFilter();
282 expect(component.rows).toEqual(expectedResult);
283 component.onClearSearch();
284 };
285
286 describe('searchableObjects', () => {
287 const testObject = {
288 obj: {
289 min: 8,
290 max: 123
291 }
292 };
11fdf7f2 293
9f95a23c
TL
294 beforeEach(() => {
295 component.data = [testObject];
296 component.columns = [{ prop: 'obj', name: 'Object' }];
11fdf7f2
TL
297 });
298
9f95a23c
TL
299 it('should not search through objects as default case', () => {
300 expect(component.searchableObjects).toBe(false);
301 expectSearch('8', []);
11fdf7f2
TL
302 });
303
9f95a23c
TL
304 it('should search through objects if searchableObjects is set to true', () => {
305 component.searchableObjects = true;
306 expectSearch('28', []);
307 expectSearch('8', [testObject]);
308 expectSearch('123', [testObject]);
309 expectSearch('max', [testObject]);
11fdf7f2
TL
310 });
311 });
9f95a23c
TL
312
313 it('should find a particular number', () => {
314 expectSearch('5', [{ a: 5, b: 50, c: true }]);
315 expectSearch('9', [{ a: 9, b: 90, c: true }]);
316 });
317
318 it('should find boolean values', () => {
319 expectSearch('true', [
320 { a: 1, b: 10, c: true },
321 { a: 3, b: 30, c: true },
322 { a: 5, b: 50, c: true },
323 { a: 7, b: 70, c: true },
324 { a: 9, b: 90, c: true }
325 ]);
326 expectSearch('false', [
327 { a: 0, b: 0, c: false },
328 { a: 2, b: 20, c: false },
329 { a: 4, b: 40, c: false },
330 { a: 6, b: 60, c: false },
331 { a: 8, b: 80, c: false }
332 ]);
333 });
334
335 it('should test search keyword preparation', () => {
336 const prepare = TableComponent.prepareSearch;
337 const expected = ['a', 'b', 'c'];
338 expect(prepare('a b c')).toEqual(expected);
339 expect(prepare('a,, b,, c')).toEqual(expected);
340 expect(prepare('a,,,, b,,, c')).toEqual(expected);
341 expect(prepare('a+b c')).toEqual(['a+b', 'c']);
342 expect(prepare('a,,,+++b,,, c')).toEqual(['a+++b', 'c']);
343 expect(prepare('"a b c" "d e f", "g, h i"')).toEqual(['a+b+c', 'd+e++f', 'g+h+i']);
344 });
345
346 it('should search for multiple values', () => {
347 expectSearch('2 20 false', [{ a: 2, b: 20, c: false }]);
348 expectSearch('false 2', [{ a: 2, b: 20, c: false }]);
349 });
350
351 it('should filter by column', () => {
352 expectSearch('index:5', [{ a: 5, b: 50, c: true }]);
353 expectSearch('times:50', [{ a: 5, b: 50, c: true }]);
354 expectSearch('times:50 index:5', [{ a: 5, b: 50, c: true }]);
355 expectSearch('Odd?:true', [
356 { a: 1, b: 10, c: true },
357 { a: 3, b: 30, c: true },
358 { a: 5, b: 50, c: true },
359 { a: 7, b: 70, c: true },
360 { a: 9, b: 90, c: true }
361 ]);
362 component.data = createFakeData(100);
363 expectSearch('index:1 odd:true times:110', [{ a: 11, b: 110, c: true }]);
364 });
365
366 it('should search through arrays', () => {
801d1391
TL
367 component.columns = [
368 { prop: 'a', name: 'Index' },
369 { prop: 'b', name: 'ArrayColumn' }
370 ];
371
372 component.data = [
373 { a: 1, b: ['foo', 'bar'] },
374 { a: 2, b: ['baz', 'bazinga'] }
375 ];
9f95a23c
TL
376 expectSearch('bar', [{ a: 1, b: ['foo', 'bar'] }]);
377 expectSearch('arraycolumn:bar arraycolumn:foo', [{ a: 1, b: ['foo', 'bar'] }]);
378 expectSearch('arraycolumn:baz arraycolumn:inga', [{ a: 2, b: ['baz', 'bazinga'] }]);
379
801d1391
TL
380 component.data = [
381 { a: 1, b: [1, 2] },
382 { a: 2, b: [3, 4] }
383 ];
9f95a23c
TL
384 expectSearch('arraycolumn:1 arraycolumn:2', [{ a: 1, b: [1, 2] }]);
385 });
386
387 it('should search with spaces', () => {
388 const expectedResult = [{ a: 2, b: 20, c: false }];
389 expectSearch(`'Index times ten':20`, expectedResult);
390 expectSearch('index+times+ten:20', expectedResult);
391 });
392
393 it('should filter results although column name is incomplete', () => {
394 component.data = createFakeData(3);
395 expectSearch(`'Index times ten'`, []);
396 expectSearch(`'Ind'`, []);
397 expectSearch(`'Ind:'`, [
398 { a: 0, b: 0, c: false },
399 { a: 1, b: 10, c: true },
400 { a: 2, b: 20, c: false }
401 ]);
402 });
403
404 it('should search if column name is incomplete', () => {
405 const expectedData = [
406 { a: 0, b: 0, c: false },
407 { a: 1, b: 10, c: true },
408 { a: 2, b: 20, c: false }
409 ];
410 component.data = _.clone(expectedData);
411 expectSearch('inde', []);
412 expectSearch('index:', expectedData);
413 expectSearch('index times te', []);
414 });
415
416 it('should restore full table after search', () => {
417 component.useData();
418 expect(component.rows.length).toBe(10);
419 component.search = '3';
420 component.updateFilter();
421 expect(component.rows.length).toBe(1);
422 component.onClearSearch();
423 expect(component.rows.length).toBe(10);
424 });
11fdf7f2
TL
425 });
426
427 describe('after ngInit', () => {
9f95a23c 428 const toggleColumn = (prop: string, checked: boolean) => {
11fdf7f2
TL
429 component.toggleColumn({
430 target: {
431 name: prop,
432 checked: checked
433 }
434 });
435 };
436
437 const equalStorageConfig = () => {
438 expect(JSON.stringify(component.userConfig)).toBe(
439 component.localStorage.getItem(component.tableName)
440 );
441 };
442
443 beforeEach(() => {
444 component.ngOnInit();
445 });
446
447 it('should have updated the column definitions', () => {
448 expect(component.columns[0].flexGrow).toBe(1);
449 expect(component.columns[1].flexGrow).toBe(2);
450 expect(component.columns[2].flexGrow).toBe(2);
451 expect(component.columns[2].resizeable).toBe(false);
452 });
453
454 it('should have table columns', () => {
9f95a23c 455 expect(component.tableColumns.length).toBe(3);
11fdf7f2
TL
456 expect(component.tableColumns).toEqual(component.columns);
457 });
458
459 it('should have a unique identifier which it searches for', () => {
460 expect(component.identifier).toBe('a');
461 expect(component.userConfig.sorts[0].prop).toBe('a');
462 expect(component.userConfig.sorts).toEqual(component.createSortingDefinition('a'));
463 equalStorageConfig();
464 });
465
466 it('should remove column "a"', () => {
467 expect(component.userConfig.sorts[0].prop).toBe('a');
468 toggleColumn('a', false);
469 expect(component.userConfig.sorts[0].prop).toBe('b');
9f95a23c 470 expect(component.tableColumns.length).toBe(2);
11fdf7f2
TL
471 equalStorageConfig();
472 });
473
474 it('should not be able to remove all columns', () => {
475 expect(component.userConfig.sorts[0].prop).toBe('a');
476 toggleColumn('a', false);
477 toggleColumn('b', false);
478 toggleColumn('c', false);
9f95a23c 479 expect(component.userConfig.sorts[0].prop).toBe('c');
11fdf7f2
TL
480 expect(component.tableColumns.length).toBe(1);
481 equalStorageConfig();
482 });
483
484 it('should enable column "a" again', () => {
485 expect(component.userConfig.sorts[0].prop).toBe('a');
486 toggleColumn('a', false);
487 toggleColumn('a', true);
488 expect(component.userConfig.sorts[0].prop).toBe('b');
9f95a23c 489 expect(component.tableColumns.length).toBe(3);
11fdf7f2
TL
490 equalStorageConfig();
491 });
492
493 afterEach(() => {
494 clearLocalStorage();
495 });
496 });
497
498 describe('reload data', () => {
499 beforeEach(() => {
500 component.ngOnInit();
501 component.data = [];
502 component['updating'] = false;
503 });
504
505 it('should call fetchData callback function', () => {
9f95a23c 506 component.fetchData.subscribe((context: any) => {
11fdf7f2
TL
507 expect(context instanceof CdTableFetchDataContext).toBeTruthy();
508 });
509 component.reloadData();
510 });
511
512 it('should call error function', () => {
513 component.data = createFakeData(5);
9f95a23c 514 component.fetchData.subscribe((context: any) => {
11fdf7f2
TL
515 context.error();
516 expect(component.loadingError).toBeTruthy();
517 expect(component.data.length).toBe(0);
518 expect(component.loadingIndicator).toBeFalsy();
519 expect(component['updating']).toBeFalsy();
520 });
521 component.reloadData();
522 });
523
524 it('should call error function with custom config', () => {
525 component.data = createFakeData(10);
9f95a23c 526 component.fetchData.subscribe((context: any) => {
11fdf7f2
TL
527 context.errorConfig.resetData = false;
528 context.errorConfig.displayError = false;
529 context.error();
530 expect(component.loadingError).toBeFalsy();
531 expect(component.data.length).toBe(10);
532 expect(component.loadingIndicator).toBeFalsy();
533 expect(component['updating']).toBeFalsy();
534 });
535 component.reloadData();
536 });
537
538 it('should update selection on refresh - "onChange"', () => {
539 spyOn(component, 'onSelect').and.callThrough();
540 component.data = createFakeData(10);
541 component.selection.selected = [_.clone(component.data[1])];
542 component.updateSelectionOnRefresh = 'onChange';
543 component.updateSelected();
544 expect(component.onSelect).toHaveBeenCalledTimes(0);
545 component.data[1].d = !component.data[1].d;
546 component.updateSelected();
547 expect(component.onSelect).toHaveBeenCalled();
548 });
549
550 it('should update selection on refresh - "always"', () => {
551 spyOn(component, 'onSelect').and.callThrough();
552 component.data = createFakeData(10);
553 component.selection.selected = [_.clone(component.data[1])];
554 component.updateSelectionOnRefresh = 'always';
555 component.updateSelected();
556 expect(component.onSelect).toHaveBeenCalled();
557 component.data[1].d = !component.data[1].d;
558 component.updateSelected();
559 expect(component.onSelect).toHaveBeenCalled();
560 });
561
562 it('should update selection on refresh - "never"', () => {
563 spyOn(component, 'onSelect').and.callThrough();
564 component.data = createFakeData(10);
565 component.selection.selected = [_.clone(component.data[1])];
566 component.updateSelectionOnRefresh = 'never';
567 component.updateSelected();
568 expect(component.onSelect).toHaveBeenCalledTimes(0);
569 component.data[1].d = !component.data[1].d;
570 component.updateSelected();
571 expect(component.onSelect).toHaveBeenCalledTimes(0);
572 });
573
574 afterEach(() => {
575 clearLocalStorage();
576 });
577 });
578
579 describe('useCustomClass', () => {
580 beforeEach(() => {
581 component.customCss = {
9f95a23c 582 'badge badge-danger': 'active',
11fdf7f2 583 'secret secret-number': 123.456,
9f95a23c 584 btn: (v) => _.isString(v) && v.startsWith('http'),
11fdf7f2
TL
585 secure: (v) => _.isString(v) && v.startsWith('https')
586 };
587 });
588
589 it('should throw an error if custom classes are not set', () => {
590 component.customCss = undefined;
591 expect(() => component.useCustomClass('active')).toThrowError('Custom classes are not set!');
592 });
593
594 it('should not return any class', () => {
595 ['', 'something', 123, { complex: 1 }, [1, 2, 3]].forEach((value) =>
596 expect(component.useCustomClass(value)).toBe(undefined)
597 );
598 });
599
600 it('should match a string and return the corresponding class', () => {
9f95a23c 601 expect(component.useCustomClass('active')).toBe('badge badge-danger');
11fdf7f2
TL
602 });
603
604 it('should match a number and return the corresponding class', () => {
605 expect(component.useCustomClass(123.456)).toBe('secret secret-number');
606 });
607
608 it('should match against a function and return the corresponding class', () => {
9f95a23c 609 expect(component.useCustomClass('http://no.ssl')).toBe('btn');
11fdf7f2
TL
610 });
611
612 it('should match against multiple functions and return the corresponding classes', () => {
9f95a23c 613 expect(component.useCustomClass('https://secure.it')).toBe('btn secure');
11fdf7f2
TL
614 });
615 });
616});