diff --git a/projects/ngx-datatable/src/lib/components/header/header-cell.component.spec.ts b/projects/ngx-datatable/src/lib/components/header/header-cell.component.spec.ts index eb480c626..3c30917c4 100644 --- a/projects/ngx-datatable/src/lib/components/header/header-cell.component.spec.ts +++ b/projects/ngx-datatable/src/lib/components/header/header-cell.component.spec.ts @@ -1,70 +1,153 @@ -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { AfterViewInit, Component, TemplateRef, viewChild } from '@angular/core'; +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { TableColumnInternal } from '../../types/internal.types'; +import { + InnerSortEvent, + SortableTableColumnInternal, + TableColumnInternal +} from '../../types/internal.types'; +import { toInternalColumn } from '../../utils/column-helper'; import { DataTableHeaderCellComponent } from './header-cell.component'; +import { HeaderCellHarness } from './testing/header-cell.harnes'; describe('DataTableHeaderCellComponent', () => { let fixture: ComponentFixture; let component: DataTableHeaderCellComponent; + let harness: HeaderCellHarness; - beforeEach(waitForAsync(() => { + beforeEach(waitForAsync(async () => { fixture = TestBed.createComponent(DataTableHeaderCellComponent); component = fixture.componentInstance; - })); - - it('should emit new width on resize', fakeAsync(() => { fixture.componentRef.setInput('column', { name: 'test', - resizeable: true + prop: 'test', + resizeable: true, + sortable: true + }); + fixture.componentRef.setInput('sortType', 'single'); + fixture.componentRef.setInput('headerHeight', 50); + fixture.componentRef.setInput('ariaHeaderCheckboxMessage', 'Select all rows'); + fixture.componentInstance.sort.subscribe(sort => { + fixture.componentRef.setInput('sorts', [ + { + prop: sort.column.name, + dir: sort.newValue + } + ]); }); + harness = await TestbedHarnessEnvironment.harnessForFixture(fixture, HeaderCellHarness); + })); + + it('should emit new width on resize', async () => { spyOn(component.resizing, 'emit'); - fixture.detectChanges(); - const initialWidth = fixture.nativeElement.clientWidth; - const event = new MouseEvent('mousedown'); - fixture.nativeElement.querySelector('.resize-handle').dispatchEvent(event); - tick(); - const mouseMoveEvent = new MouseEvent('mousemove', { clientX: 100 }); - document.dispatchEvent(mouseMoveEvent); - const mouseUpEvent = new MouseEvent('mouseup'); - document.dispatchEvent(mouseUpEvent); + const initialWidth = await harness.cellWidth(); + await harness.resizeCell(0, 100); const newWidth = 100 + initialWidth; expect(component.resizing.emit).toHaveBeenCalledWith({ width: newWidth, - column: { name: 'test', resizeable: true } as TableColumnInternal + column: { + name: 'test', + prop: 'test', + resizeable: true, + sortable: true + } as TableColumnInternal }); - })); + }); - it('should emit sort event', () => { - fixture.componentRef.setInput('column', { - prop: 'test', - sortable: true - }); + it('should emit sort event', async () => { spyOn(component.sort, 'emit'); - fixture.detectChanges(); - const event = new MouseEvent('click'); - fixture.nativeElement.querySelector('.datatable-header-cell-label').dispatchEvent(event); + await harness.applySort(); expect(component.sort.emit).toHaveBeenCalled(); }); - it('should not render resize handle when showResizeHandle is false (last column)', () => { - fixture.componentRef.setInput('column', { - name: 'test', - resizeable: true - }); + it('should not render resize handle when showResizeHandle is false (last column)', async () => { fixture.componentRef.setInput('showResizeHandle', false); - fixture.detectChanges(); - const resizeHandle = fixture.nativeElement.querySelector('.resize-handle'); - expect(resizeHandle).toBeNull(); + expect(await harness.hasResizeHandle()).toBe(false); + }); + + it('should render resize handle when showResizeHandle is true', async () => { + fixture.componentRef.setInput('showResizeHandle', true); + expect(await harness.hasResizeHandle()).toBe(true); }); - it('should render resize handle when showResizeHandle is true', () => { + it('should emit select when checkbox is clicked', async () => { fixture.componentRef.setInput('column', { name: 'test', - resizeable: true + headerCheckboxable: true }); - fixture.componentRef.setInput('showResizeHandle', true); + spyOn(component.select, 'emit'); + await harness.selectAllRows(); + expect(component.select.emit).toHaveBeenCalled(); + }); + + it('should toggle sort direction on sort button click', async () => { + await harness.applySort(); + expect(await harness.getSortDirection()).toBe('asc'); + await harness.applySort(); + expect(await harness.getSortDirection()).toBe('desc'); + }); + + it('should sort on enter key press', async () => { + spyOn(component.sort, 'emit'); + await harness.applySort(true); + expect(component.sort.emit).toHaveBeenCalled(); + }); +}); + +@Component({ + imports: [DataTableHeaderCellComponent], + template: ` + + Custom Header for {{ column.name }} + + ` +}) +class TestHeaderCellComponent implements AfterViewInit { + column: TableColumnInternal = toInternalColumn([ + { + name: 'test', + sortable: true + } + ])[0]; + + readonly headerCellTemplate = viewChild('headerCellTemplate', { read: TemplateRef }); + + sort(event: InnerSortEvent) {} + + ngAfterViewInit() { + this.column = { ...this.column, headerTemplate: this.headerCellTemplate() }; + } +} + +describe('DataTableHeaderCellComponent with template', () => { + let fixture: ComponentFixture; + let harness: HeaderCellHarness; + + beforeEach(waitForAsync(async () => { + fixture = TestBed.createComponent(TestHeaderCellComponent); + harness = await TestbedHarnessEnvironment.harnessForFixture(fixture, HeaderCellHarness); + })); + + it('should render custom header template', async () => { fixture.detectChanges(); - const resizeHandle = fixture.nativeElement.querySelector('.resize-handle'); - expect(resizeHandle).not.toBeNull(); + expect(await harness.getHeaderCellText()).toContain('Custom Header for test'); + }); + + it('should call sort function on custom button click', async () => { + spyOn(fixture.componentInstance, 'sort'); + await harness.clickCustomSortButton(); + expect(fixture.componentInstance.sort).toHaveBeenCalledWith({ + column: fixture.componentInstance.column as SortableTableColumnInternal, + prevValue: undefined, + newValue: 'asc' + }); }); }); diff --git a/projects/ngx-datatable/src/lib/components/header/testing/header-cell.harnes.ts b/projects/ngx-datatable/src/lib/components/header/testing/header-cell.harnes.ts new file mode 100644 index 000000000..083a6f92d --- /dev/null +++ b/projects/ngx-datatable/src/lib/components/header/testing/header-cell.harnes.ts @@ -0,0 +1,76 @@ +import { ComponentHarness } from '@angular/cdk/testing'; + +export class HeaderCellHarness extends ComponentHarness { + static readonly hostSelector = 'datatable-header-cell'; + + private cellLabel = this.locatorForOptional('.datatable-header-cell-label'); + private cellWrapper = this.locatorFor('.datatable-header-cell-template-wrap'); + private cellResizeHandle = this.locatorForOptional('.resize-handle'); + private cellCheckbox = this.locatorForOptional('.datatable-checkbox'); + private sortBtn = this.locatorForOptional('.sort-btn'); + private customSortButton = this.locatorForOptional('.custom-sort-button'); + + async getHeaderCellText(): Promise { + const label = await this.cellLabel(); + const wrapperText = await (await this.cellWrapper()).text(); + return label?.text() ?? wrapperText; + } + + async hasResizeHandle(): Promise { + const resizeHandle = await this.cellResizeHandle(); + return !!resizeHandle; + } + + async resizeCell(startPosX: number, pixelToResize: number): Promise { + const resizeHandle = await this.cellResizeHandle(); + if (resizeHandle) { + await resizeHandle.dispatchEvent('mousedown', { clientX: startPosX, screenX: startPosX }); + const mouseMove = new MouseEvent('mousemove', { + clientX: startPosX + pixelToResize, + screenX: startPosX + pixelToResize + }); + document.dispatchEvent(mouseMove); + const mouseUp = new MouseEvent('mouseup'); + document.dispatchEvent(mouseUp); + } + } + + async selectAllRows(): Promise { + const checkbox = await this.cellCheckbox(); + if (checkbox && !(await checkbox.getProperty('checked'))) { + await checkbox.click(); + } + } + + async applySort(withKeyboard = false): Promise { + const sortButton = await this.sortBtn(); + if (sortButton && !withKeyboard) { + await sortButton.click(); + } else { + (await this.host()).dispatchEvent('keydown', { + key: 'Enter' + }); + } + } + + async getSortDirection(): Promise { + const sortButton = await this.sortBtn(); + if (sortButton) { + const isAscending = await sortButton.hasClass('sort-asc'); + const isDescending = await sortButton.hasClass('sort-desc'); + return isAscending ? 'asc' : isDescending ? 'desc' : undefined; + } + return undefined; + } + + async cellWidth(): Promise { + const hostElement = await this.host(); + const width = await hostElement.getProperty('offsetWidth'); + return width ?? 0; + } + + async clickCustomSortButton(): Promise { + const button = await this.customSortButton(); + await button!.click(); + } +} diff --git a/projects/ngx-datatable/src/lib/utils/events.ts b/projects/ngx-datatable/src/lib/utils/events.ts index b283a0a11..fb4efef9c 100644 --- a/projects/ngx-datatable/src/lib/utils/events.ts +++ b/projects/ngx-datatable/src/lib/utils/events.ts @@ -12,5 +12,10 @@ export const getPositionFromEvent = ( screenX: number; screenY: number; } => { - return event instanceof MouseEvent ? (event as MouseEvent) : (event.changedTouches[0] as Touch); + return event instanceof MouseEvent || + event.type === 'mousedown' || + event.type === 'mouseup' || + event.type === 'mousemove' + ? (event as MouseEvent) + : (event.changedTouches[0] as Touch); };