Skip to content

Commit 3140223

Browse files
committed
test(cdk/table): add tests for virtual scrolling
Sets up some tests for the virtual scrolling logic.
1 parent 97d497f commit 3140223

File tree

2 files changed

+173
-9
lines changed

2 files changed

+173
-9
lines changed

src/cdk/table/BUILD.bazel

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ ng_project(
4848
"//:node_modules/rxjs",
4949
"//src/cdk/bidi",
5050
"//src/cdk/collections",
51+
"//src/cdk/scrolling",
52+
"//src/cdk/testing/private",
5153
],
5254
)
5355

src/cdk/table/table.spec.ts

Lines changed: 171 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,17 @@ import {
1313
Type,
1414
ViewChild,
1515
inject,
16+
signal,
1617
} from '@angular/core';
1718
import {By} from '@angular/platform-browser';
18-
import {ComponentFixture, TestBed, fakeAsync, flush, waitForAsync} from '@angular/core/testing';
19+
import {
20+
ComponentFixture,
21+
TestBed,
22+
fakeAsync,
23+
flush,
24+
tick,
25+
waitForAsync,
26+
} from '@angular/core/testing';
1927
import {BehaviorSubject, Observable, combineLatest, of as observableOf} from 'rxjs';
2028
import {map} from 'rxjs/operators';
2129
import {CdkColumnDef} from './cell';
@@ -36,6 +44,8 @@ import {
3644
getTableUnknownDataSourceError,
3745
} from './table-errors';
3846
import {NgClass} from '@angular/common';
47+
import {CdkVirtualScrollViewport, ScrollingModule} from '../scrolling';
48+
import {dispatchFakeEvent} from '../testing/private';
3949

4050
describe('CdkTable', () => {
4151
let fixture: ComponentFixture<any>;
@@ -1995,6 +2005,107 @@ describe('CdkTable', () => {
19952005
expect(noDataRow).toBeTruthy();
19962006
expect(noDataRow.getAttribute('colspan')).toEqual('3');
19972007
});
2008+
2009+
describe('virtual scrolling', () => {
2010+
let fixture: ComponentFixture<TableWithVirtualScroll>;
2011+
let table: HTMLTableElement;
2012+
2013+
beforeEach(fakeAsync(() => {
2014+
fixture = TestBed.createComponent(TableWithVirtualScroll);
2015+
2016+
// Init logic copied from the virtual scroll tests.
2017+
fixture.detectChanges();
2018+
flush();
2019+
fixture.detectChanges();
2020+
flush();
2021+
tick(16);
2022+
flush();
2023+
fixture.detectChanges();
2024+
table = fixture.nativeElement.querySelector('table');
2025+
}));
2026+
2027+
function triggerScroll(offset: number) {
2028+
const viewport = fixture.componentInstance.viewport;
2029+
viewport.scrollToOffset(offset);
2030+
dispatchFakeEvent(viewport.scrollable!.getElementRef().nativeElement, 'scroll');
2031+
tick(16);
2032+
}
2033+
2034+
it('should not render the full data set when using virtual scrolling', fakeAsync(() => {
2035+
expect(fixture.componentInstance.dataSource.data.length).toBeGreaterThan(2000);
2036+
expect(getRows(table).length).toBe(10);
2037+
}));
2038+
2039+
it('should maintain a limited amount of data as the user is scrolling', fakeAsync(() => {
2040+
expect(getRows(table).length).toBe(10);
2041+
2042+
triggerScroll(500);
2043+
expect(getRows(table).length).toBe(13);
2044+
2045+
triggerScroll(500);
2046+
expect(getRows(table).length).toBe(13);
2047+
2048+
triggerScroll(1000);
2049+
expect(getRows(table).length).toBe(12);
2050+
}));
2051+
2052+
it('should update the table data as the user is scrolling', fakeAsync(() => {
2053+
expectTableToMatchContent(table, [
2054+
['Column A', 'Column B', 'Column C'],
2055+
['a_1', 'b_1', 'c_1'],
2056+
['a_2', 'b_2', 'c_2'],
2057+
['a_3', 'b_3', 'c_3'],
2058+
['a_4', 'b_4', 'c_4'],
2059+
['a_5', 'b_5', 'c_5'],
2060+
['a_6', 'b_6', 'c_6'],
2061+
['a_7', 'b_7', 'c_7'],
2062+
['a_8', 'b_8', 'c_8'],
2063+
['a_9', 'b_9', 'c_9'],
2064+
['a_10', 'b_10', 'c_10'],
2065+
['Footer A', 'Footer B', 'Footer C'],
2066+
]);
2067+
2068+
triggerScroll(1000);
2069+
2070+
expectTableToMatchContent(table, [
2071+
['Column A', 'Column B', 'Column C'],
2072+
['a_18', 'b_18', 'c_18'],
2073+
['a_19', 'b_19', 'c_19'],
2074+
['a_20', 'b_20', 'c_20'],
2075+
['a_21', 'b_21', 'c_21'],
2076+
['a_22', 'b_22', 'c_22'],
2077+
['a_23', 'b_23', 'c_23'],
2078+
['a_24', 'b_24', 'c_24'],
2079+
['a_25', 'b_25', 'c_25'],
2080+
['a_26', 'b_26', 'c_26'],
2081+
['a_27', 'b_27', 'c_27'],
2082+
['a_28', 'b_28', 'c_28'],
2083+
['a_29', 'b_29', 'c_29'],
2084+
['Footer A', 'Footer B', 'Footer C'],
2085+
]);
2086+
}));
2087+
2088+
it('should update the position of sticky cells as the user is scrolling', fakeAsync(() => {
2089+
const assertStickyOffsets = (position: number) => {
2090+
getHeaderCells(table).forEach(cell => expect(cell.style.top).toBe(`${position * -1}px`));
2091+
getFooterCells(table).forEach(cell => expect(cell.style.bottom).toBe(`${position}px`));
2092+
};
2093+
2094+
assertStickyOffsets(0);
2095+
triggerScroll(1000);
2096+
assertStickyOffsets(884);
2097+
}));
2098+
2099+
it('should force tables with virtual scrolling to have a fixed layout', fakeAsync(() => {
2100+
expect(fixture.componentInstance.isFixedLayout()).toBe(true);
2101+
expect(table.classList).toContain('cdk-table-fixed-layout');
2102+
2103+
fixture.componentInstance.isFixedLayout.set(false);
2104+
fixture.detectChanges();
2105+
2106+
expect(table.classList).toContain('cdk-table-fixed-layout');
2107+
}));
2108+
});
19982109
});
19992110

