/*! * @license * Copyright 2019 Alfresco Software, Ltd. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { SimpleChange, NO_ERRORS_SCHEMA, QueryList, Component, TemplateRef, ViewChild } from '@angular/core'; import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing'; import { MatCheckboxChange } from '@angular/material/checkbox'; import { DataColumn } from '../../data/data-column.model'; import { DataRow } from '../../data/data-row.model'; import { DataSorting } from '../../data/data-sorting.model'; import { ObjectDataColumn } from '../../data/object-datacolumn.model'; import { ObjectDataTableAdapter } from '../../data/object-datatable-adapter'; import { DataTableComponent, ShowHeaderMode } from './datatable.component'; import { setupTestBed } from '../../../testing/setup-test-bed'; import { CoreTestingModule } from '../../../testing/core.testing.module'; import { DataColumnListComponent } from '../../../data-column/data-column-list.component'; import { DataColumnComponent } from '../../../data-column/data-column.component'; import { TranslateModule } from '@ngx-translate/core'; @Component({selector: 'adf-custom-column-template-component', template: ` `}) class CustomColumnTemplateComponent { @ViewChild('tmplRef', { static: true }) templateRef: TemplateRef; } class FakeDataRow implements DataRow { isDropTarget = false; isSelected = true; hasValue() { return true; } getValue() { return '1'; } imageErrorResolver() { return './assets/images/ft_ic_miscellaneous.svg'; } } export function resolverFn(row: DataRow, col: DataColumn) { const value = row.getValue(col.key); if (col.key === 'name') { return `${row.getValue('firstName')} - ${row.getValue('lastName')}`; } return value; } describe('DataTable', () => { let fixture: ComponentFixture; let dataTable: DataTableComponent; let element: any; setupTestBed({ imports: [ TranslateModule.forRoot(), CoreTestingModule ], schemas: [NO_ERRORS_SCHEMA] }); beforeEach(() => { fixture = TestBed.createComponent(DataTableComponent); dataTable = fixture.componentInstance; element = fixture.debugElement.nativeElement; }); afterEach(() => { fixture.destroy(); }); it('should preserve the historical selection order', () => { dataTable.data = new ObjectDataTableAdapter( [{ id: 0 }, { id: 1 }, { id: 2 }], [new ObjectDataColumn({ key: 'id' })] ); const rows = dataTable.data.getRows(); dataTable.selectRow(rows[2], true); dataTable.selectRow(rows[0], true); dataTable.selectRow(rows[1], true); const selection = dataTable.selection; expect(selection[0].getValue('id')).toBe(2); expect(selection[1].getValue('id')).toBe(0); expect(selection[2].getValue('id')).toBe(1); }); it('should update schema if columns change', fakeAsync(() => { dataTable.columnList = new DataColumnListComponent(); dataTable.columnList.columns = new QueryList(); dataTable.data = new ObjectDataTableAdapter([], []); spyOn(dataTable.data, 'setColumns').and.callThrough(); dataTable.ngAfterContentInit(); dataTable.columnList.columns.reset([new DataColumnComponent()]); dataTable.columnList.columns.notifyOnChanges(); tick(100); expect(dataTable.data.setColumns).toHaveBeenCalled(); })); it('should use the cardview style if cardview is true', () => { const newData = new ObjectDataTableAdapter( [ { name: '1' }, { name: '2' } ], [new ObjectDataColumn({ key: 'name' })] ); dataTable.display = 'gallery'; dataTable.ngOnChanges({ data: new SimpleChange(null, newData, false) }); fixture.detectChanges(); expect(element.querySelector('.adf-datatable-card')).not.toBeNull(); expect(element.querySelector('.adf-datatable')).toBeNull(); }); it('should use the cardview style if cardview is false', () => { const newData = new ObjectDataTableAdapter( [ { name: '1' }, { name: '2' } ], [new ObjectDataColumn({ key: 'name' })] ); dataTable.ngOnChanges({ data: new SimpleChange(null, newData, false) }); fixture.detectChanges(); expect(element.querySelector('.adf-datatable-card')).toBeNull(); expect(element.querySelector('.adf-datatable-list')).not.toBeNull(); }); describe('Header modes', () => { const newData = new ObjectDataTableAdapter( [ { name: '1' }, { name: '2' } ], [new ObjectDataColumn({ key: 'name' })] ); const emptyData = new ObjectDataTableAdapter(); it('should show the header if showHeader is `Data` and there is data', () => { dataTable.showHeader = ShowHeaderMode.Data; dataTable.loading = false; dataTable.ngOnChanges({ data: new SimpleChange(null, newData, false) }); fixture.detectChanges(); expect(element.querySelector('.adf-datatable-header')).toBeDefined(); }); it('should hide the header if showHeader is `Data` and there is no data', () => { dataTable.showHeader = ShowHeaderMode.Data; dataTable.loading = false; dataTable.ngOnChanges({ data: new SimpleChange(null, emptyData, false) }); fixture.detectChanges(); expect(element.querySelector('.adf-datatable-header')).toBeNull(); }); it('should always show the header if showHeader is `Always`', () => { dataTable.showHeader = ShowHeaderMode.Always; dataTable.loading = false; dataTable.ngOnChanges({ data: new SimpleChange(null, newData, false) }); fixture.detectChanges(); expect(element.querySelector('.adf-datatable-header')).toBeDefined(); dataTable.ngOnChanges({ data: new SimpleChange(null, emptyData, false) }); fixture.detectChanges(); expect(element.querySelector('.adf-datatable-header')).toBeDefined(); }); it('should never show the header if showHeader is `Never`', () => { dataTable.showHeader = ShowHeaderMode.Never; dataTable.loading = false; dataTable.ngOnChanges({ data: new SimpleChange(null, newData, false) }); fixture.detectChanges(); expect(element.querySelector('.adf-datatable-header')).toBeNull(); dataTable.ngOnChanges({ data: new SimpleChange(null, emptyData, false) }); fixture.detectChanges(); expect(element.querySelector('.adf-datatable-header')).toBeNull(); }); it('should never show the header if noPermission is true', () => { dataTable.loading = false; dataTable.noPermission = true; dataTable.ngOnChanges({ data: new SimpleChange(null, emptyData, false) }); dataTable.showHeader = ShowHeaderMode.Data; fixture.detectChanges(); expect(element.querySelector('.adf-datatable-header')).toBeNull(); dataTable.showHeader = ShowHeaderMode.Always; fixture.detectChanges(); expect(element.querySelector('.adf-datatable-header')).toBeNull(); dataTable.showHeader = ShowHeaderMode.Never; fixture.detectChanges(); expect(element.querySelector('.adf-datatable-header')).toBeNull(); }); it('should never show the header if loading is true', () => { dataTable.loading = true; dataTable.ngOnChanges({ data: new SimpleChange(null, emptyData, false) }); dataTable.showHeader = ShowHeaderMode.Data; fixture.detectChanges(); expect(element.querySelector('.adf-datatable-header')).toBeNull(); dataTable.showHeader = ShowHeaderMode.Always; fixture.detectChanges(); expect(element.querySelector('.adf-datatable-header')).toBeNull(); dataTable.showHeader = ShowHeaderMode.Never; fixture.detectChanges(); expect(element.querySelector('.adf-datatable-header')).toBeNull(); }); }); it('should emit "sorting-changed" DOM event', (done) => { const column = new ObjectDataColumn({ key: 'name', sortable: true, direction: 'asc' }); dataTable.data = new ObjectDataTableAdapter( [ { name: '1' }, { name: '2' } ], [column] ); dataTable.data.setSorting(new DataSorting('name', 'desc')); fixture.nativeElement.addEventListener('sorting-changed', (event: CustomEvent) => { expect(event.detail.key).toBe('name'); expect(event.detail.direction).toBe('asc'); done(); }); dataTable.ngOnChanges({}); fixture.detectChanges(); dataTable.ngAfterViewInit(); dataTable.onColumnHeaderClick(column); }); it('should change the rows on changing of the data', () => { const newData = new ObjectDataTableAdapter( [ { name: 'TEST' }, { name: 'FAKE' } ], [new ObjectDataColumn({ key: 'name' })] ); dataTable.data = new ObjectDataTableAdapter( [ { name: '1' }, { name: '2' } ], [new ObjectDataColumn({ key: 'name' })] ); dataTable.ngOnChanges({ data: new SimpleChange(null, newData, false) }); fixture.detectChanges(); expect(element.querySelector('[data-automation-id="text_TEST"]')).not.toBeNull(); expect(element.querySelector('[data-automation-id="text_FAKE"]')).not.toBeNull(); }); it('should set rows to the data when rows defined', () => { const dataRows = [ { name: 'test1' }, { name: 'test2' }, { name: 'test3' }, { name: 'test4' } ]; dataTable.data = new ObjectDataTableAdapter([], [new ObjectDataColumn({ key: 'name' })] ); dataTable.ngOnChanges({ rows: new SimpleChange(null, dataRows, false) }); fixture.detectChanges(); const rows = dataTable.data.getRows(); expect(rows[0].getValue('name')).toEqual('test1'); expect(rows[1].getValue('name')).toEqual('test2'); }); it('should double click if keydown "enter key" on row', () => { const event = new KeyboardEvent('keydown', { code: 'Enter', key: 'Enter' } as KeyboardEventInit ); const dataRows = [ { name: 'test1'}, { name: 'test2' } ]; dataTable.data = new ObjectDataTableAdapter([], [new ObjectDataColumn({ key: 'name' })] ); dataTable.ngOnChanges({ rows: new SimpleChange(null, dataRows, false) }); fixture.detectChanges(); dataTable.ngAfterViewInit(); const rowElement = document.querySelectorAll('.adf-datatable-body .adf-datatable-row')[0]; spyOn(dataTable.rowDblClick, 'emit'); rowElement.dispatchEvent(event); expect(dataTable.rowDblClick.emit).toHaveBeenCalled(); }); it('should set custom sort order', () => { const dataSortObj = new DataSorting('dummyName', 'asc'); const dataRows = [ { name: 'test1' }, { name: 'test2' }, { name: 'test3' }, { name: 'test4' } ]; dataTable.sorting = ['dummyName', 'asc']; dataTable.ngOnChanges({ rows: new SimpleChange(null, dataRows, false) }); fixture.detectChanges(); const dataSort = dataTable.data.getSorting(); expect(dataSort).toEqual(dataSortObj); }); it('should reset selection on mode change', () => { spyOn(dataTable, 'resetSelection').and.callThrough(); dataTable.data = new ObjectDataTableAdapter( [ { name: '1' }, { name: '2' } ], [new ObjectDataColumn({ key: 'name' })] ); const rows = dataTable.data.getRows(); rows[0].isSelected = true; rows[1].isSelected = true; expect(rows[0].isSelected).toBeTruthy(); expect(rows[1].isSelected).toBeTruthy(); dataTable.ngOnChanges({ selectionMode: new SimpleChange(null, 'multiple', false) }); expect(dataTable.resetSelection).toHaveBeenCalled(); }); it('should select the row where isSelected is true', () => { dataTable.rows = [ { name: 'TEST1' }, { name: 'FAKE2' }, { name: 'TEST2', isSelected: true }, { name: 'FAKE2' }]; dataTable.data = new ObjectDataTableAdapter([], [new ObjectDataColumn({ key: 'name' })] ); fixture.detectChanges(); const rows = dataTable.data.getRows(); expect(rows[0].isSelected).toBeFalsy(); expect(rows[1].isSelected).toBeFalsy(); expect(rows[2].isSelected).toBeTruthy(); expect(rows[3].isSelected).toBeFalsy(); }); it('should not select any row when isSelected is not defined', () => { const dataRows = [ { name: 'TEST1' }, { name: 'FAKE2' }, { name: 'TEST2' } ]; dataTable.data = new ObjectDataTableAdapter(dataRows, [new ObjectDataColumn({ key: 'name' })] ); dataTable.ngOnChanges({ rows: new SimpleChange(null, dataRows, false) }); fixture.detectChanges(); const rows = dataTable.data.getRows(); expect(rows[0].isSelected).toBeFalsy(); expect(rows[1].isSelected).toBeFalsy(); expect(rows[2].isSelected).toBeFalsy(); }); it('should select only one row with [single] selection mode', (done) => { dataTable.selectionMode = 'single'; dataTable.data = new ObjectDataTableAdapter( [ { name: '1', isSelected: true }, { name: '2' } ], [new ObjectDataColumn({ key: 'name' })] ); const rows = dataTable.data.getRows(); dataTable.ngOnChanges({}); fixture.detectChanges(); dataTable.rowClick.subscribe(() => { expect(rows[0].isSelected).toBeFalsy(); expect(rows[1].isSelected).toBeTruthy(); done(); }); dataTable.onRowClick(rows[1], new MouseEvent('click')); }); it('should select only one row with [single] selection mode and key modifier', (done) => { dataTable.selectionMode = 'single'; dataTable.data = new ObjectDataTableAdapter( [ { name: '1', isSelected: true }, { name: '2' } ], [new ObjectDataColumn({ key: 'name' })] ); const rows = dataTable.data.getRows(); dataTable.ngOnChanges({}); fixture.detectChanges(); dataTable.rowClick.subscribe(() => { expect(rows[0].isSelected).toBeFalsy(); expect(rows[1].isSelected).toBeTruthy(); done(); }); dataTable.onRowClick(rows[1], new MouseEvent('click', { metaKey: true })); }); it('should select only one row with [single] selection mode pressing enter key', () => { dataTable.selectionMode = 'single'; dataTable.data = new ObjectDataTableAdapter( [ { name: '1' }, { name: '2' } ], [new ObjectDataColumn({ key: 'name' })] ); const rows = dataTable.data.getRows(); dataTable.ngOnChanges({}); dataTable.onEnterKeyPressed(rows[0], null); expect(rows[0].isSelected).toBeTruthy(); expect(rows[1].isSelected).toBeFalsy(); dataTable.onEnterKeyPressed(rows[1], null); expect(rows[0].isSelected).toBeFalsy(); expect(rows[1].isSelected).toBeTruthy(); }); it('should select multiple rows with [multiple] selection mode pressing enter key', () => { dataTable.selectionMode = 'multiple'; dataTable.data = new ObjectDataTableAdapter( [ { name: '1' }, { name: '2' } ], [new ObjectDataColumn({ key: 'name' })] ); const rows = dataTable.data.getRows(); const event = new KeyboardEvent('enter', { metaKey: true }); dataTable.ngOnChanges({}); dataTable.onEnterKeyPressed(rows[0], event); dataTable.onEnterKeyPressed(rows[1], event); expect(rows[0].isSelected).toBeTruthy(); expect(rows[1].isSelected).toBeTruthy(); }); it('should NOT unselect the row with [single] selection mode', (done) => { dataTable.selectionMode = 'single'; dataTable.data = new ObjectDataTableAdapter( [ { name: '1', isSelected: true }, { name: '2' } ], [new ObjectDataColumn({ key: 'name' })] ); const rows = dataTable.data.getRows(); dataTable.ngOnChanges({}); fixture.detectChanges(); dataTable.rowClick.subscribe(() => { expect(rows[0].isSelected).toBeTruthy(); expect(rows[1].isSelected).toBeFalsy(); done(); }); dataTable.onRowClick(rows[0], null); }); it('should unselect the row with [multiple] selection mode and modifier key', (done) => { dataTable.selectionMode = 'multiple'; dataTable.data = new ObjectDataTableAdapter( [{ name: '1', isSelected: true }], [new ObjectDataColumn({ key: 'name' })] ); const rows = dataTable.data.getRows(); rows[0].isSelected = true; dataTable.ngOnChanges({}); fixture.detectChanges(); dataTable.rowClick.subscribe(() => { expect(rows[0].isSelected).toBeFalsy(); done(); }); dataTable.onRowClick(rows[0], { metaKey: true, preventDefault() { } }); }); it('should select multiple rows with [multiple] selection mode and modifier key', (done) => { dataTable.selectionMode = 'multiple'; dataTable.data = new ObjectDataTableAdapter( [ { name: '1', isSelected: true }, { name: '2' } ], [new ObjectDataColumn({ key: 'name' })] ); const rows = dataTable.data.getRows(); rows[0].isSelected = true; const event = new MouseEvent('click', { metaKey: true }); dataTable.selection.push(rows[0]); dataTable.ngOnChanges({}); fixture.detectChanges(); dataTable.rowClick.subscribe(() => { expect(rows[0].isSelected).toBeTruthy(); expect(rows[1].isSelected).toBeTruthy(); done(); }); dataTable.onRowClick(rows[1], event); }); it('should put actions menu to the right by default', () => { dataTable.data = new ObjectDataTableAdapter( [ { name: '1' }, { name: '2' }, { name: '3' }, { name: '4' } ], [new ObjectDataColumn({ key: 'name' })] ); dataTable.actions = true; fixture.detectChanges(); const actions = element.querySelectorAll('[id^=action_menu_right]'); expect(actions.length).toBe(4); }); it('should put actions menu to the left', () => { dataTable.data = new ObjectDataTableAdapter( [ { name: '1' }, { name: '2' }, { name: '3' }, { name: '4' } ], [new ObjectDataColumn({ key: 'name' })] ); dataTable.actions = true; dataTable.actionsPosition = 'left'; fixture.detectChanges(); const actions = element.querySelectorAll('[id^=action_menu_left]'); expect(actions.length).toBe(4); }); it('should show only visible actions', () => { const unfilteredActions = [ { title: 'action1', name: 'view1', visible: true }, { title: 'action2', name: 'view2', visible: false }, { title: 'action3', name: 'view3', visible: null }, { title: 'action4', name: 'view4' } ]; const actions = dataTable.getVisibleActions(unfilteredActions); expect(actions.length).toBe(2); expect(actions[0].title).toBe('action1'); expect(actions[1].title).toBe('action4'); }); it('should initialize default adapter', () => { const table = new DataTableComponent(null, null); expect(table.data).toBeUndefined(); table.ngOnChanges({ 'data': new SimpleChange('123', {}, true) }); expect(table.data).toEqual(jasmine.any(ObjectDataTableAdapter)); }); it('should initialize with custom data', () => { const data = new ObjectDataTableAdapter([], []); dataTable.data = data; dataTable.ngAfterContentInit(); expect(dataTable.data).toBe(data); }); it('should emit row click event', (done) => { const row = {}; dataTable.data = new ObjectDataTableAdapter([], []); dataTable.rowClick.subscribe((e) => { expect(e.value).toBe(row); done(); }); dataTable.ngOnChanges({}); fixture.detectChanges(); dataTable.onRowClick(row, null); }); it('should emit double click if there are two single click in 250ms', (done) => { const row = {}; dataTable.data = new ObjectDataTableAdapter([], []); dataTable.ngOnChanges({}); fixture.detectChanges(); dataTable.rowDblClick.subscribe(() => { done(); }); dataTable.onRowClick(row, null); setTimeout(() => { dataTable.onRowClick(row, null); } , 240); }); it('should emit double click if there are more than two single click in 250ms', (done) => { const row = {}; dataTable.data = new ObjectDataTableAdapter([], []); dataTable.ngOnChanges({}); fixture.detectChanges(); dataTable.rowDblClick.subscribe(() => { done(); }); dataTable.onRowClick(row, null); setTimeout(() => { dataTable.onRowClick(row, null); dataTable.onRowClick(row, null); } , 240); }); it('should emit single click if there are two single click in more than 250ms', (done) => { const row = {}; let clickCount = 0; dataTable.data = new ObjectDataTableAdapter([], []); dataTable.ngOnChanges({}); fixture.detectChanges(); dataTable.rowClick.subscribe(() => { clickCount += 1; if (clickCount === 2) { done(); } }); dataTable.onRowClick(row, null); setTimeout(() => { dataTable.onRowClick(row, null); } , 260); }); it('should emit row-click dom event', (done) => { const row = {}; dataTable.data = new ObjectDataTableAdapter([], []); fixture.nativeElement.addEventListener('row-click', (e) => { expect(e.detail.value).toBe(row); done(); }); dataTable.ngOnChanges({}); fixture.detectChanges(); dataTable.onRowClick(row, null); }); it('should emit row-dblclick dom event', (done) => { const row = {}; dataTable.data = new ObjectDataTableAdapter([], []); fixture.nativeElement.addEventListener('row-dblclick', (e) => { expect(e.detail.value).toBe(row); done(); }); dataTable.ngOnChanges({}); fixture.detectChanges(); dataTable.onRowClick(row, null); dataTable.onRowClick(row, null); }); it('should prevent default behaviour on row click event', () => { const e = jasmine.createSpyObj('event', ['preventDefault']); dataTable.ngAfterContentInit(); dataTable.onRowClick(null, e); expect(e.preventDefault).toHaveBeenCalled(); }); it('should prevent default behaviour on row double-click event', () => { const e = jasmine.createSpyObj('event', ['preventDefault']); dataTable.ngOnChanges({}); dataTable.ngAfterContentInit(); dataTable.onRowDblClick(null, e); expect(e.preventDefault).toHaveBeenCalled(); }); it('should not sort if column is missing', () => { dataTable.ngOnChanges({ 'data': new SimpleChange('123', {}, true) }); fixture.detectChanges(); dataTable.ngAfterViewInit(); const adapter = dataTable.data; spyOn(adapter, 'setSorting').and.callThrough(); dataTable.onColumnHeaderClick(null); expect(adapter.setSorting).not.toHaveBeenCalled(); }); it('should not sort upon clicking non-sortable column header', () => { dataTable.ngOnChanges({ 'data': new SimpleChange('123', {}, true) }); fixture.detectChanges(); dataTable.ngAfterViewInit(); const adapter = dataTable.data; spyOn(adapter, 'setSorting').and.callThrough(); const column = new ObjectDataColumn({ key: 'column_1' }); dataTable.onColumnHeaderClick(column); expect(adapter.setSorting).not.toHaveBeenCalled(); }); it('should set sorting upon column header clicked', () => { dataTable.ngOnChanges({ 'data': new SimpleChange('123', {}, true) }); fixture.detectChanges(); dataTable.ngAfterViewInit(); const adapter = dataTable.data; spyOn(adapter, 'setSorting').and.callThrough(); const column = new ObjectDataColumn({ key: 'column_1', sortable: true }); dataTable.onColumnHeaderClick(column); expect(adapter.setSorting).toHaveBeenCalledWith( jasmine.objectContaining({ key: 'column_1', direction: 'asc' }) ); }); it('should invert sorting upon column header clicked', () => { dataTable.ngOnChanges({ 'data': new SimpleChange('123', {}, true) }); fixture.detectChanges(); dataTable.ngAfterViewInit(); const adapter = dataTable.data; const sorting = new DataSorting('column_1', 'asc'); spyOn(adapter, 'setSorting').and.callThrough(); spyOn(adapter, 'getSorting').and.returnValue(sorting); const column = new ObjectDataColumn({ key: 'column_1', sortable: true }); // check first click on the header dataTable.onColumnHeaderClick(column); expect(adapter.setSorting).toHaveBeenCalledWith( jasmine.objectContaining({ key: 'column_1', direction: 'desc' }) ); // check second click on the header sorting.direction = 'desc'; dataTable.onColumnHeaderClick(column); expect(adapter.setSorting).toHaveBeenCalledWith( jasmine.objectContaining({ key: 'column_1', direction: 'asc' }) ); }); it('should indicate column that has sorting applied', () => { dataTable.data = new ObjectDataTableAdapter( [{ name: '1' }, { name: '2' }], [ new ObjectDataColumn({ key: 'name', sortable: true }), new ObjectDataColumn({ key: 'other', sortable: true }) ] ); fixture.detectChanges(); dataTable.ngAfterViewInit(); const [col1, col2] = dataTable.getSortableColumns(); dataTable.onColumnHeaderClick(col2); expect(dataTable.isColumnSortActive(col1)).toBe(false); expect(dataTable.isColumnSortActive(col2)).toBe(true); }); it('should return false for columns that have no sorting', () => { dataTable.data = new ObjectDataTableAdapter( [{ name: '1' }, { name: '2' }], [ new ObjectDataColumn({ key: 'name', sortable: false }), new ObjectDataColumn({ key: 'other', sortable: false }) ] ); const [col1, col2] = dataTable.getSortableColumns(); expect(dataTable.isColumnSortActive(col1)).toBe(false); expect(dataTable.isColumnSortActive(col2)).toBe(false); }); it('should invert "select all" status', () => { expect(dataTable.isSelectAllChecked).toBeFalsy(); dataTable.onSelectAllClick( { checked: true }); expect(dataTable.isSelectAllChecked).toBeTruthy(); dataTable.onSelectAllClick( { checked: false }); expect(dataTable.isSelectAllChecked).toBeFalsy(); }); it('should reset selection upon data rows change', () => { const data = new ObjectDataTableAdapter([{}, {}, {}], []); dataTable.data = data; dataTable.multiselect = true; dataTable.ngAfterContentInit(); dataTable.onSelectAllClick( { checked: true }); expect(dataTable.selection.every((entry) => entry.isSelected)); data.setRows([]); fixture.detectChanges(); expect(dataTable.selection.every((entry) => !entry.isSelected)); }); it('should update rows on "select all" click', () => { const data = new ObjectDataTableAdapter([{}, {}, {}], []); const rows = data.getRows(); dataTable.data = data; dataTable.multiselect = true; dataTable.ngAfterContentInit(); dataTable.onSelectAllClick( { checked: true }); expect(dataTable.isSelectAllChecked).toBe(true); for (let i = 0; i < rows.length; i++) { expect(rows[i].isSelected).toBe(true); } dataTable.onSelectAllClick( { checked: false }); expect(dataTable.isSelectAllChecked).toBe(false); for (let i = 0; i < rows.length; i++) { expect(rows[i].isSelected).toBe(false); } }); it('should allow "select all" calls with no rows', () => { dataTable.multiselect = true; dataTable.ngOnChanges({ 'data': new SimpleChange('123', {}, true) }); dataTable.onSelectAllClick( { checked: true }); expect(dataTable.isSelectAllChecked).toBe(true); }); it('should have indeterminate state for "select all" when at least 1 row is selected or not all rows', () => { dataTable.data = new ObjectDataTableAdapter( [{ name: '1' }, { name: '2' }], [ new ObjectDataColumn({ key: 'name', sortable: false }), new ObjectDataColumn({ key: 'other', sortable: false }) ] ); const rows = dataTable.data.getRows(); dataTable.multiselect = true; dataTable.onCheckboxChange(rows[0], { checked: true }); expect(dataTable.isSelectAllIndeterminate).toBe(true); expect(dataTable.isSelectAllChecked).toBe(false); dataTable.onCheckboxChange(rows[1], { checked: true }); expect(dataTable.isSelectAllIndeterminate).toBe(false); expect(dataTable.isSelectAllChecked).toBe(true); dataTable.onCheckboxChange(rows[0], { checked: false }); dataTable.onCheckboxChange(rows[1], { checked: false }); expect(dataTable.isSelectAllIndeterminate).toBe(false); expect(dataTable.isSelectAllChecked).toBe(false); }); it('should allow select row when multi-select enabled', () => { const data = new ObjectDataTableAdapter([{}, {}], []); const rows = data.getRows(); dataTable.multiselect = true; dataTable.ngOnChanges({ 'data': new SimpleChange('123', data, true) }); expect(rows[0].isSelected).toBe(false); expect(rows[1].isSelected).toBe(false); dataTable.onCheckboxChange(rows[1], { checked: true }); expect(rows[0].isSelected).toBe(false); expect(rows[1].isSelected).toBe(true); dataTable.onCheckboxChange(rows[0], { checked: true }); expect(rows[0].isSelected).toBe(true); expect(rows[1].isSelected).toBe(true); }); it('should require multiselect option to toggle row state', () => { const data = new ObjectDataTableAdapter([{}, {}, {}], []); const rows = data.getRows(); dataTable.data = data; dataTable.multiselect = false; dataTable.ngAfterContentInit(); dataTable.onSelectAllClick( { checked: true }); expect(dataTable.isSelectAllChecked).toBe(true); for (let i = 0; i < rows.length; i++) { expect(rows[i].isSelected).toBe(false); } }); it('should require row and column for icon value check', () => { expect(dataTable.isIconValue(null, null)).toBeFalsy(); expect(dataTable.isIconValue( {}, null)).toBeFalsy(); expect(dataTable.isIconValue(null, {})).toBeFalsy(); }); it('should use special material url scheme', () => { const column = {}; const row: any = { getValue: function () { return 'material-icons://android'; } }; expect(dataTable.isIconValue(row, column)).toBeTruthy(); }); it('should not use special material url scheme', () => { const column = {}; const row: any = { getValue: function () { return 'http://www.google.com'; } }; expect(dataTable.isIconValue(row, column)).toBeFalsy(); }); it('should parse icon value', () => { const column = {}; const row: any = { getValue: function () { return 'material-icons://android'; } }; expect(dataTable.asIconValue(row, column)).toBe('android'); }); it('should not parse icon value', () => { const column = {}; const row: any = { getValue: function () { return 'http://www.google.com'; } }; expect(dataTable.asIconValue(row, column)).toBe(null); }); it('should parse icon values to a valid i18n key', () => { expect(dataTable.iconAltTextKey('custom')).toBe('ICONS.custom'); expect(dataTable.iconAltTextKey('/path/to/custom')).toBe('ICONS.custom'); expect(dataTable.iconAltTextKey('/path/to/custom.svg')).toBe('ICONS.custom'); }); it('should require column and direction to evaluate sorting state', () => { expect(dataTable.isColumnSorted(null, null)).toBeFalsy(); expect(dataTable.isColumnSorted( {}, null)).toBeFalsy(); expect(dataTable.isColumnSorted(null, 'asc')).toBeFalsy(); }); it('should require adapter sorting to evaluate sorting state', () => { dataTable.ngOnChanges({ 'data': new SimpleChange('123', {}, true) }); spyOn(dataTable.data, 'getSorting').and.returnValue(null); expect(dataTable.isColumnSorted( {}, 'asc')).toBeFalsy(); }); it('should evaluate column sorting state', () => { dataTable.ngOnChanges({ 'data': new SimpleChange('123', {}, true) }); spyOn(dataTable.data, 'getSorting').and.returnValue(new DataSorting('column_1', 'asc')); expect(dataTable.isColumnSorted( { key: 'column_1' }, 'asc')).toBeTruthy(); expect(dataTable.isColumnSorted( { key: 'column_2' }, 'desc')).toBeFalsy(); }); it('should replace image source with fallback thumbnail on error', () => { const event = { target: { src: 'missing-image' } }; const row = new FakeDataRow(); dataTable.fallbackThumbnail = ''; dataTable.onImageLoadingError(event, row); expect(event.target.src).toBe(dataTable.fallbackThumbnail); }); it('should replace image source with miscellaneous icon when fallback is not available', () => { const originalSrc = 'missing-image'; const event = { target: { src: originalSrc } }; const row = new FakeDataRow(); dataTable.fallbackThumbnail = null; dataTable.onImageLoadingError(event, row); expect(event.target.src).toBe('./assets/images/ft_ic_miscellaneous.svg'); }); it('should not get cell tooltip when row is not provided', () => { const col = { key: 'name', type: 'text' }; expect(dataTable.getCellTooltip(null, col)).toBeNull(); }); it('should not get cell tooltip when column is not provided', () => { const row = {}; expect(dataTable.getCellTooltip(row, null)).toBeNull(); }); it('should not get cell tooltip when formatter is not provided', () => { const col = { key: 'name', type: 'text' }; const row = {}; expect(dataTable.getCellTooltip(row, col)).toBeNull(); }); it('should use formatter function to generate tooltip', () => { const tooltip = 'tooltip value'; const col = { key: 'name', type: 'text', formatTooltip: () => tooltip }; const row = {}; expect(dataTable.getCellTooltip(row, col)).toBe(tooltip); }); it('should return null value from the tooltip formatter', () => { const col = { key: 'name', type: 'text', formatTooltip: () => null }; const row = {}; expect(dataTable.getCellTooltip(row, col)).toBeNull(); }); it('should reset the menu cache after rows change', () => { let emitted = 0; dataTable.showRowActionsMenu.subscribe(() => { emitted++; }); const column = {}; const row: any = { getValue: function () { return 'id'; } }; dataTable.getRowActions(row, column); dataTable.ngOnChanges({ 'data': new SimpleChange('123', {}, true) }); dataTable.getRowActions(row, column); expect(emitted).toBe(2); }); it('should enable sticky header if the stickyHeader is set to true and header is visible', () => { dataTable.data = new ObjectDataTableAdapter( [ { name: '1' }, { name: '2' }, { name: '3' }, { name: '4' } ], [new ObjectDataColumn({ key: 'name', title: 'Name' })] ); dataTable.stickyHeader = true; dataTable.loading = false; dataTable.noPermission = false; fixture.detectChanges(); expect(element.querySelector('.adf-sticky-header')).not.toBeNull(); }); it('should disable sticky header if component is loading', () => { dataTable.data = new ObjectDataTableAdapter( [ { name: '1' }, { name: '2' }, { name: '3' }, { name: '4' } ], [new ObjectDataColumn({ key: 'name', title: 'Name' })] ); dataTable.stickyHeader = true; dataTable.loading = true; dataTable.noPermission = false; fixture.detectChanges(); expect(element.querySelector('.adf-sticky-header')).toBeNull(); }); it('should disable sticky header if user has no permissions', () => { dataTable.data = new ObjectDataTableAdapter( [ { name: '1' }, { name: '2' }, { name: '3' }, { name: '4' } ], [new ObjectDataColumn({ key: 'name', title: 'Name' })] ); dataTable.stickyHeader = true; dataTable.loading = false; dataTable.noPermission = true; fixture.detectChanges(); expect(element.querySelector('.adf-sticky-header')).toBeNull(); }); it('should disable sticky header if user has no content', () => { dataTable.data = new ObjectDataTableAdapter( [], [new ObjectDataColumn({ key: 'name', title: 'Name' })] ); dataTable.stickyHeader = true; dataTable.loading = false; dataTable.noPermission = false; fixture.detectChanges(); expect(element.querySelector('.adf-sticky-header')).toBeNull(); }); it('should be able to define values using the resolver function', () => { dataTable.data = new ObjectDataTableAdapter([ { id: 1, firstName: 'foo', lastName: 'bar' }, { id: 2, firstName: 'bar', lastName: 'baz' }], [new ObjectDataColumn({ key: 'id' }), new ObjectDataColumn({ key: 'name' })] ); spyOn(dataTable, 'resolverFn').and.callFake(resolverFn); fixture.detectChanges(); const id1 = element.querySelector('[data-automation-id="text_1'); const id2 = element.querySelector('[data-automation-id="text_2'); const names = element.querySelectorAll('[data-automation-id="text_undefined"]'); expect(id1.innerText).toEqual('1'); expect(names[0].innerText).toEqual('foo - bar'); expect(id2.innerText).toEqual('2'); expect(names[1].innerText).toEqual('bar - baz'); expect(dataTable.data.getRows().length).toEqual(2); expect(dataTable.resolverFn).toHaveBeenCalledTimes(4); }); it('should update data columns when columns input changes', () => { const existingDataColumnsSchema = [new ObjectDataColumn({ key: 'id' })]; const existingData = [{ id: 'fake-data' }]; dataTable.data = new ObjectDataTableAdapter( existingData, existingDataColumnsSchema ); const newDataColumnsSchema = { key: 'new-column'}; const columnsChange = new SimpleChange(null, [newDataColumnsSchema], false); dataTable.ngOnChanges({ 'columns': columnsChange }); const expectedNewDataColumns = [new ObjectDataColumn(newDataColumnsSchema)]; expect(dataTable.data.getColumns()).toEqual(expectedNewDataColumns); }); }); describe('Accesibility', () => { let fixture: ComponentFixture; let dataTable: DataTableComponent; let element: any; let columnCustomTemplate: TemplateRef; setupTestBed({ imports: [ TranslateModule.forRoot(), CoreTestingModule ], declarations: [CustomColumnTemplateComponent], schemas: [NO_ERRORS_SCHEMA] }); beforeEach(() => { columnCustomTemplate = TestBed.createComponent(CustomColumnTemplateComponent).componentInstance.templateRef; fixture = TestBed.createComponent(DataTableComponent); dataTable = fixture.componentInstance; element = fixture.debugElement.nativeElement; }); afterEach(() => { fixture.destroy(); }); it('should have accessibility tags', () => { const dataRows = [ { name: 'test1' }, { name: 'test2' }, { name: 'test3' }, { name: 'test4' } ]; dataTable.data = new ObjectDataTableAdapter([], [new ObjectDataColumn({ key: 'name' })] ); dataTable.ngOnChanges({ rows: new SimpleChange(null, dataRows, false) }); fixture.detectChanges(); const datatableAttributes = element.querySelector('.adf-datatable-list').attributes; const datatableHeaderAttributes = element.querySelector('.adf-datatable-list .adf-datatable-header').attributes; const datatableHeaderCellAttributes = element.querySelector('.adf-datatable-cell-header').attributes; const datatableBodyAttributes = element.querySelector('.adf-datatable-body').attributes; const datatableBodyRowAttributes = element.querySelector('.adf-datatable-body .adf-datatable-row').attributes; const datatableBodyCellAttributes = element.querySelector('.adf-datatable-body .adf-datatable-cell').attributes; expect(datatableAttributes.getNamedItem('role').value).toEqual('grid'); expect(datatableHeaderAttributes.getNamedItem('role').value).toEqual('rowgroup'); expect(datatableHeaderCellAttributes.getNamedItem('role').value).toEqual('columnheader'); expect(datatableBodyAttributes.getNamedItem('role').value).toEqual('rowgroup'); expect(datatableBodyRowAttributes.getNamedItem('role').value).toEqual('row'); expect(datatableBodyCellAttributes.getNamedItem('role').value).toEqual('gridcell'); }); describe('aria-sort', () => { let column: DataColumn; beforeEach(() => { column = new ObjectDataColumn({ key: 'key' }); }); it('should return correct translation key when no sort is applied', () => { spyOn(dataTable, 'isColumnSortActive').and.returnValue(false); expect(dataTable.getAriaSort(column)).toBe('ADF-DATATABLE.ACCESSIBILITY.SORT_NONE'); }); it('should return translation key when column sort is ascending', () => { const isColumnSortedAsc = true; spyOn(dataTable, 'isColumnSortActive').and.returnValue(true); spyOn(dataTable, 'isColumnSorted').and.returnValue(isColumnSortedAsc); expect(dataTable.getAriaSort(column)).toBe('ADF-DATATABLE.ACCESSIBILITY.SORT_ASCENDING'); }); it('should return translation key when column sort is descending', () => { const isColumnSortedAsc = false; spyOn(dataTable, 'isColumnSortActive').and.returnValue(true); spyOn(dataTable, 'isColumnSorted').and.returnValue(isColumnSortedAsc); expect(dataTable.getAriaSort(column)).toBe('ADF-DATATABLE.ACCESSIBILITY.SORT_DESCENDING'); }); }); it('should focus next row on ArrowDown event', () => { const event = new KeyboardEvent('keyup', { code: 'ArrowDown', key: 'ArrowDown', keyCode: 40 } as KeyboardEventInit ); const dataRows = [ { name: 'test1'}, { name: 'test2' } ]; dataTable.data = new ObjectDataTableAdapter([], [new ObjectDataColumn({ key: 'name' })] ); dataTable.ngOnChanges({ rows: new SimpleChange(null, dataRows, false) }); fixture.detectChanges(); dataTable.ngAfterViewInit(); const rowElement = document.querySelectorAll('.adf-datatable-body .adf-datatable-row')[0]; const rowCellElement = rowElement.querySelector('.adf-datatable-cell'); rowCellElement.dispatchEvent(new MouseEvent('click')); fixture.debugElement.nativeElement.dispatchEvent(event); expect(document.activeElement.getAttribute('data-automation-id')).toBe('datatable-row-1'); }); it('should focus previous row on ArrowUp event', () => { const event = new KeyboardEvent('keyup', { code: 'ArrowUp', key: 'ArrowUp', keyCode: 38 } as KeyboardEventInit ); const dataRows = [ { name: 'test1'}, { name: 'test2' } ]; dataTable.data = new ObjectDataTableAdapter([], [new ObjectDataColumn({ key: 'name' })] ); dataTable.ngOnChanges({ rows: new SimpleChange(null, dataRows, false) }); fixture.detectChanges(); dataTable.ngAfterViewInit(); const rowElement = document.querySelectorAll('.adf-datatable-body .adf-datatable-row')[1]; const rowCellElement = rowElement.querySelector('.adf-datatable-cell'); rowCellElement.dispatchEvent(new MouseEvent('click')); fixture.debugElement.nativeElement.dispatchEvent(event); expect(document.activeElement.getAttribute('data-automation-id')).toBe('datatable-row-0'); }); it('should select header row when `showHeader` is `Always`', () => { const event = new KeyboardEvent('keyup', { code: 'ArrowUp', key: 'ArrowUp', keyCode: 38 } as KeyboardEventInit ); const dataRows = [ { name: 'test1'}, { name: 'test2' } ]; dataTable.data = new ObjectDataTableAdapter([], [new ObjectDataColumn({ key: 'name' })] ); dataTable.showHeader = ShowHeaderMode.Always; dataTable.ngOnChanges({ rows: new SimpleChange(null, dataRows, false) }); fixture.detectChanges(); dataTable.ngAfterViewInit(); const rowElement = document.querySelector('.adf-datatable-row[data-automation-id="datatable-row-0"]'); const rowCellElement = rowElement.querySelector('.adf-datatable-cell'); rowCellElement.dispatchEvent(new MouseEvent('click')); fixture.debugElement.nativeElement.dispatchEvent(event); expect(document.activeElement.getAttribute('data-automation-id')).toBe('datatable-row-header'); }); it('should not select header row when `showHeader` is `Never`', () => { const event = new KeyboardEvent('keyup', { code: 'ArrowUp', key: 'ArrowUp', keyCode: 38 } as KeyboardEventInit ); const dataRows = [ { name: 'test1'}, { name: 'test2' } ]; dataTable.data = new ObjectDataTableAdapter([], [new ObjectDataColumn({ key: 'name' })] ); dataTable.showHeader = ShowHeaderMode.Never; dataTable.ngOnChanges({ rows: new SimpleChange(null, dataRows, false) }); fixture.detectChanges(); dataTable.ngAfterViewInit(); const rowElement = document.querySelector('.adf-datatable-row[data-automation-id="datatable-row-0"]'); const rowCellElement = rowElement.querySelector('.adf-datatable-cell'); rowCellElement.dispatchEvent(new MouseEvent('click')); fixture.debugElement.nativeElement.dispatchEvent(event); expect(document.activeElement.getAttribute('data-automation-id')).toBe('datatable-row-1'); }); it('should remove cell focus when [focus] is set to false', () => { dataTable.showHeader = ShowHeaderMode.Never; const dataRows = [ { name: 'name1' } ]; dataTable.data = new ObjectDataTableAdapter([], [new ObjectDataColumn({ key: 'name', template: columnCustomTemplate, focus: false })] ); dataTable.ngOnChanges({ rows: new SimpleChange(null, dataRows, false) }); fixture.detectChanges(); dataTable.ngAfterViewInit(); const cell = document.querySelector('.adf-datatable-row[data-automation-id="datatable-row-0"] .adf-cell-value'); expect(cell.getAttribute('tabindex')).toBe(null); }); it('should allow element focus when [focus] is set to true', () => { dataTable.showHeader = ShowHeaderMode.Never; const dataRows = [ { name: 'name1' } ]; dataTable.data = new ObjectDataTableAdapter([], [new ObjectDataColumn({ key: 'name', template: columnCustomTemplate, focus: true })] ); dataTable.ngOnChanges({ rows: new SimpleChange(null, dataRows, false) }); fixture.detectChanges(); dataTable.ngAfterViewInit(); const cell = document.querySelector('.adf-datatable-row[data-automation-id="datatable-row-0"] .adf-cell-value'); expect(cell.getAttribute('tabindex')).toBe('0'); }); });