20002111
interface TestData {
@@ -2032,15 +2143,18 @@ class FakeDataSource extends DataSource<TestData> {
20322143
this.isConnected = false;
20332144
}
20342145

2035-
addData() {
2036-
const nextIndex = this.data.length + 1;
2037-
2146+
addData(amount = 1) {
20382147
let copiedData = this.data.slice();
2039-
copiedData.push({
2040-
a: `a_${nextIndex}`,
2041-
b: `b_${nextIndex}`,
2042-
c: `c_${nextIndex}`,
2043-
});
2148+
2149+
for (let i = 0; i < amount; i++) {
2150+
const nextIndex = copiedData.length + 1;
2151+
2152+
copiedData.push({
2153+
a: `a_${nextIndex}`,
2154+
b: `b_${nextIndex}`,
2155+
c: `c_${nextIndex}`,
2156+
});
2157+
}
20442158

20452159
this.data = copiedData;
20462160
}
@@ -3176,6 +3290,54 @@ class WrapNativeHtmlTableAppOnPush {
31763290
dataSource = new FakeDataSource();
31773291
}
31783292

3293+
@Component({
3294+
template: `
3295+
<cdk-virtual-scroll-viewport class="scroll-container" [itemSize]="52">
3296+
<table cdk-table [dataSource]="dataSource" [fixedLayout]="isFixedLayout()">
3297+
<ng-container cdkColumnDef="column_a">
3298+
<th cdk-header-cell *cdkHeaderCellDef>Column A</th>
3299+
<td cdk-cell *cdkCellDef="let row"> {{row.a}}</td>
3300+
<td cdk-footer-cell *cdkFooterCellDef>Footer A</td>
3301+
</ng-container>
3302+
3303+
<ng-container cdkColumnDef="column_b">
3304+
<th cdk-header-cell *cdkHeaderCellDef>Column B</th>
3305+
<td cdk-cell *cdkCellDef="let row"> {{row.b}}</td>
3306+
<td cdk-footer-cell *cdkFooterCellDef>Footer B</td>
3307+
</ng-container>
3308+
3309+
<ng-container cdkColumnDef="column_c">
3310+
<th cdk-header-cell *cdkHeaderCellDef>Column C</th>
3311+
<td cdk-cell *cdkCellDef="let row"> {{row.c}}</td>
3312+
<td cdk-footer-cell *cdkFooterCellDef>Footer C</td>
3313+
</ng-container>
3314+
3315+
<tr cdk-header-row *cdkHeaderRowDef="columnsToRender; sticky: true"></tr>
3316+
<tr cdk-row *cdkRowDef="let row; columns: columnsToRender"></tr>
3317+
<tr cdk-footer-row *cdkFooterRowDef="columnsToRender; sticky: true"></tr>
3318+
</table>
3319+
</cdk-virtual-scroll-viewport>
3320+
`,
3321+
imports: [CdkTableModule, ScrollingModule],
3322+
styles: `
3323+
.scroll-container {
3324+
height: 300px;
3325+
overflow: auto;
3326+
}
3327+
`,
3328+
})
3329+
class TableWithVirtualScroll {
3330+
@ViewChild(CdkTable) table: CdkTable<TestData>;
3331+
@ViewChild(CdkVirtualScrollViewport) viewport: CdkVirtualScrollViewport;
3332+
dataSource = new FakeDataSource();
3333+
columnsToRender = ['column_a', 'column_b', 'column_c'];
3334+
isFixedLayout = signal(true);
3335+
3336+
constructor() {
3337+
this.dataSource.addData(2000);
3338+
}
3339+
}
3340+
31793341
function getElements(element: Element, query: string): HTMLElement[] {
31803342
return [].slice.call(element.querySelectorAll(query));
31813343
}

0 commit comments

Comments
 (0)